Compare commits

...

36 Commits

Author SHA1 Message Date
Acid Chicken (硫酸鶏)
d02198417b chore(backend): polyfill position 2024-06-01 16:51:07 +09:00
Acid Chicken (硫酸鶏)
9366515565 test(backend): polyfill position 2024-06-01 16:39:34 +09:00
Acid Chicken (硫酸鶏)
623eb410bb test(backend): polyfill erm 2024-06-01 16:28:07 +09:00
Acid Chicken (硫酸鶏)
b233444ed6 fix(backend): explicitly set query runner on insertOne 2024-06-01 16:12:02 +09:00
Acid Chicken
2b8056a852 fix(backend): use insertOne insteadof insert/findOneOrFail combination (#13908)
* fix(backend): use insertOne insteadof insert/findOneOrFail combination

* fix: typo

* fix(backend): inherit mainAlias?

* refactor(backend): use extend

* fix(backend): invalid entityTarget

* fix(backend): fake where

* chore: debug

* chore: debug

* test: log

* fix(backend): column names

* fix(backend): remove dummy from

* revert: log

* fix(backend): position

* fix(backend): automatic aliasing

* chore(backend): alias

* chore(backend): remove from

* fix(backend): type

* fix(backend): avoid pure name

* test(backend): fix type

* chore(backend): use cte

* fix(backend): avoid useless alias

* fix(backend): fix typo

* fix(backend): __disambiguation__

* fix(backend): quote

* chore(backend): t

* chore(backend): accessible

* chore(backend): concrete returning

* fix(backend): quote

* chore: log more

* chore: log metadata

* chore(backend): use raw

* fix(backend): returning column name

* fix(backend): transform

* build(backend): wanna logging

* build(backend): transform empty

* build(backend): build alias

* build(backend): restore name

* chore: return entity

* fix: test case

* test(backend): 204

* chore(backend): log sql

* chore(backend): assert user joined

* fix(backend): typo

* chore(backend): log long sql

* chore(backend): log join

* chore(backend): log join depth null

* chore(backend): joinAttributes

* chore(backend): override createJoinExpression

* chore: join log

* fix(backend): escape

* test(backend): log log

* chore(backend): join gonna success?

* chore(backend): relations

* chore(backend): undefined

* chore(backend): target

* chore(backend): remove log

* chore(backend): log chart update

* chore(backend): log columns

* chore(backend): check hasMetadata

* chore(backend): unshift id when not included

* chore(backend): missing select

* chore(backend): remove debug code
2024-06-01 11:16:44 +09:00
github-actions[bot]
ecf7945fe8 [skip ci] Update CHANGELOG.md (prepend template) 2024-05-31 12:25:00 +00:00
github-actions[bot]
cc1ee0106f Merge pull request #13801 from misskey-dev/release/2024.5.0
Release: 2024.5.0
2024-05-31 12:24:57 +00:00
github-actions[bot]
6078081c33 [skip ci] Release: 2024.5.0 2024-05-31 12:24:53 +00:00
github-actions[bot]
a59aa20be8 Bump version to 2024.5.0-rc.13 2024-05-31 12:18:52 +00:00
syuilo
61eec93f4e Revert "2024.5.0"
This reverts commit 27d1b7e615.
2024-05-31 21:16:35 +09:00
syuilo
27d1b7e615 2024.5.0 2024-05-31 21:09:19 +09:00
github-actions[bot]
316d192bc0 Bump version to 2024.5.0-rc.12 2024-05-31 12:05:47 +00:00
syuilo
2eaa3e256f Update README.md for Sentry 2024-05-31 20:42:02 +09:00
github-actions[bot]
46164f879b Bump version to 2024.5.0-rc.11 2024-05-31 11:20:13 +00:00
github-actions[bot]
374c8791d7 Bump version to 2024.5.0-rc.10 2024-05-31 11:13:42 +00:00
syuilo
e8f523f00a Merge branch 'develop' into release/2024.5.0 2024-05-31 20:11:55 +09:00
syuilo
030082f756 🎨 2024-05-31 19:35:27 +09:00
github-actions[bot]
dc55adbaf7 Bump version to 2024.5.0-rc.9 2024-05-31 07:06:41 +00:00
syuilo
90ba1ca1f9 Merge branch 'develop' into release/2024.5.0 2024-05-31 16:06:00 +09:00
zyoshoka
514a65e453 perf(backend): avoid N+1 selects from user table when packing many entities (#13911)
* perf(backend): avoid N+1 selects from `user` table when packing many entities

* perf(backend): use `packMany` instead of mapping to `pack`
2024-05-31 15:32:42 +09:00
syuilo
a3468fd05b Merge branch 'develop' into release/2024.5.0 2024-05-31 14:59:32 +09:00
syuilo
97be1a53ad Update 1717117195275-inquiryUrl.js 2024-05-31 14:59:02 +09:00
github-actions[bot]
1e007b63aa Bump version to 2024.5.0-rc.8 2024-05-31 04:38:45 +00:00
syuilo
a0c596b030 Merge branch 'develop' into release/2024.5.0 2024-05-31 13:28:25 +09:00
syuilo
eaa85f5aa3 fix test 2024-05-31 13:28:11 +09:00
syuilo
dfeaa1145b Merge branch 'develop' into release/2024.5.0 2024-05-31 13:19:49 +09:00
syuilo
0082747237 New Crowdin updates (#13892)
* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Chinese Traditional)

* New translations ja-jp.yml (Chinese Simplified)

* New translations ja-jp.yml (Chinese Traditional)
2024-05-31 13:19:37 +09:00
syuilo
5b8f8e7087 fix(backend): fix backward compatibility of antenna 2024-05-31 11:24:17 +09:00
syuilo
be11fd7508 enhance: サーバーのお問い合わせ先URLを設定できるように 2024-05-31 10:12:23 +09:00
syuilo
ac4a001e9f fix code style 2024-05-31 10:11:11 +09:00
KanariKanaru
24d4124ffc fix(frontend): ノートにテキストがなくてもファイルが5つ以上あるときは折りたたむように (#13907)
* fix: ノートにテキストがなくてもファイルが5つ以上あるときは折りたたむように

* 冗長な記述を修正

* Update CHANGELOG.md
2024-05-30 17:36:58 +09:00
zyoshoka
eaadd643eb chore(misskey-js): fix repository and add license in package.json (#13902) 2024-05-29 20:57:48 +09:00
Kisaragi
cf670e8a3d refactor(backend): avoid as any on CustomEmojiService.ts (#13903) 2024-05-29 07:12:50 +09:00
Kisaragi
e57ce4fa0f chore(backend): rename local variable (#13904)
much -> matched
2024-05-29 07:12:20 +09:00
Kisaragi
44cafbb9f2 refactor: avoid as any[] on FetchInstanceMetadataService.ts (#13905)
* refactor: avoid `as any[]` on FetchInstanceMetadataService.ts

* apply suggestion

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2024-05-29 07:11:29 +09:00
github-actions[bot]
f75e46752e Bump version to 2024.5.0-rc.7 2024-05-28 09:18:21 +00:00
85 changed files with 832 additions and 395 deletions

View File

@@ -1,3 +1,15 @@
## Unreleased
### General
-
### Client
-
### Server
-
## 2024.5.0
### Note
@@ -20,6 +32,7 @@
- Enhance: Goneを出さずに終了したサーバーへの配信停止を自動的に行うように
- もしそのようなサーバーからから配信が届いた場合には自動的に配信を再開します
- Enhance: 配信停止の理由を表示するように
- Enhance: サーバーのお問い合わせ先URLを設定できるようになりました
- Fix: Play作成時に設定した公開範囲が機能していない問題を修正
- Fix: 正規化されていない状態のhashtagが連合されてきたhtmlに含まれているとhashtagが正しくhashtagに復元されない問題を修正
- Fix: みつけるのアンケート欄にてチャンネルのアンケートが含まれてしまう問題を修正
@@ -73,6 +86,7 @@
- Fix: 通知をグループ化している際に、人数が正常に表示されないことがある問題を修正
- Fix: 連合なしの状態の読み書きができない問題を修正
- Fix: `/share` で日本語等を含むurlがurlエンコードされない問題を修正
- Fix: ファイルを5つ以上添付してもテキストがないとートが折りたたまれない問題を修正
### Server
- Enhance: エンドポイント`antennas/update`の必須項目を`antennaId`のみに

View File

@@ -28,6 +28,10 @@
## Thanks
<a href="https://sentry.io/"><img src="https://github.com/misskey-dev/misskey/assets/4439005/98576556-222f-467a-94be-e98dbda1d852" height="30" alt="Sentry" /></a>
Thanks to [Sentry](https://sentry.io/) for providing the error tracking platform that helps us catch unexpected errors.
<a href="https://www.chromatic.com/"><img src="https://user-images.githubusercontent.com/321738/84662277-e3db4f80-af1b-11ea-88f5-91d67a5e59f6.png" height="30" alt="Chromatic" /></a>
Thanks to [Chromatic](https://www.chromatic.com/) for providing the visual testing platform that helps us review UI changes and catch visual regressions.

12
locales/index.d.ts vendored
View File

@@ -3364,6 +3364,10 @@ export interface Locale extends ILocale {
* 管理者情報が設定されていません。
*/
"noMaintainerInformationWarning": string;
/**
* 問い合わせ先URLが設定されていません。
*/
"noInquiryUrlWarning": string;
/**
* Botプロテクションが設定されていません。
*/
@@ -5471,6 +5475,14 @@ export interface Locale extends ILocale {
* 有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。
*/
"fanoutTimelineDbFallbackDescription": string;
/**
* 問い合わせ先URL
*/
"inquiryUrl": string;
/**
* サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。
*/
"inquiryUrlDescription": string;
};
"_accountMigration": {
/**

View File

@@ -837,6 +837,7 @@ administration: "管理"
accounts: "アカウント"
switch: "切り替え"
noMaintainerInformationWarning: "管理者情報が設定されていません。"
noInquiryUrlWarning: "問い合わせ先URLが設定されていません。"
noBotProtectionWarning: "Botプロテクションが設定されていません。"
configure: "設定する"
postToGallery: "ギャラリーへ投稿"
@@ -1383,6 +1384,8 @@ _serverSettings:
fanoutTimelineDescription: "有効にすると、各種タイムラインを取得する際のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。サーバーのメモリ容量が少ない場合、または動作が不安定な場合は無効にすることができます。"
fanoutTimelineDbFallback: "データベースへのフォールバック"
fanoutTimelineDbFallbackDescription: "有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。"
inquiryUrl: "問い合わせ先URL"
inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。"
_accountMigration:
moveFrom: "別のアカウントからこのアカウントに移行"

View File

@@ -316,6 +316,7 @@ selectFile: "选择文件"
selectFiles: "选择文件"
selectFolder: "选择文件夹"
selectFolders: "选择多个文件夹"
fileNotSelected: "未选择文件"
renameFile: "重命名文件"
folderName: "文件夹名称"
createFolder: "创建文件夹"
@@ -2358,6 +2359,7 @@ _deck:
alwaysShowMainColumn: "总是显示主列"
columnAlign: "列对齐"
addColumn: "添加列"
newNoteNotificationSettings: "新帖子通知设定"
configureColumn: "列设置"
swapLeft: "向左移动"
swapRight: "向右移动"

View File

@@ -316,6 +316,7 @@ selectFile: "選擇檔案"
selectFiles: "選擇檔案"
selectFolder: "選擇資料夾"
selectFolders: "選擇資料夾"
fileNotSelected: "尚未選擇檔案"
renameFile: "重新命名檔案"
folderName: "資料夾名稱"
createFolder: "新增資料夾"
@@ -471,7 +472,7 @@ retype: "重新輸入"
noteOf: "{user}的貼文"
quoteAttached: "引用"
quoteQuestion: "是否要引用?"
attachAsFileQuestion: "剪貼簿的文字較長。請問是否要改成附加檔案呢?"
attachAsFileQuestion: "剪貼簿的文字較長。請問是否要將其以文字檔的方式附加呢?"
noMessagesYet: "沒有訊息"
newMessageExists: "有新的訊息"
onlyOneFileCanBeAttached: "只能加入一個附件"
@@ -1025,6 +1026,7 @@ thisPostMayBeAnnoyingHome: "發佈到首頁"
thisPostMayBeAnnoyingCancel: "退出"
thisPostMayBeAnnoyingIgnore: "直接發佈貼文"
collapseRenotes: "省略顯示已看過的轉發貼文"
collapseRenotesDescription: "將已做過反應和轉發的貼文折疊顯示。"
internalServerError: "內部伺服器錯誤"
internalServerErrorDescription: "內部伺服器出現意外錯誤。"
copyErrorInfo: "複製錯誤資訊"
@@ -1241,8 +1243,8 @@ alwaysConfirmFollow: "點擊追隨時總是顯示確認訊息"
inquiry: "聯絡我們"
_delivery:
status: "傳送狀態"
stop: "已凍結"
resume: "繼續傳送"
stop: "停止傳送"
resume: "恢復傳送"
_type:
none: "直播中"
manuallySuspended: "手動暫停中"
@@ -1373,6 +1375,8 @@ _serverSettings:
fanoutTimelineDescription: "如果啟用的話檢索各個時間軸的性能會顯著提昇資料庫的負荷也會減少。不過Redis 的記憶體使用量會增加。如果伺服器的記憶體容量比較少或者運行不穩定,可以停用。"
fanoutTimelineDbFallback: "資料庫的回退"
fanoutTimelineDbFallbackDescription: "若啟用,在時間軸沒有快取的情況下將執行回退處理以額外查詢資料庫。若停用,可以透過不執行回退處理來進一步減少伺服器的負荷,但會限制可取得的時間軸範圍。"
inquiryUrl: "聯絡表單網址"
inquiryUrlDescription: "指定伺服器運營者的聯絡表單網址或包含運營者聯絡資訊網頁的網址。"
_accountMigration:
moveFrom: "從其他帳戶遷移到這個帳戶"
moveFromSub: "為另一個帳戶建立別名"
@@ -2358,6 +2362,7 @@ _deck:
alwaysShowMainColumn: "總是顯示主欄"
columnAlign: "對齊欄位"
addColumn: "新增欄位"
newNoteNotificationSettings: "新貼文通知的設定"
configureColumn: "欄位的設定"
swapLeft: "向左移動"
swapRight: "向右移動"

View File

@@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2024.5.0-rc.6",
"version": "2024.5.0",
"codename": "nasubi",
"repository": {
"type": "git",

View File

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

View File

@@ -67,7 +67,7 @@ export class AnnouncementService {
@bindThis
public async create(values: Partial<MiAnnouncement>, moderator?: MiUser): Promise<{ raw: MiAnnouncement; packed: Packed<'Announcement'> }> {
const announcement = await this.announcementsRepository.insert({
const announcement = await this.announcementsRepository.insertOne({
id: this.idService.gen(),
updatedAt: null,
title: values.title,
@@ -79,7 +79,7 @@ export class AnnouncementService {
silence: values.silence,
needConfirmationToRead: values.needConfirmationToRead,
userId: values.userId,
}).then(x => this.announcementsRepository.findOneByOrFail(x.identifiers[0]));
});
const packed = await this.announcementEntityService.pack(announcement);

View File

@@ -55,10 +55,10 @@ export class AvatarDecorationService implements OnApplicationShutdown {
@bindThis
public async create(options: Partial<MiAvatarDecoration>, moderator?: MiUser): Promise<MiAvatarDecoration> {
const created = await this.avatarDecorationsRepository.insert({
const created = await this.avatarDecorationsRepository.insertOne({
id: this.idService.gen(),
...options,
}).then(x => this.avatarDecorationsRepository.findOneByOrFail(x.identifiers[0]));
});
this.globalEventService.publishInternalEvent('avatarDecorationCreated', created);

View File

@@ -45,13 +45,13 @@ export class ClipService {
throw new ClipService.TooManyClipsError();
}
const clip = await this.clipsRepository.insert({
const clip = await this.clipsRepository.insertOne({
id: this.idService.gen(),
userId: me.id,
name: name,
isPublic: isPublic,
description: description,
}).then(x => this.clipsRepository.findOneByOrFail(x.identifiers[0]));
});
return clip;
}

View File

@@ -68,7 +68,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
localOnly: boolean;
roleIdsThatCanBeUsedThisEmojiAsReaction: MiRole['id'][];
}, moderator?: MiUser): Promise<MiEmoji> {
const emoji = await this.emojisRepository.insert({
const emoji = await this.emojisRepository.insertOne({
id: this.idService.gen(),
updatedAt: new Date(),
name: data.name,
@@ -82,7 +82,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
isSensitive: data.isSensitive,
localOnly: data.localOnly,
roleIdsThatCanBeUsedThisEmojiAsReaction: data.roleIdsThatCanBeUsedThisEmojiAsReaction,
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
});
if (data.host == null) {
this.localEmojisCache.refresh();
@@ -346,10 +346,11 @@ export class CustomEmojiService implements OnApplicationShutdown {
@bindThis
public async populateEmojis(emojiNames: string[], noteUserHost: string | null): Promise<Record<string, string>> {
const emojis = await Promise.all(emojiNames.map(x => this.populateEmoji(x, noteUserHost)));
const res = {} as any;
const res = {} as Record<string, string>;
for (let i = 0; i < emojiNames.length; i++) {
if (emojis[i] != null) {
res[emojiNames[i]] = emojis[i];
const resolvedEmoji = emojis[i];
if (resolvedEmoji != null) {
res[emojiNames[i]] = resolvedEmoji;
}
}
return res;

View File

@@ -220,7 +220,7 @@ export class DriveService {
file.size = size;
file.storedInternal = false;
return await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0]));
return await this.driveFilesRepository.insertOne(file);
} else { // use internal storage
const accessKey = randomUUID();
const thumbnailAccessKey = 'thumbnail-' + randomUUID();
@@ -254,7 +254,7 @@ export class DriveService {
file.md5 = hash;
file.size = size;
return await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0]));
return await this.driveFilesRepository.insertOne(file);
}
}
@@ -497,20 +497,20 @@ export class DriveService {
if (user && !force) {
// Check if there is a file with the same hash
const much = await this.driveFilesRepository.findOneBy({
const matched = await this.driveFilesRepository.findOneBy({
md5: info.md5,
userId: user.id,
});
if (much) {
this.registerLogger.info(`file with same hash is found: ${much.id}`);
if (sensitive && !much.isSensitive) {
if (matched) {
this.registerLogger.info(`file with same hash is found: ${matched.id}`);
if (sensitive && !matched.isSensitive) {
// The file is federated as sensitive for this time, but was federated as non-sensitive before.
// Therefore, update the file to sensitive.
await this.driveFilesRepository.update({ id: much.id }, { isSensitive: true });
much.isSensitive = true;
await this.driveFilesRepository.update({ id: matched.id }, { isSensitive: true });
matched.isSensitive = true;
}
return much;
return matched;
}
}
@@ -615,7 +615,7 @@ export class DriveService {
file.type = info.type.mime;
file.storedInternal = false;
file = await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0]));
file = await this.driveFilesRepository.insertOne(file);
} catch (err) {
// duplicate key error (when already registered)
if (isDuplicateKeyValueError(err)) {

View File

@@ -55,11 +55,11 @@ export class FederatedInstanceService implements OnApplicationShutdown {
const index = await this.instancesRepository.findOneBy({ host });
if (index == null) {
const i = await this.instancesRepository.insert({
const i = await this.instancesRepository.insertOne({
id: this.idService.gen(),
host,
firstRetrievedAt: new Date(),
}).then(x => this.instancesRepository.findOneByOrFail(x.identifiers[0]));
});
this.federatedInstanceCache.set(host, i);
return i;

View File

@@ -154,7 +154,7 @@ export class FetchInstanceMetadataService {
throw new Error('No wellknown links');
}
const links = wellknown.links as any[];
const links = wellknown.links as ({ rel: string, href: string; })[];
const link1_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/1.0');
const link2_0 = links.find(link => link.rel === 'http://nodeinfo.diaspora.software/ns/schema/2.0');

View File

@@ -53,11 +53,11 @@ export class RelayService {
@bindThis
public async addRelay(inbox: string): Promise<MiRelay> {
const relay = await this.relaysRepository.insert({
const relay = await this.relaysRepository.insertOne({
id: this.idService.gen(),
inbox,
status: 'requesting',
}).then(x => this.relaysRepository.findOneByOrFail(x.identifiers[0]));
});
const relayActor = await this.getRelayActor();
const follow = await this.apRendererService.renderFollowRelay(relay, relayActor);

View File

@@ -281,7 +281,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
@bindThis
private async matched(parentId: MiUser['id'], childId: MiUser['id'], options: { noIrregularRules: boolean; }): Promise<MiReversiGame> {
const game = await this.reversiGamesRepository.insert({
const game = await this.reversiGamesRepository.insertOne({
id: this.idService.gen(),
user1Id: parentId,
user2Id: childId,
@@ -294,10 +294,7 @@ export class ReversiService implements OnApplicationShutdown, OnModuleInit {
bw: 'random',
isLlotheo: false,
noIrregularRules: options.noIrregularRules,
}).then(x => this.reversiGamesRepository.findOneOrFail({
where: { id: x.identifiers[0].id },
relations: ['user1', 'user2'],
}));
}, { relations: ['user1', 'user2'] });
this.cacheGame(game);
const packed = await this.reversiGameEntityService.packDetail(game);

View File

@@ -471,12 +471,12 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
}
}
const created = await this.roleAssignmentsRepository.insert({
const created = await this.roleAssignmentsRepository.insertOne({
id: this.idService.gen(now),
expiresAt: expiresAt,
roleId: roleId,
userId: userId,
}).then(x => this.roleAssignmentsRepository.findOneByOrFail(x.identifiers[0]));
});
this.rolesRepository.update(roleId, {
lastUsedAt: new Date(),
@@ -558,7 +558,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
@bindThis
public async create(values: Partial<MiRole>, moderator?: MiUser): Promise<MiRole> {
const date = new Date();
const created = await this.rolesRepository.insert({
const created = await this.rolesRepository.insertOne({
id: this.idService.gen(date.getTime()),
updatedAt: date,
lastUsedAt: date,
@@ -576,7 +576,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
canEditMembersByModerator: values.canEditMembersByModerator,
displayOrder: values.displayOrder,
policies: values.policies,
}).then(x => this.rolesRepository.findOneByOrFail(x.identifiers[0]));
});
this.globalEventService.publishInternalEvent('roleCreated', created);

View File

@@ -517,7 +517,7 @@ export class UserFollowingService implements OnModuleInit {
followerId: follower.id,
});
const followRequest = await this.followRequestsRepository.insert({
const followRequest = await this.followRequestsRepository.insertOne({
id: this.idService.gen(),
followerId: follower.id,
followeeId: followee.id,
@@ -531,7 +531,7 @@ export class UserFollowingService implements OnModuleInit {
followeeHost: followee.host,
followeeInbox: this.userEntityService.isRemoteUser(followee) ? followee.inbox : undefined,
followeeSharedInbox: this.userEntityService.isRemoteUser(followee) ? followee.sharedInbox : undefined,
}).then(x => this.followRequestsRepository.findOneByOrFail(x.identifiers[0]));
});
// Publish receiveRequest event
if (this.userEntityService.isLocalUser(followee)) {

View File

@@ -407,7 +407,7 @@ export class ApNoteService {
this.logger.info(`register emoji host=${host}, name=${name}`);
return await this.emojisRepository.insert({
return await this.emojisRepository.insertOne({
id: this.idService.gen(),
host,
name,
@@ -416,7 +416,7 @@ export class ApNoteService {
publicUrl: tag.icon.url,
updatedAt: new Date(),
aliases: [],
}).then(x => this.emojisRepository.findOneByOrFail(x.identifiers[0]));
});
}));
}
}

View File

@@ -14,7 +14,8 @@ import { EntitySchema, LessThan, Between } from 'typeorm';
import { dateUTC, isTimeSame, isTimeBefore, subtractTime, addTime } from '@/misc/prelude/time.js';
import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
import type { Repository, DataSource } from 'typeorm';
import { MiRepository, miRepository } from '@/models/_.js';
import type { DataSource, Repository } from 'typeorm';
const COLUMN_PREFIX = '___' as const;
const UNIQUE_TEMP_COLUMN_PREFIX = 'unique_temp___' as const;
@@ -145,10 +146,10 @@ export default abstract class Chart<T extends Schema> {
group: string | null;
}[] = [];
// ↓にしたいけどfindOneとかで型エラーになる
//private repositoryForHour: Repository<RawRecord<T>>;
//private repositoryForDay: Repository<RawRecord<T>>;
private repositoryForHour: Repository<{ id: number; group?: string | null; date: number; }>;
private repositoryForDay: Repository<{ id: number; group?: string | null; date: number; }>;
//private repositoryForHour: Repository<RawRecord<T>> & MiRepository<RawRecord<T>>;
//private repositoryForDay: Repository<RawRecord<T>> & MiRepository<RawRecord<T>>;
private repositoryForHour: Repository<{ id: number; group?: string | null; date: number; }> & MiRepository<{ id: number; group?: string | null; date: number; }>;
private repositoryForDay: Repository<{ id: number; group?: string | null; date: number; }> & MiRepository<{ id: number; group?: string | null; date: number; }>;
/**
* 1日に一回程度実行されれば良いような計算処理を入れる(主にCASCADE削除などアプリケーション側で感知できない変動によるズレの修正用)
@@ -211,6 +212,10 @@ export default abstract class Chart<T extends Schema> {
} {
const createEntity = (span: 'hour' | 'day'): EntitySchema => new EntitySchema({
name:
span === 'hour' ? `ChartX${name}` :
span === 'day' ? `ChartDayX${name}` :
new Error('not happen') as never,
tableName:
span === 'hour' ? `__chart__${camelToSnake(name)}` :
span === 'day' ? `__chart_day__${camelToSnake(name)}` :
new Error('not happen') as never,
@@ -271,8 +276,8 @@ export default abstract class Chart<T extends Schema> {
this.logger = logger;
const { hour, day } = Chart.schemaToEntity(name, schema, grouped);
this.repositoryForHour = db.getRepository<{ id: number; group?: string | null; date: number; }>(hour);
this.repositoryForDay = db.getRepository<{ id: number; group?: string | null; date: number; }>(day);
this.repositoryForHour = db.getRepository<{ id: number; group?: string | null; date: number; }>(hour).extend(miRepository as MiRepository<{ id: number; group?: string | null; date: number; }>);
this.repositoryForDay = db.getRepository<{ id: number; group?: string | null; date: number; }>(day).extend(miRepository as MiRepository<{ id: number; group?: string | null; date: number; }>);
}
@bindThis
@@ -387,11 +392,11 @@ export default abstract class Chart<T extends Schema> {
}
// 新規ログ挿入
log = await repository.insert({
log = await repository.insertOne({
date: date,
...(group ? { group: group } : {}),
...columns,
}).then(x => repository.findOneByOrFail(x.identifiers[0])) as RawRecord<T>;
}) as RawRecord<T>;
this.logger.info(`${this.name + (group ? `:${group}` : '')}(${span}): New commit created`);

View File

@@ -10,6 +10,8 @@ import { awaitAll } from '@/misc/prelude/await-all.js';
import type { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
import { isNotNull } from '@/misc/is-not-null.js';
import type { Packed } from '@/misc/json-schema.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable()
@@ -26,6 +28,11 @@ export class AbuseUserReportEntityService {
@bindThis
public async pack(
src: MiAbuseUserReport['id'] | MiAbuseUserReport,
hint?: {
packedReporter?: Packed<'UserDetailedNotMe'>,
packedTargetUser?: Packed<'UserDetailedNotMe'>,
packedAssignee?: Packed<'UserDetailedNotMe'>,
},
) {
const report = typeof src === 'object' ? src : await this.abuseUserReportsRepository.findOneByOrFail({ id: src });
@@ -37,13 +44,13 @@ export class AbuseUserReportEntityService {
reporterId: report.reporterId,
targetUserId: report.targetUserId,
assigneeId: report.assigneeId,
reporter: this.userEntityService.pack(report.reporter ?? report.reporterId, null, {
reporter: hint?.packedReporter ?? this.userEntityService.pack(report.reporter ?? report.reporterId, null, {
schema: 'UserDetailedNotMe',
}),
targetUser: this.userEntityService.pack(report.targetUser ?? report.targetUserId, null, {
targetUser: hint?.packedTargetUser ?? this.userEntityService.pack(report.targetUser ?? report.targetUserId, null, {
schema: 'UserDetailedNotMe',
}),
assignee: report.assigneeId ? this.userEntityService.pack(report.assignee ?? report.assigneeId, null, {
assignee: report.assigneeId ? hint?.packedAssignee ?? this.userEntityService.pack(report.assignee ?? report.assigneeId, null, {
schema: 'UserDetailedNotMe',
}) : null,
forwarded: report.forwarded,
@@ -51,9 +58,24 @@ export class AbuseUserReportEntityService {
}
@bindThis
public packMany(
reports: any[],
public async packMany(
reports: MiAbuseUserReport[],
) {
return Promise.all(reports.map(x => this.pack(x)));
const _reporters = reports.map(({ reporter, reporterId }) => reporter ?? reporterId);
const _targetUsers = reports.map(({ targetUser, targetUserId }) => targetUser ?? targetUserId);
const _assignees = reports.map(({ assignee, assigneeId }) => assignee ?? assigneeId).filter(isNotNull);
const _userMap = await this.userEntityService.packMany(
[..._reporters, ..._targetUsers, ..._assignees],
null,
{ schema: 'UserDetailedNotMe' },
).then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(
reports.map(report => {
const packedReporter = _userMap.get(report.reporterId);
const packedTargetUser = _userMap.get(report.targetUserId);
const packedAssignee = report.assigneeId != null ? _userMap.get(report.assigneeId) : undefined;
return this.pack(report, { packedReporter, packedTargetUser, packedAssignee });
}),
);
}
}

View File

@@ -43,6 +43,7 @@ export class AntennaEntityService {
withFile: antenna.withFile,
isActive: antenna.isActive,
hasUnreadNote: false, // TODO
notify: false, // 後方互換性のため
};
}
}

View File

@@ -29,6 +29,9 @@ export class BlockingEntityService {
public async pack(
src: MiBlocking['id'] | MiBlocking,
me?: { id: MiUser['id'] } | null | undefined,
hint?: {
blockee?: Packed<'UserDetailedNotMe'>,
},
): Promise<Packed<'Blocking'>> {
const blocking = typeof src === 'object' ? src : await this.blockingsRepository.findOneByOrFail({ id: src });
@@ -36,17 +39,20 @@ export class BlockingEntityService {
id: blocking.id,
createdAt: this.idService.parse(blocking.id).date.toISOString(),
blockeeId: blocking.blockeeId,
blockee: this.userEntityService.pack(blocking.blockeeId, me, {
blockee: hint?.blockee ?? this.userEntityService.pack(blocking.blockeeId, me, {
schema: 'UserDetailedNotMe',
}),
});
}
@bindThis
public packMany(
blockings: any[],
public async packMany(
blockings: MiBlocking[],
me: { id: MiUser['id'] },
) {
return Promise.all(blockings.map(x => this.pack(x, me)));
const _blockees = blockings.map(({ blockee, blockeeId }) => blockee ?? blockeeId);
const _userMap = await this.userEntityService.packMany(_blockees, me, { schema: 'UserDetailedNotMe' })
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(blockings.map(blocking => this.pack(blocking, me, { blockee: _userMap.get(blocking.blockeeId) })));
}
}

View File

@@ -35,6 +35,9 @@ export class ClipEntityService {
public async pack(
src: MiClip['id'] | MiClip,
me?: { id: MiUser['id'] } | null | undefined,
hint?: {
packedUser?: Packed<'UserLite'>
},
): Promise<Packed<'Clip'>> {
const meId = me ? me.id : null;
const clip = typeof src === 'object' ? src : await this.clipsRepository.findOneByOrFail({ id: src });
@@ -44,7 +47,7 @@ export class ClipEntityService {
createdAt: this.idService.parse(clip.id).date.toISOString(),
lastClippedAt: clip.lastClippedAt ? clip.lastClippedAt.toISOString() : null,
userId: clip.userId,
user: this.userEntityService.pack(clip.user ?? clip.userId),
user: hint?.packedUser ?? this.userEntityService.pack(clip.user ?? clip.userId),
name: clip.name,
description: clip.description,
isPublic: clip.isPublic,
@@ -55,11 +58,14 @@ export class ClipEntityService {
}
@bindThis
public packMany(
public async packMany(
clips: MiClip[],
me?: { id: MiUser['id'] } | null | undefined,
) {
return Promise.all(clips.map(x => this.pack(x, me)));
const _users = clips.map(({ user, userId }) => user ?? userId);
const _userMap = await this.userEntityService.packMany(_users, me)
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(clips.map(clip => this.pack(clip, me, { packedUser: _userMap.get(clip.userId) })));
}
}

View File

@@ -222,6 +222,9 @@ export class DriveFileEntityService {
public async packNullable(
src: MiDriveFile['id'] | MiDriveFile,
options?: PackOptions,
hint?: {
packedUser?: Packed<'UserLite'>
},
): Promise<Packed<'DriveFile'> | null> {
const opts = Object.assign({
detail: false,
@@ -249,7 +252,7 @@ export class DriveFileEntityService {
detail: true,
}) : null,
userId: file.userId,
user: (opts.withUser && file.userId) ? this.userEntityService.pack(file.userId) : null,
user: (opts.withUser && file.userId) ? hint?.packedUser ?? this.userEntityService.pack(file.userId) : null,
});
}
@@ -258,7 +261,10 @@ export class DriveFileEntityService {
files: MiDriveFile[],
options?: PackOptions,
): Promise<Packed<'DriveFile'>[]> {
const items = await Promise.all(files.map(f => this.packNullable(f, options)));
const _user = files.map(({ user, userId }) => user ?? userId).filter(isNotNull);
const _userMap = await this.userEntityService.packMany(_user)
.then(users => new Map(users.map(user => [user.id, user])));
const items = await Promise.all(files.map(f => this.packNullable(f, options, f.userId ? { packedUser: _userMap.get(f.userId) } : {})));
return items.filter(isNotNull);
}

View File

@@ -33,6 +33,9 @@ export class FlashEntityService {
public async pack(
src: MiFlash['id'] | MiFlash,
me?: { id: MiUser['id'] } | null | undefined,
hint?: {
packedUser?: Packed<'UserLite'>
},
): Promise<Packed<'Flash'>> {
const meId = me ? me.id : null;
const flash = typeof src === 'object' ? src : await this.flashsRepository.findOneByOrFail({ id: src });
@@ -42,7 +45,7 @@ export class FlashEntityService {
createdAt: this.idService.parse(flash.id).date.toISOString(),
updatedAt: flash.updatedAt.toISOString(),
userId: flash.userId,
user: this.userEntityService.pack(flash.user ?? flash.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意
user: hint?.packedUser ?? this.userEntityService.pack(flash.user ?? flash.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意
title: flash.title,
summary: flash.summary,
script: flash.script,
@@ -52,11 +55,14 @@ export class FlashEntityService {
}
@bindThis
public packMany(
flashs: MiFlash[],
public async packMany(
flashes: MiFlash[],
me?: { id: MiUser['id'] } | null | undefined,
) {
return Promise.all(flashs.map(x => this.pack(x, me)));
const _users = flashes.map(({ user, userId }) => user ?? userId);
const _userMap = await this.userEntityService.packMany(_users, me)
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(flashes.map(flash => this.pack(flash, me, { packedUser: _userMap.get(flash.userId) })));
}
}

View File

@@ -10,6 +10,7 @@ import type { } from '@/models/Blocking.js';
import type { MiUser } from '@/models/User.js';
import type { MiFollowRequest } from '@/models/FollowRequest.js';
import { bindThis } from '@/decorators.js';
import type { Packed } from '@/misc/json-schema.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable()
@@ -26,14 +27,36 @@ export class FollowRequestEntityService {
public async pack(
src: MiFollowRequest['id'] | MiFollowRequest,
me?: { id: MiUser['id'] } | null | undefined,
hint?: {
packedFollower?: Packed<'UserLite'>,
packedFollowee?: Packed<'UserLite'>,
},
) {
const request = typeof src === 'object' ? src : await this.followRequestsRepository.findOneByOrFail({ id: src });
return {
id: request.id,
follower: await this.userEntityService.pack(request.followerId, me),
followee: await this.userEntityService.pack(request.followeeId, me),
follower: hint?.packedFollower ?? await this.userEntityService.pack(request.followerId, me),
followee: hint?.packedFollowee ?? await this.userEntityService.pack(request.followeeId, me),
};
}
@bindThis
public async packMany(
requests: MiFollowRequest[],
me?: { id: MiUser['id'] } | null | undefined,
) {
const _followers = requests.map(({ follower, followerId }) => follower ?? followerId);
const _followees = requests.map(({ followee, followeeId }) => followee ?? followeeId);
const _userMap = await this.userEntityService.packMany([..._followers, ..._followees], me)
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(
requests.map(req => {
const packedFollower = _userMap.get(req.followerId);
const packedFollowee = _userMap.get(req.followeeId);
return this.pack(req, me, { packedFollower, packedFollowee });
}),
);
}
}

View File

@@ -78,6 +78,10 @@ export class FollowingEntityService {
populateFollowee?: boolean;
populateFollower?: boolean;
},
hint?: {
packedFollowee?: Packed<'UserDetailedNotMe'>,
packedFollower?: Packed<'UserDetailedNotMe'>,
},
): Promise<Packed<'Following'>> {
const following = typeof src === 'object' ? src : await this.followingsRepository.findOneByOrFail({ id: src });
@@ -88,25 +92,35 @@ export class FollowingEntityService {
createdAt: this.idService.parse(following.id).date.toISOString(),
followeeId: following.followeeId,
followerId: following.followerId,
followee: opts.populateFollowee ? this.userEntityService.pack(following.followee ?? following.followeeId, me, {
followee: opts.populateFollowee ? hint?.packedFollowee ?? this.userEntityService.pack(following.followee ?? following.followeeId, me, {
schema: 'UserDetailedNotMe',
}) : undefined,
follower: opts.populateFollower ? this.userEntityService.pack(following.follower ?? following.followerId, me, {
follower: opts.populateFollower ? hint?.packedFollower ?? this.userEntityService.pack(following.follower ?? following.followerId, me, {
schema: 'UserDetailedNotMe',
}) : undefined,
});
}
@bindThis
public packMany(
followings: any[],
public async packMany(
followings: MiFollowing[],
me?: { id: MiUser['id'] } | null | undefined,
opts?: {
populateFollowee?: boolean;
populateFollower?: boolean;
},
) {
return Promise.all(followings.map(x => this.pack(x, me, opts)));
const _followees = opts?.populateFollowee ? followings.map(({ followee, followeeId }) => followee ?? followeeId) : [];
const _followers = opts?.populateFollower ? followings.map(({ follower, followerId }) => follower ?? followerId) : [];
const _userMap = await this.userEntityService.packMany([..._followees, ..._followers], me, { schema: 'UserDetailedNotMe' })
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(
followings.map(following => {
const packedFollowee = opts?.populateFollowee ? _userMap.get(following.followeeId) : undefined;
const packedFollower = opts?.populateFollower ? _userMap.get(following.followerId) : undefined;
return this.pack(following, me, opts, { packedFollowee, packedFollower });
}),
);
}
}

View File

@@ -35,6 +35,9 @@ export class GalleryPostEntityService {
public async pack(
src: MiGalleryPost['id'] | MiGalleryPost,
me?: { id: MiUser['id'] } | null | undefined,
hint?: {
packedUser?: Packed<'UserLite'>
},
): Promise<Packed<'GalleryPost'>> {
const meId = me ? me.id : null;
const post = typeof src === 'object' ? src : await this.galleryPostsRepository.findOneByOrFail({ id: src });
@@ -44,7 +47,7 @@ export class GalleryPostEntityService {
createdAt: this.idService.parse(post.id).date.toISOString(),
updatedAt: post.updatedAt.toISOString(),
userId: post.userId,
user: this.userEntityService.pack(post.user ?? post.userId, me),
user: hint?.packedUser ?? this.userEntityService.pack(post.user ?? post.userId, me),
title: post.title,
description: post.description,
fileIds: post.fileIds,
@@ -58,11 +61,14 @@ export class GalleryPostEntityService {
}
@bindThis
public packMany(
public async packMany(
posts: MiGalleryPost[],
me?: { id: MiUser['id'] } | null | undefined,
) {
return Promise.all(posts.map(x => this.pack(x, me)));
const _users = posts.map(({ user, userId }) => user ?? userId);
const _userMap = await this.userEntityService.packMany(_users, me)
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(posts.map(post => this.pack(post, me, { packedUser: _userMap.get(post.userId) })));
}
}

View File

@@ -12,6 +12,7 @@ import type { MiUser } from '@/models/User.js';
import type { MiRegistrationTicket } from '@/models/RegistrationTicket.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';
@Injectable()
@@ -29,6 +30,10 @@ export class InviteCodeEntityService {
public async pack(
src: MiRegistrationTicket['id'] | MiRegistrationTicket,
me?: { id: MiUser['id'] } | null | undefined,
hints?: {
packedCreatedBy?: Packed<'UserLite'>,
packedUsedBy?: Packed<'UserLite'>,
},
): Promise<Packed<'InviteCode'>> {
const target = typeof src === 'object' ? src : await this.registrationTicketsRepository.findOneOrFail({
where: {
@@ -42,18 +47,28 @@ export class InviteCodeEntityService {
code: target.code,
expiresAt: target.expiresAt ? target.expiresAt.toISOString() : null,
createdAt: this.idService.parse(target.id).date.toISOString(),
createdBy: target.createdBy ? await this.userEntityService.pack(target.createdBy, me) : null,
usedBy: target.usedBy ? await this.userEntityService.pack(target.usedBy, me) : null,
createdBy: target.createdBy ? hints?.packedCreatedBy ?? await this.userEntityService.pack(target.createdBy, me) : null,
usedBy: target.usedBy ? hints?.packedUsedBy ?? await this.userEntityService.pack(target.usedBy, me) : null,
usedAt: target.usedAt ? target.usedAt.toISOString() : null,
used: !!target.usedAt,
});
}
@bindThis
public packMany(
targets: any[],
public async packMany(
tickets: MiRegistrationTicket[],
me: { id: MiUser['id'] },
) {
return Promise.all(targets.map(x => this.pack(x, me)));
const _createdBys = tickets.map(({ createdBy, createdById }) => createdBy ?? createdById).filter(isNotNull);
const _usedBys = tickets.map(({ usedBy, usedById }) => usedBy ?? usedById).filter(isNotNull);
const _userMap = await this.userEntityService.packMany([..._createdBys, ..._usedBys], me)
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(
tickets.map(ticket => {
const packedCreatedBy = ticket.createdById != null ? _userMap.get(ticket.createdById) : undefined;
const packedUsedBy = ticket.usedById != null ? _userMap.get(ticket.usedById) : undefined;
return this.pack(ticket, me, { packedCreatedBy, packedUsedBy });
}),
);
}
}

View File

@@ -67,6 +67,7 @@ export class MetaEntityService {
feedbackUrl: instance.feedbackUrl,
impressumUrl: instance.impressumUrl,
privacyPolicyUrl: instance.privacyPolicyUrl,
inquiryUrl: instance.inquiryUrl,
disableRegistration: instance.disableRegistration,
emailRequiredForSignup: instance.emailRequiredForSignup,
enableHcaptcha: instance.enableHcaptcha,

View File

@@ -8,9 +8,10 @@ import { DI } from '@/di-symbols.js';
import type { ModerationLogsRepository } from '@/models/_.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import type { } from '@/models/Blocking.js';
import type { MiModerationLog } from '@/models/ModerationLog.js';
import { MiModerationLog } from '@/models/ModerationLog.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
import type { Packed } from '@/misc/json-schema.js';
import { UserEntityService } from './UserEntityService.js';
@Injectable()
@@ -27,6 +28,9 @@ export class ModerationLogEntityService {
@bindThis
public async pack(
src: MiModerationLog['id'] | MiModerationLog,
hint?: {
packedUser?: Packed<'UserDetailedNotMe'>,
},
) {
const log = typeof src === 'object' ? src : await this.moderationLogsRepository.findOneByOrFail({ id: src });
@@ -36,17 +40,20 @@ export class ModerationLogEntityService {
type: log.type,
info: log.info,
userId: log.userId,
user: this.userEntityService.pack(log.user ?? log.userId, null, {
user: hint?.packedUser ?? this.userEntityService.pack(log.user ?? log.userId, null, {
schema: 'UserDetailedNotMe',
}),
});
}
@bindThis
public packMany(
reports: any[],
public async packMany(
reports: MiModerationLog[],
) {
return Promise.all(reports.map(x => this.pack(x)));
const _users = reports.map(({ user, userId }) => user ?? userId);
const _userMap = await this.userEntityService.packMany(_users, null, { schema: 'UserDetailedNotMe' })
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(reports.map(report => this.pack(report, { packedUser: _userMap.get(report.userId) })));
}
}

View File

@@ -30,6 +30,9 @@ export class MutingEntityService {
public async pack(
src: MiMuting['id'] | MiMuting,
me?: { id: MiUser['id'] } | null | undefined,
hints?: {
packedMutee?: Packed<'UserDetailedNotMe'>,
},
): Promise<Packed<'Muting'>> {
const muting = typeof src === 'object' ? src : await this.mutingsRepository.findOneByOrFail({ id: src });
@@ -38,18 +41,21 @@ export class MutingEntityService {
createdAt: this.idService.parse(muting.id).date.toISOString(),
expiresAt: muting.expiresAt ? muting.expiresAt.toISOString() : null,
muteeId: muting.muteeId,
mutee: this.userEntityService.pack(muting.muteeId, me, {
mutee: hints?.packedMutee ?? this.userEntityService.pack(muting.muteeId, me, {
schema: 'UserDetailedNotMe',
}),
});
}
@bindThis
public packMany(
mutings: any[],
public async packMany(
mutings: MiMuting[],
me: { id: MiUser['id'] },
) {
return Promise.all(mutings.map(x => this.pack(x, me)));
const _mutees = mutings.map(({ mutee, muteeId }) => mutee ?? muteeId);
const _userMap = await this.userEntityService.packMany(_mutees, me, { schema: 'UserDetailedNotMe' })
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(mutings.map(muting => this.pack(muting, me, { packedMutee: _userMap.get(muting.muteeId) })));
}
}

View File

@@ -290,6 +290,7 @@ export class NoteEntityService implements OnModuleInit {
_hint_?: {
myReactions: Map<MiNote['id'], string | null>;
packedFiles: Map<MiNote['fileIds'][number], Packed<'DriveFile'> | null>;
packedUsers: Map<MiUser['id'], Packed<'UserLite'>>
};
},
): Promise<Packed<'Note'>> {
@@ -319,12 +320,13 @@ export class NoteEntityService implements OnModuleInit {
.filter(x => x.startsWith(':') && x.includes('@') && !x.includes('@.')) // リモートカスタム絵文字のみ
.map(x => this.reactionService.decodeReaction(x).reaction.replaceAll(':', ''));
const packedFiles = options?._hint_?.packedFiles;
const packedUsers = options?._hint_?.packedUsers;
const packed: Packed<'Note'> = await awaitAll({
id: note.id,
createdAt: this.idService.parse(note.id).date.toISOString(),
userId: note.userId,
user: this.userEntityService.pack(note.user ?? note.userId, me),
user: packedUsers?.get(note.userId) ?? this.userEntityService.pack(note.user ?? note.userId, me),
text: text,
cw: note.cw,
visibility: note.visibility,
@@ -449,12 +451,20 @@ export class NoteEntityService implements OnModuleInit {
// TODO: 本当は renote とか reply がないのに renoteId とか replyId があったらここで解決しておく
const fileIds = notes.map(n => [n.fileIds, n.renote?.fileIds, n.reply?.fileIds]).flat(2).filter(isNotNull);
const packedFiles = fileIds.length > 0 ? await this.driveFileEntityService.packManyByIdsMap(fileIds) : new Map();
const users = [
...notes.map(({ user, userId }) => user ?? userId),
...notes.map(({ replyUserId }) => replyUserId).filter(isNotNull),
...notes.map(({ renoteUserId }) => renoteUserId).filter(isNotNull),
];
const packedUsers = await this.userEntityService.packMany(users, me)
.then(users => new Map(users.map(u => [u.id, u])));
return await Promise.all(notes.map(n => this.pack(n, me, {
...options,
_hint_: {
myReactions: myReactionsMap,
packedFiles,
packedUsers,
},
})));
}

View File

@@ -52,6 +52,9 @@ export class NoteReactionEntityService implements OnModuleInit {
options?: {
withNote: boolean;
},
hints?: {
packedUser?: Packed<'UserLite'>
},
): Promise<Packed<'NoteReaction'>> {
const opts = Object.assign({
withNote: false,
@@ -62,7 +65,7 @@ export class NoteReactionEntityService implements OnModuleInit {
return {
id: reaction.id,
createdAt: this.idService.parse(reaction.id).date.toISOString(),
user: await this.userEntityService.pack(reaction.user ?? reaction.userId, me),
user: hints?.packedUser ?? await this.userEntityService.pack(reaction.user ?? reaction.userId, me),
type: this.reactionService.convertLegacyReaction(reaction.reaction),
...(opts.withNote ? {
note: await this.noteEntityService.pack(reaction.note ?? reaction.noteId, me),
@@ -81,7 +84,9 @@ export class NoteReactionEntityService implements OnModuleInit {
const opts = Object.assign({
withNote: false,
}, options);
return Promise.all(reactions.map(reaction => this.pack(reaction, me, opts)));
const _users = reactions.map(({ user, userId }) => user ?? userId);
const _userMap = await this.userEntityService.packMany(_users, me)
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(reactions.map(reaction => this.pack(reaction, me, opts, { packedUser: _userMap.get(reaction.userId) })));
}
}

View File

@@ -40,6 +40,9 @@ export class PageEntityService {
public async pack(
src: MiPage['id'] | MiPage,
me?: { id: MiUser['id'] } | null | undefined,
hint?: {
packedUser?: Packed<'UserLite'>
},
): Promise<Packed<'Page'>> {
const meId = me ? me.id : null;
const page = typeof src === 'object' ? src : await this.pagesRepository.findOneByOrFail({ id: src });
@@ -91,7 +94,7 @@ export class PageEntityService {
createdAt: this.idService.parse(page.id).date.toISOString(),
updatedAt: page.updatedAt.toISOString(),
userId: page.userId,
user: this.userEntityService.pack(page.user ?? page.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意
user: hint?.packedUser ?? this.userEntityService.pack(page.user ?? page.userId, me), // { schema: 'UserDetailed' } すると無限ループするので注意
content: page.content,
variables: page.variables,
title: page.title,
@@ -110,11 +113,14 @@ export class PageEntityService {
}
@bindThis
public packMany(
public async packMany(
pages: MiPage[],
me?: { id: MiUser['id'] } | null | undefined,
) {
return Promise.all(pages.map(x => this.pack(x, me)));
const _users = pages.map(({ user, userId }) => user ?? userId);
const _userMap = await this.userEntityService.packMany(_users, me)
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(pages.map(page => this.pack(page, me, { packedUser: _userMap.get(page.userId) })));
}
}

View File

@@ -30,6 +30,9 @@ export class RenoteMutingEntityService {
public async pack(
src: MiRenoteMuting['id'] | MiRenoteMuting,
me?: { id: MiUser['id'] } | null | undefined,
hints?: {
packedMutee?: Packed<'UserDetailedNotMe'>
},
): Promise<Packed<'RenoteMuting'>> {
const muting = typeof src === 'object' ? src : await this.renoteMutingsRepository.findOneByOrFail({ id: src });
@@ -37,18 +40,21 @@ export class RenoteMutingEntityService {
id: muting.id,
createdAt: this.idService.parse(muting.id).date.toISOString(),
muteeId: muting.muteeId,
mutee: this.userEntityService.pack(muting.muteeId, me, {
mutee: hints?.packedMutee ?? this.userEntityService.pack(muting.muteeId, me, {
schema: 'UserDetailedNotMe',
}),
});
}
@bindThis
public packMany(
mutings: any[],
public async packMany(
mutings: MiRenoteMuting[],
me: { id: MiUser['id'] },
) {
return Promise.all(mutings.map(x => this.pack(x, me)));
const _users = mutings.map(({ mutee, muteeId }) => mutee ?? muteeId);
const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailedNotMe' })
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(mutings.map(muting => this.pack(muting, me, { packedMutee: _userMap.get(muting.muteeId) })));
}
}

View File

@@ -28,13 +28,15 @@ export class ReversiGameEntityService {
@bindThis
public async packDetail(
src: MiReversiGame['id'] | MiReversiGame,
hint?: {
packedUser1?: Packed<'UserLite'>,
packedUser2?: Packed<'UserLite'>,
},
): Promise<Packed<'ReversiGameDetailed'>> {
const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src });
const users = await Promise.all([
this.userEntityService.pack(game.user1 ?? game.user1Id),
this.userEntityService.pack(game.user2 ?? game.user2Id),
]);
const user1 = hint?.packedUser1 ?? await this.userEntityService.pack(game.user1 ?? game.user1Id);
const user2 = hint?.packedUser2 ?? await this.userEntityService.pack(game.user2 ?? game.user2Id);
return await awaitAll({
id: game.id,
@@ -49,10 +51,10 @@ export class ReversiGameEntityService {
user2Ready: game.user2Ready,
user1Id: game.user1Id,
user2Id: game.user2Id,
user1: users[0],
user2: users[1],
user1,
user2,
winnerId: game.winnerId,
winner: game.winnerId ? users.find(u => u.id === game.winnerId)! : null,
winner: game.winnerId ? [user1, user2].find(u => u.id === game.winnerId)! : null,
surrenderedUserId: game.surrenderedUserId,
timeoutUserId: game.timeoutUserId,
black: game.black,
@@ -68,22 +70,35 @@ export class ReversiGameEntityService {
}
@bindThis
public packDetailMany(
xs: MiReversiGame[],
public async packDetailMany(
games: MiReversiGame[],
) {
return Promise.all(xs.map(x => this.packDetail(x)));
const _user1s = games.map(({ user1, user1Id }) => user1 ?? user1Id);
const _user2s = games.map(({ user2, user2Id }) => user2 ?? user2Id);
const _userMap = await this.userEntityService.packMany([..._user1s, ..._user2s])
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(
games.map(game => {
return this.packDetail(game, {
packedUser1: _userMap.get(game.user1Id),
packedUser2: _userMap.get(game.user2Id),
});
}),
);
}
@bindThis
public async packLite(
src: MiReversiGame['id'] | MiReversiGame,
hint?: {
packedUser1?: Packed<'UserLite'>,
packedUser2?: Packed<'UserLite'>,
},
): Promise<Packed<'ReversiGameLite'>> {
const game = typeof src === 'object' ? src : await this.reversiGamesRepository.findOneByOrFail({ id: src });
const users = await Promise.all([
this.userEntityService.pack(game.user1 ?? game.user1Id),
this.userEntityService.pack(game.user2 ?? game.user2Id),
]);
const user1 = hint?.packedUser1 ?? await this.userEntityService.pack(game.user1 ?? game.user1Id);
const user2 = hint?.packedUser2 ?? await this.userEntityService.pack(game.user2 ?? game.user2Id);
return await awaitAll({
id: game.id,
@@ -94,10 +109,10 @@ export class ReversiGameEntityService {
isEnded: game.isEnded,
user1Id: game.user1Id,
user2Id: game.user2Id,
user1: users[0],
user2: users[1],
user1,
user2,
winnerId: game.winnerId,
winner: game.winnerId ? users.find(u => u.id === game.winnerId)! : null,
winner: game.winnerId ? [user1, user2].find(u => u.id === game.winnerId)! : null,
surrenderedUserId: game.surrenderedUserId,
timeoutUserId: game.timeoutUserId,
black: game.black,
@@ -111,10 +126,21 @@ export class ReversiGameEntityService {
}
@bindThis
public packLiteMany(
xs: MiReversiGame[],
public async packLiteMany(
games: MiReversiGame[],
) {
return Promise.all(xs.map(x => this.packLite(x)));
const _user1s = games.map(({ user1, user1Id }) => user1 ?? user1Id);
const _user2s = games.map(({ user2, user2Id }) => user2 ?? user2Id);
const _userMap = await this.userEntityService.packMany([..._user1s, ..._user2s])
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(
games.map(game => {
return this.packLite(game, {
packedUser1: _userMap.get(game.user1Id),
packedUser2: _userMap.get(game.user2Id),
});
}),
);
}
}

View File

@@ -50,11 +50,14 @@ export class UserListEntityService {
public async packMembershipsMany(
memberships: MiUserListMembership[],
) {
const _users = memberships.map(({ user, userId }) => user ?? userId);
const _userMap = await this.userEntityService.packMany(_users)
.then(users => new Map(users.map(u => [u.id, u])));
return Promise.all(memberships.map(async x => ({
id: x.id,
createdAt: this.idService.parse(x.id).date.toISOString(),
userId: x.userId,
user: await this.userEntityService.pack(x.userId),
user: _userMap.get(x.userId) ?? await this.userEntityService.pack(x.userId),
withReplies: x.withReplies,
})));
}

View File

@@ -376,6 +376,12 @@ export class MiMeta {
})
public privacyPolicyUrl: string | null;
@Column('varchar', {
length: 1024,
nullable: true,
})
public inquiryUrl: string | null;
@Column('varchar', {
length: 8192,
nullable: true,

View File

@@ -5,409 +5,409 @@
import { Module } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiAvatarDecoration, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook, MiBubbleGameRecord, MiReversiGame } from './_.js';
import { MiRepository, MiAbuseUserReport, MiAccessToken, MiAd, MiAnnouncement, MiAnnouncementRead, MiAntenna, MiApp, MiAuthSession, MiAvatarDecoration, MiBlocking, MiChannel, MiChannelFavorite, MiChannelFollowing, MiClip, MiClipFavorite, MiClipNote, MiDriveFile, MiDriveFolder, MiEmoji, MiFlash, MiFlashLike, MiFollowRequest, MiFollowing, MiGalleryLike, MiGalleryPost, MiHashtag, MiInstance, MiMeta, MiModerationLog, MiMuting, MiNote, MiNoteFavorite, MiNoteReaction, MiNoteThreadMuting, MiNoteUnread, MiPage, MiPageLike, MiPasswordResetRequest, MiPoll, MiPollVote, MiPromoNote, MiPromoRead, MiRegistrationTicket, MiRegistryItem, MiRelay, MiRenoteMuting, MiRetentionAggregation, MiRole, MiRoleAssignment, MiSignin, MiSwSubscription, MiUsedUsername, MiUser, MiUserIp, MiUserKeypair, MiUserList, MiUserListFavorite, MiUserListMembership, MiUserMemo, MiUserNotePining, MiUserPending, MiUserProfile, MiUserPublickey, MiUserSecurityKey, MiWebhook, MiBubbleGameRecord, MiReversiGame, miRepository } from './_.js';
import type { DataSource } from 'typeorm';
import type { Provider } from '@nestjs/common';
const $usersRepository: Provider = {
provide: DI.usersRepository,
useFactory: (db: DataSource) => db.getRepository(MiUser),
useFactory: (db: DataSource) => db.getRepository(MiUser).extend(miRepository as MiRepository<MiUser>),
inject: [DI.db],
};
const $notesRepository: Provider = {
provide: DI.notesRepository,
useFactory: (db: DataSource) => db.getRepository(MiNote),
useFactory: (db: DataSource) => db.getRepository(MiNote).extend(miRepository as MiRepository<MiNote>),
inject: [DI.db],
};
const $announcementsRepository: Provider = {
provide: DI.announcementsRepository,
useFactory: (db: DataSource) => db.getRepository(MiAnnouncement),
useFactory: (db: DataSource) => db.getRepository(MiAnnouncement).extend(miRepository as MiRepository<MiAnnouncement>),
inject: [DI.db],
};
const $announcementReadsRepository: Provider = {
provide: DI.announcementReadsRepository,
useFactory: (db: DataSource) => db.getRepository(MiAnnouncementRead),
useFactory: (db: DataSource) => db.getRepository(MiAnnouncementRead).extend(miRepository as MiRepository<MiAnnouncementRead>),
inject: [DI.db],
};
const $appsRepository: Provider = {
provide: DI.appsRepository,
useFactory: (db: DataSource) => db.getRepository(MiApp),
useFactory: (db: DataSource) => db.getRepository(MiApp).extend(miRepository as MiRepository<MiApp>),
inject: [DI.db],
};
const $avatarDecorationsRepository: Provider = {
provide: DI.avatarDecorationsRepository,
useFactory: (db: DataSource) => db.getRepository(MiAvatarDecoration),
useFactory: (db: DataSource) => db.getRepository(MiAvatarDecoration).extend(miRepository as MiRepository<MiAvatarDecoration>),
inject: [DI.db],
};
const $noteFavoritesRepository: Provider = {
provide: DI.noteFavoritesRepository,
useFactory: (db: DataSource) => db.getRepository(MiNoteFavorite),
useFactory: (db: DataSource) => db.getRepository(MiNoteFavorite).extend(miRepository as MiRepository<MiNoteFavorite>),
inject: [DI.db],
};
const $noteThreadMutingsRepository: Provider = {
provide: DI.noteThreadMutingsRepository,
useFactory: (db: DataSource) => db.getRepository(MiNoteThreadMuting),
useFactory: (db: DataSource) => db.getRepository(MiNoteThreadMuting).extend(miRepository as MiRepository<MiNoteThreadMuting>),
inject: [DI.db],
};
const $noteReactionsRepository: Provider = {
provide: DI.noteReactionsRepository,
useFactory: (db: DataSource) => db.getRepository(MiNoteReaction),
useFactory: (db: DataSource) => db.getRepository(MiNoteReaction).extend(miRepository as MiRepository<MiNoteReaction>),
inject: [DI.db],
};
const $noteUnreadsRepository: Provider = {
provide: DI.noteUnreadsRepository,
useFactory: (db: DataSource) => db.getRepository(MiNoteUnread),
useFactory: (db: DataSource) => db.getRepository(MiNoteUnread).extend(miRepository as MiRepository<MiNoteUnread>),
inject: [DI.db],
};
const $pollsRepository: Provider = {
provide: DI.pollsRepository,
useFactory: (db: DataSource) => db.getRepository(MiPoll),
useFactory: (db: DataSource) => db.getRepository(MiPoll).extend(miRepository as MiRepository<MiPoll>),
inject: [DI.db],
};
const $pollVotesRepository: Provider = {
provide: DI.pollVotesRepository,
useFactory: (db: DataSource) => db.getRepository(MiPollVote),
useFactory: (db: DataSource) => db.getRepository(MiPollVote).extend(miRepository as MiRepository<MiPollVote>),
inject: [DI.db],
};
const $userProfilesRepository: Provider = {
provide: DI.userProfilesRepository,
useFactory: (db: DataSource) => db.getRepository(MiUserProfile),
useFactory: (db: DataSource) => db.getRepository(MiUserProfile).extend(miRepository as MiRepository<MiUserProfile>),
inject: [DI.db],
};
const $userKeypairsRepository: Provider = {
provide: DI.userKeypairsRepository,
useFactory: (db: DataSource) => db.getRepository(MiUserKeypair),
useFactory: (db: DataSource) => db.getRepository(MiUserKeypair).extend(miRepository as MiRepository<MiUserKeypair>),
inject: [DI.db],
};
const $userPendingsRepository: Provider = {
provide: DI.userPendingsRepository,
useFactory: (db: DataSource) => db.getRepository(MiUserPending),
useFactory: (db: DataSource) => db.getRepository(MiUserPending).extend(miRepository as MiRepository<MiUserPending>),
inject: [DI.db],
};
const $userSecurityKeysRepository: Provider = {
provide: DI.userSecurityKeysRepository,
useFactory: (db: DataSource) => db.getRepository(MiUserSecurityKey),
useFactory: (db: DataSource) => db.getRepository(MiUserSecurityKey).extend(miRepository as MiRepository<MiUserSecurityKey>),
inject: [DI.db],
};
const $userPublickeysRepository: Provider = {
provide: DI.userPublickeysRepository,
useFactory: (db: DataSource) => db.getRepository(MiUserPublickey),
useFactory: (db: DataSource) => db.getRepository(MiUserPublickey).extend(miRepository as MiRepository<MiUserPublickey>),
inject: [DI.db],
};
const $userListsRepository: Provider = {
provide: DI.userListsRepository,
useFactory: (db: DataSource) => db.getRepository(MiUserList),
useFactory: (db: DataSource) => db.getRepository(MiUserList).extend(miRepository as MiRepository<MiUserList>),
inject: [DI.db],
};
const $userListFavoritesRepository: Provider = {
provide: DI.userListFavoritesRepository,
useFactory: (db: DataSource) => db.getRepository(MiUserListFavorite),
useFactory: (db: DataSource) => db.getRepository(MiUserListFavorite).extend(miRepository as MiRepository<MiUserListFavorite>),
inject: [DI.db],
};
const $userListMembershipsRepository: Provider = {
provide: DI.userListMembershipsRepository,
useFactory: (db: DataSource) => db.getRepository(MiUserListMembership),
useFactory: (db: DataSource) => db.getRepository(MiUserListMembership).extend(miRepository as MiRepository<MiUserListMembership>),
inject: [DI.db],
};
const $userNotePiningsRepository: Provider = {
provide: DI.userNotePiningsRepository,
useFactory: (db: DataSource) => db.getRepository(MiUserNotePining),
useFactory: (db: DataSource) => db.getRepository(MiUserNotePining).extend(miRepository as MiRepository<MiUserNotePining>),
inject: [DI.db],
};
const $userIpsRepository: Provider = {
provide: DI.userIpsRepository,
useFactory: (db: DataSource) => db.getRepository(MiUserIp),
useFactory: (db: DataSource) => db.getRepository(MiUserIp).extend(miRepository as MiRepository<MiUserIp>),
inject: [DI.db],
};
const $usedUsernamesRepository: Provider = {
provide: DI.usedUsernamesRepository,
useFactory: (db: DataSource) => db.getRepository(MiUsedUsername),
useFactory: (db: DataSource) => db.getRepository(MiUsedUsername).extend(miRepository as MiRepository<MiUsedUsername>),
inject: [DI.db],
};
const $followingsRepository: Provider = {
provide: DI.followingsRepository,
useFactory: (db: DataSource) => db.getRepository(MiFollowing),
useFactory: (db: DataSource) => db.getRepository(MiFollowing).extend(miRepository as MiRepository<MiFollowing>),
inject: [DI.db],
};
const $followRequestsRepository: Provider = {
provide: DI.followRequestsRepository,
useFactory: (db: DataSource) => db.getRepository(MiFollowRequest),
useFactory: (db: DataSource) => db.getRepository(MiFollowRequest).extend(miRepository as MiRepository<MiFollowRequest>),
inject: [DI.db],
};
const $instancesRepository: Provider = {
provide: DI.instancesRepository,
useFactory: (db: DataSource) => db.getRepository(MiInstance),
useFactory: (db: DataSource) => db.getRepository(MiInstance).extend(miRepository as MiRepository<MiInstance>),
inject: [DI.db],
};
const $emojisRepository: Provider = {
provide: DI.emojisRepository,
useFactory: (db: DataSource) => db.getRepository(MiEmoji),
useFactory: (db: DataSource) => db.getRepository(MiEmoji).extend(miRepository as MiRepository<MiEmoji>),
inject: [DI.db],
};
const $driveFilesRepository: Provider = {
provide: DI.driveFilesRepository,
useFactory: (db: DataSource) => db.getRepository(MiDriveFile),
useFactory: (db: DataSource) => db.getRepository(MiDriveFile).extend(miRepository as MiRepository<MiDriveFile>),
inject: [DI.db],
};
const $driveFoldersRepository: Provider = {
provide: DI.driveFoldersRepository,
useFactory: (db: DataSource) => db.getRepository(MiDriveFolder),
useFactory: (db: DataSource) => db.getRepository(MiDriveFolder).extend(miRepository as MiRepository<MiDriveFolder>),
inject: [DI.db],
};
const $metasRepository: Provider = {
provide: DI.metasRepository,
useFactory: (db: DataSource) => db.getRepository(MiMeta),
useFactory: (db: DataSource) => db.getRepository(MiMeta).extend(miRepository as MiRepository<MiMeta>),
inject: [DI.db],
};
const $mutingsRepository: Provider = {
provide: DI.mutingsRepository,
useFactory: (db: DataSource) => db.getRepository(MiMuting),
useFactory: (db: DataSource) => db.getRepository(MiMuting).extend(miRepository as MiRepository<MiMuting>),
inject: [DI.db],
};
const $renoteMutingsRepository: Provider = {
provide: DI.renoteMutingsRepository,
useFactory: (db: DataSource) => db.getRepository(MiRenoteMuting),
useFactory: (db: DataSource) => db.getRepository(MiRenoteMuting).extend(miRepository as MiRepository<MiRenoteMuting>),
inject: [DI.db],
};
const $blockingsRepository: Provider = {
provide: DI.blockingsRepository,
useFactory: (db: DataSource) => db.getRepository(MiBlocking),
useFactory: (db: DataSource) => db.getRepository(MiBlocking).extend(miRepository as MiRepository<MiBlocking>),
inject: [DI.db],
};
const $swSubscriptionsRepository: Provider = {
provide: DI.swSubscriptionsRepository,
useFactory: (db: DataSource) => db.getRepository(MiSwSubscription),
useFactory: (db: DataSource) => db.getRepository(MiSwSubscription).extend(miRepository as MiRepository<MiSwSubscription>),
inject: [DI.db],
};
const $hashtagsRepository: Provider = {
provide: DI.hashtagsRepository,
useFactory: (db: DataSource) => db.getRepository(MiHashtag),
useFactory: (db: DataSource) => db.getRepository(MiHashtag).extend(miRepository as MiRepository<MiHashtag>),
inject: [DI.db],
};
const $abuseUserReportsRepository: Provider = {
provide: DI.abuseUserReportsRepository,
useFactory: (db: DataSource) => db.getRepository(MiAbuseUserReport),
useFactory: (db: DataSource) => db.getRepository(MiAbuseUserReport).extend(miRepository as MiRepository<MiAbuseUserReport>),
inject: [DI.db],
};
const $registrationTicketsRepository: Provider = {
provide: DI.registrationTicketsRepository,
useFactory: (db: DataSource) => db.getRepository(MiRegistrationTicket),
useFactory: (db: DataSource) => db.getRepository(MiRegistrationTicket).extend(miRepository as MiRepository<MiRegistrationTicket>),
inject: [DI.db],
};
const $authSessionsRepository: Provider = {
provide: DI.authSessionsRepository,
useFactory: (db: DataSource) => db.getRepository(MiAuthSession),
useFactory: (db: DataSource) => db.getRepository(MiAuthSession).extend(miRepository as MiRepository<MiAuthSession>),
inject: [DI.db],
};
const $accessTokensRepository: Provider = {
provide: DI.accessTokensRepository,
useFactory: (db: DataSource) => db.getRepository(MiAccessToken),
useFactory: (db: DataSource) => db.getRepository(MiAccessToken).extend(miRepository as MiRepository<MiAccessToken>),
inject: [DI.db],
};
const $signinsRepository: Provider = {
provide: DI.signinsRepository,
useFactory: (db: DataSource) => db.getRepository(MiSignin),
useFactory: (db: DataSource) => db.getRepository(MiSignin).extend(miRepository as MiRepository<MiSignin>),
inject: [DI.db],
};
const $pagesRepository: Provider = {
provide: DI.pagesRepository,
useFactory: (db: DataSource) => db.getRepository(MiPage),
useFactory: (db: DataSource) => db.getRepository(MiPage).extend(miRepository as MiRepository<MiPage>),
inject: [DI.db],
};
const $pageLikesRepository: Provider = {
provide: DI.pageLikesRepository,
useFactory: (db: DataSource) => db.getRepository(MiPageLike),
useFactory: (db: DataSource) => db.getRepository(MiPageLike).extend(miRepository as MiRepository<MiPageLike>),
inject: [DI.db],
};
const $galleryPostsRepository: Provider = {
provide: DI.galleryPostsRepository,
useFactory: (db: DataSource) => db.getRepository(MiGalleryPost),
useFactory: (db: DataSource) => db.getRepository(MiGalleryPost).extend(miRepository as MiRepository<MiGalleryPost>),
inject: [DI.db],
};
const $galleryLikesRepository: Provider = {
provide: DI.galleryLikesRepository,
useFactory: (db: DataSource) => db.getRepository(MiGalleryLike),
useFactory: (db: DataSource) => db.getRepository(MiGalleryLike).extend(miRepository as MiRepository<MiGalleryLike>),
inject: [DI.db],
};
const $moderationLogsRepository: Provider = {
provide: DI.moderationLogsRepository,
useFactory: (db: DataSource) => db.getRepository(MiModerationLog),
useFactory: (db: DataSource) => db.getRepository(MiModerationLog).extend(miRepository as MiRepository<MiModerationLog>),
inject: [DI.db],
};
const $clipsRepository: Provider = {
provide: DI.clipsRepository,
useFactory: (db: DataSource) => db.getRepository(MiClip),
useFactory: (db: DataSource) => db.getRepository(MiClip).extend(miRepository as MiRepository<MiClip>),
inject: [DI.db],
};
const $clipNotesRepository: Provider = {
provide: DI.clipNotesRepository,
useFactory: (db: DataSource) => db.getRepository(MiClipNote),
useFactory: (db: DataSource) => db.getRepository(MiClipNote).extend(miRepository as MiRepository<MiClipNote>),
inject: [DI.db],
};
const $clipFavoritesRepository: Provider = {
provide: DI.clipFavoritesRepository,
useFactory: (db: DataSource) => db.getRepository(MiClipFavorite),
useFactory: (db: DataSource) => db.getRepository(MiClipFavorite).extend(miRepository as MiRepository<MiClipFavorite>),
inject: [DI.db],
};
const $antennasRepository: Provider = {
provide: DI.antennasRepository,
useFactory: (db: DataSource) => db.getRepository(MiAntenna),
useFactory: (db: DataSource) => db.getRepository(MiAntenna).extend(miRepository as MiRepository<MiAntenna>),
inject: [DI.db],
};
const $promoNotesRepository: Provider = {
provide: DI.promoNotesRepository,
useFactory: (db: DataSource) => db.getRepository(MiPromoNote),
useFactory: (db: DataSource) => db.getRepository(MiPromoNote).extend(miRepository as MiRepository<MiPromoNote>),
inject: [DI.db],
};
const $promoReadsRepository: Provider = {
provide: DI.promoReadsRepository,
useFactory: (db: DataSource) => db.getRepository(MiPromoRead),
useFactory: (db: DataSource) => db.getRepository(MiPromoRead).extend(miRepository as MiRepository<MiPromoRead>),
inject: [DI.db],
};
const $relaysRepository: Provider = {
provide: DI.relaysRepository,
useFactory: (db: DataSource) => db.getRepository(MiRelay),
useFactory: (db: DataSource) => db.getRepository(MiRelay).extend(miRepository as MiRepository<MiRelay>),
inject: [DI.db],
};
const $channelsRepository: Provider = {
provide: DI.channelsRepository,
useFactory: (db: DataSource) => db.getRepository(MiChannel),
useFactory: (db: DataSource) => db.getRepository(MiChannel).extend(miRepository as MiRepository<MiChannel>),
inject: [DI.db],
};
const $channelFollowingsRepository: Provider = {
provide: DI.channelFollowingsRepository,
useFactory: (db: DataSource) => db.getRepository(MiChannelFollowing),
useFactory: (db: DataSource) => db.getRepository(MiChannelFollowing).extend(miRepository as MiRepository<MiChannelFollowing>),
inject: [DI.db],
};
const $channelFavoritesRepository: Provider = {
provide: DI.channelFavoritesRepository,
useFactory: (db: DataSource) => db.getRepository(MiChannelFavorite),
useFactory: (db: DataSource) => db.getRepository(MiChannelFavorite).extend(miRepository as MiRepository<MiChannelFavorite>),
inject: [DI.db],
};
const $registryItemsRepository: Provider = {
provide: DI.registryItemsRepository,
useFactory: (db: DataSource) => db.getRepository(MiRegistryItem),
useFactory: (db: DataSource) => db.getRepository(MiRegistryItem).extend(miRepository as MiRepository<MiRegistryItem>),
inject: [DI.db],
};
const $webhooksRepository: Provider = {
provide: DI.webhooksRepository,
useFactory: (db: DataSource) => db.getRepository(MiWebhook),
useFactory: (db: DataSource) => db.getRepository(MiWebhook).extend(miRepository as MiRepository<MiWebhook>),
inject: [DI.db],
};
const $adsRepository: Provider = {
provide: DI.adsRepository,
useFactory: (db: DataSource) => db.getRepository(MiAd),
useFactory: (db: DataSource) => db.getRepository(MiAd).extend(miRepository as MiRepository<MiAd>),
inject: [DI.db],
};
const $passwordResetRequestsRepository: Provider = {
provide: DI.passwordResetRequestsRepository,
useFactory: (db: DataSource) => db.getRepository(MiPasswordResetRequest),
useFactory: (db: DataSource) => db.getRepository(MiPasswordResetRequest).extend(miRepository as MiRepository<MiPasswordResetRequest>),
inject: [DI.db],
};
const $retentionAggregationsRepository: Provider = {
provide: DI.retentionAggregationsRepository,
useFactory: (db: DataSource) => db.getRepository(MiRetentionAggregation),
useFactory: (db: DataSource) => db.getRepository(MiRetentionAggregation).extend(miRepository as MiRepository<MiRetentionAggregation>),
inject: [DI.db],
};
const $flashsRepository: Provider = {
provide: DI.flashsRepository,
useFactory: (db: DataSource) => db.getRepository(MiFlash),
useFactory: (db: DataSource) => db.getRepository(MiFlash).extend(miRepository as MiRepository<MiFlash>),
inject: [DI.db],
};
const $flashLikesRepository: Provider = {
provide: DI.flashLikesRepository,
useFactory: (db: DataSource) => db.getRepository(MiFlashLike),
useFactory: (db: DataSource) => db.getRepository(MiFlashLike).extend(miRepository as MiRepository<MiFlashLike>),
inject: [DI.db],
};
const $rolesRepository: Provider = {
provide: DI.rolesRepository,
useFactory: (db: DataSource) => db.getRepository(MiRole),
useFactory: (db: DataSource) => db.getRepository(MiRole).extend(miRepository as MiRepository<MiRole>),
inject: [DI.db],
};
const $roleAssignmentsRepository: Provider = {
provide: DI.roleAssignmentsRepository,
useFactory: (db: DataSource) => db.getRepository(MiRoleAssignment),
useFactory: (db: DataSource) => db.getRepository(MiRoleAssignment).extend(miRepository as MiRepository<MiRoleAssignment>),
inject: [DI.db],
};
const $userMemosRepository: Provider = {
provide: DI.userMemosRepository,
useFactory: (db: DataSource) => db.getRepository(MiUserMemo),
useFactory: (db: DataSource) => db.getRepository(MiUserMemo).extend(miRepository as MiRepository<MiUserMemo>),
inject: [DI.db],
};
const $bubbleGameRecordsRepository: Provider = {
provide: DI.bubbleGameRecordsRepository,
useFactory: (db: DataSource) => db.getRepository(MiBubbleGameRecord),
useFactory: (db: DataSource) => db.getRepository(MiBubbleGameRecord).extend(miRepository as MiRepository<MiBubbleGameRecord>),
inject: [DI.db],
};
const $reversiGamesRepository: Provider = {
provide: DI.reversiGamesRepository,
useFactory: (db: DataSource) => db.getRepository(MiReversiGame),
useFactory: (db: DataSource) => db.getRepository(MiReversiGame).extend(miRepository as MiRepository<MiReversiGame>),
inject: [DI.db],
};

View File

@@ -3,6 +3,10 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { FindOneOptions, InsertQueryBuilder, ObjectLiteral, QueryRunner, ReplicationMode, Repository, SelectQueryBuilder } from 'typeorm';
import { RelationCountLoader } from 'typeorm/query-builder/relation-count/RelationCountLoader.js';
import { RelationIdLoader } from 'typeorm/query-builder/relation-id/RelationIdLoader.js';
import { RawSqlResultsToEntityTransformer } from 'typeorm/query-builder/transformer/RawSqlResultsToEntityTransformer.js';
import { MiAbuseUserReport } from '@/models/AbuseUserReport.js';
import { MiAccessToken } from '@/models/AccessToken.js';
import { MiAd } from '@/models/Ad.js';
@@ -70,8 +74,102 @@ import { MiFlashLike } from '@/models/FlashLike.js';
import { MiUserListFavorite } from '@/models/UserListFavorite.js';
import { MiBubbleGameRecord } from '@/models/BubbleGameRecord.js';
import { MiReversiGame } from '@/models/ReversiGame.js';
import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js';
import type { Repository } from 'typeorm';
interface AsyncDisposableReference<T> extends AsyncDisposable {
readonly value: T;
}
// SEEALSO: <https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-2.html#using-declarations-and-explicit-resource-management>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
Symbol.dispose ??= Symbol('Symbol.dispose');
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
Symbol.asyncDispose ??= Symbol('Symbol.asyncDispose');
export interface MiRepository<T extends ObjectLiteral> {
createTableColumnNames(this: Repository<T> & MiRepository<T>, queryBuilder: InsertQueryBuilder<T>): string[];
createTableColumnNamesWithPrimaryKey(this: Repository<T> & MiRepository<T>, queryBuilder: InsertQueryBuilder<T>): string[];
insertOne(this: Repository<T> & MiRepository<T>, entity: QueryDeepPartialEntity<T>, findOptions?: Pick<FindOneOptions<T>, 'relations'>): Promise<T>;
selectAliasColumnNames(this: Repository<T> & MiRepository<T>, queryBuilder: InsertQueryBuilder<T>, builder: SelectQueryBuilder<T>): void;
useQueryRunner(this: Repository<T> & MiRepository<T>, mode: ReplicationMode): AsyncDisposableReference<QueryRunner>;
}
export const miRepository = {
createTableColumnNames(queryBuilder) {
// @ts-expect-error -- protected
const insertedColumns = queryBuilder.getInsertedColumns();
if (insertedColumns.length) {
return insertedColumns.map(column => column.databaseName);
}
if (!queryBuilder.expressionMap.mainAlias?.hasMetadata && !queryBuilder.expressionMap.insertColumns.length) {
// @ts-expect-error -- protected
const valueSets = queryBuilder.getValueSets();
if (valueSets.length === 1) {
return Object.keys(valueSets[0]);
}
}
return queryBuilder.expressionMap.insertColumns;
},
createTableColumnNamesWithPrimaryKey(queryBuilder) {
const columnNames = this.createTableColumnNames(queryBuilder);
if (!columnNames.includes('id')) {
columnNames.unshift('id');
}
return columnNames;
},
async insertOne(entity, findOptions?) {
await using queryRunnerADR = this.useQueryRunner('master');
const queryRunner = queryRunnerADR.value;
const queryBuilder = this.createQueryBuilder(undefined, queryRunner).insert().values(entity);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const mainAlias = queryBuilder.expressionMap.mainAlias!;
const name = mainAlias.name;
mainAlias.name = 't';
const columnNames = this.createTableColumnNamesWithPrimaryKey(queryBuilder);
queryBuilder.returning(columnNames.reduce((a, c) => `${a}, ${queryBuilder.escape(c)}`, '').slice(2));
const builder = this.createQueryBuilder(undefined, queryRunner).addCommonTableExpression(queryBuilder, 'cte', { columnNames });
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
builder.expressionMap.mainAlias!.tablePath = 'cte';
this.selectAliasColumnNames(queryBuilder, builder);
if (findOptions) {
builder.setFindOptions(findOptions);
}
const raw = await builder.execute();
mainAlias.name = name;
const relationId = await new RelationIdLoader(builder.connection, queryRunner, builder.expressionMap.relationIdAttributes).load(raw);
const relationCount = await new RelationCountLoader(builder.connection, queryRunner, builder.expressionMap.relationCountAttributes).load(raw);
const result = new RawSqlResultsToEntityTransformer(builder.expressionMap, builder.connection.driver, relationId, relationCount, queryRunner).transform(raw, mainAlias);
return result[0];
},
selectAliasColumnNames(queryBuilder, builder) {
let selectOrAddSelect = (selection: string, selectionAliasName?: string) => {
selectOrAddSelect = (selection, selectionAliasName) => builder.addSelect(selection, selectionAliasName);
return builder.select(selection, selectionAliasName);
};
for (const columnName of this.createTableColumnNamesWithPrimaryKey(queryBuilder)) {
selectOrAddSelect(`${builder.alias}.${columnName}`, `${builder.alias}_${columnName}`);
}
},
useQueryRunner(mode) {
if (this.queryRunner?.getReplicationMode() === mode) {
return {
value: this.queryRunner,
[Symbol.asyncDispose]() {
return Promise.resolve();
},
};
}
const queryRunner = this.manager.connection.createQueryRunner(mode);
return {
value: queryRunner,
[Symbol.asyncDispose]() {
return queryRunner.release();
},
};
},
} satisfies MiRepository<ObjectLiteral>;
export {
MiAbuseUserReport,
@@ -143,70 +241,70 @@ export {
MiReversiGame,
};
export type AbuseUserReportsRepository = Repository<MiAbuseUserReport>;
export type AccessTokensRepository = Repository<MiAccessToken>;
export type AdsRepository = Repository<MiAd>;
export type AnnouncementsRepository = Repository<MiAnnouncement>;
export type AnnouncementReadsRepository = Repository<MiAnnouncementRead>;
export type AntennasRepository = Repository<MiAntenna>;
export type AppsRepository = Repository<MiApp>;
export type AvatarDecorationsRepository = Repository<MiAvatarDecoration>;
export type AuthSessionsRepository = Repository<MiAuthSession>;
export type BlockingsRepository = Repository<MiBlocking>;
export type ChannelFollowingsRepository = Repository<MiChannelFollowing>;
export type ChannelFavoritesRepository = Repository<MiChannelFavorite>;
export type ClipsRepository = Repository<MiClip>;
export type ClipNotesRepository = Repository<MiClipNote>;
export type ClipFavoritesRepository = Repository<MiClipFavorite>;
export type DriveFilesRepository = Repository<MiDriveFile>;
export type DriveFoldersRepository = Repository<MiDriveFolder>;
export type EmojisRepository = Repository<MiEmoji>;
export type FollowingsRepository = Repository<MiFollowing>;
export type FollowRequestsRepository = Repository<MiFollowRequest>;
export type GalleryLikesRepository = Repository<MiGalleryLike>;
export type GalleryPostsRepository = Repository<MiGalleryPost>;
export type HashtagsRepository = Repository<MiHashtag>;
export type InstancesRepository = Repository<MiInstance>;
export type MetasRepository = Repository<MiMeta>;
export type ModerationLogsRepository = Repository<MiModerationLog>;
export type MutingsRepository = Repository<MiMuting>;
export type RenoteMutingsRepository = Repository<MiRenoteMuting>;
export type NotesRepository = Repository<MiNote>;
export type NoteFavoritesRepository = Repository<MiNoteFavorite>;
export type NoteReactionsRepository = Repository<MiNoteReaction>;
export type NoteThreadMutingsRepository = Repository<MiNoteThreadMuting>;
export type NoteUnreadsRepository = Repository<MiNoteUnread>;
export type PagesRepository = Repository<MiPage>;
export type PageLikesRepository = Repository<MiPageLike>;
export type PasswordResetRequestsRepository = Repository<MiPasswordResetRequest>;
export type PollsRepository = Repository<MiPoll>;
export type PollVotesRepository = Repository<MiPollVote>;
export type PromoNotesRepository = Repository<MiPromoNote>;
export type PromoReadsRepository = Repository<MiPromoRead>;
export type RegistrationTicketsRepository = Repository<MiRegistrationTicket>;
export type RegistryItemsRepository = Repository<MiRegistryItem>;
export type RelaysRepository = Repository<MiRelay>;
export type SigninsRepository = Repository<MiSignin>;
export type SwSubscriptionsRepository = Repository<MiSwSubscription>;
export type UsedUsernamesRepository = Repository<MiUsedUsername>;
export type UsersRepository = Repository<MiUser>;
export type UserIpsRepository = Repository<MiUserIp>;
export type UserKeypairsRepository = Repository<MiUserKeypair>;
export type UserListsRepository = Repository<MiUserList>;
export type UserListFavoritesRepository = Repository<MiUserListFavorite>;
export type UserListMembershipsRepository = Repository<MiUserListMembership>;
export type UserNotePiningsRepository = Repository<MiUserNotePining>;
export type UserPendingsRepository = Repository<MiUserPending>;
export type UserProfilesRepository = Repository<MiUserProfile>;
export type UserPublickeysRepository = Repository<MiUserPublickey>;
export type UserSecurityKeysRepository = Repository<MiUserSecurityKey>;
export type WebhooksRepository = Repository<MiWebhook>;
export type ChannelsRepository = Repository<MiChannel>;
export type RetentionAggregationsRepository = Repository<MiRetentionAggregation>;
export type RolesRepository = Repository<MiRole>;
export type RoleAssignmentsRepository = Repository<MiRoleAssignment>;
export type FlashsRepository = Repository<MiFlash>;
export type FlashLikesRepository = Repository<MiFlashLike>;
export type UserMemoRepository = Repository<MiUserMemo>;
export type BubbleGameRecordsRepository = Repository<MiBubbleGameRecord>;
export type ReversiGamesRepository = Repository<MiReversiGame>;
export type AbuseUserReportsRepository = Repository<MiAbuseUserReport> & MiRepository<MiAbuseUserReport>;
export type AccessTokensRepository = Repository<MiAccessToken> & MiRepository<MiAccessToken>;
export type AdsRepository = Repository<MiAd> & MiRepository<MiAd>;
export type AnnouncementsRepository = Repository<MiAnnouncement> & MiRepository<MiAnnouncement>;
export type AnnouncementReadsRepository = Repository<MiAnnouncementRead> & MiRepository<MiAnnouncementRead>;
export type AntennasRepository = Repository<MiAntenna> & MiRepository<MiAntenna>;
export type AppsRepository = Repository<MiApp> & MiRepository<MiApp>;
export type AvatarDecorationsRepository = Repository<MiAvatarDecoration> & MiRepository<MiAvatarDecoration>;
export type AuthSessionsRepository = Repository<MiAuthSession> & MiRepository<MiAuthSession>;
export type BlockingsRepository = Repository<MiBlocking> & MiRepository<MiBlocking>;
export type ChannelFollowingsRepository = Repository<MiChannelFollowing> & MiRepository<MiChannelFollowing>;
export type ChannelFavoritesRepository = Repository<MiChannelFavorite> & MiRepository<MiChannelFavorite>;
export type ClipsRepository = Repository<MiClip> & MiRepository<MiClip>;
export type ClipNotesRepository = Repository<MiClipNote> & MiRepository<MiClipNote>;
export type ClipFavoritesRepository = Repository<MiClipFavorite> & MiRepository<MiClipFavorite>;
export type DriveFilesRepository = Repository<MiDriveFile> & MiRepository<MiDriveFile>;
export type DriveFoldersRepository = Repository<MiDriveFolder> & MiRepository<MiDriveFolder>;
export type EmojisRepository = Repository<MiEmoji> & MiRepository<MiEmoji>;
export type FollowingsRepository = Repository<MiFollowing> & MiRepository<MiFollowing>;
export type FollowRequestsRepository = Repository<MiFollowRequest> & MiRepository<MiFollowRequest>;
export type GalleryLikesRepository = Repository<MiGalleryLike> & MiRepository<MiGalleryLike>;
export type GalleryPostsRepository = Repository<MiGalleryPost> & MiRepository<MiGalleryPost>;
export type HashtagsRepository = Repository<MiHashtag> & MiRepository<MiHashtag>;
export type InstancesRepository = Repository<MiInstance> & MiRepository<MiInstance>;
export type MetasRepository = Repository<MiMeta> & MiRepository<MiMeta>;
export type ModerationLogsRepository = Repository<MiModerationLog> & MiRepository<MiModerationLog>;
export type MutingsRepository = Repository<MiMuting> & MiRepository<MiMuting>;
export type RenoteMutingsRepository = Repository<MiRenoteMuting> & MiRepository<MiRenoteMuting>;
export type NotesRepository = Repository<MiNote> & MiRepository<MiNote>;
export type NoteFavoritesRepository = Repository<MiNoteFavorite> & MiRepository<MiNoteFavorite>;
export type NoteReactionsRepository = Repository<MiNoteReaction> & MiRepository<MiNoteReaction>;
export type NoteThreadMutingsRepository = Repository<MiNoteThreadMuting> & MiRepository<MiNoteThreadMuting>;
export type NoteUnreadsRepository = Repository<MiNoteUnread> & MiRepository<MiNoteUnread>;
export type PagesRepository = Repository<MiPage> & MiRepository<MiPage>;
export type PageLikesRepository = Repository<MiPageLike> & MiRepository<MiPageLike>;
export type PasswordResetRequestsRepository = Repository<MiPasswordResetRequest> & MiRepository<MiPasswordResetRequest>;
export type PollsRepository = Repository<MiPoll> & MiRepository<MiPoll>;
export type PollVotesRepository = Repository<MiPollVote> & MiRepository<MiPollVote>;
export type PromoNotesRepository = Repository<MiPromoNote> & MiRepository<MiPromoNote>;
export type PromoReadsRepository = Repository<MiPromoRead> & MiRepository<MiPromoRead>;
export type RegistrationTicketsRepository = Repository<MiRegistrationTicket> & MiRepository<MiRegistrationTicket>;
export type RegistryItemsRepository = Repository<MiRegistryItem> & MiRepository<MiRegistryItem>;
export type RelaysRepository = Repository<MiRelay> & MiRepository<MiRelay>;
export type SigninsRepository = Repository<MiSignin> & MiRepository<MiSignin>;
export type SwSubscriptionsRepository = Repository<MiSwSubscription> & MiRepository<MiSwSubscription>;
export type UsedUsernamesRepository = Repository<MiUsedUsername> & MiRepository<MiUsedUsername>;
export type UsersRepository = Repository<MiUser> & MiRepository<MiUser>;
export type UserIpsRepository = Repository<MiUserIp> & MiRepository<MiUserIp>;
export type UserKeypairsRepository = Repository<MiUserKeypair> & MiRepository<MiUserKeypair>;
export type UserListsRepository = Repository<MiUserList> & MiRepository<MiUserList>;
export type UserListFavoritesRepository = Repository<MiUserListFavorite> & MiRepository<MiUserListFavorite>;
export type UserListMembershipsRepository = Repository<MiUserListMembership> & MiRepository<MiUserListMembership>;
export type UserNotePiningsRepository = Repository<MiUserNotePining> & MiRepository<MiUserNotePining>;
export type UserPendingsRepository = Repository<MiUserPending> & MiRepository<MiUserPending>;
export type UserProfilesRepository = Repository<MiUserProfile> & MiRepository<MiUserProfile>;
export type UserPublickeysRepository = Repository<MiUserPublickey> & MiRepository<MiUserPublickey>;
export type UserSecurityKeysRepository = Repository<MiUserSecurityKey> & MiRepository<MiUserSecurityKey>;
export type WebhooksRepository = Repository<MiWebhook> & MiRepository<MiWebhook>;
export type ChannelsRepository = Repository<MiChannel> & MiRepository<MiChannel>;
export type RetentionAggregationsRepository = Repository<MiRetentionAggregation> & MiRepository<MiRetentionAggregation>;
export type RolesRepository = Repository<MiRole> & MiRepository<MiRole>;
export type RoleAssignmentsRepository = Repository<MiRoleAssignment> & MiRepository<MiRoleAssignment>;
export type FlashsRepository = Repository<MiFlash> & MiRepository<MiFlash>;
export type FlashLikesRepository = Repository<MiFlashLike> & MiRepository<MiFlashLike>;
export type UserMemoRepository = Repository<MiUserMemo> & MiRepository<MiUserMemo>;
export type BubbleGameRecordsRepository = Repository<MiBubbleGameRecord> & MiRepository<MiBubbleGameRecord>;
export type ReversiGamesRepository = Repository<MiReversiGame> & MiRepository<MiReversiGame>;

View File

@@ -95,5 +95,10 @@ export const packedAntennaSchema = {
optional: false, nullable: false,
default: false,
},
notify: {
type: 'boolean',
optional: false, nullable: false,
default: false,
},
},
} as const;

View File

@@ -227,6 +227,10 @@ export const packedMetaLiteSchema = {
type: 'string',
optional: false, nullable: true,
},
inquiryUrl: {
type: 'string',
optional: false, nullable: true,
},
serverRules: {
type: 'array',
optional: false, nullable: false,

View File

@@ -76,7 +76,7 @@ export class ImportAntennasProcessorService {
this.logger.warn('Validation Failed');
continue;
}
const result = await this.antennasRepository.insert({
const result = await this.antennasRepository.insertOne({
id: this.idService.gen(now.getTime()),
lastUsedAt: now,
userId: job.data.user.id,
@@ -91,7 +91,7 @@ export class ImportAntennasProcessorService {
excludeBots: antenna.excludeBots,
withReplies: antenna.withReplies,
withFile: antenna.withFile,
}).then(x => this.antennasRepository.findOneByOrFail(x.identifiers[0]));
});
this.logger.succ('Antenna created: ' + result.id);
this.globalEventService.publishInternalEvent('antennaCreated', result);
}

View File

@@ -79,11 +79,11 @@ export class ImportUserListsProcessorService {
});
if (list == null) {
list = await this.userListsRepository.insert({
list = await this.userListsRepository.insertOne({
id: this.idService.gen(),
userId: user.id,
name: listName,
}).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0]));
});
}
let target = this.utilityService.isSelfHost(host!) ? await this.usersRepository.findOneBy({

View File

@@ -37,12 +37,12 @@ export class NodeinfoServerService {
@bindThis
public getLinks() {
return [{
rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1',
href: this.config.url + nodeinfo2_1path
}, {
rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0',
href: this.config.url + nodeinfo2_0path,
}];
rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1',
href: this.config.url + nodeinfo2_1path,
}, {
rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0',
href: this.config.url + nodeinfo2_0path,
}];
}
@bindThis
@@ -108,6 +108,7 @@ export class NodeinfoServerService {
langs: meta.langs,
tosUrl: meta.termsOfServiceUrl,
privacyPolicyUrl: meta.privacyPolicyUrl,
inquiryUrl: meta.inquiryUrl,
impressumUrl: meta.impressumUrl,
repositoryUrl: meta.repositoryUrl,
feedbackUrl: meta.feedbackUrl,

View File

@@ -29,13 +29,13 @@ export class SigninService {
public signin(request: FastifyRequest, reply: FastifyReply, user: MiLocalUser) {
setImmediate(async () => {
// Append signin history
const record = await this.signinsRepository.insert({
const record = await this.signinsRepository.insertOne({
id: this.idService.gen(),
userId: user.id,
ip: request.ip,
headers: request.headers as any,
success: true,
}).then(x => this.signinsRepository.findOneByOrFail(x.identifiers[0]));
});
// Publish signin event
this.globalEventService.publishMainStream(user.id, 'signin', await this.signinEntityService.pack(record));

View File

@@ -183,13 +183,13 @@ export class SignupApiService {
const salt = await bcrypt.genSalt(8);
const hash = await bcrypt.hash(password, salt);
const pendingUser = await this.userPendingsRepository.insert({
const pendingUser = await this.userPendingsRepository.insertOne({
id: this.idService.gen(),
code,
email: emailAddress!,
username: username,
password: hash,
}).then(x => this.userPendingsRepository.findOneByOrFail(x.identifiers[0]));
});
const link = `${this.config.url}/signup-complete/${code}`;

View File

@@ -50,7 +50,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
const ad = await this.adsRepository.insert({
const ad = await this.adsRepository.insertOne({
id: this.idService.gen(),
expiresAt: new Date(ps.expiresAt),
startsAt: new Date(ps.startsAt),
@@ -61,7 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
ratio: ps.ratio,
place: ps.place,
memo: ps.memo,
}).then(r => this.adsRepository.findOneByOrFail({ id: r.identifiers[0].id }));
});
this.moderationLogService.log(me, 'createAd', {
adId: ad.id,

View File

@@ -66,11 +66,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const ticketsPromises = [];
for (let i = 0; i < ps.count; i++) {
ticketsPromises.push(this.registrationTicketsRepository.insert({
ticketsPromises.push(this.registrationTicketsRepository.insertOne({
id: this.idService.gen(),
expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null,
code: generateInviteCode(),
}).then(x => this.registrationTicketsRepository.findOneByOrFail(x.identifiers[0])));
}));
}
const tickets = await Promise.all(ticketsPromises);

View File

@@ -427,6 +427,10 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
inquiryUrl: {
type: 'string',
optional: false, nullable: true,
},
repositoryUrl: {
type: 'string',
optional: false, nullable: true,
@@ -513,6 +517,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
feedbackUrl: instance.feedbackUrl,
impressumUrl: instance.impressumUrl,
privacyPolicyUrl: instance.privacyPolicyUrl,
inquiryUrl: instance.inquiryUrl,
disableRegistration: instance.disableRegistration,
emailRequiredForSignup: instance.emailRequiredForSignup,
enableHcaptcha: instance.enableHcaptcha,

View File

@@ -89,10 +89,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.limit(ps.limit)
.getMany();
const _users = assigns.map(({ user, userId }) => user ?? userId);
const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailed' })
.then(users => new Map(users.map(u => [u.id, u])));
return await Promise.all(assigns.map(async assign => ({
id: assign.id,
createdAt: this.idService.parse(assign.id).date.toISOString(),
user: await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }),
user: _userMap.get(assign.userId) ?? await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }),
expiresAt: assign.expiresAt?.toISOString() ?? null,
})));
});

View File

@@ -107,6 +107,7 @@ export const paramDef = {
feedbackUrl: { type: 'string', nullable: true },
impressumUrl: { type: 'string', nullable: true },
privacyPolicyUrl: { type: 'string', nullable: true },
inquiryUrl: { type: 'string', nullable: true },
useObjectStorage: { type: 'boolean' },
objectStorageBaseUrl: { type: 'string', nullable: true },
objectStorageBucket: { type: 'string', nullable: true },
@@ -422,6 +423,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.privacyPolicyUrl = ps.privacyPolicyUrl;
}
if (ps.inquiryUrl !== undefined) {
set.inquiryUrl = ps.inquiryUrl;
}
if (ps.useObjectStorage !== undefined) {
set.useObjectStorage = ps.useObjectStorage;
}

View File

@@ -112,7 +112,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const now = new Date();
const antenna = await this.antennasRepository.insert({
const antenna = await this.antennasRepository.insertOne({
id: this.idService.gen(now.getTime()),
lastUsedAt: now,
userId: me.id,
@@ -127,7 +127,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
excludeBots: ps.excludeBots,
withReplies: ps.withReplies,
withFile: ps.withFile,
}).then(x => this.antennasRepository.findOneByOrFail(x.identifiers[0]));
});
this.globalEventService.publishInternalEvent('antennaCreated', antenna);

View File

@@ -54,7 +54,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const permission = unique(ps.permission.map(v => v.replace(/^(.+)(\/|-)(read|write)$/, '$3:$1')));
// Create account
const app = await this.appsRepository.insert({
const app = await this.appsRepository.insertOne({
id: this.idService.gen(),
userId: me ? me.id : null,
name: ps.name,
@@ -62,7 +62,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
permission,
callbackUrl: ps.callbackUrl,
secret: secret,
}).then(x => this.appsRepository.findOneByOrFail(x.identifiers[0]));
});
return await this.appEntityService.pack(app, null, {
detail: true,

View File

@@ -78,11 +78,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const token = randomUUID();
// Create session token document
const doc = await this.authSessionsRepository.insert({
const doc = await this.authSessionsRepository.insertOne({
id: this.idService.gen(),
appId: app.id,
token: token,
}).then(x => this.authSessionsRepository.findOneByOrFail(x.identifiers[0]));
});
return {
token: doc.token,

View File

@@ -80,7 +80,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
}
const channel = await this.channelsRepository.insert({
const channel = await this.channelsRepository.insertOne({
id: this.idService.gen(),
userId: me.id,
name: ps.name,
@@ -89,7 +89,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
isSensitive: ps.isSensitive ?? false,
...(ps.color !== undefined ? { color: ps.color } : {}),
allowRenoteToExternal: ps.allowRenoteToExternal ?? true,
} as MiChannel).then(x => this.channelsRepository.findOneByOrFail(x.identifiers[0]));
} as MiChannel);
return await this.channelEntityService.pack(channel, me);
});

View File

@@ -54,7 +54,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
folderId: ps.folderId ?? IsNull(),
});
return await Promise.all(files.map(file => this.driveFileEntityService.pack(file, { self: true })));
return await this.driveFileEntityService.packMany(files, { self: true });
});
}
}

View File

@@ -75,12 +75,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
// Create folder
const folder = await this.driveFoldersRepository.insert({
const folder = await this.driveFoldersRepository.insertOne({
id: this.idService.gen(),
name: ps.name,
parentId: parent !== null ? parent.id : null,
userId: me.id,
}).then(x => this.driveFoldersRepository.findOneByOrFail(x.identifiers[0]));
});
const folderObj = await this.driveFolderEntityService.pack(folder);

View File

@@ -59,7 +59,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const flash = await this.flashsRepository.insert({
const flash = await this.flashsRepository.insertOne({
id: this.idService.gen(),
userId: me.id,
updatedAt: new Date(),
@@ -68,7 +68,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
script: ps.script,
permissions: ps.permissions,
visibility: ps.visibility,
}).then(x => this.flashsRepository.findOneByOrFail(x.identifiers[0]));
});
return await this.flashEntityService.pack(flash);
});

View File

@@ -71,7 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.limit(ps.limit)
.getMany();
return await Promise.all(requests.map(req => this.followRequestEntityService.pack(req)));
return await this.followRequestEntityService.packMany(requests, me);
});
}
}

View File

@@ -76,7 +76,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new Error();
}
const post = await this.galleryPostsRepository.insert(new MiGalleryPost({
const post = await this.galleryPostsRepository.insertOne(new MiGalleryPost({
id: this.idService.gen(),
updatedAt: new Date(),
title: ps.title,
@@ -84,7 +84,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
userId: me.id,
isSensitive: ps.isSensitive,
fileIds: files.map(file => file.id),
})).then(x => this.galleryPostsRepository.findOneByOrFail(x.identifiers[0]));
}));
return await this.galleryPostEntityService.pack(post, me);
});

View File

@@ -89,14 +89,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.tooManyWebhooks);
}
const webhook = await this.webhooksRepository.insert({
const webhook = await this.webhooksRepository.insertOne({
id: this.idService.gen(),
userId: me.id,
name: ps.name,
url: ps.url,
secret: ps.secret,
on: ps.on,
}).then(x => this.webhooksRepository.findOneByOrFail(x.identifiers[0]));
});
this.globalEventService.publishInternalEvent('webhookCreated', webhook);

View File

@@ -66,13 +66,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
}
const ticket = await this.registrationTicketsRepository.insert({
const ticket = await this.registrationTicketsRepository.insertOne({
id: this.idService.gen(),
createdBy: me,
createdById: me.id,
expiresAt: policies.inviteExpirationTime ? new Date(Date.now() + (policies.inviteExpirationTime * 1000 * 60)) : null,
code: generateInviteCode(),
}).then(x => this.registrationTicketsRepository.findOneByOrFail(x.identifiers[0]));
});
return await this.inviteCodeEntityService.pack(ticket, me);
});

View File

@@ -144,12 +144,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
// Create vote
const vote = await this.pollVotesRepository.insert({
const vote = await this.pollVotesRepository.insertOne({
id: this.idService.gen(createdAt.getTime()),
noteId: note.id,
userId: me.id,
choice: ps.choice,
}).then(x => this.pollVotesRepository.findOneByOrFail(x.identifiers[0]));
});
// Increment votes count
const index = ps.choice + 1; // In SQL, array index is 1 based

View File

@@ -76,7 +76,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const reactions = await query.limit(ps.limit).getMany();
return await Promise.all(reactions.map(reaction => this.noteReactionEntityService.pack(reaction, me)));
return await this.noteReactionEntityService.packMany(reactions, me);
});
}
}

View File

@@ -102,7 +102,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
});
const page = await this.pagesRepository.insert(new MiPage({
const page = await this.pagesRepository.insertOne(new MiPage({
id: this.idService.gen(),
updatedAt: new Date(),
title: ps.title,
@@ -117,7 +117,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
alignCenter: ps.alignCenter,
hideTitleWhenPinned: ps.hideTitleWhenPinned,
font: ps.font,
})).then(x => this.pagesRepository.findOneByOrFail(x.identifiers[0]));
}));
return await this.pageEntityService.pack(page);
});

View File

@@ -92,9 +92,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.limit(ps.limit)
.getMany();
const _users = assigns.map(({ user, userId }) => user ?? userId);
const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailed' })
.then(users => new Map(users.map(u => [u.id, u])));
return await Promise.all(assigns.map(async assign => ({
id: assign.id,
user: await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }),
user: _userMap.get(assign.userId) ?? await this.userEntityService.pack(assign.user!, me, { schema: 'UserDetailed' }),
})));
});
}

View File

@@ -118,12 +118,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const repliedUsersSorted = Object.keys(repliedUsers).sort((a, b) => repliedUsers[b] - repliedUsers[a]);
// Extract top replied users
const topRepliedUsers = repliedUsersSorted.slice(0, ps.limit);
const topRepliedUserIds = repliedUsersSorted.slice(0, ps.limit);
// Make replies object (includes weights)
const repliesObj = await Promise.all(topRepliedUsers.map(async (user) => ({
user: await this.userEntityService.pack(user, me, { schema: 'UserDetailed' }),
weight: repliedUsers[user] / peak,
const _userMap = await this.userEntityService.packMany(topRepliedUserIds, me, { schema: 'UserDetailed' })
.then(users => new Map(users.map(u => [u.id, u])));
const repliesObj = await Promise.all(topRepliedUserIds.map(async (userId) => ({
user: _userMap.get(userId) ?? await this.userEntityService.pack(userId, me, { schema: 'UserDetailed' }),
weight: repliedUsers[userId] / peak,
})));
return repliesObj;

View File

@@ -104,11 +104,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.tooManyUserLists);
}
const userList = await this.userListsRepository.insert({
const userList = await this.userListsRepository.insertOne({
id: this.idService.gen(),
userId: me.id,
name: ps.name,
} as MiUserList).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0]));
} as MiUserList);
const users = (await this.userListMembershipsRepository.findBy({
userListId: ps.listId,

View File

@@ -65,11 +65,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.tooManyUserLists);
}
const userList = await this.userListsRepository.insert({
const userList = await this.userListsRepository.insertOne({
id: this.idService.gen(),
userId: me.id,
name: ps.name,
} as MiUserList).then(x => this.userListsRepository.findOneByOrFail(x.identifiers[0]));
} as MiUserList);
return await this.userListEntityService.pack(userList);
});

View File

@@ -82,14 +82,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.cannotReportAdmin);
}
const report = await this.abuseUserReportsRepository.insert({
const report = await this.abuseUserReportsRepository.insertOne({
id: this.idService.gen(),
targetUserId: user.id,
targetUserHost: user.host,
reporterId: me.id,
reporterHost: null,
comment: ps.comment,
}).then(x => this.abuseUserReportsRepository.findOneByOrFail(x.identifiers[0]));
});
// Publish event to moderators
setImmediate(async () => {

View File

@@ -117,9 +117,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (user != null) _users.push(user);
}
return await Promise.all(_users.map(u => this.userEntityService.pack(u, me, {
schema: 'UserDetailed',
})));
const _userMap = await this.userEntityService.packMany(_users, me, { schema: 'UserDetailed' })
.then(users => new Map(users.map(u => [u.id, u])));
return _users.map(u => _userMap.get(u.id)!);
} else {
// Lookup user
if (typeof ps.host === 'string' && typeof ps.username === 'string') {

View File

@@ -157,6 +157,7 @@ describe('アンテナ', () => {
withReplies: false,
excludeBots: false,
localOnly: false,
notify: false,
};
assert.deepStrictEqual(response, expected);
});

View File

@@ -9,7 +9,7 @@ process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import { loadConfig } from '@/config.js';
import { MiUser, UsersRepository } from '@/models/_.js';
import { MiRepository, MiUser, UsersRepository, miRepository } from '@/models/_.js';
import { secureRndstr } from '@/misc/secure-rndstr.js';
import { jobQueue } from '@/boot/common.js';
import { api, initTestDb, signup, sleep, successfulApiCall, uploadFile } from '../utils.js';
@@ -42,7 +42,7 @@ describe('Account Move', () => {
dave = await signup({ username: 'dave' });
eve = await signup({ username: 'eve' });
frank = await signup({ username: 'frank' });
Users = connection.getRepository(MiUser);
Users = connection.getRepository(MiUser).extend(miRepository as MiRepository<MiUser>);
}, 1000 * 60 * 2);
afterAll(async () => {

View File

@@ -0,0 +1,33 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import { ReversiMatchResponse } from 'misskey-js/entities.js';
import { api, signup } from '../utils.js';
import type * as misskey from 'misskey-js';
describe('ReversiGame', () => {
let alice: misskey.entities.SignupResponse;
let bob: misskey.entities.SignupResponse;
beforeAll(async () => {
alice = await signup({ username: 'alice' });
bob = await signup({ username: 'bob' });
}, 1000 * 60 * 2);
test('matches when alice invites bob and bob accepts', async () => {
const response1 = await api('reversi/match', { userId: bob.id }, alice);
assert.strictEqual(response1.status, 204);
assert.strictEqual(response1.body, null);
const response2 = await api('reversi/match', { userId: alice.id }, bob);
assert.strictEqual(response2.status, 200);
assert.notStrictEqual(response2.body, null);
const body = response2.body as ReversiMatchResponse;
assert.strictEqual(body.user1.id, alice.id);
assert.strictEqual(body.user2.id, bob.id);
});
});

View File

@@ -4,77 +4,81 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkModalWindow ref="dialogEl" @close="cancel()" @closed="$emit('closed')">
<template #header>:{{ emoji.name }}:</template>
<template #default>
<MkSpacer>
<div style="display: flex; flex-direction: column; gap: 1em;">
<div :class="$style.emojiImgWrapper">
<MkCustomEmoji :name="emoji.name" :normal="true" :useOriginalSize="true" style="height: 100%;"></MkCustomEmoji>
</div>
<MkKeyValue :copy="`:${emoji.name}:`">
<template #key>{{ i18n.ts.name }}</template>
<template #value>{{ emoji.name }}</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.tags }}</template>
<template #value>
<div v-if="emoji.aliases.length === 0">{{ i18n.ts.none }}</div>
<div v-else :class="$style.aliases">
<span v-for="alias in emoji.aliases" :key="alias" :class="$style.alias">
{{ alias }}
</span>
</div>
</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.category }}</template>
<template #value>{{ emoji.category ?? i18n.ts.none }}</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.sensitive }}</template>
<template #value>{{ emoji.isSensitive ? i18n.ts.yes : i18n.ts.no }}</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.localOnly }}</template>
<template #value>{{ emoji.localOnly ? i18n.ts.yes : i18n.ts.no }}</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.license }}</template>
<template #value><Mfm :text="emoji.license ?? i18n.ts.none" /></template>
</MkKeyValue>
<MkKeyValue :copy="emoji.url">
<template #key>{{ i18n.ts.emojiUrl }}</template>
<template #value>
<MkLink :url="emoji.url" target="_blank">{{ emoji.url }}</MkLink>
</template>
</MkKeyValue>
</div>
</MkSpacer>
</template>
</MkModalWindow>
<MkModalWindow ref="dialogEl" @close="cancel()" @closed="$emit('closed')">
<template #header>:{{ emoji.name }}:</template>
<template #default>
<MkSpacer>
<div style="display: flex; flex-direction: column; gap: 1em;">
<div :class="$style.emojiImgWrapper">
<MkCustomEmoji :name="emoji.name" :normal="true" :useOriginalSize="true" style="height: 100%;"></MkCustomEmoji>
</div>
<MkKeyValue :copy="`:${emoji.name}:`">
<template #key>{{ i18n.ts.name }}</template>
<template #value>{{ emoji.name }}</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.tags }}</template>
<template #value>
<div v-if="emoji.aliases.length === 0">{{ i18n.ts.none }}</div>
<div v-else :class="$style.aliases">
<span v-for="alias in emoji.aliases" :key="alias" :class="$style.alias">
{{ alias }}
</span>
</div>
</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.category }}</template>
<template #value>{{ emoji.category ?? i18n.ts.none }}</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.sensitive }}</template>
<template #value>{{ emoji.isSensitive ? i18n.ts.yes : i18n.ts.no }}</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.localOnly }}</template>
<template #value>{{ emoji.localOnly ? i18n.ts.yes : i18n.ts.no }}</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.license }}</template>
<template #value><Mfm :text="emoji.license ?? i18n.ts.none"/></template>
</MkKeyValue>
<MkKeyValue :copy="emoji.url">
<template #key>{{ i18n.ts.emojiUrl }}</template>
<template #value>
<MkLink :url="emoji.url" target="_blank">{{ emoji.url }}</MkLink>
</template>
</MkKeyValue>
</div>
</MkSpacer>
</template>
</MkModalWindow>
</template>
<script lang="ts" setup>
import * as Misskey from 'misskey-js';
import { defineProps, shallowRef } from 'vue';
import MkLink from '@/components/MkLink.vue';
import { i18n } from '@/i18n.js';
import MkModalWindow from '@/components/MkModalWindow.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import MkLink from './MkLink.vue';
const props = defineProps<{
emoji: Misskey.entities.EmojiDetailed,
}>();
const emit = defineEmits<{
(ev: 'ok', cropped: Misskey.entities.DriveFile): void;
(ev: 'cancel'): void;
(ev: 'closed'): void;
}>();
const dialogEl = shallowRef<InstanceType<typeof MkModalWindow>>();
const cancel = () => {
function cancel() {
emit('cancel');
dialogEl.value!.close();
};
}
</script>
<style lang="scss" module>

View File

@@ -12,10 +12,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<img :src="instance.iconUrl || '/favicon.ico'" alt="" class="icon"/>
</div>
<MkInfo v-if="thereIsUnresolvedAbuseReport" warn class="info">{{ i18n.ts.thereIsUnresolvedAbuseReportWarning }} <MkA to="/admin/abuses" class="_link">{{ i18n.ts.check }}</MkA></MkInfo>
<MkInfo v-if="noMaintainerInformation" warn class="info">{{ i18n.ts.noMaintainerInformationWarning }} <MkA to="/admin/settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
<MkInfo v-if="noBotProtection" warn class="info">{{ i18n.ts.noBotProtectionWarning }} <MkA to="/admin/security" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
<MkInfo v-if="noEmailServer" warn class="info">{{ i18n.ts.noEmailServerWarning }} <MkA to="/admin/email-settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
<div class="_gaps_s">
<MkInfo v-if="thereIsUnresolvedAbuseReport" warn>{{ i18n.ts.thereIsUnresolvedAbuseReportWarning }} <MkA to="/admin/abuses" class="_link">{{ i18n.ts.check }}</MkA></MkInfo>
<MkInfo v-if="noMaintainerInformation" warn>{{ i18n.ts.noMaintainerInformationWarning }} <MkA to="/admin/settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
<MkInfo v-if="noInquiryUrl" warn>{{ i18n.ts.noInquiryUrlWarning }} <MkA to="/admin/moderation" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
<MkInfo v-if="noBotProtection" warn>{{ i18n.ts.noBotProtectionWarning }} <MkA to="/admin/security" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
<MkInfo v-if="noEmailServer" warn>{{ i18n.ts.noEmailServerWarning }} <MkA to="/admin/email-settings" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
</div>
<MkSuperMenu :def="menuDef" :grid="narrow"></MkSuperMenu>
</div>
@@ -61,6 +64,7 @@ const pageProps = ref({});
let noMaintainerInformation = isEmpty(instance.maintainerName) || isEmpty(instance.maintainerEmail);
let noBotProtection = !instance.disableRegistration && !instance.enableHcaptcha && !instance.enableRecaptcha && !instance.enableTurnstile;
let noEmailServer = !instance.enableEmail;
let noInquiryUrl = isEmpty(instance.inquiryUrl);
const thereIsUnresolvedAbuseReport = ref(false);
const currentPage = computed(() => router.currentRef.value.child);
@@ -348,10 +352,6 @@ defineExpose({
> .nav {
.lxpfedzu {
> .info {
margin: 16px 0;
}
> .banner {
margin: 16px;

View File

@@ -30,6 +30,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.privacyPolicyUrl }}</template>
</MkInput>
<MkInput v-model="inquiryUrl" type="url">
<template #prefix><i class="ti ti-link"></i></template>
<template #label>{{ i18n.ts._serverSettings.inquiryUrl }}</template>
<template #caption>{{ i18n.ts._serverSettings.inquiryUrlDescription }}</template>
</MkInput>
<MkTextarea v-model="preservedUsernames">
<template #label>{{ i18n.ts.preservedUsernames }}</template>
<template #caption>{{ i18n.ts.preservedUsernamesDescription }}</template>
@@ -86,6 +92,7 @@ const hiddenTags = ref<string>('');
const preservedUsernames = ref<string>('');
const tosUrl = ref<string | null>(null);
const privacyPolicyUrl = ref<string | null>(null);
const inquiryUrl = ref<string | null>(null);
async function init() {
const meta = await misskeyApi('admin/meta');
@@ -97,6 +104,7 @@ async function init() {
preservedUsernames.value = meta.preservedUsernames.join('\n');
tosUrl.value = meta.tosUrl;
privacyPolicyUrl.value = meta.privacyPolicyUrl;
inquiryUrl.value = meta.inquiryUrl;
}
function save() {
@@ -105,6 +113,7 @@ function save() {
emailRequiredForSignup: emailRequiredForSignup.value,
tosUrl: tosUrl.value,
privacyPolicyUrl: privacyPolicyUrl.value,
inquiryUrl: inquiryUrl.value,
sensitiveWords: sensitiveWords.value.split('\n'),
prohibitedWords: prohibitedWords.value.split('\n'),
hiddenTags: hiddenTags.value.split('\n'),

View File

@@ -7,7 +7,21 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkStickyContainer>
<template #header><MkPageHeader/></template>
<MkSpacer :contentMax="600" :marginMin="20">
<div>{{ instance.maintainerEmail }}</div>
<div class="_gaps">
<MkKeyValue>
<template #key>{{ i18n.ts.inquiry }}</template>
<template #value>
<MkLink :url="instance.inquiryUrl" target="_blank">{{ instance.inquiryUrl }}</MkLink>
</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.email }}</template>
<template #value>
<div>{{ instance.maintainerEmail }}</div>
</template>
</MkKeyValue>
</div>
</MkSpacer>
</MkStickyContainer>
</template>
@@ -16,6 +30,8 @@ SPDX-License-Identifier: AGPL-3.0-only
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { instance } from '@/instance.js';
import MkKeyValue from '@/components/MkKeyValue.vue';
import MkLink from '@/components/MkLink.vue';
definePageMetadata(() => ({
title: i18n.ts.inquiry,

View File

@@ -6,15 +6,16 @@
import * as Misskey from 'misskey-js';
export function shouldCollapsed(note: Misskey.entities.Note, urls: string[]): boolean {
const collapsed = note.cw == null && note.text != null && (
(note.text.includes('$[x2')) ||
(note.text.includes('$[x3')) ||
(note.text.includes('$[x4')) ||
(note.text.includes('$[scale')) ||
(note.text.split('\n').length > 9) ||
(note.text.length > 500) ||
(note.files.length >= 5) ||
(urls.length >= 4)
const collapsed = note.cw == null && (
note.text != null && (
(note.text.includes('$[x2')) ||
(note.text.includes('$[x3')) ||
(note.text.includes('$[x4')) ||
(note.text.includes('$[scale')) ||
(note.text.split('\n').length > 9) ||
(note.text.length > 500) ||
(urls.length >= 4)
) || note.files.length >= 5
);
return collapsed;

View File

@@ -1,8 +1,9 @@
{
"type": "module",
"name": "misskey-js",
"version": "2024.5.0-rc.6",
"version": "2024.5.0",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",
"types": "./built/index.d.ts",
"exports": {
@@ -30,7 +31,8 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/misskey-dev/misskey.js.git"
"url": "https://github.com/misskey-dev/misskey.git",
"directory": "packages/misskey-js"
},
"devDependencies": {
"@microsoft/api-extractor": "7.43.1",

View File

@@ -4449,6 +4449,8 @@ export type components = {
isActive: boolean;
/** @default false */
hasUnreadNote: boolean;
/** @default false */
notify: boolean;
};
Clip: {
/**
@@ -4831,6 +4833,7 @@ export type components = {
impressumUrl: string | null;
logoImageUrl: string | null;
privacyPolicyUrl: string | null;
inquiryUrl: string | null;
serverRules: string[];
themeColor: string | null;
policies: components['schemas']['RolePolicies'];
@@ -4978,6 +4981,7 @@ export type operations = {
shortName: string | null;
objectStorageS3ForcePathStyle: boolean;
privacyPolicyUrl: string | null;
inquiryUrl: string | null;
repositoryUrl: string | null;
/**
* @deprecated
@@ -8906,6 +8910,7 @@ export type operations = {
feedbackUrl?: string | null;
impressumUrl?: string | null;
privacyPolicyUrl?: string | null;
inquiryUrl?: string | null;
useObjectStorage?: boolean;
objectStorageBaseUrl?: string | null;
objectStorageBucket?: string | null;