feat: chat history finished

This commit is contained in:
Sergey Yarkov 2022-02-12 18:23:20 +03:00
parent 3e16fe6a19
commit 28fe68aa26
9 changed files with 195 additions and 24 deletions

View File

@ -124,6 +124,20 @@ export default class MeController extends BaseController {
return this.sendResponse(ctx, result.data, result.message, result.status);
}
/**
* Get messages of conversation by chat id
* GET /me/messages
*/
public async chatHistory(ctx: HttpContextContract) {
const result = await this.meService.fetchChatHistory(ctx.params.chat_id, ctx.auth);
if (!result.success && result.error) {
throw new Exception(result.message, result.status, result.error.code);
}
return this.sendResponse(ctx, result.data, result.message, result.status);
}
}
new Ioc().make(MeController);

View File

@ -5,15 +5,36 @@ import Redis from '@ioc:Adonis/Addons/Redis';
import Mail from '@ioc:Adonis/Addons/Mail';
import Hash from '@ioc:Adonis/Core/Hash';
import Logger from '@ioc:Adonis/Core/Logger';
/**
* Datatypes
*/
import RoleEnum from 'App/Datatypes/Enums/RoleEnum';
import HttpStatusEnum from 'App/Datatypes/Enums/HttpStatusEnum';
import IResponse from 'App/Datatypes/Interfaces/IResponse';
import CodeErrorEnum from 'App/Datatypes/Enums/CodeErrorEnum';
/**
* Repositories
*/
import ContactRepository from 'App/Repositories/ContactRepository';
import UserRepository from 'App/Repositories/UserRepository';
import UpdateContactsValidator from 'App/Validators/Contacts/UpdateContactsValidator';
import CodeErrorEnum from 'App/Datatypes/Enums/CodeErrorEnum';
import RoleHelper from 'App/Helpers/RoleHelper';
import RoleEnum from 'App/Datatypes/Enums/RoleEnum';
/**
* Validator
*/
import UpdateUserInfoValidator from 'App/Validators/User/UpdateUserInfoValidator';
import UpdateContactsValidator from 'App/Validators/Contacts/UpdateContactsValidator';
/**
* Helpers
*/
import RoleHelper from 'App/Helpers/RoleHelper';
/**
* Services
*/
import Ws from './WsService';
@inject()
export default class MeService {
@ -336,6 +357,41 @@ export default class MeService {
data: { ...data },
};
}
/**
* Fetch history of chat
*
* @param chatId Id of user
* @param auth AuthContract
* @returns IResponse
*/
public async fetchChatHistory(chatId: string, auth: AuthContract): Promise<IResponse> {
const firstUser = await auth.use(this.authGuard).authenticate();
const secondUser = await this.userRepository.getById(chatId);
if (!secondUser) {
return {
success: false,
status: HttpStatusEnum.NOT_FOUND,
message: 'Chat not found.',
data: {},
error: {
code: 'E_NOT_FOUND',
},
};
}
const history = await Ws.messageStore.getHistory(firstUser.id, secondUser.id);
return {
success: true,
status: HttpStatusEnum.OK,
message: 'Chat history fetched.',
data: {
history,
},
};
}
}
new Ioc().make(MeService);

View File

