mirror of
https://github.com/sergeyyarkov/educt-server.git
synced 2026-10-11 18:29:15 +03:00
feat: chat history finished
This commit is contained in:
parent
3e16fe6a19
commit
28fe68aa26
@ -124,6 +124,20 @@ export default class MeController extends BaseController {
|
|||||||
|
|
||||||
return this.sendResponse(ctx, result.data, result.message, result.status);
|
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);
|
new Ioc().make(MeController);
|
||||||
|
|||||||
@ -5,15 +5,36 @@ import Redis from '@ioc:Adonis/Addons/Redis';
|
|||||||
import Mail from '@ioc:Adonis/Addons/Mail';
|
import Mail from '@ioc:Adonis/Addons/Mail';
|
||||||
import Hash from '@ioc:Adonis/Core/Hash';
|
import Hash from '@ioc:Adonis/Core/Hash';
|
||||||
import Logger from '@ioc:Adonis/Core/Logger';
|
import Logger from '@ioc:Adonis/Core/Logger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Datatypes
|
||||||
|
*/
|
||||||
|
import RoleEnum from 'App/Datatypes/Enums/RoleEnum';
|
||||||
import HttpStatusEnum from 'App/Datatypes/Enums/HttpStatusEnum';
|
import HttpStatusEnum from 'App/Datatypes/Enums/HttpStatusEnum';
|
||||||
import IResponse from 'App/Datatypes/Interfaces/IResponse';
|
import IResponse from 'App/Datatypes/Interfaces/IResponse';
|
||||||
|
import CodeErrorEnum from 'App/Datatypes/Enums/CodeErrorEnum';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repositories
|
||||||
|
*/
|
||||||
import ContactRepository from 'App/Repositories/ContactRepository';
|
import ContactRepository from 'App/Repositories/ContactRepository';
|
||||||
import UserRepository from 'App/Repositories/UserRepository';
|
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';
|
* Validator
|
||||||
import RoleEnum from 'App/Datatypes/Enums/RoleEnum';
|
*/
|
||||||
import UpdateUserInfoValidator from 'App/Validators/User/UpdateUserInfoValidator';
|
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()
|
@inject()
|
||||||
export default class MeService {
|
export default class MeService {
|
||||||
@ -336,6 +357,41 @@ export default class MeService {
|
|||||||
data: { ...data },
|
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);
|
new Ioc().make(MeService);
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { Server } from 'socket.io';
|
import { Server } from 'socket.io';
|
||||||
import Redis from '@ioc:Adonis/Addons/Redis';
|
import Redis from '@ioc:Adonis/Addons/Redis';
|
||||||
import AdonisServer from '@ioc:Adonis/Core/Server';
|
import AdonisServer from '@ioc:Adonis/Core/Server';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Controllers
|
* Controllers
|
||||||
@ -11,8 +12,12 @@ import OnlineController from 'App/Controllers/Ws/OnlineController';
|
|||||||
* Middlewares
|
* Middlewares
|
||||||
*/
|
*/
|
||||||
import AuthMiddleware from 'App/Middleware/Ws/Auth';
|
import AuthMiddleware from 'App/Middleware/Ws/Auth';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores
|
||||||
|
*/
|
||||||
import RedisSessionStore from 'App/Store/SessionStore';
|
import RedisSessionStore from 'App/Store/SessionStore';
|
||||||
import { DateTime } from 'luxon';
|
import RedisMessageStore from 'App/Store/MessageStore';
|
||||||
|
|
||||||
export interface ServerToClientEvents {
|
export interface ServerToClientEvents {
|
||||||
/**
|
/**
|
||||||
@ -54,6 +59,8 @@ class WsService {
|
|||||||
|
|
||||||
public sessionStore: RedisSessionStore;
|
public sessionStore: RedisSessionStore;
|
||||||
|
|
||||||
|
public messageStore: RedisMessageStore;
|
||||||
|
|
||||||
public booted = false;
|
public booted = false;
|
||||||
|
|
||||||
public boot() {
|
public boot() {
|
||||||
@ -68,7 +75,12 @@ class WsService {
|
|||||||
credentials: true,
|
credentials: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores
|
||||||
|
*/
|
||||||
this.sessionStore = new RedisSessionStore(Redis.connection('session'));
|
this.sessionStore = new RedisSessionStore(Redis.connection('session'));
|
||||||
|
this.messageStore = new RedisMessageStore(Redis.connection('message'));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clean up sessions before start
|
* Clean up sessions before start
|
||||||
@ -103,9 +115,17 @@ class WsService {
|
|||||||
*/
|
*/
|
||||||
socket.emit('user:session', { sessionId: socket.data.sessionId, userId: socket.data.userId });
|
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) {
|
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
69
app/Store/MessageStore.ts
Normal 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;
|
||||||
@ -51,6 +51,14 @@ const redisConfig: RedisConfig = {
|
|||||||
keyPrefix: 'session-',
|
keyPrefix: 'session-',
|
||||||
healthCheck: true,
|
healthCheck: true,
|
||||||
},
|
},
|
||||||
|
message: {
|
||||||
|
host: Env.get('REDIS_HOST'),
|
||||||
|
port: Env.get('REDIS_PORT'),
|
||||||
|
password: Env.get('REDIS_PASSWORD', ''),
|
||||||
|
keyPrefix: '',
|
||||||
|
db: 2,
|
||||||
|
healthCheck: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -9,5 +9,6 @@ declare module '@ioc:Adonis/Addons/Redis' {
|
|||||||
interface RedisConnectionsList {
|
interface RedisConnectionsList {
|
||||||
local: RedisConnectionConfig;
|
local: RedisConnectionConfig;
|
||||||
session: RedisConnectionConfig;
|
session: RedisConnectionConfig;
|
||||||
|
message: RedisConnectionConfig;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
32
package-lock.json
generated
32
package-lock.json
generated
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "1.0.1",
|
"version": "1.1.0",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -937,13 +937,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@adonisjs/redis": {
|
"@adonisjs/redis": {
|
||||||
"version": "7.0.9",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@adonisjs/redis/-/redis-7.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/@adonisjs/redis/-/redis-7.1.0.tgz",
|
||||||
"integrity": "sha512-d+NDJuC3BG2p5YhH1vMqLXzgWDaaosk2YTklmCyvZIT0LAueXNVCyJrxduAdRA2EH/81Y5MyRhubEBnp/wQl4w==",
|
"integrity": "sha512-jbe6gYMWmONVKUxQRM4Cu3851YofK91Tr1nzT1P924i/8Nb4btW4tYoU9qmjRaFfjfybQPordmEcFXXSiABDhw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@poppinss/utils": "^3.2.0",
|
"@poppinss/utils": "^3.3.1",
|
||||||
"@types/ioredis": "^4.27.1",
|
"@types/ioredis": "^4.28.3",
|
||||||
"ioredis": "^4.27.8"
|
"ioredis": "^4.28.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@poppinss/utils": {
|
"@poppinss/utils": {
|
||||||
@ -971,9 +971,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"slugify": {
|
"slugify": {
|
||||||
"version": "1.6.1",
|
"version": "1.6.5",
|
||||||
"resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.5.tgz",
|
||||||
"integrity": "sha512-5ofqMTbetNhxlzjYYLBaZFQd6oiTuSkQlyfPEFIMwgUABlZQ0hbk5xIV9Ydd5jghWeRoO7GkiJliUvTpLOjNRA=="
|
"integrity": "sha512-8mo9bslnBO3tr5PEVFzMPIWwWnipGS0xVbYf65zxDqfNwmzYn1LpiKNrR6DlClusuvo+hDHd1zKpmfAe83NQSQ=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -2503,9 +2503,9 @@
|
|||||||
"integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ=="
|
"integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ=="
|
||||||
},
|
},
|
||||||
"@types/ioredis": {
|
"@types/ioredis": {
|
||||||
"version": "4.27.8",
|
"version": "4.28.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ioredis/-/ioredis-4.27.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ioredis/-/ioredis-4.28.8.tgz",
|
||||||
"integrity": "sha512-THsyghYuFI6h/UzwEaeSeagRxiDG1P/NIiL5uTjN7bcbQHwDP6nMWJfEmY0iuu3pOzl1j0FgRpAYz7WWX2eW0Q==",
|
"integrity": "sha512-mULOyO2smtvkE1zmzRRA4P0+1UjEqusi014kXOL1q3CY0RgqkR5/wKvv+vAJbPw2Q66wPyylKeevUy+m/FaRMg==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
@ -5525,9 +5525,9 @@
|
|||||||
"integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw=="
|
"integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw=="
|
||||||
},
|
},
|
||||||
"ioredis": {
|
"ioredis": {
|
||||||
"version": "4.28.0",
|
"version": "4.28.5",
|
||||||
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-4.28.5.tgz",
|
||||||
"integrity": "sha512-I+zkeeWp3XFgPT2CtJKxvaF5FjGBGt4yGYljRjQecdQKteThuAsKqffeF1lgHVlYnuNeozRbPOCDNZ7tDWPeig==",
|
"integrity": "sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"cluster-key-slot": "^1.1.0",
|
"cluster-key-slot": "^1.1.0",
|
||||||
"debug": "^4.3.1",
|
"debug": "^4.3.1",
|
||||||
|
|||||||
@ -40,7 +40,7 @@
|
|||||||
"@adonisjs/drive-s3": "^1.0.5",
|
"@adonisjs/drive-s3": "^1.0.5",
|
||||||
"@adonisjs/lucid": "^16.3.2",
|
"@adonisjs/lucid": "^16.3.2",
|
||||||
"@adonisjs/mail": "^7.2.4",
|
"@adonisjs/mail": "^7.2.4",
|
||||||
"@adonisjs/redis": "^7.0.9",
|
"@adonisjs/redis": "^7.1.0",
|
||||||
"@adonisjs/repl": "^3.1.7",
|
"@adonisjs/repl": "^3.1.7",
|
||||||
"@adonisjs/view": "^6.1.1",
|
"@adonisjs/view": "^6.1.1",
|
||||||
"@types/socket.io": "^3.0.2",
|
"@types/socket.io": "^3.0.2",
|
||||||
|
|||||||
@ -2,6 +2,9 @@ import Route from '@ioc:Adonis/Core/Route';
|
|||||||
|
|
||||||
Route.group(() => {
|
Route.group(() => {
|
||||||
Route.get('/', 'Api/v1/MeController.show').middleware('role:admin,teacher,student').as('me.show');
|
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('/info', 'Api/v1/MeController.updateInfo').middleware('role:admin,teacher,student').as('me.update-info');
|
||||||
Route.patch('/email', 'Api/v1/MeController.changeEmail')
|
Route.patch('/email', 'Api/v1/MeController.changeEmail')
|
||||||
.middleware('role:admin,teacher,student')
|
.middleware('role:admin,teacher,student')
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user