mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-08-18 15:38:36 +00:00
Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b5a1eedcc0 | |||
| 5c4778a492 | |||
| 1cf465ad93 | |||
| 0ceadf5f44 | |||
| b0d1510abc | |||
| 41b11902a8 | |||
| 1cdfdfac31 | |||
| 981d327bf3 | |||
| 41cd2eff0c | |||
| 1b38954fcd | |||
| bd6123ed5c | |||
| 3923e39810 | |||
| 1c61d7da51 | |||
| a5b858b45b | |||
| 1cdd177478 | |||
| 0623cabc81 | |||
| 7fbb4828ce | |||
| a4d2273682 | |||
| 25090b6bc0 | |||
| 4d987e8843 | |||
| 87b88d7347 | |||
| eaac254498 | |||
| 3df1f0e780 | |||
| 23ca8b2286 | |||
| f95f5691d3 | |||
| eeae7a3c16 | |||
| 398f9a6b9b | |||
| 28044973e7 | |||
| 25aae46c59 | |||
| 9cc4c7d46b | |||
| dfc313cadb | |||
| 17fa6cd13a | |||
| 91db5ad709 | |||
| 8f35f576e0 | |||
| fcd0f0c8d2 | |||
| da35321f28 | |||
| 640ddb2a58 | |||
| ab7e9770b1 | |||
| 6f1c91d9c7 | |||
| 510952f861 | |||
| e90f664922 | |||
| 4cca0c220e | |||
| be9762c169 | |||
| c1bd11eada | |||
| 2debf09bf2 | |||
| a4a3606435 | |||
| 9f3d80a97a | |||
| e57e88ebc6 | |||
| d490936bf6 | |||
| 63a8520191 | |||
| f88e647a83 | |||
| 4263f62e7b | |||
| d0b8940723 | |||
| f8e28df711 | |||
| 19605600d3 |
+4
-2
@@ -1,12 +1,11 @@
|
||||
## 2026.8.0
|
||||
|
||||
### General
|
||||
- Enhance: ノート翻訳時、CWも翻訳対象に含めるように
|
||||
-
|
||||
|
||||
### Client
|
||||
- Enhance: 画像ビューワーで、ピクセルアートの拡大表示に適したモードを追加(画像ビューワー起動時に画面上の詳細メニューから有効化できます)
|
||||
- Fix: 画像の表示時にBlurhashが描画されない場合があるのを修正
|
||||
- Fix: モバイルでの利用時に一部のテキストが選択できない問題を修正
|
||||
|
||||
### Server
|
||||
- Fix: 既にミュートしているスレッドに対して再度スレッドミュートを作成しようとするとサーバーエラーになる問題を修正
|
||||
@@ -30,6 +29,9 @@
|
||||
例えば `allowPrivateNetworks` を 2026.6.0 までの `example.yml` の構文を元に記述している場合は、配列の閉じ括弧のインデントを上げるか、リスト表示に書き換える必要があります。詳しくは https://github.com/misskey-dev/misskey/pull/17701 をご覧ください。
|
||||
|
||||
### General
|
||||
- Feat: 自分に表示する公開ロールとロールバッジをユーザー側で選べるように
|
||||
- 必ず表示する必要があるロールを作成することもできます(既存のロールについてはすべて表示必須設定となります。必要に応じて設定を変更してください)
|
||||
- 今後新規作成するロールのデフォルト値は表示必須設定なし(ユーザーが表示・非表示を選べる)となります。
|
||||
- Feat: コントロールパネルから二要素認証を解除できるように
|
||||
- Feat: 条件に一致したURLプレビューのサムネイルを隠すことができるように
|
||||
(Based on https://github.com/MisskeyIO/misskey/pull/214)
|
||||
|
||||
@@ -1421,7 +1421,6 @@ append: "Afegeix al final"
|
||||
prepend: "Afegeix al principi"
|
||||
urlPreviewSensitiveList: "Llista d'URLs per restringir la visualització de miniatures"
|
||||
urlPreviewSensitiveListDescription: "Si separeu els termes amb un espai, s'interpretarà com una condició 'AND'; si els separeu amb un salt de línia, s'interpretarà com una condició 'OR'. Si envolupeu els termes amb barres obliqües, s'interpretarà com una expressió regular. Si es troba una coincidència, la miniatura ja no es mostrarà."
|
||||
pixelatedZoom: "Mode de zoom d'art pixelat"
|
||||
_imageEditing:
|
||||
_vars:
|
||||
caption: "Títol de l'arxiu"
|
||||
|
||||
@@ -1421,7 +1421,6 @@ append: "Añadir al final"
|
||||
prepend: "Añadir al principio"
|
||||
urlPreviewSensitiveList: "URL para restringir la visualización de miniaturas"
|
||||
urlPreviewSensitiveListDescription: "Si se separan con un espacio, se interpretará como una condición «AND»; si se separan con un salto de línea, se interpretará como una condición «OR». Si se escriben entre barras, se interpretarán como expresiones regulares. Si se encuentra una coincidencia, no se mostrará la miniatura."
|
||||
pixelatedZoom: "Modo de ampliación para pixel art"
|
||||
_imageEditing:
|
||||
_vars:
|
||||
caption: "Título del archivo"
|
||||
|
||||
+3
-32
@@ -174,7 +174,7 @@ emojiUrl: "پیوند شکلک"
|
||||
addEmoji: "افزودن شکلک"
|
||||
settingGuide: "تنظیمات پیشنهادی"
|
||||
cacheRemoteFiles: "کش کردن پرونده های خارجی"
|
||||
cacheRemoteFilesDescription: "در صورت فعال کردن این قابلیت پرونده های خارجی (در سرور های دیگر) در فضای ذخیره این سرور بارگیری می شود. این کار سرعت نمایش پرونده ها را افزایش می دهد، اما با این کار فضای داخلی سرور بیشتر اشغال می شود. مدت زمانی که هر کاربر می تواند از سرور خارجی کش کند، براساس محدودیت ظرفیت حافظه اعمال شده بر نقش اوست. اگر از این محدودیت فراتر رود پرونده های قدیمی حافظه پنهان (کش) حذف و تبدیل به لینک پیوند می شود. اگر این تنظیم غیرفعال باشد پرونده های خارجی از همان ابتدا به صورت لینک پیوند ذخیره می شود.\n"
|
||||
cacheRemoteFilesDescription: "در صورت فعال کردن این قابلیت پرونده های خارجی (در سرور های دیگر) در فضای ذخیره این سرور بارگیری می شود. این کار سرعت نمایش تصاویر را افزایش می دهد، اما با این کار فضای داخلی سرور بیشتر اشغال می شود. مدت زمانی که هر کاربر می تواند از سرور خارجی کش کند، براساس محدودیت ظرفیت حافظه اعمال شده بر نقش اوست. اگر از این محدودیت فراتر رود پرونده های قدیمی حافظه پنهان (کش) حذف و تبدیل به لینک پیوند می شود. اگر این تنظیم غیرفعال باشد پرونده های خارجی از همان ابتدا به صورت لینک پیوند ذخیره می شود.\n"
|
||||
youCanCleanRemoteFilesCache: "شما می توانید حافظه پنهان (کش) را با کلیک بر دکمه 🗑️ در پایین صفحه مدیریت پرونده ها حذف کنید."
|
||||
cacheRemoteSensitiveFiles: "کش کردن پرونده های حساس سرور های خارجی"
|
||||
cacheRemoteSensitiveFilesDescription: "با غیر فعال کردن این قابلیت پرونده هایی که علامت حساس خورده اند در سرور های خارجی به جای اینکه در حافظه پنهان ذخیره شوند، مستقیما به ان پیوند داده می شوند."
|
||||
@@ -200,47 +200,23 @@ removeWallpaper: "حذف تصویر پسزمینه"
|
||||
searchWith: "جستجو: {q}"
|
||||
youHaveNoLists: "شما فهرستی ندارید"
|
||||
followConfirm: "آیا می خواهید {name} را دنبال کنید؟"
|
||||
proxyAccount: "حساب نماینده (پروکسی)"
|
||||
proxyAccount: "حساب وکیل"
|
||||
host: "میزبان"
|
||||
selectSelf: "انتخاب خودت"
|
||||
selectUser: "انتخاب کاربر"
|
||||
recipient: "گیرنده"
|
||||
annotation: "حاشیه نویسی"
|
||||
federation: "شبکهاجتماعیغیرمتمرکز"
|
||||
instances: "سرور"
|
||||
registeredAt: "ثبت شده در"
|
||||
latestRequestReceivedAt: "آخرین درخواست دریافت شده"
|
||||
latestStatus: "جدیدترین وضعیت"
|
||||
storageUsage: "حافظه مصرفی"
|
||||
charts: "نمودارها"
|
||||
perHour: "هر ۱ ساعت"
|
||||
perDay: "هر ۱ روز"
|
||||
stopActivityDelivery: "جلوگیری از ارائه فعالیت ها"
|
||||
blockThisInstance: "این سرور را مسدود کن"
|
||||
silenceThisInstance: "این سرور را بیصدا کن"
|
||||
mediaSilenceThisInstance: "محتوای چندرسانه ای این سرور را بیصدا کن"
|
||||
operations: "عملیات ها"
|
||||
software: "نرمافزار"
|
||||
softwareName: "نام نرمافزار"
|
||||
version: "نگارش"
|
||||
metadata: "متادیتا"
|
||||
withNFiles: "{n} پرونده"
|
||||
monitor: "رصدگر"
|
||||
jobQueue: "صفکار"
|
||||
perHour: "در ساعت"
|
||||
cpuAndMemory: "پردازنده و حافظه رم"
|
||||
network: "شبکه"
|
||||
disk: "حافظه ذخیره"
|
||||
instanceInfo: "اطلاعات سرور"
|
||||
statistics: "آمار"
|
||||
clearQueue: "پاکسازی صف"
|
||||
clearQueueConfirmTitle: "آیا می خواهید این صف را پاک کنید؟"
|
||||
clearQueueConfirmText: "هر یادداشت تحویل داده نشده در شبکه اجتماعی غیرمتمرکز منتشر نخواهد شد. معمولا این عملیات لازم نیست."
|
||||
clearCachedFiles: "پاک کردن حافظه پنهان"
|
||||
clearCachedFilesConfirm: "آیا می خواهید که تمام پرونده هایی که از سرور های در حافظه پنهان دریافت کردید حذف شود."
|
||||
blockedInstances: "سرور های مسدود شده"
|
||||
blockedInstancesDescription: "نام سرور هایی که می خواهید مسدود کنید را فهرست کنید و با کلید اینتر (هر سرور در اول خط جدید) از هم جدا کنید. سرور های فهرست شده دیگه قابلیت ارتباط گیری با این سرور را ندارند."
|
||||
silencedInstances: "سرور های بیصدا شده"
|
||||
silencedInstancesDescription: "نام سرور هایی که میخواهید بیصدا کنید را فهرست کنید و با کلید اینتر (هر سرور در اول خط جدید) از هم جدا کنید. تمام حساب ها از سرور های لیست شده \"بیصدا\" در نظر گرفته می شود و همهی دنبال کردن ها به کاربر های این سرور به درخواست دنبال کردن تبدیل میشود. این قابلیت بر روی سرور های مسدود شده تاثیری نخواهد داشت."
|
||||
noCustomEmojis: "هیچ شکلکی نیست"
|
||||
remove: "حذف"
|
||||
pinnedNotes: "یادداشت سنجاق شده"
|
||||
@@ -251,7 +227,6 @@ smtpHost: "میزبان"
|
||||
smtpUser: "نام کاربری"
|
||||
smtpPass: "گذرواژه"
|
||||
theKeywordWhenSearchingForCustomEmoji: "این کلمه کلیدی است که هنگام جستجو در میان شکلک های سفارشی به کار گرفته می شود."
|
||||
clearCache: "پاک کردن حافظه پنهان"
|
||||
user: "کاربران"
|
||||
searchByGoogle: "جستجو"
|
||||
file: "پرونده ها"
|
||||
@@ -281,8 +256,6 @@ _widgets:
|
||||
instanceInfo: "اطلاعات سرور"
|
||||
notifications: "اعلان ها"
|
||||
timeline: "نوار زمانی"
|
||||
federation: "شبکهاجتماعیغیرمتمرکز"
|
||||
jobQueue: "صفکار"
|
||||
_userList:
|
||||
chooseList: "انتخاب یک فهرست"
|
||||
_cw:
|
||||
@@ -297,8 +270,6 @@ _exportOrImport:
|
||||
muteList: "سکوت"
|
||||
blockingList: "مسدود کردن"
|
||||
userLists: "فهرست ها"
|
||||
_charts:
|
||||
federation: "شبکهاجتماعیغیرمتمرکز"
|
||||
_notification:
|
||||
youWereFollowed: "شما دنبال شده اید"
|
||||
_types:
|
||||
|
||||
@@ -1040,7 +1040,6 @@ thisPostMayBeAnnoyingHome: "Publier vers le fil principal"
|
||||
thisPostMayBeAnnoyingCancel: "Annuler"
|
||||
thisPostMayBeAnnoyingIgnore: "Publier quand-même"
|
||||
collapseRenotes: "Réduire les renotes déjà vues"
|
||||
collapseRenotesDescription: "Réduire les notes que vous avez renoté ou déjà réagi."
|
||||
internalServerError: "Erreur interne du serveur"
|
||||
internalServerErrorDescription: "Une erreur inattendue s'est produite sur le serveur."
|
||||
copyErrorInfo: "Copier les détails de l’erreur"
|
||||
@@ -1284,7 +1283,6 @@ postForm: "Formulaire de publication"
|
||||
information: "Informations"
|
||||
inMinutes: "min"
|
||||
inDays: "j"
|
||||
createUserSpecifiedNote: "Créer une note directe"
|
||||
widgets: "Widgets"
|
||||
presets: "Préréglage"
|
||||
previewingThemeRestore: "Restaurer"
|
||||
@@ -1760,8 +1758,6 @@ _aboutMisskey:
|
||||
patrons: "Contributeurs"
|
||||
projectMembers: "Membres du projet"
|
||||
_displayOfSensitiveMedia:
|
||||
respect: "Cacher les médias marqués comme sensibles"
|
||||
ignore: "Afficher les médias marqués comme sensibles"
|
||||
force: "Masquer tous les médias"
|
||||
_instanceTicker:
|
||||
none: "Cacher "
|
||||
|
||||
@@ -2084,6 +2084,8 @@ _role:
|
||||
isConditionalRole: "これはコンディショナルロールです。"
|
||||
isPublic: "公開ロール"
|
||||
descriptionOfIsPublic: "ユーザーのプロフィールでこのロールが表示されます。"
|
||||
isPublicDisplayRequired: "非表示を許可しない(常に表示)"
|
||||
descriptionOfIsPublicDisplayRequired: "有効にすると、ユーザーはこの公開ロール/ロールバッジを非表示にできません。"
|
||||
options: "オプション"
|
||||
policies: "ポリシー"
|
||||
baseRole: "ベースロール"
|
||||
@@ -3246,6 +3248,13 @@ _gridComponent:
|
||||
patternNotMatch: "この値は{pattern}のパターンに一致しません"
|
||||
notUnique: "この値は一意である必要があります"
|
||||
|
||||
_roleDisplay:
|
||||
title: "ロールの表示/非表示"
|
||||
description: "自分に割り当てられているロールを確認したり、プロフィールやノート上で表示・公開するロールを選択したりできます。"
|
||||
roleExplorableAlert: "このロールは、管理者により、{link}への表示とロールタイムラインの有効化が設定されています。プロフィール上で非表示にすることはできますが、あなたにこのロールが付与されていることが知られる可能性があります。"
|
||||
displayToggle: "ロール/ロールバッジを表示する"
|
||||
alwaysShownByAdmin: "管理者の設定により非表示にすることはできません。"
|
||||
|
||||
_roleSelectDialog:
|
||||
notSelected: "選択されていません"
|
||||
|
||||
|
||||
@@ -1421,7 +1421,6 @@ append: "加到最后"
|
||||
prepend: "加到最前"
|
||||
urlPreviewSensitiveList: "限制显示缩略图的 URL"
|
||||
urlPreviewSensitiveListDescription: "AND 条件用空格分隔,OR 条件用换行符分隔,正则表达式用斜线包裹。成功匹配则不再显示缩略图。"
|
||||
pixelatedZoom: "像素画放大模式"
|
||||
_imageEditing:
|
||||
_vars:
|
||||
caption: "文件标题"
|
||||
|
||||
@@ -1421,7 +1421,6 @@ append: "加在最後"
|
||||
prepend: "加在前面"
|
||||
urlPreviewSensitiveList: "限制縮圖顯示的 URL"
|
||||
urlPreviewSensitiveListDescription: "以空格指定為 AND,以換行指定為 OR。若以斜線(/)包圍則視為正規表達式。符合條件時,將不再顯示縮圖。"
|
||||
pixelatedZoom: "像素藝術縮放模式"
|
||||
_imageEditing:
|
||||
_vars:
|
||||
caption: "檔案標題"
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class RoleDisplayVisibility1783059479536 {
|
||||
name = 'RoleDisplayVisibility1783059479536';
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "user" ADD "hiddenRoleIds" character varying(32) array NOT NULL DEFAULT '{}'`);
|
||||
await queryRunner.query(`ALTER TABLE "role" ADD "isPublicDisplayRequired" boolean NOT NULL DEFAULT false`);
|
||||
|
||||
// 既存のロールについてはすべて強制表示とする(新規作成分についてはfalse)
|
||||
await queryRunner.query(`UPDATE "role" SET "isPublicDisplayRequired" = true`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "role" DROP COLUMN "isPublicDisplayRequired"`);
|
||||
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "hiddenRoleIds"`);
|
||||
}
|
||||
}
|
||||
@@ -676,6 +676,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
||||
target: values.target,
|
||||
condFormula: values.condFormula,
|
||||
isPublic: values.isPublic,
|
||||
isPublicDisplayRequired: values.isPublicDisplayRequired ?? false,
|
||||
isAdministrator: values.isAdministrator,
|
||||
isModerator: values.isModerator,
|
||||
isExplorable: values.isExplorable,
|
||||
|
||||
@@ -55,6 +55,7 @@ function generateDummyUser(override?: Partial<MiUser>): MiUser {
|
||||
makeNotesHiddenBefore: null,
|
||||
chatScope: 'mutual',
|
||||
emojis: [],
|
||||
hiddenRoleIds: [],
|
||||
score: 0,
|
||||
host: null,
|
||||
inbox: null,
|
||||
|
||||
@@ -64,6 +64,7 @@ export class RoleEntityService {
|
||||
target: role.target,
|
||||
condFormula: role.condFormula,
|
||||
isPublic: role.isPublic,
|
||||
isPublicDisplayRequired: role.isPublicDisplayRequired,
|
||||
isAdministrator: role.isAdministrator,
|
||||
isModerator: role.isModerator,
|
||||
isExplorable: role.isExplorable,
|
||||
@@ -83,5 +84,32 @@ export class RoleEntityService {
|
||||
) {
|
||||
return Promise.all(roles.map(x => this.pack(x, me)));
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packLite(
|
||||
src: MiRole['id'] | MiRole,
|
||||
): Promise<Packed<'RoleLite'>> {
|
||||
const role = typeof src === 'object' ? src : await this.rolesRepository.findOneByOrFail({ id: src });
|
||||
|
||||
return {
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
color: role.color,
|
||||
iconUrl: role.iconUrl,
|
||||
description: role.description,
|
||||
isModerator: role.isModerator,
|
||||
isAdministrator: role.isAdministrator,
|
||||
asBadge: role.asBadge,
|
||||
isPublicDisplayRequired: role.isPublicDisplayRequired,
|
||||
isExplorable: role.isExplorable,
|
||||
displayOrder: role.displayOrder,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public packLiteMany(
|
||||
roles: (MiRole | MiRole['id'])[],
|
||||
) {
|
||||
return Promise.all(roles.map(x => this.packLite(x)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { Promiseable } from '@/misc/prelude/await-all.js';
|
||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
|
||||
import type { MiRole } from '@/models/Role.js';
|
||||
import type { MiLocalUser, MiPartialLocalUser, MiPartialRemoteUser, MiRemoteUser, MiUser } from '@/models/User.js';
|
||||
import {
|
||||
birthdaySchema,
|
||||
@@ -51,6 +52,7 @@ import { ChatService } from '@/core/ChatService.js';
|
||||
import type { OnModuleInit } from '@nestjs/common';
|
||||
import type { NoteEntityService } from './NoteEntityService.js';
|
||||
import type { PageEntityService } from './PageEntityService.js';
|
||||
import type { RoleEntityService } from './RoleEntityService.js';
|
||||
import { toArray } from '@/misc/prelude/array.js';
|
||||
|
||||
const Ajv = _Ajv.default;
|
||||
@@ -88,6 +90,7 @@ export class UserEntityService implements OnModuleInit {
|
||||
private apPersonService: ApPersonService;
|
||||
private noteEntityService: NoteEntityService;
|
||||
private pageEntityService: PageEntityService;
|
||||
private roleEntityService: RoleEntityService;
|
||||
private customEmojiService: CustomEmojiService;
|
||||
private announcementService: AnnouncementService;
|
||||
private roleService: RoleService;
|
||||
@@ -144,6 +147,7 @@ export class UserEntityService implements OnModuleInit {
|
||||
this.apPersonService = this.moduleRef.get('ApPersonService');
|
||||
this.noteEntityService = this.moduleRef.get('NoteEntityService');
|
||||
this.pageEntityService = this.moduleRef.get('PageEntityService');
|
||||
this.roleEntityService = this.moduleRef.get('RoleEntityService');
|
||||
this.customEmojiService = this.moduleRef.get('CustomEmojiService');
|
||||
this.announcementService = this.moduleRef.get('AnnouncementService');
|
||||
this.roleService = this.moduleRef.get('RoleService');
|
||||
@@ -402,6 +406,30 @@ export class UserEntityService implements OnModuleInit {
|
||||
return `${this.config.url}/users/${userId}`;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private prepareRoles<T extends Pick<MiRole, 'id' | 'isPublic' | 'displayOrder' | 'isPublicDisplayRequired'>>(roles: T[], user: MiUser, iAmModerator: boolean, hideByUserPreference = true): T[] {
|
||||
const hiddenRoleIds = new Set(user.hiddenRoleIds);
|
||||
return roles.filter(role => (role.isPublic || iAmModerator) && (role.isPublicDisplayRequired || iAmModerator || !hideByUserPreference || !hiddenRoleIds.has(role.id))).sort((a, b) => b.displayOrder - a.displayOrder);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private sanitizeHiddenRoleIds(roles: Pick<MiRole, 'id' | 'isPublic' | 'isPublicDisplayRequired'>[], user: MiUser): MiRole['id'][] {
|
||||
const hideableRoleIds = new Set(roles
|
||||
.filter(role => role.isPublic && !role.isPublicDisplayRequired)
|
||||
.map(role => role.id));
|
||||
const sanitizedRoleIds: MiRole['id'][] = [];
|
||||
const seenRoleIds = new Set<MiRole['id']>();
|
||||
|
||||
for (const roleId of user.hiddenRoleIds) {
|
||||
if (!hideableRoleIds.has(roleId) || seenRoleIds.has(roleId)) continue;
|
||||
|
||||
sanitizedRoleIds.push(roleId);
|
||||
seenRoleIds.add(roleId);
|
||||
}
|
||||
|
||||
return sanitizedRoleIds;
|
||||
}
|
||||
|
||||
public async pack<S extends 'MeDetailed' | 'UserDetailedNotMe' | 'UserDetailed' | 'UserLite' = 'UserLite'>(
|
||||
src: MiUser['id'] | MiUser,
|
||||
me?: { id: MiUser['id']; } | null | undefined,
|
||||
@@ -425,6 +453,7 @@ export class UserEntityService implements OnModuleInit {
|
||||
const meId = me ? me.id : null;
|
||||
const isMe = meId === user.id;
|
||||
const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false;
|
||||
const userRoles = isDetailed ? this.roleService.getUserRoles(user.id) : null;
|
||||
|
||||
const profile = isDetailed
|
||||
? (opts.userProfile ?? await this.userProfilesRepository.findOneByOrFail({ userId: user.id }))
|
||||
@@ -514,10 +543,9 @@ export class UserEntityService implements OnModuleInit {
|
||||
emojis: this.customEmojiService.populateEmojis(user.emojis, user.host),
|
||||
onlineStatus: this.getOnlineStatus(user),
|
||||
// パフォーマンス上の理由で、明示的に設定しない場合はローカルユーザーのみ取得
|
||||
badgeRoles: (this.meta.showRoleBadgesOfRemoteUsers || user.host == null) ? this.roleService.getUserBadgeRoles(user.id).then((rs) => rs
|
||||
.filter((r) => r.isPublic || iAmModerator)
|
||||
.sort((a, b) => b.displayOrder - a.displayOrder)
|
||||
badgeRoles: (this.meta.showRoleBadgesOfRemoteUsers || user.host == null) ? this.roleService.getUserBadgeRoles(user.id).then((rs) => this.prepareRoles(rs, user, iAmModerator)
|
||||
.map((r) => ({
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
iconUrl: r.iconUrl,
|
||||
displayOrder: r.displayOrder,
|
||||
@@ -560,16 +588,7 @@ export class UserEntityService implements OnModuleInit {
|
||||
followingVisibility: profile!.followingVisibility,
|
||||
chatScope: user.chatScope,
|
||||
canChat: this.roleService.getUserPolicies(user.id).then(r => r.chatAvailability === 'available'),
|
||||
roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({
|
||||
id: role.id,
|
||||
name: role.name,
|
||||
color: role.color,
|
||||
iconUrl: role.iconUrl,
|
||||
description: role.description,
|
||||
isModerator: role.isModerator,
|
||||
isAdministrator: role.isAdministrator,
|
||||
displayOrder: role.displayOrder,
|
||||
}))),
|
||||
roles: userRoles!.then(roles => this.roleEntityService.packLiteMany(this.prepareRoles(roles, user, iAmModerator, !isMe))),
|
||||
memo: memo,
|
||||
moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined,
|
||||
} : {}),
|
||||
@@ -598,6 +617,7 @@ export class UserEntityService implements OnModuleInit {
|
||||
preventAiLearning: profile!.preventAiLearning,
|
||||
isExplorable: user.isExplorable,
|
||||
isDeleted: user.isDeleted,
|
||||
hiddenRoleIds: userRoles!.then(roles => this.sanitizeHiddenRoleIds(roles, user)),
|
||||
twoFactorBackupCodesStock: profile?.twoFactorBackupSecret?.length === 5 ? 'full' : (profile?.twoFactorBackupSecret?.length ?? 0) > 0 ? 'partial' : 'none',
|
||||
hideOnlineStatus: user.hideOnlineStatus,
|
||||
hasUnreadSpecifiedNotes: false, // 後方互換性のため
|
||||
|
||||
@@ -227,6 +227,11 @@ export class MiRole {
|
||||
})
|
||||
public isPublic: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public isPublicDisplayRequired: boolean;
|
||||
|
||||
// trueの場合ユーザー名の横にバッジとして表示
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
|
||||
import { id } from './util/id.js';
|
||||
import { MiDriveFile } from './DriveFile.js';
|
||||
import type { MiRole } from './Role.js';
|
||||
|
||||
@Entity('user')
|
||||
@Index(['usernameLower', 'host'], { unique: true })
|
||||
@@ -229,6 +230,11 @@ export class MiUser {
|
||||
})
|
||||
public emojis: string[];
|
||||
|
||||
@Column('varchar', {
|
||||
length: 32, array: true, default: '{}',
|
||||
})
|
||||
public hiddenRoleIds: MiRole['id'][];
|
||||
|
||||
// チャットを許可する相手
|
||||
// everyone: 誰からでも
|
||||
// followers: フォロワーのみ
|
||||
|
||||
@@ -369,6 +369,21 @@ export const packedRoleLiteSchema = {
|
||||
optional: false, nullable: false,
|
||||
example: false,
|
||||
},
|
||||
asBadge: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
example: false,
|
||||
},
|
||||
isPublicDisplayRequired: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
example: false,
|
||||
},
|
||||
isExplorable: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
example: false,
|
||||
},
|
||||
displayOrder: {
|
||||
type: 'integer',
|
||||
optional: false, nullable: false,
|
||||
@@ -412,6 +427,11 @@ export const packedRoleSchema = {
|
||||
optional: false, nullable: false,
|
||||
example: false,
|
||||
},
|
||||
isPublicDisplayRequired: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
example: false,
|
||||
},
|
||||
isExplorable: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
|
||||
@@ -176,6 +176,11 @@ export const packedUserLiteSchema = {
|
||||
type: 'object',
|
||||
nullable: false, optional: false,
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
nullable: false, optional: false,
|
||||
format: 'id',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
nullable: false, optional: false,
|
||||
@@ -511,6 +516,15 @@ export const packedMeDetailedOnlySchema = {
|
||||
type: 'boolean',
|
||||
nullable: false, optional: false,
|
||||
},
|
||||
hiddenRoleIds: {
|
||||
type: 'array',
|
||||
nullable: false, optional: false,
|
||||
items: {
|
||||
type: 'string',
|
||||
nullable: false, optional: false,
|
||||
format: 'id',
|
||||
},
|
||||
},
|
||||
twoFactorBackupCodesStock: {
|
||||
type: 'string',
|
||||
enum: ['full', 'partial', 'none'],
|
||||
|
||||
@@ -32,6 +32,7 @@ export const paramDef = {
|
||||
target: { type: 'string', enum: ['manual', 'conditional'] },
|
||||
condFormula: { type: 'object' },
|
||||
isPublic: { type: 'boolean' },
|
||||
isPublicDisplayRequired: { type: 'boolean', default: false },
|
||||
isModerator: { type: 'boolean' },
|
||||
isAdministrator: { type: 'boolean' },
|
||||
isExplorable: { type: 'boolean', default: false }, // optional for backward compatibility
|
||||
|
||||
@@ -37,6 +37,7 @@ export const paramDef = {
|
||||
target: { type: 'string', enum: ['manual', 'conditional'] },
|
||||
condFormula: { type: 'object' },
|
||||
isPublic: { type: 'boolean' },
|
||||
isPublicDisplayRequired: { type: 'boolean' },
|
||||
isModerator: { type: 'boolean' },
|
||||
isAdministrator: { type: 'boolean' },
|
||||
isExplorable: { type: 'boolean' },
|
||||
@@ -75,6 +76,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
target: ps.target,
|
||||
condFormula: ps.condFormula,
|
||||
isPublic: ps.isPublic,
|
||||
isPublicDisplayRequired: ps.isPublicDisplayRequired,
|
||||
isModerator: ps.isModerator,
|
||||
isAdministrator: ps.isAdministrator,
|
||||
isExplorable: ps.isExplorable,
|
||||
|
||||
@@ -191,6 +191,12 @@ export const paramDef = {
|
||||
followingVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
|
||||
followersVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
|
||||
chatScope: { type: 'string', enum: ['everyone', 'followers', 'following', 'mutual', 'none'] },
|
||||
hiddenRoleIds: {
|
||||
type: 'array',
|
||||
maxItems: 256,
|
||||
uniqueItems: true,
|
||||
items: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
mutedWords: muteWords,
|
||||
hardMutedWords: muteWords,
|
||||
@@ -293,6 +299,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
if (ps.followingVisibility !== undefined) profileUpdates.followingVisibility = ps.followingVisibility;
|
||||
if (ps.followersVisibility !== undefined) profileUpdates.followersVisibility = ps.followersVisibility;
|
||||
if (ps.chatScope !== undefined) updates.chatScope = ps.chatScope;
|
||||
if (ps.hiddenRoleIds !== undefined) {
|
||||
const roles = await this.roleService.getUserRoles(user.id);
|
||||
const allowedRoleIds = new Set(roles.filter(role => role.isPublic && !role.isPublicDisplayRequired).map(role => role.id));
|
||||
const hiddenRoleIds: string[] = [];
|
||||
const seenRoleIds = new Set<string>();
|
||||
|
||||
for (const roleId of ps.hiddenRoleIds) {
|
||||
if (seenRoleIds.has(roleId) || !allowedRoleIds.has(roleId)) continue;
|
||||
seenRoleIds.add(roleId);
|
||||
hiddenRoleIds.push(roleId);
|
||||
}
|
||||
|
||||
updates.hiddenRoleIds = hiddenRoleIds;
|
||||
}
|
||||
|
||||
function checkMuteWordCount(mutedWords: (string[] | string)[], limit: number) {
|
||||
const count = (arr: (string[] | string)[]) => {
|
||||
|
||||
@@ -10,9 +10,9 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
import { GetterService } from '@/server/api/GetterService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { MiMeta } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
@@ -83,12 +83,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
throw new ApiError(meta.errors.cannotTranslateInvisibleNote);
|
||||
}
|
||||
|
||||
let text = note.text ?? '';
|
||||
if (note.cw != null) {
|
||||
text = `${note.cw}\n-----\n${text}`;
|
||||
}
|
||||
|
||||
if (text.trim() === '') {
|
||||
if (note.text == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -100,7 +95,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||
if (targetLang.includes('-')) targetLang = targetLang.split('-')[0];
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.append('text', text);
|
||||
params.append('text', note.text);
|
||||
params.append('target_lang', targetLang);
|
||||
|
||||
const endpoint = this.serverSettings.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate';
|
||||
|
||||
@@ -128,6 +128,7 @@ describe('ユーザー', () => {
|
||||
preventAiLearning: user.preventAiLearning,
|
||||
isExplorable: user.isExplorable,
|
||||
isDeleted: user.isDeleted,
|
||||
hiddenRoleIds: user.hiddenRoleIds ?? [],
|
||||
twoFactorBackupCodesStock: user.twoFactorBackupCodesStock,
|
||||
hideOnlineStatus: user.hideOnlineStatus,
|
||||
hasUnreadSpecifiedNotes: user.hasUnreadSpecifiedNotes,
|
||||
@@ -660,12 +661,16 @@ describe('ユーザー', () => {
|
||||
description: rolePublic.description,
|
||||
isModerator: rolePublic.isModerator,
|
||||
isAdministrator: rolePublic.isAdministrator,
|
||||
asBadge: rolePublic.asBadge,
|
||||
isPublicDisplayRequired: rolePublic.isPublicDisplayRequired,
|
||||
isExplorable: rolePublic.isExplorable,
|
||||
displayOrder: rolePublic.displayOrder,
|
||||
}]);
|
||||
});
|
||||
test('を取得することができ、バッヂロールがセットされていること', async () => {
|
||||
const response = await successfulApiCall({ endpoint: 'users/show', parameters: { userId: userRoleBadge.id }, user: alice });
|
||||
assert.deepStrictEqual(response.badgeRoles, [{
|
||||
id: roleBadge.id,
|
||||
name: roleBadge.name,
|
||||
iconUrl: roleBadge.iconUrl,
|
||||
displayOrder: roleBadge.displayOrder,
|
||||
@@ -678,9 +683,42 @@ describe('ユーザー', () => {
|
||||
description: roleBadge.description,
|
||||
isModerator: roleBadge.isModerator,
|
||||
isAdministrator: roleBadge.isAdministrator,
|
||||
asBadge: roleBadge.asBadge,
|
||||
isPublicDisplayRequired: roleBadge.isPublicDisplayRequired,
|
||||
isExplorable: roleBadge.isExplorable,
|
||||
displayOrder: roleBadge.displayOrder,
|
||||
}]);
|
||||
});
|
||||
test('i/updateでhiddenRoleIdsが保存時に表示可能な割り当て済みロールだけへsanitizeされること', async () => {
|
||||
const user = await signup({ username: 'userHiddenRoles' });
|
||||
const visibleRole = await role(root, { isPublic: true, name: 'Hideable Role' });
|
||||
const secondVisibleRole = await role(root, { isPublic: true, name: 'Second Hideable Role' });
|
||||
const privateRole = await role(root, { isPublic: false, name: 'Private Role' });
|
||||
const forcedRole = await role(root, { isPublic: true, isPublicDisplayRequired: true, name: 'Forced Role' });
|
||||
const unassignedRole = await role(root, { isPublic: true, name: 'Unassigned Role' });
|
||||
|
||||
await api('admin/roles/assign', { userId: user.id, roleId: visibleRole.id }, root);
|
||||
await api('admin/roles/assign', { userId: user.id, roleId: secondVisibleRole.id }, root);
|
||||
await api('admin/roles/assign', { userId: user.id, roleId: privateRole.id }, root);
|
||||
await api('admin/roles/assign', { userId: user.id, roleId: forcedRole.id }, root);
|
||||
|
||||
const response = await successfulApiCall({
|
||||
endpoint: 'i/update',
|
||||
parameters: {
|
||||
hiddenRoleIds: [
|
||||
secondVisibleRole.id,
|
||||
'unknownroleid',
|
||||
unassignedRole.id,
|
||||
privateRole.id,
|
||||
forcedRole.id,
|
||||
visibleRole.id,
|
||||
],
|
||||
},
|
||||
user,
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(response.hiddenRoleIds, [secondVisibleRole.id, visibleRole.id]);
|
||||
});
|
||||
test('をID指定のリスト形式で取得することができる(空)', async () => {
|
||||
const parameters = { userIds: [] };
|
||||
const response = await successfulApiCall({ endpoint: 'users/show', parameters, user: alice });
|
||||
|
||||
@@ -14,7 +14,10 @@ import { genAidx } from '@/misc/id/aidx.js';
|
||||
import {
|
||||
BlockingsRepository,
|
||||
FollowingsRepository, FollowRequestsRepository,
|
||||
MiRole,
|
||||
MiUserProfile, MutingsRepository, RenoteMutingsRepository,
|
||||
RoleAssignmentsRepository,
|
||||
RolesRepository,
|
||||
UserMemoRepository,
|
||||
UserProfilesRepository,
|
||||
UsersRepository,
|
||||
@@ -27,6 +30,7 @@ import { PageEntityService } from '@/core/entities/PageEntityService.js';
|
||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
import { AnnouncementService } from '@/core/AnnouncementService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { RoleEntityService } from '@/core/entities/RoleEntityService.js';
|
||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
@@ -67,6 +71,13 @@ describe('UserEntityService', () => {
|
||||
let blockingRepository: BlockingsRepository;
|
||||
let mutingRepository: MutingsRepository;
|
||||
let renoteMutingsRepository: RenoteMutingsRepository;
|
||||
let rolesRepository: RolesRepository;
|
||||
let roleAssignmentsRepository: RoleAssignmentsRepository;
|
||||
let roleService: RoleService;
|
||||
type RoleServiceCacheController = {
|
||||
rolesCache: { delete(): void };
|
||||
roleAssignmentByUserIdCache: { delete(userId: MiUser['id']): void };
|
||||
};
|
||||
|
||||
async function createUser(userData: Partial<MiUser> = {}, profileData: Partial<MiUserProfile> = {}) {
|
||||
const un = secureRndstr(16);
|
||||
@@ -87,6 +98,32 @@ describe('UserEntityService', () => {
|
||||
return user;
|
||||
}
|
||||
|
||||
async function createRole(roleData: Partial<MiRole> = {}) {
|
||||
const role = await rolesRepository
|
||||
.insert({
|
||||
id: genAidx(Date.now()),
|
||||
updatedAt: new Date(),
|
||||
lastUsedAt: new Date(),
|
||||
name: '',
|
||||
description: '',
|
||||
...roleData,
|
||||
})
|
||||
.then(x => rolesRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
(roleService as unknown as RoleServiceCacheController).rolesCache.delete();
|
||||
return role;
|
||||
}
|
||||
|
||||
async function assignRole(user: MiUser, role: MiRole) {
|
||||
await roleAssignmentsRepository.insert({
|
||||
id: genAidx(Date.now()),
|
||||
userId: user.id,
|
||||
roleId: role.id,
|
||||
});
|
||||
|
||||
(roleService as unknown as RoleServiceCacheController).roleAssignmentByUserIdCache.delete(user.id);
|
||||
}
|
||||
|
||||
async function memo(writer: MiUser, target: MiUser, memo: string) {
|
||||
await userMemosRepository.insert({
|
||||
id: genAidx(Date.now()),
|
||||
@@ -146,6 +183,7 @@ describe('UserEntityService', () => {
|
||||
ApPersonService,
|
||||
NoteEntityService,
|
||||
PageEntityService,
|
||||
RoleEntityService,
|
||||
CustomEmojiService,
|
||||
AnnouncementService,
|
||||
RoleService,
|
||||
@@ -196,6 +234,9 @@ describe('UserEntityService', () => {
|
||||
blockingRepository = app.get<BlockingsRepository>(DI.blockingsRepository);
|
||||
mutingRepository = app.get<MutingsRepository>(DI.mutingsRepository);
|
||||
renoteMutingsRepository = app.get<RenoteMutingsRepository>(DI.renoteMutingsRepository);
|
||||
rolesRepository = app.get<RolesRepository>(DI.rolesRepository);
|
||||
roleAssignmentsRepository = app.get<RoleAssignmentsRepository>(DI.roleAssignmentsRepository);
|
||||
roleService = app.get<RoleService>(RoleService);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
@@ -249,6 +290,60 @@ describe('UserEntityService', () => {
|
||||
expect(actual.achievements).toEqual(achievements);
|
||||
});
|
||||
|
||||
test('MeDetailed hiddenRoleIds are sanitized to assigned public non-forced roles in stored order', async() => {
|
||||
const me = await createUser();
|
||||
const visibleRole = await createRole({ name: 'visible', isPublic: true });
|
||||
const secondVisibleRole = await createRole({ name: 'visible2', isPublic: true });
|
||||
const privateRole = await createRole({ name: 'private', isPublic: false });
|
||||
const forcedRole = await createRole({ name: 'forced', isPublic: true, isPublicDisplayRequired: true });
|
||||
const unassignedRole = await createRole({ name: 'unassigned', isPublic: true });
|
||||
|
||||
await assignRole(me, visibleRole);
|
||||
await assignRole(me, secondVisibleRole);
|
||||
await assignRole(me, privateRole);
|
||||
await assignRole(me, forcedRole);
|
||||
await usersRepository.update(me.id, {
|
||||
hiddenRoleIds: [
|
||||
secondVisibleRole.id,
|
||||
'unknownroleid',
|
||||
unassignedRole.id,
|
||||
privateRole.id,
|
||||
forcedRole.id,
|
||||
visibleRole.id,
|
||||
secondVisibleRole.id,
|
||||
],
|
||||
});
|
||||
const updatedMe = await usersRepository.findOneByOrFail({ id: me.id });
|
||||
|
||||
const actual = await service.pack(updatedMe, updatedMe, { schema: 'MeDetailed' });
|
||||
|
||||
expect(actual.hiddenRoleIds).toEqual([secondVisibleRole.id, visibleRole.id]);
|
||||
});
|
||||
|
||||
test('UserDetailed filters hidden display roles for normal viewers, preserves forced roles, and bypasses for moderators', async() => {
|
||||
const [viewer, moderator, target] = await Promise.all([createUser(), createUser(), createUser()]);
|
||||
const hiddenRole = await createRole({ name: 'hidden', isPublic: true, asBadge: true, displayOrder: 30 });
|
||||
const visibleRole = await createRole({ name: 'visible', isPublic: true, asBadge: true, displayOrder: 20 });
|
||||
const forcedRole = await createRole({ name: 'forced', isPublic: true, isPublicDisplayRequired: true, asBadge: true, displayOrder: 10 });
|
||||
const moderatorRole = await createRole({ name: 'moderator', isModerator: true });
|
||||
|
||||
await assignRole(target, hiddenRole);
|
||||
await assignRole(target, visibleRole);
|
||||
await assignRole(target, forcedRole);
|
||||
await assignRole(moderator, moderatorRole);
|
||||
await usersRepository.update(target.id, { hiddenRoleIds: [hiddenRole.id, forcedRole.id] });
|
||||
const updatedTarget = await usersRepository.findOneByOrFail({ id: target.id });
|
||||
|
||||
const normalView = await service.pack(updatedTarget, viewer, { schema: 'UserDetailed' });
|
||||
const moderatorView = await service.pack(updatedTarget, moderator, { schema: 'UserDetailed' });
|
||||
|
||||
expect(normalView.roles.map(role => role.id)).toEqual([visibleRole.id, forcedRole.id]);
|
||||
expect(normalView.badgeRoles?.map(role => role.id)).toEqual([visibleRole.id, forcedRole.id]);
|
||||
expect(normalView.badgeRoles?.every(role => typeof role.id === 'string')).toBe(true);
|
||||
expect(moderatorView.roles.map(role => role.id)).toEqual([hiddenRole.id, visibleRole.id, forcedRole.id]);
|
||||
expect(moderatorView.badgeRoles?.map(role => role.id)).toEqual([hiddenRole.id, visibleRole.id, forcedRole.id]);
|
||||
});
|
||||
|
||||
test('alsoKnownAs as string does not throw', async () => {
|
||||
const me = await createUser();
|
||||
const who = await createUser();
|
||||
|
||||
@@ -357,6 +357,7 @@ export function role(params: {
|
||||
isPublic?: boolean,
|
||||
isExplorable?: boolean,
|
||||
asBadge?: boolean,
|
||||
isPublicDisplayRequired?: boolean,
|
||||
canEditMembersByModerator?: boolean,
|
||||
usersCount?: number,
|
||||
}, seed?: string): entities.Role {
|
||||
@@ -380,6 +381,7 @@ export function role(params: {
|
||||
isPublic: params.isPublic ?? true,
|
||||
isExplorable: params.isExplorable ?? true,
|
||||
asBadge: params.asBadge ?? true,
|
||||
isPublicDisplayRequired: params.isPublicDisplayRequired ?? false,
|
||||
canEditMembersByModerator: params.canEditMembersByModerator ?? false,
|
||||
usersCount: params.usersCount ?? 10,
|
||||
preserveAssignmentOnMoveAccount: false,
|
||||
|
||||
@@ -69,8 +69,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div :class="$style.noteHeaderUsername">
|
||||
<MkAcct :user="appearNote.user"/>
|
||||
</div>
|
||||
<div v-if="appearNote.user.badgeRoles" :class="$style.noteHeaderBadgeRoles">
|
||||
<img v-for="(role, i) in appearNote.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.noteHeaderBadgeRole" :src="role.iconUrl!"/>
|
||||
<div v-if="badgeRoles" :class="$style.noteHeaderBadgeRoles">
|
||||
<img v-for="role in badgeRoles" :key="role.id" v-tooltip="role.name" :class="$style.noteHeaderBadgeRole" :src="role.iconUrl!"/>
|
||||
</div>
|
||||
</div>
|
||||
<MkInstanceTicker v-if="showTicker" :host="appearNote.user.host" :instance="appearNote.user.instance"/>
|
||||
@@ -251,6 +251,7 @@ import { Paginator } from '@/utility/paginator.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import number from '@/filters/number.js';
|
||||
import { DI } from '@/di.js';
|
||||
import { $i } from '@/i.js';
|
||||
import type { Keymap } from '@/utility/hotkey.js';
|
||||
|
||||
// コンポーネント外部の依存関係
|
||||
@@ -332,6 +333,14 @@ provide(DI.mfmEmojiReactCallback, reactViaMfmEmoji);
|
||||
// MkNoteDetailed固有
|
||||
const tab = ref(props.initialTab);
|
||||
const reactionTabType = ref<string | null>(null);
|
||||
const badgeRoles = computed(() => {
|
||||
const roles = appearNote.user.badgeRoles;
|
||||
if (roles == null || $i == null || $i.id !== appearNote.userId) return roles;
|
||||
|
||||
// 自分のノートを自分で見た場合レスポンスに非表示ロールも含まれるので、別途除外する必要がある
|
||||
const hiddenRoleIds = new Set($i.hiddenRoleIds ?? []);
|
||||
return roles.filter(role => !hiddenRoleIds.has(role.id));
|
||||
});
|
||||
|
||||
const renotesPaginator = markRaw(new Paginator('notes/renotes', {
|
||||
limit: 10,
|
||||
|
||||
@@ -13,8 +13,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkA>
|
||||
<div v-if="note.user.isBot" :class="$style.isBot">bot</div>
|
||||
<div :class="$style.username"><MkAcct :user="note.user"/></div>
|
||||
<div v-if="note.user.badgeRoles" :class="$style.badgeRoles">
|
||||
<img v-for="(role, i) in note.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl!"/>
|
||||
<div v-if="badgeRoles" :class="$style.badgeRoles">
|
||||
<img v-for="role in badgeRoles" :key="role.id" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl!"/>
|
||||
</div>
|
||||
<div :class="$style.info">
|
||||
<div v-if="mock">
|
||||
@@ -35,17 +35,26 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { inject } from 'vue';
|
||||
import { computed, inject } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { $i } from '@/i.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { notePage } from '@/filters/note.js';
|
||||
import { userPage } from '@/filters/user.js';
|
||||
import { DI } from '@/di.js';
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
}>();
|
||||
|
||||
const badgeRoles = computed(() => {
|
||||
const roles = props.note.user.badgeRoles;
|
||||
if (roles == null || $i == null || $i.id !== props.note.userId) return roles;
|
||||
|
||||
// 自分のプロフィールを自分で見た場合レスポンスに非表示ロールも含まれるので、別途除外する必要がある
|
||||
const hiddenRoleIds = new Set(($i.hiddenRoleIds) ?? []);
|
||||
return roles.filter(role => !hiddenRoleIds.has(role.id));
|
||||
});
|
||||
const mock = inject(DI.mock, false);
|
||||
</script>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="_selectable">
|
||||
<div>
|
||||
<XValue :value="value" :collapsed="false"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -37,7 +37,7 @@ const props = defineProps<{
|
||||
id?: string;
|
||||
}>();
|
||||
|
||||
type RoleLike = Pick<Misskey.entities.Role, 'name' | 'description' | 'isAdministrator' | 'isModerator' | 'color' | 'iconUrl' | 'target' | 'isPublic' | 'isExplorable' | 'asBadge' | 'canEditMembersByModerator' | 'displayOrder' | 'preserveAssignmentOnMoveAccount'> & {
|
||||
type RoleLike = Pick<Misskey.entities.Role, 'name' | 'description' | 'isAdministrator' | 'isModerator' | 'color' | 'iconUrl' | 'target' | 'isPublic' | 'isPublicDisplayRequired' | 'isExplorable' | 'asBadge' | 'canEditMembersByModerator' | 'displayOrder' | 'preserveAssignmentOnMoveAccount'> & {
|
||||
condFormula: any;
|
||||
policies: any;
|
||||
};
|
||||
@@ -62,6 +62,7 @@ if (props.id) {
|
||||
target: 'manual',
|
||||
condFormula: { id: genId(), type: 'isRemote' },
|
||||
isPublic: false,
|
||||
isPublicDisplayRequired: false,
|
||||
isExplorable: false,
|
||||
asBadge: false,
|
||||
canEditMembersByModerator: false,
|
||||
|
||||
@@ -62,7 +62,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<template #caption>{{ i18n.ts._role.descriptionOfIsPublic }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<MkSwitch v-model="role.asBadge" :readonly="readonly">
|
||||
<MkSwitch v-model="role.isPublicDisplayRequired" :disabled="!role.isPublic" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.isPublicDisplayRequired }}</template>
|
||||
<template #caption>{{ i18n.ts._role.descriptionOfIsPublicDisplayRequired }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<MkSwitch v-model="role.asBadge" :disabled="!role.isPublic" :readonly="readonly">
|
||||
<template #label>{{ i18n.ts._role.asBadge }}</template>
|
||||
<template #caption>{{ i18n.ts._role.descriptionOfAsBadge }}</template>
|
||||
</MkSwitch>
|
||||
@@ -110,7 +115,7 @@ import { instance } from '@/instance.js';
|
||||
import { deepClone } from '@/utility/clone.js';
|
||||
import type { PolicyMeta } from './roles.policy-editor.vue';
|
||||
|
||||
type RoleLike = Pick<Misskey.entities.Role, 'name' | 'description' | 'isAdministrator' | 'isModerator' | 'color' | 'iconUrl' | 'target' | 'isPublic' | 'isExplorable' | 'asBadge' | 'canEditMembersByModerator' | 'displayOrder' | 'preserveAssignmentOnMoveAccount'> & {
|
||||
type RoleLike = Pick<Misskey.entities.Role, 'name' | 'description' | 'isAdministrator' | 'isModerator' | 'color' | 'iconUrl' | 'target' | 'isPublic' | 'isPublicDisplayRequired' | 'isExplorable' | 'asBadge' | 'canEditMembersByModerator' | 'displayOrder' | 'preserveAssignmentOnMoveAccount'> & {
|
||||
id?: Misskey.entities.Role['id'] | null;
|
||||
condFormula: any;
|
||||
policies: any;
|
||||
@@ -200,6 +205,7 @@ const save = throttle(100, () => {
|
||||
isAdministrator: role.value.isAdministrator,
|
||||
isModerator: role.value.isModerator,
|
||||
isPublic: role.value.isPublic,
|
||||
isPublicDisplayRequired: role.value.isPublicDisplayRequired,
|
||||
isExplorable: role.value.isExplorable,
|
||||
asBadge: role.value.asBadge,
|
||||
canEditMembersByModerator: role.value.canEditMembersByModerator,
|
||||
|
||||
@@ -49,17 +49,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkFolder>
|
||||
</SearchMarker>
|
||||
|
||||
<SearchMarker :keywords="['roles']">
|
||||
<MkFolder>
|
||||
<template #icon><SearchIcon><i class="ti ti-badges"></i></SearchIcon></template>
|
||||
<template #label><SearchLabel>{{ i18n.ts.rolesAssignedToMe }}</SearchLabel></template>
|
||||
|
||||
<div class="_gaps_s">
|
||||
<MkRolePreview v-for="role in $i.roles" :key="role.id" :role="role" :forModeration="false"/>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</SearchMarker>
|
||||
|
||||
<SearchMarker :keywords="['account', 'move', 'migration']">
|
||||
<MkFolder>
|
||||
<template #icon><SearchIcon><i class="ti ti-plane"></i></SearchIcon></template>
|
||||
@@ -126,8 +115,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div class="_gaps_s">
|
||||
<!-- ページ移動したのでリンクを残してある。そのうち消しても良いかも -->
|
||||
<FormLink to="/settings/roles">
|
||||
<template #icon><i class="ti ti-badges"></i></template>
|
||||
{{ i18n.ts.rolesAssignedToMe }}
|
||||
</FormLink>
|
||||
|
||||
<FormLink to="/registry"><template #icon><i class="ti ti-adjustments"></i></template>{{ i18n.ts.registry }}</FormLink>
|
||||
<FormLink to="/registry"><template #icon><i class="ti ti-adjustments"></i></template>{{ i18n.ts.registry }}</FormLink>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
@@ -166,7 +162,6 @@ import { i18n } from '@/i18n.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import FormSection from '@/components/form/section.vue';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import MkRolePreview from '@/components/MkRolePreview.vue';
|
||||
import { signout } from '@/signout.js';
|
||||
import { hideAllTips as _hideAllTips, resetAllTips as _resetAllTips } from '@/tips.js';
|
||||
import { suggestReload } from '@/utility/reload-suggest.js';
|
||||
|
||||
@@ -72,6 +72,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</MkSwitch>
|
||||
</SearchMarker>
|
||||
|
||||
<FormLink to="/settings/roles">
|
||||
<template #icon><i class="ti ti-badges"></i></template>
|
||||
<SearchLabel>{{ i18n.ts._roleDisplay.title }}</SearchLabel>
|
||||
</FormLink>
|
||||
|
||||
<SearchMarker :keywords="['chat']">
|
||||
<FormSection>
|
||||
<template #label><SearchLabel>{{ i18n.ts.directMessage }}</SearchLabel></template>
|
||||
@@ -223,6 +228,7 @@ import { instance } from '@/instance.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import FormSlot from '@/components/form/slot.vue';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import { formatDateTimeString } from '@/utility/format-time-string.js';
|
||||
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<SearchMarker path="/settings/roles" :label="i18n.ts.roles" :keywords="['role', 'badge']" icon="ti ti-badges">
|
||||
<div class="_gaps_m">
|
||||
<MkFeatureBanner icon="/fluent-emoji/1f3f7.png" color="#ffbf00">
|
||||
<SearchText>{{ i18n.ts._roleDisplay.description }}</SearchText>
|
||||
</MkFeatureBanner>
|
||||
<div class="_gaps_s">
|
||||
<MkResult v-if="roleDisplayRoles.length === 0" type="empty"/>
|
||||
<div v-for="role in roleDisplayRoles" :key="role.id" class="_panel _gaps" :class="$style.roleItem">
|
||||
<MkRolePreview :role="role" :forModeration="false"/>
|
||||
<MkSwitch
|
||||
:modelValue="isRoleDisplayShown(role)"
|
||||
:disabled="role.isPublicDisplayRequired"
|
||||
@update:modelValue="value => updateRoleDisplay(role, value)"
|
||||
>
|
||||
<template #label>{{ i18n.ts._roleDisplay.displayToggle }}</template>
|
||||
<template v-if="role.isPublicDisplayRequired" #caption>{{ i18n.ts._roleDisplay.alwaysShownByAdmin }}</template>
|
||||
<template v-else-if="role.isExplorable" #caption>
|
||||
<i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> <I18n :src="i18n.ts._roleDisplay.roleExplorableAlert">
|
||||
<template #link>
|
||||
<MkA class="_link" :to="`/roles/${role.id}`">{{ i18n.ts.explore }}</MkA>
|
||||
</template>
|
||||
</I18n>
|
||||
</template>
|
||||
</MkSwitch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SearchMarker>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import type * as Misskey from 'misskey-js';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkFeatureBanner from '@/components/MkFeatureBanner.vue';
|
||||
import MkRolePreview from '@/components/MkRolePreview.vue';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { ensureSignin } from '@/i.js';
|
||||
import { definePage } from '@/page.js';
|
||||
import { updateCurrentAccount } from '@/accounts.js';
|
||||
|
||||
const $i = ensureSignin();
|
||||
|
||||
const hiddenRoleIds = ref([...getHiddenRoleIds($i)]);
|
||||
const roleDisplayRoles = computed(() => $i.roles);
|
||||
|
||||
function getHiddenRoleIds(user: { hiddenRoleIds?: string[] }): string[] {
|
||||
return user.hiddenRoleIds ?? [];
|
||||
}
|
||||
|
||||
function isRoleDisplayShown(role: Misskey.entities.RoleLite): boolean {
|
||||
return role.isPublicDisplayRequired === true || !hiddenRoleIds.value.includes(role.id);
|
||||
}
|
||||
|
||||
async function updateRoleDisplay(role: Misskey.entities.RoleLite, visible: boolean) {
|
||||
if (role.isPublicDisplayRequired === true) return;
|
||||
|
||||
const nextHiddenRoleIds = new Set(hiddenRoleIds.value);
|
||||
if (visible) {
|
||||
nextHiddenRoleIds.delete(role.id);
|
||||
} else {
|
||||
nextHiddenRoleIds.add(role.id);
|
||||
}
|
||||
|
||||
const nextIds = roleDisplayRoles.value
|
||||
.filter(role => role.isPublicDisplayRequired !== true && nextHiddenRoleIds.has(role.id))
|
||||
.map(role => role.id);
|
||||
|
||||
const updated = await misskeyApi('i/update', {
|
||||
hiddenRoleIds: nextIds,
|
||||
});
|
||||
|
||||
updateCurrentAccount(updated);
|
||||
hiddenRoleIds.value = [...getHiddenRoleIds(updated)];
|
||||
}
|
||||
|
||||
definePage(() => ({
|
||||
title: i18n.ts.roles,
|
||||
icon: 'ti ti-badges',
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.roleItem {
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -53,8 +53,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<div><MkSparkle><Mfm :plain="true" :text="user.followedMessage" :author="user" class="_selectable"/></MkSparkle></div>
|
||||
</MkFukidashi>
|
||||
</div>
|
||||
<div v-if="user.roles.length > 0" class="roles">
|
||||
<span v-for="role in user.roles" :key="role.id" v-tooltip="role.description" class="role" :style="{ '--color': role.color ?? '' }">
|
||||
<div v-if="visibleProfileRoles.length > 0" class="roles">
|
||||
<span v-for="role in visibleProfileRoles" :key="role.id" v-tooltip="role.description" class="role" :style="{ '--color': role.color ?? '' }">
|
||||
<MkA v-adaptive-bg :to="`/roles/${role.id}`">
|
||||
<img v-if="role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="role.iconUrl"/>
|
||||
{{ role.name }}
|
||||
@@ -222,6 +222,14 @@ const emit = defineEmits<{
|
||||
const router = useRouter();
|
||||
|
||||
const user = ref(props.user);
|
||||
const visibleProfileRoles = computed(() => {
|
||||
const roles = user.value.roles;
|
||||
if ($i == null || $i.id !== user.value.id) return roles;
|
||||
|
||||
// 自分のプロフィールを自分で見た場合レスポンスに非表示ロールも含まれるので、別途除外する必要がある
|
||||
const hiddenRoleIds = new Set($i.hiddenRoleIds ?? []);
|
||||
return roles.filter(role => role.isPublicDisplayRequired === true || !hiddenRoleIds.has(role.id));
|
||||
});
|
||||
const narrow = ref<null | boolean>(null);
|
||||
const rootEl = useTemplateRef('rootEl');
|
||||
const bannerEl = useTemplateRef('bannerEl');
|
||||
|
||||
@@ -88,6 +88,10 @@ export const ROUTE_DEF = [{
|
||||
path: '/privacy',
|
||||
name: 'privacy',
|
||||
component: page(() => import('@/pages/settings/privacy.vue')),
|
||||
}, {
|
||||
path: '/roles',
|
||||
name: 'roles',
|
||||
component: page(() => import('@/pages/settings/roles.vue')),
|
||||
}, {
|
||||
path: '/emoji-palette',
|
||||
name: 'emoji-palette',
|
||||
|
||||
@@ -303,18 +303,12 @@ export function getNoteMenu(props: {
|
||||
|
||||
async function translate(): Promise<void> {
|
||||
if (props.translation.value != null) return;
|
||||
|
||||
let text = appearNote.text ?? '';
|
||||
if (appearNote.cw != null) {
|
||||
text = `${appearNote.cw}\n-----\n${text}`;
|
||||
}
|
||||
|
||||
if (prefer.s['experimental.enableWebTranslatorApi'] && isInBrowserTranslationAvailable && text.trim() !== '') {
|
||||
if (prefer.s['experimental.enableWebTranslatorApi'] && isInBrowserTranslationAvailable && appearNote.text != null) {
|
||||
props.translating.value = true;
|
||||
try {
|
||||
// @ts-expect-error 実験的なAPIなので型定義がない
|
||||
const detector = await LanguageDetector.create();
|
||||
const langResult = await detector.detect(text);
|
||||
const langResult = await detector.detect(appearNote.text);
|
||||
let localStorageLang = miLocalStorage.getItem('lang');
|
||||
if (localStorageLang != null) {
|
||||
localStorageLang = localStorageLang.split('-')[0];
|
||||
@@ -324,7 +318,7 @@ export function getNoteMenu(props: {
|
||||
if (langResult[0]?.detectedLanguage === localStorageLang || langResult[0]?.detectedLanguage === navigator.language) {
|
||||
props.translation.value = {
|
||||
sourceLang: langResult[0]?.detectedLanguage ?? 'unknown',
|
||||
text: text,
|
||||
text: appearNote.text,
|
||||
};
|
||||
return;
|
||||
}
|
||||
@@ -334,7 +328,7 @@ export function getNoteMenu(props: {
|
||||
sourceLanguage: langResult[0]?.detectedLanguage,
|
||||
targetLanguage: localStorageLang ?? navigator.language,
|
||||
});
|
||||
const translated = await translator.translate(text);
|
||||
const translated = await translator.translate(appearNote.text);
|
||||
props.translation.value = {
|
||||
sourceLang: langResult[0]?.detectedLanguage ?? 'unknown',
|
||||
text: translated,
|
||||
|
||||
@@ -8030,6 +8030,14 @@ export interface Locale extends ILocale {
|
||||
* ユーザーのプロフィールでこのロールが表示されます。
|
||||
*/
|
||||
"descriptionOfIsPublic": string;
|
||||
/**
|
||||
* 非表示を許可しない(常に表示)
|
||||
*/
|
||||
"isPublicDisplayRequired": string;
|
||||
/**
|
||||
* 有効にすると、ユーザーはこの公開ロール/ロールバッジを非表示にできません。
|
||||
*/
|
||||
"descriptionOfIsPublicDisplayRequired": string;
|
||||
/**
|
||||
* オプション
|
||||
*/
|
||||
@@ -12183,6 +12191,28 @@ export interface Locale extends ILocale {
|
||||
"notUnique": string;
|
||||
};
|
||||
};
|
||||
"_roleDisplay": {
|
||||
/**
|
||||
* ロールの表示/非表示
|
||||
*/
|
||||
"title": string;
|
||||
/**
|
||||
* 自分に割り当てられているロールを確認したり、プロフィールやノート上で表示・公開するロールを選択したりできます。
|
||||
*/
|
||||
"description": string;
|
||||
/**
|
||||
* このロールは、管理者により、{link}への表示とロールタイムラインの有効化が設定されています。プロフィール上で非表示にすることはできますが、あなたにこのロールが付与されていることが知られる可能性があります。
|
||||
*/
|
||||
"roleExplorableAlert": ParameterizedString<"link">;
|
||||
/**
|
||||
* ロール/ロールバッジを表示する
|
||||
*/
|
||||
"displayToggle": string;
|
||||
/**
|
||||
* 管理者の設定により非表示にすることはできません。
|
||||
*/
|
||||
"alwaysShownByAdmin": string;
|
||||
};
|
||||
"_roleSelectDialog": {
|
||||
/**
|
||||
* 選択されていません
|
||||
|
||||
@@ -4061,6 +4061,8 @@ export type components = {
|
||||
/** @enum {string} */
|
||||
onlineStatus: 'unknown' | 'online' | 'active' | 'offline';
|
||||
badgeRoles?: {
|
||||
/** Format: id */
|
||||
id: string;
|
||||
name: string;
|
||||
iconUrl: string | null;
|
||||
displayOrder: number;
|
||||
@@ -4151,6 +4153,7 @@ export type components = {
|
||||
preventAiLearning: boolean;
|
||||
isExplorable: boolean;
|
||||
isDeleted: boolean;
|
||||
hiddenRoleIds: string[];
|
||||
/** @enum {string} */
|
||||
twoFactorBackupCodesStock: 'full' | 'partial' | 'none';
|
||||
hideOnlineStatus: boolean;
|
||||
@@ -5322,6 +5325,12 @@ export type components = {
|
||||
isModerator: boolean;
|
||||
/** @example false */
|
||||
isAdministrator: boolean;
|
||||
/** @example false */
|
||||
asBadge: boolean;
|
||||
/** @example false */
|
||||
isPublicDisplayRequired: boolean;
|
||||
/** @example false */
|
||||
isExplorable: boolean;
|
||||
/** @example 0 */
|
||||
displayOrder: number;
|
||||
};
|
||||
@@ -5336,6 +5345,8 @@ export type components = {
|
||||
/** @example false */
|
||||
isPublic: boolean;
|
||||
/** @example false */
|
||||
isPublicDisplayRequired: boolean;
|
||||
/** @example false */
|
||||
isExplorable: boolean;
|
||||
/** @example false */
|
||||
asBadge: boolean;
|
||||
@@ -11041,6 +11052,8 @@ export interface operations {
|
||||
target: 'manual' | 'conditional';
|
||||
condFormula: Record<string, never>;
|
||||
isPublic: boolean;
|
||||
/** @default false */
|
||||
isPublicDisplayRequired?: boolean;
|
||||
isModerator: boolean;
|
||||
isAdministrator: boolean;
|
||||
/** @default false */
|
||||
@@ -11376,6 +11389,7 @@ export interface operations {
|
||||
target?: 'manual' | 'conditional';
|
||||
condFormula?: Record<string, never>;
|
||||
isPublic?: boolean;
|
||||
isPublicDisplayRequired?: boolean;
|
||||
isModerator?: boolean;
|
||||
isAdministrator?: boolean;
|
||||
isExplorable?: boolean;
|
||||
@@ -27898,6 +27912,7 @@ export interface operations {
|
||||
followersVisibility?: 'public' | 'followers' | 'private';
|
||||
/** @enum {string} */
|
||||
chatScope?: 'everyone' | 'followers' | 'following' | 'mutual' | 'none';
|
||||
hiddenRoleIds?: string[];
|
||||
/** Format: misskey:id */
|
||||
pinnedPageId?: string | null;
|
||||
mutedWords?: (string[] | string)[];
|
||||
|
||||
Reference in New Issue
Block a user