diff options
Diffstat (limited to 'packages/backend')
10 files changed, 79 insertions, 41 deletions
diff --git a/packages/backend/src/core/AbuseReportService.ts b/packages/backend/src/core/AbuseReportService.ts index bccb9f86f6..8b3c596f50 100644 --- a/packages/backend/src/core/AbuseReportService.ts +++ b/packages/backend/src/core/AbuseReportService.ts @@ -14,6 +14,7 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import { SystemAccountService } from '@/core/SystemAccountService.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; +import { trackPromise } from '@/misc/promise-tracker.js'; import { IdService } from './IdService.js'; @Injectable() @@ -68,11 +69,11 @@ export class AbuseReportService { reports.push(report); } - return Promise.all([ + trackPromise(Promise.all([ this.abuseReportNotificationService.notifyAdminStream(reports), this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReport'), this.abuseReportNotificationService.notifyMail(reports), - ]); + ])); } /** diff --git a/packages/backend/src/core/CacheService.ts b/packages/backend/src/core/CacheService.ts index 2d37cd6bab..84f6df3e21 100644 --- a/packages/backend/src/core/CacheService.ts +++ b/packages/backend/src/core/CacheService.ts @@ -351,6 +351,11 @@ export class CacheService implements OnApplicationShutdown { } @bindThis + public findOptionalUserById(userId: MiUser['id']) { + return this.userByIdCache.fetchMaybe(userId, async () => await this.usersRepository.findOneBy({ id: userId }) ?? undefined); + } + + @bindThis public async getFollowStats(userId: MiUser['id']): Promise<FollowStats> { return await this.userFollowStatsCache.fetch(userId, async () => { const stats = { diff --git a/packages/backend/src/core/UserSuspendService.ts b/packages/backend/src/core/UserSuspendService.ts index ddadab7022..5868ba6678 100644 --- a/packages/backend/src/core/UserSuspendService.ts +++ b/packages/backend/src/core/UserSuspendService.ts @@ -4,7 +4,7 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import { Not, IsNull } from 'typeorm'; +import { Not, IsNull, DataSource } from 'typeorm'; import type { FollowingsRepository, FollowRequestsRepository, UsersRepository } from '@/models/_.js'; import { MiUser } from '@/models/User.js'; import { QueueService } from '@/core/QueueService.js'; @@ -21,6 +21,7 @@ import { LoggerService } from '@/core/LoggerService.js'; import type Logger from '@/logger.js'; import { renderInlineError } from '@/misc/render-inline-error.js'; import { trackPromise } from '@/misc/promise-tracker.js'; +import { InternalEventService } from '@/core/InternalEventService.js'; @Injectable() export class UserSuspendService { @@ -36,12 +37,16 @@ export class UserSuspendService { @Inject(DI.followRequestsRepository) private followRequestsRepository: FollowRequestsRepository, + @Inject(DI.db) + private db: DataSource, + private userEntityService: UserEntityService, private queueService: QueueService, private globalEventService: GlobalEventService, private apRendererService: ApRendererService, private moderationLogService: ModerationLogService, private readonly cacheService: CacheService, + private readonly internalEventService: InternalEventService, loggerService: LoggerService, ) { @@ -56,6 +61,8 @@ export class UserSuspendService { isSuspended: true, }); + await this.internalEventService.emit(user.host == null ? 'localUserUpdated' : 'remoteUserUpdated', { id: user.id }); + await this.moderationLogService.log(moderator, 'suspend', { userId: user.id, userUsername: user.username, @@ -74,6 +81,8 @@ export class UserSuspendService { isSuspended: false, }); + await this.internalEventService.emit(user.host == null ? 'localUserUpdated' : 'remoteUserUpdated', { id: user.id }); + await this.moderationLogService.log(moderator, 'unsuspend', { userId: user.id, userUsername: user.username, @@ -178,30 +187,29 @@ export class UserSuspendService { // Freeze follow relations with all remote users await this.followingsRepository .createQueryBuilder('following') - .orWhere({ - followeeId: user.id, - followerHost: Not(IsNull()), - }) .update({ isFollowerHibernated: true, }) + .where({ + followeeId: user.id, + followerHost: Not(IsNull()), + }) .execute(); } @bindThis private async unFreezeAll(user: MiUser): Promise<void> { // Restore follow relations with all remote users - await this.followingsRepository - .createQueryBuilder('following') - .innerJoin(MiUser, 'follower', 'user.id = following.followerId') - .andWhere('follower.isHibernated = false') // Don't unfreeze if the follower is *actually* frozen - .andWhere({ - followeeId: user.id, - followerHost: Not(IsNull()), - }) - .update({ - isFollowerHibernated: false, - }) - .execute(); + + // TypeORM does not support UPDATE with JOIN: https://github.com/typeorm/typeorm/issues/564#issuecomment-310331468 + await this.db.query(` + UPDATE "following" + SET "isFollowerHibernated" = false + FROM "user" + WHERE "user"."id" = "following"."followerId" + AND "user"."isHibernated" = false -- Don't unfreeze if the follower is *actually* frozen + AND "followeeId" = $1 + AND "followeeHost" IS NOT NULL + `, [user.id]); } } diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 30124949bb..f1a2522c04 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -376,7 +376,8 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { const url = this.apUtilityService.findBestObjectUrl(person); - const verifiedLinks = url ? await verifyFieldLinks(fields, url, this.httpRequestService) : []; + const profileUrls = url ? [url, person.id] : [person.id]; + const verifiedLinks = await verifyFieldLinks(fields, profileUrls, this.httpRequestService); // Create user let user: MiRemoteUser | null = null; @@ -625,7 +626,8 @@ export class ApPersonService implements OnModuleInit, OnApplicationShutdown { const url = this.apUtilityService.findBestObjectUrl(person); - const verifiedLinks = url ? await verifyFieldLinks(fields, url, this.httpRequestService) : []; + const profileUrls = url ? [url, person.id] : [person.id]; + const verifiedLinks = await verifyFieldLinks(fields, profileUrls, this.httpRequestService); const updates = { lastFetchedAt: new Date(), diff --git a/packages/backend/src/misc/verify-field-link.ts b/packages/backend/src/misc/verify-field-link.ts index 37161f16e5..31a356be37 100644 --- a/packages/backend/src/misc/verify-field-link.ts +++ b/packages/backend/src/misc/verify-field-link.ts @@ -8,7 +8,7 @@ import type { HttpRequestService } from '@/core/HttpRequestService.js'; type Field = { name: string, value: string }; -export async function verifyFieldLinks(fields: Field[], profile_url: string, httpRequestService: HttpRequestService): Promise<string[]> { +export async function verifyFieldLinks(fields: Field[], profileUrls: string[], httpRequestService: HttpRequestService): Promise<string[]> { const verified_links = []; for (const field_url of fields) { try { @@ -19,7 +19,7 @@ export async function verifyFieldLinks(fields: Field[], profile_url: string, htt const links = doc('a[rel~="me"][href], link[rel~="me"][href]').toArray(); - const includesProfileLinks = links.some(link => link.attribs.href === profile_url); + const includesProfileLinks = links.some(link => profileUrls.includes(link.attribs.href)); if (includesProfileLinks) { verified_links.push(field_url.value); } diff --git a/packages/backend/src/server/SkRateLimiterService.md b/packages/backend/src/server/SkRateLimiterService.md index 55786664e1..4ec097ea8e 100644 --- a/packages/backend/src/server/SkRateLimiterService.md +++ b/packages/backend/src/server/SkRateLimiterService.md @@ -81,7 +81,7 @@ The Atomic Leaky Bucket algorithm is described here, in pseudocode: # * Delta Timestamp - Difference between current and expected timestamp value # 0 - Calculations -dripRate = ceil(limit.dripRate ?? 1000); +dripRate = ceil((limit.dripRate ?? 1000) * factor); dripSize = ceil(limit.dripSize ?? 1); bucketSize = max(ceil(limit.size / factor), 1); maxExpiration = max(ceil((dripRate * ceil(bucketSize / dripSize)) / 1000), 1);; diff --git a/packages/backend/src/server/SkRateLimiterService.ts b/packages/backend/src/server/SkRateLimiterService.ts index 35e87b0fe8..a53c58ba5a 100644 --- a/packages/backend/src/server/SkRateLimiterService.ts +++ b/packages/backend/src/server/SkRateLimiterService.ts @@ -206,7 +206,7 @@ export class SkRateLimiterService { // 0 - Calculate const now = this.timeService.now; const bucketSize = Math.max(Math.ceil(limit.size / factor), 1); - const dripRate = Math.ceil(limit.dripRate ?? 1000); + const dripRate = Math.ceil((limit.dripRate ?? 1000) * factor); const dripSize = Math.ceil(limit.dripSize ?? 1); const fullResetMs = dripRate * Math.ceil(bucketSize / dripSize); const fullResetSec = Math.max(Math.ceil(fullResetMs / 1000), 1); diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 5767880531..65dcf6301f 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -603,11 +603,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- this.globalEventService.publishInternalEvent('localUserUpdated', { id: user.id }); } - const verified_links = await verifyFieldLinks(newFields, `${this.config.url}/@${user.username}`, this.httpRequestService); + const profileUrls = [ + this.userEntityService.genLocalUserUri(user.id), + `${this.config.url}/@${user.username}`, + ]; + const verifiedLinks = await verifyFieldLinks(newFields, profileUrls, this.httpRequestService); await this.userProfilesRepository.update(user.id, { ...profileUpdates, - verifiedLinks: verified_links, + verifiedLinks, }); const iObj = await this.userEntityService.pack(user.id, user, { diff --git a/packages/backend/src/server/api/endpoints/users/report-abuse.ts b/packages/backend/src/server/api/endpoints/users/report-abuse.ts index 81c0c526f0..fc2b57c4a5 100644 --- a/packages/backend/src/server/api/endpoints/users/report-abuse.ts +++ b/packages/backend/src/server/api/endpoints/users/report-abuse.ts @@ -9,6 +9,7 @@ import { GetterService } from '@/server/api/GetterService.js'; import { RoleService } from '@/core/RoleService.js'; import { AbuseReportService } from '@/core/AbuseReportService.js'; import { ApiError } from '../../error.js'; +import { CacheService } from '@/core/CacheService.js'; export const meta = { tags: ['users'], @@ -60,13 +61,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- private getterService: GetterService, private roleService: RoleService, private abuseReportService: AbuseReportService, + private readonly cacheService: CacheService, ) { super(meta, paramDef, async (ps, me) => { // Lookup user - const targetUser = await this.getterService.getUser(ps.userId).catch(err => { - if (err.id === '15348ddd-432d-49c2-8a5a-8069753becff') throw new ApiError(meta.errors.noSuchUser); - throw err; - }); + const targetUser = await this.cacheService.findOptionalUserById(ps.userId); + if (!targetUser) { + throw new ApiError(meta.errors.noSuchUser); + } if (targetUser.id === me.id) { throw new ApiError(meta.errors.cannotReportYourself); diff --git a/packages/backend/test/unit/server/api/SkRateLimiterServiceTests.ts b/packages/backend/test/unit/server/api/SkRateLimiterServiceTests.ts index b1f100698b..f7250600e3 100644 --- a/packages/backend/test/unit/server/api/SkRateLimiterServiceTests.ts +++ b/packages/backend/test/unit/server/api/SkRateLimiterServiceTests.ts @@ -303,9 +303,12 @@ describe(SkRateLimiterService, () => { const i1 = await serviceUnderTest().limit(limit, actor); // 1 + 1 = 2 const i2 = await serviceUnderTest().limit(limit, actor); // 2 + 1 = 3 + mockTimeService.now += 500; // 3 - 1 = 2 (at 1/2 time) + const i3 = await serviceUnderTest().limit(limit, actor); expect(i1.blocked).toBeFalsy(); expect(i2.blocked).toBeTruthy(); + expect(i3.blocked).toBeFalsy(); }); it('should set counter expiration', async () => { @@ -563,11 +566,15 @@ describe(SkRateLimiterService, () => { mockDefaultUserPolicies.rateLimitFactor = 0.5; limitCounter = 1; limitTimestamp = 0; - mockTimeService.now += 500; - const info = await serviceUnderTest().limit(limit, actor); + const i1 = await serviceUnderTest().limit(limit, actor); + const i2 = await serviceUnderTest().limit(limit, actor); + mockTimeService.now += 500; + const i3 = await serviceUnderTest().limit(limit, actor); - expect(info.blocked).toBeFalsy(); + expect(i1.blocked).toBeFalsy(); + expect(i2.blocked).toBeTruthy(); + expect(i3.blocked).toBeFalsy(); }); it('should set counter expiration', async () => { @@ -738,12 +745,17 @@ describe(SkRateLimiterService, () => { it('should scale limit by factor', async () => { mockDefaultUserPolicies.rateLimitFactor = 0.5; - limitCounter = 10; + limitCounter = 1; limitTimestamp = 0; - const info = await serviceUnderTest().limit(limit, actor); // 10 + 1 = 11 + const i1 = await serviceUnderTest().limit(limit, actor); + const i2 = await serviceUnderTest().limit(limit, actor); + mockTimeService.now += 500; + const i3 = await serviceUnderTest().limit(limit, actor); - expect(info.blocked).toBeTruthy(); + expect(i1.blocked).toBeFalsy(); + expect(i2.blocked).toBeTruthy(); + expect(i3.blocked).toBeFalsy(); }); it('should set counter expiration', async () => { @@ -932,13 +944,17 @@ describe(SkRateLimiterService, () => { it('should scale limit and interval by factor', async () => { mockDefaultUserPolicies.rateLimitFactor = 0.5; - limitCounter = 5; + limitCounter = 19; limitTimestamp = 0; - mockTimeService.now += 500; - const info = await serviceUnderTest().limit(limit, actor); + const i1 = await serviceUnderTest().limit(limit, actor); + const i2 = await serviceUnderTest().limit(limit, actor); + mockTimeService.now += 500; + const i3 = await serviceUnderTest().limit(limit, actor); - expect(info.blocked).toBeFalsy(); + expect(i1.blocked).toBeFalsy(); + expect(i2.blocked).toBeTruthy(); + expect(i3.blocked).toBeFalsy(); }); it('should set counter expiration', async () => { |