Merge remote-tracking branch 'misskey-dev/develop' into io

This commit is contained in:
まっちゃとーにゅ 2024-02-25 03:36:45 +09:00
commit e4ee9580e3
No known key found for this signature in database
GPG Key ID: 6AFBBF529601C1DB
88 changed files with 1371 additions and 871 deletions

View File

@ -14,6 +14,7 @@
## 202x.x.x (unreleased) ## 202x.x.x (unreleased)
### General ### General
- Enhance: サーバーごとにモデレーションノートを残せるように
### Client ### Client
- Enhance: ノート作成画面のファイル添付メニューの区切り線の位置を調整 - Enhance: ノート作成画面のファイル添付メニューの区切り線の位置を調整
@ -21,9 +22,11 @@
- Fix: MFMのオートコンプリートが出るべき状況で出ないことがある問題を修正 - Fix: MFMのオートコンプリートが出るべき状況で出ないことがある問題を修正
- Fix: チャートのラベルが消えている問題を修正 - Fix: チャートのラベルが消えている問題を修正
- Fix: 画面表示後最初の音声再生が爆音になることがある問題を修正 - Fix: 画面表示後最初の音声再生が爆音になることがある問題を修正
- Fix: 絵文字サジェストの順位で、絵文字自体の名前が同じものよりもタグで一致しているものが優先されてしまう問題を修正
### Server ### Server
- Fix: nodeinfoにenableMcaptchaとenableTurnstileが無いのを修正 - Fix: nodeinfoにenableMcaptchaとenableTurnstileが無いのを修正
- エンドポイント`flash/update`の`flashId`以外のパラメータは必須ではなくなりました
- Fix: 禁止キーワードを含むートがDelayed Queueに追加されて再処理される問題を修正 - Fix: 禁止キーワードを含むートがDelayed Queueに追加されて再処理される問題を修正
## 2024.2.0 ## 2024.2.0

68
locales/index.d.ts vendored
View File

@ -4900,6 +4900,14 @@ export interface Locale extends ILocale {
* *
*/ */
"replaying": string; "replaying": string;
/**
*
*/
"endReplay": string;
/**
*
*/
"copyReplayData": string;
/** /**
* *
*/ */
@ -4928,6 +4936,18 @@ export interface Locale extends ILocale {
* *
*/ */
"enableHorizontalSwipe": string; "enableHorizontalSwipe": string;
/**
*
*/
"loading": string;
/**
*
*/
"surrender": string;
/**
*
*/
"gameRetry": string;
/** /**
* *
*/ */
@ -4953,6 +4973,40 @@ export interface Locale extends ILocale {
* *
*/ */
"howToPlay": string; "howToPlay": string;
/**
*
*/
"hold": string;
"_score": {
/**
*
*/
"score": string;
/**
*
*/
"scoreYen": string;
/**
*
*/
"highScore": string;
/**
*
*/
"maxChain": string;
/**
* {yen}
*/
"yen": ParameterizedString<"yen">;
/**
* {qty}
*/
"estimatedQty": ParameterizedString<"qty">;
/**
* {onigiriQtyWithUnit}
*/
"scoreSweets": ParameterizedString<"onigiriQtyWithUnit">;
};
"_howToPlay": { "_howToPlay": {
/** /**
* 調 * 調
@ -9400,7 +9454,7 @@ export interface Locale extends ILocale {
*/ */
"updateServerSettings": string; "updateServerSettings": string;
/** /**
* *
*/ */
"updateUserNote": string; "updateUserNote": string;
/** /**
@ -9447,6 +9501,10 @@ export interface Locale extends ILocale {
* *
*/ */
"unsuspendRemoteInstance": string; "unsuspendRemoteInstance": string;
/**
*
*/
"updateRemoteInstanceNote": string;
/** /**
* *
*/ */
@ -9883,6 +9941,14 @@ export interface Locale extends ILocale {
* *
*/ */
"disallowIrregularRules": string; "disallowIrregularRules": string;
/**
*
*/
"showBoardLabels": string;
/**
*
*/
"useAvatarAsStone": string;
}; };
"_offlineScreen": { "_offlineScreen": {
/** /**

View File

@ -1221,6 +1221,8 @@ soundWillBePlayed: "サウンドが再生されます"
showReplay: "リプレイを見る" showReplay: "リプレイを見る"
replay: "リプレイ" replay: "リプレイ"
replaying: "リプレイ中" replaying: "リプレイ中"
endReplay: "リプレイを終了"
copyReplayData: "リプレイデータをコピー"
ranking: "ランキング" ranking: "ランキング"
lastNDays: "直近{n}日" lastNDays: "直近{n}日"
backToTitle: "タイトルへ" backToTitle: "タイトルへ"
@ -1228,6 +1230,9 @@ hemisphere: "お住まいの地域"
withSensitive: "センシティブなファイルを含むノートを表示" withSensitive: "センシティブなファイルを含むノートを表示"
userSaysSomethingSensitive: "{name}のセンシティブなファイルを含む投稿" userSaysSomethingSensitive: "{name}のセンシティブなファイルを含む投稿"
enableHorizontalSwipe: "スワイプしてタブを切り替える" enableHorizontalSwipe: "スワイプしてタブを切り替える"
loading: "読み込み中"
surrender: "やめる"
gameRetry: "リトライ"
abuseReportCategory: "通報の種類" abuseReportCategory: "通報の種類"
selectCategory: "カテゴリを選択" selectCategory: "カテゴリを選択"
reportComplete: "通報完了" reportComplete: "通報完了"
@ -1236,6 +1241,15 @@ muteThisUser: "このユーザーをミュートする"
_bubbleGame: _bubbleGame:
howToPlay: "遊び方" howToPlay: "遊び方"
hold: "ホールド"
_score:
score: "スコア"
scoreYen: "稼いだ金額"
highScore: "ハイスコア"
maxChain: "最大チェーン数"
yen: "{yen}円"
estimatedQty: "{qty}個分"
scoreSweets: "おにぎり {onigiriQtyWithUnit}"
_howToPlay: _howToPlay:
section1: "位置を調整してハコにモノを落とします。" section1: "位置を調整してハコにモノを落とします。"
section2: "同じ種類のモノがくっつくと別のモノに変化して、スコアが得られます。" section2: "同じ種類のモノがくっつくと別のモノに変化して、スコアが得られます。"
@ -2496,7 +2510,7 @@ _moderationLogTypes:
updateCustomEmoji: "カスタム絵文字更新" updateCustomEmoji: "カスタム絵文字更新"
deleteCustomEmoji: "カスタム絵文字削除" deleteCustomEmoji: "カスタム絵文字削除"
updateServerSettings: "サーバー設定更新" updateServerSettings: "サーバー設定更新"
updateUserNote: "モデレーションノート更新" updateUserNote: "ユーザーのモデレーションノート更新"
deleteDriveFile: "ファイルを削除" deleteDriveFile: "ファイルを削除"
deleteNote: "ノートを削除" deleteNote: "ノートを削除"
createGlobalAnnouncement: "全体のお知らせを作成" createGlobalAnnouncement: "全体のお知らせを作成"
@ -2508,6 +2522,7 @@ _moderationLogTypes:
resetPassword: "パスワードをリセット" resetPassword: "パスワードをリセット"
suspendRemoteInstance: "リモートサーバーを停止" suspendRemoteInstance: "リモートサーバーを停止"
unsuspendRemoteInstance: "リモートサーバーを再開" unsuspendRemoteInstance: "リモートサーバーを再開"
updateRemoteInstanceNote: "リモートサーバーのモデレーションノート更新"
markSensitiveDriveFile: "ファイルをセンシティブ付与" markSensitiveDriveFile: "ファイルをセンシティブ付与"
unmarkSensitiveDriveFile: "ファイルをセンシティブ解除" unmarkSensitiveDriveFile: "ファイルをセンシティブ解除"
resolveAbuseReport: "通報を解決" resolveAbuseReport: "通報を解決"
@ -2633,6 +2648,8 @@ _reversi:
opponentHasSettingsChanged: "相手が設定を変更しました" opponentHasSettingsChanged: "相手が設定を変更しました"
allowIrregularRules: "変則許可 (完全フリー)" allowIrregularRules: "変則許可 (完全フリー)"
disallowIrregularRules: "変則なし" disallowIrregularRules: "変則なし"
showBoardLabels: "盤面に行・列番号を表示"
useAvatarAsStone: "石をアイコンにする"
_offlineScreen: _offlineScreen:
title: "オフライン - サーバーに接続できません" title: "オフライン - サーバーに接続できません"

View File

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export class PerInstanceModNote1708399372194 {
name = 'PerInstanceModNote1708399372194'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "instance" ADD "moderationNote" character varying(16384) NOT NULL DEFAULT ''`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "moderationNote"`);
}
}

View File

@ -118,6 +118,7 @@
"got": "14.2.0", "got": "14.2.0",
"happy-dom": "10.0.3", "happy-dom": "10.0.3",
"hpagent": "1.2.0", "hpagent": "1.2.0",
"htmlescape": "^1.1.1",
"http-link-header": "1.1.1", "http-link-header": "1.1.1",
"ioredis": "5.3.2", "ioredis": "5.3.2",
"ip-cidr": "3.1.0", "ip-cidr": "3.1.0",
@ -196,6 +197,7 @@
"@types/color-convert": "2.0.3", "@types/color-convert": "2.0.3",
"@types/content-disposition": "0.5.8", "@types/content-disposition": "0.5.8",
"@types/fluent-ffmpeg": "2.1.24", "@types/fluent-ffmpeg": "2.1.24",
"@types/htmlescape": "^1.1.3",
"@types/http-link-header": "1.0.5", "@types/http-link-header": "1.0.5",
"@types/jest": "29.5.12", "@types/jest": "29.5.12",
"@types/js-yaml": "4.0.9", "@types/js-yaml": "4.0.9",

View File

@ -128,6 +128,7 @@ export class CacheService implements OnApplicationShutdown {
const { type, body } = obj.message as GlobalEvents['internal']['payload']; const { type, body } = obj.message as GlobalEvents['internal']['payload'];
switch (type) { switch (type) {
case 'userChangeSuspendedState': case 'userChangeSuspendedState':
case 'userChangeDeletedState':
case 'remoteUserUpdated': { case 'remoteUserUpdated': {
const user = await this.usersRepository.findOneBy({ id: body.id }); const user = await this.usersRepository.findOneBy({ id: body.id });
if (user == null) { if (user == null) {

View File

@ -115,6 +115,8 @@ import { FlashEntityService } from './entities/FlashEntityService.js';
import { FlashLikeEntityService } from './entities/FlashLikeEntityService.js'; import { FlashLikeEntityService } from './entities/FlashLikeEntityService.js';
import { RoleEntityService } from './entities/RoleEntityService.js'; import { RoleEntityService } from './entities/RoleEntityService.js';
import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js'; import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js';
import { MetaEntityService } from './entities/MetaEntityService.js';
import { ApAudienceService } from './activitypub/ApAudienceService.js'; import { ApAudienceService } from './activitypub/ApAudienceService.js';
import { ApDbResolverService } from './activitypub/ApDbResolverService.js'; import { ApDbResolverService } from './activitypub/ApDbResolverService.js';
import { ApDeliverManagerService } from './activitypub/ApDeliverManagerService.js'; import { ApDeliverManagerService } from './activitypub/ApDeliverManagerService.js';
@ -253,6 +255,7 @@ const $FlashEntityService: Provider = { provide: 'FlashEntityService', useExisti
const $FlashLikeEntityService: Provider = { provide: 'FlashLikeEntityService', useExisting: FlashLikeEntityService }; const $FlashLikeEntityService: Provider = { provide: 'FlashLikeEntityService', useExisting: FlashLikeEntityService };
const $RoleEntityService: Provider = { provide: 'RoleEntityService', useExisting: RoleEntityService }; const $RoleEntityService: Provider = { provide: 'RoleEntityService', useExisting: RoleEntityService };
const $ReversiGameEntityService: Provider = { provide: 'ReversiGameEntityService', useExisting: ReversiGameEntityService }; const $ReversiGameEntityService: Provider = { provide: 'ReversiGameEntityService', useExisting: ReversiGameEntityService };
const $MetaEntityService: Provider = { provide: 'MetaEntityService', useExisting: MetaEntityService };
const $ApAudienceService: Provider = { provide: 'ApAudienceService', useExisting: ApAudienceService }; const $ApAudienceService: Provider = { provide: 'ApAudienceService', useExisting: ApAudienceService };
const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useExisting: ApDbResolverService }; const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useExisting: ApDbResolverService };
@ -391,6 +394,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
FlashLikeEntityService, FlashLikeEntityService,
RoleEntityService, RoleEntityService,
ReversiGameEntityService, ReversiGameEntityService,
MetaEntityService,
ApAudienceService, ApAudienceService,
ApDbResolverService, ApDbResolverService,
ApDeliverManagerService, ApDeliverManagerService,
@ -524,6 +529,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$FlashLikeEntityService, $FlashLikeEntityService,
$RoleEntityService, $RoleEntityService,
$ReversiGameEntityService, $ReversiGameEntityService,
$MetaEntityService,
$ApAudienceService, $ApAudienceService,
$ApDbResolverService, $ApDbResolverService,
$ApDeliverManagerService, $ApDeliverManagerService,
@ -657,6 +664,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
FlashLikeEntityService, FlashLikeEntityService,
RoleEntityService, RoleEntityService,
ReversiGameEntityService, ReversiGameEntityService,
MetaEntityService,
ApAudienceService, ApAudienceService,
ApDbResolverService, ApDbResolverService,
ApDeliverManagerService, ApDeliverManagerService,
@ -789,6 +798,8 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$FlashLikeEntityService, $FlashLikeEntityService,
$RoleEntityService, $RoleEntityService,
$ReversiGameEntityService, $ReversiGameEntityService,
$MetaEntityService,
$ApAudienceService, $ApAudienceService,
$ApDbResolverService, $ApDbResolverService,
$ApDeliverManagerService, $ApDeliverManagerService,

View File

@ -9,6 +9,7 @@ import { QueueService } from '@/core/QueueService.js';
import { UserSuspendService } from '@/core/UserSuspendService.js'; import { UserSuspendService } from '@/core/UserSuspendService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
@Injectable() @Injectable()
export class DeleteAccountService { export class DeleteAccountService {
@ -18,6 +19,7 @@ export class DeleteAccountService {
private userSuspendService: UserSuspendService, private userSuspendService: UserSuspendService,
private queueService: QueueService, private queueService: QueueService,
private globalEventService: GlobalEventService,
) { ) {
} }
@ -39,5 +41,7 @@ export class DeleteAccountService {
await this.usersRepository.update(user.id, { await this.usersRepository.update(user.id, {
isDeleted: true, isDeleted: true,
}); });
this.globalEventService.publishInternalEvent('userChangeDeletedState', { id: user.id, isDeleted: true });
} }
} }

View File

@ -209,6 +209,7 @@ type SerializedAll<T> = {
export interface InternalEventTypes { export interface InternalEventTypes {
userChangeSuspendedState: { id: MiUser['id']; isSuspended: MiUser['isSuspended']; }; userChangeSuspendedState: { id: MiUser['id']; isSuspended: MiUser['isSuspended']; };
userChangeDeletedState: { id: MiUser['id']; isDeleted: MiUser['isDeleted']; };
userTokenRegenerated: { id: MiUser['id']; oldToken: string; newToken: string; }; userTokenRegenerated: { id: MiUser['id']; oldToken: string; newToken: string; };
remoteUserUpdated: { id: MiUser['id']; }; remoteUserUpdated: { id: MiUser['id']; };
follow: { followerId: MiUser['id']; followeeId: MiUser['id']; }; follow: { followerId: MiUser['id']; followeeId: MiUser['id']; };

View File

@ -61,6 +61,7 @@ import { UtilityService } from '@/core/UtilityService.js';
import { UserBlockingService } from '@/core/UserBlockingService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js';
import { isReply } from '@/misc/is-reply.js'; import { isReply } from '@/misc/is-reply.js';
import { trackPromise } from '@/misc/promise-tracker.js'; import { trackPromise } from '@/misc/promise-tracker.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { IdentifiableError } from '@/misc/identifiable-error.js'; import { IdentifiableError } from '@/misc/identifiable-error.js';
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention'; type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
@ -867,7 +868,7 @@ export class NoteCreateService implements OnApplicationShutdown {
const mentions = extractMentions(tokens); const mentions = extractMentions(tokens);
let mentionedUsers = (await Promise.all(mentions.map(m => let mentionedUsers = (await Promise.all(mentions.map(m =>
this.remoteUserResolveService.resolveUser(m.username, m.host ?? user.host).catch(() => null), this.remoteUserResolveService.resolveUser(m.username, m.host ?? user.host).catch(() => null),
))).filter(x => x != null) as MiUser[]; ))).filter(isNotNull);
// Drop duplicate users // Drop duplicate users
mentionedUsers = mentionedUsers.filter((u, i, self) => mentionedUsers = mentionedUsers.filter((u, i, self) =>

View File

@ -88,46 +88,47 @@ export class NoteReadService implements OnApplicationShutdown {
userId: MiUser['id'], userId: MiUser['id'],
notes: (MiNote | Packed<'Note'>)[], notes: (MiNote | Packed<'Note'>)[],
): Promise<void> { ): Promise<void> {
const readMentions: (MiNote | Packed<'Note'>)[] = []; if (notes.length === 0) return;
const readSpecifiedNotes: (MiNote | Packed<'Note'>)[] = [];
const noteIds = new Set<MiNote['id']>();
for (const note of notes) { for (const note of notes) {
if (note.mentions && note.mentions.includes(userId)) { if (note.mentions && note.mentions.includes(userId)) {
readMentions.push(note); noteIds.add(note.id);
} else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) { } else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) {
readSpecifiedNotes.push(note); noteIds.add(note.id);
} }
} }
if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0)) { if (noteIds.size === 0) return;
// Remove the record
await this.noteUnreadsRepository.delete({
userId: userId,
noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id)]),
});
// TODO: ↓まとめてクエリしたい // Remove the record
await this.noteUnreadsRepository.delete({
userId: userId,
noteId: In(Array.from(noteIds)),
});
trackPromise(this.noteUnreadsRepository.countBy({ // TODO: ↓まとめてクエリしたい
userId: userId,
isMentioned: true,
}).then(mentionsCount => {
if (mentionsCount === 0) {
// 全て既読になったイベントを発行
this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions');
}
}));
trackPromise(this.noteUnreadsRepository.countBy({ trackPromise(this.noteUnreadsRepository.countBy({
userId: userId, userId: userId,
isSpecified: true, isMentioned: true,
}).then(specifiedCount => { }).then(mentionsCount => {
if (specifiedCount === 0) { if (mentionsCount === 0) {
// 全て既読になったイベントを発行 // 全て既読になったイベントを発行
this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes'); this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions');
} }
})); }));
}
trackPromise(this.noteUnreadsRepository.countBy({
userId: userId,
isSpecified: true,
}).then(specifiedCount => {
if (specifiedCount === 0) {
// 全て既読になったイベントを発行
this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
}
}));
} }
@bindThis @bindThis

View File

@ -115,12 +115,19 @@ export class PushNotificationService implements OnApplicationShutdown {
endpoint: subscription.endpoint, endpoint: subscription.endpoint,
auth: subscription.auth, auth: subscription.auth,
publickey: subscription.publickey, publickey: subscription.publickey,
}).then(() => {
this.refreshCache(userId);
}); });
} }
}); });
} }
} }
@bindThis
public refreshCache(userId: string): void {
this.subscriptionsCache.refresh(userId);
}
@bindThis @bindThis
public dispose(): void { public dispose(): void {
this.subscriptionsCache.dispose(); this.subscriptionsCache.dispose();

View File

@ -29,6 +29,7 @@ import type { Config } from '@/config.js';
import { AccountMoveService } from '@/core/AccountMoveService.js'; import { AccountMoveService } from '@/core/AccountMoveService.js';
import { UtilityService } from '@/core/UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import type { ThinUser } from '@/queue/types.js';
import Logger from '../logger.js'; import Logger from '../logger.js';
const logger = new Logger('following/create'); const logger = new Logger('following/create');
@ -93,20 +94,43 @@ export class UserFollowingService implements OnModuleInit {
this.userBlockingService = this.moduleRef.get('UserBlockingService'); this.userBlockingService = this.moduleRef.get('UserBlockingService');
} }
@bindThis
public async deliverAccept(follower: MiRemoteUser, followee: MiPartialLocalUser, requestId?: string) {
const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, requestId), followee));
this.queueService.deliver(followee, content, follower.inbox, false);
}
/**
* ThinUserでなくともユーザーの情報が最新でない場合はこちらを使うべき
*/
@bindThis
public async followByThinUser(
_follower: ThinUser,
_followee: ThinUser,
options: Parameters<typeof this.follow>[2] = {},
) {
const [follower, followee] = await Promise.all([
this.usersRepository.findOneByOrFail({ id: _follower.id }),
this.usersRepository.findOneByOrFail({ id: _followee.id }),
]) as [MiLocalUser | MiRemoteUser, MiLocalUser | MiRemoteUser];
await this.follow(follower, followee, options);
}
@bindThis @bindThis
public async follow( public async follow(
_follower: { id: MiUser['id'] }, follower: MiLocalUser | MiRemoteUser,
_followee: { id: MiUser['id'] }, followee: MiLocalUser | MiRemoteUser,
{ requestId, silent = false, withReplies }: { { requestId, silent = false, withReplies }: {
requestId?: string, requestId?: string,
silent?: boolean, silent?: boolean,
withReplies?: boolean, withReplies?: boolean,
} = {}, } = {},
): Promise<void> { ): Promise<void> {
const [follower, followee] = await Promise.all([ if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isRemoteUser(followee)) {
this.usersRepository.findOneByOrFail({ id: _follower.id }), // What?
this.usersRepository.findOneByOrFail({ id: _followee.id }), throw new Error('Remote user cannot follow remote user.');
]) as [MiLocalUser | MiRemoteUser, MiLocalUser | MiRemoteUser]; }
// check blocking // check blocking
const [blocking, blocked] = await Promise.all([ const [blocking, blocked] = await Promise.all([
@ -128,6 +152,24 @@ export class UserFollowingService implements OnModuleInit {
if (blocked) throw new IdentifiableError('3338392a-f764-498d-8855-db939dcf8c48', 'You have been blocked by this user.'); if (blocked) throw new IdentifiableError('3338392a-f764-498d-8855-db939dcf8c48', 'You have been blocked by this user.');
} }
if (await this.followingsRepository.exists({
where: {
followerId: follower.id,
followeeId: followee.id,
},
})) {
// すでにフォロー関係が存在している場合
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
// リモート → ローカル: acceptを送り返しておしまい
this.deliverAccept(follower, followee, requestId);
return;
}
if (this.userEntityService.isLocalUser(follower)) {
// ローカル → リモート/ローカル: 例外
throw new IdentifiableError('ec3f65c0-a9d1-47d9-8791-b2e7b9dcdced', 'already following');
}
}
const followeeProfile = await this.userProfilesRepository.findOneByOrFail({ userId: followee.id }); const followeeProfile = await this.userProfilesRepository.findOneByOrFail({ userId: followee.id });
// フォロー対象が鍵アカウントである or // フォロー対象が鍵アカウントである or
// フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or // フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or
@ -188,8 +230,7 @@ export class UserFollowingService implements OnModuleInit {
await this.insertFollowingDoc(followee, follower, silent, withReplies); await this.insertFollowingDoc(followee, follower, silent, withReplies);
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee, requestId), followee)); this.deliverAccept(follower, followee, requestId);
this.queueService.deliver(followee, content, follower.inbox, false);
} }
} }
@ -570,8 +611,7 @@ export class UserFollowingService implements OnModuleInit {
await this.insertFollowingDoc(followee, follower, false, request.withReplies); await this.insertFollowingDoc(followee, follower, false, request.withReplies);
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
const content = this.apRendererService.addContext(this.apRendererService.renderAccept(this.apRendererService.renderFollow(follower, followee as MiPartialLocalUser, request.requestId!), followee)); this.deliverAccept(follower, followee as MiPartialLocalUser, request.requestId ?? undefined);
this.queueService.deliver(followee, content, follower.inbox, false);
} }
this.userEntityService.pack(followee.id, followee, { this.userEntityService.pack(followee.id, followee, {

View File

@ -8,6 +8,7 @@ import promiseLimit from 'promise-limit';
import type { MiRemoteUser, MiUser } from '@/models/User.js'; import type { MiRemoteUser, MiUser } from '@/models/User.js';
import { concat, unique } from '@/misc/prelude/array.js'; import { concat, unique } from '@/misc/prelude/array.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { getApIds } from './type.js'; import { getApIds } from './type.js';
import { ApPersonService } from './models/ApPersonService.js'; import { ApPersonService } from './models/ApPersonService.js';
import type { ApObject } from './type.js'; import type { ApObject } from './type.js';
@ -40,7 +41,7 @@ export class ApAudienceService {
const limit = promiseLimit<MiUser | null>(2); const limit = promiseLimit<MiUser | null>(2);
const mentionedUsers = (await Promise.all( const mentionedUsers = (await Promise.all(
others.map(id => limit(() => this.apPersonService.resolvePerson(id, resolver).catch(() => null))), others.map(id => limit(() => this.apPersonService.resolvePerson(id, resolver).catch(() => null))),
)).filter((x): x is MiUser => x != null); )).filter(isNotNull);
if (toGroups.public.length > 0) { if (toGroups.public.length > 0) {
return { return {

View File

@ -27,6 +27,7 @@ import { QueueService } from '@/core/QueueService.js';
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/_.js'; import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/_.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { MiLocalUser, MiRemoteUser } from '@/models/User.js'; import type { MiLocalUser, MiRemoteUser } from '@/models/User.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
import { ApNoteService } from './models/ApNoteService.js'; import { ApNoteService } from './models/ApNoteService.js';
import { ApLoggerService } from './ApLoggerService.js'; import { ApLoggerService } from './ApLoggerService.js';
@ -84,7 +85,6 @@ export class ApInboxService {
private apPersonService: ApPersonService, private apPersonService: ApPersonService,
private apQuestionService: ApQuestionService, private apQuestionService: ApQuestionService,
private queueService: QueueService, private queueService: QueueService,
private cacheService: CacheService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
) { ) {
this.logger = this.apLoggerService.logger; this.logger = this.apLoggerService.logger;
@ -532,7 +532,7 @@ export class ApInboxService {
const userIds = uris const userIds = uris
.filter(uri => uri.startsWith(this.config.url + '/users/')) .filter(uri => uri.startsWith(this.config.url + '/users/'))
.map(uri => uri.split('/').at(-1)) .map(uri => uri.split('/').at(-1))
.filter((userId): userId is string => userId !== undefined); .filter(isNotNull);
const users = await this.usersRepository.findBy({ const users = await this.usersRepository.findBy({
id: In(userIds), id: In(userIds),
}); });

View File

@ -315,7 +315,7 @@ export class ApRendererService {
const getPromisedFiles = async (ids: string[]): Promise<MiDriveFile[]> => { const getPromisedFiles = async (ids: string[]): Promise<MiDriveFile[]> => {
if (ids.length === 0) return []; if (ids.length === 0) return [];
const items = await this.driveFilesRepository.findBy({ id: In(ids) }); const items = await this.driveFilesRepository.findBy({ id: In(ids) });
return ids.map(id => items.find(item => item.id === id)).filter((item): item is MiDriveFile => item != null); return ids.map(id => items.find(item => item.id === id)).filter(isNotNull);
}; };
let inReplyTo; let inReplyTo;

View File

@ -8,6 +8,7 @@ import promiseLimit from 'promise-limit';
import type { MiUser } from '@/models/_.js'; import type { MiUser } from '@/models/_.js';
import { toArray, unique } from '@/misc/prelude/array.js'; import { toArray, unique } from '@/misc/prelude/array.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { isMention } from '../type.js'; import { isMention } from '../type.js';
import { Resolver } from '../ApResolverService.js'; import { Resolver } from '../ApResolverService.js';
import { ApPersonService } from './ApPersonService.js'; import { ApPersonService } from './ApPersonService.js';
@ -27,7 +28,7 @@ export class ApMentionService {
const limit = promiseLimit<MiUser | null>(2); const limit = promiseLimit<MiUser | null>(2);
const mentionedUsers = (await Promise.all( const mentionedUsers = (await Promise.all(
hrefs.map(x => limit(() => this.apPersonService.resolvePerson(x, resolver).catch(() => null))), hrefs.map(x => limit(() => this.apPersonService.resolvePerson(x, resolver).catch(() => null))),
)).filter((x): x is MiUser => x != null); )).filter(isNotNull);
return mentionedUsers; return mentionedUsers;
} }

View File

@ -37,6 +37,7 @@ import { ApQuestionService } from './ApQuestionService.js';
import { ApImageService } from './ApImageService.js'; import { ApImageService } from './ApImageService.js';
import type { Resolver } from '../ApResolverService.js'; import type { Resolver } from '../ApResolverService.js';
import type { IObject, IPost } from '../type.js'; import type { IObject, IPost } from '../type.js';
import { isNotNull } from '@/misc/is-not-null.js';
@Injectable() @Injectable()
export class ApNoteService { export class ApNoteService {
@ -228,7 +229,7 @@ export class ApNoteService {
} }
}; };
const uris = unique([note._misskey_quote, note.quoteUrl].filter((x): x is string => typeof x === 'string')); const uris = unique([note._misskey_quote, note.quoteUrl].filter(isNotNull));
const results = await Promise.all(uris.map(tryResolveNote)); const results = await Promise.all(uris.map(tryResolveNote));
quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0); quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0);

View File

@ -39,6 +39,7 @@ import { MetaService } from '@/core/MetaService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import type { AccountMoveService } from '@/core/AccountMoveService.js'; import type { AccountMoveService } from '@/core/AccountMoveService.js';
import { checkHttps } from '@/misc/check-https.js'; import { checkHttps } from '@/misc/check-https.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js'; import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js';
import { extractApHashtags } from './tag.js'; import { extractApHashtags } from './tag.js';
import type { OnModuleInit } from '@nestjs/common'; import type { OnModuleInit } from '@nestjs/common';
@ -656,7 +657,7 @@ export class ApPersonService implements OnModuleInit {
// とりあえずidを別の時間で生成して順番を維持 // とりあえずidを別の時間で生成して順番を維持
let td = 0; let td = 0;
for (const note of featuredNotes.filter((note): note is MiNote => note != null)) { for (const note of featuredNotes.filter(isNotNull)) {
td -= 1000; td -= 1000;
transactionalEntityManager.insert(MiUserNotePining, { transactionalEntityManager.insert(MiUserNotePining, {
id: this.idService.gen(Date.now() + td), id: this.idService.gen(Date.now() + td),

View File

@ -10,6 +10,7 @@ import type { Config } from '@/config.js';
import type { IPoll } from '@/models/Poll.js'; import type { IPoll } from '@/models/Poll.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { isQuestion } from '../type.js'; import { isQuestion } from '../type.js';
import { ApLoggerService } from '../ApLoggerService.js'; import { ApLoggerService } from '../ApLoggerService.js';
import { ApResolverService } from '../ApResolverService.js'; import { ApResolverService } from '../ApResolverService.js';
@ -51,7 +52,7 @@ export class ApQuestionService {
const choices = question[multiple ? 'anyOf' : 'oneOf'] const choices = question[multiple ? 'anyOf' : 'oneOf']
?.map((x) => x.name) ?.map((x) => x.name)
.filter((x): x is string => typeof x === 'string') .filter(isNotNull)
?? []; ?? [];
const votes = question[multiple ? 'anyOf' : 'oneOf']?.map((x) => x.replies?.totalItems ?? x._misskey_votes ?? 0); const votes = question[multiple ? 'anyOf' : 'oneOf']?.map((x) => x.replies?.totalItems ?? x._misskey_votes ?? 0);

View File

@ -4,6 +4,7 @@
*/ */
import { toArray } from '@/misc/prelude/array.js'; import { toArray } from '@/misc/prelude/array.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { isHashtag } from '../type.js'; import { isHashtag } from '../type.js';
import type { IObject, IApHashtag } from '../type.js'; import type { IObject, IApHashtag } from '../type.js';
@ -15,7 +16,7 @@ export function extractApHashtags(tags: IObject | IObject[] | null | undefined):
return hashtags.map(tag => { return hashtags.map(tag => {
const m = tag.name.match(/^#(.+)/); const m = tag.name.match(/^#(.+)/);
return m ? m[1] : null; return m ? m[1] : null;
}).filter((x): x is string => x != null); }).filter(isNotNull);
} }
export function extractApHashtagObjects(tags: IObject | IObject[] | null | undefined): IApHashtag[] { export function extractApHashtagObjects(tags: IObject | IObject[] | null | undefined): IApHashtag[] {

View File

@ -8,12 +8,15 @@ import type { Packed } from '@/misc/json-schema.js';
import type { MiInstance } from '@/models/Instance.js'; import type { MiInstance } from '@/models/Instance.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { UtilityService } from '../UtilityService.js'; import { UtilityService } from '@/core/UtilityService.js';
import { RoleService } from '@/core/RoleService.js';
import { MiUser } from '@/models/User.js';
@Injectable() @Injectable()
export class InstanceEntityService { export class InstanceEntityService {
constructor( constructor(
private metaService: MetaService, private metaService: MetaService,
private roleService: RoleService,
private utilityService: UtilityService, private utilityService: UtilityService,
) { ) {
@ -22,8 +25,11 @@ export class InstanceEntityService {
@bindThis @bindThis
public async pack( public async pack(
instance: MiInstance, instance: MiInstance,
me: { id: MiUser['id']; } | null | undefined,
): Promise<Packed<'FederationInstance'>> { ): Promise<Packed<'FederationInstance'>> {
const meta = await this.metaService.fetch(); const meta = await this.metaService.fetch();
const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false;
return { return {
id: instance.id, id: instance.id,
firstRetrievedAt: instance.firstRetrievedAt.toISOString(), firstRetrievedAt: instance.firstRetrievedAt.toISOString(),
@ -49,14 +55,16 @@ export class InstanceEntityService {
themeColor: instance.themeColor, themeColor: instance.themeColor,
infoUpdatedAt: instance.infoUpdatedAt ? instance.infoUpdatedAt.toISOString() : null, infoUpdatedAt: instance.infoUpdatedAt ? instance.infoUpdatedAt.toISOString() : null,
latestRequestReceivedAt: instance.latestRequestReceivedAt ? instance.latestRequestReceivedAt.toISOString() : null, latestRequestReceivedAt: instance.latestRequestReceivedAt ? instance.latestRequestReceivedAt.toISOString() : null,
moderationNote: iAmModerator ? instance.moderationNote : null,
}; };
} }
@bindThis @bindThis
public async packMany( public async packMany(
instances: MiInstance[], instances: MiInstance[],
me: { id: MiUser['id'] } | null | undefined,
) : Promise<Packed<'FederationInstance'>[]> { ) : Promise<Packed<'FederationInstance'>[]> {
return (await Promise.allSettled(instances.map(x => this.pack(x)))) return (await Promise.allSettled(instances.map(x => this.pack(x, me))))
.filter(result => result.status === 'fulfilled') .filter(result => result.status === 'fulfilled')
.map(result => (result as PromiseFulfilledResult<Packed<'FederationInstance'>>).value); .map(result => (result as PromiseFulfilledResult<Packed<'FederationInstance'>>).value);
} }

View File

@ -0,0 +1,150 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import JSON5 from 'json5';
import type { Packed } from '@/misc/json-schema.js';
import type { MiMeta } from '@/models/Meta.js';
import type { AdsRepository } from '@/models/_.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { MetaService } from '@/core/MetaService.js';
import { bindThis } from '@/decorators.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
@Injectable()
export class MetaEntityService {
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.adsRepository)
private adsRepository: AdsRepository,
private userEntityService: UserEntityService,
private metaService: MetaService,
private instanceActorService: InstanceActorService,
) { }
@bindThis
public async pack(meta?: MiMeta): Promise<Packed<'MetaLite'>> {
let instance = meta;
if (!instance) {
instance = await this.metaService.fetch();
}
const ads = await this.adsRepository.createQueryBuilder('ads')
.where('ads.expiresAt > :now', { now: new Date() })
.andWhere('ads.startsAt <= :now', { now: new Date() })
.andWhere(new Brackets(qb => {
// 曜日のビットフラグを確認する
qb.where('ads.dayOfWeek & :dayOfWeek > 0', { dayOfWeek: 1 << new Date().getDay() })
.orWhere('ads.dayOfWeek = 0');
}))
.getMany();
return {
maintainerName: instance.maintainerName,
maintainerEmail: instance.maintainerEmail,
version: this.config.version,
name: instance.name,
shortName: instance.shortName,
uri: this.config.url,
description: instance.description,
langs: instance.langs,
tosUrl: instance.termsOfServiceUrl,
repositoryUrl: instance.repositoryUrl,
feedbackUrl: instance.feedbackUrl,
impressumUrl: instance.impressumUrl,
privacyPolicyUrl: instance.privacyPolicyUrl,
disableRegistration: instance.disableRegistration,
emailRequiredForSignup: instance.emailRequiredForSignup,
enableHcaptcha: instance.enableHcaptcha,
hcaptchaSiteKey: instance.hcaptchaSiteKey,
enableMcaptcha: instance.enableMcaptcha,
mcaptchaSiteKey: instance.mcaptchaSitekey,
mcaptchaInstanceUrl: instance.mcaptchaInstanceUrl,
enableRecaptcha: instance.enableRecaptcha,
recaptchaSiteKey: instance.recaptchaSiteKey,
enableTurnstile: instance.enableTurnstile,
turnstileSiteKey: instance.turnstileSiteKey,
swPublickey: instance.swPublicKey,
themeColor: instance.themeColor,
mascotImageUrl: instance.mascotImageUrl ?? '/assets/ai.png',
bannerUrl: instance.bannerUrl,
infoImageUrl: instance.infoImageUrl,
serverErrorImageUrl: instance.serverErrorImageUrl,
notFoundImageUrl: instance.notFoundImageUrl,
iconUrl: instance.iconUrl,
backgroundImageUrl: instance.backgroundImageUrl,
logoImageUrl: instance.logoImageUrl,
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH,
// クライアントの手間を減らすためあらかじめJSONに変換しておく
defaultLightTheme: instance.defaultLightTheme ? JSON.stringify(JSON5.parse(instance.defaultLightTheme)) : null,
defaultDarkTheme: instance.defaultDarkTheme ? JSON.stringify(JSON5.parse(instance.defaultDarkTheme)) : null,
ads: ads.map(ad => ({
id: ad.id,
url: ad.url,
place: ad.place as 'square' | 'horizontal' | 'horizontal-big' | 'vertical',
ratio: ad.ratio,
imageUrl: ad.imageUrl,
dayOfWeek: ad.dayOfWeek,
})),
notesPerOneAd: instance.notesPerOneAd,
enableEmail: instance.enableEmail,
enableServiceWorker: instance.enableServiceWorker,
translatorAvailable: instance.deeplAuthKey != null,
serverRules: instance.serverRules,
policies: { ...DEFAULT_POLICIES, ...instance.policies },
mediaProxy: this.config.mediaProxy,
};
}
@bindThis
public async packDetailed(meta?: MiMeta): Promise<Packed<'MetaDetailed'>> {
let instance = meta;
if (!instance) {
instance = await this.metaService.fetch();
}
const packed = await this.pack(instance);
const proxyAccount = instance.proxyAccountId ? await this.userEntityService.pack(instance.proxyAccountId, null).catch(() => null) : null;
return {
...packed,
cacheRemoteFiles: instance.cacheRemoteFiles,
cacheRemoteSensitiveFiles: instance.cacheRemoteSensitiveFiles,
requireSetup: !await this.instanceActorService.realLocalUsersPresent(),
proxyAccountName: proxyAccount ? proxyAccount.username : null,
features: {
localTimeline: instance.policies.ltlAvailable,
globalTimeline: instance.policies.gtlAvailable,
registration: !instance.disableRegistration,
emailRequiredForSignup: instance.emailRequiredForSignup,
hCaptcha: instance.enableHcaptcha,
mCaptcha: instance.enableMcaptcha,
reCaptcha: instance.enableRecaptcha,
turnstile: instance.enableTurnstile,
objectStorage: instance.useObjectStorage,
serviceWorker: instance.enableServiceWorker,
miauth: true,
},
};
}
}

View File

@ -77,7 +77,11 @@ export class NoteReactionEntityService implements OnModuleInit {
withNote: boolean; withNote: boolean;
}, },
) : Promise<Packed<'NoteReaction'>[]> { ) : Promise<Packed<'NoteReaction'>[]> {
return (await Promise.allSettled(reactions.map(x => this.pack(x, me, options)))) const opts = Object.assign({
withNote: false,
}, options);
return (await Promise.allSettled(reactions.map(x => this.pack(x, me, opts))))
.filter(result => result.status === 'fulfilled') .filter(result => result.status === 'fulfilled')
.map(result => (result as PromiseFulfilledResult<Packed<'NoteReaction'>>).value); .map(result => (result as PromiseFulfilledResult<Packed<'NoteReaction'>>).value);
} }

View File

@ -13,6 +13,7 @@ import type { MiPage } from '@/models/Page.js';
import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiDriveFile } from '@/models/DriveFile.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { isNotNull } from '@/misc/is-not-null.js';
import { UserEntityService } from './UserEntityService.js'; import { UserEntityService } from './UserEntityService.js';
import { DriveFileEntityService } from './DriveFileEntityService.js'; import { DriveFileEntityService } from './DriveFileEntityService.js';
@ -101,7 +102,7 @@ export class PageEntityService {
script: page.script, script: page.script,
eyeCatchingImageId: page.eyeCatchingImageId, eyeCatchingImageId: page.eyeCatchingImageId,
eyeCatchingImage: page.eyeCatchingImageId ? await this.driveFileEntityService.pack(page.eyeCatchingImageId, me) : null, eyeCatchingImage: page.eyeCatchingImageId ? await this.driveFileEntityService.pack(page.eyeCatchingImageId, me) : null,
attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter((x): x is MiDriveFile => x != null), me), attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter(isNotNull), me),
likedCount: page.likedCount, likedCount: page.likedCount,
isLiked: meId ? await this.pageLikesRepository.exists({ where: { pageId: page.id, userId: meId } }) : undefined, isLiked: meId ? await this.pageLikesRepository.exists({ where: { pageId: page.id, userId: meId } }) : undefined,
}); });

View File

@ -26,6 +26,7 @@ import { IdService } from '@/core/IdService.js';
import type { AnnouncementService } from '@/core/AnnouncementService.js'; import type { AnnouncementService } from '@/core/AnnouncementService.js';
import type { CustomEmojiService } from '@/core/CustomEmojiService.js'; import type { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js'; import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
import { isNotNull } from '@/misc/is-not-null.js';
import type { OnModuleInit } from '@nestjs/common'; import type { OnModuleInit } from '@nestjs/common';
import type { NoteEntityService } from './NoteEntityService.js'; import type { NoteEntityService } from './NoteEntityService.js';
import type { DriveFileEntityService } from './DriveFileEntityService.js'; import type { DriveFileEntityService } from './DriveFileEntityService.js';
@ -383,7 +384,7 @@ export class UserEntityService implements OnModuleInit {
movedTo: user.movedToUri ? this.apPersonService.resolvePerson(user.movedToUri).then(user => user.id).catch(() => null) : null, movedTo: user.movedToUri ? this.apPersonService.resolvePerson(user.movedToUri).then(user => user.id).catch(() => null) : null,
alsoKnownAs: user.alsoKnownAs alsoKnownAs: user.alsoKnownAs
? Promise.all(user.alsoKnownAs.map(uri => this.apPersonService.fetchPerson(uri).then(user => user?.id).catch(() => null))) ? Promise.all(user.alsoKnownAs.map(uri => this.apPersonService.fetchPerson(uri).then(user => user?.id).catch(() => null)))
.then(xs => xs.length === 0 ? null : xs.filter(x => x != null) as string[]) .then(xs => xs.length === 0 ? null : xs.filter(isNotNull))
: null, : null,
createdAt: this.idService.parse(user.id).date.toISOString(), createdAt: this.idService.parse(user.id).date.toISOString(),
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null, updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,

View File

@ -3,8 +3,6 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
// we are using {} as "any non-nullish value" as expected export function isNotNull<T extends NonNullable<unknown>>(input: T | undefined | null): input is T {
// eslint-disable-next-line @typescript-eslint/ban-types
export function isNotNull<T extends {}>(input: T | undefined | null): input is T {
return input != null; return input != null;
} }

View File

@ -52,6 +52,11 @@ import {
} from '@/models/json-schema/role.js'; } from '@/models/json-schema/role.js';
import { packedAdSchema } from '@/models/json-schema/ad.js'; import { packedAdSchema } from '@/models/json-schema/ad.js';
import { packedReversiGameLiteSchema, packedReversiGameDetailedSchema } from '@/models/json-schema/reversi-game.js'; import { packedReversiGameLiteSchema, packedReversiGameDetailedSchema } from '@/models/json-schema/reversi-game.js';
import {
packedMetaLiteSchema,
packedMetaDetailedOnlySchema,
packedMetaDetailedSchema,
} from '@/models/json-schema/meta.js';
export const refs = { export const refs = {
UserLite: packedUserLiteSchema, UserLite: packedUserLiteSchema,
@ -107,6 +112,9 @@ export const refs = {
RolePolicies: packedRolePoliciesSchema, RolePolicies: packedRolePoliciesSchema,
ReversiGameLite: packedReversiGameLiteSchema, ReversiGameLite: packedReversiGameLiteSchema,
ReversiGameDetailed: packedReversiGameDetailedSchema, ReversiGameDetailed: packedReversiGameDetailedSchema,
MetaLite: packedMetaLiteSchema,
MetaDetailedOnly: packedMetaDetailedOnlySchema,
MetaDetailed: packedMetaDetailedSchema,
AbuseUserReport: packedAbuseUserReportSchema, AbuseUserReport: packedAbuseUserReportSchema,
ModerationLog: packedModerationLogSchema, ModerationLog: packedModerationLogSchema,
}; };

View File

@ -144,4 +144,9 @@ export class MiInstance {
nullable: true, nullable: true,
}) })
public infoUpdatedAt: Date | null; public infoUpdatedAt: Date | null;
@Column('varchar', {
length: 16384, default: '',
})
public moderationNote: string;
} }

View File

@ -111,5 +111,9 @@ export const packedFederationInstanceSchema = {
optional: false, nullable: true, optional: false, nullable: true,
format: 'date-time', format: 'date-time',
}, },
moderationNote: {
type: 'string',
optional: true, nullable: true,
},
}, },
} as const; } as const;

View File

@ -0,0 +1,329 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export const packedMetaLiteSchema = {
type: 'object',
optional: false, nullable: false,
properties: {
maintainerName: {
type: 'string',
optional: false, nullable: true,
},
maintainerEmail: {
type: 'string',
optional: false, nullable: true,
},
version: {
type: 'string',
optional: false, nullable: false,
},
name: {
type: 'string',
optional: false, nullable: true,
},
shortName: {
type: 'string',
optional: false, nullable: true,
},
uri: {
type: 'string',
optional: false, nullable: false,
format: 'url',
example: 'https://misskey.example.com',
},
description: {
type: 'string',
optional: false, nullable: true,
},
langs: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
},
},
tosUrl: {
type: 'string',
optional: false, nullable: true,
},
repositoryUrl: {
type: 'string',
optional: false, nullable: true,
default: 'https://github.com/misskey-dev/misskey',
},
feedbackUrl: {
type: 'string',
optional: false, nullable: true,
default: 'https://github.com/misskey-dev/misskey/issues/new',
},
defaultDarkTheme: {
type: 'string',
optional: false, nullable: true,
},
defaultLightTheme: {
type: 'string',
optional: false, nullable: true,
},
disableRegistration: {
type: 'boolean',
optional: false, nullable: false,
},
emailRequiredForSignup: {
type: 'boolean',
optional: false, nullable: false,
},
enableHcaptcha: {
type: 'boolean',
optional: false, nullable: false,
},
hcaptchaSiteKey: {
type: 'string',
optional: false, nullable: true,
},
enableMcaptcha: {
type: 'boolean',
optional: false, nullable: false,
},
mcaptchaSiteKey: {
type: 'string',
optional: false, nullable: true,
},
mcaptchaInstanceUrl: {
type: 'string',
optional: false, nullable: true,
},
enableRecaptcha: {
type: 'boolean',
optional: false, nullable: false,
},
recaptchaSiteKey: {
type: 'string',
optional: false, nullable: true,
},
enableTurnstile: {
type: 'boolean',
optional: false, nullable: false,
},
turnstileSiteKey: {
type: 'string',
optional: false, nullable: true,
},
swPublickey: {
type: 'string',
optional: false, nullable: true,
},
mascotImageUrl: {
type: 'string',
optional: false, nullable: false,
default: '/assets/ai.png',
},
bannerUrl: {
type: 'string',
optional: false, nullable: true,
},
serverErrorImageUrl: {
type: 'string',
optional: false, nullable: true,
},
infoImageUrl: {
type: 'string',
optional: false, nullable: true,
},
notFoundImageUrl: {
type: 'string',
optional: false, nullable: true,
},
iconUrl: {
type: 'string',
optional: false, nullable: true,
},
maxNoteTextLength: {
type: 'number',
optional: false, nullable: false,
},
ads: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
properties: {
id: {
type: 'string',
optional: false, nullable: false,
format: 'id',
example: 'xxxxxxxxxx',
},
url: {
type: 'string',
optional: false, nullable: false,
format: 'url',
},
place: {
type: 'string',
optional: false, nullable: false,
enum: ['square', 'horizontal', 'horizontal-big', 'vertical'],
},
ratio: {
type: 'number',
optional: false, nullable: false,
},
imageUrl: {
type: 'string',
optional: false, nullable: false,
format: 'url',
},
dayOfWeek: {
type: 'integer',
optional: false, nullable: false,
},
},
},
},
notesPerOneAd: {
type: 'number',
optional: false, nullable: false,
default: 0,
},
enableEmail: {
type: 'boolean',
optional: false, nullable: false,
},
enableServiceWorker: {
type: 'boolean',
optional: false, nullable: false,
},
translatorAvailable: {
type: 'boolean',
optional: false, nullable: false,
},
mediaProxy: {
type: 'string',
optional: false, nullable: false,
},
backgroundImageUrl: {
type: 'string',
optional: false, nullable: true,
},
impressumUrl: {
type: 'string',
optional: false, nullable: true,
},
logoImageUrl: {
type: 'string',
optional: false, nullable: true,
},
privacyPolicyUrl: {
type: 'string',
optional: false, nullable: true,
},
serverRules: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'string',
},
},
themeColor: {
type: 'string',
optional: false, nullable: true,
},
policies: {
type: 'object',
optional: false, nullable: false,
ref: 'RolePolicies',
},
},
} as const;
export const packedMetaDetailedOnlySchema = {
type: 'object',
optional: false, nullable: false,
properties: {
features: {
type: 'object',
optional: true, nullable: false,
properties: {
registration: {
type: 'boolean',
optional: false, nullable: false,
},
emailRequiredForSignup: {
type: 'boolean',
optional: false, nullable: false,
},
localTimeline: {
type: 'boolean',
optional: false, nullable: false,
},
globalTimeline: {
type: 'boolean',
optional: false, nullable: false,
},
hCaptcha: {
type: 'boolean',
optional: false, nullable: false,
},
mCaptcha: {
type: 'boolean',
optional: false, nullable: false,
},
reCaptcha: {
type: 'boolean',
optional: false, nullable: false,
},
turnstile: {
type: 'boolean',
optional: false, nullable: false,
},
objectStorage: {
type: 'boolean',
optional: false, nullable: false,
},
serviceWorker: {
type: 'boolean',
optional: false, nullable: false,
},
miauth: {
type: 'boolean',
optional: true, nullable: false,
default: true,
},
},
},
proxyAccountName: {
type: 'string',
optional: false, nullable: true,
},
requireSetup: {
type: 'boolean',
optional: false, nullable: false,
example: false,
},
cacheRemoteFiles: {
type: 'boolean',
optional: false, nullable: false,
},
cacheRemoteSensitiveFiles: {
type: 'boolean',
optional: false, nullable: false,
},
},
} as const;
export const packedMetaDetailedSchema = {
type: 'object',
allOf: [
{
type: 'object',
ref: 'MetaLite',
},
{
type: 'object',
ref: 'MetaDetailedOnly',
},
],
} as const;

