mirror of
https://github.com/paricafe/misskey.git
synced 2024-11-25 00:36:42 -06:00
parent
c48cbd95f6
commit
80b5fda292
18 changed files with 193 additions and 24 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
</div>
|
||||
</header>
|
||||
<div class="text">
|
||||
<misskey-flavored-markdown v-if="note.text" :text="note.text"/>
|
||||
<misskey-flavored-markdown v-if="note.text" :text="note.text" :customEmojis="p.emojis"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -45,7 +45,7 @@
|
|||
<div class="text">
|
||||
<span v-if="p.isHidden" style="opacity: 0.5">%i18n:@private%</span>
|
||||
<span v-if="p.deletedAt" style="opacity: 0.5">%i18n:@deleted%</span>
|
||||
<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i"/>
|
||||
<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i" :customEmojis="p.emojis" />
|
||||
</div>
|
||||
<div class="files" v-if="p.files.length > 0">
|
||||
<mk-media-list :media-list="p.files" :raw="true"/>
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
<div class="text">
|
||||
<span v-if="appearNote.isHidden" style="opacity: 0.5">%i18n:@private%</span>
|
||||
<a class="reply" v-if="appearNote.reply">%fa:reply%</a>
|
||||
<misskey-flavored-markdown v-if="appearNote.text" :text="appearNote.text" :i="$store.state.i" :class="$style.text"/>
|
||||
<misskey-flavored-markdown v-if="appearNote.text" :text="appearNote.text" :i="$store.state.i" :class="$style.text" :customEmojis="appearNote.emojis"/>
|
||||
<a class="rp" v-if="appearNote.renote">RN:</a>
|
||||
</div>
|
||||
<div class="files" v-if="appearNote.files.length > 0">
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<span v-if="note.isHidden" style="opacity: 0.5">%i18n:@private%</span>
|
||||
<span v-if="note.deletedAt" style="opacity: 0.5">%i18n:@deleted%</span>
|
||||
<a class="reply" v-if="note.replyId">%fa:reply%</a>
|
||||
<misskey-flavored-markdown v-if="note.text" :text="note.text" :i="$store.state.i"/>
|
||||
<misskey-flavored-markdown v-if="note.text" :text="note.text" :i="$store.state.i" :customEmojis="note.emojis"/>
|
||||
<a class="rp" v-if="note.renoteId" :href="`/notes/${note.renoteId}`">RN: ...</a>
|
||||
</div>
|
||||
<details v-if="note.files.length > 0">
|
||||
|
|
|
@ -43,7 +43,7 @@
|
|||
<div class="text">
|
||||
<span v-if="p.isHidden" style="opacity: 0.5">(%i18n:@private%)</span>
|
||||
<span v-if="p.deletedAt" style="opacity: 0.5">(%i18n:@deleted%)</span>
|
||||
<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i"/>
|
||||
<misskey-flavored-markdown v-if="p.text" :text="p.text" :i="$store.state.i" :customEmojis="p.emojis"/>
|
||||
</div>
|
||||
<div class="files" v-if="p.files.length > 0">
|
||||
<mk-media-list :media-list="p.files" :raw="true"/>
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
<div class="text">
|
||||
<span v-if="appearNote.isHidden" style="opacity: 0.5">(%i18n:@private%)</span>
|
||||
<a class="reply" v-if="appearNote.reply">%fa:reply%</a>
|
||||
<misskey-flavored-markdown v-if="appearNote.text" :text="appearNote.text" :i="$store.state.i" :class="$style.text"/>
|
||||
<misskey-flavored-markdown v-if="appearNote.text" :text="appearNote.text" :i="$store.state.i" :class="$style.text" :customEmojis="appearNote.emojis"/>
|
||||
<a class="rp" v-if="appearNote.renote != null">RN:</a>
|
||||
</div>
|
||||
<div class="files" v-if="appearNote.files.length > 0">
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<span v-if="note.isHidden" style="opacity: 0.5">(%i18n:@private%)</span>
|
||||
<span v-if="note.deletedAt" style="opacity: 0.5">(%i18n:@deleted%)</span>
|
||||
<a class="reply" v-if="note.replyId">%fa:reply%</a>
|
||||
<misskey-flavored-markdown v-if="note.text" :text="note.text" :i="$store.state.i"/>
|
||||
<misskey-flavored-markdown v-if="note.text" :text="note.text" :i="$store.state.i" :customEmojis="note.emojis"/>
|
||||
<a class="rp" v-if="note.renoteId">RN: ...</a>
|
||||
</div>
|
||||
<details v-if="note.files.length > 0">
|
||||
|
|
22
src/models/emoji.ts
Normal file
22
src/models/emoji.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import db from '../db/mongodb';
|
||||
|
||||
const Emoji = db.get<IEmoji>('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 });
|
||||
};
|
|
@ -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<INote>('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;
|
||||
|
|
6
src/remote/activitypub/misc/get-emoji-names.ts
Normal file
6
src/remote/activitypub/misc/get-emoji-names.ts
Normal file
|
@ -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);
|
||||
}
|
5
src/remote/activitypub/models/icon.ts
Normal file
5
src/remote/activitypub/models/icon.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export type IIcon = {
|
||||
type: string;
|
||||
mediaType?: string;
|
||||
url?: string;
|
||||
};
|
|
@ -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: [],
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
12
src/remote/activitypub/models/tag.ts
Normal file
12
src/remote/activitypub/models/tag.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { IIcon } from "./icon";
|
||||
|
||||
/***
|
||||
* tag (ActivityPub)
|
||||
*/
|
||||
export type ITag = {
|
||||
id: string;
|
||||
type: string;
|
||||
name?: string;
|
||||
updated?: Date;
|
||||
icon?: IIcon;
|
||||
};
|
14
src/remote/activitypub/renderer/emoji.ts
Normal file
14
src/remote/activitypub/renderer/emoji.ts
Normal file
|
@ -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
|
||||
}
|
||||
});
|
|
@ -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<any> {
|
||||
const promisedFiles: Promise<IDriveFile[]> = note.fileIds
|
||||
|
@ -75,10 +79,6 @@ export default async function renderNote(note: INote, dive = true): Promise<any>
|
|||
|
||||
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<any>
|
|||
}).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<any>
|
|||
tag
|
||||
};
|
||||
}
|
||||
|
||||
async function getEmojis(names: string[]): Promise<IEmoji[]> {
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
31
src/tools/add-emoji.ts
Normal file
31
src/tools/add-emoji.ts
Normal file
|
@ -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<any> {
|
||||
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);
|
||||
});
|
Loading…
Reference in a new issue