Skip to content

Commit

Permalink
feat(messages): add base messages app
Browse files Browse the repository at this point in the history
  • Loading branch information
antonstjernquist committed Sep 15, 2024
1 parent 55ebcea commit 6a4b82e
Show file tree
Hide file tree
Showing 25 changed files with 801 additions and 25 deletions.
29 changes: 27 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions src/server/database/knex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import { getConnectionOptions } from './utils';
import { createCallsTable } from './schemas/Call';
import { createDevicesTable } from './schemas/Device';
import { createSimCardsTable } from './schemas/SimCard';
import { createMessageTable } from './schemas/Message';
import { createReadReceiptTable } from './schemas/ReadReceipt';
import { createConversationTable } from './schemas/Conversation';

export let DBInstance: knex.Knex;

Expand Down Expand Up @@ -37,4 +40,7 @@ export function initDB() {
createCallsTable();
createDevicesTable();
createSimCardsTable();
createMessageTable();
createConversationTable();
createReadReceiptTable();
}
10 changes: 10 additions & 0 deletions src/server/database/schemas/Conversation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createDbTable } from '../utils';

export const createConversationTable = () => {
createDbTable('conversation', (table) => {
table.increments('id').primary();
table.string('label').notNullable();
// table.json('messages').notNullable(); // Assuming messages are stored as JSON array of message IDs
// table.json('participants').notNullable(); // Assuming participants are stored as JSON array of simcard IDs
});
};
25 changes: 25 additions & 0 deletions src/server/database/schemas/Message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { createDbTable, DATABASE_PREFIX } from '../utils';
import { DBInstance } from '../knex';
import { Message } from '../../../shared/Types';

export type InsertMessage = Pick<Message, 'receiver_id' | 'sender_id' | 'content'>;

export const createMessageTable = () => {
createDbTable('message', (table) => {
table.increments('id').primary();
table
.integer('sender_id')
.unsigned()
.notNullable()
.references('id')
.inTable(`${DATABASE_PREFIX}sim_card`);
table
.integer('receiver_id')
.unsigned()
.notNullable()
.references('id')
.inTable(`${DATABASE_PREFIX}sim_card`);
table.string('content').notNullable();
table.dateTime('created_at').notNullable().defaultTo(DBInstance.fn.now());
});
};
4 changes: 4 additions & 0 deletions src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
} from '../shared/network';
import { parseObjectToIsoString } from './utils/date';
import { emitMiddleware } from './middlewares/emitMiddleware';
import { messagesRouter } from './router/messages';
import { conversationsRouter } from './router/conversations';

function bootstrap() {
initDB();
Expand All @@ -40,6 +42,8 @@ function bootstrap() {
app.use(activeCallRouter.routes());
app.use(callsRouter.routes());
app.use(devicesRouter.routes());
app.use(messagesRouter.routes());
app.use(conversationsRouter.routes());

app.use(router.routes());

Expand Down
64 changes: 64 additions & 0 deletions src/server/repositories/MessageRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Message, MessageWithPhoneNumbers } from '../../shared/Types';
import { DBInstance } from '../database/knex';
import { InsertMessage } from '../database/schemas/Message';

const tableName = 'tmp_phone_message';

class MessageRepository {
public async getMessages(): Promise<Message[]> {
return await DBInstance(tableName);
}

public async getMessageById(messageId: number): Promise<Message | null> {
return await DBInstance(tableName).where('id', messageId).first();
}

public async getMessagesBySid(sid: number): Promise<MessageWithPhoneNumbers[]> {
return await DBInstance(tableName)
.leftJoin('tmp_phone_sim_card as sender', 'sender.id', 'tmp_phone_message.sender_id')
.leftJoin('tmp_phone_sim_card as receiver', 'receiver.id', 'tmp_phone_message.receiver_id')
.select(
'tmp_phone_message.*',
'sender.phone_number as sender_phone_number',
'receiver.phone_number as receiver_phone_number',
)
.where('sender_id', sid)
.orWhere('receiver_id', sid)
.orderBy('tmp_phone_message.created_at', 'desc');
}

public async getMessagesBySenderId(senderId: number): Promise<Message[]> {
return await DBInstance(tableName).where('sender_id', senderId).orderBy('created_at', 'desc');
}

public async getMessagesByReceiverId(receiverId: number): Promise<Message[]> {
return await DBInstance(tableName)
.where('receiver_id', receiverId)
.orderBy('created_at', 'desc');
}

public async getConversation(senderId: number, receiverId: number): Promise<Message[]> {
return await DBInstance(tableName)
.where('sender_id', senderId)
.andWhere('receiver_id', receiverId)
.orWhere('sender_id', receiverId)
.andWhere('receiver_id', senderId)
.orderBy('created_at', 'asc');
}

public async createMessage(message: InsertMessage): Promise<Message> {
const [newId] = await DBInstance(tableName).insert(message);
return await DBInstance(tableName).select('*').where('id', newId).first();
}

public async updateMessage(message: Message): Promise<Message> {
await DBInstance(tableName).where('id', message.id).update(message);
return await this.getMessageById(message.id);
}

public async deleteMessage(messageId: number): Promise<void> {
await DBInstance(tableName).where('id', messageId).delete();
}
}

export default new MessageRepository();
38 changes: 38 additions & 0 deletions src/server/router/conversations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Router } from 'fivem-router';
import { handleError } from '../utils/errors';
import ConversationService from '../services/ConversationService';
import z from 'zod';

