Compare commits

..

13 commits

Author SHA1 Message Date
8723cffd45 fix?
All checks were successful
Lint / pnpm_install (pull_request) Successful in 30s
API report (misskey.js) / report (pull_request) Successful in 43s
Test (misskey.js) / test (22.x) (pull_request) Successful in 58s
Test (frontend) / vitest (22.x) (pull_request) Successful in 1m31s
Test (production install and build) / production (22.x) (pull_request) Successful in 1m3s
Lint / lint (backend) (pull_request) Successful in 1m3s
Lint / lint (misskey-js) (pull_request) Successful in 34s
Test (backend) / validate-api-json (22.x) (pull_request) Successful in 1m31s
Lint / lint (sw) (pull_request) Successful in 31s
Test (backend) / unit (22.x) (pull_request) Successful in 2m41s
Lint / typecheck (misskey-js) (pull_request) Successful in 33s
Lint / typecheck (backend) (pull_request) Successful in 1m25s
Test (backend) / e2e (22.x) (pull_request) Successful in 6m31s
Lint / lint (frontend) (pull_request) Successful in 7m32s
2025-02-04 17:02:21 +01:00
d56abc802a now?
Some checks failed
Lint / pnpm_install (pull_request) Successful in 32s
API report (misskey.js) / report (push) Successful in 45s
API report (misskey.js) / report (pull_request) Successful in 46s
Test (misskey.js) / test (22.x) (pull_request) Successful in 48s
Test (frontend) / vitest (22.x) (pull_request) Successful in 1m25s
Test (production install and build) / production (22.x) (pull_request) Successful in 1m3s
Test (backend) / validate-api-json (22.x) (pull_request) Successful in 1m29s
Lint / lint (backend) (pull_request) Successful in 1m3s
Test (backend) / unit (22.x) (pull_request) Successful in 2m52s
Lint / lint (sw) (pull_request) Successful in 30s
Lint / lint (misskey-js) (pull_request) Successful in 34s
Lint / typecheck (misskey-js) (pull_request) Successful in 32s
Lint / typecheck (backend) (pull_request) Successful in 1m25s
Test (backend) / e2e (22.x) (pull_request) Failing after 6m42s
Lint / lint (frontend) (pull_request) Successful in 7m41s
2025-02-04 16:44:46 +01:00
0a0edca5c6 small update to home
Some checks failed
Lint / pnpm_install (pull_request) Successful in 43s
API report (misskey.js) / report (pull_request) Successful in 45s
Test (misskey.js) / test (22.x) (pull_request) Successful in 57s
Test (frontend) / vitest (22.x) (pull_request) Successful in 1m23s
Test (production install and build) / production (22.x) (pull_request) Successful in 1m6s
Lint / lint (backend) (pull_request) Successful in 1m3s
Test (backend) / validate-api-json (22.x) (pull_request) Successful in 1m30s
Lint / lint (misskey-js) (pull_request) Successful in 33s
Lint / lint (sw) (pull_request) Successful in 29s
Test (backend) / unit (22.x) (pull_request) Successful in 2m34s
Lint / typecheck (misskey-js) (pull_request) Successful in 30s
Lint / typecheck (backend) (pull_request) Successful in 1m21s
Test (backend) / e2e (22.x) (pull_request) Failing after 6m29s
Lint / lint (frontend) (pull_request) Successful in 7m31s
2025-02-04 16:00:14 +01:00
0c7922edbc please
Some checks failed
Lint / pnpm_install (pull_request) Successful in 27s
Test (misskey.js) / test (22.x) (pull_request) Successful in 45s
Test (frontend) / vitest (22.x) (pull_request) Failing after 1m22s
Test (production install and build) / production (22.x) (pull_request) Successful in 1m2s
Lint / lint (backend) (pull_request) Successful in 1m0s
Test (backend) / unit (22.x) (pull_request) Successful in 2m27s
Test (backend) / validate-api-json (22.x) (pull_request) Successful in 1m23s
Lint / lint (sw) (pull_request) Successful in 29s
Lint / lint (misskey-js) (pull_request) Successful in 34s
Lint / typecheck (misskey-js) (pull_request) Successful in 30s
API report (misskey.js) / report (pull_request) Successful in 3m47s
Lint / typecheck (backend) (pull_request) Successful in 1m23s
Test (backend) / e2e (22.x) (pull_request) Failing after 6m23s
Lint / lint (frontend) (pull_request) Successful in 7m31s
2025-02-04 15:40:45 +01:00
5de77871ec maybe fixed listenbrainz?
Some checks failed
Lint / pnpm_install (pull_request) Successful in 29s
Test (misskey.js) / test (22.x) (pull_request) Successful in 48s
Test (frontend) / vitest (22.x) (pull_request) Failing after 1m20s
Test (production install and build) / production (22.x) (pull_request) Successful in 1m2s
Lint / lint (backend) (pull_request) Successful in 56s
Test (backend) / unit (22.x) (pull_request) Successful in 2m28s
Lint / lint (sw) (pull_request) Successful in 29s
API report (misskey.js) / report (pull_request) Successful in 5m40s
Lint / typecheck (misskey-js) (pull_request) Successful in 29s
Test (backend) / e2e (22.x) (pull_request) Failing after 6m23s
Lint / lint (misskey-js) (pull_request) Successful in 5m13s
Lint / typecheck (backend) (pull_request) Successful in 5m19s
Test (backend) / validate-api-json (22.x) (pull_request) Successful in 10m8s
Lint / lint (frontend) (pull_request) Successful in 9m53s
2025-02-04 15:17:46 +01:00
f65e3bdf88 removed listenbrainz from signup response test (was a mistake)
Some checks failed
API report (misskey.js) / report (push) Successful in 40s
API report (misskey.js) / report (pull_request) Successful in 44s
Lint / pnpm_install (pull_request) Successful in 28s
Test (misskey.js) / test (22.x) (pull_request) Successful in 45s
Test (frontend) / vitest (22.x) (pull_request) Failing after 1m15s
Test (production install and build) / production (22.x) (pull_request) Successful in 1m3s
Lint / lint (backend) (pull_request) Successful in 1m2s
Test (backend) / validate-api-json (22.x) (pull_request) Successful in 1m26s
Lint / lint (misskey-js) (pull_request) Successful in 33s
Lint / lint (sw) (pull_request) Successful in 30s
Test (backend) / unit (22.x) (pull_request) Successful in 3m40s
Lint / typecheck (misskey-js) (pull_request) Successful in 30s
Lint / typecheck (backend) (pull_request) Successful in 1m23s
Test (backend) / e2e (22.x) (pull_request) Failing after 6m30s
Lint / lint (frontend) (pull_request) Successful in 7m37s
2025-02-04 14:42:03 +01:00
6d4908e5da Merge branch 'main' into cherrypick/listenbrainz
Some checks failed
API report (misskey.js) / report (push) Successful in 42s
Lint / pnpm_install (pull_request) Successful in 27s
Test (production install and build) / production (22.x) (pull_request) Successful in 1m3s
Test (backend) / validate-api-json (22.x) (pull_request) Successful in 1m27s
Lint / lint (backend) (pull_request) Successful in 1m1s
Lint / lint (misskey-js) (pull_request) Successful in 32s
Lint / lint (sw) (pull_request) Successful in 29s
Lint / typecheck (misskey-js) (pull_request) Successful in 31s
Lint / typecheck (backend) (pull_request) Failing after 1m23s
Lint / lint (frontend) (pull_request) Successful in 7m39s
Test (frontend) / vitest (22.x) (pull_request) Failing after 1m10s
Test (backend) / unit (22.x) (pull_request) Successful in 2m32s
Test (backend) / e2e (22.x) (pull_request) Failing after 6m20s
2025-02-04 13:18:21 +00:00
94918b8f95 Add listenbrainz field to backend test
Some checks failed
Check Misskey JS version / Check version (pull_request) Successful in 11s
Lint / pnpm_install (pull_request) Successful in 30s
API report (misskey.js) / report (pull_request) Successful in 46s
Test (misskey.js) / test (22.x) (pull_request) Successful in 50s
Test (frontend) / vitest (22.x) (pull_request) Failing after 1m21s
Test (production install and build) / production (22.x) (pull_request) Successful in 1m2s
Test (backend) / validate-api-json (22.x) (pull_request) Successful in 1m28s
Test (backend) / unit (22.x) (pull_request) Successful in 2m37s
Lint / lint (backend) (pull_request) Successful in 1m1s
Lint / lint (sw) (pull_request) Successful in 28s
Lint / lint (misskey-js) (pull_request) Successful in 33s
Lint / typecheck (misskey-js) (pull_request) Successful in 31s
Lint / typecheck (backend) (pull_request) Failing after 1m24s
Test (backend) / e2e (22.x) (pull_request) Failing after 7m14s
Lint / lint (frontend) (pull_request) Successful in 7m47s
2025-02-04 14:17:54 +01:00
888326a4e6 please linter ;-;
Some checks failed
Check Misskey JS version / Check version (pull_request) Successful in 10s
Lint / pnpm_install (pull_request) Successful in 27s
API report (misskey.js) / report (pull_request) Successful in 43s
Test (misskey.js) / test (22.x) (pull_request) Successful in 49s
Test (production install and build) / production (22.x) (pull_request) Successful in 1m2s
Test (backend) / unit (22.x) (pull_request) Successful in 2m38s
Test (backend) / validate-api-json (22.x) (pull_request) Successful in 1m27s
Lint / lint (backend) (pull_request) Successful in 1m14s
Lint / lint (misskey-js) (pull_request) Successful in 34s
Lint / lint (sw) (pull_request) Successful in 29s
Lint / typecheck (misskey-js) (pull_request) Successful in 32s
Lint / typecheck (backend) (pull_request) Successful in 1m40s
Test (backend) / e2e (22.x) (pull_request) Failing after 6m32s
Lint / lint (frontend) (pull_request) Successful in 7m24s
Test (frontend) / vitest (22.x) (pull_request) Failing after 1m8s
2025-02-03 18:16:01 +01:00
6933b43e71 Merge branch 'cherrypick/listenbrainz' of https://git.woem.men/woem.men/forkey into cherrypick/listenbrainz
Some checks failed
Lint / pnpm_install (pull_request) Successful in 27s
Test (production install and build) / production (22.x) (pull_request) Successful in 56s
Test (backend) / validate-api-json (22.x) (pull_request) Successful in 1m20s
Lint / lint (backend) (pull_request) Successful in 58s
Lint / lint (misskey-js) (pull_request) Successful in 31s
Lint / lint (sw) (pull_request) Successful in 28s
Lint / typecheck (backend) (pull_request) Successful in 1m24s
Lint / typecheck (misskey-js) (pull_request) Successful in 1m26s
Test (frontend) / vitest (22.x) (pull_request) Failing after 2m28s
Lint / lint (frontend) (pull_request) Failing after 7m53s
Test (backend) / unit (22.x) (pull_request) Successful in 5m30s
Test (backend) / e2e (22.x) (pull_request) Failing after 6m56s
2025-02-03 16:13:03 +01:00
c2be49c68d fix linter problems 2025-02-03 16:12:52 +01:00
0d4f11c365 Merge branch 'main' into cherrypick/listenbrainz
Some checks failed
Lint / pnpm_install (pull_request) Successful in 2m36s
Test (backend) / unit (22.x) (pull_request) Successful in 6m40s
Test (frontend) / vitest (22.x) (pull_request) Failing after 1m39s
Test (backend) / e2e (22.x) (pull_request) Failing after 9m31s
Test (production install and build) / production (22.x) (pull_request) Successful in 2m24s
Test (backend) / validate-api-json (22.x) (pull_request) Successful in 3m6s
Lint / lint (backend) (pull_request) Failing after 1m34s
Lint / lint (frontend) (pull_request) Failing after 7m36s
Lint / lint (misskey-js) (pull_request) Successful in 1m58s
Lint / lint (sw) (pull_request) Successful in 2m8s
Lint / typecheck (misskey-js) (pull_request) Successful in 1m57s
Lint / typecheck (backend) (pull_request) Successful in 19m38s
2025-01-19 21:44:11 +00:00
85818f1610 add listenbrainz from sharkey
Some checks failed
Lint / pnpm_install (pull_request) Successful in 3m15s
Test (backend) / e2e (22.x) (pull_request) Failing after 9m0s
Test (backend) / unit (22.x) (pull_request) Successful in 9m59s
Test (frontend) / vitest (22.x) (pull_request) Failing after 4m43s
Test (production install and build) / production (22.x) (pull_request) Successful in 5m56s
Test (backend) / validate-api-json (22.x) (pull_request) Successful in 3m2s
Lint / lint (backend) (pull_request) Failing after 4m2s
Lint / lint (frontend) (pull_request) Failing after 8m30s
Lint / lint (misskey-js) (pull_request) Successful in 5m30s
Lint / lint (sw) (pull_request) Successful in 3m26s
Lint / typecheck (backend) (pull_request) Successful in 6m5s
Lint / typecheck (misskey-js) (pull_request) Successful in 11m58s
2025-01-19 21:06:42 +01:00
38 changed files with 322 additions and 425 deletions

