Merge branch 'develop' into fetch-outbox
This commit is contained in:
commit
6b26ce3768
|
@ -104,6 +104,7 @@ redis:
|
||||||
# apiKey: ''
|
# apiKey: ''
|
||||||
# ssl: true
|
# ssl: true
|
||||||
# index: ''
|
# index: ''
|
||||||
|
# scope: local
|
||||||
|
|
||||||
# ┌───────────────┐
|
# ┌───────────────┐
|
||||||
#───┘ ID generation └───────────────────────────────────────────
|
#───┘ ID generation └───────────────────────────────────────────
|
||||||
|
|
12
CHANGELOG.md
12
CHANGELOG.md
|
@ -17,6 +17,10 @@
|
||||||
### General
|
### General
|
||||||
- identicon生成を無効にしてパフォーマンスを向上させることができるようになりました
|
- identicon生成を無効にしてパフォーマンスを向上させることができるようになりました
|
||||||
- サーバーのマシン情報の公開を無効にしてパフォーマンスを向上させることができるようになりました
|
- サーバーのマシン情報の公開を無効にしてパフォーマンスを向上させることができるようになりました
|
||||||
|
- 招待機能を改善しました
|
||||||
|
* 過去に発行した招待コードを確認できるようになりました
|
||||||
|
* ロールごとに招待コードの発行数制限と制限対象期間、有効期限を設定できるようになりました
|
||||||
|
* 招待コードを作成したユーザーと使用したユーザーを確認できるようになりました
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
- deck UIのカラムのメニューからアンテナとリストの編集画面を開けるように
|
- deck UIのカラムのメニューからアンテナとリストの編集画面を開けるように
|
||||||
|
@ -41,13 +45,19 @@
|
||||||
- Fix: フォルダーのページネーションが機能しない #11180
|
- Fix: フォルダーのページネーションが機能しない #11180
|
||||||
- Fix: 長い文章を投稿する際、プレビューが画面からはみ出る問題を修正
|
- Fix: 長い文章を投稿する際、プレビューが画面からはみ出る問題を修正
|
||||||
- Fix: システムフォント設定が正しく反映されない問題を修正
|
- Fix: システムフォント設定が正しく反映されない問題を修正
|
||||||
|
- Fix: アンケート終了時のプッシュ通知が正しく表示されない問題を修正
|
||||||
|
- Fix: MasterVolumeが0の時だけでなく各通知音の音量設定が0のときも、HTMLAudioElement.playが実行されないように変更
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
- JSON.parse の回数を削減することで、ストリーミングのパフォーマンスを向上しました
|
- JSON.parse の回数を削減することで、ストリーミングのパフォーマンスを向上しました
|
||||||
- nsfwjs のモデルロードを排他することで、重複ロードによってメモリ使用量が増加しないように
|
- nsfwjs のモデルロードを排他することで、重複ロードによってメモリ使用量が増加しないように
|
||||||
- 連合の配送ジョブのパフォーマンスを向上(ロック機構の見直し、Redisキャッシュの活用)
|
- 連合の配送ジョブのパフォーマンスを向上(ロック機構の見直し、Redisキャッシュの活用)
|
||||||
- 全体的なDBクエリのパフォーマンスを向上
|
|
||||||
- featuredノートのsignedGet回数を減らしました
|
- featuredノートのsignedGet回数を減らしました
|
||||||
|
- リモートサーバーからのNSFW映像のキャッシュだけを無効化できるオプションを追加
|
||||||
|
- MeilisearchにIndexするノートの範囲を設定できるように
|
||||||
|
- Fix: Remove Meilisearch index when notes are deleted
|
||||||
|
- Fix: 非英語環境でのPostgreSQLのエラーハンドリングを修正
|
||||||
|
- Fix: インスタンスのアイコンがbase64の場合の挙動を修正
|
||||||
|
|
||||||
## 13.13.2
|
## 13.13.2
|
||||||
|
|
||||||
|
|
|
@ -51,11 +51,7 @@ export default function generateDTS() {
|
||||||
ts.NodeFlags.Const | ts.NodeFlags.Ambient | ts.NodeFlags.ContextFlags,
|
ts.NodeFlags.Const | ts.NodeFlags.Ambient | ts.NodeFlags.ContextFlags,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ts.factory.createExportAssignment(
|
ts.factory.createExportDefault(ts.factory.createIdentifier('locales')),
|
||||||
undefined,
|
|
||||||
true,
|
|
||||||
ts.factory.createIdentifier('locales'),
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
const printed = ts.createPrinter({
|
const printed = ts.createPrinter({
|
||||||
newLine: ts.NewLineKind.LineFeed,
|
newLine: ts.NewLineKind.LineFeed,
|
||||||
|
|
|
@ -159,6 +159,8 @@ export interface Locale {
|
||||||
"settingGuide": string;
|
"settingGuide": string;
|
||||||
"cacheRemoteFiles": string;
|
"cacheRemoteFiles": string;
|
||||||
"cacheRemoteFilesDescription": string;
|
"cacheRemoteFilesDescription": string;
|
||||||
|
"cacheRemoteSensitiveFiles": string;
|
||||||
|
"cacheRemoteSensitiveFilesDescription": string;
|
||||||
"flagAsBot": string;
|
"flagAsBot": string;
|
||||||
"flagAsBotDescription": string;
|
"flagAsBotDescription": string;
|
||||||
"flagAsCat": string;
|
"flagAsCat": string;
|
||||||
|
@ -1075,6 +1077,23 @@ export interface Locale {
|
||||||
"enableServerMachineStats": string;
|
"enableServerMachineStats": string;
|
||||||
"enableIdenticonGeneration": string;
|
"enableIdenticonGeneration": string;
|
||||||
"turnOffToImprovePerformance": 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": {
|
"_initialAccountSetting": {
|
||||||
"accountCreated": string;
|
"accountCreated": string;
|
||||||
"letsStartAccountSetup": string;
|
"letsStartAccountSetup": string;
|
||||||
|
@ -1465,6 +1484,9 @@ export interface Locale {
|
||||||
"ltlAvailable": string;
|
"ltlAvailable": string;
|
||||||
"canPublicNote": string;
|
"canPublicNote": string;
|
||||||
"canInvite": string;
|
"canInvite": string;
|
||||||
|
"inviteLimit": string;
|
||||||
|
"inviteLimitCycle": string;
|
||||||
|
"inviteExpirationTime": string;
|
||||||
"canManageCustomEmojis": string;
|
"canManageCustomEmojis": string;
|
||||||
"driveCapacity": string;
|
"driveCapacity": string;
|
||||||
"alwaysMarkNsfw": string;
|
"alwaysMarkNsfw": string;
|
||||||
|
|
|
@ -156,6 +156,8 @@ addEmoji: "絵文字を追加"
|
||||||
settingGuide: "おすすめ設定"
|
settingGuide: "おすすめ設定"
|
||||||
cacheRemoteFiles: "リモートのファイルをキャッシュする"
|
cacheRemoteFiles: "リモートのファイルをキャッシュする"
|
||||||
cacheRemoteFilesDescription: "この設定を無効にすると、リモートファイルをキャッシュせず直リンクするようになります。サーバーのストレージを節約できますが、サムネイルが生成されないので通信量が増加します。"
|
cacheRemoteFilesDescription: "この設定を無効にすると、リモートファイルをキャッシュせず直リンクするようになります。サーバーのストレージを節約できますが、サムネイルが生成されないので通信量が増加します。"
|
||||||
|
cacheRemoteSensitiveFiles: "リモートのセンシティブなファイルをキャッシュする"
|
||||||
|
cacheRemoteSensitiveFilesDescription: "この設定を無効にすると、リモートのセンシティブなファイルはキャッシュせず直リンクするようになります。"
|
||||||
flagAsBot: "Botとして設定"
|
flagAsBot: "Botとして設定"
|
||||||
flagAsBotDescription: "このアカウントがプログラムによって運用される場合は、このフラグをオンにします。オンにすると、反応の連鎖を防ぐためのフラグとして他の開発者に役立ったり、Misskeyのシステム上での扱いがBotに合ったものになります。"
|
flagAsBotDescription: "このアカウントがプログラムによって運用される場合は、このフラグをオンにします。オンにすると、反応の連鎖を防ぐためのフラグとして他の開発者に役立ったり、Misskeyのシステム上での扱いがBotに合ったものになります。"
|
||||||
flagAsCat: "にゃああああああああああああああ!!!!!!!!!!!!"
|
flagAsCat: "にゃああああああああああああああ!!!!!!!!!!!!"
|
||||||
|
@ -1072,6 +1074,23 @@ branding: "ブランディング"
|
||||||
enableServerMachineStats: "サーバーのマシン情報を公開する"
|
enableServerMachineStats: "サーバーのマシン情報を公開する"
|
||||||
enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする"
|
enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする"
|
||||||
turnOffToImprovePerformance: "オフにするとパフォーマンスが向上します。"
|
turnOffToImprovePerformance: "オフにするとパフォーマンスが向上します。"
|
||||||
|
createInviteCode: "招待コードを作成"
|
||||||
|
createWithOptions: "オプションを指定して作成"
|
||||||
|
createCount: "作成数"
|
||||||
|
inviteCodeCreated: "招待コードを作成しました"
|
||||||
|
inviteLimitExceeded: "作成できる招待コードの数が上限に達しています。"
|
||||||
|
createLimitRemaining: "作成できる招待コード: 残り {limit} 個"
|
||||||
|
inviteLimitResetCycle: "{time}で最大 {limit} 個の招待コードを作成できます。"
|
||||||
|
expirationDate: "有効期限"
|
||||||
|
noExpirationDate: "有効期限を設けない"
|
||||||
|
inviteCodeUsedAt: "招待コードが使用された日時"
|
||||||
|
registeredUserUsingInviteCode: "招待コードを使用したユーザー"
|
||||||
|
waitingForMailAuth: "メール認証待ち"
|
||||||
|
inviteCodeCreator: "招待コードを作成したユーザー"
|
||||||
|
usedAt: "使用日時"
|
||||||
|
unused: "未使用"
|
||||||
|
used: "使用済み"
|
||||||
|
expired: "期限切れ"
|
||||||
|
|
||||||
_initialAccountSetting:
|
_initialAccountSetting:
|
||||||
accountCreated: "アカウントの作成が完了しました!"
|
accountCreated: "アカウントの作成が完了しました!"
|
||||||
|
@ -1387,6 +1406,9 @@ _role:
|
||||||
ltlAvailable: "ローカルタイムラインの閲覧"
|
ltlAvailable: "ローカルタイムラインの閲覧"
|
||||||
canPublicNote: "パブリック投稿の許可"
|
canPublicNote: "パブリック投稿の許可"
|
||||||
canInvite: "サーバー招待コードの発行"
|
canInvite: "サーバー招待コードの発行"
|
||||||
|
inviteLimit: "招待コードの作成可能数"
|
||||||
|
inviteLimitCycle: "招待コードの発行間隔"
|
||||||
|
inviteExpirationTime: "招待コードの有効期限"
|
||||||
canManageCustomEmojis: "カスタム絵文字の管理"
|
canManageCustomEmojis: "カスタム絵文字の管理"
|
||||||
driveCapacity: "ドライブ容量"
|
driveCapacity: "ドライブ容量"
|
||||||
alwaysMarkNsfw: "ファイルにNSFWを常に付与"
|
alwaysMarkNsfw: "ファイルにNSFWを常に付与"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "misskey",
|
"name": "misskey",
|
||||||
"version": "13.14.0-beta.2",
|
"version": "13.14.0-beta.3",
|
||||||
"codename": "nasubi",
|
"codename": "nasubi",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -59,8 +59,8 @@
|
||||||
"@typescript-eslint/eslint-plugin": "5.61.0",
|
"@typescript-eslint/eslint-plugin": "5.61.0",
|
||||||
"@typescript-eslint/parser": "5.61.0",
|
"@typescript-eslint/parser": "5.61.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "12.17.0",
|
"cypress": "12.17.1",
|
||||||
"eslint": "8.44.0",
|
"eslint": "8.45.0",
|
||||||
"start-server-and-test": "2.0.0"
|
"start-server-and-test": "2.0.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
|
|
|
@ -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"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
export class NsfwCache1689102832143 {
|
||||||
|
name = 'NsfwCache1689102832143'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" ADD "cacheRemoteSensitiveFiles" boolean NOT NULL DEFAULT true`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "cacheRemoteSensitiveFiles"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -74,7 +74,7 @@
|
||||||
"@peertube/http-signature": "1.7.0",
|
"@peertube/http-signature": "1.7.0",
|
||||||
"@sinonjs/fake-timers": "10.3.0",
|
"@sinonjs/fake-timers": "10.3.0",
|
||||||
"@swc/cli": "0.1.62",
|
"@swc/cli": "0.1.62",
|
||||||
"@swc/core": "1.3.68",
|
"@swc/core": "1.3.69",
|
||||||
"accepts": "1.3.8",
|
"accepts": "1.3.8",
|
||||||
"ajv": "8.12.0",
|
"ajv": "8.12.0",
|
||||||
"archiver": "5.3.1",
|
"archiver": "5.3.1",
|
||||||
|
@ -82,11 +82,11 @@
|
||||||
"autwh": "0.1.0",
|
"autwh": "0.1.0",
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
"blurhash": "2.0.5",
|
"blurhash": "2.0.5",
|
||||||
"bullmq": "4.2.0",
|
"bullmq": "4.3.0",
|
||||||
"cacheable-lookup": "7.0.0",
|
"cacheable-lookup": "7.0.0",
|
||||||
"cbor": "9.0.0",
|
"cbor": "9.0.0",
|
||||||
"chalk": "5.2.0",
|
"chalk": "5.3.0",
|
||||||
"chalk-template": "0.4.0",
|
"chalk-template": "1.1.0",
|
||||||
"chokidar": "3.5.3",
|
"chokidar": "3.5.3",
|
||||||
"cli-highlight": "2.1.11",
|
"cli-highlight": "2.1.11",
|
||||||
"color-convert": "2.0.1",
|
"color-convert": "2.0.1",
|
||||||
|
@ -100,7 +100,7 @@
|
||||||
"fluent-ffmpeg": "2.1.2",
|
"fluent-ffmpeg": "2.1.2",
|
||||||
"form-data": "4.0.0",
|
"form-data": "4.0.0",
|
||||||
"got": "13.0.0",
|
"got": "13.0.0",
|
||||||
"happy-dom": "10.0.3",
|
"happy-dom": "10.3.2",
|
||||||
"hpagent": "1.2.0",
|
"hpagent": "1.2.0",
|
||||||
"ioredis": "5.3.2",
|
"ioredis": "5.3.2",
|
||||||
"ip-cidr": "3.1.0",
|
"ip-cidr": "3.1.0",
|
||||||
|
@ -141,14 +141,14 @@
|
||||||
"rxjs": "7.8.1",
|
"rxjs": "7.8.1",
|
||||||
"s-age": "1.1.2",
|
"s-age": "1.1.2",
|
||||||
"sanitize-html": "2.11.0",
|
"sanitize-html": "2.11.0",
|
||||||
"semver": "7.5.3",
|
"semver": "7.5.4",
|
||||||
"sharp": "0.32.1",
|
"sharp": "0.32.3",
|
||||||
"sharp-read-bmp": "github:misskey-dev/sharp-read-bmp",
|
"sharp-read-bmp": "github:misskey-dev/sharp-read-bmp",
|
||||||
"slacc": "0.0.9",
|
"slacc": "0.0.9",
|
||||||
"strict-event-emitter-types": "2.0.0",
|
"strict-event-emitter-types": "2.0.0",
|
||||||
"stringz": "2.1.0",
|
"stringz": "2.1.0",
|
||||||
"summaly": "github:misskey-dev/summaly",
|
"summaly": "github:misskey-dev/summaly",
|
||||||
"systeminformation": "5.18.6",
|
"systeminformation": "5.18.7",
|
||||||
"tinycolor2": "1.6.0",
|
"tinycolor2": "1.6.0",
|
||||||
"tmp": "0.2.1",
|
"tmp": "0.2.1",
|
||||||
"tsc-alias": "1.8.7",
|
"tsc-alias": "1.8.7",
|
||||||
|
@ -158,7 +158,6 @@
|
||||||
"typescript": "5.1.6",
|
"typescript": "5.1.6",
|
||||||
"ulid": "2.3.0",
|
"ulid": "2.3.0",
|
||||||
"unzipper": "0.10.14",
|
"unzipper": "0.10.14",
|
||||||
"uuid": "9.0.0",
|
|
||||||
"vary": "1.1.2",
|
"vary": "1.1.2",
|
||||||
"web-push": "3.6.3",
|
"web-push": "3.6.3",
|
||||||
"ws": "8.13.0",
|
"ws": "8.13.0",
|
||||||
|
@ -175,14 +174,14 @@
|
||||||
"@types/content-disposition": "0.5.5",
|
"@types/content-disposition": "0.5.5",
|
||||||
"@types/escape-regexp": "0.0.1",
|
"@types/escape-regexp": "0.0.1",
|
||||||
"@types/fluent-ffmpeg": "2.1.21",
|
"@types/fluent-ffmpeg": "2.1.21",
|
||||||
"@types/jest": "29.5.2",
|
"@types/jest": "29.5.3",
|
||||||
"@types/js-yaml": "4.0.5",
|
"@types/js-yaml": "4.0.5",
|
||||||
"@types/jsdom": "21.1.1",
|
"@types/jsdom": "21.1.1",
|
||||||
"@types/jsonld": "1.5.9",
|
"@types/jsonld": "1.5.9",
|
||||||
"@types/jsrsasign": "10.5.8",
|
"@types/jsrsasign": "10.5.8",
|
||||||
"@types/mime-types": "2.1.1",
|
"@types/mime-types": "2.1.1",
|
||||||
"@types/ms": "^0.7.31",
|
"@types/ms": "^0.7.31",
|
||||||
"@types/node": "20.4.0",
|
"@types/node": "20.4.2",
|
||||||
"@types/node-fetch": "3.0.3",
|
"@types/node-fetch": "3.0.3",
|
||||||
"@types/nodemailer": "6.4.8",
|
"@types/nodemailer": "6.4.8",
|
||||||
"@types/oauth": "0.9.1",
|
"@types/oauth": "0.9.1",
|
||||||
|
@ -201,7 +200,6 @@
|
||||||
"@types/tinycolor2": "1.4.3",
|
"@types/tinycolor2": "1.4.3",
|
||||||
"@types/tmp": "0.2.3",
|
"@types/tmp": "0.2.3",
|
||||||
"@types/unzipper": "0.10.6",
|
"@types/unzipper": "0.10.6",
|
||||||
"@types/uuid": "9.0.2",
|
|
||||||
"@types/vary": "1.1.0",
|
"@types/vary": "1.1.0",
|
||||||
"@types/web-push": "3.3.2",
|
"@types/web-push": "3.3.2",
|
||||||
"@types/websocket": "1.0.5",
|
"@types/websocket": "1.0.5",
|
||||||
|
@ -210,7 +208,7 @@
|
||||||
"@typescript-eslint/parser": "5.61.0",
|
"@typescript-eslint/parser": "5.61.0",
|
||||||
"aws-sdk-client-mock": "3.0.0",
|
"aws-sdk-client-mock": "3.0.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"eslint": "8.44.0",
|
"eslint": "8.45.0",
|
||||||
"eslint-plugin-import": "2.27.5",
|
"eslint-plugin-import": "2.27.5",
|
||||||
"execa": "7.1.1",
|
"execa": "7.1.1",
|
||||||
"jest": "29.6.1",
|
"jest": "29.6.1",
|
||||||
|
|
|
@ -63,6 +63,7 @@ export type Source = {
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
ssl?: boolean;
|
ssl?: boolean;
|
||||||
index: string;
|
index: string;
|
||||||
|
scope?: 'local' | 'global' | string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
proxy?: string;
|
proxy?: string;
|
||||||
|
|
|
@ -81,6 +81,7 @@ import { GalleryLikeEntityService } from './entities/GalleryLikeEntityService.js
|
||||||
import { GalleryPostEntityService } from './entities/GalleryPostEntityService.js';
|
import { GalleryPostEntityService } from './entities/GalleryPostEntityService.js';
|
||||||
import { HashtagEntityService } from './entities/HashtagEntityService.js';
|
import { HashtagEntityService } from './entities/HashtagEntityService.js';
|
||||||
import { InstanceEntityService } from './entities/InstanceEntityService.js';
|
import { InstanceEntityService } from './entities/InstanceEntityService.js';
|
||||||
|
import { InviteCodeEntityService } from './entities/InviteCodeEntityService.js';
|
||||||
import { ModerationLogEntityService } from './entities/ModerationLogEntityService.js';
|
import { ModerationLogEntityService } from './entities/ModerationLogEntityService.js';
|
||||||
import { MutingEntityService } from './entities/MutingEntityService.js';
|
import { MutingEntityService } from './entities/MutingEntityService.js';
|
||||||
import { RenoteMutingEntityService } from './entities/RenoteMutingEntityService.js';
|
import { RenoteMutingEntityService } from './entities/RenoteMutingEntityService.js';
|
||||||
|
@ -205,6 +206,7 @@ const $GalleryLikeEntityService: Provider = { provide: 'GalleryLikeEntityService
|
||||||
const $GalleryPostEntityService: Provider = { provide: 'GalleryPostEntityService', useExisting: GalleryPostEntityService };
|
const $GalleryPostEntityService: Provider = { provide: 'GalleryPostEntityService', useExisting: GalleryPostEntityService };
|
||||||
const $HashtagEntityService: Provider = { provide: 'HashtagEntityService', useExisting: HashtagEntityService };
|
const $HashtagEntityService: Provider = { provide: 'HashtagEntityService', useExisting: HashtagEntityService };
|
||||||
const $InstanceEntityService: Provider = { provide: 'InstanceEntityService', useExisting: InstanceEntityService };
|
const $InstanceEntityService: Provider = { provide: 'InstanceEntityService', useExisting: InstanceEntityService };
|
||||||
|
const $InviteCodeEntityService: Provider = { provide: 'InviteCodeEntityService', useExisting: InviteCodeEntityService };
|
||||||
const $ModerationLogEntityService: Provider = { provide: 'ModerationLogEntityService', useExisting: ModerationLogEntityService };
|
const $ModerationLogEntityService: Provider = { provide: 'ModerationLogEntityService', useExisting: ModerationLogEntityService };
|
||||||
const $MutingEntityService: Provider = { provide: 'MutingEntityService', useExisting: MutingEntityService };
|
const $MutingEntityService: Provider = { provide: 'MutingEntityService', useExisting: MutingEntityService };
|
||||||
const $RenoteMutingEntityService: Provider = { provide: 'RenoteMutingEntityService', useExisting: RenoteMutingEntityService };
|
const $RenoteMutingEntityService: Provider = { provide: 'RenoteMutingEntityService', useExisting: RenoteMutingEntityService };
|
||||||
|
@ -329,6 +331,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
GalleryPostEntityService,
|
GalleryPostEntityService,
|
||||||
HashtagEntityService,
|
HashtagEntityService,
|
||||||
InstanceEntityService,
|
InstanceEntityService,
|
||||||
|
InviteCodeEntityService,
|
||||||
ModerationLogEntityService,
|
ModerationLogEntityService,
|
||||||
MutingEntityService,
|
MutingEntityService,
|
||||||
RenoteMutingEntityService,
|
RenoteMutingEntityService,
|
||||||
|
@ -448,6 +451,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
$GalleryPostEntityService,
|
$GalleryPostEntityService,
|
||||||
$HashtagEntityService,
|
$HashtagEntityService,
|
||||||
$InstanceEntityService,
|
$InstanceEntityService,
|
||||||
|
$InviteCodeEntityService,
|
||||||
$ModerationLogEntityService,
|
$ModerationLogEntityService,
|
||||||
$MutingEntityService,
|
$MutingEntityService,
|
||||||
$RenoteMutingEntityService,
|
$RenoteMutingEntityService,
|
||||||
|
@ -567,6 +571,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
GalleryPostEntityService,
|
GalleryPostEntityService,
|
||||||
HashtagEntityService,
|
HashtagEntityService,
|
||||||
InstanceEntityService,
|
InstanceEntityService,
|
||||||
|
InviteCodeEntityService,
|
||||||
ModerationLogEntityService,
|
ModerationLogEntityService,
|
||||||
MutingEntityService,
|
MutingEntityService,
|
||||||
RenoteMutingEntityService,
|
RenoteMutingEntityService,
|
||||||
|
@ -685,6 +690,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
||||||
$GalleryPostEntityService,
|
$GalleryPostEntityService,
|
||||||
$HashtagEntityService,
|
$HashtagEntityService,
|
||||||
$InstanceEntityService,
|
$InstanceEntityService,
|
||||||
|
$InviteCodeEntityService,
|
||||||
$ModerationLogEntityService,
|
$ModerationLogEntityService,
|
||||||
$MutingEntityService,
|
$MutingEntityService,
|
||||||
$RenoteMutingEntityService,
|
$RenoteMutingEntityService,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
|
import { randomUUID } from 'node:crypto';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
import { IsNull, DataSource } from 'typeorm';
|
import { IsNull, DataSource } from 'typeorm';
|
||||||
import { genRsaKeyPair } from '@/misc/gen-key-pair.js';
|
import { genRsaKeyPair } from '@/misc/gen-key-pair.js';
|
||||||
import { User } from '@/models/entities/User.js';
|
import { User } from '@/models/entities/User.js';
|
||||||
|
@ -24,7 +24,7 @@ export class CreateSystemUserService {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async createSystemUser(username: string): Promise<User> {
|
public async createSystemUser(username: string): Promise<User> {
|
||||||
const password = uuid();
|
const password = randomUUID();
|
||||||
|
|
||||||
// Generate hash of password
|
// Generate hash of password
|
||||||
const salt = await bcrypt.genSalt(8);
|
const salt = await bcrypt.genSalt(8);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
|
import { randomUUID } from 'node:crypto';
|
||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
import { sharpBmp } from 'sharp-read-bmp';
|
import { sharpBmp } from 'sharp-read-bmp';
|
||||||
import { IsNull } from 'typeorm';
|
import { IsNull } from 'typeorm';
|
||||||
|
@ -162,7 +162,7 @@ export class DriveService {
|
||||||
?? `${ meta.objectStorageUseSSL ? 'https' : 'http' }://${ meta.objectStorageEndpoint }${ meta.objectStoragePort ? `:${meta.objectStoragePort}` : '' }/${ meta.objectStorageBucket }`;
|
?? `${ meta.objectStorageUseSSL ? 'https' : 'http' }://${ meta.objectStorageEndpoint }${ meta.objectStoragePort ? `:${meta.objectStoragePort}` : '' }/${ meta.objectStorageBucket }`;
|
||||||
|
|
||||||
// for original
|
// for original
|
||||||
const key = `${meta.objectStoragePrefix}/${uuid()}${ext}`;
|
const key = `${meta.objectStoragePrefix}/${randomUUID()}${ext}`;
|
||||||
const url = `${ baseUrl }/${ key }`;
|
const url = `${ baseUrl }/${ key }`;
|
||||||
|
|
||||||
// for alts
|
// for alts
|
||||||
|
@ -179,7 +179,7 @@ export class DriveService {
|
||||||
];
|
];
|
||||||
|
|
||||||
if (alts.webpublic) {
|
if (alts.webpublic) {
|
||||||
webpublicKey = `${meta.objectStoragePrefix}/webpublic-${uuid()}.${alts.webpublic.ext}`;
|
webpublicKey = `${meta.objectStoragePrefix}/webpublic-${randomUUID()}.${alts.webpublic.ext}`;
|
||||||
webpublicUrl = `${ baseUrl }/${ webpublicKey }`;
|
webpublicUrl = `${ baseUrl }/${ webpublicKey }`;
|
||||||
|
|
||||||
this.registerLogger.info(`uploading webpublic: ${webpublicKey}`);
|
this.registerLogger.info(`uploading webpublic: ${webpublicKey}`);
|
||||||
|
@ -187,7 +187,7 @@ export class DriveService {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (alts.thumbnail) {
|
if (alts.thumbnail) {
|
||||||
thumbnailKey = `${meta.objectStoragePrefix}/thumbnail-${uuid()}.${alts.thumbnail.ext}`;
|
thumbnailKey = `${meta.objectStoragePrefix}/thumbnail-${randomUUID()}.${alts.thumbnail.ext}`;
|
||||||
thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`;
|
thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`;
|
||||||
|
|
||||||
this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`);
|
this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`);
|
||||||
|
@ -212,9 +212,9 @@ export class DriveService {
|
||||||
|
|
||||||
return await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0]));
|
return await this.driveFilesRepository.insert(file).then(x => this.driveFilesRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
} else { // use internal storage
|
} else { // use internal storage
|
||||||
const accessKey = uuid();
|
const accessKey = randomUUID();
|
||||||
const thumbnailAccessKey = 'thumbnail-' + uuid();
|
const thumbnailAccessKey = 'thumbnail-' + randomUUID();
|
||||||
const webpublicAccessKey = 'webpublic-' + uuid();
|
const webpublicAccessKey = 'webpublic-' + randomUUID();
|
||||||
|
|
||||||
const url = this.internalStorageService.saveFromPath(accessKey, path);
|
const url = this.internalStorageService.saveFromPath(accessKey, path);
|
||||||
|
|
||||||
|
@ -584,9 +584,9 @@ export class DriveService {
|
||||||
if (isLink) {
|
if (isLink) {
|
||||||
file.url = url;
|
file.url = url;
|
||||||
// ローカルプロキシ用
|
// ローカルプロキシ用
|
||||||
file.accessKey = uuid();
|
file.accessKey = randomUUID();
|
||||||
file.thumbnailAccessKey = 'thumbnail-' + uuid();
|
file.thumbnailAccessKey = 'thumbnail-' + randomUUID();
|
||||||
file.webpublicAccessKey = 'webpublic-' + uuid();
|
file.webpublicAccessKey = 'webpublic-' + randomUUID();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -713,9 +713,9 @@ export class DriveService {
|
||||||
webpublicUrl: null,
|
webpublicUrl: null,
|
||||||
storedInternal: false,
|
storedInternal: false,
|
||||||
// ローカルプロキシ用
|
// ローカルプロキシ用
|
||||||
accessKey: uuid(),
|
accessKey: randomUUID(),
|
||||||
thumbnailAccessKey: 'thumbnail-' + uuid(),
|
thumbnailAccessKey: 'thumbnail-' + randomUUID(),
|
||||||
webpublicAccessKey: 'webpublic-' + uuid(),
|
webpublicAccessKey: 'webpublic-' + randomUUID(),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.driveFilesRepository.delete(file.id);
|
this.driveFilesRepository.delete(file.id);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import * as http from 'node:http';
|
import * as http from 'node:http';
|
||||||
import * as https from 'node:https';
|
import * as https from 'node:https';
|
||||||
|
import * as net from 'node:net';
|
||||||
import CacheableLookup from 'cacheable-lookup';
|
import CacheableLookup from 'cacheable-lookup';
|
||||||
import fetch from 'node-fetch';
|
import fetch from 'node-fetch';
|
||||||
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
||||||
|
@ -46,14 +47,14 @@ export class HttpRequestService {
|
||||||
this.http = new http.Agent({
|
this.http = new http.Agent({
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
keepAliveMsecs: 30 * 1000,
|
keepAliveMsecs: 30 * 1000,
|
||||||
lookup: cache.lookup,
|
lookup: cache.lookup as unknown as net.LookupFunction,
|
||||||
} as http.AgentOptions);
|
});
|
||||||
|
|
||||||
this.https = new https.Agent({
|
this.https = new https.Agent({
|
||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
keepAliveMsecs: 30 * 1000,
|
keepAliveMsecs: 30 * 1000,
|
||||||
lookup: cache.lookup,
|
lookup: cache.lookup as unknown as net.LookupFunction,
|
||||||
} as https.AgentOptions);
|
});
|
||||||
|
|
||||||
const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128);
|
const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128);
|
||||||
|
|
||||||
|
@ -144,7 +145,7 @@ export class HttpRequestService {
|
||||||
method: args.method ?? 'GET',
|
method: args.method ?? 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': this.config.userAgent,
|
'User-Agent': this.config.userAgent,
|
||||||
...(args.headers ?? {})
|
...(args.headers ?? {}),
|
||||||
},
|
},
|
||||||
body: args.body,
|
body: args.body,
|
||||||
size: args.size ?? 10 * 1024 * 1024,
|
size: args.size ?? 10 * 1024 * 1024,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
import { randomUUID } from 'node:crypto';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
import type { IActivity } from '@/core/activitypub/type.js';
|
import type { IActivity } from '@/core/activitypub/type.js';
|
||||||
import type { DriveFile } from '@/models/entities/DriveFile.js';
|
import type { DriveFile } from '@/models/entities/DriveFile.js';
|
||||||
import type { Webhook, webhookEventTypes } from '@/models/entities/Webhook.js';
|
import type { Webhook, webhookEventTypes } from '@/models/entities/Webhook.js';
|
||||||
|
@ -416,7 +416,7 @@ export class QueueService {
|
||||||
to: webhook.url,
|
to: webhook.url,
|
||||||
secret: webhook.secret,
|
secret: webhook.secret,
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
eventId: uuid(),
|
eventId: randomUUID(),
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.webhookDeliverQueue.add(webhook.id, data, {
|
return this.webhookDeliverQueue.add(webhook.id, data, {
|
||||||
|
|
|
@ -71,7 +71,7 @@ export class RemoteUserResolveService {
|
||||||
return await this.apPersonService.createPerson(self.href);
|
return await this.apPersonService.createPerson(self.href);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ユーザー情報が古い場合は、WebFilgerからやりなおして返す
|
// ユーザー情報が古い場合は、WebFingerからやりなおして返す
|
||||||
if (user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
|
if (user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
|
||||||
// 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する
|
// 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する
|
||||||
await this.usersRepository.update(user.id, {
|
await this.usersRepository.update(user.id, {
|
||||||
|
|
|
@ -21,6 +21,9 @@ export type RolePolicies = {
|
||||||
ltlAvailable: boolean;
|
ltlAvailable: boolean;
|
||||||
canPublicNote: boolean;
|
canPublicNote: boolean;
|
||||||
canInvite: boolean;
|
canInvite: boolean;
|
||||||
|
inviteLimit: number;
|
||||||
|
inviteLimitCycle: number;
|
||||||
|
inviteExpirationTime: number;
|
||||||
canManageCustomEmojis: boolean;
|
canManageCustomEmojis: boolean;
|
||||||
canSearchNotes: boolean;
|
canSearchNotes: boolean;
|
||||||
canHideAds: boolean;
|
canHideAds: boolean;
|
||||||
|
@ -42,6 +45,9 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
||||||
ltlAvailable: true,
|
ltlAvailable: true,
|
||||||
canPublicNote: true,
|
canPublicNote: true,
|
||||||
canInvite: false,
|
canInvite: false,
|
||||||
|
inviteLimit: 0,
|
||||||
|
inviteLimitCycle: 60 * 24 * 7,
|
||||||
|
inviteExpirationTime: 0,
|
||||||
canManageCustomEmojis: false,
|
canManageCustomEmojis: false,
|
||||||
canSearchNotes: false,
|
canSearchNotes: false,
|
||||||
canHideAds: false,
|
canHideAds: false,
|
||||||
|
@ -277,6 +283,9 @@ export class RoleService implements OnApplicationShutdown {
|
||||||
ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)),
|
ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)),
|
||||||
canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)),
|
canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)),
|
||||||
canInvite: calc('canInvite', 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)),
|
canManageCustomEmojis: calc('canManageCustomEmojis', vs => vs.some(v => v === true)),
|
||||||
canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)),
|
canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)),
|
||||||
canHideAds: calc('canHideAds', 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()
|
@Injectable()
|
||||||
export class SearchService {
|
export class SearchService {
|
||||||
|
private readonly meilisearchIndexScope: 'local' | 'global' | string[] = 'local';
|
||||||
private meilisearchNoteIndex: Index | null = null;
|
private meilisearchNoteIndex: Index | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -92,6 +93,10 @@ export class SearchService {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.meilisearch?.scope) {
|
||||||
|
this.meilisearchIndexScope = config.meilisearch.scope;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
|
@ -100,7 +105,22 @@ export class SearchService {
|
||||||
if (!['home', 'public'].includes(note.visibility)) return;
|
if (!['home', 'public'].includes(note.visibility)) return;
|
||||||
|
|
||||||
if (this.meilisearch) {
|
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,
|
id: note.id,
|
||||||
createdAt: note.createdAt.getTime(),
|
createdAt: note.createdAt.getTime(),
|
||||||
userId: note.userId,
|
userId: note.userId,
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { createPublicKey } from 'node:crypto';
|
import { createPublicKey, randomUUID } from 'node:crypto';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
import * as mfm from 'mfm-js';
|
import * as mfm from 'mfm-js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
|
@ -613,7 +612,7 @@ export class ApRendererService {
|
||||||
@bindThis
|
@bindThis
|
||||||
public addContext<T extends IObject>(x: T): T & { '@context': any; id: string; } {
|
public addContext<T extends IObject>(x: T): T & { '@context': any; id: string; } {
|
||||||
if (typeof x === 'object' && x.id == null) {
|
if (typeof x === 'object' && x.id == null) {
|
||||||
x.id = `${this.config.url}/${uuid()}`;
|
x.id = `${this.config.url}/${randomUUID()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.assign({
|
return Object.assign({
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import type { DriveFilesRepository } from '@/models/index.js';
|
import type { DriveFilesRepository } from '@/models/index.js';
|
||||||
import type { Config } from '@/config.js';
|
|
||||||
import type { RemoteUser } from '@/models/entities/User.js';
|
import type { RemoteUser } from '@/models/entities/User.js';
|
||||||
import type { DriveFile } from '@/models/entities/DriveFile.js';
|
import type { DriveFile } from '@/models/entities/DriveFile.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
|
@ -20,9 +19,6 @@ export class ApImageService {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.config)
|
|
||||||
private config: Config,
|
|
||||||
|
|
||||||
@Inject(DI.driveFilesRepository)
|
@Inject(DI.driveFilesRepository)
|
||||||
private driveFilesRepository: DriveFilesRepository,
|
private driveFilesRepository: DriveFilesRepository,
|
||||||
|
|
||||||
|
@ -47,7 +43,7 @@ export class ApImageService {
|
||||||
const image = await this.apResolverService.createResolver().resolve(value);
|
const image = await this.apResolverService.createResolver().resolve(value);
|
||||||
|
|
||||||
if (image.url == null) {
|
if (image.url == null) {
|
||||||
throw new Error('invalid image: url not privided');
|
throw new Error('invalid image: url not provided');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof image.url !== 'string') {
|
if (typeof image.url !== 'string') {
|
||||||
|
@ -62,12 +58,17 @@ export class ApImageService {
|
||||||
|
|
||||||
const instance = await this.metaService.fetch();
|
const instance = await this.metaService.fetch();
|
||||||
|
|
||||||
|
// Cache if remote file cache is on AND either
|
||||||
|
// 1. remote sensitive file is also on
|
||||||
|
// 2. or the image is not sensitive
|
||||||
|
const shouldBeCached = instance.cacheRemoteFiles && (instance.cacheRemoteSensitiveFiles || !image.sensitive);
|
||||||
|
|
||||||
const file = await this.driveService.uploadFromUrl({
|
const file = await this.driveService.uploadFromUrl({
|
||||||
url: image.url,
|
url: image.url,
|
||||||
user: actor,
|
user: actor,
|
||||||
uri: image.url,
|
uri: image.url,
|
||||||
sensitive: image.sensitive,
|
sensitive: image.sensitive,
|
||||||
isLink: !instance.cacheRemoteFiles,
|
isLink: !shouldBeCached,
|
||||||
comment: truncate(image.name ?? undefined, DB_MAX_IMAGE_COMMENT_LENGTH),
|
comment: truncate(image.name ?? undefined, DB_MAX_IMAGE_COMMENT_LENGTH),
|
||||||
});
|
});
|
||||||
if (!file.isLink || file.url === image.url) return file;
|
if (!file.isLink || file.url === image.url) return file;
|
||||||
|
|
|
@ -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)));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 { packedBlockingSchema } from '@/models/json-schema/blocking.js';
|
||||||
import { packedNoteReactionSchema } from '@/models/json-schema/note-reaction.js';
|
import { packedNoteReactionSchema } from '@/models/json-schema/note-reaction.js';
|
||||||
import { packedHashtagSchema } from '@/models/json-schema/hashtag.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 { packedPageSchema } from '@/models/json-schema/page.js';
|
||||||
import { packedNoteFavoriteSchema } from '@/models/json-schema/note-favorite.js';
|
import { packedNoteFavoriteSchema } from '@/models/json-schema/note-favorite.js';
|
||||||
import { packedChannelSchema } from '@/models/json-schema/channel.js';
|
import { packedChannelSchema } from '@/models/json-schema/channel.js';
|
||||||
|
@ -52,6 +53,7 @@ export const refs = {
|
||||||
RenoteMuting: packedRenoteMutingSchema,
|
RenoteMuting: packedRenoteMutingSchema,
|
||||||
Blocking: packedBlockingSchema,
|
Blocking: packedBlockingSchema,
|
||||||
Hashtag: packedHashtagSchema,
|
Hashtag: packedHashtagSchema,
|
||||||
|
InviteCode: packedInviteCodeSchema,
|
||||||
Page: packedPageSchema,
|
Page: packedPageSchema,
|
||||||
Channel: packedChannelSchema,
|
Channel: packedChannelSchema,
|
||||||
QueueCount: packedQueueCountSchema,
|
QueueCount: packedQueueCountSchema,
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm';
|
import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm';
|
||||||
import { id } from '../id.js';
|
import { id } from '../id.js';
|
||||||
import { User } from './User.js';
|
import { User } from './User.js';
|
||||||
import type { Clip } from './Clip.js';
|
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Meta {
|
export class Meta {
|
||||||
|
@ -126,6 +125,11 @@ export class Meta {
|
||||||
})
|
})
|
||||||
public cacheRemoteFiles: boolean;
|
public cacheRemoteFiles: boolean;
|
||||||
|
|
||||||
|
@Column('boolean', {
|
||||||
|
default: true,
|
||||||
|
})
|
||||||
|
public cacheRemoteSensitiveFiles: boolean;
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
...id(),
|
...id(),
|
||||||
nullable: true,
|
nullable: true,
|
||||||
|
|
|
@ -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 { id } from '../id.js';
|
||||||
|
import { User } from './User.js';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class RegistrationTicket {
|
export class RegistrationTicket {
|
||||||
@PrimaryColumn(id())
|
@PrimaryColumn(id())
|
||||||
public id: string;
|
public id: string;
|
||||||
|
|
||||||
@Column('timestamp with time zone')
|
|
||||||
public createdAt: Date;
|
|
||||||
|
|
||||||
@Index({ unique: true })
|
@Index({ unique: true })
|
||||||
@Column('varchar', {
|
@Column('varchar', {
|
||||||
length: 64,
|
length: 64,
|
||||||
})
|
})
|
||||||
public code: string;
|
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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,8 +1,8 @@
|
||||||
|
import { randomUUID } from 'node:crypto';
|
||||||
import { pipeline } from 'node:stream';
|
import { pipeline } from 'node:stream';
|
||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
import { promisify } from 'node:util';
|
import { promisify } from 'node:util';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { getIpHash } from '@/misc/get-ip-hash.js';
|
import { getIpHash } from '@/misc/get-ip-hash.js';
|
||||||
import type { LocalUser, User } from '@/models/entities/User.js';
|
import type { LocalUser, User } from '@/models/entities/User.js';
|
||||||
|
@ -362,7 +362,7 @@ export class ApiCallService implements OnApplicationShutdown {
|
||||||
if (err instanceof ApiError || err instanceof AuthenticationError) {
|
if (err instanceof ApiError || err instanceof AuthenticationError) {
|
||||||
throw err;
|
throw err;
|
||||||
} else {
|
} else {
|
||||||
const errId = uuid();
|
const errId = randomUUID();
|
||||||
this.logger.error(`Internal error occurred in ${ep.name}: ${err.message}`, {
|
this.logger.error(`Internal error occurred in ${ep.name}: ${err.message}`, {
|
||||||
ep: ep.name,
|
ep: ep.name,
|
||||||
ps: data,
|
ps: data,
|
||||||
|
|
|
@ -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_getIndexStats from './endpoints/admin/get-index-stats.js';
|
||||||
import * as ep___admin_getTableStats from './endpoints/admin/get-table-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___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_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_clear from './endpoints/admin/queue/clear.js';
|
||||||
import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.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_list from './endpoints/i/webhooks/list.js';
|
||||||
import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.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___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___meta from './endpoints/meta.js';
|
||||||
import * as ep___emojis from './endpoints/emojis.js';
|
import * as ep___emojis from './endpoints/emojis.js';
|
||||||
import * as ep___emoji from './endpoints/emoji.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_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_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 $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_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_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 };
|
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_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_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 $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 $meta: Provider = { provide: 'ep:meta', useClass: ep___meta.default };
|
||||||
const $emojis: Provider = { provide: 'ep:emojis', useClass: ep___emojis.default };
|
const $emojis: Provider = { provide: 'ep:emojis', useClass: ep___emojis.default };
|
||||||
const $emoji: Provider = { provide: 'ep:emoji', useClass: ep___emoji.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_getIndexStats,
|
||||||
$admin_getTableStats,
|
$admin_getTableStats,
|
||||||
$admin_getUserIps,
|
$admin_getUserIps,
|
||||||
$invite,
|
$admin_invite_create,
|
||||||
|
$admin_invite_list,
|
||||||
$admin_promo_create,
|
$admin_promo_create,
|
||||||
$admin_queue_clear,
|
$admin_queue_clear,
|
||||||
$admin_queue_deliverDelayed,
|
$admin_queue_deliverDelayed,
|
||||||
|
@ -914,6 +925,10 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||||
$i_webhooks_show,
|
$i_webhooks_show,
|
||||||
$i_webhooks_update,
|
$i_webhooks_update,
|
||||||
$i_webhooks_delete,
|
$i_webhooks_delete,
|
||||||
|
$invite_create,
|
||||||
|
$invite_delete,
|
||||||
|
$invite_list,
|
||||||
|
$invite_limit,
|
||||||
$meta,
|
$meta,
|
||||||
$emojis,
|
$emojis,
|
||||||
$emoji,
|
$emoji,
|
||||||
|
@ -1060,7 +1075,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||||
$admin_getIndexStats,
|
$admin_getIndexStats,
|
||||||
$admin_getTableStats,
|
$admin_getTableStats,
|
||||||
$admin_getUserIps,
|
$admin_getUserIps,
|
||||||
$invite,
|
$admin_invite_create,
|
||||||
|
$admin_invite_list,
|
||||||
$admin_promo_create,
|
$admin_promo_create,
|
||||||
$admin_queue_clear,
|
$admin_queue_clear,
|
||||||
$admin_queue_deliverDelayed,
|
$admin_queue_deliverDelayed,
|
||||||
|
@ -1252,6 +1268,10 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
||||||
$i_webhooks_show,
|
$i_webhooks_show,
|
||||||
$i_webhooks_update,
|
$i_webhooks_update,
|
||||||
$i_webhooks_delete,
|
$i_webhooks_delete,
|
||||||
|
$invite_create,
|
||||||
|
$invite_delete,
|
||||||
|
$invite_list,
|
||||||
|
$invite_limit,
|
||||||
$meta,
|
$meta,
|
||||||
$emojis,
|
$emojis,
|
||||||
$emoji,
|
$emoji,
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
import { IsNull } from 'typeorm';
|
import { IsNull } from 'typeorm';
|
||||||
import { DI } from '@/di-symbols.js';
|
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 type { Config } from '@/config.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import { CaptchaService } from '@/core/CaptchaService.js';
|
import { CaptchaService } from '@/core/CaptchaService.js';
|
||||||
|
@ -109,13 +109,15 @@ export class SignupApiService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let ticket: RegistrationTicket | null = null;
|
||||||
|
|
||||||
if (instance.disableRegistration) {
|
if (instance.disableRegistration) {
|
||||||
if (invitationCode == null || typeof invitationCode !== 'string') {
|
if (invitationCode == null || typeof invitationCode !== 'string') {
|
||||||
reply.code(400);
|
reply.code(400);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ticket = await this.registrationTicketsRepository.findOneBy({
|
ticket = await this.registrationTicketsRepository.findOneBy({
|
||||||
code: invitationCode,
|
code: invitationCode,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -124,7 +126,15 @@ export class SignupApiService {
|
||||||
return;
|
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 (instance.emailRequiredForSignup) {
|
||||||
|
@ -148,14 +158,14 @@ export class SignupApiService {
|
||||||
const salt = await bcrypt.genSalt(8);
|
const salt = await bcrypt.genSalt(8);
|
||||||
const hash = await bcrypt.hash(password, salt);
|
const hash = await bcrypt.hash(password, salt);
|
||||||
|
|
||||||
await this.userPendingsRepository.insert({
|
const pendingUser = await this.userPendingsRepository.insert({
|
||||||
id: this.idService.genId(),
|
id: this.idService.genId(),
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
code,
|
code,
|
||||||
email: emailAddress!,
|
email: emailAddress!,
|
||||||
username: username,
|
username: username,
|
||||||
password: hash,
|
password: hash,
|
||||||
});
|
}).then(x => this.userPendingsRepository.findOneByOrFail(x.identifiers[0]));
|
||||||
|
|
||||||
const link = `${this.config.url}/signup-complete/${code}`;
|
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:<br><a href="${link}">${link}</a>`,
|
||||||
`To complete signup, please click this link: ${link}`);
|
`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);
|
reply.code(204);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
|
@ -176,6 +193,14 @@ export class SignupApiService {
|
||||||
includeSecrets: true,
|
includeSecrets: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (ticket) {
|
||||||
|
await this.registrationTicketsRepository.update(ticket.id, {
|
||||||
|
usedAt: new Date(),
|
||||||
|
usedBy: account,
|
||||||
|
usedById: account.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...res,
|
...res,
|
||||||
token: secret,
|
token: secret,
|
||||||
|
@ -212,6 +237,15 @@ export class SignupApiService {
|
||||||
emailVerifyCode: null,
|
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);
|
return this.signinService.signin(request, reply, account as LocalUser);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new FastifyReplyError(400, typeof err === 'string' ? err : (err as Error).toString());
|
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_getIndexStats from './endpoints/admin/get-index-stats.js';
|
||||||
import * as ep___admin_getTableStats from './endpoints/admin/get-table-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___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_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_clear from './endpoints/admin/queue/clear.js';
|
||||||
import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.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_list from './endpoints/i/webhooks/list.js';
|
||||||
import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.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___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___meta from './endpoints/meta.js';
|
||||||
import * as ep___emojis from './endpoints/emojis.js';
|
import * as ep___emojis from './endpoints/emojis.js';
|
||||||
import * as ep___emoji from './endpoints/emoji.js';
|
import * as ep___emoji from './endpoints/emoji.js';
|
||||||
|
@ -376,7 +381,8 @@ const eps = [
|
||||||
['admin/get-index-stats', ep___admin_getIndexStats],
|
['admin/get-index-stats', ep___admin_getIndexStats],
|
||||||
['admin/get-table-stats', ep___admin_getTableStats],
|
['admin/get-table-stats', ep___admin_getTableStats],
|
||||||
['admin/get-user-ips', ep___admin_getUserIps],
|
['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/promo/create', ep___admin_promo_create],
|
||||||
['admin/queue/clear', ep___admin_queue_clear],
|
['admin/queue/clear', ep___admin_queue_clear],
|
||||||
['admin/queue/deliver-delayed', ep___admin_queue_deliverDelayed],
|
['admin/queue/deliver-delayed', ep___admin_queue_deliverDelayed],
|
||||||
|
@ -568,6 +574,10 @@ const eps = [
|
||||||
['i/webhooks/show', ep___i_webhooks_show],
|
['i/webhooks/show', ep___i_webhooks_show],
|
||||||
['i/webhooks/update', ep___i_webhooks_update],
|
['i/webhooks/update', ep___i_webhooks_update],
|
||||||
['i/webhooks/delete', ep___i_webhooks_delete],
|
['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],
|
['meta', ep___meta],
|
||||||
['emojis', ep___emojis],
|
['emojis', ep___emojis],
|
||||||
['emoji', ep___emoji],
|
['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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import { MetaService } from '@/core/MetaService.js';
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
|
@ -20,6 +19,10 @@ export const meta = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
cacheRemoteSensitiveFiles: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
emailRequiredForSignup: {
|
emailRequiredForSignup: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -332,6 +335,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
enableServiceWorker: instance.enableServiceWorker,
|
enableServiceWorker: instance.enableServiceWorker,
|
||||||
translatorAvailable: instance.deeplAuthKey != null,
|
translatorAvailable: instance.deeplAuthKey != null,
|
||||||
cacheRemoteFiles: instance.cacheRemoteFiles,
|
cacheRemoteFiles: instance.cacheRemoteFiles,
|
||||||
|
cacheRemoteSensitiveFiles: instance.cacheRemoteSensitiveFiles,
|
||||||
pinnedUsers: instance.pinnedUsers,
|
pinnedUsers: instance.pinnedUsers,
|
||||||
hiddenTags: instance.hiddenTags,
|
hiddenTags: instance.hiddenTags,
|
||||||
blockedHosts: instance.blockedHosts,
|
blockedHosts: instance.blockedHosts,
|
||||||
|
|
|
@ -43,6 +43,7 @@ export const paramDef = {
|
||||||
defaultLightTheme: { type: 'string', nullable: true },
|
defaultLightTheme: { type: 'string', nullable: true },
|
||||||
defaultDarkTheme: { type: 'string', nullable: true },
|
defaultDarkTheme: { type: 'string', nullable: true },
|
||||||
cacheRemoteFiles: { type: 'boolean' },
|
cacheRemoteFiles: { type: 'boolean' },
|
||||||
|
cacheRemoteSensitiveFiles: { type: 'boolean' },
|
||||||
emailRequiredForSignup: { type: 'boolean' },
|
emailRequiredForSignup: { type: 'boolean' },
|
||||||
enableHcaptcha: { type: 'boolean' },
|
enableHcaptcha: { type: 'boolean' },
|
||||||
hcaptchaSiteKey: { type: 'string', nullable: true },
|
hcaptchaSiteKey: { type: 'string', nullable: true },
|
||||||
|
@ -193,6 +194,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
set.cacheRemoteFiles = ps.cacheRemoteFiles;
|
set.cacheRemoteFiles = ps.cacheRemoteFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ps.cacheRemoteSensitiveFiles !== undefined) {
|
||||||
|
set.cacheRemoteSensitiveFiles = ps.cacheRemoteSensitiveFiles;
|
||||||
|
}
|
||||||
|
|
||||||
if (ps.emailRequiredForSignup !== undefined) {
|
if (ps.emailRequiredForSignup !== undefined) {
|
||||||
set.emailRequiredForSignup = ps.emailRequiredForSignup;
|
set.emailRequiredForSignup = ps.emailRequiredForSignup;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { v4 as uuid } from 'uuid';
|
import { randomUUID } from 'node:crypto';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { AppsRepository, AuthSessionsRepository } from '@/models/index.js';
|
import type { AppsRepository, AuthSessionsRepository } from '@/models/index.js';
|
||||||
|
@ -71,7 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate token
|
// Generate token
|
||||||
const token = uuid();
|
const token = randomUUID();
|
||||||
|
|
||||||
// Create session token document
|
// Create session token document
|
||||||
const doc = await this.authSessionsRepository.insert({
|
const doc = await this.authSessionsRepository.insert({
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { MoreThan } from 'typeorm';
|
||||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||||
import type { RegistrationTicketsRepository } from '@/models/index.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 { DI } from '@/di-symbols.js';
|
||||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
|
||||||
|
|
||||||
export const meta = {
|
export const meta = {
|
||||||
tags: ['meta'],
|
tags: ['meta'],
|
||||||
|
@ -15,12 +15,9 @@ export const meta = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
properties: {
|
properties: {
|
||||||
code: {
|
remaining: {
|
||||||
type: 'string',
|
type: 'integer',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: true,
|
||||||
example: '2ERUA5VR',
|
|
||||||
maxLength: 8,
|
|
||||||
minLength: 8,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -39,21 +36,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
@Inject(DI.registrationTicketsRepository)
|
@Inject(DI.registrationTicketsRepository)
|
||||||
private registrationTicketsRepository: RegistrationTicketsRepository,
|
private registrationTicketsRepository: RegistrationTicketsRepository,
|
||||||
|
|
||||||
private idService: IdService,
|
private roleService: RoleService,
|
||||||
) {
|
) {
|
||||||
super(meta, paramDef, async (ps, me) => {
|
super(meta, paramDef, async (ps, me) => {
|
||||||
const code = secureRndstr(8, {
|
const policies = await this.roleService.getUserPolicies(me.id);
|
||||||
chars: '23456789ABCDEFGHJKLMNPQRSTUVWXYZ', // [0-9A-Z] w/o [01IO] (32 patterns)
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.registrationTicketsRepository.insert({
|
const count = policies.inviteLimit ? await this.registrationTicketsRepository.countBy({
|
||||||
id: this.idService.genId(),
|
createdAt: MoreThan(new Date(Date.now() - (policies.inviteExpirationTime * 60 * 1000))),
|
||||||
createdAt: new Date(),
|
createdById: me.id,
|
||||||
code,
|
}) : null;
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
code,
|
remaining: count !== null ? Math.max(0, policies.inviteLimit - count) : null,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
|
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -83,6 +83,10 @@ export const meta = {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
},
|
},
|
||||||
|
cacheRemoteSensitiveFiles: {
|
||||||
|
type: 'boolean',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
},
|
||||||
emailRequiredForSignup: {
|
emailRequiredForSignup: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
optional: false, nullable: false,
|
optional: false, nullable: false,
|
||||||
|
@ -272,7 +276,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
.orWhere('ads.dayOfWeek = 0');
|
.orWhere('ads.dayOfWeek = 0');
|
||||||
}))
|
}))
|
||||||
.getMany();
|
.getMany();
|
||||||
|
|
||||||
const response: any = {
|
const response: any = {
|
||||||
maintainerName: instance.maintainerName,
|
maintainerName: instance.maintainerName,
|
||||||
maintainerEmail: instance.maintainerEmail,
|
maintainerEmail: instance.maintainerEmail,
|
||||||
|
@ -329,6 +333,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||||
|
|
||||||
...(ps.detail ? {
|
...(ps.detail ? {
|
||||||
cacheRemoteFiles: instance.cacheRemoteFiles,
|
cacheRemoteFiles: instance.cacheRemoteFiles,
|
||||||
|
cacheRemoteSensitiveFiles: instance.cacheRemoteSensitiveFiles,
|
||||||
requireSetup: (await this.usersRepository.countBy({
|
requireSetup: (await this.usersRepository.countBy({
|
||||||
host: IsNull(),
|
host: IsNull(),
|
||||||
})) === 0,
|
})) === 0,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
|
import { randomUUID } from 'node:crypto';
|
||||||
import { dirname } from 'node:path';
|
import { dirname } from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
import { createBullBoard } from '@bull-board/api';
|
import { createBullBoard } from '@bull-board/api';
|
||||||
import { BullAdapter } from '@bull-board/api/bullAdapter.js';
|
import { BullAdapter } from '@bull-board/api/bullAdapter.js';
|
||||||
import { FastifyAdapter } from '@bull-board/fastify';
|
import { FastifyAdapter } from '@bull-board/fastify';
|
||||||
|
@ -676,7 +676,7 @@ export class ClientServerService {
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.setErrorHandler(async (error, request, reply) => {
|
fastify.setErrorHandler(async (error, request, reply) => {
|
||||||
const errId = uuid();
|
const errId = randomUUID();
|
||||||
this.clientLoggerService.logger.error(`Internal error occured in ${request.routerPath}: ${error.message}`, {
|
this.clientLoggerService.logger.error(`Internal error occured in ${request.routerPath}: ${error.message}`, {
|
||||||
path: request.routerPath,
|
path: request.routerPath,
|
||||||
params: request.params,
|
params: request.params,
|
||||||
|
|
|
@ -35,7 +35,7 @@ html
|
||||||
link(rel='prefetch' href=infoImageUrl)
|
link(rel='prefetch' href=infoImageUrl)
|
||||||
link(rel='prefetch' href=notFoundImageUrl)
|
link(rel='prefetch' href=notFoundImageUrl)
|
||||||
//- https://github.com/misskey-dev/misskey/issues/9842
|
//- https://github.com/misskey-dev/misskey/issues/9842
|
||||||
link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.24.0')
|
link(rel='stylesheet' href='/assets/tabler-icons/tabler-icons.min.css?v2.25.0')
|
||||||
link(rel='modulepreload' href=`/vite/${clientEntry.file}`)
|
link(rel='modulepreload' href=`/vite/${clientEntry.file}`)
|
||||||
|
|
||||||
if !config.clientManifestExists
|
if !config.clientManifestExists
|
||||||
|
|
|
@ -4,6 +4,7 @@ import * as assert from 'assert';
|
||||||
import { Test } from '@nestjs/testing';
|
import { Test } from '@nestjs/testing';
|
||||||
import { jest } from '@jest/globals';
|
import { jest } from '@jest/globals';
|
||||||
|
|
||||||
|
import { ApImageService } from '@/core/activitypub/models/ApImageService.js';
|
||||||
import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js';
|
import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js';
|
||||||
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
||||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||||
|
@ -11,9 +12,12 @@ import { GlobalModule } from '@/GlobalModule.js';
|
||||||
import { CoreModule } from '@/core/CoreModule.js';
|
import { CoreModule } from '@/core/CoreModule.js';
|
||||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import type { IActivity, IActor, ICollection, IObject, IOrderedCollection, IOrderedCollectionPage, IPost } from '@/core/activitypub/type.js';
|
import type { IActivity, IApDocument, IActor, ICollection, IObject, IOrderedCollection, IOrderedCollectionPage, IPost } from '@/core/activitypub/type.js';
|
||||||
import { Note } from '@/models/index.js';
|
import { Meta, Note } from '@/models/index.js';
|
||||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||||
|
import { DownloadService } from '@/core/DownloadService.js';
|
||||||
|
import { MetaService } from '@/core/MetaService.js';
|
||||||
|
import type { RemoteUser } from '@/models/entities/User.js';
|
||||||
import { MockResolver } from '../misc/mock-resolver.js';
|
import { MockResolver } from '../misc/mock-resolver.js';
|
||||||
|
|
||||||
const host = 'https://host1.test';
|
const host = 'https://host1.test';
|
||||||
|
@ -120,16 +124,47 @@ function createRandomPagedOutbox(actor: NonTransientIActor): NonTransientIOrdere
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createRandomRemoteUser(
|
||||||
|
resolver: MockResolver,
|
||||||
|
personService: ApPersonService,
|
||||||
|
): Promise<RemoteUser> {
|
||||||
|
const actor = createRandomActor();
|
||||||
|
resolver.register(actor.id, actor);
|
||||||
|
|
||||||
|
return await personService.createPerson(actor.id, resolver);
|
||||||
|
}
|
||||||
|
|
||||||
describe('ActivityPub', () => {
|
describe('ActivityPub', () => {
|
||||||
|
let imageService: ApImageService;
|
||||||
let noteService: ApNoteService;
|
let noteService: ApNoteService;
|
||||||
let personService: ApPersonService;
|
let personService: ApPersonService;
|
||||||
let rendererService: ApRendererService;
|
let rendererService: ApRendererService;
|
||||||
let resolver: MockResolver;
|
let resolver: MockResolver;
|
||||||
|
|
||||||
|
const metaInitial = {
|
||||||
|
cacheRemoteFiles: true,
|
||||||
|
cacheRemoteSensitiveFiles: true,
|
||||||
|
blockedHosts: [] as string[],
|
||||||
|
sensitiveWords: [] as string[],
|
||||||
|
} as Meta;
|
||||||
|
let meta = metaInitial;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const app = await Test.createTestingModule({
|
const app = await Test.createTestingModule({
|
||||||
imports: [GlobalModule, CoreModule],
|
imports: [GlobalModule, CoreModule],
|
||||||
}).compile();
|
})
|
||||||
|
.overrideProvider(DownloadService).useValue({
|
||||||
|
async downloadUrl(): Promise<{ filename: string }> {
|
||||||
|
return {
|
||||||
|
filename: 'dummy.tmp',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.overrideProvider(MetaService).useValue({
|
||||||
|
async fetch(): Promise<Meta> {
|
||||||
|
return meta;
|
||||||
|
},
|
||||||
|
}).compile();
|
||||||
|
|
||||||
await app.init();
|
await app.init();
|
||||||
app.enableShutdownHooks();
|
app.enableShutdownHooks();
|
||||||
|
@ -137,6 +172,7 @@ describe('ActivityPub', () => {
|
||||||
noteService = app.get<ApNoteService>(ApNoteService);
|
noteService = app.get<ApNoteService>(ApNoteService);
|
||||||
personService = app.get<ApPersonService>(ApPersonService);
|
personService = app.get<ApPersonService>(ApPersonService);
|
||||||
rendererService = app.get<ApRendererService>(ApRendererService);
|
rendererService = app.get<ApRendererService>(ApRendererService);
|
||||||
|
imageService = app.get<ApImageService>(ApImageService);
|
||||||
resolver = new MockResolver(await app.resolve<LoggerService>(LoggerService));
|
resolver = new MockResolver(await app.resolve<LoggerService>(LoggerService));
|
||||||
|
|
||||||
// Prevent ApPersonService from fetching instance, as it causes Jest import-after-test error
|
// Prevent ApPersonService from fetching instance, as it causes Jest import-after-test error
|
||||||
|
@ -365,4 +401,91 @@ describe('ActivityPub', () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Images', () => {
|
||||||
|
test('Create images', async () => {
|
||||||
|
const imageObject: IApDocument = {
|
||||||
|
type: 'Document',
|
||||||
|
mediaType: 'image/png',
|
||||||
|
url: 'http://host1.test/foo.png',
|
||||||
|
name: '',
|
||||||
|
};
|
||||||
|
const driveFile = await imageService.createImage(
|
||||||
|
await createRandomRemoteUser(resolver, personService),
|
||||||
|
imageObject,
|
||||||
|
);
|
||||||
|
assert.ok(!driveFile.isLink);
|
||||||
|
|
||||||
|
const sensitiveImageObject: IApDocument = {
|
||||||
|
type: 'Document',
|
||||||
|
mediaType: 'image/png',
|
||||||
|
url: 'http://host1.test/bar.png',
|
||||||
|
name: '',
|
||||||
|
sensitive: true,
|
||||||
|
};
|
||||||
|
const sensitiveDriveFile = await imageService.createImage(
|
||||||
|
await createRandomRemoteUser(resolver, personService),
|
||||||
|
sensitiveImageObject,
|
||||||
|
);
|
||||||
|
assert.ok(!sensitiveDriveFile.isLink);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cacheRemoteFiles=false disables caching', async () => {
|
||||||
|
meta = { ...metaInitial, cacheRemoteFiles: false };
|
||||||
|
|
||||||
|
const imageObject: IApDocument = {
|
||||||
|
type: 'Document',
|
||||||
|
mediaType: 'image/png',
|
||||||
|
url: 'http://host1.test/foo.png',
|
||||||
|
name: '',
|
||||||
|
};
|
||||||
|
const driveFile = await imageService.createImage(
|
||||||
|
await createRandomRemoteUser(resolver, personService),
|
||||||
|
imageObject,
|
||||||
|
);
|
||||||
|
assert.ok(driveFile.isLink);
|
||||||
|
|
||||||
|
const sensitiveImageObject: IApDocument = {
|
||||||
|
type: 'Document',
|
||||||
|
mediaType: 'image/png',
|
||||||
|
url: 'http://host1.test/bar.png',
|
||||||
|
name: '',
|
||||||
|
sensitive: true,
|
||||||
|
};
|
||||||
|
const sensitiveDriveFile = await imageService.createImage(
|
||||||
|
await createRandomRemoteUser(resolver, personService),
|
||||||
|
sensitiveImageObject,
|
||||||
|
);
|
||||||
|
assert.ok(sensitiveDriveFile.isLink);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cacheRemoteSensitiveFiles=false only affects sensitive files', async () => {
|
||||||
|
meta = { ...metaInitial, cacheRemoteSensitiveFiles: false };
|
||||||
|
|
||||||
|
const imageObject: IApDocument = {
|
||||||
|
type: 'Document',
|
||||||
|
mediaType: 'image/png',
|
||||||
|
url: 'http://host1.test/foo.png',
|
||||||
|
name: '',
|
||||||
|
};
|
||||||
|
const driveFile = await imageService.createImage(
|
||||||
|
await createRandomRemoteUser(resolver, personService),
|
||||||
|
imageObject,
|
||||||
|
);
|
||||||
|
assert.ok(!driveFile.isLink);
|
||||||
|
|
||||||
|
const sensitiveImageObject: IApDocument = {
|
||||||
|
type: 'Document',
|
||||||
|
mediaType: 'image/png',
|
||||||
|
url: 'http://host1.test/bar.png',
|
||||||
|
name: '',
|
||||||
|
sensitive: true,
|
||||||
|
};
|
||||||
|
const sensitiveDriveFile = await imageService.createImage(
|
||||||
|
await createRandomRemoteUser(resolver, personService),
|
||||||
|
sensitiveImageObject,
|
||||||
|
);
|
||||||
|
assert.ok(sensitiveDriveFile.isLink);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -115,3 +115,27 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host = 'mi
|
||||||
url: null,
|
url: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function inviteCode(isUsed = false, hasExpiration = false, isExpired = false, isCreatedBySystem = false) {
|
||||||
|
const date = new Date();
|
||||||
|
const createdAt = new Date();
|
||||||
|
createdAt.setDate(date.getDate() - 1)
|
||||||
|
const expiresAt = new Date();
|
||||||
|
|
||||||
|
if (isExpired) {
|
||||||
|
expiresAt.setHours(date.getHours() - 1)
|
||||||
|
} else {
|
||||||
|
expiresAt.setHours(date.getHours() + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: "9gyqzizw77",
|
||||||
|
code: "SLF3JKF7UV2H9",
|
||||||
|
expiresAt: hasExpiration ? expiresAt.toISOString() : null,
|
||||||
|
createdAt: createdAt.toISOString(),
|
||||||
|
createdBy: isCreatedBySystem ? null : userDetailed('8i3rvznx32'),
|
||||||
|
usedBy: isUsed ? userDetailed('3i3r2znx1v') : null,
|
||||||
|
usedAt: isUsed ? date.toISOString() : null,
|
||||||
|
used: isUsed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -403,6 +403,7 @@ function toStories(component: string): Promise<string> {
|
||||||
glob('src/components/MkSignupServerRules.vue'),
|
glob('src/components/MkSignupServerRules.vue'),
|
||||||
glob('src/components/MkUserSetupDialog.vue'),
|
glob('src/components/MkUserSetupDialog.vue'),
|
||||||
glob('src/components/MkUserSetupDialog.*.vue'),
|
glob('src/components/MkUserSetupDialog.*.vue'),
|
||||||
|
glob('src/components/MkInviteCode.vue'),
|
||||||
glob('src/pages/user/home.vue'),
|
glob('src/pages/user/home.vue'),
|
||||||
]);
|
]);
|
||||||
const components = globs.flat();
|
const components = globs.flat();
|
||||||
|
|
|
@ -20,9 +20,9 @@
|
||||||
"@rollup/plugin-replace": "5.0.2",
|
"@rollup/plugin-replace": "5.0.2",
|
||||||
"@rollup/pluginutils": "5.0.2",
|
"@rollup/pluginutils": "5.0.2",
|
||||||
"@syuilo/aiscript": "0.13.3",
|
"@syuilo/aiscript": "0.13.3",
|
||||||
"@tabler/icons-webfont": "2.24.0",
|
"@tabler/icons-webfont": "2.25.0",
|
||||||
"@vitejs/plugin-vue": "4.2.3",
|
"@vitejs/plugin-vue": "4.2.3",
|
||||||
"@vue-macros/reactivity-transform": "0.3.11",
|
"@vue-macros/reactivity-transform": "0.3.14",
|
||||||
"@vue/compiler-sfc": "3.3.4",
|
"@vue/compiler-sfc": "3.3.4",
|
||||||
"astring": "1.8.6",
|
"astring": "1.8.6",
|
||||||
"autosize": "6.0.1",
|
"autosize": "6.0.1",
|
||||||
|
@ -70,30 +70,30 @@
|
||||||
"typescript": "5.1.6",
|
"typescript": "5.1.6",
|
||||||
"uuid": "9.0.0",
|
"uuid": "9.0.0",
|
||||||
"vanilla-tilt": "1.8.0",
|
"vanilla-tilt": "1.8.0",
|
||||||
"vite": "4.4.1",
|
"vite": "4.4.4",
|
||||||
"vue": "3.3.4",
|
"vue": "3.3.4",
|
||||||
"vue-prism-editor": "2.0.0-alpha.2",
|
"vue-prism-editor": "2.0.0-alpha.2",
|
||||||
"vuedraggable": "next"
|
"vuedraggable": "next"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@storybook/addon-actions": "7.0.26",
|
"@storybook/addon-actions": "7.0.27",
|
||||||
"@storybook/addon-essentials": "7.0.26",
|
"@storybook/addon-essentials": "7.0.27",
|
||||||
"@storybook/addon-interactions": "7.0.26",
|
"@storybook/addon-interactions": "7.0.27",
|
||||||
"@storybook/addon-links": "7.0.26",
|
"@storybook/addon-links": "7.0.27",
|
||||||
"@storybook/addon-storysource": "7.0.26",
|
"@storybook/addon-storysource": "7.0.27",
|
||||||
"@storybook/addons": "7.0.26",
|
"@storybook/addons": "7.0.27",
|
||||||
"@storybook/blocks": "7.0.26",
|
"@storybook/blocks": "7.0.27",
|
||||||
"@storybook/core-events": "7.0.26",
|
"@storybook/core-events": "7.0.27",
|
||||||
"@storybook/jest": "0.1.0",
|
"@storybook/jest": "0.1.0",
|
||||||
"@storybook/manager-api": "7.0.26",
|
"@storybook/manager-api": "7.0.27",
|
||||||
"@storybook/preview-api": "7.0.26",
|
"@storybook/preview-api": "7.0.27",
|
||||||
"@storybook/react": "7.0.26",
|
"@storybook/react": "7.0.27",
|
||||||
"@storybook/react-vite": "7.0.26",
|
"@storybook/react-vite": "7.0.27",
|
||||||
"@storybook/testing-library": "0.2.0",
|
"@storybook/testing-library": "0.2.0",
|
||||||
"@storybook/theming": "7.0.26",
|
"@storybook/theming": "7.0.27",
|
||||||
"@storybook/types": "7.0.26",
|
"@storybook/types": "7.0.27",
|
||||||
"@storybook/vue3": "7.0.26",
|
"@storybook/vue3": "7.0.27",
|
||||||
"@storybook/vue3-vite": "7.0.26",
|
"@storybook/vue3-vite": "7.0.27",
|
||||||
"@testing-library/jest-dom": "5.16.5",
|
"@testing-library/jest-dom": "5.16.5",
|
||||||
"@testing-library/vue": "7.0.0",
|
"@testing-library/vue": "7.0.0",
|
||||||
"@types/escape-regexp": "0.0.1",
|
"@types/escape-regexp": "0.0.1",
|
||||||
|
@ -102,10 +102,10 @@
|
||||||
"@types/gulp-rename": "2.0.2",
|
"@types/gulp-rename": "2.0.2",
|
||||||
"@types/matter-js": "0.18.5",
|
"@types/matter-js": "0.18.5",
|
||||||
"@types/micromatch": "4.0.2",
|
"@types/micromatch": "4.0.2",
|
||||||
"@types/node": "20.4.0",
|
"@types/node": "20.4.2",
|
||||||
"@types/punycode": "2.1.0",
|
"@types/punycode": "2.1.0",
|
||||||
"@types/sanitize-html": "2.9.0",
|
"@types/sanitize-html": "2.9.0",
|
||||||
"@types/testing-library__jest-dom": "5.14.7",
|
"@types/testing-library__jest-dom": "5.14.8",
|
||||||
"@types/throttle-debounce": "5.0.0",
|
"@types/throttle-debounce": "5.0.0",
|
||||||
"@types/tinycolor2": "1.4.3",
|
"@types/tinycolor2": "1.4.3",
|
||||||
"@types/uuid": "9.0.2",
|
"@types/uuid": "9.0.2",
|
||||||
|
@ -118,12 +118,12 @@
|
||||||
"acorn": "8.10.0",
|
"acorn": "8.10.0",
|
||||||
"chokidar-cli": "3.0.0",
|
"chokidar-cli": "3.0.0",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"cypress": "12.17.0",
|
"cypress": "12.17.1",
|
||||||
"eslint": "8.44.0",
|
"eslint": "8.45.0",
|
||||||
"eslint-plugin-import": "2.27.5",
|
"eslint-plugin-import": "2.27.5",
|
||||||
"eslint-plugin-vue": "9.15.1",
|
"eslint-plugin-vue": "9.15.1",
|
||||||
"fast-glob": "3.3.0",
|
"fast-glob": "3.3.0",
|
||||||
"happy-dom": "10.0.3",
|
"happy-dom": "10.3.2",
|
||||||
"micromatch": "4.0.5",
|
"micromatch": "4.0.5",
|
||||||
"msw": "1.2.2",
|
"msw": "1.2.2",
|
||||||
"msw-storybook-addon": "1.8.0",
|
"msw-storybook-addon": "1.8.0",
|
||||||
|
@ -131,13 +131,13 @@
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"start-server-and-test": "2.0.0",
|
"start-server-and-test": "2.0.0",
|
||||||
"storybook": "7.0.26",
|
"storybook": "7.0.27",
|
||||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||||
"summaly": "github:misskey-dev/summaly",
|
"summaly": "github:misskey-dev/summaly",
|
||||||
"vite-plugin-turbosnap": "1.0.2",
|
"vite-plugin-turbosnap": "1.0.2",
|
||||||
"vitest": "0.33.0",
|
"vitest": "0.33.0",
|
||||||
"vitest-fetch-mock": "0.2.2",
|
"vitest-fetch-mock": "0.2.2",
|
||||||
"vue-eslint-parser": "9.3.1",
|
"vue-eslint-parser": "9.3.1",
|
||||||
"vue-tsc": "1.8.4"
|
"vue-tsc": "1.8.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||||
|
import { StoryObj } from '@storybook/vue3';
|
||||||
|
import { rest } from 'msw';
|
||||||
|
import { userDetailed, inviteCode } from '../../.storybook/fakes';
|
||||||
|
import { commonHandlers } from '../../.storybook/mocks';
|
||||||
|
import MkInviteCode from './MkInviteCode.vue';
|
||||||
|
|
||||||
|
export const Default = {
|
||||||
|
render(args) {
|
||||||
|
return {
|
||||||
|
components: {
|
||||||
|
MkInviteCode,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
return {
|
||||||
|
args,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
props() {
|
||||||
|
return {
|
||||||
|
...this.args,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
template: '<MkInviteCode v-bind="props" />',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
invite: inviteCode() as any,
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
layout: 'centered',
|
||||||
|
msw: {
|
||||||
|
handlers: [
|
||||||
|
...commonHandlers,
|
||||||
|
rest.post('/api/users/show', (req, res, ctx) => {
|
||||||
|
return res(ctx.json(userDetailed(req.params.userId as string)));
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
decorators: [() => ({
|
||||||
|
template: '<div style="width:100cqmin"><story/></div>',
|
||||||
|
})],
|
||||||
|
} satisfies StoryObj<typeof MkInviteCode>;
|
||||||
|
|
||||||
|
export const Used = {
|
||||||
|
...Default,
|
||||||
|
args: {
|
||||||
|
invite: inviteCode(true) as any
|
||||||
|
},
|
||||||
|
} satisfies StoryObj<typeof MkInviteCode>;
|
||||||
|
|
||||||
|
export const Expired = {
|
||||||
|
...Default,
|
||||||
|
args: {
|
||||||
|
invite: inviteCode(false, true, true) as any
|
||||||
|
},
|
||||||
|
} satisfies StoryObj<typeof MkInviteCode>;
|
|
@ -0,0 +1,123 @@
|
||||||
|
<template>
|
||||||
|
<MkFolder>
|
||||||
|
<template #label>{{ invite.code }}</template>
|
||||||
|
<template #suffix>
|
||||||
|
<span v-if="invite.used">{{ i18n.ts.used }}</span>
|
||||||
|
<span v-else-if="isExpired" style="color: var(--error)">{{ i18n.ts.expired }}</span>
|
||||||
|
<span v-else style="color: var(--success)">{{ i18n.ts.unused }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="_gaps_s" :class="$style.root">
|
||||||
|
<div :class="$style.items">
|
||||||
|
<div>
|
||||||
|
<div :class="$style.label">{{ i18n.ts.invitationCode }}</div>
|
||||||
|
<div>{{ invite.code }}</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="moderator">
|
||||||
|
<div :class="$style.label">{{ i18n.ts.inviteCodeCreator }}</div>
|
||||||
|
<div v-if="invite.createdBy" :class="$style.user">
|
||||||
|
<MkAvatar :user="invite.createdBy" :class="$style.avatar" link preview/>
|
||||||
|
<MkUserName :user="invite.createdBy" :nowrap="false"/>
|
||||||
|
<div v-if="moderator">({{ invite.createdBy.id }})</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>system</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="invite.used">
|
||||||
|
<div :class="$style.label">{{ i18n.ts.registeredUserUsingInviteCode }}</div>
|
||||||
|
<div v-if="invite.usedBy" :class="$style.user">
|
||||||
|
<MkAvatar :user="invite.usedBy" :class="$style.avatar" link preview/>
|
||||||
|
<MkUserName :user="invite.usedBy" :nowrap="false"/>
|
||||||
|
<div v-if="moderator">({{ invite.usedBy.id }})</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>{{ i18n.ts.unknown }} ({{ i18n.ts.waitingForMailAuth }})</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="invite.expiresAt && !invite.used">
|
||||||
|
<div :class="$style.label">{{ i18n.ts.expirationDate }}</div>
|
||||||
|
<div><MkTime :time="invite.expiresAt" mode="absolute"/></div>
|
||||||
|
</div>
|
||||||
|
<div v-if="invite.usedAt">
|
||||||
|
<div :class="$style.label">{{ i18n.ts.inviteCodeUsedAt }}</div>
|
||||||
|
<div><MkTime :time="invite.usedAt" mode="absolute"/></div>
|
||||||
|
</div>
|
||||||
|
<div v-if="moderator">
|
||||||
|
<div :class="$style.label">{{ i18n.ts.createdAt }}</div>
|
||||||
|
<div><MkTime :time="invite.createdAt" mode="absolute"/></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.buttons">
|
||||||
|
<MkButton v-if="!invite.used && !isExpired" primary rounded @click="copyInviteCode()">{{ i18n.ts.copy }}</MkButton>
|
||||||
|
<MkButton v-if="!invite.used || moderator" danger rounded @click="deleteCode()">{{ i18n.ts.delete }}</MkButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MkFolder>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import * as misskey from 'misskey-js';
|
||||||
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
import * as os from '@/os';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
invite: misskey.entities.Invite;
|
||||||
|
moderator?: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emits = defineEmits<{
|
||||||
|
(event: 'deleted', value: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const isExpired = computed(() => {
|
||||||
|
return props.invite.expiresAt && new Date(props.invite.expiresAt) < new Date();
|
||||||
|
});
|
||||||
|
|
||||||
|
function deleteCode() {
|
||||||
|
os.apiWithDialog('invite/delete', {
|
||||||
|
inviteId: props.invite.id,
|
||||||
|
});
|
||||||
|
emits('deleted', props.invite.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyInviteCode() {
|
||||||
|
copyToClipboard(props.invite.code);
|
||||||
|
os.success();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.root {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||||
|
grid-gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 0.85em;
|
||||||
|
padding: 0 0 8px 0;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
--height: 24px;
|
||||||
|
width: var(--height);
|
||||||
|
height: var(--height);
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -57,6 +57,9 @@ export const ROLE_POLICIES = [
|
||||||
'ltlAvailable',
|
'ltlAvailable',
|
||||||
'canPublicNote',
|
'canPublicNote',
|
||||||
'canInvite',
|
'canInvite',
|
||||||
|
'inviteLimit',
|
||||||
|
'inviteLimitCycle',
|
||||||
|
'inviteExpirationTime',
|
||||||
'canManageCustomEmojis',
|
'canManageCustomEmojis',
|
||||||
'canSearchNotes',
|
'canSearchNotes',
|
||||||
'canHideAds',
|
'canHideAds',
|
||||||
|
|
|
@ -80,7 +80,7 @@ const menuDef = $computed(() => [{
|
||||||
}, ...(instance.disableRegistration ? [{
|
}, ...(instance.disableRegistration ? [{
|
||||||
type: 'button',
|
type: 'button',
|
||||||
icon: 'ti ti-user-plus',
|
icon: 'ti ti-user-plus',
|
||||||
text: i18n.ts.invite,
|
text: i18n.ts.createInviteCode,
|
||||||
action: invite,
|
action: invite,
|
||||||
}] : [])],
|
}] : [])],
|
||||||
}, {
|
}, {
|
||||||
|
@ -95,6 +95,11 @@ const menuDef = $computed(() => [{
|
||||||
text: i18n.ts.users,
|
text: i18n.ts.users,
|
||||||
to: '/admin/users',
|
to: '/admin/users',
|
||||||
active: currentPage?.route.name === 'users',
|
active: currentPage?.route.name === 'users',
|
||||||
|
}, {
|
||||||
|
icon: 'ti ti-user-plus',
|
||||||
|
text: i18n.ts.invite,
|
||||||
|
to: '/admin/invites',
|
||||||
|
active: currentPage?.route.name === 'invites',
|
||||||
}, {
|
}, {
|
||||||
icon: 'ti ti-badges',
|
icon: 'ti ti-badges',
|
||||||
text: i18n.ts.roles,
|
text: i18n.ts.roles,
|
||||||
|
@ -240,10 +245,10 @@ provideMetadataReceiver((info) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const invite = () => {
|
const invite = () => {
|
||||||
os.api('invite').then(x => {
|
os.api('admin/invite/create').then(x => {
|
||||||
os.alert({
|
os.alert({
|
||||||
type: 'info',
|
type: 'info',
|
||||||
text: x.code,
|
text: x?.[0].code,
|
||||||
});
|
});
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
os.alert({
|
os.alert({
|
||||||
|
|
|
@ -0,0 +1,126 @@
|
||||||
|
<template>
|
||||||
|
<MkStickyContainer>
|
||||||
|
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||||
|
<MkSpacer :contentMax="800">
|
||||||
|
<div class="_gaps_m">
|
||||||
|
<MkFolder :expanded="false">
|
||||||
|
<template #icon><i class="ti ti-plus"></i></template>
|
||||||
|
<template #label>{{ i18n.ts.createInviteCode }}</template>
|
||||||
|
|
||||||
|
<div class="_gaps_m">
|
||||||
|
<MkSwitch v-model="noExpirationDate">
|
||||||
|
<template #label>{{ i18n.ts.noExpirationDate }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
<MkInput v-if="!noExpirationDate" v-model="expiresAt" type="datetime-local">
|
||||||
|
<template #label>{{ i18n.ts.expirationDate }}</template>
|
||||||
|
</MkInput>
|
||||||
|
<MkInput v-model="createCount" type="number">
|
||||||
|
<template #label>{{ i18n.ts.createCount }}</template>
|
||||||
|
</MkInput>
|
||||||
|
<MkButton primary rounded @click="createWithOptions">{{ i18n.ts.create }}</MkButton>
|
||||||
|
</div>
|
||||||
|
</MkFolder>
|
||||||
|
|
||||||
|
<div :class="$style.inputs">
|
||||||
|
<MkSelect v-model="type" :class="$style.input">
|
||||||
|
<template #label>{{ i18n.ts.state }}</template>
|
||||||
|
<option value="all">{{ i18n.ts.all }}</option>
|
||||||
|
<option value="unused">{{ i18n.ts.unused }}</option>
|
||||||
|
<option value="used">{{ i18n.ts.used }}</option>
|
||||||
|
<option value="expired">{{ i18n.ts.expired }}</option>
|
||||||
|
</MkSelect>
|
||||||
|
<MkSelect v-model="sort" :class="$style.input">
|
||||||
|
<template #label>{{ i18n.ts.sort }}</template>
|
||||||
|
<option value="+createdAt">{{ i18n.ts.createdAt }} ({{ i18n.ts.ascendingOrder }})</option>
|
||||||
|
<option value="-createdAt">{{ i18n.ts.createdAt }} ({{ i18n.ts.descendingOrder }})</option>
|
||||||
|
<option value="+usedAt">{{ i18n.ts.usedAt }} ({{ i18n.ts.ascendingOrder }})</option>
|
||||||
|
<option value="-usedAt">{{ i18n.ts.usedAt }} ({{ i18n.ts.descendingOrder }})</option>
|
||||||
|
</MkSelect>
|
||||||
|
</div>
|
||||||
|
<MkPagination ref="pagingComponent" :pagination="pagination">
|
||||||
|
<template #default="{ items }">
|
||||||
|
<div class="_gaps_s">
|
||||||
|
<MkInviteCode v-for="item in items" :key="item.id" :invite="(item as any)" :onDeleted="deleted" moderator/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</MkPagination>
|
||||||
|
</div>
|
||||||
|
</MkSpacer>
|
||||||
|
</MkStickyContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref, shallowRef } from 'vue';
|
||||||
|
import XHeader from './_header_.vue';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
|
import MkSelect from '@/components/MkSelect.vue';
|
||||||
|
import MkInput from '@/components/MkInput.vue';
|
||||||
|
import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
|
import MkPagination, { Paging } from '@/components/MkPagination.vue';
|
||||||
|
import MkInviteCode from '@/components/MkInviteCode.vue';
|
||||||
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
|
|
||||||
|
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
|
||||||
|
|
||||||
|
let type = ref('all');
|
||||||
|
let sort = ref('+createdAt');
|
||||||
|
|
||||||
|
const pagination: Paging = {
|
||||||
|
endpoint: 'admin/invite/list' as const,
|
||||||
|
limit: 10,
|
||||||
|
params: computed(() => ({
|
||||||
|
type: type.value,
|
||||||
|
sort: sort.value,
|
||||||
|
})),
|
||||||
|
offsetMode: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const expiresAt = ref('');
|
||||||
|
const noExpirationDate = ref(true);
|
||||||
|
const createCount = ref(1);
|
||||||
|
|
||||||
|
async function createWithOptions() {
|
||||||
|
const options = {
|
||||||
|
expiresAt: noExpirationDate.value ? null : expiresAt.value,
|
||||||
|
count: createCount.value,
|
||||||
|
};
|
||||||
|
|
||||||
|
const tickets = await os.api('admin/invite/create', options);
|
||||||
|
os.alert({
|
||||||
|
type: 'success',
|
||||||
|
title: i18n.ts.inviteCodeCreated,
|
||||||
|
text: tickets?.map(x => x.code).join('\n'),
|
||||||
|
});
|
||||||
|
|
||||||
|
tickets?.forEach(ticket => pagingComponent.value?.prepend(ticket));
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleted(id: string) {
|
||||||
|
if (pagingComponent.value) {
|
||||||
|
pagingComponent.value.items.delete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const headerActions = $computed(() => []);
|
||||||
|
const headerTabs = $computed(() => []);
|
||||||
|
|
||||||
|
definePageMetadata({
|
||||||
|
title: i18n.ts.invite,
|
||||||
|
icon: 'ti ti-user-plus',
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.inputs {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -171,6 +171,65 @@
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.inviteLimit, 'inviteLimit'])">
|
||||||
|
<template #label>{{ i18n.ts._role._options.inviteLimit }}</template>
|
||||||
|
<template #suffix>
|
||||||
|
<span v-if="role.policies.inviteLimit.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||||
|
<span v-else>{{ role.policies.inviteLimit.value }}</span>
|
||||||
|
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.inviteLimit)"></i></span>
|
||||||
|
</template>
|
||||||
|
<div class="_gaps">
|
||||||
|
<MkSwitch v-model="role.policies.inviteLimit.useDefault" :readonly="readonly">
|
||||||
|
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
<MkInput v-model="role.policies.inviteLimit.value" :disabled="role.policies.inviteLimit.useDefault" type="number" :readonly="readonly">
|
||||||
|
</MkInput>
|
||||||
|
<MkRange v-model="role.policies.inviteLimit.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||||
|
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||||
|
</MkRange>
|
||||||
|
</div>
|
||||||
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.inviteLimitCycle, 'inviteLimitCycle'])">
|
||||||
|
<template #label>{{ i18n.ts._role._options.inviteLimitCycle }}</template>
|
||||||
|
<template #suffix>
|
||||||
|
<span v-if="role.policies.inviteLimitCycle.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||||
|
<span v-else>{{ role.policies.inviteLimitCycle.value + i18n.ts._time.minute }}</span>
|
||||||
|
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.inviteLimitCycle)"></i></span>
|
||||||
|
</template>
|
||||||
|
<div class="_gaps">
|
||||||
|
<MkSwitch v-model="role.policies.inviteLimitCycle.useDefault" :readonly="readonly">
|
||||||
|
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
<MkInput v-model="role.policies.inviteLimitCycle.value" :disabled="role.policies.inviteLimitCycle.useDefault" type="number" :readonly="readonly">
|
||||||
|
<template #suffix>{{ i18n.ts._time.minute }}</template>
|
||||||
|
</MkInput>
|
||||||
|
<MkRange v-model="role.policies.inviteLimitCycle.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||||
|
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||||
|
</MkRange>
|
||||||
|
</div>
|
||||||
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.inviteExpirationTime, 'inviteExpirationTime'])">
|
||||||
|
<template #label>{{ i18n.ts._role._options.inviteExpirationTime }}</template>
|
||||||
|
<template #suffix>
|
||||||
|
<span v-if="role.policies.inviteExpirationTime.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
|
||||||
|
<span v-else>{{ role.policies.inviteExpirationTime.value + i18n.ts._time.minute }}</span>
|
||||||
|
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.inviteExpirationTime)"></i></span>
|
||||||
|
</template>
|
||||||
|
<div class="_gaps">
|
||||||
|
<MkSwitch v-model="role.policies.inviteExpirationTime.useDefault" :readonly="readonly">
|
||||||
|
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
<MkInput v-model="role.policies.inviteExpirationTime.value" :disabled="role.policies.inviteExpirationTime.useDefault" type="number" :readonly="readonly">
|
||||||
|
<template #suffix>{{ i18n.ts._time.minute }}</template>
|
||||||
|
</MkInput>
|
||||||
|
<MkRange v-model="role.policies.inviteExpirationTime.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
|
||||||
|
<template #label>{{ i18n.ts._role.priority }}</template>
|
||||||
|
</MkRange>
|
||||||
|
</div>
|
||||||
|
</MkFolder>
|
||||||
|
|
||||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canManageCustomEmojis, 'canManageCustomEmojis'])">
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.canManageCustomEmojis, 'canManageCustomEmojis'])">
|
||||||
<template #label>{{ i18n.ts._role._options.canManageCustomEmojis }}</template>
|
<template #label>{{ i18n.ts._role._options.canManageCustomEmojis }}</template>
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
|
|
|
@ -51,6 +51,29 @@
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.inviteLimit, 'inviteLimit'])">
|
||||||
|
<template #label>{{ i18n.ts._role._options.inviteLimit }}</template>
|
||||||
|
<template #suffix>{{ policies.inviteLimit }}</template>
|
||||||
|
<MkInput v-model="policies.inviteLimit" type="number">
|
||||||
|
</MkInput>
|
||||||
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.inviteLimitCycle, 'inviteLimitCycle'])">
|
||||||
|
<template #label>{{ i18n.ts._role._options.inviteLimitCycle }}</template>
|
||||||
|
<template #suffix>{{ policies.inviteLimitCycle + i18n.ts._time.minute }}</template>
|
||||||
|
<MkInput v-model="policies.inviteLimitCycle" type="number">
|
||||||
|
<template #suffix>{{ i18n.ts._time.minute }}</template>
|
||||||
|
</MkInput>
|
||||||
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.inviteExpirationTime, 'inviteExpirationTime'])">
|
||||||
|
<template #label>{{ i18n.ts._role._options.inviteExpirationTime }}</template>
|
||||||
|
<template #suffix>{{ policies.inviteExpirationTime + i18n.ts._time.minute }}</template>
|
||||||
|
<MkInput v-model="policies.inviteExpirationTime" type="number">
|
||||||
|
<template #suffix>{{ i18n.ts._time.minute }}</template>
|
||||||
|
</MkInput>
|
||||||
|
</MkFolder>
|
||||||
|
|
||||||
<MkFolder v-if="matchQuery([i18n.ts._role._options.canManageCustomEmojis, 'canManageCustomEmojis'])">
|
<MkFolder v-if="matchQuery([i18n.ts._role._options.canManageCustomEmojis, 'canManageCustomEmojis'])">
|
||||||
<template #label>{{ i18n.ts._role._options.canManageCustomEmojis }}</template>
|
<template #label>{{ i18n.ts._role._options.canManageCustomEmojis }}</template>
|
||||||
<template #suffix>{{ policies.canManageCustomEmojis ? i18n.ts.yes : i18n.ts.no }}</template>
|
<template #suffix>{{ policies.canManageCustomEmojis ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||||
|
|
|
@ -37,6 +37,13 @@
|
||||||
<template #label>{{ i18n.ts.cacheRemoteFiles }}</template>
|
<template #label>{{ i18n.ts.cacheRemoteFiles }}</template>
|
||||||
<template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}</template>
|
<template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}</template>
|
||||||
</MkSwitch>
|
</MkSwitch>
|
||||||
|
|
||||||
|
<template v-if="cacheRemoteFiles">
|
||||||
|
<MkSwitch v-model="cacheRemoteSensitiveFiles">
|
||||||
|
<template #label>{{ i18n.ts.cacheRemoteSensitiveFiles }}</template>
|
||||||
|
<template #caption>{{ i18n.ts.cacheRemoteSensitiveFilesDescription }}</template>
|
||||||
|
</MkSwitch>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
|
||||||
|
@ -104,7 +111,6 @@ import { fetchInstance } from '@/instance';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkColorInput from '@/components/MkColorInput.vue';
|
|
||||||
|
|
||||||
let name: string | null = $ref(null);
|
let name: string | null = $ref(null);
|
||||||
let description: string | null = $ref(null);
|
let description: string | null = $ref(null);
|
||||||
|
@ -112,13 +118,14 @@ let maintainerName: string | null = $ref(null);
|
||||||
let maintainerEmail: string | null = $ref(null);
|
let maintainerEmail: string | null = $ref(null);
|
||||||
let pinnedUsers: string = $ref('');
|
let pinnedUsers: string = $ref('');
|
||||||
let cacheRemoteFiles: boolean = $ref(false);
|
let cacheRemoteFiles: boolean = $ref(false);
|
||||||
|
let cacheRemoteSensitiveFiles: boolean = $ref(false);
|
||||||
let enableServiceWorker: boolean = $ref(false);
|
let enableServiceWorker: boolean = $ref(false);
|
||||||
let swPublicKey: any = $ref(null);
|
let swPublicKey: any = $ref(null);
|
||||||
let swPrivateKey: any = $ref(null);
|
let swPrivateKey: any = $ref(null);
|
||||||
let deeplAuthKey: string = $ref('');
|
let deeplAuthKey: string = $ref('');
|
||||||
let deeplIsPro: boolean = $ref(false);
|
let deeplIsPro: boolean = $ref(false);
|
||||||
|
|
||||||
async function init() {
|
async function init(): Promise<void> {
|
||||||
const meta = await os.api('admin/meta');
|
const meta = await os.api('admin/meta');
|
||||||
name = meta.name;
|
name = meta.name;
|
||||||
description = meta.description;
|
description = meta.description;
|
||||||
|
@ -126,6 +133,7 @@ async function init() {
|
||||||
maintainerEmail = meta.maintainerEmail;
|
maintainerEmail = meta.maintainerEmail;
|
||||||
pinnedUsers = meta.pinnedUsers.join('\n');
|
pinnedUsers = meta.pinnedUsers.join('\n');
|
||||||
cacheRemoteFiles = meta.cacheRemoteFiles;
|
cacheRemoteFiles = meta.cacheRemoteFiles;
|
||||||
|
cacheRemoteSensitiveFiles = meta.cacheRemoteSensitiveFiles;
|
||||||
enableServiceWorker = meta.enableServiceWorker;
|
enableServiceWorker = meta.enableServiceWorker;
|
||||||
swPublicKey = meta.swPublickey;
|
swPublicKey = meta.swPublickey;
|
||||||
swPrivateKey = meta.swPrivateKey;
|
swPrivateKey = meta.swPrivateKey;
|
||||||
|
@ -133,7 +141,7 @@ async function init() {
|
||||||
deeplIsPro = meta.deeplIsPro;
|
deeplIsPro = meta.deeplIsPro;
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
function save(): void {
|
||||||
os.apiWithDialog('admin/update-meta', {
|
os.apiWithDialog('admin/update-meta', {
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
|
@ -141,6 +149,7 @@ function save() {
|
||||||
maintainerEmail,
|
maintainerEmail,
|
||||||
pinnedUsers: pinnedUsers.split('\n'),
|
pinnedUsers: pinnedUsers.split('\n'),
|
||||||
cacheRemoteFiles,
|
cacheRemoteFiles,
|
||||||
|
cacheRemoteSensitiveFiles,
|
||||||
enableServiceWorker,
|
enableServiceWorker,
|
||||||
swPublicKey,
|
swPublicKey,
|
||||||
swPrivateKey,
|
swPrivateKey,
|
||||||
|
|
|
@ -0,0 +1,114 @@
|
||||||
|
<template>
|
||||||
|
<MkStickyContainer>
|
||||||
|
<template #header>
|
||||||
|
<MkPageHeader/>
|
||||||
|
</template>
|
||||||
|
<MKSpacer v-if="!instance.disableRegistration || !($i && ($i.isAdmin || $i.policies.canInvite))" :contentMax="1200">
|
||||||
|
<div :class="$style.root">
|
||||||
|
<img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/>
|
||||||
|
<div :class="$style.text">
|
||||||
|
<i class="ti ti-alert-triangle"></i>
|
||||||
|
{{ i18n.ts.nothing }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MKSpacer>
|
||||||
|
<MkSpacer v-else :contentMax="800">
|
||||||
|
<div class="_gaps_m" style="text-align: center;">
|
||||||
|
<div v-if="resetCycle && inviteLimit">{{ i18n.t('inviteLimitResetCycle', { time: resetCycle, limit: inviteLimit }) }}</div>
|
||||||
|
<MkButton inline primary rounded :disabled="currentInviteLimit !== null && currentInviteLimit <= 0" @click="create"><i class="ti ti-user-plus"></i> {{ i18n.ts.createInviteCode }}</MkButton>
|
||||||
|
<div v-if="currentInviteLimit !== null">{{ i18n.t('createLimitRemaining', { limit: currentInviteLimit }) }}</div>
|
||||||
|
|
||||||
|
<MkPagination ref="pagingComponent" :pagination="pagination">
|
||||||
|
<template #default="{ items }">
|
||||||
|
<div class="_gaps_s">
|
||||||
|
<MkInviteCode v-for="item in (items as Invite[])" :key="item.id" :invite="item" :onDeleted="deleted"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</MkPagination>
|
||||||
|
</div>
|
||||||
|
</MkSpacer>
|
||||||
|
</MkStickyContainer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref, shallowRef } from 'vue';
|
||||||
|
import type { Invite } from 'misskey-js/built/entities';
|
||||||
|
import { i18n } from '@/i18n';
|
||||||
|
import * as os from '@/os';
|
||||||
|
import MkButton from '@/components/MkButton.vue';
|
||||||
|
import MkPagination, { Paging } from '@/components/MkPagination.vue';
|
||||||
|
import MkInviteCode from '@/components/MkInviteCode.vue';
|
||||||
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
|
import { serverErrorImageUrl, instance } from '@/instance';
|
||||||
|
import { $i } from '@/account';
|
||||||
|
|
||||||
|
const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
|
||||||
|
const currentInviteLimit = ref<null | number>(null);
|
||||||
|
const inviteLimit = (($i != null && $i.policies.inviteLimit) || (($i == null && instance.policies.inviteLimit))) as number;
|
||||||
|
const inviteLimitCycle = (($i != null && $i.policies.inviteLimitCycle) || ($i == null && instance.policies.inviteLimitCycle)) as number;
|
||||||
|
|
||||||
|
const pagination: Paging = {
|
||||||
|
endpoint: 'invite/list' as const,
|
||||||
|
limit: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetCycle = computed<null | string>(() => {
|
||||||
|
if (!inviteLimitCycle) return null;
|
||||||
|
|
||||||
|
const minutes = inviteLimitCycle;
|
||||||
|
if (minutes < 60) return minutes + i18n.ts._time.minute;
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
if (hours < 24) return hours + i18n.ts._time.hour;
|
||||||
|
return Math.floor(hours / 24) + i18n.ts._time.day;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function create() {
|
||||||
|
const ticket = await os.api('invite/create');
|
||||||
|
os.alert({
|
||||||
|
type: 'success',
|
||||||
|
title: i18n.ts.inviteCodeCreated,
|
||||||
|
text: ticket.code,
|
||||||
|
});
|
||||||
|
|
||||||
|
pagingComponent.value?.prepend(ticket);
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleted(id: string) {
|
||||||
|
if (pagingComponent.value) {
|
||||||
|
pagingComponent.value.items.delete(id);
|
||||||
|
}
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function update() {
|
||||||
|
currentInviteLimit.value = (await os.api('invite/limit')).remaining;
|
||||||
|
}
|
||||||
|
|
||||||
|
update();
|
||||||
|
|
||||||
|
definePageMetadata({
|
||||||
|
title: i18n.ts.invite,
|
||||||
|
icon: 'ti ti-user-plus',
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.root {
|
||||||
|
padding: 32px;
|
||||||
|
text-align: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img {
|
||||||
|
vertical-align: bottom;
|
||||||
|
width: 128px;
|
||||||
|
height: 128px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -14,7 +14,7 @@
|
||||||
|
|
||||||
<div v-if="items.length > 0" class="_gaps">
|
<div v-if="items.length > 0" class="_gaps">
|
||||||
<MkA v-for="list in items" :key="list.id" class="_panel" :class="$style.list" :to="`/my/lists/${ list.id }`">
|
<MkA v-for="list in items" :key="list.id" class="_panel" :class="$style.list" :to="`/my/lists/${ list.id }`">
|
||||||
<div style="margin-bottom: 4px;">{{ list.name }}</div>
|
<div style="margin-bottom: 4px;">{{ list.name }} <span :class="$style.nUsers">({{ i18n.t('nUsers', { n: `${list.userIds.length}/${$i?.policies['userEachUserListsLimit']}` }) }})</span></div>
|
||||||
<MkAvatars :userIds="list.userIds" :limit="10"/>
|
<MkAvatars :userIds="list.userIds" :limit="10"/>
|
||||||
</MkA>
|
</MkA>
|
||||||
</div>
|
</div>
|
||||||
|
@ -32,6 +32,7 @@ import { i18n } from '@/i18n';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
import { userListsCache } from '@/cache';
|
import { userListsCache } from '@/cache';
|
||||||
import { infoImageUrl } from '@/instance';
|
import { infoImageUrl } from '@/instance';
|
||||||
|
import { $i } from '@/account';
|
||||||
|
|
||||||
const items = $computed(() => userListsCache.value.value ?? []);
|
const items = $computed(() => userListsCache.value.value ?? []);
|
||||||
|
|
||||||
|
@ -66,10 +67,6 @@ const headerTabs = $computed(() => []);
|
||||||
definePageMetadata({
|
definePageMetadata({
|
||||||
title: i18n.ts.manageLists,
|
title: i18n.ts.manageLists,
|
||||||
icon: 'ti ti-list',
|
icon: 'ti ti-list',
|
||||||
action: {
|
|
||||||
icon: 'ti ti-plus',
|
|
||||||
handler: create,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onActivated(() => {
|
onActivated(() => {
|
||||||
|
@ -90,4 +87,9 @@ onActivated(() => {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nUsers {
|
||||||
|
font-size: .9em;
|
||||||
|
opacity: .7;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
|
|
||||||
<MkFolder defaultOpen>
|
<MkFolder defaultOpen>
|
||||||
<template #label>{{ i18n.ts.members }}</template>
|
<template #label>{{ i18n.ts.members }}</template>
|
||||||
|
<template #caption>{{ i18n.t('nUsers', { n: `${list.userIds.length}/${$i?.policies['userEachUserListsLimit']}` }) }}</template>
|
||||||
|
|
||||||
<div class="_gaps_s">
|
<div class="_gaps_s">
|
||||||
<MkButton rounded primary style="margin: 0 auto;" @click="addUser()">{{ i18n.ts.addUser }}</MkButton>
|
<MkButton rounded primary style="margin: 0 auto;" @click="addUser()">{{ i18n.ts.addUser }}</MkButton>
|
||||||
|
@ -29,6 +30,10 @@
|
||||||
</MkA>
|
</MkA>
|
||||||
<button class="_button" :class="$style.remove" @click="removeUser(user, $event)"><i class="ti ti-x"></i></button>
|
<button class="_button" :class="$style.remove" @click="removeUser(user, $event)"><i class="ti ti-x"></i></button>
|
||||||
</div>
|
</div>
|
||||||
|
<MkButton v-if="!fetching && queueUserIds.length !== 0" v-appear="enableInfiniteScroll ? fetchMoreUsers : null" :class="$style.more" :style="{ cursor: 'pointer' }" primary rounded @click="fetchMoreUsers">
|
||||||
|
{{ i18n.ts.loadMore }}
|
||||||
|
</MkButton>
|
||||||
|
<MkLoading v-if="fetching" class="loading"/>
|
||||||
</div>
|
</div>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
</div>
|
</div>
|
||||||
|
@ -49,34 +54,57 @@ import MkSwitch from '@/components/MkSwitch.vue';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
import MkInput from '@/components/MkInput.vue';
|
import MkInput from '@/components/MkInput.vue';
|
||||||
import { userListsCache } from '@/cache';
|
import { userListsCache } from '@/cache';
|
||||||
|
import { UserList, UserLite } from 'misskey-js/built/entities';
|
||||||
|
import { $i } from '@/account';
|
||||||
|
import { defaultStore } from '@/store';
|
||||||
|
const {
|
||||||
|
enableInfiniteScroll,
|
||||||
|
} = defaultStore.reactiveState;
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
listId: string;
|
listId: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let list = $ref(null);
|
const FETCH_USERS_LIMIT = 20;
|
||||||
let users = $ref([]);
|
|
||||||
|
let list = $ref<UserList | null>(null);
|
||||||
|
let users = $ref<UserLite[]>([]);
|
||||||
|
let queueUserIds = $ref<string[]>([]);
|
||||||
|
let fetching = $ref(true);
|
||||||
const isPublic = ref(false);
|
const isPublic = ref(false);
|
||||||
const name = ref('');
|
const name = ref('');
|
||||||
|
|
||||||
function fetchList() {
|
function fetchList() {
|
||||||
|
fetching = true;
|
||||||
os.api('users/lists/show', {
|
os.api('users/lists/show', {
|
||||||
listId: props.listId,
|
listId: props.listId,
|
||||||
}).then(_list => {
|
}).then(_list => {
|
||||||
list = _list;
|
list = _list;
|
||||||
name.value = list.name;
|
name.value = list.name;
|
||||||
isPublic.value = list.isPublic;
|
isPublic.value = list.isPublic;
|
||||||
|
queueUserIds = list.userIds;
|
||||||
|
|
||||||
os.api('users/show', {
|
return fetchMoreUsers();
|
||||||
userIds: list.userIds,
|
});
|
||||||
}).then(_users => {
|
}
|
||||||
users = _users;
|
|
||||||
});
|
function fetchMoreUsers() {
|
||||||
|
if (!list) return;
|
||||||
|
if (fetching && users.length !== 0) return; // fetchingがtrueならやめるが、usersが空なら続行
|
||||||
|
fetching = true;
|
||||||
|
os.api('users/show', {
|
||||||
|
userIds: queueUserIds.slice(0, FETCH_USERS_LIMIT),
|
||||||
|
}).then(_users => {
|
||||||
|
users = users.concat(_users);
|
||||||
|
queueUserIds = queueUserIds.slice(FETCH_USERS_LIMIT);
|
||||||
|
}).finally(() => {
|
||||||
|
fetching = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function addUser() {
|
function addUser() {
|
||||||
os.selectUser().then(user => {
|
os.selectUser().then(user => {
|
||||||
|
if (!list) return;
|
||||||
os.apiWithDialog('users/lists/push', {
|
os.apiWithDialog('users/lists/push', {
|
||||||
listId: list.id,
|
listId: list.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
@ -92,6 +120,7 @@ async function removeUser(user, ev) {
|
||||||
icon: 'ti ti-x',
|
icon: 'ti ti-x',
|
||||||
danger: true,
|
danger: true,
|
||||||
action: async () => {
|
action: async () => {
|
||||||
|
if (!list) return;
|
||||||
os.api('users/lists/pull', {
|
os.api('users/lists/pull', {
|
||||||
listId: list.id,
|
listId: list.id,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
|
@ -103,6 +132,7 @@ async function removeUser(user, ev) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteList() {
|
async function deleteList() {
|
||||||
|
if (!list) return;
|
||||||
const { canceled } = await os.confirm({
|
const { canceled } = await os.confirm({
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
text: i18n.t('removeAreYouSure', { x: list.name }),
|
text: i18n.t('removeAreYouSure', { x: list.name }),
|
||||||
|
@ -117,6 +147,7 @@ async function deleteList() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateSettings() {
|
async function updateSettings() {
|
||||||
|
if (!list) return;
|
||||||
await os.apiWithDialog('users/lists/update', {
|
await os.apiWithDialog('users/lists/update', {
|
||||||
listId: list.id,
|
listId: list.id,
|
||||||
name: name.value,
|
name: name.value,
|
||||||
|
@ -166,6 +197,11 @@ definePageMetadata(computed(() => list ? {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.more {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||||
backdrop-filter: var(--blur, blur(15px));
|
backdrop-filter: var(--blur, blur(15px));
|
||||||
|
|
|
@ -201,6 +201,10 @@ export const routes = [{
|
||||||
}, {
|
}, {
|
||||||
path: '/about-misskey',
|
path: '/about-misskey',
|
||||||
component: page(() => import('./pages/about-misskey.vue')),
|
component: page(() => import('./pages/about-misskey.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/invite',
|
||||||
|
name: 'invite',
|
||||||
|
component: page(() => import('./pages/invite.vue')),
|
||||||
}, {
|
}, {
|
||||||
path: '/ads',
|
path: '/ads',
|
||||||
component: page(() => import('./pages/ads.vue')),
|
component: page(() => import('./pages/ads.vue')),
|
||||||
|
@ -428,6 +432,10 @@ export const routes = [{
|
||||||
path: '/server-rules',
|
path: '/server-rules',
|
||||||
name: 'server-rules',
|
name: 'server-rules',
|
||||||
component: page(() => import('./pages/admin/server-rules.vue')),
|
component: page(() => import('./pages/admin/server-rules.vue')),
|
||||||
|
}, {
|
||||||
|
path: '/invites',
|
||||||
|
name: 'invites',
|
||||||
|
component: page(() => import('./pages/admin/invites.vue')),
|
||||||
}, {
|
}, {
|
||||||
path: '/',
|
path: '/',
|
||||||
component: page(() => import('./pages/_empty_.vue')),
|
component: page(() => import('./pages/_empty_.vue')),
|
||||||
|
|
|
@ -132,9 +132,7 @@ export function play(type: 'noteMy' | 'note' | 'antenna' | 'channel' | 'notifica
|
||||||
}
|
}
|
||||||
|
|
||||||
export function playFile(file: string, volume: number) {
|
export function playFile(file: string, volume: number) {
|
||||||
const masterVolume = soundConfigStore.state.sound_masterVolume;
|
|
||||||
if (masterVolume === 0) return;
|
|
||||||
|
|
||||||
const audio = setVolume(getAudio(file), volume);
|
const audio = setVolume(getAudio(file), volume);
|
||||||
|
if (audio.volume === 0) return;
|
||||||
audio.play();
|
audio.play();
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,12 @@ export function openInstanceMenu(ev: MouseEvent) {
|
||||||
text: i18n.ts.ads,
|
text: i18n.ts.ads,
|
||||||
icon: 'ti ti-ad',
|
icon: 'ti ti-ad',
|
||||||
to: '/ads',
|
to: '/ads',
|
||||||
}, {
|
}, ($i && ($i.isAdmin || $i.policies.canInvite) && instance.disableRegistration) ? {
|
||||||
|
type: 'link',
|
||||||
|
to: '/invite',
|
||||||
|
text: i18n.ts.invite,
|
||||||
|
icon: 'ti ti-user-plus',
|
||||||
|
} : undefined, {
|
||||||
type: 'parent',
|
type: 'parent',
|
||||||
text: i18n.ts.tools,
|
text: i18n.ts.tools,
|
||||||
icon: 'ti ti-tool',
|
icon: 'ti ti-tool',
|
||||||
|
@ -52,23 +57,7 @@ export function openInstanceMenu(ev: MouseEvent) {
|
||||||
to: '/clicker',
|
to: '/clicker',
|
||||||
text: '🍪👈',
|
text: '🍪👈',
|
||||||
icon: 'ti ti-cookie',
|
icon: 'ti ti-cookie',
|
||||||
}, ($i && ($i.isAdmin || $i.policies.canInvite) && instance.disableRegistration) ? {
|
}, ($i && ($i.isAdmin || $i.policies.canManageCustomEmojis)) ? {
|
||||||
text: i18n.ts.invite,
|
|
||||||
icon: 'ti ti-user-plus',
|
|
||||||
action: () => {
|
|
||||||
os.api('invite').then(x => {
|
|
||||||
os.alert({
|
|
||||||
type: 'info',
|
|
||||||
text: x.code,
|
|
||||||
});
|
|
||||||
}).catch(err => {
|
|
||||||
os.alert({
|
|
||||||
type: 'error',
|
|
||||||
text: err,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
} : undefined, ($i && ($i.isAdmin || $i.policies.canManageCustomEmojis)) ? {
|
|
||||||
type: 'link',
|
type: 'link',
|
||||||
to: '/custom-emojis-manager',
|
to: '/custom-emojis-manager',
|
||||||
text: i18n.ts.manageCustomEmojis,
|
text: i18n.ts.manageCustomEmojis,
|
||||||
|
|
|
@ -271,6 +271,7 @@ type DetailedInstanceMetadata = LiteInstanceMetadata & {
|
||||||
pinnedPages: string[];
|
pinnedPages: string[];
|
||||||
pinnedClipId: string | null;
|
pinnedClipId: string | null;
|
||||||
cacheRemoteFiles: boolean;
|
cacheRemoteFiles: boolean;
|
||||||
|
cacheRemoteSensitiveFiles: boolean;
|
||||||
requireSetup: boolean;
|
requireSetup: boolean;
|
||||||
proxyAccountName: string | null;
|
proxyAccountName: string | null;
|
||||||
features: Record<string, any>;
|
features: Record<string, any>;
|
||||||
|
@ -327,6 +328,10 @@ export type Endpoints = {
|
||||||
req: TODO;
|
req: TODO;
|
||||||
res: TODO;
|
res: TODO;
|
||||||
};
|
};
|
||||||
|
'admin/meta': {
|
||||||
|
req: TODO;
|
||||||
|
res: TODO;
|
||||||
|
};
|
||||||
'admin/reset-password': {
|
'admin/reset-password': {
|
||||||
req: TODO;
|
req: TODO;
|
||||||
res: TODO;
|
res: TODO;
|
||||||
|
@ -481,6 +486,14 @@ export type Endpoints = {
|
||||||
req: TODO;
|
req: TODO;
|
||||||
res: TODO;
|
res: TODO;
|
||||||
};
|
};
|
||||||
|
'admin/invite/create': {
|
||||||
|
req: TODO;
|
||||||
|
res: TODO;
|
||||||
|
};
|
||||||
|
'admin/invite/list': {
|
||||||
|
req: TODO;
|
||||||
|
res: TODO;
|
||||||
|
};
|
||||||
'admin/moderators/add': {
|
'admin/moderators/add': {
|
||||||
req: TODO;
|
req: TODO;
|
||||||
res: TODO;
|
res: TODO;
|
||||||
|
@ -1549,6 +1562,28 @@ export type Endpoints = {
|
||||||
req: TODO;
|
req: TODO;
|
||||||
res: TODO;
|
res: TODO;
|
||||||
};
|
};
|
||||||
|
'invite/create': {
|
||||||
|
req: NoParams;
|
||||||
|
res: Invite;
|
||||||
|
};
|
||||||
|
'invite/delete': {
|
||||||
|
req: {
|
||||||
|
inviteId: Invite['id'];
|
||||||
|
};
|
||||||
|
res: null;
|
||||||
|
};
|
||||||
|
'invite/list': {
|
||||||
|
req: {
|
||||||
|
limit?: number;
|
||||||
|
sinceId?: Invite['id'];
|
||||||
|
untilId?: Invite['id'];
|
||||||
|
};
|
||||||
|
res: Invite[];
|
||||||
|
};
|
||||||
|
'invite/limit': {
|
||||||
|
req: NoParams;
|
||||||
|
res: InviteLimit;
|
||||||
|
};
|
||||||
'messaging/history': {
|
'messaging/history': {
|
||||||
req: {
|
req: {
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
@ -2210,6 +2245,8 @@ declare namespace entities {
|
||||||
Blocking,
|
Blocking,
|
||||||
Instance,
|
Instance,
|
||||||
Signin,
|
Signin,
|
||||||
|
Invite,
|
||||||
|
InviteLimit,
|
||||||
UserSorting,
|
UserSorting,
|
||||||
OriginType
|
OriginType
|
||||||
}
|
}
|
||||||
|
@ -2310,6 +2347,23 @@ type Instance = {
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type InstanceMetadata = LiteInstanceMetadata | DetailedInstanceMetadata;
|
type InstanceMetadata = LiteInstanceMetadata | DetailedInstanceMetadata;
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
type Invite = {
|
||||||
|
id: ID;
|
||||||
|
code: string;
|
||||||
|
expiresAt: DateString | null;
|
||||||
|
createdAt: DateString;
|
||||||
|
createdBy: UserLite | null;
|
||||||
|
usedBy: UserLite | null;
|
||||||
|
usedAt: DateString | null;
|
||||||
|
used: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
type InviteLimit = {
|
||||||
|
remaining: number;
|
||||||
|
};
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
function isAPIError(reason: any): reason is APIError;
|
function isAPIError(reason: any): reason is APIError;
|
||||||
|
|
||||||
|
@ -2756,7 +2810,7 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u
|
||||||
//
|
//
|
||||||
// src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts
|
// src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts
|
||||||
// src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts
|
// src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts
|
||||||
// src/api.types.ts:620:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts
|
// src/api.types.ts:629:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts
|
||||||
// src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts
|
// src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts
|
||||||
|
|
||||||
// (No @packageDocumentation comment for this package)
|
// (No @packageDocumentation comment for this package)
|
||||||
|
|
|
@ -20,13 +20,13 @@
|
||||||
"url": "git+https://github.com/misskey-dev/misskey.js.git"
|
"url": "git+https://github.com/misskey-dev/misskey.js.git"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@microsoft/api-extractor": "7.36.1",
|
"@microsoft/api-extractor": "7.36.2",
|
||||||
"@swc/jest": "0.2.26",
|
"@swc/jest": "0.2.26",
|
||||||
"@types/jest": "29.5.2",
|
"@types/jest": "29.5.3",
|
||||||
"@types/node": "20.4.0",
|
"@types/node": "20.4.2",
|
||||||
"@typescript-eslint/eslint-plugin": "5.61.0",
|
"@typescript-eslint/eslint-plugin": "5.61.0",
|
||||||
"@typescript-eslint/parser": "5.61.0",
|
"@typescript-eslint/parser": "5.61.0",
|
||||||
"eslint": "8.44.0",
|
"eslint": "8.45.0",
|
||||||
"jest": "29.6.1",
|
"jest": "29.6.1",
|
||||||
"jest-fetch-mock": "3.0.3",
|
"jest-fetch-mock": "3.0.3",
|
||||||
"jest-websocket-mock": "2.4.0",
|
"jest-websocket-mock": "2.4.0",
|
||||||
|
@ -39,7 +39,7 @@
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@swc/cli": "0.1.62",
|
"@swc/cli": "0.1.62",
|
||||||
"@swc/core": "1.3.68",
|
"@swc/core": "1.3.69",
|
||||||
"eventemitter3": "5.0.1",
|
"eventemitter3": "5.0.1",
|
||||||
"reconnecting-websocket": "4.4.0"
|
"reconnecting-websocket": "4.4.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import type {
|
||||||
Ad, Announcement, Antenna, App, AuthSession, Blocking, Channel, Clip, DateString, DetailedInstanceMetadata, DriveFile, DriveFolder, Following, FollowingFolloweePopulated, FollowingFollowerPopulated, FollowRequest, GalleryPost, Instance,
|
Ad, Announcement, Antenna, App, AuthSession, Blocking, Channel, Clip, DateString, DetailedInstanceMetadata, DriveFile, DriveFolder, Following, FollowingFolloweePopulated, FollowingFollowerPopulated, FollowRequest, GalleryPost, Instance,
|
||||||
LiteInstanceMetadata,
|
LiteInstanceMetadata,
|
||||||
MeDetailed,
|
MeDetailed,
|
||||||
Note, NoteFavorite, OriginType, Page, ServerInfo, Stats, User, UserDetailed, MeSignup, UserGroup, UserList, UserSorting, Notification, NoteReaction, Signin, MessagingMessage,
|
Note, NoteFavorite, OriginType, Page, ServerInfo, Stats, User, UserDetailed, MeSignup, UserGroup, UserList, UserSorting, Notification, NoteReaction, Signin, MessagingMessage, Invite, InviteLimit,
|
||||||
} from './entities.js';
|
} from './entities.js';
|
||||||
|
|
||||||
type TODO = Record<string, any> | null;
|
type TODO = Record<string, any> | null;
|
||||||
|
@ -20,6 +20,7 @@ export type Endpoints = {
|
||||||
'admin/get-table-stats': { req: TODO; res: TODO; };
|
'admin/get-table-stats': { req: TODO; res: TODO; };
|
||||||
'admin/invite': { req: TODO; res: TODO; };
|
'admin/invite': { req: TODO; res: TODO; };
|
||||||
'admin/logs': { req: TODO; res: TODO; };
|
'admin/logs': { req: TODO; res: TODO; };
|
||||||
|
'admin/meta': { req: TODO; res: TODO; };
|
||||||
'admin/reset-password': { req: TODO; res: TODO; };
|
'admin/reset-password': { req: TODO; res: TODO; };
|
||||||
'admin/resolve-abuse-user-report': { req: TODO; res: TODO; };
|
'admin/resolve-abuse-user-report': { req: TODO; res: TODO; };
|
||||||
'admin/resync-chart': { req: TODO; res: TODO; };
|
'admin/resync-chart': { req: TODO; res: TODO; };
|
||||||
|
@ -57,6 +58,8 @@ export type Endpoints = {
|
||||||
'admin/federation/refresh-remote-instance-metadata': { req: TODO; res: TODO; };
|
'admin/federation/refresh-remote-instance-metadata': { req: TODO; res: TODO; };
|
||||||
'admin/federation/remove-all-following': { req: TODO; res: TODO; };
|
'admin/federation/remove-all-following': { req: TODO; res: TODO; };
|
||||||
'admin/federation/update-instance': { req: TODO; res: TODO; };
|
'admin/federation/update-instance': { req: TODO; res: TODO; };
|
||||||
|
'admin/invite/create': { req: TODO; res: TODO; };
|
||||||
|
'admin/invite/list': { req: TODO; res: TODO; };
|
||||||
'admin/moderators/add': { req: TODO; res: TODO; };
|
'admin/moderators/add': { req: TODO; res: TODO; };
|
||||||
'admin/moderators/remove': { req: TODO; res: TODO; };
|
'admin/moderators/remove': { req: TODO; res: TODO; };
|
||||||
'admin/promo/create': { req: TODO; res: TODO; };
|
'admin/promo/create': { req: TODO; res: TODO; };
|
||||||
|
@ -440,6 +443,12 @@ export type Endpoints = {
|
||||||
'i/2fa/remove-key': { req: TODO; res: TODO; };
|
'i/2fa/remove-key': { req: TODO; res: TODO; };
|
||||||
'i/2fa/unregister': { req: TODO; res: TODO; };
|
'i/2fa/unregister': { req: TODO; res: TODO; };
|
||||||
|
|
||||||
|
// invite
|
||||||
|
'invite/create': { req: NoParams; res: Invite; };
|
||||||
|
'invite/delete': { req: { inviteId: Invite['id']; }; res: null; };
|
||||||
|
'invite/list': { req: { limit?: number; sinceId?: Invite['id']; untilId?: Invite['id'] }; res: Invite[]; };
|
||||||
|
'invite/limit': { req: NoParams; res: InviteLimit; };
|
||||||
|
|
||||||
// messaging
|
// messaging
|
||||||
'messaging/history': { req: { limit?: number; group?: boolean; }; res: MessagingMessage[]; };
|
'messaging/history': { req: { limit?: number; group?: boolean; }; res: MessagingMessage[]; };
|
||||||
'messaging/messages': { req: { userId?: User['id']; groupId?: UserGroup['id']; limit?: number; sinceId?: MessagingMessage['id']; untilId?: MessagingMessage['id']; markAsRead?: boolean; }; res: MessagingMessage[]; };
|
'messaging/messages': { req: { userId?: User['id']; groupId?: UserGroup['id']; limit?: number; sinceId?: MessagingMessage['id']; untilId?: MessagingMessage['id']; markAsRead?: boolean; }; res: MessagingMessage[]; };
|
||||||
|
|
|
@ -338,6 +338,7 @@ export type DetailedInstanceMetadata = LiteInstanceMetadata & {
|
||||||
pinnedPages: string[];
|
pinnedPages: string[];
|
||||||
pinnedClipId: string | null;
|
pinnedClipId: string | null;
|
||||||
cacheRemoteFiles: boolean;
|
cacheRemoteFiles: boolean;
|
||||||
|
cacheRemoteSensitiveFiles: boolean;
|
||||||
requireSetup: boolean;
|
requireSetup: boolean;
|
||||||
proxyAccountName: string | null;
|
proxyAccountName: string | null;
|
||||||
features: Record<string, any>;
|
features: Record<string, any>;
|
||||||
|
@ -516,6 +517,21 @@ export type Signin = {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Invite = {
|
||||||
|
id: ID;
|
||||||
|
code: string;
|
||||||
|
expiresAt: DateString | null;
|
||||||
|
createdAt: DateString;
|
||||||
|
createdBy: UserLite | null;
|
||||||
|
usedBy: UserLite | null;
|
||||||
|
usedAt: DateString | null;
|
||||||
|
used: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InviteLimit = {
|
||||||
|
remaining: number;
|
||||||
|
}
|
||||||
|
|
||||||
export type UserSorting =
|
export type UserSorting =
|
||||||
| '+follower'
|
| '+follower'
|
||||||
| '-follower'
|
| '-follower'
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@typescript-eslint/parser": "5.61.0",
|
"@typescript-eslint/parser": "5.61.0",
|
||||||
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.67",
|
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.67",
|
||||||
"eslint": "8.44.0",
|
"eslint": "8.45.0",
|
||||||
"eslint-plugin-import": "2.27.5",
|
"eslint-plugin-import": "2.27.5",
|
||||||
"typescript": "5.1.6"
|
"typescript": "5.1.6"
|
||||||
},
|
},
|
||||||
|
|
|
@ -199,6 +199,13 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
|
||||||
tag: `achievement:${data.body.achievement}`,
|
tag: `achievement:${data.body.achievement}`,
|
||||||
}];
|
}];
|
||||||
|
|
||||||
|
case 'pollEnded':
|
||||||
|
return [t('_notification.pollEnded'), {
|
||||||
|
body: data.body.note.text ?? '',
|
||||||
|
badge: iconUrl('chart-arrows'),
|
||||||
|
data,
|
||||||
|
}];
|
||||||
|
|
||||||
case 'app':
|
case 'app':
|
||||||
return [data.body.header ?? data.body.body, {
|
return [data.body.header ?? data.body.body, {
|
||||||
body: data.body.header ? data.body.body : '',
|
body: data.body.header ? data.body.body : '',
|
||||||
|
|
1545
pnpm-lock.yaml
1545
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue