mirror of
				https://codeberg.org/yeentown/barkey.git
				synced 2025-11-04 07:24:13 +00:00 
			
		
		
		
	リモートのカスタム絵文字リアクションを表示できるように (#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
		
		Reference in a new issue