Cherrypick listenbrainz #52
18 changed files with 249 additions and 3 deletions
|
@ -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"
|
||||
|
|
4
locales/index.d.ts
vendored
4
locales/index.d.ts
vendored
|
@ -1244,6 +1244,10 @@ export interface Locale extends ILocale {
|
|||
* 誕生日
|
||||
*/
|
||||
"birthday": string;
|
||||
/**
|
||||
* listenbrainzのユーザー名を入力してください。
|
||||
*/
|
||||
"listenbrainzDescription": string;
|
||||
/**
|
||||
* {age}歳
|
||||
*/
|
||||
|
|
|
@ -307,6 +307,7 @@ activity: "アクティビティ"
|
|||
images: "画像"
|
||||
image: "画像"
|
||||
birthday: "誕生日"
|
||||
listenbrainzDescription: "listenbrainzのユーザー名を入力してください。"
|
||||
yearsOld: "{age}歳"
|
||||
registeredDate: "登録日"
|
||||
location: "場所"
|
||||
|
|
20
packages/backend/migration/1691264431000-add-lb-to-user.js
Normal file
20
packages/backend/migration/1691264431000-add-lb-to-user.js
Normal 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"
|
||||
`);
|
||||
}
|
||||
}
|
|
@ -537,6 +537,10 @@ export class ApRendererService {
|
|||
person['vcard:Address'] = profile.location;
|
||||
}
|
||||
|
||||
if (profile.listenbrainz) {
|
||||
person.listenbrainz = profile.listenbrainz;
|
||||
}
|
||||
|
||||
return person;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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 =>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<typeof meta, typeof paramDef> { // 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) {
|
||||
|
|
|
@ -90,6 +90,7 @@ describe('ユーザー', () => {
|
|||
securityKeys: user.securityKeys,
|
||||
roles: user.roles,
|
||||
memo: user.memo,
|
||||
listenbrainz: user.listenbrainz,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -37,6 +37,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #prefix><i class="ti ti-cake"></i></template>
|
||||
</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">
|
||||
<template #label>{{ i18n.ts.language }}</template>
|
||||
<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,
|
||||
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,
|
||||
|
|
|
@ -198,6 +198,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkLazy>
|
||||
<XActivity :key="user.id" :user="user"/>
|
||||
</MkLazy>
|
||||
<MkLazy v-if="user.listenbrainz && listenbrainzdata">
|
||||
<XListenBrainz :key="user.id" :user="user" :collapsed="true"/>
|
||||
</MkLazy>
|
||||
</template>
|
||||
<div v-if="!disableNotes">
|
||||
<MkLazy>
|
||||
|
@ -209,6 +212,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div v-if="!narrow" class="sub _gaps" style="container-type: inline-size;">
|
||||
<XFiles :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>
|
||||
</MkSpacer>
|
||||
|
@ -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 | {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 () => {
|
||||
await misskeyApi('admin/update-user-note', { userId: props.user.id, text: moderationNote.value });
|
||||
});
|
||||
|
|
140
packages/frontend/src/pages/user/index.listenbrainz.vue
Normal file
140
packages/frontend/src/pages/user/index.listenbrainz.vue
Normal 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>
|
|
@ -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 = {
|
|||
};
|
||||
};
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue