support for activitypub

This commit is contained in:
Peter Jaszkowiak 2024-11-09 14:39:06 -07:00
parent 3a4d93f958
commit 4ddcbba7e4
15 changed files with 159 additions and 62 deletions

View file

@ -1,4 +1,4 @@
const nconf = require.main.require('nconf');
const nconf = require.main?.require('nconf');

export function getBaseUrl(): string {
const relative_path = nconf.get('relative_path') || '';

View file

@ -13,9 +13,9 @@ import { getBaseUrl } from './base-url';
import { clearCache } from './parse';
import { getAll as getCustomizations } from './customizations';

const nconf = require.main.require('nconf');
const winston = require.main.require('winston');
const plugins = require.main.require('./src/plugins');
const nconf = require.main?.require('nconf');
const winston = require.main?.require('winston');
const plugins = require.main?.require('./src/plugins');

export const assetsDir = join(__dirname, '../emoji');

@ -65,8 +65,8 @@ export default async function build(): Promise<void> {
packsInfo.push({
name: pack.name,
id: pack.id,
attribution: pack.attribution,
license: pack.license,
attribution: pack.attribution || '',
license: pack.license || '',
});

Object.keys(pack.dictionary).forEach((key) => {
@ -76,7 +76,7 @@ export default async function build(): Promise<void> {
if (!table[name]) {
table[name] = {
name,
character: emoji.character || `:${name}:`,
character: emoji.character,
image: emoji.image || '',
pack: pack.id,
aliases: emoji.aliases || [],
@ -122,7 +122,7 @@ export default async function build(): Promise<void> {

table[name] = {
name,
character: `:${name}:`,
character: undefined,
pack: 'customizations',
keywords: [],
image: emoji.image,
@ -160,7 +160,7 @@ export default async function build(): Promise<void> {

// generate CSS styles
cssBuilders.setBaseUrl(getBaseUrl());
const css = packs.map(pack => cssBuilders[pack.mode](pack)).join('\n');
const css = packs.map(pack => (cssBuilders as any)[pack.mode](pack)).join('\n');
const cssFile = `${css}\n.emoji-customizations {
display: inline-block;
height: 23px;
@ -185,7 +185,7 @@ export default async function build(): Promise<void> {
pack.font.woff,
pack.font.ttf,
pack.font.woff2,
].filter(Boolean);
].filter(Boolean) as [string];

await mkdirp(dir);
await Promise.all(fontFiles.map(async (file) => {

View file

@ -7,9 +7,9 @@ import * as settings from './settings';
import { build } from './pubsub';
import * as customizations from './customizations';

const nconf = require.main.require('nconf');
const { setupApiRoute, setupAdminPageRoute } = require.main.require('./src/routes/helpers');
const { formatApiResponse } = require.main.require('./src/controllers/helpers');
const nconf = require.main?.require('nconf');
const { setupApiRoute, setupAdminPageRoute } = require.main?.require('./src/routes/helpers');
const { formatApiResponse } = require.main?.require('./src/controllers/helpers');

// eslint-disable-next-line import/no-dynamic-require, @typescript-eslint/no-var-requires
const version: string = require(join(__dirname, '../../package.json')).version;

View file

@ -1,6 +1,6 @@
import { basename } from 'path';

const buster = require.main.require('./src/meta').config['cache-buster'];
const buster = require.main?.require('./src/meta').config['cache-buster'];
let baseUrl = '';

export function setBaseUrl(url:string):void {
@ -16,7 +16,7 @@ export function images(pack: EmojiDefinition): string {
'}';
}

export function sprite(pack: EmojiDefinition): string {
export function sprite(pack: EmojiDefinition & { mode: 'sprite' }): string {
const classes = Object.keys(pack.dictionary).map(name => `.emoji-${pack.id}.emoji--${name} {` +
`background-position: ${pack.dictionary[name].backgroundPosition};` +
'}');
@ -50,7 +50,7 @@ export function sprite(pack: EmojiDefinition): string {
${classes.join('')}`.split('\n').map(x => x.trim()).join('');
}

export function font(pack: EmojiDefinition): string {
export function font(pack: EmojiDefinition & { mode: 'font' }): string {
const route = `${baseUrl}/plugins/nodebb-plugin-emoji/emoji/${pack.id}`;

return `@font-face {

View file

@ -1,4 +1,4 @@
const db = require.main.require('./src/database');
const db = require.main?.require('./src/database');

const emojisKey = 'emoji:customizations:emojis';
const adjunctsKey = 'emoji:customizations:adjuncts';

View file

@ -5,9 +5,9 @@ import { build } from './pubsub';
import controllers from './controllers';
import { getBaseUrl } from './base-url';

const nconf = require.main.require('nconf');
const buster = require.main.require('./src/meta').config['cache-buster'];
const file = require.main.require('./src/file');
const nconf = require.main?.require('nconf');
const buster = require.main?.require('./src/meta').config['cache-buster'];
const file = require.main?.require('./src/file');

export async function init(params: any): Promise<void> {
controllers(params);

View file

@ -2,24 +2,26 @@ import { readFile } from 'fs-extra';

import { tableFile, aliasesFile, asciiFile, charactersFile } from './build';

const buster = require.main.require('./src/meta').config['cache-buster'];
const winston = require.main.require('winston');
const buster = require.main?.require('./src/meta').config['cache-buster'];
const winston = require.main?.require('winston');

let metaCache: {
interface MetaCache {
table: MetaData.Table;
aliases: MetaData.Aliases;
ascii: MetaData.Ascii;
asciiPattern: RegExp;
characters: MetaData.Characters;
charPattern: RegExp;
} = null;
}

let metaCache: MetaCache | null = null;
export function clearCache(): void {
metaCache = null;
}

const escapeRegExpChars = (text: string) => text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');

const getTable = async (): Promise<typeof metaCache> => {
async function getTable(): Promise<MetaCache> {
if (metaCache) {
return metaCache;
}
@ -64,7 +66,7 @@ const getTable = async (): Promise<typeof metaCache> => {
};

return metaCache;
};
}

const outsideCode = /(^|<\/code>)([^<]*|<(?!code[^>]*>))*(<code[^>]*>|$)/g;
const outsideElements = /(<[^>]*>)?([^<>]*)/g;
@ -91,10 +93,18 @@ export function setOptions(newOptions: ParseOptions): void {
Object.assign(options, newOptions);
}

export const buildEmoji = (emoji: StoredEmoji, whole: string, returnCharacter = false): string => {
export const buildEmoji = (
emoji: StoredEmoji,
whole: string,
returnCharacter: boolean,
onReplace: (e: StoredEmoji, w: string) => void
): string => {
if (returnCharacter && emoji.character) {
return emoji.character;
return emoji.character || whole;
}

onReplace(emoji, whole);

if (emoji.image) {
const route = `${options.baseUrl}/plugins/nodebb-plugin-emoji/emoji/${emoji.pack}`;
return `<img
@ -114,12 +124,13 @@ export const buildEmoji = (emoji: StoredEmoji, whole: string, returnCharacter =

const replaceAscii = (
str: string,
{ ascii, asciiPattern, table }: (typeof metaCache),
returnCharacter = false
{ ascii, asciiPattern, table }: MetaCache,
returnCharacter: boolean,
onReplace: (e: StoredEmoji, w: string) => void
) => str.replace(asciiPattern, (full: string, before: string, text: string) => {
const emoji = ascii[text] && table[ascii[text]];
if (emoji) {
return `${before}${buildEmoji(emoji, text, returnCharacter)}`;
return `${before}${buildEmoji(emoji, text, returnCharacter, onReplace)}`;
}

return full;
@ -127,17 +138,23 @@ const replaceAscii = (

const replaceNative = (
str: string,
{ characters, charPattern, table }: (typeof metaCache)
{ characters, charPattern, table }: MetaCache,
onReplace: (e: StoredEmoji, w: string) => void
) => str.replace(charPattern, (char: string) => {
const name = characters[char];
if (table[name]) {
return `:${name}:`;
const emoji = table[name];
if (emoji) {
return buildEmoji(emoji, char, false, onReplace);
}

return char;
});

const parse = async (content: string, returnCharacter = false): Promise<string> => {
async function parse(
content: string,
returnCharacter = false,
onReplace: (e: StoredEmoji, w: string) => void = () => {}
): Promise<string> {
if (!content) {
return content;
}
@ -155,14 +172,14 @@ const parse = async (content: string, returnCharacter = false): Promise<string>
outsideCodeStr => outsideCodeStr.replace(outsideElements, (_, inside, outside) => {
let output = outside;

if (options.native) {
if (options.native && !returnCharacter) {
// avoid parsing native inside HTML tags
// also avoid converting ascii characters
output = output.replace(
/(<[^>]+>)|([^0-9a-zA-Z`~!@#$%^&*()\-=_+{}|[\]\\:";'<>?,./\s\n]+)/g,
(full: string, tag: string, text: string) => {
if (text) {
return replaceNative(text, store);
return replaceNative(text, store, onReplace);
}

return full;
@ -175,19 +192,19 @@ const parse = async (content: string, returnCharacter = false): Promise<string>
const emoji = table[name] || table[aliases[name]];

if (emoji) {
return buildEmoji(emoji, whole, returnCharacter);
return buildEmoji(emoji, whole, returnCharacter, onReplace);
}

return whole;
});

if (options.ascii) {
// avoid parsing native inside HTML tags
// avoid parsing ascii inside HTML tags
output = output.replace(
/(<[^>]+>)|([^<]+)/g,
(full: string, tag: string, text: string) => {
if (text) {
return replaceAscii(text, store, returnCharacter);
return replaceAscii(text, store, returnCharacter, onReplace);
}

return full;
@ -200,13 +217,17 @@ const parse = async (content: string, returnCharacter = false): Promise<string>
);

return parsed;
};
}

export function raw(content: string): Promise<string> {
return parse(content);
}

export async function post(data: { postData: { content: string } }): Promise<any> {
export async function post(data: { postData: { content: string } }, type: string): Promise<any> {
if (type === 'activitypub.note') {
return data;
}

// eslint-disable-next-line no-param-reassign
data.postData.content = await parse(data.postData.content);
return data;
@ -280,3 +301,57 @@ export async function email(
}
return data;
}

let mimeModule: typeof import('mime') | null = null;
async function importMime(): Promise<typeof import('mime').default> {
if (!mimeModule) {
mimeModule = await import('mime');
}
return mimeModule.default;
}

export async function activitypubNote(data: {
object: {
source: { content: string },
'@context'?: { 'Emoji'?: string },
tag: {
id: string;
type: 'Emoji';
name: string;
icon: {
type: 'Image';
mediaType: string;
url: string;
}
}[]
},
post: unknown
}): Promise<any> {
const mime = await importMime();

/* eslint-disable no-param-reassign */
data.object['@context'] = data.object['@context'] || {};
data.object['@context'].Emoji = 'http://joinmastodon.org/ns#Emoji';

data.object.tag = data.object.tag || [];

await parse(data.object.source.content, false, (emoji, whole) => {
if (!emoji.image) {
return;
}

const route = `${options.baseUrl}/plugins/nodebb-plugin-emoji/emoji/${emoji.pack}`;
data.object.tag.push({
id: emoji.name,
type: 'Emoji',
name: whole,
icon: {
type: 'Image',
mediaType: mime.getType(emoji.image) || '',
url: `${route}/${emoji.image}?${buster}`,
},
});
});

return data;
}

View file

@ -2,9 +2,9 @@ import * as os from 'os';

import buildAssets from './build';

const nconf = require.main.require('nconf');
const winston = require.main.require('winston');
const pubsub = require.main.require('./src/pubsub');
const nconf = require.main?.require('nconf');
const winston = require.main?.require('winston');
const pubsub = require.main?.require('./src/pubsub');

const primary = nconf.get('isPrimary') === 'true' || nconf.get('isPrimary') === true;
const hostname = os.hostname();

View file

@ -3,7 +3,7 @@ const settings: {
set(key: string, value: unknown): Promise<void>;
getOne(key: string, field: string): Promise<unknown>;
setOne(key: string, field: string, value: unknown): Promise<void>;
} = require.main.require('./src/meta').settings;
} = require.main?.require('./src/meta').settings;

const defaults: Settings = {
parseNative: true,

46
lib/types.d.ts vendored
View file

@ -40,7 +40,7 @@ interface Emoji {
categories?: string[];
}

interface EmojiDefinition {
type EmojiDefinition = {
/**
* human-friendly name of this emoji pack
*/
@ -68,34 +68,59 @@ interface EmojiDefinition {
*/
license?: string;

/**
* A map of emoji names to `Emoji`
*/
dictionary: {
[name: string]: Emoji;
};
} & ({
/**
* The mode of this emoji pack.
* `images` for individual image files.
* `sprite` for a single image sprite file.
* `font` for an emoji font.
*/
mode: 'images' | 'sprite' | 'font';
mode: 'images';

/**
* **`images` mode** options
*/
images?: {
images: {
/** Path to the directory where the image files are located */
directory: string;
};
} | {
/**
* The mode of this emoji pack.
* `images` for individual image files.
* `sprite` for a single image sprite file.
* `font` for an emoji font.
*/
mode: 'sprite';

/**
* **`sprite` mode** options
*/
sprite?: {
sprite: {
/** Path to the sprite image file */
file: string;
/** CSS `background-size` */
backgroundSize: string;
};
} | {
/**
* The mode of this emoji pack.
* `images` for individual image files.
* `sprite` for a single image sprite file.
* `font` for an emoji font.
*/
mode: 'font';

/**
* **`font` mode** options
*/
font?: {
font: {
/** Path to the emoji font `.eot` file (for old IE support) */
eot?: string;
/** Path to the emoji font `.ttf` file */
@ -111,20 +136,13 @@ interface EmojiDefinition {
/** CSS `font-family` name */
family: string;
};

/**
* A map of emoji names to one of the following
*/
dictionary: {
[name: string]: Emoji;
};
}
});

declare type NodeBack<T = any> = (err?: Error, ...args: T[]) => void;

interface StoredEmoji {
name: string;
character: string;
character?: string;
image: string;
pack: string;
aliases: string[];