Merge branch 'develop' into fetch-outbox
This commit is contained in:
commit
6b26ce3768
|
@ -104,6 +104,7 @@ redis:
|
|||
# apiKey: ''
|
||||
# ssl: true
|
||||
# index: ''
|
||||
# scope: local
|
||||
|
||||
# ┌───────────────┐
|
||||
#───┘ ID generation └───────────────────────────────────────────
|
||||
|
|
12
CHANGELOG.md
12
CHANGELOG.md
|
@ -17,6 +17,10 @@
|
|||
### General
|
||||
- identicon生成を無効にしてパフォーマンスを向上させることができるようになりました
|
||||
- サーバーのマシン情報の公開を無効にしてパフォーマンスを向上させることができるようになりました
|
||||
- 招待機能を改善しました
|
||||
* 過去に発行した招待コードを確認できるようになりました
|
||||
* ロールごとに招待コードの発行数制限と制限対象期間、有効期限を設定できるようになりました
|
||||
* 招待コードを作成したユーザーと使用したユーザーを確認できるようになりました
|
||||
|
||||
### Client
|
||||
- deck UIのカラムのメニューからアンテナとリストの編集画面を開けるように
|
||||
|
@ -41,13 +45,19 @@
|
|||
- Fix: フォルダーのページネーションが機能しない #11180
|
||||
- Fix: 長い文章を投稿する際、プレビューが画面からはみ出る問題を修正
|
||||
- Fix: システムフォント設定が正しく反映されない問題を修正
|
||||
- Fix: アンケート終了時のプッシュ通知が正しく表示されない問題を修正
|
||||
- Fix: MasterVolumeが0の時だけでなく各通知音の音量設定が0のときも、HTMLAudioElement.playが実行されないように変更
|
||||
|
||||
### Server
|
||||
- JSON.parse の回数を削減することで、ストリーミングのパフォーマンスを向上しました
|
||||
- nsfwjs のモデルロードを排他することで、重複ロードによってメモリ使用量が増加しないように
|
||||
- 連合の配送ジョブのパフォーマンスを向上(ロック機構の見直し、Redisキャッシュの活用)
|
||||
- 全体的なDBクエリのパフォーマンスを向上
|
||||
- featuredノートのsignedGet回数を減らしました
|
||||
- リモートサーバーからのNSFW映像のキャッシュだけを無効化できるオプションを追加
|
||||
- MeilisearchにIndexするノートの範囲を設定できるように
|
||||
- Fix: Remove Meilisearch index when notes are deleted
|
||||
- Fix: 非英語環境でのPostgreSQLのエラーハンドリングを修正
|
||||
- Fix: インスタンスのアイコンがbase64の場合の挙動を修正
|
||||
|
||||
## 13.13.2
|
||||
|
||||
|
|
|
@ -51,11 +51,7 @@ export default function generateDTS() {
|
|||
ts.NodeFlags.Const | ts.NodeFlags.Ambient | ts.NodeFlags.ContextFlags,
|
||||
),
|
||||
),
|
||||
ts.factory.createExportAssignment(
|
||||
undefined,
|
||||
true,
|
||||
ts.factory.createIdentifier('locales'),
|
||||
),
|
||||
ts.factory.createExportDefault(ts.factory.createIdentifier('locales')),
|
||||
];
|
||||
const printed = ts.createPrinter({
|
||||
newLine: ts.NewLineKind.LineFeed,
|
||||
|
|
22
locales/index.d.ts
vendored
22
locales/index.d.ts
vendored
|
@ -159,6 +159,8 @@ export interface Locale {
|
|||
"settingGuide": string;
|
||||
"cacheRemoteFiles": string;
|
||||
"cacheRemoteFilesDescription": string;
|
||||
"cacheRemoteSensitiveFiles": string;
|
||||
"cacheRemoteSensitiveFilesDescription": string;
|
||||
"flagAsBot": string;
|
||||
"flagAsBotDescription": string;
|
||||
"flagAsCat": string;
|
||||
|
@ -1075,6 +1077,23 @@ export interface Locale {
|
|||
"enableServerMachineStats": string;
|
||||
"enableIdenticonGeneration": string;
|
||||
"turnOffToImprovePerformance": string;
|
||||
"createInviteCode": string;
|
||||
"createWithOptions": string;
|
||||
"createCount": string;
|
||||
"inviteCodeCreated": string;
|
||||
"inviteLimitExceeded": string;
|
||||
"createLimitRemaining": string;
|
||||
"inviteLimitResetCycle": string;
|
||||
"expirationDate": string;
|
||||
"noExpirationDate": string;
|
||||
"inviteCodeUsedAt": string;
|
||||
"registeredUserUsingInviteCode": string;
|
||||
"waitingForMailAuth": string;
|
||||
"inviteCodeCreator": string;
|
||||
"usedAt": string;
|
||||
"unused": string;
|
||||
"used": string;
|
||||
"expired": string;
|
||||
"_initialAccountSetting": {
|
||||
"accountCreated": string;
|
||||
"letsStartAccountSetup": string;
|
||||
|
@ -1465,6 +1484,9 @@ export interface Locale {
|
|||
"ltlAvailable": string;
|
||||
"canPublicNote": string;
|
||||
"canInvite": string;
|
||||
"inviteLimit": string;
|
||||
"inviteLimitCycle": string;
|
||||
"inviteExpirationTime": string;
|
||||
"canManageCustomEmojis": string;
|
||||
"driveCapacity": string;
|
||||
"alwaysMarkNsfw": string;
|
||||
|
|
|
@ -156,6 +156,8 @@ addEmoji: "絵文字を追加"
|
|||
settingGuide: "おすすめ設定"
|
||||
cacheRemoteFiles: "リモートのファイルをキャッシュする"
|
||||
cacheRemoteFilesDescription: "この設定を無効にすると、リモートファイルをキャッシュせず直リンクするようになります。サーバーのストレージを節約できますが、サムネイルが生成されないので通信量が増加します。"
|
||||
cacheRemoteSensitiveFiles: "リモートのセンシティブなファイルをキャッシュする"
|
||||
cacheRemoteSensitiveFilesDescription: "この設定を無効にすると、リモートのセンシティブなファイルはキャッシュせず直リンクするようになります。"
|
||||
flagAsBot: "Botとして設定"
|
||||
flagAsBotDescription: "このアカウントがプログラムによって運用される場合は、このフラグをオンにします。オンにすると、反応の連鎖を防ぐためのフラグとして他の開発者に役立ったり、Misskeyのシステム上での扱いがBotに合ったものになります。"
|
||||
flagAsCat: "にゃああああああああああああああ!!!!!!!!!!!!"
|
||||
|
@ -1072,6 +1074,23 @@ branding: "ブランディング"
|
|||
enableServerMachineStats: "サーバーのマシン情報を公開する"
|
||||
enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする"
|
||||
turnOffToImprovePerformance: "オフにするとパフォーマンスが向上します。"
|
||||
createInviteCode: "招待コードを作成"
|
||||
createWithOptions: "オプションを指定して作成"
|
||||
createCount: "作成数"
|
||||
inviteCodeCreated: "招待コードを作成しました"
|
||||
inviteLimitExceeded: "作成できる招待コードの数が上限に達しています。"
|
||||
createLimitRemaining: "作成できる招待コード: 残り {limit} 個"
|
||||
inviteLimitResetCycle: "{time}で最大 {limit} 個の招待コードを作成できます。"
|
||||
expirationDate: "有効期限"
|
||||
noExpirationDate: "有効期限を設けない"
|
||||
inviteCodeUsedAt: "招待コードが使用された日時"
|
||||
registeredUserUsingInviteCode: "招待コードを使用したユーザー"
|
||||
waitingForMailAuth: "メール認証待ち"
|
||||
inviteCodeCreator: "招待コードを作成したユーザー"
|
||||
usedAt: "使用日時"
|
||||
unused: "未使用"
|
||||
used: "使用済み"
|
||||
expired: "期限切れ"
|
||||
|
||||
_initialAccountSetting:
|
||||
accountCreated: "アカウントの作成が完了しました!"
|
||||
|
@ -1387,6 +1406,9 @@ _role:
|
|||
ltlAvailable: "ローカルタイムラインの閲覧"
|
||||
canPublicNote: "パブリック投稿の許可"
|
||||
canInvite: "サーバー招待コードの発行"
|
||||
inviteLimit: "招待コードの作成可能数"
|
||||
inviteLimitCycle: "招待コードの発行間隔"
|
||||
inviteExpirationTime: "招待コードの有効期限"
|
||||
canManageCustomEmojis: "カスタム絵文字の管理"
|
||||
driveCapacity: "ドライブ容量"
|
||||
alwaysMarkNsfw: "ファイルにNSFWを常に付与"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "misskey",
|
||||
"version": "13.14.0-beta.2",
|
||||
"version": "13.14.0-beta.3",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -59,8 +59,8 @@
|
|||
"@typescript-eslint/eslint-plugin": "5.61.0",
|
||||
"@typescript-eslint/parser": "5.61.0",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "12.17.0",
|
||||
"eslint": "8.44.0",
|
||||
"cypress": "12.17.1",
|
||||
"eslint": "8.45.0",
|
||||
"start-server-and-test": "2.0.0"
|
||||
},
|
||||
"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"`);
|
||||
}
|
||||
}
|
11
packages/backend/migration/1689102832143-nsfw-cache.js
Normal file
11
packages/backend/migration/1689102832143-nsfw-cache.js
Normal file
|
@ -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",
|
||||
"@sinonjs/fake-timers": "10.3.0",
|
||||
"@swc/cli": "0.1.62",
|
||||
"@swc/core": "1.3.68",
|
||||
"@swc/core": "1.3.69",
|
||||
"accepts": "1.3.8",
|
||||
"ajv": "8.12.0",
|
||||
"archiver": "5.3.1",
|
||||
|
@ -82,11 +82,11 @@
|
|||
"autwh": "0.1.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"blurhash": "2.0.5",
|
||||
"bullmq": "4.2.0",
|
||||
"bullmq": "4.3.0",
|
||||
"cacheable-lookup": "7.0.0",
|
||||
"cbor": "9.0.0",
|
||||
"chalk": "5.2.0",
|
||||
"chalk-template": "0.4.0",
|
||||
"chalk": "5.3.0",
|
||||
"chalk-template": "1.1.0",
|
||||
"chokidar": "3.5.3",
|
||||
"cli-highlight": "2.1.11",
|
||||
"color-convert": "2.0.1",
|
||||
|
@ -100,7 +100,7 @@
|
|||
"fluent-ffmpeg": "2.1.2",
|
||||
"form-data": "4.0.0",
|
||||
"got": "13.0.0",
|
||||
"happy-dom": "10.0.3",
|
||||
"happy-dom": "10.3.2",
|
||||
"hpagent": "1.2.0",
|
||||
"ioredis": "5.3.2",
|
||||
"ip-cidr": "3.1.0",
|
||||
|
@ -141,14 +141,14 @@
|
|||
"rxjs": "7.8.1",
|
||||
"s-age": "1.1.2",
|
||||
"sanitize-html": "2.11.0",
|
||||
"semver": "7.5.3",
|
||||
"sharp": "0.32.1",
|
||||
"semver": "7.5.4",
|
||||
"sharp": "0.32.3",
|
||||
"sharp-read-bmp": "github:misskey-dev/sharp-read-bmp",
|
||||
"slacc": "0.0.9",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"stringz": "2.1.0",
|
||||
"summaly": "github:misskey-dev/summaly",
|
||||
"systeminformation": "5.18.6",
|
||||
"systeminformation": "5.18.7",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tmp": "0.2.1",
|
||||
"tsc-alias": "1.8.7",
|
||||
|
@ -158,7 +158,6 @@
|
|||
"typescript": "5.1.6",
|
||||
"ulid": "2.3.0",
|
||||
"unzipper": "0.10.14",
|
||||
"uuid": "9.0.0",
|
||||
"vary": "1.1.2",
|
||||
"web-push": "3.6.3",
|
||||
"ws": "8.13.0",
|
||||
|
@ -175,14 +174,14 @@
|
|||
"@types/content-disposition": "0.5.5",
|
||||
"@types/escape-regexp": "0.0.1",
|
||||
"@types/fluent-ffmpeg": "2.1.21",
|
||||
"@types/jest": "29.5.2",
|
||||
"@types/jest": "29.5.3",
|
||||
"@types/js-yaml": "4.0.5",
|
||||
"@types/jsdom": "21.1.1",
|
||||
"@types/jsonld": "1.5.9",
|
||||
"@types/jsrsasign": "10.5.8",
|
||||
"@types/mime-types": "2.1.1",
|
||||
"@types/ms": "^0.7.31",
|
||||
"@types/node": "20.4.0",
|
||||
"@types/node": "20.4.2",
|
||||
"@types/node-fetch": "3.0.3",
|
||||
"@types/nodemailer": "6.4.8",
|
||||
"@types/oauth": "0.9.1",
|
||||
|
@ -201,7 +200,6 @@
|
|||
"@types/tinycolor2": "1.4.3",
|
||||
"@types/tmp": "0.2.3",
|
||||
"@types/unzipper": "0.10.6",
|
||||
"@types/uuid": "9.0.2",
|
||||
"@types/vary": "1.1.0",
|
||||
"@types/web-push": "3.3.2",
|
||||
"@types/websocket": "1.0.5",
|
||||
|
@ -210,7 +208,7 @@
|
|||
"@typescript-eslint/parser": "5.61.0",
|
||||
"aws-sdk-client-mock": "3.0.0",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "8.44.0",
|
||||
"eslint": "8.45.0",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"execa": "7.1.1",
|
||||
"jest": "29.6.1",
|
||||
|
|
|
@ -63,6 +63,7 @@ export type Source = {
|
|||
apiKey: string;
|
||||
ssl?: boolean;
|
||||
index: string;
|
||||
scope?: 'local' | 'global' | string[];
|
||||
};
|
||||
|
||||
proxy?: string;
|
||||
|
|
|
@ -81,6 +81,7 @@ import { GalleryLikeEntityService } from './entities/GalleryLikeEntityService.js
|
|||
import { GalleryPostEntityService } from './entities/GalleryPostEntityService.js';
|
||||
import { HashtagEntityService } from './entities/HashtagEntityService.js';
|
||||
import { InstanceEntityService } from './entities/InstanceEntityService.js';
|
||||
import { InviteCodeEntityService } from './entities/InviteCodeEntityService.js';
|
||||
import { ModerationLogEntityService } from './entities/ModerationLogEntityService.js';
|
||||
import { MutingEntityService } from './entities/MutingEntityService.js';
|
||||
import { RenoteMutingEntityService } from './entities/RenoteMutingEntityService.js';
|
||||
|
@ -205,6 +206,7 @@ const $GalleryLikeEntityService: Provider = { provide: 'GalleryLikeEntityService
|
|||
const $GalleryPostEntityService: Provider = { provide: 'GalleryPostEntityService', useExisting: GalleryPostEntityService };
|
||||
const $HashtagEntityService: Provider = { provide: 'HashtagEntityService', useExisting: HashtagEntityService };
|
||||
const $InstanceEntityService: Provider = { provide: 'InstanceEntityService', useExisting: InstanceEntityService };
|
||||
const $InviteCodeEntityService: Provider = { provide: 'InviteCodeEntityService', useExisting: InviteCodeEntityService };
|
||||
const $ModerationLogEntityService: Provider = { provide: 'ModerationLogEntityService', useExisting: ModerationLogEntityService };
|
||||
const $MutingEntityService: Provider = { provide: 'MutingEntityService', useExisting: MutingEntityService };
|
||||
const $RenoteMutingEntityService: Provider = { provide: 'RenoteMutingEntityService', useExisting: RenoteMutingEntityService };
|
||||
|
@ -329,6 +331,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
GalleryPostEntityService,
|
||||
HashtagEntityService,
|
||||
InstanceEntityService,
|
||||
InviteCodeEntityService,
|
||||
ModerationLogEntityService,
|
||||
MutingEntityService,
|
||||
RenoteMutingEntityService,
|
||||
|
@ -448,6 +451,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$GalleryPostEntityService,
|
||||
$HashtagEntityService,
|
||||
$InstanceEntityService,
|
||||
$InviteCodeEntityService,
|
||||
$ModerationLogEntityService,
|
||||
$MutingEntityService,
|
||||
$RenoteMutingEntityService,
|
||||
|
@ -567,6 +571,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
GalleryPostEntityService,
|
||||
HashtagEntityService,
|
||||
InstanceEntityService,
|
||||
InviteCodeEntityService,
|
||||
ModerationLogEntityService,
|
||||
MutingEntityService,
|
||||
RenoteMutingEntityService,
|
||||
|
@ -685,6 +690,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$GalleryPostEntityService,
|
||||
$HashtagEntityService,
|
||||
$InstanceEntityService,
|
||||
$InviteCodeEntityService,
|
||||
$ModerationLogEntityService,
|
||||
$MutingEntityService,
|
||||
$RenoteMutingEntityService,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { randomUUID } from 'node:crypto';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { IsNull, DataSource } from 'typeorm';
|
||||
import { genRsaKeyPair } from '@/misc/gen-key-pair.js';
|
||||
import { User } from '@/models/entities/User.js';
|
||||
|
@ -24,7 +24,7 @@ export class CreateSystemUserService {
|
|||
|
||||
@bindThis
|
||||
public async createSystemUser(username: string): Promise<User> {
|
||||
const password = uuid();
|
||||
const password = randomUUID();
|
||||
|
||||
// Generate hash of password
|
||||
const salt = await bcrypt.genSalt(8);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { randomUUID } from 'node:crypto';
|
||||
import * as fs from 'node:fs';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import sharp from 'sharp';
|
||||
import { sharpBmp } from 'sharp-read-bmp';
|
||||
import { IsNull } from 'typeorm';
|
||||
|
@ -162,7 +162,7 @@ export class DriveService {
|
|||
?? `${ meta.objectStorageUseSSL ? 'https' : 'http' }://${ meta.objectStorageEndpoint }${ meta.objectStoragePort ? `:${meta.objectStoragePort}` : '' }/${ meta.objectStorageBucket }`;
|
||||
|
||||
// for original
|
||||
const key = `${meta.objectStoragePrefix}/${uuid()}${ext}`;
|
||||
const key = `${meta.objectStoragePrefix}/${randomUUID()}${ext}`;
|
||||
const url = `${ baseUrl }/${ key }`;
|
||||
|
||||
// for alts
|
||||
|
@ -179,7 +179,7 @@ export class DriveService {
|
|||
];
|
||||
|
||||
if (alts.webpublic) {
|
||||
webpublicKey = `${meta.objectStoragePrefix}/webpublic-${uuid()}.${alts.webpublic.ext}`;
|
||||
webpublicKey = `${meta.objectStoragePrefix}/webpublic-${randomUUID()}.${alts.webpublic.ext}`;
|
||||
webpublicUrl = `${ baseUrl }/${ webpublicKey }`;
|
||||
|
||||
this.registerLogger.info(`uploading webpublic: ${webpublicKey}`);
|
||||
|
@ -187,7 +187,7 @@ export class DriveService {
|
|||
}
|
||||
|
||||
if (alts.thumbnail) {
|
||||
thumbnailKey = `${meta.objectStoragePrefix}/thumbnail-${uuid()}.${alts.thumbnail.ext}`;
|
||||
thumbnailKey = `${meta.objectStoragePrefix}/thumbnail-${randomUUID()}.${alts.thumbnail.ext}`;
|
||||
thumbnailUrl = `${ baseUrl }/${ 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]));
|
||||
} else { // use internal storage
|
||||
const accessKey = uuid();
|
||||
const thumbnailAccessKey = 'thumbnail-' + uuid();
|
||||
const webpublicAccessKey = 'webpublic-' + uuid();
|
||||
const accessKey = randomUUID();
|
||||
const thumbnailAccessKey = 'thumbnail-' + randomUUID();
|
||||
const webpublicAccessKey = 'webpublic-' + randomUUID();
|
||||
|
||||
const url = this.internalStorageService.saveFromPath(accessKey, path);
|
||||
|
||||
|
@ -584,9 +584,9 @@ export class DriveService {
|
|||
if (isLink) {
|
||||
file.url = url;
|
||||
// ローカルプロキシ用
|
||||
file.accessKey = uuid();
|
||||
file.thumbnailAccessKey = 'thumbnail-' + uuid();
|
||||
file.webpublicAccessKey = 'webpublic-' + uuid();
|
||||
file.accessKey = randomUUID();
|
||||
file.thumbnailAccessKey = 'thumbnail-' + randomUUID();
|
||||
file.webpublicAccessKey = 'webpublic-' + randomUUID();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -713,9 +713,9 @@ export class DriveService {
|
|||
webpublicUrl: null,
|
||||
storedInternal: false,
|
||||
// ローカルプロキシ用
|
||||
accessKey: uuid(),
|
||||
thumbnailAccessKey: 'thumbnail-' + uuid(),
|
||||
webpublicAccessKey: 'webpublic-' + uuid(),
|
||||
accessKey: randomUUID(),
|
||||
thumbnailAccessKey: 'thumbnail-' + randomUUID(),
|
||||
webpublicAccessKey: 'webpublic-' + randomUUID(),
|
||||
});
|
||||
} else {
|
||||
this.driveFilesRepository.delete(file.id);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import * as http from 'node:http';
|
||||
import * as https from 'node:https';
|
||||
import * as net from 'node:net';
|
||||
import CacheableLookup from 'cacheable-lookup';
|
||||
import fetch from 'node-fetch';
|
||||
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
||||
|
@ -46,14 +47,14 @@ export class HttpRequestService {
|
|||
this.http = new http.Agent({
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 30 * 1000,
|
||||
lookup: cache.lookup,
|
||||
} as http.AgentOptions);
|
||||
lookup: cache.lookup as unknown as net.LookupFunction,
|
||||
});
|
||||
|
||||
this.https = new https.Agent({
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 30 * 1000,
|
||||
lookup: cache.lookup,
|
||||
} as https.AgentOptions);
|
||||
lookup: cache.lookup as unknown as net.LookupFunction,
|
||||
});
|
||||
|
||||
const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128);
|
||||
|
||||
|
@ -144,7 +145,7 @@ export class HttpRequestService {
|
|||
method: args.method ?? 'GET',
|
||||
headers: {
|
||||
'User-Agent': this.config.userAgent,
|
||||
...(args.headers ?? {})
|
||||
...(args.headers ?? {}),
|
||||
},
|
||||
body: args.body,
|
||||
size: args.size ?? 10 * 1024 * 1024,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { randomUUID } from 'node:crypto';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import type { IActivity } from '@/core/activitypub/type.js';
|
||||
import type { DriveFile } from '@/models/entities/DriveFile.js';
|
||||
import type { Webhook, webhookEventTypes } from '@/models/entities/Webhook.js';
|
||||
|
@ -416,7 +416,7 @@ export class QueueService {
|
|||
to: webhook.url,
|
||||
secret: webhook.secret,
|
||||
createdAt: Date.now(),
|
||||
eventId: uuid(),
|
||||
eventId: randomUUID(),
|
||||
};
|
||||
|
||||
return this.webhookDeliverQueue.add(webhook.id, data, {
|
||||
|
|
|
@ -71,7 +71,7 @@ export class RemoteUserResolveService {
|
|||
return await this.apPersonService.createPerson(self.href);
|
||||
}
|
||||
|
||||
// ユーザー情報が古い場合は、WebFilgerからやりなおして返す
|
||||
// ユーザー情報が古い場合は、WebFingerからやりなおして返す
|
||||
if (user.lastFetchedAt == null || Date.now() - user.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
|
||||
// 繋がらないインスタンスに何回も試行するのを防ぐ, 後続の同様処理の連続試行を防ぐ ため 試行前にも更新する
|
||||
await this.usersRepository.update(user.id, {
|
||||
|
|
|
@ -21,6 +21,9 @@ export type RolePolicies = {
|
|||
ltlAvailable: boolean;
|
||||
canPublicNote: boolean;
|
||||
canInvite: boolean;
|
||||
inviteLimit: number;
|
||||
inviteLimitCycle: number;
|
||||
inviteExpirationTime: number;
|
||||
canManageCustomEmojis: boolean;
|
||||
canSearchNotes: boolean;
|
||||
canHideAds: boolean;
|
||||
|
@ -42,6 +45,9 @@ export const DEFAULT_POLICIES: RolePolicies = {
|
|||
ltlAvailable: true,
|
||||
canPublicNote: true,
|
||||
canInvite: false,
|
||||
inviteLimit: 0,
|
||||
inviteLimitCycle: 60 * 24 * 7,
|
||||
inviteExpirationTime: 0,
|
||||
canManageCustomEmojis: false,
|
||||
canSearchNotes: false,
|
||||
canHideAds: false,
|
||||
|
@ -277,6 +283,9 @@ export class RoleService implements OnApplicationShutdown {
|
|||
ltlAvailable: calc('ltlAvailable', vs => vs.some(v => v === true)),
|
||||
canPublicNote: calc('canPublicNote', vs => vs.some(v => v === true)),
|
||||
canInvite: calc('canInvite', vs => vs.some(v => v === true)),
|
||||
inviteLimit: calc('inviteLimit', vs => Math.max(...vs)),
|
||||
inviteLimitCycle: calc('inviteLimitCycle', vs => Math.max(...vs)),
|
||||
inviteExpirationTime: calc('inviteExpirationTime', vs => Math.max(...vs)),
|
||||
canManageCustomEmojis: calc('canManageCustomEmojis', vs => vs.some(v => v === true)),
|
||||
canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)),
|
||||
canHideAds: calc('canHideAds', vs => vs.some(v => v === true)),
|
||||
|
|
|
@ -52,6 +52,7 @@ function compileQuery(q: Q): string {
|
|||
|
||||
@Injectable()
|
||||
export class SearchService {
|
||||
private readonly meilisearchIndexScope: 'local' | 'global' | string[] = 'local';
|
||||
private meilisearchNoteIndex: Index | null = null;
|
||||
|
||||
constructor(
|
||||
|
@ -92,6 +93,10 @@ export class SearchService {
|
|||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (config.meilisearch?.scope) {
|
||||
this.meilisearchIndexScope = config.meilisearch.scope;
|
||||
}
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
@ -100,7 +105,22 @@ export class SearchService {
|
|||
if (!['home', 'public'].includes(note.visibility)) return;
|
||||
|
||||
if (this.meilisearch) {
|
||||
this.meilisearchNoteIndex!.addDocuments([{
|
||||
switch (this.meilisearchIndexScope) {
|
||||
case 'global':
|
||||
break;
|
||||
|
||||
case 'local':
|
||||
if (note.userHost == null) break;
|
||||
return;
|
||||
|
||||
default: {
|
||||
if (note.userHost == null) break;
|
||||
if (this.meilisearchIndexScope.includes(note.userHost)) break;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await this.meilisearchNoteIndex?.addDocuments([{
|
||||
id: note.id,
|
||||
createdAt: note.createdAt.getTime(),
|
||||
userId: note.userId,
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { createPublicKey } from 'node:crypto';
|
||||
import { createPublicKey, randomUUID } from 'node:crypto';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { In } from 'typeorm';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import * as mfm from 'mfm-js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { Config } from '@/config.js';
|
||||
|
@ -613,7 +612,7 @@ export class ApRendererService {
|
|||
@bindThis
|
||||
public addContext<T extends IObject>(x: T): T & { '@context': any; id: string; } {
|
||||
if (typeof x === 'object' && x.id == null) {
|
||||
x.id = `${this.config.url}/${uuid()}`;
|
||||
x.id = `${this.config.url}/${randomUUID()}`;
|
||||
}
|
||||
|
||||
return Object.assign({
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { DriveFilesRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { RemoteUser } from '@/models/entities/User.js';
|
||||
import type { DriveFile } from '@/models/entities/DriveFile.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
|
@ -20,9 +19,6 @@ export class ApImageService {
|
|||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
@Inject(DI.driveFilesRepository)
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
|
@ -47,7 +43,7 @@ export class ApImageService {
|
|||
const image = await this.apResolverService.createResolver().resolve(value);
|
||||
|
||||
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') {
|
||||
|
@ -62,12 +58,17 @@ export class ApImageService {
|
|||
|
||||
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({
|
||||
url: image.url,
|
||||
user: actor,
|
||||
uri: image.url,
|
||||
sensitive: image.sensitive,
|
||||
isLink: !instance.cacheRemoteFiles,
|
||||
isLink: !shouldBeCached,
|
||||
comment: truncate(image.name ?? undefined, DB_MAX_IMAGE_COMMENT_LENGTH),
|
||||
});
|
||||
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)));
|
||||
}
|
||||
}
|
20
packages/backend/src/misc/generate-invite-code.ts
Normal file
20
packages/backend/src/misc/generate-invite-code.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { secureRndstr } from './secure-rndstr.js';
|
||||
|
||||
const CHARS = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ'; // [0-9A-Z] w/o [01IO] (32 patterns)
|
||||
|
||||
export function generateInviteCode(): string {
|
||||
const code = secureRndstr(8, {
|
||||
chars: CHARS,
|
||||
});
|
||||
|
||||
const uniqueId = [];
|
||||
let n = Math.floor(Date.now() / 1000 / 60);
|
||||
while (true) {
|
||||
uniqueId.push(CHARS[n % CHARS.length]);
|
||||
const t = Math.floor(n / CHARS.length);
|
||||
if (!t) break;
|
||||
n = t;
|
||||
}
|
||||
|
||||
return code + uniqueId.reverse().join('');
|
||||
}
|
|
@ -19,6 +19,7 @@ import { packedRenoteMutingSchema } from '@/models/json-schema/renote-muting.js'
|
|||
import { packedBlockingSchema } from '@/models/json-schema/blocking.js';
|
||||
import { packedNoteReactionSchema } from '@/models/json-schema/note-reaction.js';
|
||||
import { packedHashtagSchema } from '@/models/json-schema/hashtag.js';
|
||||
import { packedInviteCodeSchema } from '@/models/json-schema/invite-code.js';
|
||||
import { packedPageSchema } from '@/models/json-schema/page.js';
|
||||
import { packedNoteFavoriteSchema } from '@/models/json-schema/note-favorite.js';
|
||||
import { packedChannelSchema } from '@/models/json-schema/channel.js';
|
||||
|
@ -52,6 +53,7 @@ export const refs = {
|
|||
RenoteMuting: packedRenoteMutingSchema,
|
||||
Blocking: packedBlockingSchema,
|
||||
Hashtag: packedHashtagSchema,
|
||||
InviteCode: packedInviteCodeSchema,
|
||||
Page: packedPageSchema,
|
||||
Channel: packedChannelSchema,
|
||||
QueueCount: packedQueueCountSchema,
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { Entity, Column, PrimaryColumn, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import { id } from '../id.js';
|
||||
import { User } from './User.js';
|
||||
import type { Clip } from './Clip.js';
|
||||
|
||||
@Entity()
|
||||
export class Meta {
|
||||
|
@ -126,6 +125,11 @@ export class Meta {
|
|||
})
|
||||
public cacheRemoteFiles: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: true,
|
||||
})
|
||||
public cacheRemoteSensitiveFiles: boolean;
|
||||
|
||||
@Column({
|
||||
...id(),
|
||||
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 { User } from './User.js';
|
||||
|
||||
@Entity()
|
||||
export class RegistrationTicket {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Column('timestamp with time zone')
|
||||
public createdAt: Date;
|
||||
|
||||
@Index({ unique: true })
|
||||
@Column('varchar', {
|
||||
length: 64,
|
||||
})
|
||||
public code: string;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
nullable: true,
|
||||
})
|
||||
public expiresAt: Date | null;
|
||||
|
||||
@Column('timestamp with time zone')
|
||||
public createdAt: Date;
|
||||
|
||||
@ManyToOne(type => User, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public createdBy: User | null;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
nullable: true,
|
||||
})
|
||||
public createdById: User['id'] | null;
|
||||
|
||||
@OneToOne(type => User, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public usedBy: User | null;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
nullable: true,
|
||||
})
|
||||
public usedById: User['id'] | null;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
nullable: true,
|
||||
})
|
||||
public usedAt: Date | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 32,
|
||||
nullable: true,
|
||||
})
|
||||
public pendingUserId: string | null;
|
||||
}
|
||||
|
|
45
packages/backend/src/models/json-schema/invite-code.ts
Normal file
45
packages/backend/src/models/json-schema/invite-code.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
export const packedInviteCodeSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'id',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
code: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
example: 'GR6S02ERUA5VR',
|
||||
},
|
||||
expiresAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
format: 'date-time',
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
format: 'date-time',
|
||||
},
|
||||
createdBy: {
|
||||
type: 'object',
|
||||
optional: false, nullable: true,
|
||||
ref: 'UserLite',
|
||||
},
|
||||
usedBy: {
|
||||
type: 'object',
|
||||
optional: false, nullable: true,
|
||||
ref: 'UserLite',
|
||||
},
|
||||
usedAt: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
format: 'date-time',
|
||||
},
|
||||
used: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
|
@ -1,8 +1,8 @@
|
|||
import { randomUUID } from 'node:crypto';
|
||||
import { pipeline } from 'node:stream';
|
||||
import * as fs from 'node:fs';
|
||||
import { promisify } from 'node:util';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { getIpHash } from '@/misc/get-ip-hash.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) {
|
||||
throw err;
|
||||
} else {
|
||||
const errId = uuid();
|
||||
const errId = randomUUID();
|
||||
this.logger.error(`Internal error occurred in ${ep.name}: ${err.message}`, {
|
||||
ep: ep.name,
|
||||
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_getTableStats from './endpoints/admin/get-table-stats.js';
|
||||
import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js';
|
||||
import * as ep___invite from './endpoints/invite.js';
|
||||
import * as ep___admin_invite_create from './endpoints/admin/invite/create.js';
|
||||
import * as ep___admin_invite_list from './endpoints/admin/invite/list.js';
|
||||
import * as ep___admin_promo_create from './endpoints/admin/promo/create.js';
|
||||
import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js';
|
||||
import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js';
|
||||
|
@ -230,6 +231,10 @@ import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
|
|||
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
|
||||
import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js';
|
||||
import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js';
|
||||
import * as ep___invite_create from './endpoints/invite/create.js';
|
||||
import * as ep___invite_delete from './endpoints/invite/delete.js';
|
||||
import * as ep___invite_list from './endpoints/invite/list.js';
|
||||
import * as ep___invite_limit from './endpoints/invite/limit.js';
|
||||
import * as ep___meta from './endpoints/meta.js';
|
||||
import * as ep___emojis from './endpoints/emojis.js';
|
||||
import * as ep___emoji from './endpoints/emoji.js';
|
||||
|
@ -378,7 +383,8 @@ const $admin_federation_updateInstance: Provider = { provide: 'ep:admin/federati
|
|||
const $admin_getIndexStats: Provider = { provide: 'ep:admin/get-index-stats', useClass: ep___admin_getIndexStats.default };
|
||||
const $admin_getTableStats: Provider = { provide: 'ep:admin/get-table-stats', useClass: ep___admin_getTableStats.default };
|
||||
const $admin_getUserIps: Provider = { provide: 'ep:admin/get-user-ips', useClass: ep___admin_getUserIps.default };
|
||||
const $invite: Provider = { provide: 'ep:invite', useClass: ep___invite.default };
|
||||
const $admin_invite_create: Provider = { provide: 'ep:admin/invite/create', useClass: ep___admin_invite_create.default };
|
||||
const $admin_invite_list: Provider = { provide: 'ep:admin/invite/list', useClass: ep___admin_invite_list.default };
|
||||
const $admin_promo_create: Provider = { provide: 'ep:admin/promo/create', useClass: ep___admin_promo_create.default };
|
||||
const $admin_queue_clear: Provider = { provide: 'ep:admin/queue/clear', useClass: ep___admin_queue_clear.default };
|
||||
const $admin_queue_deliverDelayed: Provider = { provide: 'ep:admin/queue/deliver-delayed', useClass: ep___admin_queue_deliverDelayed.default };
|
||||
|
@ -570,6 +576,10 @@ const $i_webhooks_list: Provider = { provide: 'ep:i/webhooks/list', useClass: ep
|
|||
const $i_webhooks_show: Provider = { provide: 'ep:i/webhooks/show', useClass: ep___i_webhooks_show.default };
|
||||
const $i_webhooks_update: Provider = { provide: 'ep:i/webhooks/update', useClass: ep___i_webhooks_update.default };
|
||||
const $i_webhooks_delete: Provider = { provide: 'ep:i/webhooks/delete', useClass: ep___i_webhooks_delete.default };
|
||||
const $invite_create: Provider = { provide: 'ep:invite/create', useClass: ep___invite_create.default };
|
||||
const $invite_delete: Provider = { provide: 'ep:invite/delete', useClass: ep___invite_delete.default };
|
||||
const $invite_list: Provider = { provide: 'ep:invite/list', useClass: ep___invite_list.default };
|
||||
const $invite_limit: Provider = { provide: 'ep:invite/limit', useClass: ep___invite_limit.default };
|
||||
const $meta: Provider = { provide: 'ep:meta', useClass: ep___meta.default };
|
||||
const $emojis: Provider = { provide: 'ep:emojis', useClass: ep___emojis.default };
|
||||
const $emoji: Provider = { provide: 'ep:emoji', useClass: ep___emoji.default };
|
||||
|
@ -722,7 +732,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||
$admin_getIndexStats,
|
||||
$admin_getTableStats,
|
||||
$admin_getUserIps,
|
||||
$invite,
|
||||
$admin_invite_create,
|
||||
$admin_invite_list,
|
||||
$admin_promo_create,
|
||||
$admin_queue_clear,
|
||||
$admin_queue_deliverDelayed,
|
||||
|
@ -914,6 +925,10 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||
$i_webhooks_show,
|
||||
$i_webhooks_update,
|
||||
$i_webhooks_delete,
|
||||
$invite_create,
|
||||
$invite_delete,
|
||||
$invite_list,
|
||||
$invite_limit,
|
||||
$meta,
|
||||
$emojis,
|
||||
$emoji,
|
||||
|
@ -1060,7 +1075,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||
$admin_getIndexStats,
|
||||
$admin_getTableStats,
|
||||
$admin_getUserIps,
|
||||
$invite,
|
||||
$admin_invite_create,
|
||||
$admin_invite_list,
|
||||
$admin_promo_create,
|
||||
$admin_queue_clear,
|
||||
$admin_queue_deliverDelayed,
|
||||
|
@ -1252,6 +1268,10 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||
$i_webhooks_show,
|
||||
$i_webhooks_update,
|
||||
$i_webhooks_delete,
|
||||
$invite_create,
|
||||
$invite_delete,
|
||||
$invite_list,
|
||||
$invite_limit,
|
||||
$meta,
|
||||
$emojis,
|
||||
$emoji,
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||
import bcrypt from 'bcryptjs';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
|
||||
import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository, RegistrationTicket } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { CaptchaService } from '@/core/CaptchaService.js';
|
||||
|
@ -109,13 +109,15 @@ export class SignupApiService {
|
|||
}
|
||||
}
|
||||
|
||||
let ticket: RegistrationTicket | null = null;
|
||||
|
||||
if (instance.disableRegistration) {
|
||||
if (invitationCode == null || typeof invitationCode !== 'string') {
|
||||
reply.code(400);
|
||||
return;
|
||||
}
|
||||
|
||||
const ticket = await this.registrationTicketsRepository.findOneBy({
|
||||
ticket = await this.registrationTicketsRepository.findOneBy({
|
||||
code: invitationCode,
|
||||
});
|
||||
|
||||
|
@ -124,7 +126,15 @@ export class SignupApiService {
|
|||
return;
|
||||
}
|
||||
|
||||
this.registrationTicketsRepository.delete(ticket.id);
|
||||
if (ticket.expiresAt && ticket.expiresAt < new Date()) {
|
||||
reply.code(400);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ticket.usedAt) {
|
||||
reply.code(400);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (instance.emailRequiredForSignup) {
|
||||
|
@ -148,14 +158,14 @@ export class SignupApiService {
|
|||
const salt = await bcrypt.genSalt(8);
|
||||
const hash = await bcrypt.hash(password, salt);
|
||||
|
||||
await this.userPendingsRepository.insert({
|
||||
const pendingUser = await this.userPendingsRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
code,
|
||||
email: emailAddress!,
|
||||
username: username,
|
||||
password: hash,
|
||||
});
|
||||
}).then(x => this.userPendingsRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
const link = `${this.config.url}/signup-complete/${code}`;
|
||||
|
||||
|
@ -163,6 +173,13 @@ export class SignupApiService {
|
|||
`To complete signup, please click this link:<br><a href="${link}">${link}</a>`,
|
||||
`To complete signup, please click this link: ${link}`);
|
||||
|
||||
if (ticket) {
|
||||
await this.registrationTicketsRepository.update(ticket.id, {
|
||||
usedAt: new Date(),
|
||||
pendingUserId: pendingUser.id,
|
||||
});
|
||||
}
|
||||
|
||||
reply.code(204);
|
||||
return;
|
||||
} else {
|
||||
|
@ -176,6 +193,14 @@ export class SignupApiService {
|
|||
includeSecrets: true,
|
||||
});
|
||||
|
||||
if (ticket) {
|
||||
await this.registrationTicketsRepository.update(ticket.id, {
|
||||
usedAt: new Date(),
|
||||
usedBy: account,
|
||||
usedById: account.id,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
...res,
|
||||
token: secret,
|
||||
|
@ -212,6 +237,15 @@ export class SignupApiService {
|
|||
emailVerifyCode: null,
|
||||
});
|
||||
|
||||
const ticket = await this.registrationTicketsRepository.findOneBy({ pendingUserId: pendingUser.id });
|
||||
if (ticket) {
|
||||
await this.registrationTicketsRepository.update(ticket.id, {
|
||||
usedBy: account,
|
||||
usedById: account.id,
|
||||
pendingUserId: null,
|
||||
});
|
||||
}
|
||||
|
||||
return this.signinService.signin(request, reply, account as LocalUser);
|
||||
} catch (err) {
|
||||
throw new FastifyReplyError(400, typeof err === 'string' ? err : (err as Error).toString());
|
||||
|
|
|
@ -38,7 +38,8 @@ import * as ep___admin_federation_updateInstance from './endpoints/admin/federat
|
|||
import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js';
|
||||
import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js';
|
||||
import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js';
|
||||
import * as ep___invite from './endpoints/invite.js';
|
||||
import * as ep___admin_invite_create from './endpoints/admin/invite/create.js';
|
||||
import * as ep___admin_invite_list from './endpoints/admin/invite/list.js';
|
||||
import * as ep___admin_promo_create from './endpoints/admin/promo/create.js';
|
||||
import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js';
|
||||
import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js';
|
||||
|
@ -230,6 +231,10 @@ import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
|
|||
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
|
||||
import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js';
|
||||
import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js';
|
||||
import * as ep___invite_create from './endpoints/invite/create.js';
|
||||
import * as ep___invite_delete from './endpoints/invite/delete.js';
|
||||
import * as ep___invite_list from './endpoints/invite/list.js';
|
||||
import * as ep___invite_limit from './endpoints/invite/limit.js';
|
||||
import * as ep___meta from './endpoints/meta.js';
|
||||
import * as ep___emojis from './endpoints/emojis.js';
|
||||
import * as ep___emoji from './endpoints/emoji.js';
|
||||
|
@ -376,7 +381,8 @@ const eps = [
|
|||
['admin/get-index-stats', ep___admin_getIndexStats],
|
||||
['admin/get-table-stats', ep___admin_getTableStats],
|
||||
['admin/get-user-ips', ep___admin_getUserIps],
|
||||
['invite', ep___invite],
|
||||
['admin/invite/create', ep___admin_invite_create],
|
||||
['admin/invite/list', ep___admin_invite_list],
|
||||
['admin/promo/create', ep___admin_promo_create],
|
||||
['admin/queue/clear', ep___admin_queue_clear],
|
||||
['admin/queue/deliver-delayed', ep___admin_queue_deliverDelayed],
|
||||
|
@ -568,6 +574,10 @@ const eps = [
|
|||
['i/webhooks/show', ep___i_webhooks_show],
|
||||
['i/webhooks/update', ep___i_webhooks_update],
|
||||
['i/webhooks/delete', ep___i_webhooks_delete],
|
||||
['invite/create', ep___invite_create],
|
||||
['invite/delete', ep___invite_delete],
|
||||
['invite/list', ep___invite_list],
|
||||
['invite/limit', ep___invite_limit],
|
||||
['meta', ep___meta],
|
||||
['emojis', ep___emojis],
|
||||
['emoji', ep___emoji],
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { RegistrationTicketsRepository } from '@/models/index.js';
|
||||
import { InviteCodeEntityService } from '@/core/entities/InviteCodeEntityService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { generateInviteCode } from '@/misc/generate-invite-code.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
|
||||
errors: {
|
||||
invalidDateTime: {
|
||||
message: 'Invalid date-time format',
|
||||
code: 'INVALID_DATE_TIME',
|
||||
id: 'f1380b15-3760-4c6c-a1db-5c3aaf1cbd49',
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
code: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
example: 'GR6S02ERUA5VR',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
count: { type: 'integer', minimum: 1, maximum: 100, default: 1 },
|
||||
expiresAt: { type: 'string', nullable: true },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.registrationTicketsRepository)
|
||||
private registrationTicketsRepository: RegistrationTicketsRepository,
|
||||
|
||||
private inviteCodeEntityService: InviteCodeEntityService,
|
||||
private idService: IdService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
if (ps.expiresAt && isNaN(Date.parse(ps.expiresAt))) {
|
||||
throw new ApiError(meta.errors.invalidDateTime);
|
||||
}
|
||||
|
||||
const ticketsPromises = [];
|
||||
|
||||
for (let i = 0; i < ps.count; i++) {
|
||||
ticketsPromises.push(this.registrationTicketsRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null,
|
||||
code: generateInviteCode(),
|
||||
}).then(x => this.registrationTicketsRepository.findOneByOrFail(x.identifiers[0])));
|
||||
}
|
||||
|
||||
const tickets = await Promise.all(ticketsPromises);
|
||||
return await this.inviteCodeEntityService.packMany(tickets, me);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { RegistrationTicketsRepository } from '@/models/index.js';
|
||||
import { InviteCodeEntityService } from '@/core/entities/InviteCodeEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
|
||||
offset: { type: 'integer', default: 0 },
|
||||
type: { type: 'string', enum: ['unused', 'used', 'expired', 'all'], default: 'all' },
|
||||
sort: { type: 'string', enum: ['+createdAt', '-createdAt', '+usedAt', '-usedAt'] },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.registrationTicketsRepository)
|
||||
private registrationTicketsRepository: RegistrationTicketsRepository,
|
||||
|
||||
private inviteCodeEntityService: InviteCodeEntityService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.registrationTicketsRepository.createQueryBuilder('ticket')
|
||||
.leftJoinAndSelect('ticket.createdBy', 'createdBy')
|
||||
.leftJoinAndSelect('ticket.usedBy', 'usedBy');
|
||||
|
||||
switch (ps.type) {
|
||||
case 'unused': query.andWhere('ticket.usedBy IS NULL'); break;
|
||||
case 'used': query.andWhere('ticket.usedBy IS NOT NULL'); break;
|
||||
case 'expired': query.andWhere('ticket.expiresAt < :now', { now: new Date() }); break;
|
||||
}
|
||||
|
||||
switch (ps.sort) {
|
||||
case '+createdAt': query.orderBy('ticket.createdAt', 'DESC'); break;
|
||||
case '-createdAt': query.orderBy('ticket.createdAt', 'ASC'); break;
|
||||
case '+usedAt': query.orderBy('ticket.usedAt', 'DESC', 'NULLS LAST'); break;
|
||||
case '-usedAt': query.orderBy('ticket.usedAt', 'ASC', 'NULLS FIRST'); break;
|
||||
default: query.orderBy('ticket.id', 'DESC'); break;
|
||||
}
|
||||
|
||||
query.limit(ps.limit);
|
||||
query.skip(ps.offset);
|
||||
|
||||
const tickets = await query.getMany();
|
||||
|
||||
return await this.inviteCodeEntityService.packMany(tickets, me);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,5 +1,4 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import type { Config } from '@/config.js';
|
||||
|
@ -20,6 +19,10 @@ export const meta = {
|
|||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
cacheRemoteSensitiveFiles: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
emailRequiredForSignup: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
|
@ -332,6 +335,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
enableServiceWorker: instance.enableServiceWorker,
|
||||
translatorAvailable: instance.deeplAuthKey != null,
|
||||
cacheRemoteFiles: instance.cacheRemoteFiles,
|
||||
cacheRemoteSensitiveFiles: instance.cacheRemoteSensitiveFiles,
|
||||
pinnedUsers: instance.pinnedUsers,
|
||||
hiddenTags: instance.hiddenTags,
|
||||
blockedHosts: instance.blockedHosts,
|
||||
|
|
|
@ -43,6 +43,7 @@ export const paramDef = {
|
|||
defaultLightTheme: { type: 'string', nullable: true },
|
||||
defaultDarkTheme: { type: 'string', nullable: true },
|
||||
cacheRemoteFiles: { type: 'boolean' },
|
||||
cacheRemoteSensitiveFiles: { type: 'boolean' },
|
||||
emailRequiredForSignup: { type: 'boolean' },
|
||||
enableHcaptcha: { type: 'boolean' },
|
||||
hcaptchaSiteKey: { type: 'string', nullable: true },
|
||||
|
@ -193,6 +194,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
set.cacheRemoteFiles = ps.cacheRemoteFiles;
|
||||
}
|
||||
|
||||
if (ps.cacheRemoteSensitiveFiles !== undefined) {
|
||||
set.cacheRemoteSensitiveFiles = ps.cacheRemoteSensitiveFiles;
|
||||
}
|
||||
|
||||
if (ps.emailRequiredForSignup !== undefined) {
|
||||
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 { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { AppsRepository, AuthSessionsRepository } from '@/models/index.js';
|
||||
|
@ -71,7 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
}
|
||||
|
||||
// Generate token
|
||||
const token = uuid();
|
||||
const token = randomUUID();
|
||||
|
||||
// Create session token document
|
||||
const doc = await this.authSessionsRepository.insert({
|
||||
|
|
82
packages/backend/src/server/api/endpoints/invite/create.ts
Normal file
82
packages/backend/src/server/api/endpoints/invite/create.ts
Normal file
|
@ -0,0 +1,82 @@
|
|||
import { MoreThan } from 'typeorm';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { RegistrationTicketsRepository } from '@/models/index.js';
|
||||
import { InviteCodeEntityService } from '@/core/entities/InviteCodeEntityService.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { generateInviteCode } from '@/misc/generate-invite-code.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['meta'],
|
||||
|
||||
requireCredential: true,
|
||||
requireRolePolicy: 'canInvite',
|
||||
|
||||
errors: {
|
||||
exceededCreateLimit: {
|
||||
message: 'You have exceeded the limit for creating an invitation code.',
|
||||
code: 'EXCEEDED_LIMIT_OF_CREATE_INVITE_CODE',
|
||||
id: '8b165dd3-6f37-4557-8db1-73175d63c641',
|
||||
},
|
||||
},
|
||||
|
||||
res: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
code: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
example: 'GR6S02ERUA5VR',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.registrationTicketsRepository)
|
||||
private registrationTicketsRepository: RegistrationTicketsRepository,
|
||||
|
||||
private inviteCodeEntityService: InviteCodeEntityService,
|
||||
private idService: IdService,
|
||||
private roleService: RoleService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const policies = await this.roleService.getUserPolicies(me.id);
|
||||
|
||||
if (policies.inviteLimit) {
|
||||
const count = await this.registrationTicketsRepository.countBy({
|
||||
createdAt: MoreThan(new Date(Date.now() - (policies.inviteLimitCycle * 1000 * 60))),
|
||||
createdById: me.id,
|
||||
});
|
||||
|
||||
if (count >= policies.inviteLimit) {
|
||||
throw new ApiError(meta.errors.exceededCreateLimit);
|
||||
}
|
||||
}
|
||||
|
||||
const ticket = await this.registrationTicketsRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
createdBy: me,
|
||||
createdById: me.id,
|
||||
expiresAt: policies.inviteExpirationTime ? new Date(Date.now() + (policies.inviteExpirationTime * 1000 * 60)) : null,
|
||||
code: generateInviteCode(),
|
||||
}).then(x => this.registrationTicketsRepository.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
return await this.inviteCodeEntityService.pack(ticket, me);
|
||||
});
|
||||
}
|
||||
}
|
71
packages/backend/src/server/api/endpoints/invite/delete.ts
Normal file
71
packages/backend/src/server/api/endpoints/invite/delete.ts
Normal file
|
@ -0,0 +1,71 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { RegistrationTicketsRepository } from '@/models/index.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['meta'],
|
||||
|
||||
requireCredential: true,
|
||||
requireRolePolicy: 'canInvite',
|
||||
|
||||
errors: {
|
||||
noSuchCode: {
|
||||
message: 'No such invite code.',
|
||||
code: 'NO_SUCH_INVITE_CODE',
|
||||
id: 'cd4f9ae4-7854-4e3e-8df9-c296f051e634',
|
||||
},
|
||||
|
||||
cantDelete: {
|
||||
message: 'You can\'t delete this invite code.',
|
||||
code: 'CAN_NOT_DELETE_INVITE_CODE',
|
||||
id: 'ff17af39-000c-4d4e-abdf-848fa30fc1ce',
|
||||
},
|
||||
|
||||
accessDenied: {
|
||||
message: 'Access denied.',
|
||||
code: 'ACCESS_DENIED',
|
||||
id: '5eb8d909-2540-4970-90b8-dd6f86088121',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
inviteId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['inviteId'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.registrationTicketsRepository)
|
||||
private registrationTicketsRepository: RegistrationTicketsRepository,
|
||||
|
||||
private roleService: RoleService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const ticket = await this.registrationTicketsRepository.findOneBy({ id: ps.inviteId });
|
||||
const isModerator = await this.roleService.isModerator(me);
|
||||
|
||||
if (ticket == null) {
|
||||
throw new ApiError(meta.errors.noSuchCode);
|
||||
}
|
||||
|
||||
if (ticket.createdById !== me.id && !isModerator) {
|
||||
throw new ApiError(meta.errors.accessDenied);
|
||||
}
|
||||
|
||||
if (ticket.usedAt && !isModerator) {
|
||||
throw new ApiError(meta.errors.cantDelete);
|
||||
}
|
||||
|
||||
await this.registrationTicketsRepository.delete(ticket.id);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { MoreThan } from 'typeorm';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { RegistrationTicketsRepository } from '@/models/index.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['meta'],
|
||||
|
@ -15,12 +15,9 @@ export const meta = {
|
|||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
properties: {
|
||||
code: {
|
||||
type: 'string',
|
||||
optional: false, nullable: false,
|
||||
example: '2ERUA5VR',
|
||||
maxLength: 8,
|
||||
minLength: 8,
|
||||
remaining: {
|
||||
type: 'integer',
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -39,21 +36,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
@Inject(DI.registrationTicketsRepository)
|
||||
private registrationTicketsRepository: RegistrationTicketsRepository,
|
||||
|
||||
private idService: IdService,
|
||||
private roleService: RoleService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const code = secureRndstr(8, {
|
||||
chars: '23456789ABCDEFGHJKLMNPQRSTUVWXYZ', // [0-9A-Z] w/o [01IO] (32 patterns)
|
||||
});
|
||||
const policies = await this.roleService.getUserPolicies(me.id);
|
||||
|
||||
await this.registrationTicketsRepository.insert({
|
||||
id: this.idService.genId(),
|
||||
createdAt: new Date(),
|
||||
code,
|
||||
});
|
||||
const count = policies.inviteLimit ? await this.registrationTicketsRepository.countBy({
|
||||
createdAt: MoreThan(new Date(Date.now() - (policies.inviteExpirationTime * 60 * 1000))),
|
||||
createdById: me.id,
|
||||
}) : null;
|
||||
|
||||
return {
|
||||
code,
|
||||
remaining: count !== null ? Math.max(0, policies.inviteLimit - count) : null,
|
||||
};
|
||||
});
|
||||
}
|
58
packages/backend/src/server/api/endpoints/invite/list.ts
Normal file
58
packages/backend/src/server/api/endpoints/invite/list.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import type { RegistrationTicketsRepository } from '@/models/index.js';
|
||||
import { InviteCodeEntityService } from '@/core/entities/InviteCodeEntityService.js';
|
||||
import { QueryService } from '@/core/QueryService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['meta'],
|
||||
|
||||
requireCredential: true,
|
||||
requireRolePolicy: 'canInvite',
|
||||
|
||||
res: {
|
||||
type: 'array',
|
||||
optional: false, nullable: false,
|
||||
items: {
|
||||
type: 'object',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
@Inject(DI.registrationTicketsRepository)
|
||||
private registrationTicketsRepository: RegistrationTicketsRepository,
|
||||
|
||||
private inviteCodeEntityService: InviteCodeEntityService,
|
||||
private queryService: QueryService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const query = this.queryService.makePaginationQuery(this.registrationTicketsRepository.createQueryBuilder('ticket'), ps.sinceId, ps.untilId)
|
||||
.andWhere('ticket.createdById = :meId', { meId: me.id })
|
||||
.leftJoinAndSelect('ticket.createdBy', 'createdBy')
|
||||
.leftJoinAndSelect('ticket.usedBy', 'usedBy');
|
||||
|
||||
const tickets = await query
|
||||
.limit(ps.limit)
|
||||
.getMany();
|
||||
|
||||
return await this.inviteCodeEntityService.packMany(tickets, me);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -83,6 +83,10 @@ export const meta = {
|
|||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
cacheRemoteSensitiveFiles: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
},
|
||||
emailRequiredForSignup: {
|
||||
type: 'boolean',
|
||||
optional: false, nullable: false,
|
||||
|
@ -329,6 +333,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
|
||||
...(ps.detail ? {
|
||||
cacheRemoteFiles: instance.cacheRemoteFiles,
|
||||
cacheRemoteSensitiveFiles: instance.cacheRemoteSensitiveFiles,
|
||||
requireSetup: (await this.usersRepository.countBy({
|
||||
host: IsNull(),
|
||||
})) === 0,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { randomUUID } from 'node:crypto';
|
||||
import { dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { createBullBoard } from '@bull-board/api';
|
||||
import { BullAdapter } from '@bull-board/api/bullAdapter.js';
|
||||
import { FastifyAdapter } from '@bull-board/fastify';
|
||||
|
@ -676,7 +676,7 @@ export class ClientServerService {
|
|||
});
|
||||
|
||||
fastify.setErrorHandler(async (error, request, reply) => {
|
||||
const errId = uuid();
|
||||
const errId = randomUUID();
|
||||
this.clientLoggerService.logger.error(`Internal error occured in ${request.routerPath}: ${error.message}`, {
|
||||
path: request.routerPath,
|
||||
params: request.params,
|
||||
|
|
|
@ -35,7 +35,7 @@ html
|
|||
link(rel='prefetch' href=infoImageUrl)
|
||||
link(rel='prefetch' href=notFoundImageUrl)
|
||||
//- 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}`)
|
||||
|
||||
if !config.clientManifestExists
|
||||
|
|
|
@ -4,6 +4,7 @@ import * as assert from 'assert';
|
|||
import { Test } from '@nestjs/testing';
|
||||
import { jest } from '@jest/globals';
|
||||
|
||||
import { ApImageService } from '@/core/activitypub/models/ApImageService.js';
|
||||
import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js';
|
||||
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
|
||||
import { ApRendererService } from '@/core/activitypub/ApRendererService.js';
|
||||
|
@ -11,9 +12,12 @@ import { GlobalModule } from '@/GlobalModule.js';
|
|||
import { CoreModule } from '@/core/CoreModule.js';
|
||||
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
|
||||
import { LoggerService } from '@/core/LoggerService.js';
|
||||
import type { IActivity, IActor, ICollection, IObject, IOrderedCollection, IOrderedCollectionPage, IPost } from '@/core/activitypub/type.js';
|
||||
import { Note } from '@/models/index.js';
|
||||
import type { IActivity, IApDocument, IActor, ICollection, IObject, IOrderedCollection, IOrderedCollectionPage, IPost } from '@/core/activitypub/type.js';
|
||||
import { Meta, Note } from '@/models/index.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';
|
||||
|
||||
const host = 'https://host1.test';
|
||||
|
@ -120,15 +124,46 @@ 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', () => {
|
||||
let imageService: ApImageService;
|
||||
let noteService: ApNoteService;
|
||||
let personService: ApPersonService;
|
||||
let rendererService: ApRendererService;
|
||||
let resolver: MockResolver;
|
||||
|
||||
const metaInitial = {
|
||||
cacheRemoteFiles: true,
|
||||
cacheRemoteSensitiveFiles: true,
|
||||
blockedHosts: [] as string[],
|
||||
sensitiveWords: [] as string[],
|
||||
} as Meta;
|
||||
let meta = metaInitial;
|
||||
|
||||
beforeAll(async () => {
|
||||
const app = await Test.createTestingModule({
|
||||
imports: [GlobalModule, CoreModule],
|
||||
})
|
||||
.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();
|
||||
|
@ -137,6 +172,7 @@ describe('ActivityPub', () => {
|
|||
noteService = app.get<ApNoteService>(ApNoteService);
|
||||
personService = app.get<ApPersonService>(ApPersonService);
|
||||
rendererService = app.get<ApRendererService>(ApRendererService);
|
||||
imageService = app.get<ApImageService>(ApImageService);
|
||||
resolver = new MockResolver(await app.resolve<LoggerService>(LoggerService));
|
||||
|
||||
// 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,
|
||||
};
|
||||
}
|
||||
|
||||
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/MkUserSetupDialog.vue'),
|
||||
glob('src/components/MkUserSetupDialog.*.vue'),
|
||||
glob('src/components/MkInviteCode.vue'),
|
||||
glob('src/pages/user/home.vue'),
|
||||
]);
|
||||
const components = globs.flat();
|
||||
|
|
|
@ -20,9 +20,9 @@
|
|||
"@rollup/plugin-replace": "5.0.2",
|
||||
"@rollup/pluginutils": "5.0.2",
|
||||
"@syuilo/aiscript": "0.13.3",
|
||||
"@tabler/icons-webfont": "2.24.0",
|
||||
"@tabler/icons-webfont": "2.25.0",
|
||||
"@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",
|
||||
"astring": "1.8.6",
|
||||
"autosize": "6.0.1",
|
||||
|
@ -70,30 +70,30 @@
|
|||
"typescript": "5.1.6",
|
||||
"uuid": "9.0.0",
|
||||
"vanilla-tilt": "1.8.0",
|
||||
"vite": "4.4.1",
|
||||
"vite": "4.4.4",
|
||||
"vue": "3.3.4",
|
||||
"vue-prism-editor": "2.0.0-alpha.2",
|
||||
"vuedraggable": "next"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-actions": "7.0.26",
|
||||
"@storybook/addon-essentials": "7.0.26",
|
||||
"@storybook/addon-interactions": "7.0.26",
|
||||
"@storybook/addon-links": "7.0.26",
|
||||
"@storybook/addon-storysource": "7.0.26",
|
||||
"@storybook/addons": "7.0.26",
|
||||
"@storybook/blocks": "7.0.26",
|
||||
"@storybook/core-events": "7.0.26",
|
||||
"@storybook/addon-actions": "7.0.27",
|
||||
"@storybook/addon-essentials": "7.0.27",
|
||||
"@storybook/addon-interactions": "7.0.27",
|
||||
"@storybook/addon-links": "7.0.27",
|
||||
"@storybook/addon-storysource": "7.0.27",
|
||||
"@storybook/addons": "7.0.27",
|
||||
"@storybook/blocks": "7.0.27",
|
||||
"@storybook/core-events": "7.0.27",
|
||||
"@storybook/jest": "0.1.0",
|
||||
"@storybook/manager-api": "7.0.26",
|
||||
"@storybook/preview-api": "7.0.26",
|
||||
"@storybook/react": "7.0.26",
|
||||
"@storybook/react-vite": "7.0.26",
|
||||
"@storybook/manager-api": "7.0.27",
|
||||
"@storybook/preview-api": "7.0.27",
|
||||
"@storybook/react": "7.0.27",
|
||||
"@storybook/react-vite": "7.0.27",
|
||||
"@storybook/testing-library": "0.2.0",
|
||||
"@storybook/theming": "7.0.26",
|
||||
"@storybook/types": "7.0.26",
|
||||
"@storybook/vue3": "7.0.26",
|
||||
"@storybook/vue3-vite": "7.0.26",
|
||||
"@storybook/theming": "7.0.27",
|
||||
"@storybook/types": "7.0.27",
|
||||
"@storybook/vue3": "7.0.27",
|
||||
"@storybook/vue3-vite": "7.0.27",
|
||||
"@testing-library/jest-dom": "5.16.5",
|
||||
"@testing-library/vue": "7.0.0",
|
||||
"@types/escape-regexp": "0.0.1",
|
||||
|
@ -102,10 +102,10 @@
|
|||
"@types/gulp-rename": "2.0.2",
|
||||
"@types/matter-js": "0.18.5",
|
||||
"@types/micromatch": "4.0.2",
|
||||
"@types/node": "20.4.0",
|
||||
"@types/node": "20.4.2",
|
||||
"@types/punycode": "2.1.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/tinycolor2": "1.4.3",
|
||||
"@types/uuid": "9.0.2",
|
||||
|
@ -118,12 +118,12 @@
|
|||
"acorn": "8.10.0",
|
||||
"chokidar-cli": "3.0.0",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "12.17.0",
|
||||
"eslint": "8.44.0",
|
||||
"cypress": "12.17.1",
|
||||
"eslint": "8.45.0",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"eslint-plugin-vue": "9.15.1",
|
||||
"fast-glob": "3.3.0",
|
||||
"happy-dom": "10.0.3",
|
||||
"happy-dom": "10.3.2",
|
||||
"micromatch": "4.0.5",
|
||||
"msw": "1.2.2",
|
||||
"msw-storybook-addon": "1.8.0",
|
||||
|
@ -131,13 +131,13 @@
|
|||
"react": "18.2.0",
|
||||
"react-dom": "18.2.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",
|
||||
"summaly": "github:misskey-dev/summaly",
|
||||
"vite-plugin-turbosnap": "1.0.2",
|
||||
"vitest": "0.33.0",
|
||||
"vitest-fetch-mock": "0.2.2",
|
||||
"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>;
|
123
packages/frontend/src/components/MkInviteCode.vue
Normal file
123
packages/frontend/src/components/MkInviteCode.vue
Normal file
|
@ -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',
|
||||
'canPublicNote',
|
||||
'canInvite',
|
||||
'inviteLimit',
|
||||
'inviteLimitCycle',
|
||||
'inviteExpirationTime',
|
||||
'canManageCustomEmojis',
|
||||
'canSearchNotes',
|
||||
'canHideAds',
|
||||
|
|
|
@ -80,7 +80,7 @@ const menuDef = $computed(() => [{
|
|||
}, ...(instance.disableRegistration ? [{
|
||||
type: 'button',
|
||||
icon: 'ti ti-user-plus',
|
||||
text: i18n.ts.invite,
|
||||
text: i18n.ts.createInviteCode,
|
||||
action: invite,
|
||||
}] : [])],
|
||||
}, {
|
||||
|
@ -95,6 +95,11 @@ const menuDef = $computed(() => [{
|
|||
text: i18n.ts.users,
|
||||
to: '/admin/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',
|
||||
text: i18n.ts.roles,
|
||||
|
@ -240,10 +245,10 @@ provideMetadataReceiver((info) => {
|
|||
});
|
||||
|
||||
const invite = () => {
|
||||
os.api('invite').then(x => {
|
||||
os.api('admin/invite/create').then(x => {
|
||||
os.alert({
|
||||
type: 'info',
|
||||
text: x.code,
|
||||
text: x?.[0].code,
|
||||
});
|
||||
}).catch(err => {
|
||||
os.alert({
|
||||
|
|
126
packages/frontend/src/pages/admin/invites.vue
Normal file
126
packages/frontend/src/pages/admin/invites.vue
Normal file
|
@ -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>
|
||||
</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'])">
|
||||
<template #label>{{ i18n.ts._role._options.canManageCustomEmojis }}</template>
|
||||
<template #suffix>
|
||||
|
|
|
@ -51,6 +51,29 @@
|
|||
</MkSwitch>
|
||||
</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'])">
|
||||
<template #label>{{ i18n.ts._role._options.canManageCustomEmojis }}</template>
|
||||
<template #suffix>{{ policies.canManageCustomEmojis ? i18n.ts.yes : i18n.ts.no }}</template>
|
||||
|
|
|
@ -37,6 +37,13 @@
|
|||
<template #label>{{ i18n.ts.cacheRemoteFiles }}</template>
|
||||
<template #caption>{{ i18n.ts.cacheRemoteFilesDescription }}</template>
|
||||
</MkSwitch>
|
||||
|
||||
<template v-if="cacheRemoteFiles">
|
||||
<MkSwitch v-model="cacheRemoteSensitiveFiles">
|
||||
<template #label>{{ i18n.ts.cacheRemoteSensitiveFiles }}</template>
|
||||
<template #caption>{{ i18n.ts.cacheRemoteSensitiveFilesDescription }}</template>
|
||||
</MkSwitch>
|
||||
</template>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
|
@ -104,7 +111,6 @@ import { fetchInstance } from '@/instance';
|
|||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkColorInput from '@/components/MkColorInput.vue';
|
||||
|
||||
let name: 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 pinnedUsers: string = $ref('');
|
||||
let cacheRemoteFiles: boolean = $ref(false);
|
||||
let cacheRemoteSensitiveFiles: boolean = $ref(false);
|
||||
let enableServiceWorker: boolean = $ref(false);
|
||||
let swPublicKey: any = $ref(null);
|
||||
let swPrivateKey: any = $ref(null);
|
||||
let deeplAuthKey: string = $ref('');
|
||||
let deeplIsPro: boolean = $ref(false);
|
||||
|
||||
async function init() {
|
||||
async function init(): Promise<void> {
|
||||
const meta = await os.api('admin/meta');
|
||||
name = meta.name;
|
||||
description = meta.description;
|
||||
|
@ -126,6 +133,7 @@ async function init() {
|
|||
maintainerEmail = meta.maintainerEmail;
|
||||
pinnedUsers = meta.pinnedUsers.join('\n');
|
||||
cacheRemoteFiles = meta.cacheRemoteFiles;
|
||||
cacheRemoteSensitiveFiles = meta.cacheRemoteSensitiveFiles;
|
||||
enableServiceWorker = meta.enableServiceWorker;
|
||||
swPublicKey = meta.swPublickey;
|
||||
swPrivateKey = meta.swPrivateKey;
|
||||
|
@ -133,7 +141,7 @@ async function init() {
|
|||
deeplIsPro = meta.deeplIsPro;
|
||||
}
|
||||
|
||||
function save() {
|
||||
function save(): void {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
name,
|
||||
description,
|
||||
|
@ -141,6 +149,7 @@ function save() {
|
|||
maintainerEmail,
|
||||
pinnedUsers: pinnedUsers.split('\n'),
|
||||
cacheRemoteFiles,
|
||||
cacheRemoteSensitiveFiles,
|
||||
enableServiceWorker,
|
||||
swPublicKey,
|
||||
swPrivateKey,
|
||||
|
|
114
packages/frontend/src/pages/invite.vue
Normal file
114
packages/frontend/src/pages/invite.vue
Normal file
|
@ -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">
|
||||
<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"/>
|
||||
</MkA>
|
||||
</div>
|
||||
|
@ -32,6 +32,7 @@ import { i18n } from '@/i18n';
|
|||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import { userListsCache } from '@/cache';
|
||||
import { infoImageUrl } from '@/instance';
|
||||
import { $i } from '@/account';
|
||||
|
||||
const items = $computed(() => userListsCache.value.value ?? []);
|
||||
|
||||
|
@ -66,10 +67,6 @@ const headerTabs = $computed(() => []);
|
|||
definePageMetadata({
|
||||
title: i18n.ts.manageLists,
|
||||
icon: 'ti ti-list',
|
||||
action: {
|
||||
icon: 'ti ti-plus',
|
||||
handler: create,
|
||||
},
|
||||
});
|
||||
|
||||
onActivated(() => {
|
||||
|
@ -90,4 +87,9 @@ onActivated(() => {
|
|||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.nUsers {
|
||||
font-size: .9em;
|
||||
opacity: .7;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
|
||||
<MkFolder defaultOpen>
|
||||
<template #label>{{ i18n.ts.members }}</template>
|
||||
<template #caption>{{ i18n.t('nUsers', { n: `${list.userIds.length}/${$i?.policies['userEachUserListsLimit']}` }) }}</template>
|
||||
|
||||
<div class="_gaps_s">
|
||||
<MkButton rounded primary style="margin: 0 auto;" @click="addUser()">{{ i18n.ts.addUser }}</MkButton>
|
||||
|
@ -29,6 +30,10 @@
|
|||
</MkA>
|
||||
<button class="_button" :class="$style.remove" @click="removeUser(user, $event)"><i class="ti ti-x"></i></button>
|
||||
</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>
|
||||
</MkFolder>
|
||||
</div>
|
||||
|
@ -49,34 +54,57 @@ import MkSwitch from '@/components/MkSwitch.vue';
|
|||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
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<{
|
||||
listId: string;
|
||||
}>();
|
||||
|
||||
let list = $ref(null);
|
||||
let users = $ref([]);
|
||||
const FETCH_USERS_LIMIT = 20;
|
||||
|
||||
let list = $ref<UserList | null>(null);
|
||||
let users = $ref<UserLite[]>([]);
|
||||
let queueUserIds = $ref<string[]>([]);
|
||||
let fetching = $ref(true);
|
||||
const isPublic = ref(false);
|
||||
const name = ref('');
|
||||
|
||||
function fetchList() {
|
||||
fetching = true;
|
||||
os.api('users/lists/show', {
|
||||
listId: props.listId,
|
||||
}).then(_list => {
|
||||
list = _list;
|
||||
name.value = list.name;
|
||||
isPublic.value = list.isPublic;
|
||||
queueUserIds = list.userIds;
|
||||
|
||||
os.api('users/show', {
|
||||
userIds: list.userIds,
|
||||
}).then(_users => {
|
||||
users = _users;
|
||||
return fetchMoreUsers();
|
||||
});
|
||||
}
|
||||
|
||||
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() {
|
||||
os.selectUser().then(user => {
|
||||
if (!list) return;
|
||||
os.apiWithDialog('users/lists/push', {
|
||||
listId: list.id,
|
||||
userId: user.id,
|
||||
|
@ -92,6 +120,7 @@ async function removeUser(user, ev) {
|
|||
icon: 'ti ti-x',
|
||||
danger: true,
|
||||
action: async () => {
|
||||
if (!list) return;
|
||||
os.api('users/lists/pull', {
|
||||
listId: list.id,
|
||||
userId: user.id,
|
||||
|
@ -103,6 +132,7 @@ async function removeUser(user, ev) {
|
|||
}
|
||||
|
||||
async function deleteList() {
|
||||
if (!list) return;
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.t('removeAreYouSure', { x: list.name }),
|
||||
|
@ -117,6 +147,7 @@ async function deleteList() {
|
|||
}
|
||||
|
||||
async function updateSettings() {
|
||||
if (!list) return;
|
||||
await os.apiWithDialog('users/lists/update', {
|
||||
listId: list.id,
|
||||
name: name.value,
|
||||
|
@ -166,6 +197,11 @@ definePageMetadata(computed(() => list ? {
|
|||
align-self: center;
|
||||
}
|
||||
|
||||
.more {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
|
|
|
@ -201,6 +201,10 @@ export const routes = [{
|
|||
}, {
|
||||
path: '/about-misskey',
|
||||
component: page(() => import('./pages/about-misskey.vue')),
|
||||
}, {
|
||||
path: '/invite',
|
||||
name: 'invite',
|
||||
component: page(() => import('./pages/invite.vue')),
|
||||
}, {
|
||||
path: '/ads',
|
||||
component: page(() => import('./pages/ads.vue')),
|
||||
|
@ -428,6 +432,10 @@ export const routes = [{
|
|||
path: '/server-rules',
|
||||
name: 'server-rules',
|
||||
component: page(() => import('./pages/admin/server-rules.vue')),
|
||||
}, {
|
||||
path: '/invites',
|
||||
name: 'invites',
|
||||
component: page(() => import('./pages/admin/invites.vue')),
|
||||
}, {
|
||||
path: '/',
|
||||
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) {
|
||||
const masterVolume = soundConfigStore.state.sound_masterVolume;
|
||||
if (masterVolume === 0) return;
|
||||
|
||||
const audio = setVolume(getAudio(file), volume);
|
||||
if (audio.volume === 0) return;
|
||||
audio.play();
|
||||
}
|
||||
|
|
|
@ -33,7 +33,12 @@ export function openInstanceMenu(ev: MouseEvent) {
|
|||
text: i18n.ts.ads,
|
||||
icon: 'ti ti-ad',
|
||||
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',
|
||||
text: i18n.ts.tools,
|
||||
icon: 'ti ti-tool',
|
||||
|
@ -52,23 +57,7 @@ export function openInstanceMenu(ev: MouseEvent) {
|
|||
to: '/clicker',
|
||||
text: '🍪👈',
|
||||
icon: 'ti ti-cookie',
|
||||
}, ($i && ($i.isAdmin || $i.policies.canInvite) && instance.disableRegistration) ? {
|
||||
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)) ? {
|
||||
}, ($i && ($i.isAdmin || $i.policies.canManageCustomEmojis)) ? {
|
||||
type: 'link',
|
||||
to: '/custom-emojis-manager',
|
||||
text: i18n.ts.manageCustomEmojis,
|
||||
|
|
|
@ -271,6 +271,7 @@ type DetailedInstanceMetadata = LiteInstanceMetadata & {
|
|||
pinnedPages: string[];
|
||||
pinnedClipId: string | null;
|
||||
cacheRemoteFiles: boolean;
|
||||
cacheRemoteSensitiveFiles: boolean;
|
||||
requireSetup: boolean;
|
||||
proxyAccountName: string | null;
|
||||
features: Record<string, any>;
|
||||
|
@ -327,6 +328,10 @@ export type Endpoints = {
|
|||
req: TODO;
|
||||
res: TODO;
|
||||
};
|
||||
'admin/meta': {
|
||||
req: TODO;
|
||||
res: TODO;
|
||||
};
|
||||
'admin/reset-password': {
|
||||
req: TODO;
|
||||
res: TODO;
|
||||
|
@ -481,6 +486,14 @@ export type Endpoints = {
|
|||
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;
|
||||
|
@ -1549,6 +1562,28 @@ export type Endpoints = {
|
|||
req: 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': {
|
||||
req: {
|
||||
limit?: number;
|
||||
|
@ -2210,6 +2245,8 @@ declare namespace entities {
|
|||
Blocking,
|
||||
Instance,
|
||||
Signin,
|
||||
Invite,
|
||||
InviteLimit,
|
||||
UserSorting,
|
||||
OriginType
|
||||
}
|
||||
|
@ -2310,6 +2347,23 @@ type Instance = {
|
|||
// @public (undocumented)
|
||||
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)
|
||||
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: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
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
|
|
|
@ -20,13 +20,13 @@
|
|||
"url": "git+https://github.com/misskey-dev/misskey.js.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/api-extractor": "7.36.1",
|
||||
"@microsoft/api-extractor": "7.36.2",
|
||||
"@swc/jest": "0.2.26",
|
||||
"@types/jest": "29.5.2",
|
||||
"@types/node": "20.4.0",
|
||||
"@types/jest": "29.5.3",
|
||||
"@types/node": "20.4.2",
|
||||
"@typescript-eslint/eslint-plugin": "5.61.0",
|
||||
"@typescript-eslint/parser": "5.61.0",
|
||||
"eslint": "8.44.0",
|
||||
"eslint": "8.45.0",
|
||||
"jest": "29.6.1",
|
||||
"jest-fetch-mock": "3.0.3",
|
||||
"jest-websocket-mock": "2.4.0",
|
||||
|
@ -39,7 +39,7 @@
|
|||
],
|
||||
"dependencies": {
|
||||
"@swc/cli": "0.1.62",
|
||||
"@swc/core": "1.3.68",
|
||||
"@swc/core": "1.3.69",
|
||||
"eventemitter3": "5.0.1",
|
||||
"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,
|
||||
LiteInstanceMetadata,
|
||||
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';
|
||||
|
||||
type TODO = Record<string, any> | null;
|
||||
|
@ -20,6 +20,7 @@ export type Endpoints = {
|
|||
'admin/get-table-stats': { req: TODO; res: TODO; };
|
||||
'admin/invite': { req: TODO; res: TODO; };
|
||||
'admin/logs': { req: TODO; res: TODO; };
|
||||
'admin/meta': { req: TODO; res: TODO; };
|
||||
'admin/reset-password': { req: TODO; res: TODO; };
|
||||
'admin/resolve-abuse-user-report': { 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/remove-all-following': { 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/remove': { 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/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/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[]; };
|
||||
|
|
|
@ -338,6 +338,7 @@ export type DetailedInstanceMetadata = LiteInstanceMetadata & {
|
|||
pinnedPages: string[];
|
||||
pinnedClipId: string | null;
|
||||
cacheRemoteFiles: boolean;
|
||||
cacheRemoteSensitiveFiles: boolean;
|
||||
requireSetup: boolean;
|
||||
proxyAccountName: string | null;
|
||||
features: Record<string, any>;
|
||||
|
@ -516,6 +517,21 @@ export type Signin = {
|
|||
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 =
|
||||
| '+follower'
|
||||
| '-follower'
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
"devDependencies": {
|
||||
"@typescript-eslint/parser": "5.61.0",
|
||||
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.67",
|
||||
"eslint": "8.44.0",
|
||||
"eslint": "8.45.0",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"typescript": "5.1.6"
|
||||
},
|
||||
|
|
|
@ -199,6 +199,13 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
|
|||
tag: `achievement:${data.body.achievement}`,
|
||||
}];
|
||||
|
||||
case 'pollEnded':
|
||||
return [t('_notification.pollEnded'), {
|
||||
body: data.body.note.text ?? '',
|
||||
badge: iconUrl('chart-arrows'),
|
||||
data,
|
||||
}];
|
||||
|
||||
case 'app':
|
||||
return [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 a new issue