View File

@ -35,7 +35,7 @@ export class RelationshipProcessorService {
@bindThis @bindThis
public async processFollow(job: Bull.Job<RelationshipJobData>): Promise<string> { public async processFollow(job: Bull.Job<RelationshipJobData>): Promise<string> {
this.logger.info(`${job.data.from.id} is trying to follow ${job.data.to.id} ${job.data.withReplies ? 'with replies' : 'without replies'}`); this.logger.info(`${job.data.from.id} is trying to follow ${job.data.to.id} ${job.data.withReplies ? 'with replies' : 'without replies'}`);
await this.userFollowingService.follow(job.data.from, job.data.to, { await this.userFollowingService.followByThinUser(job.data.from, job.data.to, {
requestId: job.data.requestId, requestId: job.data.requestId,
silent: job.data.silent, silent: job.data.silent,
withReplies: job.data.withReplies, withReplies: job.data.withReplies,

View File

@ -24,8 +24,9 @@ export const paramDef = {
properties: { properties: {
host: { type: 'string' }, host: { type: 'string' },
isSuspended: { type: 'boolean' }, isSuspended: { type: 'boolean' },
moderationNote: { type: 'string' },
}, },
required: ['host', 'isSuspended'], required: ['host'],
} as const; } as const;
@Injectable() @Injectable()
@ -47,9 +48,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
await this.federatedInstanceService.update(instance.id, { await this.federatedInstanceService.update(instance.id, {
isSuspended: ps.isSuspended, isSuspended: ps.isSuspended,
moderationNote: ps.moderationNote,
}); });
if (instance.isSuspended !== ps.isSuspended) { if (ps.isSuspended != null && instance.isSuspended !== ps.isSuspended) {
if (ps.isSuspended) { if (ps.isSuspended) {
this.moderationLogService.log(me, 'suspendRemoteInstance', { this.moderationLogService.log(me, 'suspendRemoteInstance', {
id: instance.id, id: instance.id,
@ -62,6 +64,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}); });
} }
} }
if (ps.moderationNote != null && instance.moderationNote !== ps.moderationNote) {
this.moderationLogService.log(me, 'updateRemoteInstanceNote', {
id: instance.id,
host: instance.host,
before: instance.moderationNote,
after: ps.moderationNote,
});
}
}); });
} }
} }

View File

@ -124,9 +124,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
notes.sort((a, b) => a.id > b.id ? -1 : 1); notes.sort((a, b) => a.id > b.id ? -1 : 1);
} }
if (notes.length > 0) { this.noteReadService.read(me.id, notes);
this.noteReadService.read(me.id, notes);
}
return await this.noteEntityService.packMany(notes, me); return await this.noteEntityService.packMany(notes, me);
}); });

View File

@ -177,7 +177,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const instances = await query.limit(ps.limit).offset(ps.offset).getMany(); const instances = await query.limit(ps.limit).offset(ps.offset).getMany();
logger.info('Fetched federated instances.', { count: instances.length }); logger.info('Fetched federated instances.', { count: instances.length });
return await this.instanceEntityService.packMany(instances); return await this.instanceEntityService.packMany(instances, me);
}); });
} }
} }

View File

@ -43,7 +43,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const instance = await this.instancesRepository const instance = await this.instancesRepository
.findOneBy({ host: this.utilityService.toPuny(ps.host) }); .findOneBy({ host: this.utilityService.toPuny(ps.host) });
return instance ? await this.instanceEntityService.pack(instance) : null; return instance ? await this.instanceEntityService.pack(instance, me) : null;
}); });
} }
} }

View File

@ -107,9 +107,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const gotPubCount = topPubInstances.map(x => x.followingCount).reduce((a, b) => a + b, 0); const gotPubCount = topPubInstances.map(x => x.followingCount).reduce((a, b) => a + b, 0);
return await awaitAll({ return await awaitAll({
topSubInstances: this.instanceEntityService.packMany(topSubInstances), topSubInstances: this.instanceEntityService.packMany(topSubInstances, null),
otherFollowersCount: Math.max(0, allSubCount - gotSubCount), otherFollowersCount: Math.max(0, allSubCount - gotSubCount),
topPubInstances: this.instanceEntityService.packMany(topPubInstances), topPubInstances: this.instanceEntityService.packMany(topPubInstances, null),
otherFollowingCount: Math.max(0, allPubCount - gotPubCount), otherFollowingCount: Math.max(0, allPubCount - gotPubCount),
}); });
}); });

View File

@ -52,7 +52,7 @@ export const paramDef = {
} }, } },
visibility: { type: 'string', enum: ['public', 'private'] }, visibility: { type: 'string', enum: ['public', 'private'] },
}, },
required: ['flashId', 'title', 'summary', 'script', 'permissions'], required: ['flashId'],
} as const; } as const;
@Injectable() @Injectable()
@ -72,11 +72,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
await this.flashsRepository.update(flash.id, { await this.flashsRepository.update(flash.id, {
updatedAt: new Date(), updatedAt: new Date(),
title: ps.title, ...Object.fromEntries(
summary: ps.summary, Object.entries(ps).filter(
script: ps.script, ([key, val]) => (key !== 'flashId') && Object.hasOwn(paramDef.properties, key)
permissions: ps.permissions, )
visibility: ps.visibility, ),
}); });
}); });
} }

View File

@ -100,22 +100,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw err; throw err;
}); });
// Check if already following
const exist = await this.followingsRepository.exists({
where: {
followerId: follower.id,
followeeId: followee.id,
},
});
if (exist) {
throw new ApiError(meta.errors.alreadyFollowing);
}
try { try {
await this.userFollowingService.follow(follower, followee, { withReplies: ps.withReplies }); await this.userFollowingService.follow(follower, followee, { withReplies: ps.withReplies });
} catch (e) { } catch (e) {
if (e instanceof IdentifiableError) { if (e instanceof IdentifiableError) {
if (e.id === 'ec3f65c0-a9d1-47d9-8791-b2e7b9dcdced') throw new ApiError(meta.errors.alreadyFollowing);
if (e.id === '710e8fb0-b8c3-4922-be49-d5d93d8e6a6e') throw new ApiError(meta.errors.blocking); if (e.id === '710e8fb0-b8c3-4922-be49-d5d93d8e6a6e') throw new ApiError(meta.errors.blocking);
if (e.id === '3338392a-f764-498d-8855-db939dcf8c48') throw new ApiError(meta.errors.blocked); if (e.id === '3338392a-f764-498d-8855-db939dcf8c48') throw new ApiError(meta.errors.blocked);
} }

View File

@ -12,6 +12,7 @@ import type { MiDriveFile } from '@/models/DriveFile.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { isNotNull } from '@/misc/is-not-null.js';
export const meta = { export const meta = {
tags: ['gallery'], tags: ['gallery'],
@ -70,7 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
id: fileId, id: fileId,
userId: me.id, userId: me.id,
}), }),
))).filter((file): file is MiDriveFile => file != null); ))).filter(isNotNull);
if (files.length === 0) { if (files.length === 0) {
throw new Error(); throw new Error();

View File

@ -10,6 +10,7 @@ import type { DriveFilesRepository, GalleryPostsRepository } from '@/models/_.js
import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiDriveFile } from '@/models/DriveFile.js';
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { isNotNull } from '@/misc/is-not-null.js';
export const meta = { export const meta = {
tags: ['gallery'], tags: ['gallery'],
@ -68,7 +69,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
id: fileId, id: fileId,
userId: me.id, userId: me.id,
}), }),
))).filter((file): file is MiDriveFile => file != null); ))).filter(isNotNull);
if (files.length === 0) { if (files.length === 0) {
throw new Error(); throw new Error();

View File

@ -43,7 +43,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const hashtags = await this.hashtagsRepository.createQueryBuilder('tag') const hashtags = await this.hashtagsRepository.createQueryBuilder('tag')
.where('tag.name like :q', { q: sqlLikeEscape(ps.query.toLowerCase()) + '%' }) .where('tag.name like :q', { q: sqlLikeEscape(ps.query.toLowerCase()) + '%' })
.orderBy('tag.count', 'DESC') .orderBy('tag.mentionedLocalUsersCount', 'DESC')
.groupBy('tag.id') .groupBy('tag.id')
.limit(ps.limit) .limit(ps.limit)
.offset(ps.offset) .offset(ps.offset)

View File

@ -3,18 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { LessThanOrEqual, MoreThan, Brackets } from 'typeorm'; import { Injectable } from '@nestjs/common';
import { Inject, Injectable } from '@nestjs/common';
import JSON5 from 'json5';
import type { AdsRepository } from '@/models/_.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { MetaEntityService } from '@/core/entities/MetaEntityService.js';
import { MetaService } from '@/core/MetaService.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import { DEFAULT_POLICIES } from '@/core/RoleService.js';
export const meta = { export const meta = {
tags: ['meta'], tags: ['meta'],
@ -23,294 +14,10 @@ export const meta = {
res: { res: {
type: 'object', type: 'object',
optional: false, nullable: false, oneOf: [
properties: { { type: 'object', ref: 'MetaLite' },
maintainerName: { { type: 'object', ref: 'MetaDetailed' },
type: 'string', ],
optional: false, nullable: true,
},
maintainerEmail: {
type: 'string',
optional: false, nullable: true,
},
version: {
type: 'string',
optional: false, nullable: false,
},
name: {
type: 'string',
optional: false, nullable: false,
},
shortName: {
type: 'string',
optional: false, nullable: true,
},
uri: {
type: 'string',
optional: false, nullable: false,
format: 'url',
example: 'https://misskey.example.com',
},
description: {
type: 'string',
optional: false, nullable: true,
},
langs: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
},
},
tosUrl: {
type: 'string',
optional: false, nullable: true,
},
repositoryUrl: {
type: 'string',
optional: false, nullable: true,
default: 'https://github.com/misskey-dev/misskey',
},
feedbackUrl: {
type: 'string',
optional: false, nullable: true,
default: 'https://github.com/misskey-dev/misskey/issues/new',
},
defaultDarkTheme: {
type: 'string',
optional: false, nullable: true,
},
defaultLightTheme: {
type: 'string',
optional: false, nullable: true,
},
disableRegistration: {
type: 'boolean',
optional: false, nullable: false,
},
cacheRemoteFiles: {
type: 'boolean',
optional: false, nullable: false,
},
cacheRemoteSensitiveFiles: {
type: 'boolean',
optional: false, nullable: false,
},
emailRequiredForSignup: {
type: 'boolean',
optional: false, nullable: false,
},
enableHcaptcha: {
type: 'boolean',
optional: false, nullable: false,
},
hcaptchaSiteKey: {
type: 'string',
optional: false, nullable: true,
},
enableMcaptcha: {
type: 'boolean',
optional: false, nullable: false,
},
mcaptchaSiteKey: {
type: 'string',
optional: false, nullable: true,
},
mcaptchaInstanceUrl: {
type: 'string',
optional: false, nullable: true,
},
enableRecaptcha: {
type: 'boolean',
optional: false, nullable: false,
},
recaptchaSiteKey: {
type: 'string',
optional: false, nullable: true,
},
enableTurnstile: {
type: 'boolean',
optional: false, nullable: false,
},
turnstileSiteKey: {
type: 'string',
optional: false, nullable: true,
},
swPublickey: {
type: 'string',
optional: false, nullable: true,
},
mascotImageUrl: {
type: 'string',
optional: false, nullable: false,
default: '/assets/ai.png',
},
bannerUrl: {
type: 'string',
optional: false, nullable: false,
},
serverErrorImageUrl: {
type: 'string',
optional: false, nullable: true,
},
infoImageUrl: {
type: 'string',
optional: false, nullable: true,
},
notFoundImageUrl: {
type: 'string',
optional: false, nullable: true,
},
iconUrl: {
type: 'string',
optional: false, nullable: true,
},
maxNoteTextLength: {
type: 'number',
optional: false, nullable: false,
},
ads: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
properties: {
id: {
type: 'string',
optional: false, nullable: false,
format: 'id',
example: 'xxxxxxxxxx',
},
url: {
type: 'string',
optional: false, nullable: false,
format: 'url',
},
place: {
type: 'string',
optional: false, nullable: false,
enum: ['square', 'horizontal', 'horizontal-big', 'vertical'],
},
ratio: {
type: 'number',
optional: false, nullable: false,
},
imageUrl: {
type: 'string',
optional: false, nullable: false,
format: 'url',
},
dayOfWeek: {
type: 'integer',
optional: false, nullable: false,
},
},
},
},
notesPerOneAd: {
type: 'number',
optional: false, nullable: false,
default: 0,
},
requireSetup: {
type: 'boolean',
optional: false, nullable: false,
example: false,
},
enableEmail: {
type: 'boolean',
optional: false, nullable: false,
},
enableServiceWorker: {
type: 'boolean',
optional: false, nullable: false,
},
translatorAvailable: {
type: 'boolean',
optional: false, nullable: false,
},
proxyAccountName: {
type: 'string',
optional: false, nullable: true,
},
mediaProxy: {
type: 'string',
optional: false, nullable: false,
},
features: {
type: 'object',
optional: true, nullable: false,
properties: {
registration: {
type: 'boolean',
optional: false, nullable: false,
},
localTimeline: {
type: 'boolean',
optional: false, nullable: false,
},
globalTimeline: {
type: 'boolean',
optional: false, nullable: false,
},
hcaptcha: {
type: 'boolean',
optional: false, nullable: false,
},
recaptcha: {
type: 'boolean',
optional: false, nullable: false,
},
objectStorage: {
type: 'boolean',
optional: false, nullable: false,
},
serviceWorker: {
type: 'boolean',
optional: false, nullable: false,
},
miauth: {
type: 'boolean',
optional: true, nullable: false,
default: true,
},
},
},
backgroundImageUrl: {
type: 'string',
optional: false, nullable: true,
},
impressumUrl: {
type: 'string',
optional: false, nullable: true,
},
logoImageUrl: {
type: 'string',
optional: false, nullable: true,
},
privacyPolicyUrl: {
type: 'string',
optional: false, nullable: true,
},
serverRules: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'string',
},
},
themeColor: {
type: 'string',
optional: false, nullable: true,
},
policies: {
type: 'object',
optional: false, nullable: false,
ref: 'RolePolicies',
},
},
}, },
} as const; } as const;
@ -325,114 +32,10 @@ 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.config) private metaEntityService: MetaEntityService,
private config: Config,
@Inject(DI.adsRepository)
private adsRepository: AdsRepository,
private userEntityService: UserEntityService,
private metaService: MetaService,
private instanceActorService: InstanceActorService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const instance = await this.metaService.fetch(true); return ps.detail ? await this.metaEntityService.packDetailed() : await this.metaEntityService.pack();
const ads = await this.adsRepository.createQueryBuilder('ads')
.where('ads.expiresAt > :now', { now: new Date() })
.andWhere('ads.startsAt <= :now', { now: new Date() })
.andWhere(new Brackets(qb => {
// 曜日のビットフラグを確認する
qb.where('ads.dayOfWeek & :dayOfWeek > 0', { dayOfWeek: 1 << new Date().getDay() })
.orWhere('ads.dayOfWeek = 0');
}))
.getMany();
const response: any = {
maintainerName: instance.maintainerName,
maintainerEmail: instance.maintainerEmail,
version: this.config.version,
name: instance.name,
shortName: instance.shortName,
uri: this.config.url,
description: instance.description,
langs: instance.langs,
tosUrl: instance.termsOfServiceUrl,
repositoryUrl: instance.repositoryUrl,
feedbackUrl: instance.feedbackUrl,
impressumUrl: instance.impressumUrl,
privacyPolicyUrl: instance.privacyPolicyUrl,
disableRegistration: instance.disableRegistration,
emailRequiredForSignup: instance.emailRequiredForSignup,
enableHcaptcha: instance.enableHcaptcha,
hcaptchaSiteKey: instance.hcaptchaSiteKey,
enableMcaptcha: instance.enableMcaptcha,
mcaptchaSiteKey: instance.mcaptchaSitekey,
mcaptchaInstanceUrl: instance.mcaptchaInstanceUrl,
enableRecaptcha: instance.enableRecaptcha,
recaptchaSiteKey: instance.recaptchaSiteKey,
enableTurnstile: instance.enableTurnstile,
turnstileSiteKey: instance.turnstileSiteKey,
swPublickey: instance.swPublicKey,
themeColor: instance.themeColor,
mascotImageUrl: instance.mascotImageUrl,
bannerUrl: instance.bannerUrl,
infoImageUrl: instance.infoImageUrl,
serverErrorImageUrl: instance.serverErrorImageUrl,
notFoundImageUrl: instance.notFoundImageUrl,
iconUrl: instance.iconUrl,
backgroundImageUrl: instance.backgroundImageUrl,
logoImageUrl: instance.logoImageUrl,
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH,
// クライアントの手間を減らすためあらかじめJSONに変換しておく
defaultLightTheme: instance.defaultLightTheme ? JSON.stringify(JSON5.parse(instance.defaultLightTheme)) : null,
defaultDarkTheme: instance.defaultDarkTheme ? JSON.stringify(JSON5.parse(instance.defaultDarkTheme)) : null,
ads: ads.map(ad => ({
id: ad.id,
url: ad.url,
place: ad.place,
ratio: ad.ratio,
imageUrl: ad.imageUrl,
dayOfWeek: ad.dayOfWeek,
})),
notesPerOneAd: instance.notesPerOneAd,
enableEmail: instance.enableEmail,
enableServiceWorker: instance.enableServiceWorker,
translatorAvailable: instance.deeplAuthKey != null,
serverRules: instance.serverRules,
policies: { ...DEFAULT_POLICIES, ...instance.policies },
mediaProxy: this.config.mediaProxy,
...(ps.detail ? {
cacheRemoteFiles: instance.cacheRemoteFiles,
cacheRemoteSensitiveFiles: instance.cacheRemoteSensitiveFiles,
requireSetup: !await this.instanceActorService.realLocalUsersPresent(),
} : {}),
};
if (ps.detail) {
const proxyAccount = instance.proxyAccountId ? await this.userEntityService.pack(instance.proxyAccountId, null).catch(() => null) : null;
response.proxyAccountName = proxyAccount ? proxyAccount.username : null;
response.features = {
registration: !instance.disableRegistration,
emailRequiredForSignup: instance.emailRequiredForSignup,
hcaptcha: instance.enableHcaptcha,
recaptcha: instance.enableRecaptcha,
turnstile: instance.enableTurnstile,
objectStorage: instance.useObjectStorage,
serviceWorker: instance.enableServiceWorker,
miauth: true,
};
}
return response;
}); });
} }
} }

View File

@ -12,6 +12,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { isNotNull } from '@/misc/is-not-null.js';
export const meta = { export const meta = {
tags: ['users'], tags: ['users'],
@ -52,7 +53,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
host: acct.host ?? IsNull(), host: acct.host ?? IsNull(),
}))); })));
return await this.userEntityService.packMany(users.filter(x => x !== null) as MiUser[], me, { schema: 'UserDetailed' }); return await this.userEntityService.packMany(users.filter(isNotNull), me, { schema: 'UserDetailed' });
}); });
} }
} }

View File

@ -9,6 +9,7 @@ import type { SwSubscriptionsRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { PushNotificationService } from '@/core/PushNotificationService.js';
export const meta = { export const meta = {
tags: ['account'], tags: ['account'],
@ -66,6 +67,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private idService: IdService, private idService: IdService,
private metaService: MetaService, private metaService: MetaService,
private pushNotificationService: PushNotificationService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
// if already subscribed // if already subscribed
@ -97,6 +99,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
sendReadMessage: ps.sendReadMessage, sendReadMessage: ps.sendReadMessage,
}); });
this.pushNotificationService.refreshCache(me.id);
return { return {
state: 'subscribed' as const, state: 'subscribed' as const,
key: instance.swPublicKey, key: instance.swPublicKey,

View File

@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
import type { SwSubscriptionsRepository } from '@/models/_.js'; import type { SwSubscriptionsRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { PushNotificationService } from '@/core/PushNotificationService.js';
export const meta = { export const meta = {
tags: ['account'], tags: ['account'],
@ -29,12 +30,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor( constructor(
@Inject(DI.swSubscriptionsRepository) @Inject(DI.swSubscriptionsRepository)
private swSubscriptionsRepository: SwSubscriptionsRepository, private swSubscriptionsRepository: SwSubscriptionsRepository,
private pushNotificationService: PushNotificationService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
await this.swSubscriptionsRepository.delete({ await this.swSubscriptionsRepository.delete({
...(me ? { userId: me.id } : {}), ...(me ? { userId: me.id } : {}),
endpoint: ps.endpoint, endpoint: ps.endpoint,
}); });
if (me) {
this.pushNotificationService.refreshCache(me.id);
}
}); });
} }
} }

View File

