Cherrypick listenbrainz #52

Open
leah wants to merge 13 commits from cherrypick/listenbrainz into main
18 changed files with 249 additions and 3 deletions

View file

@ -305,6 +305,7 @@ activity: "Activity"
images: "Images" images: "Images"
image: "Image" image: "Image"
birthday: "Birthday" birthday: "Birthday"
listenbrainzDescription: "Input your listenbrainz username here"
yearsOld: "{age} years old" yearsOld: "{age} years old"
registeredDate: "Joined on" registeredDate: "Joined on"
location: "Location" location: "Location"

4
locales/index.d.ts vendored
View file

@ -1244,6 +1244,10 @@ export interface Locale extends ILocale {
* *
*/ */
"birthday": string; "birthday": string;
/**
* listenbrainzのユーザー名を入力してください
*/
"listenbrainzDescription": string;
/** /**
* {age} * {age}
*/ */

View file

@ -307,6 +307,7 @@ activity: "アクティビティ"
images: "画像" images: "画像"
image: "画像" image: "画像"
birthday: "誕生日" birthday: "誕生日"
listenbrainzDescription: "listenbrainzのユーザー名を入力してください。"
yearsOld: "{age}歳" yearsOld: "{age}歳"
registeredDate: "登録日" registeredDate: "登録日"
location: "場所" location: "場所"

View file

@ -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"
`);
}
}

View file

@ -537,6 +537,10 @@ export class ApRendererService {
person['vcard:Address'] = profile.location; person['vcard:Address'] = profile.location;
} }
if (profile.listenbrainz) {
person.listenbrainz = profile.listenbrainz;
}
return person; return person;
} }

View file

@ -553,6 +553,9 @@ export const CONTEXTS: (string | Context)[] = [
'_misskey_votes': 'misskey:_misskey_votes', '_misskey_votes': 'misskey:_misskey_votes',
'_misskey_summary': 'misskey:_misskey_summary', '_misskey_summary': 'misskey:_misskey_summary',
'isCat': 'misskey:isCat', 'isCat': 'misskey:isCat',
sharkey: 'https://joinsharkey.org/ns#',
listenbrainz: 'sharkey:listenbrainz',
// vcard // vcard
vcard: 'http://www.w3.org/2006/vcard/ns#', vcard: 'http://www.w3.org/2006/vcard/ns#',
} satisfies Context, } satisfies Context,

View file

@ -410,6 +410,7 @@ export class ApPersonService implements OnModuleInit {
birthday: bday?.[0] ?? null, birthday: bday?.[0] ?? null,
location: person['vcard:Address'] ?? null, location: person['vcard:Address'] ?? null,
userHost: host, userHost: host,
listenbrainz: person.listenbrainz ?? null,
})); }));
if (person.publicKey) { if (person.publicKey) {
@ -615,6 +616,7 @@ export class ApPersonService implements OnModuleInit {
followersVisibility, followersVisibility,
birthday: bday?.[0] ?? null, birthday: bday?.[0] ?? null,
location: person['vcard:Address'] ?? null, location: person['vcard:Address'] ?? null,
listenbrainz: person.listenbrainz ?? null,
}); });
this.globalEventService.publishInternalEvent('remoteUserUpdated', { id: exist.id }); this.globalEventService.publishInternalEvent('remoteUserUpdated', { id: exist.id });

View file

@ -191,6 +191,7 @@ export interface IActor extends IObject {
}; };
'vcard:bday'?: string; 'vcard:bday'?: string;
'vcard:Address'?: string; 'vcard:Address'?: string;
listenbrainz?: string;
} }
export const isCollection = (object: IObject): object is ICollection => export const isCollection = (object: IObject): object is ICollection =>

View file

@ -18,6 +18,7 @@ import type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser
import { import {
birthdaySchema, birthdaySchema,
descriptionSchema, descriptionSchema,
listenbrainzSchema,
localUsernameSchema, localUsernameSchema,
locationSchema, locationSchema,
nameSchema, nameSchema,
@ -156,6 +157,7 @@ export class UserEntityService implements OnModuleInit {
public validateDescription = ajv.compile(descriptionSchema); public validateDescription = ajv.compile(descriptionSchema);
public validateLocation = ajv.compile(locationSchema); public validateLocation = ajv.compile(locationSchema);
public validateBirthday = ajv.compile(birthdaySchema); public validateBirthday = ajv.compile(birthdaySchema);
public validateListenBrainz = ajv.compile(listenbrainzSchema);
//#endregion //#endregion
public isLocalUser = isLocalUser; public isLocalUser = isLocalUser;
@ -530,6 +532,7 @@ export class UserEntityService implements OnModuleInit {
description: profile!.description, description: profile!.description,
location: profile!.location, location: profile!.location,
birthday: profile!.birthday, birthday: profile!.birthday,
listenbrainz: profile!.listenbrainz,
lang: profile!.lang, lang: profile!.lang,
fields: profile!.fields, fields: profile!.fields,
verifiedLinks: profile!.verifiedLinks, verifiedLinks: profile!.verifiedLinks,

View file

@ -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 nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
export const descriptionSchema = { type: 'string', minLength: 1, maxLength: 1500 } 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 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; export const birthdaySchema = { type: 'string', pattern: /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.toString().slice(1, -1) } as const;

View file

@ -37,6 +37,13 @@ export class MiUserProfile {
}) })
public birthday: string | null; public birthday: string | null;
@Column("varchar", {
length: 128,
nullable: true,
comment: "The ListenBrainz username of the User.",
})
public listenbrainz: string | null;
@Column('varchar', { @Column('varchar', {
length: 2048, nullable: true, length: 2048, nullable: true,
comment: 'The description (bio) of the User.', comment: 'The description (bio) of the User.',

View file

@ -268,6 +268,12 @@ export const packedUserDetailedNotMeOnlySchema = {
nullable: true, optional: false, nullable: true, optional: false,
example: '2018-03-12', example: '2018-03-12',
}, },
listenbrainz: {
type: "string",
nullable: true,
optional: false,
example: "Steve",
},
lang: { lang: {
type: 'string', type: 'string',
nullable: true, optional: false, nullable: true, optional: false,

View file

@ -13,7 +13,7 @@ import { extractHashtags } from '@/misc/extract-hashtags.js';
import * as Acct from '@/misc/acct.js'; import * as Acct from '@/misc/acct.js';
import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/_.js'; import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/_.js';
import type { MiLocalUser, MiUser } from '@/models/User.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 type { MiUserProfile } from '@/models/UserProfile.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { langmap } from '@/misc/langmap.js'; import { langmap } from '@/misc/langmap.js';
@ -150,6 +150,7 @@ export const paramDef = {
description: { ...descriptionSchema, nullable: true }, description: { ...descriptionSchema, nullable: true },
location: { ...locationSchema, nullable: true }, location: { ...locationSchema, nullable: true },
birthday: { ...birthdaySchema, nullable: true }, birthday: { ...birthdaySchema, nullable: true },
listenbrainz: { ...listenbrainzSchema, nullable: true },
lang: { type: 'string', enum: [null, ...Object.keys(langmap)] as string[], nullable: true }, lang: { type: 'string', enum: [null, ...Object.keys(langmap)] as string[], nullable: true },
avatarId: { type: 'string', format: 'misskey:id', nullable: true }, avatarId: { type: 'string', format: 'misskey:id', nullable: true },
avatarDecorations: { type: 'array', maxItems: 16, items: { avatarDecorations: { type: 'array', maxItems: 16, items: {
@ -306,6 +307,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (ps.lang !== undefined) profileUpdates.lang = ps.lang; if (ps.lang !== undefined) profileUpdates.lang = ps.lang;
if (ps.location !== undefined) profileUpdates.location = ps.location; if (ps.location !== undefined) profileUpdates.location = ps.location;
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday; 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.followingVisibility !== undefined) profileUpdates.followingVisibility = ps.followingVisibility;
if (ps.followersVisibility !== undefined) profileUpdates.followersVisibility = ps.followersVisibility; if (ps.followersVisibility !== undefined) profileUpdates.followersVisibility = ps.followersVisibility;
if (ps.mutedWords !== undefined) { if (ps.mutedWords !== undefined) {

View file

@ -90,6 +90,7 @@ describe('ユーザー', () => {
securityKeys: user.securityKeys, securityKeys: user.securityKeys,
roles: user.roles, roles: user.roles,
memo: user.memo, memo: user.memo,
listenbrainz: user.listenbrainz,
}); });
}; };

View file

@ -37,6 +37,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #prefix><i class="ti ti-cake"></i></template> <template #prefix><i class="ti ti-cake"></i></template>
</MkInput> </MkInput>
<MkInput v-model="profile.listenbrainz" manualSave>
<template #label>ListenBrainz</template>
<template #caption>{{i18n.ts.listenbrainzDescription}}</template>
<template #prefix><i class="ti ti-headphones"></i></template>
</MkInput>
<MkSelect v-model="profile.lang"> <MkSelect v-model="profile.lang">
<template #label>{{ i18n.ts.language }}</template> <template #label>{{ i18n.ts.language }}</template>
<option v-for="x in Object.keys(langmap)" :key="x" :value="x">{{ langmap[x].nativeName }}</option> <option v-for="x in Object.keys(langmap)" :key="x" :value="x">{{ langmap[x].nativeName }}</option>
@ -217,6 +223,7 @@ const profile = reactive({
description: $i.description, description: $i.description,
location: $i.location, location: $i.location,
birthday: $i.birthday, birthday: $i.birthday,
listenbrainz: $i?.listenbrainz,
lang: $i.lang, lang: $i.lang,
isBot: $i.isBot ?? false, isBot: $i.isBot ?? false,
isCat: $i.isCat ?? false, isCat: $i.isCat ?? false,
@ -300,6 +307,7 @@ function save() {
location: profile.location || null, location: profile.location || null,
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
birthday: profile.birthday || null, birthday: profile.birthday || null,
listenbrainz: profile.listenbrainz || null,
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
lang: profile.lang || null, lang: profile.lang || null,
isBot: !!profile.isBot, isBot: !!profile.isBot,

View file

@ -198,6 +198,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkLazy> <MkLazy>
<XActivity :key="user.id" :user="user"/> <XActivity :key="user.id" :user="user"/>
</MkLazy> </MkLazy>
<MkLazy v-if="user.listenbrainz && listenbrainzdata">
<XListenBrainz :key="user.id" :user="user" :collapsed="true"/>
</MkLazy>
</template> </template>
<div v-if="!disableNotes"> <div v-if="!disableNotes">
<MkLazy> <MkLazy>
@ -209,6 +212,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="!narrow" class="sub _gaps" style="container-type: inline-size;"> <div v-if="!narrow" class="sub _gaps" style="container-type: inline-size;">
<XFiles :key="user.id" :user="user"/> <XFiles :key="user.id" :user="user"/>
<XActivity :key="user.id" :user="user"/> <XActivity :key="user.id" :user="user"/>
<XListenBrainz
v-if="user.listenbrainz && listenbrainzdata"
:key="user.id"
:user="user"
style="margin-top: var(--margin)"
/>
</div> </div>
</div> </div>
</MkSpacer> </MkSpacer>
@ -258,6 +267,7 @@ function calcAge(birthdate: string): number {
const XFiles = defineAsyncComponent(() => import('./index.files.vue')); const XFiles = defineAsyncComponent(() => import('./index.files.vue'));
const XActivity = defineAsyncComponent(() => import('./index.activity.vue')); const XActivity = defineAsyncComponent(() => import('./index.activity.vue'));
const XListenBrainz = defineAsyncComponent(() => import("./index.listenbrainz.vue"));
const XTimeline = defineAsyncComponent(() => import('./index.timeline.vue')); const XTimeline = defineAsyncComponent(() => import('./index.timeline.vue'));
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
@ -283,6 +293,26 @@ const moderationNote = ref(props.user.moderationNote);
const editModerationNote = ref(false); const editModerationNote = ref(false);
const movedFromLog = ref<null | {movedFromId:string;}[]>(null); const movedFromLog = ref<null | {movedFromId:string;}[]>(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 () => { watch(moderationNote, async () => {
await misskeyApi('admin/update-user-note', { userId: props.user.id, text: moderationNote.value }); await misskeyApi('admin/update-user-note', { userId: props.user.id, text: moderationNote.value });
}); });

View file

@ -0,0 +1,140 @@
<!--
SPDX-FileCopyrightText: amelia and other Sharkey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkContainer :foldable="true" :expanded="!collapsed">
<template #header>
<i
class="ph-headphones ph-bold ph-lg"
style="margin-right: 0.5em"
></i>Music
</template>
<div style="padding: 8px">
<div class="flex">
<a :href="listenbrainz.musicbrainzurl">
<img class="image" :src="listenbrainz.img" :alt="listenbrainz.title"/>
<div class="flex flex-col items-start">
<p class="text-sm font-bold">Now Playing: {{ listenbrainz.title }}</p>
<p class="text-xs font-medium">{{ listenbrainz.artist }}</p>
</div>
</a>
<a :href="listenbrainz.listenbrainzurl">
<div class="playicon">
<i class="ph-play ph-bold ph-lg"></i>
</div>
</a>
</div>
</div>
</MkContainer>
</template>
<script lang="ts" setup>
import * as misskey from 'misskey-js';
import MkContainer from '@/components/MkContainer.vue';
const props = withDefaults(
defineProps<{
user: misskey.entities.UserDetailed;
collapsed?: boolean;
}>(), {
collapsed: false,
},
);
const listenbrainz = { title: '', artist: '', lastlisten: '', img: '', musicbrainzurl: '', listenbrainzurl: '' };
if (props.user.listenbrainz) {
const getLMData = async (title: string, artist: string) => {
const response = await fetch(`https://api.listenbrainz.org/1/metadata/lookup/?artist_name=${artist}&recording_name=${title}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
const data = await response.json();
if (!data.recording_name) {
return null;
}
const titler: string = data.recording_name;
const artistr: string = data.artist_credit_name;
const img: string = data.release_mbid ? `https://coverartarchive.org/release/${data.release_mbid}/front-250` : 'https://coverartarchive.org/img/big_logo.svg';
const musicbrainzurl: string = data.recording_mbid ? `https://musicbrainz.org/recording/${data.recording_mbid}` : '#';
const listenbrainzurl: string = data.recording_mbid ? `https://listenbrainz.org/player?recording_mbids=${data.recording_mbid}` : '#';
return [titler, artistr, img, musicbrainzurl, listenbrainzurl];
};
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) {
const title: string = data.payload.listens[0].track_metadata.track_name;
const artist: string = data.payload.listens[0].track_metadata.artist_name;
const lastlisten: string = data.payload.listens[0].playing_now;
const img = 'https://coverartarchive.org/img/big_logo.svg';
await getLMData(title, artist).then((lmData) => {
if (!lmData) {
listenbrainz.title = title;
listenbrainz.img = img;
listenbrainz.artist = artist;
listenbrainz.lastlisten = lastlisten;
} else {
listenbrainz.title = lmData[0];
listenbrainz.img = lmData[2];
listenbrainz.artist = lmData[1];
listenbrainz.lastlisten = lastlisten;
listenbrainz.musicbrainzurl = lmData[3];
listenbrainz.listenbrainzurl = lmData[4];
}
});
}
}
</script>
<style lang="scss" scoped>
.flex {
display: flex;
align-items: center;
}
.flex a {
display: flex;
align-items: center;
text-decoration: none;
}
.image {
height: 4.8rem;
margin-right: 0.7rem;
}
.items-start {
align-items: flex-start;
}
.flex-col {
display: flex;
flex-direction: column;
}
.text-sm {
font-size: 0.875rem;
margin: 0 0 0.3rem;
}
.font-bold {
font-weight: 700;
}
.text-xs {
font-size: 0.75rem;
margin: 0;
}
.font-medium {
font-weight: 500;
}
.playicon {
display: flex;
align-items: center;
justify-content: center;
width: 3rem;
height: 3rem;
font-size: 1.7rem;
padding-left: 3rem;
}
</style>

