snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
import { api } from '@rocket.chat/core-services';
|
||||
import { EmojiCustom } from '@rocket.chat/models';
|
||||
import limax from 'limax';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { trim } from '../../../../../lib/utils/stringUtils';
|
||||
import { hasPermissionAsync } from '../../../authorization/hasPermission';
|
||||
import { RocketChatFileEmojiCustomInstance } from '../startup/emoji-custom';
|
||||
|
||||
export type EmojiData = {
|
||||
_id?: string;
|
||||
name: string;
|
||||
aliases?: string;
|
||||
extension: string;
|
||||
previousName?: string;
|
||||
previousExtension?: string;
|
||||
newFile?: boolean;
|
||||
};
|
||||
|
||||
type EmojiDataWithParsedAliases = Omit<EmojiData, 'aliases' | '_id'> & { _id: string; aliases: string[] };
|
||||
|
||||
export async function insertOrUpdateEmoji(userId: string | null, emojiData: EmojiData): Promise<EmojiDataWithParsedAliases> {
|
||||
if (!userId || !(await hasPermissionAsync(userId, 'manage-emoji'))) {
|
||||
throw new Meteor.Error('not_authorized');
|
||||
}
|
||||
|
||||
if (!trim(emojiData.name)) {
|
||||
throw new Meteor.Error('error-the-field-is-required', 'The field Name is required', {
|
||||
method: 'insertOrUpdateEmoji',
|
||||
field: 'Name',
|
||||
});
|
||||
}
|
||||
|
||||
emojiData.name = limax(emojiData.name, { replacement: '_' });
|
||||
|
||||
// allow all characters except colon, whitespace, comma, >, <, &, ", ', /, \, (, )
|
||||
// more practical than allowing specific sets of characters; also allows foreign languages
|
||||
const nameValidation = /[\s,:><&"'\/\\\(\)]/;
|
||||
const aliasValidation = /[:><&\|"'\/\\\(\)]/;
|
||||
|
||||
// silently strip colon; this allows for uploading :emojiname: as emojiname
|
||||
emojiData.name = emojiData.name.replace(/:/g, '');
|
||||
emojiData.aliases = emojiData.aliases?.replace(/:/g, '');
|
||||
|
||||
if (nameValidation.test(emojiData.name)) {
|
||||
throw new Meteor.Error('error-input-is-not-a-valid-field', `${emojiData.name} is not a valid name`, {
|
||||
method: 'insertOrUpdateEmoji',
|
||||
input: emojiData.name,
|
||||
field: 'Name',
|
||||
});
|
||||
}
|
||||
|
||||
let aliases: string[] = [];
|
||||
if (emojiData.aliases) {
|
||||
if (aliasValidation.test(emojiData.aliases)) {
|
||||
throw new Meteor.Error('error-input-is-not-a-valid-field', `${emojiData.aliases} is not a valid alias set`, {
|
||||
method: 'insertOrUpdateEmoji',
|
||||
input: emojiData.aliases,
|
||||
field: 'Alias_Set',
|
||||
});
|
||||
}
|
||||
aliases = emojiData.aliases
|
||||
.split(/\s*,\s*/)
|
||||
.filter(Boolean)
|
||||
.map((alias) => limax(alias, { replacement: '_' }))
|
||||
.filter((alias) => alias !== emojiData.name);
|
||||
}
|
||||
|
||||
emojiData.extension = emojiData.extension === 'svg+xml' ? 'png' : emojiData.extension;
|
||||
|
||||
const conflictingEmoji = await EmojiCustom.findOneByNamesOrAliases([emojiData.name, ...aliases], emojiData._id, {
|
||||
projection: { _id: 1 },
|
||||
});
|
||||
|
||||
if (conflictingEmoji) {
|
||||
throw new Meteor.Error('Custom_Emoji_Error_Name_Or_Alias_Already_In_Use', 'The custom emoji or one of its aliases is already in use', {
|
||||
method: 'insertOrUpdateEmoji',
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof emojiData.extension === 'undefined') {
|
||||
throw new Meteor.Error('error-the-field-is-required', 'The custom emoji file is required', {
|
||||
method: 'insertOrUpdateEmoji',
|
||||
});
|
||||
}
|
||||
|
||||
if (!emojiData._id) {
|
||||
// insert emoji
|
||||
const createEmoji = {
|
||||
name: emojiData.name,
|
||||
aliases,
|
||||
extension: emojiData.extension,
|
||||
};
|
||||
|
||||
const _id = (await EmojiCustom.create(createEmoji)).insertedId;
|
||||
|
||||
void api.broadcast('emoji.updateCustom', createEmoji);
|
||||
|
||||
return { ...emojiData, ...createEmoji, _id };
|
||||
}
|
||||
|
||||
// update emoji
|
||||
if (emojiData.newFile) {
|
||||
await RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${emojiData.name}.${emojiData.extension}`));
|
||||
await RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${emojiData.name}.${emojiData.previousExtension}`));
|
||||
await RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${emojiData.previousName}.${emojiData.extension}`));
|
||||
await RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${emojiData.previousName}.${emojiData.previousExtension}`));
|
||||
|
||||
await EmojiCustom.setExtension(emojiData._id, emojiData.extension);
|
||||
} else if (emojiData.name !== emojiData.previousName) {
|
||||
const rs = await RocketChatFileEmojiCustomInstance.getFileWithReadStream(
|
||||
encodeURIComponent(`${emojiData.previousName}.${emojiData.previousExtension}`),
|
||||
);
|
||||
|
||||
if (rs) {
|
||||
await RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${emojiData.name}.${emojiData.extension}`));
|
||||
const ws = RocketChatFileEmojiCustomInstance.createWriteStream(
|
||||
encodeURIComponent(`${emojiData.name}.${emojiData.previousExtension}`),
|
||||
rs.contentType,
|
||||
);
|
||||
ws.on('end', () =>
|
||||
RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${emojiData.previousName}.${emojiData.previousExtension}`)),
|
||||
);
|
||||
rs.readStream.pipe(ws);
|
||||
}
|
||||
}
|
||||
|
||||
if (emojiData.name !== emojiData.previousName) {
|
||||
await EmojiCustom.setName(emojiData._id, emojiData.name);
|
||||
}
|
||||
|
||||
if (emojiData.aliases) {
|
||||
await EmojiCustom.setAliases(emojiData._id, aliases);
|
||||
} else {
|
||||
await EmojiCustom.setAliases(emojiData._id, []);
|
||||
}
|
||||
|
||||
void api.broadcast('emoji.updateCustom', { ...emojiData, aliases });
|
||||
|
||||
return { ...emojiData, aliases } as EmojiDataWithParsedAliases;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { api, Media } from '@rocket.chat/core-services';
|
||||
import { EmojiCustom } from '@rocket.chat/models';
|
||||
import { Random } from '@rocket.chat/random';
|
||||
import limax from 'limax';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import sharp from 'sharp';
|
||||
|
||||
import type { EmojiData } from './insertOrUpdateEmoji';
|
||||
import { hasPermissionAsync } from '../../../authorization/hasPermission';
|
||||
import { RocketChatFile } from '../../file';
|
||||
import { RocketChatFileEmojiCustomInstance } from '../startup/emoji-custom';
|
||||
|
||||
const getFile = async (file: Buffer, extension: string) => {
|
||||
if (extension !== 'svg+xml') {
|
||||
return file;
|
||||
}
|
||||
|
||||
return sharp(file).png().toBuffer();
|
||||
};
|
||||
|
||||
export type EmojiDataWithAliases = Omit<EmojiData, 'aliases'> & { aliases?: string | string[] };
|
||||
|
||||
export async function uploadEmojiCustom(
|
||||
userId: string | null,
|
||||
binaryContent: string,
|
||||
contentType: string,
|
||||
emojiData: EmojiDataWithAliases,
|
||||
) {
|
||||
return uploadEmojiCustomWithBuffer(userId, Buffer.from(binaryContent, 'binary'), contentType, emojiData);
|
||||
}
|
||||
|
||||
export async function uploadEmojiCustomWithBuffer(
|
||||
userId: string | null,
|
||||
buffer: Buffer,
|
||||
contentType: string,
|
||||
emojiData: EmojiDataWithAliases,
|
||||
): Promise<void> {
|
||||
// technically, since this method doesnt have any datatype validations, users can
|
||||
// upload videos as emojis. The FE won't play them, but they will waste space for sure.
|
||||
if (!userId || !(await hasPermissionAsync(userId, 'manage-emoji'))) {
|
||||
throw new Meteor.Error('not_authorized');
|
||||
}
|
||||
|
||||
if (!Array.isArray(emojiData.aliases)) {
|
||||
// delete aliases for notification purposes. here, it is a string or undefined rather than an array
|
||||
delete emojiData.aliases;
|
||||
}
|
||||
|
||||
emojiData.name = limax(emojiData.name, { replacement: '_' });
|
||||
|
||||
const file = await getFile(buffer, emojiData.extension);
|
||||
emojiData.extension = emojiData.extension === 'svg+xml' ? 'png' : emojiData.extension;
|
||||
|
||||
let fileBuffer;
|
||||
// sharp doesn't support these formats without imagemagick or libvips installed
|
||||
// so they will be stored as they are :(
|
||||
if (['gif', 'x-icon', 'bmp', 'webm'].includes(emojiData.extension)) {
|
||||
fileBuffer = file;
|
||||
} else {
|
||||
// This is to support the idea of having "sticker-like" emojis
|
||||
const { data: resizedEmojiBuffer } = await Media.resizeFromBuffer(file, 512, 512, true, false, false, 'inside');
|
||||
fileBuffer = resizedEmojiBuffer;
|
||||
}
|
||||
|
||||
const rs = RocketChatFile.bufferToStream(fileBuffer);
|
||||
await RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${emojiData.name}.${emojiData.extension}`));
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const ws = RocketChatFileEmojiCustomInstance.createWriteStream(
|
||||
encodeURIComponent(`${emojiData.name}.${emojiData.extension}`),
|
||||
contentType,
|
||||
);
|
||||
ws.on('end', async () => {
|
||||
const etag = Random.hexString(6);
|
||||
await EmojiCustom.setETagByName(emojiData.name, etag);
|
||||
setTimeout(() => api.broadcast('emoji.updateCustom', { ...emojiData, etag }), 500);
|
||||
resolve();
|
||||
});
|
||||
|
||||
rs.pipe(ws);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { EmojiCustom } from '@rocket.chat/models';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { WebApp } from 'meteor/webapp';
|
||||
import _ from 'underscore';
|
||||
|
||||
import { settings } from '../../../../settings';
|
||||
import { SystemLogger } from '../../../logger/system';
|
||||
import { RocketChatFile } from '../../file';
|
||||
|
||||
export let RocketChatFileEmojiCustomInstance;
|
||||
|
||||
const writeSvgFallback = (res, req) => {
|
||||
res.setHeader('Content-Type', 'image/svg+xml');
|
||||
res.setHeader('Cache-Control', 'public, max-age=0');
|
||||
res.setHeader('Expires', '-1');
|
||||
res.setHeader('Last-Modified', 'Thu, 01 Jan 2015 00:00:00 GMT');
|
||||
|
||||
const reqModifiedHeader = req.headers['if-modified-since'];
|
||||
if (reqModifiedHeader != null) {
|
||||
if (reqModifiedHeader === 'Thu, 01 Jan 2015 00:00:00 GMT') {
|
||||
res.writeHead(304);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const color = '#000';
|
||||
const initials = '?';
|
||||
|
||||
const svg = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" pointer-events="none" width="50" height="50" style="width: 50px; height: 50px; background-color: ${color};">
|
||||
<text text-anchor="middle" y="50%" x="50%" dy="0.36em" pointer-events="auto" fill="#ffffff" font-family="Helvetica, Arial, Lucida Grande, sans-serif" style="font-weight: 400; font-size: 28px;">
|
||||
${initials}
|
||||
</text>
|
||||
</svg>`;
|
||||
|
||||
res.write(svg);
|
||||
res.end();
|
||||
};
|
||||
|
||||
const initializeEmojiCustomStorage = () => {
|
||||
let storeType = 'GridFS';
|
||||
|
||||
if (settings.get('EmojiUpload_Storage_Type')) {
|
||||
storeType = settings.get('EmojiUpload_Storage_Type');
|
||||
}
|
||||
|
||||
const RocketChatStore = RocketChatFile[storeType];
|
||||
|
||||
if (RocketChatStore == null) {
|
||||
throw new Error(`Invalid RocketChatStore type [${storeType}]`);
|
||||
}
|
||||
|
||||
SystemLogger.info({
|
||||
msg: 'Using custom emoji storage',
|
||||
storeType,
|
||||
});
|
||||
|
||||
let path = '~/uploads';
|
||||
if (settings.get('EmojiUpload_FileSystemPath') != null) {
|
||||
if (settings.get('EmojiUpload_FileSystemPath').trim() !== '') {
|
||||
path = settings.get('EmojiUpload_FileSystemPath');
|
||||
}
|
||||
}
|
||||
|
||||
RocketChatFileEmojiCustomInstance = new RocketChatStore({
|
||||
name: 'custom_emoji',
|
||||
absolutePath: path,
|
||||
});
|
||||
};
|
||||
|
||||
Meteor.startup(() => {
|
||||
initializeEmojiCustomStorage();
|
||||
return WebApp.connectHandlers.use('/emoji-custom/', async (req, res /* , next*/) => {
|
||||
const params = { emoji: decodeURIComponent(req.url.replace(/^\//, '').replace(/\?.*$/, '')) };
|
||||
|
||||
if (_.isEmpty(params.emoji)) {
|
||||
res.writeHead(403);
|
||||
res.write('Forbidden');
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
res.setHeader('Content-Disposition', 'inline');
|
||||
|
||||
const emoji = await EmojiCustom.findOneByName(params.emoji.split('.')[0], { projection: { _id: 1 } });
|
||||
|
||||
if (!emoji) {
|
||||
return writeSvgFallback(res, req);
|
||||
}
|
||||
|
||||
const file = await RocketChatFileEmojiCustomInstance.getFileWithReadStream(encodeURIComponent(params.emoji));
|
||||
|
||||
if (!file) {
|
||||
// use code from username initials renderer until file upload is complete
|
||||
return writeSvgFallback(res, req);
|
||||
}
|
||||
|
||||
const fileUploadDate = file.uploadDate != null ? file.uploadDate.toUTCString() : undefined;
|
||||
|
||||
const reqModifiedHeader = req.headers['if-modified-since'];
|
||||
if (reqModifiedHeader != null && reqModifiedHeader === fileUploadDate) {
|
||||
res.setHeader('Last-Modified', reqModifiedHeader);
|
||||
res.writeHead(304);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
res.setHeader('Cache-Control', 'public, max-age=31536000');
|
||||
res.setHeader('Last-Modified', fileUploadDate || new Date().toUTCString());
|
||||
res.setHeader('Content-Length', file.length);
|
||||
|
||||
if (/^svg$/i.test(params.emoji.split('.').pop())) {
|
||||
res.setHeader('Content-Type', 'image/svg+xml');
|
||||
} else if (/^png$/i.test(params.emoji.split('.').pop())) {
|
||||
res.setHeader('Content-Type', 'image/png');
|
||||
} else {
|
||||
res.setHeader('Content-Type', 'image/jpeg');
|
||||
}
|
||||
|
||||
file.readStream.pipe(res);
|
||||
});
|
||||
});
|
||||
|
||||
settings.watchMultiple(['EmojiUpload_Storage_Type', 'EmojiUpload_FileSystemPath'], initializeEmojiCustomStorage);
|
||||
Reference in New Issue
Block a user