From a62b7eb4aa790b6901b62e59edb473b192419b15 Mon Sep 17 00:00:00 2001 From: Khaled FERJANI Date: Tue, 10 Dec 2024 16:17:45 +0100 Subject: [PATCH 01/15] =?UTF-8?q?=E2=9C=A8=20feat:=20added=20metrics=20ser?= =?UTF-8?q?vice?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/metrics-api/services/index.ts | 203 ++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 packages/tom-server/src/metrics-api/services/index.ts diff --git a/packages/tom-server/src/metrics-api/services/index.ts b/packages/tom-server/src/metrics-api/services/index.ts new file mode 100644 index 00000000..f98fd558 --- /dev/null +++ b/packages/tom-server/src/metrics-api/services/index.ts @@ -0,0 +1,203 @@ +import type { MatrixDBBackend } from '@twake/matrix-identity-server' +import type { + IMetricsService, + MatrixUserInfo, + UserActivityStats, + UserMessageCount, + UserMessageEvent +} from '../types' +import type { TwakeLogger } from '@twake/logger' + +class MetricsService implements IMetricsService { + private readonly ONE_DAY_IN_MS = 24 * 60 * 60 * 1000 + private readonly ONE_WEEK_IN_MS = 7 * this.ONE_DAY_IN_MS + private readonly ONE_MONTH_IN_MS = 30 * this.ONE_DAY_IN_MS + + constructor( + private readonly matrixDb: MatrixDBBackend, + private readonly logger: TwakeLogger + ) {} + + /** + * Fetches the user activity stats. + * + * new signups and login in the last 24 hours, 7 days and 30 days + * + * @returns {Promise} + */ + getUserActivityStats = async (): Promise => { + try { + const users = await this._fetchUsersList() + const dailyActiveUsers = this._getDailyActiveUsers(users) + const weeklyActiveUsers = this._getWeeklyActiveUsers(users) + const monthlyActiveUsers = this._getMonthlyActiveUsers(users) + const weeklyNewUsers = this._getWeeklyNewUsers(users) + const monthlyNewUsers = this._getMonthlyNewUsers(users) + + return { + dailyActiveUsers, + weeklyActiveUsers, + monthlyActiveUsers, + weeklyNewUsers, + monthlyNewUsers + } + } catch (error) { + this.logger.error(`Failed to fetch user activity stats`, { error }) + + throw Error('Failed to fetch user activity stats') + } + } + + /** + * Fetches the message count of each user. + * + * @returns {Promise} + */ + getUserMessageStats = async (): Promise => { + try { + const stats: UserMessageCount[] = [] + const users = await this._fetchUsersList() + + await Promise.all( + users.map(async (user) => { + const messageCount = await this._getUserMessageCount(user.name) + stats.push(messageCount) + }) + ) + + return stats.sort((a, b) => b.message_count - a.message_count) + } catch (error) { + this.logger.error(`Failed to fetch user message count`, { error }) + + throw Error('Failed to fetch user message count') + } + } + + /** + * Fetches the list of users from the database + * @returns {Promise} + */ + private readonly _fetchUsersList = async (): Promise => { + try { + const queryResult = (await this.matrixDb.getAll('users', [ + 'name', + 'displayname', + 'avatar_url', + 'creation_ts', + 'last_seen_ts' + ])) as unknown as MatrixUserInfo[] + + return queryResult + } catch (error) { + this.logger.error(`Failed to fetch users list`, { error }) + + throw Error('Failed to fetch users list') + } + } + + /** + * Gets the Daily active users + * + * @returns {MatrixUserInfo[]} + */ + private readonly _getDailyActiveUsers = ( + users: MatrixUserInfo[] + ): MatrixUserInfo[] => { + const currentEpochTime = new Date().getTime() + const oneDayAgo = currentEpochTime - this.ONE_DAY_IN_MS + + return users.filter( + (user) => user.last_seen_ts >= oneDayAgo || user.creation_ts >= oneDayAgo + ) + } + + /** + * Gets the weekly active users + * + * @returns {MatrixUserInfo[]} + */ + private readonly _getWeeklyActiveUsers = ( + users: MatrixUserInfo[] + ): MatrixUserInfo[] => { + const currentEpochTime = new Date().getTime() + const oneWeekAgo = currentEpochTime - this.ONE_WEEK_IN_MS + + return users.filter( + (user) => + user.last_seen_ts >= oneWeekAgo || user.creation_ts >= oneWeekAgo + ) + } + + /** + * Gets the monthly active users + * + * @returns {MatrixUserInfo[]} + */ + private readonly _getMonthlyActiveUsers = ( + users: MatrixUserInfo[] + ): MatrixUserInfo[] => { + const currentEpochTime = new Date().getTime() + const oneMonthAgo = currentEpochTime - this.ONE_MONTH_IN_MS + + return users.filter( + (user) => + user.last_seen_ts >= oneMonthAgo || user.creation_ts >= oneMonthAgo + ) + } + + /** + * Gets the weekly new users + * + * @returns {MatrixUserInfo[]} + */ + private readonly _getWeeklyNewUsers = ( + users: MatrixUserInfo[] + ): MatrixUserInfo[] => { + const currentEpochTime = new Date().getTime() + const oneWeekAgo = currentEpochTime - this.ONE_WEEK_IN_MS + + return users.filter((user) => user.creation_ts >= oneWeekAgo) + } + + /** + * Gets the monthly new users + * + * @returns {MatrixUserInfo[]} + */ + private readonly _getMonthlyNewUsers = ( + users: MatrixUserInfo[] + ): MatrixUserInfo[] => { + const currentEpochTime = new Date().getTime() + const oneMonthAgo = currentEpochTime - this.ONE_MONTH_IN_MS + + return users.filter((user) => user.creation_ts >= oneMonthAgo) + } + + /** + * Fetches the message count of a specific user. + * + * @param {string} userId - the user id. + * @returns {Promise} - the message count of the user. + */ + private readonly _getUserMessageCount = async ( + userId: string + ): Promise => { + try { + const queryResult = (await this.matrixDb.get('events', ['sender'], { + sender: userId, + type: 'm.room.message' + })) as unknown as UserMessageEvent[] + + return { + user_id: userId, + message_count: queryResult.length + } + } catch (error) { + this.logger.error(`Failed to fetch user message count`, { error }) + + throw Error('Failed to fetch user message count') + } + } +} + +export default MetricsService From 7b3c39c26999509d1e37702a54b1a34609ed82da Mon Sep 17 00:00:00 2001 From: Khaled FERJANI Date: Tue, 10 Dec 2024 16:18:24 +0100 Subject: [PATCH 02/15] =?UTF-8?q?=E2=9C=A8=20feat:=20added=20metrics=20API?= =?UTF-8?q?=20controller?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/metrics-api/controllers/index.ts | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 packages/tom-server/src/metrics-api/controllers/index.ts diff --git a/packages/tom-server/src/metrics-api/controllers/index.ts b/packages/tom-server/src/metrics-api/controllers/index.ts new file mode 100644 index 00000000..acedc903 --- /dev/null +++ b/packages/tom-server/src/metrics-api/controllers/index.ts @@ -0,0 +1,58 @@ +import { type TwakeLogger } from '@twake/logger' +import { type MatrixDBBackend } from '@twake/matrix-identity-server' +import { type NextFunction, type Request, type Response } from 'express' +import { type IMetricsService } from '../types' +import MetricsService from '../services' + +export default class MetricsApiController { + private readonly metricsService: IMetricsService + + constructor( + private readonly db: MatrixDBBackend, + private readonly logger: TwakeLogger + ) { + this.metricsService = new MetricsService(this.db, this.logger) + } + + /** + * Fetches the users activity stats + * + * @param {Request} _req - the request object. + * @param {Response} res - the response object. + * @param {NextFunction} next - the next hundler + */ + getActivityStats = async ( + _req: Request, + res: Response, + next: NextFunction + ): Promise => { + try { + const stats = await this.metricsService.getUserActivityStats() + + res.status(200).json(stats) + } catch (err) { + next(err) + } + } + + /** + * Fetches the users message stats + * + * @param {Request} _req - the request object. + * @param {Response} res - the response object. + * @param {NextFunction} next - the next hundler + */ + getMessageStats = async ( + _req: Request, + res: Response, + next: NextFunction + ): Promise => { + try { + const stats = await this.metricsService.getUserMessageStats() + + res.status(200).json(stats) + } catch (err) { + next(err) + } + } +} From 7256401d5c13e20ac5adeab26c73a72a6fc8a9f8 Mon Sep 17 00:00:00 2001 From: Khaled FERJANI Date: Tue, 10 Dec 2024 16:18:39 +0100 Subject: [PATCH 03/15] =?UTF-8?q?=E2=9C=A8=20feat:=20added=20metrics=20API?= =?UTF-8?q?=20middleware?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/metrics-api/middlewares/index.ts | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 packages/tom-server/src/metrics-api/middlewares/index.ts diff --git a/packages/tom-server/src/metrics-api/middlewares/index.ts b/packages/tom-server/src/metrics-api/middlewares/index.ts new file mode 100644 index 00000000..aa9be901 --- /dev/null +++ b/packages/tom-server/src/metrics-api/middlewares/index.ts @@ -0,0 +1,74 @@ +import { type MatrixDBBackend } from '@twake/matrix-identity-server' +import { type AuthRequest } from '../../types' +import { type NextFunction, type Response } from 'express' +import { type TwakeLogger } from '@twake/logger' + +export default class MetricsApiMiddleware { + constructor( + private readonly matrixDb: MatrixDBBackend, + private readonly logger: TwakeLogger + ) {} + + /** + * Checks if the user is an admin + * + * @param {AuthRequest} req - request object + * @param {Response} res - response object + * @param {NextFunction} next - next function + */ + checkPermissions = async ( + req: AuthRequest, + res: Response, + next: NextFunction + ): Promise => { + try { + const { userId } = req + + if (userId === undefined) { + throw new Error('Unauthenticated', { + cause: 'userId is missing' + }) + } + + const isAdmin = await this._checkAdmin(userId) + + if (!isAdmin) { + this.logger.warn('User is not an admin', { userId }) + res.status(403).json({ message: 'Forbidden' }) + + return + } + + next() + } catch (err) { + res.status(400).json({ message: 'Bad Request' }) + } + } + + /** + * checks if the user is an admin + * + * @param {string} userId - the user id to check + * @returns {Promise} - true if the user is an admin, false otherwise + */ + private readonly _checkAdmin = async (userId: string): Promise => { + try { + const user = await this.matrixDb.get('users', ['name'], { + name: userId, + admin: 1 + }) + + if (user.length === 0) { + this.logger.warn('User is not an admin', { userId }) + + return false + } + + return true + } catch (error) { + this.logger.error('Failed to check if user is admin', { error }) + + return false + } + } +} From 4108da7b79b87a80c3baa4831433a80584adada8 Mon Sep 17 00:00:00 2001 From: Khaled FERJANI Date: Tue, 10 Dec 2024 16:18:58 +0100 Subject: [PATCH 04/15] =?UTF-8?q?=E2=9C=A8=20feat:=20added=20metrics=20API?= =?UTF-8?q?=20routes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/metrics-api/routes/index.ts | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 packages/tom-server/src/metrics-api/routes/index.ts diff --git a/packages/tom-server/src/metrics-api/routes/index.ts b/packages/tom-server/src/metrics-api/routes/index.ts new file mode 100644 index 00000000..59dc182a --- /dev/null +++ b/packages/tom-server/src/metrics-api/routes/index.ts @@ -0,0 +1,124 @@ +/* eslint-disable @typescript-eslint/no-misused-promises */ +import { + getLogger, + type Config as LoggerConfig, + type TwakeLogger +} from '@twake/logger' +import { type AuthenticationFunction, type Config } from '../../types' +import { type MatrixDBBackend } from '@twake/matrix-identity-server' +import { Router } from 'express' +import authMiddleware from '../../utils/middlewares/auth.middleware' +import MetricsApiController from '../controllers' +import MetricsApiMiddleware from '../middlewares' + +export const PATH = '/_twake/v1/metrics' + +export default ( + config: Config, + matrixDb: MatrixDBBackend, + authenticator: AuthenticationFunction, + defaultLogger?: TwakeLogger +): Router => { + const logger = defaultLogger ?? getLogger(config as unknown as LoggerConfig) + const router = Router() + const authenticate = authMiddleware(authenticator, logger) + const controller = new MetricsApiController(matrixDb, logger) + const middleware = new MetricsApiMiddleware(matrixDb, logger) + + /** + * @openapi + * components: + * schemas: + * ActivityMetric: + * type: object + * properties: + * dailyActiveUsers: + * type: number + * weeklyActiveUsers: + * type: number + * monthlyActiveUsers: + * type: number + * weeklyNewUsers: + * type: number + * monthlyNewUsers: + * type: number + * MessageMetric: + * type: object + * array: + * items: + * type: object + * properties: + * user_id: + * type: string + * message_count: + * type: number + */ + + /** + * @openapi + * /_twake/v1/metrics/activity: + * get: + * tags: + * - Metrics + * description: Get user activity metrics + * responses: + * 200: + * description: Activity metrics found + * content: + * application/json: + * schema: + * type: object + * $ref: '#/components/schemas/ActivityMetric' + * 500: + * description: Internal error + * 400: + * description: Bad request + * 401: + * description: Unauthorized + * 403: + * description: Forbidden + * 404: + * description: Activity metrics not found + */ + router.get( + `${PATH}/activity`, + authenticate, + middleware.checkPermissions, + controller.getActivityStats + ) + + /** + * @openapi + * /_twake/v1/metrics/messages: + * get: + * tags: + * - Metrics + * description: Get user messages metrics + * responses: + * 200: + * description: Messages metrics found + * content: + * application/json: + * schema: + * type: object + * $ref: '#/components/schemas/MessageMetric' + * 500: + * description: Internal error + * 400: + * description: Bad request + * 401: + * description: Unauthorized + * 403: + * description: Forbidden + * 404: + * description: Messages metrics not found + */ + router.get( + `${PATH}/messages`, + authenticate, + middleware.checkPermissions, + controller.getMessageStats + ) + + return router +} From ce2af1e19b1c0d1bc492744119cc214ef1a44c68 Mon Sep 17 00:00:00 2001 From: Khaled FERJANI Date: Tue, 10 Dec 2024 16:19:57 +0100 Subject: [PATCH 05/15] =?UTF-8?q?=F0=9F=8F=B7=EF=B8=8F=20chore:=20added=20?= =?UTF-8?q?metrics=20API=20related=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/tom-server/src/metrics-api/types.ts | 39 ++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 packages/tom-server/src/metrics-api/types.ts diff --git a/packages/tom-server/src/metrics-api/types.ts b/packages/tom-server/src/metrics-api/types.ts new file mode 100644 index 00000000..0ab7f5b5 --- /dev/null +++ b/packages/tom-server/src/metrics-api/types.ts @@ -0,0 +1,39 @@ +export interface IMetricsService { + getUserActivityStats: () => Promise + getUserMessageStats: () => Promise +} + +export interface UserActivityStats { + /** + * active users stats + */ + dailyActiveUsers: MatrixUserInfo[] + weeklyActiveUsers: MatrixUserInfo[] + monthlyActiveUsers: MatrixUserInfo[] + + /** + * New sign-ups stats + */ + weeklyNewUsers: MatrixUserInfo[] + monthlyNewUsers: MatrixUserInfo[] +} + +export interface MatrixUserInfo { + name: string + displayname: string + avatar_url: string + last_seen_ts: number + creation_ts: number +} + +export interface UserMessageCount { + user_id: string + message_count: number +} + +export interface UserMessageEvent { + sender: string + type: string + room_id: string + content: string +} \ No newline at end of file From 8029f75b4938ad5be7af5e4397b3afcae2f528cd Mon Sep 17 00:00:00 2001 From: Khaled FERJANI Date: Tue, 10 Dec 2024 16:20:26 +0100 Subject: [PATCH 06/15] =?UTF-8?q?=F0=9F=8E=A8=20chore:=20added=20events=20?= =?UTF-8?q?table=20to=20MatrixDB=20collections?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/matrix-identity-server/src/matrixDb/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/matrix-identity-server/src/matrixDb/index.ts b/packages/matrix-identity-server/src/matrixDb/index.ts index 0e9a5a49..b3c88fa8 100644 --- a/packages/matrix-identity-server/src/matrixDb/index.ts +++ b/packages/matrix-identity-server/src/matrixDb/index.ts @@ -11,6 +11,7 @@ export type Collections = | 'room_aliases' | 'room_stats_state' | 'event_json' + | 'events' type Get = ( table: Collections, From 0ec01a417b75b901970514fe76b33a176f2f2577 Mon Sep 17 00:00:00 2001 From: Khaled FERJANI Date: Tue, 10 Dec 2024 16:21:00 +0100 Subject: [PATCH 07/15] =?UTF-8?q?=F0=9F=8E=A8=20chore:=20export=20metrics?= =?UTF-8?q?=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/tom-server/src/metrics-api/index.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 packages/tom-server/src/metrics-api/index.ts diff --git a/packages/tom-server/src/metrics-api/index.ts b/packages/tom-server/src/metrics-api/index.ts new file mode 100644 index 00000000..eec21749 --- /dev/null +++ b/packages/tom-server/src/metrics-api/index.ts @@ -0,0 +1 @@ +export { default } from './routes' From 155166e055fb0fceabb2719a9441414de0e5b71a Mon Sep 17 00:00:00 2001 From: Khaled FERJANI Date: Tue, 10 Dec 2024 16:21:35 +0100 Subject: [PATCH 08/15] =?UTF-8?q?=F0=9F=8E=A8=20feat:=20integrate=20matric?= =?UTF-8?q?s=20API=20routes=20to=20main=20app?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/tom-server/src/index.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/tom-server/src/index.ts b/packages/tom-server/src/index.ts index b180e308..77bc2c67 100644 --- a/packages/tom-server/src/index.ts +++ b/packages/tom-server/src/index.ts @@ -22,6 +22,7 @@ import VaultServer from './vault-api' import WellKnown from './wellKnown' import ActiveContacts from './active-contacts-api' import QRCode from './qrcode-api' +import MetricsRouter from './metrics-api' export default class TwakeServer { conf: Config @@ -147,6 +148,12 @@ export default class TwakeServer { this.logger ) const qrCodeApi = QRCode(this.idServer, this.conf, this.logger) + const metricsApi = MetricsRouter( + this.conf, + this.matrixDb.db, + this.idServer.authenticate, + this.logger + ) this.endpoints.use(privateNoteApi) this.endpoints.use(mutualRoolsApi) @@ -156,6 +163,7 @@ export default class TwakeServer { this.endpoints.use(smsApi) this.endpoints.use(activeContactsApi) this.endpoints.use(qrCodeApi) + this.endpoints.use(metricsApi) if ( this.conf.opensearch_is_activated != null && From c35a20538058879f6e911bac4f03a5804db633c9 Mon Sep 17 00:00:00 2001 From: Khaled FERJANI Date: Tue, 10 Dec 2024 16:22:19 +0100 Subject: [PATCH 09/15] =?UTF-8?q?=F0=9F=9A=A8=20chore:=20lint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/tom-server/src/metrics-api/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tom-server/src/metrics-api/types.ts b/packages/tom-server/src/metrics-api/types.ts index 0ab7f5b5..8572b73f 100644 --- a/packages/tom-server/src/metrics-api/types.ts +++ b/packages/tom-server/src/metrics-api/types.ts @@ -36,4 +36,4 @@ export interface UserMessageEvent { type: string room_id: string content: string -} \ No newline at end of file +} From 8cf16b7f712fc0dc86796ea92a3e77c320e53383 Mon Sep 17 00:00:00 2001 From: Khaled FERJANI Date: Tue, 10 Dec 2024 16:22:38 +0100 Subject: [PATCH 10/15] =?UTF-8?q?=F0=9F=A7=AA=20chore:=20added=20controlle?= =?UTF-8?q?r=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/metrics-api/tests/controller.test.ts | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 packages/tom-server/src/metrics-api/tests/controller.test.ts diff --git a/packages/tom-server/src/metrics-api/tests/controller.test.ts b/packages/tom-server/src/metrics-api/tests/controller.test.ts new file mode 100644 index 00000000..d7e4e86b --- /dev/null +++ b/packages/tom-server/src/metrics-api/tests/controller.test.ts @@ -0,0 +1,216 @@ +import bodyParser from 'body-parser' +import express, { type NextFunction, type Response } from 'express' +import supertest from 'supertest' +import type { AuthRequest, Config } from '../../types' +import router, { PATH } from '../routes' +import type { TwakeLogger } from '@twake/logger' +import { type MatrixDBBackend } from '@twake/matrix-identity-server' +import { type UserMessageCount, type MatrixUserInfo } from '../types' + +const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000 +const ONE_WEEK_IN_MS = 7 * ONE_DAY_IN_MS +const ONE_MONTH_IN_MS = 30 * ONE_DAY_IN_MS + +const TODAY_USER = { + avatar_url: '', + creation_ts: 1, + displayname: 'user 1', + last_seen_ts: new Date().getTime(), + name: 'user1' +} satisfies MatrixUserInfo + +const PRE_TODAY_USER = { + avatar_url: '', + creation_ts: 1, + displayname: 'user 1', + last_seen_ts: new Date().getTime() - ONE_DAY_IN_MS - 1, + name: 'user2' +} + +const PRE_WEEK_USER = { + avatar_url: '', + creation_ts: 1, + displayname: 'user 2', + last_seen_ts: new Date().getTime() - ONE_WEEK_IN_MS - 1, + name: 'user3' +} + +const PRE_MONTH_USER = { + avatar_url: '', + creation_ts: 1, + displayname: 'user 3', + last_seen_ts: new Date().getTime() - ONE_MONTH_IN_MS - 1, + name: 'user4' +} + +const messages = [ + { + sender: 'user1', + type: 'm.room.message', + origin_server_ts: 1, + content: { + body: 'message 1' + } + }, + { + sender: 'user1', + type: 'm.room.message', + origin_server_ts: 1, + content: { + body: 'message 3' + } + }, + { + sender: 'user1', + type: 'm.room.message', + origin_server_ts: 1, + content: { + body: 'message 4' + } + } +] + +const app = express() + +const dbMock = { + get: jest.fn(), + getAll: jest.fn(), + insert: jest.fn(), + update: jest.fn(), + deleteEqual: jest.fn(), + getCount: jest.fn() +} + +const loggerMock = { + info: jest.fn(), + error: jest.fn(), + warn: jest.fn() +} + +const authenticatorMock = jest + .fn() + .mockImplementation((_req, _res, callbackMethod) => { + callbackMethod('test', 'test') + }) + +jest.mock('../middlewares/index.ts', () => { + const passiveMiddlewareMock = ( + _req: AuthRequest, + _res: Response, + next: NextFunction + ): void => { + next() + } + + return function () { + return { + checkPermissions: passiveMiddlewareMock + } + } +}) + +app.use(bodyParser.json()) +app.use(bodyParser.urlencoded({ extended: true })) +app.use( + router( + {} as unknown as Config, + dbMock as unknown as MatrixDBBackend, + authenticatorMock, + loggerMock as unknown as TwakeLogger + ) +) + +describe('the mectrics API controller', () => { + describe('the getActivityStats handler', () => { + it('should try to fetch the user activity metrics', async () => { + dbMock.getAll.mockResolvedValue([TODAY_USER]) + + const response = await supertest(app).get(`${PATH}/activity`).send() + + expect(response.status).toBe(200) + expect(response.body).toEqual({ + dailyActiveUsers: [TODAY_USER], + weeklyActiveUsers: [TODAY_USER], + monthlyActiveUsers: [TODAY_USER], + weeklyNewUsers: [], + monthlyNewUsers: [] + }) + }) + + it('should calculate daily active users correctly', async () => { + dbMock.getAll.mockResolvedValue([TODAY_USER, PRE_TODAY_USER]) + + const response = await supertest(app).get(`${PATH}/activity`).send() + + expect(response.status).toBe(200) + expect(response.body).toEqual({ + dailyActiveUsers: [TODAY_USER], + weeklyActiveUsers: [TODAY_USER, PRE_TODAY_USER], + monthlyActiveUsers: [TODAY_USER, PRE_TODAY_USER], + weeklyNewUsers: [], + monthlyNewUsers: [] + }) + }) + + it('should calculate weekly active users correctly', async () => { + dbMock.getAll.mockResolvedValue([TODAY_USER, PRE_WEEK_USER]) + + const response = await supertest(app).get(`${PATH}/activity`).send() + + expect(response.status).toBe(200) + expect(response.body).toEqual({ + dailyActiveUsers: [TODAY_USER], + weeklyActiveUsers: [TODAY_USER], + monthlyActiveUsers: [TODAY_USER, PRE_WEEK_USER], + weeklyNewUsers: [], + monthlyNewUsers: [] + }) + }) + + it('should calculate monthly active users correctly', async () => { + dbMock.getAll.mockResolvedValue([PRE_WEEK_USER, PRE_MONTH_USER]) + + const response = await supertest(app).get(`${PATH}/activity`).send() + + expect(response.status).toBe(200) + expect(response.body).toEqual({ + dailyActiveUsers: [], + weeklyActiveUsers: [], + monthlyActiveUsers: [PRE_WEEK_USER], + weeklyNewUsers: [], + monthlyNewUsers: [] + }) + }) + + it('should return an error if something wrong happens while fetching the activity stats', async () => { + dbMock.getAll.mockRejectedValue(new Error('test')) + + const response = await supertest(app).get(`${PATH}/activity`).send() + + expect(response.status).toBe(500) + }) + }) + + describe('the getMessageStats handler', () => { + it('should try to fetch the message stats', async () => { + dbMock.getAll.mockResolvedValue([TODAY_USER]) + + dbMock.get.mockResolvedValue(messages) + + const response = await supertest(app).get(`${PATH}/messages`).send() + + expect(response.status).toBe(200) + expect(response.body).toEqual([ + { message_count: 3, user_id: 'user1' } + ] satisfies UserMessageCount[]) + }) + + it('should return an error if something wrong happens while fetching the message stats', async () => { + dbMock.get.mockRejectedValue(new Error('test')) + + const response = await supertest(app).get(`${PATH}/messages`).send() + + expect(response.status).toBe(500) + }) + }) +}) From 8ef89a7c21e267fed41f1785172568152646f6a8 Mon Sep 17 00:00:00 2001 From: Khaled FERJANI Date: Tue, 10 Dec 2024 16:22:53 +0100 Subject: [PATCH 11/15] =?UTF-8?q?=F0=9F=A7=AA=20chore:=20added=20middlewar?= =?UTF-8?q?e=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/metrics-api/tests/middleware.test.ts | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 packages/tom-server/src/metrics-api/tests/middleware.test.ts diff --git a/packages/tom-server/src/metrics-api/tests/middleware.test.ts b/packages/tom-server/src/metrics-api/tests/middleware.test.ts new file mode 100644 index 00000000..9727696c --- /dev/null +++ b/packages/tom-server/src/metrics-api/tests/middleware.test.ts @@ -0,0 +1,98 @@ +import type { AuthRequest } from '../../types' +import type { Response, NextFunction } from 'express' +import Middleware from '../middlewares' +import { type MatrixDBBackend } from '@twake/matrix-identity-server' +import { type TwakeLogger } from '@twake/logger' + +let mockRequest: Partial +let mockResponse: Partial + +const dbMock = { + get: jest.fn(), + getAll: jest.fn(), + insert: jest.fn(), + update: jest.fn(), + deleteEqual: jest.fn(), + getCount: jest.fn() +} + +const loggerMock = { + info: jest.fn(), + error: jest.fn(), + warn: jest.fn() +} + +const nextFunction: NextFunction = jest.fn() +const metricsMiddleware = new Middleware( + dbMock as unknown as MatrixDBBackend, + loggerMock as unknown as TwakeLogger +) + +beforeEach(() => { + mockRequest = { + body: {}, + query: {}, + userId: 'test' + } + + mockResponse = { + json: jest.fn(), + status: jest.fn().mockReturnThis() + } +}) + +describe('the Metrics API middleware', () => { + it('should not call the next handler when the user is not administrator', async () => { + dbMock.get.mockResolvedValue([]) + + await metricsMiddleware.checkPermissions( + mockRequest as AuthRequest, + mockResponse as Response, + nextFunction + ) + + expect(nextFunction).not.toHaveBeenCalled() + }) + + it('should call the next handler when the user is administrator', async () => { + dbMock.get.mockResolvedValue([{ name: 'user', admin: 1 }]) + + await metricsMiddleware.checkPermissions( + mockRequest as AuthRequest, + mockResponse as Response, + nextFunction + ) + + expect(nextFunction).toHaveBeenCalled() + }) + + it('should return 403 if the user is not administrator', async () => { + dbMock.get.mockResolvedValue([]) + + await metricsMiddleware.checkPermissions( + mockRequest as AuthRequest, + mockResponse as Response, + nextFunction + ) + + expect(mockResponse.status).toHaveBeenCalledWith(403) + expect(mockResponse.json).toHaveBeenCalledWith({ + message: 'Forbidden' + }) + }) + + it('should return 400 if something wrong happens', async () => { + dbMock.get.mockRejectedValue(new Error('Something wrong happened')) + + await metricsMiddleware.checkPermissions( + {} as unknown as AuthRequest, + mockResponse as Response, + nextFunction + ) + + expect(mockResponse.status).toHaveBeenCalledWith(400) + expect(mockResponse.json).toHaveBeenCalledWith({ + message: 'Bad Request' + }) + }) +}) From 179ccc40ae607249dacd2bdcd7f14979afc97ecd Mon Sep 17 00:00:00 2001 From: Khaled FERJANI Date: Tue, 10 Dec 2024 16:23:05 +0100 Subject: [PATCH 12/15] =?UTF-8?q?=F0=9F=A7=AA=20chore:=20added=20route=20t?= =?UTF-8?q?est?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/metrics-api/tests/router.test.ts | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 packages/tom-server/src/metrics-api/tests/router.test.ts diff --git a/packages/tom-server/src/metrics-api/tests/router.test.ts b/packages/tom-server/src/metrics-api/tests/router.test.ts new file mode 100644 index 00000000..256bf3a6 --- /dev/null +++ b/packages/tom-server/src/metrics-api/tests/router.test.ts @@ -0,0 +1,142 @@ +import express, { type Response, type NextFunction } from 'express' +import bodyParser from 'body-parser' +import type { AuthRequest, Config } from '../../types' +import IdServer from '../../identity-server' +import type { ConfigDescription } from '@twake/config-parser' +import type { TwakeLogger } from '@twake/logger' +import { IdentityServerDb, type MatrixDB } from '@twake/matrix-identity-server' +import router, { PATH } from '../routes' +import errorMiddleware from '../../utils/middlewares/error.middleware' +import JEST_PROCESS_ROOT_PATH from '../../../jest.globals' +import fs from 'fs' +import path from 'path' +import supertest from 'supertest' + +const mockLogger: Partial = { + debug: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + close: jest.fn() +} + +jest + .spyOn(IdentityServerDb.prototype, 'get') + .mockResolvedValue([{ data: '"test"' }]) + +const idServer = new IdServer( + { + get: jest.fn() + } as unknown as MatrixDB, + {} as unknown as Config, + { + database_engine: 'sqlite', + database_host: 'test.db', + rate_limiting_window: 5000, + rate_limiting_nb_requests: 10, + template_dir: './templates', + userdb_host: './tokens.db' + } as unknown as ConfigDescription, + mockLogger as TwakeLogger +) + +const app = express() +const middlewareSpy = jest.fn().mockImplementation((_req, _res, next) => { + next() +}) + +jest.mock('../middlewares', () => { + return function () { + return { + checkPermissions: middlewareSpy + } + } +}) + +jest.mock('../controllers', () => { + const passiveController = ( + _req: AuthRequest, + res: Response, + _next: NextFunction + ): void => { + res.status(200).json({ message: 'test' }) + } + + return function () { + return { + getActivityStats: passiveController, + getMessageStats: passiveController + } + } +}) + +app.use(bodyParser.json()) +app.use(bodyParser.urlencoded({ extended: true })) + +describe('the Metrics API router', () => { + beforeAll((done) => { + idServer.ready + .then(() => { + app.use( + router( + idServer.conf, + idServer.matrixDb.db, + idServer.authenticate, + idServer.logger + ) + ) + + app.use(errorMiddleware(idServer.logger)) + done() + }) + .catch((e) => { + done(e) + }) + }) + + afterAll(() => { + idServer.cleanJobs() + + const pathFilesToDelete = [ + path.join(JEST_PROCESS_ROOT_PATH, 'test.db'), + path.join(JEST_PROCESS_ROOT_PATH, 'tokens.db') + ] + + pathFilesToDelete.forEach((path) => { + if (fs.existsSync(path)) fs.unlinkSync(path) + }) + }) + + it('should reject if rate limit is exceeded', async () => { + let response + + for (let i = 0; i < 11; i++) { + response = await supertest(app) + .get(`${PATH}/activity`) + .set('Authorization', 'Bearer test') + } + + expect((response as unknown as Response).status).toEqual(429) + await new Promise((resolve) => setTimeout(resolve, 6000)) + }) + + it('should not call the validation middleware if the Bearer token is not set', async () => { + const response = await supertest(app).get(`${PATH}/activity`) + + expect(response.status).toEqual(401) + expect(middlewareSpy).not.toHaveBeenCalled() + }) + + it('should call the validation middleware if the Bearer token is set', async () => { + await supertest(app) + .get(`${PATH}/activity`) + .set('Authorization', 'Bearer test') + + expect(middlewareSpy).toHaveBeenCalled() + }) + + it('should call the validation middleware if the access_token is set in the query', async () => { + await supertest(app).get(`${PATH}/activity`).query({ access_token: 'test' }) + + expect(middlewareSpy).toHaveBeenCalled() + }) +}) From 460ba2f240a0718217c8bd50d45786a755b27310 Mon Sep 17 00:00:00 2001 From: Khaled FERJANI Date: Tue, 10 Dec 2024 16:23:24 +0100 Subject: [PATCH 13/15] =?UTF-8?q?=F0=9F=A7=AA=20chore:=20added=20metrics?= =?UTF-8?q?=20service=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/metrics-api/tests/service.test.ts | 240 ++++++++++++++++++ 1 file changed, 240 insertions(+) create mode 100644 packages/tom-server/src/metrics-api/tests/service.test.ts diff --git a/packages/tom-server/src/metrics-api/tests/service.test.ts b/packages/tom-server/src/metrics-api/tests/service.test.ts new file mode 100644 index 00000000..9dab9acc --- /dev/null +++ b/packages/tom-server/src/metrics-api/tests/service.test.ts @@ -0,0 +1,240 @@ +import { type TwakeLogger } from '@twake/logger' +import Service from '../services' +import { type MatrixDBBackend } from '@twake/matrix-identity-server' + +const dbMock = { + get: jest.fn(), + getAll: jest.fn(), + insert: jest.fn(), + update: jest.fn(), + deleteEqual: jest.fn(), + getCount: jest.fn() +} + +const loggerMock = { + info: jest.fn(), + error: jest.fn(), + warn: jest.fn() +} + +const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000 +const ONE_WEEK_IN_MS = 7 * ONE_DAY_IN_MS +const ONE_MONTH_IN_MS = 30 * ONE_DAY_IN_MS + +const TODAY_USER = { + avatar_url: '', + creation_ts: 1, + displayname: 'user 1', + last_seen_ts: new Date().getTime(), + name: 'user1' +} + +const PRE_TODAY_USER = { + avatar_url: '', + creation_ts: 1, + displayname: 'user 1', + last_seen_ts: new Date().getTime() - ONE_DAY_IN_MS - 1, + name: 'user2' +} + +const PRE_WEEK_USER = { + avatar_url: '', + creation_ts: 1, + displayname: 'user 2', + last_seen_ts: new Date().getTime() - ONE_WEEK_IN_MS - 1, + name: 'user3' +} + +const PRE_MONTH_USER = { + avatar_url: '', + creation_ts: 1, + displayname: 'user 3', + last_seen_ts: new Date().getTime() - ONE_MONTH_IN_MS - 1, + name: 'user4' +} + +const messages = [ + { + sender: 'user1', + type: 'm.room.message', + origin_server_ts: 1, + content: { + body: 'message 1' + } + }, + { + sender: 'user1', + type: 'm.room.message', + origin_server_ts: 1, + content: { + body: 'message 3' + } + }, + { + sender: 'user1', + type: 'm.room.message', + origin_server_ts: 1, + content: { + body: 'message 4' + } + } +] + +afterEach(() => { + jest.restoreAllMocks() +}) + +describe('the Metrics API Service', () => { + const metricsService = new Service( + dbMock as unknown as MatrixDBBackend, + loggerMock as unknown as TwakeLogger + ) + + describe('the getUserActivityStats function', () => { + it('should attempts to get user activity stats', async () => { + dbMock.getAll.mockResolvedValue([TODAY_USER]) + + const result = await metricsService.getUserActivityStats() + + expect(result).toEqual({ + dailyActiveUsers: [TODAY_USER], + weeklyActiveUsers: [TODAY_USER], + monthlyActiveUsers: [TODAY_USER], + weeklyNewUsers: [], + monthlyNewUsers: [] + }) + }) + + it('should return empty arrays if no users are found', async () => { + dbMock.getAll.mockResolvedValue([]) + + const result = await metricsService.getUserActivityStats() + + expect(result).toEqual({ + dailyActiveUsers: [], + weeklyActiveUsers: [], + monthlyActiveUsers: [], + weeklyNewUsers: [], + monthlyNewUsers: [] + }) + }) + + it('should return only today active users', async () => { + dbMock.getAll.mockResolvedValue([TODAY_USER, PRE_TODAY_USER]) + + const result = await metricsService.getUserActivityStats() + + expect(result).toEqual({ + dailyActiveUsers: [TODAY_USER], + weeklyActiveUsers: [TODAY_USER, PRE_TODAY_USER], + monthlyActiveUsers: [TODAY_USER, PRE_TODAY_USER], + weeklyNewUsers: [], + monthlyNewUsers: [] + }) + }) + + it('should return only weekly active users', async () => { + dbMock.getAll.mockResolvedValue([ + TODAY_USER, + PRE_TODAY_USER, + PRE_WEEK_USER + ]) + + const result = await metricsService.getUserActivityStats() + + expect(result).toEqual({ + dailyActiveUsers: [TODAY_USER], + weeklyActiveUsers: [TODAY_USER, PRE_TODAY_USER], + monthlyActiveUsers: [TODAY_USER, PRE_TODAY_USER, PRE_WEEK_USER], + weeklyNewUsers: [], + monthlyNewUsers: [] + }) + }) + + it('should return only monthly active users', async () => { + dbMock.getAll.mockResolvedValue([ + TODAY_USER, + PRE_TODAY_USER, + PRE_WEEK_USER, + PRE_MONTH_USER + ]) + + const result = await metricsService.getUserActivityStats() + + expect(result).toEqual({ + dailyActiveUsers: [TODAY_USER], + weeklyActiveUsers: [TODAY_USER, PRE_TODAY_USER], + monthlyActiveUsers: [TODAY_USER, PRE_TODAY_USER, PRE_WEEK_USER], + weeklyNewUsers: [], + monthlyNewUsers: [] + }) + }) + }) + + describe('the getUserMessageStats function', () => { + it('should attempts to get user message stats', async () => { + dbMock.getAll.mockResolvedValue([TODAY_USER]) + dbMock.get.mockResolvedValue(messages) + + const result = await metricsService.getUserMessageStats() + + expect(result).toEqual([ + { + user_id: 'user1', + message_count: 3 + } + ]) + }) + + it('should return empty array if no users found', async () => { + dbMock.getAll.mockResolvedValue([]) + + const result = await metricsService.getUserMessageStats() + + expect(result).toEqual([]) + }) + + it('should return zero message count for users with no messages', async () => { + dbMock.getAll.mockResolvedValue([TODAY_USER]) + dbMock.get.mockResolvedValue([]) + + const result = await metricsService.getUserMessageStats() + + expect(result).toEqual([ + { + user_id: 'user1', + message_count: 0 + } + ]) + }) + + it('should count messages correctly for multiple users', async () => { + dbMock.getAll.mockResolvedValue([TODAY_USER, PRE_TODAY_USER]) + dbMock.get + .mockResolvedValueOnce(messages) // For first user + .mockResolvedValueOnce([messages[0]]) // For second user + + const result = await metricsService.getUserMessageStats() + + expect(result).toEqual([ + { + user_id: 'user1', + message_count: 3 + }, + { + user_id: 'user2', + message_count: 1 + } + ]) + }) + + it('should handle errors when getting messages', async () => { + dbMock.getAll.mockResolvedValue([TODAY_USER]) + dbMock.get.mockRejectedValue(new Error('DB Error')) + + await expect(metricsService.getUserMessageStats()).rejects.toThrow( + 'Failed to fetch user message count' + ) + }) + }) +}) From 38f6bd4dae2f7e22cf7d2c405b627d74667e9463 Mon Sep 17 00:00:00 2001 From: Khaled FERJANI Date: Tue, 10 Dec 2024 16:46:32 +0100 Subject: [PATCH 14/15] =?UTF-8?q?=F0=9F=90=9B=20fix:=20swagger=20openapi?= =?UTF-8?q?=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/metrics-api/routes/index.ts | 53 ++++++++----------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/packages/tom-server/src/metrics-api/routes/index.ts b/packages/tom-server/src/metrics-api/routes/index.ts index 59dc182a..46644683 100644 --- a/packages/tom-server/src/metrics-api/routes/index.ts +++ b/packages/tom-server/src/metrics-api/routes/index.ts @@ -30,28 +30,27 @@ export default ( * components: * schemas: * ActivityMetric: - * type: object - * properties: - * dailyActiveUsers: - * type: number - * weeklyActiveUsers: - * type: number - * monthlyActiveUsers: - * type: number - * weeklyNewUsers: - * type: number - * monthlyNewUsers: - * type: number + * type: object + * properties: + * dailyActiveUsers: + * type: number + * weeklyActiveUsers: + * type: number + * monthlyActiveUsers: + * type: number + * weeklyNewUsers: + * type: number + * monthlyNewUsers: + * type: number * MessageMetric: - * type: object - * array: - * items: - * type: object - * properties: - * user_id: - * type: string - * message_count: - * type: number + * type: array + * items: + * type: object + * properties: + * user_id: + * type: string + * message_count: + * type: number */ /** @@ -68,17 +67,13 @@ export default ( * application/json: * schema: * type: object - * $ref: '#/components/schemas/ActivityMetric' + * $ref: '#/components/schemas/ActivityMetric' * 500: * description: Internal error * 400: * description: Bad request - * 401: - * description: Unauthorized * 403: * description: Forbidden - * 404: - * description: Activity metrics not found */ router.get( `${PATH}/activity`, @@ -101,17 +96,13 @@ export default ( * application/json: * schema: * type: object - * $ref: '#/components/schemas/MessageMetric' + * $ref: '#/components/schemas/MessageMetric' * 500: * description: Internal error * 400: * description: Bad request - * 401: - * description: Unauthorized * 403: * description: Forbidden - * 404: - * description: Messages metrics not found */ router.get( `${PATH}/messages`, From f57b2e265a3fe5daf2e95895e1bc4ce908e99330 Mon Sep 17 00:00:00 2001 From: Khaled FERJANI Date: Tue, 10 Dec 2024 16:47:08 +0100 Subject: [PATCH 15/15] =?UTF-8?q?=F0=9F=93=9D=20chore:=20generate=20swagge?= =?UTF-8?q?r=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/openapi.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/openapi.json b/docs/openapi.json index aeecbc58..03f7e714 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -1 +1 @@ -{"openapi":"3.0.0","info":{"title":"Twake on Matrix APIs documentation","version":"0.0.1","description":"This is The documentation of all available APIs of this repository"},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"MatrixError":{"type":"object","properties":{"errcode":{"type":"string","description":"A Matrix error code"},"error":{"type":"string","description":"A human-readable error message"}},"required":["error"]},"ActiveContacts":{"type":"object","description":"the list of active contacts","properties":{"contacts":{"type":"string","description":"active contacts"}}},"MutualRooms":{"type":"array","items":{"type":"object","properties":{"roomId":{"type":"string","description":"the room id"},"name":{"type":"string","description":"the room name"},"topic":{"type":"string","description":"the room topic"},"room_type":{"type":"string","description":"the room type"}}}},"PrivateNote":{"type":"object","properties":{"id":{"type":"string","description":"The private note id"},"content":{"type":"string","description":"The private note content"},"authorId":{"type":"string","description":"The author user id"},"targetId":{"type":"string","description":"The target user id"}}},"CreatePrivateNote":{"type":"object","properties":{"content":{"type":"string","description":"The private note content"},"authorId":{"type":"string","description":"The author user id"},"targetId":{"type":"string","description":"The target user id"}}},"UpdatePrivateNote":{"type":"object","properties":{"id":{"type":"string","description":"The private note id"},"content":{"type":"string","description":"The private note content"}}},"RoomTags":{"type":"object","properties":{"tags":{"description":"the room tags list","type":"array","items":{"type":"string"}}}},"RoomTagCreation":{"type":"object","properties":{"content":{"type":"array","description":"the room tags strings","items":{"type":"string"}},"roomId":{"type":"string","description":"the room id"}}},"RoomTagsUpdate":{"type":"object","properties":{"content":{"type":"array","description":"the room tags strings","items":{"type":"string"}}}},"sms":{"type":"object","properties":{"to":{"oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"text":{"type":"string"}}},"UserInfo":{"type":"object","properties":{"uid":{"type":"string","description":"the user id"},"givenName":{"type":"string","description":"the user given name"},"sn":{"type":"string","description":"the user surname"}}}},"responses":{"InternalServerError":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"The message describing the internal error"}}}}}},"Unauthorized":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"errcode":"M_UNAUTHORIZED","error":"Unauthorized"}}}},"BadRequest":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"errcode":"M_MISSING_PARAMS","error":"Properties are missing in the request body"}}}},"Forbidden":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"errcode":"M_FORBIDDEN","error":"Forbidden"}}}},"Conflict":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"error":"Conflict"}}}},"PermanentRedirect":{"description":"Permanent Redirect","headers":{"Location":{"schema":{"type":"string","description":"URL to use for recdirect"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"errcode":"M_UNKNOWN","error":"This non-standard endpoint has been removed"}}}},"NotFound":{"description":"Private note not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"errcode":"M_NOT_FOUND","error":"Not Found"}}}},"Unrecognized":{"description":"Unrecognized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"errcode":"M_UNRECOGNIZED","error":"Unrecognized"}}}},"Created":{"description":"Created"},"NoContent":{"description":"operation successful and no content returned"},"InternalError":{"description":"Internal error"}},"parameters":{"target_userid":{"name":"target_userid","in":"path","required":true,"description":"the target user id","schema":{"type":"string"}},"user_id":{"name":"user_id","in":"query","description":"the author user id","required":true,"schema":{"type":"string"}},"target_user_id":{"name":"target_user_id","in":"query","description":"the target user id","required":true,"schema":{"type":"string"}},"private_note_id":{"name":"private_note_id","in":"path","description":"the private note id","required":true,"schema":{"type":"string"}},"roomId":{"in":"path","name":"roomId","description":"the room id","required":true,"schema":{"type":"string"}},"userId":{"in":"path","name":"userId","description":"the user id","required":true,"schema":{"type":"string"}}}},"security":[{"bearerAuth":[]}],"paths":{"/_matrix/identity/v2":{"get":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#get_matrixidentityv2"}},"/_matrix/identity/v2/hash_details":{"get":{"tags":["Federated identity service"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#get_matrixidentityv2hash_details"}},"/_matrix/identity/v2/lookup":{"post":{"tags":["Federated identity service"],"description":"Extends https://spec.matrix.org/v1.6/identity-service-api/#post_matrixidentityv2lookup to display inactive users and 3PID users","requestBody":{"description":"Object containing hashes of mails/phones to search","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"addresses":{"type":"array","items":{"type":"string","description":"List of (hashed) addresses to lookup"}},"algorithm":{"type":"string","description":"Algorithm the client is using to encode the addresses"},"pepper":{"type":"string","description":"Pepper from '/hash_details'"}},"required":["addresses","algorithm","pepper"]},"example":{"addresses":["4kenr7N9drpCJ4AfalmlGQVsOn3o2RHjkADUpXJWZUc","nlo35_T5fzSGZzJApqu8lgIudJvmOQtDaHtr-I4rU7I"],"algorithm":"sha256","pepper":"matrixrocks"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"mappings":{"type":"object","additionalProperties":{"type":"string"},"description":"List of active accounts"},"inactive_mappings":{"type":"object","additionalProperties":{"type":"string"},"description":"List of inactive accounts"},"third_party_mappings":{"type":"object","description":"List of hashed addresses by identity server hostname","properties":{"hostname":{"type":"object","properties":{"actives":{"type":"array","items":{"type":"string","description":"List of (hashed) active accounts addresses matching request body addresses"}},"inactives":{"type":"array","items":{"type":"string","description":"List of (hashed) inactive accounts addresses matching request body addresses"}}}}}}}},"example":{"mappings":{"4kenr7N9drpCJ4AfalmlGQVsOn3o2RHjkADUpXJWZUc":"@dwho:company.com"},"inactive_mappings":{"nlo35_T5fzSGZzJApqu8lgIudJvmOQtDaHtr-I4rU7I":"@rtyler:company.com"},"third_party_mappings":{"identity1.example.com":{"actives":["78jnr7N9drpCJ4AfalmlGQVsOn3o2RHjkADUpXJWZUc","gtr42_T5fzSGZzJAmlp5lgIudJvmOQtDaHtr-I4rU7I"],"inactives":["qfgt57N9drpCJ4AfalmlGQVsOn3o2RHjkADUpXJWZUc","lnbc8_T5fzSGZzJAmlp5lgIudJvmOQtDaHtr-I4rU7I"]}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/Unrecognized"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_matrix/identity/v2/account":{"get":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#get_matrixidentityv2account"}},"/_matrix/identity/v2/account/register":{"post":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#post_matrixidentityv2accountregister"}},"/_matrix/identity/v2/account/logout":{"post":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#post_matrixidentityv2accountlogout"}},"/_matrix/identity/v2/terms":{"get":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#get_matrixidentityv2terms"}},"/_matrix/identity/v2/validate/email/requestToken":{"post":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#post_matrixidentityv2validateemailrequesttoken"}},"/_matrix/identity/v2/validate/email/submitToken":{"post":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#post_matrixidentityv2validateemailsubmittoken"}},"/_matrix/identity/versions":{"get":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#get_matrixidentityversions"}},"/_twake/identity/v1/lookup/match":{"post":{"tags":["Identity server"],"description":"Looks up the Organization User IDs which match value sent","requestBody":{"description":"Object containing detail for the search and the returned data","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"scope":{"type":"array","items":{"type":"string","description":"List of fields to search in (uid, mail,...)"}},"fields":{"type":"array","items":{"type":"string","description":"List of fields to return for matching users (uid, mail, mobile, displayName, givenName, cn, sn)"}},"val":{"type":"string","description":"Optional value to search"},"limit":{"type":"integer","description":"Optional max number of result to return (default 30)"},"offset":{"type":"integer","description":"Optional offset for pagination"}},"required":["scope","fields"]},"example":{"scope":["mail","uid"],"fields":["uid","displayName","sn","givenName","mobile"],"val":"rtyler","limit":3}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"matches":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string","description":"Matrix address"},"uid":{"type":"string","description":"id of a matching user"},"mail":{"type":"string","description":"email address of a matching user"}}},"description":"List of users that match"}}},"example":{"matches":[{"uid":"dwho","mail":"dwho@badwolf.com"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/_twake/identity/v1/lookup/diff":{"post":{"tags":["Identity server"],"description":"Looks up the Organization User IDs updated since X","requestBody":{"description":"Object containing the timestamp","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"since":{"type":"integer","description":"timestamp"},"fields":{"type":"array","items":{"type":"string","description":"List of fields to return for matching users"}},"limit":{"type":"integer","description":"Optional max number of result to return (default 30)"},"offset":{"type":"integer","description":"Optional offset for pagination"}}},"example":{"since":1685074279,"fields":["uid","mail"],"limit":3}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"matches":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string","description":"Matrix address"},"timestamp":{"type":"integer","description":"current server timestamp"},"uid":{"type":"string","description":"id of a matching user"},"mail":{"type":"string","description":"email address of a matching user"}}},"description":"List of users that match"}}},"example":{"matches":[{"uid":"dwho","mail":"dwho@badwolf.com"}]}}}}}}},"/_twake/recoveryWords":{"get":{"tags":["Vault API"],"description":"Allow for the connected user to retrieve its recovery words","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"words":{"type":"string","description":"Recovery words of the connected user"}}},"example":{"words":"This is the recovery sentence of rtyler"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Connected user has no recovery sentence"}}},"example":{"error":"User has no recovery sentence"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Connected user has multiple recovery sentence"}}},"example":{"error":"User has more than one recovery sentence"}}}},"500":{"$ref":"#/components/responses/InternalServerError"}}},"post":{"tags":["Vault API"],"description":"Store connected user recovery words in database","requestBody":{"description":"Object containing the recovery words of the connected user","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"words":{"type":"string","description":"The recovery words of the connected user"}},"required":["words"]},"example":{"words":"This is the recovery sentence of rtyler"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Message indicating that words have been successfully saved"}},"example":{"message":"Saved recovery words sucessfully"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/InternalServerError"}}},"delete":{"tags":["Vault API"],"description":"Delete the user recovery words in the database","responses":{"204":{"description":"Delete success"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Connected user has no recovery sentence"}}},"example":{"error":"User has no recovery sentence"}}}},"500":{"$ref":"#/components/responses/InternalServerError"}}},"put":{"tags":["Vault API"],"description":"Update stored connected user recovery words in database","requestBody":{"description":"Object containing the recovery words of the connected user","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"words":{"type":"string","description":"The new recovery words of the connected user"}},"required":["words"]},"example":{"words":"This is the updated recovery sentence of rtyler"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Message indicating that words have been successfully updated"}},"example":{"message":"Updated recovery words sucessfully"}}}}},"400":{"description":"Bad request"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/.well-knwon/matrix/client":{"get":{"tags":["Auto configuration"],"description":"Get server metadata for auto configuration","responses":{"200":{"description":"Give server metadata","content":{"application/json":{"schema":{"type":"object","properties":{"m.homeserver":{"type":"object","properties":{"base_url":{"type":"string","description":"Base URL of Matrix server"}}},"m.identity_server":{"type":"object","properties":{"base_url":{"type":"string","description":"Base URL of Identity server"}}},"m.federated_identity_services":{"type":"object","properties":{"base_urls":{"type":"array","items":{"type":"string","description":"Base URL of Federated identity service"},"description":"Available Federated identity services Base URL list"}}},"t.server":{"type":"object","properties":{"base_url":{"type":"string","description":"Base URL of Identity server"},"server_name":{"type":"string","description":"Domain handled by Matrix server"}}},"m.integrations":{"type":"object","properties":{"jitsi":{"type":"object","properties":{"preferredDomain":{"type":"string","description":"Jitsi's preffered domain"},"baseUrl":{"type":"string","description":"URL of Jitsi server"},"useJwt":{"type":"boolean","description":"True if Jitsi server requires a JWT"},"jwt":{"type":"object","properties":{"algorithm":{"type":"string","description":"algorithm used to generate JWT"},"secret":{"type":"string","description":"password of JWTs"},"issuer":{"type":"string","description":"issuer of JWTs"}}}}}}},"m.authentication":{"type":"object","properties":{"issuer":{"type":"string","description":"URL of OIDC issuer"}}}}},"example":{"m.homeserver":{"base_url":"matrix.example.com"},"m.identity_server":{"base_url":"global-id-server.twake.app"},"m.federated_identity_services":{"base_urls":["global-federated_identity_service.twake.app","other-federated-identity-service.twake.app"]},"m.integrations":{"jitsi":{"baseUrl":"https://jitsi.example.com/","preferredDomain":"jitsi.example.com","useJwt":false}},"m.authentication":{"issuer":"https://auth.example.com"},"t.server":{"base_url":"https://tom.example.com","server_name":"example.com"}}}}}}}},"/_matrix/identity/v2/lookups":{"post":{"tags":["Federated identity service"],"description":"Implements https://github.com/guimard/matrix-spec-proposals/blob/unified-identity-service/proposals/4004-unified-identity-service-view.md","requestBody":{"description":"Object containing hashes to store in federated identity service database","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"mappings":{"type":"object","description":"List of hashed addresses by identity server hostname","properties":{"hostname":{"type":"array","items":{"type":"object","properties":{"hash":{"type":"string"},"active":{"type":"number"}}}}}},"algorithm":{"type":"string","description":"Algorithm the client is using to encode the addresses"},"pepper":{"type":"string","description":"Pepper from '/hash_details'"}},"required":["addresses","algorithm","pepper"]},"example":{"mappings":{"identity1.example.com":[{"hash":"4kenr7N9drpCJ4AfalmlGQVsOn3o2RHjkADUpXJWZUc","active":1},{"hash":"nlo35_T5fzSGZzJApqu8lgIudJvmOQtDaHtr-I4rU7I","active":0}]},"algorithm":"sha256","pepper":"matrixrocks"}}}},"responses":{"201":{"description":"Success"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/Unrecognized"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_matrix/app/v1/transactions/{txnId}":{"put":{"parameters":[{"in":"path","name":"txnId","required":true,"schema":{"type":"integer"},"description":"The transaction id"}],"tags":["Application server"],"description":"Implements https://spec.matrix.org/v1.6/application-service-api/#put_matrixappv1transactionstxnid","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object"}}}},"308":{"$ref":"#/components/responses/PermanentRedirect"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object"}}}},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_matrix/app/v1/users/{userId}":{"get":{"parameters":[{"in":"path","name":"userId","required":true,"schema":{"type":"integer"},"description":"The user id"}],"tags":["Application server"],"description":"Implements https://spec.matrix.org/v1.6/application-service-api/#get_matrixappv1usersuserid","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_matrix/app/v1/rooms/{roomAlias}":{"get":{"parameters":[{"in":"path","name":"roomAlias","required":true,"schema":{"type":"integer"},"description":"The room alias"}],"tags":["Application server"],"description":"Implements https://spec.matrix.org/v1.6/application-service-api/#get_matrixappv1roomsroomalias","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_twake/v1/activecontacts":{"get":{"tags":["Active contacts"],"description":"Get the list of active contacts","responses":{"200":{"description":"Active contacts found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActiveContacts"}}}},"401":{"description":"user is unauthorized"},"404":{"description":"Active contacts not found"},"500":{"description":"Internal error"}}},"post":{"tags":["Active contacts"],"description":"Create or update the list of active contacts","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActiveContacts"}}}},"responses":{"201":{"description":"Active contacts saved"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}},"delete":{"tags":["Active contacts"],"description":"Delete the list of active contacts","responses":{"200":{"description":"Active contacts deleted"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error/"}}}},"/_twake/app/v1/rooms":{"post":{"tags":["Application server"],"description":"Implements https://www.notion.so/Automatic-channels-89ba6f97bc90474ca482a28cf3228d3e","requestBody":{"description":"Object containing room's details","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"ldapFilter":{"type":"object","additionalProperties":true,"description":"An object containing keys/values to build a ldap filter"},"aliasName":{"type":"string","description":"The desired room alias local part. If aliasName is equal to foo the complete room alias will be"},"name":{"type":"string","description":"The room name"},"topic":{"type":"string","description":"A short message detailing what is currently being discussed in the room."},"visibility":{"type":"string","enum":["public","private"],"description":"visibility values:\n * `public` - The room will be shown in the published room list\n * `private` - Hide the room from the published room list\n"}},"required":["ldapFilter","aliasName"]},"example":{"ldapFilter":{"mail":"example@test.com","cn":"example"},"aliasName":"exp","name":"Example","topic":"This is an example of a room topic","visibility":"public"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"errcode":{"type":"string"},"error":{"type":"string"}},"additionalProperties":{"type":"string"},"description":"List of users uid not added to the new room due to an error"},"example":[{"uid":"test1","errcode":"M_FORBIDDEN","error":"The user has been banned from the room"},{"uid":"test2","errcode":"M_UNKNOWN","error":"Internal server error"}]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"examples":{"example1":{"value":{"error":"Error field: Invalid value (property: name)"}},"example2":{"value":{"errcode":"M_NOT_JSON","error":"Not_json"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"examples":{"example1":{"value":{"error":"This room already exits in Twake database"}},"example2":{"value":{"errcode":"M_ROOM_IN_USE","error":"A room with alias foo already exists in Matrix database"}}}}}},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_twake/mutual_rooms/{target_userid}":{"get":{"tags":["Mutual Rooms"],"description":"Get the list of mutual rooms between two users","parameters":[{"$ref":"#/components/parameters/target_userid"}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MutualRooms"}}}},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"404":{"description":"Not found"},"500":{"description":"Internal error"}}}},"/_twake/private_note":{"get":{"tags":["Private Note"],"description":"Get the private note made by the user for a target user","parameters":[{"$ref":"#/components/parameters/user_id"},{"$ref":"#/components/parameters/target_user_id"}],"responses":{"200":{"description":"Private note found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PrivateNote"}}}},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"404":{"description":"Private note not found"},"500":{"description":"Internal error"}}},"post":{"tags":["Private Note"],"description":"Create a private note for a target user","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePrivateNote"}}}},"responses":{"201":{"description":"Private note created"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}},"put":{"tags":["Private Note"],"description":"Update a private note","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePrivateNote"}}}},"responses":{"204":{"description":"Private note created"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}}},"/_twake/private_note/{private_note_id}":{"delete":{"tags":["Private Note"],"description":"Delete a private note","parameters":[{"$ref":"#/components/parameters/private_note_id"}],"responses":{"204":{"description":"Private note deleted"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}}},"/_twake/v1/qrcode":{"get":{"tags":["QR Code"],"description":"Get access QR Code","responses":{"200":{"description":"QR code generated","content":{"image/svg+xml":{"schema":{"type":"string"}}}},"400":{"description":"Access token is missing"},"401":{"description":"Unauthorized"},"500":{"description":"Internal server error"}}}},"/_twake/v1/room_tags/{roomId}":{"get":{"tags":["Room tags"],"description":"Get room tags","parameters":[{"$ref":"#/components/parameters/roomId"}],"responses":{"200":{"description":"Room tags found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomTags"}}}},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}},"put":{"tags":["Room tags"],"description":"Update room tags","parameters":[{"$ref":"#/components/parameters/roomId"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomTagsUpdate"}}}},"responses":{"204":{"description":"Room tags updated"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}},"delete":{"tags":["Room tags"],"description":"delete tags for a room","parameters":[{"$ref":"#/components/parameters/roomId"}],"responses":{"204":{"description":"Room tags deleted"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}}},"/_twake/v1/room_tags":{"post":{"tags":["Room tags"],"description":"Create room tags","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomTagCreation"}}}},"responses":{"201":{"description":"Room tags created"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}}},"/_twake/app/v1/search":{"post":{"tags":["Search Engine"],"description":"Search performs with OpenSearch on Tchat messages and rooms","requestBody":{"description":"Object containing search query details","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"searchValue":{"type":"string","description":"Value used to perform the search on rooms and messages data"}},"required":["searchValue"]},"example":{"searchValue":"hello"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"rooms":{"type":"array","description":"List of rooms whose name contains the search value","items":{"type":"object","properties":{"room_id":{"type":"string"},"name":{"type":"string"},"avatar_url":{"type":"string","description":"Url of the room's avatar"}}}},"messages":{"type":"array","description":"List of messages whose content or/and sender display name contain the search value","items":{"type":"object","properties":{"room_id":{"type":"string"},"event_id":{"type":"string","description":"Id of the message"},"content":{"type":"string"},"display_name":{"type":"string","description":"Sender display name"},"avatar_url":{"type":"string","description":"Sender's avatar url if it is a direct chat, otherwise it is the room's avatar url"},"room_name":{"type":"string","description":"Room's name in case of the message is not part of a direct chat"}}}},"mails":{"type":"array","description":"List of mails from Tmail whose meta or content contain the search value","items":{"type":"object","properties":{"attachments":{"type":"array","items":{"type":"object","properties":{"contentDisposition":{"type":"string"},"fileExtension":{"type":"string"},"fileName":{"type":"string"},"mediaType":{"type":"string"},"subtype":{"type":"string"},"textContent":{"type":"string"}}}},"bcc":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string"},"domain":{"type":"string"},"name":{"type":"string"}}}},"cc":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string"},"domain":{"type":"string"},"name":{"type":"string"}}}},"date":{"type":"string"},"from":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string"},"domain":{"type":"string"},"name":{"type":"string"}}}},"hasAttachment":{"type":"boolean"},"headers":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"string"}}}},"htmlBody":{"type":"string"},"isAnswered":{"type":"boolean"},"isDeleted":{"type":"boolean"},"isDraft":{"type":"boolean"},"isFlagged":{"type":"boolean"},"isRecent":{"type":"boolean"},"isUnread":{"type":"boolean"},"mailboxId":{"type":"string"},"mediaType":{"type":"string"},"messageId":{"type":"string"},"mimeMessageID":{"type":"string"},"modSeq":{"type":"number"},"saveDate":{"type":"string"},"sentDate":{"type":"string"},"size":{"type":"number"},"subject":{"type":"array","items":{"type":"string"}},"subtype":{"type":"string"},"textBody":{"type":"string"},"threadId":{"type":"string"},"to":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string"},"domain":{"type":"string"},"name":{"type":"string"}}}},"uid":{"type":"number"},"userFlags":{"type":"array","items":{"type":"string"}}}}}}},"example":{"rooms":[{"room_id":"!dYqMpBXVQgKWETVAtJ:example.com","name":"Hello world room","avatar_url":"mxc://linagora.com/IBGFusHnOOzCNfePjaIVHpgR"},{"room_id":"!dugSgNYwppGGoeJwYB:example.com","name":"Worldwide room","avatar_url":null}],"messages":[{"room_id":"!dYqMpBXVQgKWETVAtJ:example.com","event_id":"$c0hW6db_GUjk0NRBUuO12IyMpi48LE_tQK6sH3dkd1U","content":"Hello world","display_name":"Anakin Skywalker","avatar_url":"mxc://linagora.com/IBGFusHnOOzCNfePjaIVHpgR","room_name":"Hello world room"},{"room_id":"!ftGqINYwppGGoeJwYB:example.com","event_id":"$IUzFofxHCvvoHJ-k2nfx7OlWOO8AuPvlHHqkeJLzxJ8","content":"Hello world my friends in direct chat","display_name":"Luke Skywalker","avatar_url":"mxc://matrix.org/wefh34uihSDRGhw34"}],"mails":[{"id":"message1","attachments":[{"contentDisposition":"attachment","fileExtension":"jpg","fileName":"image1.jpg","mediaType":"image/jpeg","textContent":"A beautiful galaxy far, far away."}],"bcc":[{"address":"okenobi@example.com","domain":"example.com","name":"Obi-Wan Kenobi"}],"cc":[{"address":"pamidala@example.com","domain":"example.com","name":"Padme Amidala"}],"date":"2024-02-24T10:15:00Z","from":[{"address":"dmaul@example.com","domain":"example.com","name":"Dark Maul"}],"hasAttachment":true,"headers":[{"name":"Header5","value":"Value5"},{"name":"Header6","value":"Value6"}],"htmlBody":"

A beautiful galaxy far, far away.

","isAnswered":true,"isDeleted":false,"isDraft":false,"isFlagged":true,"isRecent":true,"isUnread":false,"mailboxId":"mailbox3","mediaType":"image/jpeg","messageId":"message3","mimeMessageID":"mimeMessageID3","modSeq":98765,"saveDate":"2024-02-24T10:15:00Z","sentDate":"2024-02-24T10:15:00Z","size":4096,"subject":["Star Wars Message 3"],"subtype":"subtype3","textBody":"A beautiful galaxy far, far away.","threadId":"thread3","to":[{"address":"kren@example.com","domain":"example.com","name":"Kylo Ren"}],"uid":987654,"userFlags":["Flag4","Flag5"]}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/Unrecognized"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_twake/app/v1/opensearch/restore":{"post":{"tags":["Search Engine"],"description":"Restore OpenSearch indexes using Matrix homeserver database","requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object"}}}},"405":{"$ref":"#/components/responses/Unrecognized"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_twake/sms":{"post":{"requestBody":{"description":"SMS object","required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sms"}}}},"tags":["SMS"],"description":"Send an SMS to a phone number","responses":{"200":{"description":"SMS sent successfully"},"400":{"description":"Invalid request"},"401":{"description":"Unauthorized"},"500":{"description":"Internal server error"}}}},"/_twake/v1/user_info/{userId}":{"get":{"tags":["User Info"],"description":"Get user info","parameters":[{"$ref":"#/components/parameters/userId"}],"responses":{"200":{"description":"User info found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserInfo"}}}},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"404":{"description":"User info not found"},"500":{"description":"Internal server error"}}}}},"tags":[]} \ No newline at end of file +{"openapi":"3.0.0","info":{"title":"Twake on Matrix APIs documentation","version":"0.0.1","description":"This is The documentation of all available APIs of this repository"},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"MatrixError":{"type":"object","properties":{"errcode":{"type":"string","description":"A Matrix error code"},"error":{"type":"string","description":"A human-readable error message"}},"required":["error"]},"ActiveContacts":{"type":"object","description":"the list of active contacts","properties":{"contacts":{"type":"string","description":"active contacts"}}},"ActivityMetric":{"type":"object","properties":{"dailyActiveUsers":{"type":"number"},"weeklyActiveUsers":{"type":"number"},"monthlyActiveUsers":{"type":"number"},"weeklyNewUsers":{"type":"number"},"monthlyNewUsers":{"type":"number"}}},"MessageMetric":{"type":"array","items":{"type":"object","properties":{"user_id":{"type":"string"},"message_count":{"type":"number"}}}},"MutualRooms":{"type":"array","items":{"type":"object","properties":{"roomId":{"type":"string","description":"the room id"},"name":{"type":"string","description":"the room name"},"topic":{"type":"string","description":"the room topic"},"room_type":{"type":"string","description":"the room type"}}}},"PrivateNote":{"type":"object","properties":{"id":{"type":"string","description":"The private note id"},"content":{"type":"string","description":"The private note content"},"authorId":{"type":"string","description":"The author user id"},"targetId":{"type":"string","description":"The target user id"}}},"CreatePrivateNote":{"type":"object","properties":{"content":{"type":"string","description":"The private note content"},"authorId":{"type":"string","description":"The author user id"},"targetId":{"type":"string","description":"The target user id"}}},"UpdatePrivateNote":{"type":"object","properties":{"id":{"type":"string","description":"The private note id"},"content":{"type":"string","description":"The private note content"}}},"RoomTags":{"type":"object","properties":{"tags":{"description":"the room tags list","type":"array","items":{"type":"string"}}}},"RoomTagCreation":{"type":"object","properties":{"content":{"type":"array","description":"the room tags strings","items":{"type":"string"}},"roomId":{"type":"string","description":"the room id"}}},"RoomTagsUpdate":{"type":"object","properties":{"content":{"type":"array","description":"the room tags strings","items":{"type":"string"}}}},"sms":{"type":"object","properties":{"to":{"oneOf":[{"type":"string"},{"type":"array","items":{"type":"string"}}]},"text":{"type":"string"}}},"UserInfo":{"type":"object","properties":{"uid":{"type":"string","description":"the user id"},"givenName":{"type":"string","description":"the user given name"},"sn":{"type":"string","description":"the user surname"}}}},"responses":{"InternalServerError":{"description":"Internal server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"The message describing the internal error"}}}}}},"Unauthorized":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"errcode":"M_UNAUTHORIZED","error":"Unauthorized"}}}},"BadRequest":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"errcode":"M_MISSING_PARAMS","error":"Properties are missing in the request body"}}}},"Forbidden":{"description":"Forbidden","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"errcode":"M_FORBIDDEN","error":"Forbidden"}}}},"Conflict":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"error":"Conflict"}}}},"PermanentRedirect":{"description":"Permanent Redirect","headers":{"Location":{"schema":{"type":"string","description":"URL to use for recdirect"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"errcode":"M_UNKNOWN","error":"This non-standard endpoint has been removed"}}}},"NotFound":{"description":"Private note not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"errcode":"M_NOT_FOUND","error":"Not Found"}}}},"Unrecognized":{"description":"Unrecognized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"example":{"errcode":"M_UNRECOGNIZED","error":"Unrecognized"}}}},"Created":{"description":"Created"},"NoContent":{"description":"operation successful and no content returned"},"InternalError":{"description":"Internal error"}},"parameters":{"target_userid":{"name":"target_userid","in":"path","required":true,"description":"the target user id","schema":{"type":"string"}},"user_id":{"name":"user_id","in":"query","description":"the author user id","required":true,"schema":{"type":"string"}},"target_user_id":{"name":"target_user_id","in":"query","description":"the target user id","required":true,"schema":{"type":"string"}},"private_note_id":{"name":"private_note_id","in":"path","description":"the private note id","required":true,"schema":{"type":"string"}},"roomId":{"in":"path","name":"roomId","description":"the room id","required":true,"schema":{"type":"string"}},"userId":{"in":"path","name":"userId","description":"the user id","required":true,"schema":{"type":"string"}}}},"security":[{"bearerAuth":[]}],"paths":{"/_matrix/identity/v2":{"get":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#get_matrixidentityv2"}},"/_matrix/identity/v2/hash_details":{"get":{"tags":["Federated identity service"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#get_matrixidentityv2hash_details"}},"/_matrix/identity/v2/lookup":{"post":{"tags":["Federated identity service"],"description":"Extends https://spec.matrix.org/v1.6/identity-service-api/#post_matrixidentityv2lookup to display inactive users and 3PID users","requestBody":{"description":"Object containing hashes of mails/phones to search","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"addresses":{"type":"array","items":{"type":"string","description":"List of (hashed) addresses to lookup"}},"algorithm":{"type":"string","description":"Algorithm the client is using to encode the addresses"},"pepper":{"type":"string","description":"Pepper from '/hash_details'"}},"required":["addresses","algorithm","pepper"]},"example":{"addresses":["4kenr7N9drpCJ4AfalmlGQVsOn3o2RHjkADUpXJWZUc","nlo35_T5fzSGZzJApqu8lgIudJvmOQtDaHtr-I4rU7I"],"algorithm":"sha256","pepper":"matrixrocks"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"mappings":{"type":"object","additionalProperties":{"type":"string"},"description":"List of active accounts"},"inactive_mappings":{"type":"object","additionalProperties":{"type":"string"},"description":"List of inactive accounts"},"third_party_mappings":{"type":"object","description":"List of hashed addresses by identity server hostname","properties":{"hostname":{"type":"object","properties":{"actives":{"type":"array","items":{"type":"string","description":"List of (hashed) active accounts addresses matching request body addresses"}},"inactives":{"type":"array","items":{"type":"string","description":"List of (hashed) inactive accounts addresses matching request body addresses"}}}}}}}},"example":{"mappings":{"4kenr7N9drpCJ4AfalmlGQVsOn3o2RHjkADUpXJWZUc":"@dwho:company.com"},"inactive_mappings":{"nlo35_T5fzSGZzJApqu8lgIudJvmOQtDaHtr-I4rU7I":"@rtyler:company.com"},"third_party_mappings":{"identity1.example.com":{"actives":["78jnr7N9drpCJ4AfalmlGQVsOn3o2RHjkADUpXJWZUc","gtr42_T5fzSGZzJAmlp5lgIudJvmOQtDaHtr-I4rU7I"],"inactives":["qfgt57N9drpCJ4AfalmlGQVsOn3o2RHjkADUpXJWZUc","lnbc8_T5fzSGZzJAmlp5lgIudJvmOQtDaHtr-I4rU7I"]}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/Unrecognized"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_matrix/identity/v2/account":{"get":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#get_matrixidentityv2account"}},"/_matrix/identity/v2/account/register":{"post":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#post_matrixidentityv2accountregister"}},"/_matrix/identity/v2/account/logout":{"post":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#post_matrixidentityv2accountlogout"}},"/_matrix/identity/v2/terms":{"get":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#get_matrixidentityv2terms"}},"/_matrix/identity/v2/validate/email/requestToken":{"post":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#post_matrixidentityv2validateemailrequesttoken"}},"/_matrix/identity/v2/validate/email/submitToken":{"post":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#post_matrixidentityv2validateemailsubmittoken"}},"/_matrix/identity/versions":{"get":{"tags":["Identity server"],"description":"Implements https://spec.matrix.org/v1.6/identity-service-api/#get_matrixidentityversions"}},"/_twake/identity/v1/lookup/match":{"post":{"tags":["Identity server"],"description":"Looks up the Organization User IDs which match value sent","requestBody":{"description":"Object containing detail for the search and the returned data","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"scope":{"type":"array","items":{"type":"string","description":"List of fields to search in (uid, mail,...)"}},"fields":{"type":"array","items":{"type":"string","description":"List of fields to return for matching users (uid, mail, mobile, displayName, givenName, cn, sn)"}},"val":{"type":"string","description":"Optional value to search"},"limit":{"type":"integer","description":"Optional max number of result to return (default 30)"},"offset":{"type":"integer","description":"Optional offset for pagination"}},"required":["scope","fields"]},"example":{"scope":["mail","uid"],"fields":["uid","displayName","sn","givenName","mobile"],"val":"rtyler","limit":3}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"matches":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string","description":"Matrix address"},"uid":{"type":"string","description":"id of a matching user"},"mail":{"type":"string","description":"email address of a matching user"}}},"description":"List of users that match"}}},"example":{"matches":[{"uid":"dwho","mail":"dwho@badwolf.com"}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/_twake/identity/v1/lookup/diff":{"post":{"tags":["Identity server"],"description":"Looks up the Organization User IDs updated since X","requestBody":{"description":"Object containing the timestamp","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"since":{"type":"integer","description":"timestamp"},"fields":{"type":"array","items":{"type":"string","description":"List of fields to return for matching users"}},"limit":{"type":"integer","description":"Optional max number of result to return (default 30)"},"offset":{"type":"integer","description":"Optional offset for pagination"}}},"example":{"since":1685074279,"fields":["uid","mail"],"limit":3}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"matches":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string","description":"Matrix address"},"timestamp":{"type":"integer","description":"current server timestamp"},"uid":{"type":"string","description":"id of a matching user"},"mail":{"type":"string","description":"email address of a matching user"}}},"description":"List of users that match"}}},"example":{"matches":[{"uid":"dwho","mail":"dwho@badwolf.com"}]}}}}}}},"/_twake/recoveryWords":{"get":{"tags":["Vault API"],"description":"Allow for the connected user to retrieve its recovery words","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"words":{"type":"string","description":"Recovery words of the connected user"}}},"example":{"words":"This is the recovery sentence of rtyler"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Connected user has no recovery sentence"}}},"example":{"error":"User has no recovery sentence"}}}},"409":{"description":"Conflict","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Connected user has multiple recovery sentence"}}},"example":{"error":"User has more than one recovery sentence"}}}},"500":{"$ref":"#/components/responses/InternalServerError"}}},"post":{"tags":["Vault API"],"description":"Store connected user recovery words in database","requestBody":{"description":"Object containing the recovery words of the connected user","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"words":{"type":"string","description":"The recovery words of the connected user"}},"required":["words"]},"example":{"words":"This is the recovery sentence of rtyler"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Message indicating that words have been successfully saved"}},"example":{"message":"Saved recovery words sucessfully"}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/InternalServerError"}}},"delete":{"tags":["Vault API"],"description":"Delete the user recovery words in the database","responses":{"204":{"description":"Delete success"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Connected user has no recovery sentence"}}},"example":{"error":"User has no recovery sentence"}}}},"500":{"$ref":"#/components/responses/InternalServerError"}}},"put":{"tags":["Vault API"],"description":"Update stored connected user recovery words in database","requestBody":{"description":"Object containing the recovery words of the connected user","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"words":{"type":"string","description":"The new recovery words of the connected user"}},"required":["words"]},"example":{"words":"This is the updated recovery sentence of rtyler"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Message indicating that words have been successfully updated"}},"example":{"message":"Updated recovery words sucessfully"}}}}},"400":{"description":"Bad request"},"401":{"$ref":"#/components/responses/Unauthorized"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/.well-knwon/matrix/client":{"get":{"tags":["Auto configuration"],"description":"Get server metadata for auto configuration","responses":{"200":{"description":"Give server metadata","content":{"application/json":{"schema":{"type":"object","properties":{"m.homeserver":{"type":"object","properties":{"base_url":{"type":"string","description":"Base URL of Matrix server"}}},"m.identity_server":{"type":"object","properties":{"base_url":{"type":"string","description":"Base URL of Identity server"}}},"m.federated_identity_services":{"type":"object","properties":{"base_urls":{"type":"array","items":{"type":"string","description":"Base URL of Federated identity service"},"description":"Available Federated identity services Base URL list"}}},"t.server":{"type":"object","properties":{"base_url":{"type":"string","description":"Base URL of Identity server"},"server_name":{"type":"string","description":"Domain handled by Matrix server"}}},"m.integrations":{"type":"object","properties":{"jitsi":{"type":"object","properties":{"preferredDomain":{"type":"string","description":"Jitsi's preffered domain"},"baseUrl":{"type":"string","description":"URL of Jitsi server"},"useJwt":{"type":"boolean","description":"True if Jitsi server requires a JWT"},"jwt":{"type":"object","properties":{"algorithm":{"type":"string","description":"algorithm used to generate JWT"},"secret":{"type":"string","description":"password of JWTs"},"issuer":{"type":"string","description":"issuer of JWTs"}}}}}}},"m.authentication":{"type":"object","properties":{"issuer":{"type":"string","description":"URL of OIDC issuer"}}}}},"example":{"m.homeserver":{"base_url":"matrix.example.com"},"m.identity_server":{"base_url":"global-id-server.twake.app"},"m.federated_identity_services":{"base_urls":["global-federated_identity_service.twake.app","other-federated-identity-service.twake.app"]},"m.integrations":{"jitsi":{"baseUrl":"https://jitsi.example.com/","preferredDomain":"jitsi.example.com","useJwt":false}},"m.authentication":{"issuer":"https://auth.example.com"},"t.server":{"base_url":"https://tom.example.com","server_name":"example.com"}}}}}}}},"/_matrix/identity/v2/lookups":{"post":{"tags":["Federated identity service"],"description":"Implements https://github.com/guimard/matrix-spec-proposals/blob/unified-identity-service/proposals/4004-unified-identity-service-view.md","requestBody":{"description":"Object containing hashes to store in federated identity service database","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"mappings":{"type":"object","description":"List of hashed addresses by identity server hostname","properties":{"hostname":{"type":"array","items":{"type":"object","properties":{"hash":{"type":"string"},"active":{"type":"number"}}}}}},"algorithm":{"type":"string","description":"Algorithm the client is using to encode the addresses"},"pepper":{"type":"string","description":"Pepper from '/hash_details'"}},"required":["addresses","algorithm","pepper"]},"example":{"mappings":{"identity1.example.com":[{"hash":"4kenr7N9drpCJ4AfalmlGQVsOn3o2RHjkADUpXJWZUc","active":1},{"hash":"nlo35_T5fzSGZzJApqu8lgIudJvmOQtDaHtr-I4rU7I","active":0}]},"algorithm":"sha256","pepper":"matrixrocks"}}}},"responses":{"201":{"description":"Success"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/Unrecognized"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_matrix/app/v1/transactions/{txnId}":{"put":{"parameters":[{"in":"path","name":"txnId","required":true,"schema":{"type":"integer"},"description":"The transaction id"}],"tags":["Application server"],"description":"Implements https://spec.matrix.org/v1.6/application-service-api/#put_matrixappv1transactionstxnid","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object"}}}},"308":{"$ref":"#/components/responses/PermanentRedirect"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object"}}}},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_matrix/app/v1/users/{userId}":{"get":{"parameters":[{"in":"path","name":"userId","required":true,"schema":{"type":"integer"},"description":"The user id"}],"tags":["Application server"],"description":"Implements https://spec.matrix.org/v1.6/application-service-api/#get_matrixappv1usersuserid","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_matrix/app/v1/rooms/{roomAlias}":{"get":{"parameters":[{"in":"path","name":"roomAlias","required":true,"schema":{"type":"integer"},"description":"The room alias"}],"tags":["Application server"],"description":"Implements https://spec.matrix.org/v1.6/application-service-api/#get_matrixappv1roomsroomalias","responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_twake/v1/activecontacts":{"get":{"tags":["Active contacts"],"description":"Get the list of active contacts","responses":{"200":{"description":"Active contacts found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActiveContacts"}}}},"401":{"description":"user is unauthorized"},"404":{"description":"Active contacts not found"},"500":{"description":"Internal error"}}},"post":{"tags":["Active contacts"],"description":"Create or update the list of active contacts","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActiveContacts"}}}},"responses":{"201":{"description":"Active contacts saved"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}},"delete":{"tags":["Active contacts"],"description":"Delete the list of active contacts","responses":{"200":{"description":"Active contacts deleted"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error/"}}}},"/_twake/app/v1/rooms":{"post":{"tags":["Application server"],"description":"Implements https://www.notion.so/Automatic-channels-89ba6f97bc90474ca482a28cf3228d3e","requestBody":{"description":"Object containing room's details","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"ldapFilter":{"type":"object","additionalProperties":true,"description":"An object containing keys/values to build a ldap filter"},"aliasName":{"type":"string","description":"The desired room alias local part. If aliasName is equal to foo the complete room alias will be"},"name":{"type":"string","description":"The room name"},"topic":{"type":"string","description":"A short message detailing what is currently being discussed in the room."},"visibility":{"type":"string","enum":["public","private"],"description":"visibility values:\n * `public` - The room will be shown in the published room list\n * `private` - Hide the room from the published room list\n"}},"required":["ldapFilter","aliasName"]},"example":{"ldapFilter":{"mail":"example@test.com","cn":"example"},"aliasName":"exp","name":"Example","topic":"This is an example of a room topic","visibility":"public"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"errcode":{"type":"string"},"error":{"type":"string"}},"additionalProperties":{"type":"string"},"description":"List of users uid not added to the new room due to an error"},"example":[{"uid":"test1","errcode":"M_FORBIDDEN","error":"The user has been banned from the room"},{"uid":"test2","errcode":"M_UNKNOWN","error":"Internal server error"}]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"examples":{"example1":{"value":{"error":"Error field: Invalid value (property: name)"}},"example2":{"value":{"errcode":"M_NOT_JSON","error":"Not_json"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"Conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatrixError"},"examples":{"example1":{"value":{"error":"This room already exits in Twake database"}},"example2":{"value":{"errcode":"M_ROOM_IN_USE","error":"A room with alias foo already exists in Matrix database"}}}}}},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_twake/v1/metrics/activity":{"get":{"tags":["Metrics"],"description":"Get user activity metrics","responses":{"200":{"description":"Activity metrics found","content":{"application/json":{"schema":{"type":"object","$ref":"#/components/schemas/ActivityMetric"}}}},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Activity metrics not found"},"500":{"description":"Internal error"}}}},"/_twake/v1/metrics/messages":{"get":{"tags":["Metrics"],"description":"Get user messages metrics","responses":{"200":{"description":"Messages metrics found","content":{"application/json":{"schema":{"type":"object","$ref":"#/components/schemas/MessageMetric"}}}},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden"},"404":{"description":"Messages metrics not found"},"500":{"description":"Internal error"}}}},"/_twake/mutual_rooms/{target_userid}":{"get":{"tags":["Mutual Rooms"],"description":"Get the list of mutual rooms between two users","parameters":[{"$ref":"#/components/parameters/target_userid"}],"responses":{"200":{"description":"Successful operation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MutualRooms"}}}},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"404":{"description":"Not found"},"500":{"description":"Internal error"}}}},"/_twake/private_note":{"get":{"tags":["Private Note"],"description":"Get the private note made by the user for a target user","parameters":[{"$ref":"#/components/parameters/user_id"},{"$ref":"#/components/parameters/target_user_id"}],"responses":{"200":{"description":"Private note found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PrivateNote"}}}},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"404":{"description":"Private note not found"},"500":{"description":"Internal error"}}},"post":{"tags":["Private Note"],"description":"Create a private note for a target user","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePrivateNote"}}}},"responses":{"201":{"description":"Private note created"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}},"put":{"tags":["Private Note"],"description":"Update a private note","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePrivateNote"}}}},"responses":{"204":{"description":"Private note created"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}}},"/_twake/private_note/{private_note_id}":{"delete":{"tags":["Private Note"],"description":"Delete a private note","parameters":[{"$ref":"#/components/parameters/private_note_id"}],"responses":{"204":{"description":"Private note deleted"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}}},"/_twake/v1/qrcode":{"get":{"tags":["QR Code"],"description":"Get access QR Code","responses":{"200":{"description":"QR code generated","content":{"image/svg+xml":{"schema":{"type":"string"}}}},"400":{"description":"Access token is missing"},"401":{"description":"Unauthorized"},"500":{"description":"Internal server error"}}}},"/_twake/v1/room_tags/{roomId}":{"get":{"tags":["Room tags"],"description":"Get room tags","parameters":[{"$ref":"#/components/parameters/roomId"}],"responses":{"200":{"description":"Room tags found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomTags"}}}},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}},"put":{"tags":["Room tags"],"description":"Update room tags","parameters":[{"$ref":"#/components/parameters/roomId"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomTagsUpdate"}}}},"responses":{"204":{"description":"Room tags updated"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}},"delete":{"tags":["Room tags"],"description":"delete tags for a room","parameters":[{"$ref":"#/components/parameters/roomId"}],"responses":{"204":{"description":"Room tags deleted"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}}},"/_twake/v1/room_tags":{"post":{"tags":["Room tags"],"description":"Create room tags","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoomTagCreation"}}}},"responses":{"201":{"description":"Room tags created"},"400":{"description":"Bad request"},"401":{"description":"user is unauthorized"},"500":{"description":"Internal error"}}}},"/_twake/app/v1/search":{"post":{"tags":["Search Engine"],"description":"Search performs with OpenSearch on Tchat messages and rooms","requestBody":{"description":"Object containing search query details","required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"searchValue":{"type":"string","description":"Value used to perform the search on rooms and messages data"}},"required":["searchValue"]},"example":{"searchValue":"hello"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"rooms":{"type":"array","description":"List of rooms whose name contains the search value","items":{"type":"object","properties":{"room_id":{"type":"string"},"name":{"type":"string"},"avatar_url":{"type":"string","description":"Url of the room's avatar"}}}},"messages":{"type":"array","description":"List of messages whose content or/and sender display name contain the search value","items":{"type":"object","properties":{"room_id":{"type":"string"},"event_id":{"type":"string","description":"Id of the message"},"content":{"type":"string"},"display_name":{"type":"string","description":"Sender display name"},"avatar_url":{"type":"string","description":"Sender's avatar url if it is a direct chat, otherwise it is the room's avatar url"},"room_name":{"type":"string","description":"Room's name in case of the message is not part of a direct chat"}}}},"mails":{"type":"array","description":"List of mails from Tmail whose meta or content contain the search value","items":{"type":"object","properties":{"attachments":{"type":"array","items":{"type":"object","properties":{"contentDisposition":{"type":"string"},"fileExtension":{"type":"string"},"fileName":{"type":"string"},"mediaType":{"type":"string"},"subtype":{"type":"string"},"textContent":{"type":"string"}}}},"bcc":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string"},"domain":{"type":"string"},"name":{"type":"string"}}}},"cc":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string"},"domain":{"type":"string"},"name":{"type":"string"}}}},"date":{"type":"string"},"from":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string"},"domain":{"type":"string"},"name":{"type":"string"}}}},"hasAttachment":{"type":"boolean"},"headers":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"string"}}}},"htmlBody":{"type":"string"},"isAnswered":{"type":"boolean"},"isDeleted":{"type":"boolean"},"isDraft":{"type":"boolean"},"isFlagged":{"type":"boolean"},"isRecent":{"type":"boolean"},"isUnread":{"type":"boolean"},"mailboxId":{"type":"string"},"mediaType":{"type":"string"},"messageId":{"type":"string"},"mimeMessageID":{"type":"string"},"modSeq":{"type":"number"},"saveDate":{"type":"string"},"sentDate":{"type":"string"},"size":{"type":"number"},"subject":{"type":"array","items":{"type":"string"}},"subtype":{"type":"string"},"textBody":{"type":"string"},"threadId":{"type":"string"},"to":{"type":"array","items":{"type":"object","properties":{"address":{"type":"string"},"domain":{"type":"string"},"name":{"type":"string"}}}},"uid":{"type":"number"},"userFlags":{"type":"array","items":{"type":"string"}}}}}}},"example":{"rooms":[{"room_id":"!dYqMpBXVQgKWETVAtJ:example.com","name":"Hello world room","avatar_url":"mxc://linagora.com/IBGFusHnOOzCNfePjaIVHpgR"},{"room_id":"!dugSgNYwppGGoeJwYB:example.com","name":"Worldwide room","avatar_url":null}],"messages":[{"room_id":"!dYqMpBXVQgKWETVAtJ:example.com","event_id":"$c0hW6db_GUjk0NRBUuO12IyMpi48LE_tQK6sH3dkd1U","content":"Hello world","display_name":"Anakin Skywalker","avatar_url":"mxc://linagora.com/IBGFusHnOOzCNfePjaIVHpgR","room_name":"Hello world room"},{"room_id":"!ftGqINYwppGGoeJwYB:example.com","event_id":"$IUzFofxHCvvoHJ-k2nfx7OlWOO8AuPvlHHqkeJLzxJ8","content":"Hello world my friends in direct chat","display_name":"Luke Skywalker","avatar_url":"mxc://matrix.org/wefh34uihSDRGhw34"}],"mails":[{"id":"message1","attachments":[{"contentDisposition":"attachment","fileExtension":"jpg","fileName":"image1.jpg","mediaType":"image/jpeg","textContent":"A beautiful galaxy far, far away."}],"bcc":[{"address":"okenobi@example.com","domain":"example.com","name":"Obi-Wan Kenobi"}],"cc":[{"address":"pamidala@example.com","domain":"example.com","name":"Padme Amidala"}],"date":"2024-02-24T10:15:00Z","from":[{"address":"dmaul@example.com","domain":"example.com","name":"Dark Maul"}],"hasAttachment":true,"headers":[{"name":"Header5","value":"Value5"},{"name":"Header6","value":"Value6"}],"htmlBody":"

A beautiful galaxy far, far away.

","isAnswered":true,"isDeleted":false,"isDraft":false,"isFlagged":true,"isRecent":true,"isUnread":false,"mailboxId":"mailbox3","mediaType":"image/jpeg","messageId":"message3","mimeMessageID":"mimeMessageID3","modSeq":98765,"saveDate":"2024-02-24T10:15:00Z","sentDate":"2024-02-24T10:15:00Z","size":4096,"subject":["Star Wars Message 3"],"subtype":"subtype3","textBody":"A beautiful galaxy far, far away.","threadId":"thread3","to":[{"address":"kren@example.com","domain":"example.com","name":"Kylo Ren"}],"uid":987654,"userFlags":["Flag4","Flag5"]}]}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"405":{"$ref":"#/components/responses/Unrecognized"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_twake/app/v1/opensearch/restore":{"post":{"tags":["Search Engine"],"description":"Restore OpenSearch indexes using Matrix homeserver database","requestBody":{"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"204":{"description":"Success","content":{"application/json":{"schema":{"type":"object"}}}},"405":{"$ref":"#/components/responses/Unrecognized"},"500":{"$ref":"#/components/responses/InternalServerError"}}}},"/_twake/sms":{"post":{"requestBody":{"description":"SMS object","required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/sms"}}}},"tags":["SMS"],"description":"Send an SMS to a phone number","responses":{"200":{"description":"SMS sent successfully"},"400":{"description":"Invalid request"},"401":{"description":"Unauthorized"},"500":{"description":"Internal server error"}}}},"/_twake/v1/user_info/{userId}":{"get":{"tags":["User Info"],"description":"Get user info","parameters":[{"$ref":"#/components/parameters/userId"}],"responses":{"200":{"description":"User info found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserInfo"}}}},"400":{"description":"Bad request"},"401":{"description":"Unauthorized"},"404":{"description":"User info not found"},"500":{"description":"Internal server error"}}}}},"tags":[]} \ No newline at end of file