Merge remote-tracking branch 'mattyateaFork/develop' into develop
# Conflicts: # CHANGELOG.md # README.md # locales/index.d.ts # locales/ja-JP.yml # package.json # packages/backend/src/core/activitypub/models/ApNoteService.ts # packages/backend/src/server/api/endpoints/admin/avatar-decorations/list.ts # packages/backend/src/server/api/endpoints/get-avatar-decorations.ts # packages/backend/test/unit/entities/UserEntityService.ts # packages/frontend/src/components/MkFollowButton.vue # packages/frontend/src/components/MkTimeline.vue # packages/frontend/src/pages/about.vue # packages/frontend/src/pages/emoji-edit-dialog.vue # packages/frontend/src/ui/universal.vue
This commit is contained in:
commit
71382a6f85
297 changed files with 60420 additions and 4574 deletions
|
|
@ -130,7 +130,7 @@ export class CacheService implements OnApplicationShutdown {
|
|||
case 'userChangeSuspendedState':
|
||||
case 'userChangeDeletedState':
|
||||
case 'remoteUserUpdated':
|
||||
case 'localUserUpdated': {
|
||||
{
|
||||
const user = await this.usersRepository.findOneBy({ id: body.id });
|
||||
if (user == null) {
|
||||
this.userByIdCache.delete(body.id);
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import { MetaService } from './MetaService.js';
|
|||
import { MfmService } from './MfmService.js';
|
||||
import { ModerationLogService } from './ModerationLogService.js';
|
||||
import { NoteCreateService } from './NoteCreateService.js';
|
||||
import { NoteUpdateService } from './NoteUpdateService.js';
|
||||
import { NoteDeleteService } from './NoteDeleteService.js';
|
||||
import { NotePiningService } from './NotePiningService.js';
|
||||
import { NoteReadService } from './NoteReadService.js';
|
||||
|
|
@ -101,6 +102,7 @@ import { ClipEntityService } from './entities/ClipEntityService.js';
|
|||
import { DriveFileEntityService } from './entities/DriveFileEntityService.js';
|
||||
import { DriveFolderEntityService } from './entities/DriveFolderEntityService.js';
|
||||
import { EmojiEntityService } from './entities/EmojiEntityService.js';
|
||||
import { EmojiRequestsEntityService } from './entities/EmojiRequestsEntityService.js';
|
||||
import { FollowingEntityService } from './entities/FollowingEntityService.js';
|
||||
import { FollowRequestEntityService } from './entities/FollowRequestEntityService.js';
|
||||
import { GalleryLikeEntityService } from './entities/GalleryLikeEntityService.js';
|
||||
|
|
@ -181,6 +183,7 @@ const $MetaService: Provider = { provide: 'MetaService', useExisting: MetaServic
|
|||
const $MfmService: Provider = { provide: 'MfmService', useExisting: MfmService };
|
||||
const $ModerationLogService: Provider = { provide: 'ModerationLogService', useExisting: ModerationLogService };
|
||||
const $NoteCreateService: Provider = { provide: 'NoteCreateService', useExisting: NoteCreateService };
|
||||
const $NoteUpdateService: Provider = { provide: 'NoteUpdateService', useExisting: NoteUpdateService };
|
||||
const $NoteDeleteService: Provider = { provide: 'NoteDeleteService', useExisting: NoteDeleteService };
|
||||
const $NotePiningService: Provider = { provide: 'NotePiningService', useExisting: NotePiningService };
|
||||
const $NoteReadService: Provider = { provide: 'NoteReadService', useExisting: NoteReadService };
|
||||
|
|
@ -245,6 +248,7 @@ const $ClipEntityService: Provider = { provide: 'ClipEntityService', useExisting
|
|||
const $DriveFileEntityService: Provider = { provide: 'DriveFileEntityService', useExisting: DriveFileEntityService };
|
||||
const $DriveFolderEntityService: Provider = { provide: 'DriveFolderEntityService', useExisting: DriveFolderEntityService };
|
||||
const $EmojiEntityService: Provider = { provide: 'EmojiEntityService', useExisting: EmojiEntityService };
|
||||
const $EmojiRequestsEntityService: Provider = { provide: 'EmojiRequestsEntityService', useExisting: EmojiRequestsEntityService };
|
||||
const $FollowingEntityService: Provider = { provide: 'FollowingEntityService', useExisting: FollowingEntityService };
|
||||
const $FollowRequestEntityService: Provider = { provide: 'FollowRequestEntityService', useExisting: FollowRequestEntityService };
|
||||
const $GalleryLikeEntityService: Provider = { provide: 'GalleryLikeEntityService', useExisting: GalleryLikeEntityService };
|
||||
|
|
@ -327,6 +331,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
MfmService,
|
||||
ModerationLogService,
|
||||
NoteCreateService,
|
||||
NoteUpdateService,
|
||||
NoteDeleteService,
|
||||
NotePiningService,
|
||||
NoteReadService,
|
||||
|
|
@ -391,6 +396,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
DriveFileEntityService,
|
||||
DriveFolderEntityService,
|
||||
EmojiEntityService,
|
||||
EmojiRequestsEntityService,
|
||||
FollowingEntityService,
|
||||
FollowRequestEntityService,
|
||||
GalleryLikeEntityService,
|
||||
|
|
@ -469,6 +475,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$MfmService,
|
||||
$ModerationLogService,
|
||||
$NoteCreateService,
|
||||
$NoteUpdateService,
|
||||
$NoteDeleteService,
|
||||
$NotePiningService,
|
||||
$NoteReadService,
|
||||
|
|
@ -533,6 +540,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$DriveFileEntityService,
|
||||
$DriveFolderEntityService,
|
||||
$EmojiEntityService,
|
||||
$EmojiRequestsEntityService,
|
||||
$FollowingEntityService,
|
||||
$FollowRequestEntityService,
|
||||
$GalleryLikeEntityService,
|
||||
|
|
@ -612,6 +620,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
MfmService,
|
||||
ModerationLogService,
|
||||
NoteCreateService,
|
||||
NoteUpdateService,
|
||||
NoteDeleteService,
|
||||
NotePiningService,
|
||||
NoteReadService,
|
||||
|
|
@ -675,6 +684,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
DriveFileEntityService,
|
||||
DriveFolderEntityService,
|
||||
EmojiEntityService,
|
||||
EmojiRequestsEntityService,
|
||||
FollowingEntityService,
|
||||
FollowRequestEntityService,
|
||||
GalleryLikeEntityService,
|
||||
|
|
@ -753,6 +763,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$MfmService,
|
||||
$ModerationLogService,
|
||||
$NoteCreateService,
|
||||
$NoteUpdateService,
|
||||
$NoteDeleteService,
|
||||
$NotePiningService,
|
||||
$NoteReadService,
|
||||
|
|
@ -816,6 +827,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$DriveFileEntityService,
|
||||
$DriveFolderEntityService,
|
||||
$EmojiEntityService,
|
||||
$EmojiRequestsEntityService,
|
||||
$FollowingEntityService,
|
||||
$FollowRequestEntityService,
|
||||
$GalleryLikeEntityService,
|
||||
|
|
|
|||
|
|
@ -12,13 +12,13 @@ import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
|||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import type { MiEmoji } from '@/models/Emoji.js';
|
||||
import type { EmojisRepository, MiRole, MiUser } from '@/models/_.js';
|
||||
import type { EmojisRepository, EmojiRequestsRepository, MiRole, MiUser } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MemoryKVCache, RedisSingleCache } from '@/misc/cache.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { query } from '@/misc/prelude/url.js';
|
||||
import type { Serialized } from '@/types.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { MiEmojiRequest } from '@/models/EmojiRequest.js';
|
||||
|
||||
const parseEmojiStrRegexp = /^([-\w]+)(?:@([\w.-]+))?$/;
|
||||
|
||||
|
|
@ -34,6 +34,9 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||
@Inject(DI.emojisRepository)
|
||||
private emojisRepository: EmojisRepository,
|
||||
|
||||
@Inject(DI.emojiRequestsRepository)
|
||||
private emojiRequestsRepository: EmojiRequestsRepository,
|
||||
|
||||
private utilityService: UtilityService,
|
||||
private idService: IdService,
|
||||
private emojiEntityService: EmojiEntityService,
|
||||
|
|
@ -56,6 +59,41 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async request(data: {
|
||||
driveFile: MiDriveFile;
|
||||
name: string;
|
||||
category: string | null;
|
||||
aliases: string[];
|
||||
license: string | null;
|
||||
isSensitive: boolean;
|
||||
localOnly: boolean;
|
||||
}, me?: MiUser): Promise<MiEmojiRequest> {
|
||||
const emoji = await this.emojiRequestsRepository.insert({
|
||||
id: this.idService.gen(),
|
||||
updatedAt: new Date(),
|
||||
name: data.name,
|
||||
category: data.category,
|
||||
aliases: data.aliases,
|
||||
originalUrl: data.driveFile.url,
|
||||
publicUrl: data.driveFile.webpublicUrl ?? data.driveFile.url,
|
||||
type: data.driveFile.webpublicType ?? data.driveFile.type,
|
||||
license: data.license,
|
||||
isSensitive: data.isSensitive,
|
||||
localOnly: data.localOnly,
|
||||
fileId: data.driveFile.id,
|
||||
}).then(x => this.emojiRequestsRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
if (me) {
|
||||
this.moderationLogService.log(me, 'addCustomEmoji', {
|
||||
emojiId: emoji.id,
|
||||
emoji: emoji,
|
||||
});
|
||||
}
|
||||
|
||||
return emoji;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async add(data: {
|
||||
driveFile: MiDriveFile;
|
||||
|
|
@ -159,6 +197,36 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async updateRequest(id: MiEmojiRequest['id'], data: {
|
||||
driveFile?: MiDriveFile;
|
||||
name?: string;
|
||||
category?: string | null;
|
||||
aliases?: string[];
|
||||
license?: string | null;
|
||||
isSensitive?: boolean;
|
||||
localOnly?: boolean;
|
||||
}, moderator?: MiUser): Promise<void> {
|
||||
const emoji = await this.emojiRequestsRepository.findOneByOrFail({ id: id });
|
||||
const sameNameEmoji = await this.emojiRequestsRepository.findOneBy({ name: data.name });
|
||||
if (sameNameEmoji != null && sameNameEmoji.id !== id) throw new Error('name already exists');
|
||||
|
||||
await this.emojiRequestsRepository.update(emoji.id, {
|
||||
updatedAt: new Date(),
|
||||
name: data.name,
|
||||
category: data.category,
|
||||
aliases: data.aliases,
|
||||
license: data.license,
|
||||
isSensitive: data.isSensitive,
|
||||
localOnly: data.localOnly,
|
||||
originalUrl: data.driveFile != null ? data.driveFile.url : undefined,
|
||||
publicUrl: data.driveFile != null ? (data.driveFile.webpublicUrl ?? data.driveFile.url) : undefined,
|
||||
type: data.driveFile != null ? (data.driveFile.webpublicType ?? data.driveFile.type) : undefined,
|
||||
});
|
||||
|
||||
this.localEmojisCache.refresh();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async addAliasesBulk(ids: MiEmoji['id'][], aliases: string[]) {
|
||||
const emojis = await this.emojisRepository.findBy({
|
||||
|
|
@ -267,6 +335,13 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async deleteRequest(id: MiEmojiRequest['id']) {
|
||||
const emoji = await this.emojiRequestsRepository.findOneByOrFail({ id: id });
|
||||
|
||||
await this.emojiRequestsRepository.delete(emoji.id);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async deleteBulk(ids: MiEmoji['id'][], moderator?: MiUser) {
|
||||
const emojis = await this.emojisRepository.findBy({
|
||||
|
|
@ -389,11 +464,21 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||
return this.emojisRepository.exists({ where: { name, host: IsNull() } });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public checkRequestDuplicate(name: string): Promise<boolean> {
|
||||
return this.emojiRequestsRepository.exist({ where: { name } });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public getEmojiById(id: string): Promise<MiEmoji | null> {
|
||||
return this.emojisRepository.findOneBy({ id });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public getEmojiRequestById(id: string): Promise<MiEmojiRequest | null> {
|
||||
return this.emojiRequestsRepository.findOneBy({ id });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public getEmojiByName(name: string): Promise<MiEmoji | null> {
|
||||
return this.emojisRepository.findOneBy({ name, host: IsNull() });
|
||||
|
|
|
|||
|
|
@ -213,6 +213,17 @@ export class EmailService {
|
|||
reason: validated.reason ? formatReason[validated.reason] ?? null : null,
|
||||
};
|
||||
}
|
||||
if (meta.enableActiveEmailValidation) {
|
||||
const dispose = await this.httpRequestService.send('https://raw.githubusercontent.com/mattyatea/disposable-email-domains/master/disposable_email_blocklist.conf', {
|
||||
method: 'GET',
|
||||
});
|
||||
const disposableEmailDomains = (await dispose.text()).split('\n');
|
||||
const domain = emailAddress.split('@')[1];
|
||||
console.log(domain)
|
||||
if (disposableEmailDomains.includes(domain)) {
|
||||
validated = { valid: false, reason: 'disposable' };
|
||||
}
|
||||
}
|
||||
|
||||
const emailDomain: string = emailAddress.split('@')[1];
|
||||
const isBanned = this.utilityService.isBlockedHost(meta.bannedEmailDomains, emailDomain);
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ export class FanoutTimelineEndpointService {
|
|||
|
||||
if (ps.me) {
|
||||
const me = ps.me;
|
||||
|
||||
const [
|
||||
userIdsWhoMeMuting,
|
||||
userIdsWhoMeMutingRenotes,
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ export interface NoteEventTypes {
|
|||
};
|
||||
updated: {
|
||||
cw: string | null;
|
||||
text: string;
|
||||
text: string | null;
|
||||
};
|
||||
reacted: {
|
||||
reaction: string;
|
||||
|
|
@ -160,6 +160,8 @@ export interface AdminEventTypes {
|
|||
targetUserId: MiUser['id'],
|
||||
reporterId: MiUser['id'],
|
||||
comment: string;
|
||||
notes: any[];
|
||||
noteIds: string[];
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
import { URL } from 'node:url';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as parse5 from 'parse5';
|
||||
import { Window, XMLSerializer } from 'happy-dom';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import serialize from 'w3c-xmlserializer';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { intersperse } from '@/misc/prelude/array.js';
|
||||
|
|
@ -243,7 +244,7 @@ export class MfmService {
|
|||
return null;
|
||||
}
|
||||
|
||||
const { window } = new Window();
|
||||
const { window } = new JSDOM() as unknown as { window: Window };
|
||||
|
||||
const doc = window.document;
|
||||
|
||||
|
|
@ -461,6 +462,6 @@ export class MfmService {
|
|||
|
||||
appendChildren(nodes, body);
|
||||
|
||||
return new XMLSerializer().serializeToString(body);
|
||||
return serialize(body);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,19 +15,16 @@ import { extractHashtags } from '@/misc/extract-hashtags.js';
|
|||
import type { IMentionedRemoteUsers } from '@/models/Note.js';
|
||||
import { MiNote } from '@/models/Note.js';
|
||||
import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import type { MiApp } from '@/models/App.js';
|
||||
import { concat } from '@/misc/prelude/array.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js';
|
||||
import type { IPoll } from '@/models/Poll.js';
|
||||
import { MiPoll } from '@/models/Poll.js';
|
||||
import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js';
|
||||
import { checkWordMute } from '@/misc/check-word-mute.js';
|
||||
import type { MiChannel } from '@/models/Channel.js';
|
||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||
import { MemorySingleCache } from '@/misc/cache.js';
|
||||
import type { MiUserProfile } from '@/models/UserProfile.js';
|
||||
import type { MiNoteCreateOption as Option, MiMinimumUser as MinimumUser } from '@/types.js';
|
||||
import { RelayService } from '@/core/RelayService.js';
|
||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
|
@ -128,14 +125,17 @@ type MinimumUser = {
|
|||
|
||||
type Option = {
|
||||
createdAt?: Date | null;
|
||||
updatedAt?: Date | null;
|
||||
name?: string | null;
|
||||
text?: string | null;
|
||||
reply?: MiNote | null;
|
||||
renote?: MiNote | null;
|
||||
files?: MiDriveFile[] | null;
|
||||
poll?: IPoll | null;
|
||||
event?: IEvent | null;
|
||||
localOnly?: boolean | null;
|
||||
reactionAcceptance?: MiNote['reactionAcceptance'];
|
||||
disableRightClick?: boolean | null;
|
||||
cw?: string | null;
|
||||
visibility?: string;
|
||||
visibleUsers?: MinimumUser[] | null;
|
||||
|
|
@ -227,6 +227,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
host: MiUser['host'];
|
||||
isBot: MiUser['isBot'];
|
||||
isCat: MiUser['isCat'];
|
||||
isGorilla: MiUser['isGorilla'];
|
||||
}, data: Option, silent = false): Promise<MiNote> {
|
||||
// チャンネル外にリプライしたら対象のスコープに合わせる
|
||||
// (クライアントサイドでやっても良い処理だと思うけどとりあえずサーバーサイドで)
|
||||
|
|
@ -262,13 +263,50 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
}
|
||||
}
|
||||
|
||||
const hasProhibitedWords = await this.checkProhibitedWordsContain({
|
||||
cw: data.cw,
|
||||
text: data.text,
|
||||
pollChoices: data.poll?.choices,
|
||||
}, meta.prohibitedWords);
|
||||
if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', meta.prohibitedWords)) {
|
||||
const { DiscordWebhookUrlWordBlock } = (await this.metaService.fetch());
|
||||
const regexpregexp = /^\/(.+)\/(.*)$/;
|
||||
let matchedString = '';
|
||||
for (const filter of meta.prohibitedWords) {
|
||||
// represents RegExp
|
||||
const regexp = filter.match(regexpregexp);
|
||||
// This should never happen due to input sanitisation.
|
||||
if (!regexp) {
|
||||
const words = filter.split(' ');
|
||||
const foundWord = words.find(keyword => (data.cw ?? data.text ?? '').includes(keyword));
|
||||
if (foundWord) {
|
||||
matchedString = foundWord;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
const match = new RE2(regexp[1], regexp[2]).exec(data.cw ?? data.text ?? '');
|
||||
if (match) {
|
||||
matchedString = match[0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasProhibitedWords) {
|
||||
if (DiscordWebhookUrlWordBlock) {
|
||||
const data_disc = { 'username': 'ノートブロックお知らせ',
|
||||
'content':
|
||||
'ユーザー名 :' + user.username + '\n' +
|
||||
'url : ' + user.host + '\n' +
|
||||
'contents : ' + data.text + '\n' +
|
||||
'引っかかったワード :' + matchedString,
|
||||
'allowed_mentions': {
|
||||
'parse': [],
|
||||
},
|
||||
};
|
||||
|
||||
await fetch(DiscordWebhookUrlWordBlock, {
|
||||
'method': 'post',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data_disc),
|
||||
});
|
||||
}
|
||||
throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words');
|
||||
}
|
||||
|
||||
|
|
@ -364,6 +402,15 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
mentionedUsers = data.apMentions ?? await this.extractMentionedUsers(user, combinedTokens);
|
||||
}
|
||||
|
||||
const willCauseNotification = mentionedUsers.filter(u => u.host === null).length > 0 || data.reply?.userHost === null || data.renote?.userHost === null;
|
||||
|
||||
if (user.host !== null && willCauseNotification) {
|
||||
const userEntity = await this.usersRepository.findOneBy({ id: user.id });
|
||||
if ((userEntity?.followersCount ?? 0) === 0) {
|
||||
throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words');
|
||||
}
|
||||
}
|
||||
|
||||
tags = tags.filter(tag => Array.from(tag).length <= 128).splice(0, 32);
|
||||
|
||||
if (data.reply && (user.id !== data.reply.userId) && !mentionedUsers.some(u => u.id === data.reply!.userId)) {
|
||||
|
|
@ -962,6 +1009,9 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
this.fanoutTimelineService.push('localTimelineWithFiles', note.id, 500, r);
|
||||
}
|
||||
}
|
||||
if (note.visibility === 'public' && note.userHost !== null) {
|
||||
this.fanoutTimelineService.push(`remoteLocalTimeline:${note.userHost}`, note.id, 1000, r);
|
||||
}
|
||||
}
|
||||
|
||||
if (Math.random() < 0.1) {
|
||||
|
|
|
|||
297
packages/backend/src/core/NoteUpdateService.ts
Normal file
297
packages/backend/src/core/NoteUpdateService.ts
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey, cherrypick contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { setImmediate } from 'node:timers/promises';
|
||||
import util from 'util';
|
||||
import { In, DataSource } from 'typeorm';
|
||||
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
|
||||
import * as mfm from 'mfm-js';
|
||||
import type { IMentionedRemoteUsers } from '@/models/Note.js';
|
||||
import { MiNote } from '@/models/Note.js';
|
||||
import type { NotesRepository, UsersRepository } from '@/models/_.js';
|
||||
import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js';
|
||||
import { RelayService } from '@/core/RelayService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
||||
import { SearchService } from '@/core/SearchService.js';
|
||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||
import { MiDriveFile } from '@/models/_.js';
|
||||
import { MiPoll, IPoll } from '@/models/Poll.js';
|
||||
import { concat } from '@/misc/prelude/array.js';
|
||||
import { extractHashtags } from '@/misc/extract-hashtags.js';
|
||||
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
|
||||
|
||||
type MinimumUser = {
|
||||
id: MiUser['id'];
|
||||
host: MiUser['host'];
|
||||
username: MiUser['username'];
|
||||
uri: MiUser['uri'];
|
||||
};
|
||||
|
||||
type Option = {
|
||||
updatedAt?: Date | null;
|
||||
files?: MiDriveFile[] | null;
|
||||
name?: string | null;
|
||||
text?: string | null;
|
||||
cw?: string | null;
|
||||
apHashtags?: string[] | null;
|
||||
apEmojis?: string[] | null;
|
||||
poll?: IPoll | null;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class NoteUpdateService implements OnApplicationShutdown {
|
||||
#shutdownController = new AbortController();
|
||||
|
||||
constructor(
|
||||
@Inject(DI.db)
|
||||
private db: DataSource,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private relayService: RelayService,
|
||||
private apDeliverManagerService: ApDeliverManagerService,
|
||||
private apRendererService: ApRendererService,
|
||||
private searchService: SearchService,
|
||||
private activeUsersChart: ActiveUsersChart,
|
||||
) { }
|
||||
|
||||
@bindThis
|
||||
public async update(user: {
|
||||
id: MiUser['id'];
|
||||
username: MiUser['username'];
|
||||
host: MiUser['host'];
|
||||
isBot: MiUser['isBot'];
|
||||
}, data: Option, note: MiNote, silent = false): Promise<MiNote | null> {
|
||||
if (data.updatedAt == null) data.updatedAt = new Date();
|
||||
|
||||
if (data.text) {
|
||||
if (data.text.length > DB_MAX_NOTE_TEXT_LENGTH) {
|
||||
data.text = data.text.slice(0, DB_MAX_NOTE_TEXT_LENGTH);
|
||||
}
|
||||
data.text = data.text.trim();
|
||||
} else {
|
||||
data.text = null;
|
||||
}
|
||||
|
||||
let tags = data.apHashtags;
|
||||
let emojis = data.apEmojis;
|
||||
|
||||
// Parse MFM if needed
|
||||
if (!tags || !emojis) {
|
||||
const tokens = data.text ? mfm.parse(data.text)! : [];
|
||||
const cwTokens = data.cw ? mfm.parse(data.cw)! : [];
|
||||
const choiceTokens = data.poll && data.poll.choices
|
||||
? concat(data.poll.choices.map(choice => mfm.parse(choice)!))
|
||||
: [];
|
||||
|
||||
const combinedTokens = tokens.concat(cwTokens).concat(choiceTokens);
|
||||
|
||||
tags = data.apHashtags ?? extractHashtags(combinedTokens);
|
||||
|
||||
emojis = data.apEmojis ?? extractCustomEmojisFromMfm(combinedTokens);
|
||||
}
|
||||
|
||||
tags = tags.filter(tag => Array.from(tag ?? '').length <= 128).splice(0, 32);
|
||||
|
||||
const updatedNote = await this.updateNote(user, note, data, tags, emojis);
|
||||
|
||||
if (updatedNote) {
|
||||
setImmediate('post updated', { signal: this.#shutdownController.signal }).then(
|
||||
() => this.postNoteUpdated(updatedNote, user, silent),
|
||||
() => { /* aborted, ignore this */ },
|
||||
);
|
||||
}
|
||||
|
||||
return updatedNote;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async updateNote(user: {
|
||||
id: MiUser['id']; host: MiUser['host'];
|
||||
}, note: MiNote, data: Option, tags: string[], emojis: string[]): Promise<MiNote | null> {
|
||||
const updatedAtHistory = note.updatedAtHistory ? note.updatedAtHistory : [];
|
||||
|
||||
const values = new MiNote({
|
||||
updatedAt: data.updatedAt!,
|
||||
fileIds: data.files ? data.files.map(file => file.id) : [],
|
||||
text: data.text,
|
||||
hasPoll: data.poll != null,
|
||||
cw: data.cw ?? null,
|
||||
tags: tags.map(tag => normalizeForSearch(tag)),
|
||||
emojis,
|
||||
attachedFileTypes: data.files ? data.files.map(file => file.type) : [],
|
||||
updatedAtHistory: [...updatedAtHistory, new Date()],
|
||||
noteEditHistory: [...note.noteEditHistory, (note.cw ? note.cw + '\n' : '') + note.text!],
|
||||
});
|
||||
|
||||
// 投稿を更新
|
||||
try {
|
||||
if (note.hasPoll && values.hasPoll) {
|
||||
// Start transaction
|
||||
await this.db.transaction(async transactionalEntityManager => {
|
||||
await transactionalEntityManager.update(MiNote, { id: note.id }, values);
|
||||
|
||||
if (values.hasPoll) {
|
||||
const old_poll = await transactionalEntityManager.findOneBy(MiPoll, { noteId: note.id });
|
||||
if (old_poll!.choices.toString() !== data.poll!.choices.toString() || old_poll!.multiple !== data.poll!.multiple) {
|
||||
await transactionalEntityManager.delete(MiPoll, { noteId: note.id });
|
||||
const poll = new MiPoll({
|
||||
noteId: note.id,
|
||||
choices: data.poll!.choices,
|
||||
expiresAt: data.poll!.expiresAt,
|
||||
multiple: data.poll!.multiple,
|
||||
votes: new Array(data.poll!.choices.length).fill(0),
|
||||
noteVisibility: note.visibility,
|
||||
userId: user.id,
|
||||
userHost: user.host,
|
||||
});
|
||||
await transactionalEntityManager.insert(MiPoll, poll);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (!note.hasPoll && values.hasPoll) {
|
||||
// Start transaction
|
||||
await this.db.transaction(async transactionalEntityManager => {
|
||||
await transactionalEntityManager.update(MiNote, { id: note.id }, values);
|
||||
|
||||
if (values.hasPoll) {
|
||||
const poll = new MiPoll({
|
||||
noteId: note.id,
|
||||
choices: data.poll!.choices,
|
||||
expiresAt: data.poll!.expiresAt,
|
||||
multiple: data.poll!.multiple,
|
||||
votes: new Array(data.poll!.choices.length).fill(0),
|
||||
noteVisibility: note.visibility,
|
||||
userId: user.id,
|
||||
userHost: user.host,
|
||||
});
|
||||
|
||||
await transactionalEntityManager.insert(MiPoll, poll);
|
||||
}
|
||||
});
|
||||
} else if (note.hasPoll && !values.hasPoll) {
|
||||
// Start transaction
|
||||
await this.db.transaction(async transactionalEntityManager => {
|
||||
await transactionalEntityManager.update(MiNote, { id: note.id }, values);
|
||||
|
||||
if (!values.hasPoll) {
|
||||
await transactionalEntityManager.delete(MiPoll, { noteId: note.id });
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await this.notesRepository.update({ id: note.id }, values);
|
||||
}
|
||||
|
||||
return await this.notesRepository.findOneBy({ id: note.id });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async postNoteUpdated(note: MiNote, user: {
|
||||
id: MiUser['id'];
|
||||
username: MiUser['username'];
|
||||
host: MiUser['host'];
|
||||
isBot: MiUser['isBot'];
|
||||
}, silent: boolean) {
|
||||
if (!silent) {
|
||||
if (this.userEntityService.isLocalUser(user)) this.activeUsersChart.write(user);
|
||||
|
||||
this.globalEventService.publishNoteStream(note.id, 'updated', { cw: note.cw, text: note.text });
|
||||
|
||||
//#region AP deliver
|
||||
if (this.userEntityService.isLocalUser(user)) {
|
||||
await (async () => {
|
||||
// @ts-ignore
|
||||
const noteActivity = await this.renderNoteActivity(note, user);
|
||||
|
||||
await this.deliverToConcerned(user, note, noteActivity);
|
||||
})();
|
||||
}
|
||||
//#endregion
|
||||
}
|
||||
|
||||
// Register to search database
|
||||
this.reIndex(note);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async renderNoteActivity(note: MiNote, user: MiUser) {
|
||||
const content = this.apRendererService.renderUpdate(await this.apRendererService.renderNote(note, false), user);
|
||||
|
||||
return this.apRendererService.addContext(content);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async getMentionedRemoteUsers(note: MiNote) {
|
||||
const where = [] as any[];
|
||||
|
||||
// mention / reply / dm
|
||||
const uris = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri);
|
||||
if (uris.length > 0) {
|
||||
where.push(
|
||||
{ uri: In(uris) },
|
||||
);
|
||||
}
|
||||
|
||||
// renote / quote
|
||||
if (note.renoteUserId) {
|
||||
where.push({
|
||||
id: note.renoteUserId,
|
||||
});
|
||||
}
|
||||
|
||||
if (where.length === 0) return [];
|
||||
|
||||
return await this.usersRepository.find({
|
||||
where,
|
||||
}) as MiRemoteUser[];
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async deliverToConcerned(user: { id: MiLocalUser['id']; host: null; }, note: MiNote, content: any) {
|
||||
console.log('deliverToConcerned', util.inspect(content, { depth: null }));
|
||||
await this.apDeliverManagerService.deliverToFollowers(user, content);
|
||||
await this.relayService.deliverToRelays(user, content);
|
||||
const remoteUsers = await this.getMentionedRemoteUsers(note);
|
||||
for (const remoteUser of remoteUsers) {
|
||||
await this.apDeliverManagerService.deliverToUser(user, content, remoteUser);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private reIndex(note: MiNote) {
|
||||
if (note.text == null && note.cw == null) return;
|
||||
|
||||
this.searchService.unindexNote(note);
|
||||
this.searchService.indexNote(note);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.#shutdownController.abort();
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public onApplicationShutdown(signal?: string | undefined): void {
|
||||
this.dispose();
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ import type { Provider } from '@nestjs/common';
|
|||
|
||||
export type SystemQueue = Bull.Queue<Record<string, unknown>>;
|
||||
export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>;
|
||||
export type ScheduleNotePostQueue = Bull.Queue<ScheduleNotePostJobData>;
|
||||
export type DeliverQueue = Bull.Queue<DeliverJobData>;
|
||||
export type InboxQueue = Bull.Queue<InboxJobData>;
|
||||
export type DbQueue = Bull.Queue;
|
||||
|
|
@ -41,6 +42,12 @@ const $endedPollNotification: Provider = {
|
|||
inject: [DI.config],
|
||||
};
|
||||
|
||||
const $scheduleNotePost: Provider = {
|
||||
provide: 'queue:scheduleNotePost',
|
||||
useFactory: (config: Config) => new Bull.Queue(QUEUE.SCHEDULE_NOTE_POST, baseQueueOptions(config, QUEUE.SCHEDULE_NOTE_POST)),
|
||||
inject: [DI.config],
|
||||
};
|
||||
|
||||
const $deliver: Provider = {
|
||||
provide: 'queue:deliver',
|
||||
useFactory: (config: Config) => new Bull.Queue(QUEUE.DELIVER, baseQueueOptions(config, QUEUE.DELIVER)),
|
||||
|
|
@ -89,6 +96,7 @@ const $systemWebhookDeliver: Provider = {
|
|||
providers: [
|
||||
$system,
|
||||
$endedPollNotification,
|
||||
$scheduleNotePost,
|
||||
$deliver,
|
||||
$inbox,
|
||||
$db,
|
||||
|
|
@ -100,6 +108,7 @@ const $systemWebhookDeliver: Provider = {
|
|||
exports: [
|
||||
$system,
|
||||
$endedPollNotification,
|
||||
$scheduleNotePost,
|
||||
$deliver,
|
||||
$inbox,
|
||||
$db,
|
||||
|
|
@ -113,6 +122,7 @@ export class QueueModule implements OnApplicationShutdown {
|
|||
constructor(
|
||||
@Inject('queue:system') public systemQueue: SystemQueue,
|
||||
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
|
||||
@Inject('queue:scheduleNotePost') public scheduleNotePostQueue: ScheduleNotePostQueue,
|
||||
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
|
||||
@Inject('queue:inbox') public inboxQueue: InboxQueue,
|
||||
@Inject('queue:db') public dbQueue: DbQueue,
|
||||
|
|
@ -129,6 +139,7 @@ export class QueueModule implements OnApplicationShutdown {
|
|||
await Promise.all([
|
||||
this.systemQueue.close(),
|
||||
this.endedPollNotificationQueue.close(),
|
||||
this.scheduleNotePostQueue.close(),
|
||||
this.deliverQueue.close(),
|
||||
this.inboxQueue.close(),
|
||||
this.dbQueue.close(),
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { DI } from '@/di-symbols.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js';
|
||||
import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
|
||||
|
||||
import type {
|
||||
DbJobData,
|
||||
DeliverJobData,
|
||||
|
|
@ -32,6 +33,7 @@ import type {
|
|||
SystemQueue,
|
||||
UserWebhookDeliverQueue,
|
||||
SystemWebhookDeliverQueue,
|
||||
ScheduleNotePostQueue
|
||||
} from './QueueModule.js';
|
||||
import type httpSignature from '@peertube/http-signature';
|
||||
import type * as Bull from 'bullmq';
|
||||
|
|
@ -44,6 +46,7 @@ export class QueueService {
|
|||
|
||||
@Inject('queue:system') public systemQueue: SystemQueue,
|
||||
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
|
||||
@Inject('queue:scheduleNotePost') public ScheduleNotePostQueue: ScheduleNotePostQueue,
|
||||
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
|
||||
@Inject('queue:inbox') public inboxQueue: InboxQueue,
|
||||
@Inject('queue:db') public dbQueue: DbQueue,
|
||||
|
|
|
|||
|
|
@ -166,29 +166,56 @@ export class ReactionService {
|
|||
userId: user.id,
|
||||
reaction,
|
||||
};
|
||||
if (user.host == null) {
|
||||
const exists = await this.noteReactionsRepository.findOneBy({
|
||||
noteId: note.id,
|
||||
userId: user.id,
|
||||
reaction: record.reaction,
|
||||
});
|
||||
|
||||
// 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,
|
||||
});
|
||||
const count = await this.noteReactionsRepository.countBy({
|
||||
noteId: note.id,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (exists.reaction !== reaction) {
|
||||
// 別のリアクションがすでにされていたら置き換える
|
||||
await this.delete(user, note);
|
||||
if (count > 3) {
|
||||
throw new IdentifiableError('51c42bb4-931a-456b-bff7-e5a8a70dd298');
|
||||
}
|
||||
|
||||
if (exists == null) {
|
||||
if (user.host == null) {
|
||||
await this.noteReactionsRepository.insert(record);
|
||||
} else {
|
||||
// 同じリアクションがすでにされていたらエラー
|
||||
throw new IdentifiableError('51c42bb4-931a-456b-bff7-e5a8a70dd298');
|
||||
}
|
||||
} else {
|
||||
throw e;
|
||||
// 同じリアクションがすでにされていたらエラー
|
||||
throw new IdentifiableError('51c42bb4-931a-456b-bff7-e5a8a70dd298');
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await this.noteReactionsRepository.insert(record);
|
||||
} catch (e) {
|
||||
if (isDuplicateKeyValueError(e)) {
|
||||
const exists = await this.noteReactionsRepository.findOneByOrFail({
|
||||
noteId: note.id,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Create reaction
|
||||
|
||||
// Increment reactions count
|
||||
const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`;
|
||||
|
|
@ -281,17 +308,24 @@ export class ReactionService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async delete(user: { id: MiUser['id']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote) {
|
||||
public async delete(user: { id: MiUser['id']; host: MiUser['host']; isBot: MiUser['isBot']; }, note: MiNote, reaction?: string) {
|
||||
// if already unreacted
|
||||
const exist = await this.noteReactionsRepository.findOneBy({
|
||||
noteId: note.id,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
let exist;
|
||||
if (reaction == null) {
|
||||
exist = await this.noteReactionsRepository.findOneBy({
|
||||
noteId: note.id,
|
||||
userId: user.id,
|
||||
});
|
||||
} else {
|
||||
exist = await this.noteReactionsRepository.findOneBy({
|
||||
noteId: note.id,
|
||||
userId: user.id,
|
||||
reaction: reaction.replace(/@./, ''),
|
||||
});
|
||||
}
|
||||
if (exist == null) {
|
||||
throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted');
|
||||
}
|
||||
|
||||
// Delete reaction
|
||||
const result = await this.noteReactionsRepository.delete(exist.id);
|
||||
|
||||
|
|
|
|||
|
|
@ -35,12 +35,15 @@ export type RolePolicies = {
|
|||
gtlAvailable: boolean;
|
||||
ltlAvailable: boolean;
|
||||
canPublicNote: boolean;
|
||||
canEditNote: boolean;
|
||||
canScheduleNote: boolean;
|
||||
mentionLimit: number;
|
||||
canInvite: boolean;
|
||||
inviteLimit: number;
|
||||
inviteLimitCycle: number;
|
||||
inviteExpirationTime: number;
|
||||
canManageCustomEmojis: boolean;
|
||||
canRequestCustomEmojis: boolean;
|
||||
canManageAvatarDecorations: boolean;
|
||||
canSearchNotes: boolean;
|
||||
canUseTranslator: boolean;
|
||||
|
|
@ -57,6 +60,9 @@ export type RolePolicies = {
|
|||
userEachUserListsLimit: number;
|
||||
rateLimitFactor: number;
|
||||
avatarDecorationLimit: number;
|
||||
emojiPickerProfileLimit: number;
|
||||
listPinnedLimit: number;
|
||||
localTimelineAnyLimit: number;
|
||||
};
|
||||
|
||||
export const DEFAULT_POLICIES: RolePolicies = {
|
||||
|
|
@ -64,11 +70,14 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
|||
ltlAvailable: true,
|
||||
canPublicNote: true,
|
||||
mentionLimit: 20,
|
||||
canEditNote: true,
|
||||
canScheduleNote: true,
|
||||
canInvite: false,
|
||||
inviteLimit: 0,
|
||||
inviteLimitCycle: 60 * 24 * 7,
|
||||
inviteExpirationTime: 0,
|
||||
canManageCustomEmojis: false,
|
||||
canRequestCustomEmojis: false,
|
||||
canManageAvatarDecorations: false,
|
||||
canSearchNotes: false,
|
||||
canUseTranslator: true,
|
||||
|
|
@ -85,6 +94,9 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
|||
userEachUserListsLimit: 50,
|
||||
rateLimitFactor: 1,
|
||||
avatarDecorationLimit: 1,
|
||||
emojiPickerProfileLimit: 2,
|
||||
listPinnedLimit: 2,
|
||||
localTimelineAnyLimit: 3,
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
|
|
@ -364,13 +376,17 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||
gtlAvailable: calc('gtlAvailable', vs => vs.some(v => v === true)),
|
||||
ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)),
|
||||
canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)),
|
||||
canScheduleNote: calc('canScheduleNote', vs => vs.some(v => v === true)),
|
||||
canEditNote: calc('canEditNote', vs => vs.some(v => v === true)),
|
||||
mentionLimit: calc('mentionLimit', vs => Math.max(...vs)),
|
||||
canInvite: calc('canInvite', vs => vs.some(v => v === true)),
|
||||
inviteLimit: calc('inviteLimit', vs => Math.max(...vs)),
|
||||
inviteLimitCycle: calc('inviteLimitCycle', vs => Math.max(...vs)),
|
||||
inviteExpirationTime: calc('inviteExpirationTime', vs => Math.max(...vs)),
|
||||
canManageCustomEmojis: calc('canManageCustomEmojis', vs => vs.some(v => v === true)),
|
||||
canRequestCustomEmojis: calc('canRequestCustomEmojis', vs => vs.some(v => v === true)),
|
||||
canManageAvatarDecorations: calc('canManageAvatarDecorations', vs => vs.some(v => v === true)),
|
||||
canRequestCustomEmojis: calc('canRequestCustomEmojis', vs => vs.some(v => v === true)),
|
||||
canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)),
|
||||
canUseTranslator: calc('canUseTranslator', vs => vs.some(v => v === true)),
|
||||
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
|
||||
|
|
@ -386,6 +402,9 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||
userEachUserListsLimit: calc('userEachUserListsLimit', vs => Math.max(...vs)),
|
||||
rateLimitFactor: calc('rateLimitFactor', vs => Math.max(...vs)),
|
||||
avatarDecorationLimit: calc('avatarDecorationLimit', vs => Math.max(...vs)),
|
||||
emojiPickerProfileLimit: calc('emojiPickerProfileLimit', vs => Math.max(...vs)),
|
||||
listPinnedLimit: calc('listPinnedLimit', vs => Math.max(...vs)),
|
||||
localTimelineAnyLimit: calc('localTimelineAnyLimit', vs => Math.max(...vs)),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { NotePiningService } from '@/core/NotePiningService.js';
|
|||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
import { NoteDeleteService } from '@/core/NoteDeleteService.js';
|
||||
import { NoteCreateService } from '@/core/NoteCreateService.js';
|
||||
import { NoteUpdateService } from '@/core/NoteUpdateService.js';
|
||||
import { concat, toArray, toSingle, unique } from '@/misc/prelude/array.js';
|
||||
import { AppLockService } from '@/core/AppLockService.js';
|
||||
import type Logger from '@/logger.js';
|
||||
|
|
@ -73,6 +74,7 @@ export class ApInboxService {
|
|||
private notePiningService: NotePiningService,
|
||||
private userBlockingService: UserBlockingService,
|
||||
private noteCreateService: NoteCreateService,
|
||||
private noteUpdateService: NoteUpdateService,
|
||||
private noteDeleteService: NoteDeleteService,
|
||||
private appLockService: AppLockService,
|
||||
private apResolverService: ApResolverService,
|
||||
|
|
@ -751,11 +753,13 @@ export class ApInboxService {
|
|||
|
||||
@bindThis
|
||||
private async update(actor: MiRemoteUser, activity: IUpdate): Promise<string> {
|
||||
const uri = getApId(activity);
|
||||
|
||||
if (actor.uri !== activity.actor) {
|
||||
return 'skip: invalid actor';
|
||||
}
|
||||
|
||||
this.logger.debug('Update');
|
||||
this.logger.debug(`Update: ${uri}`);
|
||||
|
||||
const resolver = this.apResolverService.createResolver();
|
||||
|
||||
|
|
@ -767,14 +771,51 @@ export class ApInboxService {
|
|||
if (isActor(object)) {
|
||||
await this.apPersonService.updatePerson(actor.uri, resolver, object);
|
||||
return 'ok: Person updated';
|
||||
} else if (getApType(object) === 'Question') {
|
||||
} /*else if (getApType(object) === 'Question') {
|
||||
await this.apQuestionService.updateQuestion(object, resolver).catch(err => console.error(err));
|
||||
return 'ok: Question updated';
|
||||
}*/ else if (getApType(object) === 'Note' || getApType(object) === 'Question') {
|
||||
await this.updateNote(resolver, actor, object, false, activity);
|
||||
return 'ok: Note updated';
|
||||
} else {
|
||||
return `skip: Unknown type: ${getApType(object)}`;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async updateNote(resolver: Resolver, actor: MiRemoteUser, note: IObject, silent = false, activity?: IUpdate): Promise<string> {
|
||||
const uri = getApId(note);
|
||||
|
||||
if (typeof note === 'object') {
|
||||
if (actor.uri !== note.attributedTo) {
|
||||
return 'skip: actor.uri !== note.attributedTo';
|
||||
}
|
||||
|
||||
if (typeof note.id === 'string') {
|
||||
if (this.utilityService.extractDbHost(actor.uri) !== this.utilityService.extractDbHost(note.id)) {
|
||||
return 'skip: host in actor.uri !== note.id';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const unlock = await this.appLockService.getApLock(uri);
|
||||
|
||||
try {
|
||||
const target = await this.notesRepository.findOneBy({uri: uri});
|
||||
if (!target) return `skip: target note not located: ${uri}`;
|
||||
await this.apNoteService.updateNote(note, target, resolver, silent);
|
||||
return 'ok';
|
||||
} catch (err) {
|
||||
if (err instanceof StatusError && err.isClientError) {
|
||||
return `skip ${err.statusCode}`;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
} finally {
|
||||
unlock();
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async move(actor: MiRemoteUser, activity: IMove): Promise<string> {
|
||||
// fetch the new and old accounts
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ export class ApRendererService {
|
|||
actor: this.userEntityService.genLocalUserUri(note.userId),
|
||||
type: 'Announce',
|
||||
published: this.idService.parse(note.id).date.toISOString(),
|
||||
updated: note.updatedAt?.toISOString() ?? undefined,
|
||||
to,
|
||||
cc,
|
||||
object,
|
||||
|
|
@ -438,6 +439,7 @@ export class ApRendererService {
|
|||
_misskey_quote: quote,
|
||||
quoteUrl: quote,
|
||||
published: this.idService.parse(note.id).date.toISOString(),
|
||||
updated: note.updatedAt?.toISOString() ?? undefined,
|
||||
to,
|
||||
cc,
|
||||
inReplyTo,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-project
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project , Type4ny-project, cherrypick contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { forwardRef, Inject, Injectable } from '@nestjs/common';
|
||||
import { In } from 'typeorm';
|
||||
import promiseLimit from 'promise-limit';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { PollsRepository, EmojisRepository } from '@/models/_.js';
|
||||
import type { PollsRepository, EmojisRepository, NotesRepository } from '@/models/_.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { MiRemoteUser } from '@/models/User.js';
|
||||
import type { MiNote } from '@/models/Note.js';
|
||||
|
|
@ -24,7 +25,8 @@ import { UtilityService } from '@/core/UtilityService.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import { checkHttps } from '@/misc/check-https.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { getOneApId, getApId, getOneApHrefNullable, validPost, isEmoji, getApType } from '../type.js';
|
||||
import { NoteUpdateService } from '@/core/NoteUpdateService.js';
|
||||
import { getApId, getApType, getOneApHrefNullable, getOneApId, isEmoji, validPost } from '../type.js';
|
||||
import { ApLoggerService } from '../ApLoggerService.js';
|
||||
import { ApMfmService } from '../ApMfmService.js';
|
||||
import { ApDbResolverService } from '../ApDbResolverService.js';
|
||||
|
|
@ -52,6 +54,9 @@ export class ApNoteService {
|
|||
@Inject(DI.emojisRepository)
|
||||
private emojisRepository: EmojisRepository,
|
||||
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
|
||||
private idService: IdService,
|
||||
private apMfmService: ApMfmService,
|
||||
private apResolverService: ApResolverService,
|
||||
|
|
@ -69,6 +74,7 @@ export class ApNoteService {
|
|||
private appLockService: AppLockService,
|
||||
private pollService: PollService,
|
||||
private noteCreateService: NoteCreateService,
|
||||
private noteUpdateService: NoteUpdateService,
|
||||
private apDbResolverService: ApDbResolverService,
|
||||
private apLoggerService: ApLoggerService,
|
||||
) {
|
||||
|
|
@ -295,6 +301,7 @@ export class ApNoteService {
|
|||
try {
|
||||
return await this.noteCreateService.create(actor, {
|
||||
createdAt: note.published ? new Date(note.published) : null,
|
||||
updatedAt: note.updated ? new Date(note.updated) : null,
|
||||
files,
|
||||
reply,
|
||||
renote: quote,
|
||||
|
|
@ -324,6 +331,85 @@ export class ApNoteService {
|
|||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async updateNote(value: string | IObject, target: MiNote, resolver?: Resolver, silent = false): Promise<MiNote | null> {
|
||||
if (resolver == null) resolver = this.apResolverService.createResolver();
|
||||
|
||||
const object = await resolver.resolve(value);
|
||||
const entryUri = getApId(value);
|
||||
|
||||
const err = this.validateNote(object, entryUri);
|
||||
if (err) {
|
||||
this.logger.error(err.message, {
|
||||
resolver: { history: resolver.getHistory() },
|
||||
value,
|
||||
object,
|
||||
});
|
||||
throw new Error('invalid note');
|
||||
}
|
||||
|
||||
const note = object as IPost;
|
||||
|
||||
// 投稿者をフェッチ
|
||||
if (note.attributedTo == null) {
|
||||
throw new Error('invalid note.attributedTo: ' + note.attributedTo);
|
||||
}
|
||||
|
||||
const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo), resolver) as MiRemoteUser;
|
||||
|
||||
// 投稿者が凍結されていたらスキップ
|
||||
if (actor.isSuspended) {
|
||||
throw new Error('actor has been suspended');
|
||||
}
|
||||
|
||||
const limit = promiseLimit<MiDriveFile>(2);
|
||||
const files = (await Promise.all(toArray(note.attachment).map(attach => (
|
||||
limit(() => this.apImageService.resolveImage(actor, {
|
||||
...attach,
|
||||
sensitive: note.sensitive, // Noteがsensitiveなら添付もsensitiveにする
|
||||
}))
|
||||
))));
|
||||
|
||||
const cw = note.summary === '' ? null : note.summary;
|
||||
|
||||
// テキストのパース
|
||||
let text: string | null = null;
|
||||
if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') {
|
||||
text = note.source.content;
|
||||
} else if (typeof note._misskey_content !== 'undefined') {
|
||||
text = note._misskey_content;
|
||||
} else if (typeof note.content === 'string') {
|
||||
text = this.apMfmService.htmlToMfm(note.content, note.tag);
|
||||
}
|
||||
|
||||
const apHashtags = extractApHashtags(note.tag);
|
||||
|
||||
const emojis = await this.extractEmojis(note.tag ?? [], actor.host).catch(e => {
|
||||
this.logger.info(`extractEmojis: ${e}`);
|
||||
return [];
|
||||
});
|
||||
|
||||
const apEmojis = emojis.map(emoji => emoji.name);
|
||||
|
||||
const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined);
|
||||
|
||||
try {
|
||||
return await this.noteUpdateService.update(actor, {
|
||||
updatedAt: note.updated ? new Date(note.updated) : null,
|
||||
files,
|
||||
name: note.name,
|
||||
cw,
|
||||
text,
|
||||
apHashtags,
|
||||
apEmojis,
|
||||
poll,
|
||||
}, target, silent);
|
||||
} catch (err: any) {
|
||||
this.logger.warn(`note update failed: ${err}`);
|
||||
return err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Noteを解決します。
|
||||
*
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export interface IObject {
|
|||
summary?: string;
|
||||
_misskey_summary?: string;
|
||||
published?: string;
|
||||
updated?: string;
|
||||
cc?: ApObject;
|
||||
to?: ApObject;
|
||||
attributedTo?: ApObject;
|
||||
|
|
|
|||
|
|
@ -65,21 +65,21 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
|
|||
this.followingsRepository.createQueryBuilder('following')
|
||||
.select('COUNT(DISTINCT following.followeeHost)')
|
||||
.where('following.followeeHost IS NOT NULL')
|
||||
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT IN (:...blocked)', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||
.andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
|
||||
.getRawOne()
|
||||
.then(x => parseInt(x.count, 10)),
|
||||
this.followingsRepository.createQueryBuilder('following')
|
||||
.select('COUNT(DISTINCT following.followerHost)')
|
||||
.where('following.followerHost IS NOT NULL')
|
||||
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT IN (:...blocked)', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||
.andWhere(`following.followerHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
|
||||
.getRawOne()
|
||||
.then(x => parseInt(x.count, 10)),
|
||||
this.followingsRepository.createQueryBuilder('following')
|
||||
.select('COUNT(DISTINCT following.followeeHost)')
|
||||
.where('following.followeeHost IS NOT NULL')
|
||||
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT IN (:...blocked)', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||
.andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`)
|
||||
.andWhere(`following.followeeHost IN (${ pubsubSubQuery.getQuery() })`)
|
||||
.setParameters(pubsubSubQuery.getParameters())
|
||||
|
|
@ -88,7 +88,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
|
|||
this.instancesRepository.createQueryBuilder('instance')
|
||||
.select('COUNT(instance.id)')
|
||||
.where(`instance.host IN (${ subInstancesQuery.getQuery() })`)
|
||||
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT IN (:...blocked)', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||
.andWhere('instance.suspensionState = \'none\'')
|
||||
.andWhere('instance.isNotResponding = false')
|
||||
.getRawOne()
|
||||
|
|
@ -96,7 +96,7 @@ export default class FederationChart extends Chart<typeof schema> { // eslint-di
|
|||
this.instancesRepository.createQueryBuilder('instance')
|
||||
.select('COUNT(instance.id)')
|
||||
.where(`instance.host IN (${ pubInstancesQuery.getQuery() })`)
|
||||
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ANY(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||
.andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT IN (:...blocked)', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) })
|
||||
.andWhere('instance.suspensionState = \'none\'')
|
||||
.andWhere('instance.isNotResponding = false')
|
||||
.getRawOne()
|
||||
|
|
|
|||
|
|
@ -4,12 +4,15 @@
|
|||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { In } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { AbuseUserReportsRepository } from '@/models/_.js';
|
||||
import type { AbuseUserReportsRepository, NotesRepository } from '@/models/_.js';
|
||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||
import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { UserEntityService } from './UserEntityService.js';
|
||||
|
||||
|
|
@ -19,7 +22,11 @@ export class AbuseUserReportEntityService {
|
|||
@Inject(DI.abuseUserReportsRepository)
|
||||
private abuseUserReportsRepository: AbuseUserReportsRepository,
|
||||
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private noteEntityService: NoteEntityService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
}
|
||||
|
|
@ -34,11 +41,27 @@ export class AbuseUserReportEntityService {
|
|||
},
|
||||
) {
|
||||
const report = typeof src === 'object' ? src : await this.abuseUserReportsRepository.findOneByOrFail({ id: src });
|
||||
const notes = [];
|
||||
|
||||
if (report.noteIds && report.noteIds.length > 0) {
|
||||
for (const x of report.noteIds) {
|
||||
const exists = await this.notesRepository.countBy({ id: x });
|
||||
if (exists === 0) {
|
||||
notes.push('deleted');
|
||||
continue;
|
||||
}
|
||||
notes.push(await this.noteEntityService.pack(x));
|
||||
}
|
||||
} else if (report.notes.length > 0) {
|
||||
notes.push(...(report.notes));
|
||||
}
|
||||
|
||||
console.log(report.notes.length, null, notes);
|
||||
return await awaitAll({
|
||||
id: report.id,
|
||||
createdAt: this.idService.parse(report.id).date.toISOString(),
|
||||
comment: report.comment,
|
||||
notes,
|
||||
resolved: report.resolved,
|
||||
reporterId: report.reporterId,
|
||||
targetUserId: report.targetUserId,
|
||||
|
|
|
|||
|
|
@ -132,7 +132,10 @@ export class DriveFileEntityService {
|
|||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getFromUrl(url: string): Promise<MiDriveFile | null> {
|
||||
return this.driveFilesRepository.findOneBy({ url: url });
|
||||
}
|
||||
@bindThis
|
||||
public async calcDriveUsageOf(user: MiUser['id'] | { id: MiUser['id'] }): Promise<number> {
|
||||
const id = typeof user === 'object' ? user.id : user;
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ export class EmojiEntityService {
|
|||
localOnly: emoji.localOnly ? true : undefined,
|
||||
isSensitive: emoji.isSensitive ? true : undefined,
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length > 0 ? emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : undefined,
|
||||
draft: emoji.draft,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -62,6 +63,7 @@ export class EmojiEntityService {
|
|||
isSensitive: emoji.isSensitive,
|
||||
localOnly: emoji.localOnly,
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: emoji.roleIdsThatCanBeUsedThisEmojiAsReaction,
|
||||
draft: emoji.draft,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { EmojiRequestsRepository } from '@/models/_.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MiEmojiRequest } from '@/models/EmojiRequest.js';
|
||||
|
||||
@Injectable()
|
||||
export class EmojiRequestsEntityService {
|
||||
constructor(
|
||||
@Inject(DI.emojiRequestsRepository)
|
||||
private emojiRequestsRepository: EmojiRequestsRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packSimple(
|
||||
src: MiEmojiRequest['id'] | MiEmojiRequest,
|
||||
): Promise<Packed<'EmojiRequestSimple'>> {
|
||||
const emoji = typeof src === 'object' ? src : await this.emojiRequestsRepository.findOneByOrFail({ id: src });
|
||||
|
||||
return {
|
||||
aliases: emoji.aliases,
|
||||
name: emoji.name,
|
||||
category: emoji.category,
|
||||
// || emoji.originalUrl してるのは後方互換性のため(publicUrlはstringなので??はだめ)
|
||||
url: emoji.publicUrl,
|
||||
isSensitive: emoji.isSensitive ? true : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public packSimpleMany(
|
||||
emojis: any[],
|
||||
) {
|
||||
return Promise.all(emojis.map(x => this.packSimple(x)));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packDetailed(
|
||||
src: MiEmojiRequest['id'] | MiEmojiRequest,
|
||||
): Promise<Packed<'EmojiRequestDetailed'>> {
|
||||
const emoji = typeof src === 'object' ? src : await this.emojiRequestsRepository.findOneByOrFail({ id: src });
|
||||
|
||||
return {
|
||||
id: emoji.id,
|
||||
aliases: emoji.aliases,
|
||||
name: emoji.name,
|
||||
category: emoji.category,
|
||||
url: emoji.publicUrl,
|
||||
license: emoji.license,
|
||||
isSensitive: emoji.isSensitive,
|
||||
localOnly: emoji.localOnly,
|
||||
fileId: emoji.fileId,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public packDetailedMany(
|
||||
emojis: any[],
|
||||
) {
|
||||
return Promise.all(emojis.map(x => this.packDetailed(x)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -70,6 +70,11 @@ export class MetaEntityService {
|
|||
inquiryUrl: instance.inquiryUrl,
|
||||
disableRegistration: instance.disableRegistration,
|
||||
emailRequiredForSignup: instance.emailRequiredForSignup,
|
||||
bannerDark: instance.bannerDark,
|
||||
bannerLight: instance.bannerLight,
|
||||
iconDark: instance.iconDark,
|
||||
iconLight: instance.iconLight,
|
||||
googleAnalyticsId: instance.googleAnalyticsId,
|
||||
enableHcaptcha: instance.enableHcaptcha,
|
||||
hcaptchaSiteKey: instance.hcaptchaSiteKey,
|
||||
enableMcaptcha: instance.enableMcaptcha,
|
||||
|
|
@ -85,6 +90,7 @@ export class MetaEntityService {
|
|||
bannerUrl: instance.bannerUrl,
|
||||
infoImageUrl: instance.infoImageUrl,
|
||||
serverErrorImageUrl: instance.serverErrorImageUrl,
|
||||
googleAnalyticsId: instance.googleAnalyticsId,
|
||||
notFoundImageUrl: instance.notFoundImageUrl,
|
||||
iconUrl: instance.iconUrl,
|
||||
backgroundImageUrl: instance.backgroundImageUrl,
|
||||
|
|
|
|||
|
|
@ -208,6 +208,30 @@ export class NoteEntityService implements OnModuleInit {
|
|||
|
||||
return undefined;
|
||||
}
|
||||
@bindThis
|
||||
public async populateMyReactions(note: { id: MiNote['id']; reactions: MiNote['reactions']; reactionAndUserPairCache?: MiNote['reactionAndUserPairCache']; }, meId: MiUser['id'], _hint_?: {
|
||||
myReactions: Map<MiNote['id'], string | null>;
|
||||
}) {
|
||||
const reactionsCount = Object.values(note.reactions).reduce((a, b) => a + b, 0);
|
||||
|
||||
if (reactionsCount === 0) return undefined;
|
||||
|
||||
// パフォーマンスのためノートが作成されてから2秒以上経っていない場合はリアクションを取得しない
|
||||
if (this.idService.parse(note.id).date.getTime() + 2000 > Date.now()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const reactions = await this.noteReactionsRepository.findBy({
|
||||
userId: meId,
|
||||
noteId: note.id,
|
||||
});
|
||||
|
||||
if (reactions.length > 0) {
|
||||
return reactions.map(reaction => this.reactionService.convertLegacyReaction(reaction.reaction));
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async isVisibleForMe(note: MiNote, meId: MiUser['id'] | null): Promise<boolean> {
|
||||
|
|
@ -324,6 +348,9 @@ export class NoteEntityService implements OnModuleInit {
|
|||
const packed: Packed<'Note'> = await awaitAll({
|
||||
id: note.id,
|
||||
createdAt: this.idService.parse(note.id).date.toISOString(),
|
||||
updatedAt: note.updatedAt ? note.updatedAt.toISOString() : undefined,
|
||||
updatedAtHistory: note.updatedAtHistory ? note.updatedAtHistory.map(x => x.toISOString()) : undefined,
|
||||
noteEditHistory: note.noteEditHistory.length ? note.noteEditHistory : undefined,
|
||||
userId: note.userId,
|
||||
user: packedUsers?.get(note.userId) ?? this.userEntityService.pack(note.user ?? note.userId, me),
|
||||
text: text,
|
||||
|
|
@ -378,6 +405,7 @@ export class NoteEntityService implements OnModuleInit {
|
|||
|
||||
...(meId && Object.keys(note.reactions).length > 0 ? {
|
||||
myReaction: this.populateMyReaction(note, meId, options?._hint_),
|
||||
myReactions: this.populateMyReactions(note, meId, options?._hint_),
|
||||
} : {}),
|
||||
} : {}),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -162,6 +162,9 @@ export class NotificationEntityService implements OnModuleInit {
|
|||
...(notification.type === 'achievementEarned' ? {
|
||||
achievement: notification.achievement,
|
||||
} : {}),
|
||||
...(notification.type === 'loginbonus' ? {
|
||||
loginbonus: notification.loginbonus,
|
||||
} : {}),
|
||||
...(notification.type === 'app' ? {
|
||||
body: notification.customBody,
|
||||
header: notification.customHeader,
|
||||
|
|
|
|||
|
|
@ -470,9 +470,8 @@ export class UserEntityService implements OnModuleInit {
|
|||
createdAt: this.idService.parse(announcement.id).date.toISOString(),
|
||||
...announcement,
|
||||
})) : null;
|
||||
|
||||
console.log(user.getPoints);
|
||||
const notificationsInfo = isMe && isDetailed ? await this.getNotificationsInfo(user.id) : null;
|
||||
|
||||
const packed = {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
|
|
@ -506,7 +505,7 @@ export class UserEntityService implements OnModuleInit {
|
|||
iconUrl: r.iconUrl,
|
||||
displayOrder: r.displayOrder,
|
||||
}))) : undefined,
|
||||
|
||||
...(user.host == null ? { getPoints: user.getPoints } : {}),
|
||||
...(isDetailed ? {
|
||||
url: profile!.url,
|
||||
uri: user.uri,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue