diff --git a/locales/index.d.ts b/locales/index.d.ts index df6efe0d6a..58a4faee6c 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -7599,6 +7599,10 @@ export interface Locale extends ILocale { * Maximum number of scheduled notes */ "scheduleNoteMax": string; + /** + * Can appear in trending notes / users + */ + "canTrend": string; }; "_condition": { /** diff --git a/packages/backend/src/core/FeaturedService.ts b/packages/backend/src/core/FeaturedService.ts index b3335e38da..cabbb46504 100644 --- a/packages/backend/src/core/FeaturedService.ts +++ b/packages/backend/src/core/FeaturedService.ts @@ -8,6 +8,7 @@ import * as Redis from 'ioredis'; import type { MiGalleryPost, MiNote, MiUser } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; +import { RoleService } from '@/core/RoleService.js'; const GLOBAL_NOTES_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 3; // 3日ごと export const GALLERY_POSTS_RANKING_WINDOW = 1000 * 60 * 60 * 24 * 3; // 3日ごと @@ -21,6 +22,8 @@ export class FeaturedService { constructor( @Inject(DI.redis) private redisClient: Redis.Redis, // TODO: 専用のRedisサーバーを設定できるようにする + + private readonly roleService: RoleService, ) { } @@ -31,7 +34,14 @@ export class FeaturedService { } @bindThis - private async updateRankingOf(name: string, windowRange: number, element: string, score = 1): Promise { + private async updateRankingOf(name: string, windowRange: number, element: string, score: number, userId: string | null): Promise { + if (userId) { + const policies = await this.roleService.getUserPolicies(userId); + if (!policies.canTrend) { + return; + } + } + const currentWindow = this.getCurrentWindow(windowRange); const redisTransaction = this.redisClient.multi(); redisTransaction.zincrby( @@ -89,28 +99,28 @@ export class FeaturedService { } @bindThis - public updateGlobalNotesRanking(noteId: MiNote['id'], score = 1): Promise { - return this.updateRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, noteId, score); + public updateGlobalNotesRanking(note: Pick, score = 1): Promise { + return this.updateRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, note.id, score, note.userId); } @bindThis - public updateGalleryPostsRanking(galleryPostId: MiGalleryPost['id'], score = 1): Promise { - return this.updateRankingOf('featuredGalleryPostsRanking', GALLERY_POSTS_RANKING_WINDOW, galleryPostId, score); + public updateGalleryPostsRanking(galleryPost: Pick, score = 1): Promise { + return this.updateRankingOf('featuredGalleryPostsRanking', GALLERY_POSTS_RANKING_WINDOW, galleryPost.id, score, galleryPost.userId); } @bindThis - public updateInChannelNotesRanking(channelId: MiNote['channelId'], noteId: MiNote['id'], score = 1): Promise { - return this.updateRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, noteId, score); + public updateInChannelNotesRanking(channelId: MiNote['channelId'], note: Pick, score = 1): Promise { + return this.updateRankingOf(`featuredInChannelNotesRanking:${channelId}`, GLOBAL_NOTES_RANKING_WINDOW, note.id, score, note.userId); } @bindThis - public updatePerUserNotesRanking(userId: MiUser['id'], noteId: MiNote['id'], score = 1): Promise { - return this.updateRankingOf(`featuredPerUserNotesRanking:${userId}`, PER_USER_NOTES_RANKING_WINDOW, noteId, score); + public updatePerUserNotesRanking(userId: MiUser['id'], note: Pick, score = 1): Promise { + return this.updateRankingOf(`featuredPerUserNotesRanking:${userId}`, PER_USER_NOTES_RANKING_WINDOW, note.id, score, userId); } @bindThis public updateHashtagsRanking(hashtag: string, score = 1): Promise { - return this.updateRankingOf('featuredHashtagsRanking', HASHTAG_RANKING_WINDOW, hashtag, score); + return this.updateRankingOf('featuredHashtagsRanking', HASHTAG_RANKING_WINDOW, hashtag, score, null); } @bindThis diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index e961d4236c..b810ae0b5b 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -826,12 +826,12 @@ export class NoteCreateService implements OnApplicationShutdown { if (Math.random() < 0.3 && (Date.now() - this.idService.parse(renote.id).date.getTime()) < 1000 * 60 * 60 * 24 * 3) { if (renote.channelId != null) { if (renote.replyId == null) { - this.featuredService.updateInChannelNotesRanking(renote.channelId, renote.id, 5); + this.featuredService.updateInChannelNotesRanking(renote.channelId, renote, 5); } } else { if (renote.visibility === 'public' && renote.userHost == null && renote.replyId == null) { - this.featuredService.updateGlobalNotesRanking(renote.id, 5); - this.featuredService.updatePerUserNotesRanking(renote.userId, renote.id, 5); + this.featuredService.updateGlobalNotesRanking(renote, 5); + this.featuredService.updatePerUserNotesRanking(renote.userId, renote, 5); } } } diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index 0179b0680f..6cc4ef4205 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -220,12 +220,12 @@ export class ReactionService { ) { if (note.channelId != null) { if (note.replyId == null) { - this.featuredService.updateInChannelNotesRanking(note.channelId, note.id, 1); + this.featuredService.updateInChannelNotesRanking(note.channelId, note, 1); } } else { if (note.visibility === 'public' && note.userHost == null && note.replyId == null) { - this.featuredService.updateGlobalNotesRanking(note.id, 1); - this.featuredService.updatePerUserNotesRanking(note.userId, note.id, 1); + this.featuredService.updateGlobalNotesRanking(note, 1); + this.featuredService.updatePerUserNotesRanking(note.userId, note, 1); } } } diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 039932b76d..c3b60837cf 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -69,6 +69,7 @@ export type RolePolicies = { canImportMuting: boolean; canImportUserLists: boolean; chatAvailability: 'available' | 'readonly' | 'unavailable'; + canTrend: boolean; }; export const DEFAULT_POLICIES: RolePolicies = { @@ -108,6 +109,7 @@ export const DEFAULT_POLICIES: RolePolicies = { canImportMuting: true, canImportUserLists: true, chatAvailability: 'available', + canTrend: true, }; @Injectable() @@ -149,6 +151,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { ) { this.rolesCache = new MemorySingleCache(1000 * 60 * 60); // 1h this.roleAssignmentByUserIdCache = new MemoryKVCache(1000 * 60 * 5); // 5m + // TODO additional cache for final calculation? this.redisForSub.on('message', this.onMessage); } @@ -465,6 +468,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { canImportMuting: calc('canImportMuting', vs => vs.some(v => v === true)), canImportUserLists: calc('canImportUserLists', vs => vs.some(v => v === true)), chatAvailability: calc('chatAvailability', aggregateChatAvailability), + canTrend: calc('canTrend', vs => vs.some(v => v === true)), }; } diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts index 307c114c96..363be921ed 100644 --- a/packages/backend/src/models/json-schema/role.ts +++ b/packages/backend/src/models/json-schema/role.ts @@ -309,6 +309,10 @@ export const packedRolePoliciesSchema = { optional: false, nullable: false, enum: ['available', 'readonly', 'unavailable'], }, + canTrend: { + type: 'boolean', + optional: false, nullable: false, + }, }, } as const; diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts index e73110648c..ae8ad6c044 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/like.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/like.ts @@ -98,7 +98,7 @@ export default class extends Endpoint { // eslint- // ランキング更新 if (Date.now() - this.idService.parse(post.id).date.getTime() < GALLERY_POSTS_RANKING_WINDOW) { - await this.featuredService.updateGalleryPostsRanking(post.id, 1); + await this.featuredService.updateGalleryPostsRanking(post, 1); } this.galleryPostsRepository.increment({ id: post.id }, 'likedCount', 1); diff --git a/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts b/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts index b0fad1eff2..be0a5a5584 100644 --- a/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts +++ b/packages/backend/src/server/api/endpoints/gallery/posts/unlike.ts @@ -81,7 +81,7 @@ export default class extends Endpoint { // eslint- // ランキング更新 if (Date.now() - this.idService.parse(post.id).date.getTime() < GALLERY_POSTS_RANKING_WINDOW) { - await this.featuredService.updateGalleryPostsRanking(post.id, -1); + await this.featuredService.updateGalleryPostsRanking(post, -1); } this.galleryPostsRepository.decrement({ id: post.id }, 'likedCount', 1); diff --git a/packages/frontend-shared/js/const.ts b/packages/frontend-shared/js/const.ts index 8354e8b800..2865656157 100644 --- a/packages/frontend-shared/js/const.ts +++ b/packages/frontend-shared/js/const.ts @@ -176,6 +176,7 @@ export const ROLE_POLICIES = [ 'canImportMuting', 'canImportUserLists', 'chatAvailability', + 'canTrend', ] as const; export const DEFAULT_SERVER_ERROR_IMAGE_URL = '/client-assets/status/error.png'; diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue index bca619c2e1..68e6a99991 100644 --- a/packages/frontend/src/pages/admin/roles.editor.vue +++ b/packages/frontend/src/pages/admin/roles.editor.vue @@ -797,6 +797,26 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + +
+ + + + + + + + + +
+
diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue index 52b8240733..d29f1266ae 100644 --- a/packages/frontend/src/pages/admin/roles.vue +++ b/packages/frontend/src/pages/admin/roles.vue @@ -300,6 +300,14 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + + + + + {{ i18n.ts._role.new }} diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 0244a725c2..5487eca388 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -5520,6 +5520,7 @@ export type components = { scheduleNoteMax: number; /** @enum {string} */ chatAvailability: 'available' | 'readonly' | 'unavailable'; + canTrend: boolean; }; ReversiGameLite: { /** Format: id */ diff --git a/sharkey-locales/en-US.yml b/sharkey-locales/en-US.yml index 93e178c636..13f0df6c1a 100644 --- a/sharkey-locales/en-US.yml +++ b/sharkey-locales/en-US.yml @@ -239,6 +239,7 @@ _role: canImportNotes: "Can import notes" canUpdateBioMedia: "Allow users to edit their avatar or banner" scheduleNoteMax: "Maximum number of scheduled notes" + canTrend: "Can appear in trending notes / users" _condition: isLocked: "Private account" isExplorable: "Account is discoverable"