View file

@ -175,8 +175,6 @@ flagAsBot: "Mark this account as a bot"
flagAsBotDescription: "Enable this option if this account is controlled by a program. If enabled, it will act as a flag for other developers to prevent endless interaction chains with other bots and adjust Misskey's internal systems to treat this account as a bot."
flagAsCat: "Mark this account as a cat"
flagAsCatDescription: "Enable this option to mark this account as a cat."
flagSpeakAsCat: "Speak as a cat"
flagSpeakAsCatDescription: "Your posts will get nyanified when in cat mode. If this isn't working, then please check that you dont have 'Disable cat speak' on under General/Note Display"
flagShowTimelineReplies: "Show replies in timeline"
flagShowTimelineRepliesDescription: "Shows replies of users to notes of other users in the timeline if turned on."
autoAcceptFollowed: "Automatically approve follow requests from users you're following"
@ -307,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"
@ -764,7 +763,6 @@ noCrawleDescription: "Ask search engines to not index your profile page, notes,
lockedAccountInfo: "Unless you set your note visiblity to \"Followers only\", your notes will be visible to anyone, even if you require followers to be manually approved."
alwaysMarkSensitive: "Mark as sensitive by default"
loadRawImages: "Load original images instead of showing thumbnails"
disableCatSpeak: "Disable cat speak"
disableShowingAnimatedImages: "Don't play animated images"
highlightSensitiveMedia: "Highlight sensitive media"
verificationEmailSent: "A verification email has been sent. Please follow the included link to complete verification."
@ -2243,7 +2241,6 @@ _permissions:
"read:clip-favorite": "View favorited clips"
"read:federation": "Get federation data"
"write:report-abuse": "Report violation"
"write:push-notification": "Receive push notifications"
_auth:
shareAccessTitle: "Granting application permissions"
shareAccess: "Would you like to authorize \"{name}\" to access this account?"