View file

@ -3900,6 +3900,8 @@ export type components = {
location: string | null; location: string | null;
/** @example 2018-03-12 */ /** @example 2018-03-12 */
birthday: string | null; birthday: string | null;
/** @example Steve */
listenbrainz: string | null;
/** @example ja-JP */ /** @example ja-JP */
lang: string | null; lang: string | null;
fields: { fields: {
@ -4702,7 +4704,7 @@ export type components = {
blockee: components['schemas']['UserDetailedNotMe']; blockee: components['schemas']['UserDetailedNotMe'];
}; };
Hashtag: { Hashtag: {
/** @example misskey */ /** @example forkey */
tag: string; tag: string;
mentionedUsersCount: number; mentionedUsersCount: number;
mentionedLocalUsersCount: number; mentionedLocalUsersCount: number;
@ -4885,7 +4887,7 @@ export type components = {
isNotResponding: boolean; isNotResponding: boolean;
isSuspended: boolean; isSuspended: boolean;
isBlocked: boolean; isBlocked: boolean;
/** @example misskey */ /** @example forkey */
softwareName: string | null; softwareName: string | null;
softwareVersion: string | null; softwareVersion: string | null;
/** @example true */ /** @example true */
@ -5239,6 +5241,8 @@ export type components = {
enableTurnstile: boolean; enableTurnstile: boolean;
turnstileSiteKey: string | null; turnstileSiteKey: string | null;
googleAnalyticsId: string | null; googleAnalyticsId: string | null;
enableFC: boolean;
fcSiteKey: string | null;
swPublickey: string | null; swPublickey: string | null;
/** @default /assets/ai.png */ /** @default /assets/ai.png */
mascotImageUrl: string; mascotImageUrl: string;
@ -5388,6 +5392,8 @@ export type operations = {
enableTurnstile: boolean; enableTurnstile: boolean;
turnstileSiteKey: string | null; turnstileSiteKey: string | null;
googleAnalyticsId: string | null; googleAnalyticsId: string | null;
enableFC: boolean;
fcSiteKey: string | null;
swPublickey: string | null; swPublickey: string | null;
/** @default /assets/ai.png */ /** @default /assets/ai.png */
mascotImageUrl: string | null; mascotImageUrl: string | null;
@ -5414,6 +5420,7 @@ export type operations = {
mcaptchaSecretKey: string | null; mcaptchaSecretKey: string | null;
recaptchaSecretKey: string | null; recaptchaSecretKey: string | null;
turnstileSecretKey: string | null; turnstileSecretKey: string | null;
fcSecretKey: string | null;
sensitiveMediaDetection: string; sensitiveMediaDetection: string;
sensitiveMediaDetectionSensitivity: string; sensitiveMediaDetectionSensitivity: string;
setSensitiveFlagAutomatically: boolean; setSensitiveFlagAutomatically: boolean;
@ -10319,6 +10326,9 @@ export type operations = {
enableTurnstile?: boolean; enableTurnstile?: boolean;
turnstileSiteKey?: string | null; turnstileSiteKey?: string | null;
turnstileSecretKey?: string | null; turnstileSecretKey?: string | null;
enableFC?: boolean;
fcSiteKey?: string | null;
fcSecretKey?: string | null;
googleAnalyticsId?: string | null; googleAnalyticsId?: string | null;
/** @enum {string} */ /** @enum {string} */
sensitiveMediaDetection?: 'none' | 'all' | 'local' | 'remote'; sensitiveMediaDetection?: 'none' | 'all' | 'local' | 'remote';
@ -21719,6 +21729,7 @@ export type operations = {
description?: string | null; description?: string | null;
location?: string | null; location?: string | null;
birthday?: string | null; birthday?: string | null;
listenbrainz?: string | null;
/** @enum {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'; 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 */ /** Format: misskey:id */
@ -30841,3 +30852,4 @@ export type operations = {
}; };
}; };
}; };