フォロワー数、フォロー数もConditional roleで利用できるように

This commit is contained in:
syuilo 2023-01-14 08:27:23 +09:00
parent 39c058a4bb
commit 4151087d3c
10 changed files with 79 additions and 9 deletions

View file

@ -968,6 +968,10 @@ _role:
isRemote: "リモートユーザー" isRemote: "リモートユーザー"
createdLessThan: "アカウント作成から~以内" createdLessThan: "アカウント作成から~以内"
createdMoreThan: "アカウント作成から~経過" createdMoreThan: "アカウント作成から~経過"
followersLessThanOrEq: "フォロワー数が~以下"
followersMoreThanOrEq: "フォロワー数が~以上"
followingLessThanOrEq: "フォロー数が~以下"
followingMoreThanOrEq: "フォロー数が~以上"
and: "~かつ~" and: "~かつ~"
or: "~または~" or: "~または~"
not: "~ではない" not: "~ではない"

View file

@ -16,6 +16,7 @@ import { DI } from '@/di-symbols.js';
import type { MutingsRepository, BlockingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserGroupJoiningsRepository, UserListJoiningsRepository } from '@/models/index.js'; import type { MutingsRepository, BlockingsRepository, NotesRepository, AntennaNotesRepository, AntennasRepository, UserGroupJoiningsRepository, UserListJoiningsRepository } from '@/models/index.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { StreamMessages } from '@/server/api/stream/types.js';
import type { OnApplicationShutdown } from '@nestjs/common'; import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable() @Injectable()
@ -73,7 +74,7 @@ export class AntennaService implements OnApplicationShutdown {
const obj = JSON.parse(data); const obj = JSON.parse(data);
if (obj.channel === 'internal') { if (obj.channel === 'internal') {
const { type, body } = obj.message; const { type, body } = obj.message as StreamMessages['internal']['payload'];
switch (type) { switch (type) {
case 'antennaCreated': case 'antennaCreated':
this.antennas.push(body); this.antennas.push(body);

View file

@ -4,8 +4,9 @@ import Redis from 'ioredis';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { Meta } from '@/models/entities/Meta.js'; import { Meta } from '@/models/entities/Meta.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { OnApplicationShutdown } from '@nestjs/common';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { StreamMessages } from '@/server/api/stream/types.js';
import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable() @Injectable()
export class MetaService implements OnApplicationShutdown { export class MetaService implements OnApplicationShutdown {
@ -40,7 +41,7 @@ export class MetaService implements OnApplicationShutdown {
const obj = JSON.parse(data); const obj = JSON.parse(data);
if (obj.channel === 'internal') { if (obj.channel === 'internal') {
const { type, body } = obj.message; const { type, body } = obj.message as StreamMessages['internal']['payload'];
switch (type) { switch (type) {
case 'metaUpdated': { case 'metaUpdated': {
this.cache = body; this.cache = body;

View file

@ -10,6 +10,7 @@ import { MetaService } from '@/core/MetaService.js';
import { UserCacheService } from '@/core/UserCacheService.js'; import { UserCacheService } from '@/core/UserCacheService.js';
import { RoleCondFormulaValue } from '@/models/entities/Role.js'; import { RoleCondFormulaValue } from '@/models/entities/Role.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { StreamMessages } from '@/server/api/stream/types.js';
import type { OnApplicationShutdown } from '@nestjs/common'; import type { OnApplicationShutdown } from '@nestjs/common';
export type RoleOptions = { export type RoleOptions = {
@ -69,7 +70,7 @@ export class RoleService implements OnApplicationShutdown {
const obj = JSON.parse(data); const obj = JSON.parse(data);
if (obj.channel === 'internal') { if (obj.channel === 'internal') {
const { type, body } = obj.message; const { type, body } = obj.message as StreamMessages['internal']['payload'];
switch (type) { switch (type) {
case 'roleCreated': { case 'roleCreated': {
const cached = this.rolesCache.get(null); const cached = this.rolesCache.get(null);
@ -147,6 +148,18 @@ export class RoleService implements OnApplicationShutdown {
case 'createdMoreThan': { case 'createdMoreThan': {
return user.createdAt.getTime() < (Date.now() - (value.sec * 1000)); 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: default:
return false; return false;
} }

View file

@ -6,6 +6,7 @@ import type { CacheableLocalUser, CacheableUser, ILocalUser, User } from '@/mode
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { StreamMessages } from '@/server/api/stream/types.js';
import type { OnApplicationShutdown } from '@nestjs/common'; import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable() @Injectable()
@ -39,7 +40,7 @@ export class UserCacheService implements OnApplicationShutdown {
const obj = JSON.parse(data); const obj = JSON.parse(data);
if (obj.channel === 'internal') { if (obj.channel === 'internal') {
const { type, body } = obj.message; const { type, body } = obj.message as StreamMessages['internal']['payload'];
switch (type) { switch (type) {
case 'userChangeSuspendedState': case 'userChangeSuspendedState':
case 'remoteUserUpdated': { case 'remoteUserUpdated': {
@ -62,6 +63,13 @@ export class UserCacheService implements OnApplicationShutdown {
this.localUserByNativeTokenCache.set(body.newToken, user); this.localUserByNativeTokenCache.set(body.newToken, user);
break; 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: default:
break; break;
} }

View file

@ -62,6 +62,7 @@ export class UserFollowingService {
private federatedInstanceService: FederatedInstanceService, private federatedInstanceService: FederatedInstanceService,
private webhookService: WebhookService, private webhookService: WebhookService,
private apRendererService: ApRendererService, private apRendererService: ApRendererService,
private globalEventService: GlobalEventService,
private perUserFollowingChart: PerUserFollowingChart, private perUserFollowingChart: PerUserFollowingChart,
private instanceChart: InstanceChart, private instanceChart: InstanceChart,
) { ) {
@ -195,6 +196,8 @@ export class UserFollowingService {
} }
if (alreadyFollowed) return; if (alreadyFollowed) return;
this.globalEventService.publishInternalEvent('follow', { followerId: follower.id, followeeId: followee.id });
//#region Increment counts //#region Increment counts
await Promise.all([ await Promise.all([
@ -314,6 +317,8 @@ export class UserFollowingService {
follower: {id: User['id']; host: User['host']; }, follower: {id: User['id']; host: User['host']; },
followee: { id: User['id']; host: User['host']; }, followee: { id: User['id']; host: User['host']; },
): Promise<void> { ): Promise<void> {
this.globalEventService.publishInternalEvent('unfollow', { followerId: follower.id, followeeId: followee.id });
//#region Decrement following / followers counts //#region Decrement following / followers counts
await Promise.all([ await Promise.all([
this.usersRepository.decrement({ id: follower.id }, 'followingCount', 1), this.usersRepository.decrement({ id: follower.id }, 'followingCount', 1),

View file

@ -3,8 +3,9 @@ import Redis from 'ioredis';
import type { WebhooksRepository } from '@/models/index.js'; import type { WebhooksRepository } from '@/models/index.js';
import type { Webhook } from '@/models/entities/Webhook.js'; import type { Webhook } from '@/models/entities/Webhook.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import type { OnApplicationShutdown } from '@nestjs/common';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { StreamMessages } from '@/server/api/stream/types.js';
import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable() @Injectable()
export class WebhookService implements OnApplicationShutdown { export class WebhookService implements OnApplicationShutdown {
@ -39,7 +40,7 @@ export class WebhookService implements OnApplicationShutdown {
const obj = JSON.parse(data); const obj = JSON.parse(data);
if (obj.channel === 'internal') { if (obj.channel === 'internal') {
const { type, body } = obj.message; const { type, body } = obj.message as StreamMessages['internal']['payload'];
switch (type) { switch (type) {
case 'webhookCreated': case 'webhookCreated':
if (body.active) { if (body.active) {

View file

@ -34,6 +34,26 @@ type CondFormulaValueCreatedMoreThan = {
sec: number; 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 = export type RoleCondFormulaValue =
CondFormulaValueAnd | CondFormulaValueAnd |
CondFormulaValueOr | CondFormulaValueOr |
@ -41,7 +61,11 @@ export type RoleCondFormulaValue =
CondFormulaValueIsLocal | CondFormulaValueIsLocal |
CondFormulaValueIsRemote | CondFormulaValueIsRemote |
CondFormulaValueCreatedLessThan | CondFormulaValueCreatedLessThan |
CondFormulaValueCreatedMoreThan; CondFormulaValueCreatedMoreThan |
CondFormulaValueFollowersLessThanOrEq |
CondFormulaValueFollowersMoreThanOrEq |
CondFormulaValueFollowingLessThanOrEq |
CondFormulaValueFollowingMoreThanOrEq;
@Entity() @Entity()
export class Role { export class Role {

View file

@ -14,7 +14,7 @@ import type { Page } from '@/models/entities/Page.js';
import type { Packed } from '@/misc/schema.js'; import type { Packed } from '@/misc/schema.js';
import type { Webhook } from '@/models/entities/Webhook.js'; import type { Webhook } from '@/models/entities/Webhook.js';
import type { Meta } from '@/models/entities/Meta.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 Emitter from 'strict-event-emitter-types';
import type { EventEmitter } from 'events'; import type { EventEmitter } from 'events';
@ -28,6 +28,8 @@ export interface InternalStreamTypes {
userChangeSuspendedState: Serialized<{ id: User['id']; isSuspended: User['isSuspended']; }>; userChangeSuspendedState: Serialized<{ id: User['id']; isSuspended: User['isSuspended']; }>;
userTokenRegenerated: Serialized<{ id: User['id']; oldToken: User['token']; newToken: User['token']; }>; userTokenRegenerated: Serialized<{ id: User['id']; oldToken: User['token']; newToken: User['token']; }>;
remoteUserUpdated: Serialized<{ id: User['id']; }>; remoteUserUpdated: Serialized<{ id: User['id']; }>;
follow: Serialized<{ followerId: User['id']; followeeId: User['id']; }>;
unfollow: Serialized<{ followerId: User['id']; followeeId: User['id']; }>;
defaultRoleOverrideUpdated: Serialized<Role['options']>; defaultRoleOverrideUpdated: Serialized<Role['options']>;
roleCreated: Serialized<Role>; roleCreated: Serialized<Role>;
roleDeleted: Serialized<Role>; roleDeleted: Serialized<Role>;

View file

@ -6,6 +6,10 @@
<option value="isRemote">{{ i18n.ts._role._condition.isRemote }}</option> <option value="isRemote">{{ i18n.ts._role._condition.isRemote }}</option>
<option value="createdLessThan">{{ i18n.ts._role._condition.createdLessThan }}</option> <option value="createdLessThan">{{ i18n.ts._role._condition.createdLessThan }}</option>
<option value="createdMoreThan">{{ i18n.ts._role._condition.createdMoreThan }}</option> <option value="createdMoreThan">{{ i18n.ts._role._condition.createdMoreThan }}</option>
<option value="followersLessThanOrEq">{{ i18n.ts._role._condition.followersLessThanOrEq }}</option>
<option value="followersMoreThanOrEq">{{ i18n.ts._role._condition.followersMoreThanOrEq }}</option>
<option value="followingLessThanOrEq">{{ i18n.ts._role._condition.followingLessThanOrEq }}</option>
<option value="followingMoreThanOrEq">{{ i18n.ts._role._condition.followingMoreThanOrEq }}</option>
<option value="and">{{ i18n.ts._role._condition.and }}</option> <option value="and">{{ i18n.ts._role._condition.and }}</option>
<option value="or">{{ i18n.ts._role._condition.or }}</option> <option value="or">{{ i18n.ts._role._condition.or }}</option>
<option value="not">{{ i18n.ts._role._condition.not }}</option> <option value="not">{{ i18n.ts._role._condition.not }}</option>
@ -37,6 +41,9 @@
<MkInput v-else-if="type === 'createdLessThan' || type === 'createdMoreThan'" v-model="v.sec" type="number"> <MkInput v-else-if="type === 'createdLessThan' || type === 'createdMoreThan'" v-model="v.sec" type="number">
<template #suffix>sec</template> <template #suffix>sec</template>
</MkInput> </MkInput>
<MkInput v-else-if="['followersLessThanOrEq', 'followersMoreThanOrEq', 'followingLessThanOrEq', 'followingMoreThanOrEq'].includes(type)" v-model="v.value" type="number">
</MkInput>
</div> </div>
</template> </template>
@ -85,6 +92,10 @@ const type = computed({
if (t === 'not') v.value.value = { id: uuid(), type: 'isRemote' }; if (t === 'not') v.value.value = { id: uuid(), type: 'isRemote' };
if (t === 'createdLessThan') v.value.sec = 86400; if (t === 'createdLessThan') v.value.sec = 86400;
if (t === 'createdMoreThan') 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; v.value.type = t;
}, },
}); });