Compare commits
35 Commits
13.14.0-be
...
13.14.0-be
Author | SHA1 | Date | |
---|---|---|---|
![]() |
54625914c5 | ||
![]() |
8f196fc67c | ||
![]() |
52b440357f | ||
![]() |
866e3f0432 | ||
![]() |
9e330c9e38 | ||
![]() |
02957a1b5d | ||
![]() |
1c82e97350 | ||
![]() |
2b6dbd4fcb | ||
![]() |
c0dbc3b53f | ||
![]() |
af30959cb9 | ||
![]() |
35ffe3c52f | ||
![]() |
c13fd42015 | ||
![]() |
a72ef70770 | ||
![]() |
9a391bd863 | ||
![]() |
12456b22c5 | ||
![]() |
e35a370af0 | ||
![]() |
b6a432fd7b | ||
![]() |
cd9affd568 | ||
![]() |
b97694b083 | ||
![]() |
9845ccec5b | ||
![]() |
cf3e39178b | ||
![]() |
48d3341462 | ||
![]() |
791ae608a5 | ||
![]() |
f4d1fcaf67 | ||
![]() |
239ea39d6f | ||
![]() |
63e21a4ee3 | ||
![]() |
1a096c557e | ||
![]() |
53b1684c39 | ||
![]() |
9dd53527ca | ||
![]() |
59046d583d | ||
![]() |
6ba9805536 | ||
![]() |
5059d4d7e1 | ||
![]() |
74a05ec739 | ||
![]() |
60366a4558 | ||
![]() |
7ec07d5fd2 |
@@ -104,6 +104,7 @@ redis:
|
||||
# apiKey: ''
|
||||
# ssl: true
|
||||
# index: ''
|
||||
# scope: local
|
||||
|
||||
# ┌───────────────┐
|
||||
#───┘ ID generation └───────────────────────────────────────────
|
||||
|
13
CHANGELOG.md
13
CHANGELOG.md
@@ -17,6 +17,10 @@
|
||||
### General
|
||||
- identicon生成を無効にしてパフォーマンスを向上させることができるようになりました
|
||||
- サーバーのマシン情報の公開を無効にしてパフォーマンスを向上させることができるようになりました
|
||||
- 招待機能を改善しました
|
||||
* 過去に発行した招待コードを確認できるようになりました
|
||||
* ロールごとに招待コードの発行数制限と制限対象期間、有効期限を設定できるようになりました
|
||||
* 招待コードを作成したユーザーと使用したユーザーを確認できるようになりました
|
||||
|
||||
### Client
|
||||
- deck UIのカラムのメニューからアンテナとリストの編集画面を開けるように
|
||||
@@ -26,6 +30,12 @@
|
||||
- 見たことのあるRenoteを省略して表示をオンのときに自分のnoteのrenoteを省略するように
|
||||
- フォルダーやファイルに対しても開発者モード使用時、IDをコピーできるように
|
||||
- 引用対象を「もっと見る」で展開した場合、「閉じる」で畳めるように
|
||||
- プロフィールURLをコピーできるボタンを追加 #11190
|
||||
- `CURRENT_URL`で現在表示中のURLを取得できるように(AiScript)
|
||||
- ユーザーのContextMenuに「アンテナに追加」ボタンを追加
|
||||
- フォローやお気に入り登録をしていないチャンネルを開く時は概要ページを開くように
|
||||
- 画面ビューワをタップした場合、マウスクリックと同様に画像ビューワを閉じるように
|
||||
- オフライン時の画面にリロードボタンを追加
|
||||
- Fix: サーバーメトリクスが90度傾いている
|
||||
- Fix: 非ログイン時にクレデンシャルが必要なページに行くとエラーが出る問題を修正
|
||||
- Fix: sparkle内にリンクを入れるとクリック不能になる問題の修正
|
||||
@@ -33,12 +43,15 @@
|
||||
- Fix: ページ遷移でスクロール位置が保持されない問題を修正
|
||||
- Fix: フォルダーのページネーションが機能しない #11180
|
||||
- Fix: 長い文章を投稿する際、プレビューが画面からはみ出る問題を修正
|
||||
- Fix: システムフォント設定が正しく反映されない問題を修正
|
||||
- Fix: アンケート終了時のプッシュ通知が正しく表示されない問題を修正
|
||||
|
||||
### Server
|
||||
- JSON.parse の回数を削減することで、ストリーミングのパフォーマンスを向上しました
|
||||
- nsfwjs のモデルロードを排他することで、重複ロードによってメモリ使用量が増加しないように
|
||||
- 連合の配送ジョブのパフォーマンスを向上(ロック機構の見直し、Redisキャッシュの活用)
|
||||
- 全体的なDBクエリのパフォーマンスを向上
|
||||
- featuredノートのsignedGet回数を減らしました
|
||||
|
||||
## 13.13.2
|
||||
|
||||
|
@@ -2,14 +2,14 @@
|
||||
* Gulp tasks
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const gulp = require('gulp');
|
||||
const replace = require('gulp-replace');
|
||||
const terser = require('gulp-terser');
|
||||
const cssnano = require('gulp-cssnano');
|
||||
import * as fs from 'node:fs';
|
||||
import gulp from 'gulp';
|
||||
import replace from 'gulp-replace';
|
||||
import terser from 'gulp-terser';
|
||||
import cssnano from 'gulp-cssnano';
|
||||
|
||||
const locales = require('./locales');
|
||||
const meta = require('./package.json');
|
||||
import locales from './locales/index.js';
|
||||
import meta from './package.json' assert { type: "json" };
|
||||
|
||||
gulp.task('copy:backend:views', () =>
|
||||
gulp.src('./packages/backend/src/server/web/views/**/*').pipe(gulp.dest('./packages/backend/built/server/web/views'))
|
@@ -1,6 +1,6 @@
|
||||
const fs = require('fs');
|
||||
const yaml = require('js-yaml');
|
||||
const ts = require('typescript');
|
||||
import * as fs from 'node:fs';
|
||||
import * as yaml from 'js-yaml';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
function createMembers(record) {
|
||||
return Object.entries(record)
|
||||
@@ -14,7 +14,7 @@ function createMembers(record) {
|
||||
));
|
||||
}
|
||||
|
||||
module.exports = function generateDTS() {
|
||||
export default function generateDTS() {
|
||||
const locale = yaml.load(fs.readFileSync(`${__dirname}/ja-JP.yml`, 'utf-8'));
|
||||
const members = createMembers(locale);
|
||||
const elements = [
|
||||
@@ -51,11 +51,7 @@ module.exports = function generateDTS() {
|
||||
ts.NodeFlags.Const | ts.NodeFlags.Ambient | ts.NodeFlags.ContextFlags,
|
||||
),
|
||||
),
|
||||
ts.factory.createExportAssignment(
|
||||
undefined,
|
||||
true,
|
||||
ts.factory.createIdentifier('locales'),
|
||||
),
|
||||
ts.factory.createExportDefault(ts.factory.createIdentifier('locales')),
|
||||
];
|
||||
const printed = ts.createPrinter({
|
||||
newLine: ts.NewLineKind.LineFeed,
|
||||
|
24
locales/index.d.ts
vendored
24
locales/index.d.ts
vendored
@@ -52,6 +52,7 @@ export interface Locale {
|
||||
"deleteAndEdit": string;
|
||||
"deleteAndEditConfirm": string;
|
||||
"addToList": string;
|
||||
"addToAntenna": string;
|
||||
"sendMessage": string;
|
||||
"copyRSS": string;
|
||||
"copyUsername": string;
|
||||
@@ -59,6 +60,7 @@ export interface Locale {
|
||||
"copyNoteId": string;
|
||||
"copyFileId": string;
|
||||
"copyFolderId": string;
|
||||
"copyProfileUrl": string;
|
||||
"searchUser": string;
|
||||
"reply": string;
|
||||
"loadMore": string;
|
||||
@@ -1073,6 +1075,23 @@ export interface Locale {
|
||||
"enableServerMachineStats": string;
|
||||
"enableIdenticonGeneration": string;
|
||||
"turnOffToImprovePerformance": string;
|
||||
"createInviteCode": string;
|
||||
"createWithOptions": string;
|
||||
"createCount": string;
|
||||
"inviteCodeCreated": string;
|
||||
"inviteLimitExceeded": string;
|
||||
"createLimitRemaining": string;
|
||||
"inviteLimitResetCycle": string;
|
||||
"expirationDate": string;
|
||||
"noExpirationDate": string;
|
||||
"inviteCodeUsedAt": string;
|
||||
"registeredUserUsingInviteCode": string;
|
||||
"waitingForMailAuth": string;
|
||||
"inviteCodeCreator": string;
|
||||
"usedAt": string;
|
||||
"unused": string;
|
||||
"used": string;
|
||||
"expired": string;
|
||||
"_initialAccountSetting": {
|
||||
"accountCreated": string;
|
||||
"letsStartAccountSetup": string;
|
||||
@@ -1463,6 +1482,9 @@ export interface Locale {
|
||||
"ltlAvailable": string;
|
||||
"canPublicNote": string;
|
||||
"canInvite": string;
|
||||
"inviteLimit": string;
|
||||
"inviteLimitCycle": string;
|
||||
"inviteExpirationTime": string;
|
||||
"canManageCustomEmojis": string;
|
||||
"driveCapacity": string;
|
||||
"alwaysMarkNsfw": string;
|
||||
@@ -2156,4 +2178,4 @@ export interface Locale {
|
||||
declare const locales: {
|
||||
[lang: string]: Locale;
|
||||
};
|
||||
export = locales;
|
||||
export default locales;
|
||||
|
@@ -2,8 +2,8 @@
|
||||
* Languages Loader
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const yaml = require('js-yaml');
|
||||
import * as fs from 'node:fs';
|
||||
import * as yaml from 'js-yaml';
|
||||
|
||||
const merge = (...args) => args.reduce((a, c) => ({
|
||||
...a,
|
||||
@@ -51,9 +51,9 @@ const primaries = {
|
||||
// 何故か文字列にバックスペース文字が混入することがあり、YAMLが壊れるので取り除く
|
||||
const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), '');
|
||||
|
||||
const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(`${__dirname}/${c}.yml`, 'utf-8'))) || {}, a), {});
|
||||
const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(new URL(`${c}.yml`, import.meta.url), 'utf-8'))) || {}, a), {});
|
||||
|
||||
module.exports = Object.entries(locales)
|
||||
export default Object.entries(locales)
|
||||
.reduce((a, [k ,v]) => (a[k] = (() => {
|
||||
const [lang] = k.split('-');
|
||||
switch (k) {
|
||||
|
@@ -49,6 +49,7 @@ delete: "削除"
|
||||
deleteAndEdit: "削除して編集"
|
||||
deleteAndEditConfirm: "このノートを削除してもう一度編集しますか?このノートへのリアクション、Renote、返信も全て削除されます。"
|
||||
addToList: "リストに追加"
|
||||
addToAntenna: "アンテナに追加"
|
||||
sendMessage: "メッセージを送信"
|
||||
copyRSS: "RSSをコピー"
|
||||
copyUsername: "ユーザー名をコピー"
|
||||
@@ -56,6 +57,7 @@ copyUserId: "ユーザーIDをコピー"
|
||||
copyNoteId: "ノートIDをコピー"
|
||||
copyFileId: "ファイルIDをコピー"
|
||||
copyFolderId: "フォルダーIDをコピー"
|
||||
copyProfileUrl: "プロフィールURLをコピー"
|
||||
searchUser: "ユーザーを検索"
|
||||
reply: "返信"
|
||||
loadMore: "もっと見る"
|
||||
@@ -1070,6 +1072,23 @@ branding: "ブランディング"
|
||||
enableServerMachineStats: "サーバーのマシン情報を公開する"
|
||||
enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする"
|
||||
turnOffToImprovePerformance: "オフにするとパフォーマンスが向上します。"
|
||||
createInviteCode: "招待コードを作成"
|
||||
createWithOptions: "オプションを指定して作成"
|
||||
createCount: "作成数"
|
||||
inviteCodeCreated: "招待コードを作成しました"
|
||||
inviteLimitExceeded: "作成できる招待コードの数が上限に達しています。"
|
||||
createLimitRemaining: "作成できる招待コード: 残り {limit} 個"
|
||||
inviteLimitResetCycle: "{time}で最大 {limit} 個の招待コードを作成できます。"
|
||||
expirationDate: "有効期限"
|
||||
noExpirationDate: "有効期限を設けない"
|
||||
inviteCodeUsedAt: "招待コードが使用された日時"
|
||||
registeredUserUsingInviteCode: "招待コードを使用したユーザー"
|
||||
waitingForMailAuth: "メール認証待ち"
|
||||
inviteCodeCreator: "招待コードを作成したユーザー"
|
||||
usedAt: "使用日時"
|
||||
unused: "未使用"
|
||||
used: "使用済み"
|
||||
expired: "期限切れ"
|
||||
|
||||
_initialAccountSetting:
|
||||
accountCreated: "アカウントの作成が完了しました!"
|
||||
@@ -1385,6 +1404,9 @@ _role:
|
||||
ltlAvailable: "ローカルタイムラインの閲覧"
|
||||
canPublicNote: "パブリック投稿の許可"
|
||||
canInvite: "サーバー招待コードの発行"
|
||||
inviteLimit: "招待コードの作成可能数"
|
||||
inviteLimitCycle: "招待コードの発行間隔"
|
||||
inviteExpirationTime: "招待コードの有効期限"
|
||||
canManageCustomEmojis: "カスタム絵文字の管理"
|
||||
driveCapacity: "ドライブ容量"
|
||||
alwaysMarkNsfw: "ファイルにNSFWを常に付与"
|
||||
|
3
locales/package.json
Normal file
3
locales/package.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"version": "13.14.0-beta.2",
|
||||
"version": "13.14.0-beta.3",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@@ -0,0 +1,25 @@
|
||||
export class RefactorInviteSystem1688720440658 {
|
||||
name = 'RefactorInviteSystem1688720440658'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "registration_ticket" ADD "expiresAt" TIMESTAMP WITH TIME ZONE`);
|
||||
await queryRunner.query(`ALTER TABLE "registration_ticket" ADD "usedAt" TIMESTAMP WITH TIME ZONE`);
|
||||
await queryRunner.query(`ALTER TABLE "registration_ticket" ADD "pendingUserId" character varying(32)`);
|
||||
await queryRunner.query(`ALTER TABLE "registration_ticket" ADD "createdById" character varying(32)`);
|
||||
await queryRunner.query(`ALTER TABLE "registration_ticket" ADD "usedById" character varying(32)`);
|
||||
await queryRunner.query(`ALTER TABLE "registration_ticket" ADD CONSTRAINT "UQ_b6f93f2f30bdbb9a5ebdc7c7189" UNIQUE ("usedById")`);
|
||||
await queryRunner.query(`ALTER TABLE "registration_ticket" ADD CONSTRAINT "FK_beba993576db0261a15364ea96e" FOREIGN KEY ("createdById") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "registration_ticket" ADD CONSTRAINT "FK_b6f93f2f30bdbb9a5ebdc7c7189" FOREIGN KEY ("usedById") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "registration_ticket" DROP CONSTRAINT "FK_b6f93f2f30bdbb9a5ebdc7c7189"`);
|
||||
await queryRunner.query(`ALTER TABLE "registration_ticket" DROP CONSTRAINT "FK_beba993576db0261a15364ea96e"`);
|
||||
await queryRunner.query(`ALTER TABLE "registration_ticket" DROP CONSTRAINT "UQ_b6f93f2f30bdbb9a5ebdc7c7189"`);
|
||||
await queryRunner.query(`ALTER TABLE "registration_ticket" DROP COLUMN "usedById"`);
|
||||
await queryRunner.query(`ALTER TABLE "registration_ticket" DROP COLUMN "createdById"`);
|
||||
await queryRunner.query(`ALTER TABLE "registration_ticket" DROP COLUMN "pendingUserId"`);
|
||||
await queryRunner.query(`ALTER TABLE "registration_ticket" DROP COLUMN "usedAt"`);
|
||||
await queryRunner.query(`ALTER TABLE "registration_ticket" DROP COLUMN "expiresAt"`);
|
||||
}
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
export class AddIndexToRelations1688880985544 {
|
||||
name = 'AddIndexToRelations1688880985544'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`CREATE INDEX "IDX_beba993576db0261a15364ea96" ON "registration_ticket" ("createdById") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_b6f93f2f30bdbb9a5ebdc7c718" ON "registration_ticket" ("usedById") `);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_b6f93f2f30bdbb9a5ebdc7c718"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_beba993576db0261a15364ea96"`);
|
||||
}
|
||||
}
|
@@ -31,7 +31,7 @@ function greet() {
|
||||
console.log(themeColor(' | |_|___ ___| |_ ___ _ _ '));
|
||||
console.log(themeColor(' | | | | |_ -|_ -| \'_| -_| | |'));
|
||||
console.log(themeColor(' |_|_|_|_|___|___|_,_|___|_ |'));
|
||||
console.log(' ' + chalk.gray(v) + themeColor(' |___|\n'.substr(v.length)));
|
||||
console.log(' ' + chalk.gray(v) + themeColor(' |___|\n'.substring(v.length)));
|
||||
//#endregion
|
||||
|
||||
console.log(' Misskey is an open-source decentralized microblogging platform.');
|
||||
|
@@ -63,6 +63,7 @@ export type Source = {
|
||||
apiKey: string;
|
||||
ssl?: boolean;
|
||||
index: string;
|
||||
scope?: 'local' | 'global' | string[];
|
||||
};
|
||||
|
||||
proxy?: string;
|
||||
|
@@ -11,10 +11,10 @@ import type { OnApplicationShutdown } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class CacheService implements OnApplicationShutdown {
|
||||
public userByIdCache: MemoryKVCache<User>;
|
||||
public localUserByNativeTokenCache: MemoryKVCache<LocalUser | null>;
|
||||
public userByIdCache: MemoryKVCache<User, User | string>;
|
||||
public localUserByNativeTokenCache: MemoryKVCache<LocalUser | null, string | null>;
|
||||
public localUserByIdCache: MemoryKVCache<LocalUser>;
|
||||
public uriPersonCache: MemoryKVCache<User | null>;
|
||||
public uriPersonCache: MemoryKVCache<User | null, string | null>;
|
||||
public userProfileCache: RedisKVCache<UserProfile>;
|
||||
public userMutingsCache: RedisKVCache<Set<string>>;
|
||||
public userBlockingCache: RedisKVCache<Set<string>>;
|
||||
@@ -55,10 +55,41 @@ export class CacheService implements OnApplicationShutdown {
|
||||
) {
|
||||
//this.onMessage = this.onMessage.bind(this);
|
||||
|
||||
this.userByIdCache = new MemoryKVCache<User>(Infinity);
|
||||
this.localUserByNativeTokenCache = new MemoryKVCache<LocalUser | null>(Infinity);
|
||||
this.localUserByIdCache = new MemoryKVCache<LocalUser>(Infinity);
|
||||
this.uriPersonCache = new MemoryKVCache<User | null>(Infinity);
|
||||
const localUserByIdCache = new MemoryKVCache<LocalUser>(1000 * 60 * 60 * 6 /* 6h */);
|
||||
this.localUserByIdCache = localUserByIdCache;
|
||||
|
||||
// ローカルユーザーならlocalUserByIdCacheにデータを追加し、こちらにはid(文字列)だけを追加する
|
||||
const userByIdCache = new MemoryKVCache<User, User | string>(1000 * 60 * 60 * 6 /* 6h */, {
|
||||
toMapConverter: user => {
|
||||
if (user.host === null) {
|
||||
localUserByIdCache.set(user.id, user as LocalUser);
|
||||
return user.id;
|
||||
}
|
||||
|
||||
return user;
|
||||
},
|
||||
fromMapConverter: userOrId => typeof userOrId === 'string' ? localUserByIdCache.get(userOrId) : userOrId,
|
||||
});
|
||||
this.userByIdCache = userByIdCache;
|
||||
|
||||
this.localUserByNativeTokenCache = new MemoryKVCache<LocalUser | null, string | null>(Infinity, {
|
||||
toMapConverter: user => {
|
||||
if (user === null) return null;
|
||||
|
||||
localUserByIdCache.set(user.id, user);
|
||||
return user.id;
|
||||
},
|
||||
fromMapConverter: id => id === null ? null : localUserByIdCache.get(id),
|
||||
});
|
||||
this.uriPersonCache = new MemoryKVCache<User | null, string | null>(Infinity, {
|
||||
toMapConverter: user => {
|
||||
if (user === null) return null;
|
||||
|
||||
userByIdCache.set(user.id, user);
|
||||
return user.id;
|
||||
},
|
||||
fromMapConverter: id => id === null ? null : userByIdCache.get(id),
|
||||
});
|
||||
|
||||
this.userProfileCache = new RedisKVCache<UserProfile>(this.redisClient, 'userProfile', {
|
||||
lifetime: 1000 * 60 * 30, // 30m
|
||||
@@ -131,7 +162,7 @@ export class CacheService implements OnApplicationShutdown {
|
||||
const user = await this.usersRepository.findOneByOrFail({ id: body.id });
|
||||
this.userByIdCache.set(user.id, user);
|
||||
for (const [k, v] of this.uriPersonCache.cache.entries()) {
|
||||
if (v.value?.id === user.id) {
|
||||
if (v.value === user.id) {
|
||||
this.uriPersonCache.set(k, user);
|
||||
}
|
||||
}
|
||||
|
@@ -81,6 +81,7 @@ import { GalleryLikeEntityService } from './entities/GalleryLikeEntityService.js
|
||||
import { GalleryPostEntityService } from './entities/GalleryPostEntityService.js';
|
||||
import { HashtagEntityService } from './entities/HashtagEntityService.js';
|
||||
import { InstanceEntityService } from './entities/InstanceEntityService.js';
|
||||
import { InviteCodeEntityService } from './entities/InviteCodeEntityService.js';
|
||||
import { ModerationLogEntityService } from './entities/ModerationLogEntityService.js';
|
||||
import { MutingEntityService } from './entities/MutingEntityService.js';
|
||||
import { RenoteMutingEntityService } from './entities/RenoteMutingEntityService.js';
|
||||
@@ -205,6 +206,7 @@ const $GalleryLikeEntityService: Provider = { provide: 'GalleryLikeEntityService
|
||||
const $GalleryPostEntityService: Provider = { provide: 'GalleryPostEntityService', useExisting: GalleryPostEntityService };
|
||||
const $HashtagEntityService: Provider = { provide: 'HashtagEntityService', useExisting: HashtagEntityService };
|
||||
const $InstanceEntityService: Provider = { provide: 'InstanceEntityService', useExisting: InstanceEntityService };
|
||||
const $InviteCodeEntityService: Provider = { provide: 'InviteCodeEntityService', useExisting: InviteCodeEntityService };
|
||||
const $ModerationLogEntityService: Provider = { provide: 'ModerationLogEntityService', useExisting: ModerationLogEntityService };
|
||||
const $MutingEntityService: Provider = { provide: 'MutingEntityService', useExisting: MutingEntityService };
|
||||
const $RenoteMutingEntityService: Provider = { provide: 'RenoteMutingEntityService', useExisting: RenoteMutingEntityService };
|
||||
@@ -329,6 +331,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
GalleryPostEntityService,
|
||||
HashtagEntityService,
|
||||
InstanceEntityService,
|
||||
InviteCodeEntityService,
|
||||
ModerationLogEntityService,
|
||||
MutingEntityService,
|
||||
RenoteMutingEntityService,
|
||||
@@ -448,6 +451,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$GalleryPostEntityService,
|
||||
$HashtagEntityService,
|
||||
$InstanceEntityService,
|
||||
$InviteCodeEntityService,
|
||||
$ModerationLogEntityService,
|
||||
$MutingEntityService,
|
||||
$RenoteMutingEntityService,
|
||||
@@ -567,6 +571,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
GalleryPostEntityService,
|
||||
HashtagEntityService,
|
||||
InstanceEntityService,
|
||||
InviteCodeEntityService,
|
||||
ModerationLogEntityService,
|
||||
MutingEntityService,
|
||||
RenoteMutingEntityService,
|
||||
@@ -685,6 +690,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||
$GalleryPostEntityService,
|
||||
$HashtagEntityService,
|
||||
$InstanceEntityService,
|
||||
$InviteCodeEntityService,
|
||||
$ModerationLogEntityService,
|
||||
$MutingEntityService,
|
||||
$RenoteMutingEntityService,
|
||||
|
@@ -103,7 +103,7 @@ export class FetchInstanceMetadataService {
|
||||
|
||||
if (name) updates.name = name;
|
||||
if (description) updates.description = description;
|
||||
if (icon || favicon) updates.iconUrl = icon ?? favicon;
|
||||
if (icon || favicon) updates.iconUrl = (icon && !icon.includes('data:image/png;base64')) ? icon : favicon;
|
||||
if (favicon) updates.faviconUrl = favicon;
|
||||
if (themeColor) updates.themeColor = themeColor;
|
||||
|
||||
|
@@ -570,12 +570,14 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
if (data.reply) {
|
||||
// 通知
|
||||
if (data.reply.userHost === null) {
|
||||
const threadMuted = await this.noteThreadMutingsRepository.findOneBy({
|
||||
userId: data.reply.userId,
|
||||
threadId: data.reply.threadId ?? data.reply.id,
|
||||
const isThreadMuted = await this.noteThreadMutingsRepository.exist({
|
||||
where: {
|
||||
userId: data.reply.userId,
|
||||
threadId: data.reply.threadId ?? data.reply.id,
|
||||
}
|
||||
});
|
||||
|
||||
if (!threadMuted) {
|
||||
if (!isThreadMuted) {
|
||||
nm.push(data.reply.userId, 'reply');
|
||||
this.globalEventService.publishMainStream(data.reply.userId, 'reply', noteObj);
|
||||
|
||||
@@ -712,12 +714,14 @@ export class NoteCreateService implements OnApplicationShutdown {
|
||||
@bindThis
|
||||
private async createMentionedEvents(mentionedUsers: MinimumUser[], note: Note, nm: NotificationManager) {
|
||||
for (const u of mentionedUsers.filter(u => this.userEntityService.isLocalUser(u))) {
|
||||
const threadMuted = await this.noteThreadMutingsRepository.findOneBy({
|
||||
userId: u.id,
|
||||
threadId: note.threadId ?? note.id,
|
||||
const isThreadMuted = await this.noteThreadMutingsRepository.exist({
|
||||
where: {
|
||||
userId: u.id,
|
||||
threadId: note.threadId ?? note.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (threadMuted) {
|
||||
if (isThreadMuted) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@@ -17,6 +17,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { SearchService } from '@/core/SearchService.js';
|
||||
|
||||
@Injectable()
|
||||
export class NoteDeleteService {
|
||||
@@ -41,6 +42,7 @@ export class NoteDeleteService {
|
||||
private apRendererService: ApRendererService,
|
||||
private apDeliverManagerService: ApDeliverManagerService,
|
||||
private metaService: MetaService,
|
||||
private searchService: SearchService,
|
||||
private notesChart: NotesChart,
|
||||
private perUserNotesChart: PerUserNotesChart,
|
||||
private instanceChart: InstanceChart,
|
||||
@@ -53,6 +55,7 @@ export class NoteDeleteService {
|
||||
*/
|
||||
async delete(user: { id: User['id']; uri: User['uri']; host: User['host']; isBot: User['isBot']; }, note: Note, quiet = false) {
|
||||
const deletedAt = new Date();
|
||||
const cascadingNotes = await this.findCascadingNotes(note);
|
||||
|
||||
// この投稿を除く指定したユーザーによる指定したノートのリノートが存在しないとき
|
||||
if (note.renoteId && (await this.noteEntityService.countSameRenotes(user.id, note.renoteId, note.id)) === 0) {
|
||||
@@ -88,8 +91,8 @@ export class NoteDeleteService {
|
||||
}
|
||||
|
||||
// also deliever delete activity to cascaded notes
|
||||
const cascadingNotes = (await this.findCascadingNotes(note)).filter(note => !note.localOnly); // filter out local-only notes
|
||||
for (const cascadingNote of cascadingNotes) {
|
||||
const federatedLocalCascadingNotes = (cascadingNotes).filter(note => !note.localOnly && note.userHost == null); // filter out local-only notes
|
||||
for (const cascadingNote of federatedLocalCascadingNotes) {
|
||||
if (!cascadingNote.user) continue;
|
||||
if (!this.userEntityService.isLocalUser(cascadingNote.user)) continue;
|
||||
const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.apRendererService.renderTombstone(`${this.config.url}/notes/${cascadingNote.id}`), cascadingNote.user));
|
||||
@@ -114,6 +117,11 @@ export class NoteDeleteService {
|
||||
}
|
||||
}
|
||||
|
||||
for (const cascadingNote of cascadingNotes) {
|
||||
this.searchService.unindexNote(cascadingNote);
|
||||
}
|
||||
this.searchService.unindexNote(note);
|
||||
|
||||
await this.notesRepository.delete({
|
||||
id: note.id,
|
||||
userId: user.id,
|
||||
@@ -140,7 +148,7 @@ export class NoteDeleteService {
|
||||
|
||||
const cascadingNotes: Note[] = await recursive(note.id);
|
||||
|
||||
return cascadingNotes.filter(note => note.userHost === null); // filter out non-local users
|
||||
return cascadingNotes;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@@ -43,11 +43,13 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||
//#endregion
|
||||
|
||||
// スレッドミュート
|
||||
const threadMute = await this.noteThreadMutingsRepository.findOneBy({
|
||||
userId: userId,
|
||||
threadId: note.threadId ?? note.id,
|
||||
const isThreadMuted = await this.noteThreadMutingsRepository.exist({
|
||||
where: {
|
||||
userId: userId,
|
||||
threadId: note.threadId ?? note.id,
|
||||
},
|
||||
});
|
||||
if (threadMute) return;
|
||||
if (isThreadMuted) return;
|
||||
|
||||
const unread = {
|
||||
id: this.idService.genId(),
|
||||
@@ -62,9 +64,9 @@ export class NoteReadService implements OnApplicationShutdown {
|
||||
|
||||
// 2秒経っても既読にならなかったら「未読の投稿がありますよ」イベントを発行する
|
||||
setTimeout(2000, 'unread note', { signal: this.#shutdownController.signal }).then(async () => {
|
||||
const exist = await this.noteUnreadsRepository.findOneBy({ id: unread.id });
|
||||
const exist = await this.noteUnreadsRepository.exist({ where: { id: unread.id } });
|
||||
|
||||
if (exist == null) return;
|
||||
if (!exist) return;
|
||||
|
||||
if (params.isMentioned) {
|
||||
this.globalEventService.publishMainStream(userId, 'unreadMention', note.id);
|
||||
|
@@ -71,7 +71,7 @@ export class RemoteUserResolveService {
|
||||
return await this.apPersonService.createPerson(self.href);
|
||||
}
|
||||
|
||||
// ユーザー情報が古い場合は、WebFilgerからやりなおして返す
|
||||
// ユーザー情報が古い場合は、WebFingerからやりなおして返す
|
||||
if (user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
|
||||
// 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する
|
||||
await this.usersRepository.update(user.id, {
|
||||
|
@@ -21,6 +21,9 @@ export type RolePolicies = {
|
||||
ltlAvailable: boolean;
|
||||
canPublicNote: boolean;
|
||||
canInvite: boolean;
|
||||
inviteLimit: number;
|
||||
inviteLimitCycle: number;
|
||||
inviteExpirationTime: number;
|
||||
canManageCustomEmojis: boolean;
|
||||
canSearchNotes: boolean;
|
||||
canHideAds: boolean;
|
||||
@@ -42,6 +45,9 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||
ltlAvailable: true,
|
||||
canPublicNote: true,
|
||||
canInvite: false,
|
||||
inviteLimit: 0,
|
||||
inviteLimitCycle: 60 * 24 * 7,
|
||||
inviteExpirationTime: 0,
|
||||
canManageCustomEmojis: false,
|
||||
canSearchNotes: false,
|
||||
canHideAds: false,
|
||||
@@ -277,6 +283,9 @@ export class RoleService implements OnApplicationShutdown {
|
||||
ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)),
|
||||
canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)),
|
||||
canInvite: calc('canInvite', vs => vs.some(v => v === true)),
|
||||
inviteLimit: calc('inviteLimit', vs => Math.max(...vs)),
|
||||
inviteLimitCycle: calc('inviteLimitCycle', vs => Math.max(...vs)),
|
||||
inviteExpirationTime: calc('inviteExpirationTime', vs => Math.max(...vs)),
|
||||
canManageCustomEmojis: calc('canManageCustomEmojis', vs => vs.some(v => v === true)),
|
||||
canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)),
|
||||
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
|
||||
|
@@ -52,6 +52,7 @@ function compileQuery(q: Q): string {
|
||||
|
||||
@Injectable()
|
||||
export class SearchService {
|
||||
private readonly meilisearchIndexScope: 'local' | 'global' | string[] = 'local';
|
||||
private meilisearchNoteIndex: Index | null = null;
|
||||
|
||||
constructor(
|
||||
@@ -92,6 +93,10 @@ export class SearchService {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (config.meilisearch?.scope) {
|
||||
this.meilisearchIndexScope = config.meilisearch.scope;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -100,7 +105,22 @@ export class SearchService {
|
||||
if (!['home', 'public'].includes(note.visibility)) return;
|
||||
|
||||
if (this.meilisearch) {
|
||||
this.meilisearchNoteIndex!.addDocuments([{
|
||||
switch (this.meilisearchIndexScope) {
|
||||
case 'global':
|
||||
break;
|
||||
|
||||
case 'local':
|
||||
if (note.userHost == null) break;
|
||||
return;
|
||||
|
||||
default: {
|
||||
if (note.userHost == null) break;
|
||||
if (this.meilisearchIndexScope.includes(note.userHost)) break;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await this.meilisearchNoteIndex?.addDocuments([{
|
||||
id: note.id,
|
||||
createdAt: note.createdAt.getTime(),
|
||||
userId: note.userId,
|
||||
@@ -115,6 +135,15 @@ export class SearchService {
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async unindexNote(note: Note): Promise<void> {
|
||||
if (!['home', 'public'].includes(note.visibility)) return;
|
||||
|
||||
if (this.meilisearch) {
|
||||
this.meilisearchNoteIndex!.deleteDocument(note.id);
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async searchNote(q: string, me: User | null, opts: {
|
||||
userId?: Note['userId'] | null;
|
||||
|
@@ -71,12 +71,12 @@ export class SignupService {
|
||||
const secret = generateUserToken();
|
||||
|
||||
// Check username duplication
|
||||
if (await this.usersRepository.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull() })) {
|
||||
if (await this.usersRepository.exist({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) {
|
||||
throw new Error('DUPLICATED_USERNAME');
|
||||
}
|
||||
|
||||
// Check deleted username duplication
|
||||
if (await this.usedUsernamesRepository.findOneBy({ username: username.toLowerCase() })) {
|
||||
if (await this.usedUsernamesRepository.exist({ where: { username: username.toLowerCase() } })) {
|
||||
throw new Error('USED_USERNAME');
|
||||
}
|
||||
|
||||
|
@@ -122,22 +122,26 @@ export class UserFollowingService implements OnModuleInit {
|
||||
let autoAccept = false;
|
||||
|
||||
// 鍵アカウントであっても、既にフォローされていた場合はスルー
|
||||
const following = await this.followingsRepository.findOneBy({
|
||||
followerId: follower.id,
|
||||
followeeId: followee.id,
|
||||
const isFollowing = await this.followingsRepository.exist({
|
||||
where: {
|
||||
followerId: follower.id,
|
||||
followeeId: followee.id,
|
||||
},
|
||||
});
|
||||
if (following) {
|
||||
if (isFollowing) {
|
||||
autoAccept = true;
|
||||
}
|
||||
|
||||
// フォローしているユーザーは自動承認オプション
|
||||
if (!autoAccept && (this.userEntityService.isLocalUser(followee) && followeeProfile.autoAcceptFollowed)) {
|
||||
const followed = await this.followingsRepository.findOneBy({
|
||||
followerId: followee.id,
|
||||
followeeId: follower.id,
|
||||
const isFollowed = await this.followingsRepository.exist({
|
||||
where: {
|
||||
followerId: followee.id,
|
||||
followeeId: follower.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (followed) autoAccept = true;
|
||||
if (isFollowed) autoAccept = true;
|
||||
}
|
||||
|
||||
// Automatically accept if the follower is an account who has moved and the locked followee had accepted the old account.
|
||||
@@ -206,12 +210,14 @@ export class UserFollowingService implements OnModuleInit {
|
||||
|
||||
this.cacheService.userFollowingsCache.refresh(follower.id);
|
||||
|
||||
const req = await this.followRequestsRepository.findOneBy({
|
||||
followeeId: followee.id,
|
||||
followerId: follower.id,
|
||||
const requestExist = await this.followRequestsRepository.exist({
|
||||
where: {
|
||||
followeeId: followee.id,
|
||||
followerId: follower.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (req) {
|
||||
if (requestExist) {
|
||||
await this.followRequestsRepository.delete({
|
||||
followeeId: followee.id,
|
||||
followerId: follower.id,
|
||||
@@ -505,12 +511,14 @@ export class UserFollowingService implements OnModuleInit {
|
||||
}
|
||||
}
|
||||
|
||||
const request = await this.followRequestsRepository.findOneBy({
|
||||
followeeId: followee.id,
|
||||
followerId: follower.id,
|
||||
const requestExist = await this.followRequestsRepository.exist({
|
||||
where: {
|
||||
followeeId: followee.id,
|
||||
followerId: follower.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (request == null) {
|
||||
if (!requestExist) {
|
||||
throw new IdentifiableError('17447091-ce07-46dd-b331-c1fd4f15b1e7', 'request not found');
|
||||
}
|
||||
|
||||
|
@@ -16,6 +16,8 @@ type AudienceInfo = {
|
||||
visibleUsers: User[],
|
||||
};
|
||||
|
||||
type GroupedAudience = Record<'public' | 'followers' | 'other', string[]>;
|
||||
|
||||
@Injectable()
|
||||
export class ApAudienceService {
|
||||
constructor(
|
||||
@@ -67,11 +69,11 @@ export class ApAudienceService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private groupingAudience(ids: string[], actor: RemoteUser) {
|
||||
const groups = {
|
||||
public: [] as string[],
|
||||
followers: [] as string[],
|
||||
other: [] as string[],
|
||||
private groupingAudience(ids: string[], actor: RemoteUser): GroupedAudience {
|
||||
const groups: GroupedAudience = {
|
||||
public: [],
|
||||
followers: [],
|
||||
other: [],
|
||||
};
|
||||
|
||||
for (const id of ids) {
|
||||
@@ -90,7 +92,7 @@ export class ApAudienceService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private isPublic(id: string) {
|
||||
private isPublic(id: string): boolean {
|
||||
return [
|
||||
'https://www.w3.org/ns/activitystreams#Public',
|
||||
'as#Public',
|
||||
@@ -99,9 +101,7 @@ export class ApAudienceService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private isFollowers(id: string, actor: RemoteUser) {
|
||||
return (
|
||||
id === (actor.followersUri ?? `${actor.uri}/followers`)
|
||||
);
|
||||
private isFollowers(id: string, actor: RemoteUser): boolean {
|
||||
return id === (actor.followersUri ?? `${actor.uri}/followers`);
|
||||
}
|
||||
}
|
||||
|
@@ -99,13 +99,15 @@ export class ApDbResolverService implements OnApplicationShutdown {
|
||||
if (parsed.local) {
|
||||
if (parsed.type !== 'users') return null;
|
||||
|
||||
return await this.cacheService.userByIdCache.fetchMaybe(parsed.id, () => this.usersRepository.findOneBy({
|
||||
id: parsed.id,
|
||||
}).then(x => x ?? undefined)) as LocalUser | undefined ?? null;
|
||||
return await this.cacheService.userByIdCache.fetchMaybe(
|
||||
parsed.id,
|
||||
() => this.usersRepository.findOneBy({ id: parsed.id }).then(x => x ?? undefined),
|
||||
) as LocalUser | undefined ?? null;
|
||||
} else {
|
||||
return await this.cacheService.uriPersonCache.fetch(parsed.uri, () => this.usersRepository.findOneBy({
|
||||
uri: parsed.uri,
|
||||
})) as RemoteUser | null;
|
||||
return await this.cacheService.uriPersonCache.fetch(
|
||||
parsed.uri,
|
||||
() => this.usersRepository.findOneBy({ uri: parsed.uri }),
|
||||
) as RemoteUser | null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,9 +147,11 @@ export class ApDbResolverService implements OnApplicationShutdown {
|
||||
} | null> {
|
||||
const user = await this.apPersonService.resolvePerson(uri) as RemoteUser;
|
||||
|
||||
if (user == null) return null;
|
||||
|
||||
const key = await this.publicKeyByUserIdCache.fetch(user.id, () => this.userPublickeysRepository.findOneBy({ userId: user.id }), v => v != null);
|
||||
const key = await this.publicKeyByUserIdCache.fetch(
|
||||
user.id,
|
||||
() => this.userPublickeysRepository.findOneBy({ userId: user.id }),
|
||||
v => v != null,
|
||||
);
|
||||
|
||||
return {
|
||||
user,
|
||||
|
@@ -29,6 +29,121 @@ const isFollowers = (recipe: IRecipe): recipe is IFollowersRecipe =>
|
||||
const isDirect = (recipe: IRecipe): recipe is IDirectRecipe =>
|
||||
recipe.type === 'Direct';
|
||||
|
||||
class DeliverManager {
|
||||
private actor: ThinUser;
|
||||
private activity: IActivity | null;
|
||||
private recipes: IRecipe[] = [];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param userEntityService
|
||||
* @param followingsRepository
|
||||
* @param queueService
|
||||
* @param actor Actor
|
||||
* @param activity Activity to deliver
|
||||
*/
|
||||
constructor(
|
||||
private userEntityService: UserEntityService,
|
||||
private followingsRepository: FollowingsRepository,
|
||||
private queueService: QueueService,
|
||||
|
||||
actor: { id: User['id']; host: null; },
|
||||
activity: IActivity | null,
|
||||
) {
|
||||
// 型で弾いてはいるが一応ローカルユーザーかチェック
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (actor.host != null) throw new Error('actor.host must be null');
|
||||
|
||||
// パフォーマンス向上のためキューに突っ込むのはidのみに絞る
|
||||
this.actor = {
|
||||
id: actor.id,
|
||||
};
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add recipe for followers deliver
|
||||
*/
|
||||
@bindThis
|
||||
public addFollowersRecipe(): void {
|
||||
const deliver: IFollowersRecipe = {
|
||||
type: 'Followers',
|
||||
};
|
||||
|
||||
this.addRecipe(deliver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add recipe for direct deliver
|
||||
* @param to To
|
||||
*/
|
||||
@bindThis
|
||||
public addDirectRecipe(to: RemoteUser): void {
|
||||
const recipe: IDirectRecipe = {
|
||||
type: 'Direct',
|
||||
to,
|
||||
};
|
||||
|
||||
this.addRecipe(recipe);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add recipe
|
||||
* @param recipe Recipe
|
||||
*/
|
||||
@bindThis
|
||||
public addRecipe(recipe: IRecipe): void {
|
||||
this.recipes.push(recipe);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute delivers
|
||||
*/
|
||||
@bindThis
|
||||
public async execute(): Promise<void> {
|
||||
// The value flags whether it is shared or not.
|
||||
// key: inbox URL, value: whether it is sharedInbox
|
||||
const inboxes = new Map<string, boolean>();
|
||||
|
||||
// build inbox list
|
||||
// Process follower recipes first to avoid duplication when processing direct recipes later.
|
||||
if (this.recipes.some(r => isFollowers(r))) {
|
||||
// followers deliver
|
||||
// TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう
|
||||
// ただ、sharedInboxがnullなリモートユーザーも稀におり、その対応ができなさそう?
|
||||
const followers = await this.followingsRepository.find({
|
||||
where: {
|
||||
followeeId: this.actor.id,
|
||||
followerHost: Not(IsNull()),
|
||||
},
|
||||
select: {
|
||||
followerSharedInbox: true,
|
||||
followerInbox: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const following of followers) {
|
||||
const inbox = following.followerSharedInbox ?? following.followerInbox;
|
||||
if (inbox === null) throw new Error('inbox is null');
|
||||
inboxes.set(inbox, following.followerSharedInbox != null);
|
||||
}
|
||||
}
|
||||
|
||||
for (const recipe of this.recipes.filter(isDirect)) {
|
||||
// check that shared inbox has not been added yet
|
||||
if (recipe.to.sharedInbox !== null && inboxes.has(recipe.to.sharedInbox)) continue;
|
||||
|
||||
// check that they actually have an inbox
|
||||
if (recipe.to.inbox === null) continue;
|
||||
|
||||
inboxes.set(recipe.to.inbox, false);
|
||||
}
|
||||
|
||||
// deliver
|
||||
this.queueService.deliverMany(this.actor, this.activity, inboxes);
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ApDeliverManagerService {
|
||||
constructor(
|
||||
@@ -52,7 +167,7 @@ export class ApDeliverManagerService {
|
||||
* @param activity Activity
|
||||
*/
|
||||
@bindThis
|
||||
public async deliverToFollowers(actor: { id: LocalUser['id']; host: null; }, activity: IActivity) {
|
||||
public async deliverToFollowers(actor: { id: LocalUser['id']; host: null; }, activity: IActivity): Promise<void> {
|
||||
const manager = new DeliverManager(
|
||||
this.userEntityService,
|
||||
this.followingsRepository,
|
||||
@@ -71,7 +186,7 @@ export class ApDeliverManagerService {
|
||||
* @param to Target user
|
||||
*/
|
||||
@bindThis
|
||||
public async deliverToUser(actor: { id: LocalUser['id']; host: null; }, activity: IActivity, to: RemoteUser) {
|
||||
public async deliverToUser(actor: { id: LocalUser['id']; host: null; }, activity: IActivity, to: RemoteUser): Promise<void> {
|
||||
const manager = new DeliverManager(
|
||||
this.userEntityService,
|
||||
this.followingsRepository,
|
||||
@@ -84,7 +199,7 @@ export class ApDeliverManagerService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createDeliverManager(actor: { id: User['id']; host: null; }, activity: IActivity | null) {
|
||||
public createDeliverManager(actor: { id: User['id']; host: null; }, activity: IActivity | null): DeliverManager {
|
||||
return new DeliverManager(
|
||||
this.userEntityService,
|
||||
this.followingsRepository,
|
||||
@@ -95,123 +210,3 @@ export class ApDeliverManagerService {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DeliverManager {
|
||||
private actor: ThinUser;
|
||||
private activity: IActivity | null;
|
||||
private recipes: IRecipe[] = [];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param userEntityService
|
||||
* @param followingsRepository
|
||||
* @param queueService
|
||||
* @param actor Actor
|
||||
* @param activity Activity to deliver
|
||||
*/
|
||||
constructor(
|
||||
private userEntityService: UserEntityService,
|
||||
private followingsRepository: FollowingsRepository,
|
||||
private queueService: QueueService,
|
||||
|
||||
actor: { id: User['id']; host: null; },
|
||||
activity: IActivity | null,
|
||||
) {
|
||||
// 型で弾いてはいるが一応ローカルユーザーかチェック
|
||||
if (actor.host != null) throw new Error('actor.host must be null');
|
||||
|
||||
// パフォーマンス向上のためキューに突っ込むのはidのみに絞る
|
||||
this.actor = {
|
||||
id: actor.id,
|
||||
};
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add recipe for followers deliver
|
||||
*/
|
||||
@bindThis
|
||||
public addFollowersRecipe() {
|
||||
const deliver = {
|
||||
type: 'Followers',
|
||||
} as IFollowersRecipe;
|
||||
|
||||
this.addRecipe(deliver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add recipe for direct deliver
|
||||
* @param to To
|
||||
*/
|
||||
@bindThis
|
||||
public addDirectRecipe(to: RemoteUser) {
|
||||
const recipe = {
|
||||
type: 'Direct',
|
||||
to,
|
||||
} as IDirectRecipe;
|
||||
|
||||
this.addRecipe(recipe);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add recipe
|
||||
* @param recipe Recipe
|
||||
*/
|
||||
@bindThis
|
||||
public addRecipe(recipe: IRecipe) {
|
||||
this.recipes.push(recipe);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute delivers
|
||||
*/
|
||||
@bindThis
|
||||
public async execute() {
|
||||
// The value flags whether it is shared or not.
|
||||
// key: inbox URL, value: whether it is sharedInbox
|
||||
const inboxes = new Map<string, boolean>();
|
||||
|
||||
/*
|
||||
build inbox list
|
||||
|
||||
Process follower recipes first to avoid duplication when processing
|
||||
direct recipes later.
|
||||
*/
|
||||
if (this.recipes.some(r => isFollowers(r))) {
|
||||
// followers deliver
|
||||
// TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう
|
||||
// ただ、sharedInboxがnullなリモートユーザーも稀におり、その対応ができなさそう?
|
||||
const followers = await this.followingsRepository.find({
|
||||
where: {
|
||||
followeeId: this.actor.id,
|
||||
followerHost: Not(IsNull()),
|
||||
},
|
||||
select: {
|
||||
followerSharedInbox: true,
|
||||
followerInbox: true,
|
||||
},
|
||||
}) as {
|
||||
followerSharedInbox: string | null;
|
||||
followerInbox: string;
|
||||
}[];
|
||||
|
||||
for (const following of followers) {
|
||||
const inbox = following.followerSharedInbox ?? following.followerInbox;
|
||||
inboxes.set(inbox, following.followerSharedInbox != null);
|
||||
}
|
||||
}
|
||||
|
||||
this.recipes.filter((recipe): recipe is IDirectRecipe =>
|
||||
// followers recipes have already been processed
|
||||
isDirect(recipe)
|
||||
// check that shared inbox has not been added yet
|
||||
&& !(recipe.to.sharedInbox && inboxes.has(recipe.to.sharedInbox))
|
||||
// check that they actually have an inbox
|
||||
&& recipe.to.inbox != null,
|
||||
)
|
||||
.forEach(recipe => inboxes.set(recipe.to.inbox!, false));
|
||||
|
||||
// deliver
|
||||
this.queueService.deliverMany(this.actor, this.activity, inboxes);
|
||||
}
|
||||
}
|
||||
|
@@ -21,10 +21,10 @@ import { CacheService } from '@/core/CacheService.js';
|
||||
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository, } from '@/models/index.js';
|
||||
import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/index.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { RemoteUser } from '@/models/entities/User.js';
|
||||
import { getApHrefNullable, getApId, getApIds, getApType, getOneApHrefNullable, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
|
||||
import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js';
|
||||
import { ApNoteService } from './models/ApNoteService.js';
|
||||
import { ApLoggerService } from './ApLoggerService.js';
|
||||
import { ApDbResolverService } from './ApDbResolverService.js';
|
||||
@@ -86,7 +86,7 @@ export class ApInboxService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async performActivity(actor: RemoteUser, activity: IObject) {
|
||||
public async performActivity(actor: RemoteUser, activity: IObject): Promise<void> {
|
||||
if (isCollectionOrOrderedCollection(activity)) {
|
||||
const resolver = this.apResolverService.createResolver();
|
||||
for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) {
|
||||
@@ -107,7 +107,7 @@ export class ApInboxService {
|
||||
if (actor.uri) {
|
||||
if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
|
||||
setImmediate(() => {
|
||||
this.apPersonService.updatePerson(actor.uri!);
|
||||
this.apPersonService.updatePerson(actor.uri);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -229,7 +229,7 @@ export class ApInboxService {
|
||||
|
||||
@bindThis
|
||||
private async add(actor: RemoteUser, activity: IAdd): Promise<void> {
|
||||
if ('actor' in activity && actor.uri !== activity.actor) {
|
||||
if (actor.uri !== activity.actor) {
|
||||
throw new Error('invalid actor');
|
||||
}
|
||||
|
||||
@@ -273,7 +273,7 @@ export class ApInboxService {
|
||||
const unlock = await this.appLockService.getApLock(uri);
|
||||
|
||||
try {
|
||||
// 既に同じURIを持つものが登録されていないかチェック
|
||||
// 既に同じURIを持つものが登録されていないかチェック
|
||||
const exist = await this.apNoteService.fetchNote(uri);
|
||||
if (exist) {
|
||||
return;
|
||||
@@ -292,7 +292,7 @@ export class ApInboxService {
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.warn(`Error in announce target ${targetUri} - ${err.statusCode ?? err}`);
|
||||
this.logger.warn(`Error in announce target ${targetUri} - ${err.statusCode}`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
@@ -409,7 +409,7 @@ export class ApInboxService {
|
||||
|
||||
@bindThis
|
||||
private async delete(actor: RemoteUser, activity: IDelete): Promise<string> {
|
||||
if ('actor' in activity && actor.uri !== activity.actor) {
|
||||
if (actor.uri !== activity.actor) {
|
||||
throw new Error('invalid actor');
|
||||
}
|
||||
|
||||
@@ -420,7 +420,7 @@ export class ApInboxService {
|
||||
// typeが不明だけど、どうせ消えてるのでremote resolveしない
|
||||
formerType = undefined;
|
||||
} else {
|
||||
const object = activity.object as IObject;
|
||||
const object = activity.object;
|
||||
if (isTombstone(object)) {
|
||||
formerType = toSingle(object.formerType);
|
||||
} else {
|
||||
@@ -503,7 +503,10 @@ export class ApInboxService {
|
||||
// 対象ユーザーは一番最初のユーザー として あとはコメントとして格納する
|
||||
const uris = getApIds(activity.object);
|
||||
|
||||
const userIds = uris.filter(uri => uri.startsWith(this.config.url + '/users/')).map(uri => uri.split('/').pop()!);
|
||||
const userIds = uris
|
||||
.filter(uri => uri.startsWith(this.config.url + '/users/'))
|
||||
.map(uri => uri.split('/').at(-1))
|
||||
.filter((userId): userId is string => userId !== undefined);
|
||||
const users = await this.usersRepository.findBy({
|
||||
id: In(userIds),
|
||||
});
|
||||
@@ -566,7 +569,7 @@ export class ApInboxService {
|
||||
|
||||
@bindThis
|
||||
private async remove(actor: RemoteUser, activity: IRemove): Promise<void> {
|
||||
if ('actor' in activity && actor.uri !== activity.actor) {
|
||||
if (actor.uri !== activity.actor) {
|
||||
throw new Error('invalid actor');
|
||||
}
|
||||
|
||||
@@ -586,7 +589,7 @@ export class ApInboxService {
|
||||
|
||||
@bindThis
|
||||
private async undo(actor: RemoteUser, activity: IUndo): Promise<string> {
|
||||
if ('actor' in activity && actor.uri !== activity.actor) {
|
||||
if (actor.uri !== activity.actor) {
|
||||
throw new Error('invalid actor');
|
||||
}
|
||||
|
||||
@@ -618,12 +621,14 @@ export class ApInboxService {
|
||||
return 'skip: follower not found';
|
||||
}
|
||||
|
||||
const following = await this.followingsRepository.findOneBy({
|
||||
followerId: follower.id,
|
||||
followeeId: actor.id,
|
||||
const isFollowing = await this.followingsRepository.exist({
|
||||
where: {
|
||||
followerId: follower.id,
|
||||
followeeId: actor.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (following) {
|
||||
if (isFollowing) {
|
||||
await this.userFollowingService.unfollow(follower, actor);
|
||||
return 'ok: unfollowed';
|
||||
}
|
||||
@@ -673,22 +678,26 @@ export class ApInboxService {
|
||||
return 'skip: フォロー解除しようとしているユーザーはローカルユーザーではありません';
|
||||
}
|
||||
|
||||
const req = await this.followRequestsRepository.findOneBy({
|
||||
followerId: actor.id,
|
||||
followeeId: followee.id,
|
||||
const requestExist = await this.followRequestsRepository.exist({
|
||||
where: {
|
||||
followerId: actor.id,
|
||||
followeeId: followee.id,
|
||||
},
|
||||
});
|
||||
|
||||
const following = await this.followingsRepository.findOneBy({
|
||||
followerId: actor.id,
|
||||
followeeId: followee.id,
|
||||
const isFollowing = await this.followingsRepository.exist({
|
||||
where: {
|
||||
followerId: actor.id,
|
||||
followeeId: followee.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (req) {
|
||||
if (requestExist) {
|
||||
await this.userFollowingService.cancelFollowRequest(followee, actor);
|
||||
return 'ok: follow request canceled';
|
||||
}
|
||||
|
||||
if (following) {
|
||||
if (isFollowing) {
|
||||
await this.userFollowingService.unfollow(actor, followee);
|
||||
return 'ok: unfollowed';
|
||||
}
|
||||
@@ -713,7 +722,7 @@ export class ApInboxService {
|
||||
|
||||
@bindThis
|
||||
private async update(actor: RemoteUser, activity: IUpdate): Promise<string> {
|
||||
if ('actor' in activity && actor.uri !== activity.actor) {
|
||||
if (actor.uri !== activity.actor) {
|
||||
return 'skip: invalid actor';
|
||||
}
|
||||
|
||||
@@ -727,7 +736,7 @@ export class ApInboxService {
|
||||
});
|
||||
|
||||
if (isActor(object)) {
|
||||
await this.apPersonService.updatePerson(actor.uri!, resolver, object);
|
||||
await this.apPersonService.updatePerson(actor.uri, resolver, object);
|
||||
return 'ok: Person updated';
|
||||
} else if (getApType(object) === 'Question') {
|
||||
await this.apQuestionService.updateQuestion(object, resolver).catch(err => console.error(err));
|
||||
|
@@ -4,9 +4,9 @@ import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { MfmService } from '@/core/MfmService.js';
|
||||
import type { Note } from '@/models/entities/Note.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { extractApHashtagObjects } from './models/tag.js';
|
||||
import type { IObject } from './type.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
||||
@Injectable()
|
||||
export class ApMfmService {
|
||||
@@ -19,14 +19,13 @@ export class ApMfmService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public htmlToMfm(html: string, tag?: IObject | IObject[]) {
|
||||
const hashtagNames = extractApHashtagObjects(tag).map(x => x.name).filter((x): x is string => x != null);
|
||||
|
||||
public htmlToMfm(html: string, tag?: IObject | IObject[]): string {
|
||||
const hashtagNames = extractApHashtagObjects(tag).map(x => x.name);
|
||||
return this.mfmService.fromHtml(html, hashtagNames);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public getNoteHtml(note: Note) {
|
||||
public getNoteHtml(note: Note): string | null {
|
||||
if (!note.text) return '';
|
||||
return this.mfmService.toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers));
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { createPublicKey } from 'node:crypto';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { In, IsNull } from 'typeorm';
|
||||
import { In } from 'typeorm';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import * as mfm from 'mfm-js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
@@ -26,7 +26,6 @@ import { isNotNull } from '@/misc/is-not-null.js';
|
||||
import { LdSignatureService } from './LdSignatureService.js';
|
||||
import { ApMfmService } from './ApMfmService.js';
|
||||
import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js';
|
||||
import type { IIdentifier } from './models/identifier.js';
|
||||
|
||||
@Injectable()
|
||||
export class ApRendererService {
|
||||
@@ -63,7 +62,7 @@ export class ApRendererService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderAccept(object: any, user: { id: User['id']; host: null }): IAccept {
|
||||
public renderAccept(object: string | IObject, user: { id: User['id']; host: null }): IAccept {
|
||||
return {
|
||||
type: 'Accept',
|
||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||
@@ -72,7 +71,7 @@ export class ApRendererService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderAdd(user: LocalUser, target: any, object: any): IAdd {
|
||||
public renderAdd(user: LocalUser, target: string | IObject | undefined, object: string | IObject): IAdd {
|
||||
return {
|
||||
type: 'Add',
|
||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||
@@ -82,7 +81,7 @@ export class ApRendererService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderAnnounce(object: any, note: Note): IAnnounce {
|
||||
public renderAnnounce(object: string | IObject, note: Note): IAnnounce {
|
||||
const attributedTo = this.userEntityService.genLocalUserUri(note.userId);
|
||||
|
||||
let to: string[] = [];
|
||||
@@ -133,13 +132,13 @@ export class ApRendererService {
|
||||
|
||||
@bindThis
|
||||
public renderCreate(object: IObject, note: Note): ICreate {
|
||||
const activity = {
|
||||
const activity: ICreate = {
|
||||
id: `${this.config.url}/notes/${note.id}/activity`,
|
||||
actor: this.userEntityService.genLocalUserUri(note.userId),
|
||||
type: 'Create',
|
||||
published: note.createdAt.toISOString(),
|
||||
object,
|
||||
} as ICreate;
|
||||
};
|
||||
|
||||
if (object.to) activity.to = object.to;
|
||||
if (object.cc) activity.cc = object.cc;
|
||||
@@ -209,7 +208,7 @@ export class ApRendererService {
|
||||
* @param id Follower|Followee ID
|
||||
*/
|
||||
@bindThis
|
||||
public async renderFollowUser(id: User['id']) {
|
||||
public async renderFollowUser(id: User['id']): Promise<string> {
|
||||
const user = await this.usersRepository.findOneByOrFail({ id: id }) as PartialLocalUser | PartialRemoteUser;
|
||||
return this.userEntityService.getUserUri(user);
|
||||
}
|
||||
@@ -223,8 +222,8 @@ export class ApRendererService {
|
||||
return {
|
||||
id: requestId ?? `${this.config.url}/follows/${follower.id}/${followee.id}`,
|
||||
type: 'Follow',
|
||||
actor: this.userEntityService.getUserUri(follower)!,
|
||||
object: this.userEntityService.getUserUri(followee)!,
|
||||
actor: this.userEntityService.getUserUri(follower),
|
||||
object: this.userEntityService.getUserUri(followee),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -264,14 +263,14 @@ export class ApRendererService {
|
||||
public async renderLike(noteReaction: NoteReaction, note: { uri: string | null }): Promise<ILike> {
|
||||
const reaction = noteReaction.reaction;
|
||||
|
||||
const object = {
|
||||
const object: ILike = {
|
||||
type: 'Like',
|
||||
id: `${this.config.url}/likes/${noteReaction.id}`,
|
||||
actor: `${this.config.url}/users/${noteReaction.userId}`,
|
||||
object: note.uri ? note.uri : `${this.config.url}/notes/${noteReaction.noteId}`,
|
||||
content: reaction,
|
||||
_misskey_reaction: reaction,
|
||||
} as ILike;
|
||||
};
|
||||
|
||||
if (reaction.startsWith(':')) {
|
||||
const name = reaction.replaceAll(':', '');
|
||||
@@ -287,7 +286,7 @@ export class ApRendererService {
|
||||
public renderMention(mention: PartialLocalUser | PartialRemoteUser): IApMention {
|
||||
return {
|
||||
type: 'Mention',
|
||||
href: this.userEntityService.getUserUri(mention)!,
|
||||
href: this.userEntityService.getUserUri(mention),
|
||||
name: this.userEntityService.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as LocalUser).username}`,
|
||||
};
|
||||
}
|
||||
@@ -297,8 +296,8 @@ export class ApRendererService {
|
||||
src: PartialLocalUser | PartialRemoteUser,
|
||||
dst: PartialLocalUser | PartialRemoteUser,
|
||||
): IMove {
|
||||
const actor = this.userEntityService.getUserUri(src)!;
|
||||
const target = this.userEntityService.getUserUri(dst)!;
|
||||
const actor = this.userEntityService.getUserUri(src);
|
||||
const target = this.userEntityService.getUserUri(dst);
|
||||
return {
|
||||
id: `${this.config.url}/moves/${src.id}/${dst.id}`,
|
||||
actor,
|
||||
@@ -310,10 +309,10 @@ export class ApRendererService {
|
||||
|
||||
@bindThis
|
||||
public async renderNote(note: Note, dive = true): Promise<IPost> {
|
||||
const getPromisedFiles = async (ids: string[]) => {
|
||||
if (!ids || ids.length === 0) return [];
|
||||
const getPromisedFiles = async (ids: string[]): Promise<DriveFile[]> => {
|
||||
if (ids.length === 0) return [];
|
||||
const items = await this.driveFilesRepository.findBy({ id: In(ids) });
|
||||
return ids.map(id => items.find(item => item.id === id)).filter(item => item != null) as DriveFile[];
|
||||
return ids.map(id => items.find(item => item.id === id)).filter((item): item is DriveFile => item != null);
|
||||
};
|
||||
|
||||
let inReplyTo;
|
||||
@@ -323,9 +322,9 @@ export class ApRendererService {
|
||||
inReplyToNote = await this.notesRepository.findOneBy({ id: note.replyId });
|
||||
|
||||
if (inReplyToNote != null) {
|
||||
const inReplyToUser = await this.usersRepository.findOneBy({ id: inReplyToNote.userId });
|
||||
const inReplyToUserExist = await this.usersRepository.exist({ where: { id: inReplyToNote.userId } });
|
||||
|
||||
if (inReplyToUser != null) {
|
||||
if (inReplyToUserExist) {
|
||||
if (inReplyToNote.uri) {
|
||||
inReplyTo = inReplyToNote.uri;
|
||||
} else {
|
||||
@@ -375,7 +374,7 @@ export class ApRendererService {
|
||||
id: In(note.mentions),
|
||||
}) : [];
|
||||
|
||||
const hashtagTags = (note.tags ?? []).map(tag => this.renderHashtag(tag));
|
||||
const hashtagTags = note.tags.map(tag => this.renderHashtag(tag));
|
||||
const mentionTags = mentionedUsers.map(u => this.renderMention(u as LocalUser | RemoteUser));
|
||||
|
||||
const files = await getPromisedFiles(note.fileIds);
|
||||
@@ -451,37 +450,26 @@ export class ApRendererService {
|
||||
@bindThis
|
||||
public async renderPerson(user: LocalUser) {
|
||||
const id = this.userEntityService.genLocalUserUri(user.id);
|
||||
const isSystem = !!user.username.match(/\./);
|
||||
const isSystem = user.username.includes('.');
|
||||
|
||||
const [avatar, banner, profile] = await Promise.all([
|
||||
user.avatarId ? this.driveFilesRepository.findOneBy({ id: user.avatarId }) : Promise.resolve(undefined),
|
||||
user.bannerId ? this.driveFilesRepository.findOneBy({ id: user.bannerId }) : Promise.resolve(undefined),
|
||||
user.avatarId ? this.driveFilesRepository.findOneBy({ id: user.avatarId }) : undefined,
|
||||
user.bannerId ? this.driveFilesRepository.findOneBy({ id: user.bannerId }) : undefined,
|
||||
this.userProfilesRepository.findOneByOrFail({ userId: user.id }),
|
||||
]);
|
||||
|
||||
const attachment: {
|
||||
const attachment = profile.fields.map(field => ({
|
||||
type: 'PropertyValue',
|
||||
name: string,
|
||||
value: string,
|
||||
identifier?: IIdentifier,
|
||||
}[] = [];
|
||||
|
||||
if (profile.fields) {
|
||||
for (const field of profile.fields) {
|
||||
attachment.push({
|
||||
type: 'PropertyValue',
|
||||
name: field.name,
|
||||
value: (field.value != null && field.value.match(/^https?:/))
|
||||
? `<a href="${new URL(field.value).href}" rel="me nofollow noopener" target="_blank">${new URL(field.value).href}</a>`
|
||||
: field.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
name: field.name,
|
||||
value: /^https?:/.test(field.value)
|
||||
? `<a href="${new URL(field.value).href}" rel="me nofollow noopener" target="_blank">${new URL(field.value).href}</a>`
|
||||
: field.value,
|
||||
}));
|
||||
|
||||
const emojis = await this.getEmojis(user.emojis);
|
||||
const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji));
|
||||
|
||||
const hashtagTags = (user.tags ?? []).map(tag => this.renderHashtag(tag));
|
||||
const hashtagTags = user.tags.map(tag => this.renderHashtag(tag));
|
||||
|
||||
const tag = [
|
||||
...apemojis,
|
||||
@@ -490,7 +478,7 @@ export class ApRendererService {
|
||||
|
||||
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
||||
|
||||
const person = {
|
||||
const person: any = {
|
||||
type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person',
|
||||
id,
|
||||
inbox: `${id}/inbox`,
|
||||
@@ -508,11 +496,11 @@ export class ApRendererService {
|
||||
image: banner ? this.renderImage(banner) : null,
|
||||
tag,
|
||||
manuallyApprovesFollowers: user.isLocked,
|
||||
discoverable: !!user.isExplorable,
|
||||
discoverable: user.isExplorable,
|
||||
publicKey: this.renderKey(user, keypair, '#main-key'),
|
||||
isCat: user.isCat,
|
||||
attachment: attachment.length ? attachment : undefined,
|
||||
} as any;
|
||||
};
|
||||
|
||||
if (user.movedToUri) {
|
||||
person.movedTo = user.movedToUri;
|
||||
@@ -552,7 +540,7 @@ export class ApRendererService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderReject(object: any, user: { id: User['id'] }): IReject {
|
||||
public renderReject(object: string | IObject, user: { id: User['id'] }): IReject {
|
||||
return {
|
||||
type: 'Reject',
|
||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||
@@ -561,7 +549,7 @@ export class ApRendererService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderRemove(user: { id: User['id'] }, target: any, object: any): IRemove {
|
||||
public renderRemove(user: { id: User['id'] }, target: string | IObject | undefined, object: string | IObject): IRemove {
|
||||
return {
|
||||
type: 'Remove',
|
||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||
@@ -579,8 +567,8 @@ export class ApRendererService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderUndo(object: any, user: { id: User['id'] }): IUndo {
|
||||
const id = typeof object.id === 'string' && object.id.startsWith(this.config.url) ? `${object.id}/undo` : undefined;
|
||||
public renderUndo(object: string | IObject, user: { id: User['id'] }): IUndo {
|
||||
const id = typeof object !== 'string' && typeof object.id === 'string' && object.id.startsWith(this.config.url) ? `${object.id}/undo` : undefined;
|
||||
|
||||
return {
|
||||
type: 'Undo',
|
||||
@@ -592,7 +580,7 @@ export class ApRendererService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public renderUpdate(object: any, user: { id: User['id'] }): IUpdate {
|
||||
public renderUpdate(object: string | IObject, user: { id: User['id'] }): IUpdate {
|
||||
return {
|
||||
id: `${this.config.url}/users/${user.id}#updates/${new Date().getTime()}`,
|
||||
actor: this.userEntityService.genLocalUserUri(user.id),
|
||||
@@ -658,7 +646,7 @@ export class ApRendererService {
|
||||
vcard: 'http://www.w3.org/2006/vcard/ns#',
|
||||
},
|
||||
],
|
||||
}, x as T & { id: string; });
|
||||
}, x as T & { id: string });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -683,13 +671,13 @@ export class ApRendererService {
|
||||
*/
|
||||
@bindThis
|
||||
public renderOrderedCollectionPage(id: string, totalItems: any, orderedItems: any, partOf: string, prev?: string, next?: string) {
|
||||
const page = {
|
||||
const page: any = {
|
||||
id,
|
||||
partOf,
|
||||
type: 'OrderedCollectionPage',
|
||||
totalItems,
|
||||
orderedItems,
|
||||
} as any;
|
||||
};
|
||||
|
||||
if (prev) page.prev = prev;
|
||||
if (next) page.next = next;
|
||||
@@ -706,7 +694,7 @@ export class ApRendererService {
|
||||
* @param orderedItems attached objects (optional)
|
||||
*/
|
||||
@bindThis
|
||||
public renderOrderedCollection(id: string | null, totalItems: any, first?: string, last?: string, orderedItems?: IObject[]) {
|
||||
public renderOrderedCollection(id: string, totalItems: number, first?: string, last?: string, orderedItems?: IObject[]) {
|
||||
const page: any = {
|
||||
id,
|
||||
type: 'OrderedCollection',
|
||||
@@ -722,7 +710,7 @@ export class ApRendererService {
|
||||
|
||||
@bindThis
|
||||
private async getEmojis(names: string[]): Promise<Emoji[]> {
|
||||
if (names == null || names.length === 0) return [];
|
||||
if (names.length === 0) return [];
|
||||
|
||||
const allEmojis = await this.customEmojiService.localEmojisCache.fetch();
|
||||
const emojis = names.map(name => allEmojis.get(name)).filter(isNotNull);
|
||||
|
@@ -140,7 +140,7 @@ export class ApRequestService {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async signedPost(user: { id: User['id'] }, url: string, object: any) {
|
||||
public async signedPost(user: { id: User['id'] }, url: string, object: unknown): Promise<void> {
|
||||
const body = JSON.stringify(object);
|
||||
|
||||
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
||||
@@ -169,7 +169,7 @@ export class ApRequestService {
|
||||
* @param url URL to fetch
|
||||
*/
|
||||
@bindThis
|
||||
public async signedGet(url: string, user: { id: User['id'] }) {
|
||||
public async signedGet(url: string, user: { id: User['id'] }): Promise<unknown> {
|
||||
const keypair = await this.userKeypairService.getUserKeypair(user.id);
|
||||
|
||||
const req = ApRequestCreator.createSignedGet({
|
||||
|
@@ -61,10 +61,6 @@ export class Resolver {
|
||||
|
||||
@bindThis
|
||||
public async resolve(value: string | IObject): Promise<IObject> {
|
||||
if (value == null) {
|
||||
throw new Error('resolvee is null (or undefined)');
|
||||
}
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
return value;
|
||||
}
|
||||
@@ -104,11 +100,11 @@ export class Resolver {
|
||||
? await this.apRequestService.signedGet(value, this.user) as IObject
|
||||
: await this.httpRequestService.getJson(value, 'application/activity+json, application/ld+json')) as IObject;
|
||||
|
||||
if (object == null || (
|
||||
if (
|
||||
Array.isArray(object['@context']) ?
|
||||
!(object['@context'] as unknown[]).includes('https://www.w3.org/ns/activitystreams') :
|
||||
object['@context'] !== 'https://www.w3.org/ns/activitystreams'
|
||||
)) {
|
||||
) {
|
||||
throw new Error('invalid response');
|
||||
}
|
||||
|
||||
|
@@ -3,6 +3,8 @@ import { Injectable } from '@nestjs/common';
|
||||
import { HttpRequestService } from '@/core/HttpRequestService.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { CONTEXTS } from './misc/contexts.js';
|
||||
import type { JsonLdDocument } from 'jsonld';
|
||||
import type { JsonLd, RemoteDocument } from 'jsonld/jsonld-spec.js';
|
||||
|
||||
// RsaSignature2017 based from https://github.com/transmute-industries/RsaSignature2017
|
||||
|
||||
@@ -18,22 +20,21 @@ class LdSignature {
|
||||
|
||||
@bindThis
|
||||
public async signRsaSignature2017(data: any, privateKey: string, creator: string, domain?: string, created?: Date): Promise<any> {
|
||||
const options = {
|
||||
type: 'RsaSignature2017',
|
||||
creator,
|
||||
domain,
|
||||
nonce: crypto.randomBytes(16).toString('hex'),
|
||||
created: (created ?? new Date()).toISOString(),
|
||||
} as {
|
||||
const options: {
|
||||
type: string;
|
||||
creator: string;
|
||||
domain?: string;
|
||||
nonce: string;
|
||||
created: string;
|
||||
} = {
|
||||
type: 'RsaSignature2017',
|
||||
creator,
|
||||
nonce: crypto.randomBytes(16).toString('hex'),
|
||||
created: (created ?? new Date()).toISOString(),
|
||||
};
|
||||
|
||||
if (!domain) {
|
||||
delete options.domain;
|
||||
if (domain) {
|
||||
options.domain = domain;
|
||||
}
|
||||
|
||||
const toBeSigned = await this.createVerifyData(data, options);
|
||||
@@ -62,7 +63,7 @@ class LdSignature {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async createVerifyData(data: any, options: any) {
|
||||
public async createVerifyData(data: any, options: any): Promise<string> {
|
||||
const transformedOptions = {
|
||||
...options,
|
||||
'@context': 'https://w3id.org/identity/v1',
|
||||
@@ -82,7 +83,7 @@ class LdSignature {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async normalize(data: any) {
|
||||
public async normalize(data: JsonLdDocument): Promise<string> {
|
||||
const customLoader = this.getLoader();
|
||||
// XXX: Importing jsonld dynamically since Jest frequently fails to import it statically
|
||||
// https://github.com/misskey-dev/misskey/pull/9894#discussion_r1103753595
|
||||
@@ -93,14 +94,14 @@ class LdSignature {
|
||||
|
||||
@bindThis
|
||||
private getLoader() {
|
||||
return async (url: string): Promise<any> => {
|
||||
if (!url.match('^https?\:\/\/')) throw new Error(`Invalid URL ${url}`);
|
||||
return async (url: string): Promise<RemoteDocument> => {
|
||||
if (!/^https?:\/\//.test(url)) throw new Error(`Invalid URL ${url}`);
|
||||
|
||||
if (this.preLoad) {
|
||||
if (url in CONTEXTS) {
|
||||
if (this.debug) console.debug(`HIT: ${url}`);
|
||||
return {
|
||||
contextUrl: null,
|
||||
contextUrl: undefined,
|
||||
document: CONTEXTS[url],
|
||||
documentUrl: url,
|
||||
};
|
||||
@@ -110,7 +111,7 @@ class LdSignature {
|
||||
if (this.debug) console.debug(`MISS: ${url}`);
|
||||
const document = await this.fetchDocument(url);
|
||||
return {
|
||||
contextUrl: null,
|
||||
contextUrl: undefined,
|
||||
document: document,
|
||||
documentUrl: url,
|
||||
};
|
||||
@@ -118,13 +119,17 @@ class LdSignature {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async fetchDocument(url: string) {
|
||||
const json = await this.httpRequestService.send(url, {
|
||||
headers: {
|
||||
Accept: 'application/ld+json, application/json',
|
||||
private async fetchDocument(url: string): Promise<JsonLd> {
|
||||
const json = await this.httpRequestService.send(
|
||||
url,
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/ld+json, application/json',
|
||||
},
|
||||
timeout: this.loderTimeout,
|
||||
},
|
||||
timeout: this.loderTimeout,
|
||||
}, { throwErrorWhenResponseNotOk: false }).then(res => {
|
||||
{ throwErrorWhenResponseNotOk: false },
|
||||
).then(res => {
|
||||
if (!res.ok) {
|
||||
throw new Error(`${res.status} ${res.statusText}`);
|
||||
} else {
|
||||
@@ -132,7 +137,7 @@ class LdSignature {
|
||||
}
|
||||
});
|
||||
|
||||
return json;
|
||||
return json as JsonLd;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import type { JsonLd } from 'jsonld/jsonld-spec.js';
|
||||
|
||||
/* eslint:disable:quotemark indent */
|
||||
const id_v1 = {
|
||||
'@context': {
|
||||
@@ -86,7 +88,7 @@ const id_v1 = {
|
||||
'accessControl': { '@id': 'perm:accessControl', '@type': '@id' },
|
||||
'writePermission': { '@id': 'perm:writePermission', '@type': '@id' },
|
||||
},
|
||||
};
|
||||
} satisfies JsonLd;
|
||||
|
||||
const security_v1 = {
|
||||
'@context': {
|
||||
@@ -137,7 +139,7 @@ const security_v1 = {
|
||||
'signatureAlgorithm': 'sec:signingAlgorithm',
|
||||
'signatureValue': 'sec:signatureValue',
|
||||
},
|
||||
};
|
||||
} satisfies JsonLd;
|
||||
|
||||
const activitystreams = {
|
||||
'@context': {
|
||||
@@ -517,9 +519,9 @@ const activitystreams = {
|
||||
'@type': '@id',
|
||||
},
|
||||
},
|
||||
};
|
||||
} satisfies JsonLd;
|
||||
|
||||
export const CONTEXTS: Record<string, unknown> = {
|
||||
export const CONTEXTS: Record<string, JsonLd> = {
|
||||
'https://w3id.org/identity/v1': id_v1,
|
||||
'https://w3id.org/security/v1': security_v1,
|
||||
'https://www.w3.org/ns/activitystreams': activitystreams,
|
||||
|
@@ -177,7 +177,7 @@ export class ApNoteService {
|
||||
|
||||
// リプライ
|
||||
const reply: Note | null = note.inReplyTo
|
||||
? await this.resolveNote(note.inReplyTo, resolver)
|
||||
? await this.resolveNote(note.inReplyTo, { resolver })
|
||||
.then(x => {
|
||||
if (x == null) {
|
||||
this.logger.warn('Specified inReplyTo, but not found');
|
||||
@@ -200,7 +200,7 @@ export class ApNoteService {
|
||||
| { status: 'ok'; res: Note }
|
||||
| { status: 'permerror' | 'temperror' }
|
||||
> => {
|
||||
if (!uri.match(/^https?:/)) return { status: 'permerror' };
|
||||
if (!/^https?:/.test(uri)) return { status: 'permerror' };
|
||||
try {
|
||||
const res = await this.resolveNote(uri);
|
||||
if (res == null) return { status: 'permerror' };
|
||||
@@ -293,9 +293,8 @@ export class ApNoteService {
|
||||
* リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
|
||||
*/
|
||||
@bindThis
|
||||
public async resolveNote(value: string | IObject, resolver?: Resolver): Promise<Note | null> {
|
||||
const uri = typeof value === 'string' ? value : value.id;
|
||||
if (uri == null) throw new Error('missing uri');
|
||||
public async resolveNote(value: string | IObject, options: { sentFrom?: URL, resolver?: Resolver } = {}): Promise<Note | null> {
|
||||
const uri = getApId(value);
|
||||
|
||||
// ブロックしていたら中断
|
||||
const meta = await this.metaService.fetch();
|
||||
@@ -318,7 +317,8 @@ export class ApNoteService {
|
||||
// リモートサーバーからフェッチしてきて登録
|
||||
// ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにノートが生成されるが
|
||||
// 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。
|
||||
return await this.createNote(uri, resolver, true);
|
||||
const createFrom = options.sentFrom?.origin === new URL(uri).origin ? value : uri;
|
||||
return await this.createNote(createFrom, options.resolver, true);
|
||||
} finally {
|
||||
unlock();
|
||||
}
|
||||
|
@@ -260,7 +260,7 @@ export class ApPersonService implements OnModuleInit {
|
||||
// Create user
|
||||
let user: RemoteUser | null = null;
|
||||
try {
|
||||
// Start transaction
|
||||
// Start transaction
|
||||
await this.db.transaction(async transactionalEntityManager => {
|
||||
user = await transactionalEntityManager.save(new User({
|
||||
id: this.idService.genId(),
|
||||
@@ -306,9 +306,9 @@ export class ApPersonService implements OnModuleInit {
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
// duplicate key error
|
||||
// duplicate key error
|
||||
if (isDuplicateKeyValueError(e)) {
|
||||
// /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応
|
||||
// /users/@a => /users/:id のように入力がaliasなときにエラーになることがあるのを対応
|
||||
const u = await this.usersRepository.findOneBy({ uri: person.id });
|
||||
if (u == null) throw new Error('already registered');
|
||||
|
||||
@@ -604,7 +604,10 @@ export class ApPersonService implements OnModuleInit {
|
||||
const featuredNotes = await Promise.all(items
|
||||
.filter(item => getApType(item) === 'Note') // TODO: Noteでなくてもいいかも
|
||||
.slice(0, 5)
|
||||
.map(item => limit(() => this.apNoteService.resolveNote(item, _resolver))));
|
||||
.map(item => limit(() => this.apNoteService.resolveNote(item, {
|
||||
resolver: _resolver,
|
||||
sentFrom: new URL(user.uri),
|
||||
}))));
|
||||
|
||||
await this.db.transaction(async transactionalEntityManager => {
|
||||
await transactionalEntityManager.delete(UserNotePining, { userId: user.id });
|
||||
|
@@ -194,7 +194,6 @@ export interface IApPropertyValue extends IObject {
|
||||
}
|
||||
|
||||
export const isPropertyValue = (object: IObject): object is IApPropertyValue =>
|
||||
object &&
|
||||
getApType(object) === 'PropertyValue' &&
|
||||
typeof object.name === 'string' &&
|
||||
'value' in object &&
|
||||
|
@@ -254,7 +254,7 @@ export default abstract class Chart<T extends Schema> {
|
||||
private convertRawRecord(x: RawRecord<T>): KVs<T> {
|
||||
const kvs = {} as Record<string, number>;
|
||||
for (const k of Object.keys(x).filter((k) => k.startsWith(COLUMN_PREFIX)) as (keyof Columns<T>)[]) {
|
||||
kvs[(k as string).substr(COLUMN_PREFIX.length).split(COLUMN_DELIMITER).join('.')] = x[k] as unknown as number;
|
||||
kvs[(k as string).substring(COLUMN_PREFIX.length).split(COLUMN_DELIMITER).join('.')] = x[k] as unknown as number;
|
||||
}
|
||||
return kvs as KVs<T>;
|
||||
}
|
||||
@@ -627,7 +627,7 @@ export default abstract class Chart<T extends Schema> {
|
||||
}
|
||||
|
||||
// 要求された範囲の最も古い箇所に位置するログが存在しなかったら
|
||||
} else if (!isTimeSame(new Date(logs[logs.length - 1].date * 1000), gt)) {
|
||||
} else if (!isTimeSame(new Date(logs.at(-1)!.date * 1000), gt)) {
|
||||
// 要求された範囲の最も古い箇所時点での最も新しいログを持ってきて末尾に追加する
|
||||
// (隙間埋めできないため)
|
||||
const outdatedLog = await repository.findOne({
|
||||
|
@@ -47,17 +47,26 @@ export class ChannelEntityService {
|
||||
|
||||
const banner = channel.bannerId ? await this.driveFilesRepository.findOneBy({ id: channel.bannerId }) : null;
|
||||
|
||||
const hasUnreadNote = meId ? (await this.noteUnreadsRepository.findOneBy({ noteChannelId: channel.id, userId: meId })) != null : undefined;
|
||||
const hasUnreadNote = meId ? await this.noteUnreadsRepository.exist({
|
||||
where: {
|
||||
noteChannelId: channel.id,
|
||||
userId: meId
|
||||
},
|
||||
}) : undefined;
|
||||
|
||||
const following = meId ? await this.channelFollowingsRepository.findOneBy({
|
||||
followerId: meId,
|
||||
followeeId: channel.id,
|
||||
}) : null;
|
||||
const isFollowing = meId ? await this.channelFollowingsRepository.exist({
|
||||
where: {
|
||||
followerId: meId,
|
||||
followeeId: channel.id,
|
||||
},
|
||||
}) : false;
|
||||
|
||||
const favorite = meId ? await this.channelFavoritesRepository.findOneBy({
|
||||
userId: meId,
|
||||
channelId: channel.id,
|
||||
}) : null;
|
||||
const isFavorited = meId ? await this.channelFavoritesRepository.exist({
|
||||
where: {
|
||||
userId: meId,
|
||||
channelId: channel.id,
|
||||
},
|
||||
}) : false;
|
||||
|
||||
const pinnedNotes = channel.pinnedNoteIds.length > 0 ? await this.notesRepository.find({
|
||||
where: {
|
||||
@@ -80,8 +89,8 @@ export class ChannelEntityService {
|
||||
notesCount: channel.notesCount,
|
||||
|
||||
...(me ? {
|
||||
isFollowing: following != null,
|
||||
isFavorited: favorite != null,
|
||||
isFollowing,
|
||||
isFavorited,
|
||||
hasUnreadNote,
|
||||
} : {}),
|
||||
|
||||
|
@@ -39,7 +39,7 @@ export class ClipEntityService {
|
||||
description: clip.description,
|
||||
isPublic: clip.isPublic,
|
||||
favoritedCount: await this.clipFavoritesRepository.countBy({ clipId: clip.id }),
|
||||
isFavorited: meId ? await this.clipFavoritesRepository.findOneBy({ clipId: clip.id, userId: meId }).then(x => x != null) : undefined,
|
||||
isFavorited: meId ? await this.clipFavoritesRepository.exist({ where: { clipId: clip.id, userId: meId } }) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -40,7 +40,7 @@ export class FlashEntityService {
|
||||
summary: flash.summary,
|
||||
script: flash.script,
|
||||
likedCount: flash.likedCount,
|
||||
isLiked: meId ? await this.flashLikesRepository.findOneBy({ flashId: flash.id, userId: meId }).then(x => x != null) : undefined,
|
||||
isLiked: meId ? await this.flashLikesRepository.exist({ where: { flashId: flash.id, userId: meId } }) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -46,7 +46,7 @@ export class GalleryPostEntityService {
|
||||
tags: post.tags.length > 0 ? post.tags : undefined,
|
||||
isSensitive: post.isSensitive,
|
||||
likedCount: post.likedCount,
|
||||
isLiked: meId ? await this.galleryLikesRepository.findOneBy({ postId: post.id, userId: meId }).then(x => x != null) : undefined,
|
||||
isLiked: meId ? await this.galleryLikesRepository.exist({ where: { postId: post.id, userId: meId } }) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,52 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { RegistrationTicketsRepository } from '@/models/index.js';
|
||||
import { awaitAll } from '@/misc/prelude/await-all.js';
|
||||
import type { Packed } from '@/misc/json-schema.js';
|
||||
import type { User } from '@/models/entities/User.js';
|
||||
import type { RegistrationTicket } from '@/models/entities/RegistrationTicket.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { UserEntityService } from './UserEntityService.js';
|
||||
|
||||
@Injectable()
|
||||
export class InviteCodeEntityService {
|
||||
constructor(
|
||||
@Inject(DI.registrationTicketsRepository)
|
||||
private registrationTicketsRepository: RegistrationTicketsRepository,
|
||||
|
||||
private userEntityService: UserEntityService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async pack(
|
||||
src: RegistrationTicket['id'] | RegistrationTicket,
|
||||
me?: { id: User['id'] } | null | undefined,
|
||||
): Promise<Packed<'InviteCode'>> {
|
||||
const target = typeof src === 'object' ? src : await this.registrationTicketsRepository.findOneOrFail({
|
||||
where: {
|
||||
id: src,
|
||||
},
|
||||
relations: ['createdBy', 'usedBy'],
|
||||
});
|
||||
|
||||
return await awaitAll({
|
||||
id: target.id,
|
||||
code: target.code,
|
||||
expiresAt: target.expiresAt ? target.expiresAt.toISOString() : null,
|
||||
createdAt: target.createdAt.toISOString(),
|
||||
createdBy: target.createdBy ? await this.userEntityService.pack(target.createdBy, me) : null,
|
||||
usedBy: target.usedBy ? await this.userEntityService.pack(target.usedBy, me) : null,
|
||||
usedAt: target.usedAt ? target.usedAt.toISOString() : null,
|
||||
used: !!target.usedAt,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public packMany(
|
||||
targets: any[],
|
||||
me: { id: User['id'] },
|
||||
) {
|
||||
return Promise.all(targets.map(x => this.pack(x, me)));
|
||||
}
|
||||
}
|
@@ -106,16 +106,14 @@ export class NoteEntityService implements OnModuleInit {
|
||||
hide = false;
|
||||
} else {
|
||||
// フォロワーかどうか
|
||||
const following = await this.followingsRepository.findOneBy({
|
||||
followeeId: packedNote.userId,
|
||||
followerId: meId,
|
||||
const isFollowing = await this.followingsRepository.exist({
|
||||
where: {
|
||||
followeeId: packedNote.userId,
|
||||
followerId: meId,
|
||||
},
|
||||
});
|
||||
|
||||
if (following == null) {
|
||||
hide = true;
|
||||
} else {
|
||||
hide = false;
|
||||
}
|
||||
hide = !isFollowing;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -97,7 +97,7 @@ export class PageEntityService {
|
||||
eyeCatchingImage: page.eyeCatchingImageId ? await this.driveFileEntityService.pack(page.eyeCatchingImageId) : null,
|
||||
attachedFiles: this.driveFileEntityService.packMany((await Promise.all(attachedFiles)).filter((x): x is DriveFile => x != null)),
|
||||
likedCount: page.likedCount,
|
||||
isLiked: meId ? await this.pageLikesRepository.findOneBy({ pageId: page.id, userId: meId }).then(x => x != null) : undefined,
|
||||
isLiked: meId ? await this.pageLikesRepository.exist({ where: { pageId: page.id, userId: meId } }) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -230,12 +230,14 @@ export class UserEntityService implements OnModuleInit {
|
||||
/*
|
||||
const myAntennas = (await this.antennaService.getAntennas()).filter(a => a.userId === userId);
|
||||
|
||||
const unread = myAntennas.length > 0 ? await this.antennaNotesRepository.findOneBy({
|
||||
antennaId: In(myAntennas.map(x => x.id)),
|
||||
read: false,
|
||||
}) : null;
|
||||
const isUnread = (myAntennas.length > 0 ? await this.antennaNotesRepository.exist({
|
||||
where: {
|
||||
antennaId: In(myAntennas.map(x => x.id)),
|
||||
read: false,
|
||||
},
|
||||
}) : false);
|
||||
|
||||
return unread != null;
|
||||
return isUnread;
|
||||
*/
|
||||
return false; // TODO
|
||||
}
|
||||
|
@@ -4,7 +4,7 @@ export type Acct = {
|
||||
};
|
||||
|
||||
export function parse(acct: string): Acct {
|
||||
if (acct.startsWith('@')) acct = acct.substr(1);
|
||||
if (acct.startsWith('@')) acct = acct.substring(1);
|
||||
const split = acct.split('@', 2);
|
||||
return { username: split[0], host: split[1] ?? null };
|
||||
}
|
||||
|
@@ -181,14 +181,28 @@ export class RedisSingleCache<T> {
|
||||
|
||||
// TODO: メモリ節約のためあまり参照されないキーを定期的に削除できるようにする?
|
||||
|
||||
export class MemoryKVCache<T> {
|
||||
public cache: Map<string, { date: number; value: T; }>;
|
||||
function nothingToDo<T, V = T>(value: T): V {
|
||||
return value as unknown as V;
|
||||
}
|
||||
|
||||
export class MemoryKVCache<T, V = T> {
|
||||
public cache: Map<string, { date: number; value: V; }>;
|
||||
private lifetime: number;
|
||||
private gcIntervalHandle: NodeJS.Timer;
|
||||
private toMapConverter: (value: T) => V;
|
||||
private fromMapConverter: (cached: V) => T | undefined;
|
||||
|
||||
constructor(lifetime: MemoryKVCache<never>['lifetime']) {
|
||||
constructor(lifetime: MemoryKVCache<never>['lifetime'], options: {
|
||||
toMapConverter: (value: T) => V;
|
||||
fromMapConverter: (cached: V) => T | undefined;
|
||||
} = {
|
||||
toMapConverter: nothingToDo,
|
||||
fromMapConverter: nothingToDo,
|
||||
}) {
|
||||
this.cache = new Map();
|
||||
this.lifetime = lifetime;
|
||||
this.toMapConverter = options.toMapConverter;
|
||||
this.fromMapConverter = options.fromMapConverter;
|
||||
|
||||
this.gcIntervalHandle = setInterval(() => {
|
||||
this.gc();
|
||||
@@ -199,7 +213,7 @@ export class MemoryKVCache<T> {
|
||||
public set(key: string, value: T): void {
|
||||
this.cache.set(key, {
|
||||
date: Date.now(),
|
||||
value,
|
||||
value: this.toMapConverter(value),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -211,7 +225,7 @@ export class MemoryKVCache<T> {
|
||||
this.cache.delete(key);
|
||||
return undefined;
|
||||
}
|
||||
return cached.value;
|
||||
return this.fromMapConverter(cached.value);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -222,9 +236,10 @@ export class MemoryKVCache<T> {
|
||||
/**
|
||||
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
|
||||
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
|
||||
* fetcherの引数はcacheに保存されている値があれば渡されます
|
||||
*/
|
||||
@bindThis
|
||||
public async fetch(key: string, fetcher: () => Promise<T>, validator?: (cachedValue: T) => boolean): Promise<T> {
|
||||
public async fetch(key: string, fetcher: (value: V | undefined) => Promise<T>, validator?: (cachedValue: T) => boolean): Promise<T> {
|
||||
const cachedValue = this.get(key);
|
||||
if (cachedValue !== undefined) {
|
||||
if (validator) {
|
||||
@@ -239,7 +254,7 @@ export class MemoryKVCache<T> {
|
||||
}
|
||||
|
||||
// Cache MISS
|
||||
const value = await fetcher();
|
||||
const value = await fetcher(this.cache.get(key)?.value);
|
||||
this.set(key, value);
|
||||
return value;
|
||||
}
|
||||
@@ -247,9 +262,10 @@ export class MemoryKVCache<T> {
|
||||
/**
|
||||
* キャッシュがあればそれを返し、無ければfetcherを呼び出して結果をキャッシュ&返します
|
||||
* optional: キャッシュが存在してもvalidatorでfalseを返すとキャッシュ無効扱いにします
|
||||
* fetcherの引数はcacheに保存されている値があれば渡されます
|
||||
*/
|
||||
@bindThis
|
||||
public async fetchMaybe(key: string, fetcher: () => Promise<T | undefined>, validator?: (cachedValue: T) => boolean): Promise<T | undefined> {
|
||||
public async fetchMaybe(key: string, fetcher: (value: V | undefined) => Promise<T | undefined>, validator?: (cachedValue: T) => boolean): Promise<T | undefined> {
|
||||
const cachedValue = this.get(key);
|
||||
if (cachedValue !== undefined) {
|
||||
if (validator) {
|
||||
@@ -264,7 +280,7 @@ export class MemoryKVCache<T> {
|
||||
}
|
||||
|
||||
// Cache MISS
|
||||
const value = await fetcher();
|
||||
const value = await fetcher(this.cache.get(key)?.value);
|
||||
if (value !== undefined) {
|
||||
this.set(key, value);
|
||||
}
|
||||
|
20
packages/backend/src/misc/generate-invite-code.ts
Normal file
20
packages/backend/src/misc/generate-invite-code.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { secureRndstr } from './secure-rndstr.js';
|
||||
|
||||
const CHARS = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ'; // [0-9A-Z] w/o [01IO] (32 patterns)
|
||||
|
||||
export function generateInviteCode(): string {
|
||||
const code = secureRndstr(8, {
|
||||
chars: CHARS,
|
||||
});
|
||||
|
||||
const uniqueId = [];
|
||||
let n = Math.floor(Date.now() / 1000 / 60);
|
||||
while (true) {
|
||||
uniqueId.push(CHARS[n % CHARS.length]);
|
||||
const t = Math.floor(n / CHARS.length);
|
||||
if (!t) break;
|
||||
n = t;
|
||||
}
|
||||
|
||||
return code + uniqueId.reverse().join('');
|
||||
}
|
@@ -19,6 +19,7 @@ import { packedRenoteMutingSchema } from '@/models/json-schema/renote-muting.js'
|
||||
import { packedBlockingSchema } from '@/models/json-schema/blocking.js';
|
||||
import { packedNoteReactionSchema } from '@/models/json-schema/note-reaction.js';
|
||||
import { packedHashtagSchema } from '@/models/json-schema/hashtag.js';
|
||||
import { packedInviteCodeSchema } from '@/models/json-schema/invite-code.js';
|
||||
import { packedPageSchema } from '@/models/json-schema/page.js';
|
||||
import { packedNoteFavoriteSchema } from '@/models/json-schema/note-favorite.js';
|
||||
import { packedChannelSchema } from '@/models/json-schema/channel.js';
|
||||
@@ -52,6 +53,7 @@ export const refs = {
|
||||
RenoteMuting: packedRenoteMutingSchema,
|
||||
Blocking: packedBlockingSchema,
|
||||
Hashtag: packedHashtagSchema,
|
||||
InviteCode: packedInviteCodeSchema,
|
||||
Page: packedPageSchema,
|
||||
Channel: packedChannelSchema,
|
||||
QueueCount: packedQueueCountSchema,
|
||||
|
@@ -67,8 +67,9 @@ export function maximum(xs: number[]): number {
|
||||
export function groupBy<T>(f: EndoRelation<T>, xs: T[]): T[][] {
|
||||
const groups = [] as T[][];
|
||||
for (const x of xs) {
|
||||
if (groups.length !== 0 && f(groups[groups.length - 1][0], x)) {
|
||||
groups[groups.length - 1].push(x);
|
||||
const lastGroup = groups.at(-1);
|
||||
if (lastGroup !== undefined && f(lastGroup[0], x)) {
|
||||
lastGroup.push(x);
|
||||
} else {
|
||||
groups.push([x]);
|
||||
}
|
||||
|
@@ -1,17 +1,60 @@
|
||||
import { PrimaryColumn, Entity, Index, Column } from 'typeorm';
|
||||
import { PrimaryColumn, Entity, Index, Column, ManyToOne, JoinColumn, OneToOne } from 'typeorm';
|
||||
import { id } from '../id.js';
|
||||
import { User } from './User.js';
|
||||
|
||||
@Entity()
|
||||
export class RegistrationTicket {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Column('timestamp with time zone')
|
||||
public createdAt: Date;
|
||||
|
||||
@Index({ unique: true })
|
||||
@Column('varchar', {
|
||||
length: 64,
|
||||
})
|
||||
public code: string;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
nullable: true,
|
||||
})
|
||||
public expiresAt: Date | null;
|
||||
|
||||
@Column('timestamp with time zone')
|
||||
public createdAt: Date;
|
||||
|
||||
@ManyToOne(type => User, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public createdBy: User | null;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
nullable: true,
|
||||
})
|
||||
public createdById: User['id'] | null;
|
||||
|
||||
@OneToOne(type => User, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public usedBy: User | null;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
nullable: true,
|
||||
})
|
||||
public usedById: User['id'] | null;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
nullable: true,
|
||||
})
|
||||
public usedAt: Date | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 32,
|
||||
nullable: true,
|
||||
})
|
||||
public pendingUserId: string | null;
|
||||
}
|
||||
|
45
packages/backend/src/models/json-schema/invite-code.ts
Normal file
45
packages/backend/src/models/json-schema/invite-code.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
export const packedInviteCodeSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
code: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
example: 'GR6S02ERUA5VR',
|
||||
},
|
||||
expiresAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
format: 'date-time',
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'date-time',
|
||||
},
|
||||
createdBy: {
|
||||
type: 'object',
|
||||
optional: false, nullable: true,
|
||||
ref: 'UserLite',
|
||||
},
|
||||
usedBy: {
|
||||
type: 'object',
|
||||
optional: false, nullable: true,
|
||||
ref: 'UserLite',
|
||||
},
|
||||
usedAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
format: 'date-time',
|
||||
},
|
||||
used: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
@@ -1,7 +1,7 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { IsNull, MoreThan, Not } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { DriveFilesRepository } from '@/models/index.js';
|
||||
import type { DriveFile, DriveFilesRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { DriveService } from '@/core/DriveService.js';
|
||||
@@ -31,7 +31,7 @@ export class CleanRemoteFilesProcessorService {
|
||||
this.logger.info('Deleting cached remote files...');
|
||||
|
||||
let deletedCount = 0;
|
||||
let cursor: any = null;
|
||||
let cursor: DriveFile['id'] | null = null;
|
||||
|
||||
while (true) {
|
||||
const files = await this.driveFilesRepository.find({
|
||||
@@ -51,7 +51,7 @@ export class CleanRemoteFilesProcessorService {
|
||||
break;
|
||||
}
|
||||
|
||||
cursor = files[files.length - 1].id;
|
||||
cursor = files.at(-1)?.id ?? null;
|
||||
|
||||
await Promise.all(files.map(file => this.driveService.deleteFileSync(file, true)));
|
||||
|
||||
|
@@ -12,6 +12,7 @@ import { bindThis } from '@/decorators.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
import type { DbUserDeleteJobData } from '../types.js';
|
||||
import { SearchService } from "@/core/SearchService.js";
|
||||
|
||||
@Injectable()
|
||||
export class DeleteAccountProcessorService {
|
||||
@@ -36,6 +37,7 @@ export class DeleteAccountProcessorService {
|
||||
private driveService: DriveService,
|
||||
private emailService: EmailService,
|
||||
private queueLoggerService: QueueLoggerService,
|
||||
private searchService: SearchService,
|
||||
) {
|
||||
this.logger = this.queueLoggerService.logger.createSubLogger('delete-account');
|
||||
}
|
||||
@@ -68,9 +70,13 @@ export class DeleteAccountProcessorService {
|
||||
break;
|
||||
}
|
||||
|
||||
cursor = notes[notes.length - 1].id;
|
||||
cursor = notes.at(-1)?.id ?? null;
|
||||
|
||||
await this.notesRepository.delete(notes.map(note => note.id));
|
||||
|
||||
for (const note of notes) {
|
||||
await this.searchService.unindexNote(note);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.succ('All of notes deleted');
|
||||
@@ -95,7 +101,7 @@ export class DeleteAccountProcessorService {
|
||||
break;
|
||||
}
|
||||
|
||||
cursor = files[files.length - 1].id;
|
||||
cursor = files.at(-1)?.id ?? null;
|
||||
|
||||
for (const file of files) {
|
||||
await this.driveService.deleteFileSync(file);
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { MoreThan } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { UsersRepository, DriveFilesRepository } from '@/models/index.js';
|
||||
import type { UsersRepository, DriveFilesRepository, DriveFile } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { DriveService } from '@/core/DriveService.js';
|
||||
@@ -40,7 +40,7 @@ export class DeleteDriveFilesProcessorService {
|
||||
}
|
||||
|
||||
let deletedCount = 0;
|
||||
let cursor: any = null;
|
||||
let cursor: DriveFile['id'] | null = null;
|
||||
|
||||
while (true) {
|
||||
const files = await this.driveFilesRepository.find({
|
||||
@@ -59,7 +59,7 @@ export class DeleteDriveFilesProcessorService {
|
||||
break;
|
||||
}
|
||||
|
||||
cursor = files[files.length - 1].id;
|
||||
cursor = files.at(-1)?.id ?? null;
|
||||
|
||||
for (const file of files) {
|
||||
await this.driveService.deleteFileSync(file);
|
||||
|
@@ -3,7 +3,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import { MoreThan } from 'typeorm';
|
||||
import { format as dateFormat } from 'date-fns';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { UsersRepository, BlockingsRepository } from '@/models/index.js';
|
||||
import type { UsersRepository, BlockingsRepository, Blocking } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { DriveService } from '@/core/DriveService.js';
|
||||
@@ -53,7 +53,7 @@ export class ExportBlockingProcessorService {
|
||||
const stream = fs.createWriteStream(path, { flags: 'a' });
|
||||
|
||||
let exportedCount = 0;
|
||||
let cursor: any = null;
|
||||
let cursor: Blocking['id'] | null = null;
|
||||
|
||||
while (true) {
|
||||
const blockings = await this.blockingsRepository.find({
|
||||
@@ -72,7 +72,7 @@ export class ExportBlockingProcessorService {
|
||||
break;
|
||||
}
|
||||
|
||||
cursor = blockings[blockings.length - 1].id;
|
||||
cursor = blockings.at(-1)?.id ?? null;
|
||||
|
||||
for (const block of blockings) {
|
||||
const u = await this.usersRepository.findOneBy({ id: block.blockeeId });
|
||||
|
@@ -94,7 +94,7 @@ export class ExportFavoritesProcessorService {
|
||||
break;
|
||||
}
|
||||
|
||||
cursor = favorites[favorites.length - 1].id;
|
||||
cursor = favorites.at(-1)?.id ?? null;
|
||||
|
||||
for (const favorite of favorites) {
|
||||
let poll: Poll | undefined;
|
||||
|
@@ -79,7 +79,7 @@ export class ExportFollowingProcessorService {
|
||||
break;
|
||||
}
|
||||
|
||||
cursor = followings[followings.length - 1].id;
|
||||
cursor = followings.at(-1)?.id ?? null;
|
||||
|
||||
for (const following of followings) {
|
||||
const u = await this.usersRepository.findOneBy({ id: following.followeeId });
|
||||
|
@@ -3,7 +3,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import { IsNull, MoreThan } from 'typeorm';
|
||||
import { format as dateFormat } from 'date-fns';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { MutingsRepository, UsersRepository, BlockingsRepository } from '@/models/index.js';
|
||||
import type { MutingsRepository, UsersRepository, BlockingsRepository, Muting } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { DriveService } from '@/core/DriveService.js';
|
||||
@@ -56,7 +56,7 @@ export class ExportMutingProcessorService {
|
||||
const stream = fs.createWriteStream(path, { flags: 'a' });
|
||||
|
||||
let exportedCount = 0;
|
||||
let cursor: any = null;
|
||||
let cursor: Muting['id'] | null = null;
|
||||
|
||||
while (true) {
|
||||
const mutes = await this.mutingsRepository.find({
|
||||
@@ -76,7 +76,7 @@ export class ExportMutingProcessorService {
|
||||
break;
|
||||
}
|
||||
|
||||
cursor = mutes[mutes.length - 1].id;
|
||||
cursor = mutes.at(-1)?.id ?? null;
|
||||
|
||||
for (const mute of mutes) {
|
||||
const u = await this.usersRepository.findOneBy({ id: mute.muteeId });
|
||||
|
@@ -90,7 +90,7 @@ export class ExportNotesProcessorService {
|
||||
break;
|
||||
}
|
||||
|
||||
cursor = notes[notes.length - 1].id;
|
||||
cursor = notes.at(-1)?.id ?? null;
|
||||
|
||||
for (const note of notes) {
|
||||
let poll: Poll | undefined;
|
||||
|
@@ -181,7 +181,7 @@ export class ActivityPubServerService {
|
||||
undefined,
|
||||
inStock ? `${partOf}?${url.query({
|
||||
page: 'true',
|
||||
cursor: followings[followings.length - 1].id,
|
||||
cursor: followings.at(-1)!.id,
|
||||
})}` : undefined,
|
||||
);
|
||||
|
||||
@@ -189,7 +189,11 @@ export class ActivityPubServerService {
|
||||
return (this.apRendererService.addContext(rendered));
|
||||
} else {
|
||||
// index page
|
||||
const rendered = this.apRendererService.renderOrderedCollection(partOf, user.followersCount, `${partOf}?page=true`);
|
||||
const rendered = this.apRendererService.renderOrderedCollection(
|
||||
partOf,
|
||||
user.followersCount,
|
||||
`${partOf}?page=true`,
|
||||
);
|
||||
reply.header('Cache-Control', 'public, max-age=180');
|
||||
this.setResponseType(request, reply);
|
||||
return (this.apRendererService.addContext(rendered));
|
||||
@@ -269,7 +273,7 @@ export class ActivityPubServerService {
|
||||
undefined,
|
||||
inStock ? `${partOf}?${url.query({
|
||||
page: 'true',
|
||||
cursor: followings[followings.length - 1].id,
|
||||
cursor: followings.at(-1)!.id,
|
||||
})}` : undefined,
|
||||
);
|
||||
|
||||
@@ -277,7 +281,11 @@ export class ActivityPubServerService {
|
||||
return (this.apRendererService.addContext(rendered));
|
||||
} else {
|
||||
// index page
|
||||
const rendered = this.apRendererService.renderOrderedCollection(partOf, user.followingCount, `${partOf}?page=true`);
|
||||
const rendered = this.apRendererService.renderOrderedCollection(
|
||||
partOf,
|
||||
user.followingCount,
|
||||
`${partOf}?page=true`,
|
||||
);
|
||||
reply.header('Cache-Control', 'public, max-age=180');
|
||||
this.setResponseType(request, reply);
|
||||
return (this.apRendererService.addContext(rendered));
|
||||
@@ -310,7 +318,10 @@ export class ActivityPubServerService {
|
||||
|
||||
const rendered = this.apRendererService.renderOrderedCollection(
|
||||
`${this.config.url}/users/${userId}/collections/featured`,
|
||||
renderedNotes.length, undefined, undefined, renderedNotes,
|
||||
renderedNotes.length,
|
||||
undefined,
|
||||
undefined,
|
||||
renderedNotes,
|
||||
);
|
||||
|
||||
reply.header('Cache-Control', 'public, max-age=180');
|
||||
@@ -387,7 +398,7 @@ export class ActivityPubServerService {
|
||||
})}` : undefined,
|
||||
notes.length ? `${partOf}?${url.query({
|
||||
page: 'true',
|
||||
until_id: notes[notes.length - 1].id,
|
||||
until_id: notes.at(-1)!.id,
|
||||
})}` : undefined,
|
||||
);
|
||||
|
||||
@@ -395,7 +406,9 @@ export class ActivityPubServerService {
|
||||
return (this.apRendererService.addContext(rendered));
|
||||
} else {
|
||||
// index page
|
||||
const rendered = this.apRendererService.renderOrderedCollection(partOf, user.notesCount,
|
||||
const rendered = this.apRendererService.renderOrderedCollection(
|
||||
partOf,
|
||||
user.notesCount,
|
||||
`${partOf}?page=true`,
|
||||
`${partOf}?page=true&since_id=000000000000000000000000`,
|
||||
);
|
||||
|
@@ -38,7 +38,8 @@ import * as ep___admin_federation_updateInstance from './endpoints/admin/federat
|
||||
import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js';
|
||||
import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js';
|
||||
import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js';
|
||||
import * as ep___invite from './endpoints/invite.js';
|
||||
import * as ep___admin_invite_create from './endpoints/admin/invite/create.js';
|
||||
import * as ep___admin_invite_list from './endpoints/admin/invite/list.js';
|
||||
import * as ep___admin_promo_create from './endpoints/admin/promo/create.js';
|
||||
import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js';
|
||||
import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js';
|
||||
@@ -230,6 +231,10 @@ import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
|
||||
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
|
||||
import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js';
|
||||
import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js';
|
||||
import * as ep___invite_create from './endpoints/invite/create.js';
|
||||
import * as ep___invite_delete from './endpoints/invite/delete.js';
|
||||
import * as ep___invite_list from './endpoints/invite/list.js';
|
||||
import * as ep___invite_limit from './endpoints/invite/limit.js';
|
||||
import * as ep___meta from './endpoints/meta.js';
|
||||
import * as ep___emojis from './endpoints/emojis.js';
|
||||
import * as ep___emoji from './endpoints/emoji.js';
|
||||
@@ -378,7 +383,8 @@ const $admin_federation_updateInstance: Provider = { provide: 'ep:admin/federati
|
||||
const $admin_getIndexStats: Provider = { provide: 'ep:admin/get-index-stats', useClass: ep___admin_getIndexStats.default };
|
||||
const $admin_getTableStats: Provider = { provide: 'ep:admin/get-table-stats', useClass: ep___admin_getTableStats.default };
|
||||
const $admin_getUserIps: Provider = { provide: 'ep:admin/get-user-ips', useClass: ep___admin_getUserIps.default };
|
||||
const $invite: Provider = { provide: 'ep:invite', useClass: ep___invite.default };
|
||||
const $admin_invite_create: Provider = { provide: 'ep:admin/invite/create', useClass: ep___admin_invite_create.default };
|
||||
const $admin_invite_list: Provider = { provide: 'ep:admin/invite/list', useClass: ep___admin_invite_list.default };
|
||||
const $admin_promo_create: Provider = { provide: 'ep:admin/promo/create', useClass: ep___admin_promo_create.default };
|
||||
const $admin_queue_clear: Provider = { provide: 'ep:admin/queue/clear', useClass: ep___admin_queue_clear.default };
|
||||
const $admin_queue_deliverDelayed: Provider = { provide: 'ep:admin/queue/deliver-delayed', useClass: ep___admin_queue_deliverDelayed.default };
|
||||
@@ -570,6 +576,10 @@ const $i_webhooks_list: Provider = { provide: 'ep:i/webhooks/list', useClass: ep
|
||||
const $i_webhooks_show: Provider = { provide: 'ep:i/webhooks/show', useClass: ep___i_webhooks_show.default };
|
||||
const $i_webhooks_update: Provider = { provide: 'ep:i/webhooks/update', useClass: ep___i_webhooks_update.default };
|
||||
const $i_webhooks_delete: Provider = { provide: 'ep:i/webhooks/delete', useClass: ep___i_webhooks_delete.default };
|
||||
const $invite_create: Provider = { provide: 'ep:invite/create', useClass: ep___invite_create.default };
|
||||
const $invite_delete: Provider = { provide: 'ep:invite/delete', useClass: ep___invite_delete.default };
|
||||
const $invite_list: Provider = { provide: 'ep:invite/list', useClass: ep___invite_list.default };
|
||||
const $invite_limit: Provider = { provide: 'ep:invite/limit', useClass: ep___invite_limit.default };
|
||||
const $meta: Provider = { provide: 'ep:meta', useClass: ep___meta.default };
|
||||
const $emojis: Provider = { provide: 'ep:emojis', useClass: ep___emojis.default };
|
||||
const $emoji: Provider = { provide: 'ep:emoji', useClass: ep___emoji.default };
|
||||
@@ -722,7 +732,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$admin_getIndexStats,
|
||||
$admin_getTableStats,
|
||||
$admin_getUserIps,
|
||||
$invite,
|
||||
$admin_invite_create,
|
||||
$admin_invite_list,
|
||||
$admin_promo_create,
|
||||
$admin_queue_clear,
|
||||
$admin_queue_deliverDelayed,
|
||||
@@ -914,6 +925,10 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$i_webhooks_show,
|
||||
$i_webhooks_update,
|
||||
$i_webhooks_delete,
|
||||
$invite_create,
|
||||
$invite_delete,
|
||||
$invite_list,
|
||||
$invite_limit,
|
||||
$meta,
|
||||
$emojis,
|
||||
$emoji,
|
||||
@@ -1060,7 +1075,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$admin_getIndexStats,
|
||||
$admin_getTableStats,
|
||||
$admin_getUserIps,
|
||||
$invite,
|
||||
$admin_invite_create,
|
||||
$admin_invite_list,
|
||||
$admin_promo_create,
|
||||
$admin_queue_clear,
|
||||
$admin_queue_deliverDelayed,
|
||||
@@ -1252,6 +1268,10 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||
$i_webhooks_show,
|
||||
$i_webhooks_update,
|
||||
$i_webhooks_delete,
|
||||
$invite_create,
|
||||
$invite_delete,
|
||||
$invite_list,
|
||||
$invite_limit,
|
||||
$meta,
|
||||
$emojis,
|
||||
$emoji,
|
||||
|
@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
|
||||
import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository, RegistrationTicket } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { CaptchaService } from '@/core/CaptchaService.js';
|
||||
@@ -109,13 +109,15 @@ export class SignupApiService {
|
||||
}
|
||||
}
|
||||
|
||||
let ticket: RegistrationTicket | null = null;
|
||||
|
||||
if (instance.disableRegistration) {
|
||||
if (invitationCode == null || typeof invitationCode !== 'string') {
|
||||
reply.code(400);
|
||||
return;
|
||||
}
|
||||
|
||||
const ticket = await this.registrationTicketsRepository.findOneBy({
|
||||
ticket = await this.registrationTicketsRepository.findOneBy({
|
||||
code: invitationCode,
|
||||
});
|
||||
|
||||
@@ -124,16 +126,24 @@ export class SignupApiService {
|
||||
return;
|
||||
}
|
||||
|
||||
this.registrationTicketsRepository.delete(ticket.id);
|
||||
if (ticket.expiresAt && ticket.expiresAt < new Date()) {
|
||||
reply.code(400);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ticket.usedAt) {
|
||||
reply.code(400);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (instance.emailRequiredForSignup) {
|
||||
if (await this.usersRepository.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull() })) {
|
||||
if (await this.usersRepository.exist({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) {
|
||||
throw new FastifyReplyError(400, 'DUPLICATED_USERNAME');
|
||||
}
|
||||
|
||||
// Check deleted username duplication
|
||||
if (await this.usedUsernamesRepository.findOneBy({ username: username.toLowerCase() })) {
|
||||
if (await this.usedUsernamesRepository.exist({ where: { username: username.toLowerCase() } })) {
|
||||
throw new FastifyReplyError(400, 'USED_USERNAME');
|
||||
}
|
||||
|
||||
@@ -148,14 +158,14 @@ export class SignupApiService {
|
||||
const salt = await bcrypt.genSalt(8);
|
||||
const hash = await bcrypt.hash(password, salt);
|
||||
|
||||
await this.userPendingsRepository.insert({
|
||||
const pendingUser = await this.userPendingsRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
code,
|
||||
email: emailAddress!,
|
||||
username: username,
|
||||
password: hash,
|
||||
});
|
||||
}).then(x => this.userPendingsRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
const link = `${this.config.url}/signup-complete/${code}`;
|
||||
|
||||
@@ -163,6 +173,13 @@ export class SignupApiService {
|
||||
`To complete signup, please click this link:<br><a href="${link}">${link}</a>`,
|
||||
`To complete signup, please click this link: ${link}`);
|
||||
|
||||
if (ticket) {
|
||||
await this.registrationTicketsRepository.update(ticket.id, {
|
||||
usedAt: new Date(),
|
||||
pendingUserId: pendingUser.id,
|
||||
});
|
||||
}
|
||||
|
||||
reply.code(204);
|
||||
return;
|
||||
} else {
|
||||
@@ -176,6 +193,14 @@ export class SignupApiService {
|
||||
includeSecrets: true,
|
||||
});
|
||||
|
||||
if (ticket) {
|
||||
await this.registrationTicketsRepository.update(ticket.id, {
|
||||
usedAt: new Date(),
|
||||
usedBy: account,
|
||||
usedById: account.id,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...res,
|
||||
token: secret,
|
||||
@@ -212,6 +237,15 @@ export class SignupApiService {
|
||||
emailVerifyCode: null,
|
||||
});
|
||||
|
||||
const ticket = await this.registrationTicketsRepository.findOneBy({ pendingUserId: pendingUser.id });
|
||||
if (ticket) {
|
||||
await this.registrationTicketsRepository.update(ticket.id, {
|
||||
usedBy: account,
|
||||
usedById: account.id,
|
||||
pendingUserId: null,
|
||||
});
|
||||
}
|
||||
|
||||
return this.signinService.signin(request, reply, account as LocalUser);
|
||||
} catch (err) {
|
||||
throw new FastifyReplyError(400, typeof err === 'string' ? err : (err as Error).toString());
|
||||
|
@@ -38,7 +38,8 @@ import * as ep___admin_federation_updateInstance from './endpoints/admin/federat
|
||||
import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js';
|
||||
import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js';
|
||||
import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js';
|
||||
import * as ep___invite from './endpoints/invite.js';
|
||||
import * as ep___admin_invite_create from './endpoints/admin/invite/create.js';
|
||||
import * as ep___admin_invite_list from './endpoints/admin/invite/list.js';
|
||||
import * as ep___admin_promo_create from './endpoints/admin/promo/create.js';
|
||||
import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js';
|
||||
import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js';
|
||||
@@ -230,6 +231,10 @@ import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
|
||||
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
|
||||
import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js';
|
||||
import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js';
|
||||
import * as ep___invite_create from './endpoints/invite/create.js';
|
||||
import * as ep___invite_delete from './endpoints/invite/delete.js';
|
||||
import * as ep___invite_list from './endpoints/invite/list.js';
|
||||
import * as ep___invite_limit from './endpoints/invite/limit.js';
|
||||
import * as ep___meta from './endpoints/meta.js';
|
||||
import * as ep___emojis from './endpoints/emojis.js';
|
||||
import * as ep___emoji from './endpoints/emoji.js';
|
||||
@@ -376,7 +381,8 @@ const eps = [
|
||||
['admin/get-index-stats', ep___admin_getIndexStats],
|
||||
['admin/get-table-stats', ep___admin_getTableStats],
|
||||
['admin/get-user-ips', ep___admin_getUserIps],
|
||||
['invite', ep___invite],
|
||||
['admin/invite/create', ep___admin_invite_create],
|
||||
['admin/invite/list', ep___admin_invite_list],
|
||||
['admin/promo/create', ep___admin_promo_create],
|
||||
['admin/queue/clear', ep___admin_queue_clear],
|
||||
['admin/queue/deliver-delayed', ep___admin_queue_deliverDelayed],
|
||||
@@ -568,6 +574,10 @@ const eps = [
|
||||
['i/webhooks/show', ep___i_webhooks_show],
|
||||
['i/webhooks/update', ep___i_webhooks_update],
|
||||
['i/webhooks/delete', ep___i_webhooks_delete],
|
||||
['invite/create', ep___invite_create],
|
||||
['invite/delete', ep___invite_delete],
|
||||
['invite/list', ep___invite_list],
|
||||
['invite/limit', ep___invite_limit],
|
||||
['meta', ep___meta],
|
||||
['emojis', ep___emojis],
|
||||
['emoji', ep___emoji],
|
||||
|
@@ -0,0 +1,80 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { RegistrationTicketsRepository } from '@/models/index.js';
|
||||
import { InviteCodeEntityService } from '@/core/entities/InviteCodeEntityService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { generateInviteCode } from '@/misc/generate-invite-code.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
|
||||
errors: {
|
||||
invalidDateTime: {
|
||||
message: 'Invalid date-time format',
|
||||
code: 'INVALID_DATE_TIME',
|
||||
id: 'f1380b15-3760-4c6c-a1db-5c3aaf1cbd49',
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
code: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
example: 'GR6S02ERUA5VR',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
count: { type: 'integer', minimum: 1, maximum: 100, default: 1 },
|
||||
expiresAt: { type: 'string', nullable: true },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.registrationTicketsRepository)
|
||||
private registrationTicketsRepository: RegistrationTicketsRepository,
|
||||
|
||||
private inviteCodeEntityService: InviteCodeEntityService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
if (ps.expiresAt && isNaN(Date.parse(ps.expiresAt))) {
|
||||
throw new ApiError(meta.errors.invalidDateTime);
|
||||
}
|
||||
|
||||
const ticketsPromises = [];
|
||||
|
||||
for (let i = 0; i < ps.count; i++) {
|
||||
ticketsPromises.push(this.registrationTicketsRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null,
|
||||
code: generateInviteCode(),
|
||||
}).then(x => this.registrationTicketsRepository.findOneByOrFail(x.identifiers[0])));
|
||||
}
|
||||
|
||||
const tickets = await Promise.all(ticketsPromises);
|
||||
return await this.inviteCodeEntityService.packMany(tickets, me);
|
||||
});
|
||||
}
|
||||
}
|
@@ -0,0 +1,70 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { RegistrationTicketsRepository } from '@/models/index.js';
|
||||
import { InviteCodeEntityService } from '@/core/entities/InviteCodeEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
|
||||
offset: { type: 'integer', default: 0 },
|
||||
type: { type: 'string', enum: ['unused', 'used', 'expired', 'all'], default: 'all' },
|
||||
sort: { type: 'string', enum: ['+createdAt', '-createdAt', '+usedAt', '-usedAt'] },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.registrationTicketsRepository)
|
||||
private registrationTicketsRepository: RegistrationTicketsRepository,
|
||||
|
||||
private inviteCodeEntityService: InviteCodeEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.registrationTicketsRepository.createQueryBuilder('ticket')
|
||||
.leftJoinAndSelect('ticket.createdBy', 'createdBy')
|
||||
.leftJoinAndSelect('ticket.usedBy', 'usedBy');
|
||||
|
||||
switch (ps.type) {
|
||||
case 'unused': query.andWhere('ticket.usedBy IS NULL'); break;
|
||||
case 'used': query.andWhere('ticket.usedBy IS NOT NULL'); break;
|
||||
case 'expired': query.andWhere('ticket.expiresAt < :now', { now: new Date() }); break;
|
||||
}
|
||||
|
||||
switch (ps.sort) {
|
||||
case '+createdAt': query.orderBy('ticket.createdAt', 'DESC'); break;
|
||||
case '-createdAt': query.orderBy('ticket.createdAt', 'ASC'); break;
|
||||
case '+usedAt': query.orderBy('ticket.usedAt', 'DESC', 'NULLS LAST'); break;
|
||||
case '-usedAt': query.orderBy('ticket.usedAt', 'ASC', 'NULLS FIRST'); break;
|
||||
default: query.orderBy('ticket.id', 'DESC'); break;
|
||||
}
|
||||
|
||||
query.limit(ps.limit);
|
||||
query.skip(ps.offset);
|
||||
|
||||
const tickets = await query.getMany();
|
||||
|
||||
return await this.inviteCodeEntityService.packMany(tickets, me);
|
||||
});
|
||||
}
|
||||
}
|
@@ -50,9 +50,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
throw e;
|
||||
});
|
||||
|
||||
const exist = await this.promoNotesRepository.findOneBy({ noteId: note.id });
|
||||
const exist = await this.promoNotesRepository.exist({ where: { noteId: note.id } });
|
||||
|
||||
if (exist != null) {
|
||||
if (exist) {
|
||||
throw new ApiError(meta.errors.alreadyPromoted);
|
||||
}
|
||||
|
||||
|
@@ -69,8 +69,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps) => {
|
||||
const role = await this.rolesRepository.findOneBy({ id: ps.roleId });
|
||||
if (role == null) {
|
||||
const roleExist = await this.rolesRepository.exist({ where: { id: ps.roleId } });
|
||||
if (!roleExist) {
|
||||
throw new ApiError(meta.errors.noSuchRole);
|
||||
}
|
||||
|
||||
|
@@ -58,12 +58,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
const accessToken = secureRndstr(32);
|
||||
|
||||
// Fetch exist access token
|
||||
const exist = await this.accessTokensRepository.findOneBy({
|
||||
appId: session.appId,
|
||||
userId: me.id,
|
||||
const exist = await this.accessTokensRepository.exist({
|
||||
where: {
|
||||
appId: session.appId,
|
||||
userId: me.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist == null) {
|
||||
if (!exist) {
|
||||
const app = await this.appsRepository.findOneByOrFail({ id: session.appId });
|
||||
|
||||
// Generate Hash
|
||||
|
@@ -84,12 +84,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
});
|
||||
|
||||
// Check if already blocking
|
||||
const exist = await this.blockingsRepository.findOneBy({
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
const exist = await this.blockingsRepository.exist({
|
||||
where: {
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist != null) {
|
||||
if (exist) {
|
||||
throw new ApiError(meta.errors.alreadyBlocking);
|
||||
}
|
||||
|
||||
|
@@ -84,12 +84,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
});
|
||||
|
||||
// Check not blocking
|
||||
const exist = await this.blockingsRepository.findOneBy({
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
const exist = await this.blockingsRepository.exist({
|
||||
where: {
|
||||
blockerId: blocker.id,
|
||||
blockeeId: blockee.id,
|
||||
}
|
||||
});
|
||||
|
||||
if (exist == null) {
|
||||
if (!exist) {
|
||||
throw new ApiError(meta.errors.notBlocking);
|
||||
}
|
||||
|
||||
|
@@ -87,12 +87,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
throw e;
|
||||
});
|
||||
|
||||
const exist = await this.clipNotesRepository.findOneBy({
|
||||
noteId: note.id,
|
||||
clipId: clip.id,
|
||||
const exist = await this.clipNotesRepository.exist({
|
||||
where: {
|
||||
noteId: note.id,
|
||||
clipId: clip.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist != null) {
|
||||
if (exist) {
|
||||
throw new ApiError(meta.errors.alreadyClipped);
|
||||
}
|
||||
|
||||
|
@@ -58,12 +58,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
throw new ApiError(meta.errors.noSuchClip);
|
||||
}
|
||||
|
||||
const exist = await this.clipFavoritesRepository.findOneBy({
|
||||
clipId: clip.id,
|
||||
userId: me.id,
|
||||
const exist = await this.clipFavoritesRepository.exist({
|
||||
where: {
|
||||
clipId: clip.id,
|
||||
userId: me.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist != null) {
|
||||
if (exist) {
|
||||
throw new ApiError(meta.errors.alreadyFavorited);
|
||||
}
|
||||
|
||||
|
@@ -34,12 +34,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const file = await this.driveFilesRepository.findOneBy({
|
||||
md5: ps.md5,
|
||||
userId: me.id,
|
||||
const exist = await this.driveFilesRepository.exist({
|
||||
where: {
|
||||
md5: ps.md5,
|
||||
userId: me.id,
|
||||
},
|
||||
});
|
||||
|
||||
return file != null;
|
||||
return exist;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -66,12 +66,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
}
|
||||
|
||||
// if already liked
|
||||
const exist = await this.flashLikesRepository.findOneBy({
|
||||
flashId: flash.id,
|
||||
userId: me.id,
|
||||
const exist = await this.flashLikesRepository.exist({
|
||||
where: {
|
||||
flashId: flash.id,
|
||||
userId: me.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist != null) {
|
||||
if (exist) {
|
||||
throw new ApiError(meta.errors.alreadyLiked);
|
||||
}
|
||||
|
||||
|
@@ -99,12 +99,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
});
|
||||
|
||||
// Check if already following
|
||||
const exist = await this.followingsRepository.findOneBy({
|
||||
followerId: follower.id,
|
||||
followeeId: followee.id,
|
||||
const exist = await this.followingsRepository.exist({
|
||||
where: {
|
||||
followerId: follower.id,
|
||||
followeeId: followee.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist != null) {
|
||||
if (exist) {
|
||||
throw new ApiError(meta.errors.alreadyFollowing);
|
||||
}
|
||||
|
||||
|
@@ -84,12 +84,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
});
|
||||
|
||||
// Check not following
|
||||
const exist = await this.followingsRepository.findOneBy({
|
||||
followerId: follower.id,
|
||||
followeeId: followee.id,
|
||||
const exist = await this.followingsRepository.exist({
|
||||
where: {
|
||||
followerId: follower.id,
|
||||
followeeId: followee.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist == null) {
|
||||
if (!exist) {
|
||||
throw new ApiError(meta.errors.notFollowing);
|
||||
}
|
||||
|
||||
|
@@ -66,12 +66,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
}
|
||||
|
||||
// if already liked
|
||||
const exist = await this.galleryLikesRepository.findOneBy({
|
||||
postId: post.id,
|
||||
userId: me.id,
|
||||
const exist = await this.galleryLikesRepository.exist({
|
||||
where: {
|
||||
postId: post.id,
|
||||
userId: me.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist != null) {
|
||||
if (exist) {
|
||||
throw new ApiError(meta.errors.alreadyLiked);
|
||||
}
|
||||
|
||||
|
@@ -66,8 +66,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
private downloadService: DownloadService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const users = await this.usersRepository.findOneBy({ id: me.id });
|
||||
if (users === null) throw new ApiError(meta.errors.noSuchUser);
|
||||
const userExist = await this.usersRepository.exist({ where: { id: me.id } });
|
||||
if (!userExist) throw new ApiError(meta.errors.noSuchUser);
|
||||
const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
|
||||
if (file === null) throw new ApiError(meta.errors.noSuchFile);
|
||||
if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
|
||||
|
@@ -47,19 +47,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
// Check if announcement exists
|
||||
const announcement = await this.announcementsRepository.findOneBy({ id: ps.announcementId });
|
||||
const announcementExist = await this.announcementsRepository.exist({ where: { id: ps.announcementId } });
|
||||
|
||||
if (announcement == null) {
|
||||
if (!announcementExist) {
|
||||
throw new ApiError(meta.errors.noSuchAnnouncement);
|
||||
}
|
||||
|
||||
// Check if already read
|
||||
const read = await this.announcementReadsRepository.findOneBy({
|
||||
announcementId: ps.announcementId,
|
||||
userId: me.id,
|
||||
const alreadyRead = await this.announcementReadsRepository.exist({
|
||||
where: {
|
||||
announcementId: ps.announcementId,
|
||||
userId: me.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (read != null) {
|
||||
if (alreadyRead) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@@ -28,9 +28,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
private globalEventService: GlobalEventService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const token = await this.accessTokensRepository.findOneBy({ id: ps.tokenId });
|
||||
const tokenExist = await this.accessTokensRepository.exist({ where: { id: ps.tokenId } });
|
||||
|
||||
if (token) {
|
||||
if (tokenExist) {
|
||||
await this.accessTokensRepository.delete({
|
||||
id: ps.tokenId,
|
||||
userId: me.id,
|
||||
|
82
packages/backend/src/server/api/endpoints/invite/create.ts
Normal file
82
packages/backend/src/server/api/endpoints/invite/create.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { MoreThan } from 'typeorm';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { RegistrationTicketsRepository } from '@/models/index.js';
|
||||
import { InviteCodeEntityService } from '@/core/entities/InviteCodeEntityService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { generateInviteCode } from '@/misc/generate-invite-code.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['meta'],
|
||||
|
||||
requireCredential: true,
|
||||
requireRolePolicy: 'canInvite',
|
||||
|
||||
errors: {
|
||||
exceededCreateLimit: {
|
||||
message: 'You have exceeded the limit for creating an invitation code.',
|
||||
code: 'EXCEEDED_LIMIT_OF_CREATE_INVITE_CODE',
|
||||
id: '8b165dd3-6f37-4557-8db1-73175d63c641',
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
code: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
example: 'GR6S02ERUA5VR',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.registrationTicketsRepository)
|
||||
private registrationTicketsRepository: RegistrationTicketsRepository,
|
||||
|
||||
private inviteCodeEntityService: InviteCodeEntityService,
|
||||
private idService: IdService,
|
||||
private roleService: RoleService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const policies = await this.roleService.getUserPolicies(me.id);
|
||||
|
||||
if (policies.inviteLimit) {
|
||||
const count = await this.registrationTicketsRepository.countBy({
|
||||
createdAt: MoreThan(new Date(Date.now() - (policies.inviteLimitCycle * 1000 * 60))),
|
||||
createdById: me.id,
|
||||
});
|
||||
|
||||
if (count >= policies.inviteLimit) {
|
||||
throw new ApiError(meta.errors.exceededCreateLimit);
|
||||
}
|
||||
}
|
||||
|
||||
const ticket = await this.registrationTicketsRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
71
packages/backend/src/server/api/endpoints/invite/delete.ts
Normal file
71
packages/backend/src/server/api/endpoints/invite/delete.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { RegistrationTicketsRepository } from '@/models/index.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['meta'],
|
||||
|
||||
requireCredential: true,
|
||||
requireRolePolicy: 'canInvite',
|
||||
|
||||
errors: {
|
||||
noSuchCode: {
|
||||
message: 'No such invite code.',
|
||||
code: 'NO_SUCH_INVITE_CODE',
|
||||
id: 'cd4f9ae4-7854-4e3e-8df9-c296f051e634',
|
||||
},
|
||||
|
||||
cantDelete: {
|
||||
message: 'You can\'t delete this invite code.',
|
||||
code: 'CAN_NOT_DELETE_INVITE_CODE',
|
||||
id: 'ff17af39-000c-4d4e-abdf-848fa30fc1ce',
|
||||
},
|
||||
|
||||
accessDenied: {
|
||||
message: 'Access denied.',
|
||||
code: 'ACCESS_DENIED',
|
||||
id: '5eb8d909-2540-4970-90b8-dd6f86088121',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
inviteId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['inviteId'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.registrationTicketsRepository)
|
||||
private registrationTicketsRepository: RegistrationTicketsRepository,
|
||||
|
||||
private roleService: RoleService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const ticket = await this.registrationTicketsRepository.findOneBy({ id: ps.inviteId });
|
||||
const isModerator = await this.roleService.isModerator(me);
|
||||
|
||||
if (ticket == null) {
|
||||
throw new ApiError(meta.errors.noSuchCode);
|
||||
}
|
||||
|
||||
if (ticket.createdById !== me.id && !isModerator) {
|
||||
throw new ApiError(meta.errors.accessDenied);
|
||||
}
|
||||
|
||||
if (ticket.usedAt && !isModerator) {
|
||||
throw new ApiError(meta.errors.cantDelete);
|
||||
}
|
||||
|
||||
await this.registrationTicketsRepository.delete(ticket.id);
|
||||
});
|
||||
}
|
||||
}
|
@@ -1,9 +1,9 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { MoreThan } from 'typeorm';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { RegistrationTicketsRepository } from '@/models/index.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['meta'],
|
||||
@@ -15,12 +15,9 @@ export const meta = {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
code: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
example: '2ERUA5VR',
|
||||
maxLength: 8,
|
||||
minLength: 8,
|
||||
remaining: {
|
||||
type: 'integer',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -39,21 +36,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
@Inject(DI.registrationTicketsRepository)
|
||||
private registrationTicketsRepository: RegistrationTicketsRepository,
|
||||
|
||||
private idService: IdService,
|
||||
private roleService: RoleService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const code = secureRndstr(8, {
|
||||
chars: '23456789ABCDEFGHJKLMNPQRSTUVWXYZ', // [0-9A-Z] w/o [01IO] (32 patterns)
|
||||
});
|
||||
const policies = await this.roleService.getUserPolicies(me.id);
|
||||
|
||||
await this.registrationTicketsRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
code,
|
||||
});
|
||||
const count = policies.inviteLimit ? await this.registrationTicketsRepository.countBy({
|
||||
createdAt: MoreThan(new Date(Date.now() - (policies.inviteExpirationTime * 60 * 1000))),
|
||||
createdById: me.id,
|
||||
}) : null;
|
||||
|
||||
return {
|
||||
code,
|
||||
remaining: count !== null ? Math.max(0, policies.inviteLimit - count) : null,
|
||||
};
|
||||
});
|
||||
}
|
58
packages/backend/src/server/api/endpoints/invite/list.ts
Normal file
58
packages/backend/src/server/api/endpoints/invite/list.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { RegistrationTicketsRepository } from '@/models/index.js';
|
||||
import { InviteCodeEntityService } from '@/core/entities/InviteCodeEntityService.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['meta'],
|
||||
|
||||
requireCredential: true,
|
||||
requireRolePolicy: 'canInvite',
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.registrationTicketsRepository)
|
||||
private registrationTicketsRepository: RegistrationTicketsRepository,
|
||||
|
||||
private inviteCodeEntityService: InviteCodeEntityService,
|
||||
private queryService: QueryService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.queryService.makePaginationQuery(this.registrationTicketsRepository.createQueryBuilder('ticket'), ps.sinceId, ps.untilId)
|
||||
.andWhere('ticket.createdById = :meId', { meId: me.id })
|
||||
.leftJoinAndSelect('ticket.createdBy', 'createdBy')
|
||||
.leftJoinAndSelect('ticket.usedBy', 'usedBy');
|
||||
|
||||
const tickets = await query
|
||||
.limit(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return await this.inviteCodeEntityService.packMany(tickets, me);
|
||||
});
|
||||
}
|
||||
}
|
@@ -79,12 +79,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
});
|
||||
|
||||
// Check if already muting
|
||||
const exist = await this.mutingsRepository.findOneBy({
|
||||
muterId: muter.id,
|
||||
muteeId: mutee.id,
|
||||
const exist = await this.mutingsRepository.exist({
|
||||
where: {
|
||||
muterId: muter.id,
|
||||
muteeId: mutee.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist != null) {
|
||||
if (exist) {
|
||||
throw new ApiError(meta.errors.alreadyMuting);
|
||||
}
|
||||
|
||||
|
@@ -217,11 +217,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
|
||||
// Check blocking
|
||||
if (renote.userId !== me.id) {
|
||||
const block = await this.blockingsRepository.findOneBy({
|
||||
blockerId: renote.userId,
|
||||
blockeeId: me.id,
|
||||
const blockExist = await this.blockingsRepository.exist({
|
||||
where: {
|
||||
blockerId: renote.userId,
|
||||
blockeeId: me.id,
|
||||
},
|
||||
});
|
||||
if (block) {
|
||||
if (blockExist) {
|
||||
throw new ApiError(meta.errors.youHaveBeenBlocked);
|
||||
}
|
||||
}
|
||||
@@ -240,11 +242,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
|
||||
// Check blocking
|
||||
if (reply.userId !== me.id) {
|
||||
const block = await this.blockingsRepository.findOneBy({
|
||||
blockerId: reply.userId,
|
||||
blockeeId: me.id,
|
||||
const blockExist = await this.blockingsRepository.exist({
|
||||
where: {
|
||||
blockerId: reply.userId,
|
||||
blockeeId: me.id,
|
||||
},
|
||||
});
|
||||
if (block) {
|
||||
if (blockExist) {
|
||||
throw new ApiError(meta.errors.youHaveBeenBlocked);
|
||||
}
|
||||
}
|
||||
|
@@ -63,12 +63,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
});
|
||||
|
||||
// if already favorited
|
||||
const exist = await this.noteFavoritesRepository.findOneBy({
|
||||
noteId: note.id,
|
||||
userId: me.id,
|
||||
const exist = await this.noteFavoritesRepository.exist({
|
||||
where: {
|
||||
noteId: note.id,
|
||||
userId: me.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist != null) {
|
||||
if (exist) {
|
||||
throw new ApiError(meta.errors.alreadyFavorited);
|
||||
}
|
||||
|
||||
|
@@ -66,12 +66,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
}
|
||||
|
||||
// if already liked
|
||||
const exist = await this.pageLikesRepository.findOneBy({
|
||||
pageId: page.id,
|
||||
userId: me.id,
|
||||
const exist = await this.pageLikesRepository.exist({
|
||||
where: {
|
||||
pageId: page.id,
|
||||
userId: me.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist != null) {
|
||||
if (exist) {
|
||||
throw new ApiError(meta.errors.alreadyLiked);
|
||||
}
|
||||
|
||||
|
@@ -44,12 +44,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
throw err;
|
||||
});
|
||||
|
||||
const exist = await this.promoReadsRepository.findOneBy({
|
||||
noteId: note.id,
|
||||
userId: me.id,
|
||||
const exist = await this.promoReadsRepository.exist({
|
||||
where: {
|
||||
noteId: note.id,
|
||||
userId: me.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist != null) {
|
||||
if (exist) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@@ -97,11 +97,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
if (me == null) {
|
||||
throw new ApiError(meta.errors.forbidden);
|
||||
} else if (me.id !== user.id) {
|
||||
const following = await this.followingsRepository.findOneBy({
|
||||
followeeId: user.id,
|
||||
followerId: me.id,
|
||||
const isFollowing = await this.followingsRepository.exist({
|
||||
where: {
|
||||
followeeId: user.id,
|
||||
followerId: me.id,
|
||||
},
|
||||
});
|
||||
if (following == null) {
|
||||
if (!isFollowing) {
|
||||
throw new ApiError(meta.errors.forbidden);
|
||||
}
|
||||
}
|
||||
|
@@ -97,11 +97,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
if (me == null) {
|
||||
throw new ApiError(meta.errors.forbidden);
|
||||
} else if (me.id !== user.id) {
|
||||
const following = await this.followingsRepository.findOneBy({
|
||||
followeeId: user.id,
|
||||
followerId: me.id,
|
||||
const isFollowing = await this.followingsRepository.exist({
|
||||
where: {
|
||||
followeeId: user.id,
|
||||
followerId: me.id,
|
||||
},
|
||||
});
|
||||
if (following == null) {
|
||||
if (!isFollowing) {
|
||||
throw new ApiError(meta.errors.forbidden);
|
||||
}
|
||||
}
|
||||
|
@@ -84,11 +84,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
private roleService: RoleService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const list = await this.userListsRepository.findOneBy({
|
||||
id: ps.listId,
|
||||
isPublic: true,
|
||||
const listExist = await this.userListsRepository.exist({
|
||||
where: {
|
||||
id: ps.listId,
|
||||
isPublic: true,
|
||||
},
|
||||
});
|
||||
if (list === null) throw new ApiError(meta.errors.noSuchList);
|
||||
if (!listExist) throw new ApiError(meta.errors.noSuchList);
|
||||
const currentCount = await this.userListsRepository.countBy({
|
||||
userId: me.id,
|
||||
});
|
||||
@@ -114,18 +116,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
});
|
||||
|
||||
if (currentUser.id !== me.id) {
|
||||
const block = await this.blockingsRepository.findOneBy({
|
||||
blockerId: currentUser.id,
|
||||
blockeeId: me.id,
|
||||
const blockExist = await this.blockingsRepository.exist({
|
||||
where: {
|
||||
blockerId: currentUser.id,
|
||||
blockeeId: me.id,
|
||||
},
|
||||
});
|
||||
if (block) {
|
||||
if (blockExist) {
|
||||
throw new ApiError(meta.errors.youHaveBeenBlocked);
|
||||
}
|
||||
}
|
||||
|
||||
const exist = await this.userListJoiningsRepository.findOneBy({
|
||||
userListId: userList.id,
|
||||
userId: currentUser.id,
|
||||
const exist = await this.userListJoiningsRepository.exist({
|
||||
where: {
|
||||
userListId: userList.id,
|
||||
userId: currentUser.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist) {
|
||||
|
@@ -41,21 +41,25 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
private idService: IdService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const userList = await this.userListsRepository.findOneBy({
|
||||
id: ps.listId,
|
||||
isPublic: true,
|
||||
const userListExist = await this.userListsRepository.exist({
|
||||
where: {
|
||||
id: ps.listId,
|
||||
isPublic: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (userList === null) {
|
||||
if (!userListExist) {
|
||||
throw new ApiError(meta.errors.noSuchList);
|
||||
}
|
||||
|
||||
const exist = await this.userListFavoritesRepository.findOneBy({
|
||||
userId: me.id,
|
||||
userListId: ps.listId,
|
||||
const exist = await this.userListFavoritesRepository.exist({
|
||||
where: {
|
||||
userId: me.id,
|
||||
userListId: ps.listId,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist !== null) {
|
||||
if (exist) {
|
||||
throw new ApiError(meta.errors.alreadyFavorited);
|
||||
}
|
||||
|
||||
|
@@ -100,18 +100,22 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
|
||||
// Check blocking
|
||||
if (user.id !== me.id) {
|
||||
const block = await this.blockingsRepository.findOneBy({
|
||||
blockerId: user.id,
|
||||
blockeeId: me.id,
|
||||
const blockExist = await this.blockingsRepository.exist({
|
||||
where: {
|
||||
blockerId: user.id,
|
||||
blockeeId: me.id,
|
||||
},
|
||||
});
|
||||
if (block) {
|
||||
if (blockExist) {
|
||||
throw new ApiError(meta.errors.youHaveBeenBlocked);
|
||||
}
|
||||
}
|
||||
|
||||
const exist = await this.userListJoiningsRepository.findOneBy({
|
||||
userListId: userList.id,
|
||||
userId: user.id,
|
||||
const exist = await this.userListJoiningsRepository.exist({
|
||||
where: {
|
||||
userListId: userList.id,
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (exist) {
|
||||
|
@@ -69,10 +69,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
userListId: ps.listId,
|
||||
});
|
||||
if (me !== null) {
|
||||
additionalProperties.isLiked = (await this.userListFavoritesRepository.findOneBy({
|
||||
userId: me.id,
|
||||
userListId: ps.listId,
|
||||
}) !== null);
|
||||
additionalProperties.isLiked = await this.userListFavoritesRepository.exist({
|
||||
where: {
|
||||
userId: me.id,
|
||||
userListId: ps.listId,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
additionalProperties.isLiked = false;
|
||||
}
|
||||
|
@@ -39,12 +39,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
private userListFavoritesRepository: UserListFavoritesRepository,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const userList = await this.userListsRepository.findOneBy({
|
||||
id: ps.listId,
|
||||
isPublic: true,
|
||||
const userListExist = await this.userListsRepository.exist({
|
||||
where: {
|
||||
id: ps.listId,
|
||||
isPublic: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (userList === null) {
|
||||
if (!userListExist) {
|
||||
throw new ApiError(meta.errors.noSuchList);
|
||||
}
|
||||
|
||||
|
@@ -34,11 +34,13 @@ class UserListChannel extends Channel {
|
||||
this.listId = params.listId as string;
|
||||
|
||||
// Check existence and owner
|
||||
const list = await this.userListsRepository.findOneBy({
|
||||
id: this.listId,
|
||||
userId: this.user!.id,
|
||||
const listExist = await this.userListsRepository.exist({
|
||||
where: {
|
||||
id: this.listId,
|
||||
userId: this.user!.id,
|
||||
},
|
||||
});
|
||||
if (!list) return;
|
||||
if (!listExist) return;
|
||||
|
||||
// Subscribe stream
|
||||
this.subscriber.on(`userListStream:${this.listId}`, this.send);
|
||||
|
@@ -18,7 +18,8 @@ type MockResponse = {
|
||||
};
|
||||
|
||||
export class MockResolver extends Resolver {
|
||||
private _rs = new Map<string, MockResponse>();
|
||||
#responseMap = new Map<string, MockResponse>();
|
||||
#remoteGetTrials: string[] = [];
|
||||
|
||||
constructor(loggerService: LoggerService) {
|
||||
super(
|
||||
@@ -38,18 +39,28 @@ export class MockResolver extends Resolver {
|
||||
);
|
||||
}
|
||||
|
||||
public async _register(uri: string, content: string | Record<string, any>, type = 'application/activity+json') {
|
||||
this._rs.set(uri, {
|
||||
public register(uri: string, content: string | Record<string, any>, type = 'application/activity+json'): void {
|
||||
this.#responseMap.set(uri, {
|
||||
type,
|
||||
content: typeof content === 'string' ? content : JSON.stringify(content),
|
||||
});
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this.#responseMap.clear();
|
||||
this.#remoteGetTrials.length = 0;
|
||||
}
|
||||
|
||||
public remoteGetTrials(): string[] {
|
||||
return this.#remoteGetTrials;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async resolve(value: string | IObject): Promise<IObject> {
|
||||
if (typeof value !== 'string') return value;
|
||||
|
||||
const r = this._rs.get(value);
|
||||
this.#remoteGetTrials.push(value);
|
||||
const r = this.#responseMap.get(value);
|
||||
|
||||
if (!r) {
|
||||
throw new Error('Not registed for mock');
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user