@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
import type { SwSubscriptionsRepository } from '@/models/_.js'; import type { SwSubscriptionsRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { PushNotificationService } from '@/core/PushNotificationService.js';
import { ApiError } from '../../error.js'; import { ApiError } from '../../error.js';
export const meta = { export const meta = {
@ -58,6 +59,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor( constructor(
@Inject(DI.swSubscriptionsRepository) @Inject(DI.swSubscriptionsRepository)
private swSubscriptionsRepository: SwSubscriptionsRepository, private swSubscriptionsRepository: SwSubscriptionsRepository,
private pushNotificationService: PushNotificationService,
) { ) {
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const swSubscription = await this.swSubscriptionsRepository.findOneBy({ const swSubscription = await this.swSubscriptionsRepository.findOneBy({
@ -77,6 +80,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
sendReadMessage: swSubscription.sendReadMessage, sendReadMessage: swSubscription.sendReadMessage,
}); });
this.pushNotificationService.refreshCache(me.id);
return { return {
userId: swSubscription.userId, userId: swSubscription.userId,
endpoint: swSubscription.endpoint, endpoint: swSubscription.endpoint,

View File

@ -19,6 +19,7 @@ import fastifyView from '@fastify/view';
import fastifyCookie from '@fastify/cookie'; import fastifyCookie from '@fastify/cookie';
import fastifyProxy from '@fastify/http-proxy'; import fastifyProxy from '@fastify/http-proxy';
import vary from 'vary'; import vary from 'vary';
import htmlSafeJsonStringify from 'htmlescape';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { getNoteSummary } from '@/misc/get-note-summary.js'; import { getNoteSummary } from '@/misc/get-note-summary.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
@ -28,12 +29,12 @@ import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, Obj
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { PageEntityService } from '@/core/entities/PageEntityService.js'; import { PageEntityService } from '@/core/entities/PageEntityService.js';
import { MetaEntityService } from '@/core/entities/MetaEntityService.js';
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
import type { ChannelsRepository, ClipsRepository, FlashsRepository, GalleryPostsRepository, MiMeta, NotesRepository, PagesRepository, ReversiGamesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import type { ChannelsRepository, ClipsRepository, FlashsRepository, GalleryPostsRepository, MiMeta, NotesRepository, PagesRepository, ReversiGamesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { deepClone } from '@/misc/clone.js';
import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js'; import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { FlashEntityService } from '@/core/entities/FlashEntityService.js'; import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
@ -93,6 +94,7 @@ export class ClientServerService {
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
private pageEntityService: PageEntityService, private pageEntityService: PageEntityService,
private metaEntityService: MetaEntityService,
private galleryPostEntityService: GalleryPostEntityService, private galleryPostEntityService: GalleryPostEntityService,
private clipEntityService: ClipEntityService, private clipEntityService: ClipEntityService,
private channelEntityService: ChannelEntityService, private channelEntityService: ChannelEntityService,
@ -173,7 +175,7 @@ export class ClientServerService {
} }
@bindThis @bindThis
private generateCommonPugData(meta: MiMeta) { private async generateCommonPugData(meta: MiMeta) {
return { return {
instanceName: meta.name ?? 'Misskey', instanceName: meta.name ?? 'Misskey',
icon: meta.iconUrl, icon: meta.iconUrl,
@ -183,6 +185,8 @@ export class ClientServerService {
infoImageUrl: meta.infoImageUrl ?? 'https://xn--931a.moe/assets/info.jpg', infoImageUrl: meta.infoImageUrl ?? 'https://xn--931a.moe/assets/info.jpg',
notFoundImageUrl: meta.notFoundImageUrl ?? 'https://xn--931a.moe/assets/not-found.jpg', notFoundImageUrl: meta.notFoundImageUrl ?? 'https://xn--931a.moe/assets/not-found.jpg',
instanceUrl: this.config.url, instanceUrl: this.config.url,
metaJson: htmlSafeJsonStringify(await this.metaEntityService.packDetailed(meta)),
now: Date.now(),
}; };
} }
@ -434,7 +438,7 @@ export class ClientServerService {
url: this.config.url, url: this.config.url,
title: meta.name ?? 'Misskey', title: meta.name ?? 'Misskey',
desc: meta.description, desc: meta.description,
...this.generateCommonPugData(meta), ...await this.generateCommonPugData(meta),
}); });
}; };
@ -521,7 +525,7 @@ export class ClientServerService {
user, profile, me, user, profile, me,
avatarUrl: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user), avatarUrl: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user),
sub: request.params.sub, sub: request.params.sub,
...this.generateCommonPugData(meta), ...await this.generateCommonPugData(meta),
}); });
} else { } else {
// リモートユーザーなので // リモートユーザーなので
@ -572,7 +576,7 @@ export class ClientServerService {
avatarUrl: _note.user.avatarUrl, avatarUrl: _note.user.avatarUrl,
// TODO: Let locale changeable by instance setting // TODO: Let locale changeable by instance setting
summary: getNoteSummary(_note), summary: getNoteSummary(_note),
...this.generateCommonPugData(meta), ...await this.generateCommonPugData(meta),
}); });
} catch (err) { } catch (err) {
if ((err as IdentifiableError).id === '8ca4f428-b32e-4f83-ac43-406ed7cd0452') { if ((err as IdentifiableError).id === '8ca4f428-b32e-4f83-ac43-406ed7cd0452') {
@ -618,7 +622,7 @@ export class ClientServerService {
page: _page, page: _page,
profile, profile,
avatarUrl: _page.user.avatarUrl, avatarUrl: _page.user.avatarUrl,
...this.generateCommonPugData(meta), ...await this.generateCommonPugData(meta),
}); });
} catch (err) { } catch (err) {
if ((err as IdentifiableError).id === '8ca4f428-b32e-4f83-ac43-406ed7cd0452') { if ((err as IdentifiableError).id === '8ca4f428-b32e-4f83-ac43-406ed7cd0452') {
@ -651,7 +655,7 @@ export class ClientServerService {
flash: _flash, flash: _flash,
profile, profile,
avatarUrl: _flash.user.avatarUrl, avatarUrl: _flash.user.avatarUrl,
...this.generateCommonPugData(meta), ...await this.generateCommonPugData(meta),
}); });
} catch (err) { } catch (err) {
if ((err as IdentifiableError).id === '8ca4f428-b32e-4f83-ac43-406ed7cd0452') { if ((err as IdentifiableError).id === '8ca4f428-b32e-4f83-ac43-406ed7cd0452') {
@ -684,7 +688,7 @@ export class ClientServerService {
clip: _clip, clip: _clip,
profile, profile,
avatarUrl: _clip.user.avatarUrl, avatarUrl: _clip.user.avatarUrl,
...this.generateCommonPugData(meta), ...await this.generateCommonPugData(meta),
}); });
} catch (err) { } catch (err) {
if ((err as IdentifiableError).id === '8ca4f428-b32e-4f83-ac43-406ed7cd0452') { if ((err as IdentifiableError).id === '8ca4f428-b32e-4f83-ac43-406ed7cd0452') {
@ -715,7 +719,7 @@ export class ClientServerService {
post: _post, post: _post,
profile, profile,
avatarUrl: _post.user.avatarUrl, avatarUrl: _post.user.avatarUrl,
...this.generateCommonPugData(meta), ...await this.generateCommonPugData(meta),
}); });
} catch (err) { } catch (err) {
if ((err as IdentifiableError).id === '8ca4f428-b32e-4f83-ac43-406ed7cd0452') { if ((err as IdentifiableError).id === '8ca4f428-b32e-4f83-ac43-406ed7cd0452') {
@ -740,7 +744,7 @@ export class ClientServerService {
reply.header('Cache-Control', 'public, max-age=15'); reply.header('Cache-Control', 'public, max-age=15');
return await reply.view('channel', { return await reply.view('channel', {
channel: _channel, channel: _channel,
...this.generateCommonPugData(meta), ...await this.generateCommonPugData(meta),
}); });
} else { } else {
return await renderBase(reply); return await renderBase(reply);
@ -759,7 +763,7 @@ export class ClientServerService {
reply.header('Cache-Control', 'public, max-age=3600'); reply.header('Cache-Control', 'public, max-age=3600');
return await reply.view('reversi-game', { return await reply.view('reversi-game', {
game: _game, game: _game,
...this.generateCommonPugData(meta), ...await this.generateCommonPugData(meta),
}); });
} else { } else {
return await renderBase(reply); return await renderBase(reply);

View File

@ -61,6 +61,9 @@ html
meta(property='og:image' content= img) meta(property='og:image' content= img)
meta(property='twitter:card' content='summary') meta(property='twitter:card' content='summary')
script(type='application/json' id='misskey_meta' data-generated-at=now)
!= metaJson
style style
include ../style.css include ../style.css

View File

@ -69,6 +69,7 @@ export const moderationLogTypes = [
'resetPassword', 'resetPassword',
'suspendRemoteInstance', 'suspendRemoteInstance',
'unsuspendRemoteInstance', 'unsuspendRemoteInstance',
'updateRemoteInstanceNote',
'markSensitiveDriveFile', 'markSensitiveDriveFile',
'unmarkSensitiveDriveFile', 'unmarkSensitiveDriveFile',
'resolveAbuseReport', 'resolveAbuseReport',
@ -212,6 +213,12 @@ export type ModerationLogPayloads = {
id: string; id: string;
host: string; host: string;
}; };
updateRemoteInstanceNote: {
id: string;
host: string;
before: string | null;
after: string | null;
};
markSensitiveDriveFile: { markSensitiveDriveFile: {
fileId: string; fileId: string;
fileUserId: string | null; fileUserId: string | null;

View File

@ -11,7 +11,7 @@ import { alert, confirm, popup, post, toast } from '@/os.js';
import { useStream } from '@/stream.js'; import { useStream } from '@/stream.js';
import * as sound from '@/scripts/sound.js'; import * as sound from '@/scripts/sound.js';
import { $i, signout, updateAccount } from '@/account.js'; import { $i, signout, updateAccount } from '@/account.js';
import { fetchInstance, instance } from '@/instance.js'; import { instance } from '@/instance.js';
import { ColdDeviceStorage, defaultStore } from '@/store.js'; import { ColdDeviceStorage, defaultStore } from '@/store.js';
import { makeHotkey } from '@/scripts/hotkey.js'; import { makeHotkey } from '@/scripts/hotkey.js';
import { reactionPicker } from '@/scripts/reaction-picker.js'; import { reactionPicker } from '@/scripts/reaction-picker.js';
@ -226,12 +226,10 @@ export async function mainBoot() {
} }
} }
fetchInstance().then(() => { const modifiedVersionMustProminentlyOfferInAgplV3Section13Read = miLocalStorage.getItem('modifiedVersionMustProminentlyOfferInAgplV3Section13Read');
const modifiedVersionMustProminentlyOfferInAgplV3Section13Read = miLocalStorage.getItem('modifiedVersionMustProminentlyOfferInAgplV3Section13Read'); if (modifiedVersionMustProminentlyOfferInAgplV3Section13Read !== 'true' && instance.repositoryUrl !== 'https://github.com/misskey-dev/misskey') {
if (modifiedVersionMustProminentlyOfferInAgplV3Section13Read !== 'true' && instance.repositoryUrl !== 'https://github.com/misskey-dev/misskey') { popup(defineAsyncComponent(() => import('@/components/MkSourceCodeAvailablePopup.vue')), {}, {}, 'closed');
popup(defineAsyncComponent(() => import('@/components/MkSourceCodeAvailablePopup.vue')), {}, {}, 'closed'); }
}
});
if ('Notification' in window) { if ('Notification' in window) {
// 許可を得ていなかったらリクエスト // 許可を得ていなかったらリクエスト

View File

@ -57,18 +57,7 @@ import { i18n } from '@/i18n.js';
import { miLocalStorage } from '@/local-storage.js'; import { miLocalStorage } from '@/local-storage.js';
import { customEmojis } from '@/custom-emojis.js'; import { customEmojis } from '@/custom-emojis.js';
import { MFM_TAGS, MFM_PARAMS } from '@/const.js'; import { MFM_TAGS, MFM_PARAMS } from '@/const.js';
import { searchEmoji, EmojiDef } from '@/scripts/search-emoji.js';
type EmojiDef = {
emoji: string;
name: string;
url: string;
aliasOf?: string;
} | {
emoji: string;
name: string;
aliasOf?: string;
isCustomEmoji?: true;
};
const lib = emojilist.filter(x => x.category !== 'flags'); const lib = emojilist.filter(x => x.category !== 'flags');
@ -249,7 +238,7 @@ function exec() {
return; return;
} }
emojis.value = emojiAutoComplete(props.q, emojiDb.value); emojis.value = searchEmoji(props.q, emojiDb.value);
} else if (props.type === 'mfmTag') { } else if (props.type === 'mfmTag') {
if (!props.q || props.q === '') { if (!props.q || props.q === '') {
mfmTags.value = MFM_TAGS; mfmTags.value = MFM_TAGS;
@ -267,87 +256,6 @@ function exec() {
} }
} }
type EmojiScore = { emoji: EmojiDef, score: number };
function emojiAutoComplete(query: string | null, emojiDb: EmojiDef[], max = 30): EmojiDef[] {
if (!query) {
return [];
}
const matched = new Map<string, EmojiScore>();
//
emojiDb.some(x => {
if (x.name === query && !matched.has(x.aliasOf ?? x.name)) {
matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length + 2 });
}
return matched.size === max;
});
//
if (matched.size < max) {
emojiDb.some(x => {
if (x.name.startsWith(query) && !x.aliasOf) {
matched.set(x.name, { emoji: x, score: query.length + 1 });
}
return matched.size === max;
});
}
//
if (matched.size < max) {
emojiDb.some(x => {
if (x.name.startsWith(query) && !matched.has(x.aliasOf ?? x.name)) {
matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length });
}
return matched.size === max;
});
}
//
if (matched.size < max) {
emojiDb.some(x => {
if (x.name.includes(query) && !matched.has(x.aliasOf ?? x.name)) {
matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length - 1 });
}
return matched.size === max;
});
}
// 3
if (matched.size < max && query.length > 3) {
const queryChars = [...query];
const hitEmojis = new Map<string, EmojiScore>();
for (const x of emojiDb) {
//
let pos = 0;
let hit = 0;
for (const c of queryChars) {
pos = x.name.indexOf(c, pos);
if (pos <= -1) break;
hit++;
}
//
if (hit > Math.ceil(queryChars.length / 2) && hit - 2 > (matched.get(x.aliasOf ?? x.name)?.score ?? 0)) {
hitEmojis.set(x.aliasOf ?? x.name, { emoji: x, score: hit - 2 });
}
}
// 66
[...hitEmojis.values()]
.sort((x, y) => y.score - x.score)
.slice(0, 6)
.forEach(it => matched.set(it.emoji.name, it));
}
return [...matched.values()]
.sort((x, y) => y.score - x.score)
.slice(0, max)
.map(it => it.emoji);
}
function onMousedown(event: Event) { function onMousedown(event: Event) {
if (!contains(rootEl.value, event.target) && (rootEl.value !== event.target)) props.close(); if (!contains(rootEl.value, event.target) && (rootEl.value !== event.target)) props.close();
} }

View File

@ -16,6 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:key="emoji" :key="emoji"
:data-emoji="emoji" :data-emoji="emoji"
class="_button item" class="_button item"
:disabled="disabledEmojis?.value.includes(emoji)"
@pointerenter="computeButtonTitle" @pointerenter="computeButtonTitle"
@click="emit('chosen', emoji, $event)" @click="emit('chosen', emoji, $event)"
> >
@ -48,6 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:key="emoji" :key="emoji"
:data-emoji="emoji" :data-emoji="emoji"
class="_button item" class="_button item"
:disabled="disabledEmojis?.value.includes(emoji)"
@pointerenter="computeButtonTitle" @pointerenter="computeButtonTitle"
@click="emit('chosen', emoji, $event)" @click="emit('chosen', emoji, $event)"
> >
@ -67,6 +69,7 @@ import MkEmojiPickerSection from '@/components/MkEmojiPicker.section.vue';
const props = defineProps<{ const props = defineProps<{
emojis: string[] | Ref<string[]>; emojis: string[] | Ref<string[]>;
disabledEmojis?: Ref<string[]>;
initialShown?: boolean; initialShown?: boolean;
hasChildSection?: boolean; hasChildSection?: boolean;
customEmojiTree?: CustomEmojiFolderTree[]; customEmojiTree?: CustomEmojiFolderTree[];

View File

@ -14,6 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
v-for="emoji in searchResultCustom" v-for="emoji in searchResultCustom"
:key="emoji.name" :key="emoji.name"
class="_button item" class="_button item"
:disabled="!canReact(emoji)"
:title="emoji.name" :title="emoji.name"
tabindex="0" tabindex="0"
@click="chosen(emoji, $event)" @click="chosen(emoji, $event)"
@ -39,16 +40,17 @@ SPDX-License-Identifier: AGPL-3.0-only
<section v-if="showPinned && (pinned && pinned.length > 0)"> <section v-if="showPinned && (pinned && pinned.length > 0)">
<div class="body"> <div class="body">
<button <button
v-for="emoji in pinned" v-for="emoji in pinnedEmojisDef"
:key="emoji" :key="getKey(emoji)"
:data-emoji="emoji" :data-emoji="getKey(emoji)"
class="_button item" class="_button item"
:disabled="!canReact(emoji)"
tabindex="0" tabindex="0"
@pointerenter="computeButtonTitle" @pointerenter="computeButtonTitle"
@click="chosen(emoji, $event)" @click="chosen(emoji, $event)"
> >
<MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true"/> <MkCustomEmoji v-if="!emoji.hasOwnProperty('char')" class="emoji" :name="getKey(emoji)" :normal="true"/>
<MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/> <MkEmoji v-else class="emoji" :emoji="getKey(emoji)" :normal="true"/>
</button> </button>
</div> </div>
</section> </section>
@ -57,15 +59,16 @@ SPDX-License-Identifier: AGPL-3.0-only
<header class="_acrylic"><i class="ti ti-clock ti-fw"></i> {{ i18n.ts.recentUsed }}</header> <header class="_acrylic"><i class="ti ti-clock ti-fw"></i> {{ i18n.ts.recentUsed }}</header>
<div class="body"> <div class="body">
<button <button
v-for="emoji in recentlyUsedEmojis" v-for="emoji in recentlyUsedEmojisDef"
:key="emoji" :key="getKey(emoji)"
class="_button item" class="_button item"
:data-emoji="emoji" :disabled="!canReact(emoji)"
:data-emoji="getKey(emoji)"
@pointerenter="computeButtonTitle" @pointerenter="computeButtonTitle"
@click="chosen(emoji, $event)" @click="chosen(emoji, $event)"
> >
<MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true"/> <MkCustomEmoji v-if="!emoji.hasOwnProperty('char')" class="emoji" :name="getKey(emoji)" :normal="true"/>
<MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/> <MkEmoji v-else class="emoji" :emoji="getKey(emoji)" :normal="true"/>
</button> </button>
</div> </div>
</section> </section>
@ -76,7 +79,8 @@ SPDX-License-Identifier: AGPL-3.0-only
v-for="child in customEmojiFolderRoot.children" v-for="child in customEmojiFolderRoot.children"
:key="`custom:${child.category}`" :key="`custom:${child.category}`"
:initialShown="false" :initialShown="false"
:emojis="computed(() => customEmojis.filter(e => child.category === '' ? (e.category === 'null' || !e.category) : e.category === child.category).filter(filterAvailable).map(e => `:${e.name}:`))" :emojis="computed(() => customEmojis.filter(e => filterCategory(e, child.value)).map(e => `:${e.name}:`))"
:disabledEmojis="computed(() => customEmojis.filter(e => filterCategory(e, child.value)).filter(e => !canReact(e)).map(e => `:${e.name}:`))"
:hasChildSection="child.children.length !== 0" :hasChildSection="child.children.length !== 0"
:customEmojiTree="child.children" :customEmojiTree="child.children"
@chosen="chosen" @chosen="chosen"
@ -104,6 +108,7 @@ import * as Misskey from 'misskey-js';
import XSection from '@/components/MkEmojiPicker.section.vue'; import XSection from '@/components/MkEmojiPicker.section.vue';
import { import {
emojilist, emojilist,
unicodeEmojisMap,
emojiCharByCategory, emojiCharByCategory,
UnicodeEmojiDef, UnicodeEmojiDef,
unicodeEmojiCategories as categories, unicodeEmojiCategories as categories,
@ -146,6 +151,13 @@ const {
recentlyUsedEmojis, recentlyUsedEmojis,
} = defaultStore.reactiveState; } = defaultStore.reactiveState;
const recentlyUsedEmojisDef = computed(() => {
return recentlyUsedEmojis.value.map(getDef);
});
const pinnedEmojisDef = computed(() => {
return pinned.value?.map(getDef);
});
const pinned = computed(() => props.pinnedEmojis); const pinned = computed(() => props.pinnedEmojis);
const size = computed(() => emojiPickerScale.value); const size = computed(() => emojiPickerScale.value);
const width = computed(() => emojiPickerWidth.value); const width = computed(() => emojiPickerWidth.value);
@ -341,14 +353,18 @@ watch(q, () => {
return matches; return matches;
}; };
searchResultCustom.value = Array.from(searchCustom()).filter(filterAvailable); searchResultCustom.value = Array.from(searchCustom());
searchResultUnicode.value = Array.from(searchUnicode()); searchResultUnicode.value = Array.from(searchUnicode());
}); });
function filterAvailable(emoji: Misskey.entities.EmojiSimple): boolean { function canReact(emoji: Misskey.entities.EmojiSimple | UnicodeEmojiDef): boolean {
return !props.targetNote || checkReactionPermissions($i!, props.targetNote, emoji); return !props.targetNote || checkReactionPermissions($i!, props.targetNote, emoji);
} }
function filterCategory(emoji: Misskey.entities.EmojiSimple, category: string): boolean {
return category === '' ? (emoji.category === 'null' || !emoji.category) : emoji.category === category;
}
function focus() { function focus() {
if (!['smartphone', 'tablet'].includes(deviceKind) && !isTouchUsing) { if (!['smartphone', 'tablet'].includes(deviceKind) && !isTouchUsing) {
searchEl.value?.focus({ searchEl.value?.focus({
@ -366,6 +382,14 @@ function getKey(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef):
return typeof emoji === 'string' ? emoji : 'char' in emoji ? emoji.char : `:${emoji.name}:`; return typeof emoji === 'string' ? emoji : 'char' in emoji ? emoji.char : `:${emoji.name}:`;
} }
function getDef(emoji: string) {
if (emoji.includes(':')) {
return customEmojisMap.get(emoji.replace(/:/g, ''))!;
} else {
return unicodeEmojisMap.get(emoji)!;
}
}
/** @see MkEmojiPicker.section.vue */ /** @see MkEmojiPicker.section.vue */
function computeButtonTitle(ev: MouseEvent): void { function computeButtonTitle(ev: MouseEvent): void {
const elm = ev.target as HTMLElement; const elm = ev.target as HTMLElement;
@ -530,6 +554,18 @@ defineExpose({
width: auto; width: auto;
height: auto; height: auto;
min-width: 0; min-width: 0;
&:disabled {
cursor: not-allowed;
background: linear-gradient(-45deg, transparent 0% 48%, var(--X6) 48% 52%, transparent 52% 100%);
opacity: 1;
> .emoji {
filter: grayscale(1);
mix-blend-mode: exclusion;
opacity: 0.8;
}
}
} }
} }
} }
@ -552,6 +588,18 @@ defineExpose({
width: auto; width: auto;
height: auto; height: auto;
min-width: 0; min-width: 0;
&:disabled {
cursor: not-allowed;
background: linear-gradient(-45deg, transparent 0% 48%, var(--X6) 48% 52%, transparent 52% 100%);
opacity: 1;
> .emoji {
filter: grayscale(1);
mix-blend-mode: exclusion;
opacity: 0.8;
}
}
} }
} }
} }
@ -667,6 +715,18 @@ defineExpose({
box-shadow: inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15); box-shadow: inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15);
} }
&:disabled {
cursor: not-allowed;
background: linear-gradient(-45deg, transparent 0% 48%, var(--X6) 48% 52%, transparent 52% 100%);
opacity: 1;
> .emoji {
filter: grayscale(1);
mix-blend-mode: exclusion;
opacity: 0.8;
}
}
> .emoji { > .emoji {
height: 1.25em; height: 1.25em;
vertical-align: -.25em; vertical-align: -.25em;

View File

@ -33,7 +33,8 @@ import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import * as sound from '@/scripts/sound.js'; import * as sound from '@/scripts/sound.js';
import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js'; import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js';
import { customEmojis } from '@/custom-emojis.js'; import { customEmojisMap } from '@/custom-emojis.js';
import { unicodeEmojisMap } from '@/scripts/emojilist.js';
const props = defineProps<{ const props = defineProps<{
reaction: string; reaction: string;
@ -50,13 +51,11 @@ const emit = defineEmits<{
const buttonEl = shallowRef<HTMLElement>(); const buttonEl = shallowRef<HTMLElement>();
const isCustomEmoji = computed(() => props.reaction.includes(':')); const emojiName = computed(() => props.reaction.replace(/:/g, '').replace(/@\./, ''));
const emoji = computed(() => isCustomEmoji.value ? customEmojis.value.find(emoji => emoji.name === props.reaction.replace(/:/g, '').replace(/@\./, '')) : null); const emoji = computed(() => customEmojisMap.get(emojiName.value) ?? unicodeEmojisMap.get(props.reaction));
const canToggle = computed(() => { const canToggle = computed(() => {
return !props.reaction.match(/@\w/) && $i return !props.reaction.match(/@\w/) && $i && emoji.value && checkReactionPermissions($i, props.note, emoji.value);
&& (emoji.value && checkReactionPermissions($i, props.note, emoji.value))
|| !isCustomEmoji.value;
}); });
const canGetInfo = computed(() => !props.reaction.match(/@\w/) && props.reaction.includes(':')); const canGetInfo = computed(() => !props.reaction.match(/@\w/) && props.reaction.includes(':'));

View File

@ -10,7 +10,7 @@ import MkTime from './MkTime.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { dateTimeFormat } from '@/scripts/intl-const.js'; import { dateTimeFormat } from '@/scripts/intl-const.js';
const now = new Date('2023-04-01T00:00:00.000Z'); const now = new Date('2023-04-01T00:00:00.000Z');
const future = new Date(8640000000000000); const future = new Date('3000-04-01T00:00:00.000Z');
const oneHourAgo = new Date(now.getTime() - 3600000); const oneHourAgo = new Date(now.getTime() - 3600000);
const oneDayAgo = new Date(now.getTime() - 86400000); const oneDayAgo = new Date(now.getTime() - 86400000);
const oneWeekAgo = new Date(now.getTime() - 604800000); const oneWeekAgo = new Date(now.getTime() - 604800000);
@ -49,11 +49,12 @@ export const Empty = {
export const RelativeFuture = { export const RelativeFuture = {
...Empty, ...Empty,
async play({ canvasElement }) { async play({ canvasElement }) {
await expect(canvasElement).toHaveTextContent(i18n.ts._ago.future); await expect(canvasElement).toHaveTextContent(i18n.tsx._timeIn.years({ n: 977 }));
}, },
args: { args: {
...Empty.args, ...Empty.args,
time: future, time: future,
origin: now,
}, },
} satisfies StoryObj<typeof MkTime>; } satisfies StoryObj<typeof MkTime>;
export const AbsoluteFuture = { export const AbsoluteFuture = {

View File

@ -11,13 +11,24 @@ import { DEFAULT_INFO_IMAGE_URL, DEFAULT_NOT_FOUND_IMAGE_URL, DEFAULT_SERVER_ERR
// TODO: 他のタブと永続化されたstateを同期 // TODO: 他のタブと永続化されたstateを同期
const cached = miLocalStorage.getItem('instance'); //#region loader
const providedMetaEl = document.getElementById('misskey_meta');
let cachedMeta = miLocalStorage.getItem('instance') ? JSON.parse(miLocalStorage.getItem('instance')!) : null;
let cachedAt = miLocalStorage.getItem('instanceCachedAt') ? parseInt(miLocalStorage.getItem('instanceCachedAt')!) : 0;
const providedMeta = providedMetaEl?.textContent ? JSON.parse(providedMetaEl.textContent) : null;
const providedAt = providedMetaEl?.dataset.generatedAt ? parseInt(providedMetaEl.dataset.generatedAt) : 0;
if (providedAt > cachedAt) {
miLocalStorage.setItem('instance', JSON.stringify(providedMeta));
miLocalStorage.setItem('instanceCachedAt', providedAt.toString());
cachedMeta = providedMeta;
cachedAt = providedAt;
}
//#endregion
// TODO: instanceをリアクティブにするかは再考の余地あり // TODO: instanceをリアクティブにするかは再考の余地あり
export const instance: Misskey.entities.MetaResponse = reactive(cached ? JSON.parse(cached) : { export const instance: Misskey.entities.MetaResponse = reactive(cachedMeta ?? {});
// TODO: set default values
});
export const serverErrorImageUrl = computed(() => instance.serverErrorImageUrl ?? DEFAULT_SERVER_ERROR_IMAGE_URL); export const serverErrorImageUrl = computed(() => instance.serverErrorImageUrl ?? DEFAULT_SERVER_ERROR_IMAGE_URL);
@ -25,7 +36,15 @@ export const infoImageUrl = computed(() => instance.infoImageUrl ?? DEFAULT_INFO
export const notFoundImageUrl = computed(() => instance.notFoundImageUrl ?? DEFAULT_NOT_FOUND_IMAGE_URL); export const notFoundImageUrl = computed(() => instance.notFoundImageUrl ?? DEFAULT_NOT_FOUND_IMAGE_URL);
export async function fetchInstance() { export async function fetchInstance(force = false): Promise<void> {
if (!force) {
const cachedAt = miLocalStorage.getItem('instanceCachedAt') ? parseInt(miLocalStorage.getItem('instanceCachedAt')!) : 0;
if (Date.now() - cachedAt < 1000 * 60 * 60) {
return;
}
}
const meta = await misskeyApi('meta', { const meta = await misskeyApi('meta', {
detail: false, detail: false,
}); });
@ -35,4 +54,5 @@ export async function fetchInstance() {
} }
miLocalStorage.setItem('instance', JSON.stringify(instance)); miLocalStorage.setItem('instance', JSON.stringify(instance));
miLocalStorage.setItem('instanceCachedAt', Date.now().toString());
} }

View File

@ -7,6 +7,7 @@ type Keys =
'v' | 'v' |
'lastVersion' | 'lastVersion' |
'instance' | 'instance' |
'instanceCachedAt' |
'account' | 'account' |
'accounts' | 'accounts' |
'latestDonationInfoShownAt' | 'latestDonationInfoShownAt' |

View File

@ -142,7 +142,7 @@ function save() {
turnstileSiteKey: turnstileSiteKey.value, turnstileSiteKey: turnstileSiteKey.value,
turnstileSecretKey: turnstileSecretKey.value, turnstileSecretKey: turnstileSecretKey.value,
}).then(() => { }).then(() => {
fetchInstance(); fetchInstance(true);
}); });
} }
</script> </script>

View File

@ -169,7 +169,7 @@ function save() {
feedbackUrl: feedbackUrl.value === '' ? null : feedbackUrl.value, feedbackUrl: feedbackUrl.value === '' ? null : feedbackUrl.value,
manifestJsonOverride: manifestJsonOverride.value === '' ? '{}' : JSON.stringify(JSON5.parse(manifestJsonOverride.value)), manifestJsonOverride: manifestJsonOverride.value === '' ? '{}' : JSON.stringify(JSON5.parse(manifestJsonOverride.value)),
}).then(() => { }).then(() => {
fetchInstance(); fetchInstance(true);
}); });
} }

