mirror of
https://github.com/misskey-dev/misskey.git
synced 2024-12-29 09:58:36 +01:00
Merge branch 'develop' into feature/emoji-grid
# Conflicts: # packages/backend/src/core/CustomEmojiService.ts
This commit is contained in:
commit
122fba32f5
109 changed files with 1771 additions and 1036 deletions
22
CHANGELOG.md
22
CHANGELOG.md
|
@ -11,6 +11,27 @@
|
|||
-
|
||||
|
||||
-->
|
||||
## 202x.x.x (unreleased)
|
||||
|
||||
### General
|
||||
- Enhance: サーバーごとにモデレーションノートを残せるように
|
||||
|
||||
### Client
|
||||
- Enhance: ノート作成画面のファイル添付メニューの区切り線の位置を調整
|
||||
- Fix: syuilo/misskeyの時代からあるインスタンスが改変されたバージョンであると誤認識される問題
|
||||
- Fix: MFMのオートコンプリートが出るべき状況で出ないことがある問題を修正
|
||||
- Fix: チャートのラベルが消えている問題を修正
|
||||
- Fix: 画面表示後最初の音声再生が爆音になることがある問題を修正
|
||||
- Fix: 絵文字サジェストの順位で、絵文字自体の名前が同じものよりもタグで一致しているものが優先されてしまう問題を修正
|
||||
|
||||
### Server
|
||||
- Fix: nodeinfoにenableMcaptchaとenableTurnstileが無いのを修正
|
||||
- エンドポイント`flash/update`の`flashId`以外のパラメータは必須ではなくなりました
|
||||
- Fix: 禁止キーワードを含むノートがDelayed Queueに追加されて再処理される問題を修正
|
||||
- エンドポイント`admin/emoji/update`の各種修正
|
||||
- 必須パラメータを`id`または`name`のいずれかのみに
|
||||
- `id`の代わりに`name`で絵文字を指定可能に(`id`・`name`両指定時は従来通り`name`を変更する挙動)
|
||||
- `category`および`licence`が指定なしの時勝手にnullに上書きされる挙動を修正
|
||||
|
||||
## 2024.2.0
|
||||
|
||||
|
@ -83,6 +104,7 @@
|
|||
- Fix: エラー画像URLを設定した後解除すると,デフォルトの画像が表示されない問題の修正
|
||||
- Fix: MkCodeEditorで行がずれていってしまう問題の修正
|
||||
- Fix: Summaly proxy利用時にプレイヤーが動作しないことがあるのを修正 #13196
|
||||
- Fix: ユーザの情報のポップアップが消えなくなることがある問題を修正
|
||||
|
||||
### Server
|
||||
- Enhance: 連合先のレートリミットを超過した際にリトライするようになりました
|
||||
|
|
68
locales/index.d.ts
vendored
68
locales/index.d.ts
vendored
|
@ -4856,6 +4856,14 @@ export interface Locale extends ILocale {
|
|||
* リプレイ中
|
||||
*/
|
||||
"replaying": string;
|
||||
/**
|
||||
* リプレイを終了
|
||||
*/
|
||||
"endReplay": string;
|
||||
/**
|
||||
* リプレイデータをコピー
|
||||
*/
|
||||
"copyReplayData": string;
|
||||
/**
|
||||
* ランキング
|
||||
*/
|
||||
|
@ -4884,11 +4892,57 @@ export interface Locale extends ILocale {
|
|||
* スワイプしてタブを切り替える
|
||||
*/
|
||||
"enableHorizontalSwipe": string;
|
||||
/**
|
||||
* 読み込み中
|
||||
*/
|
||||
"loading": string;
|
||||
/**
|
||||
* やめる
|
||||
*/
|
||||
"surrender": string;
|
||||
/**
|
||||
* リトライ
|
||||
*/
|
||||
"gameRetry": string;
|
||||
"_bubbleGame": {
|
||||
/**
|
||||
* 遊び方
|
||||
*/
|
||||
"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": {
|
||||
/**
|
||||
* 位置を調整してハコにモノを落とします。
|
||||
|
@ -9172,7 +9226,7 @@ export interface Locale extends ILocale {
|
|||
*/
|
||||
"updateServerSettings": string;
|
||||
/**
|
||||
* モデレーションノート更新
|
||||
* ユーザーのモデレーションノート更新
|
||||
*/
|
||||
"updateUserNote": string;
|
||||
/**
|
||||
|
@ -9219,6 +9273,10 @@ export interface Locale extends ILocale {
|
|||
* リモートサーバーを再開
|
||||
*/
|
||||
"unsuspendRemoteInstance": string;
|
||||
/**
|
||||
* リモートサーバーのモデレーションノート更新
|
||||
*/
|
||||
"updateRemoteInstanceNote": string;
|
||||
/**
|
||||
* ファイルをセンシティブ付与
|
||||
*/
|
||||
|
@ -9655,6 +9713,14 @@ export interface Locale extends ILocale {
|
|||
* 変則なし
|
||||
*/
|
||||
"disallowIrregularRules": string;
|
||||
/**
|
||||
* 盤面に行・列番号を表示
|
||||
*/
|
||||
"showBoardLabels": string;
|
||||
/**
|
||||
* 石をアイコンにする
|
||||
*/
|
||||
"useAvatarAsStone": string;
|
||||
};
|
||||
"_offlineScreen": {
|
||||
/**
|
||||
|
|
|
@ -1210,6 +1210,8 @@ soundWillBePlayed: "サウンドが再生されます"
|
|||
showReplay: "リプレイを見る"
|
||||
replay: "リプレイ"
|
||||
replaying: "リプレイ中"
|
||||
endReplay: "リプレイを終了"
|
||||
copyReplayData: "リプレイデータをコピー"
|
||||
ranking: "ランキング"
|
||||
lastNDays: "直近{n}日"
|
||||
backToTitle: "タイトルへ"
|
||||
|
@ -1217,9 +1219,21 @@ hemisphere: "お住まいの地域"
|
|||
withSensitive: "センシティブなファイルを含むノートを表示"
|
||||
userSaysSomethingSensitive: "{name}のセンシティブなファイルを含む投稿"
|
||||
enableHorizontalSwipe: "スワイプしてタブを切り替える"
|
||||
loading: "読み込み中"
|
||||
surrender: "やめる"
|
||||
gameRetry: "リトライ"
|
||||
|
||||
_bubbleGame:
|
||||
howToPlay: "遊び方"
|
||||
hold: "ホールド"
|
||||
_score:
|
||||
score: "スコア"
|
||||
scoreYen: "稼いだ金額"
|
||||
highScore: "ハイスコア"
|
||||
maxChain: "最大チェーン数"
|
||||
yen: "{yen}円"
|
||||
estimatedQty: "{qty}個分"
|
||||
scoreSweets: "おにぎり {onigiriQtyWithUnit}"
|
||||
_howToPlay:
|
||||
section1: "位置を調整してハコにモノを落とします。"
|
||||
section2: "同じ種類のモノがくっつくと別のモノに変化して、スコアが得られます。"
|
||||
|
@ -2434,7 +2448,7 @@ _moderationLogTypes:
|
|||
updateCustomEmoji: "カスタム絵文字更新"
|
||||
deleteCustomEmoji: "カスタム絵文字削除"
|
||||
updateServerSettings: "サーバー設定更新"
|
||||
updateUserNote: "モデレーションノート更新"
|
||||
updateUserNote: "ユーザーのモデレーションノート更新"
|
||||
deleteDriveFile: "ファイルを削除"
|
||||
deleteNote: "ノートを削除"
|
||||
createGlobalAnnouncement: "全体のお知らせを作成"
|
||||
|
@ -2446,6 +2460,7 @@ _moderationLogTypes:
|
|||
resetPassword: "パスワードをリセット"
|
||||
suspendRemoteInstance: "リモートサーバーを停止"
|
||||
unsuspendRemoteInstance: "リモートサーバーを再開"
|
||||
updateRemoteInstanceNote: "リモートサーバーのモデレーションノート更新"
|
||||
markSensitiveDriveFile: "ファイルをセンシティブ付与"
|
||||
unmarkSensitiveDriveFile: "ファイルをセンシティブ解除"
|
||||
resolveAbuseReport: "通報を解決"
|
||||
|
@ -2571,6 +2586,8 @@ _reversi:
|
|||
opponentHasSettingsChanged: "相手が設定を変更しました"
|
||||
allowIrregularRules: "変則許可 (完全フリー)"
|
||||
disallowIrregularRules: "変則なし"
|
||||
showBoardLabels: "盤面に行・列番号を表示"
|
||||
useAvatarAsStone: "石をアイコンにする"
|
||||
|
||||
_offlineScreen:
|
||||
title: "オフライン - サーバーに接続できません"
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
export class RepositoryUrlFromSyuiloToMisskeyDev1708266695091 {
|
||||
name = 'RepositoryUrlFromSyuiloToMisskeyDev1708266695091'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`UPDATE "meta" SET "repositoryUrl" = 'https://github.com/misskey-dev/misskey' WHERE "repositoryUrl" = 'https://github.com/syuilo/misskey'`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
// no valid down migration
|
||||
}
|
||||
}
|
|
@ -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"`);
|
||||
}
|
||||
}
|
|
@ -79,7 +79,7 @@
|
|||
"@fastify/multipart": "8.1.0",
|
||||
"@fastify/static": "6.12.0",
|
||||
"@fastify/view": "8.2.0",
|
||||
"@misskey-dev/sharp-read-bmp": "^1.1.1",
|
||||
"@misskey-dev/sharp-read-bmp": "^1.2.0",
|
||||
"@misskey-dev/summaly": "^5.0.3",
|
||||
"@nestjs/common": "10.2.10",
|
||||
"@nestjs/core": "10.2.10",
|
||||
|
@ -118,6 +118,7 @@
|
|||
"got": "14.1.0",
|
||||
"happy-dom": "10.0.3",
|
||||
"hpagent": "1.2.0",
|
||||
"htmlescape": "^1.1.1",
|
||||
"http-link-header": "1.1.1",
|
||||
"ioredis": "5.3.2",
|
||||
"ip-cidr": "3.1.0",
|
||||
|
@ -164,7 +165,7 @@
|
|||
"rxjs": "7.8.1",
|
||||
"sanitize-html": "2.11.0",
|
||||
"secure-json-parse": "2.7.0",
|
||||
"sharp": "0.32.6",
|
||||
"sharp": "0.33.2",
|
||||
"slacc": "0.0.10",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
|
@ -194,6 +195,7 @@
|
|||
"@types/color-convert": "2.0.3",
|
||||
"@types/content-disposition": "0.5.8",
|
||||
"@types/fluent-ffmpeg": "2.1.24",
|
||||
"@types/htmlescape": "^1.1.3",
|
||||
"@types/http-link-header": "1.0.5",
|
||||
"@types/jest": "29.5.11",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
|
|
|
@ -20,7 +20,6 @@ import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
|||
import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { CacheService } from '@/core/CacheService.js';
|
||||
import { ProxyAccountService } from '@/core/ProxyAccountService.js';
|
||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
|
@ -60,7 +59,6 @@ export class AccountMoveService {
|
|||
private instanceChart: InstanceChart,
|
||||
private metaService: MetaService,
|
||||
private relayService: RelayService,
|
||||
private cacheService: CacheService,
|
||||
private queueService: QueueService,
|
||||
) {
|
||||
}
|
||||
|
@ -84,7 +82,7 @@ export class AccountMoveService {
|
|||
Object.assign(src, update);
|
||||
|
||||
// Update cache
|
||||
this.cacheService.uriPersonCache.set(srcUri, src);
|
||||
this.globalEventService.publishInternalEvent('localUserUpdated', src);
|
||||
|
||||
const srcPerson = await this.apRendererService.renderPerson(src);
|
||||
const updateAct = this.apRendererService.addContext(this.apRendererService.renderUpdate(srcPerson, src));
|
||||
|
|
|
@ -128,10 +128,13 @@ export class CacheService implements OnApplicationShutdown {
|
|||
const { type, body } = obj.message as GlobalEvents['internal']['payload'];
|
||||
switch (type) {
|
||||
case 'userChangeSuspendedState':
|
||||
case 'remoteUserUpdated': {
|
||||
case 'userChangeDeletedState':
|
||||
case 'remoteUserUpdated':
|
||||
case 'localUserUpdated': {
|
||||
const user = await this.usersRepository.findOneBy({ id: body.id });
|
||||
if (user == null) {
|
||||
this.userByIdCache.delete(body.id);
|
||||
this.localUserByIdCache.delete(body.id);
|
||||
for (const [k, v] of this.uriPersonCache.cache.entries()) {
|
||||
if (v.value?.id === body.id) {
|
||||
this.uriPersonCache.delete(k);
|
||||
|
|
|
@ -116,6 +116,7 @@ import { FlashEntityService } from './entities/FlashEntityService.js';
|
|||
import { FlashLikeEntityService } from './entities/FlashLikeEntityService.js';
|
||||
import { RoleEntityService } from './entities/RoleEntityService.js';
|
||||
import { ReversiGameEntityService } from './entities/ReversiGameEntityService.js';
|
||||
import { MetaEntityService } from './entities/MetaEntityService.js';
|
||||
|
||||
import { ApAudienceService } from './activitypub/ApAudienceService.js';
|
||||
import { ApDbResolverService } from './activitypub/ApDbResolverService.js';
|
||||
|
@ -254,6 +255,7 @@ const $FlashEntityService: Provider = { provide: 'FlashEntityService', useExisti
|
|||
const $FlashLikeEntityService: Provider = { provide: 'FlashLikeEntityService', useExisting: FlashLikeEntityService };
|
||||
const $RoleEntityService: Provider = { provide: 'RoleEntityService', useExisting: RoleEntityService };
|
||||
const $ReversiGameEntityService: Provider = { provide: 'ReversiGameEntityService', useExisting: ReversiGameEntityService };
|
||||
const $MetaEntityService: Provider = { provide: 'MetaEntityService', useExisting: MetaEntityService };
|
||||
|
||||
const $ApAudienceService: Provider = { provide: 'ApAudienceService', useExisting: ApAudienceService };
|
||||
const $ApDbResolverService: Provider = { provide: 'ApDbResolverService', useExisting: ApDbResolverService };
|
||||
|
@ -393,6 +395,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
FlashLikeEntityService,
|
||||
RoleEntityService,
|
||||
ReversiGameEntityService,
|
||||
MetaEntityService,
|
||||
|
||||
ApAudienceService,
|
||||
ApDbResolverService,
|
||||
|
@ -528,6 +531,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$FlashLikeEntityService,
|
||||
$RoleEntityService,
|
||||
$ReversiGameEntityService,
|
||||
$MetaEntityService,
|
||||
|
||||
$ApAudienceService,
|
||||
$ApDbResolverService,
|
||||
|
@ -663,6 +667,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
FlashLikeEntityService,
|
||||
RoleEntityService,
|
||||
ReversiGameEntityService,
|
||||
MetaEntityService,
|
||||
|
||||
ApAudienceService,
|
||||
ApDbResolverService,
|
||||
|
@ -797,6 +802,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$FlashLikeEntityService,
|
||||
$RoleEntityService,
|
||||
$ReversiGameEntityService,
|
||||
$MetaEntityService,
|
||||
|
||||
$ApAudienceService,
|
||||
$ApDbResolverService,
|
||||
|
|
|
@ -439,6 +439,11 @@ export class CustomEmojiService implements OnApplicationShutdown {
|
|||
return this.emojisRepository.findOneBy({ id });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public getEmojiByName(name: string): Promise<MiEmoji | null> {
|
||||
return this.emojisRepository.findOneBy({ name, host: IsNull() });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async fetchEmojis(params?: FetchEmojisParams) {
|
||||
function multipleWordsToQuery(
|
||||
|
|
|
@ -9,6 +9,7 @@ import { QueueService } from '@/core/QueueService.js';
|
|||
import { UserSuspendService } from '@/core/UserSuspendService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { GlobalEventService } from '@/core/GlobalEventService.js';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteAccountService {
|
||||
|
@ -18,6 +19,7 @@ export class DeleteAccountService {
|
|||
|
||||
private userSuspendService: UserSuspendService,
|
||||
private queueService: QueueService,
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
}
|
||||
|
||||
|
@ -39,5 +41,7 @@ export class DeleteAccountService {
|
|||
await this.usersRepository.update(user.id, {
|
||||
isDeleted: true,
|
||||
});
|
||||
|
||||
this.globalEventService.publishInternalEvent('userChangeDeletedState', { id: user.id, isDeleted: true });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import isSvg from 'is-svg';
|
|||
import probeImageSize from 'probe-image-size';
|
||||
import { type predictionType } from 'nsfwjs';
|
||||
import sharp from 'sharp';
|
||||
import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
|
||||
import { encode } from 'blurhash';
|
||||
import { createTempDir } from '@/misc/create-temp.js';
|
||||
import { AiService } from '@/core/AiService.js';
|
||||
|
@ -122,7 +123,7 @@ export class FileInfoService {
|
|||
'image/avif',
|
||||
'image/svg+xml',
|
||||
].includes(type.mime)) {
|
||||
blurhash = await this.getBlurhash(path).catch(e => {
|
||||
blurhash = await this.getBlurhash(path, type.mime).catch(e => {
|
||||
warnings.push(`getBlurhash failed: ${e}`);
|
||||
return undefined;
|
||||
});
|
||||
|
@ -407,9 +408,9 @@ export class FileInfoService {
|
|||
* Calculate average color of image
|
||||
*/
|
||||
@bindThis
|
||||
private getBlurhash(path: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
sharp(path)
|
||||
private getBlurhash(path: string, type: string): Promise<string> {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
(await sharpBmp(path, type))
|
||||
.raw()
|
||||
.ensureAlpha()
|
||||
.resize(64, 64, { fit: 'inside' })
|
||||
|
|
|
@ -209,8 +209,10 @@ type SerializedAll<T> = {
|
|||
|
||||
export interface InternalEventTypes {
|
||||
userChangeSuspendedState: { id: MiUser['id']; isSuspended: MiUser['isSuspended']; };
|
||||
userChangeDeletedState: { id: MiUser['id']; isDeleted: MiUser['isDeleted']; };
|
||||
userTokenRegenerated: { id: MiUser['id']; oldToken: string; newToken: string; };
|
||||
remoteUserUpdated: { id: MiUser['id']; };
|
||||
localUserUpdated: { id: MiUser['id']; };
|
||||
follow: { followerId: MiUser['id']; followeeId: MiUser['id']; };
|
||||
unfollow: { followerId: MiUser['id']; followeeId: MiUser['id']; };
|
||||
blockingCreated: { blockerId: MiUser['id']; blockeeId: MiUser['id']; };
|
||||
|
|
|
@ -59,6 +59,8 @@ import { UtilityService } from '@/core/UtilityService.js';
|
|||
import { UserBlockingService } from '@/core/UserBlockingService.js';
|
||||
import { isReply } from '@/misc/is-reply.js';
|
||||
import { trackPromise } from '@/misc/promise-tracker.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
|
||||
type NotificationType = 'reply' | 'renote' | 'quote' | 'mention';
|
||||
|
||||
|
@ -151,8 +153,6 @@ type Option = {
|
|||
export class NoteCreateService implements OnApplicationShutdown {
|
||||
#shutdownController = new AbortController();
|
||||
|
||||
public static ContainsProhibitedWordsError = class extends Error {};
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
@ -264,7 +264,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
}
|
||||
|
||||
if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', meta.prohibitedWords)) {
|
||||
throw new NoteCreateService.ContainsProhibitedWordsError();
|
||||
throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words');
|
||||
}
|
||||
|
||||
const inSilencedInstance = this.utilityService.isSilencedHost(meta.silencedHosts, user.host);
|
||||
|
@ -817,7 +817,7 @@ export class NoteCreateService implements OnApplicationShutdown {
|
|||
const mentions = extractMentions(tokens);
|
||||
let mentionedUsers = (await Promise.all(mentions.map(m =>
|
||||
this.remoteUserResolveService.resolveUser(m.username, m.host ?? user.host).catch(() => null),
|
||||
))).filter(x => x != null) as MiUser[];
|
||||
))).filter(isNotNull);
|
||||
|
||||
// Drop duplicate users
|
||||
mentionedUsers = mentionedUsers.filter((u, i, self) =>
|
||||
|
|
|
@ -88,46 +88,47 @@ export class NoteReadService implements OnApplicationShutdown {
|
|||
userId: MiUser['id'],
|
||||
notes: (MiNote | Packed<'Note'>)[],
|
||||
): Promise<void> {
|
||||
const readMentions: (MiNote | Packed<'Note'>)[] = [];
|
||||
const readSpecifiedNotes: (MiNote | Packed<'Note'>)[] = [];
|
||||
if (notes.length === 0) return;
|
||||
|
||||
const noteIds = new Set<MiNote['id']>();
|
||||
|
||||
for (const note of notes) {
|
||||
if (note.mentions && note.mentions.includes(userId)) {
|
||||
readMentions.push(note);
|
||||
noteIds.add(note.id);
|
||||
} else if (note.visibleUserIds && note.visibleUserIds.includes(userId)) {
|
||||
readSpecifiedNotes.push(note);
|
||||
noteIds.add(note.id);
|
||||
}
|
||||
}
|
||||
|
||||
if ((readMentions.length > 0) || (readSpecifiedNotes.length > 0)) {
|
||||
// Remove the record
|
||||
await this.noteUnreadsRepository.delete({
|
||||
userId: userId,
|
||||
noteId: In([...readMentions.map(n => n.id), ...readSpecifiedNotes.map(n => n.id)]),
|
||||
});
|
||||
if (noteIds.size === 0) return;
|
||||
|
||||
// TODO: ↓まとめてクエリしたい
|
||||
// Remove the record
|
||||
await this.noteUnreadsRepository.delete({
|
||||
userId: userId,
|
||||
noteId: In(Array.from(noteIds)),
|
||||
});
|
||||
|
||||
trackPromise(this.noteUnreadsRepository.countBy({
|
||||
userId: userId,
|
||||
isMentioned: true,
|
||||
}).then(mentionsCount => {
|
||||
if (mentionsCount === 0) {
|
||||
// 全て既読になったイベントを発行
|
||||
this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions');
|
||||
}
|
||||
}));
|
||||
// TODO: ↓まとめてクエリしたい
|
||||
|
||||
trackPromise(this.noteUnreadsRepository.countBy({
|
||||
userId: userId,
|
||||
isSpecified: true,
|
||||
}).then(specifiedCount => {
|
||||
if (specifiedCount === 0) {
|
||||
// 全て既読になったイベントを発行
|
||||
this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
|
||||
}
|
||||
}));
|
||||
}
|
||||
trackPromise(this.noteUnreadsRepository.countBy({
|
||||
userId: userId,
|
||||
isMentioned: true,
|
||||
}).then(mentionsCount => {
|
||||
if (mentionsCount === 0) {
|
||||
// 全て既読になったイベントを発行
|
||||
this.globalEventService.publishMainStream(userId, 'readAllUnreadMentions');
|
||||
}
|
||||
}));
|
||||
|
||||
trackPromise(this.noteUnreadsRepository.countBy({
|
||||
userId: userId,
|
||||
isSpecified: true,
|
||||
}).then(specifiedCount => {
|
||||
if (specifiedCount === 0) {
|
||||
// 全て既読になったイベントを発行
|
||||
this.globalEventService.publishMainStream(userId, 'readAllUnreadSpecifiedNotes');
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
|
@ -115,12 +115,19 @@ export class PushNotificationService implements OnApplicationShutdown {
|
|||
endpoint: subscription.endpoint,
|
||||
auth: subscription.auth,
|
||||
publickey: subscription.publickey,
|
||||
}).then(() => {
|
||||
this.refreshCache(userId);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public refreshCache(userId: string): void {
|
||||
this.subscriptionsCache.refresh(userId);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public dispose(): void {
|
||||
this.subscriptionsCache.dispose();
|
||||
|
|
|
@ -322,35 +322,36 @@ export class ReactionService {
|
|||
//#endregion
|
||||
}
|
||||
|
||||
/**
|
||||
* 文字列タイプのレガシーな形式のリアクションを現在の形式に変換しつつ、
|
||||
* データベース上には存在する「0個のリアクションがついている」という情報を削除する。
|
||||
*/
|
||||
@bindThis
|
||||
public convertLegacyReactions(reactions: Record<string, number>) {
|
||||
const _reactions = {} as Record<string, number>;
|
||||
public convertLegacyReactions(reactions: MiNote['reactions']): MiNote['reactions'] {
|
||||
return Object.entries(reactions)
|
||||
.filter(([, count]) => {
|
||||
// `ReactionService.prototype.delete`ではリアクション削除時に、
|
||||
// `MiNote['reactions']`のエントリの値をデクリメントしているが、
|
||||
// デクリメントしているだけなのでエントリ自体は0を値として持つ形で残り続ける。
|
||||
// そのため、この処理がなければ、「0個のリアクションがついている」ということになってしまう。
|
||||
return count > 0;
|
||||
})
|
||||
.map(([reaction, count]) => {
|
||||
// unchecked indexed access
|
||||
const convertedReaction = legacies[reaction] as string | undefined;
|
||||
|
||||
for (const reaction of Object.keys(reactions)) {
|
||||
if (reactions[reaction] <= 0) continue;
|
||||
const key = this.decodeReaction(convertedReaction ?? reaction).reaction;
|
||||
|
||||
if (Object.keys(legacies).includes(reaction)) {
|
||||
if (_reactions[legacies[reaction]]) {
|
||||
_reactions[legacies[reaction]] += reactions[reaction];
|
||||
} else {
|
||||
_reactions[legacies[reaction]] = reactions[reaction];
|
||||
}
|
||||
} else {
|
||||
if (_reactions[reaction]) {
|
||||
_reactions[reaction] += reactions[reaction];
|
||||
} else {
|
||||
_reactions[reaction] = reactions[reaction];
|
||||
}
|
||||
}
|
||||
}
|
||||
return [key, count] as const;
|
||||
})
|
||||
.reduce<MiNote['reactions']>((acc, [key, count]) => {
|
||||
// unchecked indexed access
|
||||
const prevCount = acc[key] as number | undefined;
|
||||
|
||||
const _reactions2 = {} as Record<string, number>;
|
||||
acc[key] = (prevCount ?? 0) + count;
|
||||
|
||||
for (const reaction of Object.keys(_reactions)) {
|
||||
_reactions2[this.decodeReaction(reaction).reaction] = _reactions[reaction];
|
||||
}
|
||||
|
||||
return _reactions2;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
|
@ -30,6 +30,7 @@ import type { Config } from '@/config.js';
|
|||
import { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
|
||||
import type { ThinUser } from '@/queue/types.js';
|
||||
import Logger from '../logger.js';
|
||||
|
||||
const logger = new Logger('following/create');
|
||||
|
@ -94,21 +95,35 @@ export class UserFollowingService implements OnModuleInit {
|
|||
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);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async follow(
|
||||
_follower: { id: MiUser['id'] },
|
||||
_followee: { id: MiUser['id'] },
|
||||
_follower: ThinUser,
|
||||
_followee: ThinUser,
|
||||
{ requestId, silent = false, withReplies }: {
|
||||
requestId?: string,
|
||||
silent?: boolean,
|
||||
withReplies?: boolean,
|
||||
} = {},
|
||||
): Promise<void> {
|
||||
/**
|
||||
* 必ず最新のユーザー情報を取得する
|
||||
*/
|
||||
const [follower, followee] = await Promise.all([
|
||||
this.usersRepository.findOneByOrFail({ id: _follower.id }),
|
||||
this.usersRepository.findOneByOrFail({ id: _followee.id }),
|
||||
]) as [MiLocalUser | MiRemoteUser, MiLocalUser | MiRemoteUser];
|
||||
|
||||
if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isRemoteUser(followee)) {
|
||||
// What?
|
||||
throw new Error('Remote user cannot follow remote user.');
|
||||
}
|
||||
|
||||
// check blocking
|
||||
const [blocking, blocked] = await Promise.all([
|
||||
this.userBlockingService.checkBlocked(follower.id, followee.id),
|
||||
|
@ -129,6 +144,24 @@ export class UserFollowingService implements OnModuleInit {
|
|||
if (blocked) throw new IdentifiableError('3338392a-f764-498d-8855-db939dcf8c48', 'blocked');
|
||||
}
|
||||
|
||||
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 });
|
||||
// フォロー対象が鍵アカウントである or
|
||||
// フォロワーがBotであり、フォロー対象がBotからのフォローに慎重である or
|
||||
|
@ -189,8 +222,7 @@ export class UserFollowingService implements OnModuleInit {
|
|||
await this.insertFollowingDoc(followee, follower, silent, withReplies);
|
||||
|
||||
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.queueService.deliver(followee, content, follower.inbox, false);
|
||||
this.deliverAccept(follower, followee, requestId);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -571,8 +603,7 @@ export class UserFollowingService implements OnModuleInit {
|
|||
await this.insertFollowingDoc(followee, follower, false, request.withReplies);
|
||||
|
||||
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.queueService.deliver(followee, content, follower.inbox, false);
|
||||
this.deliverAccept(follower, followee as MiPartialLocalUser, request.requestId ?? undefined);
|
||||
}
|
||||
|
||||
this.userEntityService.pack(followee.id, followee, {
|
||||
|
|
|
@ -8,6 +8,7 @@ import promiseLimit from 'promise-limit';
|
|||
import type { MiRemoteUser, MiUser } from '@/models/User.js';
|
||||
import { concat, unique } from '@/misc/prelude/array.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
import { getApIds } from './type.js';
|
||||
import { ApPersonService } from './models/ApPersonService.js';
|
||||
import type { ApObject } from './type.js';
|
||||
|
@ -40,7 +41,7 @@ export class ApAudienceService {
|
|||
const limit = promiseLimit<MiUser | null>(2);
|
||||
const mentionedUsers = (await Promise.all(
|
||||
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) {
|
||||
return {
|
||||
|
|
|
@ -27,6 +27,7 @@ import { QueueService } from '@/core/QueueService.js';
|
|||
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/_.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { 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 { ApNoteService } from './models/ApNoteService.js';
|
||||
import { ApLoggerService } from './ApLoggerService.js';
|
||||
|
@ -84,7 +85,6 @@ export class ApInboxService {
|
|||
private apPersonService: ApPersonService,
|
||||
private apQuestionService: ApQuestionService,
|
||||
private queueService: QueueService,
|
||||
private cacheService: CacheService,
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
this.logger = this.apLoggerService.logger;
|
||||
|
@ -521,7 +521,7 @@ export class ApInboxService {
|
|||
const userIds = uris
|
||||
.filter(uri => uri.startsWith(this.config.url + '/users/'))
|
||||
.map(uri => uri.split('/').at(-1))
|
||||
.filter((userId): userId is string => userId !== undefined);
|
||||
.filter(isNotNull);
|
||||
const users = await this.usersRepository.findBy({
|
||||
id: In(userIds),
|
||||
});
|
||||
|
|
|
@ -315,7 +315,7 @@ export class ApRendererService {
|
|||
const getPromisedFiles = async (ids: string[]): Promise<MiDriveFile[]> => {
|
||||
if (ids.length === 0) return [];
|
||||
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;
|
||||
|
|
|
@ -8,6 +8,7 @@ import promiseLimit from 'promise-limit';
|
|||
import type { MiUser } from '@/models/_.js';
|
||||
import { toArray, unique } from '@/misc/prelude/array.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
import { isMention } from '../type.js';
|
||||
import { Resolver } from '../ApResolverService.js';
|
||||
import { ApPersonService } from './ApPersonService.js';
|
||||
|
@ -27,7 +28,7 @@ export class ApMentionService {
|
|||
const limit = promiseLimit<MiUser | null>(2);
|
||||
const mentionedUsers = (await Promise.all(
|
||||
hrefs.map(x => limit(() => this.apPersonService.resolvePerson(x, resolver).catch(() => null))),
|
||||
)).filter((x): x is MiUser => x != null);
|
||||
)).filter(isNotNull);
|
||||
|
||||
return mentionedUsers;
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ import { ApQuestionService } from './ApQuestionService.js';
|
|||
import { ApImageService } from './ApImageService.js';
|
||||
import type { Resolver } from '../ApResolverService.js';
|
||||
import type { IObject, IPost } from '../type.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
|
||||
@Injectable()
|
||||
export class ApNoteService {
|
||||
|
@ -221,7 +222,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));
|
||||
|
||||
quote = results.filter((x): x is { status: 'ok', res: MiNote } => x.status === 'ok').map(x => x.res).at(0);
|
||||
|
|
|
@ -38,6 +38,7 @@ import { MetaService } from '@/core/MetaService.js';
|
|||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||
import type { AccountMoveService } from '@/core/AccountMoveService.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 { extractApHashtags } from './tag.js';
|
||||
import type { OnModuleInit } from '@nestjs/common';
|
||||
|
@ -636,7 +637,7 @@ export class ApPersonService implements OnModuleInit {
|
|||
|
||||
// とりあえずidを別の時間で生成して順番を維持
|
||||
let td = 0;
|
||||
for (const note of featuredNotes.filter((note): note is MiNote => note != null)) {
|
||||
for (const note of featuredNotes.filter(isNotNull)) {
|
||||
td -= 1000;
|
||||
transactionalEntityManager.insert(MiUserNotePining, {
|
||||
id: this.idService.gen(Date.now() + td),
|
||||
|
|
|
@ -10,6 +10,7 @@ import type { Config } from '@/config.js';
|
|||
import type { IPoll } from '@/models/Poll.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
import { isQuestion } from '../type.js';
|
||||
import { ApLoggerService } from '../ApLoggerService.js';
|
||||
import { ApResolverService } from '../ApResolverService.js';
|
||||
|
@ -51,7 +52,7 @@ export class ApQuestionService {
|
|||
|
||||
const choices = question[multiple ? 'anyOf' : 'oneOf']
|
||||
?.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);
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
*/
|
||||
|
||||
import { toArray } from '@/misc/prelude/array.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
import { isHashtag } 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 => {
|
||||
const m = tag.name.match(/^#(.+)/);
|
||||
return m ? m[1] : null;
|
||||
}).filter((x): x is string => x != null);
|
||||
}).filter(isNotNull);
|
||||
}
|
||||
|
||||
export function extractApHashtagObjects(tags: IObject | IObject[] | null | undefined): IApHashtag[] {
|
||||
|
|
|
@ -259,7 +259,7 @@ export class DriveFileEntityService {
|
|||
options?: PackOptions,
|
||||
): Promise<Packed<'DriveFile'>[]> {
|
||||
const items = await Promise.all(files.map(f => this.packNullable(f, options)));
|
||||
return items.filter((x): x is Packed<'DriveFile'> => x != null);
|
||||
return items.filter(isNotNull);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
|
@ -8,12 +8,15 @@ import type { Packed } from '@/misc/json-schema.js';
|
|||
import type { MiInstance } from '@/models/Instance.js';
|
||||
import { MetaService } from '@/core/MetaService.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()
|
||||
export class InstanceEntityService {
|
||||
constructor(
|
||||
private metaService: MetaService,
|
||||
private roleService: RoleService,
|
||||
|
||||
private utilityService: UtilityService,
|
||||
) {
|
||||
|
@ -22,8 +25,11 @@ export class InstanceEntityService {
|
|||
@bindThis
|
||||
public async pack(
|
||||
instance: MiInstance,
|
||||
me?: { id: MiUser['id']; } | null | undefined,
|
||||
): Promise<Packed<'FederationInstance'>> {
|
||||
const meta = await this.metaService.fetch();
|
||||
const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false;
|
||||
|
||||
return {
|
||||
id: instance.id,
|
||||
firstRetrievedAt: instance.firstRetrievedAt.toISOString(),
|
||||
|
@ -48,6 +54,7 @@ export class InstanceEntityService {
|
|||
themeColor: instance.themeColor,
|
||||
infoUpdatedAt: instance.infoUpdatedAt ? instance.infoUpdatedAt.toISOString() : null,
|
||||
latestRequestReceivedAt: instance.latestRequestReceivedAt ? instance.latestRequestReceivedAt.toISOString() : null,
|
||||
moderationNote: iAmModerator ? instance.moderationNote : null,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
154
packages/backend/src/core/entities/MetaEntityService.ts
Normal file
154
packages/backend/src/core/entities/MetaEntityService.ts
Normal file
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* 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();
|
||||
|
||||
const packed: Packed<'MetaLite'> = {
|
||||
maintainerName: instance.maintainerName,
|
||||
maintainerEmail: instance.maintainerEmail,
|
||||
|
||||
version: this.config.version,
|
||||
providesTarball: this.config.publishTarballInsteadOfProvideRepositoryUrl,
|
||||
|
||||
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,
|
||||
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,
|
||||
};
|
||||
|
||||
return packed;
|
||||
}
|
||||
|
||||
@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).catch(() => null) : null;
|
||||
|
||||
const packDetailed: Packed<'MetaDetailed'> = {
|
||||
...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,
|
||||
recaptcha: instance.enableRecaptcha,
|
||||
turnstile: instance.enableTurnstile,
|
||||
objectStorage: instance.useObjectStorage,
|
||||
serviceWorker: instance.enableServiceWorker,
|
||||
miauth: true,
|
||||
},
|
||||
};
|
||||
|
||||
return packDetailed;
|
||||
}
|
||||
}
|
||||
|
|
@ -69,4 +69,19 @@ export class NoteReactionEntityService implements OnModuleInit {
|
|||
} : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async packMany(
|
||||
reactions: MiNoteReaction[],
|
||||
me?: { id: MiUser['id'] } | null | undefined,
|
||||
options?: {
|
||||
withNote: boolean;
|
||||
},
|
||||
): Promise<Packed<'NoteReaction'>[]> {
|
||||
const opts = Object.assign({
|
||||
withNote: false,
|
||||
}, options);
|
||||
|
||||
return Promise.all(reactions.map(reaction => this.pack(reaction, me, opts)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import type { MiPage } from '@/models/Page.js';
|
|||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
import { UserEntityService } from './UserEntityService.js';
|
||||
import { DriveFileEntityService } from './DriveFileEntityService.js';
|
||||
|
||||
|
@ -102,7 +103,7 @@ export class PageEntityService {
|
|||
script: page.script,
|
||||
eyeCatchingImageId: page.eyeCatchingImageId,
|
||||
eyeCatchingImage: page.eyeCatchingImageId ? await this.driveFileEntityService.pack(page.eyeCatchingImageId) : null,
|
||||
attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter((x): x is MiDriveFile => x != null)),
|
||||
attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter(isNotNull)),
|
||||
likedCount: page.likedCount,
|
||||
isLiked: meId ? await this.pageLikesRepository.exists({ where: { pageId: page.id, userId: meId } }) : undefined,
|
||||
});
|
||||
|
|
|
@ -25,6 +25,7 @@ import { IdService } from '@/core/IdService.js';
|
|||
import type { AnnouncementService } from '@/core/AnnouncementService.js';
|
||||
import type { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
import { AvatarDecorationService } from '@/core/AvatarDecorationService.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
import type { OnModuleInit } from '@nestjs/common';
|
||||
import type { NoteEntityService } from './NoteEntityService.js';
|
||||
import type { DriveFileEntityService } from './DriveFileEntityService.js';
|
||||
|
@ -384,7 +385,7 @@ export class UserEntityService implements OnModuleInit {
|
|||
movedTo: user.movedToUri ? this.apPersonService.resolvePerson(user.movedToUri).then(user => user.id).catch(() => null) : null,
|
||||
alsoKnownAs: user.alsoKnownAs
|
||||
? 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,
|
||||
createdAt: this.idService.parse(user.id).date.toISOString(),
|
||||
updatedAt: user.updatedAt ? user.updatedAt.toISOString() : null,
|
||||
|
|
|
@ -187,6 +187,10 @@ export class RedisSingleCache<T> {
|
|||
// TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする?
|
||||
|
||||
export class MemoryKVCache<T> {
|
||||
/**
|
||||
* データを持つマップ
|
||||
* @deprecated これを直接操作するべきではない
|
||||
*/
|
||||
public cache: Map<string, { date: number; value: T; }>;
|
||||
private lifetime: number;
|
||||
private gcIntervalHandle: NodeJS.Timeout;
|
||||
|
@ -201,6 +205,10 @@ export class MemoryKVCache<T> {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
/**
|
||||
* Mapにキャッシュをセットします
|
||||
* @deprecated これを直接呼び出すべきではない。InternalEventなどで変更を全てのプロセス/マシンに通知するべき
|
||||
*/
|
||||
public set(key: string, value: T): void {
|
||||
this.cache.set(key, {
|
||||
date: Date.now(),
|
||||
|
|
|
@ -3,8 +3,6 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
// we are using {} as "any non-nullish value" as expected
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
export function isNotNull<T extends {}>(input: T | undefined | null): input is T {
|
||||
export function isNotNull<T extends NonNullable<unknown>>(input: T | undefined | null): input is T {
|
||||
return input != null;
|
||||
}
|
||||
|
|
|
@ -54,6 +54,11 @@ import {
|
|||
} from '@/models/json-schema/role.js';
|
||||
import { packedAdSchema } from '@/models/json-schema/ad.js';
|
||||
import { packedReversiGameLiteSchema, packedReversiGameDetailedSchema } from '@/models/json-schema/reversi-game.js';
|
||||
import {
|
||||
packedMetaLiteSchema,
|
||||
packedMetaDetailedOnlySchema,
|
||||
packedMetaDetailedSchema,
|
||||
} from '@/models/json-schema/meta.js';
|
||||
|
||||
export const refs = {
|
||||
UserLite: packedUserLiteSchema,
|
||||
|
@ -104,6 +109,9 @@ export const refs = {
|
|||
RolePolicies: packedRolePoliciesSchema,
|
||||
ReversiGameLite: packedReversiGameLiteSchema,
|
||||
ReversiGameDetailed: packedReversiGameDetailedSchema,
|
||||
MetaLite: packedMetaLiteSchema,
|
||||
MetaDetailedOnly: packedMetaDetailedOnlySchema,
|
||||
MetaDetailed: packedMetaDetailedSchema,
|
||||
};
|
||||
|
||||
export type Packed<x extends keyof typeof refs> = SchemaType<typeof refs[x]>;
|
||||
|
|
|
@ -144,4 +144,9 @@ export class MiInstance {
|
|||
nullable: true,
|
||||
})
|
||||
public infoUpdatedAt: Date | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 16384, default: '',
|
||||
})
|
||||
public moderationNote: string;
|
||||
}
|
||||
|
|
|
@ -253,6 +253,8 @@ export class MiMeta {
|
|||
})
|
||||
public turnstileSecretKey: string | null;
|
||||
|
||||
// chaptcha系を追加した際にはnodeinfoのレスポンスに追加するのを忘れないようにすること
|
||||
|
||||
@Column('enum', {
|
||||
enum: ['none', 'all', 'local', 'remote'],
|
||||
default: 'none',
|
||||
|
|
|
@ -107,5 +107,9 @@ export const packedFederationInstanceSchema = {
|
|||
optional: false, nullable: true,
|
||||
format: 'date-time',
|
||||
},
|
||||
moderationNote: {
|
||||
type: 'string',
|
||||
optional: true, nullable: true,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
|
328
packages/backend/src/models/json-schema/meta.ts
Normal file
328
packages/backend/src/models/json-schema/meta.ts
Normal file
|
@ -0,0 +1,328 @@
|
|||
/*
|
||||
* 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,
|
||||
},
|
||||
providesTarball: {
|
||||
type: 'boolean',
|
||||
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,
|
||||
},
|
||||
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,
|
||||
},
|
||||
turnstile: {
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
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;
|
|
@ -24,6 +24,7 @@ import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
|||
import { LdSignatureService } from '@/core/activitypub/LdSignatureService.js';
|
||||
import { ApInboxService } from '@/core/activitypub/ApInboxService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type { InboxJobData } from '../types.js';
|
||||
|
||||
|
@ -180,7 +181,14 @@ export class InboxProcessorService {
|
|||
});
|
||||
|
||||
// アクティビティを処理
|
||||
await this.apInboxService.performActivity(authUser.user, activity);
|
||||
try {
|
||||
await this.apInboxService.performActivity(authUser.user, activity);
|
||||
} catch (e) {
|
||||
if (e instanceof IdentifiableError) {
|
||||
if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') return 'blocked notes with prohibited words';
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
return 'ok';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -117,6 +117,8 @@ export class NodeinfoServerService {
|
|||
emailRequiredForSignup: meta.emailRequiredForSignup,
|
||||
enableHcaptcha: meta.enableHcaptcha,
|
||||
enableRecaptcha: meta.enableRecaptcha,
|
||||
enableMcaptcha: meta.enableMcaptcha,
|
||||
enableTurnstile: meta.enableTurnstile,
|
||||
maxNoteTextLength: MAX_NOTE_TEXT_LENGTH,
|
||||
enableEmail: meta.enableEmail,
|
||||
enableServiceWorker: meta.enableServiceWorker,
|
||||
|
|
|
@ -57,7 +57,10 @@ export const paramDef = {
|
|||
type: 'string',
|
||||
} },
|
||||
},
|
||||
required: ['id', 'name', 'aliases'],
|
||||
anyOf: [
|
||||
{ required: ['id'] },
|
||||
{ required: ['name'] },
|
||||
],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
|
@ -70,27 +73,33 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
let driveFile;
|
||||
|
||||
if (ps.fileId) {
|
||||
driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
||||
if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
|
||||
}
|
||||
const emoji = await this.customEmojiService.getEmojiById(ps.id);
|
||||
if (emoji != null) {
|
||||
if (ps.name !== emoji.name) {
|
||||
|
||||
let emojiId;
|
||||
if (ps.id) {
|
||||
emojiId = ps.id;
|
||||
const emoji = await this.customEmojiService.getEmojiById(ps.id);
|
||||
if (!emoji) throw new ApiError(meta.errors.noSuchEmoji);
|
||||
if (ps.name && (ps.name !== emoji.name)) {
|
||||
const isDuplicate = await this.customEmojiService.checkDuplicate(ps.name);
|
||||
if (isDuplicate) throw new ApiError(meta.errors.sameNameEmojiExists);
|
||||
}
|
||||
} else {
|
||||
throw new ApiError(meta.errors.noSuchEmoji);
|
||||
if (!ps.name) throw new Error('Invalid Params unexpectedly passed. This is a BUG. Please report it to the development team.');
|
||||
const emoji = await this.customEmojiService.getEmojiByName(ps.name);
|
||||
if (!emoji) throw new ApiError(meta.errors.noSuchEmoji);
|
||||
emojiId = emoji.id;
|
||||
}
|
||||
|
||||
await this.customEmojiService.update(ps.id, {
|
||||
await this.customEmojiService.update(emojiId, {
|
||||
driveFile,
|
||||
name: ps.name,
|
||||
category: ps.category ?? null,
|
||||
category: ps.category,
|
||||
aliases: ps.aliases,
|
||||
license: ps.license ?? null,
|
||||
license: ps.license,
|
||||
isSensitive: ps.isSensitive,
|
||||
localOnly: ps.localOnly,
|
||||
roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction,
|
||||
|
|
|
@ -24,8 +24,9 @@ export const paramDef = {
|
|||
properties: {
|
||||
host: { type: 'string' },
|
||||
isSuspended: { type: 'boolean' },
|
||||
moderationNote: { type: 'string' },
|
||||
},
|
||||
required: ['host', 'isSuspended'],
|
||||
required: ['host'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
|
@ -47,9 +48,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
await this.federatedInstanceService.update(instance.id, {
|
||||
isSuspended: ps.isSuspended,
|
||||
moderationNote: ps.moderationNote,
|
||||
});
|
||||
|
||||
if (instance.isSuspended !== ps.isSuspended) {
|
||||
if (ps.isSuspended != null && instance.isSuspended !== ps.isSuspended) {
|
||||
if (ps.isSuspended) {
|
||||
this.moderationLogService.log(me, 'suspendRemoteInstance', {
|
||||
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,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -124,9 +124,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
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);
|
||||
});
|
||||
|
|
|
@ -43,7 +43,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
const instance = await this.instancesRepository
|
||||
.findOneBy({ host: this.utilityService.toPuny(ps.host) });
|
||||
|
||||
return instance ? await this.instanceEntityService.pack(instance) : null;
|
||||
return instance ? await this.instanceEntityService.pack(instance, me) : null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,7 +51,7 @@ export const paramDef = {
|
|||
} },
|
||||
visibility: { type: 'string', enum: ['public', 'private'] },
|
||||
},
|
||||
required: ['flashId', 'title', 'summary', 'script', 'permissions'],
|
||||
required: ['flashId'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
|
@ -71,11 +71,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
await this.flashsRepository.update(flash.id, {
|
||||
updatedAt: new Date(),
|
||||
title: ps.title,
|
||||
summary: ps.summary,
|
||||
script: ps.script,
|
||||
permissions: ps.permissions,
|
||||
visibility: ps.visibility,
|
||||
...Object.fromEntries(
|
||||
Object.entries(ps).filter(
|
||||
([key, val]) => (key !== 'flashId') && Object.hasOwn(paramDef.properties, key)
|
||||
)
|
||||
),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -71,7 +71,7 @@ export const paramDef = {
|
|||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
withReplies: { type: 'boolean' }
|
||||
withReplies: { type: 'boolean' },
|
||||
},
|
||||
required: ['userId'],
|
||||
} as const;
|
||||
|
@ -100,22 +100,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
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 {
|
||||
await this.userFollowingService.follow(follower, followee, { withReplies: ps.withReplies });
|
||||
} catch (e) {
|
||||
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 === '3338392a-f764-498d-8855-db939dcf8c48') throw new ApiError(meta.errors.blocked);
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import type { MiDriveFile } from '@/models/DriveFile.js';
|
|||
import { IdService } from '@/core/IdService.js';
|
||||
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['gallery'],
|
||||
|
@ -69,7 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
id: fileId,
|
||||
userId: me.id,
|
||||
}),
|
||||
))).filter((file): file is MiDriveFile => file != null);
|
||||
))).filter(isNotNull);
|
||||
|
||||
if (files.length === 0) {
|
||||
throw new Error();
|
||||
|
|
|
@ -10,6 +10,7 @@ import type { DriveFilesRepository, GalleryPostsRepository } from '@/models/_.js
|
|||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['gallery'],
|
||||
|
@ -67,7 +68,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
id: fileId,
|
||||
userId: me.id,
|
||||
}),
|
||||
))).filter((file): file is MiDriveFile => file != null);
|
||||
))).filter(isNotNull);
|
||||
|
||||
if (files.length === 0) {
|
||||
throw new Error();
|
||||
|
|
|
@ -43,7 +43,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
super(meta, paramDef, async (ps, me) => {
|
||||
const hashtags = await this.hashtagsRepository.createQueryBuilder('tag')
|
||||
.where('tag.name like :q', { q: sqlLikeEscape(ps.query.toLowerCase()) + '%' })
|
||||
.orderBy('tag.count', 'DESC')
|
||||
.orderBy('tag.mentionedLocalUsersCount', 'DESC')
|
||||
.groupBy('tag.id')
|
||||
.limit(ps.limit)
|
||||
.offset(ps.offset)
|
||||
|
|
|
@ -456,9 +456,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
this.hashtagService.updateUsertags(user, tags);
|
||||
//#endregion
|
||||
|
||||
if (Object.keys(updates).length > 0) await this.usersRepository.update(user.id, updates);
|
||||
if (Object.keys(updates).includes('alsoKnownAs')) {
|
||||
this.cacheService.uriPersonCache.set(this.userEntityService.genLocalUserUri(user.id), { ...user, ...updates });
|
||||
if (Object.keys(updates).length > 0) {
|
||||
await this.usersRepository.update(user.id, updates);
|
||||
this.globalEventService.publishInternalEvent('localUserUpdated', { id: user.id });
|
||||
}
|
||||
|
||||
await this.userProfilesRepository.update(user.id, {
|
||||
|
|
|
@ -3,18 +3,9 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { IsNull, LessThanOrEqual, MoreThan, Brackets } from 'typeorm';
|
||||
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 { Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.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';
|
||||
import { MetaEntityService } from '@/core/entities/MetaEntityService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['meta'],
|
||||
|
@ -23,297 +14,10 @@ export const meta = {
|
|||
|
||||
res: {
|
||||
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,
|
||||
},
|
||||
providesTarball: {
|
||||
type: 'boolean',
|
||||
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,
|
||||
},
|
||||
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',
|
||||
},
|
||||
},
|
||||
oneOf: [
|
||||
{ type: 'object', ref: 'MetaLite' },
|
||||
{ type: 'object', ref: 'MetaDetailed' },
|
||||
],
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
@ -328,115 +32,10 @@ export const paramDef = {
|
|||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.adsRepository)
|
||||
private adsRepository: AdsRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
private metaService: MetaService,
|
||||
private instanceActorService: InstanceActorService,
|
||||
private metaEntityService: MetaEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const instance = await this.metaService.fetch(true);
|
||||
|
||||
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,
|
||||
providesTarball: this.config.publishTarballInsteadOfProvideRepositoryUrl,
|
||||
|
||||
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).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;
|
||||
return ps.detail ? await this.metaEntityService.packDetailed() : await this.metaEntityService.pack();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import { DI } from '@/di-symbols.js';
|
|||
import { isPureRenote } from '@/misc/is-pure-renote.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { UtilityService } from '@/core/UtilityService.js';
|
||||
import { IdentifiableError } from '@/misc/identifiable-error.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
|
@ -376,8 +377,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
};
|
||||
} catch (e) {
|
||||
// TODO: 他のErrorもここでキャッチしてエラーメッセージを当てるようにしたい
|
||||
if (e instanceof NoteCreateService.ContainsProhibitedWordsError) {
|
||||
throw new ApiError(meta.errors.containsProhibitedWords);
|
||||
if (e instanceof IdentifiableError) {
|
||||
if (e.id === '689ee33f-f97c-479a-ac49-1b9f8140af99') throw new ApiError(meta.errors.containsProhibitedWords);
|
||||
}
|
||||
|
||||
throw e;
|
||||
|
|
|
@ -12,6 +12,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
|
|||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { isNotNull } from '@/misc/is-not-null.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['users'],
|
||||
|
@ -52,7 +53,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
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' });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import type { SwSubscriptionsRepository } from '@/models/_.js';
|
|||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { PushNotificationService } from '@/core/PushNotificationService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account'],
|
||||
|
@ -66,6 +67,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
private idService: IdService,
|
||||
private metaService: MetaService,
|
||||
private pushNotificationService: PushNotificationService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
// if already subscribed
|
||||
|
@ -97,6 +99,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
sendReadMessage: ps.sendReadMessage,
|
||||
});
|
||||
|
||||
this.pushNotificationService.refreshCache(me.id);
|
||||
|
||||
return {
|
||||
state: 'subscribed' as const,
|
||||
key: instance.swPublicKey,
|
||||
|
|
|
@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||
import type { SwSubscriptionsRepository } from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { PushNotificationService } from '@/core/PushNotificationService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['account'],
|
||||
|
@ -29,12 +30,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
constructor(
|
||||
@Inject(DI.swSubscriptionsRepository)
|
||||
private swSubscriptionsRepository: SwSubscriptionsRepository,
|
||||
|
||||
private pushNotificationService: PushNotificationService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.swSubscriptionsRepository.delete({
|
||||
...(me ? { userId: me.id } : {}),
|
||||
endpoint: ps.endpoint,
|
||||
});
|
||||
|
||||
if (me) {
|
||||
this.pushNotificationService.refreshCache(me.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||
import type { SwSubscriptionsRepository } from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { PushNotificationService } from '@/core/PushNotificationService.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
|
@ -58,6 +59,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
constructor(
|
||||
@Inject(DI.swSubscriptionsRepository)
|
||||
private swSubscriptionsRepository: SwSubscriptionsRepository,
|
||||
|
||||
private pushNotificationService: PushNotificationService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const swSubscription = await this.swSubscriptionsRepository.findOneBy({
|
||||
|
@ -77,6 +80,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
sendReadMessage: swSubscription.sendReadMessage,
|
||||
});
|
||||
|
||||
this.pushNotificationService.refreshCache(me.id);
|
||||
|
||||
return {
|
||||
userId: swSubscription.userId,
|
||||
endpoint: swSubscription.endpoint,
|
||||
|
|
|
@ -98,7 +98,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
.limit(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return await Promise.all(reactions.map(reaction => this.noteReactionEntityService.pack(reaction, me, { withNote: true })));
|
||||
return await this.noteReactionEntityService.packMany(reactions, me, { withNote: true });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import fastifyView from '@fastify/view';
|
|||
import fastifyCookie from '@fastify/cookie';
|
||||
import fastifyProxy from '@fastify/http-proxy';
|
||||
import vary from 'vary';
|
||||
import htmlSafeJsonStringify from 'htmlescape';
|
||||
import type { Config } from '@/config.js';
|
||||
import { getNoteSummary } from '@/misc/get-note-summary.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 { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { PageEntityService } from '@/core/entities/PageEntityService.js';
|
||||
import { MetaEntityService } from '@/core/entities/MetaEntityService.js';
|
||||
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
|
||||
import { ClipEntityService } from '@/core/entities/ClipEntityService.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 Logger from '@/logger.js';
|
||||
import { deepClone } from '@/misc/clone.js';
|
||||
import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
|
||||
|
@ -93,6 +94,7 @@ export class ClientServerService {
|
|||
private userEntityService: UserEntityService,
|
||||
private noteEntityService: NoteEntityService,
|
||||
private pageEntityService: PageEntityService,
|
||||
private metaEntityService: MetaEntityService,
|
||||
private galleryPostEntityService: GalleryPostEntityService,
|
||||
private clipEntityService: ClipEntityService,
|
||||
private channelEntityService: ChannelEntityService,
|
||||
|
@ -173,7 +175,7 @@ export class ClientServerService {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
private generateCommonPugData(meta: MiMeta) {
|
||||
private async generateCommonPugData(meta: MiMeta) {
|
||||
return {
|
||||
instanceName: meta.name ?? 'Misskey',
|
||||
icon: meta.iconUrl,
|
||||
|
@ -183,6 +185,8 @@ export class ClientServerService {
|
|||
infoImageUrl: meta.infoImageUrl ?? 'https://xn--931a.moe/assets/info.jpg',
|
||||
notFoundImageUrl: meta.notFoundImageUrl ?? 'https://xn--931a.moe/assets/not-found.jpg',
|
||||
instanceUrl: this.config.url,
|
||||
metaJson: htmlSafeJsonStringify(await this.metaEntityService.packDetailed(meta)),
|
||||
now: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -433,7 +437,7 @@ export class ClientServerService {
|
|||
url: this.config.url,
|
||||
title: meta.name ?? 'Misskey',
|
||||
desc: meta.description,
|
||||
...this.generateCommonPugData(meta),
|
||||
...await this.generateCommonPugData(meta),
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -520,7 +524,7 @@ export class ClientServerService {
|
|||
user, profile, me,
|
||||
avatarUrl: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user),
|
||||
sub: request.params.sub,
|
||||
...this.generateCommonPugData(meta),
|
||||
...await this.generateCommonPugData(meta),
|
||||
});
|
||||
} else {
|
||||
// リモートユーザーなので
|
||||
|
@ -570,7 +574,7 @@ export class ClientServerService {
|
|||
avatarUrl: _note.user.avatarUrl,
|
||||
// TODO: Let locale changeable by instance setting
|
||||
summary: getNoteSummary(_note),
|
||||
...this.generateCommonPugData(meta),
|
||||
...await this.generateCommonPugData(meta),
|
||||
});
|
||||
} else {
|
||||
return await renderBase(reply);
|
||||
|
@ -609,7 +613,7 @@ export class ClientServerService {
|
|||
page: _page,
|
||||
profile,
|
||||
avatarUrl: _page.user.avatarUrl,
|
||||
...this.generateCommonPugData(meta),
|
||||
...await this.generateCommonPugData(meta),
|
||||
});
|
||||
} else {
|
||||
return await renderBase(reply);
|
||||
|
@ -635,7 +639,7 @@ export class ClientServerService {
|
|||
flash: _flash,
|
||||
profile,
|
||||
avatarUrl: _flash.user.avatarUrl,
|
||||
...this.generateCommonPugData(meta),
|
||||
...await this.generateCommonPugData(meta),
|
||||
});
|
||||
} else {
|
||||
return await renderBase(reply);
|
||||
|
@ -661,7 +665,7 @@ export class ClientServerService {
|
|||
clip: _clip,
|
||||
profile,
|
||||
avatarUrl: _clip.user.avatarUrl,
|
||||
...this.generateCommonPugData(meta),
|
||||
...await this.generateCommonPugData(meta),
|
||||
});
|
||||
} else {
|
||||
return await renderBase(reply);
|
||||
|
@ -685,7 +689,7 @@ export class ClientServerService {
|
|||
post: _post,
|
||||
profile,
|
||||
avatarUrl: _post.user.avatarUrl,
|
||||
...this.generateCommonPugData(meta),
|
||||
...await this.generateCommonPugData(meta),
|
||||
});
|
||||
} else {
|
||||
return await renderBase(reply);
|
||||
|
@ -704,7 +708,7 @@ export class ClientServerService {
|
|||
reply.header('Cache-Control', 'public, max-age=15');
|
||||
return await reply.view('channel', {
|
||||
channel: _channel,
|
||||
...this.generateCommonPugData(meta),
|
||||
...await this.generateCommonPugData(meta),
|
||||
});
|
||||
} else {
|
||||
return await renderBase(reply);
|
||||
|
@ -723,7 +727,7 @@ export class ClientServerService {
|
|||
reply.header('Cache-Control', 'public, max-age=3600');
|
||||
return await reply.view('reversi-game', {
|
||||
game: _game,
|
||||
...this.generateCommonPugData(meta),
|
||||
...await this.generateCommonPugData(meta),
|
||||
});
|
||||
} else {
|
||||
return await renderBase(reply);
|
||||
|
|
|
@ -68,6 +68,9 @@ html
|
|||
var VERSION = "#{version}";
|
||||
var CLIENT_ENTRY = "#{clientEntry.file}";
|
||||
|
||||
script(type='application/json' id='misskey_meta' data-generated-at=now)
|
||||
!= metaJson
|
||||
|
||||
script
|
||||
include ../boot.js
|
||||
|
||||
|
|
|
@ -69,6 +69,7 @@ export const moderationLogTypes = [
|
|||
'resetPassword',
|
||||
'suspendRemoteInstance',
|
||||
'unsuspendRemoteInstance',
|
||||
'updateRemoteInstanceNote',
|
||||
'markSensitiveDriveFile',
|
||||
'unmarkSensitiveDriveFile',
|
||||
'resolveAbuseReport',
|
||||
|
@ -209,6 +210,12 @@ export type ModerationLogPayloads = {
|
|||
id: string;
|
||||
host: string;
|
||||
};
|
||||
updateRemoteInstanceNote: {
|
||||
id: string;
|
||||
host: string;
|
||||
before: string | null;
|
||||
after: string | null;
|
||||
};
|
||||
markSensitiveDriveFile: {
|
||||
fileId: string;
|
||||
fileUserId: string | null;
|
||||
|
|
|
@ -90,4 +90,45 @@ describe('ReactionService', () => {
|
|||
assert.strictEqual(await reactionService.normalize('unknown'), '❤');
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertLegacyReactions', () => {
|
||||
test('空の入力に対しては何もしない', () => {
|
||||
const input = {};
|
||||
assert.deepStrictEqual(reactionService.convertLegacyReactions(input), input);
|
||||
});
|
||||
|
||||
test('Unicode絵文字リアクションを変換してしまわない', () => {
|
||||
const input = { '👍': 1, '🍮': 2 };
|
||||
assert.deepStrictEqual(reactionService.convertLegacyReactions(input), input);
|
||||
});
|
||||
|
||||
test('カスタム絵文字リアクションを変換してしまわない', () => {
|
||||
const input = { ':like@.:': 1, ':pudding@example.tld:': 2 };
|
||||
assert.deepStrictEqual(reactionService.convertLegacyReactions(input), input);
|
||||
});
|
||||
|
||||
test('文字列によるレガシーなリアクションを変換する', () => {
|
||||
const input = { 'like': 1, 'pudding': 2 };
|
||||
const output = { '👍': 1, '🍮': 2 };
|
||||
assert.deepStrictEqual(reactionService.convertLegacyReactions(input), output);
|
||||
});
|
||||
|
||||
test('host部分が省略されたレガシーなカスタム絵文字リアクションを変換する', () => {
|
||||
const input = { ':custom_emoji:': 1 };
|
||||
const output = { ':custom_emoji@.:': 1 };
|
||||
assert.deepStrictEqual(reactionService.convertLegacyReactions(input), output);
|
||||
});
|
||||
|
||||
test('「0個のリアクション」情報を削除する', () => {
|
||||
const input = { 'angry': 0 };
|
||||
const output = {};
|
||||
assert.deepStrictEqual(reactionService.convertLegacyReactions(input), output);
|
||||
});
|
||||
|
||||
test('host部分の有無によりデコードすると同じ表記になるカスタム絵文字リアクションの個数情報を正しく足し合わせる', () => {
|
||||
const input = { ':custom_emoji:': 1, ':custom_emoji@.:': 2 };
|
||||
const output = { ':custom_emoji@.:': 3 };
|
||||
assert.deepStrictEqual(reactionService.convertLegacyReactions(input), output);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -401,7 +401,8 @@ function toStories(component: string): Promise<string> {
|
|||
// glob('src/{components,pages,ui,widgets}/**/*.vue')
|
||||
(async () => {
|
||||
const globs = await Promise.all([
|
||||
glob('src/components/global/*.vue'),
|
||||
glob('src/components/global/Mk*.vue'),
|
||||
glob('src/components/global/RouterView.vue'),
|
||||
glob('src/components/Mk{A,B}*.vue'),
|
||||
glob('src/components/MkDigitalClock.vue'),
|
||||
glob('src/components/MkGalleryPostPreview.vue'),
|
||||
|
|
|
@ -11,7 +11,7 @@ import { alert, confirm, popup, post, toast } from '@/os.js';
|
|||
import { useStream } from '@/stream.js';
|
||||
import * as sound from '@/scripts/sound.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 { makeHotkey } from '@/scripts/hotkey.js';
|
||||
import { reactionPicker } from '@/scripts/reaction-picker.js';
|
||||
|
@ -235,12 +235,10 @@ export async function mainBoot() {
|
|||
}
|
||||
}
|
||||
|
||||
fetchInstance().then(() => {
|
||||
const modifiedVersionMustProminentlyOfferInAgplV3Section13Read = miLocalStorage.getItem('modifiedVersionMustProminentlyOfferInAgplV3Section13Read');
|
||||
if (modifiedVersionMustProminentlyOfferInAgplV3Section13Read !== 'true' && instance.repositoryUrl !== 'https://github.com/misskey-dev/misskey') {
|
||||
popup(defineAsyncComponent(() => import('@/components/MkSourceCodeAvailablePopup.vue')), {}, {}, 'closed');
|
||||
}
|
||||
});
|
||||
const modifiedVersionMustProminentlyOfferInAgplV3Section13Read = miLocalStorage.getItem('modifiedVersionMustProminentlyOfferInAgplV3Section13Read');
|
||||
if (modifiedVersionMustProminentlyOfferInAgplV3Section13Read !== 'true' && instance.repositoryUrl !== 'https://github.com/misskey-dev/misskey') {
|
||||
popup(defineAsyncComponent(() => import('@/components/MkSourceCodeAvailablePopup.vue')), {}, {}, 'closed');
|
||||
}
|
||||
|
||||
if ('Notification' in window) {
|
||||
// 許可を得ていなかったらリクエスト
|
||||
|
|
|
@ -57,18 +57,7 @@ import { i18n } from '@/i18n.js';
|
|||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { customEmojis } from '@/custom-emojis.js';
|
||||
import { MFM_TAGS, MFM_PARAMS } from '@/const.js';
|
||||
|
||||
type EmojiDef = {
|
||||
emoji: string;
|
||||
name: string;
|
||||
url: string;
|
||||
aliasOf?: string;
|
||||
} | {
|
||||
emoji: string;
|
||||
name: string;
|
||||
aliasOf?: string;
|
||||
isCustomEmoji?: true;
|
||||
};
|
||||
import { searchEmoji, EmojiDef } from '@/scripts/search-emoji.js';
|
||||
|
||||
const lib = emojilist.filter(x => x.category !== 'flags');
|
||||
|
||||
|
@ -249,7 +238,7 @@ function exec() {
|
|||
return;
|
||||
}
|
||||
|
||||
emojis.value = emojiAutoComplete(props.q, emojiDb.value);
|
||||
emojis.value = searchEmoji(props.q, emojiDb.value);
|
||||
} else if (props.type === 'mfmTag') {
|
||||
if (!props.q || props.q === '') {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
// ヒットしたものを全部追加すると雑多になるので、先頭の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);
|
||||
}
|
||||
|
||||
function onMousedown(event: Event) {
|
||||
if (!contains(rootEl.value, event.target) && (rootEl.value !== event.target)) props.close();
|
||||
}
|
||||
|
|
|
@ -240,7 +240,7 @@ const render = () => {
|
|||
},
|
||||
external: externalTooltipHandler,
|
||||
callbacks: {
|
||||
label: (item) => chartData?.bytes ? bytes(item.parsed.y * 1000, 1) : item.parsed.y.toString(),
|
||||
label: (item) => `${item.dataset.label}: ${chartData?.bytes ? bytes(item.parsed.y * 1000, 1) : item.parsed.y.toString()}`,
|
||||
},
|
||||
},
|
||||
zoom: props.detailed ? {
|
||||
|
|
|
@ -52,7 +52,7 @@ async function fetchLanguage(to: string): Promise<void> {
|
|||
return bundle.id === language || bundle.aliases?.includes(language);
|
||||
});
|
||||
if (bundles.length > 0) {
|
||||
console.log(`Loading language: ${language}`);
|
||||
if (_DEV_) console.log(`Loading language: ${language}`);
|
||||
await highlighter.loadLanguage(bundles[0].import);
|
||||
codeLang.value = language;
|
||||
} else {
|
||||
|
|
|
@ -16,6 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:key="emoji"
|
||||
:data-emoji="emoji"
|
||||
class="_button item"
|
||||
:disabled="disabledEmojis?.value.includes(emoji)"
|
||||
@pointerenter="computeButtonTitle"
|
||||
@click="emit('chosen', emoji, $event)"
|
||||
>
|
||||
|
@ -48,6 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:key="emoji"
|
||||
:data-emoji="emoji"
|
||||
class="_button item"
|
||||
:disabled="disabledEmojis?.value.includes(emoji)"
|
||||
@pointerenter="computeButtonTitle"
|
||||
@click="emit('chosen', emoji, $event)"
|
||||
>
|
||||
|
@ -67,6 +69,7 @@ import MkEmojiPickerSection from '@/components/MkEmojiPicker.section.vue';
|
|||
|
||||
const props = defineProps<{
|
||||
emojis: string[] | Ref<string[]>;
|
||||
disabledEmojis?: Ref<string[]>;
|
||||
initialShown?: boolean;
|
||||
hasChildSection?: boolean;
|
||||
customEmojiTree?: CustomEmojiFolderTree[];
|
||||
|
|
|
@ -14,6 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
v-for="emoji in searchResultCustom"
|
||||
:key="emoji.name"
|
||||
class="_button item"
|
||||
:disabled="!canReact(emoji)"
|
||||
:title="emoji.name"
|
||||
tabindex="0"
|
||||
@click="chosen(emoji, $event)"
|
||||
|
@ -39,16 +40,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<section v-if="showPinned && (pinned && pinned.length > 0)">
|
||||
<div class="body">
|
||||
<button
|
||||
v-for="emoji in pinned"
|
||||
:key="emoji"
|
||||
:data-emoji="emoji"
|
||||
v-for="emoji in pinnedEmojisDef"
|
||||
:key="getKey(emoji)"
|
||||
:data-emoji="getKey(emoji)"
|
||||
class="_button item"
|
||||
:disabled="!canReact(emoji)"
|
||||
tabindex="0"
|
||||
@pointerenter="computeButtonTitle"
|
||||
@click="chosen(emoji, $event)"
|
||||
>
|
||||
<MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true"/>
|
||||
<MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/>
|
||||
<MkCustomEmoji v-if="!emoji.hasOwnProperty('char')" class="emoji" :name="getKey(emoji)" :normal="true"/>
|
||||
<MkEmoji v-else class="emoji" :emoji="getKey(emoji)" :normal="true"/>
|
||||
</button>
|
||||
</div>
|
||||
</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>
|
||||
<div class="body">
|
||||
<button
|
||||
v-for="emoji in recentlyUsedEmojis"
|
||||
:key="emoji"
|
||||
v-for="emoji in recentlyUsedEmojisDef"
|
||||
:key="getKey(emoji)"
|
||||
class="_button item"
|
||||
:data-emoji="emoji"
|
||||
:disabled="!canReact(emoji)"
|
||||
:data-emoji="getKey(emoji)"
|
||||
@pointerenter="computeButtonTitle"
|
||||
@click="chosen(emoji, $event)"
|
||||
>
|
||||
<MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true"/>
|
||||
<MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/>
|
||||
<MkCustomEmoji v-if="!emoji.hasOwnProperty('char')" class="emoji" :name="getKey(emoji)" :normal="true"/>
|
||||
<MkEmoji v-else class="emoji" :emoji="getKey(emoji)" :normal="true"/>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -76,7 +79,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
v-for="child in customEmojiFolderRoot.children"
|
||||
:key="`custom:${child.value}`"
|
||||
:initialShown="false"
|
||||
:emojis="computed(() => customEmojis.filter(e => child.value === '' ? (e.category === 'null' || !e.category) : e.category === child.value).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"
|
||||
:customEmojiTree="child.children"
|
||||
@chosen="chosen"
|
||||
|
@ -104,6 +108,7 @@ import * as Misskey from 'misskey-js';
|
|||
import XSection from '@/components/MkEmojiPicker.section.vue';
|
||||
import {
|
||||
emojilist,
|
||||
unicodeEmojisMap,
|
||||
emojiCharByCategory,
|
||||
UnicodeEmojiDef,
|
||||
unicodeEmojiCategories as categories,
|
||||
|
@ -146,6 +151,13 @@ const {
|
|||
recentlyUsedEmojis,
|
||||
} = defaultStore.reactiveState;
|
||||
|
||||
const recentlyUsedEmojisDef = computed(() => {
|
||||
return recentlyUsedEmojis.value.map(getDef);
|
||||
});
|
||||
const pinnedEmojisDef = computed(() => {
|
||||
return pinned.value?.map(getDef);
|
||||
});
|
||||
|
||||
const pinned = computed(() => props.pinnedEmojis);
|
||||
const size = computed(() => emojiPickerScale.value);
|
||||
const width = computed(() => emojiPickerWidth.value);
|
||||
|
@ -337,14 +349,18 @@ watch(q, () => {
|
|||
return matches;
|
||||
};
|
||||
|
||||
searchResultCustom.value = Array.from(searchCustom()).filter(filterAvailable);
|
||||
searchResultCustom.value = Array.from(searchCustom());
|
||||
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);
|
||||
}
|
||||
|
||||
function filterCategory(emoji: Misskey.entities.EmojiSimple, category: string): boolean {
|
||||
return category === '' ? (emoji.category === 'null' || !emoji.category) : emoji.category === category;
|
||||
}
|
||||
|
||||
function focus() {
|
||||
if (!['smartphone', 'tablet'].includes(deviceKind) && !isTouchUsing) {
|
||||
searchEl.value?.focus({
|
||||
|
@ -362,6 +378,14 @@ function getKey(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef):
|
|||
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 */
|
||||
function computeButtonTitle(ev: MouseEvent): void {
|
||||
const elm = ev.target as HTMLElement;
|
||||
|
@ -526,6 +550,18 @@ defineExpose({
|
|||
width: auto;
|
||||
height: auto;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -548,6 +584,18 @@ defineExpose({
|
|||
width: auto;
|
||||
height: auto;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -663,6 +711,18 @@ defineExpose({
|
|||
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 {
|
||||
height: 1.25em;
|
||||
vertical-align: -.25em;
|
||||
|
|
|
@ -152,11 +152,11 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent): void {
|
|||
icon: 'ti ti-crop',
|
||||
action: () : void => { crop(file); },
|
||||
}] : [], {
|
||||
type: 'divider',
|
||||
}, {
|
||||
text: i18n.ts.attachCancel,
|
||||
icon: 'ti ti-circle-x',
|
||||
action: () => { detachMedia(file.id); },
|
||||
}, {
|
||||
type: 'divider',
|
||||
}, {
|
||||
text: i18n.ts.deleteFile,
|
||||
icon: 'ti ti-trash',
|
||||
|
|
|
@ -33,7 +33,8 @@ import { defaultStore } from '@/store.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import * as sound from '@/scripts/sound.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<{
|
||||
reaction: string;
|
||||
|
@ -50,13 +51,11 @@ const emit = defineEmits<{
|
|||
|
||||
const buttonEl = shallowRef<HTMLElement>();
|
||||
|
||||
const isCustomEmoji = computed(() => props.reaction.includes(':'));
|
||||
const emoji = computed(() => isCustomEmoji.value ? customEmojis.value.find(emoji => emoji.name === props.reaction.replace(/:/g, '').replace(/@\./, '')) : null);
|
||||
const emojiName = computed(() => props.reaction.replace(/:/g, '').replace(/@\./, ''));
|
||||
const emoji = computed(() => customEmojisMap.get(emojiName.value) ?? unicodeEmojisMap.get(props.reaction));
|
||||
|
||||
const canToggle = computed(() => {
|
||||
return !props.reaction.match(/@\w/) && $i
|
||||
&& (emoji.value && checkReactionPermissions($i, props.note, emoji.value))
|
||||
|| !isCustomEmoji.value;
|
||||
return !props.reaction.match(/@\w/) && $i && emoji.value && checkReactionPermissions($i, props.note, emoji.value);
|
||||
});
|
||||
const canGetInfo = computed(() => !props.reaction.match(/@\w/) && props.reaction.includes(':'));
|
||||
|
||||
|
|
|
@ -32,7 +32,8 @@ export const Default = {
|
|||
async play({ canvasElement }) {
|
||||
const canvas = within(canvasElement);
|
||||
const a = canvas.getByRole<HTMLAnchorElement>('link');
|
||||
await expect(a.href).toMatch(/^https?:\/\/.*#test$/);
|
||||
// FIXME: 通るけどその後落ちるのでコメントアウト
|
||||
// await expect(a.href).toMatch(/^https?:\/\/.*#test$/);
|
||||
await userEvent.pointer({ keys: '[MouseRight]', target: a });
|
||||
await tick();
|
||||
const menu = canvas.getByRole('menu');
|
||||
|
@ -44,6 +45,7 @@ export const Default = {
|
|||
},
|
||||
args: {
|
||||
to: '#test',
|
||||
behavior: 'browser',
|
||||
},
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
|
|
|
@ -10,7 +10,7 @@ import MkTime from './MkTime.vue';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { dateTimeFormat } from '@/scripts/intl-const.js';
|
||||
const now = new Date('2023-04-01T00:00:00.000Z');
|
||||
const future = new Date(8640000000000000);
|
||||
const future = new Date('2024-04-01T00:00:00.000Z');
|
||||
const oneHourAgo = new Date(now.getTime() - 3600000);
|
||||
const oneDayAgo = new Date(now.getTime() - 86400000);
|
||||
const oneWeekAgo = new Date(now.getTime() - 604800000);
|
||||
|
@ -49,11 +49,12 @@ export const Empty = {
|
|||
export const RelativeFuture = {
|
||||
...Empty,
|
||||
async play({ canvasElement }) {
|
||||
await expect(canvasElement).toHaveTextContent(i18n.ts._ago.future);
|
||||
await expect(canvasElement).toHaveTextContent(i18n.tsx._timeIn.years({ n: 1 })); // n (1) = future (2024) - now (2023)
|
||||
},
|
||||
args: {
|
||||
...Empty.args,
|
||||
time: future,
|
||||
origin: now,
|
||||
},
|
||||
} satisfies StoryObj<typeof MkTime>;
|
||||
export const AbsoluteFuture = {
|
||||
|
|
|
@ -99,7 +99,6 @@ export class UserPreview {
|
|||
this.el.removeEventListener('mouseover', this.onMouseover);
|
||||
this.el.removeEventListener('mouseleave', this.onMouseleave);
|
||||
this.el.removeEventListener('click', this.onClick);
|
||||
window.clearInterval(this.checkTimer);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,13 +11,24 @@ import { DEFAULT_INFO_IMAGE_URL, DEFAULT_NOT_FOUND_IMAGE_URL, DEFAULT_SERVER_ERR
|
|||
|
||||
// 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 && providedMetaEl.textContent ? JSON.parse(providedMetaEl.textContent) : null;
|
||||
const providedAt = providedMetaEl && 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をリアクティブにするかは再考の余地あり
|
||||
|
||||
export const instance: Misskey.entities.MetaResponse = reactive(cached ? JSON.parse(cached) : {
|
||||
// TODO: set default values
|
||||
});
|
||||
export const instance: Misskey.entities.MetaResponse = reactive(cachedMeta ?? {});
|
||||
|
||||
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 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', {
|
||||
detail: false,
|
||||
});
|
||||
|
@ -35,4 +54,5 @@ export async function fetchInstance() {
|
|||
}
|
||||
|
||||
miLocalStorage.setItem('instance', JSON.stringify(instance));
|
||||
miLocalStorage.setItem('instanceCachedAt', Date.now().toString());
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ type Keys =
|
|||
'v' |
|
||||
'lastVersion' |
|
||||
'instance' |
|
||||
'instanceCachedAt' |
|
||||
'account' |
|
||||
'accounts' |
|
||||
'latestDonationInfoShownAt' |
|
||||
|
|
|
@ -142,7 +142,7 @@ function save() {
|
|||
turnstileSiteKey: turnstileSiteKey.value,
|
||||
turnstileSecretKey: turnstileSecretKey.value,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
fetchInstance(true);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -169,7 +169,7 @@ function save() {
|
|||
feedbackUrl: feedbackUrl.value === '' ? null : feedbackUrl.value,
|
||||
manifestJsonOverride: manifestJsonOverride.value === '' ? '{}' : JSON.stringify(JSON5.parse(manifestJsonOverride.value)),
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
fetchInstance(true);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -124,7 +124,7 @@ function save() {
|
|||
smtpUser: smtpUser.value,
|
||||
smtpPass: smtpPass.value,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
fetchInstance(true);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -61,7 +61,7 @@ function save() {
|
|||
deeplAuthKey: deeplAuthKey.value,
|
||||
deeplIsPro: deeplIsPro.value,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
fetchInstance(true);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -50,7 +50,7 @@ function save() {
|
|||
silencedHosts: silencedHosts.value.split('\n') || [],
|
||||
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
fetchInstance(true);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -110,7 +110,7 @@ function save() {
|
|||
hiddenTags: hiddenTags.value.split('\n'),
|
||||
preservedUsernames: preservedUsernames.value.split('\n'),
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
fetchInstance(true);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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"/>
|
||||
</div>
|
||||
</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>
|
||||
<summary>raw</summary>
|
||||
|
|
|
@ -54,8 +54,6 @@ const pagination = {
|
|||
})),
|
||||
};
|
||||
|
||||
console.log(Misskey);
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
|
|
@ -143,7 +143,7 @@ function save() {
|
|||
objectStorageSetPublicRead: objectStorageSetPublicRead.value,
|
||||
objectStorageS3ForcePathStyle: objectStorageS3ForcePathStyle.value,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
fetchInstance(true);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -73,7 +73,7 @@ function save() {
|
|||
enableChartsForRemoteUser: enableChartsForRemoteUser.value,
|
||||
enableChartsForFederatedInstances: enableChartsForFederatedInstances.value,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
fetchInstance(true);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ function save() {
|
|||
os.apiWithDialog('admin/update-meta', {
|
||||
proxyAccountId: proxyAccountId.value,
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
fetchInstance(true);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -196,7 +196,7 @@ async function init() {
|
|||
enableTruemailApi.value = meta.enableTruemailApi;
|
||||
truemailInstance.value = meta.truemailInstance;
|
||||
truemailAuthKey.value = meta.truemailAuthKey;
|
||||
bannedEmailDomains.value = meta.bannedEmailDomains?.join('\n') || "";
|
||||
bannedEmailDomains.value = meta.bannedEmailDomains?.join('\n') || '';
|
||||
}
|
||||
|
||||
function save() {
|
||||
|
@ -221,7 +221,7 @@ function save() {
|
|||
truemailAuthKey: truemailAuthKey.value,
|
||||
bannedEmailDomains: bannedEmailDomains.value.split('\n'),
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
fetchInstance(true);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -58,7 +58,7 @@ const save = async () => {
|
|||
await os.apiWithDialog('admin/update-meta', {
|
||||
serverRules: serverRules.value,
|
||||
});
|
||||
fetchInstance();
|
||||
fetchInstance(true);
|
||||
};
|
||||
|
||||
const remove = (index: number): void => {
|
||||
|
|
|
@ -243,7 +243,7 @@ async function save(): void {
|
|||
notesPerOneAd: notesPerOneAd.value,
|
||||
});
|
||||
|
||||
fetchInstance();
|
||||
fetchInstance(true);
|
||||
}
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
|
|
@ -7,9 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkSpacer :contentMax="800">
|
||||
<div :class="$style.root">
|
||||
<div v-if="!gameLoaded" :class="$style.loadingScreen">
|
||||
<div>
|
||||
Loading...
|
||||
</div>
|
||||
<div>{{ i18n.ts.loading }}<MkEllipsis/></div>
|
||||
</div>
|
||||
<!-- ↓に対してTransitionコンポーネントを使うと何故かkeyを指定していてもキャッシュが効かず様々なコンポーネントが都度再評価されてパフォーマンスが低下する -->
|
||||
<div v-show="gameLoaded" class="_gaps_s">
|
||||
|
@ -32,18 +30,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</Transition>
|
||||
|
||||
<div :class="$style.header">
|
||||
<div :class="[$style.frame, $style.headerTitle]">
|
||||
<div :class="$style.frameInner">
|
||||
<b>BUBBLE GAME</b>
|
||||
<div>- {{ gameMode }} -</div>
|
||||
<div class="_woodenFrame" :class="[$style.headerTitle]">
|
||||
<div class="_woodenFrameInner">
|
||||
<b>{{ i18n.ts.bubbleGame }}</b>
|
||||
<div>- {{ gameMode.toUpperCase() }} -</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="[$style.frame, $style.frameH]">
|
||||
<div :class="$style.frameInner">
|
||||
<MkButton inline small @click="hold">HOLD</MkButton>
|
||||
<div class="_woodenFrame _woodenFrameH">
|
||||
<div class="_woodenFrameInner">
|
||||
<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;"/>
|
||||
</div>
|
||||
<div :class="[$style.frameInner, $style.stock]" style="text-align: center;">
|
||||
<div class="_woodenFrameInner" :class="$style.stock" style="text-align: center;">
|
||||
<TransitionGroup
|
||||
:enterActiveClass="$style.transition_stock_enterActive"
|
||||
: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 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;"/>
|
||||
<div>SCORE: <MkNumber :value="score"/>{{ getScoreUnit(gameMode) }}</div>
|
||||
<div>MAX CHAIN: <MkNumber :value="maxCombo"/></div>
|
||||
<div v-if="gameMode === 'yen'">TOTAL EARNINGS: <b><MkNumber :value="yenTotal ?? score"/>円</b></div>
|
||||
<div v-if="gameMode === 'sweets'"><b>おにぎり<MkNumber :value="score / 130"/>個分</b></div>
|
||||
<div>{{ i18n.ts._bubbleGame._score.score }}: <MkNumber :value="score"/>{{ getScoreUnit(gameMode) }}</div>
|
||||
<div>{{ i18n.ts._bubbleGame._score.maxChain }}: <MkNumber :value="maxCombo"/></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>
|
||||
<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 v-if="replaying" :class="$style.replayIndicator"><span :class="$style.replayIndicatorText"><i class="ti ti-player-play"></i> {{ i18n.ts.replaying }}</span></div>
|
||||
</div>
|
||||
|
||||
<div v-if="replaying" :class="$style.frame">
|
||||
<div :class="$style.frameInner">
|
||||
<div v-if="replaying" class="_woodenFrame">
|
||||
<div class="_woodenFrameInner">
|
||||
<div style="background: #0004;">
|
||||
<div style="height: 10px; background: var(--accent); will-change: width;" :style="{ width: `${(currentFrame / endedAtFrame) * 100}%` }"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.frameInner">
|
||||
<div class="_woodenFrameInner">
|
||||
<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 === 16" @click="replayPlaybackRate = replayPlaybackRate === 16 ? 1 : 16"><i class="ti ti-player-track-next"></i> x16</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="isGameOver" :class="$style.frame">
|
||||
<div :class="$style.frameInner">
|
||||
<div v-if="isGameOver" class="_woodenFrame">
|
||||
<div class="_woodenFrameInner">
|
||||
<div class="_buttonsCenter">
|
||||
<MkButton primary rounded @click="backToTitle">{{ i18n.ts.backToTitle }}</MkButton>
|
||||
<MkButton primary rounded @click="replay">{{ i18n.ts.showReplay }}</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 style="display: flex;">
|
||||
<div :class="$style.frame" style="flex: 1; margin-right: 10px;">
|
||||
<div :class="$style.frameInner">
|
||||
<div>SCORE: <b><MkNumber :value="score"/>{{ getScoreUnit(gameMode) }}</b></div>
|
||||
<div>HIGH SCORE: <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 class="_woodenFrame" style="flex: 1; margin-right: 10px;">
|
||||
<div class="_woodenFrameInner">
|
||||
<div>{{ i18n.ts._bubbleGame._score.score }}: <MkNumber :value="score"/>{{ getScoreUnit(gameMode) }}</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'">
|
||||
{{ 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 :class="[$style.frame]" style="margin-left: auto;">
|
||||
<div :class="$style.frameInner" style="text-align: center;">
|
||||
<div class="_woodenFrame" style="margin-left: auto;">
|
||||
<div class="_woodenFrameInner" style="text-align: center;">
|
||||
<div @click="showConfig = !showConfig"><i class="ti ti-settings"></i></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="showConfig" :class="$style.frame">
|
||||
<div :class="$style.frameInner">
|
||||
<div v-if="showConfig" class="_woodenFrame">
|
||||
<div class="_woodenFrameInner">
|
||||
<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)">
|
||||
<template #label>BGM {{ i18n.ts.volume }}</template>
|
||||
|
@ -153,8 +167,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="$style.frame">
|
||||
<div :class="$style.frameInner">
|
||||
<div class="_woodenFrame">
|
||||
<div class="_woodenFrameInner">
|
||||
<div>FUSION RECIPE</div>
|
||||
<div>
|
||||
<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 :class="$style.frame">
|
||||
<div :class="$style.frameInner">
|
||||
<MkButton v-if="!isGameOver && !replaying" full danger @click="surrender">Surrender</MkButton>
|
||||
<MkButton v-else full @click="restart">Retry</MkButton>
|
||||
<div class="_woodenFrame">
|
||||
<div class="_woodenFrameInner">
|
||||
<MkButton v-if="!isGameOver && !replaying" full danger @click="surrender">{{ i18n.ts.surrender }}</MkButton>
|
||||
<MkButton v-else full @click="restart">{{ i18n.ts.gameRetry }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1313,38 +1327,6 @@ definePageMetadata(() => ({
|
|||
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 {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
|
|
|
@ -15,13 +15,13 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkSpacer v-if="!gameStarted" :contentMax="800">
|
||||
<div :class="$style.root">
|
||||
<div class="_gaps">
|
||||
<div :class="$style.frame" style="text-align: center;">
|
||||
<div :class="$style.frameInner">
|
||||
<div class="_woodenFrame" style="text-align: center;">
|
||||
<div class="_woodenFrameInner">
|
||||
<img src="/client-assets/drop-and-fusion/logo.png" style="display: block; max-width: 100%; max-height: 200px; margin: auto;"/>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.frame" style="text-align: center;">
|
||||
<div :class="$style.frameInner">
|
||||
<div class="_woodenFrame" style="text-align: center;">
|
||||
<div class="_woodenFrameInner">
|
||||
<div class="_gaps" style="padding: 16px;">
|
||||
<MkSelect v-model="gameMode">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.frameInner">
|
||||
<div class="_woodenFrameInner">
|
||||
<div class="_gaps" style="padding: 16px;">
|
||||
<div style="font-size: 90%;"><i class="ti ti-music"></i> {{ i18n.ts.soundWillBePlayed }}</div>
|
||||
<MkSwitch v-model="mute">
|
||||
|
@ -42,10 +42,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.frame">
|
||||
<div :class="$style.frameInner">
|
||||
<div class="_woodenFrame">
|
||||
<div class="_woodenFrameInner">
|
||||
<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-for="r in ranking" :key="r.id" :class="$style.rankingRecord">
|
||||
<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 :class="$style.frame">
|
||||
<div :class="$style.frameInner" style="padding: 16px;">
|
||||
<div class="_woodenFrame">
|
||||
<div class="_woodenFrameInner" style="padding: 16px;">
|
||||
<div style="font-weight: bold;">{{ i18n.ts._bubbleGame.howToPlay }}</div>
|
||||
<ol>
|
||||
<li>{{ i18n.ts._bubbleGame._howToPlay.section1 }}</li>
|
||||
|
@ -67,8 +67,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.frame">
|
||||
<div :class="$style.frameInner">
|
||||
<div class="_woodenFrame">
|
||||
<div class="_woodenFrameInner">
|
||||
<div class="_gaps_s" style="padding: 16px;">
|
||||
<div><b>Credit</b></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 {
|
||||
display: flex;
|
||||
line-height: 24px;
|
||||
|
|
|
@ -39,6 +39,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<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>
|
||||
<MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton>
|
||||
<MkTextarea v-model="moderationNote" manualSave>
|
||||
<template #label>{{ i18n.ts.moderationNote }}</template>
|
||||
</MkTextarea>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
|
@ -119,7 +122,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkChart from '@/components/MkChart.vue';
|
||||
import MkObjectView from '@/components/MkObjectView.vue';
|
||||
|
@ -141,6 +144,7 @@ import MkPagination from '@/components/MkPagination.vue';
|
|||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
|
||||
import { dateString } from '@/filters/date.js';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
host: string;
|
||||
|
@ -155,6 +159,7 @@ const suspended = ref(false);
|
|||
const isBlocked = ref(false);
|
||||
const isSilenced = ref(false);
|
||||
const faviconUrl = ref<string | null>(null);
|
||||
const moderationNote = ref('');
|
||||
|
||||
const usersPagination = {
|
||||
endpoint: iAmModerator ? 'admin/show-users' : 'users' as const,
|
||||
|
@ -167,6 +172,10 @@ const usersPagination = {
|
|||
offsetMode: true,
|
||||
};
|
||||
|
||||
watch(moderationNote, async () => {
|
||||
await misskeyApi('admin/federation/update-instance', { host: instance.value.host, moderationNote: moderationNote.value });
|
||||
});
|
||||
|
||||
async function fetch(): Promise<void> {
|
||||
if (iAmAdmin) {
|
||||
meta.value = await misskeyApi('admin/meta');
|
||||
|
@ -178,6 +187,7 @@ async function fetch(): Promise<void> {
|
|||
isBlocked.value = instance.value?.isBlocked ?? false;
|
||||
isSilenced.value = instance.value?.isSilenced ?? false;
|
||||
faviconUrl.value = getProxiedImageUrlNullable(instance.value?.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.value?.iconUrl, 'preview');
|
||||
moderationNote.value = instance.value?.moderationNote;
|
||||
}
|
||||
|
||||
async function toggleBlock(): Promise<void> {
|
||||
|
|
|
@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div :class="$style.board">
|
||||
<div class="_woodenFrame">
|
||||
<div :class="$style.boardInner">
|
||||
<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>
|
||||
|
@ -124,8 +124,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkFolder>
|
||||
<template #label>{{ i18n.ts.options }}</template>
|
||||
<div class="_gaps_s" style="text-align: left;">
|
||||
<MkSwitch v-model="showBoardLabels">Show labels</MkSwitch>
|
||||
<MkSwitch v-model="useAvatarAsStone">useAvatarAsStone</MkSwitch>
|
||||
<MkSwitch v-model="showBoardLabels">{{ i18n.ts._reversi.showBoardLabels }}</MkSwitch>
|
||||
<MkSwitch v-model="useAvatarAsStone">{{ i18n.ts._reversi.useAvatarAsStone }}</MkSwitch>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
|
@ -248,7 +248,7 @@ if (game.value.isStarted && !game.value.isEnded) {
|
|||
crc32: crc32.toString(),
|
||||
}).then((res) => {
|
||||
if (res.desynced) {
|
||||
console.log('resynced');
|
||||
if (_DEV_) console.log('resynced');
|
||||
restoreGame(res.game!);
|
||||
}
|
||||
});
|
||||
|
@ -500,17 +500,6 @@ $gap: 4px;
|
|||
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 {
|
||||
padding: 32px;
|
||||
|
||||
|
|
|
@ -93,9 +93,11 @@ export class Autocomplete {
|
|||
return;
|
||||
}
|
||||
|
||||
const afterLastMfmParam = text.split(/\$\[[a-zA-Z]+/).pop();
|
||||
|
||||
const isMention = mentionIndex !== -1;
|
||||
const isHashtag = hashtagIndex !== -1;
|
||||
const isMfmParam = mfmParamIndex !== -1 && text.split(/\$\[[a-zA-Z]+/).pop()?.includes('.');
|
||||
const isMfmParam = mfmParamIndex !== -1 && afterLastMfmParam?.includes('.') && !afterLastMfmParam?.includes(' ');
|
||||
const isMfmTag = mfmTagIndex !== -1 && !isMfmParam;
|
||||
const isEmoji = emojiIndex !== -1 && text.split(/:[a-z0-9_+\-]+:/).pop()!.includes(':');
|
||||
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
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 ?? [];
|
||||
return !(emoji.localOnly && note.user.host !== me.host)
|
||||
&& !(emoji.isSensitive && (note.reactionAcceptance === 'nonSensitiveOnly' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote'))
|
||||
|
|
|
@ -2,14 +2,18 @@ import { unisonReload } from '@/scripts/unison-reload.js';
|
|||
import * as os from '@/os.js';
|
||||
import { miLocalStorage } from '@/local-storage.js';
|
||||
import { fetchCustomEmojis } from '@/custom-emojis.js';
|
||||
import { fetchInstance } from '@/instance.js';
|
||||
|
||||
export async function clearCache() {
|
||||
os.waiting();
|
||||
miLocalStorage.removeItem('instance');
|
||||
miLocalStorage.removeItem('instanceCachedAt');
|
||||
miLocalStorage.removeItem('locale');
|
||||
miLocalStorage.removeItem('localeVersion');
|
||||
miLocalStorage.removeItem('theme');
|
||||
miLocalStorage.removeItem('emojis');
|
||||
miLocalStorage.removeItem('lastEmojisFetchedAt');
|
||||
await fetchInstance(true);
|
||||
await fetchCustomEmojis(true);
|
||||
unisonReload();
|
||||
}
|
||||
|
|
|
@ -20,6 +20,10 @@ export const emojilist: UnicodeEmojiDef[] = _emojilist.map(x => ({
|
|||
category: unicodeEmojiCategories[x[2]],
|
||||
}));
|
||||
|
||||
export const unicodeEmojisMap = new Map<string, UnicodeEmojiDef>(
|
||||
emojilist.map(x => [x.char, x])
|
||||
);
|
||||
|
||||
const _indexByChar = new Map<string, number>();
|
||||
const _charGroupByCategory = new Map<string, string[]>();
|
||||
for (let i = 0; i < emojilist.length; i++) {
|
||||
|
|
101
packages/frontend/src/scripts/search-emoji.ts
Normal file
101
packages/frontend/src/scripts/search-emoji.ts
Normal 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);
|
||||
}
|
|
@ -126,7 +126,7 @@ export async function loadAudio(url: string, options?: { useCache?: boolean; })
|
|||
*/
|
||||
export function playMisskeySfx(operationType: OperationType) {
|
||||
const sound = defaultStore.state[`sound_${operationType}`];
|
||||
if (sound.type == null || !canPlay) return;
|
||||
if (sound.type == null || !canPlay || ('userActivation' in navigator && !navigator.userActivation.hasBeenActive)) return;
|
||||
|
||||
canPlay = false;
|
||||
playMisskeySfxFile(sound).finally(() => {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue