merge: upstream changes for 2024.11 (!742)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/742 Closes #645 and #646 Approved-by: Hazelnoot <acomputerdog@gmail.com> Approved-by: Marie <github@yuugi.dev>
This commit is contained in:
commit
e2352839e4
616 changed files with 16825 additions and 7991 deletions
|
|
@ -22,6 +22,7 @@ import { RoleService } from '@/core/RoleService.js';
|
|||
import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { IdService } from './IdService.js';
|
||||
|
||||
@Injectable()
|
||||
|
|
@ -42,6 +43,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
|
|||
private emailService: EmailService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
private globalEventService: GlobalEventService,
|
||||
private userEntityService: UserEntityService,
|
||||
) {
|
||||
this.redisForSub.on('message', this.onMessage);
|
||||
}
|
||||
|
|
@ -59,7 +61,10 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
|
|||
return;
|
||||
}
|
||||
|
||||
const moderatorIds = await this.roleService.getModeratorIds(true, true);
|
||||
const moderatorIds = await this.roleService.getModeratorIds({
|
||||
includeAdmins: true,
|
||||
excludeExpire: true,
|
||||
});
|
||||
|
||||
for (const moderatorId of moderatorIds) {
|
||||
for (const abuseReport of abuseReports) {
|
||||
|
|
@ -135,6 +140,26 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
|
|||
return;
|
||||
}
|
||||
|
||||
const usersMap = await this.userEntityService.packMany(
|
||||
[
|
||||
...new Set([
|
||||
...abuseReports.map(it => it.reporter ?? it.reporterId),
|
||||
...abuseReports.map(it => it.targetUser ?? it.targetUserId),
|
||||
...abuseReports.map(it => it.assignee ?? it.assigneeId),
|
||||
].filter(x => x != null)),
|
||||
],
|
||||
null,
|
||||
{ schema: 'UserLite' },
|
||||
).then(it => new Map(it.map(it => [it.id, it])));
|
||||
const convertedReports = abuseReports.map(it => {
|
||||
return {
|
||||
...it,
|
||||
reporter: usersMap.get(it.reporterId) ?? null,
|
||||
targetUser: usersMap.get(it.targetUserId) ?? null,
|
||||
assignee: it.assigneeId ? (usersMap.get(it.assigneeId) ?? null) : null,
|
||||
};
|
||||
});
|
||||
|
||||
const recipientWebhookIds = await this.fetchWebhookRecipients()
|
||||
.then(it => it
|
||||
.filter(it => it.isActive && it.systemWebhookId && it.method === 'webhook')
|
||||
|
|
@ -142,7 +167,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
|
|||
.filter(x => x != null));
|
||||
for (const webhookId of recipientWebhookIds) {
|
||||
await Promise.all(
|
||||
abuseReports.map(it => {
|
||||
convertedReports.map(it => {
|
||||
return this.systemWebhookService.enqueueSystemWebhook(
|
||||
webhookId,
|
||||
type,
|
||||
|
|
@ -263,8 +288,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
|
|||
.log(updater, 'createAbuseReportNotificationRecipient', {
|
||||
recipientId: id,
|
||||
recipient: created,
|
||||
})
|
||||
.then();
|
||||
});
|
||||
|
||||
return created;
|
||||
}
|
||||
|
|
@ -302,8 +326,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
|
|||
recipientId: params.id,
|
||||
before: beforeEntity,
|
||||
after: afterEntity,
|
||||
})
|
||||
.then();
|
||||
});
|
||||
|
||||
return afterEntity;
|
||||
}
|
||||
|
|
@ -324,8 +347,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
|
|||
.log(updater, 'deleteAbuseReportNotificationRecipient', {
|
||||
recipientId: id,
|
||||
recipient: entity,
|
||||
})
|
||||
.then();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -348,7 +370,10 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
|
|||
}
|
||||
|
||||
// モデレータ権限の有無で通知先設定を振り分ける
|
||||
const authorizedUserIds = await this.roleService.getModeratorIds(true, true);
|
||||
const authorizedUserIds = await this.roleService.getModeratorIds({
|
||||
includeAdmins: true,
|
||||
excludeExpire: true,
|
||||
});
|
||||
const authorizedUserRecipients = Array.of<MiAbuseReportNotificationRecipient>();
|
||||
const unauthorizedUserRecipients = Array.of<MiAbuseReportNotificationRecipient>();
|
||||
for (const recipient of userRecipients) {
|
||||
|
|
|
|||
|
|
@ -20,8 +20,10 @@ export class AbuseReportService {
|
|||
constructor(
|
||||
@Inject(DI.abuseUserReportsRepository)
|
||||
private abuseUserReportsRepository: AbuseUserReportsRepository,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
private idService: IdService,
|
||||
private abuseReportNotificationService: AbuseReportNotificationService,
|
||||
private queueService: QueueService,
|
||||
|
|
@ -77,62 +79,98 @@ export class AbuseReportService {
|
|||
* - SystemWebhook
|
||||
*
|
||||
* @param params 通報内容. もし複数件の通報に対応した時のために、あらかじめ複数件を処理できる前提で考える
|
||||
* @param operator 通報を処理したユーザ
|
||||
* @param moderator 通報を処理したユーザ
|
||||
* @see AbuseReportNotificationService.notify
|
||||
*/
|
||||
@bindThis
|
||||
public async resolve(
|
||||
params: {
|
||||
reportId: string;
|
||||
forward: boolean;
|
||||
resolvedAs: MiAbuseUserReport['resolvedAs'];
|
||||
}[],
|
||||
operator: MiUser,
|
||||
moderator: MiUser,
|
||||
) {
|
||||
const paramsMap = new Map(params.map(it => [it.reportId, it]));
|
||||
const reports = await this.abuseUserReportsRepository.findBy({
|
||||
id: In(params.map(it => it.reportId)),
|
||||
});
|
||||
|
||||
const targetUserMap = new Map();
|
||||
for (const report of reports) {
|
||||
const shouldForward = paramsMap.get(report.id)!.forward;
|
||||
|
||||
if (shouldForward && report.targetUserHost != null) {
|
||||
targetUserMap.set(report.id, await this.usersRepository.findOneByOrFail({ id: report.targetUserId }));
|
||||
} else {
|
||||
targetUserMap.set(report.id, null);
|
||||
}
|
||||
}
|
||||
|
||||
for (const report of reports) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const ps = paramsMap.get(report.id)!;
|
||||
|
||||
await this.abuseUserReportsRepository.update(report.id, {
|
||||
resolved: true,
|
||||
assigneeId: operator.id,
|
||||
forwarded: ps.forward && report.targetUserHost !== null,
|
||||
assigneeId: moderator.id,
|
||||
resolvedAs: ps.resolvedAs,
|
||||
});
|
||||
|
||||
const targetUser = targetUserMap.get(report.id)!;
|
||||
if (targetUser != null) {
|
||||
const actor = await this.instanceActorService.getInstanceActor();
|
||||
|
||||
// eslint-disable-next-line
|
||||
const flag = this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment);
|
||||
const contextAssignedFlag = this.apRendererService.addContext(flag);
|
||||
this.queueService.deliver(actor, contextAssignedFlag, targetUser.inbox, false);
|
||||
}
|
||||
|
||||
this.moderationLogService
|
||||
.log(operator, 'resolveAbuseReport', {
|
||||
.log(moderator, 'resolveAbuseReport', {
|
||||
reportId: report.id,
|
||||
report: report,
|
||||
forwarded: ps.forward && report.targetUserHost !== null,
|
||||
resolvedAs: ps.resolvedAs,
|
||||
});
|
||||
}
|
||||
|
||||
return this.abuseUserReportsRepository.findBy({ id: In(reports.map(it => it.id)) })
|
||||
.then(reports => this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReportResolved'));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async forward(
|
||||
reportId: MiAbuseUserReport['id'],
|
||||
moderator: MiUser,
|
||||
) {
|
||||
const report = await this.abuseUserReportsRepository.findOneByOrFail({ id: reportId });
|
||||
|
||||
if (report.targetUserHost == null) {
|
||||
throw new Error('The target user host is null.');
|
||||
}
|
||||
|
||||
if (report.forwarded) {
|
||||
throw new Error('The report has already been forwarded.');
|
||||
}
|
||||
|
||||
await this.abuseUserReportsRepository.update(report.id, {
|
||||
forwarded: true,
|
||||
});
|
||||
|
||||
const actor = await this.instanceActorService.getInstanceActor();
|
||||
const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId });
|
||||
|
||||
const flag = this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment);
|
||||
const contextAssignedFlag = this.apRendererService.addContext(flag);
|
||||
this.queueService.deliver(actor, contextAssignedFlag, targetUser.inbox, false);
|
||||
|
||||
this.moderationLogService
|
||||
.log(moderator, 'forwardAbuseReport', {
|
||||
reportId: report.id,
|
||||
report: report,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async update(
|
||||
reportId: MiAbuseUserReport['id'],
|
||||
params: {
|
||||
moderationNote?: MiAbuseUserReport['moderationNote'];
|
||||
},
|
||||
moderator: MiUser,
|
||||
) {
|
||||
const report = await this.abuseUserReportsRepository.findOneByOrFail({ id: reportId });
|
||||
|
||||
await this.abuseUserReportsRepository.update(report.id, {
|
||||
moderationNote: params.moderationNote,
|
||||
});
|
||||
|
||||
if (params.moderationNote != null && report.moderationNote !== params.moderationNote) {
|
||||
this.moderationLogService.log(moderator, 'updateAbuseReportNote', {
|
||||
reportId: report.id,
|
||||
report: report,
|
||||
before: report.moderationNote,
|
||||
after: params.moderationNote,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -274,13 +274,15 @@ export class AccountMoveService {
|
|||
}
|
||||
|
||||
// Update instance stats by decreasing remote followers count by the number of local followers who were following the old account.
|
||||
if (this.userEntityService.isRemoteUser(oldAccount)) {
|
||||
this.federatedInstanceService.fetch(oldAccount.host).then(async i => {
|
||||
this.instancesRepository.decrement({ id: i.id }, 'followersCount', localFollowerIds.length);
|
||||
if (this.meta.enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateFollowers(i.host, false);
|
||||
}
|
||||
});
|
||||
if (this.meta.enableStatsForFederatedInstances) {
|
||||
if (this.userEntityService.isRemoteUser(oldAccount)) {
|
||||
this.federatedInstanceService.fetchOrRegister(oldAccount.host).then(async i => {
|
||||
this.instancesRepository.decrement({ id: i.id }, 'followersCount', localFollowerIds.length);
|
||||
if (this.meta.enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateFollowers(i.host, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: expensive?
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ export class AnnouncementService {
|
|||
updatedAt: null,
|
||||
title: values.title,
|
||||
text: values.text,
|
||||
imageUrl: values.imageUrl,
|
||||
imageUrl: values.imageUrl || null,
|
||||
icon: values.icon,
|
||||
display: values.display,
|
||||
forExistingUsers: values.forExistingUsers,
|
||||
|
|
@ -209,6 +209,13 @@ export class AnnouncementService {
|
|||
return;
|
||||
}
|
||||
|
||||
const announcement = await this.announcementsRepository.findOneBy({ id: announcementId });
|
||||
if (announcement != null && announcement.userId === user.id) {
|
||||
await this.announcementsRepository.update(announcementId, {
|
||||
isActive: false,
|
||||
});
|
||||
}
|
||||
|
||||
if ((await this.getUnreadAnnouncements(user)).length === 0) {
|
||||
this.globalEventService.publishMainStream(user.id, 'readAllAnnouncements');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -149,5 +149,18 @@ export class CaptchaService {
|
|||
throw new Error(`turnstile-failed: ${errorCodes}`);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async verifyTestcaptcha(response: string | null | undefined): Promise<void> {
|
||||
if (response == null) {
|
||||
throw new Error('testcaptcha-failed: no response provided');
|
||||
}
|
||||
|
||||
const success = response === 'testcaptcha-passed';
|
||||
|
||||
if (!success) {
|
||||
throw new Error('testcaptcha-failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationSe
|
|||
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||
import { UserSearchService } from '@/core/UserSearchService.js';
|
||||
import { WebhookTestService } from '@/core/WebhookTestService.js';
|
||||
import { FlashService } from '@/core/FlashService.js';
|
||||
import { TimeService } from '@/core/TimeService.js';
|
||||
import { EnvService } from '@/core/EnvService.js';
|
||||
import { AccountMoveService } from './AccountMoveService.js';
|
||||
|
|
@ -222,6 +223,7 @@ const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useEx
|
|||
const $WebhookTestService: Provider = { provide: 'WebhookTestService', useExisting: WebhookTestService };
|
||||
const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
|
||||
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
|
||||
const $FlashService: Provider = { provide: 'FlashService', useExisting: FlashService };
|
||||
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
|
||||
const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService };
|
||||
const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: FeaturedService };
|
||||
|
|
@ -375,6 +377,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
|
|||
WebhookTestService,
|
||||
UtilityService,
|
||||
FileInfoService,
|
||||
FlashService,
|
||||
SearchService,
|
||||
ClipService,
|
||||
FeaturedService,
|
||||
|
|
@ -526,6 +529,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
|
|||
$WebhookTestService,
|
||||
$UtilityService,
|
||||
$FileInfoService,
|
||||
$FlashService,
|
||||
$SearchService,
|
||||
$ClipService,
|
||||
$FeaturedService,
|
||||
|
|
@ -676,6 +680,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
|
|||
WebhookTestService,
|
||||
UtilityService,
|
||||
FileInfoService,
|
||||
FlashService,
|
||||
SearchService,
|
||||
ClipService,
|
||||
FeaturedService,
|
||||
|
|
|
|||
|
|
@ -112,19 +112,33 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async update(id: MiEmoji['id'], data: {
|
||||
public async update(data: (
|
||||
{ id: MiEmoji['id'], name?: string; } | { name: string; id?: MiEmoji['id'], }
|
||||
) & {
|
||||
driveFile?: MiDriveFile;
|
||||
name?: string;
|
||||
category?: string | null;
|
||||
aliases?: string[];
|
||||
license?: string | null;
|
||||
isSensitive?: boolean;
|
||||
localOnly?: boolean;
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction?: MiRole['id'][];
|
||||
}, moderator?: MiUser): Promise<void> {
|
||||
const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
|
||||
const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() });
|
||||
if (sameNameEmoji != null && sameNameEmoji.id !== id) throw new Error('name already exists');
|
||||
}, moderator?: MiUser): Promise<
|
||||
null
|
||||
| 'NO_SUCH_EMOJI'
|
||||
| 'SAME_NAME_EMOJI_EXISTS'
|
||||
> {
|
||||
const emoji = data.id
|
||||
? await this.getEmojiById(data.id)
|
||||
: await this.getEmojiByName(data.name!);
|
||||
if (emoji === null) return 'NO_SUCH_EMOJI';
|
||||
const id = emoji.id;
|
||||
|
||||
// IDと絵文字名が両方指定されている場合は絵文字名の変更を行うため重複チェックが必要
|
||||
const doNameUpdate = data.id && data.name && (data.name !== emoji.name);
|
||||
if (doNameUpdate) {
|
||||
const isDuplicate = await this.checkDuplicate(data.name!);
|
||||
if (isDuplicate) return 'SAME_NAME_EMOJI_EXISTS';
|
||||
}
|
||||
|
||||
await this.emojisRepository.update(emoji.id, {
|
||||
updatedAt: new Date(),
|
||||
|
|
@ -151,7 +165,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||
|
||||
const packed = await this.emojiEntityService.packDetailed(emoji.id);
|
||||
|
||||
if (emoji.name === data.name) {
|
||||
if (!doNameUpdate) {
|
||||
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
||||
emojis: [packed],
|
||||
});
|
||||
|
|
@ -173,6 +187,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||
after: updated,
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export class FederatedInstanceService implements OnApplicationShutdown {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async fetch(host: string): Promise<MiInstance> {
|
||||
public async fetchOrRegister(host: string): Promise<MiInstance> {
|
||||
host = this.utilityService.toPuny(host);
|
||||
|
||||
const cached = await this.federatedInstanceCache.get(host);
|
||||
|
|
@ -85,6 +85,24 @@ export class FederatedInstanceService implements OnApplicationShutdown {
|
|||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async fetch(host: string): Promise<MiInstance | null> {
|
||||
host = this.utilityService.toPuny(host);
|
||||
|
||||
const cached = await this.federatedInstanceCache.get(host);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
const index = await this.instancesRepository.findOneBy({ host });
|
||||
|
||||
if (index == null) {
|
||||
this.federatedInstanceCache.set(host, null);
|
||||
return null;
|
||||
} else {
|
||||
this.federatedInstanceCache.set(host, index);
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async update(id: MiInstance['id'], data: Partial<MiInstance>): Promise<void> {
|
||||
const result = await this.instancesRepository.createQueryBuilder().update()
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ export class FetchInstanceMetadataService {
|
|||
|
||||
try {
|
||||
if (!force) {
|
||||
const _instance = await this.federatedInstanceService.fetch(host);
|
||||
const _instance = await this.federatedInstanceService.fetchOrRegister(host);
|
||||
const now = Date.now();
|
||||
if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24)) {
|
||||
// unlock at the finally caluse
|
||||
|
|
|
|||
40
packages/backend/src/core/FlashService.ts
Normal file
40
packages/backend/src/core/FlashService.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { type FlashsRepository } from '@/models/_.js';
|
||||
|
||||
/**
|
||||
* MisskeyPlay関係のService
|
||||
*/
|
||||
@Injectable()
|
||||
export class FlashService {
|
||||
constructor(
|
||||
@Inject(DI.flashsRepository)
|
||||
private flashRepository: FlashsRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 人気のあるPlay一覧を取得する.
|
||||
*/
|
||||
public async featured(opts?: { offset?: number, limit: number }) {
|
||||
const builder = this.flashRepository.createQueryBuilder('flash')
|
||||
.andWhere('flash.likedCount > 0')
|
||||
.andWhere('flash.visibility = :visibility', { visibility: 'public' })
|
||||
.addOrderBy('flash.likedCount', 'DESC')
|
||||
.addOrderBy('flash.updatedAt', 'DESC')
|
||||
.addOrderBy('flash.id', 'DESC');
|
||||
|
||||
if (opts?.offset) {
|
||||
builder.skip(opts.offset);
|
||||
}
|
||||
|
||||
builder.take(opts?.limit ?? 10);
|
||||
|
||||
return await builder.getMany();
|
||||
}
|
||||
}
|
||||
|
|
@ -51,13 +51,13 @@ import { FeaturedService } from '@/core/FeaturedService.js';
|
|||
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { isReply } from '@/misc/is-reply.js';
|
||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||
import { isUserRelated } from '@/misc/is-user-related.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { LatestNoteService } from '@/core/LatestNoteService.js';
|
||||
import { CollapsedQueue } from '@/misc/collapsed-queue.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
|
||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||
|
||||
|
|
@ -224,7 +224,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
private cacheService: CacheService,
|
||||
private latestNoteService: LatestNoteService,
|
||||
) {
|
||||
this.updateNotesCountQueue = new CollapsedQueue(60 * 1000 * 5, this.collapseNotesCount, this.performUpdateNotesCount);
|
||||
this.updateNotesCountQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseNotesCount, this.performUpdateNotesCount);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
@ -413,7 +413,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
}
|
||||
|
||||
if (user.host && !data.cw) {
|
||||
await this.federatedInstanceService.fetch(user.host).then(async i => {
|
||||
await this.federatedInstanceService.fetchOrRegister(user.host).then(async i => {
|
||||
if (i.isNSFW && !this.isPureRenote(data)) {
|
||||
data.cw = 'Instance is marked as NSFW';
|
||||
}
|
||||
|
|
@ -565,17 +565,17 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
}
|
||||
|
||||
// Register host
|
||||
if (this.userEntityService.isRemoteUser(user)) {
|
||||
this.federatedInstanceService.fetch(user.host).then(async i => {
|
||||
if (note.renote && note.text) {
|
||||
this.updateNotesCountQueue.enqueue(i.id, 1);
|
||||
} else if (!note.renote) {
|
||||
this.updateNotesCountQueue.enqueue(i.id, 1);
|
||||
}
|
||||
if (this.meta.enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateNote(i.host, note, true);
|
||||
}
|
||||
});
|
||||
if (this.meta.enableStatsForFederatedInstances) {
|
||||
if (this.userEntityService.isRemoteUser(user)) {
|
||||
this.federatedInstanceService.fetchOrRegister(user.host).then(async i => {
|
||||
if (note.renote && note.text || !note.renote) {
|
||||
this.updateNotesCountQueue.enqueue(i.id, 1);
|
||||
}
|
||||
if (this.meta.enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateNote(i.host, note, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ハッシュタグ更新
|
||||
|
|
@ -608,13 +608,21 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
this.followingsRepository.findBy({
|
||||
followeeId: user.id,
|
||||
notify: 'normal',
|
||||
}).then(followings => {
|
||||
}).then(async followings => {
|
||||
if (note.visibility !== 'specified') {
|
||||
const isPureRenote = this.isRenote(data) && !this.isQuote(data) ? true : false;
|
||||
for (const following of followings) {
|
||||
// TODO: ワードミュート考慮
|
||||
this.notificationService.createNotification(following.followerId, 'note', {
|
||||
noteId: note.id,
|
||||
}, user.id);
|
||||
let isRenoteMuted = false;
|
||||
if (isPureRenote) {
|
||||
const userIdsWhoMeMutingRenotes = await this.cacheService.renoteMutingsCache.fetch(following.followerId);
|
||||
isRenoteMuted = userIdsWhoMeMutingRenotes.has(user.id);
|
||||
}
|
||||
if (!isRenoteMuted) {
|
||||
this.notificationService.createNotification(following.followerId, 'note', {
|
||||
noteId: note.id,
|
||||
}, user.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -115,25 +115,22 @@ export class NoteDeleteService {
|
|||
this.perUserNotesChart.update(user, note, false);
|
||||
}
|
||||
|
||||
if (note.renoteId && note.text) {
|
||||
// Decrement notes count (user)
|
||||
this.decNotesCountOfUser(user);
|
||||
} else if (!note.renoteId) {
|
||||
if (note.renoteId && note.text || !note.renoteId) {
|
||||
// Decrement notes count (user)
|
||||
this.decNotesCountOfUser(user);
|
||||
}
|
||||
|
||||
if (this.userEntityService.isRemoteUser(user)) {
|
||||
this.federatedInstanceService.fetch(user.host).then(async i => {
|
||||
if (note.renoteId && note.text) {
|
||||
this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1);
|
||||
} else if (!note.renoteId) {
|
||||
this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1);
|
||||
}
|
||||
if (this.meta.enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateNote(i.host, note, false);
|
||||
}
|
||||
});
|
||||
if (this.meta.enableStatsForFederatedInstances) {
|
||||
if (this.userEntityService.isRemoteUser(user)) {
|
||||
this.federatedInstanceService.fetchOrRegister(user.host).then(async i => {
|
||||
if (note.renoteId && note.text || !note.renoteId) {
|
||||
this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1);
|
||||
}
|
||||
if (this.meta.enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateNote(i.host, note, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -220,7 +220,7 @@ export class NoteEditService implements OnApplicationShutdown {
|
|||
private latestNoteService: LatestNoteService,
|
||||
private noteCreateService: NoteCreateService,
|
||||
) {
|
||||
this.updateNotesCountQueue = new CollapsedQueue(60 * 1000 * 5, this.collapseNotesCount, this.performUpdateNotesCount);
|
||||
this.updateNotesCountQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseNotesCount, this.performUpdateNotesCount);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
@ -441,7 +441,7 @@ export class NoteEditService implements OnApplicationShutdown {
|
|||
}
|
||||
|
||||
if (user.host && !data.cw) {
|
||||
await this.federatedInstanceService.fetch(user.host).then(async i => {
|
||||
await this.federatedInstanceService.fetchOrRegister(user.host).then(async i => {
|
||||
if (i.isNSFW && !this.noteCreateService.isPureRenote(data)) {
|
||||
data.cw = 'Instance is marked as NSFW';
|
||||
}
|
||||
|
|
@ -592,13 +592,17 @@ export class NoteEditService implements OnApplicationShutdown {
|
|||
noindex: MiUser['noindex'];
|
||||
}, data: Option, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) {
|
||||
// Register host
|
||||
if (this.userEntityService.isRemoteUser(user)) {
|
||||
this.federatedInstanceService.fetch(user.host).then(async i => {
|
||||
this.updateNotesCountQueue.enqueue(i.id, 1);
|
||||
if (this.meta.enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateNote(i.host, note, true);
|
||||
}
|
||||
});
|
||||
if (this.meta.enableStatsForFederatedInstances) {
|
||||
if (this.userEntityService.isRemoteUser(user)) {
|
||||
this.federatedInstanceService.fetchOrRegister(user.host).then(async i => {
|
||||
if (note.renote && note.text || !note.renote) {
|
||||
this.updateNotesCountQueue.enqueue(i.id, 1);
|
||||
}
|
||||
if (this.meta.enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateNote(i.host, note, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ハッシュタグ更新
|
||||
|
|
|
|||
|
|
@ -7,13 +7,15 @@ import { randomUUID } from 'node:crypto';
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { IActivity } from '@/core/activitypub/type.js';
|
||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import type { MiWebhook, webhookEventTypes } from '@/models/Webhook.js';
|
||||
import type { MiWebhook, WebhookEventTypes } from '@/models/Webhook.js';
|
||||
import type { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js';
|
||||
import type { Config } from '@/config.js';
|
||||
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 SystemWebhookPayload } from '@/core/SystemWebhookService.js';
|
||||
import { type UserWebhookPayload } from './UserWebhookService.js';
|
||||
import type {
|
||||
DbJobData,
|
||||
DeliverJobData,
|
||||
|
|
@ -30,8 +32,8 @@ import type {
|
|||
ObjectStorageQueue,
|
||||
RelationshipQueue,
|
||||
SystemQueue,
|
||||
UserWebhookDeliverQueue,
|
||||
SystemWebhookDeliverQueue,
|
||||
UserWebhookDeliverQueue,
|
||||
ScheduleNotePostQueue,
|
||||
} from './QueueModule.js';
|
||||
import type httpSignature from '@peertube/http-signature';
|
||||
|
|
@ -96,6 +98,13 @@ export class QueueService {
|
|||
repeat: { pattern: '0 0 * * *' },
|
||||
removeOnComplete: true,
|
||||
});
|
||||
|
||||
this.systemQueue.add('checkModeratorsActivity', {
|
||||
}, {
|
||||
// 毎時30分に起動
|
||||
repeat: { pattern: '30 * * * *' },
|
||||
removeOnComplete: true,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
@ -522,10 +531,10 @@ export class QueueService {
|
|||
* @see UserWebhookDeliverProcessorService
|
||||
*/
|
||||
@bindThis
|
||||
public userWebhookDeliver(
|
||||
public userWebhookDeliver<T extends WebhookEventTypes>(
|
||||
webhook: MiWebhook,
|
||||
type: typeof webhookEventTypes[number],
|
||||
content: unknown,
|
||||
type: T,
|
||||
content: UserWebhookPayload<T>,
|
||||
opts?: { attempts?: number },
|
||||
) {
|
||||
const data: UserWebhookDeliverJobData = {
|
||||
|
|
@ -554,10 +563,10 @@ export class QueueService {
|
|||
* @see SystemWebhookDeliverProcessorService
|
||||
*/
|
||||
@bindThis
|
||||
public systemWebhookDeliver(
|
||||
public systemWebhookDeliver<T extends SystemWebhookEventType>(
|
||||
webhook: MiSystemWebhook,
|
||||
type: SystemWebhookEventType,
|
||||
content: unknown,
|
||||
type: T,
|
||||
content: SystemWebhookPayload<T>,
|
||||
opts?: { attempts?: number },
|
||||
) {
|
||||
const data: SystemWebhookDeliverJobData = {
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
|||
|
||||
@Injectable()
|
||||
export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||
private rootUserIdCache: MemorySingleCache<MiUser['id']>;
|
||||
private rolesCache: MemorySingleCache<MiRole[]>;
|
||||
private roleAssignmentByUserIdCache: MemoryKVCache<MiRoleAssignment[]>;
|
||||
private notificationService: NotificationService;
|
||||
|
|
@ -142,6 +143,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||
private moderationLogService: ModerationLogService,
|
||||
private fanoutTimelineService: FanoutTimelineService,
|
||||
) {
|
||||
this.rootUserIdCache = new MemorySingleCache<MiUser['id']>(1000 * 60 * 60 * 24 * 7); // 1week. rootユーザのIDは不変なので長めに
|
||||
this.rolesCache = new MemorySingleCache<MiRole[]>(1000 * 60 * 60); // 1h
|
||||
this.roleAssignmentByUserIdCache = new MemoryKVCache<MiRoleAssignment[]>(1000 * 60 * 5); // 5m
|
||||
|
||||
|
|
@ -425,49 +427,78 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async isExplorable(role: { id: MiRole['id']} | null): Promise<boolean> {
|
||||
public async isExplorable(role: { id: MiRole['id'] } | null): Promise<boolean> {
|
||||
if (role == null) return false;
|
||||
const check = await this.rolesRepository.findOneBy({ id: role.id });
|
||||
if (check == null) return false;
|
||||
return check.isExplorable;
|
||||
}
|
||||
|
||||
/**
|
||||
* モデレーター権限のロールが割り当てられているユーザID一覧を取得する.
|
||||
*
|
||||
* @param opts.includeAdmins 管理者権限も含めるか(デフォルト: true)
|
||||
* @param opts.includeRoot rootユーザも含めるか(デフォルト: false)
|
||||
* @param opts.excludeExpire 期限切れのロールを除外するか(デフォルト: false)
|
||||
*/
|
||||
@bindThis
|
||||
public async getModeratorIds(includeAdmins = true, excludeExpire = false): Promise<MiUser['id'][]> {
|
||||
public async getModeratorIds(opts?: {
|
||||
includeAdmins?: boolean,
|
||||
includeRoot?: boolean,
|
||||
excludeExpire?: boolean,
|
||||
}): Promise<MiUser['id'][]> {
|
||||
const includeAdmins = opts?.includeAdmins ?? true;
|
||||
const includeRoot = opts?.includeRoot ?? false;
|
||||
const excludeExpire = opts?.excludeExpire ?? false;
|
||||
|
||||
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
|
||||
const moderatorRoles = includeAdmins
|
||||
? roles.filter(r => r.isModerator || r.isAdministrator)
|
||||
: roles.filter(r => r.isModerator);
|
||||
|
||||
// TODO: isRootなアカウントも含める
|
||||
const assigns = moderatorRoles.length > 0
|
||||
? await this.roleAssignmentsRepository.findBy({ roleId: In(moderatorRoles.map(r => r.id)) })
|
||||
: [];
|
||||
|
||||
// Setを経由して重複を除去(ユーザIDは重複する可能性があるので)
|
||||
const now = Date.now();
|
||||
const result = [
|
||||
// Setを経由して重複を除去(ユーザIDは重複する可能性があるので)
|
||||
...new Set(
|
||||
assigns
|
||||
.filter(it =>
|
||||
(excludeExpire)
|
||||
? (it.expiresAt == null || it.expiresAt.getTime() > now)
|
||||
: true,
|
||||
)
|
||||
.map(a => a.userId),
|
||||
),
|
||||
];
|
||||
const resultSet = new Set(
|
||||
assigns
|
||||
.filter(it =>
|
||||
(excludeExpire)
|
||||
? (it.expiresAt == null || it.expiresAt.getTime() > now)
|
||||
: true,
|
||||
)
|
||||
.map(a => a.userId),
|
||||
);
|
||||
|
||||
return result.sort((x, y) => x.localeCompare(y));
|
||||
if (includeRoot) {
|
||||
const rootUserId = await this.rootUserIdCache.fetch(async () => {
|
||||
const it = await this.usersRepository.createQueryBuilder('users')
|
||||
.select('id')
|
||||
.where({ isRoot: true })
|
||||
.getRawOne<{ id: string }>();
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return it!.id;
|
||||
});
|
||||
resultSet.add(rootUserId);
|
||||
}
|
||||
|
||||
return [...resultSet].sort((x, y) => x.localeCompare(y));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getModerators(includeAdmins = true): Promise<MiUser[]> {
|
||||
const ids = await this.getModeratorIds(includeAdmins);
|
||||
const users = ids.length > 0 ? await this.usersRepository.findBy({
|
||||
id: In(ids),
|
||||
}) : [];
|
||||
return users;
|
||||
public async getModerators(opts?: {
|
||||
includeAdmins?: boolean,
|
||||
includeRoot?: boolean,
|
||||
excludeExpire?: boolean,
|
||||
}): Promise<MiUser[]> {
|
||||
const ids = await this.getModeratorIds(opts);
|
||||
return ids.length > 0
|
||||
? await this.usersRepository.findBy({
|
||||
id: In(ids),
|
||||
})
|
||||
: [];
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
|
|||
|
|
@ -156,8 +156,8 @@ export class SignupService {
|
|||
}));
|
||||
});
|
||||
|
||||
this.usersChart.update(account, true).then();
|
||||
this.userService.notifySystemWebhook(account, 'userCreated').then();
|
||||
this.usersChart.update(account, true);
|
||||
this.userService.notifySystemWebhook(account, 'userCreated');
|
||||
|
||||
return { account, secret };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,8 +15,39 @@ import { QueueService } from '@/core/QueueService.js';
|
|||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import Logger from '@/logger.js';
|
||||
import { Packed } from '@/misc/json-schema.js';
|
||||
import { AbuseReportResolveType } from '@/models/AbuseUserReport.js';
|
||||
import { ModeratorInactivityRemainingTime } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
|
||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||
|
||||
export type AbuseReportPayload = {
|
||||
id: string;
|
||||
targetUserId: string;
|
||||
targetUser: Packed<'UserLite'> | null;
|
||||
targetUserHost: string | null;
|
||||
reporterId: string;
|
||||
reporter: Packed<'UserLite'> | null;
|
||||
reporterHost: string | null;
|
||||
assigneeId: string | null;
|
||||
assignee: Packed<'UserLite'> | null;
|
||||
resolved: boolean;
|
||||
forwarded: boolean;
|
||||
comment: string;
|
||||
moderationNote: string;
|
||||
resolvedAs: AbuseReportResolveType | null;
|
||||
};
|
||||
|
||||
export type InactiveModeratorsWarningPayload = {
|
||||
remainingTime: ModeratorInactivityRemainingTime;
|
||||
};
|
||||
|
||||
export type SystemWebhookPayload<T extends SystemWebhookEventType> =
|
||||
T extends 'abuseReport' | 'abuseReportResolved' ? AbuseReportPayload :
|
||||
T extends 'userCreated' ? Packed<'UserLite'> :
|
||||
T extends 'inactiveModeratorsWarning' ? InactiveModeratorsWarningPayload :
|
||||
T extends 'inactiveModeratorsInvitationOnlyChanged' ? Record<string, never> :
|
||||
never;
|
||||
|
||||
@Injectable()
|
||||
export class SystemWebhookService implements OnApplicationShutdown {
|
||||
private logger: Logger;
|
||||
|
|
@ -101,8 +132,7 @@ export class SystemWebhookService implements OnApplicationShutdown {
|
|||
.log(updater, 'createSystemWebhook', {
|
||||
systemWebhookId: webhook.id,
|
||||
webhook: webhook,
|
||||
})
|
||||
.then();
|
||||
});
|
||||
|
||||
return webhook;
|
||||
}
|
||||
|
|
@ -139,8 +169,7 @@ export class SystemWebhookService implements OnApplicationShutdown {
|
|||
systemWebhookId: beforeEntity.id,
|
||||
before: beforeEntity,
|
||||
after: afterEntity,
|
||||
})
|
||||
.then();
|
||||
});
|
||||
|
||||
return afterEntity;
|
||||
}
|
||||
|
|
@ -158,8 +187,7 @@ export class SystemWebhookService implements OnApplicationShutdown {
|
|||
.log(updater, 'deleteSystemWebhook', {
|
||||
systemWebhookId: webhook.id,
|
||||
webhook,
|
||||
})
|
||||
.then();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -171,7 +199,7 @@ export class SystemWebhookService implements OnApplicationShutdown {
|
|||
public async enqueueSystemWebhook<T extends SystemWebhookEventType>(
|
||||
webhook: MiSystemWebhook | MiSystemWebhook['id'],
|
||||
type: T,
|
||||
content: unknown,
|
||||
content: SystemWebhookPayload<T>,
|
||||
) {
|
||||
const webhookEntity = typeof webhook === 'string'
|
||||
? (await this.fetchActiveSystemWebhooks()).find(a => a.id === webhook)
|
||||
|
|
|
|||
|
|
@ -305,20 +305,22 @@ export class UserFollowingService implements OnModuleInit {
|
|||
//#endregion
|
||||
|
||||
//#region Update instance stats
|
||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||||
this.federatedInstanceService.fetch(follower.host).then(async i => {
|
||||
this.instancesRepository.increment({ id: i.id }, 'followingCount', 1);
|
||||
if (this.meta.enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateFollowing(i.host, true);
|
||||
}
|
||||
});
|
||||
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||||
this.federatedInstanceService.fetch(followee.host).then(async i => {
|
||||
this.instancesRepository.increment({ id: i.id }, 'followersCount', 1);
|
||||
if (this.meta.enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateFollowers(i.host, true);
|
||||
}
|
||||
});
|
||||
if (this.meta.enableStatsForFederatedInstances) {
|
||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||||
this.federatedInstanceService.fetchOrRegister(follower.host).then(async i => {
|
||||
this.instancesRepository.increment({ id: i.id }, 'followingCount', 1);
|
||||
if (this.meta.enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateFollowing(i.host, true);
|
||||
}
|
||||
});
|
||||
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||||
this.federatedInstanceService.fetchOrRegister(followee.host).then(async i => {
|
||||
this.instancesRepository.increment({ id: i.id }, 'followersCount', 1);
|
||||
if (this.meta.enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateFollowers(i.host, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
||||
|
|
@ -437,20 +439,22 @@ export class UserFollowingService implements OnModuleInit {
|
|||
//#endregion
|
||||
|
||||
//#region Update instance stats
|
||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||||
this.federatedInstanceService.fetch(follower.host).then(async i => {
|
||||
this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1);
|
||||
if (this.meta.enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateFollowing(i.host, false);
|
||||
}
|
||||
});
|
||||
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||||
this.federatedInstanceService.fetch(followee.host).then(async i => {
|
||||
this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1);
|
||||
if (this.meta.enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateFollowers(i.host, false);
|
||||
}
|
||||
});
|
||||
if (this.meta.enableStatsForFederatedInstances) {
|
||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||||
this.federatedInstanceService.fetchOrRegister(follower.host).then(async i => {
|
||||
this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1);
|
||||
if (this.meta.enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateFollowing(i.host, false);
|
||||
}
|
||||
});
|
||||
} else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||||
this.federatedInstanceService.fetchOrRegister(followee.host).then(async i => {
|
||||
this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1);
|
||||
if (this.meta.enableChartsForFederatedInstances) {
|
||||
this.instanceChart.updateFollowers(i.host, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
//#endregion
|
||||
|
||||
|
|
|
|||
|
|
@ -6,11 +6,23 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import * as Redis from 'ioredis';
|
||||
import { type WebhooksRepository } from '@/models/_.js';
|
||||
import { MiWebhook } from '@/models/Webhook.js';
|
||||
import { MiWebhook, WebhookEventTypes } from '@/models/Webhook.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { GlobalEvents } from '@/core/GlobalEventService.js';
|
||||
import type { OnApplicationShutdown } from '@nestjs/common';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
|
||||
export type UserWebhookPayload<T extends WebhookEventTypes> =
|
||||
T extends 'note' | 'reply' | 'renote' |'mention' | 'edited' ? {
|
||||
note: Packed<'Note'>,
|
||||
} :
|
||||
T extends 'follow' | 'unfollow' ? {
|
||||
user: Packed<'UserDetailedNotMe'>,
|
||||
} :
|
||||
T extends 'followed' ? {
|
||||
user: Packed<'UserLite'>,
|
||||
} : never;
|
||||
|
||||
@Injectable()
|
||||
export class UserWebhookService implements OnApplicationShutdown {
|
||||
|
|
|
|||
|
|
@ -123,6 +123,7 @@ export class UtilityService {
|
|||
return host;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private specialSuffix(hostname: string): string | null {
|
||||
// masto.host provides domain names for its clients, we have to
|
||||
// treat it as if it were a public suffix
|
||||
|
|
@ -143,6 +144,7 @@ export class UtilityService {
|
|||
return host;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public isFederationAllowedHost(host: string): boolean {
|
||||
if (this.meta.federation === 'none') return false;
|
||||
if (this.meta.federation === 'specified' && !this.meta.federationHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`))) return false;
|
||||
|
|
|
|||
|
|
@ -246,14 +246,12 @@ export class WebAuthnService {
|
|||
|
||||
@bindThis
|
||||
public async verifyAuthentication(userId: MiUser['id'], response: AuthenticationResponseJSON): Promise<boolean> {
|
||||
const challenge = await this.redisClient.get(`webauthn:challenge:${userId}`);
|
||||
const challenge = await this.redisClient.getdel(`webauthn:challenge:${userId}`);
|
||||
|
||||
if (!challenge) {
|
||||
throw new IdentifiableError('2d16e51c-007b-4edd-afd2-f7dd02c947f6', 'challenge not found');
|
||||
}
|
||||
|
||||
await this.redisClient.del(`webauthn:challenge:${userId}`);
|
||||
|
||||
const key = await this.userSecurityKeysRepository.findOneBy({
|
||||
id: response.id,
|
||||
userId: userId,
|
||||
|
|
|
|||
|
|
@ -7,16 +7,17 @@ import { Injectable } from '@nestjs/common';
|
|||
import { MiAbuseUserReport, MiNote, MiUser, MiWebhook } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWebhook.js';
|
||||
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||
import { AbuseReportPayload, SystemWebhookPayload, SystemWebhookService } from '@/core/SystemWebhookService.js';
|
||||
import { Packed } from '@/misc/json-schema.js';
|
||||
import { type WebhookEventTypes } from '@/models/Webhook.js';
|
||||
import { UserWebhookService } from '@/core/UserWebhookService.js';
|
||||
import { type UserWebhookPayload, UserWebhookService } from '@/core/UserWebhookService.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { ModeratorInactivityRemainingTime } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
|
||||
|
||||
const oneDayMillis = 24 * 60 * 60 * 1000;
|
||||
|
||||
function generateAbuseReport(override?: Partial<MiAbuseUserReport>): MiAbuseUserReport {
|
||||
return {
|
||||
function generateAbuseReport(override?: Partial<MiAbuseUserReport>): AbuseReportPayload {
|
||||
const result: MiAbuseUserReport = {
|
||||
id: 'dummy-abuse-report1',
|
||||
targetUserId: 'dummy-target-user',
|
||||
targetUser: null,
|
||||
|
|
@ -29,8 +30,17 @@ function generateAbuseReport(override?: Partial<MiAbuseUserReport>): MiAbuseUser
|
|||
comment: 'This is a dummy report for testing purposes.',
|
||||
targetUserHost: null,
|
||||
reporterHost: null,
|
||||
resolvedAs: null,
|
||||
moderationNote: 'foo',
|
||||
...override,
|
||||
};
|
||||
|
||||
return {
|
||||
...result,
|
||||
targetUser: result.targetUser ? toPackedUserLite(result.targetUser) : null,
|
||||
reporter: result.reporter ? toPackedUserLite(result.reporter) : null,
|
||||
assignee: result.assignee ? toPackedUserLite(result.assignee) : null,
|
||||
};
|
||||
}
|
||||
|
||||
function generateDummyUser(override?: Partial<MiUser>): MiUser {
|
||||
|
|
@ -73,6 +83,9 @@ function generateDummyUser(override?: Partial<MiUser>): MiUser {
|
|||
isExplorable: true,
|
||||
isHibernated: false,
|
||||
isDeleted: false,
|
||||
requireSigninToViewContents: false,
|
||||
makeNotesFollowersOnlyBefore: null,
|
||||
makeNotesHiddenBefore: null,
|
||||
emojis: [],
|
||||
score: 0,
|
||||
host: null,
|
||||
|
|
@ -289,7 +302,8 @@ const dummyUser3 = generateDummyUser({
|
|||
|
||||
@Injectable()
|
||||
export class WebhookTestService {
|
||||
public static NoSuchWebhookError = class extends Error {};
|
||||
public static NoSuchWebhookError = class extends Error {
|
||||
};
|
||||
|
||||
constructor(
|
||||
private userWebhookService: UserWebhookService,
|
||||
|
|
@ -307,10 +321,10 @@ export class WebhookTestService {
|
|||
* - 送信対象イベント(on)に関する設定
|
||||
*/
|
||||
@bindThis
|
||||
public async testUserWebhook(
|
||||
public async testUserWebhook<T extends WebhookEventTypes>(
|
||||
params: {
|
||||
webhookId: MiWebhook['id'],
|
||||
type: WebhookEventTypes,
|
||||
type: T,
|
||||
override?: Partial<Omit<MiWebhook, 'id'>>,
|
||||
},
|
||||
sender: MiUser | null,
|
||||
|
|
@ -322,7 +336,7 @@ export class WebhookTestService {
|
|||
}
|
||||
|
||||
const webhook = webhooks[0];
|
||||
const send = (contents: unknown) => {
|
||||
const send = <U extends WebhookEventTypes>(type: U, contents: UserWebhookPayload<U>) => {
|
||||
const merged = {
|
||||
...webhook,
|
||||
...params.override,
|
||||
|
|
@ -330,7 +344,7 @@ export class WebhookTestService {
|
|||
|
||||
// テスト目的なのでUserWebhookServiceの機能を経由せず直接キューに追加する(チェック処理などをスキップする意図).
|
||||
// また、Jobの試行回数も1回だけ.
|
||||
this.queueService.userWebhookDeliver(merged, params.type, contents, { attempts: 1 });
|
||||
this.queueService.userWebhookDeliver(merged, type, contents, { attempts: 1 });
|
||||
};
|
||||
|
||||
const dummyNote1 = generateDummyNote({
|
||||
|
|
@ -362,33 +376,45 @@ export class WebhookTestService {
|
|||
|
||||
switch (params.type) {
|
||||
case 'note': {
|
||||
send(toPackedNote(dummyNote1));
|
||||
send('note', { note: toPackedNote(dummyNote1) });
|
||||
break;
|
||||
}
|
||||
case 'reply': {
|
||||
send(toPackedNote(dummyReply1));
|
||||
send('reply', { note: toPackedNote(dummyReply1) });
|
||||
break;
|
||||
}
|
||||
case 'renote': {
|
||||
send(toPackedNote(dummyRenote1));
|
||||
send('renote', { note: toPackedNote(dummyRenote1) });
|
||||
break;
|
||||
}
|
||||
case 'mention': {
|
||||
send(toPackedNote(dummyMention1));
|
||||
send('mention', { note: toPackedNote(dummyMention1) });
|
||||
break;
|
||||
}
|
||||
case 'edited': {
|
||||
send('edited', { note: toPackedNote(dummyNote1) });
|
||||
break;
|
||||
}
|
||||
case 'follow': {
|
||||
send(toPackedUserDetailedNotMe(dummyUser1));
|
||||
send('follow', { user: toPackedUserDetailedNotMe(dummyUser1) });
|
||||
break;
|
||||
}
|
||||
case 'followed': {
|
||||
send(toPackedUserLite(dummyUser2));
|
||||
send('followed', { user: toPackedUserLite(dummyUser2) });
|
||||
break;
|
||||
}
|
||||
case 'unfollow': {
|
||||
send(toPackedUserDetailedNotMe(dummyUser3));
|
||||
send('unfollow', { user: toPackedUserDetailedNotMe(dummyUser3) });
|
||||
break;
|
||||
}
|
||||
// まだ実装されていない (#9485)
|
||||
case 'reaction':
|
||||
return;
|
||||
default: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const _exhaustiveAssertion: never = params.type;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -401,10 +427,10 @@ export class WebhookTestService {
|
|||
* - 送信対象イベント(on)に関する設定
|
||||
*/
|
||||
@bindThis
|
||||
public async testSystemWebhook(
|
||||
public async testSystemWebhook<T extends SystemWebhookEventType>(
|
||||
params: {
|
||||
webhookId: MiSystemWebhook['id'],
|
||||
type: SystemWebhookEventType,
|
||||
type: T,
|
||||
override?: Partial<Omit<MiSystemWebhook, 'id'>>,
|
||||
},
|
||||
) {
|
||||
|
|
@ -414,7 +440,7 @@ export class WebhookTestService {
|
|||
}
|
||||
|
||||
const webhook = webhooks[0];
|
||||
const send = (contents: unknown) => {
|
||||
const send = <U extends SystemWebhookEventType>(type: U, contents: SystemWebhookPayload<U>) => {
|
||||
const merged = {
|
||||
...webhook,
|
||||
...params.override,
|
||||
|
|
@ -422,12 +448,12 @@ export class WebhookTestService {
|
|||
|
||||
// テスト目的なのでSystemWebhookServiceの機能を経由せず直接キューに追加する(チェック処理などをスキップする意図).
|
||||
// また、Jobの試行回数も1回だけ.
|
||||
this.queueService.systemWebhookDeliver(merged, params.type, contents, { attempts: 1 });
|
||||
this.queueService.systemWebhookDeliver(merged, type, contents, { attempts: 1 });
|
||||
};
|
||||
|
||||
switch (params.type) {
|
||||
case 'abuseReport': {
|
||||
send(generateAbuseReport({
|
||||
send('abuseReport', generateAbuseReport({
|
||||
targetUserId: dummyUser1.id,
|
||||
targetUser: dummyUser1,
|
||||
reporterId: dummyUser2.id,
|
||||
|
|
@ -436,7 +462,7 @@ export class WebhookTestService {
|
|||
break;
|
||||
}
|
||||
case 'abuseReportResolved': {
|
||||
send(generateAbuseReport({
|
||||
send('abuseReportResolved', generateAbuseReport({
|
||||
targetUserId: dummyUser1.id,
|
||||
targetUser: dummyUser1,
|
||||
reporterId: dummyUser2.id,
|
||||
|
|
@ -448,9 +474,30 @@ export class WebhookTestService {
|
|||
break;
|
||||
}
|
||||
case 'userCreated': {
|
||||
send(toPackedUserLite(dummyUser1));
|
||||
send('userCreated', toPackedUserLite(dummyUser1));
|
||||
break;
|
||||
}
|
||||
case 'inactiveModeratorsWarning': {
|
||||
const dummyTime: ModeratorInactivityRemainingTime = {
|
||||
time: 100000,
|
||||
asDays: 1,
|
||||
asHours: 24,
|
||||
};
|
||||
|
||||
send('inactiveModeratorsWarning', {
|
||||
remainingTime: dummyTime,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'inactiveModeratorsInvitationOnlyChanged': {
|
||||
send('inactiveModeratorsInvitationOnlyChanged', {});
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const _exhaustiveAssertion: never = params.type;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -135,6 +135,7 @@ export class ApInboxService {
|
|||
if (actor.uri) {
|
||||
if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
|
||||
setImmediate(() => {
|
||||
// 同一ユーザーの情報を再度処理するので、使用済みのresolverを再利用してはいけない
|
||||
this.apPersonService.updatePerson(actor.uri);
|
||||
});
|
||||
}
|
||||
|
|
@ -572,7 +573,7 @@ export class ApInboxService {
|
|||
@bindThis
|
||||
private async flag(actor: MiRemoteUser, activity: IFlag): Promise<string> {
|
||||
// Make sure the source instance is allowed to send reports.
|
||||
const instance = await this.federatedInstanceService.fetch(actor.host);
|
||||
const instance = await this.federatedInstanceService.fetchOrRegister(actor.host);
|
||||
if (instance.rejectReports) {
|
||||
throw new Bull.UnrecoverableError(`Rejecting report from instance: ${actor.host}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -520,6 +520,9 @@ export class ApRendererService {
|
|||
summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null,
|
||||
_misskey_summary: profile.description,
|
||||
_misskey_followedMessage: profile.followedMessage,
|
||||
_misskey_requireSigninToViewContents: user.requireSigninToViewContents,
|
||||
_misskey_makeNotesFollowersOnlyBefore: user.makeNotesFollowersOnlyBefore,
|
||||
_misskey_makeNotesHiddenBefore: user.makeNotesHiddenBefore,
|
||||
icon: avatar ? this.renderImage(avatar) : null,
|
||||
image: banner ? this.renderImage(banner) : null,
|
||||
backgroundUrl: background ? this.renderImage(background) : null,
|
||||
|
|
|
|||
|
|
@ -11,14 +11,14 @@ import { DI } from '@/di-symbols.js';
|
|||
import type { Config } from '@/config.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import { UserKeypairService } from '@/core/UserKeypairService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import type { IObject } from './type.js';
|
||||
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
|
||||
import { assertActivityMatchesUrls } from '@/core/activitypub/misc/check-against-url.js';
|
||||
import { UtilityService } from "@/core/UtilityService.js";
|
||||
import type { IObject } from './type.js';
|
||||
|
||||
type Request = {
|
||||
url: string;
|
||||
|
|
|
|||
|
|
@ -558,6 +558,9 @@ const extension_context_definition = {
|
|||
'_misskey_votes': 'misskey:_misskey_votes',
|
||||
'_misskey_summary': 'misskey:_misskey_summary',
|
||||
'_misskey_followedMessage': 'misskey:_misskey_followedMessage',
|
||||
'_misskey_requireSigninToViewContents': 'misskey:_misskey_requireSigninToViewContents',
|
||||
'_misskey_makeNotesFollowersOnlyBefore': 'misskey:_misskey_makeNotesFollowersOnlyBefore',
|
||||
'_misskey_makeNotesHiddenBefore': 'misskey:_misskey_makeNotesHiddenBefore',
|
||||
'isCat': 'misskey:isCat',
|
||||
// Firefish
|
||||
firefish: 'https://joinfirefish.org/ns#',
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ export class ApImageService {
|
|||
// 2. or the image is not sensitive
|
||||
const shouldBeCached = this.meta.cacheRemoteFiles && (this.meta.cacheRemoteSensitiveFiles || !image.sensitive);
|
||||
|
||||
await this.federatedInstanceService.fetch(actor.host).then(async i => {
|
||||
await this.federatedInstanceService.fetchOrRegister(actor.host).then(async i => {
|
||||
if (i.isNSFW) {
|
||||
image.sensitive = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,6 +109,10 @@ export class ApNoteService {
|
|||
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`);
|
||||
}
|
||||
|
||||
if (object.published && !this.idService.isSafeT(new Date(object.published).valueOf())) {
|
||||
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', 'invalid Note: published timestamp is malformed');
|
||||
}
|
||||
|
||||
if (actor) {
|
||||
const attribution = (object.attributedTo) ? getOneApId(object.attributedTo) : actor.uri;
|
||||
if (attribution !== actor.uri) {
|
||||
|
|
@ -119,10 +123,6 @@ export class ApNoteService {
|
|||
}
|
||||
}
|
||||
|
||||
if (object.published && !this.idService.isSafeT(new Date(object.published).valueOf())) {
|
||||
return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', 'invalid Note: published timestamp is malformed');
|
||||
}
|
||||
|
||||
if (note) {
|
||||
const url = (object.url) ? getOneApId(object.url) : note.url;
|
||||
if (url && url !== note.url) {
|
||||
|
|
@ -411,7 +411,7 @@ export class ApNoteService {
|
|||
const object = await resolver.resolve(value);
|
||||
|
||||
const entryUri = getApId(value);
|
||||
const err = this.validateNote(object, entryUri);
|
||||
const err = this.validateNote(object, entryUri, actor, user, updatedNote);
|
||||
if (err) {
|
||||
this.logger.error(err.message, {
|
||||
resolver: { history: resolver.getHistory() },
|
||||
|
|
|
|||
|
|
@ -255,6 +255,12 @@ export class ApPersonService implements OnModuleInit {
|
|||
if (user == null) throw new Error('failed to create user: user is null');
|
||||
|
||||
const [avatar, banner, background] = await Promise.all([icon, image, bgimg].map(img => {
|
||||
// icon and image may be arrays
|
||||
// see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-icon
|
||||
if (Array.isArray(img)) {
|
||||
img = img.find(item => item && item.url) ?? null;
|
||||
}
|
||||
|
||||
// if we have an explicitly missing image, return an
|
||||
// explicitly-null set of values
|
||||
if ((img == null) || (typeof img === 'object' && img.url == null)) {
|
||||
|
|
@ -398,7 +404,7 @@ export class ApPersonService implements OnModuleInit {
|
|||
usernameLower: person.preferredUsername?.toLowerCase(),
|
||||
host,
|
||||
inbox: person.inbox,
|
||||
sharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox,
|
||||
sharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox ?? null,
|
||||
notesCount: outboxcollection?.totalItems ?? 0,
|
||||
followersCount: followerscollection?.totalItems ?? 0,
|
||||
followingCount: followingcollection?.totalItems ?? 0,
|
||||
|
|
@ -409,6 +415,9 @@ export class ApPersonService implements OnModuleInit {
|
|||
isBot,
|
||||
isCat: (person as any).isCat === true,
|
||||
speakAsCat: (person as any).speakAsCat != null ? (person as any).speakAsCat === true : (person as any).isCat === true,
|
||||
requireSigninToViewContents: (person as any).requireSigninToViewContents === true,
|
||||
makeNotesFollowersOnlyBefore: (person as any).makeNotesFollowersOnlyBefore ?? null,
|
||||
makeNotesHiddenBefore: (person as any).makeNotesHiddenBefore ?? null,
|
||||
emojis,
|
||||
})) as MiRemoteUser;
|
||||
|
||||
|
|
@ -462,13 +471,15 @@ export class ApPersonService implements OnModuleInit {
|
|||
this.cacheService.uriPersonCache.set(user.uri, user);
|
||||
|
||||
// Register host
|
||||
this.federatedInstanceService.fetch(host).then(i => {
|
||||
this.instancesRepository.increment({ id: i.id }, 'usersCount', 1);
|
||||
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
|
||||
if (this.meta.enableChartsForFederatedInstances) {
|
||||
this.instanceChart.newUser(i.host);
|
||||
}
|
||||
});
|
||||
if (this.meta.enableStatsForFederatedInstances) {
|
||||
this.federatedInstanceService.fetchOrRegister(host).then(i => {
|
||||
this.instancesRepository.increment({ id: i.id }, 'usersCount', 1);
|
||||
if (this.meta.enableChartsForFederatedInstances) {
|
||||
this.instanceChart.newUser(i.host);
|
||||
}
|
||||
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
|
||||
});
|
||||
}
|
||||
|
||||
this.usersChart.update(user, true);
|
||||
|
||||
|
|
@ -574,7 +585,7 @@ export class ApPersonService implements OnModuleInit {
|
|||
const updates = {
|
||||
lastFetchedAt: new Date(),
|
||||
inbox: person.inbox,
|
||||
sharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox,
|
||||
sharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox ?? null,
|
||||
followersUri: person.followers ? getApId(person.followers) : undefined,
|
||||
featured: person.featured,
|
||||
emojis: emojiNames,
|
||||
|
|
@ -653,7 +664,7 @@ export class ApPersonService implements OnModuleInit {
|
|||
// 該当ユーザーが既にフォロワーになっていた場合はFollowingもアップデートする
|
||||
await this.followingsRepository.update(
|
||||
{ followerId: exist.id },
|
||||
{ followerSharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox },
|
||||
{ followerSharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox ?? null },
|
||||
);
|
||||
|
||||
await this.updateFeatured(exist.id, resolver).catch(err => this.logger.error(err));
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ import type { IPoll } from '@/models/Poll.js';
|
|||
import type { MiRemoteUser } from '@/models/User.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { getApId, getApType, getNullableApId, getOneApId, isQuestion } from '../type.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { ApLoggerService } from '../ApLoggerService.js';
|
||||
import { ApResolverService } from '../ApResolverService.js';
|
||||
import type { Resolver } from '../ApResolverService.js';
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@ export interface IObject {
|
|||
summary?: string | null;
|
||||
_misskey_summary?: string;
|
||||
_misskey_followedMessage?: string | null;
|
||||
_misskey_requireSigninToViewContents?: boolean;
|
||||
_misskey_makeNotesFollowersOnlyBefore?: number | null;
|
||||
_misskey_makeNotesHiddenBefore?: number | null;
|
||||
published?: string;
|
||||
cc?: ApObject;
|
||||
to?: ApObject;
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ export class AbuseUserReportEntityService {
|
|||
schema: 'UserDetailedNotMe',
|
||||
}) : null,
|
||||
forwarded: report.forwarded,
|
||||
resolvedAs: report.resolvedAs,
|
||||
moderationNote: report.moderationNote,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,10 +5,8 @@
|
|||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { FlashsRepository, FlashLikesRepository } from '@/models/_.js';
|
||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||
import type { FlashLikesRepository, FlashsRepository } from '@/models/_.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { } from '@/models/Blocking.js';
|
||||
import type { MiUser } from '@/models/User.js';
|
||||
import type { MiFlash } from '@/models/Flash.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
|
@ -20,10 +18,8 @@ export class FlashEntityService {
|
|||
constructor(
|
||||
@Inject(DI.flashsRepository)
|
||||
private flashsRepository: FlashsRepository,
|
||||
|
||||
@Inject(DI.flashLikesRepository)
|
||||
private flashLikesRepository: FlashLikesRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
|
|
@ -34,25 +30,36 @@ export class FlashEntityService {
|
|||
src: MiFlash['id'] | MiFlash,
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
hint?: {
|
||||
packedUser?: Packed<'UserLite'>
|
||||
packedUser?: Packed<'UserLite'>,
|
||||
likedFlashIds?: MiFlash['id'][],
|
||||
},
|
||||
): Promise<Packed<'Flash'>> {
|
||||
const meId = me ? me.id : null;
|
||||
const flash = typeof src === 'object' ? src : await this.flashsRepository.findOneByOrFail({ id: src });
|
||||
|
||||
return await awaitAll({
|
||||
// { schema: 'UserDetailed' } すると無限ループするので注意
|
||||
const user = hint?.packedUser ?? await this.userEntityService.pack(flash.user ?? flash.userId, me);
|
||||
|
||||
let isLiked = undefined;
|
||||
if (meId) {
|
||||
isLiked = hint?.likedFlashIds
|
||||
? hint.likedFlashIds.includes(flash.id)
|
||||
: await this.flashLikesRepository.exists({ where: { flashId: flash.id, userId: meId } });
|
||||
}
|
||||
|
||||
return {
|
||||
id: flash.id,
|
||||
createdAt: this.idService.parse(flash.id).date.toISOString(),
|
||||
updatedAt: flash.updatedAt.toISOString(),
|
||||
userId: flash.userId,
|
||||
user: hint?.packedUser ?? this.userEntityService.pack(flash.user ?? flash.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意
|
||||
user: user,
|
||||
title: flash.title,
|
||||
summary: flash.summary,
|
||||
script: flash.script,
|
||||
visibility: flash.visibility,
|
||||
likedCount: flash.likedCount,
|
||||
isLiked: meId ? await this.flashLikesRepository.exists({ where: { flashId: flash.id, userId: meId } }) : undefined,
|
||||
});
|
||||
isLiked: isLiked,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
@ -63,7 +70,19 @@ export class FlashEntityService {
|
|||
const _users = flashes.map(({ user, userId }) => user ?? userId);
|
||||
const _userMap = await this.userEntityService.packMany(_users, me)
|
||||
.then(users => new Map(users.map(u => [u.id, u])));
|
||||
return Promise.all(flashes.map(flash => this.pack(flash, me, { packedUser: _userMap.get(flash.userId) })));
|
||||
const _likedFlashIds = me
|
||||
? await this.flashLikesRepository.createQueryBuilder('flashLike')
|
||||
.select('flashLike.flashId')
|
||||
.where('flashLike.userId = :userId', { userId: me.id })
|
||||
.getRawMany<{ flashLike_flashId: string }>()
|
||||
.then(likes => [...new Set(likes.map(like => like.flashLike_flashId))])
|
||||
: [];
|
||||
return Promise.all(
|
||||
flashes.map(flash => this.pack(flash, me, {
|
||||
packedUser: _userMap.get(flash.userId),
|
||||
likedFlashIds: _likedFlashIds,
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -100,6 +100,7 @@ export class MetaEntityService {
|
|||
turnstileSiteKey: instance.turnstileSiteKey,
|
||||
enableFC: instance.enableFC,
|
||||
fcSiteKey: instance.fcSiteKey,
|
||||
enableTestcaptcha: instance.enableTestcaptcha,
|
||||
swPublickey: instance.swPublicKey,
|
||||
themeColor: instance.themeColor,
|
||||
mascotImageUrl: instance.mascotImageUrl ?? '/assets/ai.png',
|
||||
|
|
|
|||
|
|
@ -25,6 +25,30 @@ import type { UserEntityService } from './UserEntityService.js';
|
|||
import type { DriveFileEntityService } from './DriveFileEntityService.js';
|
||||
import type { Config } from '@/config.js';
|
||||
|
||||
// is-renote.tsとよしなにリンク
|
||||
function isPureRenote(note: MiNote): note is MiNote & { renoteId: MiNote['id']; renote: MiNote } {
|
||||
return (
|
||||
note.renote != null &&
|
||||
note.reply == null &&
|
||||
note.text == null &&
|
||||
note.cw == null &&
|
||||
(note.fileIds == null || note.fileIds.length === 0) &&
|
||||
!note.hasPoll
|
||||
);
|
||||
}
|
||||
|
||||
function getAppearNoteIds(notes: MiNote[]): Set<string> {
|
||||
const appearNoteIds = new Set<string>();
|
||||
for (const note of notes) {
|
||||
if (isPureRenote(note)) {
|
||||
appearNoteIds.add(note.renoteId);
|
||||
} else {
|
||||
appearNoteIds.add(note.id);
|
||||
}
|
||||
}
|
||||
return appearNoteIds;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class NoteEntityService implements OnModuleInit {
|
||||
private userEntityService: UserEntityService;
|
||||
|
|
@ -86,52 +110,86 @@ export class NoteEntityService implements OnModuleInit {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
private async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null) {
|
||||
private async hideNote(packedNote: Packed<'Note'>, meId: MiUser['id'] | null): Promise<void> {
|
||||
// FIXME: このvisibility変更処理が当関数にあるのは若干不自然かもしれない(関数名を treatVisibility とかに変える手もある)
|
||||
if (packedNote.visibility === 'public' || packedNote.visibility === 'home') {
|
||||
const followersOnlyBefore = packedNote.user.makeNotesFollowersOnlyBefore;
|
||||
if ((followersOnlyBefore != null)
|
||||
&& (
|
||||
(followersOnlyBefore <= 0 && (Date.now() - new Date(packedNote.createdAt).getTime() > 0 - (followersOnlyBefore * 1000)))
|
||||
|| (followersOnlyBefore > 0 && (new Date(packedNote.createdAt).getTime() < followersOnlyBefore * 1000))
|
||||
)
|
||||
) {
|
||||
packedNote.visibility = 'followers';
|
||||
}
|
||||
}
|
||||
|
||||
if (meId === packedNote.userId) return;
|
||||
|
||||
// TODO: isVisibleForMe を使うようにしても良さそう(型違うけど)
|
||||
let hide = false;
|
||||
|
||||
// visibility が specified かつ自分が指定されていなかったら非表示
|
||||
if (packedNote.visibility === 'specified') {
|
||||
if (meId == null) {
|
||||
hide = true;
|
||||
} else if (meId === packedNote.userId) {
|
||||
hide = false;
|
||||
} else {
|
||||
// 指定されているかどうか
|
||||
const specified = packedNote.visibleUserIds!.some((id: any) => meId === id);
|
||||
if (packedNote.user.requireSigninToViewContents && meId == null) {
|
||||
hide = true;
|
||||
}
|
||||
|
||||
if (specified) {
|
||||
if (!hide) {
|
||||
const hiddenBefore = packedNote.user.makeNotesHiddenBefore;
|
||||
if ((hiddenBefore != null)
|
||||
&& (
|
||||
(hiddenBefore <= 0 && (Date.now() - new Date(packedNote.createdAt).getTime() > 0 - (hiddenBefore * 1000)))
|
||||
|| (hiddenBefore > 0 && (new Date(packedNote.createdAt).getTime() < hiddenBefore * 1000))
|
||||
)
|
||||
) {
|
||||
hide = true;
|
||||
}
|
||||
}
|
||||
|
||||
// visibility が specified かつ自分が指定されていなかったら非表示
|
||||
if (!hide) {
|
||||
if (packedNote.visibility === 'specified') {
|
||||
if (meId == null) {
|
||||
hide = true;
|
||||
} else if (meId === packedNote.userId) {
|
||||
hide = false;
|
||||
} else {
|
||||
hide = true;
|
||||
// 指定されているかどうか
|
||||
const specified = packedNote.visibleUserIds!.some(id => meId === id);
|
||||
|
||||
if (!specified) {
|
||||
hide = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// visibility が followers かつ自分が投稿者のフォロワーでなかったら非表示
|
||||
if (packedNote.visibility === 'followers') {
|
||||
if (meId == null) {
|
||||
hide = true;
|
||||
} else if (meId === packedNote.userId) {
|
||||
hide = false;
|
||||
} else if (packedNote.reply && (meId === packedNote.reply.userId)) {
|
||||
// 自分の投稿に対するリプライ
|
||||
hide = false;
|
||||
} else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) {
|
||||
// 自分へのメンション
|
||||
hide = false;
|
||||
} else if (packedNote.renote && (meId === packedNote.renote.userId)) {
|
||||
hide = false;
|
||||
} else {
|
||||
// フォロワーかどうか
|
||||
const isFollowing = await this.followingsRepository.exists({
|
||||
where: {
|
||||
followeeId: packedNote.userId,
|
||||
followerId: meId,
|
||||
},
|
||||
});
|
||||
if (!hide) {
|
||||
if (packedNote.visibility === 'followers') {
|
||||
if (meId == null) {
|
||||
hide = true;
|
||||
} else if (meId === packedNote.userId) {
|
||||
hide = false;
|
||||
} else if (packedNote.reply && (meId === packedNote.reply.userId)) {
|
||||
// 自分の投稿に対するリプライ
|
||||
hide = false;
|
||||
} else if (packedNote.mentions && packedNote.mentions.some(id => meId === id)) {
|
||||
// 自分へのメンション
|
||||
hide = false;
|
||||
} else if (packedNote.renote && (meId === packedNote.renote.userId)) {
|
||||
hide = false;
|
||||
} else {
|
||||
// フォロワーかどうか
|
||||
// TODO: 当関数呼び出しごとにクエリが走るのは重そうだからなんとかする
|
||||
const isFollowing = await this.followingsRepository.exists({
|
||||
where: {
|
||||
followeeId: packedNote.userId,
|
||||
followerId: meId,
|
||||
},
|
||||
});
|
||||
|
||||
hide = !isFollowing;
|
||||
hide = !isFollowing;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -163,6 +221,7 @@ export class NoteEntityService implements OnModuleInit {
|
|||
packedNote.reactionEmojis = {};
|
||||
packedNote.reactions = {};
|
||||
packedNote.isHidden = true;
|
||||
// TODO: hiddenReason みたいなのを提供しても良さそう
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -257,7 +316,7 @@ export class NoteEntityService implements OnModuleInit {
|
|||
return true;
|
||||
} else {
|
||||
// 指定されているかどうか
|
||||
return note.visibleUserIds.some((id: any) => meId === id);
|
||||
return note.visibleUserIds.some(id => meId === id);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -459,7 +518,7 @@ export class NoteEntityService implements OnModuleInit {
|
|||
) {
|
||||
if (notes.length === 0) return [];
|
||||
|
||||
const bufferedReactions = this.meta.enableReactionsBuffering ? await this.reactionsBufferingService.getMany(notes.map(x => x.id)) : null;
|
||||
const bufferedReactions = this.meta.enableReactionsBuffering ? await this.reactionsBufferingService.getMany([...getAppearNoteIds(notes)]) : null;
|
||||
|
||||
const meId = me ? me.id : null;
|
||||
const myReactionsMap = new Map<MiNote['id'], string | null>();
|
||||
|
|
@ -470,7 +529,7 @@ export class NoteEntityService implements OnModuleInit {
|
|||
const oldId = this.idService.gen(Date.now() - 2000);
|
||||
|
||||
for (const note of notes) {
|
||||
if (note.renote && (note.text == null && note.fileIds.length === 0)) { // pure renote
|
||||
if (isPureRenote(note)) {
|
||||
const reactionsCount = Object.values(this.reactionsBufferingService.mergeReactions(note.renote.reactions, bufferedReactions?.get(note.renote.id)?.deltas ?? {})).reduce((a, b) => a + b, 0);
|
||||
if (reactionsCount === 0) {
|
||||
myReactionsMap.set(note.renote.id, null);
|
||||
|
|
|
|||
|
|
@ -543,6 +543,9 @@ export class UserEntityService implements OnModuleInit {
|
|||
isSilenced: user.isSilenced || this.roleService.getUserPolicies(user.id).then(r => !r.canPublicNote),
|
||||
speakAsCat: user.speakAsCat ?? false,
|
||||
approved: user.approved,
|
||||
requireSigninToViewContents: user.requireSigninToViewContents === false ? undefined : true,
|
||||
makeNotesFollowersOnlyBefore: user.makeNotesFollowersOnlyBefore ?? undefined,
|
||||
makeNotesHiddenBefore: user.makeNotesHiddenBefore ?? undefined,
|
||||
instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? {
|
||||
name: instance.name,
|
||||
softwareName: instance.softwareName,
|
||||
|
|
@ -598,11 +601,6 @@ export class UserEntityService implements OnModuleInit {
|
|||
publicReactions: this.isLocalUser(user) ? profile!.publicReactions : false, // https://github.com/misskey-dev/misskey/issues/12964
|
||||
followersVisibility: profile!.followersVisibility,
|
||||
followingVisibility: profile!.followingVisibility,
|
||||
twoFactorEnabled: profile!.twoFactorEnabled,
|
||||
usePasswordLessLogin: profile!.usePasswordLessLogin,
|
||||
securityKeys: profile!.twoFactorEnabled
|
||||
? this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1)
|
||||
: false,
|
||||
roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
|
|
@ -617,6 +615,14 @@ export class UserEntityService implements OnModuleInit {
|
|||
moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined,
|
||||
} : {}),
|
||||
|
||||
...(isDetailed && (isMe || iAmModerator) ? {
|
||||
twoFactorEnabled: profile!.twoFactorEnabled,
|
||||
usePasswordLessLogin: profile!.usePasswordLessLogin,
|
||||
securityKeys: profile!.twoFactorEnabled
|
||||
? this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1)
|
||||
: false,
|
||||
} : {}),
|
||||
|
||||
...(isDetailed && isMe ? {
|
||||
avatarId: user.avatarId,
|
||||
bannerId: user.bannerId,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue