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..fe6265e3f --- /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" + `); + } +} \ No newline at end of file diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 3dd64ce62..b6af3cb8c 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -14,7 +14,7 @@ import type { Promiseable } from '@/misc/prelude/await-all.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js'; import type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js'; -import { birthdaySchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/User.js'; +import { birthdaySchema, listenbrainzSchema, descriptionSchema, localUsernameSchema, locationSchema, nameSchema, passwordSchema } from '@/models/User.js'; import type { UsersRepository, UserSecurityKeysRepository, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, DriveFilesRepository, NoteUnreadsRepository, UserNotePiningsRepository, UserProfilesRepository, AnnouncementReadsRepository, AnnouncementsRepository, MiUserProfile, RenoteMutingsRepository, UserMemoRepository } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; @@ -139,6 +139,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; @@ -381,6 +382,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 b040d302c..8f0122a90 100644 --- a/packages/backend/src/models/User.ts +++ b/packages/backend/src/models/User.ts @@ -280,4 +280,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 e4405c9da..09c2c6446 100644 --- a/packages/backend/src/models/UserProfile.ts +++ b/packages/backend/src/models/UserProfile.ts @@ -34,6 +34,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 f15b225a3..5112e680e 100644 --- a/packages/backend/src/models/json-schema/user.ts +++ b/packages/backend/src/models/json-schema/user.ts @@ -145,6 +145,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 b11e09195..204906370 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 { notificationTypes } from '@/types.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; @@ -129,6 +129,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 }, bannerId: { type: 'string', format: 'misskey:id', nullable: true }, @@ -224,6 +225,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.ffVisibility !== undefined) profileUpdates.ffVisibility = ps.ffVisibility; if (ps.mutedWords !== undefined) { // TODO: ちゃんと数える diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue index 5e4889f61..8c12fcf35 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -32,6 +32,11 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + + @@ -132,6 +137,7 @@ const profile = reactive({ description: $i.description, location: $i.location, birthday: $i.birthday, + listenbrainz: $i?.listenbrainz, lang: $i.lang, isBot: $i.isBot, isCat: $i.isCat, @@ -179,6 +185,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 385c81a97..6ddc81e1c 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -137,6 +137,12 @@ SPDX-License-Identifier: AGPL-3.0-only
+
@@ -166,7 +172,6 @@ import { confetti } from '@/scripts/confetti.js'; import MkNotes from '@/components/MkNotes.vue'; import { api } from '@/os.js'; import { isFfVisibleForMe } from '@/scripts/isFfVisibleForMe.js'; - function calcAge(birthdate: string): number { const date = new Date(birthdate); const now = new Date(); @@ -184,6 +189,7 @@ function calcAge(birthdate: string): number { const XPhotos = defineAsyncComponent(() => import('./index.photos.vue')); const XActivity = defineAsyncComponent(() => import('./index.activity.vue')); +const XListenBrainz = defineAsyncComponent(() => import("./index.listenbrainz.vue"));; const props = withDefaults(defineProps<{ user: Misskey.entities.UserDetailed; @@ -205,6 +211,24 @@ let isEditingMemo = $ref(false); let moderationNote = $ref(props.user.moderationNote); let editModerationNote = $ref(false); +let listenbrainzdata = false; +if (props.user.listenbrainz) { + 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 = true; + } + } catch(err) { + listenbrainzdata = false; + } +} + watch($$(moderationNote), async () => { await os.api('admin/update-user-note', { userId: props.user.id, text: moderationNote }); }); 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..266ff1403 --- /dev/null +++ b/packages/frontend/src/pages/user/index.listenbrainz.vue @@ -0,0 +1,138 @@ + + + + + \ No newline at end of file