export const conversationsRouter = new Router({
prefix: '/conversations',
});

conversationsRouter.add('/', async (ctx, next) => {
/** Return my messages */
try {
const conversations = await ConversationService.getMyConversations(ctx);

ctx.body = {
ok: true,
payload: conversations,
};
} catch (error) {
handleError(error, ctx);
}
});

conversationsRouter.add('/:phoneNumber', async (ctx, next) => {
try {
const { phoneNumber } = z.object({ phoneNumber: z.string().min(2).max(15) }).parse(ctx.params);

const messages = await ConversationService.getConversation(ctx, phoneNumber);
console.log(messages);

ctx.body = {
ok: true,
payload: messages,
};
} catch (error) {
handleError(error, ctx);
}
});
62 changes: 62 additions & 0 deletions src/server/router/messages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { Router } from 'fivem-router';
import z from 'zod';
import { handleError } from '../utils/errors';
import MessageService from '../services/MessageService';

export const messagesRouter = new Router({
prefix: '/messages',
});

messagesRouter.add('/', async (ctx, next) => {
/** Return my messages */
try {
const messages = await MessageService.getMessages(ctx);

ctx.body = {
ok: true,
payload: messages,
};
} catch (error) {
handleError(error, ctx);
}
});

const sendMessageSchema = z.object({
content: z.string().min(1).max(255),
phoneNumber: z.string().min(2).max(15),
});

messagesRouter.add('/send', async (ctx, next) => {
try {
const { content, phoneNumber } = sendMessageSchema.parse(ctx.request.body);

// Send message to phoneNumber
const message = await MessageService.sendMessage(ctx, content, phoneNumber);

ctx.body = {
ok: true,
payload: message,
};
} catch (error) {
handleError(error, ctx);
}

await next();
});

messagesRouter.add('/conversation/:phoneNumber', async (ctx, next) => {
try {
const { phoneNumber } = z.object({ phoneNumber: z.string().min(2).max(15) }).parse(ctx.params);

const messages = await MessageService.getConversation(ctx, phoneNumber);

ctx.body = {
ok: true,
payload: messages,
};
} catch (error) {
handleError(error, ctx);
}

await next();
});
63 changes: 63 additions & 0 deletions src/server/services/ConversationService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { RouterContext } from 'fivem-router';
import { SimCardNotActiveError, SimcardNotFoundError } from '../../shared/Errors';
import MessageRepository from '../repositories/MessageRepository';
import SimCardRepository from '../repositories/SimCardRepository';
import DeviceRepository from '../repositories/DeviceRepository';
import { Conversation, MessageWithPhoneNumbers } from '../../shared/Types';

class ConversationService {
messageRepository: typeof MessageRepository;
simCardRepository: typeof SimCardRepository;
deviceRepository: typeof DeviceRepository;

constructor(
messageRepository: typeof MessageRepository,
simCardRepository: typeof SimCardRepository,
deviceRepository: typeof DeviceRepository,
) {
this.messageRepository = messageRepository;
this.simCardRepository = simCardRepository;
this.deviceRepository = deviceRepository;
}

async getMyConversations(ctx: RouterContext): Promise<string[]> {
const messages = await this.messageRepository.getMessagesBySid(ctx.device.sim_card_id);

const createConversationId = (myNumber: string) => (message: MessageWithPhoneNumbers) => {
const { sender_phone_number, receiver_phone_number } = message;
const [first, second] = [sender_phone_number, receiver_phone_number].sort();
return first === myNumber ? second : first;
};

const conversations = messages.map(createConversationId(ctx.device.phone_number));
const uniqueConversations = Array.from(new Set(conversations));

return uniqueConversations;
}

async getConversation(ctx: RouterContext, phoneNumber: string) {
const device = await this.deviceRepository.getDeviceById(ctx.device.id);

if (!device) {
throw new SimcardNotFoundError('SENDER');
}

if (!device.sim_card_id) {
throw new SimcardNotFoundError('SENDER');
}

const receiverSimcard = await this.simCardRepository.getSimCardByPhoneNumber(phoneNumber);

if (!receiverSimcard) {
throw new SimcardNotFoundError('RECEIVER');
}

if (!receiverSimcard.is_active) {
throw new SimCardNotActiveError('RECEIVER');
}

return await this.messageRepository.getConversation(device.sim_card_id, receiverSimcard.id);
}
}

export default new ConversationService(MessageRepository, SimCardRepository, DeviceRepository);
Loading

0 comments on commit 6a4b82e

Please sign in to comment.