View File

@ -124,7 +124,7 @@ function save() {
smtpUser: smtpUser.value, smtpUser: smtpUser.value,
smtpPass: smtpPass.value, smtpPass: smtpPass.value,
}).then(() => { }).then(() => {
fetchInstance(); fetchInstance(true);
}); });
} }

View File

@ -61,7 +61,7 @@ function save() {
deeplAuthKey: deeplAuthKey.value, deeplAuthKey: deeplAuthKey.value,
deeplIsPro: deeplIsPro.value, deeplIsPro: deeplIsPro.value,
}).then(() => { }).then(() => {
fetchInstance(); fetchInstance(true);
}); });
} }

View File

@ -57,7 +57,7 @@ function save() {
sensitiveMediaHosts: sensitiveMediaHosts.value.split('\n') || [], sensitiveMediaHosts: sensitiveMediaHosts.value.split('\n') || [],
}).then(() => { }).then(() => {
fetchInstance(); fetchInstance(true);
}); });
} }

View File

@ -118,7 +118,7 @@ function save() {
preservedUsernames: preservedUsernames.value.split('\n'), preservedUsernames: preservedUsernames.value.split('\n'),
urlPreviewDenyList: urlPreviewDenyList.value?.split('\n'), urlPreviewDenyList: urlPreviewDenyList.value?.split('\n'),
}).then(() => { }).then(() => {
fetchInstance(); fetchInstance(true);
}); });
} }

View File

@ -110,6 +110,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/> <CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/>
</div> </div>
</template> </template>
<template v-else-if="log.type === 'updateRemoteInstanceNote'">
<div>{{ i18n.ts.user }}: {{ log.info.userId }}</div>
<div :class="$style.diff">
<CodeDiff :context="5" :hideHeader="true" :oldString="log.info.before ?? ''" :newString="log.info.after ?? ''" maxHeight="300px"/>
</div>
</template>
<details> <details>
<summary>raw</summary> <summary>raw</summary>

View File

@ -143,7 +143,7 @@ function save() {
objectStorageSetPublicRead: objectStorageSetPublicRead.value, objectStorageSetPublicRead: objectStorageSetPublicRead.value,
objectStorageS3ForcePathStyle: objectStorageS3ForcePathStyle.value, objectStorageS3ForcePathStyle: objectStorageS3ForcePathStyle.value,
}).then(() => { }).then(() => {
fetchInstance(); fetchInstance(true);
}); });
} }

View File

@ -73,7 +73,7 @@ function save() {
enableChartsForRemoteUser: enableChartsForRemoteUser.value, enableChartsForRemoteUser: enableChartsForRemoteUser.value,
enableChartsForFederatedInstances: enableChartsForFederatedInstances.value, enableChartsForFederatedInstances: enableChartsForFederatedInstances.value,
}).then(() => { }).then(() => {
fetchInstance(); fetchInstance(true);
}); });
} }

View File

@ -56,7 +56,7 @@ function save() {
os.apiWithDialog('admin/update-meta', { os.apiWithDialog('admin/update-meta', {
proxyAccountId: proxyAccountId.value, proxyAccountId: proxyAccountId.value,
}).then(() => { }).then(() => {
fetchInstance(); fetchInstance(true);
}); });
} }

View File

@ -234,7 +234,7 @@ async function init() {
enableTruemailApi.value = meta.enableTruemailApi; enableTruemailApi.value = meta.enableTruemailApi;
truemailInstance.value = meta.truemailInstance; truemailInstance.value = meta.truemailInstance;
truemailAuthKey.value = meta.truemailAuthKey; truemailAuthKey.value = meta.truemailAuthKey;
bannedEmailDomains.value = meta.bannedEmailDomains?.join('\n') || ""; bannedEmailDomains.value = meta.bannedEmailDomains?.join('\n') || '';
} }
function save() { function save() {
@ -259,7 +259,7 @@ function save() {
truemailAuthKey: truemailAuthKey.value, truemailAuthKey: truemailAuthKey.value,
bannedEmailDomains: bannedEmailDomains.value.split('\n'), bannedEmailDomains: bannedEmailDomains.value.split('\n'),
}).then(() => { }).then(() => {
fetchInstance(); fetchInstance(true);
}); });
} }

View File

@ -58,7 +58,7 @@ const save = async () => {
await os.apiWithDialog('admin/update-meta', { await os.apiWithDialog('admin/update-meta', {
serverRules: serverRules.value, serverRules: serverRules.value,
}); });
fetchInstance(); fetchInstance(true);
}; };
const remove = (index: number): void => { const remove = (index: number): void => {

View File

@ -238,7 +238,7 @@ async function save(): void {
notesPerOneAd: notesPerOneAd.value, notesPerOneAd: notesPerOneAd.value,
}); });
fetchInstance(); fetchInstance(true);
} }
const headerTabs = computed(() => []); const headerTabs = computed(() => []);

View File