16
locales/index.d.ts vendored
View file

@ -724,14 +724,6 @@ export interface Locale extends ILocale {
*
*/
"flagAsCatDescription": string;
/**
*
*/
"flagSpeakAsCat": string;
/**
* 稿
*/
"flagSpeakAsCatDescription": string;
/**
*
*/
@ -1252,6 +1244,10 @@ export interface Locale extends ILocale {
*
*/
"birthday": string;
/**
* listenbrainzのユーザー名を入力してください
*/
"listenbrainzDescription": string;
/**
* {age}
*/
@ -3072,10 +3068,6 @@ export interface Locale extends ILocale {
*
*/
"loadRawImages": string;
/**
*
*/
"disableCatSpeak": string;
/**
*
*/

View file

@ -72,7 +72,7 @@ export function build() {
.reduce((a, [k, v]) => (a[k] = (() => {
const [lang] = k.split('-');
switch (k) {
case 'ja-JP': return merge(locales['en-US'], v);
case 'ja-JP': return v;
case 'ja-KS':
case 'en-US': return merge(locales['ja-JP'], v);
default: return merge(

View file

@ -177,8 +177,6 @@ flagAsBot: "Botとして設定"
flagAsBotDescription: "このアカウントがプログラムによって運用される場合は、このフラグをオンにします。オンにすると、反応の連鎖を防ぐためのフラグとして他の開発者に役立ったり、Misskeyのシステム上での扱いがBotに合ったものになります。"
flagAsCat: "にゃああああああああああああああ!!!!!!!!!!!!"
flagAsCatDescription: "にゃにゃにゃ??"
flagSpeakAsCat: "猫語で話す"
flagSpeakAsCatDescription: "有効にすると、あなたの投稿の 「な」を「にゃ」にします。"
flagShowTimelineReplies: "タイムラインにノートへの返信を表示する"
flagShowTimelineRepliesDescription: "オンにすると、タイムラインにユーザーのノート以外にもそのユーザーの他のノートへの返信を表示します。"
autoAcceptFollowed: "フォロー中ユーザーからのフォロリクを自動承認"
@ -309,6 +307,7 @@ activity: "アクティビティ"
images: "画像"
image: "画像"
birthday: "誕生日"
listenbrainzDescription: "listenbrainzのユーザー名を入力してください。"
yearsOld: "{age}歳"
registeredDate: "登録日"
location: "場所"
@ -764,7 +763,6 @@ noCrawleDescription: "外部の検索エンジンにあなたのユーザーペ
lockedAccountInfo: "フォローを承認制にしても、ノートの公開範囲を「フォロワー」にしない限り、誰でもあなたのノートを見ることができます。"
alwaysMarkSensitive: "デフォルトでメディアをセンシティブ設定にする"
loadRawImages: "添付画像のサムネイルをオリジナル画質にする"
disableCatSpeak: "猫の話し方を無効にする"
disableShowingAnimatedImages: "アニメーション画像を再生しない"
highlightSensitiveMedia: "メディアがセンシティブであることを分かりやすく表示"
verificationEmailSent: "確認のメールを送信しました。メールに記載されたリンクにアクセスして、設定を完了してください。"

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

@ -1,12 +0,0 @@
export class SpeakAsCat1696386694000 {
name = "SpeakAsCat1696386694000";
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" ADD "speakAsCat" boolean NOT NULL DEFAULT true`);
await queryRunner.query(`COMMENT ON COLUMN "user"."speakAsCat" IS 'Whether to speak as a cat if chosen.'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "speakAsCat"`);
}
}

View file

@ -1,18 +0,0 @@
export class MastodonOauth1736599563231 {
name = 'MastodonOauth1736599563231'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "app" ADD "website" character varying(128)`);
await queryRunner.query(`COMMENT ON COLUMN "app"."website" IS 'Application website.'`);
await queryRunner.query(`ALTER TABLE "app" ADD "mastodonScopes" character varying(64) array`);
await queryRunner.query(`COMMENT ON COLUMN "app"."mastodonScopes" IS 'Mastodon app scopes, only set for apps created with Mastodon API.'`);
await queryRunner.query(`ALTER TABLE "app" ADD "redirectUris" character varying(512) array DEFAULT '{}' NOT NULL`);
await queryRunner.query(`COMMENT ON COLUMN "app"."redirectUris" IS 'Redirect URIs.'`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "app" DROP COLUMN "website"`);
await queryRunner.query(`ALTER TABLE "app" DROP COLUMN "mastodonScopes"`);
await queryRunner.query(`ALTER TABLE "app" DROP COLUMN "redirectUris"`);
}
}

View file

@ -1,11 +0,0 @@
export class NoteUserIdIdIndex1736888704471 {
name = 'NoteUserIdIdIndex1736888704471'
async up(queryRunner) {
await queryRunner.query(`CREATE INDEX "IDX_note_userId_id" ON "note" ("userId", "id") `);
}
async down(queryRunner) {
await queryRunner.query(`DROP INDEX "public"."IDX_note_userId_id"`);
}
}

View file

@ -518,7 +518,6 @@ export class ApRendererService {
discoverable: user.isExplorable,
publicKey: this.renderKey(user, keypair, '#main-key'),
isCat: user.isCat,
speakAsCat: user.speakAsCat,
attachment: attachment.length ? attachment : undefined,
};
@ -538,6 +537,10 @@ export class ApRendererService {
person['vcard:Address'] = profile.location;
}
if (profile.listenbrainz) {
person.listenbrainz = profile.listenbrainz;
}
return person;
}

View file

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

View file

@ -389,7 +389,6 @@ export class ApPersonService implements OnModuleInit {
tags,
isBot,
isCat: (person as any).isCat === true,
speakAsCat: (person as any).speakAsCat != null ? (person as any).speakAsCat === true : (person as any).isCat === true,
emojis,
})) as MiRemoteUser;
@ -411,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) {
@ -563,13 +563,12 @@ export class ApPersonService implements OnModuleInit {
tags,
isBot: getApType(object) === 'Service' || getApType(object) === 'Application',
isCat: (person as any).isCat === true,
speakAsCat: (person as any).speakAsCat != null ? (person as any).speakAsCat === true : (person as any).isCat === true,
isLocked: person.manuallyApprovesFollowers,
movedToUri: person.movedTo ?? null,
alsoKnownAs: person.alsoKnownAs ?? null,
isExplorable: person.discoverable,
...((policy.canUpdateAvatar || policy.canUpdateBanner) ? await this.resolveAvatarAndBanner(exist, policy.canUpdateAvatar ? person.icon : exist.avatarUrl, policy.canUpdateBanner ? person.image : exist.bannerUrl).catch(() => ({})) : {}),
} as Partial<MiRemoteUser> & Pick<MiRemoteUser, 'isBot' | 'isCat' | 'speakAsCat' | 'isLocked' | 'movedToUri' | 'alsoKnownAs' | 'isExplorable'>;
} as Partial<MiRemoteUser> & Pick<MiRemoteUser, 'isBot' | 'isCat' | 'isLocked' | 'movedToUri' | 'alsoKnownAs' | 'isExplorable'>;
const moving = ((): boolean => {
// 移行先がない→ある
@ -617,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 });

View file

@ -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 =>

View file

@ -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;
@ -489,7 +491,6 @@ export class UserEntityService implements OnModuleInit {
}))) : [],
isBot: user.isBot,
isCat: user.isCat,
speakAsCat: user.speakAsCat,
instance: user.host ? this.federatedInstanceService.federatedInstanceCache.fetch(user.host).then(instance => instance ? {
name: instance.name,
softwareName: instance.softwareName,
@ -531,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,

View file

@ -1,42 +0,0 @@
import { permissions } from 'misskey-js';
const mastodonToMisskeyScopes: Map<string, (typeof permissions)[number][]> = new Map([
['profile', ['read:account']],
['follow', ['read:following', 'write:following', 'read:blocks', 'write:blocks', 'read:mutes', 'write:mutes']],
['push', ['write:push-notification']],
['read:accounts', ['read:account']],
['read:blocks', ['read:blocks']],
['read:bookmarks', ['read:favorites']],
['read:favourites', ['read:reactions']],
['read:filters', ['read:account']],
['read:follows', ['read:following']],
['read:lists', ['read:account']],
['read:mutes', ['read:mutes']],
['read:notifications', ['read:notifications']],
['read:search', []],
['read:statuses', []],
['write:accounts', ['write:account']],
['write:blocks', ['write:blocks']],
['write:bookmarks', ['write:favorites']],
['write:conversations', ['write:notes']],
['write:favourites', ['write:reactions']],
['write:filters', ['write:account']],
['write:follows', ['write:following']],
['write:lists', ['write:account']],
['write:media', ['read:drive', 'write:drive']],
['write:mutes', ['write:mutes']],
['write:notifications', ['write:notifications']],
['write:reports', ['write:report-abuse']],
['write:statuses', ['write:notes']],
]);
function setHighLevelScope(scopeName: string) {
const granularScopes = Array.from(mastodonToMisskeyScopes)
.flatMap(([key, value]) => key.startsWith(scopeName + ':') ? value : []);
mastodonToMisskeyScopes.set(scopeName, Array.from(new Set(granularScopes)));
}
setHighLevelScope('read');
setHighLevelScope('write');
export { mastodonToMisskeyScopes };

View file

@ -63,22 +63,4 @@ export class MiApp {
comment: 'The callbackUrl of the App.',
})
public callbackUrl: string | null;
@Column('varchar', {
length: 128, nullable: true,
comment: 'Application website.',
})
public website: string;
@Column('varchar', {
length: 64, array: true, nullable: true,
comment: 'Mastodon app scopes, only set for apps created with Mastodon API.',
})
public mastodonScopes: string[];
@Column('varchar', {
length: 512, array: true, nullable: true,
comment: 'Redirect URIs.',
})
public redirectUris: string[];
}

View file

@ -15,7 +15,6 @@ import type { MiDriveFile } from './DriveFile.js';
@Index('IDX_NOTE_MENTIONS', { synchronize: false })
@Index('IDX_NOTE_FILE_IDS', { synchronize: false })
@Index('IDX_NOTE_VISIBLE_USER_IDS', { synchronize: false })
@Index('IDX_note_userId_id', ['userId', 'id'])
export class MiNote {
@PrimaryColumn(id())
public id: string;

View file

@ -186,12 +186,6 @@ export class MiUser {
})
public isCat: boolean;
@Column('boolean', {
default: false,
comment: 'Whether the User speaks in nya.',
})
public speakAsCat: boolean;
@Column('boolean', {
default: false,
comment: 'Whether the User is the root.',
@ -313,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;

View file

@ -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.',

View file

@ -115,10 +115,6 @@ export const packedUserLiteSchema = {
type: 'boolean',
nullable: false, optional: true,
},
speakAsCat: {
type: 'boolean',
nullable: false, optional: true,
},
instance: {
type: 'object',
nullable: false, optional: true,
@ -272,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,

View file

@ -7,7 +7,6 @@ import { Inject, Injectable } from '@nestjs/common';
import cors from '@fastify/cors';
import multipart from '@fastify/multipart';
import fastifyCookie from '@fastify/cookie';
import fastifyFormbody from '@fastify/formbody';
import { ModuleRef } from '@nestjs/core';
import type { Config } from '@/config.js';
import type { InstancesRepository, AccessTokensRepository } from '@/models/_.js';
@ -64,13 +63,6 @@ export class ApiServerService {
done();
});
fastify.register(this.createMisskeyServer);
fastify.register(this.createMastodonServer);
done();
}
@bindThis
private createMisskeyServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
for (const endpoint of endpoints) {
const ep = {
name: endpoint.name,
@ -114,6 +106,32 @@ export class ApiServerService {
}
}
const createEndpoint = (endpoint: IMastodonEndpoint): IMastodonEndpoint & { exec: any } => ({
name: endpoint.name,
method: endpoint.method,
meta: endpoint.meta,
params: endpoint.params,
exec: this.moduleRef.get(`mep:${endpoint.method}:${endpoint.name}`, { strict: false }).exec,
});
const groupedMastodonEndpoints = Array.from(Map.groupBy(mastodonEndpoints.map(createEndpoint), endpoint => endpoint.name))
.map(([name, endpoints]) => ({ name, endpoints: new Map(endpoints.map(endpoint => [endpoint.method, endpoint])) }));
for (const { name, endpoints } of groupedMastodonEndpoints) {
fastify.all<{
Params: { endpoint: string; },
Body: Record<string, unknown>,
Querystring: Record<string, unknown>,
}>('/' + name, async (request, reply) => {
const ep = endpoints.get(request.method);
if (!ep) {
reply.code(405);
reply.send();
return;
}
await this.apiCallService.handleMastodonRequest(ep, request, reply);
return reply;
});
}
fastify.post<{
Body: {
username: string;
@ -194,39 +212,4 @@ export class ApiServerService {
done();
}
@bindThis
private createMastodonServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
fastify.register(fastifyFormbody);
const createEndpoint = (endpoint: IMastodonEndpoint): IMastodonEndpoint & { exec: any } => ({
name: endpoint.name,
method: endpoint.method,
meta: endpoint.meta,
params: endpoint.params,
exec: this.moduleRef.get(`mep:${endpoint.method}:${endpoint.name}`, { strict: false }).exec,
});
const groupedMastodonEndpoints = Array.from(Map.groupBy(mastodonEndpoints.map(createEndpoint), endpoint => endpoint.name))
.map(([name, endpoints]) => ({ name, endpoints: new Map(endpoints.map(endpoint => [endpoint.method, endpoint])) }));
for (const { name, endpoints } of groupedMastodonEndpoints) {
fastify.all<{
Params: { endpoint: string; },
Body: Record<string, unknown>,
Querystring: Record<string, unknown>,
}>('/' + name, async (request, reply) => {
const ep = endpoints.get(request.method);
if (!ep) {
reply.code(405);
reply.send();
return;
}
await this.apiCallService.handleMastodonRequest(ep, request, reply);
return reply;
});
}
done();
}
}

View file

@ -2,10 +2,8 @@ import { Module, Provider } from '@nestjs/common';
import { CoreModule } from '@/core/CoreModule.js';
import * as mep___accounts_lookup_v1_get from './mastodon/accounts/lookup/v1/get.js';
import * as mep___apps_v1_post from './mastodon/apps/v1/post.js';
const $accounts_lookup_v1_get: Provider = { provide: 'mep:GET:v1/accounts/lookup', useClass: mep___accounts_lookup_v1_get.default };
const $apps_v1_post: Provider = { provide: 'mep:POST:v1/apps', useClass: mep___apps_v1_post.default };
@Module({
imports: [
@ -13,11 +11,9 @@ const $apps_v1_post: Provider = { provide: 'mep:POST:v1/apps', useClass: mep___a
],
providers: [
$accounts_lookup_v1_get,
$apps_v1_post,
],
exports: [
$accounts_lookup_v1_get,
$apps_v1_post,
],
})
export class MastodonEndpointsModule {}

View file

@ -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: {
@ -187,7 +188,6 @@ export const paramDef = {
preventAiLearning: { type: 'boolean' },
isBot: { type: 'boolean' },
isCat: { type: 'boolean' },
speakAsCat: { type: 'boolean' },
injectFeaturedNote: { type: 'boolean' },
receiveAnnouncementEmail: { type: 'boolean' },
alwaysMarkNsfw: { type: 'boolean' },
@ -307,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) {
@ -342,7 +343,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (typeof ps.noCrawle === 'boolean') profileUpdates.noCrawle = ps.noCrawle;
if (typeof ps.preventAiLearning === 'boolean') profileUpdates.preventAiLearning = ps.preventAiLearning;
if (typeof ps.isCat === 'boolean') updates.isCat = ps.isCat;
if (typeof ps.speakAsCat === 'boolean') updates.speakAsCat = ps.speakAsCat;
if (typeof ps.injectFeaturedNote === 'boolean') profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
if (typeof ps.receiveAnnouncementEmail === 'boolean') profileUpdates.receiveAnnouncementEmail = ps.receiveAnnouncementEmail;
if (typeof ps.alwaysMarkNsfw === 'boolean') {

View file

@ -1,10 +1,8 @@
import { Schema } from '@/misc/json-schema.js';
import * as mep___accounts_lookup_v1_get from './mastodon/accounts/lookup/v1/get.js';
import * as mep___apps_v1_post from './mastodon/apps/v1/post.js';
const eps = [
['GET', 'v1/accounts/lookup', mep___accounts_lookup_v1_get],
['POST', 'v1/apps', mep___apps_v1_post],
];
export interface IMastodonEndpointMeta {

View file

@ -1,80 +0,0 @@
import { Inject, Injectable } from '@nestjs/common';
import { MastodonEndpoint } from '@/server/api/mastodon-endpoint-base.js';
import { MastodonApiError } from '@/server/api/error.js';
import { secureRndstr } from '@/misc/secure-rndstr.js';
import type { AppsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { IdService } from '@/core/IdService.js';
import { mastodonToMisskeyScopes } from '@/misc/mastodon/mastodon-to-misskey-scopes.js';
export const meta = {
requireCredential: false,
} as const;
export const paramDef = {
type: 'object',
properties: {
client_name: { type: 'string', minLength: 1 },
redirect_uri: { type: 'string', minLength: 1 },
redirect_uris: {
anyOf: [
{ type: 'array', minItems: 1, items: { type: 'string', minLength: 1 } },
{ type: 'string', minLength: 1 },
],
},
scopes: { type: 'string', minLength: 1 },
website: { type: 'string' },
},
anyOf: [
{ required: ['redirect_uri'] },
{ required: ['redirect_uris'] },
],
required: ['client_name'],
} as const;
@Injectable()
export default class extends MastodonEndpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.appsRepository)
private appsRepository: AppsRepository,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const redirectUrlsRawValue = ps.redirect_uris ?? ps.redirect_uri ?? [];
const redirectUris = typeof redirectUrlsRawValue === 'string' ? redirectUrlsRawValue.split('\n') : redirectUrlsRawValue;
const secret = secureRndstr(32);
const mastodonScopes = (ps.scopes ?? 'read').split(' ');
const scopes = mastodonScopes.flatMap(scope => {
const misskeyScopes = mastodonToMisskeyScopes.get(scope);
if (!misskeyScopes) {
throw new MastodonApiError('Scopes doesn\'t match configured on the server.', 400);
}
return misskeyScopes;
});
const clientId = this.idService.gen();
await this.appsRepository.insert({
id: clientId,
userId: me ? me.id : null,
name: ps.client_name,
description: ps.website ?? '',
permission: scopes,
callbackUrl: null,
secret: secret,
website: ps.website,
mastodonScopes: mastodonScopes,
redirectUris: redirectUris,
});
return {
id: clientId,
name: ps.client_name,
website: ps.website,
scopes: mastodonScopes,
redirect_uri: redirectUris.join('\n'),
redirect_uris: redirectUris,
client_id: `mastodon:${clientId}`,
client_secret: secret,
};
});
}
}

View file

@ -34,7 +34,6 @@ import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import type {
AccessTokensRepository,
AppsRepository,
IndieAuthClientsRepository,
UserProfilesRepository,
UsersRepository,
@ -46,15 +45,9 @@ import { LoggerService } from '@/core/LoggerService.js';
import Logger from '@/logger.js';
import { StatusError } from '@/misc/status-error.js';
import { normalizeEmailAddress } from '@/misc/normalize-email-address.js';
import { mastodonToMisskeyScopes } from '@/misc/mastodon/mastodon-to-misskey-scopes.js';
import type { ServerResponse } from 'node:http';
import type { FastifyInstance } from 'fastify';
function extractMastodonAppId(clientId: string): string | null {
const MASTODON_CLIENT_ID_PREFIX = 'mastodon:';
return clientId.startsWith(MASTODON_CLIENT_ID_PREFIX) ? clientId.substring(MASTODON_CLIENT_ID_PREFIX.length) : null;
}
// TODO: Consider migrating to @node-oauth/oauth2-server once
// https://github.com/node-oauth/node-oauth2-server/issues/180 is figured out.
// Upstream the various validations and RFC9207 implementation in that case.
@ -265,8 +258,6 @@ export class OAuth2ProviderService {
private usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
@Inject(DI.appsRepository)
private appsRepository: AppsRepository,
private idService: IdService,
private cacheService: CacheService,
@ -337,25 +328,13 @@ export class OAuth2ProviderService {
if (body.client_id !== granted.clientId) return;
if (redirectUri !== granted.redirectUri) return;
const mastodonAppId = extractMastodonAppId(granted.clientId);
// https://datatracker.ietf.org/doc/html/rfc7636.html#section-4.6
// For Mastodon API, code verifier isn't necessary (but if code challenge was provided, then it should be verified)
if (!mastodonAppId || granted.codeChallenge || body.code_verifier) {
if (!body.code_verifier) return;
if (!(await verifyChallenge(body.code_verifier as string, granted.codeChallenge))) return;
}
if (!body.code_verifier) return;
if (!(await verifyChallenge(body.code_verifier as string, granted.codeChallenge))) return;
const accessToken = secureRndstr(128);
const now = new Date();
let scopes = granted.scopes;
let name = granted.clientId;
if (mastodonAppId) {
scopes = [...new Set(granted.scopes.flatMap((scope: string) => mastodonToMisskeyScopes.get(scope)))];
name = (await this.appsRepository.findOneBy({ id: mastodonAppId }))?.name ?? name;
}
// NOTE: we don't have a setup for automatic token expiration
await accessTokensRepository.insert({
id: idService.gen(now.getTime()),
@ -363,9 +342,8 @@ export class OAuth2ProviderService {
userId: granted.userId,
token: accessToken,
hash: accessToken,
name: name,
permission: scopes,
appId: mastodonAppId,
name: granted.clientId,
permission: granted.scopes,
});
grantedState = await this.redisClient.get(`oauth2:authorization:${code}:state`);
@ -376,7 +354,7 @@ export class OAuth2ProviderService {
}
granted.grantedToken = accessToken;
this.#logger.info(`Generated access token for ${granted.clientId} for user ${granted.userId}, with scope: [${scopes}]`);
this.#logger.info(`Generated access token for ${granted.clientId} for user ${granted.userId}, with scope: [${granted.scopes}]`);
await this.redisClient.set(`oauth2:authorization:${code}`, JSON.stringify(granted), 'EX', 60 * 5);
return [accessToken, undefined, { scope: granted.scopes.join(' ') }];
@ -440,75 +418,50 @@ export class OAuth2ProviderService {
this.#logger.info(`Validating authorization parameters, with client_id: ${clientID}, redirect_uri: ${redirectURI}, scope: ${scope}`);
let clientInfo: ClientInformation;
const mastodonAppId = extractMastodonAppId(clientID);
if (mastodonAppId) {
const app = await this.appsRepository.findOneBy({ id: mastodonAppId });
if (!app) {
throw new AuthorizationError('unrecognized client id', 'invalid_request');
}
clientInfo = {
id: clientID,
name: app.name,
redirectUris: app.redirectUris,
};
if (codeChallengeMethod && codeChallengeMethod !== 'S256') {
throw new AuthorizationError('`code_challenge_method` parameter must be set as S256', 'invalid_request');
}
try {
const scopes = [...new Set(scope)].filter(s => mastodonToMisskeyScopes.has(s));
if (!scopes.length) {
throw new AuthorizationError('`scope` parameter has no known scope', 'invalid_scope');
}
areq.scope = scopes;
} catch (err) {
return [err as Error, clientInfo, redirectURI];
}
} else {
const clientUrl = validateClientId(clientID);
const clientUrl = validateClientId(clientID);
// https://indieauth.spec.indieweb.org/#client-information-discovery
// "the server may want to resolve the domain name first and avoid fetching the document
// if the IP address is within the loopback range defined by [RFC5735]
// or any other implementation-specific internal IP address."
if (process.env.NODE_ENV !== 'test' || process.env.MISSKEY_TEST_CHECK_IP_RANGE === '1') {
const lookup = await dns.lookup(clientUrl.hostname);
if (ipaddr.parse(lookup.address).range() !== 'unicast') {
throw new AuthorizationError('client_id resolves to disallowed IP range.', 'invalid_request');
}
}
// Find client information from the database.
const registeredClientInfo = await this.indieAuthClientsRepository.findOneBy({ id: clientUrl.href }) as ClientInformation | null;
// Find client information from the remote.
clientInfo = registeredClientInfo ?? await discoverClientInformation(this.#logger, this.httpRequestService, clientUrl.href);
try {
const scopes = [...new Set(scope)].filter(s => (<readonly string[]>kinds).includes(s));
if (!scopes.length) {
throw new AuthorizationError('`scope` parameter has no known scope', 'invalid_scope');
}
areq.scope = scopes;
// Require PKCE parameters. This requirement is skipped for Mastodon clients, as Mastodon API doesn't require it.
// Recommended by https://indieauth.spec.indieweb.org/#authorization-request, but also prevents downgrade attack:
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#name-pkce-downgrade-attack
if (typeof codeChallenge !== 'string') {
throw new AuthorizationError('`code_challenge` parameter is required', 'invalid_request');
}
if (codeChallengeMethod !== 'S256') {
throw new AuthorizationError('`code_challenge_method` parameter must be set as S256', 'invalid_request');
}
} catch (err) {
return [err as Error, clientInfo, redirectURI];
// https://indieauth.spec.indieweb.org/#client-information-discovery
// "the server may want to resolve the domain name first and avoid fetching the document
// if the IP address is within the loopback range defined by [RFC5735]
// or any other implementation-specific internal IP address."
if (process.env.NODE_ENV !== 'test' || process.env.MISSKEY_TEST_CHECK_IP_RANGE === '1') {
const lookup = await dns.lookup(clientUrl.hostname);
if (ipaddr.parse(lookup.address).range() !== 'unicast') {
throw new AuthorizationError('client_id resolves to disallowed IP range.', 'invalid_request');
}
}
// Find client information from the database.
const registeredClientInfo = await this.indieAuthClientsRepository.findOneBy({ id: clientUrl.href }) as ClientInformation | null;
// Find client information from the remote.
const clientInfo = registeredClientInfo ?? await discoverClientInformation(this.#logger, this.httpRequestService, clientUrl.href);
// Require the redirect URI to be included in an explicit list, per
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.1.3
if (!clientInfo.redirectUris.includes(redirectURI)) {
throw new AuthorizationError('Invalid redirect_uri', 'invalid_request');
}
try {
const scopes = [...new Set(scope)].filter(s => (<readonly string[]>kinds).includes(s));
if (!scopes.length) {
throw new AuthorizationError('`scope` parameter has no known scope', 'invalid_scope');
}
areq.scope = scopes;
// Require PKCE parameters.
// Recommended by https://indieauth.spec.indieweb.org/#authorization-request, but also prevents downgrade attack:
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#name-pkce-downgrade-attack
if (typeof codeChallenge !== 'string') {
throw new AuthorizationError('`code_challenge` parameter is required', 'invalid_request');
}
if (codeChallengeMethod !== 'S256') {
throw new AuthorizationError('`code_challenge_method` parameter must be set as S256', 'invalid_request');
}
} catch (err) {
return [err as Error, clientInfo, redirectURI];
}
return [null, clientInfo, redirectURI];
})().then(args => done(...args), err => done(err));
}) as ValidateFunctionArity2));

View file

@ -40,7 +40,6 @@ describe('ユーザー', () => {
avatarDecorations: user.avatarDecorations,
isBot: user.isBot,
isCat: user.isCat,
speakAsCat: user.speakAsCat,
instance: user.instance,
emojis: user.emojis,
onlineStatus: user.onlineStatus,
@ -91,6 +90,7 @@ describe('ユーザー', () => {
securityKeys: user.securityKeys,
roles: user.roles,
memo: user.memo,
listenbrainz: user.listenbrainz,
});
};
@ -312,7 +312,6 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response.avatarDecorations, []);
assert.strictEqual(response.isBot, false);
assert.strictEqual(response.isCat, false);
assert.strictEqual(response.speakAsCat, false);
assert.strictEqual(response.instance, undefined);
assert.deepStrictEqual(response.emojis, {});
assert.strictEqual(response.onlineStatus, 'unknown');
@ -448,8 +447,6 @@ describe('ユーザー', () => {
{ parameters: () => ({ isBot: false }) },
{ parameters: () => ({ isCat: true }) },
{ parameters: () => ({ isCat: false }) },
{ parameters: () => ({ speakAsCat: true }) },
{ parameters: () => ({ speakAsCat: false }) },
{ parameters: () => ({ injectFeaturedNote: true }) },
{ parameters: () => ({ injectFeaturedNote: false }) },
{ parameters: () => ({ receiveAnnouncementEmail: true }) },

View file

@ -100,7 +100,6 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host = 'mi
isBlocking: false,
isBot: false,
isCat: false,
speakAsCat: false,
isFollowed: false,
isFollowing: false,
isLocked: false,

View file

@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { nextTick, onMounted, onUnmounted, shallowRef, watch } from 'vue';
import { nextTick, onMounted, onUnmounted, shallowRef } from 'vue';
import * as os from '@/os.js';
import { calcPopupPosition } from '@/scripts/popup-position.js';
import { defaultStore } from '@/store.js';
@ -70,7 +70,7 @@ function setPosition() {
el.value.style.top = data.top + 'px';
}
let loopHandler: number | undefined;
let loopHandler;
onMounted(() => {
nextTick(() => {
@ -81,23 +81,12 @@ onMounted(() => {
loopHandler = window.requestAnimationFrame(loop);
};
watch(() => props.showing, show => {
if (show) {
if (!loopHandler) {
loop();
}
} else if (loopHandler) {
window.cancelAnimationFrame(loopHandler);
loopHandler = undefined;
}
});
loop();
});
});
onUnmounted(() => {
if (loopHandler) {
window.cancelAnimationFrame(loopHandler);
}
window.cancelAnimationFrame(loopHandler);
});
</script>

View file

@ -55,7 +55,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
provide('linkNavigationBehavior', props.linkNavigationBehavior);
const isNote = props.isNote ?? true;
const shouldNyaize = props.nyaize === 'respect' && props.author?.isCat && props.author?.speakAsCat && !defaultStore.state.disableCatSpeak;
const shouldNyaize = props.nyaize ? props.nyaize === 'respect' ? props.author?.isCat : false : false;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (props.text == null || props.text === '') return;
@ -85,7 +85,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
switch (token.type) {
case 'text': {
let text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n');
if (!disableNyaize && shouldNyaize) {
if (!disableNyaize && shouldNyaize) {
text = doNyaize(text);
}

View file

@ -63,7 +63,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="showReactionsCount">{{ i18n.ts.showReactionsCount }}</MkSwitch>
<MkSwitch v-model="showGapBetweenNotesInTimeline">{{ i18n.ts.showGapBetweenNotesInTimeline }}</MkSwitch>
<MkSwitch v-model="loadRawImages">{{ i18n.ts.loadRawImages }}</MkSwitch>
<MkSwitch v-model="disableCatSpeak">{{ i18n.ts.disableCatSpeak }}</MkSwitch>
<MkRadios v-model="reactionsDisplaySize">
<template #label>{{ i18n.ts.reactionsDisplaySize }}</template>
<option value="small">{{ i18n.ts.small }}</option>
@ -300,7 +299,6 @@ const disableDrawer = computed(defaultStore.makeGetterSetter('disableDrawer'));
const disableShowingAnimatedImages = computed(defaultStore.makeGetterSetter('disableShowingAnimatedImages'));
const forceShowAds = computed(defaultStore.makeGetterSetter('forceShowAds'));
const loadRawImages = computed(defaultStore.makeGetterSetter('loadRawImages'));
const disableCatSpeak = computed(defaultStore.makeGetterSetter('disableCatSpeak'));
const highlightSensitiveMedia = computed(defaultStore.makeGetterSetter('highlightSensitiveMedia'));
const imageNewTab = computed(defaultStore.makeGetterSetter('imageNewTab'));
const warnMissingAltText = computed(defaultStore.makeGetterSetter('warnMissingAltText'));

View file

@ -78,7 +78,6 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
'warnMissingAltText',
'imageNewTab',
'dataSaver',
'disableCatSpeak',
'disableShowingAnimatedImages',
'emojiStyle',
'disableDrawer',

View file

@ -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>
@ -171,7 +177,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m">
<MkSwitch v-model="profile.isCat">{{ i18n.ts.flagAsCat }}<template #caption>{{ i18n.ts.flagAsCatDescription }}</template></MkSwitch>
<MkSwitch v-if="profile.isCat" v-model="profile.speakAsCat">{{ i18n.ts.flagSpeakAsCat }}<template #caption>{{ i18n.ts.flagSpeakAsCatDescription }}</template></MkSwitch>
<MkSwitch v-model="profile.isBot">{{ i18n.ts.flagAsBot }}<template #caption>{{ i18n.ts.flagAsBotDescription }}</template></MkSwitch>
</div>
</MkFolder>
@ -218,10 +223,10 @@ 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,
speakAsCat: $i.speakAsCat ?? false,
});
watch(() => profile, () => {
@ -302,11 +307,11 @@ 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,
isCat: !!profile.isCat,
speakAsCat: !!profile.speakAsCat,
});
globalEvents.emit('requestClearPageCache');
claimAchievement('profileFilled');

View file

@ -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>
@ -225,7 +234,7 @@ import MkTextarea from '@/components/MkTextarea.vue';
import MkOmit from '@/components/MkOmit.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkButton from '@/components/MkButton.vue';
import { getScrollContainer } from '@/scripts/scroll.js';
import { getScrollPosition } from '@/scripts/scroll.js';
import { getUserMenu } from '@/scripts/get-user-menu.js';
import number from '@/filters/number.js';
import { userPage } from '@/filters/user.js';
@ -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<{
@ -282,7 +292,26 @@ const isEditingMemo = ref(false);
const moderationNote = ref(props.user.moderationNote);
const editModerationNote = ref(false);
const movedFromLog = ref<null | {movedFromId:string;}[]>(null);
let scrollEl: null | HTMLElement = 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 });
@ -314,19 +343,15 @@ async function fetchMovedFromLog() {
}
function parallaxLoop() {
requestNextParallaxFrame();
parallax();
}
function requestNextParallaxFrame() {
parallaxAnimationId.value = window.requestAnimationFrame(parallaxLoop);
parallax();
}
function parallax() {
const banner = bannerEl.value as any;
if (banner == null) return;
const top = scrollEl?.scrollTop ?? scrollY;
const top = getScrollPosition(rootEl.value);
if (top < 0) return;
@ -335,23 +360,6 @@ function parallax() {
banner.style.backgroundPosition = `center calc(50% - ${pos}px)`;
}
function startParallax() {
(scrollEl ?? window).removeEventListener('scroll', startParallax);
requestNextParallaxFrame();
}
function addScrollEvent() {
(scrollEl ?? window).addEventListener('scroll', startParallax);
}
function cancelParallax() {
if (parallaxAnimationId.value) {
window.cancelAnimationFrame(parallaxAnimationId.value);
parallaxAnimationId.value = null;
addScrollEvent();
}
}
function showMemoTextarea() {
isEditingMemo.value = true;
nextTick(() => {
@ -416,10 +424,7 @@ watch([props.user], () => {
});
onMounted(() => {
scrollEl = getScrollContainer(rootEl.value);
addScrollEvent();
(scrollEl ?? window).addEventListener('scrollend', cancelParallax);
parallax();
window.requestAnimationFrame(parallaxLoop);
narrow.value = rootEl.value!.clientWidth < 1000;
if (props.user.birthday) {
@ -442,7 +447,11 @@ onMounted(() => {
});
});
onUnmounted(cancelParallax);
onUnmounted(() => {
if (parallaxAnimationId.value) {
window.cancelAnimationFrame(parallaxAnimationId.value);
}
});
</script>
<style lang="scss" scoped>

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

@ -259,10 +259,6 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: window.matchMedia('(prefers-reduced-motion)').matches,
},
disableCatSpeak: {
where: 'account',
default: false,
},
emojiStyle: {
where: 'device',
default: 'twemoji', // twemoji / fluentEmoji / native

View file

@ -2820,7 +2820,7 @@ type PagesUpdateRequest = operations['pages___update']['requestBody']['content']
function parse(acct: string): Acct;
// @public (undocumented)
export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "read:admin:abuse-report-resolvers", "write:admin:abuse-report-resolvers", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:regenerate-user-token", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-account-move-log", "read:admin:show-user", "read:admin:show-users", "write:admin:suspend-user", "write:admin:approve-user", "write:admin:decline-user", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-name", "write:admin:user-note", "write:admin:user-avatar", "write:admin:user-banner", "write:admin:user-mutual-link", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:indie-auth", "read:admin:indie-auth", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:sso", "read:admin:sso", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse", "write:push-notification"];
export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "read:admin:abuse-report-resolvers", "write:admin:abuse-report-resolvers", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:regenerate-user-token", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-account-move-log", "read:admin:show-user", "read:admin:show-users", "write:admin:suspend-user", "write:admin:approve-user", "write:admin:decline-user", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-name", "write:admin:user-note", "write:admin:user-avatar", "write:admin:user-banner", "write:admin:user-mutual-link", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:indie-auth", "read:admin:indie-auth", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:sso", "read:admin:sso", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"];
// @public (undocumented)
type PingResponse = operations['ping']['responses']['200']['content']['application/json'];

View file

@ -3853,7 +3853,6 @@ export type components = {
}[];
isBot?: boolean;
isCat?: boolean;
speakAsCat?: boolean;
instance?: {
name: string | null;
softwareName: string | null;
@ -3901,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: {
@ -21728,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 */
@ -21756,7 +21758,6 @@ export type operations = {
preventAiLearning?: boolean;
isBot?: boolean;
isCat?: boolean;
speakAsCat?: boolean;
injectFeaturedNote?: boolean;
receiveAnnouncementEmail?: boolean;
alwaysMarkNsfw?: boolean;

View file

@ -103,7 +103,6 @@ export const permissions = [
'read:clip-favorite',
'read:federation',
'write:report-abuse',
'write:push-notification', // Mastodon permission
] as const;
export const moderationLogTypes = [