@ -1,6 +1,7 @@
import { Server } from 'socket.io';
import Redis from '@ioc:Adonis/Addons/Redis';
import AdonisServer from '@ioc:Adonis/Core/Server';
import { DateTime } from 'luxon';
/**
* Controllers
@ -11,8 +12,12 @@ import OnlineController from 'App/Controllers/Ws/OnlineController';
* Middlewares
*/
import AuthMiddleware from 'App/Middleware/Ws/Auth';
/**
* Stores
*/
import RedisSessionStore from 'App/Store/SessionStore';
import { DateTime } from 'luxon';
import RedisMessageStore from 'App/Store/MessageStore';
export interface ServerToClientEvents {
/**
@ -54,6 +59,8 @@ class WsService {
public sessionStore: RedisSessionStore;
public messageStore: RedisMessageStore;
public booted = false;
public boot() {
@ -68,7 +75,12 @@ class WsService {
credentials: true,
},
});
/**
* Stores
*/
this.sessionStore = new RedisSessionStore(Redis.connection('session'));
this.messageStore = new RedisMessageStore(Redis.connection('message'));
/**
* Clean up sessions before start
@ -103,9 +115,17 @@ class WsService {
*/
socket.emit('user:session', { sessionId: socket.data.sessionId, userId: socket.data.userId });
socket.on('chat:message', ({ content, to }) => {
socket.on('chat:message', async ({ content, to }) => {
if (isExistSocketData) {
this.io.to(to).emit('chat:message', { content, from: userId, to, time: DateTime.now().toISO() });
const message = {
content,
from: userId,
to,
time: DateTime.now().toISO(),
};
await this.messageStore.add(userId, to, message);
this.io.to(to).emit('chat:message', message);
}
});

69
app/Store/MessageStore.ts Normal file
View File

@ -0,0 +1,69 @@
import { RedisConnectionContract } from '@ioc:Adonis/Addons/Redis';
import Logger from '@ioc:Adonis/Core/Logger';
import { DateTime } from 'luxon';
import { nanoid } from 'nanoid';
type MessageDataType = {
from: string;
to: string;
content: string;
time: string;
};
const MESSAGE_TTL = 185 * 24 * 60 * 60; // 185 days
class RedisMessageStore {
public redisClient: RedisConnectionContract;
constructor(redisClient: RedisConnectionContract) {
this.redisClient = redisClient;
}
public async add(firstUserId: string, secondUserId: string, data: MessageDataType): Promise<'OK' | 'ERROR'> {
try {
const msgId = nanoid();
const msgKey = `${firstUserId}:${msgId}`;
const key = `message:${msgKey}`;
await this.redisClient
.multi()
.hmset(key, {
from: data.from,
to: data.to,
content: data.content,
time: DateTime.now().toISO(),
})
.expire(key, MESSAGE_TTL)
.exec();
const convKey = this.createConversationKey(firstUserId, secondUserId);
await this.redisClient.lpush(convKey, msgKey);
return 'OK';
} catch (error) {
Logger.error(error);
return 'ERROR';
}
}
public async getHistory(firstUserId: string, secondUserId: string): Promise<MessageDataType[]> {
const convKey = this.createConversationKey(firstUserId, secondUserId);
const msgKeys = await this.redisClient.lrange(convKey, 0, -1);
const messages = await Promise.all(msgKeys.map(key => `message:${key}`).map(key => this.getMessage(key)));
return messages.sort((m1, m2) => new Date(m1.time).valueOf() - new Date(m2.time).valueOf());
}
private async getMessage(key: string): Promise<MessageDataType> {
const message = (await this.redisClient.hgetall(key)) as MessageDataType;
return message;
}
// eslint-disable-next-line class-methods-use-this
private createConversationKey(userId1: string, userId2: string): string {
const [id1, id2] = [userId1, userId2].sort();
return `conversation:${id1}-${id2}`;
}
}
export default RedisMessageStore;

View File

@ -51,6 +51,14 @@ const redisConfig: RedisConfig = {
keyPrefix: 'session-',
healthCheck: true,
},
message: {
host: Env.get('REDIS_HOST'),
port: Env.get('REDIS_PORT'),
password: Env.get('REDIS_PASSWORD', ''),
keyPrefix: '',
db: 2,
healthCheck: true,
},
},
};

View File

@ -9,5 +9,6 @@ declare module '@ioc:Adonis/Addons/Redis' {
interface RedisConnectionsList {
local: RedisConnectionConfig;
session: RedisConnectionConfig;
message: RedisConnectionConfig;
}
}

32
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "server",
"version": "1.0.1",
"version": "1.1.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -937,13 +937,13 @@
}
},
"@adonisjs/redis": {
"version": "7.0.9",
"resolved": "https://registry.npmjs.org/@adonisjs/redis/-/redis-7.0.9.tgz",
"integrity": "sha512-d+NDJuC3BG2p5YhH1vMqLXzgWDaaosk2YTklmCyvZIT0LAueXNVCyJrxduAdRA2EH/81Y5MyRhubEBnp/wQl4w==",
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@adonisjs/redis/-/redis-7.1.0.tgz",
"integrity": "sha512-jbe6gYMWmONVKUxQRM4Cu3851YofK91Tr1nzT1P924i/8Nb4btW4tYoU9qmjRaFfjfybQPordmEcFXXSiABDhw==",
"requires": {
"@poppinss/utils": "^3.2.0",
"@types/ioredis": "^4.27.1",
"ioredis": "^4.27.8"
"@poppinss/utils": "^3.3.1",
"@types/ioredis": "^4.28.3",
"ioredis": "^4.28.2"
},
"dependencies": {
"@poppinss/utils": {
@ -971,9 +971,9 @@
}
},
"slugify": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.1.tgz",
"integrity": "sha512-5ofqMTbetNhxlzjYYLBaZFQd6oiTuSkQlyfPEFIMwgUABlZQ0hbk5xIV9Ydd5jghWeRoO7GkiJliUvTpLOjNRA=="
"version": "1.6.5",
"resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.5.tgz",
"integrity": "sha512-8mo9bslnBO3tr5PEVFzMPIWwWnipGS0xVbYf65zxDqfNwmzYn1LpiKNrR6DlClusuvo+hDHd1zKpmfAe83NQSQ=="
}
}
},
@ -2503,9 +2503,9 @@
"integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ=="
},
"@types/ioredis": {
"version": "4.27.8",
"resolved": "https://registry.npmjs.org/@types/ioredis/-/ioredis-4.27.8.tgz",
"integrity": "sha512-THsyghYuFI6h/UzwEaeSeagRxiDG1P/NIiL5uTjN7bcbQHwDP6nMWJfEmY0iuu3pOzl1j0FgRpAYz7WWX2eW0Q==",
"version": "4.28.8",
"resolved": "https://registry.npmjs.org/@types/ioredis/-/ioredis-4.28.8.tgz",
"integrity": "sha512-mULOyO2smtvkE1zmzRRA4P0+1UjEqusi014kXOL1q3CY0RgqkR5/wKvv+vAJbPw2Q66wPyylKeevUy+m/FaRMg==",
"requires": {
"@types/node": "*"
}
@ -5525,9 +5525,9 @@
"integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw=="
},
"ioredis": {
"version": "4.28.0",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.0.tgz",
"integrity": "sha512-I+zkeeWp3XFgPT2CtJKxvaF5FjGBGt4yGYljRjQecdQKteThuAsKqffeF1lgHVlYnuNeozRbPOCDNZ7tDWPeig==",
"version": "4.28.5",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.5.tgz",
"integrity": "sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A==",
"requires": {
"cluster-key-slot": "^1.1.0",
"debug": "^4.3.1",

View File

@ -40,7 +40,7 @@
"@adonisjs/drive-s3": "^1.0.5",
"@adonisjs/lucid": "^16.3.2",
"@adonisjs/mail": "^7.2.4",
"@adonisjs/redis": "^7.0.9",
"@adonisjs/redis": "^7.1.0",
"@adonisjs/repl": "^3.1.7",
"@adonisjs/view": "^6.1.1",
"@types/socket.io": "^3.0.2",

View File

@ -2,6 +2,9 @@ import Route from '@ioc:Adonis/Core/Route';
Route.group(() => {
Route.get('/', 'Api/v1/MeController.show').middleware('role:admin,teacher,student').as('me.show');
Route.get('/messages/:chat_id', 'Api/v1/MeController.chatHistory')
.middleware('role:admin,teacher,student')
.as('me.chat-history');
Route.patch('/info', 'Api/v1/MeController.updateInfo').middleware('role:admin,teacher,student').as('me.update-info');
Route.patch('/email', 'Api/v1/MeController.changeEmail')
.middleware('role:admin,teacher,student')