diff --git a/locales/en-US.yml b/locales/en-US.yml index fa8b5e91d..a63f47a60 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -305,6 +305,7 @@ activity: "Activity" images: "Images" image: "Image" birthday: "Birthday" +listenbrainzDescription: "Input your listenbrainz username here" yearsOld: "{age} years old" registeredDate: "Joined on" location: "Location" diff --git a/locales/index.d.ts b/locales/index.d.ts index 33eee1c8b..2a183435e 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1244,6 +1244,10 @@ export interface Locale extends ILocale { * 誕生日 */ "birthday": string; + /** + * listenbrainzのユーザー名を入力してください。 + */ + "listenbrainzDescription": string; /** * {age}歳 */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 00695e436..8f9b39dde 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -307,6 +307,7 @@ activity: "アクティビティ" images: "画像" image: "画像" birthday: "誕生日" +listenbrainzDescription: "listenbrainzのユーザー名を入力してください。" yearsOld: "{age}歳" registeredDate: "登録日" location: "場所" diff --git a/packages/backend/migration/1691264431000-add-lb-to-user.js b/packages/backend/migration/1691264431000-add-lb-to-user.js new file mode 100644 index 000000000..4bfbf34bb --- /dev/null +++ b/packages/backend/migration/1691264431000-add-lb-to-user.js @@ -0,0 +1,20 @@ +export class AddLbToUser1691264431000 { + name = "AddLbToUser1691264431000"; + + async up(queryRunner) { + await queryRunner.query(` + ALTER TABLE "user_profile" + ADD "listenbrainz" character varying(128) NULL + `); + await queryRunner.query(` + COMMENT ON COLUMN "user_profile"."listenbrainz" + IS 'listenbrainz username to fetch currently playing.' + `); + } + + async down(queryRunner) { + await queryRunner.query(` + ALTER TABLE "user_profile" DROP COLUMN "listenbrainz" + `); + } +} diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 40d06b3ae..f982a1ebe 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -537,6 +537,10 @@ export class ApRendererService { person['vcard:Address'] = profile.location; } + if (profile.listenbrainz) { + person.listenbrainz = profile.listenbrainz; + } + return person; } diff --git a/packages/backend/src/core/activitypub/misc/contexts.ts b/packages/backend/src/core/activitypub/misc/contexts.ts index 653fcdcb8..c7b35bba7 100644 --- a/packages/backend/src/core/activitypub/misc/contexts.ts +++ b/packages/backend/src/core/activitypub/misc/contexts.ts @@ -553,6 +553,9 @@ export const CONTEXTS: (string | Context)[] = [ '_misskey_votes': 'misskey:_misskey_votes', '_misskey_summary': 'misskey:_misskey_summary', 'isCat': 'misskey:isCat', + + sharkey: 'https://joinsharkey.org/ns#', + listenbrainz: 'sharkey:listenbrainz', // vcard vcard: 'http://www.w3.org/2006/vcard/ns#', } satisfies Context, diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index a4d0ef60d..f67cb10d7 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -410,6 +410,7 @@ export class ApPersonService implements OnModuleInit { birthday: bday?.[0] ?? null, location: person['vcard:Address'] ?? null, userHost: host, + listenbrainz: person.listenbrainz ?? null, })); if (person.publicKey) { @@ -615,6 +616,7 @@ export class ApPersonService implements OnModuleInit { followersVisibility, birthday: bday?.[0] ?? null, location: person['vcard:Address'] ?? null, + listenbrainz: person.listenbrainz ?? null, }); this.globalEventService.publishInternalEvent('remoteUserUpdated', { id: exist.id }); diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 16812b7a4..d8baba49a 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -191,6 +191,7 @@ export interface IActor extends IObject { }; 'vcard:bday'?: string; 'vcard:Address'?: string; + listenbrainz?: string; } export const isCollection = (object: IObject): object is ICollection => diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index e366a86b4..647384154 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -18,6 +18,7 @@ import type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser import { birthdaySchema, descriptionSchema, + listenbrainzSchema, localUsernameSchema, locationSchema, nameSchema, @@ -156,6 +157,7 @@ export class UserEntityService implements OnModuleInit { public validateDescription = ajv.compile(descriptionSchema); public validateLocation = ajv.compile(locationSchema); public validateBirthday = ajv.compile(birthdaySchema); + public validateListenBrainz = ajv.compile(listenbrainzSchema); //#endregion public isLocalUser = isLocalUser; @@ -530,6 +532,7 @@ export class UserEntityService implements OnModuleInit { description: profile!.description, location: profile!.location, birthday: profile!.birthday, + listenbrainz: profile!.listenbrainz, lang: profile!.lang, fields: profile!.fields, verifiedLinks: profile!.verifiedLinks, diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts index 7db66f1f8..e9650e8d7 100644 --- a/packages/backend/src/models/User.ts +++ b/packages/backend/src/models/User.ts @@ -307,4 +307,5 @@ export const passwordSchema = { type: 'string', minLength: 1 } as const; export const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const; export const descriptionSchema = { type: 'string', minLength: 1, maxLength: 1500 } as const; export const locationSchema = { type: 'string', minLength: 1, maxLength: 50 } as const; +export const listenbrainzSchema = { type: "string", minLength: 1, maxLength: 128 } as const; export const birthdaySchema = { type: 'string', pattern: /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.toString().slice(1, -1) } as const; diff --git a/packages/backend/src/models/UserProfile.ts b/packages/backend/src/models/UserProfile.ts index 6a1d68625..2f781f4fb 100644 --- a/packages/backend/src/models/UserProfile.ts +++ b/packages/backend/src/models/UserProfile.ts @@ -37,6 +37,13 @@ export class MiUserProfile { }) public birthday: string | null; + @Column("varchar", { + length: 128, + nullable: true, + comment: "The ListenBrainz username of the User.", + }) + public listenbrainz: string | null; + @Column('varchar', { length: 2048, nullable: true, comment: 'The description (bio) of the User.', diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts index 1fc71c2ac..35033e3f4 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -268,6 +268,12 @@ export const packedUserDetailedNotMeOnlySchema = { nullable: true, optional: false, example: '2018-03-12', }, + listenbrainz: { + type: "string", + nullable: true, + optional: false, + example: "Steve", + }, lang: { type: 'string', nullable: true, optional: false, diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 62d64a8ff..c32a41a0a 100644 --- a/packages/backend/src/server/api/endpoints/i/update.ts +++ b/packages/backend/src/server/api/endpoints/i/update.ts @@ -13,7 +13,7 @@ import { extractHashtags } from '@/misc/extract-hashtags.js'; import * as Acct from '@/misc/acct.js'; import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/_.js'; import type { MiLocalUser, MiUser } from '@/models/User.js'; -import { birthdaySchema, descriptionSchema, locationSchema, nameSchema } from '@/models/User.js'; +import { birthdaySchema, listenbrainzSchema, descriptionSchema, locationSchema, nameSchema } from '@/models/User.js'; import type { MiUserProfile } from '@/models/UserProfile.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { langmap } from '@/misc/langmap.js'; @@ -150,6 +150,7 @@ export const paramDef = { description: { ...descriptionSchema, nullable: true }, location: { ...locationSchema, nullable: true }, birthday: { ...birthdaySchema, nullable: true }, + listenbrainz: { ...listenbrainzSchema, nullable: true }, lang: { type: 'string', enum: [null, ...Object.keys(langmap)] as string[], nullable: true }, avatarId: { type: 'string', format: 'misskey:id', nullable: true }, avatarDecorations: { type: 'array', maxItems: 16, items: { @@ -306,6 +307,7 @@ export default class extends Endpoint { // eslint- if (ps.lang !== undefined) profileUpdates.lang = ps.lang; if (ps.location !== undefined) profileUpdates.location = ps.location; if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday; + if (ps.listenbrainz !== undefined) profileUpdates.listenbrainz = ps.listenbrainz; if (ps.followingVisibility !== undefined) profileUpdates.followingVisibility = ps.followingVisibility; if (ps.followersVisibility !== undefined) profileUpdates.followersVisibility = ps.followersVisibility; if (ps.mutedWords !== undefined) { diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts index c7aca5958..ba0c3b2fb 100644 --- a/packages/backend/test/e2e/users.ts +++ b/packages/backend/test/e2e/users.ts @@ -90,6 +90,7 @@ describe('ユーザー', () => { securityKeys: user.securityKeys, roles: user.roles, memo: user.memo, + listenbrainz: user.listenbrainz, }); }; diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue index dfeba525a..9a3d91709 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -37,6 +37,12 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + + + @@ -217,6 +223,7 @@ const profile = reactive({ description: $i.description, location: $i.location, birthday: $i.birthday, + listenbrainz: $i?.listenbrainz, lang: $i.lang, isBot: $i.isBot ?? false, isCat: $i.isCat ?? false, @@ -300,6 +307,7 @@ function save() { location: profile.location || null, // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing birthday: profile.birthday || null, + listenbrainz: profile.listenbrainz || null, // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing lang: profile.lang || null, isBot: !!profile.isBot, diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index 2cd06bb7b..cf42f96c1 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -198,6 +198,9 @@ SPDX-License-Identifier: AGPL-3.0-only + + +
@@ -209,6 +212,12 @@ SPDX-License-Identifier: AGPL-3.0-only
+
@@ -258,6 +267,7 @@ function calcAge(birthdate: string): number { const XFiles = defineAsyncComponent(() => import('./index.files.vue')); const XActivity = defineAsyncComponent(() => import('./index.activity.vue')); +const XListenBrainz = defineAsyncComponent(() => import("./index.listenbrainz.vue")); const XTimeline = defineAsyncComponent(() => import('./index.timeline.vue')); const props = withDefaults(defineProps<{ @@ -283,6 +293,26 @@ const moderationNote = ref(props.user.moderationNote); const editModerationNote = ref(false); const movedFromLog = ref(null); +let listenbrainzdata = ref(false); +if (props.user.listenbrainz) { + (async function() { + try { + const response = await fetch(`https://api.listenbrainz.org/1/user/${props.user.listenbrainz}/playing-now`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + const data = await response.json(); + if (data.payload.listens && data.payload.listens.length !== 0) { + listenbrainzdata.value = true; + } + } catch (err) { + listenbrainzdata.value = false; + } + })(); +} + watch(moderationNote, async () => { await misskeyApi('admin/update-user-note', { userId: props.user.id, text: moderationNote.value }); }); diff --git a/packages/frontend/src/pages/user/index.listenbrainz.vue b/packages/frontend/src/pages/user/index.listenbrainz.vue new file mode 100644 index 000000000..1c9ef8dd2 --- /dev/null +++ b/packages/frontend/src/pages/user/index.listenbrainz.vue @@ -0,0 +1,140 @@ + + + + + + + diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 41706def4..c088ea637 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -3900,6 +3900,8 @@ export type components = { location: string | null; /** @example 2018-03-12 */ birthday: string | null; + /** @example Steve */ + listenbrainz: string | null; /** @example ja-JP */ lang: string | null; fields: { @@ -4702,7 +4704,7 @@ export type components = { blockee: components['schemas']['UserDetailedNotMe']; }; Hashtag: { - /** @example misskey */ + /** @example forkey */ tag: string; mentionedUsersCount: number; mentionedLocalUsersCount: number; @@ -4885,7 +4887,7 @@ export type components = { isNotResponding: boolean; isSuspended: boolean; isBlocked: boolean; - /** @example misskey */ + /** @example forkey */ softwareName: string | null; softwareVersion: string | null; /** @example true */ @@ -5239,6 +5241,8 @@ export type components = { enableTurnstile: boolean; turnstileSiteKey: string | null; googleAnalyticsId: string | null; + enableFC: boolean; + fcSiteKey: string | null; swPublickey: string | null; /** @default /assets/ai.png */ mascotImageUrl: string; @@ -5388,6 +5392,8 @@ export type operations = { enableTurnstile: boolean; turnstileSiteKey: string | null; googleAnalyticsId: string | null; + enableFC: boolean; + fcSiteKey: string | null; swPublickey: string | null; /** @default /assets/ai.png */ mascotImageUrl: string | null; @@ -5414,6 +5420,7 @@ export type operations = { mcaptchaSecretKey: string | null; recaptchaSecretKey: string | null; turnstileSecretKey: string | null; + fcSecretKey: string | null; sensitiveMediaDetection: string; sensitiveMediaDetectionSensitivity: string; setSensitiveFlagAutomatically: boolean; @@ -10319,6 +10326,9 @@ export type operations = { enableTurnstile?: boolean; turnstileSiteKey?: string | null; turnstileSecretKey?: string | null; + enableFC?: boolean; + fcSiteKey?: string | null; + fcSecretKey?: string | null; googleAnalyticsId?: string | null; /** @enum {string} */ sensitiveMediaDetection?: 'none' | 'all' | 'local' | 'remote'; @@ -21719,6 +21729,7 @@ export type operations = { description?: string | null; location?: string | null; birthday?: string | null; + listenbrainz?: string | null; /** @enum {string|null} */ lang?: null | 'ach' | 'ady' | 'af' | 'af-NA' | 'af-ZA' | 'ak' | 'ar' | 'ar-AR' | 'ar-MA' | 'ar-SA' | 'ay-BO' | 'az' | 'az-AZ' | 'be-BY' | 'bg' | 'bg-BG' | 'bn' | 'bn-IN' | 'bn-BD' | 'br' | 'bs-BA' | 'ca' | 'ca-ES' | 'cak' | 'ck-US' | 'cs' | 'cs-CZ' | 'cy' | 'cy-GB' | 'da' | 'da-DK' | 'de' | 'de-AT' | 'de-DE' | 'de-CH' | 'dsb' | 'el' | 'el-GR' | 'en' | 'en-GB' | 'en-AU' | 'en-CA' | 'en-IE' | 'en-IN' | 'en-PI' | 'en-SG' | 'en-UD' | 'en-US' | 'en-ZA' | 'en@pirate' | 'eo' | 'eo-EO' | 'es' | 'es-AR' | 'es-419' | 'es-CL' | 'es-CO' | 'es-EC' | 'es-ES' | 'es-LA' | 'es-NI' | 'es-MX' | 'es-US' | 'es-VE' | 'et' | 'et-EE' | 'eu' | 'eu-ES' | 'fa' | 'fa-IR' | 'fb-LT' | 'ff' | 'fi' | 'fi-FI' | 'fo' | 'fo-FO' | 'fr' | 'fr-CA' | 'fr-FR' | 'fr-BE' | 'fr-CH' | 'fy-NL' | 'ga' | 'ga-IE' | 'gd' | 'gl' | 'gl-ES' | 'gn-PY' | 'gu-IN' | 'gv' | 'gx-GR' | 'he' | 'he-IL' | 'hi' | 'hi-IN' | 'hr' | 'hr-HR' | 'hsb' | 'ht' | 'hu' | 'hu-HU' | 'hy' | 'hy-AM' | 'id' | 'id-ID' | 'is' | 'is-IS' | 'it' | 'it-IT' | 'ja' | 'ja-JP' | 'jv-ID' | 'ka-GE' | 'kk-KZ' | 'km' | 'kl' | 'km-KH' | 'kab' | 'kn' | 'kn-IN' | 'ko' | 'ko-KR' | 'ku-TR' | 'kw' | 'la' | 'la-VA' | 'lb' | 'li-NL' | 'lt' | 'lt-LT' | 'lv' | 'lv-LV' | 'mai' | 'mg-MG' | 'mk' | 'mk-MK' | 'ml' | 'ml-IN' | 'mn-MN' | 'mr' | 'mr-IN' | 'ms' | 'ms-MY' | 'mt' | 'mt-MT' | 'my' | 'no' | 'nb' | 'nb-NO' | 'ne' | 'ne-NP' | 'nl' | 'nl-BE' | 'nl-NL' | 'nn-NO' | 'oc' | 'or-IN' | 'pa' | 'pa-IN' | 'pl' | 'pl-PL' | 'ps-AF' | 'pt' | 'pt-BR' | 'pt-PT' | 'qu-PE' | 'rm-CH' | 'ro' | 'ro-RO' | 'ru' | 'ru-RU' | 'sa-IN' | 'se-NO' | 'sh' | 'si-LK' | 'sk' | 'sk-SK' | 'sl' | 'sl-SI' | 'so-SO' | 'sq' | 'sq-AL' | 'sr' | 'sr-RS' | 'su' | 'sv' | 'sv-SE' | 'sw' | 'sw-KE' | 'ta' | 'ta-IN' | 'te' | 'te-IN' | 'tg' | 'tg-TJ' | 'th' | 'th-TH' | 'fil' | 'tlh' | 'tr' | 'tr-TR' | 'tt-RU' | 'uk' | 'uk-UA' | 'ur' | 'ur-PK' | 'uz' | 'uz-UZ' | 'vi' | 'vi-VN' | 'xh-ZA' | 'yi' | 'yi-DE' | 'zh' | 'zh-Hans' | 'zh-Hant' | 'zh-CN' | 'zh-HK' | 'zh-SG' | 'zh-TW' | 'zu-ZA'; /** Format: misskey:id */ @@ -30841,3 +30852,4 @@ export type operations = { }; }; }; +