From 80b5fda292efd70cc749910e3672d50c9a70a72e Mon Sep 17 00:00:00 2001
From: MeiMei <30769358+mei23@users.noreply.github.com>
Date: Fri, 2 Nov 2018 08:59:40 +0900
Subject: [PATCH] Remote custom emojis (#3074)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Remote custom emojis
* んほおおおおお
---
.../components/misskey-flavored-markdown.ts | 26 +++++++------
.../views/components/welcome-timeline.vue | 2 +-
.../desktop/views/components/note-detail.vue | 2 +-
.../app/desktop/views/components/note.vue | 2 +-
.../views/components/sub-note-content.vue | 2 +-
.../mobile/views/components/note-detail.vue | 2 +-
.../app/mobile/views/components/note.vue | 2 +-
.../views/components/sub-note-content.vue | 2 +-
src/models/emoji.ts | 22 +++++++++++
src/models/note.ts | 6 +++
.../activitypub/misc/get-emoji-names.ts | 6 +++
src/remote/activitypub/models/icon.ts | 5 +++
src/remote/activitypub/models/note.ts | 39 +++++++++++++++++++
src/remote/activitypub/models/tag.ts | 12 ++++++
src/remote/activitypub/renderer/emoji.ts | 14 +++++++
src/remote/activitypub/renderer/note.ts | 37 +++++++++++++++---
src/server/api/endpoints/meta.ts | 5 ++-
src/tools/add-emoji.ts | 31 +++++++++++++++
18 files changed, 193 insertions(+), 24 deletions(-)
create mode 100644 src/models/emoji.ts
create mode 100644 src/remote/activitypub/misc/get-emoji-names.ts
create mode 100644 src/remote/activitypub/models/icon.ts
create mode 100644 src/remote/activitypub/models/tag.ts
create mode 100644 src/remote/activitypub/renderer/emoji.ts
create mode 100644 src/tools/add-emoji.ts
diff --git a/src/client/app/common/views/components/misskey-flavored-markdown.ts b/src/client/app/common/views/components/misskey-flavored-markdown.ts
index 6397767cec..68f3aeed1f 100644
--- a/src/client/app/common/views/components/misskey-flavored-markdown.ts
+++ b/src/client/app/common/views/components/misskey-flavored-markdown.ts
@@ -24,6 +24,9 @@ export default Vue.component('misskey-flavored-markdown', {
i: {
type: Object,
default: null
+ },
+ customEmojis: {
+ required: false,
}
},
@@ -186,17 +189,18 @@ export default Vue.component('misskey-flavored-markdown', {
case 'emoji': {
//#region カスタム絵文字
- const customEmojis = (this.os.getMetaSync() || { emojis: [] }).emojis || [];
- const customEmoji = customEmojis.find(e => e.name == token.emoji || (e.aliases || []).includes(token.emoji));
- if (customEmoji) {
- return [createElement('img', {
- attrs: {
- src: customEmoji.url,
- alt: token.emoji,
- title: token.emoji,
- style: 'height: 2.5em; vertical-align: middle;'
- }
- })];
+ if (this.customEmojis != null) {
+ const customEmoji = this.customEmojis.find(e => e.name == token.emoji || (e.aliases || []).includes(token.emoji));
+ if (customEmoji) {
+ return [createElement('img', {
+ attrs: {
+ src: customEmoji.url,
+ alt: token.emoji,
+ title: token.emoji,
+ style: 'height: 2.5em; vertical-align: middle;'
+ }
+ })];
+ }
}
//#endregion
diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue
index 4a66db57b8..669f67288b 100644
--- a/src/client/app/common/views/components/welcome-timeline.vue
+++ b/src/client/app/common/views/components/welcome-timeline.vue
@@ -14,7 +14,7 @@
-
+
diff --git a/src/client/app/desktop/views/components/note-detail.vue b/src/client/app/desktop/views/components/note-detail.vue
index dce5b12615..1c802d790c 100644
--- a/src/client/app/desktop/views/components/note-detail.vue
+++ b/src/client/app/desktop/views/components/note-detail.vue
@@ -45,7 +45,7 @@
%i18n:@private%
%i18n:@deleted%
-
+
diff --git a/src/client/app/desktop/views/components/note.vue b/src/client/app/desktop/views/components/note.vue
index c42b863b2a..dd6cba9ce2 100644
--- a/src/client/app/desktop/views/components/note.vue
+++ b/src/client/app/desktop/views/components/note.vue
@@ -34,7 +34,7 @@
%i18n:@private%
%fa:reply%
-
+
RN:
diff --git a/src/client/app/desktop/views/components/sub-note-content.vue b/src/client/app/desktop/views/components/sub-note-content.vue
index d36d1c6745..b5e4e008dc 100644
--- a/src/client/app/desktop/views/components/sub-note-content.vue
+++ b/src/client/app/desktop/views/components/sub-note-content.vue
@@ -4,7 +4,7 @@
%i18n:@private%
%i18n:@deleted%
%fa:reply%
-
+
RN: ...
diff --git a/src/client/app/mobile/views/components/note-detail.vue b/src/client/app/mobile/views/components/note-detail.vue
index 082f72f1a9..3125255c9e 100644
--- a/src/client/app/mobile/views/components/note-detail.vue
+++ b/src/client/app/mobile/views/components/note-detail.vue
@@ -43,7 +43,7 @@
(%i18n:@private%)
(%i18n:@deleted%)
-
+
diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue
index cbac5b6450..e1b8e05c81 100644
--- a/src/client/app/mobile/views/components/note.vue
+++ b/src/client/app/mobile/views/components/note.vue
@@ -30,7 +30,7 @@
(%i18n:@private%)
%fa:reply%
-
+
RN:
diff --git a/src/client/app/mobile/views/components/sub-note-content.vue b/src/client/app/mobile/views/components/sub-note-content.vue
index 6a90d5bc1a..05d6d1d571 100644
--- a/src/client/app/mobile/views/components/sub-note-content.vue
+++ b/src/client/app/mobile/views/components/sub-note-content.vue
@@ -4,7 +4,7 @@
(%i18n:@private%)
(%i18n:@deleted%)
%fa:reply%
-
+
RN: ...
diff --git a/src/models/emoji.ts b/src/models/emoji.ts
new file mode 100644
index 0000000000..f0d0b58273
--- /dev/null
+++ b/src/models/emoji.ts
@@ -0,0 +1,22 @@
+import db from '../db/mongodb';
+
+const Emoji = db.get('emoji');
+
+Emoji.createIndex(['name', 'host'], { unique: true });
+
+export default Emoji;
+
+export type IEmoji = {
+ name: string;
+ host: string;
+ url: string;
+ aliases?: string[];
+ updatedAt?: Date;
+};
+
+export const packEmojis = async (
+ host: string,
+ // MeiTODO: filter
+) => {
+ return await Emoji.find({ host });
+};
diff --git a/src/models/note.ts b/src/models/note.ts
index 09246dea45..684e8c3b1e 100644
--- a/src/models/note.ts
+++ b/src/models/note.ts
@@ -12,6 +12,7 @@ import { packMany as packFileMany, IDriveFile } from './drive-file';
import Favorite from './favorite';
import Following from './following';
import config from '../config';
+import { packEmojis } from './emoji';
const Note = db.get('notes');
Note.createIndex('uri', { sparse: true, unique: true });
@@ -228,6 +229,11 @@ export const pack = async (
const id = _note._id;
+ // _note._userを消す前か、_note.userを解決した後でないとホストがわからない
+ if (_note._user) {
+ _note.emojis = packEmojis(_note._user.host);
+ }
+
// Rename _id to id
_note.id = _note._id;
delete _note._id;
diff --git a/src/remote/activitypub/misc/get-emoji-names.ts b/src/remote/activitypub/misc/get-emoji-names.ts
new file mode 100644
index 0000000000..f744d02fed
--- /dev/null
+++ b/src/remote/activitypub/misc/get-emoji-names.ts
@@ -0,0 +1,6 @@
+import parse from '../../../mfm/parse';
+
+export default function(text: string) {
+ if (!text) return [];
+ return parse(text).filter(t => t.type === 'emoji').map(t => (t as any).emoji);
+}
diff --git a/src/remote/activitypub/models/icon.ts b/src/remote/activitypub/models/icon.ts
new file mode 100644
index 0000000000..50794a937d
--- /dev/null
+++ b/src/remote/activitypub/models/icon.ts
@@ -0,0 +1,5 @@
+export type IIcon = {
+ type: string;
+ mediaType?: string;
+ url?: string;
+};
diff --git a/src/remote/activitypub/models/note.ts b/src/remote/activitypub/models/note.ts
index d49cf53079..be6c1bcd18 100644
--- a/src/remote/activitypub/models/note.ts
+++ b/src/remote/activitypub/models/note.ts
@@ -10,6 +10,9 @@ import { resolvePerson, updatePerson } from './person';
import { resolveImage } from './image';
import { IRemoteUser, IUser } from '../../../models/user';
import htmlToMFM from '../../../mfm/html-to-mfm';
+import Emoji from '../../../models/emoji';
+import { ITag } from './tag';
+import { toUnicode } from 'punycode';
const log = debug('misskey:activitypub');
@@ -93,6 +96,10 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
// テキストのパース
const text = note._misskey_content ? note._misskey_content : htmlToMFM(note.content);
+ await extractEmojis(note.tag, actor.host).catch(e => {
+ console.log(`extractEmojis: ${e}`);
+ });
+
// ユーザーの情報が古かったらついでに更新しておく
if (actor.updatedAt == null || Date.now() - actor.updatedAt.getTime() > 1000 * 60 * 60 * 24) {
updatePerson(note.attributedTo);
@@ -135,3 +142,35 @@ export async function resolveNote(value: string | IObject, resolver?: Resolver):
// 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。
return await createNote(uri, resolver);
}
+
+async function extractEmojis(tags: ITag[], host_: string) {
+ const host = toUnicode(host_.toLowerCase());
+
+ if (!tags) return [];
+
+ const eomjiTags = tags.filter(tag => tag.type === 'Emoji' && tag.icon && tag.icon.url);
+
+ return await Promise.all(
+ eomjiTags.map(async tag => {
+ const name = tag.name.replace(/^:/, '').replace(/:$/, '');
+
+ const exists = await Emoji.findOne({
+ host,
+ name
+ });
+
+ if (exists) {
+ return exists;
+ }
+
+ log(`register emoji host=${host}, name=${name}`);
+
+ return await Emoji.insert({
+ host,
+ name,
+ url: tag.icon.url,
+ aliases: [],
+ });
+ })
+ );
+}
diff --git a/src/remote/activitypub/models/tag.ts b/src/remote/activitypub/models/tag.ts
new file mode 100644
index 0000000000..5cdbfa43b1
--- /dev/null
+++ b/src/remote/activitypub/models/tag.ts
@@ -0,0 +1,12 @@
+import { IIcon } from "./icon";
+
+/***
+ * tag (ActivityPub)
+ */
+export type ITag = {
+ id: string;
+ type: string;
+ name?: string;
+ updated?: Date;
+ icon?: IIcon;
+};
diff --git a/src/remote/activitypub/renderer/emoji.ts b/src/remote/activitypub/renderer/emoji.ts
new file mode 100644
index 0000000000..b18337d274
--- /dev/null
+++ b/src/remote/activitypub/renderer/emoji.ts
@@ -0,0 +1,14 @@
+import { IEmoji } from '../../../models/emoji';
+import config from '../../../config';
+
+export default (emoji: IEmoji) => ({
+ id: `${config.url}/emojis/${emoji.name}`,
+ type: 'Emoji',
+ name: `:${emoji.name}:`,
+ updated: emoji.updatedAt != null ? emoji.updatedAt.toISOString() : new Date().toISOString,
+ icon: {
+ type: 'Image',
+ mediaType: 'image/png', //Mei-TODO
+ url: emoji.url
+ }
+});
diff --git a/src/remote/activitypub/renderer/note.ts b/src/remote/activitypub/renderer/note.ts
index b3ce1c03e4..a2c591de2e 100644
--- a/src/remote/activitypub/renderer/note.ts
+++ b/src/remote/activitypub/renderer/note.ts
@@ -1,12 +1,16 @@
import renderDocument from './document';
import renderHashtag from './hashtag';
import renderMention from './mention';
+import renderEmoji from './emoji';
import config from '../../../config';
import DriveFile, { IDriveFile } from '../../../models/drive-file';
import Note, { INote } from '../../../models/note';
import User from '../../../models/user';
import toHtml from '../misc/get-note-html';
import parseMfm from '../../../mfm/parse';
+import getEmojiNames from '../misc/get-emoji-names';
+import Emoji, { IEmoji } from '../../../models/emoji';
+import { unique } from '../../../prelude/array';
export default async function renderNote(note: INote, dive = true): Promise {
const promisedFiles: Promise = note.fileIds
@@ -75,10 +79,6 @@ export default async function renderNote(note: INote, dive = true): Promise
const hashtagTags = (note.tags || []).map(tag => renderHashtag(tag));
const mentionTags = mentionedUsers.map(u => renderMention(u));
- const tag = [
- ...hashtagTags,
- ...mentionTags,
- ];
const files = await promisedFiles;
@@ -108,12 +108,24 @@ export default async function renderNote(note: INote, dive = true): Promise
}).join('');
}
+ const content = toHtml(Object.assign({}, note, { text }));
+
+ const emojiNames = unique(getEmojiNames(content));
+ const emojis = await getEmojis(emojiNames);
+ const apemojis = emojis.map(emoji => renderEmoji(emoji));
+
+ const tag = [
+ ...hashtagTags,
+ ...mentionTags,
+ ...apemojis,
+ ];
+
return {
id: `${config.url}/notes/${note._id}`,
type: 'Note',
attributedTo,
summary: note.cw,
- content: toHtml(Object.assign({}, note, { text })),
+ content,
_misskey_content: text,
published: note.createdAt.toISOString(),
to,
@@ -124,3 +136,18 @@ export default async function renderNote(note: INote, dive = true): Promise
tag
};
}
+
+async function getEmojis(names: string[]): Promise {
+ if (names == null || names.length < 1) return [];
+
+ const emojis = await Promise.all(
+ names.map(async name => {
+ return await Emoji.findOne({
+ name,
+ host: null
+ });
+ })
+ );
+
+ return emojis.filter(emoji => emoji != null);
+}
diff --git a/src/server/api/endpoints/meta.ts b/src/server/api/endpoints/meta.ts
index 8bc0293832..87b6774b23 100644
--- a/src/server/api/endpoints/meta.ts
+++ b/src/server/api/endpoints/meta.ts
@@ -2,6 +2,7 @@ import * as os from 'os';
import config from '../../../config';
import Meta from '../../../models/meta';
import { ILocalUser } from '../../../models/user';
+import Emoji from '../../../models/emoji';
const pkg = require('../../../../package.json');
const client = require('../../../../built/client/meta.json');
@@ -22,6 +23,8 @@ export const meta = {
export default (params: any, me: ILocalUser) => new Promise(async (res, rej) => {
const meta: any = (await Meta.findOne()) || {};
+ const emojis = await Emoji.find({ host: null });
+
res({
maintainer: config.maintainer,
@@ -50,7 +53,7 @@ export default (params: any, me: ILocalUser) => new Promise(async (res, rej) =>
hidedTags: (me && me.isAdmin) ? meta.hidedTags : undefined,
bannerUrl: meta.bannerUrl,
maxNoteTextLength: config.maxNoteTextLength,
- emojis: meta.emojis,
+ emojis: emojis,
features: {
registration: !meta.disableRegistration,
diff --git a/src/tools/add-emoji.ts b/src/tools/add-emoji.ts
new file mode 100644
index 0000000000..875af55c14
--- /dev/null
+++ b/src/tools/add-emoji.ts
@@ -0,0 +1,31 @@
+import * as debug from 'debug';
+import Emoji from "../models/emoji";
+
+debug.enable('*');
+
+async function main(name: string, url: string, alias?: string): Promise {
+ const aliases = alias != null ? [ alias ] : [];
+
+ await Emoji.insert({
+ host: null,
+ name,
+ url,
+ aliases,
+ updatedAt: new Date()
+ });
+}
+
+const args = process.argv.slice(2);
+const name = args[0];
+const url = args[1];
+
+if (!name) throw 'require name';
+if (!url) throw 'require url';
+
+main(name, url).then(() => {
+ console.log('success');
+ process.exit(0);
+}).catch(e => {
+ console.warn(e);
+ process.exit(1);
+});