From 688ab4f029acfc0240789e2932058a28aa77c2f2 Mon Sep 17 00:00:00 2001 From: John Livingston Date: Tue, 4 Jun 2024 16:39:25 +0200 Subject: [PATCH] Custom channel emoticons WIP (#130) --- client/@types/global.d.ts | 2 + .../configuration/elements/channel-emojis.ts | 41 +++- .../configuration/services/channel-details.ts | 7 +- languages/en.yml | 2 + server/lib/conversejs/params.ts | 8 +- server/lib/emojis/emojis.ts | 232 ++++++++++++++++++ server/lib/emojis/index.ts | 2 + .../lib/middlewares/configuration/channel.ts | 32 +-- server/lib/routers/api.ts | 2 + server/lib/routers/api/configuration.ts | 72 ++++-- server/lib/routers/emojis.ts | 90 +++++++ server/lib/settings.ts | 5 + server/main.ts | 5 + shared/lib/types.ts | 8 +- .../content/en/technical/data/_index.md | 9 + 15 files changed, 469 insertions(+), 48 deletions(-) create mode 100644 server/lib/emojis/emojis.ts create mode 100644 server/lib/emojis/index.ts create mode 100644 server/lib/routers/emojis.ts diff --git a/client/@types/global.d.ts b/client/@types/global.d.ts index a9e9dd58..97b336ee 100644 --- a/client/@types/global.d.ts +++ b/client/@types/global.d.ts @@ -88,3 +88,5 @@ declare const LOC_INVALID_VALUE_NOT_IN_RANGE: string declare const LOC_CHATROOM_NOT_ACCESSIBLE: string declare const LOC_PROMOTE: string + +declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_EMOJIS_TITLE: string diff --git a/client/common/configuration/elements/channel-emojis.ts b/client/common/configuration/elements/channel-emojis.ts index 917bf935..012586d2 100644 --- a/client/common/configuration/elements/channel-emojis.ts +++ b/client/common/configuration/elements/channel-emojis.ts @@ -3,15 +3,16 @@ // SPDX-License-Identifier: AGPL-3.0-only import type { RegisterClientOptions } from '@peertube/peertube-types/client' +import type { ChannelEmojisConfiguration } from 'shared/lib/types' import { LivechatElement } from '../../lib/elements/livechat' import { registerClientOptionsContext } from '../../lib/contexts/peertube' import { ChannelDetailsService } from '../services/channel-details' import { channelDetailsServiceContext } from '../contexts/channel' -import { ChannelEmojis } from 'shared/lib/types' -// import { ptTr } from '../../lib/directives/translation' +import { ptTr } from '../../lib/directives/translation' import { Task } from '@lit/task' import { customElement, property } from 'lit/decorators.js' import { provide } from '@lit/context' +import { html } from 'lit' /** * Channel emojis configuration page. @@ -25,15 +26,37 @@ export class ChannelEmojisElement extends LivechatElement { @property({ attribute: false }) public channelId?: number - private _channelEmojis?: ChannelEmojis + private _channelEmojisConfiguration?: ChannelEmojisConfiguration @provide({ context: channelDetailsServiceContext }) private _channelDetailsService?: ChannelDetailsService - protected override render = (): void => { + protected override render = (): unknown => { return this._asyncTaskRender.render({ pending: () => {}, - complete: () => {}, + complete: () => html` +
+

+ ${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_EMOJIS_TITLE)}: + + ${this._channelEmojisConfiguration?.channel.displayName} + ${this._channelEmojisConfiguration?.channel.name} + + + + FIXME: help url OK? +

