diff --git a/locales/en-US.yml b/locales/en-US.yml index 06e9970a1..e100aeaab 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1334,6 +1334,8 @@ useNormalization: "Show the Normalize menu" alsoMuteNotification: "Also mute notifications from this user" muteNotification: "Mute notifications" unmuteNotification: "Unmute notifications" +endingCreditMembers: "Users to display in the closing credits" +endingCreditMembersDescription: "IDs of users to display on the server information page, one per line." _bubbleGame: howToPlay: "How to play" hold: "Hold" diff --git a/locales/index.d.ts b/locales/index.d.ts index e35f7f348..f2b325b88 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5508,6 +5508,14 @@ export interface Locale extends ILocale { * 通知をミュート解除する */ "unmuteNotification": string; + /** + * スタッフロールに表示するユーザー + */ + "endingCreditMembers": string; + /** + * サーバー情報ページに表示するユーザーのIDを一行に一つずつ書きます。 + */ + "endingCreditMembersDescription": string; "_bubbleGame": { /** * 遊び方 diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 979dda73f..3ccf5d597 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1370,6 +1370,8 @@ consentSelected: "選択した項目のみ許可" alsoMuteNotification: "このユーザーが送信する通知も一緒にミュートする" muteNotification: "通知をミュートする" unmuteNotification: "通知をミュート解除する" +endingCreditMembers: "スタッフロールに表示するユーザー" +endingCreditMembersDescription: "サーバー情報ページに表示するユーザーのIDを一行に一つずつ書きます。" _bubbleGame: howToPlay: "遊び方" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index be5d35218..af62a5e79 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -385,7 +385,7 @@ pinnedPagesDescription: "서버의 대문에 고정하고 싶은 페이지의 pinnedClipId: "고정할 클립의 ID" pinnedNotes: "고정된 노트" featuredGameChannels: "Misskey Games에 고정할 채널" -featuredGameChannelsDescription: "Misskey Games에 고정할 채널을 한 줄에 하나씩 적습니다." +featuredGameChannelsDescription: "Misskey Games에 고정할 채널의 ID를 한 줄에 하나씩 적습니다." hcaptcha: "hCaptcha" enableHcaptcha: "hCaptcha 활성화" hcaptchaSiteKey: "사이트 키" @@ -1359,6 +1359,8 @@ consentSelected: "선택한 항목만 허용" alsoMuteNotification: "이 유저가 보내는 알림도 같이 뮤트하기" muteNotification: "알림을 뮤트하기" unmuteNotification: "알림 뮤트를 해제하기" +endingCreditMembers: "엔딩 크레딧에 표시할 유저" +endingCreditMembersDescription: "서버 정보 페이지에 표시할 유저의 ID를 한 줄에 하나씩 적습니다." _bubbleGame: howToPlay: "설명" diff --git a/packages/backend/migration/1732695214576-EndingCreditMembers.js b/packages/backend/migration/1732695214576-EndingCreditMembers.js new file mode 100644 index 000000000..ec17da98c --- /dev/null +++ b/packages/backend/migration/1732695214576-EndingCreditMembers.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class EndingCreditMembers1732695214576 { + name = 'EndingCreditMembers1732695214576' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" ADD "endingCreditMembers" character varying(1024) array NOT NULL DEFAULT '{}'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "endingCreditMembers"`); + } +} diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts index ff081a01d..b9f37fae2 100644 --- a/packages/backend/src/models/Meta.ts +++ b/packages/backend/src/models/Meta.ts @@ -614,6 +614,11 @@ export class MiMeta { }) public featuredGameChannels: string[]; + @Column('varchar', { + length: 1024, array: true, default: '{}', + }) + public endingCreditMembers: string[]; + @Column('boolean', { default: true, }) diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 35e2bd01b..470da52a1 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -358,6 +358,7 @@ import * as ep___test from './endpoints/test.js'; import * as ep___username_available from './endpoints/username/available.js'; import * as ep___users from './endpoints/users.js'; import * as ep___users_clips from './endpoints/users/clips.js'; +import * as ep___users_credits from './endpoints/users/credits.js'; import * as ep___users_followers from './endpoints/users/followers.js'; import * as ep___users_following from './endpoints/users/following.js'; import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js'; @@ -758,6 +759,7 @@ const $test: Provider = { provide: 'ep:test', useClass: ep___test.default }; const $username_available: Provider = { provide: 'ep:username/available', useClass: ep___username_available.default }; const $users: Provider = { provide: 'ep:users', useClass: ep___users.default }; const $users_clips: Provider = { provide: 'ep:users/clips', useClass: ep___users_clips.default }; +const $users_credits: Provider = { provide: 'ep:users/credits', useClass: ep___users_credits.default }; const $users_followers: Provider = { provide: 'ep:users/followers', useClass: ep___users_followers.default }; const $users_following: Provider = { provide: 'ep:users/following', useClass: ep___users_following.default }; const $users_gallery_posts: Provider = { provide: 'ep:users/gallery/posts', useClass: ep___users_gallery_posts.default }; @@ -1162,6 +1164,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $username_available, $users, $users_clips, + $users_credits, $users_followers, $users_following, $users_gallery_posts, @@ -1558,6 +1561,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $username_available, $users, $users_clips, + $users_credits, $users_followers, $users_following, $users_gallery_posts, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index c9ea45e44..1a6c4a15e 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -358,6 +358,7 @@ import * as ep___test from './endpoints/test.js'; import * as ep___username_available from './endpoints/username/available.js'; import * as ep___users from './endpoints/users.js'; import * as ep___users_clips from './endpoints/users/clips.js'; +import * as ep___users_credits from './endpoints/users/credits.js'; import * as ep___users_followers from './endpoints/users/followers.js'; import * as ep___users_following from './endpoints/users/following.js'; import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js'; @@ -756,6 +757,7 @@ const eps = [ ['username/available', ep___username_available], ['users', ep___users], ['users/clips', ep___users_clips], + ['users/credits', ep___users_credits], ['users/followers', ep___users_followers], ['users/following', ep___users_following], ['users/gallery/posts', ep___users_gallery_posts], diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index e64844e07..77892e017 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -397,6 +397,13 @@ export const meta = { optional: false, nullable: false, }, }, + endingCreditMembers: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + }, + }, urlPreviewDenyList: { type: 'array', optional: false, nullable: false, @@ -646,6 +653,7 @@ export default class extends Endpoint { // eslint- perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax, perUserListTimelineCacheMax: instance.perUserListTimelineCacheMax, wellKnownWebsites: instance.wellKnownWebsites, + endingCreditMembers: instance.endingCreditMembers, notesPerOneAd: instance.notesPerOneAd, urlPreviewDenyList: instance.urlPreviewDenyList, featuredGameChannels: instance.featuredGameChannels, diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index f0df83a18..4ddad37fc 100644 --- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts @@ -161,6 +161,11 @@ export const paramDef = { type: 'string', }, }, + endingCreditMembers: { + type: 'array', nullable: true, items: { + type: 'string', + }, + }, urlPreviewDenyList: { type: 'array', nullable: true, items: { type: 'string', @@ -240,6 +245,10 @@ export default class extends Endpoint { // eslint- set.wellKnownWebsites = ps.wellKnownWebsites.filter(Boolean); } + if (Array.isArray(ps.endingCreditMembers)) { + set.wellKnownWebsites = ps.endingCreditMembers.filter(Boolean); + } + if (Array.isArray(ps.urlPreviewDenyList)) { set.urlPreviewDenyList = ps.urlPreviewDenyList.filter(Boolean); } diff --git a/packages/backend/src/server/api/endpoints/channels/featured-games.ts b/packages/backend/src/server/api/endpoints/channels/featured-games.ts index 46e203a3a..c598a1432 100644 --- a/packages/backend/src/server/api/endpoints/channels/featured-games.ts +++ b/packages/backend/src/server/api/endpoints/channels/featured-games.ts @@ -48,7 +48,7 @@ export default class extends Endpoint { // eslint- if (meta.featuredGameChannels.length === 0) return []; const channels = await this.channelsRepository.findBy({ - id: In(meta.featuredGameChannels) + id: In(meta.featuredGameChannels), }); return await this.channelEntityService.packMany(channels, me); diff --git a/packages/backend/src/server/api/endpoints/users/credits.ts b/packages/backend/src/server/api/endpoints/users/credits.ts new file mode 100644 index 000000000..ba8524b3b --- /dev/null +++ b/packages/backend/src/server/api/endpoints/users/credits.ts @@ -0,0 +1,66 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { In, IsNull } from 'typeorm'; +import { Inject, Injectable } from '@nestjs/common'; +import type { UsersRepository } from '@/models/_.js'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import { DI } from '@/di-symbols.js'; +import { MetaService } from '@/core/MetaService.js'; + +export const meta = { + tags: ['users'], + + requireCredential: false, + + description: 'List up the users of ending credits.', + + res: { + optional: false, nullable: false, + oneOf: [ + { + type: 'array', + items: { + type: 'object', + ref: 'UserLite', + }, + }, + ], + }, + + errors: {}, +} as const; + +export const paramDef = { + type: 'object', + properties: { + }, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor( + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + + private metaService: MetaService, + private userEntityService: UserEntityService, + ) { + super(meta, paramDef, async (ps, me) => { + const meta = await this.metaService.fetch(); + + if (meta.endingCreditMembers.length === 0) return []; + + const staffs = await this.usersRepository.findBy({ + id: In(meta.endingCreditMembers), + host: IsNull(), + }); + + return await this.userEntityService.packMany(staffs, me); + }); + } +} diff --git a/packages/frontend/src/pages/about.vue b/packages/frontend/src/pages/about.vue index 112de1849..194dbc2b8 100644 --- a/packages/frontend/src/pages/about.vue +++ b/packages/frontend/src/pages/about.vue @@ -133,15 +133,15 @@ SPDX-License-Identifier: AGPL-3.0-only
- + @{{ user.username }} - オスカー。 + {{ instance.name }} - + {{ `@${$i.username}` }} {{ i18n.ts.thankYou }} @@ -191,6 +191,7 @@ import FormSection from '@/components/form/section.vue'; import FormSuspense from '@/components/form/suspense.vue'; import FormSplit from '@/components/form/split.vue'; import MkA from '@/components/global/MkA.vue'; +import MkAvatar from '@/components/global/MkAvatar.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkKeyValue from '@/components/MkKeyValue.vue'; import MkInfo from '@/components/MkInfo.vue'; @@ -214,9 +215,13 @@ const props = withDefaults(defineProps<{ const stats = ref(null); const tab = ref(props.initialTab); const staffs = ref([]); -const staffIds = ['9ua5ndrtx7', '9urvwtlwo6']; +const instanceColor = computed(() => { + return { + color: `${instance.themeColor}`, + }; +}); -staffs.value = await misskeyApi('users/show', { userIds: staffIds }); +staffs.value = await misskeyApi('users/credits', {}); watch(tab, () => { if (tab.value === 'charts') { @@ -361,10 +366,6 @@ definePageMetadata(() => ({ font-size: 11px; font-weight: bold; - > .oscar { - color: #00ff63; - } - > .thankYou { color: #5a70ff; } diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue index 6f2f1d86e..4215c56c5 100644 --- a/packages/frontend/src/pages/admin/settings.vue +++ b/packages/frontend/src/pages/admin/settings.vue @@ -50,6 +50,11 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + + @@ -224,6 +229,7 @@ const maintainerEmail = ref(null); const impressumUrl = ref(null); const pinnedUsers = ref(''); const featuredGameChannels = ref(''); +const endingCreditMembers = ref(''); const cacheRemoteFiles = ref(false); const cacheRemoteSensitiveFiles = ref(false); const enableServiceWorker = ref(false); @@ -253,6 +259,7 @@ async function init(): Promise { impressumUrl.value = meta.impressumUrl; pinnedUsers.value = meta.pinnedUsers.join('\n'); featuredGameChannels.value = meta.featuredGameChannels.join('\n'); + endingCreditMembers.value = meta.endingCreditMembers.join('\n'); cacheRemoteFiles.value = meta.cacheRemoteFiles; cacheRemoteSensitiveFiles.value = meta.cacheRemoteSensitiveFiles; enableServiceWorker.value = meta.enableServiceWorker; @@ -283,6 +290,7 @@ async function save() { impressumUrl: impressumUrl.value, pinnedUsers: pinnedUsers.value.split('\n'), featuredGameChannels: featuredGameChannels.value.split('\n'), + endingCreditMembers: endingCreditMembers.value.split('\n'), cacheRemoteFiles: cacheRemoteFiles.value, cacheRemoteSensitiveFiles: cacheRemoteSensitiveFiles.value, enableServiceWorker: enableServiceWorker.value, diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 04dd118b0..306f55533 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -1736,6 +1736,7 @@ declare namespace entities { UsersResponse, UsersClipsRequest, UsersClipsResponse, + UsersCreditsResponse, UsersFollowersRequest, UsersFollowersResponse, UsersFollowingRequest, @@ -3148,6 +3149,9 @@ type UsersClipsRequest = operations['users___clips']['requestBody']['content'][' // @public (undocumented) type UsersClipsResponse = operations['users___clips']['responses']['200']['content']['application/json']; +// @public (undocumented) +type UsersCreditsResponse = operations['users___credits']['responses']['200']['content']['application/json']; + // @public (undocumented) type UsersFeaturedNotesRequest = operations['users___featured-notes']['requestBody']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts index e2a8d6b22..225e91cf3 100644 --- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts +++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts @@ -3918,6 +3918,17 @@ declare module '../api.js' { credential?: string | null, ): Promise>; + /** + * List up the users of ending credits. + * + * **Credential required**: *No* + */ + request( + endpoint: E, + params: P, + credential?: string | null, + ): Promise>; + /** * Show everyone that follows this user. * diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index 79f753373..77c9887ef 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -516,6 +516,7 @@ import type { UsersResponse, UsersClipsRequest, UsersClipsResponse, + UsersCreditsResponse, UsersFollowersRequest, UsersFollowersResponse, UsersFollowingRequest, @@ -946,6 +947,7 @@ export type Endpoints = { 'username/available': { req: UsernameAvailableRequest; res: UsernameAvailableResponse }; 'users': { req: UsersRequest; res: UsersResponse }; 'users/clips': { req: UsersClipsRequest; res: UsersClipsResponse }; + 'users/credits': { req: EmptyRequest; res: UsersCreditsResponse }; 'users/followers': { req: UsersFollowersRequest; res: UsersFollowersResponse }; 'users/following': { req: UsersFollowingRequest; res: UsersFollowingResponse }; 'users/gallery/posts': { req: UsersGalleryPostsRequest; res: UsersGalleryPostsResponse }; diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts index daaabf252..eced8e84c 100644 --- a/packages/misskey-js/src/autogen/entities.ts +++ b/packages/misskey-js/src/autogen/entities.ts @@ -519,6 +519,7 @@ export type UsersRequest = operations['users']['requestBody']['content']['applic export type UsersResponse = operations['users']['responses']['200']['content']['application/json']; export type UsersClipsRequest = operations['users___clips']['requestBody']['content']['application/json']; export type UsersClipsResponse = operations['users___clips']['responses']['200']['content']['application/json']; +export type UsersCreditsResponse = operations['users___credits']['responses']['200']['content']['application/json']; export type UsersFollowersRequest = operations['users___followers']['requestBody']['content']['application/json']; export type UsersFollowersResponse = operations['users___followers']['responses']['200']['content']['application/json']; export type UsersFollowingRequest = operations['users___following']['requestBody']['content']['application/json']; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index fcd23b20f..bc1190c3e 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -3370,6 +3370,15 @@ export type paths = { */ post: operations['users___clips']; }; + '/users/credits': { + /** + * users/credits + * @description List up the users of ending credits. + * + * **Credential required**: *No* + */ + post: operations['users___credits']; + }; '/users/followers': { /** * users/followers @@ -5392,6 +5401,7 @@ export type operations = { perUserListTimelineCacheMax: number; notesPerOneAd: number; wellKnownWebsites: string[]; + endingCreditMembers: string[]; urlPreviewDenyList: string[]; featuredGameChannels: string[]; backgroundImageUrl: string | null; @@ -10356,6 +10366,7 @@ export type operations = { silencedHosts?: string[] | null; sensitiveMediaHosts?: string[] | null; wellKnownWebsites?: string[] | null; + endingCreditMembers?: string[] | null; urlPreviewDenyList?: string[] | null; featuredGameChannels?: string[] | null; /** @description [Deprecated] Use "urlPreviewSummaryProxyUrl" instead. */ @@ -27697,6 +27708,52 @@ export type operations = { }; }; }; + /** + * users/credits + * @description List up the users of ending credits. + * + * **Credential required**: *No* + */ + users___credits: { + responses: { + /** @description OK (with results) */ + 200: { + content: { + 'application/json': components['schemas']['UserLite'][]; + }; + }; + /** @description Client error */ + 400: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Authentication error */ + 401: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Forbidden error */ + 403: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description I'm Ai */ + 418: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + /** @description Internal server error */ + 500: { + content: { + 'application/json': components['schemas']['Error']; + }; + }; + }; + }; /** * users/followers * @description Show everyone that follows this user.