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 6397767ce..68f3aeed1 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 4a66db57b..669f67288 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 dce5b1261..1c802d790 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 c42b863b2..dd6cba9ce 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 d36d1c674..b5e4e008d 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 082f72f1a..3125255c9 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 cbac5b645..e1b8e05c8 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 6a90d5bc1..05d6d1d57 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 000000000..f0d0b5827 --- /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 09246dea4..684e8c3b1 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 000000000..f744d02fe --- /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 000000000..50794a937 --- /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 d49cf5307..be6c1bcd1 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 000000000..5cdbfa43b --- /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 000000000..b18337d27 --- /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 b3ce1c03e..a2c591de2 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 8bc029383..87b6774b2 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 000000000..875af55c1 --- /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); +});