Custom channel emoticons WIP (#130)
This commit is contained in:
parent
607a71b8cb
commit
688ab4f029
2
client/@types/global.d.ts
vendored
2
client/@types/global.d.ts
vendored
@ -88,3 +88,5 @@ declare const LOC_INVALID_VALUE_NOT_IN_RANGE: string
|
|||||||
declare const LOC_CHATROOM_NOT_ACCESSIBLE: string
|
declare const LOC_CHATROOM_NOT_ACCESSIBLE: string
|
||||||
|
|
||||||
declare const LOC_PROMOTE: string
|
declare const LOC_PROMOTE: string
|
||||||
|
|
||||||
|
declare const LOC_LIVECHAT_CONFIGURATION_CHANNEL_EMOJIS_TITLE: string
|
||||||
|
@ -3,15 +3,16 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
|
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
|
||||||
|
import type { ChannelEmojisConfiguration } from 'shared/lib/types'
|
||||||
import { LivechatElement } from '../../lib/elements/livechat'
|
import { LivechatElement } from '../../lib/elements/livechat'
|
||||||
import { registerClientOptionsContext } from '../../lib/contexts/peertube'
|
import { registerClientOptionsContext } from '../../lib/contexts/peertube'
|
||||||
import { ChannelDetailsService } from '../services/channel-details'
|
import { ChannelDetailsService } from '../services/channel-details'
|
||||||
import { channelDetailsServiceContext } from '../contexts/channel'
|
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 { Task } from '@lit/task'
|
||||||
import { customElement, property } from 'lit/decorators.js'
|
import { customElement, property } from 'lit/decorators.js'
|
||||||
import { provide } from '@lit/context'
|
import { provide } from '@lit/context'
|
||||||
|
import { html } from 'lit'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Channel emojis configuration page.
|
* Channel emojis configuration page.
|
||||||
@ -25,15 +26,37 @@ export class ChannelEmojisElement extends LivechatElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public channelId?: number
|
public channelId?: number
|
||||||
|
|
||||||
private _channelEmojis?: ChannelEmojis
|
private _channelEmojisConfiguration?: ChannelEmojisConfiguration
|
||||||
|
|
||||||
@provide({ context: channelDetailsServiceContext })
|
@provide({ context: channelDetailsServiceContext })
|
||||||
private _channelDetailsService?: ChannelDetailsService
|
private _channelDetailsService?: ChannelDetailsService
|
||||||
|
|
||||||
protected override render = (): void => {
|
protected override render = (): unknown => {
|
||||||
return this._asyncTaskRender.render({
|
return this._asyncTaskRender.render({
|
||||||
pending: () => {},
|
pending: () => {},
|
||||||
complete: () => {},
|
complete: () => html`
|
||||||
|
<div
|
||||||
|
class="margin-content peertube-plugin-livechat-configuration peertube-plugin-livechat-configuration-channel"
|
||||||
|
>
|
||||||
|
<h1>
|
||||||
|
${ptTr(LOC_LIVECHAT_CONFIGURATION_CHANNEL_EMOJIS_TITLE)}:
|
||||||
|
<span class="peertube-plugin-livechat-configuration-channel-info">
|
||||||
|
<span>${this._channelEmojisConfiguration?.channel.displayName}</span>
|
||||||
|
<span>${this._channelEmojisConfiguration?.channel.name}</span>
|
||||||
|
</span>
|
||||||
|
<livechat-help-button .page=${'documentation/user/streamers/emojis'}>
|
||||||
|
</livechat-help-button>
|
||||||
|
FIXME: help url OK?
|
||||||
|
</h1>
|
||||||
|
<form role="form" @submit=${this._saveEmojis}>
|
||||||
|
<div class="form-group mt-5">
|
||||||
|
<button type="submit" class="peertube-button-link orange-button">
|
||||||
|
${ptTr(LOC_SAVE)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
error: (err: any) => {
|
error: (err: any) => {
|
||||||
this.registerClientOptions?.peertubeHelpers.notifier.error(err.toString())
|
this.registerClientOptions?.peertubeHelpers.notifier.error(err.toString())
|
||||||
}
|
}
|
||||||
@ -49,8 +72,14 @@ export class ChannelEmojisElement extends LivechatElement {
|
|||||||
throw new Error('Missing channelId')
|
throw new Error('Missing channelId')
|
||||||
}
|
}
|
||||||
this._channelDetailsService = new ChannelDetailsService(this.registerClientOptions)
|
this._channelDetailsService = new ChannelDetailsService(this.registerClientOptions)
|
||||||
this._channelEmojis = await this._channelDetailsService.fetchEmojis(this.channelId)
|
this._channelEmojisConfiguration = await this._channelDetailsService.fetchEmojisConfiguration(this.channelId)
|
||||||
},
|
},
|
||||||
args: () => []
|
args: () => []
|
||||||
})
|
})
|
||||||
|
|
||||||
|
private readonly _saveEmojis = (ev?: Event): void => {
|
||||||
|
ev?.preventDefault()
|
||||||
|
// TODO
|
||||||
|
this.registerClientOptions?.peertubeHelpers.notifier.error('TODO')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
|
import type { RegisterClientOptions } from '@peertube/peertube-types/client'
|
||||||
import type { ValidationError } from '../../lib/models/validation'
|
import type { ValidationError } from '../../lib/models/validation'
|
||||||
import type {
|
import type {
|
||||||
ChannelLiveChatInfos, ChannelConfiguration, ChannelConfigurationOptions, ChannelEmojis
|
ChannelLiveChatInfos, ChannelConfiguration, ChannelConfigurationOptions, ChannelEmojisConfiguration
|
||||||
} from 'shared/lib/types'
|
} from 'shared/lib/types'
|
||||||
import { ValidationErrorType } from '../../lib/models/validation'
|
import { ValidationErrorType } from '../../lib/models/validation'
|
||||||
import { getBaseRoute } from '../../../utils/uri'
|
import { getBaseRoute } from '../../../utils/uri'
|
||||||
@ -161,7 +161,7 @@ export class ChannelDetailsService {
|
|||||||
return response.json()
|
return response.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchEmojis = async (channelId: number): Promise<ChannelEmojis> => {
|
fetchEmojisConfiguration = async (channelId: number): Promise<ChannelEmojisConfiguration> => {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
getBaseRoute(this._registerClientOptions) +
|
getBaseRoute(this._registerClientOptions) +
|
||||||
'/api/configuration/channel/emojis/' +
|
'/api/configuration/channel/emojis/' +
|
||||||
@ -173,6 +173,9 @@ export class ChannelDetailsService {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (!response.ok) {
|
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.')
|
throw new Error('Can\'t get channel emojis options.')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -474,3 +474,5 @@ task_list_pick_message: |
|
|||||||
More information in the livechat plugin documentation.
|
More information in the livechat plugin documentation.
|
||||||
|
|
||||||
promote: 'Become moderator'
|
promote: 'Become moderator'
|
||||||
|
|
||||||
|
livechat_configuration_channel_emojis_title: 'Channel emojis'
|
||||||
|
@ -15,6 +15,7 @@ import { getBaseRouterRoute, getBaseStaticRoute } from '../helpers'
|
|||||||
import { getProsodyDomain } from '../prosody/config/domain'
|
import { getProsodyDomain } from '../prosody/config/domain'
|
||||||
import { getBoshUri, getWSUri } from '../uri/webchat'
|
import { getBoshUri, getWSUri } from '../uri/webchat'
|
||||||
import { ExternalAuthOIDC } from '../external-auth/oidc'
|
import { ExternalAuthOIDC } from '../external-auth/oidc'
|
||||||
|
import { Emojis } from '../emojis'
|
||||||
|
|
||||||
interface GetConverseJSParamsParams {
|
interface GetConverseJSParamsParams {
|
||||||
readonly?: boolean | 'noscroll'
|
readonly?: boolean | 'noscroll'
|
||||||
@ -284,10 +285,11 @@ async function _connectionInfos (
|
|||||||
params.forcetype ?? false
|
params.forcetype ?? false
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!settings['disable-channel-configuration'] && video?.channelId) {
|
if (video?.channelId && await Emojis.singletonSafe()?.channelHasCustomEmojis(video.channelId)) {
|
||||||
customEmojisUrl = getBaseRouterRoute(options) +
|
customEmojisUrl = getBaseRouterRoute(options) +
|
||||||
'api/configuration/channel/emojis/' +
|
'emojis/channel/' +
|
||||||
encodeURIComponent(video.channelId)
|
encodeURIComponent(video.channelId) +
|
||||||
|
'/definition'
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
options.peertubeHelpers.logger.error(err)
|
options.peertubeHelpers.logger.error(err)
|
||||||
|
232
server/lib/emojis/emojis.ts
Normal file
232
server/lib/emojis/emojis.ts
Normal file
@ -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<boolean> {
|
||||||
|
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<ChannelEmojis | undefined> {
|
||||||
|
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<boolean> {
|
||||||
|
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<ChannelEmojis> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
if (!singleton) { return }
|
||||||
|
singleton = undefined
|
||||||
|
}
|
||||||
|
}
|
2
server/lib/emojis/index.ts
Normal file
2
server/lib/emojis/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import './emojis'
|
||||||
|
export * from './emojis'
|
@ -12,13 +12,9 @@ import { isUserAdminOrModerator } from '../../helpers'
|
|||||||
* Returns a middleware handler to get the channelInfos from the channel parameter.
|
* Returns a middleware handler to get the channelInfos from the channel parameter.
|
||||||
* This is used in api related to channel configuration options.
|
* This is used in api related to channel configuration options.
|
||||||
* @param options Peertube server 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
|
* @returns middleware function
|
||||||
*/
|
*/
|
||||||
function getCheckConfigurationChannelMiddleware (
|
function getCheckConfigurationChannelMiddleware (options: RegisterServerOptions): RequestPromiseHandler {
|
||||||
options: RegisterServerOptions,
|
|
||||||
publicPage?: boolean
|
|
||||||
): RequestPromiseHandler {
|
|
||||||
return async (req: Request, res: Response, next: NextFunction) => {
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const logger = options.peertubeHelpers.logger
|
const logger = options.peertubeHelpers.logger
|
||||||
const channelId = req.params.channelId
|
const channelId = req.params.channelId
|
||||||
@ -36,20 +32,18 @@ function getCheckConfigurationChannelMiddleware (
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!publicPage) {
|
// To access this page, you must either be:
|
||||||
// To access this page, you must either be:
|
// - the channel owner,
|
||||||
// - the channel owner,
|
// - an instance modo/admin
|
||||||
// - an instance modo/admin
|
// - TODO: a channel chat moderator, as defined in this page.
|
||||||
// - TODO: a channel chat moderator, as defined in this page.
|
if (channelInfos.ownerAccountId === currentUser.Account.id) {
|
||||||
if (channelInfos.ownerAccountId === currentUser.Account.id) {
|
logger.debug('Current user is the channel owner')
|
||||||
logger.debug('Current user is the channel owner')
|
} else if (await isUserAdminOrModerator(options, res)) {
|
||||||
} else if (await isUserAdminOrModerator(options, res)) {
|
logger.debug('Current user is an instance moderator or admin')
|
||||||
logger.debug('Current user is an instance moderator or admin')
|
} else {
|
||||||
} else {
|
logger.warn('Current user tries to access a channel for which he has no right.')
|
||||||
logger.warn('Current user tries to access a channel for which he has no right.')
|
res.sendStatus(403)
|
||||||
res.sendStatus(403)
|
return
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug('User can access the configuration channel api.')
|
logger.debug('User can access the configuration channel api.')
|
||||||
|
@ -13,6 +13,7 @@ import { initAuthApiRouter, initUserAuthApiRouter } from './api/auth'
|
|||||||
import { initFederationServerInfosApiRouter } from './api/federation-server-infos'
|
import { initFederationServerInfosApiRouter } from './api/federation-server-infos'
|
||||||
import { initConfigurationApiRouter } from './api/configuration'
|
import { initConfigurationApiRouter } from './api/configuration'
|
||||||
import { initPromoteApiRouter } from './api/promote'
|
import { initPromoteApiRouter } from './api/promote'
|
||||||
|
import { initEmojisRouter } from './emojis'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initiate API routes
|
* Initiate API routes
|
||||||
@ -42,6 +43,7 @@ async function initApiRouter (options: RegisterServerOptions): Promise<Router> {
|
|||||||
|
|
||||||
await initConfigurationApiRouter(options, router)
|
await initConfigurationApiRouter(options, router)
|
||||||
await initPromoteApiRouter(options, router)
|
await initPromoteApiRouter(options, router)
|
||||||
|
await initEmojisRouter(options, router)
|
||||||
|
|
||||||
if (isDebugMode(options)) {
|
if (isDebugMode(options)) {
|
||||||
// Only add this route if the debug mode is enabled at time of the server launch.
|
// Only add this route if the debug mode is enabled at time of the server launch.
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import type { RegisterServerOptions } from '@peertube/peertube-types'
|
import type { RegisterServerOptions } from '@peertube/peertube-types'
|
||||||
import type { Router, Request, Response, NextFunction } from 'express'
|
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 { asyncMiddleware } from '../../middlewares/async'
|
||||||
import { getCheckConfigurationChannelMiddleware } from '../../middlewares/configuration/channel'
|
import { getCheckConfigurationChannelMiddleware } from '../../middlewares/configuration/channel'
|
||||||
import { checkConfigurationEnabledMiddleware } from '../../middlewares/configuration/configuration'
|
import { checkConfigurationEnabledMiddleware } from '../../middlewares/configuration/configuration'
|
||||||
@ -15,6 +15,7 @@ import {
|
|||||||
} from '../../configuration/channel/storage'
|
} from '../../configuration/channel/storage'
|
||||||
import { sanitizeChannelConfigurationOptions } from '../../configuration/channel/sanitize'
|
import { sanitizeChannelConfigurationOptions } from '../../configuration/channel/sanitize'
|
||||||
import { getConverseJSParams } from '../../../lib/conversejs/params'
|
import { getConverseJSParams } from '../../../lib/conversejs/params'
|
||||||
|
import { Emojis } from '../../../lib/emojis'
|
||||||
|
|
||||||
async function initConfigurationApiRouter (options: RegisterServerOptions, router: Router): Promise<void> {
|
async function initConfigurationApiRouter (options: RegisterServerOptions, router: Router): Promise<void> {
|
||||||
const logger = options.peertubeHelpers.logger
|
const logger = options.peertubeHelpers.logger
|
||||||
@ -57,7 +58,7 @@ async function initConfigurationApiRouter (options: RegisterServerOptions, route
|
|||||||
await getChannelConfigurationOptions(options, channelInfos.id) ??
|
await getChannelConfigurationOptions(options, channelInfos.id) ??
|
||||||
getDefaultChannelConfigurationOptions(options)
|
getDefaultChannelConfigurationOptions(options)
|
||||||
|
|
||||||
const result = {
|
const result: ChannelConfiguration = {
|
||||||
channel: channelInfos,
|
channel: channelInfos,
|
||||||
configuration: channelOptions
|
configuration: channelOptions
|
||||||
}
|
}
|
||||||
@ -101,7 +102,7 @@ async function initConfigurationApiRouter (options: RegisterServerOptions, route
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.debug('Data seems ok, storing them.')
|
logger.debug('Data seems ok, storing them.')
|
||||||
const result = {
|
const result: ChannelConfiguration = {
|
||||||
channel: channelInfos,
|
channel: channelInfos,
|
||||||
configuration: channelOptions
|
configuration: channelOptions
|
||||||
}
|
}
|
||||||
@ -113,25 +114,62 @@ async function initConfigurationApiRouter (options: RegisterServerOptions, route
|
|||||||
|
|
||||||
router.get('/configuration/channel/emojis/:channelId', asyncMiddleware([
|
router.get('/configuration/channel/emojis/:channelId', asyncMiddleware([
|
||||||
checkConfigurationEnabledMiddleware(options),
|
checkConfigurationEnabledMiddleware(options),
|
||||||
getCheckConfigurationChannelMiddleware(options, true),
|
getCheckConfigurationChannelMiddleware(options),
|
||||||
async (req: Request, res: Response, _next: NextFunction): Promise<void> => {
|
async (req: Request, res: Response, _next: NextFunction): Promise<void> => {
|
||||||
if (!res.locals.channelInfos) {
|
try {
|
||||||
logger.error('Missing channelInfos in res.locals, should not happen')
|
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)
|
res.sendStatus(500)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
// const channelInfos = res.locals.channelInfos as ChannelInfos
|
}
|
||||||
|
]))
|
||||||
|
|
||||||
const channelEmojis: ChannelEmojis = {
|
router.post('/configuration/channel/emojis/:channelId', asyncMiddleware([
|
||||||
customEmojis: [{
|
checkConfigurationEnabledMiddleware(options),
|
||||||
sn: ':test:',
|
getCheckConfigurationChannelMiddleware(options),
|
||||||
url: '/dist/images/custom_emojis/xmpp.png',
|
async (req: Request, res: Response, _next: NextFunction): Promise<void> => {
|
||||||
isCategoryEmoji: true
|
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)
|
|
||||||
}
|
}
|
||||||
]))
|
]))
|
||||||
}
|
}
|
||||||
|
90
server/lib/routers/emojis.ts
Normal file
90
server/lib/routers/emojis.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2024 John Livingston <https://www.john-livingston.fr/>
|
||||||
|
//
|
||||||
|
// 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<void> {
|
||||||
|
const logger = options.peertubeHelpers.logger
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/emojis/channel/:channelId/definition',
|
||||||
|
asyncMiddleware(async (req: Request, res: Response, _next: NextFunction): Promise<void> => {
|
||||||
|
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<void> => {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
@ -8,6 +8,7 @@ import { ensureProsodyRunning } from './prosody/ctl'
|
|||||||
import { RoomChannel } from './room-channel'
|
import { RoomChannel } from './room-channel'
|
||||||
import { BotsCtl } from './bots/ctl'
|
import { BotsCtl } from './bots/ctl'
|
||||||
import { ExternalAuthOIDC, ExternalAuthOIDCType } from './external-auth/oidc'
|
import { ExternalAuthOIDC, ExternalAuthOIDCType } from './external-auth/oidc'
|
||||||
|
import { Emojis } from './emojis'
|
||||||
import { loc } from './loc'
|
import { loc } from './loc'
|
||||||
const escapeHTML = require('escape-html')
|
const escapeHTML = require('escape-html')
|
||||||
|
|
||||||
@ -68,6 +69,10 @@ async function initSettings (options: RegisterServerOptions): Promise<void> {
|
|||||||
|
|
||||||
await ExternalAuthOIDC.initSingletons(options)
|
await ExternalAuthOIDC.initSingletons(options)
|
||||||
|
|
||||||
|
// recreating a Emojis singleton
|
||||||
|
await Emojis.destroySingleton()
|
||||||
|
await Emojis.initSingleton(options)
|
||||||
|
|
||||||
peertubeHelpers.logger.info('Saving settings, ensuring prosody is running')
|
peertubeHelpers.logger.info('Saving settings, ensuring prosody is running')
|
||||||
await ensureProsodyRunning(options)
|
await ensureProsodyRunning(options)
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ import { BotConfiguration } from './lib/configuration/bot'
|
|||||||
import { BotsCtl } from './lib/bots/ctl'
|
import { BotsCtl } from './lib/bots/ctl'
|
||||||
import { ExternalAuthOIDC } from './lib/external-auth/oidc'
|
import { ExternalAuthOIDC } from './lib/external-auth/oidc'
|
||||||
import { migrateMUCAffiliations } from './lib/prosody/migration/migrateV10'
|
import { migrateMUCAffiliations } from './lib/prosody/migration/migrateV10'
|
||||||
|
import { Emojis } from './lib/emojis'
|
||||||
import decache from 'decache'
|
import decache from 'decache'
|
||||||
|
|
||||||
// FIXME: Peertube unregister don't have any parameter.
|
// FIXME: Peertube unregister don't have any parameter.
|
||||||
@ -48,6 +49,9 @@ async function register (options: RegisterServerOptions): Promise<any> {
|
|||||||
await migrateSettings(options)
|
await migrateSettings(options)
|
||||||
|
|
||||||
await initSettings(options)
|
await initSettings(options)
|
||||||
|
|
||||||
|
await Emojis.initSingleton(options) // after settings, before routes
|
||||||
|
|
||||||
await initCustomFields(options)
|
await initCustomFields(options)
|
||||||
await initRouters(options)
|
await initRouters(options)
|
||||||
await initFederation(options)
|
await initFederation(options)
|
||||||
@ -110,6 +114,7 @@ async function unregister (): Promise<any> {
|
|||||||
await RoomChannel.destroySingleton()
|
await RoomChannel.destroySingleton()
|
||||||
await BotConfiguration.destroySingleton()
|
await BotConfiguration.destroySingleton()
|
||||||
await ExternalAuthOIDC.destroySingletons()
|
await ExternalAuthOIDC.destroySingletons()
|
||||||
|
await Emojis.destroySingleton()
|
||||||
|
|
||||||
const module = __filename
|
const module = __filename
|
||||||
OPTIONS?.peertubeHelpers.logger.info(`Unloading module ${module}...`)
|
OPTIONS?.peertubeHelpers.logger.info(`Unloading module ${module}...`)
|
||||||
|
@ -161,6 +161,11 @@ interface ChannelEmojis {
|
|||||||
customEmojis: CustomEmojiDefinition[]
|
customEmojis: CustomEmojiDefinition[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ChannelEmojisConfiguration {
|
||||||
|
channel: ChannelInfos
|
||||||
|
emojis: ChannelEmojis
|
||||||
|
}
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
ConverseJSTheme,
|
ConverseJSTheme,
|
||||||
InitConverseJSParams,
|
InitConverseJSParams,
|
||||||
@ -178,5 +183,6 @@ export type {
|
|||||||
ExternalAuthResult,
|
ExternalAuthResult,
|
||||||
ExternalAuthOIDCType,
|
ExternalAuthOIDCType,
|
||||||
CustomEmojiDefinition,
|
CustomEmojiDefinition,
|
||||||
ChannelEmojis
|
ChannelEmojis,
|
||||||
|
ChannelEmojisConfiguration
|
||||||
}
|
}
|
||||||
|
@ -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.
|
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.
|
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.
|
||||||
|
Loading…
Reference in New Issue
Block a user