+
+
+ +
+
+
+ `, error: (err: any) => { this.registerClientOptions?.peertubeHelpers.notifier.error(err.toString()) } @@ -49,8 +72,14 @@ export class ChannelEmojisElement extends LivechatElement { throw new Error('Missing channelId') } this._channelDetailsService = new ChannelDetailsService(this.registerClientOptions) - this._channelEmojis = await this._channelDetailsService.fetchEmojis(this.channelId) + this._channelEmojisConfiguration = await this._channelDetailsService.fetchEmojisConfiguration(this.channelId) }, args: () => [] }) + + private readonly _saveEmojis = (ev?: Event): void => { + ev?.preventDefault() + // TODO + this.registerClientOptions?.peertubeHelpers.notifier.error('TODO') + } } diff --git a/client/common/configuration/services/channel-details.ts b/client/common/configuration/services/channel-details.ts index 15aee4f4..51fbf5f1 100644 --- a/client/common/configuration/services/channel-details.ts +++ b/client/common/configuration/services/channel-details.ts @@ -5,7 +5,7 @@ import type { RegisterClientOptions } from '@peertube/peertube-types/client' import type { ValidationError } from '../../lib/models/validation' import type { - ChannelLiveChatInfos, ChannelConfiguration, ChannelConfigurationOptions, ChannelEmojis + ChannelLiveChatInfos, ChannelConfiguration, ChannelConfigurationOptions, ChannelEmojisConfiguration } from 'shared/lib/types' import { ValidationErrorType } from '../../lib/models/validation' import { getBaseRoute } from '../../../utils/uri' @@ -161,7 +161,7 @@ export class ChannelDetailsService { return response.json() } - fetchEmojis = async (channelId: number): Promise => { + fetchEmojisConfiguration = async (channelId: number): Promise => { const response = await fetch( getBaseRoute(this._registerClientOptions) + '/api/configuration/channel/emojis/' + @@ -173,6 +173,9 @@ export class ChannelDetailsService { ) if (!response.ok) { + if (response.status === 404) { + // File does not exist yet, that is a normal use case. + } throw new Error('Can\'t get channel emojis options.') } diff --git a/languages/en.yml b/languages/en.yml index 4d4b3517..c0b276e6 100644 --- a/languages/en.yml +++ b/languages/en.yml @@ -474,3 +474,5 @@ task_list_pick_message: | More information in the livechat plugin documentation. promote: 'Become moderator' + +livechat_configuration_channel_emojis_title: 'Channel emojis' diff --git a/server/lib/conversejs/params.ts b/server/lib/conversejs/params.ts index 1914297d..6919f1e6 100644 --- a/server/lib/conversejs/params.ts +++ b/server/lib/conversejs/params.ts @@ -15,6 +15,7 @@ import { getBaseRouterRoute, getBaseStaticRoute } from '../helpers' import { getProsodyDomain } from '../prosody/config/domain' import { getBoshUri, getWSUri } from '../uri/webchat' import { ExternalAuthOIDC } from '../external-auth/oidc' +import { Emojis } from '../emojis' interface GetConverseJSParamsParams { readonly?: boolean | 'noscroll' @@ -284,10 +285,11 @@ async function _connectionInfos ( params.forcetype ?? false ) - if (!settings['disable-channel-configuration'] && video?.channelId) { + if (video?.channelId && await Emojis.singletonSafe()?.channelHasCustomEmojis(video.channelId)) { customEmojisUrl = getBaseRouterRoute(options) + - 'api/configuration/channel/emojis/' + - encodeURIComponent(video.channelId) + 'emojis/channel/' + + encodeURIComponent(video.channelId) + + '/definition' } } catch (err) { options.peertubeHelpers.logger.error(err) diff --git a/server/lib/emojis/emojis.ts b/server/lib/emojis/emojis.ts new file mode 100644 index 00000000..20550561 --- /dev/null +++ b/server/lib/emojis/emojis.ts @@ -0,0 +1,232 @@ +import type { ChannelEmojis, CustomEmojiDefinition } from '../../../shared/lib/types' +import { RegisterServerOptions } from '@peertube/peertube-types' +import { getBaseRouterRoute } from '../helpers' +import { canonicalizePluginUri } from '../uri/canonicalize' +import * as path from 'node:path' +import * as fs from 'node:fs' + +let singleton: Emojis | undefined + +export class Emojis { + protected options: RegisterServerOptions + protected channelBasePath: string + protected channelBaseUri: string + + constructor (options: RegisterServerOptions) { + this.options = options + this.channelBasePath = path.resolve( + options.peertubeHelpers.plugin.getDataDirectoryPath(), + 'emojis', + 'channel' + ) + const baseRouterRoute = getBaseRouterRoute(options) + this.channelBaseUri = canonicalizePluginUri( + options, + baseRouterRoute + 'emojis/channel/', + { + removePluginVersion: true + } + ) + } + + /** + * Test if channel has custom emojis. + * @param channelId channel Id + */ + public async channelHasCustomEmojis (channelId: number): Promise { + const filepath = this.channelCustomEmojisDefinitionPath(channelId) + return fs.promises.access(filepath, fs.constants.F_OK).then(() => true, () => false) + } + + /** + * Get the file path for the channel definition JSON file (does not test if the file exists). + * @param channelId channel Id + */ + public channelCustomEmojisDefinitionPath (channelId: number): string { + if (typeof channelId !== 'number') { throw new Error('Invalid channelId') } + return path.resolve(this.channelBasePath, channelId.toString(), 'definition.json') + } + + /** + * Get the current definition for the channel emojis. + * @param channelId the channel id + * @returns the custom emojis definition + */ + public async channelCustomEmojisDefinition (channelId: number): Promise { + const filepath = this.channelCustomEmojisDefinitionPath(channelId) + let content + try { + content = await fs.promises.readFile(filepath) + } catch (err: any) { + if (('code' in err) && err.code === 'ENOENT') { + // File does not exist, this is normal. + return undefined + } + throw err + } + return JSON.parse(content.toString()) + } + + /** + * Test that the filename is a valid image filename. + * Valid file name are as `42.png`: an integer, with a valid image extension. + * @param fileName the filename to test + */ + public validImageFileName (fileName: string): boolean { + return /^(\d+)\.(png|jpg|gif)$/.test(fileName) + } + + /** + * Test if short name is valid. + * @param sn short name + */ + public validShortName (sn: any): boolean { + return (typeof sn === 'string') && /^:[\w-]+:$/.test(sn) + } + + /** + * Test that the url is a valid file url for the given channel. + * @param channelId channel id + * @param url the url to test + * @returns true if ok + */ + public async validFileUrl (channelId: number, url: any): Promise { + if (typeof url !== 'string') { return false } + const fileName = url.split('/').pop() ?? '' + if (!this.validImageFileName(fileName)) { return false } + const correctUrl = this.channelBaseUri + channelId.toString() + '/files/' + fileName + if (url !== correctUrl) { + return false + } + // TODO: test if file exists? + return true + } + + /** + * Returns the filepath for a given channel custom emojis + * @param channelId channel Id + * @param fileName the file name + * @returns the file path + */ + public channelCustomEmojisFilePath (channelId: number, fileName: string): string { + if (typeof channelId !== 'number') { throw new Error('Invalid channelId') } + if (!this.validImageFileName(fileName)) { throw new Error('Invalid filename') } + return path.resolve( + this.channelBasePath, + channelId.toString(), + 'files', + fileName + ) + } + + public emptyChannelDefinition (): ChannelEmojis { + return { + customEmojis: [] + } + } + + /** + * Sanitize the definition. + * Throw an error if format is not valid. + * @param channelId channel id + * @param def the definition + * @returns a proper ChannelEmojis + * @throws Error if format is not valid + */ + public async sanitizeChannelDefinition (channelId: number, def: any): Promise { + if (typeof def !== 'object') { + throw new Error('Invalid definition, type must be object') + } + if (!('customEmojis' in def) || !Array.isArray(def.customEmojis)) { + throw new Error('Invalid custom emojis entry in definition') + } + if (def.customEmojis.length > 100) { // to avoid unlimited image storage + throw new Error('Too many custom emojis') + } + + const customEmojis: CustomEmojiDefinition[] = [] + let categoryEmojiFound = false + for (const ce of def.customEmojis) { + if (typeof ce !== 'object') { + throw new Error('Invalid custom emoji') + } + if (!this.validShortName(ce.sn)) { + throw new Error('Invalid short name') + } + if (!await this.validFileUrl(channelId, ce.url)) { + throw new Error('Invalid file url') + } + + const sanitized: CustomEmojiDefinition = { + sn: ce.sn, + url: ce.url + } + + if (ce.isCategoryEmoji === true && !categoryEmojiFound) { + sanitized.isCategoryEmoji = true + categoryEmojiFound = true + } + + customEmojis.push(sanitized) + } + + const result: ChannelEmojis = { + customEmojis: customEmojis + } + return result + } + + /** + * Saves the channel custom emojis definition file. + * @param channelId the channel Id + * @param def the custom emojis definition + */ + public async saveChannelDefinition (channelId: number, def: ChannelEmojis): Promise { + const filepath = this.channelCustomEmojisDefinitionPath(channelId) + await fs.promises.mkdir(path.dirname(filepath), { + recursive: true + }) + await fs.promises.writeFile(filepath, JSON.stringify(def)) + } + + /** + * Returns the singleton, of thrown an exception if it is not initialized yet. + * Please note that this singleton won't exist if feature is disabled. + * @returns the singleton + */ + public static singleton (): Emojis { + if (!singleton) { + throw new Error('Emojis singleton not initialized yet') + } + return singleton + } + + /** + * Returns the singleton, or undefined if not initiliazed yet. + * Please note that this singleton won't exist if feature is disabled. + */ + public static singletonSafe (): Emojis | undefined { + return singleton + } + + /** + * Creates the singleton, unless the feature is disabled. + * @param options Peertube server options + */ + public static async initSingleton (options: RegisterServerOptions): Promise { + const disabled = await options.settingsManager.getSetting('disable-channel-configuration') + if (disabled) { + singleton = undefined + } else { + singleton = new Emojis(options) + } + } + + /** + * frees the singleton + */ + public static async destroySingleton (): Promise { + if (!singleton) { return } + singleton = undefined + } +} diff --git a/server/lib/emojis/index.ts b/server/lib/emojis/index.ts new file mode 100644 index 00000000..4626f5ee --- /dev/null +++ b/server/lib/emojis/index.ts @@ -0,0 +1,2 @@ +import './emojis' +export * from './emojis' diff --git a/server/lib/middlewares/configuration/channel.ts b/server/lib/middlewares/configuration/channel.ts index 8a29e8a3..7f400750 100644 --- a/server/lib/middlewares/configuration/channel.ts +++ b/server/lib/middlewares/configuration/channel.ts @@ -12,13 +12,9 @@ import { isUserAdminOrModerator } from '../../helpers' * Returns a middleware handler to get the channelInfos from the channel parameter. * This is used in api related to channel configuration options. * @param options Peertube server options - * @param publicPage If true, the page is considered public, and we don't test user rights. * @returns middleware function */ -function getCheckConfigurationChannelMiddleware ( - options: RegisterServerOptions, - publicPage?: boolean -): RequestPromiseHandler { +function getCheckConfigurationChannelMiddleware (options: RegisterServerOptions): RequestPromiseHandler { return async (req: Request, res: Response, next: NextFunction) => { const logger = options.peertubeHelpers.logger const channelId = req.params.channelId @@ -36,20 +32,18 @@ function getCheckConfigurationChannelMiddleware ( return } - if (!publicPage) { - // To access this page, you must either be: - // - the channel owner, - // - an instance modo/admin - // - TODO: a channel chat moderator, as defined in this page. - if (channelInfos.ownerAccountId === currentUser.Account.id) { - logger.debug('Current user is the channel owner') - } else if (await isUserAdminOrModerator(options, res)) { - logger.debug('Current user is an instance moderator or admin') - } else { - logger.warn('Current user tries to access a channel for which he has no right.') - res.sendStatus(403) - return - } + // To access this page, you must either be: + // - the channel owner, + // - an instance modo/admin + // - TODO: a channel chat moderator, as defined in this page. + if (channelInfos.ownerAccountId === currentUser.Account.id) { + logger.debug('Current user is the channel owner') + } else if (await isUserAdminOrModerator(options, res)) { + logger.debug('Current user is an instance moderator or admin') + } else { + logger.warn('Current user tries to access a channel for which he has no right.') + res.sendStatus(403) + return } logger.debug('User can access the configuration channel api.') diff --git a/server/lib/routers/api.ts b/server/lib/routers/api.ts index 05d4f6a5..f7be399a 100644 --- a/server/lib/routers/api.ts +++ b/server/lib/routers/api.ts @@ -13,6 +13,7 @@ import { initAuthApiRouter, initUserAuthApiRouter } from './api/auth' import { initFederationServerInfosApiRouter } from './api/federation-server-infos' import { initConfigurationApiRouter } from './api/configuration' import { initPromoteApiRouter } from './api/promote' +import { initEmojisRouter } from './emojis' /** * Initiate API routes @@ -42,6 +43,7 @@ async function initApiRouter (options: RegisterServerOptions): Promise { await initConfigurationApiRouter(options, router) await initPromoteApiRouter(options, router) + await initEmojisRouter(options, router) if (isDebugMode(options)) { // Only add this route if the debug mode is enabled at time of the server launch. diff --git a/server/lib/routers/api/configuration.ts b/server/lib/routers/api/configuration.ts index 15900d02..e2541222 100644 --- a/server/lib/routers/api/configuration.ts +++ b/server/lib/routers/api/configuration.ts @@ -4,7 +4,7 @@ import type { RegisterServerOptions } from '@peertube/peertube-types' import type { Router, Request, Response, NextFunction } from 'express' -import type { ChannelInfos, ChannelEmojis } from '../../../../shared/lib/types' +import type { ChannelConfiguration, ChannelEmojisConfiguration, ChannelInfos } from '../../../../shared/lib/types' import { asyncMiddleware } from '../../middlewares/async' import { getCheckConfigurationChannelMiddleware } from '../../middlewares/configuration/channel' import { checkConfigurationEnabledMiddleware } from '../../middlewares/configuration/configuration' @@ -15,6 +15,7 @@ import { } from '../../configuration/channel/storage' import { sanitizeChannelConfigurationOptions } from '../../configuration/channel/sanitize' import { getConverseJSParams } from '../../../lib/conversejs/params' +import { Emojis } from '../../../lib/emojis' async function initConfigurationApiRouter (options: RegisterServerOptions, router: Router): Promise { const logger = options.peertubeHelpers.logger @@ -57,7 +58,7 @@ async function initConfigurationApiRouter (options: RegisterServerOptions, route await getChannelConfigurationOptions(options, channelInfos.id) ?? getDefaultChannelConfigurationOptions(options) - const result = { + const result: ChannelConfiguration = { channel: channelInfos, configuration: channelOptions } @@ -101,7 +102,7 @@ async function initConfigurationApiRouter (options: RegisterServerOptions, route } logger.debug('Data seems ok, storing them.') - const result = { + const result: ChannelConfiguration = { channel: channelInfos, configuration: channelOptions } @@ -113,25 +114,62 @@ async function initConfigurationApiRouter (options: RegisterServerOptions, route router.get('/configuration/channel/emojis/:channelId', asyncMiddleware([ checkConfigurationEnabledMiddleware(options), - getCheckConfigurationChannelMiddleware(options, true), + getCheckConfigurationChannelMiddleware(options), async (req: Request, res: Response, _next: NextFunction): Promise => { - if (!res.locals.channelInfos) { - logger.error('Missing channelInfos in res.locals, should not happen') + try { + if (!res.locals.channelInfos) { + throw new Error('Missing channelInfos in res.locals, should not happen') + } + + const emojis = Emojis.singleton() + const channelInfos = res.locals.channelInfos as ChannelInfos + + const channelEmojis = + (await emojis.channelCustomEmojisDefinition(channelInfos.id)) ?? + emojis.emptyChannelDefinition() + + const result: ChannelEmojisConfiguration = { + channel: channelInfos, + emojis: channelEmojis + } + res.status(200) + res.json(result) + } catch (err) { + logger.error(err) res.sendStatus(500) - return } - // const channelInfos = res.locals.channelInfos as ChannelInfos + } + ])) - const channelEmojis: ChannelEmojis = { - customEmojis: [{ - sn: ':test:', - url: '/dist/images/custom_emojis/xmpp.png', - isCategoryEmoji: true - }] + router.post('/configuration/channel/emojis/:channelId', asyncMiddleware([ + checkConfigurationEnabledMiddleware(options), + getCheckConfigurationChannelMiddleware(options), + async (req: Request, res: Response, _next: NextFunction): Promise => { + try { + if (!res.locals.channelInfos) { + throw new Error('Missing channelInfos in res.locals, should not happen') + } + + const emojis = Emojis.singleton() + const channelInfos = res.locals.channelInfos as ChannelInfos + + const emojisDefinition = req.body + let emojisDefinitionSanitized + try { + emojisDefinitionSanitized = await emojis.sanitizeChannelDefinition(channelInfos.id, emojisDefinition) + } catch (err) { + logger.warn(err) + res.sendStatus(400) + return + } + + await emojis.saveChannelDefinition(channelInfos.id, emojisDefinitionSanitized) + + res.sendStatus(200) + } catch (err) { + logger.error(err) + res.sendStatus(500) } - - res.status(200) - res.json(channelEmojis) } ])) } diff --git a/server/lib/routers/emojis.ts b/server/lib/routers/emojis.ts new file mode 100644 index 00000000..f9df0291 --- /dev/null +++ b/server/lib/routers/emojis.ts @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: 2024 John Livingston +// +// SPDX-License-Identifier: AGPL-3.0-only + +import type { RegisterServerOptions } from '@peertube/peertube-types' +import type { Router, Request, Response, NextFunction } from 'express' +import { asyncMiddleware } from '../middlewares/async' +import { Emojis } from '../emojis' + +export async function initEmojisRouter ( + options: RegisterServerOptions, + router: Router +): Promise { + const logger = options.peertubeHelpers.logger + + router.get( + '/emojis/channel/:channelId/definition', + asyncMiddleware(async (req: Request, res: Response, _next: NextFunction): Promise => { + try { + const emojis = Emojis.singletonSafe() + if (!emojis) { + res.sendStatus(404) + return + } + + const channelId = parseInt(req.params.channelId) + if (!channelId || isNaN(channelId)) { + res.sendStatus(400) + return + } + + if (!await emojis.channelHasCustomEmojis(channelId)) { + res.sendStatus(404) + return + } + + res.sendFile(emojis.channelCustomEmojisDefinitionPath(channelId)) + } catch (err) { + logger.error(err) + res.sendStatus(500) + } + }) + ) + + router.get( + '/emojis/channel/:channelId/files/:fileName', + asyncMiddleware(async (req: Request, res: Response, _next: NextFunction): Promise => { + try { + const emojis = Emojis.singletonSafe() + if (!emojis) { + res.sendStatus(404) + return + } + + const channelId = parseInt(req.params.channelId) + if (!channelId || isNaN(channelId)) { + res.sendStatus(400) + return + } + + const fileName = req.params.fileName + if (!emojis.validImageFileName(fileName)) { + res.sendStatus(400) + return + } + + if (!await emojis.channelHasCustomEmojis(channelId)) { + res.sendStatus(404) + return + } + + res.sendFile( + emojis.channelCustomEmojisFilePath(channelId, fileName), + { + immutable: true, + maxAge: 1000 * 60 * 60 * 24 // 24h + }, + (err) => { + if (err) { + res.sendStatus(404) + } + } + ) + } catch (err) { + logger.error(err) + res.sendStatus(500) + } + }) + ) +} diff --git a/server/lib/settings.ts b/server/lib/settings.ts index 50d86280..eda78e1d 100644 --- a/server/lib/settings.ts +++ b/server/lib/settings.ts @@ -8,6 +8,7 @@ import { ensureProsodyRunning } from './prosody/ctl' import { RoomChannel } from './room-channel' import { BotsCtl } from './bots/ctl' import { ExternalAuthOIDC, ExternalAuthOIDCType } from './external-auth/oidc' +import { Emojis } from './emojis' import { loc } from './loc' const escapeHTML = require('escape-html') @@ -68,6 +69,10 @@ async function initSettings (options: RegisterServerOptions): Promise { await ExternalAuthOIDC.initSingletons(options) + // recreating a Emojis singleton + await Emojis.destroySingleton() + await Emojis.initSingleton(options) + peertubeHelpers.logger.info('Saving settings, ensuring prosody is running') await ensureProsodyRunning(options) diff --git a/server/main.ts b/server/main.ts index 6f88b913..497ccbea 100644 --- a/server/main.ts +++ b/server/main.ts @@ -18,6 +18,7 @@ import { BotConfiguration } from './lib/configuration/bot' import { BotsCtl } from './lib/bots/ctl' import { ExternalAuthOIDC } from './lib/external-auth/oidc' import { migrateMUCAffiliations } from './lib/prosody/migration/migrateV10' +import { Emojis } from './lib/emojis' import decache from 'decache' // FIXME: Peertube unregister don't have any parameter. @@ -48,6 +49,9 @@ async function register (options: RegisterServerOptions): Promise { await migrateSettings(options) await initSettings(options) + + await Emojis.initSingleton(options) // after settings, before routes + await initCustomFields(options) await initRouters(options) await initFederation(options) @@ -110,6 +114,7 @@ async function unregister (): Promise { await RoomChannel.destroySingleton() await BotConfiguration.destroySingleton() await ExternalAuthOIDC.destroySingletons() + await Emojis.destroySingleton() const module = __filename OPTIONS?.peertubeHelpers.logger.info(`Unloading module ${module}...`) diff --git a/shared/lib/types.ts b/shared/lib/types.ts index e7f2ff24..5058112e 100644 --- a/shared/lib/types.ts +++ b/shared/lib/types.ts @@ -161,6 +161,11 @@ interface ChannelEmojis { customEmojis: CustomEmojiDefinition[] } +interface ChannelEmojisConfiguration { + channel: ChannelInfos + emojis: ChannelEmojis +} + export type { ConverseJSTheme, InitConverseJSParams, @@ -178,5 +183,6 @@ export type { ExternalAuthResult, ExternalAuthOIDCType, CustomEmojiDefinition, - ChannelEmojis + ChannelEmojis, + ChannelEmojisConfiguration } diff --git a/support/documentation/content/en/technical/data/_index.md b/support/documentation/content/en/technical/data/_index.md index 0df904f6..c475d144 100644 --- a/support/documentation/content/en/technical/data/_index.md +++ b/support/documentation/content/en/technical/data/_index.md @@ -107,3 +107,12 @@ Note: this includes the bot username and password. Don't let it leak. The `bot/muc_domain/rooms` folder contains room configuration files. See the [xmppjs-chat-bot](https://github.com/JohnXLivingston/xmppjs-chat-bot) package help for more information. + +## emojis/channel + +The `emojis/channel` folder contains custom emojis definitions for channels. + +For example, the channel `1` will contain: + +* `emojis/channel/1/definition.json`: the JSON file containing the emojis definitions +* `emojis/channel/1/files/42.png`: N image files (png, jpg, ...), using numbers as filenames.