2023-07-27 05:31:52 +00:00
|
|
|
|
/*
|
2024-02-13 15:59:27 +00:00
|
|
|
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
2023-07-27 05:31:52 +00:00
|
|
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
|
*/
|
|
|
|
|
|
2022-09-17 18:27:08 +00:00
|
|
|
|
import { Inject, Injectable } from '@nestjs/common';
|
2023-10-06 05:24:25 +00:00
|
|
|
|
import * as Redis from 'ioredis';
|
2022-09-17 18:27:08 +00:00
|
|
|
|
import { DI } from '@/di-symbols.js';
|
2023-12-27 22:53:37 +00:00
|
|
|
|
import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository, NoteThreadMutingsRepository } from '@/models/_.js';
|
2022-09-17 18:27:08 +00:00
|
|
|
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
2023-09-20 02:33:36 +00:00
|
|
|
|
import type { MiRemoteUser, MiUser } from '@/models/User.js';
|
|
|
|
|
import type { MiNote } from '@/models/Note.js';
|
2022-09-17 18:27:08 +00:00
|
|
|
|
import { IdService } from '@/core/IdService.js';
|
2023-09-20 02:33:36 +00:00
|
|
|
|
import type { MiNoteReaction } from '@/models/NoteReaction.js';
|
2022-09-17 18:27:08 +00:00
|
|
|
|
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
|
|
|
|
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
2023-03-16 05:24:11 +00:00
|
|
|
|
import { NotificationService } from '@/core/NotificationService.js';
|
2022-09-17 18:27:08 +00:00
|
|
|
|
import PerUserReactionsChart from '@/core/chart/charts/per-user-reactions.js';
|
|
|
|
|
import { emojiRegex } from '@/misc/emoji-regex.js';
|
2022-12-04 01:16:03 +00:00
|
|
|
|
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
|
|
|
|
|
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
|
|
|
|
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
|
|
|
|
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
|
|
|
|
import { MetaService } from '@/core/MetaService.js';
|
2022-12-04 08:05:32 +00:00
|
|
|
|
import { bindThis } from '@/decorators.js';
|
2023-02-04 03:40:40 +00:00
|
|
|
|
import { UtilityService } from '@/core/UtilityService.js';
|
|
|
|
|
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
2023-04-06 02:14:43 +00:00
|
|
|
|
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
2023-05-18 09:45:49 +00:00
|
|
|
|
import { RoleService } from '@/core/RoleService.js';
|
2023-10-06 05:24:25 +00:00
|
|
|
|
import { FeaturedService } from '@/core/FeaturedService.js';
|
2024-01-08 03:28:13 +00:00
|
|
|
|
import { trackPromise } from '@/misc/promise-tracker.js';
|
2022-09-17 18:27:08 +00:00
|
|
|
|
|
2024-02-16 05:25:48 +00:00
|
|
|
|
const FALLBACK = '\u2764';
|
2023-10-19 02:19:42 +00:00
|
|
|
|
const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
|
2023-03-24 09:55:31 +00:00
|
|
|
|
|
2022-09-17 18:27:08 +00:00
|
|
|
|
const legacies: Record<string, string> = {
|
|
|
|
|
'like': '👍',
|
2024-02-16 05:25:48 +00:00
|
|
|
|
'love': '\u2764', // ハート、異体字セレクタを入れない
|
2022-09-17 18:27:08 +00:00
|
|
|
|
'laugh': '😆',
|
|
|
|
|
'hmm': '🤔',
|
|
|
|
|
'surprise': '😮',
|
|
|
|
|
'congrats': '🎉',
|
|
|
|
|
'angry': '💢',
|
|
|
|
|
'confused': '😥',
|
|
|
|
|
'rip': '😇',
|
|
|
|
|
'pudding': '🍮',
|
|
|
|
|
'star': '⭐',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type DecodedReaction = {
|
|
|
|
|
/**
|
|
|
|
|
* リアクション名 (Unicode Emoji or ':name@hostname' or ':name@.')
|
|
|
|
|
*/
|
|
|
|
|
reaction: string;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* name (カスタム絵文字の場合name, Emojiクエリに使う)
|
|
|
|
|
*/
|
|
|
|
|
name?: string;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* host (カスタム絵文字の場合host, Emojiクエリに使う)
|
|
|
|
|
*/
|
|
|
|
|
host?: string | null;
|
|
|
|
|
};
|
|
|
|
|
|
2023-05-18 09:18:25 +00:00
|
|
|
|
const isCustomEmojiRegexp = /^:([\w+-]+)(?:@\.)?:$/;
|
|
|
|
|
const decodeCustomEmojiRegexp = /^:([\w+-]+)(?:@([\w.-]+))?:$/;
|
|
|
|
|
|
2022-09-17 18:27:08 +00:00
|
|
|
|
@Injectable()
|
|
|
|
|
export class ReactionService {
|
|
|
|
|
constructor(
|
2023-10-06 05:24:25 +00:00
|
|
|
|
@Inject(DI.redis)
|
|
|
|
|
private redisClient: Redis.Redis,
|
|
|
|
|
|
2022-09-17 18:27:08 +00:00
|
|
|
|
@Inject(DI.usersRepository)
|
|
|
|
|
private usersRepository: UsersRepository,
|
|
|
|
|
|
|
|
|
|
@Inject(DI.notesRepository)
|
|
|
|
|
private notesRepository: NotesRepository,
|
|
|
|
|
|
|
|
|
|
@Inject(DI.noteReactionsRepository)
|
|
|
|
|
private noteReactionsRepository: NoteReactionsRepository,
|
|
|
|
|
|
2023-12-27 22:53:37 +00:00
|
|
|
|
@Inject(DI.noteThreadMutingsRepository)
|
|
|
|
|
private noteThreadMutingsRepository: NoteThreadMutingsRepository,
|
|
|
|
|
|
2022-09-17 18:27:08 +00:00
|
|
|
|
@Inject(DI.emojisRepository)
|
|
|
|
|
private emojisRepository: EmojisRepository,
|
|
|
|
|
|
|
|
|
|
private utilityService: UtilityService,
|
|
|
|
|
private metaService: MetaService,
|
2023-04-06 02:14:43 +00:00
|
|
|
|
private customEmojiService: CustomEmojiService,
|
2023-05-18 09:45:49 +00:00
|
|
|
|
private roleService: RoleService,
|
2022-09-17 18:27:08 +00:00
|
|
|
|
private userEntityService: UserEntityService,
|
|
|
|
|
private noteEntityService: NoteEntityService,
|
2023-02-04 03:40:40 +00:00
|
|
|
|
private userBlockingService: UserBlockingService,
|
2022-09-17 18:27:08 +00:00
|
|
|
|
private idService: IdService,
|
2023-10-06 05:24:25 +00:00
|
|
|
|
private featuredService: FeaturedService,
|
2023-02-04 01:02:03 +00:00
|
|
|
|
private globalEventService: GlobalEventService,
|
2022-09-17 18:27:08 +00:00
|
|
|
|
private apRendererService: ApRendererService,
|
|
|
|
|
private apDeliverManagerService: ApDeliverManagerService,
|
2023-03-16 05:24:11 +00:00
|
|
|
|
private notificationService: NotificationService,
|
2022-09-17 18:27:08 +00:00
|
|
|
|
private perUserReactionsChart: PerUserReactionsChart,
|
|
|
|
|
) {
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-04 06:03:09 +00:00
|
|
|
|
@bindThis
|
2023-08-16 08:51:28 +00:00
|
|
|
|
public async create(user: { id: MiUser['id']; host: MiUser['host']; isBot: MiUser['isBot'] }, note: MiNote, _reaction?: string | null) {
|
2022-09-17 18:27:08 +00:00
|
|
|
|
// Check blocking
|
|
|
|
|
if (note.userId !== user.id) {
|
2023-02-04 03:40:40 +00:00
|
|
|
|
const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id);
|
|
|
|
|
if (blocked) {
|
2022-09-17 18:27:08 +00:00
|
|
|
|
throw new IdentifiableError('e70412a4-7197-4726-8e74-f3e0deb92aa7');
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-03-16 05:24:11 +00:00
|
|
|
|
|
2022-09-17 18:27:08 +00:00
|
|
|
|
// check visibility
|
|
|
|
|
if (!await this.noteEntityService.isVisibleForMe(note, user.id)) {
|
|
|
|
|
throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.');
|
|
|
|
|
}
|
2023-03-16 05:24:11 +00:00
|
|
|
|
|
2023-05-18 09:45:49 +00:00
|
|
|
|
let reaction = _reaction ?? FALLBACK;
|
|
|
|
|
|
2023-05-19 00:43:38 +00:00
|
|
|
|
if (note.reactionAcceptance === 'likeOnly' || ((note.reactionAcceptance === 'likeOnlyForRemote' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && (user.host != null))) {
|
2024-02-16 05:25:48 +00:00
|
|
|
|
reaction = '\u2764';
|
2023-05-18 09:45:49 +00:00
|
|
|
|
} else if (_reaction) {
|
|
|
|
|
const custom = reaction.match(isCustomEmojiRegexp);
|
|
|
|
|
if (custom) {
|
|
|
|
|
const reacterHost = this.utilityService.toPunyNullable(user.host);
|
|
|
|
|
|
|
|
|
|
const name = custom[1];
|
|
|
|
|
const emoji = reacterHost == null
|
|
|
|
|
? (await this.customEmojiService.localEmojisCache.fetch()).get(name)
|
|
|
|
|
: await this.emojisRepository.findOneBy({
|
|
|
|
|
host: reacterHost,
|
|
|
|
|
name,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (emoji) {
|
|
|
|
|
if (emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0 || (await this.roleService.getUserRoles(user.id)).some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.includes(r.id))) {
|
|
|
|
|
reaction = reacterHost ? `:${name}@${reacterHost}:` : `:${name}:`;
|
2023-05-19 00:43:38 +00:00
|
|
|
|
|
|
|
|
|
// センシティブ
|
2023-12-26 09:42:37 +00:00
|
|
|
|
if ((note.reactionAcceptance === 'nonSensitiveOnly' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote') && emoji.isSensitive) {
|
2023-05-19 00:43:38 +00:00
|
|
|
|
reaction = FALLBACK;
|
|
|
|
|
}
|
2023-05-18 09:45:49 +00:00
|
|
|
|
} else {
|
|
|
|
|
// リアクションとして使う権限がない
|
|
|
|
|
reaction = FALLBACK;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
reaction = FALLBACK;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2023-10-18 00:54:18 +00:00
|
|
|
|
reaction = this.normalize(reaction);
|
2023-05-18 09:45:49 +00:00
|
|
|
|
}
|
2023-03-07 23:56:47 +00:00
|
|
|
|
}
|
2023-03-16 05:24:11 +00:00
|
|
|
|
|
2023-08-16 08:51:28 +00:00
|
|
|
|
const record: MiNoteReaction = {
|
2023-10-16 01:45:22 +00:00
|
|
|
|
id: this.idService.gen(),
|
2022-09-17 18:27:08 +00:00
|
|
|
|
noteId: note.id,
|
|
|
|
|
userId: user.id,
|
|
|
|
|
reaction,
|
|
|
|
|
};
|
2023-03-16 05:24:11 +00:00
|
|
|
|
|
2022-09-17 18:27:08 +00:00
|
|
|
|
// Create reaction
|
|
|
|
|
try {
|
|
|
|
|
await this.noteReactionsRepository.insert(record);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
if (isDuplicateKeyValueError(e)) {
|
|
|
|
|
const exists = await this.noteReactionsRepository.findOneByOrFail({
|
|
|
|
|
noteId: note.id,
|
|
|
|
|
userId: user.id,
|
|
|
|
|
});
|
2023-03-16 05:24:11 +00:00
|
|
|
|
|
2022-09-17 18:27:08 +00:00
|
|
|
|
if (exists.reaction !== reaction) {
|
|
|
|
|
// 別のリアクションがすでにされていたら置き換える
|
|
|
|
|
await this.delete(user, note);
|
|
|
|
|
await this.noteReactionsRepository.insert(record);
|
|
|
|
|
} else {
|
|
|
|
|
// 同じリアクションがすでにされていたらエラー
|
|
|
|
|
throw new IdentifiableError('51c42bb4-931a-456b-bff7-e5a8a70dd298');
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
throw e;
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-03-16 05:24:11 +00:00
|
|
|
|
|
2022-09-17 18:27:08 +00:00
|
|
|
|
// Increment reactions count
|
|
|
|
|
const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`;
|
|
|
|
|
await this.notesRepository.createQueryBuilder().update()
|
|
|
|
|
.set({
|
|
|
|
|
reactions: () => sql,
|
2023-10-19 02:19:42 +00:00
|
|
|
|
...(note.reactionAndUserPairCache.length < PER_NOTE_REACTION_USER_PAIR_CACHE_MAX ? {
|
2023-10-19 02:17:59 +00:00
|
|
|
|
reactionAndUserPairCache: () => `array_append("reactionAndUserPairCache", '${user.id}/${reaction}')`,
|
2023-10-19 00:20:19 +00:00
|
|
|
|
} : {}),
|
2022-09-17 18:27:08 +00:00
|
|
|
|
})
|
|
|
|
|
.where('id = :id', { id: note.id })
|
|
|
|
|
.execute();
|
2023-03-16 05:24:11 +00:00
|
|
|
|
|
2023-10-06 22:59:46 +00:00
|
|
|
|
// 30%の確率、セルフではない、3日以内に投稿されたノートの場合ハイライト用ランキング更新
|
|
|
|
|
if (
|
|
|
|
|
Math.random() < 0.3 &&
|
|
|
|
|
note.userId !== user.id &&
|
|
|
|
|
(Date.now() - this.idService.parse(note.id).date.getTime()) < 1000 * 60 * 60 * 24 * 3
|
|
|
|
|
) {
|
2023-10-06 05:24:25 +00:00
|
|
|
|
if (note.channelId != null) {
|
2023-10-06 22:53:14 +00:00
|
|
|
|
if (note.replyId == null) {
|
|
|
|
|
this.featuredService.updateInChannelNotesRanking(note.channelId, note.id, 1);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if (note.visibility === 'public' && note.userHost == null && note.replyId == null) {
|
|
|
|
|
this.featuredService.updateGlobalNotesRanking(note.id, 1);
|
|
|
|
|
this.featuredService.updatePerUserNotesRanking(note.userId, note.id, 1);
|
|
|
|
|
}
|
2023-10-06 05:24:25 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-24 09:48:42 +00:00
|
|
|
|
const meta = await this.metaService.fetch();
|
|
|
|
|
|
|
|
|
|
if (meta.enableChartsForRemoteUser || (user.host == null)) {
|
|
|
|
|
this.perUserReactionsChart.update(user, note);
|
|
|
|
|
}
|
2023-03-16 05:24:11 +00:00
|
|
|
|
|
2022-09-17 18:27:08 +00:00
|
|
|
|
// カスタム絵文字リアクションだったら絵文字情報も送る
|
|
|
|
|
const decodedReaction = this.decodeReaction(reaction);
|
2023-03-16 05:24:11 +00:00
|
|
|
|
|
2023-04-06 02:14:43 +00:00
|
|
|
|
const customEmoji = decodedReaction.name == null ? null : decodedReaction.host == null
|
|
|
|
|
? (await this.customEmojiService.localEmojisCache.fetch()).get(decodedReaction.name)
|
|
|
|
|
: await this.emojisRepository.findOne(
|
|
|
|
|
{
|
|
|
|
|
where: {
|
|
|
|
|
name: decodedReaction.name,
|
|
|
|
|
host: decodedReaction.host,
|
|
|
|
|
},
|
|
|
|
|
});
|
2023-03-16 05:24:11 +00:00
|
|
|
|
|
2023-02-04 01:02:03 +00:00
|
|
|
|
this.globalEventService.publishNoteStream(note.id, 'reacted', {
|
2022-09-17 18:27:08 +00:00
|
|
|
|
reaction: decodedReaction.reaction,
|
2023-04-06 02:14:43 +00:00
|
|
|
|
emoji: customEmoji != null ? {
|
|
|
|
|
name: customEmoji.host ? `${customEmoji.name}@${customEmoji.host}` : `${customEmoji.name}@.`,
|
2022-12-30 13:37:58 +00:00
|
|
|
|
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
2023-04-06 02:14:43 +00:00
|
|
|
|
url: customEmoji.publicUrl || customEmoji.originalUrl,
|
2022-09-17 18:27:08 +00:00
|
|
|
|
} : null,
|
|
|
|
|
userId: user.id,
|
|
|
|
|
});
|
2023-03-16 05:24:11 +00:00
|
|
|
|
|
2022-09-17 18:27:08 +00:00
|
|
|
|
// リアクションされたユーザーがローカルユーザーなら通知を作成
|
|
|
|
|
if (note.userHost === null) {
|
2024-02-09 18:22:06 +00:00
|
|
|
|
const isThreadMuted = await this.noteThreadMutingsRepository.exists({
|
2023-12-27 22:53:37 +00:00
|
|
|
|
where: {
|
|
|
|
|
userId: note.userId,
|
|
|
|
|
threadId: note.threadId ?? note.id,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!isThreadMuted) {
|
|
|
|
|
this.notificationService.createNotification(note.userId, 'reaction', {
|
|
|
|
|
noteId: note.id,
|
|
|
|
|
reaction: reaction,
|
|
|
|
|
}, user.id);
|
|
|
|
|
}
|
2022-09-17 18:27:08 +00:00
|
|
|
|
}
|
2023-03-16 05:24:11 +00:00
|
|
|
|
|
2022-09-17 18:27:08 +00:00
|
|
|
|
//#region 配信
|
|
|
|
|
if (this.userEntityService.isLocalUser(user) && !note.localOnly) {
|
2023-02-12 09:47:30 +00:00
|
|
|
|
const content = this.apRendererService.addContext(await this.apRendererService.renderLike(record, note));
|
2022-09-17 18:27:08 +00:00
|
|
|
|
const dm = this.apDeliverManagerService.createDeliverManager(user, content);
|
|
|
|
|
if (note.userHost !== null) {
|
|
|
|
|
const reactee = await this.usersRepository.findOneBy({ id: note.userId });
|
2023-08-16 08:51:28 +00:00
|
|
|
|
dm.addDirectRecipe(reactee as MiRemoteUser);
|
2022-09-17 18:27:08 +00:00
|
|
|
|
}
|
2023-03-16 05:24:11 +00:00
|
|
|
|
|
2022-09-17 18:27:08 +00:00
|
|
|
|
if (['public', 'home', 'followers'].includes(note.visibility)) {
|
|
|
|
|
dm.addFollowersRecipe();
|
|
|
|
|
} else if (note.visibility === 'specified') {
|
|
|
|
|
const visibleUsers = await Promise.all(note.visibleUserIds.map(id => this.usersRepository.findOneBy({ id })));
|
|
|
|
|
for (const u of visibleUsers.filter(u => u && this.userEntityService.isRemoteUser(u))) {
|
2023-08-16 08:51:28 +00:00
|
|
|
|
dm.addDirectRecipe(u as MiRemoteUser);
|
2022-09-17 18:27:08 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2023-03-16 05:24:11 +00:00
|
|
|
|
|
2024-01-08 03:28:13 +00:00
|
|
|
|
trackPromise(dm.execute());
|
2022-09-17 18:27:08 +00:00
|
|
|
|
}
|
|
|
|
|
//#endregion
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-04 06:03:09 +00:00
|
|
|
|
@bindThis
|
2023-08-16 08:51:28 +00:00
|
|
|
|
public async delete(user: { id: MiUser['id']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote) {
|
2022-09-17 18:27:08 +00:00
|
|
|
|
// if already unreacted
|
|
|
|
|
const exist = await this.noteReactionsRepository.findOneBy({
|
|
|
|
|
noteId: note.id,
|
|
|
|
|
userId: user.id,
|
|
|
|
|
});
|
2023-03-16 05:24:11 +00:00
|
|
|
|
|
2022-09-17 18:27:08 +00:00
|
|
|
|
if (exist == null) {
|
|
|
|
|
throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted');
|
|
|
|
|
}
|
2023-03-16 05:24:11 +00:00
|
|
|
|
|
2022-09-17 18:27:08 +00:00
|
|
|
|
// Delete reaction
|
|
|
|
|
const result = await this.noteReactionsRepository.delete(exist.id);
|
2023-03-16 05:24:11 +00:00
|
|
|
|
|
2022-09-17 18:27:08 +00:00
|
|
|
|
if (result.affected !== 1) {
|
|
|
|
|
throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted');
|
|
|
|
|
}
|
2023-03-16 05:24:11 +00:00
|
|
|
|
|
2022-09-17 18:27:08 +00:00
|
|
|
|
// Decrement reactions count
|
|
|
|
|
const sql = `jsonb_set("reactions", '{${exist.reaction}}', (COALESCE("reactions"->>'${exist.reaction}', '0')::int - 1)::text::jsonb)`;
|
|
|
|
|
await this.notesRepository.createQueryBuilder().update()
|
|
|
|
|
.set({
|
|
|
|
|
reactions: () => sql,
|
2023-10-19 02:17:59 +00:00
|
|
|
|
reactionAndUserPairCache: () => `array_remove("reactionAndUserPairCache", '${user.id}/${exist.reaction}')`,
|
2022-09-17 18:27:08 +00:00
|
|
|
|
})
|
|
|
|
|
.where('id = :id', { id: note.id })
|
|
|
|
|
.execute();
|
2023-03-16 05:24:11 +00:00
|
|
|
|
|
2023-02-04 01:02:03 +00:00
|
|
|
|
this.globalEventService.publishNoteStream(note.id, 'unreacted', {
|
2022-09-17 18:27:08 +00:00
|
|
|
|
reaction: this.decodeReaction(exist.reaction).reaction,
|
|
|
|
|
userId: user.id,
|
|
|
|
|
});
|
2023-03-16 05:24:11 +00:00
|
|
|
|
|
2022-09-17 18:27:08 +00:00
|
|
|
|
//#region 配信
|
|
|
|
|
if (this.userEntityService.isLocalUser(user) && !note.localOnly) {
|
2023-02-12 09:47:30 +00:00
|
|
|
|
const content = this.apRendererService.addContext(this.apRendererService.renderUndo(await this.apRendererService.renderLike(exist, note), user));
|
2022-09-17 18:27:08 +00:00
|
|
|
|
const dm = this.apDeliverManagerService.createDeliverManager(user, content);
|
|
|
|
|
if (note.userHost !== null) {
|
|
|
|
|
const reactee = await this.usersRepository.findOneBy({ id: note.userId });
|
2023-08-16 08:51:28 +00:00
|
|
|
|
dm.addDirectRecipe(reactee as MiRemoteUser);
|
2022-09-17 18:27:08 +00:00
|
|
|
|
}
|
|
|
|
|
dm.addFollowersRecipe();
|
2024-01-08 03:28:13 +00:00
|
|
|
|
trackPromise(dm.execute());
|
2022-09-17 18:27:08 +00:00
|
|
|
|
}
|
|
|
|
|
//#endregion
|
|
|
|
|
}
|
2023-03-16 05:24:11 +00:00
|
|
|
|
|
2024-02-21 05:31:50 +00:00
|
|
|
|
/**
|
|
|
|
|
* 文字列タイプのレガシーな形式のリアクションを現在の形式に変換しつつ、
|
|
|
|
|
* データベース上には存在する「0個のリアクションがついている」という情報を削除する。
|
|
|
|
|
*/
|
2022-12-04 06:03:09 +00:00
|
|
|
|
@bindThis
|
2024-02-21 05:31:50 +00:00
|
|
|
|
public convertLegacyReactions(reactions: MiNote['reactions']): MiNote['reactions'] {
|
|
|
|
|
return Object.entries(reactions)
|
|
|
|
|
.filter(([, count]) => {
|
|
|
|
|
// `ReactionService.prototype.delete`ではリアクション削除時に、
|
|
|
|
|
// `MiNote['reactions']`のエントリの値をデクリメントしているが、
|
|
|
|
|
// デクリメントしているだけなのでエントリ自体は0を値として持つ形で残り続ける。
|
|
|
|
|
// そのため、この処理がなければ、「0個のリアクションがついている」ということになってしまう。
|
|
|
|
|
return count > 0;
|
|
|
|
|
})
|
|
|
|
|
.map(([reaction, count]) => {
|
|
|
|
|
// unchecked indexed access
|
|
|
|
|
const convertedReaction = legacies[reaction] as string | undefined;
|
2022-09-17 18:27:08 +00:00
|
|
|
|
|
2024-02-21 05:31:50 +00:00
|
|
|
|
const key = this.decodeReaction(convertedReaction ?? reaction).reaction;
|
2022-09-17 18:27:08 +00:00
|
|
|
|
|
2024-02-21 05:31:50 +00:00
|
|
|
|
return [key, count] as const;
|
|
|
|
|
})
|
|
|
|
|
.reduce<MiNote['reactions']>((acc, [key, count]) => {
|
|
|
|
|
// unchecked indexed access
|
|
|
|
|
const prevCount = acc[key] as number | undefined;
|
2022-09-17 18:27:08 +00:00
|
|
|
|
|
2024-02-21 05:31:50 +00:00
|
|
|
|
acc[key] = (prevCount ?? 0) + count;
|
2022-09-17 18:27:08 +00:00
|
|
|
|
|
2024-02-21 05:31:50 +00:00
|
|
|
|
return acc;
|
|
|
|
|
}, {});
|
2022-09-17 18:27:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-12-04 06:03:09 +00:00
|
|
|
|
@bindThis
|
2023-05-18 09:45:49 +00:00
|
|
|
|
public normalize(reaction: string | null): string {
|
2023-03-24 09:55:31 +00:00
|
|
|
|
if (reaction == null) return FALLBACK;
|
2022-09-17 18:27:08 +00:00
|
|
|
|
|
|
|
|
|
// 文字列タイプのリアクションを絵文字に変換
|
|
|
|
|
if (Object.keys(legacies).includes(reaction)) return legacies[reaction];
|
|
|
|
|
|
|
|
|
|
// Unicode絵文字
|
|
|
|
|
const match = emojiRegex.exec(reaction);
|
|
|
|
|
if (match) {
|
2023-03-16 05:24:11 +00:00
|
|
|
|
// 合字を含む1つの絵文字
|
2022-09-17 18:27:08 +00:00
|
|
|
|
const unicode = match[0];
|
|
|
|
|
|
|
|
|
|
// 異体字セレクタ除去
|
|
|
|
|
return unicode.match('\u200d') ? unicode : unicode.replace(/\ufe0f/g, '');
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-24 09:55:31 +00:00
|
|
|
|
return FALLBACK;
|
2022-09-17 18:27:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-12-04 06:03:09 +00:00
|
|
|
|
@bindThis
|
2022-09-17 18:27:08 +00:00
|
|
|
|
public decodeReaction(str: string): DecodedReaction {
|
2023-05-18 09:18:25 +00:00
|
|
|
|
const custom = str.match(decodeCustomEmojiRegexp);
|
2022-09-17 18:27:08 +00:00
|
|
|
|
|
|
|
|
|
if (custom) {
|
|
|
|
|
const name = custom[1];
|
|
|
|
|
const host = custom[2] ?? null;
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
reaction: `:${name}@${host ?? '.'}:`, // ローカル分は@以降を省略するのではなく.にする
|
|
|
|
|
name,
|
|
|
|
|
host,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
reaction: str,
|
|
|
|
|
name: undefined,
|
|
|
|
|
host: undefined,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2022-12-04 06:03:09 +00:00
|
|
|
|
@bindThis
|
2022-09-17 18:27:08 +00:00
|
|
|
|
public convertLegacyReaction(reaction: string): string {
|
|
|
|
|
reaction = this.decodeReaction(reaction).reaction;
|
|
|
|
|
if (Object.keys(legacies).includes(reaction)) return legacies[reaction];
|
|
|
|
|
return reaction;
|
|
|
|
|
}
|
|
|
|
|
}
|