@ -7,9 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSpacer :contentMax="800"> <MkSpacer :contentMax="800">
<div :class="$style.root"> <div :class="$style.root">
<div v-if="!gameLoaded" :class="$style.loadingScreen"> <div v-if="!gameLoaded" :class="$style.loadingScreen">
<div> <div>{{ i18n.ts.loading }}<MkEllipsis/></div>
Loading...
</div>
</div> </div>
<!-- に対してTransitionコンポーネントを使うと何故かkeyを指定していてもキャッシュが効かず様々なコンポーネントが都度再評価されてパフォーマンスが低下する --> <!-- に対してTransitionコンポーネントを使うと何故かkeyを指定していてもキャッシュが効かず様々なコンポーネントが都度再評価されてパフォーマンスが低下する -->
<div v-show="gameLoaded" class="_gaps_s"> <div v-show="gameLoaded" class="_gaps_s">
@ -32,18 +30,18 @@ SPDX-License-Identifier: AGPL-3.0-only
</Transition> </Transition>
<div :class="$style.header"> <div :class="$style.header">
<div :class="[$style.frame, $style.headerTitle]"> <div class="_woodenFrame" :class="[$style.headerTitle]">
<div :class="$style.frameInner"> <div class="_woodenFrameInner">
<b>BUBBLE GAME</b> <b>{{ i18n.ts.bubbleGame }}</b>
<div>- {{ gameMode }} -</div> <div>- {{ gameMode.toUpperCase() }} -</div>
</div> </div>
</div> </div>
<div :class="[$style.frame, $style.frameH]"> <div class="_woodenFrame _woodenFrameH">
<div :class="$style.frameInner"> <div class="_woodenFrameInner">
<MkButton inline small @click="hold">HOLD</MkButton> <MkButton inline small @click="hold">{{ i18n.ts._bubbleGame.hold }}</MkButton>
<img v-if="holdingStock" :src="getTextureImageUrl(holdingStock.mono)" style="width: 32px; margin-left: 8px; vertical-align: bottom;"/> <img v-if="holdingStock" :src="getTextureImageUrl(holdingStock.mono)" style="width: 32px; margin-left: 8px; vertical-align: bottom;"/>
</div> </div>
<div :class="[$style.frameInner, $style.stock]" style="text-align: center;"> <div class="_woodenFrameInner" :class="$style.stock" style="text-align: center;">
<TransitionGroup <TransitionGroup
:enterActiveClass="$style.transition_stock_enterActive" :enterActiveClass="$style.transition_stock_enterActive"
:leaveActiveClass="$style.transition_stock_leaveActive" :leaveActiveClass="$style.transition_stock_leaveActive"
@ -90,58 +88,74 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="isGameOver && !replaying" :class="$style.gameOverLabel"> <div v-if="isGameOver && !replaying" :class="$style.gameOverLabel">
<div class="_gaps_s"> <div class="_gaps_s">
<img src="/client-assets/drop-and-fusion/gameover.png" style="width: 200px; max-width: 100%; display: block; margin: auto; margin-bottom: -5px;"/> <img src="/client-assets/drop-and-fusion/gameover.png" style="width: 200px; max-width: 100%; display: block; margin: auto; margin-bottom: -5px;"/>
<div>SCORE: <MkNumber :value="score"/>{{ getScoreUnit(gameMode) }}</div> <div>{{ i18n.ts._bubbleGame._score.score }}: <MkNumber :value="score"/>{{ getScoreUnit(gameMode) }}</div>
<div>MAX CHAIN: <MkNumber :value="maxCombo"/></div> <div>{{ i18n.ts._bubbleGame._score.maxChain }}: <MkNumber :value="maxCombo"/></div>
<div v-if="gameMode === 'yen'">TOTAL EARNINGS: <b><MkNumber :value="yenTotal ?? score"/></b></div> <div v-if="gameMode === 'yen'">
<div v-if="gameMode === 'sweets'"><b>おにぎり<MkNumber :value="score / 130"/>個分</b></div> {{ i18n.ts._bubbleGame._score.scoreYen }}:
<I18n :src="i18n.ts._bubbleGame._score.yen" tag="b">
<template #yen><MkNumber :value="yenTotal ?? score"/></template>
</I18n>
</div>
<I18n v-if="gameMode === 'sweets'" :src="i18n.ts._bubbleGame._score.scoreSweets" tag="div">
<template #onigiriQtyWithUnit>
<I18n :src="i18n.ts._bubbleGame._score.estimatedQty" tag="b">
<template #qty><MkNumber :value="score / 130"/></template>
</I18n>
</template>
</I18n>
</div> </div>
</div> </div>
<div v-if="replaying" :class="$style.replayIndicator"><span :class="$style.replayIndicatorText"><i class="ti ti-player-play"></i> {{ i18n.ts.replaying }}</span></div> <div v-if="replaying" :class="$style.replayIndicator"><span :class="$style.replayIndicatorText"><i class="ti ti-player-play"></i> {{ i18n.ts.replaying }}</span></div>
</div> </div>
<div v-if="replaying" :class="$style.frame"> <div v-if="replaying" class="_woodenFrame">
<div :class="$style.frameInner"> <div class="_woodenFrameInner">
<div style="background: #0004;"> <div style="background: #0004;">
<div style="height: 10px; background: var(--accent); will-change: width;" :style="{ width: `${(currentFrame / endedAtFrame) * 100}%` }"></div> <div style="height: 10px; background: var(--accent); will-change: width;" :style="{ width: `${(currentFrame / endedAtFrame) * 100}%` }"></div>
</div> </div>
</div> </div>
<div :class="$style.frameInner"> <div class="_woodenFrameInner">
<div class="_buttonsCenter"> <div class="_buttonsCenter">
<MkButton @click="endReplay"><i class="ti ti-player-stop"></i> END</MkButton> <MkButton @click="endReplay"><i class="ti ti-player-stop"></i> {{ i18n.ts.endReplay }}</MkButton>
<MkButton :primary="replayPlaybackRate === 4" @click="replayPlaybackRate = replayPlaybackRate === 4 ? 1 : 4"><i class="ti ti-player-track-next"></i> x4</MkButton> <MkButton :primary="replayPlaybackRate === 4" @click="replayPlaybackRate = replayPlaybackRate === 4 ? 1 : 4"><i class="ti ti-player-track-next"></i> x4</MkButton>
<MkButton :primary="replayPlaybackRate === 16" @click="replayPlaybackRate = replayPlaybackRate === 16 ? 1 : 16"><i class="ti ti-player-track-next"></i> x16</MkButton> <MkButton :primary="replayPlaybackRate === 16" @click="replayPlaybackRate = replayPlaybackRate === 16 ? 1 : 16"><i class="ti ti-player-track-next"></i> x16</MkButton>
</div> </div>
</div> </div>
</div> </div>
<div v-if="isGameOver" :class="$style.frame"> <div v-if="isGameOver" class="_woodenFrame">
<div :class="$style.frameInner"> <div class="_woodenFrameInner">
<div class="_buttonsCenter"> <div class="_buttonsCenter">
<MkButton primary rounded @click="backToTitle">{{ i18n.ts.backToTitle }}</MkButton> <MkButton primary rounded @click="backToTitle">{{ i18n.ts.backToTitle }}</MkButton>
<MkButton primary rounded @click="replay">{{ i18n.ts.showReplay }}</MkButton> <MkButton primary rounded @click="replay">{{ i18n.ts.showReplay }}</MkButton>
<MkButton primary rounded @click="share">{{ i18n.ts.share }}</MkButton> <MkButton primary rounded @click="share">{{ i18n.ts.share }}</MkButton>
<MkButton rounded @click="exportLog">Copy replay data</MkButton> <MkButton rounded @click="exportLog">{{ i18n.ts.copyReplayData }}</MkButton>
</div> </div>
</div> </div>
</div> </div>
<div style="display: flex;"> <div style="display: flex;">
<div :class="$style.frame" style="flex: 1; margin-right: 10px;"> <div class="_woodenFrame" style="flex: 1; margin-right: 10px;">
<div :class="$style.frameInner"> <div class="_woodenFrameInner">
<div>SCORE: <b><MkNumber :value="score"/>{{ getScoreUnit(gameMode) }}</b></div> <div>{{ i18n.ts._bubbleGame._score.score }}: <MkNumber :value="score"/>{{ getScoreUnit(gameMode) }}</div>
<div>HIGH SCORE: <b v-if="highScore"><MkNumber :value="highScore"/>{{ getScoreUnit(gameMode) }}</b><b v-else>-</b></div> <div>{{ i18n.ts._bubbleGame._score.highScore }}: <b v-if="highScore"><MkNumber :value="highScore"/>{{ getScoreUnit(gameMode) }}</b><b v-else>-</b></div>
<div v-if="gameMode === 'yen'">TOTAL EARNINGS: <b v-if="yenTotal"><MkNumber :value="yenTotal"/></b><b v-else>-</b></div> <div v-if="gameMode === 'yen'">
{{ i18n.ts._bubbleGame._score.scoreYen }}:
<I18n :src="i18n.ts._bubbleGame._score.yen" tag="b">
<template #yen><MkNumber :value="yenTotal ?? score"/></template>
</I18n>
</div>
</div> </div>
</div> </div>
<div :class="[$style.frame]" style="margin-left: auto;"> <div class="_woodenFrame" style="margin-left: auto;">
<div :class="$style.frameInner" style="text-align: center;"> <div class="_woodenFrameInner" style="text-align: center;">
<div @click="showConfig = !showConfig"><i class="ti ti-settings"></i></div> <div @click="showConfig = !showConfig"><i class="ti ti-settings"></i></div>
</div> </div>
</div> </div>
</div> </div>
<div v-if="showConfig" :class="$style.frame"> <div v-if="showConfig" class="_woodenFrame">
<div :class="$style.frameInner"> <div class="_woodenFrameInner">
<div class="_gaps"> <div class="_gaps">
<MkRange v-model="bgmVolume" :min="0" :max="1" :step="0.01" :textConverter="(v) => `${Math.floor(v * 100)}%`" :continuousUpdate="true" @dragEnded="(v) => updateSettings('bgmVolume', v)"> <MkRange v-model="bgmVolume" :min="0" :max="1" :step="0.01" :textConverter="(v) => `${Math.floor(v * 100)}%`" :continuousUpdate="true" @dragEnded="(v) => updateSettings('bgmVolume', v)">
<template #label>BGM {{ i18n.ts.volume }}</template> <template #label>BGM {{ i18n.ts.volume }}</template>
@ -153,8 +167,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</div> </div>
<div :class="$style.frame"> <div class="_woodenFrame">
<div :class="$style.frameInner"> <div class="_woodenFrameInner">
<div>FUSION RECIPE</div> <div>FUSION RECIPE</div>
<div> <div>
<div v-for="(mono, i) in game.monoDefinitions.sort((a, b) => a.level - b.level)" :key="mono.id" style="display: inline-block;"> <div v-for="(mono, i) in game.monoDefinitions.sort((a, b) => a.level - b.level)" :key="mono.id" style="display: inline-block;">
@ -165,10 +179,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</div> </div>
<div :class="$style.frame"> <div class="_woodenFrame">
<div :class="$style.frameInner"> <div class="_woodenFrameInner">
<MkButton v-if="!isGameOver && !replaying" full danger @click="surrender">Surrender</MkButton> <MkButton v-if="!isGameOver && !replaying" full danger @click="surrender">{{ i18n.ts.surrender }}</MkButton>
<MkButton v-else full @click="restart">Retry</MkButton> <MkButton v-else full @click="restart">{{ i18n.ts.gameRetry }}</MkButton>
</div> </div>
</div> </div>
</div> </div>
@ -1313,38 +1327,6 @@ definePageMetadata(() => ({
max-width: 100%; max-width: 100%;
} }
.frame {
padding: 7px;
background: #8C4F26;
box-shadow: 0 6px 16px #0007, 0 0 1px 1px #693410, inset 0 0 2px 1px #ce8a5c;
border-radius: 10px;
}
.frameH {
display: flex;
gap: 6px;
}
.frameInner {
padding: 8px;
margin-top: 8px;
background: #F1E8DC;
box-shadow: 0 0 2px 1px #ce8a5c, inset 0 0 1px 1px #693410;
border-radius: 6px;
color: #693410;
&:first-child {
margin-top: 0;
}
}
.frameDivider {
height: 0;
border: none;
border-top: 1px solid #693410;
border-bottom: 1px solid #ce8a5c;
}
.header { .header {
position: relative; position: relative;
z-index: 10; z-index: 10;

View File

@ -15,13 +15,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSpacer v-if="!gameStarted" :contentMax="800"> <MkSpacer v-if="!gameStarted" :contentMax="800">
<div :class="$style.root"> <div :class="$style.root">
<div class="_gaps"> <div class="_gaps">
<div :class="$style.frame" style="text-align: center;"> <div class="_woodenFrame" style="text-align: center;">
<div :class="$style.frameInner"> <div class="_woodenFrameInner">
<img src="/client-assets/drop-and-fusion/logo.png" style="display: block; max-width: 100%; max-height: 200px; margin: auto;"/> <img src="/client-assets/drop-and-fusion/logo.png" style="display: block; max-width: 100%; max-height: 200px; margin: auto;"/>
</div> </div>
</div> </div>
<div :class="$style.frame" style="text-align: center;"> <div class="_woodenFrame" style="text-align: center;">
<div :class="$style.frameInner"> <div class="_woodenFrameInner">
<div class="_gaps" style="padding: 16px;"> <div class="_gaps" style="padding: 16px;">
<MkSelect v-model="gameMode"> <MkSelect v-model="gameMode">
<option value="normal">NORMAL</option> <option value="normal">NORMAL</option>
@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton primary gradate large rounded inline @click="start">{{ i18n.ts.start }}</MkButton> <MkButton primary gradate large rounded inline @click="start">{{ i18n.ts.start }}</MkButton>
</div> </div>
</div> </div>
<div :class="$style.frameInner"> <div class="_woodenFrameInner">
<div class="_gaps" style="padding: 16px;"> <div class="_gaps" style="padding: 16px;">
<div style="font-size: 90%;"><i class="ti ti-music"></i> {{ i18n.ts.soundWillBePlayed }}</div> <div style="font-size: 90%;"><i class="ti ti-music"></i> {{ i18n.ts.soundWillBePlayed }}</div>
<MkSwitch v-model="mute"> <MkSwitch v-model="mute">
@ -42,10 +42,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</div> </div>
</div> </div>
<div :class="$style.frame"> <div class="_woodenFrame">
<div :class="$style.frameInner"> <div class="_woodenFrameInner">
<div class="_gaps_s" style="padding: 16px;"> <div class="_gaps_s" style="padding: 16px;">
<div><b>{{ i18n.tsx.lastNDays({ n: 7 }) }} {{ i18n.ts.ranking }}</b> ({{ gameMode }})</div> <div><b>{{ i18n.tsx.lastNDays({ n: 7 }) }} {{ i18n.ts.ranking }}</b> ({{ gameMode.toUpperCase() }})</div>
<div v-if="ranking" class="_gaps_s"> <div v-if="ranking" class="_gaps_s">
<div v-for="r in ranking" :key="r.id" :class="$style.rankingRecord"> <div v-for="r in ranking" :key="r.id" :class="$style.rankingRecord">
<MkAvatar :link="true" style="width: 24px; height: 24px; margin-right: 4px;" :user="r.user"/> <MkAvatar :link="true" style="width: 24px; height: 24px; margin-right: 4px;" :user="r.user"/>
@ -57,8 +57,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</div> </div>
</div> </div>
<div :class="$style.frame"> <div class="_woodenFrame">
<div :class="$style.frameInner" style="padding: 16px;"> <div class="_woodenFrameInner" style="padding: 16px;">
<div style="font-weight: bold;">{{ i18n.ts._bubbleGame.howToPlay }}</div> <div style="font-weight: bold;">{{ i18n.ts._bubbleGame.howToPlay }}</div>
<ol> <ol>
<li>{{ i18n.ts._bubbleGame._howToPlay.section1 }}</li> <li>{{ i18n.ts._bubbleGame._howToPlay.section1 }}</li>
@ -67,8 +67,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</ol> </ol>
</div> </div>
</div> </div>
<div :class="$style.frame"> <div class="_woodenFrame">
<div :class="$style.frameInner"> <div class="_woodenFrameInner">
<div class="_gaps_s" style="padding: 16px;"> <div class="_gaps_s" style="padding: 16px;">
<div><b>Credit</b></div> <div><b>Credit</b></div>
<div> <div>
@ -149,38 +149,6 @@ definePageMetadata(() => ({
} }
} }
.frame {
padding: 7px;
background: #8C4F26;
box-shadow: 0 6px 16px #0007, 0 0 1px 1px #693410, inset 0 0 2px 1px #ce8a5c;
border-radius: 10px;
}
.frameH {
display: flex;
gap: 6px;
}
.frameInner {
padding: 8px;
margin-top: 8px;
background: #F1E8DC;
box-shadow: 0 0 2px 1px #ce8a5c, inset 0 0 1px 1px #693410;
border-radius: 6px;
color: #693410;
&:first-child {
margin-top: 0;
}
}
.frameDivider {
height: 0;
border: none;
border-top: 1px solid #693410;
border-bottom: 1px solid #ce8a5c;
}
.rankingRecord { .rankingRecord {
display: flex; display: flex;
line-height: 24px; line-height: 24px;

View File

@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs"> <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
<div v-if="tab === 'overview'" key="overview" class="_gaps_m"> <div v-if="tab === 'overview'" key="overview" class="_gaps_m">
<div class="fnfelxur"> <div class="fnfelxur">
<img :src="faviconUrl" alt="" class="icon"/> <img v-if="faviconUrl" :src="faviconUrl" alt="" class="icon"/>
<span class="name">{{ instance.name || `(${i18n.ts.unknown})` }}</span> <span class="name">{{ instance.name || `(${i18n.ts.unknown})` }}</span>
</div> </div>
<div style="display: flex; flex-direction: column; gap: 1em;"> <div style="display: flex; flex-direction: column; gap: 1em;">
@ -35,6 +35,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<FormSection v-if="iAmModerator"> <FormSection v-if="iAmModerator">
<template #label>Moderation</template> <template #label>Moderation</template>
<div class="_gaps_s"> <div class="_gaps_s">
<MkTextarea v-model="moderationNote" manualSave>
<template #label>{{ i18n.ts.moderationNote }}</template>
</MkTextarea>
<MkSwitch v-model="suspended" :disabled="!instance" @update:modelValue="toggleSuspend">{{ i18n.ts.stopActivityDelivery }}</MkSwitch> <MkSwitch v-model="suspended" :disabled="!instance" @update:modelValue="toggleSuspend">{{ i18n.ts.stopActivityDelivery }}</MkSwitch>
<MkSwitch v-model="isBlocked" :disabled="!meta || !instance" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch> <MkSwitch v-model="isBlocked" :disabled="!meta || !instance" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</MkSwitch>
<MkSwitch v-model="isSilenced" :disabled="!meta || !instance" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch> <MkSwitch v-model="isSilenced" :disabled="!meta || !instance" @update:modelValue="toggleSilenced">{{ i18n.ts.silenceThisInstance }}</MkSwitch>
@ -120,7 +123,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed } from 'vue'; import { ref, computed, watch } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import MkChart from '@/components/MkChart.vue'; import MkChart from '@/components/MkChart.vue';
import MkObjectView from '@/components/MkObjectView.vue'; import MkObjectView from '@/components/MkObjectView.vue';
@ -142,6 +145,7 @@ import MkPagination from '@/components/MkPagination.vue';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
import { dateString } from '@/filters/date.js'; import { dateString } from '@/filters/date.js';
import MkTextarea from '@/components/MkTextarea.vue';
const props = defineProps<{ const props = defineProps<{
host: string; host: string;
@ -157,9 +161,10 @@ const isBlocked = ref(false);
const isSilenced = ref(false); const isSilenced = ref(false);
const isSensitiveMedia = ref(false); const isSensitiveMedia = ref(false);
const faviconUrl = ref<string | null>(null); const faviconUrl = ref<string | null>(null);
const moderationNote = ref('');
const usersPagination = { const usersPagination = {
endpoint: iAmModerator ? 'admin/show-users' : 'users' as const, endpoint: iAmModerator ? 'admin/show-users' as const : 'users' as const,
limit: 10, limit: 10,
params: { params: {
sort: '+updatedAt', sort: '+updatedAt',
@ -169,6 +174,15 @@ const usersPagination = {
offsetMode: true, offsetMode: true,
}; };
watch(moderationNote, async () => {
if (!instance.value) return;
await misskeyApi('admin/federation/update-instance', {
host: instance.value.host,
moderationNote: moderationNote.value
});
});
async function fetch(): Promise<void> { async function fetch(): Promise<void> {
if (iAmAdmin) { if (iAmAdmin) {
meta.value = await misskeyApi('admin/meta'); meta.value = await misskeyApi('admin/meta');
@ -181,6 +195,7 @@ async function fetch(): Promise<void> {
isSilenced.value = instance.value?.isSilenced ?? false; isSilenced.value = instance.value?.isSilenced ?? false;
isSensitiveMedia.value = instance.value?.isSensitiveMedia ?? false; isSensitiveMedia.value = instance.value?.isSensitiveMedia ?? false;
faviconUrl.value = getProxiedImageUrlNullable(instance.value?.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.value?.iconUrl, 'preview'); faviconUrl.value = getProxiedImageUrlNullable(instance.value?.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.value?.iconUrl, 'preview');
moderationNote.value = instance.value?.moderationNote ?? '';
} }
async function toggleBlock(): Promise<void> { async function toggleBlock(): Promise<void> {

View File

@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</div> </div>
<div :class="$style.board"> <div class="_woodenFrame">
<div :class="$style.boardInner"> <div :class="$style.boardInner">
<div v-if="showBoardLabels" :class="$style.labelsX"> <div v-if="showBoardLabels" :class="$style.labelsX">
<span v-for="i in game.map[0].length" :key="i" :class="$style.labelsXLabel">{{ String.fromCharCode(64 + i) }}</span> <span v-for="i in game.map[0].length" :key="i" :class="$style.labelsXLabel">{{ String.fromCharCode(64 + i) }}</span>
@ -124,8 +124,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFolder> <MkFolder>
<template #label>{{ i18n.ts.options }}</template> <template #label>{{ i18n.ts.options }}</template>
<div class="_gaps_s" style="text-align: left;"> <div class="_gaps_s" style="text-align: left;">
<MkSwitch v-model="showBoardLabels">Show labels</MkSwitch> <MkSwitch v-model="showBoardLabels">{{ i18n.ts._reversi.showBoardLabels }}</MkSwitch>
<MkSwitch v-model="useAvatarAsStone">useAvatarAsStone</MkSwitch> <MkSwitch v-model="useAvatarAsStone">{{ i18n.ts._reversi.useAvatarAsStone }}</MkSwitch>
</div> </div>
</MkFolder> </MkFolder>
@ -500,17 +500,6 @@ $gap: 4px;
text-align: center; text-align: center;
} }
.board {
width: 100%;
box-sizing: border-box;
margin: 0 auto;
padding: 7px;
background: #8C4F26;
box-shadow: 0 6px 16px #0007, 0 0 1px 1px #693410, inset 0 0 2px 1px #ce8a5c;
border-radius: 12px;
}
.boardInner { .boardInner {
padding: 32px; padding: 32px;

View File

@ -1,6 +1,10 @@
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { UnicodeEmojiDef } from './emojilist.js';
export function checkReactionPermissions(me: Misskey.entities.MeDetailed, note: Misskey.entities.Note, emoji: Misskey.entities.EmojiSimple): boolean { export function checkReactionPermissions(me: Misskey.entities.MeDetailed, note: Misskey.entities.Note, emoji: Misskey.entities.EmojiSimple | UnicodeEmojiDef): boolean {
if ('char' in emoji) return true; // UnicodeEmojiDefなら常にリアクション可能
emoji = emoji as Misskey.entities.EmojiSimple;
const roleIdsThatCanBeUsedThisEmojiAsReaction = emoji.roleIdsThatCanBeUsedThisEmojiAsReaction ?? []; const roleIdsThatCanBeUsedThisEmojiAsReaction = emoji.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [];
const roleIdsThatCanNotBeUsedThisEmojiAsReaction = emoji.roleIdsThatCanNotBeUsedThisEmojiAsReaction ?? []; const roleIdsThatCanNotBeUsedThisEmojiAsReaction = emoji.roleIdsThatCanNotBeUsedThisEmojiAsReaction ?? [];

View File

@ -2,14 +2,18 @@ import { unisonReload } from '@/scripts/unison-reload.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { miLocalStorage } from '@/local-storage.js'; import { miLocalStorage } from '@/local-storage.js';
import { fetchCustomEmojis } from '@/custom-emojis.js'; import { fetchCustomEmojis } from '@/custom-emojis.js';
import { fetchInstance } from '@/instance.js';
export async function clearCache() { export async function clearCache() {
os.waiting(); os.waiting();
miLocalStorage.removeItem('instance');
miLocalStorage.removeItem('instanceCachedAt');
miLocalStorage.removeItem('locale'); miLocalStorage.removeItem('locale');
miLocalStorage.removeItem('localeVersion'); miLocalStorage.removeItem('localeVersion');
miLocalStorage.removeItem('theme'); miLocalStorage.removeItem('theme');
miLocalStorage.removeItem('emojis'); miLocalStorage.removeItem('emojis');
miLocalStorage.removeItem('lastEmojisFetchedAt'); miLocalStorage.removeItem('lastEmojisFetchedAt');
await fetchInstance(true);
await fetchCustomEmojis(true); await fetchCustomEmojis(true);
unisonReload(); unisonReload();
} }

View File

@ -20,6 +20,10 @@ export const emojilist: UnicodeEmojiDef[] = _emojilist.map(x => ({
category: unicodeEmojiCategories[x[2]], category: unicodeEmojiCategories[x[2]],
})); }));
export const unicodeEmojisMap = new Map<string, UnicodeEmojiDef>(
emojilist.map(x => [x.char, x])
);
const _indexByChar = new Map<string, number>(); const _indexByChar = new Map<string, number>();
const _charGroupByCategory = new Map<string, string[]>(); const _charGroupByCategory = new Map<string, string[]>();
for (let i = 0; i < emojilist.length; i++) { for (let i = 0; i < emojilist.length; i++) {

View File

@ -0,0 +1,101 @@
export type EmojiDef = {
emoji: string;
name: string;
url: string;
aliasOf?: string;
} | {
emoji: string;
name: string;
aliasOf?: string;
isCustomEmoji?: true;
};
type EmojiScore = { emoji: EmojiDef, score: number };
export function searchEmoji(query: string | null, emojiDb: EmojiDef[], max = 30): EmojiDef[] {
if (!query) {
return [];
}
const matched = new Map<string, EmojiScore>();
// 完全一致(エイリアスなし)
emojiDb.some(x => {
if (x.name === query && !x.aliasOf) {
matched.set(x.name, { emoji: x, score: query.length + 3 });
}
return matched.size === max;
});
// 完全一致(エイリアス込み)
if (matched.size < max) {
emojiDb.some(x => {
if (x.name === query && !matched.has(x.aliasOf ?? x.name)) {
matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length + 2 });
}
return matched.size === max;
});
}
// 前方一致(エイリアスなし)
if (matched.size < max) {
emojiDb.some(x => {
if (x.name.startsWith(query) && !x.aliasOf && !matched.has(x.name)) {
matched.set(x.name, { emoji: x, score: query.length + 1 });
}
return matched.size === max;
});
}
// 前方一致(エイリアス込み)
if (matched.size < max) {
emojiDb.some(x => {
if (x.name.startsWith(query) && !matched.has(x.aliasOf ?? x.name)) {
matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length });
}
return matched.size === max;
});
}
// 部分一致(エイリアス込み)
if (matched.size < max) {
emojiDb.some(x => {
if (x.name.includes(query) && !matched.has(x.aliasOf ?? x.name)) {
matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length - 1 });
}
return matched.size === max;
});
}
// 簡易あいまい検索3文字以上
if (matched.size < max && query.length > 3) {
const queryChars = [...query];
const hitEmojis = new Map<string, EmojiScore>();
for (const x of emojiDb) {
// 文字列の位置を進めながら、クエリの文字を順番に探す
let pos = 0;
let hit = 0;
for (const c of queryChars) {
pos = x.name.indexOf(c, pos);
if (pos <= -1) break;
hit++;
}
// 半分以上の文字が含まれていればヒットとする
if (hit > Math.ceil(queryChars.length / 2) && hit - 2 > (matched.get(x.aliasOf ?? x.name)?.score ?? 0)) {
hitEmojis.set(x.aliasOf ?? x.name, { emoji: x, score: hit - 2 });
}
}
// ヒットしたものを全部追加すると雑多になるので、先頭の6件程度だけにしておく6件オートコンプリートのポップアップのサイズ分
[...hitEmojis.values()]
.sort((x, y) => y.score - x.score)
.slice(0, 6)
.forEach(it => matched.set(it.emoji.name, it));
}
return [...matched.values()]
.sort((x, y) => y.score - x.score)
.slice(0, max)
.map(it => it.emoji);
}

View File

@ -188,7 +188,7 @@ export async function loadAudio(url: string, options?: { useCache?: boolean; })
*/ */
export function playMisskeySfx(operationType: OperationType) { export function playMisskeySfx(operationType: OperationType) {
const sound = defaultStore.state[`sound_${operationType}`]; const sound = defaultStore.state[`sound_${operationType}`];
if (sound.type == null || !canPlay || !navigator.userActivation.hasBeenActive) return; if (sound.type == null || !canPlay || ('userActivation' in navigator && !navigator.userActivation.hasBeenActive)) return;
canPlay = false; canPlay = false;
playMisskeySfxFile(sound).finally(() => { playMisskeySfxFile(sound).finally(() => {

View File

@ -417,6 +417,39 @@ rt {
transition-timing-function: cubic-bezier(0,.5,.5,1); transition-timing-function: cubic-bezier(0,.5,.5,1);
} }
._woodenFrame {
padding: 7px;
background: #8C4F26;
box-shadow: 0 6px 16px #0007, 0 0 1px 1px #693410, inset 0 0 2px 1px #ce8a5c;
border-radius: 10px;
--bg: #F1E8DC;
--panel: #fff;
--fg: #693410;
--switchOffBg: rgba(0, 0, 0, 0.1);
--switchOffFg: rgb(255, 255, 255);
--switchOnBg: var(--accent);
--switchOnFg: rgb(255, 255, 255);
}
._woodenFrameH {
display: flex;
gap: 6px;
}
._woodenFrameInner {
padding: 8px;
margin-top: 8px;
background: var(--bg);
box-shadow: 0 0 2px 1px #ce8a5c, inset 0 0 1px 1px #693410;
border-radius: 6px;
color: var(--fg);
&:first-child {
margin-top: 0;
}
}
._transition_zoom-enter-active, ._transition_zoom-leave-active { ._transition_zoom-enter-active, ._transition_zoom-leave-active {
transition: opacity 0.5s, transform 0.5s !important; transition: opacity 0.5s, transform 0.5s !important;
} }

View File

@ -0,0 +1,34 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { assert, describe, test } from 'vitest';
import { searchEmoji } from '@/scripts/search-emoji.js';
describe('emoji autocomplete', () => {
test('名前の完全一致は名前の前方一致より優先される', async () => {
const result = searchEmoji('foooo', [{ emoji: ':foooo:', name: 'foooo' }, { emoji: ':foooobaaar:', name: 'foooobaaar' }]);
assert.equal(result[0].emoji, ':foooo:');
});
test('名前の前方一致は名前の部分一致より優先される', async () => {
const result = searchEmoji('baaa', [{ emoji: ':baaar:', name: 'baaar' }, { emoji: ':foooobaaar:', name: 'foooobaaar' }]);
assert.equal(result[0].emoji, ':baaar:');
});
test('名前の完全一致はタグの完全一致より優先される', async () => {
const result = searchEmoji('foooo', [{ emoji: ':foooo:', name: 'foooo' }, { emoji: ':baaar:', name: 'foooo', aliasOf: 'baaar' }]);
assert.equal(result[0].emoji, ':foooo:');
});
test('名前の前方一致はタグの前方一致より優先される', async () => {
const result = searchEmoji('foo', [{ emoji: ':foooo:', name: 'foooo' }, { emoji: ':baaar:', name: 'foooo', aliasOf: 'baaar' }]);
assert.equal(result[0].emoji, ':foooo:');
});
test('名前の部分一致はタグの部分一致より優先される', async () => {
const result = searchEmoji('oooo', [{ emoji: ':foooo:', name: 'foooo' }, { emoji: ':baaar:', name: 'foooo', aliasOf: 'baaar' }]);
assert.equal(result[0].emoji, ':foooo:');
});
});

View File

@ -1778,6 +1778,9 @@ declare namespace entities {
RolePolicies, RolePolicies,
ReversiGameLite, ReversiGameLite,
ReversiGameDetailed, ReversiGameDetailed,
MetaLite,
MetaDetailedOnly,
MetaDetailed,
AbuseUserReport AbuseUserReport
} }
} }
@ -2295,6 +2298,15 @@ type MeDetailed = components['schemas']['MeDetailed'];
// @public (undocumented) // @public (undocumented)
type MeDetailedOnly = components['schemas']['MeDetailedOnly']; type MeDetailedOnly = components['schemas']['MeDetailedOnly'];
// @public (undocumented)
type MetaDetailed = components['schemas']['MetaDetailed'];
// @public (undocumented)
type MetaDetailedOnly = components['schemas']['MetaDetailedOnly'];
// @public (undocumented)
type MetaLite = components['schemas']['MetaLite'];
// @public (undocumented) // @public (undocumented)
type MetaRequest = operations['meta']['requestBody']['content']['application/json']; type MetaRequest = operations['meta']['requestBody']['content']['application/json'];
@ -2388,6 +2400,9 @@ type ModerationLog = {
} | { } | {
type: 'unsuspendRemoteInstance'; type: 'unsuspendRemoteInstance';
info: ModerationLogPayloads['unsuspendRemoteInstance']; info: ModerationLogPayloads['unsuspendRemoteInstance'];
} | {
type: 'updateRemoteInstanceNote';
info: ModerationLogPayloads['updateRemoteInstanceNote'];
} | { } | {
type: 'markSensitiveDriveFile'; type: 'markSensitiveDriveFile';
info: ModerationLogPayloads['markSensitiveDriveFile']; info: ModerationLogPayloads['markSensitiveDriveFile'];
@ -2436,7 +2451,7 @@ type ModerationLog = {
}); });
// @public (undocumented) // @public (undocumented)
export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd", "createIndieAuthClient", "updateIndieAuthClient", "deleteIndieAuthClient", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner"]; export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "createInvitation", "createAd", "updateAd", "deleteAd", "createIndieAuthClient", "updateIndieAuthClient", "deleteIndieAuthClient", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetUserAvatar", "unsetUserBanner"];
// @public (undocumented) // @public (undocumented)
type MuteCreateRequest = operations['mute/create']['requestBody']['content']['application/json']; type MuteCreateRequest = operations['mute/create']['requestBody']['content']['application/json'];

View File

@ -51,5 +51,8 @@ export type Role = components['schemas']['Role'];
export type RolePolicies = components['schemas']['RolePolicies']; export type RolePolicies = components['schemas']['RolePolicies'];
export type ReversiGameLite = components['schemas']['ReversiGameLite']; export type ReversiGameLite = components['schemas']['ReversiGameLite'];
export type ReversiGameDetailed = components['schemas']['ReversiGameDetailed']; export type ReversiGameDetailed = components['schemas']['ReversiGameDetailed'];
export type MetaLite = components['schemas']['MetaLite'];
export type MetaDetailedOnly = components['schemas']['MetaDetailedOnly'];
export type MetaDetailed = components['schemas']['MetaDetailed'];
export type AbuseUserReport = components['schemas']['AbuseUserReport']; export type AbuseUserReport = components['schemas']['AbuseUserReport'];
export type ModerationLog = components['schemas']['ModerationLog']; export type ModerationLog = components['schemas']['ModerationLog'];

View File

@ -4606,6 +4606,7 @@ export type components = {
infoUpdatedAt: string | null; infoUpdatedAt: string | null;
/** Format: date-time */ /** Format: date-time */
latestRequestReceivedAt: string | null; latestRequestReceivedAt: string | null;
moderationNote?: string | null;
}; };
GalleryPost: { GalleryPost: {
/** /**
@ -4879,6 +4880,97 @@ export type components = {
logs: number[][]; logs: number[][];
map: string[]; map: string[];
}; };
MetaLite: {
maintainerName: string | null;
maintainerEmail: string | null;
version: string;
name: string | null;
shortName: string | null;
/**
* Format: url
* @example https://misskey.example.com
*/
uri: string;
description: string | null;
langs: string[];
tosUrl: string | null;
/** @default https://github.com/misskey-dev/misskey */
repositoryUrl: string | null;
/** @default https://github.com/misskey-dev/misskey/issues/new */
feedbackUrl: string | null;
defaultDarkTheme: string | null;
defaultLightTheme: string | null;
disableRegistration: boolean;
emailRequiredForSignup: boolean;
enableHcaptcha: boolean;
hcaptchaSiteKey: string | null;
enableMcaptcha: boolean;
mcaptchaSiteKey: string | null;
mcaptchaInstanceUrl: string | null;
enableRecaptcha: boolean;
recaptchaSiteKey: string | null;
enableTurnstile: boolean;
turnstileSiteKey: string | null;
swPublickey: string | null;
/** @default /assets/ai.png */
mascotImageUrl: string;
bannerUrl: string | null;
serverErrorImageUrl: string | null;
infoImageUrl: string | null;
notFoundImageUrl: string | null;
iconUrl: string | null;
maxNoteTextLength: number;
ads: ({
/**
* Format: id
* @example xxxxxxxxxx
*/
id: string;
/** Format: url */
url: string;
/** @enum {string} */
place: 'square' | 'horizontal' | 'horizontal-big' | 'vertical';
ratio: number;
/** Format: url */
imageUrl: string;
dayOfWeek: number;
})[];
/** @default 0 */
notesPerOneAd: number;
enableEmail: boolean;
enableServiceWorker: boolean;
translatorAvailable: boolean;
mediaProxy: string;
backgroundImageUrl: string | null;
impressumUrl: string | null;
logoImageUrl: string | null;
privacyPolicyUrl: string | null;
serverRules: string[];
themeColor: string | null;
policies: components['schemas']['RolePolicies'];
};
MetaDetailedOnly: {
features?: {
registration: boolean;
emailRequiredForSignup: boolean;
localTimeline: boolean;
globalTimeline: boolean;
hCaptcha: boolean;
mCaptcha: boolean;
reCaptcha: boolean;
turnstile: boolean;
objectStorage: boolean;
serviceWorker: boolean;
/** @default true */
miauth?: boolean;
};
proxyAccountName: string | null;
/** @example false */
requireSetup: boolean;
cacheRemoteFiles: boolean;
cacheRemoteSensitiveFiles: boolean;
};
MetaDetailed: components['schemas']['MetaLite'] & components['schemas']['MetaDetailedOnly'];
AbuseUserReport: { AbuseUserReport: {
/** /**
* Format: id * Format: id
@ -7686,7 +7778,8 @@ export type operations = {
content: { content: {
'application/json': { 'application/json': {
host: string; host: string;
isSuspended: boolean; isSuspended?: boolean;
moderationNote?: string;
}; };
}; };
}; };
@ -20192,91 +20285,7 @@ export type operations = {
/** @description OK (with results) */ /** @description OK (with results) */
200: { 200: {
content: { content: {
'application/json': { 'application/json': components['schemas']['MetaLite'] | components['schemas']['MetaDetailed'];
maintainerName: string | null;
maintainerEmail: string | null;
version: string;
name: string;
shortName: string | null;
/**
* Format: url
* @example https://misskey.example.com
*/
uri: string;
description: string | null;
langs: string[];
tosUrl: string | null;
/** @default https://github.com/misskey-dev/misskey */
repositoryUrl: string | null;
/** @default https://github.com/misskey-dev/misskey/issues/new */
feedbackUrl: string | null;
defaultDarkTheme: string | null;
defaultLightTheme: string | null;
disableRegistration: boolean;
cacheRemoteFiles: boolean;
cacheRemoteSensitiveFiles: boolean;
emailRequiredForSignup: boolean;
enableHcaptcha: boolean;
hcaptchaSiteKey: string | null;
enableMcaptcha: boolean;
mcaptchaSiteKey: string | null;
mcaptchaInstanceUrl: string | null;
enableRecaptcha: boolean;
recaptchaSiteKey: string | null;
enableTurnstile: boolean;
turnstileSiteKey: string | null;
swPublickey: string | null;
/** @default /assets/ai.png */
mascotImageUrl: string;
bannerUrl: string;
serverErrorImageUrl: string | null;
infoImageUrl: string | null;
notFoundImageUrl: string | null;
iconUrl: string | null;
maxNoteTextLength: number;
ads: ({
/**
* Format: id
* @example xxxxxxxxxx
*/
id: string;
/** Format: url */
url: string;
/** @enum {string} */
place: 'square' | 'horizontal' | 'horizontal-big' | 'vertical';
ratio: number;
/** Format: url */
imageUrl: string;
dayOfWeek: number;
})[];
/** @default 0 */
notesPerOneAd: number;
/** @example false */
requireSetup: boolean;
enableEmail: boolean;
enableServiceWorker: boolean;
translatorAvailable: boolean;
proxyAccountName: string | null;
mediaProxy: string;
features?: {
registration: boolean;
localTimeline: boolean;
globalTimeline: boolean;
hcaptcha: boolean;
recaptcha: boolean;
objectStorage: boolean;
serviceWorker: boolean;
/** @default true */
miauth?: boolean;
};
backgroundImageUrl: string | null;
impressumUrl: string | null;
logoImageUrl: string | null;
privacyPolicyUrl: string | null;
serverRules: string[];
themeColor: string | null;
policies: components['schemas']['RolePolicies'];
};
}; };
}; };
/** @description Client error */ /** @description Client error */
@ -23667,10 +23676,10 @@ export type operations = {
'application/json': { 'application/json': {
/** Format: misskey:id */ /** Format: misskey:id */
flashId: string; flashId: string;
title: string; title?: string;
summary: string; summary?: string;
script: string; script?: string;
permissions: string[]; permissions?: string[];
/** @enum {string} */ /** @enum {string} */
visibility?: 'public' | 'private'; visibility?: 'public' | 'private';
}; };

View File

@ -125,6 +125,7 @@ export const moderationLogTypes = [
'resetPassword', 'resetPassword',
'suspendRemoteInstance', 'suspendRemoteInstance',
'unsuspendRemoteInstance', 'unsuspendRemoteInstance',
'updateRemoteInstanceNote',
'markSensitiveDriveFile', 'markSensitiveDriveFile',
'unmarkSensitiveDriveFile', 'unmarkSensitiveDriveFile',
'resolveAbuseReport', 'resolveAbuseReport',
@ -268,6 +269,12 @@ export type ModerationLogPayloads = {
id: string; id: string;
host: string; host: string;
}; };
updateRemoteInstanceNote: {
id: string;
host: string;
before: string | null;
after: string | null;
};
markSensitiveDriveFile: { markSensitiveDriveFile: {
fileId: string; fileId: string;
fileUserId: string | null; fileUserId: string | null;

View File

@ -95,6 +95,9 @@ export type ModerationLog = {
} | { } | {
type: 'unsuspendRemoteInstance'; type: 'unsuspendRemoteInstance';
info: ModerationLogPayloads['unsuspendRemoteInstance']; info: ModerationLogPayloads['unsuspendRemoteInstance'];
} | {
type: 'updateRemoteInstanceNote';
info: ModerationLogPayloads['updateRemoteInstanceNote'];
} | { } | {
type: 'markSensitiveDriveFile'; type: 'markSensitiveDriveFile';
info: ModerationLogPayloads['markSensitiveDriveFile']; info: ModerationLogPayloads['markSensitiveDriveFile'];

View File

@ -220,6 +220,9 @@ importers:
hpagent: hpagent:
specifier: 1.2.0 specifier: 1.2.0
version: 1.2.0 version: 1.2.0
htmlescape:
specifier: ^1.1.1
version: 1.1.1
http-link-header: http-link-header:
specifier: 1.1.1 specifier: 1.1.1
version: 1.1.1 version: 1.1.1
@ -537,6 +540,9 @@ importers:
'@types/fluent-ffmpeg': '@types/fluent-ffmpeg':
specifier: 2.1.24 specifier: 2.1.24
version: 2.1.24 version: 2.1.24
'@types/htmlescape':
specifier: ^1.1.3
version: 1.1.3
'@types/http-link-header': '@types/http-link-header':
specifier: 1.0.5 specifier: 1.0.5
version: 1.0.5 version: 1.0.5
@ -7673,6 +7679,10 @@ packages:
'@types/unist': 2.0.10 '@types/unist': 2.0.10
dev: true dev: true
/@types/htmlescape@1.1.3:
resolution: {integrity: sha512-tuC81YJXGUe0q8WRtBNW+uyx79rkkzWK651ALIXXYq5/u/IxjX4iHneGF2uUqzsNp+F+9J2mFZOv9jiLTtIq0w==}
dev: true
/@types/http-cache-semantics@4.0.4: /@types/http-cache-semantics@4.0.4:
resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==}
@ -12516,6 +12526,11 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
dev: true dev: true
/htmlescape@1.1.1:
resolution: {integrity: sha512-eVcrzgbR4tim7c7soKQKtxa/kQM4TzjnlU83rcZ9bHU6t31ehfV7SktN6McWgwPWg+JYMA/O3qpGxBvFq1z2Jg==}
engines: {node: '>=0.10'}
dev: false
/htmlparser2@8.0.2: /htmlparser2@8.0.2:
resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
dependencies: dependencies: