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:
mattyatea 2024-07-15 14:59:54 +09:00
commit 71382a6f85
297 changed files with 60420 additions and 4574 deletions

View file

@ -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);

View file

@ -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,

View file

@ -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() });

View file

@ -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);

View file

@ -96,6 +96,7 @@ export class FanoutTimelineEndpointService {
if (ps.me) {
const me = ps.me;
const [
userIdsWhoMeMuting,
userIdsWhoMeMutingRenotes,

View file

@ -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[];
};
}

View file

@ -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);
}
}

View file

@ -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) {

View 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();
}
}

View file

@ -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(),

View file

@ -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,

View file

@ -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);

View file

@ -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)),
};
}

View file

@ -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

View file

@ -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,

View file

@ -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を解決します
*

View file

@ -14,6 +14,7 @@ export interface IObject {
summary?: string;
_misskey_summary?: string;
published?: string;
updated?: string;
cc?: ApObject;
to?: ApObject;
attributedTo?: ApObject;

View file

@ -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()

View file

@ -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,

View file

@ -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;

View file

@ -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,
};
}

View file

@ -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)));
}
}

View file

@ -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,

View file

@ -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_),
} : {}),
} : {}),
});

View file

@ -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,

View file

@ -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,