summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--package.json2
-rw-r--r--packages/backend/src/core/AbuseReportService.ts5
-rw-r--r--packages/backend/src/core/CacheService.ts5
-rw-r--r--packages/backend/src/core/UserSuspendService.ts42
-rw-r--r--packages/backend/src/core/activitypub/models/ApPersonService.ts6
-rw-r--r--packages/backend/src/misc/verify-field-link.ts4
-rw-r--r--packages/backend/src/server/SkRateLimiterService.md2
-rw-r--r--packages/backend/src/server/SkRateLimiterService.ts2
-rw-r--r--packages/backend/src/server/api/endpoints/i/update.ts8
-rw-r--r--packages/backend/src/server/api/endpoints/users/report-abuse.ts10
-rw-r--r--packages/backend/test/unit/server/api/SkRateLimiterServiceTests.ts36
-rw-r--r--packages/misskey-js/package.json2
12 files changed, 81 insertions, 43 deletions
diff --git a/package.json b/package.json
index 24777d9d87..5055eff10f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "sharkey",
- "version": "2025.4.3",
+ "version": "2025.4.4",
"codename": "shonk",
"repository": {
"type": "git",
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 () => {
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index 48d5912a07..059c7c2eae 100644
--- a/packages/misskey-js/package.json
+++ b/packages/misskey-js/package.json
@@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
- "version": "2025.4.3",
+ "version": "2025.4.4",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",