diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 943168061..1982681ae 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -968,6 +968,10 @@ _role: isRemote: "リモートユーザー" createdLessThan: "アカウント作成から~以内" createdMoreThan: "アカウント作成から~経過" + followersLessThanOrEq: "フォロワー数が~以下" + followersMoreThanOrEq: "フォロワー数が~以上" + followingLessThanOrEq: "フォロー数が~以下" + followingMoreThanOrEq: "フォロー数が~以上" and: "~かつ~" or: "~または~" not: "~ではない" diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts index 1b5abce29..be755f7da 100644 --- a/packages/backend/src/core/AntennaService.ts +++ b/packages/backend/src/core/AntennaService.ts @@ -16,6 +16,7 @@ import { DI } from '@/di-symbols.js'; import type { MutingsRepository, BlockingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserGroupJoiningsRepository, UserListJoiningsRepository } from '@/models/index.js'; import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; +import { StreamMessages } from '@/server/api/stream/types.js'; import type { OnApplicationShutdown } from '@nestjs/common'; @Injectable() @@ -73,7 +74,7 @@ export class AntennaService implements OnApplicationShutdown { const obj = JSON.parse(data); if (obj.channel === 'internal') { - const { type, body } = obj.message; + const { type, body } = obj.message as StreamMessages['internal']['payload']; switch (type) { case 'antennaCreated': this.antennas.push(body); diff --git a/packages/backend/src/core/MetaService.ts b/packages/backend/src/core/MetaService.ts index ff05779ae..4b792c083 100644 --- a/packages/backend/src/core/MetaService.ts +++ b/packages/backend/src/core/MetaService.ts @@ -4,8 +4,9 @@ import Redis from 'ioredis'; import { DI } from '@/di-symbols.js'; import { Meta } from '@/models/entities/Meta.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; -import type { OnApplicationShutdown } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; +import { StreamMessages } from '@/server/api/stream/types.js'; +import type { OnApplicationShutdown } from '@nestjs/common'; @Injectable() export class MetaService implements OnApplicationShutdown { @@ -40,7 +41,7 @@ export class MetaService implements OnApplicationShutdown { const obj = JSON.parse(data); if (obj.channel === 'internal') { - const { type, body } = obj.message; + const { type, body } = obj.message as StreamMessages['internal']['payload']; switch (type) { case 'metaUpdated': { this.cache = body; diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index d2056709e..e7821ebd7 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -10,6 +10,7 @@ import { MetaService } from '@/core/MetaService.js'; import { UserCacheService } from '@/core/UserCacheService.js'; import { RoleCondFormulaValue } from '@/models/entities/Role.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { StreamMessages } from '@/server/api/stream/types.js'; import type { OnApplicationShutdown } from '@nestjs/common'; export type RoleOptions = { @@ -69,7 +70,7 @@ export class RoleService implements OnApplicationShutdown { const obj = JSON.parse(data); if (obj.channel === 'internal') { - const { type, body } = obj.message; + const { type, body } = obj.message as StreamMessages['internal']['payload']; switch (type) { case 'roleCreated': { const cached = this.rolesCache.get(null); @@ -147,6 +148,18 @@ export class RoleService implements OnApplicationShutdown { case 'createdMoreThan': { return user.createdAt.getTime() < (Date.now() - (value.sec * 1000)); } + case 'followersLessThanOrEq': { + return user.followersCount <= value.value; + } + case 'followersMoreThanOrEq': { + return user.followersCount >= value.value; + } + case 'followingLessThanOrEq': { + return user.followingCount <= value.value; + } + case 'followingMoreThanOrEq': { + return user.followingCount >= value.value; + } default: return false; } diff --git a/packages/backend/src/core/UserCacheService.ts b/packages/backend/src/core/UserCacheService.ts index 4d9ee7366..29a64f584 100644 --- a/packages/backend/src/core/UserCacheService.ts +++ b/packages/backend/src/core/UserCacheService.ts @@ -6,6 +6,7 @@ import type { CacheableLocalUser, CacheableUser, ILocalUser, User } from '@/mode import { DI } from '@/di-symbols.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; +import { StreamMessages } from '@/server/api/stream/types.js'; import type { OnApplicationShutdown } from '@nestjs/common'; @Injectable() @@ -39,7 +40,7 @@ export class UserCacheService implements OnApplicationShutdown { const obj = JSON.parse(data); if (obj.channel === 'internal') { - const { type, body } = obj.message; + const { type, body } = obj.message as StreamMessages['internal']['payload']; switch (type) { case 'userChangeSuspendedState': case 'remoteUserUpdated': { @@ -62,6 +63,13 @@ export class UserCacheService implements OnApplicationShutdown { this.localUserByNativeTokenCache.set(body.newToken, user); break; } + case 'follow': { + const follower = this.userByIdCache.get(body.followerId); + if (follower) follower.followingCount++; + const followee = this.userByIdCache.get(body.followeeId); + if (followee) followee.followersCount++; + break; + } default: break; } diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index 52834c375..f1ce311ce 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -62,6 +62,7 @@ export class UserFollowingService { private federatedInstanceService: FederatedInstanceService, private webhookService: WebhookService, private apRendererService: ApRendererService, + private globalEventService: GlobalEventService, private perUserFollowingChart: PerUserFollowingChart, private instanceChart: InstanceChart, ) { @@ -195,6 +196,8 @@ export class UserFollowingService { } if (alreadyFollowed) return; + + this.globalEventService.publishInternalEvent('follow', { followerId: follower.id, followeeId: followee.id }); //#region Increment counts await Promise.all([ @@ -314,6 +317,8 @@ export class UserFollowingService { follower: {id: User['id']; host: User['host']; }, followee: { id: User['id']; host: User['host']; }, ): Promise { + this.globalEventService.publishInternalEvent('unfollow', { followerId: follower.id, followeeId: followee.id }); + //#region Decrement following / followers counts await Promise.all([ this.usersRepository.decrement({ id: follower.id }, 'followingCount', 1), diff --git a/packages/backend/src/core/WebhookService.ts b/packages/backend/src/core/WebhookService.ts index 91a39f135..36110490a 100644 --- a/packages/backend/src/core/WebhookService.ts +++ b/packages/backend/src/core/WebhookService.ts @@ -3,8 +3,9 @@ import Redis from 'ioredis'; import type { WebhooksRepository } from '@/models/index.js'; import type { Webhook } from '@/models/entities/Webhook.js'; import { DI } from '@/di-symbols.js'; -import type { OnApplicationShutdown } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; +import { StreamMessages } from '@/server/api/stream/types.js'; +import type { OnApplicationShutdown } from '@nestjs/common'; @Injectable() export class WebhookService implements OnApplicationShutdown { @@ -39,7 +40,7 @@ export class WebhookService implements OnApplicationShutdown { const obj = JSON.parse(data); if (obj.channel === 'internal') { - const { type, body } = obj.message; + const { type, body } = obj.message as StreamMessages['internal']['payload']; switch (type) { case 'webhookCreated': if (body.active) { diff --git a/packages/backend/src/models/entities/Role.ts b/packages/backend/src/models/entities/Role.ts index f7b4edc9e..a18df40d0 100644 --- a/packages/backend/src/models/entities/Role.ts +++ b/packages/backend/src/models/entities/Role.ts @@ -34,6 +34,26 @@ type CondFormulaValueCreatedMoreThan = { sec: number; }; +type CondFormulaValueFollowersLessThanOrEq = { + type: 'followersLessThanOrEq'; + value: number; +}; + +type CondFormulaValueFollowersMoreThanOrEq = { + type: 'followersMoreThanOrEq'; + value: number; +}; + +type CondFormulaValueFollowingLessThanOrEq = { + type: 'followingLessThanOrEq'; + value: number; +}; + +type CondFormulaValueFollowingMoreThanOrEq = { + type: 'followingMoreThanOrEq'; + value: number; +}; + export type RoleCondFormulaValue = CondFormulaValueAnd | CondFormulaValueOr | @@ -41,7 +61,11 @@ export type RoleCondFormulaValue = CondFormulaValueIsLocal | CondFormulaValueIsRemote | CondFormulaValueCreatedLessThan | - CondFormulaValueCreatedMoreThan; + CondFormulaValueCreatedMoreThan | + CondFormulaValueFollowersLessThanOrEq | + CondFormulaValueFollowersMoreThanOrEq | + CondFormulaValueFollowingLessThanOrEq | + CondFormulaValueFollowingMoreThanOrEq; @Entity() export class Role { diff --git a/packages/backend/src/server/api/stream/types.ts b/packages/backend/src/server/api/stream/types.ts index 3bc844f94..03837baef 100644 --- a/packages/backend/src/server/api/stream/types.ts +++ b/packages/backend/src/server/api/stream/types.ts @@ -14,7 +14,7 @@ import type { Page } from '@/models/entities/Page.js'; import type { Packed } from '@/misc/schema.js'; import type { Webhook } from '@/models/entities/Webhook.js'; import type { Meta } from '@/models/entities/Meta.js'; -import { Role, RoleAssignment } from '@/models'; +import { Following, Role, RoleAssignment } from '@/models'; import type Emitter from 'strict-event-emitter-types'; import type { EventEmitter } from 'events'; @@ -28,6 +28,8 @@ export interface InternalStreamTypes { userChangeSuspendedState: Serialized<{ id: User['id']; isSuspended: User['isSuspended']; }>; userTokenRegenerated: Serialized<{ id: User['id']; oldToken: User['token']; newToken: User['token']; }>; remoteUserUpdated: Serialized<{ id: User['id']; }>; + follow: Serialized<{ followerId: User['id']; followeeId: User['id']; }>; + unfollow: Serialized<{ followerId: User['id']; followeeId: User['id']; }>; defaultRoleOverrideUpdated: Serialized; roleCreated: Serialized; roleDeleted: Serialized; diff --git a/packages/frontend/src/pages/admin/RolesEditorFormula.vue b/packages/frontend/src/pages/admin/RolesEditorFormula.vue index 1cce5e58e..5bd380348 100644 --- a/packages/frontend/src/pages/admin/RolesEditorFormula.vue +++ b/packages/frontend/src/pages/admin/RolesEditorFormula.vue @@ -6,6 +6,10 @@ + + + + @@ -37,6 +41,9 @@ + + + @@ -85,6 +92,10 @@ const type = computed({ if (t === 'not') v.value.value = { id: uuid(), type: 'isRemote' }; if (t === 'createdLessThan') v.value.sec = 86400; if (t === 'createdMoreThan') v.value.sec = 86400; + if (t === 'followersLessThanOrEq') v.value.value = 10; + if (t === 'followersMoreThanOrEq') v.value.value = 10; + if (t === 'followingLessThanOrEq') v.value.value = 10; + if (t === 'followingMoreThanOrEq') v.value.value = 10; v.value.type = t; }, });