リモートのカスタム絵文字リアクションを表示できるように (#6239)
* リモートのカスタム絵文字リアクションを表示できるように * AP * DBマイグレーション * ローカルのリアクションの. * fix * fix * fix * space
This commit is contained in:
		
							parent
							
								
									cda1803e59
								
							
						
					
					
						commit
						9b07c5af05
					
				
					 12 changed files with 185 additions and 41 deletions
				
			
		
							
								
								
									
										12
									
								
								migration/1586641139527-remote-reaction.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								migration/1586641139527-remote-reaction.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | |||
| import {MigrationInterface, QueryRunner} from "typeorm"; | ||||
| 
 | ||||
| export class remoteReaction1586641139527 implements MigrationInterface { | ||||
|     name = 'remoteReaction1586641139527' | ||||
|     public async up(queryRunner: QueryRunner): Promise<any> { | ||||
|       await queryRunner.query(`ALTER TABLE "note_reaction" ALTER COLUMN "reaction" TYPE character varying(260)`, undefined); | ||||
|   } | ||||
| 
 | ||||
|   public async down(queryRunner: QueryRunner): Promise<any> { | ||||
|       await queryRunner.query(`ALTER TABLE "note_reaction" ALTER COLUMN "reaction" TYPE character varying(130)`, undefined); | ||||
|   } | ||||
| } | ||||
|  | @ -301,6 +301,14 @@ export default Vue.extend({ | |||
| 				case 'reacted': { | ||||
| 					const reaction = body.reaction; | ||||
| 
 | ||||
| 					if (body.emoji) { | ||||
| 						const emojis = this.appearNote.emojis || []; | ||||
| 						if (!emojis.includes(body.emoji)) { | ||||
| 							emojis.push(body.emoji); | ||||
| 							Vue.set(this.appearNote, 'emojis', emojis); | ||||
| 						} | ||||
| 					} | ||||
| 
 | ||||
| 					if (this.appearNote.reactions == null) { | ||||
| 						Vue.set(this.appearNote, 'reactions', {}); | ||||
| 					} | ||||
|  |  | |||
|  | @ -12,7 +12,7 @@ | |||
| 			<fa :icon="faReply" v-else-if="notification.type === 'reply'"/> | ||||
| 			<fa :icon="faAt" v-else-if="notification.type === 'mention'"/> | ||||
| 			<fa :icon="faQuoteLeft" v-else-if="notification.type === 'quote'"/> | ||||
| 			<x-reaction-icon v-else-if="notification.type === 'reaction'" :reaction="notification.reaction" :no-style="true"/> | ||||
| 			<x-reaction-icon v-else-if="notification.type === 'reaction'" :reaction="notification.reaction" :customEmojis="notification.note.emojis" :no-style="true"/> | ||||
| 		</div> | ||||
| 	</div> | ||||
| 	<div class="tail"> | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| <template> | ||||
| <mk-emoji :emoji="reaction.startsWith(':') ? null : reaction" :name="reaction.startsWith(':') ? reaction.substr(1, reaction.length - 2) : null" :is-reaction="true" :normal="true" :no-style="noStyle"/> | ||||
| <mk-emoji :emoji="reaction.startsWith(':') ? null : reaction" :name="reaction.startsWith(':') ? reaction.substr(1, reaction.length - 2) : null" :customEmojis="customEmojis" :is-reaction="true" :normal="true" :no-style="noStyle"/> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
|  | @ -12,6 +12,10 @@ export default Vue.extend({ | |||
| 			type: String, | ||||
| 			required: true | ||||
| 		}, | ||||
| 		customEmojis: { | ||||
| 			required: false, | ||||
| 			default: () => [] | ||||
| 		}, | ||||
| 		noStyle: { | ||||
| 			type: Boolean, | ||||
| 			required: false, | ||||
|  |  | |||
|  | @ -9,7 +9,7 @@ | |||
| 	ref="reaction" | ||||
| 	v-particle | ||||
| > | ||||
| 	<x-reaction-icon :reaction="reaction" ref="icon"/> | ||||
| 	<x-reaction-icon :reaction="reaction" :customEmojis="note.emojis" ref="icon"/> | ||||
| 	<span>{{ count }}</span> | ||||
| </button> | ||||
| </template> | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| import { emojiRegex } from './emoji-regex'; | ||||
| import { fetchMeta } from './fetch-meta'; | ||||
| import { Emojis } from '../models'; | ||||
| import { toPunyNullable } from './convert-host'; | ||||
| 
 | ||||
| const legacies: Record<string, string> = { | ||||
| 	'like':     '👍', | ||||
|  | @ -40,12 +41,20 @@ export function convertLegacyReactions(reactions: Record<string, number>) { | |||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return _reactions; | ||||
| 	const _reactions2 = {} as Record<string, number>; | ||||
| 
 | ||||
| 	for (const reaction of Object.keys(_reactions)) { | ||||
| 		_reactions2[decodeReaction(reaction).reaction] = _reactions[reaction]; | ||||
| 	} | ||||
| 
 | ||||
| 	return _reactions2; | ||||
| } | ||||
| 
 | ||||
| export async function toDbReaction(reaction?: string | null): Promise<string> { | ||||
| export async function toDbReaction(reaction?: string | null, reacterHost?: string | null): Promise<string> { | ||||
| 	if (reaction == null) return await getFallbackReaction(); | ||||
| 
 | ||||
| 	reacterHost = toPunyNullable(reacterHost); | ||||
| 
 | ||||
| 	// 文字列タイプのリアクションを絵文字に変換
 | ||||
| 	if (Object.keys(legacies).includes(reaction)) return legacies[reaction]; | ||||
| 
 | ||||
|  | @ -61,18 +70,58 @@ export async function toDbReaction(reaction?: string | null): Promise<string> { | |||
| 
 | ||||
| 	const custom = reaction.match(/^:([\w+-]+):$/); | ||||
| 	if (custom) { | ||||
| 		const name = custom[1]; | ||||
| 		const emoji = await Emojis.findOne({ | ||||
| 			host: null, | ||||
| 			name: custom[1], | ||||
| 			host: reacterHost || null, | ||||
| 			name, | ||||
| 		}); | ||||
| 
 | ||||
| 		if (emoji) return reaction; | ||||
| 		if (emoji) return reacterHost ? `:${name}@${reacterHost}:` : `:${name}:` | ||||
| 	} | ||||
| 
 | ||||
| 	return await getFallbackReaction(); | ||||
| } | ||||
| 
 | ||||
| type DecodedReaction = { | ||||
| 	/** | ||||
| 	 * リアクション名 (Unicode Emoji or ':name@hostname' or ':name@.') | ||||
| 	 */ | ||||
| 	reaction: string; | ||||
| 
 | ||||
| 	/** | ||||
| 	 * name (カスタム絵文字の場合name, Emojiクエリに使う) | ||||
| 	 */ | ||||
| 	name?: string; | ||||
| 
 | ||||
| 	/** | ||||
| 	 * host (カスタム絵文字の場合host, Emojiクエリに使う) | ||||
| 	 */ | ||||
| 	host?: string | null; | ||||
| }; | ||||
| 
 | ||||
| export function decodeReaction(str: string): DecodedReaction { | ||||
| 	const custom = str.match(/^:([\w+-]+)(?:@([\w.-]+))?:$/); | ||||
| 
 | ||||
| 	if (custom) { | ||||
| 		const name = custom[1]; | ||||
| 		const host = custom[2] || null; | ||||
| 
 | ||||
| 		return { | ||||
| 			reaction: `:${name}@${host || '.'}:`,	// ローカル分は@以降を省略するのではなく.にする
 | ||||
| 			name, | ||||
| 			host | ||||
| 		}; | ||||
| 	} | ||||
| 
 | ||||
| 	return { | ||||
| 		reaction: str, | ||||
| 		name: undefined, | ||||
| 		host: undefined | ||||
| 	}; | ||||
| } | ||||
| 
 | ||||
| export function convertLegacyReaction(reaction: string): string { | ||||
| 	reaction = decodeReaction(reaction).reaction; | ||||
| 	if (Object.keys(legacies).includes(reaction)) return legacies[reaction]; | ||||
| 	return reaction; | ||||
| } | ||||
|  |  | |||
|  | @ -36,7 +36,7 @@ export class NoteReaction { | |||
| 	public note: Note | null; | ||||
| 
 | ||||
| 	@Column('varchar', { | ||||
| 		length: 130 | ||||
| 		length: 260 | ||||
| 	}) | ||||
| 	public reaction: string; | ||||
| } | ||||
|  |  | |||
|  | @ -5,9 +5,11 @@ import { Emojis, Users, PollVotes, DriveFiles, NoteReactions, Followings, Polls | |||
| import { ensure } from '../../prelude/ensure'; | ||||
| import { SchemaType } from '../../misc/schema'; | ||||
| import { awaitAll } from '../../prelude/await-all'; | ||||
| import { convertLegacyReaction, convertLegacyReactions } from '../../misc/reaction-lib'; | ||||
| import { convertLegacyReaction, convertLegacyReactions, decodeReaction } from '../../misc/reaction-lib'; | ||||
| import { toString } from '../../mfm/toString'; | ||||
| import { parse } from '../../mfm/parse'; | ||||
| import { Emoji } from '../entities/emoji'; | ||||
| import { concat } from '../../prelude/array'; | ||||
| 
 | ||||
| export type PackedNote = SchemaType<typeof packedNoteSchema>; | ||||
| 
 | ||||
|  | @ -129,31 +131,61 @@ export class NoteRepository extends Repository<Note> { | |||
| 			}; | ||||
| 		} | ||||
| 
 | ||||
| 		/** | ||||
| 		 * 添付用emojisを解決する | ||||
| 		 * @param emojiNames Note等に添付されたカスタム絵文字名 (:は含めない) | ||||
| 		 * @param noteUserHost Noteのホスト | ||||
| 		 * @param reactionNames Note等にリアクションされたカスタム絵文字名 (:は含めない) | ||||
| 		 */ | ||||
| 		async function populateEmojis(emojiNames: string[], noteUserHost: string | null, reactionNames: string[]) { | ||||
| 			const where = [] as {}[]; | ||||
| 			let all = [] as { | ||||
| 				name: string, | ||||
| 				url: string | ||||
| 			}[]; | ||||
| 
 | ||||
| 			// カスタム絵文字
 | ||||
| 			if (emojiNames?.length > 0) { | ||||
| 				where.push({ | ||||
| 				const tmp = await Emojis.find({ | ||||
| 					where: { | ||||
| 						name: In(emojiNames), | ||||
| 						host: noteUserHost | ||||
| 				}); | ||||
| 					}, | ||||
| 					select: ['name', 'host', 'url'] | ||||
| 				}).then(emojis => emojis.map((emoji: Emoji) => { | ||||
| 					return { | ||||
| 						name: emoji.name, | ||||
| 						url: emoji.url, | ||||
| 					}; | ||||
| 				})); | ||||
| 
 | ||||
| 				all = concat([all, tmp]); | ||||
| 			} | ||||
| 
 | ||||
| 			reactionNames = reactionNames?.filter(x => x.match(/^:[^:]+:$/)).map(x => x.replace(/:/g, '')); | ||||
| 			const customReactions = reactionNames?.map(x => decodeReaction(x)).filter(x => x.name); | ||||
| 
 | ||||
| 			if (reactionNames?.length > 0) { | ||||
| 			if (customReactions?.length > 0) { | ||||
| 				const where = [] as {}[]; | ||||
| 
 | ||||
| 				for (const customReaction of customReactions) { | ||||
| 					where.push({ | ||||
| 					name: In(reactionNames), | ||||
| 					host: null | ||||
| 						name: customReaction.name, | ||||
| 						host: customReaction.host | ||||
| 					}); | ||||
| 				} | ||||
| 
 | ||||
| 			if (where.length === 0) return []; | ||||
| 
 | ||||
| 			return Emojis.find({ | ||||
| 				const tmp = await Emojis.find({ | ||||
| 					where, | ||||
| 				select: ['name', 'host', 'url', 'aliases'] | ||||
| 			}); | ||||
| 					select: ['name', 'host', 'url'] | ||||
| 				}).then(emojis => emojis.map((emoji: Emoji) => { | ||||
| 					return { | ||||
| 						name: `${emoji.name}@${emoji.host || '.'}`,	// @host付きでローカルは.
 | ||||
| 						url: emoji.url, | ||||
| 					}; | ||||
| 				})); | ||||
| 				all = concat([all, tmp]); | ||||
| 			} | ||||
| 
 | ||||
| 			return all; | ||||
| 		} | ||||
| 
 | ||||
| 		async function populateMyReaction() { | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| import { IRemoteUser } from '../../../models/entities/user'; | ||||
| import { ILike, getApId } from '../type'; | ||||
| import create from '../../../services/note/reaction/create'; | ||||
| import { fetchNote } from '../models/note'; | ||||
| import { fetchNote, extractEmojis } from '../models/note'; | ||||
| 
 | ||||
| export default async (actor: IRemoteUser, activity: ILike) => { | ||||
| 	const targetUri = getApId(activity.object); | ||||
|  | @ -11,6 +11,8 @@ export default async (actor: IRemoteUser, activity: ILike) => { | |||
| 
 | ||||
| 	if (actor.id === note.userId) return `skip: cannot react to my note`; | ||||
| 
 | ||||
| 	await extractEmojis(activity.tag || [], actor.host).catch(() => null); | ||||
| 
 | ||||
| 	await create(actor, note, activity._misskey_reaction || activity.content || activity.name); | ||||
| 	return `ok`; | ||||
| }; | ||||
|  |  | |||
|  | @ -1,12 +1,30 @@ | |||
| import config from '../../../config'; | ||||
| import { NoteReaction } from '../../../models/entities/note-reaction'; | ||||
| import { Note } from '../../../models/entities/note'; | ||||
| import { Emojis } from '../../../models'; | ||||
| import renderEmoji from './emoji'; | ||||
| 
 | ||||
| export const renderLike = (noteReaction: NoteReaction, note: Note) => ({ | ||||
| export const renderLike = async (noteReaction: NoteReaction, note: Note) => { | ||||
| 	const reaction = noteReaction.reaction; | ||||
| 
 | ||||
| 	const object =  { | ||||
| 		type: 'Like', | ||||
| 		id: `${config.url}/likes/${noteReaction.id}`, | ||||
| 		actor: `${config.url}/users/${noteReaction.userId}`, | ||||
| 		object: note.uri ? note.uri : `${config.url}/notes/${noteReaction.noteId}`, | ||||
| 	content: noteReaction.reaction, | ||||
| 	_misskey_reaction: noteReaction.reaction | ||||
| }); | ||||
| 		content: reaction, | ||||
| 		_misskey_reaction: reaction | ||||
| 	} as any; | ||||
| 
 | ||||
| 	if (reaction.startsWith(':')) { | ||||
| 		const name = reaction.replace(/:/g, ''); | ||||
| 		const emoji = await Emojis.findOne({ | ||||
| 			name, | ||||
| 			host: null | ||||
| 		}); | ||||
| 
 | ||||
| 		if (emoji) object.tag = [ renderEmoji(emoji) ]; | ||||
| 	} | ||||
| 
 | ||||
| 	return object; | ||||
| }; | ||||
|  |  | |||
|  | @ -4,10 +4,10 @@ import { renderLike } from '../../../remote/activitypub/renderer/like'; | |||
| import DeliverManager from '../../../remote/activitypub/deliver-manager'; | ||||
| import { renderActivity } from '../../../remote/activitypub/renderer'; | ||||
| import { IdentifiableError } from '../../../misc/identifiable-error'; | ||||
| import { toDbReaction } from '../../../misc/reaction-lib'; | ||||
| import { toDbReaction, decodeReaction } from '../../../misc/reaction-lib'; | ||||
| import { User, IRemoteUser } from '../../../models/entities/user'; | ||||
| import { Note } from '../../../models/entities/note'; | ||||
| import { NoteReactions, Users, NoteWatchings, Notes, UserProfiles } from '../../../models'; | ||||
| import { NoteReactions, Users, NoteWatchings, Notes, UserProfiles, Emojis } from '../../../models'; | ||||
| import { Not } from 'typeorm'; | ||||
| import { perUserReactionsChart } from '../../chart'; | ||||
| import { genId } from '../../../misc/gen-id'; | ||||
|  | @ -20,7 +20,7 @@ export default async (user: User, note: Note, reaction?: string) => { | |||
| 		throw new IdentifiableError('2d8e7297-1873-4c00-8404-792c68d7bef0', 'cannot react to my note'); | ||||
| 	} | ||||
| 
 | ||||
| 	reaction = await toDbReaction(reaction); | ||||
| 	reaction = await toDbReaction(reaction, user.host); | ||||
| 
 | ||||
| 	const exist = await NoteReactions.findOne({ | ||||
| 		noteId: note.id, | ||||
|  | @ -59,8 +59,27 @@ export default async (user: User, note: Note, reaction?: string) => { | |||
| 
 | ||||
| 	perUserReactionsChart.update(user, note); | ||||
| 
 | ||||
| 	// カスタム絵文字リアクションだったら絵文字情報も送る
 | ||||
| 	const decodedReaction = decodeReaction(reaction); | ||||
| 
 | ||||
| 	let emoji = await Emojis.findOne({ | ||||
| 		where: { | ||||
| 			name: decodedReaction.name, | ||||
| 			host: decodedReaction.host | ||||
| 		}, | ||||
| 		select: ['name', 'host', 'url'] | ||||
| 	}); | ||||
| 
 | ||||
| 	if (emoji) { | ||||
| 		emoji = { | ||||
| 			name: emoji.host ? `${emoji.name}@${emoji.host}` : `${emoji.name}`, | ||||
| 			url: emoji.url | ||||
| 		} as any; | ||||
| 	} | ||||
| 
 | ||||
| 	publishNoteStream(note.id, 'reacted', { | ||||
| 		reaction: reaction, | ||||
| 		emoji: emoji, | ||||
| 		userId: user.id | ||||
| 	}); | ||||
| 
 | ||||
|  | @ -96,7 +115,7 @@ export default async (user: User, note: Note, reaction?: string) => { | |||
| 
 | ||||
| 	//#region 配信
 | ||||
| 	if (Users.isLocalUser(user) && !note.localOnly) { | ||||
| 		const content = renderActivity(renderLike(inserted, note)); | ||||
| 		const content = renderActivity(await renderLike(inserted, note)); | ||||
| 		const dm = new DeliverManager(user, content); | ||||
| 		if (note.userHost !== null) { | ||||
| 			const reactee = await Users.findOne(note.userId) | ||||
|  |  | |||
|  | @ -44,7 +44,7 @@ export default async (user: User, note: Note) => { | |||
| 
 | ||||
| 	//#region 配信
 | ||||
| 	if (Users.isLocalUser(user) && !note.localOnly) { | ||||
| 		const content = renderActivity(renderUndo(renderLike(exist, note), user)); | ||||
| 		const content = renderActivity(renderUndo(await renderLike(exist, note), user)); | ||||
| 		const dm = new DeliverManager(user, content); | ||||
| 		if (note.userHost !== null) { | ||||
| 			const reactee = await Users.findOne(note.userId) | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue