Merge branch 'develop' into re-ed25519
This commit is contained in:
commit
c137167c2c
|
@ -4,6 +4,8 @@
|
||||||
- Feat: ノート単体・ユーザーのノート・クリップのノートの埋め込み機能
|
- Feat: ノート単体・ユーザーのノート・クリップのノートの埋め込み機能
|
||||||
- 埋め込みコードやウェブサイトへの実装方法の詳細は https://misskey-hub.net/docs/for-users/features/embed/ をご覧ください
|
- 埋め込みコードやウェブサイトへの実装方法の詳細は https://misskey-hub.net/docs/for-users/features/embed/ をご覧ください
|
||||||
- Feat: パスキーでログインボタンを実装 (#14574)
|
- Feat: パスキーでログインボタンを実装 (#14574)
|
||||||
|
- Feat: フォローされた際のメッセージを設定できるように
|
||||||
|
- Feat: 連合をホワイトリスト制にできるように
|
||||||
- Feat: UserWebhookとSystemWebhookのテスト送信機能を追加 (#14445)
|
- Feat: UserWebhookとSystemWebhookのテスト送信機能を追加 (#14445)
|
||||||
- Feat: モデレーターはユーザーにかかわらずファイルが添付されているノートを検索できるように
|
- Feat: モデレーターはユーザーにかかわらずファイルが添付されているノートを検索できるように
|
||||||
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/680)
|
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/680)
|
||||||
|
@ -19,6 +21,7 @@
|
||||||
- Enhance: ScratchpadにUIインスペクターを追加
|
- Enhance: ScratchpadにUIインスペクターを追加
|
||||||
- Enhance: Play編集画面の項目の並びを少しリデザイン
|
- Enhance: Play編集画面の項目の並びを少しリデザイン
|
||||||
- Enhance: 各種メニューをドロワー表示するかどうか設定可能に
|
- Enhance: 各種メニューをドロワー表示するかどうか設定可能に
|
||||||
|
- Enhance: AiScriptのMk:C:containerのオプションに`borderStyle`と`borderRadius`を追加
|
||||||
- Fix: サーバーメトリクスが2つ以上あるとリロード直後の表示がおかしくなる問題を修正
|
- Fix: サーバーメトリクスが2つ以上あるとリロード直後の表示がおかしくなる問題を修正
|
||||||
- Fix: コントロールパネル内のAp requests内のチャートの表示がおかしかった問題を修正
|
- Fix: コントロールパネル内のAp requests内のチャートの表示がおかしかった問題を修正
|
||||||
- Fix: 月の違う同じ日はセパレータが表示されないのを修正
|
- Fix: 月の違う同じ日はセパレータが表示されないのを修正
|
||||||
|
|
|
@ -960,6 +960,14 @@ export interface Locale extends ILocale {
|
||||||
* メディアサイレンスしたいサーバーのホストを改行で区切って設定します。メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われ、カスタム絵文字が使用できないようになります。ブロックしたインスタンスには影響しません。
|
* メディアサイレンスしたいサーバーのホストを改行で区切って設定します。メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われ、カスタム絵文字が使用できないようになります。ブロックしたインスタンスには影響しません。
|
||||||
*/
|
*/
|
||||||
"mediaSilencedInstancesDescription": string;
|
"mediaSilencedInstancesDescription": string;
|
||||||
|
/**
|
||||||
|
* 連合を許可するサーバー
|
||||||
|
*/
|
||||||
|
"federationAllowedHosts": string;
|
||||||
|
/**
|
||||||
|
* 連合を許可するサーバーのホストを改行で区切って設定します。
|
||||||
|
*/
|
||||||
|
"federationAllowedHostsDescription": string;
|
||||||
/**
|
/**
|
||||||
* ミュートとブロック
|
* ミュートとブロック
|
||||||
*/
|
*/
|
||||||
|
@ -8725,6 +8733,18 @@ export interface Locale extends ILocale {
|
||||||
* 最大{max}つまでデコレーションを付けられます。
|
* 最大{max}つまでデコレーションを付けられます。
|
||||||
*/
|
*/
|
||||||
"avatarDecorationMax": ParameterizedString<"max">;
|
"avatarDecorationMax": ParameterizedString<"max">;
|
||||||
|
/**
|
||||||
|
* フォローされた時のメッセージ
|
||||||
|
*/
|
||||||
|
"followedMessage": string;
|
||||||
|
/**
|
||||||
|
* フォローされた時に相手に表示する短いメッセージを設定できます。
|
||||||
|
*/
|
||||||
|
"followedMessageDescription": string;
|
||||||
|
/**
|
||||||
|
* フォローを承認制にしている場合、フォローリクエストを許可した時に表示されます。
|
||||||
|
*/
|
||||||
|
"followedMessageDescriptionForLockedAccount": string;
|
||||||
};
|
};
|
||||||
"_exportOrImport": {
|
"_exportOrImport": {
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -236,6 +236,8 @@ silencedInstances: "サイレンスしたサーバー"
|
||||||
silencedInstancesDescription: "サイレンスしたいサーバーのホストを改行で区切って設定します。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになります。ブロックしたインスタンスには影響しません。"
|
silencedInstancesDescription: "サイレンスしたいサーバーのホストを改行で区切って設定します。サイレンスされたサーバーに所属するアカウントはすべて「サイレンス」として扱われ、フォローがすべてリクエストになります。ブロックしたインスタンスには影響しません。"
|
||||||
mediaSilencedInstances: "メディアサイレンスしたサーバー"
|
mediaSilencedInstances: "メディアサイレンスしたサーバー"
|
||||||
mediaSilencedInstancesDescription: "メディアサイレンスしたいサーバーのホストを改行で区切って設定します。メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われ、カスタム絵文字が使用できないようになります。ブロックしたインスタンスには影響しません。"
|
mediaSilencedInstancesDescription: "メディアサイレンスしたいサーバーのホストを改行で区切って設定します。メディアサイレンスされたサーバーに所属するアカウントによるファイルはすべてセンシティブとして扱われ、カスタム絵文字が使用できないようになります。ブロックしたインスタンスには影響しません。"
|
||||||
|
federationAllowedHosts: "連合を許可するサーバー"
|
||||||
|
federationAllowedHostsDescription: "連合を許可するサーバーのホストを改行で区切って設定します。"
|
||||||
muteAndBlock: "ミュートとブロック"
|
muteAndBlock: "ミュートとブロック"
|
||||||
mutedUsers: "ミュートしたユーザー"
|
mutedUsers: "ミュートしたユーザー"
|
||||||
blockedUsers: "ブロックしたユーザー"
|
blockedUsers: "ブロックしたユーザー"
|
||||||
|
@ -2297,6 +2299,9 @@ _profile:
|
||||||
changeBanner: "バナー画像を変更"
|
changeBanner: "バナー画像を変更"
|
||||||
verifiedLinkDescription: "内容にURLを設定すると、リンク先のWebサイトに自分のプロフィールへのリンクが含まれている場合に所有者確認済みアイコンを表示させることができます。"
|
verifiedLinkDescription: "内容にURLを設定すると、リンク先のWebサイトに自分のプロフィールへのリンクが含まれている場合に所有者確認済みアイコンを表示させることができます。"
|
||||||
avatarDecorationMax: "最大{max}つまでデコレーションを付けられます。"
|
avatarDecorationMax: "最大{max}つまでデコレーションを付けられます。"
|
||||||
|
followedMessage: "フォローされた時のメッセージ"
|
||||||
|
followedMessageDescription: "フォローされた時に相手に表示する短いメッセージを設定できます。"
|
||||||
|
followedMessageDescriptionForLockedAccount: "フォローを承認制にしている場合、フォローリクエストを許可した時に表示されます。"
|
||||||
|
|
||||||
_exportOrImport:
|
_exportOrImport:
|
||||||
allNotes: "全てのノート"
|
allNotes: "全てのノート"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "2024.9.0-alpha.10",
|
"version": "2024.9.0-alpha.12",
|
||||||
"codename": "nasubi",
|
"codename": "nasubi",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class FollowedMessage1723944246767 {
|
||||||
|
name = 'FollowedMessage1723944246767';
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query('ALTER TABLE "user_profile" ADD "followedMessage" character varying(256)');
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query('ALTER TABLE "user_profile" DROP COLUMN "followedMessage"');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class UserScore1727491883993 {
|
||||||
|
name = 'UserScore1727491883993'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" ADD "score" integer NOT NULL DEFAULT '0'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "score"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class MetaFederation1727512908322 {
|
||||||
|
name = 'MetaFederation1727512908322'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "federation" character varying(128) NOT NULL DEFAULT 'all'`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "federationHosts" character varying(1024) array NOT NULL DEFAULT '{}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "federationHosts"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "federation"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -275,16 +275,19 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
followeeId: followee.id,
|
followeeId: followee.id,
|
||||||
followerId: follower.id,
|
followerId: follower.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 通知を作成
|
|
||||||
if (follower.host === null) {
|
|
||||||
this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
|
|
||||||
}, followee.id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (alreadyFollowed) return;
|
if (alreadyFollowed) return;
|
||||||
|
|
||||||
|
// 通知を作成
|
||||||
|
if (follower.host === null) {
|
||||||
|
const profile = await this.cacheService.userProfileCache.fetch(followee.id);
|
||||||
|
|
||||||
|
this.notificationService.createNotification(follower.id, 'followRequestAccepted', {
|
||||||
|
message: profile.followedMessage,
|
||||||
|
}, followee.id);
|
||||||
|
}
|
||||||
|
|
||||||
this.globalEventService.publishInternalEvent('follow', { followerId: follower.id, followeeId: followee.id });
|
this.globalEventService.publishInternalEvent('follow', { followerId: follower.id, followeeId: followee.id });
|
||||||
|
|
||||||
const [followeeUser, followerUser] = await Promise.all([
|
const [followeeUser, followerUser] = await Promise.all([
|
||||||
|
|
|
@ -10,12 +10,16 @@ import RE2 from 're2';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
|
import { MiMeta } from '@/models/Meta.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UtilityService {
|
export class UtilityService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.config)
|
@Inject(DI.config)
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
|
@Inject(DI.meta)
|
||||||
|
private meta: MiMeta,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,4 +109,19 @@ export class UtilityService {
|
||||||
if (host == null) return null;
|
if (host == null) return null;
|
||||||
return toASCII(host.toLowerCase());
|
return toASCII(host.toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public isFederationAllowedHost(host: string): boolean {
|
||||||
|
if (this.meta.federation === 'none') return false;
|
||||||
|
if (this.meta.federation === 'specified' && !this.meta.federationHosts.some(x => `.${host.toLowerCase()}`.endsWith(`.${x}`))) return false;
|
||||||
|
if (this.isBlockedHost(this.meta.blockedHosts, host)) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@bindThis
|
||||||
|
public isFederationAllowedUri(uri: string): boolean {
|
||||||
|
const host = this.extractDbHost(uri);
|
||||||
|
return this.isFederationAllowedHost(host);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,6 +68,7 @@ function generateDummyUser(override?: Partial<MiUser>): MiUser {
|
||||||
isHibernated: false,
|
isHibernated: false,
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
emojis: [],
|
emojis: [],
|
||||||
|
score: 0,
|
||||||
host: null,
|
host: null,
|
||||||
inbox: null,
|
inbox: null,
|
||||||
sharedInbox: null,
|
sharedInbox: null,
|
||||||
|
|
|
@ -283,8 +283,8 @@ export class ApInboxService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// アナウンス先をブロックしてたら中断
|
// アナウンス先が許可されているかチェック
|
||||||
if (this.utilityService.isBlockedHost(this.meta.blockedHosts, this.utilityService.extractDbHost(uri))) return;
|
if (!this.utilityService.isFederationAllowedUri(uri)) return;
|
||||||
|
|
||||||
const unlock = await this.appLockService.getApLock(uri);
|
const unlock = await this.appLockService.getApLock(uri);
|
||||||
|
|
||||||
|
|
|
@ -494,6 +494,7 @@ export class ApRendererService {
|
||||||
name: user.name,
|
name: user.name,
|
||||||
summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null,
|
summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null,
|
||||||
_misskey_summary: profile.description,
|
_misskey_summary: profile.description,
|
||||||
|
_misskey_followedMessage: profile.followedMessage,
|
||||||
icon: avatar ? this.renderImage(avatar) : null,
|
icon: avatar ? this.renderImage(avatar) : null,
|
||||||
image: banner ? this.renderImage(banner) : null,
|
image: banner ? this.renderImage(banner) : null,
|
||||||
tag,
|
tag,
|
||||||
|
|
|
@ -95,7 +95,7 @@ export class Resolver {
|
||||||
return await this.resolveLocal(value);
|
return await this.resolveLocal(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.utilityService.isBlockedHost(this.meta.blockedHosts, host)) {
|
if (!this.utilityService.isFederationAllowedHost(host)) {
|
||||||
throw new Error('Instance is blocked');
|
throw new Error('Instance is blocked');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -555,6 +555,7 @@ const extension_context_definition = {
|
||||||
'_misskey_reaction': 'misskey:_misskey_reaction',
|
'_misskey_reaction': 'misskey:_misskey_reaction',
|
||||||
'_misskey_votes': 'misskey:_misskey_votes',
|
'_misskey_votes': 'misskey:_misskey_votes',
|
||||||
'_misskey_summary': 'misskey:_misskey_summary',
|
'_misskey_summary': 'misskey:_misskey_summary',
|
||||||
|
'_misskey_followedMessage': 'misskey:_misskey_followedMessage',
|
||||||
'isCat': 'misskey:isCat',
|
'isCat': 'misskey:isCat',
|
||||||
// vcard
|
// vcard
|
||||||
vcard: 'http://www.w3.org/2006/vcard/ns#',
|
vcard: 'http://www.w3.org/2006/vcard/ns#',
|
||||||
|
|
|
@ -336,8 +336,7 @@ export class ApNoteService {
|
||||||
public async resolveNote(value: string | IObject, options: { sentFrom?: URL, resolver?: Resolver } = {}): Promise<MiNote | null> {
|
public async resolveNote(value: string | IObject, options: { sentFrom?: URL, resolver?: Resolver } = {}): Promise<MiNote | null> {
|
||||||
const uri = getApId(value);
|
const uri = getApId(value);
|
||||||
|
|
||||||
// ブロックしていたら中断
|
if (!this.utilityService.isFederationAllowedUri(uri)) {
|
||||||
if (this.utilityService.isBlockedHost(this.meta.blockedHosts, this.utilityService.extractDbHost(uri))) {
|
|
||||||
throw new StatusError('blocked host', 451);
|
throw new StatusError('blocked host', 451);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,7 @@ import type { ApNoteService } from './ApNoteService.js';
|
||||||
import type { ApMfmService } from '../ApMfmService.js';
|
import type { ApMfmService } from '../ApMfmService.js';
|
||||||
import type { ApResolverService, Resolver } from '../ApResolverService.js';
|
import type { ApResolverService, Resolver } from '../ApResolverService.js';
|
||||||
import type { ApLoggerService } from '../ApLoggerService.js';
|
import type { ApLoggerService } from '../ApLoggerService.js';
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
|
|
||||||
import type { ApImageService } from './ApImageService.js';
|
import type { ApImageService } from './ApImageService.js';
|
||||||
import type { IActor, IKey, IObject, ICollection, IOrderedCollection } from '../type.js';
|
import type { IActor, IKey, IObject, ICollection, IOrderedCollection } from '../type.js';
|
||||||
|
|
||||||
|
@ -361,8 +361,8 @@ export class ApPersonService implements OnModuleInit {
|
||||||
this.logger.error('error occurred while fetching following/followers collection', { stack: err });
|
this.logger.error('error occurred while fetching following/followers collection', { stack: err });
|
||||||
}
|
}
|
||||||
return 'private';
|
return 'private';
|
||||||
})
|
}),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
|
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
|
||||||
|
@ -424,6 +424,7 @@ export class ApPersonService implements OnModuleInit {
|
||||||
await transactionalEntityManager.save(new MiUserProfile({
|
await transactionalEntityManager.save(new MiUserProfile({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
description: _description,
|
description: _description,
|
||||||
|
followedMessage: person._misskey_followedMessage != null ? truncate(person._misskey_followedMessage, 256) : null,
|
||||||
url,
|
url,
|
||||||
fields,
|
fields,
|
||||||
followingVisibility,
|
followingVisibility,
|
||||||
|
@ -552,8 +553,8 @@ export class ApPersonService implements OnModuleInit {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return 'private';
|
return 'private';
|
||||||
})
|
}),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
|
const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/);
|
||||||
|
@ -642,6 +643,7 @@ export class ApPersonService implements OnModuleInit {
|
||||||
url,
|
url,
|
||||||
fields,
|
fields,
|
||||||
description: _description,
|
description: _description,
|
||||||
|
followedMessage: person._misskey_followedMessage != null ? truncate(person._misskey_followedMessage, 256) : null,
|
||||||
followingVisibility,
|
followingVisibility,
|
||||||
followersVisibility,
|
followersVisibility,
|
||||||
birthday: bday?.[0] ?? null,
|
birthday: bday?.[0] ?? null,
|
||||||
|
|
|
@ -13,6 +13,7 @@ export interface IObject {
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
summary?: string;
|
summary?: string;
|
||||||
_misskey_summary?: string;
|
_misskey_summary?: string;
|
||||||
|
_misskey_followedMessage?: string | null;
|
||||||
published?: string;
|
published?: string;
|
||||||
cc?: ApObject;
|
cc?: ApObject;
|
||||||
to?: ApObject;
|
to?: ApObject;
|
||||||
|
|
|
@ -59,7 +59,7 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
async #packInternal <T extends MiNotification | MiGroupedNotification> (
|
async #packInternal <T extends MiNotification | MiGroupedNotification> (
|
||||||
src: T,
|
src: T,
|
||||||
meId: MiUser['id'],
|
meId: MiUser['id'],
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
||||||
options: {
|
options: {
|
||||||
checkValidNotifier?: boolean;
|
checkValidNotifier?: boolean;
|
||||||
},
|
},
|
||||||
|
@ -159,6 +159,9 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
...(notification.type === 'roleAssigned' ? {
|
...(notification.type === 'roleAssigned' ? {
|
||||||
role: role,
|
role: role,
|
||||||
} : {}),
|
} : {}),
|
||||||
|
...(notification.type === 'followRequestAccepted' ? {
|
||||||
|
message: notification.message,
|
||||||
|
} : {}),
|
||||||
...(notification.type === 'achievementEarned' ? {
|
...(notification.type === 'achievementEarned' ? {
|
||||||
achievement: notification.achievement,
|
achievement: notification.achievement,
|
||||||
} : {}),
|
} : {}),
|
||||||
|
@ -233,7 +236,7 @@ export class NotificationEntityService implements OnModuleInit {
|
||||||
public async pack(
|
public async pack(
|
||||||
src: MiNotification | MiGroupedNotification,
|
src: MiNotification | MiGroupedNotification,
|
||||||
meId: MiUser['id'],
|
meId: MiUser['id'],
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
||||||
options: {
|
options: {
|
||||||
checkValidNotifier?: boolean;
|
checkValidNotifier?: boolean;
|
||||||
},
|
},
|
||||||
|
|
|
@ -508,7 +508,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
name: r.name,
|
name: r.name,
|
||||||
iconUrl: r.iconUrl,
|
iconUrl: r.iconUrl,
|
||||||
displayOrder: r.displayOrder,
|
displayOrder: r.displayOrder,
|
||||||
}))
|
})),
|
||||||
) : undefined,
|
) : undefined,
|
||||||
|
|
||||||
...(isDetailed ? {
|
...(isDetailed ? {
|
||||||
|
@ -567,6 +567,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
...(isDetailed && isMe ? {
|
...(isDetailed && isMe ? {
|
||||||
avatarId: user.avatarId,
|
avatarId: user.avatarId,
|
||||||
bannerId: user.bannerId,
|
bannerId: user.bannerId,
|
||||||
|
followedMessage: profile!.followedMessage,
|
||||||
isModerator: isModerator,
|
isModerator: isModerator,
|
||||||
isAdmin: isAdmin,
|
isAdmin: isAdmin,
|
||||||
injectFeaturedNote: profile!.injectFeaturedNote,
|
injectFeaturedNote: profile!.injectFeaturedNote,
|
||||||
|
@ -635,6 +636,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
isRenoteMuted: relation.isRenoteMuted,
|
isRenoteMuted: relation.isRenoteMuted,
|
||||||
notify: relation.following?.notify ?? 'none',
|
notify: relation.following?.notify ?? 'none',
|
||||||
withReplies: relation.following?.withReplies ?? false,
|
withReplies: relation.following?.withReplies ?? false,
|
||||||
|
followedMessage: relation.isFollowing ? profile!.followedMessage : undefined,
|
||||||
} : {}),
|
} : {}),
|
||||||
} as Promiseable<Packed<S>>;
|
} as Promiseable<Packed<S>>;
|
||||||
|
|
||||||
|
|
|
@ -630,4 +630,17 @@ export class MiMeta {
|
||||||
nullable: true,
|
nullable: true,
|
||||||
})
|
})
|
||||||
public urlPreviewUserAgent: string | null;
|
public urlPreviewUserAgent: string | null;
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 128,
|
||||||
|
default: 'all',
|
||||||
|
})
|
||||||
|
public federation: 'all' | 'specified' | 'none';
|
||||||
|
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 1024,
|
||||||
|
array: true,
|
||||||
|
default: '{}',
|
||||||
|
})
|
||||||
|
public federationHosts: string[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,6 +69,7 @@ export type MiNotification = {
|
||||||
id: string;
|
id: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
notifierId: MiUser['id'];
|
notifierId: MiUser['id'];
|
||||||
|
message: string | null;
|
||||||
} | {
|
} | {
|
||||||
type: 'roleAssigned';
|
type: 'roleAssigned';
|
||||||
id: string;
|
id: string;
|
||||||
|
|
|
@ -155,6 +155,11 @@ export class MiUser {
|
||||||
})
|
})
|
||||||
public tags: string[];
|
public tags: string[];
|
||||||
|
|
||||||
|
@Column('integer', {
|
||||||
|
default: 0,
|
||||||
|
})
|
||||||
|
public score: number;
|
||||||
|
|
||||||
@Column('boolean', {
|
@Column('boolean', {
|
||||||
default: false,
|
default: false,
|
||||||
comment: 'Whether the User is suspended.',
|
comment: 'Whether the User is suspended.',
|
||||||
|
@ -289,5 +294,6 @@ export const localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toStr
|
||||||
export const passwordSchema = { type: 'string', minLength: 1 } as const;
|
export const passwordSchema = { type: 'string', minLength: 1 } as const;
|
||||||
export const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
|
export const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
|
||||||
export const descriptionSchema = { type: 'string', minLength: 1, maxLength: 1500 } as const;
|
export const descriptionSchema = { type: 'string', minLength: 1, maxLength: 1500 } as const;
|
||||||
|
export const followedMessageSchema = { type: 'string', minLength: 1, maxLength: 256 } as const;
|
||||||
export const locationSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
|
export const locationSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
|
||||||
export const birthdaySchema = { type: 'string', pattern: /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.toString().slice(1, -1) } as const;
|
export const birthdaySchema = { type: 'string', pattern: /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.toString().slice(1, -1) } as const;
|
||||||
|
|
|
@ -42,6 +42,14 @@ export class MiUserProfile {
|
||||||
})
|
})
|
||||||
public description: string | null;
|
public description: string | null;
|
||||||
|
|
||||||
|
// フォローされた際のメッセージ
|
||||||
|
@Column('varchar', {
|
||||||
|
length: 256, nullable: true,
|
||||||
|
})
|
||||||
|
public followedMessage: string | null;
|
||||||
|
|
||||||
|
// TODO: 鍵アカウントの場合の、フォローリクエスト受信時のメッセージも設定できるようにする
|
||||||
|
|
||||||
@Column('jsonb', {
|
@Column('jsonb', {
|
||||||
default: [],
|
default: [],
|
||||||
})
|
})
|
||||||
|
|
|
@ -267,6 +267,10 @@ export const packedNotificationSchema = {
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
format: 'id',
|
format: 'id',
|
||||||
},
|
},
|
||||||
|
message: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
|
|
|
@ -370,6 +370,10 @@ export const packedUserDetailedNotMeOnlySchema = {
|
||||||
ref: 'RoleLite',
|
ref: 'RoleLite',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
followedMessage: {
|
||||||
|
type: 'string',
|
||||||
|
nullable: true, optional: true,
|
||||||
|
},
|
||||||
memo: {
|
memo: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
nullable: true, optional: false,
|
nullable: true, optional: false,
|
||||||
|
@ -437,6 +441,10 @@ export const packedMeDetailedOnlySchema = {
|
||||||
nullable: true, optional: false,
|
nullable: true, optional: false,
|
||||||
format: 'id',
|
format: 'id',
|
||||||
},
|
},
|
||||||
|
followedMessage: {
|
||||||
|
type: 'string',
|
||||||
|
nullable: true, optional: false,
|
||||||
|
},
|
||||||
isModerator: {
|
isModerator: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
nullable: true, optional: false,
|
nullable: true, optional: false,
|
||||||
|
|
|
@ -53,8 +53,7 @@ export class DeliverProcessorService {
|
||||||
public async process(job: Bull.Job<DeliverJobData>): Promise<string> {
|
public async process(job: Bull.Job<DeliverJobData>): Promise<string> {
|
||||||
const { host } = new URL(job.data.to);
|
const { host } = new URL(job.data.to);
|
||||||
|
|
||||||
// ブロックしてたら中断
|
if (!this.utilityService.isFederationAllowedUri(job.data.to)) {
|
||||||
if (this.utilityService.isBlockedHost(this.meta.blockedHosts, this.utilityService.toPuny(host))) {
|
|
||||||
return 'skip (blocked)';
|
return 'skip (blocked)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -79,8 +79,7 @@ export class InboxProcessorService implements OnApplicationShutdown {
|
||||||
|
|
||||||
const host = this.utilityService.toPuny(new URL(actorUri).hostname);
|
const host = this.utilityService.toPuny(new URL(actorUri).hostname);
|
||||||
|
|
||||||
// ブロックしてたら中断
|
if (!this.utilityService.isFederationAllowedHost(host)) {
|
||||||
if (this.utilityService.isBlockedHost(this.meta.blockedHosts, host)) {
|
|
||||||
return `Blocked request: ${host}`;
|
return `Blocked request: ${host}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,7 +165,7 @@ export class InboxProcessorService implements OnApplicationShutdown {
|
||||||
|
|
||||||
// ブロックしてたら中断
|
// ブロックしてたら中断
|
||||||
const ldHost = this.utilityService.extractDbHost(authUser.user.uri);
|
const ldHost = this.utilityService.extractDbHost(authUser.user.uri);
|
||||||
if (this.utilityService.isBlockedHost(this.meta.blockedHosts, ldHost)) {
|
if (this.utilityService.isFederationAllowedHost(ldHost)) {
|
||||||
throw new Bull.UnrecoverableError(`Blocked request: ${ldHost}`);
|
throw new Bull.UnrecoverableError(`Blocked request: ${ldHost}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -495,6 +495,18 @@ export const meta = {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
optional: false, nullable: true,
|
optional: false, nullable: true,
|
||||||
},
|
},
|
||||||
|
federation: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
federationHosts: {
|
||||||
|
type: 'array',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -630,6 +642,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
urlPreviewRequireContentLength: instance.urlPreviewRequireContentLength,
|
urlPreviewRequireContentLength: instance.urlPreviewRequireContentLength,
|
||||||
urlPreviewUserAgent: instance.urlPreviewUserAgent,
|
urlPreviewUserAgent: instance.urlPreviewUserAgent,
|
||||||
urlPreviewSummaryProxyUrl: instance.urlPreviewSummaryProxyUrl,
|
urlPreviewSummaryProxyUrl: instance.urlPreviewSummaryProxyUrl,
|
||||||
|
federation: instance.federation,
|
||||||
|
federationHosts: instance.federationHosts,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,10 @@ export const meta = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
followedMessage: {
|
||||||
|
type: 'string',
|
||||||
|
optional: false, nullable: true,
|
||||||
|
},
|
||||||
autoAcceptFollowed: {
|
autoAcceptFollowed: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -279,6 +283,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
return {
|
return {
|
||||||
email: profile.email,
|
email: profile.email,
|
||||||
emailVerified: profile.emailVerified,
|
emailVerified: profile.emailVerified,
|
||||||
|
followedMessage: profile.followedMessage,
|
||||||
autoAcceptFollowed: profile.autoAcceptFollowed,
|
autoAcceptFollowed: profile.autoAcceptFollowed,
|
||||||
noCrawle: profile.noCrawle,
|
noCrawle: profile.noCrawle,
|
||||||
preventAiLearning: profile.preventAiLearning,
|
preventAiLearning: profile.preventAiLearning,
|
||||||
|
|
|
@ -168,6 +168,16 @@ export const paramDef = {
|
||||||
urlPreviewRequireContentLength: { type: 'boolean' },
|
urlPreviewRequireContentLength: { type: 'boolean' },
|
||||||
urlPreviewUserAgent: { type: 'string', nullable: true },
|
urlPreviewUserAgent: { type: 'string', nullable: true },
|
||||||
urlPreviewSummaryProxyUrl: { type: 'string', nullable: true },
|
urlPreviewSummaryProxyUrl: { type: 'string', nullable: true },
|
||||||
|
federation: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['all', 'none', 'specified'],
|
||||||
|
},
|
||||||
|
federationHosts: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
required: [],
|
required: [],
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -637,6 +647,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
set.urlPreviewSummaryProxyUrl = value === '' ? null : value;
|
set.urlPreviewSummaryProxyUrl = value === '' ? null : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ps.federation !== undefined) {
|
||||||
|
set.federation = ps.federation;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(ps.federationHosts)) {
|
||||||
|
set.blockedHosts = ps.federationHosts.filter(Boolean).map(x => x.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
const before = await this.metaService.fetch(true);
|
const before = await this.metaService.fetch(true);
|
||||||
|
|
||||||
await this.metaService.update(set);
|
await this.metaService.update(set);
|
||||||
|
|
|
@ -19,8 +19,6 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import { ApiError } from '../../error.js';
|
import { ApiError } from '../../error.js';
|
||||||
import { MiMeta } from '@/models/_.js';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['federation'],
|
tags: ['federation'],
|
||||||
|
@ -89,9 +87,6 @@ export const paramDef = {
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.meta)
|
|
||||||
private serverSettings: MiMeta,
|
|
||||||
|
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
private userEntityService: UserEntityService,
|
private userEntityService: UserEntityService,
|
||||||
private noteEntityService: NoteEntityService,
|
private noteEntityService: NoteEntityService,
|
||||||
|
@ -115,8 +110,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
*/
|
*/
|
||||||
@bindThis
|
@bindThis
|
||||||
private async fetchAny(uri: string, me: MiLocalUser | null | undefined): Promise<SchemaType<typeof meta['res']> | null> {
|
private async fetchAny(uri: string, me: MiLocalUser | null | undefined): Promise<SchemaType<typeof meta['res']> | null> {
|
||||||
// ブロックしてたら中断
|
if (!this.utilityService.isFederationAllowedUri(uri)) return null;
|
||||||
if (this.utilityService.isBlockedHost(this.serverSettings.blockedHosts, this.utilityService.extractDbHost(uri))) return null;
|
|
||||||
|
|
||||||
let local = await this.mergePack(me, ...await Promise.all([
|
let local = await this.mergePack(me, ...await Promise.all([
|
||||||
this.apDbResolverService.getUserFromApId(uri),
|
this.apDbResolverService.getUserFromApId(uri),
|
||||||
|
|
|
@ -13,9 +13,8 @@ import { extractHashtags } from '@/misc/extract-hashtags.js';
|
||||||
import * as Acct from '@/misc/acct.js';
|
import * as Acct from '@/misc/acct.js';
|
||||||
import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/_.js';
|
import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/_.js';
|
||||||
import type { MiLocalUser, MiUser } from '@/models/User.js';
|
import type { MiLocalUser, MiUser } from '@/models/User.js';
|
||||||
import { birthdaySchema, descriptionSchema, locationSchema, nameSchema } from '@/models/User.js';
|
import { birthdaySchema, descriptionSchema, followedMessageSchema, locationSchema, nameSchema } from '@/models/User.js';
|
||||||
import type { MiUserProfile } from '@/models/UserProfile.js';
|
import type { MiUserProfile } from '@/models/UserProfile.js';
|
||||||
import { notificationTypes } from '@/types.js';
|
|
||||||
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
|
||||||
import { langmap } from '@/misc/langmap.js';
|
import { langmap } from '@/misc/langmap.js';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
|
@ -134,6 +133,7 @@ export const paramDef = {
|
||||||
properties: {
|
properties: {
|
||||||
name: { ...nameSchema, nullable: true },
|
name: { ...nameSchema, nullable: true },
|
||||||
description: { ...descriptionSchema, nullable: true },
|
description: { ...descriptionSchema, nullable: true },
|
||||||
|
followedMessage: { ...followedMessageSchema, nullable: true },
|
||||||
location: { ...locationSchema, nullable: true },
|
location: { ...locationSchema, nullable: true },
|
||||||
birthday: { ...birthdaySchema, nullable: true },
|
birthday: { ...birthdaySchema, nullable: true },
|
||||||
lang: { type: 'string', enum: [null, ...Object.keys(langmap)] as string[], nullable: true },
|
lang: { type: 'string', enum: [null, ...Object.keys(langmap)] as string[], nullable: true },
|
||||||
|
@ -267,6 +267,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ps.description !== undefined) profileUpdates.description = ps.description;
|
if (ps.description !== undefined) profileUpdates.description = ps.description;
|
||||||
|
if (ps.followedMessage !== undefined) profileUpdates.followedMessage = ps.followedMessage;
|
||||||
if (ps.lang !== undefined) profileUpdates.lang = ps.lang;
|
if (ps.lang !== undefined) profileUpdates.lang = ps.lang;
|
||||||
if (ps.location !== undefined) profileUpdates.location = ps.location;
|
if (ps.location !== undefined) profileUpdates.location = ps.location;
|
||||||
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
|
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
|
||||||
|
|
|
@ -7,9 +7,9 @@ process.env.NODE_ENV = 'test';
|
||||||
|
|
||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
import { inspect } from 'node:util';
|
import { inspect } from 'node:util';
|
||||||
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
|
|
||||||
import { api, post, role, signup, successfulApiCall, uploadFile } from '../utils.js';
|
import { api, post, role, signup, successfulApiCall, uploadFile } from '../utils.js';
|
||||||
import type * as misskey from 'misskey-js';
|
import type * as misskey from 'misskey-js';
|
||||||
|
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
|
||||||
|
|
||||||
describe('ユーザー', () => {
|
describe('ユーザー', () => {
|
||||||
// エンティティとしてのユーザーを主眼においたテストを記述する
|
// エンティティとしてのユーザーを主眼においたテストを記述する
|
||||||
|
@ -105,6 +105,7 @@ describe('ユーザー', () => {
|
||||||
isRenoteMuted: user.isRenoteMuted ?? false,
|
isRenoteMuted: user.isRenoteMuted ?? false,
|
||||||
notify: user.notify ?? 'none',
|
notify: user.notify ?? 'none',
|
||||||
withReplies: user.withReplies ?? false,
|
withReplies: user.withReplies ?? false,
|
||||||
|
followedMessage: user.isFollowing ? (user.followedMessage ?? null) : undefined,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -114,6 +115,7 @@ describe('ユーザー', () => {
|
||||||
...userDetailedNotMe(user),
|
...userDetailedNotMe(user),
|
||||||
avatarId: user.avatarId,
|
avatarId: user.avatarId,
|
||||||
bannerId: user.bannerId,
|
bannerId: user.bannerId,
|
||||||
|
followedMessage: user.followedMessage,
|
||||||
isModerator: user.isModerator,
|
isModerator: user.isModerator,
|
||||||
isAdmin: user.isAdmin,
|
isAdmin: user.isAdmin,
|
||||||
injectFeaturedNote: user.injectFeaturedNote,
|
injectFeaturedNote: user.injectFeaturedNote,
|
||||||
|
@ -350,6 +352,7 @@ describe('ユーザー', () => {
|
||||||
// MeDetailedOnly
|
// MeDetailedOnly
|
||||||
assert.strictEqual(response.avatarId, null);
|
assert.strictEqual(response.avatarId, null);
|
||||||
assert.strictEqual(response.bannerId, null);
|
assert.strictEqual(response.bannerId, null);
|
||||||
|
assert.strictEqual(response.followedMessage, null);
|
||||||
assert.strictEqual(response.isModerator, false);
|
assert.strictEqual(response.isModerator, false);
|
||||||
assert.strictEqual(response.isAdmin, false);
|
assert.strictEqual(response.isAdmin, false);
|
||||||
assert.strictEqual(response.injectFeaturedNote, true);
|
assert.strictEqual(response.injectFeaturedNote, true);
|
||||||
|
@ -413,6 +416,8 @@ describe('ユーザー', () => {
|
||||||
{ parameters: () => ({ description: 'x'.repeat(1500) }) },
|
{ parameters: () => ({ description: 'x'.repeat(1500) }) },
|
||||||
{ parameters: () => ({ description: 'x' }) },
|
{ parameters: () => ({ description: 'x' }) },
|
||||||
{ parameters: () => ({ description: 'My description' }) },
|
{ parameters: () => ({ description: 'My description' }) },
|
||||||
|
{ parameters: () => ({ followedMessage: null }) },
|
||||||
|
{ parameters: () => ({ followedMessage: 'Thank you' }) },
|
||||||
{ parameters: () => ({ location: null }) },
|
{ parameters: () => ({ location: null }) },
|
||||||
{ parameters: () => ({ location: 'x'.repeat(50) }) },
|
{ parameters: () => ({ location: 'x'.repeat(50) }) },
|
||||||
{ parameters: () => ({ location: 'x' }) },
|
{ parameters: () => ({ location: 'x' }) },
|
||||||
|
|
|
@ -0,0 +1,182 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { IncomingHttpHeaders } from 'node:http';
|
||||||
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, jest, test } from '@jest/globals';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||||
|
import { AuthenticationResponseJSON } from '@simplewebauthn/types';
|
||||||
|
import { HttpHeader } from 'fastify/types/utils.js';
|
||||||
|
import { MockFunctionMetadata, ModuleMocker } from 'jest-mock';
|
||||||
|
import { MiUser } from '@/models/User.js';
|
||||||
|
import { MiUserProfile, UserProfilesRepository, UsersRepository } from '@/models/_.js';
|
||||||
|
import { IdService } from '@/core/IdService.js';
|
||||||
|
import { GlobalModule } from '@/GlobalModule.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { CoreModule } from '@/core/CoreModule.js';
|
||||||
|
import { SigninWithPasskeyApiService } from '@/server/api/SigninWithPasskeyApiService.js';
|
||||||
|
import { RateLimiterService } from '@/server/api/RateLimiterService.js';
|
||||||
|
import { WebAuthnService } from '@/core/WebAuthnService.js';
|
||||||
|
import { SigninService } from '@/server/api/SigninService.js';
|
||||||
|
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||||
|
|
||||||
|
const moduleMocker = new ModuleMocker(global);
|
||||||
|
|
||||||
|
class FakeLimiter {
|
||||||
|
public async limit() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeSigninService {
|
||||||
|
public signin(..._args: any): any {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DummyFastifyReply {
|
||||||
|
public statusCode: number;
|
||||||
|
code(num: number): void {
|
||||||
|
this.statusCode = num;
|
||||||
|
}
|
||||||
|
header(_key: HttpHeader, _value: any): void {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class DummyFastifyRequest {
|
||||||
|
public ip: string;
|
||||||
|
public body: {credential: any, context: string};
|
||||||
|
public headers: IncomingHttpHeaders = { 'accept': 'application/json' };
|
||||||
|
constructor(body?: any) {
|
||||||
|
this.ip = '0.0.0.0';
|
||||||
|
this.body = body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ApiFastifyRequestType = FastifyRequest<{
|
||||||
|
Body: {
|
||||||
|
credential?: AuthenticationResponseJSON;
|
||||||
|
context?: string;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
|
||||||
|
describe('SigninWithPasskeyApiService', () => {
|
||||||
|
let app: TestingModule;
|
||||||
|
let passkeyApiService: SigninWithPasskeyApiService;
|
||||||
|
let usersRepository: UsersRepository;
|
||||||
|
let userProfilesRepository: UserProfilesRepository;
|
||||||
|
let webAuthnService: WebAuthnService;
|
||||||
|
let idService: IdService;
|
||||||
|
let FakeWebauthnVerify: ()=>Promise<string>;
|
||||||
|
|
||||||
|
async function createUser(data: Partial<MiUser> = {}) {
|
||||||
|
const user = await usersRepository
|
||||||
|
.save({
|
||||||
|
...data,
|
||||||
|
});
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createUserProfile(data: Partial<MiUserProfile> = {}) {
|
||||||
|
const userProfile = await userProfilesRepository
|
||||||
|
.save({ ...data },
|
||||||
|
);
|
||||||
|
return userProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
app = await Test.createTestingModule({
|
||||||
|
imports: [GlobalModule, CoreModule],
|
||||||
|
providers: [
|
||||||
|
SigninWithPasskeyApiService,
|
||||||
|
{ provide: RateLimiterService, useClass: FakeLimiter },
|
||||||
|
{ provide: SigninService, useClass: FakeSigninService },
|
||||||
|
],
|
||||||
|
}).useMocker((token) => {
|
||||||
|
if (typeof token === 'function') {
|
||||||
|
const mockMetadata = moduleMocker.getMetadata(token) as MockFunctionMetadata<any, any>;
|
||||||
|
const Mock = moduleMocker.generateFromMetadata(mockMetadata);
|
||||||
|
return new Mock();
|
||||||
|
}
|
||||||
|
}).compile();
|
||||||
|
passkeyApiService = app.get<SigninWithPasskeyApiService>(SigninWithPasskeyApiService);
|
||||||
|
usersRepository = app.get<UsersRepository>(DI.usersRepository);
|
||||||
|
userProfilesRepository = app.get<UserProfilesRepository>(DI.userProfilesRepository);
|
||||||
|
webAuthnService = app.get<WebAuthnService>(WebAuthnService);
|
||||||
|
idService = app.get<IdService>(IdService);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const uid = idService.gen();
|
||||||
|
FakeWebauthnVerify = async () => {
|
||||||
|
return uid;
|
||||||
|
};
|
||||||
|
jest.spyOn(webAuthnService, 'verifySignInWithPasskeyAuthentication').mockImplementation(FakeWebauthnVerify);
|
||||||
|
|
||||||
|
const dummyUser = {
|
||||||
|
id: uid, username: uid, usernameLower: uid.toLocaleLowerCase(), uri: null, host: null,
|
||||||
|
};
|
||||||
|
const dummyProfile = {
|
||||||
|
userId: uid,
|
||||||
|
password: 'qwerty',
|
||||||
|
usePasswordLessLogin: true,
|
||||||
|
};
|
||||||
|
await createUser(dummyUser);
|
||||||
|
await createUserProfile(dummyProfile);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Get Passkey Options', () => {
|
||||||
|
it('Should return passkey Auth Options', async () => {
|
||||||
|
const req = new DummyFastifyRequest({}) as ApiFastifyRequestType;
|
||||||
|
const res = new DummyFastifyReply() as unknown as FastifyReply;
|
||||||
|
const res_body = await passkeyApiService.signin(req, res);
|
||||||
|
expect(res.statusCode).toBe(200);
|
||||||
|
expect((res_body as any).option).toBeDefined();
|
||||||
|
expect(typeof (res_body as any).context).toBe('string');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('Try Passkey Auth', () => {
|
||||||
|
it('Should Success', async () => {
|
||||||
|
const req = new DummyFastifyRequest({ context: 'auth-context', credential: { dummy: [] } }) as ApiFastifyRequestType;
|
||||||
|
const res = new DummyFastifyReply() as FastifyReply;
|
||||||
|
const res_body = await passkeyApiService.signin(req, res);
|
||||||
|
expect((res_body as any).signinResponse).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should return 400 Without Auth Context', async () => {
|
||||||
|
const req = new DummyFastifyRequest({ credential: { dummy: [] } }) as ApiFastifyRequestType;
|
||||||
|
const res = new DummyFastifyReply() as FastifyReply;
|
||||||
|
const res_body = await passkeyApiService.signin(req, res);
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
expect((res_body as any).error?.id).toStrictEqual('1658cc2e-4495-461f-aee4-d403cdf073c1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should return 403 When Challenge Verify fail', async () => {
|
||||||
|
const req = new DummyFastifyRequest({ context: 'misskey-1234', credential: { dummy: [] } }) as ApiFastifyRequestType;
|
||||||
|
const res = new DummyFastifyReply() as FastifyReply;
|
||||||
|
jest.spyOn(webAuthnService, 'verifySignInWithPasskeyAuthentication')
|
||||||
|
.mockImplementation(async () => {
|
||||||
|
throw new IdentifiableError('THIS_ERROR_CODE_SHOULD_BE_FORWARDED');
|
||||||
|
});
|
||||||
|
const res_body = await passkeyApiService.signin(req, res);
|
||||||
|
expect(res.statusCode).toBe(403);
|
||||||
|
expect((res_body as any).error?.id).toStrictEqual('THIS_ERROR_CODE_SHOULD_BE_FORWARDED');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should return 403 When The user not Enabled Passwordless login', async () => {
|
||||||
|
const req = new DummyFastifyRequest({ context: 'misskey-1234', credential: { dummy: [] } }) as ApiFastifyRequestType;
|
||||||
|
const res = new DummyFastifyReply() as FastifyReply;
|
||||||
|
const userId = await FakeWebauthnVerify();
|
||||||
|
const data = { userId: userId, usePasswordLessLogin: false };
|
||||||
|
await userProfilesRepository.update({ userId: userId }, data);
|
||||||
|
const res_body = await passkeyApiService.signin(req, res);
|
||||||
|
expect(res.statusCode).toBe(403);
|
||||||
|
expect((res_body as any).error?.id).toStrictEqual('2d84773e-f7b7-4d0b-8f72-bb69b584c912');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -12,59 +12,46 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordapp/twemoji": "15.1.0",
|
"@discordapp/twemoji": "15.1.0",
|
||||||
"@github/webauthn-json": "2.1.1",
|
|
||||||
"@rollup/plugin-json": "6.1.0",
|
"@rollup/plugin-json": "6.1.0",
|
||||||
"@rollup/plugin-replace": "5.0.7",
|
"@rollup/plugin-replace": "5.0.7",
|
||||||
"@rollup/pluginutils": "5.1.0",
|
"@rollup/pluginutils": "5.1.2",
|
||||||
"@tabler/icons-webfont": "3.3.0",
|
"@tabler/icons-webfont": "3.3.0",
|
||||||
"@twemoji/parser": "15.1.1",
|
"@twemoji/parser": "15.1.1",
|
||||||
"@vitejs/plugin-vue": "5.1.4",
|
"@vitejs/plugin-vue": "5.1.4",
|
||||||
"@vue/compiler-sfc": "3.5.7",
|
"@vue/compiler-sfc": "3.5.10",
|
||||||
"astring": "1.9.0",
|
"astring": "1.9.0",
|
||||||
"buraha": "0.0.1",
|
"buraha": "0.0.1",
|
||||||
"compare-versions": "6.1.1",
|
|
||||||
"date-fns": "2.30.0",
|
|
||||||
"escape-regexp": "0.0.1",
|
|
||||||
"estree-walker": "3.0.3",
|
"estree-walker": "3.0.3",
|
||||||
"eventemitter3": "5.0.1",
|
|
||||||
"idb-keyval": "6.2.1",
|
|
||||||
"is-file-animated": "1.0.2",
|
|
||||||
"mfm-js": "0.24.0",
|
"mfm-js": "0.24.0",
|
||||||
"misskey-js": "workspace:*",
|
"misskey-js": "workspace:*",
|
||||||
"frontend-shared": "workspace:*",
|
"frontend-shared": "workspace:*",
|
||||||
"punycode": "2.3.1",
|
"punycode": "2.3.1",
|
||||||
"rollup": "4.22.2",
|
"rollup": "4.22.5",
|
||||||
"sanitize-html": "2.13.0",
|
|
||||||
"sass": "1.79.3",
|
"sass": "1.79.3",
|
||||||
"shiki": "1.12.0",
|
"shiki": "1.12.0",
|
||||||
"strict-event-emitter-types": "2.0.0",
|
|
||||||
"throttle-debounce": "5.0.2",
|
|
||||||
"tinycolor2": "1.6.0",
|
"tinycolor2": "1.6.0",
|
||||||
"tsc-alias": "1.8.10",
|
"tsc-alias": "1.8.10",
|
||||||
"tsconfig-paths": "4.2.0",
|
"tsconfig-paths": "4.2.0",
|
||||||
"typescript": "5.6.2",
|
"typescript": "5.6.2",
|
||||||
"uuid": "10.0.0",
|
"uuid": "10.0.0",
|
||||||
"json5": "2.2.3",
|
"json5": "2.2.3",
|
||||||
"vite": "5.4.7",
|
"vite": "5.4.8",
|
||||||
"vue": "3.5.7"
|
"vue": "3.5.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@misskey-dev/summaly": "5.1.0",
|
"@misskey-dev/summaly": "5.1.0",
|
||||||
"@testing-library/vue": "8.1.0",
|
"@testing-library/vue": "8.1.0",
|
||||||
"@types/escape-regexp": "0.0.3",
|
|
||||||
"@types/estree": "1.0.6",
|
"@types/estree": "1.0.6",
|
||||||
"@types/micromatch": "4.0.9",
|
"@types/micromatch": "4.0.9",
|
||||||
"@types/node": "20.14.12",
|
"@types/node": "20.14.12",
|
||||||
"@types/punycode": "2.1.4",
|
"@types/punycode": "2.1.4",
|
||||||
"@types/sanitize-html": "2.13.0",
|
|
||||||
"@types/throttle-debounce": "5.0.2",
|
|
||||||
"@types/tinycolor2": "1.4.6",
|
"@types/tinycolor2": "1.4.6",
|
||||||
"@types/uuid": "10.0.0",
|
"@types/uuid": "10.0.0",
|
||||||
"@types/ws": "8.5.12",
|
"@types/ws": "8.5.12",
|
||||||
"@typescript-eslint/eslint-plugin": "7.17.0",
|
"@typescript-eslint/eslint-plugin": "7.17.0",
|
||||||
"@typescript-eslint/parser": "7.17.0",
|
"@typescript-eslint/parser": "7.17.0",
|
||||||
"@vitest/coverage-v8": "1.6.0",
|
"@vitest/coverage-v8": "1.6.0",
|
||||||
"@vue/runtime-core": "3.5.7",
|
"@vue/runtime-core": "3.5.10",
|
||||||
"acorn": "8.12.1",
|
"acorn": "8.12.1",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"eslint-plugin-import": "2.30.0",
|
"eslint-plugin-import": "2.30.0",
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
accentDarken: ':darken<10<@accent',
|
accentDarken: ':darken<10<@accent',
|
||||||
accentLighten: ':lighten<10<@accent',
|
accentLighten: ':lighten<10<@accent',
|
||||||
accentedBg: ':alpha<0.15<@accent',
|
accentedBg: ':alpha<0.15<@accent',
|
||||||
|
love: '#dd2e44',
|
||||||
focus: ':alpha<0.3<@accent',
|
focus: ':alpha<0.3<@accent',
|
||||||
bg: '#000',
|
bg: '#000',
|
||||||
acrylicBg: ':alpha<0.5<@bg',
|
acrylicBg: ':alpha<0.5<@bg',
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
accentDarken: ':darken<10<@accent',
|
accentDarken: ':darken<10<@accent',
|
||||||
accentLighten: ':lighten<10<@accent',
|
accentLighten: ':lighten<10<@accent',
|
||||||
accentedBg: ':alpha<0.15<@accent',
|
accentedBg: ':alpha<0.15<@accent',
|
||||||
|
love: '#dd2e44',
|
||||||
focus: ':alpha<0.3<@accent',
|
focus: ':alpha<0.3<@accent',
|
||||||
bg: '#fff',
|
bg: '#fff',
|
||||||
acrylicBg: ':alpha<0.5<@bg',
|
acrylicBg: ':alpha<0.5<@bg',
|
||||||
|
|
|
@ -23,12 +23,12 @@
|
||||||
"@misskey-dev/browser-image-resizer": "2024.1.0",
|
"@misskey-dev/browser-image-resizer": "2024.1.0",
|
||||||
"@rollup/plugin-json": "6.1.0",
|
"@rollup/plugin-json": "6.1.0",
|
||||||
"@rollup/plugin-replace": "5.0.7",
|
"@rollup/plugin-replace": "5.0.7",
|
||||||
"@rollup/pluginutils": "5.1.0",
|
"@rollup/pluginutils": "5.1.2",
|
||||||
"@syuilo/aiscript": "0.19.0",
|
"@syuilo/aiscript": "0.19.0",
|
||||||
"@tabler/icons-webfont": "3.3.0",
|
"@tabler/icons-webfont": "3.3.0",
|
||||||
"@twemoji/parser": "15.1.1",
|
"@twemoji/parser": "15.1.1",
|
||||||
"@vitejs/plugin-vue": "5.1.4",
|
"@vitejs/plugin-vue": "5.1.4",
|
||||||
"@vue/compiler-sfc": "3.5.7",
|
"@vue/compiler-sfc": "3.5.10",
|
||||||
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.11",
|
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.11",
|
||||||
"astring": "1.9.0",
|
"astring": "1.9.0",
|
||||||
"broadcast-channel": "7.0.0",
|
"broadcast-channel": "7.0.0",
|
||||||
|
@ -39,11 +39,10 @@
|
||||||
"chartjs-chart-matrix": "2.0.1",
|
"chartjs-chart-matrix": "2.0.1",
|
||||||
"chartjs-plugin-gradient": "0.6.1",
|
"chartjs-plugin-gradient": "0.6.1",
|
||||||
"chartjs-plugin-zoom": "2.0.1",
|
"chartjs-plugin-zoom": "2.0.1",
|
||||||
"chromatic": "11.10.2",
|
"chromatic": "11.10.4",
|
||||||
"compare-versions": "6.1.1",
|
"compare-versions": "6.1.1",
|
||||||
"cropperjs": "2.0.0-rc.2",
|
"cropperjs": "2.0.0-rc.2",
|
||||||
"date-fns": "2.30.0",
|
"date-fns": "2.30.0",
|
||||||
"escape-regexp": "0.0.1",
|
|
||||||
"estree-walker": "3.0.3",
|
"estree-walker": "3.0.3",
|
||||||
"eventemitter3": "5.0.1",
|
"eventemitter3": "5.0.1",
|
||||||
"idb-keyval": "6.2.1",
|
"idb-keyval": "6.2.1",
|
||||||
|
@ -58,13 +57,13 @@
|
||||||
"frontend-shared": "workspace:*",
|
"frontend-shared": "workspace:*",
|
||||||
"photoswipe": "5.4.4",
|
"photoswipe": "5.4.4",
|
||||||
"punycode": "2.3.1",
|
"punycode": "2.3.1",
|
||||||
"rollup": "4.22.2",
|
"rollup": "4.22.5",
|
||||||
"sanitize-html": "2.13.0",
|
"sanitize-html": "2.13.0",
|
||||||
"sass": "1.79.3",
|
"sass": "1.79.3",
|
||||||
"shiki": "1.12.0",
|
"shiki": "1.12.0",
|
||||||
"strict-event-emitter-types": "2.0.0",
|
"strict-event-emitter-types": "2.0.0",
|
||||||
"textarea-caret": "3.1.0",
|
"textarea-caret": "3.1.0",
|
||||||
"three": "0.168.0",
|
"three": "0.169.0",
|
||||||
"throttle-debounce": "5.0.2",
|
"throttle-debounce": "5.0.2",
|
||||||
"tinycolor2": "1.6.0",
|
"tinycolor2": "1.6.0",
|
||||||
"tsc-alias": "1.8.10",
|
"tsc-alias": "1.8.10",
|
||||||
|
@ -72,32 +71,31 @@
|
||||||
"typescript": "5.6.2",
|
"typescript": "5.6.2",
|
||||||
"uuid": "10.0.0",
|
"uuid": "10.0.0",
|
||||||
"v-code-diff": "1.13.1",
|
"v-code-diff": "1.13.1",
|
||||||
"vite": "5.4.7",
|
"vite": "5.4.8",
|
||||||
"vue": "3.5.7",
|
"vue": "3.5.10",
|
||||||
"vuedraggable": "next"
|
"vuedraggable": "next"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@misskey-dev/summaly": "5.1.0",
|
"@misskey-dev/summaly": "5.1.0",
|
||||||
"@storybook/addon-actions": "8.3.2",
|
"@storybook/addon-actions": "8.3.3",
|
||||||
"@storybook/addon-essentials": "8.3.2",
|
"@storybook/addon-essentials": "8.3.3",
|
||||||
"@storybook/addon-interactions": "8.3.2",
|
"@storybook/addon-interactions": "8.3.3",
|
||||||
"@storybook/addon-links": "8.3.2",
|
"@storybook/addon-links": "8.3.3",
|
||||||
"@storybook/addon-mdx-gfm": "8.3.2",
|
"@storybook/addon-mdx-gfm": "8.3.3",
|
||||||
"@storybook/addon-storysource": "8.3.2",
|
"@storybook/addon-storysource": "8.3.3",
|
||||||
"@storybook/blocks": "8.3.2",
|
"@storybook/blocks": "8.3.3",
|
||||||
"@storybook/components": "8.3.2",
|
"@storybook/components": "8.3.3",
|
||||||
"@storybook/core-events": "8.3.2",
|
"@storybook/core-events": "8.3.3",
|
||||||
"@storybook/manager-api": "8.3.2",
|
"@storybook/manager-api": "8.3.3",
|
||||||
"@storybook/preview-api": "8.3.2",
|
"@storybook/preview-api": "8.3.3",
|
||||||
"@storybook/react": "8.3.2",
|
"@storybook/react": "8.3.3",
|
||||||
"@storybook/react-vite": "8.3.2",
|
"@storybook/react-vite": "8.3.3",
|
||||||
"@storybook/test": "8.3.2",
|
"@storybook/test": "8.3.3",
|
||||||
"@storybook/theming": "8.3.2",
|
"@storybook/theming": "8.3.3",
|
||||||
"@storybook/types": "8.3.2",
|
"@storybook/types": "8.3.3",
|
||||||
"@storybook/vue3": "8.3.2",
|
"@storybook/vue3": "8.3.3",
|
||||||
"@storybook/vue3-vite": "8.3.2",
|
"@storybook/vue3-vite": "8.3.3",
|
||||||
"@testing-library/vue": "8.1.0",
|
"@testing-library/vue": "8.1.0",
|
||||||
"@types/escape-regexp": "0.0.3",
|
|
||||||
"@types/estree": "1.0.6",
|
"@types/estree": "1.0.6",
|
||||||
"@types/matter-js": "0.19.7",
|
"@types/matter-js": "0.19.7",
|
||||||
"@types/micromatch": "4.0.9",
|
"@types/micromatch": "4.0.9",
|
||||||
|
@ -112,10 +110,10 @@
|
||||||
"@typescript-eslint/eslint-plugin": "7.17.0",
|
"@typescript-eslint/eslint-plugin": "7.17.0",
|
||||||
"@typescript-eslint/parser": "7.17.0",
|
"@typescript-eslint/parser": "7.17.0",
|
||||||
"@vitest/coverage-v8": "1.6.0",
|
"@vitest/coverage-v8": "1.6.0",
|
||||||
"@vue/runtime-core": "3.5.7",
|
"@vue/runtime-core": "3.5.10",
|
||||||
"acorn": "8.12.1",
|
"acorn": "8.12.1",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "13.14.2",
|
"cypress": "13.15.0",
|
||||||
"eslint-plugin-import": "2.30.0",
|
"eslint-plugin-import": "2.30.0",
|
||||||
"eslint-plugin-vue": "9.28.0",
|
"eslint-plugin-vue": "9.28.0",
|
||||||
"fast-glob": "3.3.2",
|
"fast-glob": "3.3.2",
|
||||||
|
@ -130,7 +128,7 @@
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"seedrandom": "3.0.5",
|
"seedrandom": "3.0.5",
|
||||||
"start-server-and-test": "2.0.8",
|
"start-server-and-test": "2.0.8",
|
||||||
"storybook": "8.3.2",
|
"storybook": "8.3.3",
|
||||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||||
"vite-plugin-turbosnap": "1.0.3",
|
"vite-plugin-turbosnap": "1.0.3",
|
||||||
"vitest": "1.6.0",
|
"vitest": "1.6.0",
|
||||||
|
|
|
@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
|
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size"/>
|
||||||
</template>
|
</template>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
<div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }]" :style="{ textAlign: c.align, backgroundColor: c.bgColor, color: c.fgColor, borderWidth: c.borderWidth ? `${c.borderWidth}px` : 0, borderColor: c.borderColor ?? 'var(--divider)', padding: c.padding ? `${c.padding}px` : 0, borderRadius: c.rounded ? '8px' : 0 }">
|
<div v-else-if="c.type === 'container'" :class="[$style.container, { [$style.fontSerif]: c.font === 'serif', [$style.fontMonospace]: c.font === 'monospace' }]" :style="containerStyle">
|
||||||
<template v-for="child in c.children" :key="child">
|
<template v-for="child in c.children" :key="child">
|
||||||
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size" :align="c.align"/>
|
<MkAsUi v-if="!g(child).hidden" :component="g(child)" :components="props.components" :size="size" :align="c.align"/>
|
||||||
</template>
|
</template>
|
||||||
|
@ -63,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Ref, ref } from 'vue';
|
import { Ref, ref, computed } from 'vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
|
@ -97,6 +97,29 @@ function g(id) {
|
||||||
} as AsUiRoot;
|
} as AsUiRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const containerStyle = computed(() => {
|
||||||
|
if (c.type !== 'container') return undefined;
|
||||||
|
|
||||||
|
// width, color, styleのうち一つでも指定があれば、枠線がちゃんと表示されるようにwidthとstyleのデフォルト値を設定
|
||||||
|
// radiusは単に角を丸める用途もあるため除外
|
||||||
|
const isBordered = c.borderWidth ?? c.borderColor ?? c.borderStyle;
|
||||||
|
|
||||||
|
const border = isBordered ? {
|
||||||
|
borderWidth: c.borderWidth ?? '1px',
|
||||||
|
borderColor: c.borderColor ?? 'var(--divider)',
|
||||||
|
borderStyle: c.borderStyle ?? 'solid',
|
||||||
|
} : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
textAlign: c.align,
|
||||||
|
backgroundColor: c.bgColor,
|
||||||
|
color: c.fgColor,
|
||||||
|
padding: c.padding ? `${c.padding}px` : 0,
|
||||||
|
borderRadius: (c.borderRadius ?? (c.rounded ? 8 : 0)) + 'px',
|
||||||
|
...border,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const valueForSwitch = ref('default' in c && typeof c.default === 'boolean' ? c.default : false);
|
const valueForSwitch = ref('default' in c && typeof c.default === 'boolean' ? c.default : false);
|
||||||
|
|
||||||
function onSwitchUpdate(v) {
|
function onSwitchUpdate(v) {
|
||||||
|
|
|
@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<span v-if="full" :class="$style.text">{{ i18n.ts.processing }}</span><MkLoading :em="true" :colored="false"/>
|
<span v-if="full" :class="$style.text">{{ i18n.ts.processing }}</span><MkLoading :em="true" :colored="false"/>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="isFollowing">
|
<template v-else-if="isFollowing">
|
||||||
<span v-if="full" :class="$style.text">{{ i18n.ts.unfollow }}</span><i class="ti ti-minus"></i>
|
<span v-if="full" :class="$style.text">{{ i18n.ts.youFollowing }}</span><i class="ti ti-minus"></i>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="!isFollowing && user.isLocked">
|
<template v-else-if="!isFollowing && user.isLocked">
|
||||||
<span v-if="full" :class="$style.text">{{ i18n.ts.followRequest }}</span><i class="ti ti-plus"></i>
|
<span v-if="full" :class="$style.text">{{ i18n.ts.followRequest }}</span><i class="ti ti-plus"></i>
|
||||||
|
|
|
@ -119,7 +119,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<i class="ti ti-ban"></i>
|
<i class="ti ti-ban"></i>
|
||||||
</button>
|
</button>
|
||||||
<button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()">
|
<button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()">
|
||||||
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i>
|
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--love);"></i>
|
||||||
<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i>
|
<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i>
|
||||||
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
|
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
|
||||||
<i v-else class="ti ti-plus"></i>
|
<i v-else class="ti ti-plus"></i>
|
||||||
|
|
|
@ -128,7 +128,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<i class="ti ti-ban"></i>
|
<i class="ti ti-ban"></i>
|
||||||
</button>
|
</button>
|
||||||
<button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()">
|
<button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()">
|
||||||
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--eventReactionHeart);"></i>
|
<i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--love);"></i>
|
||||||
<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i>
|
<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i>
|
||||||
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
|
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
|
||||||
<i v-else class="ti ti-plus"></i>
|
<i v-else class="ti ti-plus"></i>
|
||||||
|
|
|
@ -5,6 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header :class="$style.root">
|
<header :class="$style.root">
|
||||||
|
<component :is="defaultStore.state.enableCondensedLine ? 'MkCondensedLine' : 'div'" :minScale="0" style="min-width: 0;">
|
||||||
|
<div style="display: flex; white-space: nowrap; align-items: baseline;">
|
||||||
<div v-if="mock" :class="$style.name">
|
<div v-if="mock" :class="$style.name">
|
||||||
<MkUserName :user="note.user"/>
|
<MkUserName :user="note.user"/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,6 +15,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</MkA>
|
</MkA>
|
||||||
<div v-if="note.user.isBot" :class="$style.isBot">bot</div>
|
<div v-if="note.user.isBot" :class="$style.isBot">bot</div>
|
||||||
<div :class="$style.username"><MkAcct :user="note.user"/></div>
|
<div :class="$style.username"><MkAcct :user="note.user"/></div>
|
||||||
|
</div>
|
||||||
|
</component>
|
||||||
<div v-if="note.user.badgeRoles" :class="$style.badgeRoles">
|
<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!"/>
|
<img v-for="(role, i) in note.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl!"/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -40,6 +44,7 @@ import * as Misskey from 'misskey-js';
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { notePage } from '@/filters/note.js';
|
import { notePage } from '@/filters/note.js';
|
||||||
import { userPage } from '@/filters/user.js';
|
import { userPage } from '@/filters/user.js';
|
||||||
|
import { defaultStore } from '@/store.js';
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
note: Misskey.entities.Note;
|
note: Misskey.entities.Note;
|
||||||
|
|
|
@ -108,7 +108,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template v-else-if="notification.type === 'follow'">
|
<template v-else-if="notification.type === 'follow'">
|
||||||
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}</span>
|
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}</span>
|
||||||
</template>
|
</template>
|
||||||
<span v-else-if="notification.type === 'followRequestAccepted'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span>
|
<template v-else-if="notification.type === 'followRequestAccepted'">
|
||||||
|
<div :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</div>
|
||||||
|
<div v-if="notification.message" :class="$style.text" style="opacity: 0.6; font-style: oblique;">
|
||||||
|
<i class="ti ti-quote" :class="$style.quote"></i>
|
||||||
|
<span>{{ notification.message }}</span>
|
||||||
|
<i class="ti ti-quote" :class="$style.quote"></i>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<template v-else-if="notification.type === 'receiveFollowRequest'">
|
<template v-else-if="notification.type === 'receiveFollowRequest'">
|
||||||
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}</span>
|
<span :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}</span>
|
||||||
<div v-if="full && !followRequestDone" :class="$style.followRequestCommands">
|
<div v-if="full && !followRequestDone" :class="$style.followRequestCommands">
|
||||||
|
@ -211,6 +218,14 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
display: flex;
|
display: flex;
|
||||||
contain: content;
|
contain: content;
|
||||||
|
|
||||||
|
--eventFollow: #36aed2;
|
||||||
|
--eventRenote: #36d298;
|
||||||
|
--eventReply: #007aff;
|
||||||
|
--eventReactionHeart: var(--love);
|
||||||
|
--eventReaction: #e99a0b;
|
||||||
|
--eventAchievement: #cb9a11;
|
||||||
|
--eventOther: #88a6b7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.head {
|
.head {
|
||||||
|
|
|
@ -4,11 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<MkCondensedLine v-if="defaultStore.state.enableCondensedLineForAcct" :minScale="2 / 3">
|
<span>
|
||||||
<span>@{{ user.username }}</span>
|
|
||||||
<span v-if="user.host || detail || defaultStore.state.showFullAcct" style="opacity: 0.5;">@{{ user.host || host }}</span>
|
|
||||||
</MkCondensedLine>
|
|
||||||
<span v-else>
|
|
||||||
<span>@{{ user.username }}</span>
|
<span>@{{ user.username }}</span>
|
||||||
<span v-if="user.host || detail || defaultStore.state.showFullAcct" style="opacity: 0.5;">@{{ user.host || host }}</span>
|
<span v-if="user.host || detail || defaultStore.state.showFullAcct" style="opacity: 0.5;">@{{ user.host || host }}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -210,6 +210,31 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder>
|
||||||
|
<template #icon><i class="ti ti-planet"></i></template>
|
||||||
|
<template #label>{{ i18n.ts.federation }}</template>
|
||||||
|
<template v-if="federationForm.savedState.federation === 'all'" #suffix>{{ i18n.ts.all }}</template>
|
||||||
|
<template v-else-if="federationForm.savedState.federation === 'specified'" #suffix>{{ i18n.ts.specifyHost }}</template>
|
||||||
|
<template v-else-if="federationForm.savedState.federation === 'none'" #suffix>{{ i18n.ts.none }}</template>
|
||||||
|
<template v-if="federationForm.modified.value" #footer>
|
||||||
|
<MkFormFooter :form="federationForm"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="_gaps">
|
||||||
|
<MkRadios v-model="federationForm.state.federation">
|
||||||
|
<template #label>{{ i18n.ts.behavior }}<span v-if="federationForm.modifiedStates.federation" class="_modified">{{ i18n.ts.modified }}</span></template>
|
||||||
|
<option value="all">{{ i18n.ts.all }}</option>
|
||||||
|
<option value="specified">{{ i18n.ts.specifyHost }}</option>
|
||||||
|
<option value="none">{{ i18n.ts.none }}</option>
|
||||||
|
</MkRadios>
|
||||||
|
|
||||||
|
<MkTextarea v-if="federationForm.state.federation === 'specified'" v-model="federationForm.state.federationHosts">
|
||||||
|
<template #label>{{ i18n.ts.federationAllowedHosts }}<span v-if="federationForm.modifiedStates.federationHosts" class="_modified">{{ i18n.ts.modified }}</span></template>
|
||||||
|
<template #caption>{{ i18n.ts.federationAllowedHostsDescription }}</template>
|
||||||
|
</MkTextarea>
|
||||||
|
</div>
|
||||||
|
</MkFolder>
|
||||||
|
|
||||||
<MkFolder>
|
<MkFolder>
|
||||||
<template #icon><i class="ti ti-ghost"></i></template>
|
<template #icon><i class="ti ti-ghost"></i></template>
|
||||||
<template #label>{{ i18n.ts.proxyAccount }}</template>
|
<template #label>{{ i18n.ts.proxyAccount }}</template>
|
||||||
|
@ -248,6 +273,7 @@ import MkFolder from '@/components/MkFolder.vue';
|
||||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||||
import { useForm } from '@/scripts/use-form.js';
|
import { useForm } from '@/scripts/use-form.js';
|
||||||
import MkFormFooter from '@/components/MkFormFooter.vue';
|
import MkFormFooter from '@/components/MkFormFooter.vue';
|
||||||
|
import MkRadios from '@/components/MkRadios.vue';
|
||||||
|
|
||||||
const meta = await misskeyApi('admin/meta');
|
const meta = await misskeyApi('admin/meta');
|
||||||
|
|
||||||
|
@ -341,6 +367,17 @@ const urlPreviewForm = useForm({
|
||||||
fetchInstance(true);
|
fetchInstance(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const federationForm = useForm({
|
||||||
|
federation: meta.federation,
|
||||||
|
federationHosts: meta.federationHosts.join('\n'),
|
||||||
|
}, async (state) => {
|
||||||
|
await os.apiWithDialog('admin/update-meta', {
|
||||||
|
federation: state.federation,
|
||||||
|
federationHosts: state.federationHosts.split('\n'),
|
||||||
|
});
|
||||||
|
fetchInstance(true);
|
||||||
|
});
|
||||||
|
|
||||||
function chooseProxyAccount() {
|
function chooseProxyAccount() {
|
||||||
os.selectUser({ localOnly: true }).then(user => {
|
os.selectUser({ localOnly: true }).then(user => {
|
||||||
proxyAccount.value = user;
|
proxyAccount.value = user;
|
||||||
|
|
|
@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkContainer :foldable="true" :expanded="false">
|
<MkContainer :foldable="true" :expanded="false">
|
||||||
<template #header>{{ i18n.ts.uiInspector }}</template>
|
<template #header>{{ i18n.ts.uiInspector }}</template>
|
||||||
<div :class="$style.uiInspector">
|
<div :class="$style.uiInspector">
|
||||||
<div v-for="c in components" :key="c.value.id">
|
<div v-for="c in components" :key="c.value.id" :class="{ [$style.uiInspectorUnShown]: !showns.has(c.value.id) }">
|
||||||
<div :class="$style.uiInspectorType">{{ c.value.type }}</div>
|
<div :class="$style.uiInspectorType">{{ c.value.type }}</div>
|
||||||
<div :class="$style.uiInspectorId">{{ c.value.id }}</div>
|
<div :class="$style.uiInspectorId">{{ c.value.id }}</div>
|
||||||
<button :class="$style.uiInspectorPropsToggle" @click="() => uiInspectorOpenedComponents.set(c, !uiInspectorOpenedComponents.get(c))">
|
<button :class="$style.uiInspectorPropsToggle" @click="() => uiInspectorOpenedComponents.set(c, !uiInspectorOpenedComponents.get(c))">
|
||||||
|
@ -180,6 +180,20 @@ const headerActions = computed(() => []);
|
||||||
|
|
||||||
const headerTabs = computed(() => []);
|
const headerTabs = computed(() => []);
|
||||||
|
|
||||||
|
const showns = computed(() => {
|
||||||
|
const result = new Set<string>();
|
||||||
|
(function addChildrenToResult(c: AsUiComponent) {
|
||||||
|
result.add(c.id);
|
||||||
|
if (c.children) {
|
||||||
|
const childComponents = components.value.filter(v => c.children.includes(v.value.id));
|
||||||
|
for (const child of childComponents) {
|
||||||
|
addChildrenToResult(child.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})(root.value);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
definePageMetadata(() => ({
|
definePageMetadata(() => ({
|
||||||
title: i18n.ts.scratchpad,
|
title: i18n.ts.scratchpad,
|
||||||
icon: 'ti ti-terminal-2',
|
icon: 'ti ti-terminal-2',
|
||||||
|
@ -227,6 +241,10 @@ definePageMetadata(() => ({
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.uiInspectorUnShown {
|
||||||
|
color: var(--fgTransparent);
|
||||||
|
}
|
||||||
|
|
||||||
.uiInspectorType {
|
.uiInspectorType {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
border: hidden;
|
border: hidden;
|
||||||
|
|
|
@ -51,8 +51,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #label>{{ i18n.ts.experimentalFeatures }}</template>
|
<template #label>{{ i18n.ts.experimentalFeatures }}</template>
|
||||||
|
|
||||||
<div class="_gaps_m">
|
<div class="_gaps_m">
|
||||||
<MkSwitch v-model="enableCondensedLineForAcct">
|
<MkSwitch v-model="enableCondensedLine">
|
||||||
<template #label>Enable condensed line for acct</template>
|
<template #label>Enable condensed line</template>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
@ -104,7 +104,7 @@ import FormSection from '@/components/form/section.vue';
|
||||||
const $i = signinRequired();
|
const $i = signinRequired();
|
||||||
|
|
||||||
const reportError = computed(defaultStore.makeGetterSetter('reportError'));
|
const reportError = computed(defaultStore.makeGetterSetter('reportError'));
|
||||||
const enableCondensedLineForAcct = computed(defaultStore.makeGetterSetter('enableCondensedLineForAcct'));
|
const enableCondensedLine = computed(defaultStore.makeGetterSetter('enableCondensedLine'));
|
||||||
const devMode = computed(defaultStore.makeGetterSetter('devMode'));
|
const devMode = computed(defaultStore.makeGetterSetter('devMode'));
|
||||||
const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies'));
|
const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies'));
|
||||||
|
|
||||||
|
@ -142,12 +142,6 @@ async function updateRepliesAll(withReplies: boolean) {
|
||||||
misskeyApi('following/update-all', { withReplies });
|
misskeyApi('following/update-all', { withReplies });
|
||||||
}
|
}
|
||||||
|
|
||||||
watch([
|
|
||||||
enableCondensedLineForAcct,
|
|
||||||
], async () => {
|
|
||||||
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
const headerActions = computed(() => []);
|
const headerActions = computed(() => []);
|
||||||
|
|
||||||
const headerTabs = computed(() => []);
|
const headerTabs = computed(() => []);
|
||||||
|
|
|
@ -103,7 +103,6 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
|
||||||
'mediaListWithOneImageAppearance',
|
'mediaListWithOneImageAppearance',
|
||||||
'notificationPosition',
|
'notificationPosition',
|
||||||
'notificationStackAxis',
|
'notificationStackAxis',
|
||||||
'enableCondensedLineForAcct',
|
|
||||||
'keepScreenOn',
|
'keepScreenOn',
|
||||||
'defaultWithReplies',
|
'defaultWithReplies',
|
||||||
'disableStreamingTimeline',
|
'disableStreamingTimeline',
|
||||||
|
|
|
@ -88,14 +88,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #caption>{{ i18n.ts._profile.metadataDescription }}</template>
|
<template #caption>{{ i18n.ts._profile.metadataDescription }}</template>
|
||||||
</FormSlot>
|
</FormSlot>
|
||||||
|
|
||||||
<MkFolder>
|
<MkInput v-model="profile.followedMessage" :max="200" manualSave :mfmPreview="false">
|
||||||
<template #label>{{ i18n.ts.advancedSettings }}</template>
|
<template #label>{{ i18n.ts._profile.followedMessage }}<span class="_beta">{{ i18n.ts.beta }}</span></template>
|
||||||
|
<template #caption>
|
||||||
<div class="_gaps_m">
|
<div>{{ i18n.ts._profile.followedMessageDescription }}</div>
|
||||||
<MkSwitch v-model="profile.isCat">{{ i18n.ts.flagAsCat }}<template #caption>{{ i18n.ts.flagAsCatDescription }}</template></MkSwitch>
|
<div>{{ i18n.ts._profile.followedMessageDescriptionForLockedAccount }}</div>
|
||||||
<MkSwitch v-model="profile.isBot">{{ i18n.ts.flagAsBot }}<template #caption>{{ i18n.ts.flagAsBotDescription }}</template></MkSwitch>
|
</template>
|
||||||
</div>
|
</MkInput>
|
||||||
</MkFolder>
|
|
||||||
|
|
||||||
<MkSelect v-model="reactionAcceptance">
|
<MkSelect v-model="reactionAcceptance">
|
||||||
<template #label>{{ i18n.ts.reactionAcceptance }}</template>
|
<template #label>{{ i18n.ts.reactionAcceptance }}</template>
|
||||||
|
@ -105,6 +104,15 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<option value="nonSensitiveOnlyForLocalLikeOnlyForRemote">{{ i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote }}</option>
|
<option value="nonSensitiveOnlyForLocalLikeOnlyForRemote">{{ i18n.ts.nonSensitiveOnlyForLocalLikeOnlyForRemote }}</option>
|
||||||
<option value="likeOnly">{{ i18n.ts.likeOnly }}</option>
|
<option value="likeOnly">{{ i18n.ts.likeOnly }}</option>
|
||||||
</MkSelect>
|
</MkSelect>
|
||||||
|
|
||||||
|
<MkFolder>
|
||||||
|
<template #label>{{ i18n.ts.advancedSettings }}</template>
|
||||||
|
|
||||||
|
<div class="_gaps_m">
|
||||||
|
<MkSwitch v-model="profile.isCat">{{ i18n.ts.flagAsCat }}<template #caption>{{ i18n.ts.flagAsCatDescription }}</template></MkSwitch>
|
||||||
|
<MkSwitch v-model="profile.isBot">{{ i18n.ts.flagAsBot }}<template #caption>{{ i18n.ts.flagAsBotDescription }}</template></MkSwitch>
|
||||||
|
</div>
|
||||||
|
</MkFolder>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -138,6 +146,7 @@ const reactionAcceptance = computed(defaultStore.makeGetterSetter('reactionAccep
|
||||||
const profile = reactive({
|
const profile = reactive({
|
||||||
name: $i.name,
|
name: $i.name,
|
||||||
description: $i.description,
|
description: $i.description,
|
||||||
|
followedMessage: $i.followedMessage,
|
||||||
location: $i.location,
|
location: $i.location,
|
||||||
birthday: $i.birthday,
|
birthday: $i.birthday,
|
||||||
lang: $i.lang,
|
lang: $i.lang,
|
||||||
|
@ -185,6 +194,8 @@ function save() {
|
||||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
description: profile.description || null,
|
description: profile.description || null,
|
||||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
|
followedMessage: profile.followedMessage || null,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
location: profile.location || null,
|
location: profile.location || null,
|
||||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||||
birthday: profile.birthday || null,
|
birthday: profile.birthday || null,
|
||||||
|
|
|
@ -47,6 +47,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span>
|
<span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="user.followedMessage != null" class="followedMessage">
|
||||||
|
<div style="border: solid 1px var(--love); border-radius: 6px; background: color-mix(in srgb, var(--love), transparent 90%); padding: 6px 8px;">
|
||||||
|
<Mfm :text="user.followedMessage" :author="user"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div v-if="user.roles.length > 0" class="roles">
|
<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 }">
|
<span v-for="role in user.roles" :key="role.id" v-tooltip="role.description" class="role" :style="{ '--color': role.color }">
|
||||||
<MkA v-adaptive-bg :to="`/roles/${role.id}`">
|
<MkA v-adaptive-bg :to="`/roles/${role.id}`">
|
||||||
|
@ -460,6 +465,11 @@ onUnmounted(() => {
|
||||||
box-shadow: 1px 1px 3px rgba(#000, 0.2);
|
box-shadow: 1px 1px 3px rgba(#000, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .followedMessage {
|
||||||
|
padding: 24px 24px 0 154px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
> .roles {
|
> .roles {
|
||||||
padding: 24px 24px 0 154px;
|
padding: 24px 24px 0 154px;
|
||||||
font-size: 0.95em;
|
font-size: 0.95em;
|
||||||
|
@ -642,6 +652,10 @@ onUnmounted(() => {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
> .followedMessage {
|
||||||
|
padding: 16px 16px 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
> .roles {
|
> .roles {
|
||||||
padding: 16px 16px 0 16px;
|
padding: 16px 16px 0 16px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
|
@ -27,6 +27,8 @@ export type AsUiContainer = AsUiComponentBase & {
|
||||||
font?: 'serif' | 'sans-serif' | 'monospace';
|
font?: 'serif' | 'sans-serif' | 'monospace';
|
||||||
borderWidth?: number;
|
borderWidth?: number;
|
||||||
borderColor?: string;
|
borderColor?: string;
|
||||||
|
borderStyle?: 'hidden' | 'dotted' | 'dashed' | 'solid' | 'double' | 'groove' | 'ridge' | 'inset' | 'outset';
|
||||||
|
borderRadius?: number;
|
||||||
padding?: number;
|
padding?: number;
|
||||||
rounded?: boolean;
|
rounded?: boolean;
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
|
@ -173,6 +175,10 @@ function getContainerOptions(def: values.Value | undefined): Omit<AsUiContainer,
|
||||||
if (borderWidth) utils.assertNumber(borderWidth);
|
if (borderWidth) utils.assertNumber(borderWidth);
|
||||||
const borderColor = def.value.get('borderColor');
|
const borderColor = def.value.get('borderColor');
|
||||||
if (borderColor) utils.assertString(borderColor);
|
if (borderColor) utils.assertString(borderColor);
|
||||||
|
const borderStyle = def.value.get('borderStyle');
|
||||||
|
if (borderStyle) utils.assertString(borderStyle);
|
||||||
|
const borderRadius = def.value.get('borderRadius');
|
||||||
|
if (borderRadius) utils.assertNumber(borderRadius);
|
||||||
const padding = def.value.get('padding');
|
const padding = def.value.get('padding');
|
||||||
if (padding) utils.assertNumber(padding);
|
if (padding) utils.assertNumber(padding);
|
||||||
const rounded = def.value.get('rounded');
|
const rounded = def.value.get('rounded');
|
||||||
|
@ -191,6 +197,8 @@ function getContainerOptions(def: values.Value | undefined): Omit<AsUiContainer,
|
||||||
font: font?.value,
|
font: font?.value,
|
||||||
borderWidth: borderWidth?.value,
|
borderWidth: borderWidth?.value,
|
||||||
borderColor: borderColor?.value,
|
borderColor: borderColor?.value,
|
||||||
|
borderStyle: borderStyle?.value,
|
||||||
|
borderRadius: borderRadius?.value,
|
||||||
padding: padding?.value,
|
padding: padding?.value,
|
||||||
rounded: rounded?.value,
|
rounded: rounded?.value,
|
||||||
hidden: hidden?.value,
|
hidden: hidden?.value,
|
||||||
|
|
|
@ -392,9 +392,9 @@ export const defaultStore = markRaw(new Storage('base', {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: 'horizontal' as 'vertical' | 'horizontal',
|
default: 'horizontal' as 'vertical' | 'horizontal',
|
||||||
},
|
},
|
||||||
enableCondensedLineForAcct: {
|
enableCondensedLine: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
default: false,
|
default: true,
|
||||||
},
|
},
|
||||||
additionalUnicodeEmojiIndexes: {
|
additionalUnicodeEmojiIndexes: {
|
||||||
where: 'device',
|
where: 'device',
|
||||||
|
|
|
@ -18,13 +18,6 @@
|
||||||
--minBottomSpacing: var(--minBottomSpacingMobile);
|
--minBottomSpacing: var(--minBottomSpacingMobile);
|
||||||
|
|
||||||
//--ad: rgb(255 169 0 / 10%);
|
//--ad: rgb(255 169 0 / 10%);
|
||||||
--eventFollow: #36aed2;
|
|
||||||
--eventRenote: #36d298;
|
|
||||||
--eventReply: #007aff;
|
|
||||||
--eventReactionHeart: #dd2e44;
|
|
||||||
--eventReaction: #e99a0b;
|
|
||||||
--eventAchievement: #cb9a11;
|
|
||||||
--eventOther: #88a6b7;
|
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
@media (max-width: 500px) {
|
||||||
--margin: var(--marginHalf);
|
--margin: var(--marginHalf);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"name": "misskey-js",
|
"name": "misskey-js",
|
||||||
"version": "2024.9.0-alpha.10",
|
"version": "2024.9.0-alpha.12",
|
||||||
"description": "Misskey SDK for JavaScript",
|
"description": "Misskey SDK for JavaScript",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "./built/index.js",
|
"main": "./built/index.js",
|
||||||
|
|
|
@ -3789,6 +3789,7 @@ export type components = {
|
||||||
/** @default false */
|
/** @default false */
|
||||||
securityKeys: boolean;
|
securityKeys: boolean;
|
||||||
roles: components['schemas']['RoleLite'][];
|
roles: components['schemas']['RoleLite'][];
|
||||||
|
followedMessage?: string | null;
|
||||||
memo: string | null;
|
memo: string | null;
|
||||||
moderationNote?: string;
|
moderationNote?: string;
|
||||||
isFollowing?: boolean;
|
isFollowing?: boolean;
|
||||||
|
@ -3808,6 +3809,7 @@ export type components = {
|
||||||
avatarId: string | null;
|
avatarId: string | null;
|
||||||
/** Format: id */
|
/** Format: id */
|
||||||
bannerId: string | null;
|
bannerId: string | null;
|
||||||
|
followedMessage: string | null;
|
||||||
isModerator: boolean | null;
|
isModerator: boolean | null;
|
||||||
isAdmin: boolean | null;
|
isAdmin: boolean | null;
|
||||||
injectFeaturedNote: boolean;
|
injectFeaturedNote: boolean;
|
||||||
|
@ -4247,7 +4249,7 @@ export type components = {
|
||||||
user: components['schemas']['UserLite'];
|
user: components['schemas']['UserLite'];
|
||||||
/** Format: id */
|
/** Format: id */
|
||||||
userId: string;
|
userId: string;
|
||||||
} | {
|
} | ({
|
||||||
/** Format: id */
|
/** Format: id */
|
||||||
id: string;
|
id: string;
|
||||||
/** Format: date-time */
|
/** Format: date-time */
|
||||||
|
@ -4257,7 +4259,8 @@ export type components = {
|
||||||
user: components['schemas']['UserLite'];
|
user: components['schemas']['UserLite'];
|
||||||
/** Format: id */
|
/** Format: id */
|
||||||
userId: string;
|
userId: string;
|
||||||
} | {
|
message: string | null;
|
||||||
|
}) | {
|
||||||
/** Format: id */
|
/** Format: id */
|
||||||
id: string;
|
id: string;
|
||||||
/** Format: date-time */
|
/** Format: date-time */
|
||||||
|
@ -8936,6 +8939,7 @@ export type operations = {
|
||||||
'application/json': {
|
'application/json': {
|
||||||
email: string | null;
|
email: string | null;
|
||||||
emailVerified: boolean;
|
emailVerified: boolean;
|
||||||
|
followedMessage: string | null;
|
||||||
autoAcceptFollowed: boolean;
|
autoAcceptFollowed: boolean;
|
||||||
noCrawle: boolean;
|
noCrawle: boolean;
|
||||||
preventAiLearning: boolean;
|
preventAiLearning: boolean;
|
||||||
|
@ -19676,6 +19680,7 @@ export type operations = {
|
||||||
'application/json': {
|
'application/json': {
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
|
followedMessage?: string | null;
|
||||||
location?: string | null;
|
location?: string | null;
|
||||||
birthday?: string | null;
|
birthday?: string | null;
|
||||||
/** @enum {string|null} */
|
/** @enum {string|null} */
|
||||||
|
|
1173
pnpm-lock.yaml
1173
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue