Merge branch 'develop' into pizzax-indexeddb
This commit is contained in:
		@@ -16,6 +16,17 @@ module.exports = {
 | 
			
		||||
					'position': 'after'
 | 
			
		||||
				}
 | 
			
		||||
			],
 | 
			
		||||
		}]
 | 
			
		||||
		}],
 | 
			
		||||
		'no-restricted-globals': [
 | 
			
		||||
			'error',
 | 
			
		||||
			{
 | 
			
		||||
				'name': '__dirname',
 | 
			
		||||
				'message': 'Not in ESModule. Use `import.meta.url` instead.'
 | 
			
		||||
			},
 | 
			
		||||
			{
 | 
			
		||||
				'name': '__filename',
 | 
			
		||||
				'message': 'Not in ESModule. Use `import.meta.url` instead.'
 | 
			
		||||
			}
 | 
			
		||||
	]
 | 
			
		||||
	},
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,6 @@
 | 
			
		||||
		"loader=./test/loader.js"
 | 
			
		||||
	],
 | 
			
		||||
	"slow": 1000,
 | 
			
		||||
	"timeout": 35000,
 | 
			
		||||
	"timeout": 3000,
 | 
			
		||||
	"exit": true
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										89
									
								
								packages/backend/migration/1651224615271-foreign-key.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								packages/backend/migration/1651224615271-foreign-key.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,89 @@
 | 
			
		||||
export class foreignKeyReports1651224615271 {
 | 
			
		||||
    name = 'foreignKeyReports1651224615271'
 | 
			
		||||
 | 
			
		||||
    async up(queryRunner) {
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            queryRunner.query(`ALTER INDEX "public"."IDX_seoignmeoprigmkpodgrjmkpormg" RENAME TO "IDX_c8cc87bd0f2f4487d17c651fbf"`),
 | 
			
		||||
            queryRunner.query(`DROP INDEX "public"."IDX_note_on_channelId_and_id_desc"`),
 | 
			
		||||
 | 
			
		||||
            // remove unnecessary default null, see also down
 | 
			
		||||
            queryRunner.query(`ALTER TABLE "user" ALTER COLUMN "followersUri" DROP DEFAULT`),
 | 
			
		||||
            queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "session" DROP DEFAULT`),
 | 
			
		||||
            queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "appId" DROP DEFAULT`),
 | 
			
		||||
            queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "name" DROP DEFAULT`),
 | 
			
		||||
            queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "description" DROP DEFAULT`),
 | 
			
		||||
            queryRunner.query(`ALTER TABLE "access_token" ALTER COLUMN "iconUrl" DROP DEFAULT`),
 | 
			
		||||
            queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "softwareName" DROP DEFAULT`),
 | 
			
		||||
            queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "softwareVersion" DROP DEFAULT`),
 | 
			
		||||
            queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "name" DROP DEFAULT`),
 | 
			
		||||
            queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "description" DROP DEFAULT`),
 | 
			
		||||
            queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "maintainerName" DROP DEFAULT`),
 | 
			
		||||
            queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "maintainerEmail" DROP DEFAULT`),
 | 
			
		||||
            queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "iconUrl" DROP DEFAULT`),
 | 
			
		||||
            queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "faviconUrl" DROP DEFAULT`),
 | 
			
		||||
            queryRunner.query(`ALTER TABLE "instance" ALTER COLUMN "themeColor" DROP DEFAULT`),
 | 
			
		||||
            queryRunner.query(`ALTER TABLE "clip" ALTER COLUMN "description" DROP DEFAULT`),
 | 
			
		||||
            queryRunner.query(`ALTER TABLE "note" ALTER COLUMN "channelId" DROP DEFAULT`),
 | 
			
		||||
            queryRunner.query(`ALTER TABLE "abuse_user_report" ALTER COLUMN "comment" DROP DEFAULT`),
 | 
			
		||||
 | 
			
		||||
            queryRunner.query(`CREATE INDEX "IDX_315c779174fe8247ab324f036e" ON "drive_file" ("isLink")`),
 | 
			
		||||
            queryRunner.query(`CREATE INDEX "IDX_f22169eb10657bded6d875ac8f" ON "note" ("channelId")`),
 | 
			
		||||
            queryRunner.query(`CREATE INDEX "IDX_a9021cc2e1feb5f72d3db6e9f5" ON "abuse_user_report" ("targetUserId")`),
 | 
			
		||||
 | 
			
		||||
            queryRunner.query(`DELETE FROM "abuse_user_report" WHERE "targetUserId" NOT IN (SELECT "id" FROM "user")`).then(() => {
 | 
			
		||||
                queryRunner.query(`ALTER TABLE "abuse_user_report" ADD CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f" FOREIGN KEY ("targetUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
 | 
			
		||||
            }),
 | 
			
		||||
 | 
			
		||||
            queryRunner.query(`ALTER TABLE "poll" ADD CONSTRAINT "UQ_da851e06d0dfe2ef397d8b1bf1b" UNIQUE ("noteId")`),
 | 
			
		||||
            queryRunner.query(`ALTER TABLE "user_keypair" ADD CONSTRAINT "UQ_f4853eb41ab722fe05f81cedeb6" UNIQUE ("userId")`),
 | 
			
		||||
            queryRunner.query(`ALTER TABLE "user_profile" ADD CONSTRAINT "UQ_51cb79b5555effaf7d69ba1cff9" UNIQUE ("userId")`),
 | 
			
		||||
            queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "UQ_10c146e4b39b443ede016f6736d" UNIQUE ("userId")`),
 | 
			
		||||
            queryRunner.query(`ALTER TABLE "promo_note" ADD CONSTRAINT "UQ_e263909ca4fe5d57f8d4230dd5c" UNIQUE ("noteId")`),
 | 
			
		||||
 | 
			
		||||
            queryRunner.query(`ALTER TABLE "page" RENAME CONSTRAINT "FK_3126dd7c502c9e4d7597ef7ef10" TO "FK_a9ca79ad939bf06066b81c9d3aa"`),
 | 
			
		||||
 | 
			
		||||
            queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum" ADD VALUE 'pollEnded' AFTER 'pollVote'`),
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async down(queryRunner) {
 | 
			
		||||
        await Promise.all([
 | 
			
		||||
            // There is no ALTER TYPE REMOVE VALUE query, so the reverse operation is a bit more complex
 | 
			
		||||
            queryRunner.query(`UPDATE "user_profile" SET "mutingNotificationTypes" = array_remove("mutingNotificationTypes", 'pollEnded')`)
 | 
			
		||||
            .then(() =>
 | 
			
		||||
                queryRunner.query(`CREATE TYPE "public"."user_profile_mutingnotificationtypes_enum_old" AS ENUM('follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app')`)
 | 
			
		||||
            ).then(() =>
 | 
			
		||||
                queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" DROP DEFAULT`)
 | 
			
		||||
            ).then(() =>
 | 
			
		||||
                queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" TYPE "public"."user_profile_mutingnotificationtypes_enum_old"[] USING "mutingNotificationTypes"::"text"::"public"."user_profile_mutingnotificationtypes_enum_old"[]`)
 | 
			
		||||
            ).then(() =>
 | 
			
		||||
                queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "mutingNotificationTypes" SET DEFAULT '{}'`)
 | 
			
		||||
            ).then(() =>
 | 
			
		||||
                queryRunner.query(`DROP TYPE "public"."user_profile_mutingnotificationtypes_enum"`)
 | 
			
		||||
            ).then(() =>
 | 
			
		||||
                queryRunner.query(`ALTER TYPE "public"."user_profile_mutingnotificationtypes_enum_old" RENAME TO "user_profile_mutingnotificationtypes_enum"`)
 | 
			
		||||
            ),
 | 
			
		||||
 | 
			
		||||
            queryRunner.query(`ALTER TABLE "page" RENAME CONSTRAINT "FK_a9ca79ad939bf06066b81c9d3aa" TO "FK_3126dd7c502c9e4d7597ef7ef10"`),
 | 
			
		||||
 | 
			
		||||
            queryRunner.query(`ALTER TABLE "promo_note" DROP CONSTRAINT "UQ_e263909ca4fe5d57f8d4230dd5c"`),
 | 
			
		||||
            queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "UQ_10c146e4b39b443ede016f6736d"`),
 | 
			
		||||
            queryRunner.query(`ALTER TABLE "user_profile" DROP CONSTRAINT "UQ_51cb79b5555effaf7d69ba1cff9"`),
 | 
			
		||||
            queryRunner.query(`ALTER TABLE "user_keypair" DROP CONSTRAINT "UQ_f4853eb41ab722fe05f81cedeb6"`),
 | 
			
		||||
            queryRunner.query(`ALTER TABLE "poll" DROP CONSTRAINT "UQ_da851e06d0dfe2ef397d8b1bf1b"`),
 | 
			
		||||
 | 
			
		||||
            queryRunner.query(`ALTER TABLE "abuse_user_report" ALTER COLUMN "comment" SET DEFAULT '{}'`),
 | 
			
		||||
            queryRunner.query(`ALTER TABLE "abuse_user_report" DROP CONSTRAINT "FK_a9021cc2e1feb5f72d3db6e9f5f"`),
 | 
			
		||||
 | 
			
		||||
            queryRunner.query(`DROP INDEX "public"."IDX_a9021cc2e1feb5f72d3db6e9f5"`),
 | 
			
		||||
            queryRunner.query(`DROP INDEX "public"."IDX_f22169eb10657bded6d875ac8f"`),
 | 
			
		||||
            queryRunner.query(`DROP INDEX "public"."IDX_315c779174fe8247ab324f036e"`),
 | 
			
		||||
 | 
			
		||||
            /* DEFAULT's are not set again because if the column can be NULL, then DEFAULT NULL is not necessary.
 | 
			
		||||
            see also https://github.com/typeorm/typeorm/issues/7579#issuecomment-835423615 */
 | 
			
		||||
 | 
			
		||||
            queryRunner.query(`CREATE INDEX "IDX_note_on_channelId_and_id_desc" ON "note" ("id", "channelId") `),
 | 
			
		||||
            queryRunner.query(`ALTER INDEX "public"."IDX_c8cc87bd0f2f4487d17c651fbf" RENAME TO "IDX_seoignmeoprigmkpodgrjmkpormg"`),
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,36 @@
 | 
			
		||||
import tinycolor from 'tinycolor2';
 | 
			
		||||
 | 
			
		||||
export class uniformThemecolor1652859567549 {
 | 
			
		||||
	name = 'uniformThemecolor1652859567549'
 | 
			
		||||
 | 
			
		||||
	async up(queryRunner) {
 | 
			
		||||
		const formatColor = (color) => {
 | 
			
		||||
			let tc = new tinycolor(color);
 | 
			
		||||
			if (tc.isValid()) {
 | 
			
		||||
				return tc.toHexString();
 | 
			
		||||
			} else {
 | 
			
		||||
				return null;
 | 
			
		||||
			}
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		await queryRunner.query('SELECT "id", "themeColor" FROM "instance" WHERE "themeColor" IS NOT NULL')
 | 
			
		||||
		.then(instances => Promise.all(instances.map(instance => {
 | 
			
		||||
			// update theme color to uniform format, e.g. #00ff00
 | 
			
		||||
			// invalid theme colors get set to null
 | 
			
		||||
			return queryRunner.query('UPDATE "instance" SET "themeColor" = $1 WHERE "id" = $2', [formatColor(instance.themeColor), instance.id]);
 | 
			
		||||
		})));
 | 
			
		||||
 | 
			
		||||
		// also fix own theme color
 | 
			
		||||
		await queryRunner.query('SELECT "themeColor" FROM "meta" WHERE "themeColor" IS NOT NULL LIMIT 1')
 | 
			
		||||
		.then(metas => {
 | 
			
		||||
			if (metas.length > 0) {
 | 
			
		||||
				return queryRunner.query('UPDATE "meta" SET "themeColor" = $1', [formatColor(metas[0].themeColor)]);
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	async down(queryRunner) {
 | 
			
		||||
		// The original representation is not stored, so migrating back is not possible.
 | 
			
		||||
		// The new format also works in older versions so this is not a problem.
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@@ -6,7 +6,7 @@
 | 
			
		||||
		"build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json",
 | 
			
		||||
		"watch": "node watch.mjs",
 | 
			
		||||
		"lint": "eslint --quiet \"src/**/*.ts\"",
 | 
			
		||||
		"mocha": "cross-env TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" mocha",
 | 
			
		||||
		"mocha": "cross-env NODE_ENV=test TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" mocha",
 | 
			
		||||
		"test": "npm run mocha"
 | 
			
		||||
	},
 | 
			
		||||
	"resolutions": {
 | 
			
		||||
@@ -15,25 +15,24 @@
 | 
			
		||||
	},
 | 
			
		||||
	"dependencies": {
 | 
			
		||||
		"@bull-board/koa": "3.10.4",
 | 
			
		||||
		"@discordapp/twemoji": "13.1.1",
 | 
			
		||||
		"@discordapp/twemoji": "14.0.2",
 | 
			
		||||
		"@elastic/elasticsearch": "7.11.0",
 | 
			
		||||
		"@koa/cors": "3.1.0",
 | 
			
		||||
		"@koa/multer": "3.0.0",
 | 
			
		||||
		"@koa/router": "9.0.1",
 | 
			
		||||
		"@sinonjs/fake-timers": "9.1.1",
 | 
			
		||||
		"@peertube/http-signature": "1.6.0",
 | 
			
		||||
		"@sinonjs/fake-timers": "9.1.2",
 | 
			
		||||
		"@syuilo/aiscript": "0.11.1",
 | 
			
		||||
		"@typescript-eslint/eslint-plugin": "5.20.0",
 | 
			
		||||
		"@typescript-eslint/parser": "5.20.0",
 | 
			
		||||
		"abort-controller": "3.0.0",
 | 
			
		||||
		"ajv": "8.11.0",
 | 
			
		||||
		"archiver": "5.3.1",
 | 
			
		||||
		"autobind-decorator": "2.4.0",
 | 
			
		||||
		"autwh": "0.1.0",
 | 
			
		||||
		"aws-sdk": "2.1120.0",
 | 
			
		||||
		"aws-sdk": "2.1135.0",
 | 
			
		||||
		"bcryptjs": "2.4.3",
 | 
			
		||||
		"blurhash": "1.1.5",
 | 
			
		||||
		"broadcast-channel": "4.11.0",
 | 
			
		||||
		"bull": "4.8.2",
 | 
			
		||||
		"broadcast-channel": "4.12.0",
 | 
			
		||||
		"bull": "4.8.3",
 | 
			
		||||
		"cacheable-lookup": "6.0.4",
 | 
			
		||||
		"cbor": "8.1.0",
 | 
			
		||||
		"chalk": "5.0.1",
 | 
			
		||||
@@ -44,22 +43,19 @@
 | 
			
		||||
		"date-fns": "2.28.0",
 | 
			
		||||
		"deep-email-validator": "0.1.21",
 | 
			
		||||
		"escape-regexp": "0.0.1",
 | 
			
		||||
		"eslint": "8.14.0",
 | 
			
		||||
		"eslint-plugin-import": "2.26.0",
 | 
			
		||||
		"feed": "4.2.2",
 | 
			
		||||
		"file-type": "17.1.1",
 | 
			
		||||
		"fluent-ffmpeg": "2.1.2",
 | 
			
		||||
		"got": "12.0.3",
 | 
			
		||||
		"got": "12.0.4",
 | 
			
		||||
		"hpagent": "0.1.2",
 | 
			
		||||
		"http-signature": "1.3.6",
 | 
			
		||||
		"ip-cidr": "3.0.7",
 | 
			
		||||
		"ip-cidr": "3.0.8",
 | 
			
		||||
		"is-svg": "4.3.2",
 | 
			
		||||
		"js-yaml": "4.1.0",
 | 
			
		||||
		"jsdom": "19.0.0",
 | 
			
		||||
		"json5": "2.2.1",
 | 
			
		||||
		"json5-loader": "4.0.1",
 | 
			
		||||
		"jsonld": "5.2.0",
 | 
			
		||||
		"jsrsasign": "10.5.19",
 | 
			
		||||
		"jsrsasign": "10.5.22",
 | 
			
		||||
		"koa": "2.13.4",
 | 
			
		||||
		"koa-bodyparser": "4.3.0",
 | 
			
		||||
		"koa-favicon": "2.1.0",
 | 
			
		||||
@@ -69,19 +65,18 @@
 | 
			
		||||
		"koa-send": "5.0.1",
 | 
			
		||||
		"koa-slow": "2.1.0",
 | 
			
		||||
		"koa-views": "7.0.2",
 | 
			
		||||
		"mfm-js": "0.21.0",
 | 
			
		||||
		"mfm-js": "0.22.1",
 | 
			
		||||
		"mime-types": "2.1.35",
 | 
			
		||||
		"misskey-js": "0.0.14",
 | 
			
		||||
		"mocha": "9.2.2",
 | 
			
		||||
		"mocha": "10.0.0",
 | 
			
		||||
		"ms": "3.0.0-canary.1",
 | 
			
		||||
		"multer": "1.4.4",
 | 
			
		||||
		"nested-property": "4.0.0",
 | 
			
		||||
		"node-fetch": "3.2.3",
 | 
			
		||||
		"nodemailer": "6.7.3",
 | 
			
		||||
		"node-fetch": "3.2.4",
 | 
			
		||||
		"nodemailer": "6.7.5",
 | 
			
		||||
		"os-utils": "0.0.14",
 | 
			
		||||
		"parse5": "6.0.1",
 | 
			
		||||
		"pg": "8.7.3",
 | 
			
		||||
		"portscanner": "2.2.0",
 | 
			
		||||
		"private-ip": "2.3.3",
 | 
			
		||||
		"probe-image-size": "7.2.3",
 | 
			
		||||
		"promise-limit": "2.7.0",
 | 
			
		||||
@@ -101,33 +96,32 @@
 | 
			
		||||
		"s-age": "1.1.2",
 | 
			
		||||
		"sanitize-html": "2.7.0",
 | 
			
		||||
		"semver": "7.3.7",
 | 
			
		||||
		"sharp": "0.30.4",
 | 
			
		||||
		"sharp": "0.29.3",
 | 
			
		||||
		"speakeasy": "2.0.0",
 | 
			
		||||
		"strict-event-emitter-types": "2.0.0",
 | 
			
		||||
		"stringz": "2.1.0",
 | 
			
		||||
		"style-loader": "3.3.1",
 | 
			
		||||
		"summaly": "2.5.0",
 | 
			
		||||
		"syslog-pro": "1.0.0",
 | 
			
		||||
		"systeminformation": "5.11.14",
 | 
			
		||||
		"systeminformation": "5.11.15",
 | 
			
		||||
		"tinycolor2": "1.4.2",
 | 
			
		||||
		"tmp": "0.2.1",
 | 
			
		||||
		"ts-loader": "9.2.8",
 | 
			
		||||
		"ts-node": "10.7.0",
 | 
			
		||||
		"tsc-alias": "1.4.1",
 | 
			
		||||
		"tsconfig-paths": "3.14.1",
 | 
			
		||||
		"ts-loader": "9.3.0",
 | 
			
		||||
		"ts-node": "10.8.0",
 | 
			
		||||
		"tsc-alias": "1.6.7",
 | 
			
		||||
		"tsconfig-paths": "4.0.0",
 | 
			
		||||
		"twemoji-parser": "14.0.0",
 | 
			
		||||
		"typeorm": "0.3.6",
 | 
			
		||||
		"typescript": "4.6.3",
 | 
			
		||||
		"ulid": "2.3.0",
 | 
			
		||||
		"unzipper": "0.10.11",
 | 
			
		||||
		"uuid": "8.3.2",
 | 
			
		||||
		"web-push": "3.4.5",
 | 
			
		||||
		"web-push": "3.5.0",
 | 
			
		||||
		"websocket": "1.0.34",
 | 
			
		||||
		"ws": "8.5.0",
 | 
			
		||||
		"ws": "8.6.0",
 | 
			
		||||
		"xev": "3.0.2"
 | 
			
		||||
	},
 | 
			
		||||
	"devDependencies": {
 | 
			
		||||
		"@redocly/openapi-core": "1.0.0-beta.93",
 | 
			
		||||
		"@redocly/openapi-core": "1.0.0-beta.97",
 | 
			
		||||
		"@types/semver": "7.3.9",
 | 
			
		||||
		"@types/bcryptjs": "2.4.2",
 | 
			
		||||
		"@types/bull": "3.15.8",
 | 
			
		||||
@@ -138,7 +132,7 @@
 | 
			
		||||
		"@types/js-yaml": "4.0.5",
 | 
			
		||||
		"@types/jsdom": "16.2.14",
 | 
			
		||||
		"@types/jsonld": "1.5.6",
 | 
			
		||||
		"@types/jsrsasign": "10.2.1",
 | 
			
		||||
		"@types/jsrsasign": "10.5.1",
 | 
			
		||||
		"@types/koa": "2.13.4",
 | 
			
		||||
		"@types/koa-bodyparser": "4.3.7",
 | 
			
		||||
		"@types/koa-cors": "0.0.2",
 | 
			
		||||
@@ -151,12 +145,11 @@
 | 
			
		||||
		"@types/koa__multer": "2.0.4",
 | 
			
		||||
		"@types/koa__router": "8.0.11",
 | 
			
		||||
		"@types/mocha": "9.1.1",
 | 
			
		||||
		"@types/node": "17.0.25",
 | 
			
		||||
		"@types/node": "17.0.35",
 | 
			
		||||
		"@types/node-fetch": "3.0.3",
 | 
			
		||||
		"@types/nodemailer": "6.4.4",
 | 
			
		||||
		"@types/oauth": "0.9.1",
 | 
			
		||||
		"@types/parse5": "6.0.3",
 | 
			
		||||
		"@types/portscanner": "2.1.1",
 | 
			
		||||
		"@types/pug": "2.0.6",
 | 
			
		||||
		"@types/punycode": "2.1.0",
 | 
			
		||||
		"@types/qrcode": "1.4.2",
 | 
			
		||||
@@ -174,6 +167,12 @@
 | 
			
		||||
		"@types/web-push": "3.3.2",
 | 
			
		||||
		"@types/websocket": "1.0.5",
 | 
			
		||||
		"@types/ws": "8.5.3",
 | 
			
		||||
		"@typescript-eslint/eslint-plugin": "5.26.0",
 | 
			
		||||
		"@typescript-eslint/parser": "5.26.0",
 | 
			
		||||
		"typescript": "4.7.2",
 | 
			
		||||
		"eslint": "8.16.0",
 | 
			
		||||
		"eslint-plugin-import": "2.26.0",
 | 
			
		||||
 | 
			
		||||
		"cross-env": "7.0.3",
 | 
			
		||||
		"execa": "6.1.0"
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
declare module 'http-signature' {
 | 
			
		||||
declare module '@peertube/http-signature' {
 | 
			
		||||
	import { IncomingMessage, ClientRequest } from 'node:http';
 | 
			
		||||
 | 
			
		||||
	interface ISignature {
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,6 @@ import * as os from 'node:os';
 | 
			
		||||
import cluster from 'node:cluster';
 | 
			
		||||
import chalk from 'chalk';
 | 
			
		||||
import chalkTemplate from 'chalk-template';
 | 
			
		||||
import * as portscanner from 'portscanner';
 | 
			
		||||
import semver from 'semver';
 | 
			
		||||
 | 
			
		||||
import Logger from '@/services/logger.js';
 | 
			
		||||
@@ -48,11 +47,6 @@ function greet() {
 | 
			
		||||
	bootLogger.info(`Misskey v${meta.version}`, null, true);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isRoot() {
 | 
			
		||||
	// maybe process.getuid will be undefined under not POSIX environment (e.g. Windows)
 | 
			
		||||
	return process.getuid != null && process.getuid() === 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Init master process
 | 
			
		||||
 */
 | 
			
		||||
@@ -67,7 +61,6 @@ export async function masterMain() {
 | 
			
		||||
		showNodejsVersion();
 | 
			
		||||
		config = loadConfigBoot();
 | 
			
		||||
		await connectDb();
 | 
			
		||||
		await validatePort(config);
 | 
			
		||||
	} catch (e) {
 | 
			
		||||
		bootLogger.error('Fatal error occurred during initialization', null, true);
 | 
			
		||||
		process.exit(1);
 | 
			
		||||
@@ -97,8 +90,6 @@ function showEnvironment(): void {
 | 
			
		||||
		logger.warn('The environment is not in production mode.');
 | 
			
		||||
		logger.warn('DO NOT USE FOR PRODUCTION PURPOSE!', null, true);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logger.info(`You ${isRoot() ? '' : 'do not '}have root privileges`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function showNodejsVersion(): void {
 | 
			
		||||
@@ -152,29 +143,6 @@ async function connectDb(): Promise<void> {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function validatePort(config: Config): Promise<void> {
 | 
			
		||||
	const isWellKnownPort = (port: number) => port < 1024;
 | 
			
		||||
 | 
			
		||||
	async function isPortAvailable(port: number): Promise<boolean> {
 | 
			
		||||
		return await portscanner.checkPortStatus(port, '127.0.0.1') === 'closed';
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (config.port == null || Number.isNaN(config.port)) {
 | 
			
		||||
		bootLogger.error('The port is not configured. Please configure port.', null, true);
 | 
			
		||||
		process.exit(1);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (process.platform === 'linux' && isWellKnownPort(config.port) && !isRoot()) {
 | 
			
		||||
		bootLogger.error('You need root privileges to listen on well-known port on Linux', null, true);
 | 
			
		||||
		process.exit(1);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (!await isPortAvailable(config.port)) {
 | 
			
		||||
		bootLogger.error(`Port ${config.port} is already in use`, null, true);
 | 
			
		||||
		process.exit(1);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function spawnWorkers(limit: number = 1) {
 | 
			
		||||
	const workers = Math.min(limit, os.cpus().length);
 | 
			
		||||
	bootLogger.info(`Starting ${workers} worker${workers === 1 ? '' : 's'}...`);
 | 
			
		||||
@@ -186,6 +154,10 @@ function spawnWorker(): Promise<void> {
 | 
			
		||||
	return new Promise(res => {
 | 
			
		||||
		const worker = cluster.fork();
 | 
			
		||||
		worker.on('message', message => {
 | 
			
		||||
			if (message === 'listenFailed') {
 | 
			
		||||
				bootLogger.error(`The server Listen failed due to the previous error.`);
 | 
			
		||||
				process.exit(1);
 | 
			
		||||
			}
 | 
			
		||||
			if (message !== 'ready') return;
 | 
			
		||||
			res();
 | 
			
		||||
		});
 | 
			
		||||
 
 | 
			
		||||
@@ -25,6 +25,7 @@ const path = process.env.NODE_ENV === 'test'
 | 
			
		||||
 | 
			
		||||
export default function load() {
 | 
			
		||||
	const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8'));
 | 
			
		||||
	const clientManifest = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/_client_dist_/manifest.json`, 'utf-8'));
 | 
			
		||||
	const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source;
 | 
			
		||||
 | 
			
		||||
	const mixin = {} as Mixin;
 | 
			
		||||
@@ -45,6 +46,7 @@ export default function load() {
 | 
			
		||||
	mixin.authUrl = `${mixin.scheme}://${mixin.host}/auth`;
 | 
			
		||||
	mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`;
 | 
			
		||||
	mixin.userAgent = `Misskey/${meta.version} (${config.url})`;
 | 
			
		||||
	mixin.clientEntry = clientManifest['src/init.ts'].file.replace(/^_client_dist_\//, '');
 | 
			
		||||
 | 
			
		||||
	if (!config.redis.prefix) config.redis.prefix = mixin.host;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -80,6 +80,7 @@ export type Mixin = {
 | 
			
		||||
	authUrl: string;
 | 
			
		||||
	driveUrl: string;
 | 
			
		||||
	userAgent: string;
 | 
			
		||||
	clientEntry: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type Config = Source & Mixin;
 | 
			
		||||
 
 | 
			
		||||
@@ -5,9 +5,6 @@ pg.types.setTypeParser(20, Number);
 | 
			
		||||
import { Logger, DataSource } from 'typeorm';
 | 
			
		||||
import * as highlight from 'cli-highlight';
 | 
			
		||||
import config from '@/config/index.js';
 | 
			
		||||
import { envOption } from '../env.js';
 | 
			
		||||
 | 
			
		||||
import { dbLogger } from './logger.js';
 | 
			
		||||
 | 
			
		||||
import { User } from '@/models/entities/user.js';
 | 
			
		||||
import { DriveFile } from '@/models/entities/drive-file.js';
 | 
			
		||||
@@ -74,6 +71,8 @@ import { UserPending } from '@/models/entities/user-pending.js';
 | 
			
		||||
 | 
			
		||||
import { entities as charts } from '@/services/chart/entities.js';
 | 
			
		||||
import { Webhook } from '@/models/entities/webhook.js';
 | 
			
		||||
import { envOption } from '../env.js';
 | 
			
		||||
import { dbLogger } from './logger.js';
 | 
			
		||||
 | 
			
		||||
const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false);
 | 
			
		||||
 | 
			
		||||
@@ -212,7 +211,7 @@ export async function initDb() {
 | 
			
		||||
	if (db.isInitialized) {
 | 
			
		||||
		// nop
 | 
			
		||||
	} else {
 | 
			
		||||
		await db.connect();
 | 
			
		||||
		await db.initialize();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -48,6 +48,7 @@ export class Cache<T> {
 | 
			
		||||
 | 
			
		||||
		// Cache MISS
 | 
			
		||||
		const value = await fetcher();
 | 
			
		||||
		this.set(key, value);
 | 
			
		||||
		return value;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,19 @@
 | 
			
		||||
import * as tmp from 'tmp';
 | 
			
		||||
 | 
			
		||||
export function createTemp(): Promise<[string, any]> {
 | 
			
		||||
	return new Promise<[string, any]>((res, rej) => {
 | 
			
		||||
export function createTemp(): Promise<[string, () => void]> {
 | 
			
		||||
	return new Promise<[string, () => void]>((res, rej) => {
 | 
			
		||||
		tmp.file((e, path, fd, cleanup) => {
 | 
			
		||||
			if (e) return rej(e);
 | 
			
		||||
			res([path, cleanup]);
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function createTempDir(): Promise<[string, () => void]> {
 | 
			
		||||
	return new Promise<[string, () => void]>((res, rej) => {
 | 
			
		||||
		tmp.dir((e, path, cleanup) => {
 | 
			
		||||
			if (e) return rej(e);
 | 
			
		||||
			res([path, cleanup]);
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -20,9 +20,16 @@ export async function fetchMeta(noCache = false): Promise<Meta> {
 | 
			
		||||
			cache = meta;
 | 
			
		||||
			return meta;
 | 
			
		||||
		} else {
 | 
			
		||||
			const saved = await transactionalEntityManager.save(Meta, {
 | 
			
		||||
				id: 'x',
 | 
			
		||||
			}) as Meta;
 | 
			
		||||
			// metaが空のときfetchMetaが同時に呼ばれるとここが同時に呼ばれてしまうことがあるのでフェイルセーフなupsertを使う
 | 
			
		||||
			const saved = await transactionalEntityManager
 | 
			
		||||
				.upsert(
 | 
			
		||||
					Meta,
 | 
			
		||||
					{
 | 
			
		||||
						id: 'x',
 | 
			
		||||
					},
 | 
			
		||||
					['id'],
 | 
			
		||||
				)
 | 
			
		||||
				.then((x) => transactionalEntityManager.findOneByOrFail(Meta, x.identifiers[0]));
 | 
			
		||||
 | 
			
		||||
			cache = saved;
 | 
			
		||||
			return saved;
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,6 @@ export class AccessToken {
 | 
			
		||||
 | 
			
		||||
	@Column('timestamp with time zone', {
 | 
			
		||||
		nullable: true,
 | 
			
		||||
		default: null,
 | 
			
		||||
	})
 | 
			
		||||
	public lastUsedAt: Date | null;
 | 
			
		||||
 | 
			
		||||
@@ -29,7 +28,6 @@ export class AccessToken {
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 128,
 | 
			
		||||
		nullable: true,
 | 
			
		||||
		default: null,
 | 
			
		||||
	})
 | 
			
		||||
	public session: string | null;
 | 
			
		||||
 | 
			
		||||
@@ -52,7 +50,6 @@ export class AccessToken {
 | 
			
		||||
	@Column({
 | 
			
		||||
		...id(),
 | 
			
		||||
		nullable: true,
 | 
			
		||||
		default: null,
 | 
			
		||||
	})
 | 
			
		||||
	public appId: App['id'] | null;
 | 
			
		||||
 | 
			
		||||
@@ -65,21 +62,18 @@ export class AccessToken {
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 128,
 | 
			
		||||
		nullable: true,
 | 
			
		||||
		default: null,
 | 
			
		||||
	})
 | 
			
		||||
	public name: string | null;
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 512,
 | 
			
		||||
		nullable: true,
 | 
			
		||||
		default: null,
 | 
			
		||||
	})
 | 
			
		||||
	public description: string | null;
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 512,
 | 
			
		||||
		nullable: true,
 | 
			
		||||
		default: null,
 | 
			
		||||
	})
 | 
			
		||||
	public iconUrl: string | null;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,7 @@ export class AuthSession {
 | 
			
		||||
		...id(),
 | 
			
		||||
		nullable: true,
 | 
			
		||||
	})
 | 
			
		||||
	public userId: User['id'];
 | 
			
		||||
	public userId: User['id'] | null;
 | 
			
		||||
 | 
			
		||||
	@ManyToOne(type => User, {
 | 
			
		||||
		onDelete: 'CASCADE',
 | 
			
		||||
 
 | 
			
		||||
@@ -37,7 +37,7 @@ export class Clip {
 | 
			
		||||
	public isPublic: boolean;
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 2048, nullable: true, default: null,
 | 
			
		||||
		length: 2048, nullable: true,
 | 
			
		||||
		comment: 'The description of the Clip.',
 | 
			
		||||
	})
 | 
			
		||||
	public description: string | null;
 | 
			
		||||
 
 | 
			
		||||
@@ -79,7 +79,6 @@ export class DriveFile {
 | 
			
		||||
	})
 | 
			
		||||
	public properties: { width?: number; height?: number; orientation?: number; avgColor?: string };
 | 
			
		||||
 | 
			
		||||
	@Index()
 | 
			
		||||
	@Column('boolean')
 | 
			
		||||
	public storedInternal: boolean;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -36,6 +36,7 @@ export class Emoji {
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 512,
 | 
			
		||||
		default: '',
 | 
			
		||||
	})
 | 
			
		||||
	public publicUrl: string;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -107,53 +107,53 @@ export class Instance {
 | 
			
		||||
	public isSuspended: boolean;
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 64, nullable: true, default: null,
 | 
			
		||||
		length: 64, nullable: true,
 | 
			
		||||
		comment: 'The software of the Instance.',
 | 
			
		||||
	})
 | 
			
		||||
	public softwareName: string | null;
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 64, nullable: true, default: null,
 | 
			
		||||
		length: 64, nullable: true,
 | 
			
		||||
	})
 | 
			
		||||
	public softwareVersion: string | null;
 | 
			
		||||
 | 
			
		||||
	@Column('boolean', {
 | 
			
		||||
		nullable: true, default: null,
 | 
			
		||||
		nullable: true,
 | 
			
		||||
	})
 | 
			
		||||
	public openRegistrations: boolean | null;
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 256, nullable: true, default: null,
 | 
			
		||||
		length: 256, nullable: true,
 | 
			
		||||
	})
 | 
			
		||||
	public name: string | null;
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 4096, nullable: true, default: null,
 | 
			
		||||
		length: 4096, nullable: true,
 | 
			
		||||
	})
 | 
			
		||||
	public description: string | null;
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 128, nullable: true, default: null,
 | 
			
		||||
		length: 128, nullable: true,
 | 
			
		||||
	})
 | 
			
		||||
	public maintainerName: string | null;
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 256, nullable: true, default: null,
 | 
			
		||||
		length: 256, nullable: true,
 | 
			
		||||
	})
 | 
			
		||||
	public maintainerEmail: string | null;
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 256, nullable: true, default: null,
 | 
			
		||||
		length: 256, nullable: true,
 | 
			
		||||
	})
 | 
			
		||||
	public iconUrl: string | null;
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 256, nullable: true, default: null,
 | 
			
		||||
		length: 256, nullable: true,
 | 
			
		||||
	})
 | 
			
		||||
	public faviconUrl: string | null;
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 64, nullable: true, default: null,
 | 
			
		||||
		length: 64, nullable: true,
 | 
			
		||||
	})
 | 
			
		||||
	public themeColor: string | null;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -78,7 +78,7 @@ export class Meta {
 | 
			
		||||
	public blockedHosts: string[];
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 512, array: true, default: '{"/featured", "/channels", "/explore", "/pages", "/about-misskey"}',
 | 
			
		||||
		length: 512, array: true, default: '{/featured,/channels,/explore,/pages,/about-misskey}',
 | 
			
		||||
	})
 | 
			
		||||
	public pinnedPages: string[];
 | 
			
		||||
 | 
			
		||||
@@ -346,14 +346,12 @@ export class Meta {
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 8192,
 | 
			
		||||
		default: null,
 | 
			
		||||
		nullable: true,
 | 
			
		||||
	})
 | 
			
		||||
	public defaultLightTheme: string | null;
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 8192,
 | 
			
		||||
		default: null,
 | 
			
		||||
		nullable: true,
 | 
			
		||||
	})
 | 
			
		||||
	public defaultDarkTheme: string | null;
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,6 @@ export class Muting {
 | 
			
		||||
	@Index()
 | 
			
		||||
	@Column('timestamp with time zone', {
 | 
			
		||||
		nullable: true,
 | 
			
		||||
		default: null,
 | 
			
		||||
	})
 | 
			
		||||
	public expiresAt: Date | null;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -53,8 +53,8 @@ export class Note {
 | 
			
		||||
	})
 | 
			
		||||
	public threadId: string | null;
 | 
			
		||||
 | 
			
		||||
	@Column('varchar', {
 | 
			
		||||
		length: 8192, nullable: true,
 | 
			
		||||
	@Column('text', {
 | 
			
		||||
		nullable: true,
 | 
			
		||||
	})
 | 
			
		||||
	public text: string | null;
 | 
			
		||||
 | 
			
		||||
@@ -179,7 +179,7 @@ export class Note {
 | 
			
		||||
	@Index()
 | 
			
		||||
	@Column({
 | 
			
		||||
		...id(),
 | 
			
		||||
		nullable: true, default: null,
 | 
			
		||||
		nullable: true,
 | 
			
		||||
		comment: 'The ID of source channel.',
 | 
			
		||||
	})
 | 
			
		||||
	public channelId: Channel['id'] | null;
 | 
			
		||||
 
 | 
			
		||||
@@ -192,6 +192,7 @@ export class UserProfile {
 | 
			
		||||
 | 
			
		||||
	@Column('jsonb', {
 | 
			
		||||
		default: [],
 | 
			
		||||
		comment: 'List of instances muted by the user.',
 | 
			
		||||
	})
 | 
			
		||||
	public mutedInstances: string[];
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -207,7 +207,7 @@ export class User {
 | 
			
		||||
 | 
			
		||||
	@Column('boolean', {
 | 
			
		||||
		default: false,
 | 
			
		||||
		comment: 'Whether to show users replying to other users in the timeline',
 | 
			
		||||
		comment: 'Whether to show users replying to other users in the timeline.',
 | 
			
		||||
	})
 | 
			
		||||
	public showTimelineReplies: boolean;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -168,16 +168,22 @@ export const NoteRepository = db.getRepository(Note).extend({
 | 
			
		||||
				return true;
 | 
			
		||||
			} else {
 | 
			
		||||
				// フォロワーかどうか
 | 
			
		||||
				const following = await Followings.findOneBy({
 | 
			
		||||
					followeeId: note.userId,
 | 
			
		||||
					followerId: meId,
 | 
			
		||||
				});
 | 
			
		||||
				const [following, user] = await Promise.all([
 | 
			
		||||
					Followings.findOneBy({
 | 
			
		||||
						followeeId: note.userId,
 | 
			
		||||
						followerId: meId,
 | 
			
		||||
					}),
 | 
			
		||||
					Users.findOneByOrFail({ id: meId }),
 | 
			
		||||
				]);
 | 
			
		||||
 | 
			
		||||
				if (following == null) {
 | 
			
		||||
					return false;
 | 
			
		||||
				} else {
 | 
			
		||||
					return true;
 | 
			
		||||
				}
 | 
			
		||||
				/* If we know the following, everyhting is fine.
 | 
			
		||||
 | 
			
		||||
				But if we do not know the following, it might be that both the
 | 
			
		||||
				author of the note and the author of the like are remote users,
 | 
			
		||||
				in which case we can never know the following. Instead we have
 | 
			
		||||
				to assume that the users are following each other.
 | 
			
		||||
				*/
 | 
			
		||||
				return following != null || (note.userHost != null && user.host != null);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import httpSignature from 'http-signature';
 | 
			
		||||
import httpSignature from '@peertube/http-signature';
 | 
			
		||||
import { v4 as uuid } from 'uuid';
 | 
			
		||||
 | 
			
		||||
import config from '@/config/index.js';
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,11 @@
 | 
			
		||||
import Bull from 'bull';
 | 
			
		||||
import * as tmp from 'tmp';
 | 
			
		||||
import * as fs from 'node:fs';
 | 
			
		||||
 | 
			
		||||
import { queueLogger } from '../../logger.js';
 | 
			
		||||
import { addFile } from '@/services/drive/add-file.js';
 | 
			
		||||
import { format as dateFormat } from 'date-fns';
 | 
			
		||||
import { getFullApAccount } from '@/misc/convert-host.js';
 | 
			
		||||
import { createTemp } from '@/misc/create-temp.js';
 | 
			
		||||
import { Users, Blockings } from '@/models/index.js';
 | 
			
		||||
import { MoreThan } from 'typeorm';
 | 
			
		||||
import { DbUserJobData } from '@/queue/types.js';
 | 
			
		||||
@@ -22,73 +22,72 @@ export async function exportBlocking(job: Bull.Job<DbUserJobData>, done: any): P
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Create temp file
 | 
			
		||||
	const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
 | 
			
		||||
		tmp.file((e, path, fd, cleanup) => {
 | 
			
		||||
			if (e) return rej(e);
 | 
			
		||||
			res([path, cleanup]);
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
	const [path, cleanup] = await createTemp();
 | 
			
		||||
 | 
			
		||||
	logger.info(`Temp file is ${path}`);
 | 
			
		||||
 | 
			
		||||
	const stream = fs.createWriteStream(path, { flags: 'a' });
 | 
			
		||||
	try {
 | 
			
		||||
		const stream = fs.createWriteStream(path, { flags: 'a' });
 | 
			
		||||
 | 
			
		||||
	let exportedCount = 0;
 | 
			
		||||
	let cursor: any = null;
 | 
			
		||||
		let exportedCount = 0;
 | 
			
		||||
		let cursor: any = null;
 | 
			
		||||
 | 
			
		||||
	while (true) {
 | 
			
		||||
		const blockings = await Blockings.find({
 | 
			
		||||
			where: {
 | 
			
		||||
				blockerId: user.id,
 | 
			
		||||
				...(cursor ? { id: MoreThan(cursor) } : {}),
 | 
			
		||||
			},
 | 
			
		||||
			take: 100,
 | 
			
		||||
			order: {
 | 
			
		||||
				id: 1,
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
		while (true) {
 | 
			
		||||
			const blockings = await Blockings.find({
 | 
			
		||||
				where: {
 | 
			
		||||
					blockerId: user.id,
 | 
			
		||||
					...(cursor ? { id: MoreThan(cursor) } : {}),
 | 
			
		||||
				},
 | 
			
		||||
				take: 100,
 | 
			
		||||
				order: {
 | 
			
		||||
					id: 1,
 | 
			
		||||
				},
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
		if (blockings.length === 0) {
 | 
			
		||||
			job.progress(100);
 | 
			
		||||
			break;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		cursor = blockings[blockings.length - 1].id;
 | 
			
		||||
 | 
			
		||||
		for (const block of blockings) {
 | 
			
		||||
			const u = await Users.findOneBy({ id: block.blockeeId });
 | 
			
		||||
			if (u == null) {
 | 
			
		||||
				exportedCount++; continue;
 | 
			
		||||
			if (blockings.length === 0) {
 | 
			
		||||
				job.progress(100);
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const content = getFullApAccount(u.username, u.host);
 | 
			
		||||
			await new Promise<void>((res, rej) => {
 | 
			
		||||
				stream.write(content + '\n', err => {
 | 
			
		||||
					if (err) {
 | 
			
		||||
						logger.error(err);
 | 
			
		||||
						rej(err);
 | 
			
		||||
					} else {
 | 
			
		||||
						res();
 | 
			
		||||
					}
 | 
			
		||||
			cursor = blockings[blockings.length - 1].id;
 | 
			
		||||
 | 
			
		||||
			for (const block of blockings) {
 | 
			
		||||
				const u = await Users.findOneBy({ id: block.blockeeId });
 | 
			
		||||
				if (u == null) {
 | 
			
		||||
					exportedCount++; continue;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				const content = getFullApAccount(u.username, u.host);
 | 
			
		||||
				await new Promise<void>((res, rej) => {
 | 
			
		||||
					stream.write(content + '\n', err => {
 | 
			
		||||
						if (err) {
 | 
			
		||||
							logger.error(err);
 | 
			
		||||
							rej(err);
 | 
			
		||||
						} else {
 | 
			
		||||
							res();
 | 
			
		||||
						}
 | 
			
		||||
					});
 | 
			
		||||
				});
 | 
			
		||||
				exportedCount++;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const total = await Blockings.countBy({
 | 
			
		||||
				blockerId: user.id,
 | 
			
		||||
			});
 | 
			
		||||
			exportedCount++;
 | 
			
		||||
 | 
			
		||||
			job.progress(exportedCount / total);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const total = await Blockings.countBy({
 | 
			
		||||
			blockerId: user.id,
 | 
			
		||||
		});
 | 
			
		||||
		stream.end();
 | 
			
		||||
		logger.succ(`Exported to: ${path}`);
 | 
			
		||||
 | 
			
		||||
		job.progress(exportedCount / total);
 | 
			
		||||
		const fileName = 'blocking-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv';
 | 
			
		||||
		const driveFile = await addFile({ user, path, name: fileName, force: true });
 | 
			
		||||
 | 
			
		||||
		logger.succ(`Exported to: ${driveFile.id}`);
 | 
			
		||||
	} finally {
 | 
			
		||||
		cleanup();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	stream.end();
 | 
			
		||||
	logger.succ(`Exported to: ${path}`);
 | 
			
		||||
 | 
			
		||||
	const fileName = 'blocking-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv';
 | 
			
		||||
	const driveFile = await addFile({ user, path, name: fileName, force: true });
 | 
			
		||||
 | 
			
		||||
	logger.succ(`Exported to: ${driveFile.id}`);
 | 
			
		||||
	cleanup();
 | 
			
		||||
	done();
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
import Bull from 'bull';
 | 
			
		||||
import * as tmp from 'tmp';
 | 
			
		||||
import * as fs from 'node:fs';
 | 
			
		||||
 | 
			
		||||
import { ulid } from 'ulid';
 | 
			
		||||
@@ -10,6 +9,7 @@ import { addFile } from '@/services/drive/add-file.js';
 | 
			
		||||
import { format as dateFormat } from 'date-fns';
 | 
			
		||||
import { Users, Emojis } from '@/models/index.js';
 | 
			
		||||
import {  } from '@/queue/types.js';
 | 
			
		||||
import { createTempDir } from '@/misc/create-temp.js';
 | 
			
		||||
import { downloadUrl } from '@/misc/download-url.js';
 | 
			
		||||
import config from '@/config/index.js';
 | 
			
		||||
import { IsNull } from 'typeorm';
 | 
			
		||||
@@ -25,13 +25,7 @@ export async function exportCustomEmojis(job: Bull.Job, done: () => void): Promi
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Create temp dir
 | 
			
		||||
	const [path, cleanup] = await new Promise<[string, () => void]>((res, rej) => {
 | 
			
		||||
		tmp.dir((e, path, cleanup) => {
 | 
			
		||||
			if (e) return rej(e);
 | 
			
		||||
			res([path, cleanup]);
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
	const [path, cleanup] = await createTempDir();
 | 
			
		||||
 | 
			
		||||
	logger.info(`Temp dir is ${path}`);
 | 
			
		||||
 | 
			
		||||
@@ -98,12 +92,7 @@ export async function exportCustomEmojis(job: Bull.Job, done: () => void): Promi
 | 
			
		||||
	metaStream.end();
 | 
			
		||||
 | 
			
		||||
	// Create archive
 | 
			
		||||
	const [archivePath, archiveCleanup] = await new Promise<[string, () => void]>((res, rej) => {
 | 
			
		||||
		tmp.file((e, path, fd, cleanup) => {
 | 
			
		||||
			if (e) return rej(e);
 | 
			
		||||
			res([path, cleanup]);
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
	const [archivePath, archiveCleanup] = await createTemp();
 | 
			
		||||
	const archiveStream = fs.createWriteStream(archivePath);
 | 
			
		||||
	const archive = archiver('zip', {
 | 
			
		||||
		zlib: { level: 0 },
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,11 @@
 | 
			
		||||
import Bull from 'bull';
 | 
			
		||||
import * as tmp from 'tmp';
 | 
			
		||||
import * as fs from 'node:fs';
 | 
			
		||||
 | 
			
		||||
import { queueLogger } from '../../logger.js';
 | 
			
		||||
import { addFile } from '@/services/drive/add-file.js';
 | 
			
		||||
import { format as dateFormat } from 'date-fns';
 | 
			
		||||
import { getFullApAccount } from '@/misc/convert-host.js';
 | 
			
		||||
import { createTemp } from '@/misc/create-temp.js';
 | 
			
		||||
import { Users, Followings, Mutings } from '@/models/index.js';
 | 
			
		||||
import { In, MoreThan, Not } from 'typeorm';
 | 
			
		||||
import { DbUserJobData } from '@/queue/types.js';
 | 
			
		||||
@@ -23,73 +23,72 @@ export async function exportFollowing(job: Bull.Job<DbUserJobData>, done: () =>
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Create temp file
 | 
			
		||||
	const [path, cleanup] = await new Promise<[string, () => void]>((res, rej) => {
 | 
			
		||||
		tmp.file((e, path, fd, cleanup) => {
 | 
			
		||||
			if (e) return rej(e);
 | 
			
		||||
			res([path, cleanup]);
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
	const [path, cleanup] = await createTemp();
 | 
			
		||||
 | 
			
		||||
	logger.info(`Temp file is ${path}`);
 | 
			
		||||
 | 
			
		||||
	const stream = fs.createWriteStream(path, { flags: 'a' });
 | 
			
		||||
	try {
 | 
			
		||||
		const stream = fs.createWriteStream(path, { flags: 'a' });
 | 
			
		||||
 | 
			
		||||
	let cursor: Following['id'] | null = null;
 | 
			
		||||
		let cursor: Following['id'] | null = null;
 | 
			
		||||
 | 
			
		||||
	const mutings = job.data.excludeMuting ? await Mutings.findBy({
 | 
			
		||||
		muterId: user.id,
 | 
			
		||||
	}) : [];
 | 
			
		||||
		const mutings = job.data.excludeMuting ? await Mutings.findBy({
 | 
			
		||||
			muterId: user.id,
 | 
			
		||||
		}) : [];
 | 
			
		||||
 | 
			
		||||
	while (true) {
 | 
			
		||||
		const followings = await Followings.find({
 | 
			
		||||
			where: {
 | 
			
		||||
				followerId: user.id,
 | 
			
		||||
				...(mutings.length > 0 ? { followeeId: Not(In(mutings.map(x => x.muteeId))) } : {}),
 | 
			
		||||
				...(cursor ? { id: MoreThan(cursor) } : {}),
 | 
			
		||||
			},
 | 
			
		||||
			take: 100,
 | 
			
		||||
			order: {
 | 
			
		||||
				id: 1,
 | 
			
		||||
			},
 | 
			
		||||
		}) as Following[];
 | 
			
		||||
		while (true) {
 | 
			
		||||
			const followings = await Followings.find({
 | 
			
		||||
				where: {
 | 
			
		||||
					followerId: user.id,
 | 
			
		||||
					...(mutings.length > 0 ? { followeeId: Not(In(mutings.map(x => x.muteeId))) } : {}),
 | 
			
		||||
					...(cursor ? { id: MoreThan(cursor) } : {}),
 | 
			
		||||
				},
 | 
			
		||||
				take: 100,
 | 
			
		||||
				order: {
 | 
			
		||||
					id: 1,
 | 
			
		||||
				},
 | 
			
		||||
			}) as Following[];
 | 
			
		||||
 | 
			
		||||
		if (followings.length === 0) {
 | 
			
		||||
			break;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		cursor = followings[followings.length - 1].id;
 | 
			
		||||
 | 
			
		||||
		for (const following of followings) {
 | 
			
		||||
			const u = await Users.findOneBy({ id: following.followeeId });
 | 
			
		||||
			if (u == null) {
 | 
			
		||||
				continue;
 | 
			
		||||
			if (followings.length === 0) {
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if (job.data.excludeInactive && u.updatedAt && (Date.now() - u.updatedAt.getTime() > 1000 * 60 * 60 * 24 * 90)) {
 | 
			
		||||
				continue;
 | 
			
		||||
			}
 | 
			
		||||
			cursor = followings[followings.length - 1].id;
 | 
			
		||||
 | 
			
		||||
			const content = getFullApAccount(u.username, u.host);
 | 
			
		||||
			await new Promise<void>((res, rej) => {
 | 
			
		||||
				stream.write(content + '\n', err => {
 | 
			
		||||
					if (err) {
 | 
			
		||||
						logger.error(err);
 | 
			
		||||
						rej(err);
 | 
			
		||||
					} else {
 | 
			
		||||
						res();
 | 
			
		||||
					}
 | 
			
		||||
			for (const following of followings) {
 | 
			
		||||
				const u = await Users.findOneBy({ id: following.followeeId });
 | 
			
		||||
				if (u == null) {
 | 
			
		||||
					continue;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if (job.data.excludeInactive && u.updatedAt && (Date.now() - u.updatedAt.getTime() > 1000 * 60 * 60 * 24 * 90)) {
 | 
			
		||||
					continue;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				const content = getFullApAccount(u.username, u.host);
 | 
			
		||||
				await new Promise<void>((res, rej) => {
 | 
			
		||||
					stream.write(content + '\n', err => {
 | 
			
		||||
						if (err) {
 | 
			
		||||
							logger.error(err);
 | 
			
		||||
							rej(err);
 | 
			
		||||
						} else {
 | 
			
		||||
							res();
 | 
			
		||||
						}
 | 
			
		||||
					});
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		stream.end();
 | 
			
		||||
		logger.succ(`Exported to: ${path}`);
 | 
			
		||||
 | 
			
		||||
		const fileName = 'following-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv';
 | 
			
		||||
		const driveFile = await addFile({ user, path, name: fileName, force: true });
 | 
			
		||||
 | 
			
		||||
		logger.succ(`Exported to: ${driveFile.id}`);
 | 
			
		||||
	} finally {
 | 
			
		||||
		cleanup();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	stream.end();
 | 
			
		||||
	logger.succ(`Exported to: ${path}`);
 | 
			
		||||
 | 
			
		||||
	const fileName = 'following-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv';
 | 
			
		||||
	const driveFile = await addFile({ user, path, name: fileName, force: true });
 | 
			
		||||
 | 
			
		||||
	logger.succ(`Exported to: ${driveFile.id}`);
 | 
			
		||||
	cleanup();
 | 
			
		||||
	done();
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,11 @@
 | 
			
		||||
import Bull from 'bull';
 | 
			
		||||
import * as tmp from 'tmp';
 | 
			
		||||
import * as fs from 'node:fs';
 | 
			
		||||
 | 
			
		||||
import { queueLogger } from '../../logger.js';
 | 
			
		||||
import { addFile } from '@/services/drive/add-file.js';
 | 
			
		||||
import { format as dateFormat } from 'date-fns';
 | 
			
		||||
import { getFullApAccount } from '@/misc/convert-host.js';
 | 
			
		||||
import { createTemp } from '@/misc/create-temp.js';
 | 
			
		||||
import { Users, Mutings } from '@/models/index.js';
 | 
			
		||||
import { IsNull, MoreThan } from 'typeorm';
 | 
			
		||||
import { DbUserJobData } from '@/queue/types.js';
 | 
			
		||||
@@ -22,74 +22,73 @@ export async function exportMute(job: Bull.Job<DbUserJobData>, done: any): Promi
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Create temp file
 | 
			
		||||
	const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
 | 
			
		||||
		tmp.file((e, path, fd, cleanup) => {
 | 
			
		||||
			if (e) return rej(e);
 | 
			
		||||
			res([path, cleanup]);
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
	const [path, cleanup] = await createTemp();
 | 
			
		||||
 | 
			
		||||
	logger.info(`Temp file is ${path}`);
 | 
			
		||||
 | 
			
		||||
	const stream = fs.createWriteStream(path, { flags: 'a' });
 | 
			
		||||
	try {
 | 
			
		||||
		const stream = fs.createWriteStream(path, { flags: 'a' });
 | 
			
		||||
 | 
			
		||||
	let exportedCount = 0;
 | 
			
		||||
	let cursor: any = null;
 | 
			
		||||
		let exportedCount = 0;
 | 
			
		||||
		let cursor: any = null;
 | 
			
		||||
 | 
			
		||||
	while (true) {
 | 
			
		||||
		const mutes = await Mutings.find({
 | 
			
		||||
			where: {
 | 
			
		||||
				muterId: user.id,
 | 
			
		||||
				expiresAt: IsNull(),
 | 
			
		||||
				...(cursor ? { id: MoreThan(cursor) } : {}),
 | 
			
		||||
			},
 | 
			
		||||
			take: 100,
 | 
			
		||||
			order: {
 | 
			
		||||
				id: 1,
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
		while (true) {
 | 
			
		||||
			const mutes = await Mutings.find({
 | 
			
		||||
				where: {
 | 
			
		||||
					muterId: user.id,
 | 
			
		||||
					expiresAt: IsNull(),
 | 
			
		||||
					...(cursor ? { id: MoreThan(cursor) } : {}),
 | 
			
		||||
				},
 | 
			
		||||
				take: 100,
 | 
			
		||||
				order: {
 | 
			
		||||
					id: 1,
 | 
			
		||||
				},
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
		if (mutes.length === 0) {
 | 
			
		||||
			job.progress(100);
 | 
			
		||||
			break;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		cursor = mutes[mutes.length - 1].id;
 | 
			
		||||
 | 
			
		||||
		for (const mute of mutes) {
 | 
			
		||||
			const u = await Users.findOneBy({ id: mute.muteeId });
 | 
			
		||||
			if (u == null) {
 | 
			
		||||
				exportedCount++; continue;
 | 
			
		||||
			if (mutes.length === 0) {
 | 
			
		||||
				job.progress(100);
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const content = getFullApAccount(u.username, u.host);
 | 
			
		||||
			await new Promise<void>((res, rej) => {
 | 
			
		||||
				stream.write(content + '\n', err => {
 | 
			
		||||
					if (err) {
 | 
			
		||||
						logger.error(err);
 | 
			
		||||
						rej(err);
 | 
			
		||||
					} else {
 | 
			
		||||
						res();
 | 
			
		||||
					}
 | 
			
		||||
			cursor = mutes[mutes.length - 1].id;
 | 
			
		||||
 | 
			
		||||
			for (const mute of mutes) {
 | 
			
		||||
				const u = await Users.findOneBy({ id: mute.muteeId });
 | 
			
		||||
				if (u == null) {
 | 
			
		||||
					exportedCount++; continue;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				const content = getFullApAccount(u.username, u.host);
 | 
			
		||||
				await new Promise<void>((res, rej) => {
 | 
			
		||||
					stream.write(content + '\n', err => {
 | 
			
		||||
						if (err) {
 | 
			
		||||
							logger.error(err);
 | 
			
		||||
							rej(err);
 | 
			
		||||
						} else {
 | 
			
		||||
							res();
 | 
			
		||||
						}
 | 
			
		||||
					});
 | 
			
		||||
				});
 | 
			
		||||
				exportedCount++;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const total = await Mutings.countBy({
 | 
			
		||||
				muterId: user.id,
 | 
			
		||||
			});
 | 
			
		||||
			exportedCount++;
 | 
			
		||||
 | 
			
		||||
			job.progress(exportedCount / total);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const total = await Mutings.countBy({
 | 
			
		||||
			muterId: user.id,
 | 
			
		||||
		});
 | 
			
		||||
		stream.end();
 | 
			
		||||
		logger.succ(`Exported to: ${path}`);
 | 
			
		||||
 | 
			
		||||
		job.progress(exportedCount / total);
 | 
			
		||||
		const fileName = 'mute-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv';
 | 
			
		||||
		const driveFile = await addFile({ user, path, name: fileName, force: true });
 | 
			
		||||
 | 
			
		||||
		logger.succ(`Exported to: ${driveFile.id}`);
 | 
			
		||||
	} finally {
 | 
			
		||||
		cleanup();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	stream.end();
 | 
			
		||||
	logger.succ(`Exported to: ${path}`);
 | 
			
		||||
 | 
			
		||||
	const fileName = 'mute-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv';
 | 
			
		||||
	const driveFile = await addFile({ user, path, name: fileName, force: true });
 | 
			
		||||
 | 
			
		||||
	logger.succ(`Exported to: ${driveFile.id}`);
 | 
			
		||||
	cleanup();
 | 
			
		||||
	done();
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
import Bull from 'bull';
 | 
			
		||||
import * as tmp from 'tmp';
 | 
			
		||||
import * as fs from 'node:fs';
 | 
			
		||||
 | 
			
		||||
import { queueLogger } from '../../logger.js';
 | 
			
		||||
@@ -10,6 +9,7 @@ import { MoreThan } from 'typeorm';
 | 
			
		||||
import { Note } from '@/models/entities/note.js';
 | 
			
		||||
import { Poll } from '@/models/entities/poll.js';
 | 
			
		||||
import { DbUserJobData } from '@/queue/types.js';
 | 
			
		||||
import { createTemp } from '@/misc/create-temp.js';
 | 
			
		||||
 | 
			
		||||
const logger = queueLogger.createSubLogger('export-notes');
 | 
			
		||||
 | 
			
		||||
@@ -23,82 +23,81 @@ export async function exportNotes(job: Bull.Job<DbUserJobData>, done: any): Prom
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Create temp file
 | 
			
		||||
	const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
 | 
			
		||||
		tmp.file((e, path, fd, cleanup) => {
 | 
			
		||||
			if (e) return rej(e);
 | 
			
		||||
			res([path, cleanup]);
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
	const [path, cleanup] = await createTemp();
 | 
			
		||||
 | 
			
		||||
	logger.info(`Temp file is ${path}`);
 | 
			
		||||
 | 
			
		||||
	const stream = fs.createWriteStream(path, { flags: 'a' });
 | 
			
		||||
	try {
 | 
			
		||||
		const stream = fs.createWriteStream(path, { flags: 'a' });
 | 
			
		||||
 | 
			
		||||
	const write = (text: string): Promise<void> => {
 | 
			
		||||
		return new Promise<void>((res, rej) => {
 | 
			
		||||
			stream.write(text, err => {
 | 
			
		||||
				if (err) {
 | 
			
		||||
					logger.error(err);
 | 
			
		||||
					rej(err);
 | 
			
		||||
				} else {
 | 
			
		||||
					res();
 | 
			
		||||
				}
 | 
			
		||||
		const write = (text: string): Promise<void> => {
 | 
			
		||||
			return new Promise<void>((res, rej) => {
 | 
			
		||||
				stream.write(text, err => {
 | 
			
		||||
					if (err) {
 | 
			
		||||
						logger.error(err);
 | 
			
		||||
						rej(err);
 | 
			
		||||
					} else {
 | 
			
		||||
						res();
 | 
			
		||||
					}
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
		});
 | 
			
		||||
	};
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
	await write('[');
 | 
			
		||||
		await write('[');
 | 
			
		||||
 | 
			
		||||
	let exportedNotesCount = 0;
 | 
			
		||||
	let cursor: Note['id'] | null = null;
 | 
			
		||||
		let exportedNotesCount = 0;
 | 
			
		||||
		let cursor: Note['id'] | null = null;
 | 
			
		||||
 | 
			
		||||
	while (true) {
 | 
			
		||||
		const notes = await Notes.find({
 | 
			
		||||
			where: {
 | 
			
		||||
				userId: user.id,
 | 
			
		||||
				...(cursor ? { id: MoreThan(cursor) } : {}),
 | 
			
		||||
			},
 | 
			
		||||
			take: 100,
 | 
			
		||||
			order: {
 | 
			
		||||
				id: 1,
 | 
			
		||||
			},
 | 
			
		||||
		}) as Note[];
 | 
			
		||||
		while (true) {
 | 
			
		||||
			const notes = await Notes.find({
 | 
			
		||||
				where: {
 | 
			
		||||
					userId: user.id,
 | 
			
		||||
					...(cursor ? { id: MoreThan(cursor) } : {}),
 | 
			
		||||
				},
 | 
			
		||||
				take: 100,
 | 
			
		||||
				order: {
 | 
			
		||||
					id: 1,
 | 
			
		||||
				},
 | 
			
		||||
			}) as Note[];
 | 
			
		||||
 | 
			
		||||
		if (notes.length === 0) {
 | 
			
		||||
			job.progress(100);
 | 
			
		||||
			break;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		cursor = notes[notes.length - 1].id;
 | 
			
		||||
 | 
			
		||||
		for (const note of notes) {
 | 
			
		||||
			let poll: Poll | undefined;
 | 
			
		||||
			if (note.hasPoll) {
 | 
			
		||||
				poll = await Polls.findOneByOrFail({ noteId: note.id });
 | 
			
		||||
			if (notes.length === 0) {
 | 
			
		||||
				job.progress(100);
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
			const content = JSON.stringify(serialize(note, poll));
 | 
			
		||||
			const isFirst = exportedNotesCount === 0;
 | 
			
		||||
			await write(isFirst ? content : ',\n' + content);
 | 
			
		||||
			exportedNotesCount++;
 | 
			
		||||
 | 
			
		||||
			cursor = notes[notes.length - 1].id;
 | 
			
		||||
 | 
			
		||||
			for (const note of notes) {
 | 
			
		||||
				let poll: Poll | undefined;
 | 
			
		||||
				if (note.hasPoll) {
 | 
			
		||||
					poll = await Polls.findOneByOrFail({ noteId: note.id });
 | 
			
		||||
				}
 | 
			
		||||
				const content = JSON.stringify(serialize(note, poll));
 | 
			
		||||
				const isFirst = exportedNotesCount === 0;
 | 
			
		||||
				await write(isFirst ? content : ',\n' + content);
 | 
			
		||||
				exportedNotesCount++;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			const total = await Notes.countBy({
 | 
			
		||||
				userId: user.id,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			job.progress(exportedNotesCount / total);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const total = await Notes.countBy({
 | 
			
		||||
			userId: user.id,
 | 
			
		||||
		});
 | 
			
		||||
		await write(']');
 | 
			
		||||
 | 
			
		||||
		job.progress(exportedNotesCount / total);
 | 
			
		||||
		stream.end();
 | 
			
		||||
		logger.succ(`Exported to: ${path}`);
 | 
			
		||||
 | 
			
		||||
		const fileName = 'notes-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json';
 | 
			
		||||
		const driveFile = await addFile({ user, path, name: fileName, force: true });
 | 
			
		||||
 | 
			
		||||
		logger.succ(`Exported to: ${driveFile.id}`);
 | 
			
		||||
	} finally {
 | 
			
		||||
		cleanup();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	await write(']');
 | 
			
		||||
 | 
			
		||||
	stream.end();
 | 
			
		||||
	logger.succ(`Exported to: ${path}`);
 | 
			
		||||
 | 
			
		||||
	const fileName = 'notes-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json';
 | 
			
		||||
	const driveFile = await addFile({ user, path, name: fileName, force: true });
 | 
			
		||||
 | 
			
		||||
	logger.succ(`Exported to: ${driveFile.id}`);
 | 
			
		||||
	cleanup();
 | 
			
		||||
	done();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,11 @@
 | 
			
		||||
import Bull from 'bull';
 | 
			
		||||
import * as tmp from 'tmp';
 | 
			
		||||
import * as fs from 'node:fs';
 | 
			
		||||
 | 
			
		||||
import { queueLogger } from '../../logger.js';
 | 
			
		||||
import { addFile } from '@/services/drive/add-file.js';
 | 
			
		||||
import { format as dateFormat } from 'date-fns';
 | 
			
		||||
import { getFullApAccount } from '@/misc/convert-host.js';
 | 
			
		||||
import { createTemp } from '@/misc/create-temp.js';
 | 
			
		||||
import { Users, UserLists, UserListJoinings } from '@/models/index.js';
 | 
			
		||||
import { In } from 'typeorm';
 | 
			
		||||
import { DbUserJobData } from '@/queue/types.js';
 | 
			
		||||
@@ -26,46 +26,45 @@ export async function exportUserLists(job: Bull.Job<DbUserJobData>, done: any):
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	// Create temp file
 | 
			
		||||
	const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
 | 
			
		||||
		tmp.file((e, path, fd, cleanup) => {
 | 
			
		||||
			if (e) return rej(e);
 | 
			
		||||
			res([path, cleanup]);
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
	const [path, cleanup] = await createTemp();
 | 
			
		||||
 | 
			
		||||
	logger.info(`Temp file is ${path}`);
 | 
			
		||||
 | 
			
		||||
	const stream = fs.createWriteStream(path, { flags: 'a' });
 | 
			
		||||
	try {
 | 
			
		||||
		const stream = fs.createWriteStream(path, { flags: 'a' });
 | 
			
		||||
 | 
			
		||||
	for (const list of lists) {
 | 
			
		||||
		const joinings = await UserListJoinings.findBy({ userListId: list.id });
 | 
			
		||||
		const users = await Users.findBy({
 | 
			
		||||
			id: In(joinings.map(j => j.userId)),
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		for (const u of users) {
 | 
			
		||||
			const acct = getFullApAccount(u.username, u.host);
 | 
			
		||||
			const content = `${list.name},${acct}`;
 | 
			
		||||
			await new Promise<void>((res, rej) => {
 | 
			
		||||
				stream.write(content + '\n', err => {
 | 
			
		||||
					if (err) {
 | 
			
		||||
						logger.error(err);
 | 
			
		||||
						rej(err);
 | 
			
		||||
					} else {
 | 
			
		||||
						res();
 | 
			
		||||
					}
 | 
			
		||||
				});
 | 
			
		||||
		for (const list of lists) {
 | 
			
		||||
			const joinings = await UserListJoinings.findBy({ userListId: list.id });
 | 
			
		||||
			const users = await Users.findBy({
 | 
			
		||||
				id: In(joinings.map(j => j.userId)),
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			for (const u of users) {
 | 
			
		||||
				const acct = getFullApAccount(u.username, u.host);
 | 
			
		||||
				const content = `${list.name},${acct}`;
 | 
			
		||||
				await new Promise<void>((res, rej) => {
 | 
			
		||||
					stream.write(content + '\n', err => {
 | 
			
		||||
						if (err) {
 | 
			
		||||
							logger.error(err);
 | 
			
		||||
							rej(err);
 | 
			
		||||
						} else {
 | 
			
		||||
							res();
 | 
			
		||||
						}
 | 
			
		||||
					});
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		stream.end();
 | 
			
		||||
		logger.succ(`Exported to: ${path}`);
 | 
			
		||||
 | 
			
		||||
		const fileName = 'user-lists-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv';
 | 
			
		||||
		const driveFile = await addFile({ user, path, name: fileName, force: true });
 | 
			
		||||
 | 
			
		||||
		logger.succ(`Exported to: ${driveFile.id}`);
 | 
			
		||||
	} finally {
 | 
			
		||||
		cleanup();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	stream.end();
 | 
			
		||||
	logger.succ(`Exported to: ${path}`);
 | 
			
		||||
 | 
			
		||||
	const fileName = 'user-lists-' + dateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.csv';
 | 
			
		||||
	const driveFile = await addFile({ user, path, name: fileName, force: true });
 | 
			
		||||
 | 
			
		||||
	logger.succ(`Exported to: ${driveFile.id}`);
 | 
			
		||||
	cleanup();
 | 
			
		||||
	done();
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
import Bull from 'bull';
 | 
			
		||||
import * as tmp from 'tmp';
 | 
			
		||||
import * as fs from 'node:fs';
 | 
			
		||||
import unzipper from 'unzipper';
 | 
			
		||||
 | 
			
		||||
import { queueLogger } from '../../logger.js';
 | 
			
		||||
import { createTempDir } from '@/misc/create-temp.js';
 | 
			
		||||
import { downloadUrl } from '@/misc/download-url.js';
 | 
			
		||||
import { DriveFiles, Emojis } from '@/models/index.js';
 | 
			
		||||
import { DbUserImportJobData } from '@/queue/types.js';
 | 
			
		||||
@@ -25,13 +25,7 @@ export async function importCustomEmojis(job: Bull.Job<DbUserImportJobData>, don
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Create temp dir
 | 
			
		||||
	const [path, cleanup] = await new Promise<[string, () => void]>((res, rej) => {
 | 
			
		||||
		tmp.dir((e, path, cleanup) => {
 | 
			
		||||
			if (e) return rej(e);
 | 
			
		||||
			res([path, cleanup]);
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
	const [path, cleanup] = await createTempDir();
 | 
			
		||||
 | 
			
		||||
	logger.info(`Temp dir is ${path}`);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import { URL } from 'node:url';
 | 
			
		||||
import Bull from 'bull';
 | 
			
		||||
import httpSignature from 'http-signature';
 | 
			
		||||
import httpSignature from '@peertube/http-signature';
 | 
			
		||||
import perform from '@/remote/activitypub/perform.js';
 | 
			
		||||
import Logger from '@/services/logger.js';
 | 
			
		||||
import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc.js';
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@ import { Note } from '@/models/entities/note';
 | 
			
		||||
import { User } from '@/models/entities/user.js';
 | 
			
		||||
import { Webhook } from '@/models/entities/webhook';
 | 
			
		||||
import { IActivity } from '@/remote/activitypub/type.js';
 | 
			
		||||
import httpSignature from 'http-signature';
 | 
			
		||||
import httpSignature from '@peertube/http-signature';
 | 
			
		||||
 | 
			
		||||
export type DeliverJobData = {
 | 
			
		||||
	/** Actor */
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ import { fetchMeta } from '@/misc/fetch-meta.js';
 | 
			
		||||
import { getApLock } from '@/misc/app-lock.js';
 | 
			
		||||
import { parseAudience } from '../../audience.js';
 | 
			
		||||
import { StatusError } from '@/misc/fetch.js';
 | 
			
		||||
import { Notes } from '@/models/index.js';
 | 
			
		||||
 | 
			
		||||
const logger = apLogger;
 | 
			
		||||
 | 
			
		||||
@@ -52,6 +53,8 @@ export default async function(resolver: Resolver, actor: CacheableRemoteUser, ac
 | 
			
		||||
			throw e;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (!await Notes.isVisibleForMe(renote, actor.id)) return 'skip: invalid actor for this activity';
 | 
			
		||||
 | 
			
		||||
		logger.info(`Creating the (Re)Note: ${uri}`);
 | 
			
		||||
 | 
			
		||||
		const activityAudience = await parseAudience(actor, activity.to, activity.cc);
 | 
			
		||||
 
 | 
			
		||||
@@ -13,37 +13,37 @@ export default async (actor: CacheableRemoteUser, activity: IDelete): Promise<st
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 削除対象objectのtype
 | 
			
		||||
	let formarType: string | undefined;
 | 
			
		||||
	let formerType: string | undefined;
 | 
			
		||||
 | 
			
		||||
	if (typeof activity.object === 'string') {
 | 
			
		||||
		// typeが不明だけど、どうせ消えてるのでremote resolveしない
 | 
			
		||||
		formarType = undefined;
 | 
			
		||||
		formerType = undefined;
 | 
			
		||||
	} else {
 | 
			
		||||
		const object = activity.object as IObject;
 | 
			
		||||
		if (isTombstone(object)) {
 | 
			
		||||
			formarType = toSingle(object.formerType);
 | 
			
		||||
			formerType = toSingle(object.formerType);
 | 
			
		||||
		} else {
 | 
			
		||||
			formarType = toSingle(object.type);
 | 
			
		||||
			formerType = toSingle(object.type);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const uri = getApId(activity.object);
 | 
			
		||||
 | 
			
		||||
	// type不明でもactorとobjectが同じならばそれはPersonに違いない
 | 
			
		||||
	if (!formarType && actor.uri === uri) {
 | 
			
		||||
		formarType = 'Person';
 | 
			
		||||
	if (!formerType && actor.uri === uri) {
 | 
			
		||||
		formerType = 'Person';
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// それでもなかったらおそらくNote
 | 
			
		||||
	if (!formarType) {
 | 
			
		||||
		formarType = 'Note';
 | 
			
		||||
	if (!formerType) {
 | 
			
		||||
		formerType = 'Note';
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (validPost.includes(formarType)) {
 | 
			
		||||
	if (validPost.includes(formerType)) {
 | 
			
		||||
		return await deleteNote(actor, uri);
 | 
			
		||||
	} else if (validActor.includes(formarType)) {
 | 
			
		||||
	} else if (validActor.includes(formerType)) {
 | 
			
		||||
		return await deleteActor(actor, uri);
 | 
			
		||||
	} else {
 | 
			
		||||
		return `Unknown type ${formarType}`;
 | 
			
		||||
		return `Unknown type ${formerType}`;
 | 
			
		||||
	}
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ export const undoAnnounce = async (actor: CacheableRemoteUser, activity: IAnnoun
 | 
			
		||||
 | 
			
		||||
	const note = await Notes.findOneBy({
 | 
			
		||||
		uri,
 | 
			
		||||
		userId: actor.id,
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	if (!note) return 'skip: no such Announce';
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import Router from '@koa/router';
 | 
			
		||||
import json from 'koa-json-body';
 | 
			
		||||
import httpSignature from 'http-signature';
 | 
			
		||||
import httpSignature from '@peertube/http-signature';
 | 
			
		||||
 | 
			
		||||
import { renderActivity } from '@/remote/activitypub/renderer/index.js';
 | 
			
		||||
import renderNote from '@/remote/activitypub/renderer/note.js';
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,7 @@ export const paramDef = {
 | 
			
		||||
		blockedHosts: { type: 'array', nullable: true, items: {
 | 
			
		||||
			type: 'string',
 | 
			
		||||
		} },
 | 
			
		||||
		themeColor: { type: 'string', nullable: true },
 | 
			
		||||
		themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' },
 | 
			
		||||
		mascotImageUrl: { type: 'string', nullable: true },
 | 
			
		||||
		bannerUrl: { type: 'string', nullable: true },
 | 
			
		||||
		errorImageUrl: { type: 'string', nullable: true },
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,8 @@ import bcrypt from 'bcryptjs';
 | 
			
		||||
import * as speakeasy from 'speakeasy';
 | 
			
		||||
import * as QRCode from 'qrcode';
 | 
			
		||||
import config from '@/config/index.js';
 | 
			
		||||
import define from '../../../define.js';
 | 
			
		||||
import { UserProfiles } from '@/models/index.js';
 | 
			
		||||
import define from '../../../define.js';
 | 
			
		||||
 | 
			
		||||
export const meta = {
 | 
			
		||||
	requireCredential: true,
 | 
			
		||||
@@ -40,15 +40,17 @@ export default define(meta, paramDef, async (ps, user) => {
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	// Get the data URL of the authenticator URL
 | 
			
		||||
	const dataUrl = await QRCode.toDataURL(speakeasy.otpauthURL({
 | 
			
		||||
	const url = speakeasy.otpauthURL({
 | 
			
		||||
		secret: secret.base32,
 | 
			
		||||
		encoding: 'base32',
 | 
			
		||||
		label: user.username,
 | 
			
		||||
		issuer: config.host,
 | 
			
		||||
	}));
 | 
			
		||||
	});
 | 
			
		||||
	const dataUrl = await QRCode.toDataURL(url);
 | 
			
		||||
 | 
			
		||||
	return {
 | 
			
		||||
		qr: dataUrl,
 | 
			
		||||
		url,
 | 
			
		||||
		secret: secret.base32,
 | 
			
		||||
		label: user.username,
 | 
			
		||||
		issuer: config.host,
 | 
			
		||||
 
 | 
			
		||||
@@ -172,10 +172,14 @@ export default define(meta, paramDef, async (ps, user) => {
 | 
			
		||||
	let files: DriveFile[] = [];
 | 
			
		||||
	const fileIds = ps.fileIds != null ? ps.fileIds : ps.mediaIds != null ? ps.mediaIds : null;
 | 
			
		||||
	if (fileIds != null) {
 | 
			
		||||
		files = await DriveFiles.findBy({
 | 
			
		||||
			userId: user.id,
 | 
			
		||||
			id: In(fileIds),
 | 
			
		||||
		});
 | 
			
		||||
		files = await DriveFiles.createQueryBuilder('file')
 | 
			
		||||
			.where('file.userId = :userId AND file.id IN (:...fileIds)', {
 | 
			
		||||
				userId: user.id,
 | 
			
		||||
				fileIds,
 | 
			
		||||
			})
 | 
			
		||||
			.orderBy('array_position(ARRAY[:...fileIds], "id"::text)')
 | 
			
		||||
			.setParameters({ fileIds })
 | 
			
		||||
			.getMany();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	let renote: Note | null = null;
 | 
			
		||||
 
 | 
			
		||||
@@ -61,7 +61,14 @@ export default define(meta, paramDef, async (ps, me) => {
 | 
			
		||||
			.getMany();
 | 
			
		||||
	} else {
 | 
			
		||||
		const nameQuery = Users.createQueryBuilder('user')
 | 
			
		||||
			.where('user.name ILIKE :query', { query: '%' + ps.query + '%' })
 | 
			
		||||
			.where(new Brackets(qb => { 
 | 
			
		||||
				qb.where('user.name ILIKE :query', { query: '%' + ps.query + '%' });
 | 
			
		||||
 | 
			
		||||
				// Also search username if it qualifies as username
 | 
			
		||||
				if (Users.validateLocalUsername(ps.query)) {
 | 
			
		||||
					qb.orWhere('user.usernameLower LIKE :username', { username: '%' + ps.query.toLowerCase() + '%' });
 | 
			
		||||
				}
 | 
			
		||||
			}))
 | 
			
		||||
			.andWhere(new Brackets(qb => { qb
 | 
			
		||||
				.where('user.updatedAt IS NULL')
 | 
			
		||||
				.orWhere('user.updatedAt > :activeThreshold', { activeThreshold: activeThreshold });
 | 
			
		||||
 
 | 
			
		||||
@@ -59,6 +59,18 @@ export function genOpenapiSpec(lang = 'ja-JP') {
 | 
			
		||||
			desc += ` / **Permission**: *${kind}*`;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const requestType = endpoint.meta.requireFile ? 'multipart/form-data' : 'application/json';
 | 
			
		||||
		const schema = endpoint.params;
 | 
			
		||||
 | 
			
		||||
		if (endpoint.meta.requireFile) {
 | 
			
		||||
			schema.properties.file = {
 | 
			
		||||
				type: 'string',
 | 
			
		||||
				format: 'binary',
 | 
			
		||||
				description: 'The file contents.',
 | 
			
		||||
			};
 | 
			
		||||
			schema.required.push('file');
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const info = {
 | 
			
		||||
			operationId: endpoint.name,
 | 
			
		||||
			summary: endpoint.name,
 | 
			
		||||
@@ -78,8 +90,8 @@ export function genOpenapiSpec(lang = 'ja-JP') {
 | 
			
		||||
			requestBody: {
 | 
			
		||||
				required: true,
 | 
			
		||||
				content: {
 | 
			
		||||
					'application/json': {
 | 
			
		||||
						schema: endpoint.params,
 | 
			
		||||
					[requestType]: {
 | 
			
		||||
						schema,
 | 
			
		||||
					},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
 
 | 
			
		||||
@@ -1,20 +1,25 @@
 | 
			
		||||
import { randomBytes } from 'node:crypto';
 | 
			
		||||
import Koa from 'koa';
 | 
			
		||||
import bcrypt from 'bcryptjs';
 | 
			
		||||
import * as speakeasy from 'speakeasy';
 | 
			
		||||
import signin from '../common/signin.js';
 | 
			
		||||
import { IsNull } from 'typeorm';
 | 
			
		||||
import config from '@/config/index.js';
 | 
			
		||||
import { Users, Signins, UserProfiles, UserSecurityKeys, AttestationChallenges } from '@/models/index.js';
 | 
			
		||||
import { ILocalUser } from '@/models/entities/user.js';
 | 
			
		||||
import { genId } from '@/misc/gen-id.js';
 | 
			
		||||
import { fetchMeta } from '@/misc/fetch-meta.js';
 | 
			
		||||
import { verifyHcaptcha, verifyRecaptcha } from '@/misc/captcha.js';
 | 
			
		||||
import { verifyLogin, hash } from '../2fa.js';
 | 
			
		||||
import { randomBytes } from 'node:crypto';
 | 
			
		||||
import { IsNull } from 'typeorm';
 | 
			
		||||
import signin from '../common/signin.js';
 | 
			
		||||
 | 
			
		||||
export default async (ctx: Koa.Context) => {
 | 
			
		||||
	ctx.set('Access-Control-Allow-Origin', config.url);
 | 
			
		||||
	ctx.set('Access-Control-Allow-Credentials', 'true');
 | 
			
		||||
 | 
			
		||||
	const body = ctx.request.body as any;
 | 
			
		||||
 | 
			
		||||
	const instance = await fetchMeta(true);
 | 
			
		||||
 | 
			
		||||
	const username = body['username'];
 | 
			
		||||
	const password = body['password'];
 | 
			
		||||
	const token = body['token'];
 | 
			
		||||
@@ -79,6 +84,18 @@ export default async (ctx: Koa.Context) => {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (!profile.twoFactorEnabled) {
 | 
			
		||||
		if (instance.enableHcaptcha && instance.hcaptchaSecretKey) {
 | 
			
		||||
			await verifyHcaptcha(instance.hcaptchaSecretKey, body['hcaptcha-response']).catch(e => {
 | 
			
		||||
				ctx.throw(400, e);
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	
 | 
			
		||||
		if (instance.enableRecaptcha && instance.recaptchaSecretKey) {
 | 
			
		||||
			await verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(e => {
 | 
			
		||||
				ctx.throw(400, e);
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	
 | 
			
		||||
		if (same) {
 | 
			
		||||
			signin(ctx, user);
 | 
			
		||||
			return;
 | 
			
		||||
@@ -155,7 +172,7 @@ export default async (ctx: Koa.Context) => {
 | 
			
		||||
				body.credentialId
 | 
			
		||||
					.replace(/-/g, '+')
 | 
			
		||||
					.replace(/_/g, '/'),
 | 
			
		||||
					'base64'
 | 
			
		||||
				'base64',
 | 
			
		||||
			).toString('hex'),
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,11 +4,11 @@ import { dirname } from 'node:path';
 | 
			
		||||
import Koa from 'koa';
 | 
			
		||||
import send from 'koa-send';
 | 
			
		||||
import rename from 'rename';
 | 
			
		||||
import * as tmp from 'tmp';
 | 
			
		||||
import { serverLogger } from '../index.js';
 | 
			
		||||
import { contentDisposition } from '@/misc/content-disposition.js';
 | 
			
		||||
import { DriveFiles } from '@/models/index.js';
 | 
			
		||||
import { InternalStorage } from '@/services/drive/internal-storage.js';
 | 
			
		||||
import { createTemp } from '@/misc/create-temp.js';
 | 
			
		||||
import { downloadUrl } from '@/misc/download-url.js';
 | 
			
		||||
import { detectType } from '@/misc/get-file-info.js';
 | 
			
		||||
import { convertToWebp, convertToJpeg, convertToPng } from '@/services/drive/image-processor.js';
 | 
			
		||||
@@ -50,12 +50,7 @@ export default async function(ctx: Koa.Context) {
 | 
			
		||||
 | 
			
		||||
	if (!file.storedInternal) {
 | 
			
		||||
		if (file.isLink && file.uri) {	// 期限切れリモートファイル
 | 
			
		||||
			const [path, cleanup] = await new Promise<[string, any]>((res, rej) => {
 | 
			
		||||
				tmp.file((e, path, fd, cleanup) => {
 | 
			
		||||
					if (e) return rej(e);
 | 
			
		||||
					res([path, cleanup]);
 | 
			
		||||
				});
 | 
			
		||||
			});
 | 
			
		||||
			const [path, cleanup] = await createTemp();
 | 
			
		||||
 | 
			
		||||
			try {
 | 
			
		||||
				await downloadUrl(file.uri, path);
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@
 | 
			
		||||
 * Core Server
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import cluster from 'node:cluster';
 | 
			
		||||
import * as fs from 'node:fs';
 | 
			
		||||
import * as http from 'node:http';
 | 
			
		||||
import Koa from 'koa';
 | 
			
		||||
@@ -88,10 +89,10 @@ router.get('/avatar/@:acct', async ctx => {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
router.get('/identicon/:x', async ctx => {
 | 
			
		||||
	const [temp] = await createTemp();
 | 
			
		||||
	const [temp, cleanup] = await createTemp();
 | 
			
		||||
	await genIdenticon(ctx.params.x, fs.createWriteStream(temp));
 | 
			
		||||
	ctx.set('Content-Type', 'image/png');
 | 
			
		||||
	ctx.body = fs.createReadStream(temp);
 | 
			
		||||
	ctx.body = fs.createReadStream(temp).on('close', () => cleanup());
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
router.get('/verify-email/:code', async ctx => {
 | 
			
		||||
@@ -142,5 +143,26 @@ export default () => new Promise(resolve => {
 | 
			
		||||
 | 
			
		||||
	initializeStreamingServer(server);
 | 
			
		||||
 | 
			
		||||
	server.on('error', e => {
 | 
			
		||||
		switch ((e as any).code) {
 | 
			
		||||
			case 'EACCES':
 | 
			
		||||
				serverLogger.error(`You do not have permission to listen on port ${config.port}.`);
 | 
			
		||||
				break;
 | 
			
		||||
			case 'EADDRINUSE':
 | 
			
		||||
				serverLogger.error(`Port ${config.port} is already in use by another process.`);
 | 
			
		||||
				break;
 | 
			
		||||
			default:
 | 
			
		||||
				serverLogger.error(e);
 | 
			
		||||
				break;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (cluster.isWorker) {
 | 
			
		||||
			process.send!('listenFailed');
 | 
			
		||||
		} else {
 | 
			
		||||
			// disableClustering
 | 
			
		||||
			process.exit(1);
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	server.listen(config.port, resolve);
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -58,15 +58,11 @@
 | 
			
		||||
		? `?salt=${localStorage.getItem('salt')}`
 | 
			
		||||
		: '';
 | 
			
		||||
 | 
			
		||||
	const script = document.createElement('script');
 | 
			
		||||
	script.setAttribute('src', `/assets/app.${v}.js${salt}`);
 | 
			
		||||
	script.setAttribute('async', 'true');
 | 
			
		||||
	script.setAttribute('defer', 'true');
 | 
			
		||||
	script.addEventListener('error', async () => {
 | 
			
		||||
		await checkUpdate();
 | 
			
		||||
		renderError('APP_FETCH_FAILED');
 | 
			
		||||
	});
 | 
			
		||||
	document.head.appendChild(script);
 | 
			
		||||
	import(`/assets/${CLIENT_ENTRY}${salt}`)
 | 
			
		||||
		.catch(async () => {
 | 
			
		||||
			await checkUpdate();
 | 
			
		||||
			renderError('APP_FETCH_FAILED');
 | 
			
		||||
		})
 | 
			
		||||
	//#endregion
 | 
			
		||||
 | 
			
		||||
	//#region Theme
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@
 | 
			
		||||
 | 
			
		||||
import { dirname } from 'node:path';
 | 
			
		||||
import { fileURLToPath } from 'node:url';
 | 
			
		||||
import { PathOrFileDescriptor, readFileSync } from 'node:fs';
 | 
			
		||||
import ms from 'ms';
 | 
			
		||||
import Koa from 'koa';
 | 
			
		||||
import Router from '@koa/router';
 | 
			
		||||
@@ -73,6 +74,9 @@ app.use(views(_dirname + '/views', {
 | 
			
		||||
	extension: 'pug',
 | 
			
		||||
	options: {
 | 
			
		||||
		version: config.version,
 | 
			
		||||
		clientEntry: () => process.env.NODE_ENV === 'production' ?
 | 
			
		||||
			config.clientEntry :
 | 
			
		||||
			JSON.parse(readFileSync(`${_dirname}/../../../../../built/_client_dist_/manifest.json`, 'utf-8'))['src/init.ts'].file.replace(/^_client_dist_\//, ''),
 | 
			
		||||
		config,
 | 
			
		||||
	},
 | 
			
		||||
}));
 | 
			
		||||
 
 | 
			
		||||
@@ -39,28 +39,24 @@ html {
 | 
			
		||||
	width: 28px;
 | 
			
		||||
	height: 28px;
 | 
			
		||||
	transform: translateY(70px);
 | 
			
		||||
	color: var(--accent);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#splashSpinner:before,
 | 
			
		||||
#splashSpinner:after {
 | 
			
		||||
	content: " ";
 | 
			
		||||
	display: block;
 | 
			
		||||
	box-sizing: border-box;
 | 
			
		||||
	width: 28px;
 | 
			
		||||
	height: 28px;
 | 
			
		||||
	border-radius: 50%;
 | 
			
		||||
	border: solid 4px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#splashSpinner:before {
 | 
			
		||||
	border-color: currentColor;
 | 
			
		||||
	opacity: 0.3;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#splashSpinner:after {
 | 
			
		||||
#splashSpinner > .spinner {
 | 
			
		||||
	position: absolute;
 | 
			
		||||
	top: 0;
 | 
			
		||||
	border-color: currentColor transparent transparent transparent;
 | 
			
		||||
	left: 0;
 | 
			
		||||
	width: 28px;
 | 
			
		||||
	height: 28px;
 | 
			
		||||
	fill-rule: evenodd;
 | 
			
		||||
	clip-rule: evenodd;
 | 
			
		||||
	stroke-linecap: round;
 | 
			
		||||
	stroke-linejoin: round;
 | 
			
		||||
	stroke-miterlimit: 1.5;
 | 
			
		||||
}
 | 
			
		||||
#splashSpinner > .spinner.bg {
 | 
			
		||||
	opacity: 0.275;
 | 
			
		||||
}
 | 
			
		||||
#splashSpinner > .spinner.fg {
 | 
			
		||||
	animation: splashSpinner 0.5s linear infinite;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -50,6 +50,10 @@ html
 | 
			
		||||
		style
 | 
			
		||||
			include ../style.css
 | 
			
		||||
 | 
			
		||||
		script.
 | 
			
		||||
			var VERSION = "#{version}";
 | 
			
		||||
			var CLIENT_ENTRY = "#{clientEntry()}";
 | 
			
		||||
 | 
			
		||||
		script
 | 
			
		||||
			include ../boot.js
 | 
			
		||||
 | 
			
		||||
@@ -61,4 +65,14 @@ html
 | 
			
		||||
		div#splash
 | 
			
		||||
			img#splashIcon(src= icon || '/static-assets/splash.png')
 | 
			
		||||
			div#splashSpinner
 | 
			
		||||
				<svg class="spinner bg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
					<g transform="matrix(1,0,0,1,12,12)">
 | 
			
		||||
						<circle cx="64" cy="64" r="64" style="fill:none;stroke:currentColor;stroke-width:24px;"/>
 | 
			
		||||
					</g>
 | 
			
		||||
				</svg>
 | 
			
		||||
				<svg class="spinner fg" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
					<g transform="matrix(1,0,0,1,12,12)">
 | 
			
		||||
						<path d="M128,64C128,28.654 99.346,0 64,0C99.346,0 128,28.654 128,64Z" style="fill:none;stroke:currentColor;stroke-width:24px;"/>
 | 
			
		||||
					</g>
 | 
			
		||||
				</svg>
 | 
			
		||||
		block content
 | 
			
		||||
 
 | 
			
		||||
@@ -41,6 +41,7 @@ router.options(allPath, async ctx => {
 | 
			
		||||
router.get('/.well-known/host-meta', async ctx => {
 | 
			
		||||
	ctx.set('Content-Type', xrd);
 | 
			
		||||
	ctx.body = XRD({ element: 'Link', attributes: {
 | 
			
		||||
		rel: 'lrdd',
 | 
			
		||||
		type: xrd,
 | 
			
		||||
		template: `${config.url}${webFingerPath}?resource={uri}`,
 | 
			
		||||
	} });
 | 
			
		||||
 
 | 
			
		||||
@@ -1,38 +1,31 @@
 | 
			
		||||
import * as fs from 'node:fs';
 | 
			
		||||
import * as tmp from 'tmp';
 | 
			
		||||
import * as path from 'node:path';
 | 
			
		||||
import { createTemp } from '@/misc/create-temp.js';
 | 
			
		||||
import { IImage, convertToJpeg } from './image-processor.js';
 | 
			
		||||
import * as FFmpeg from 'fluent-ffmpeg';
 | 
			
		||||
import FFmpeg from 'fluent-ffmpeg';
 | 
			
		||||
 | 
			
		||||
export async function GenerateVideoThumbnail(path: string): Promise<IImage> {
 | 
			
		||||
	const [outDir, cleanup] = await new Promise<[string, any]>((res, rej) => {
 | 
			
		||||
		tmp.dir((e, path, cleanup) => {
 | 
			
		||||
			if (e) return rej(e);
 | 
			
		||||
			res([path, cleanup]);
 | 
			
		||||
export async function GenerateVideoThumbnail(source: string): Promise<IImage> {
 | 
			
		||||
	const [file, cleanup] = await createTemp();
 | 
			
		||||
	const parsed = path.parse(file);
 | 
			
		||||
 | 
			
		||||
	try {
 | 
			
		||||
		await new Promise((res, rej) => {
 | 
			
		||||
			FFmpeg({
 | 
			
		||||
				source,
 | 
			
		||||
			})
 | 
			
		||||
			.on('end', res)
 | 
			
		||||
			.on('error', rej)
 | 
			
		||||
			.screenshot({
 | 
			
		||||
				folder: parsed.dir,
 | 
			
		||||
				filename: parsed.base,
 | 
			
		||||
				count: 1,
 | 
			
		||||
				timestamps: ['5%'],
 | 
			
		||||
			});
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	await new Promise((res, rej) => {
 | 
			
		||||
		FFmpeg({
 | 
			
		||||
			source: path,
 | 
			
		||||
		})
 | 
			
		||||
		.on('end', res)
 | 
			
		||||
		.on('error', rej)
 | 
			
		||||
		.screenshot({
 | 
			
		||||
			folder: outDir,
 | 
			
		||||
			filename: 'output.png',
 | 
			
		||||
			count: 1,
 | 
			
		||||
			timestamps: ['5%'],
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	const outPath = `${outDir}/output.png`;
 | 
			
		||||
 | 
			
		||||
	// JPEGに変換 (Webpでもいいが、MastodonはWebpをサポートせず表示できなくなる)
 | 
			
		||||
	const thumbnail = await convertToJpeg(outPath, 498, 280);
 | 
			
		||||
 | 
			
		||||
	// cleanup
 | 
			
		||||
	await fs.promises.unlink(outPath);
 | 
			
		||||
	cleanup();
 | 
			
		||||
 | 
			
		||||
	return thumbnail;
 | 
			
		||||
		// JPEGに変換 (Webpでもいいが、MastodonはWebpをサポートせず表示できなくなる)
 | 
			
		||||
		return await convertToJpeg(498, 280);
 | 
			
		||||
	} finally {
 | 
			
		||||
		cleanup();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -45,29 +45,20 @@ export async function uploadFromUrl({
 | 
			
		||||
	// Create temp file
 | 
			
		||||
	const [path, cleanup] = await createTemp();
 | 
			
		||||
 | 
			
		||||
	// write content at URL to temp file
 | 
			
		||||
	await downloadUrl(url, path);
 | 
			
		||||
 | 
			
		||||
	let driveFile: DriveFile;
 | 
			
		||||
	let error;
 | 
			
		||||
 | 
			
		||||
	try {
 | 
			
		||||
		driveFile = await addFile({ user, path, name, comment, folderId, force, isLink, url, uri, sensitive });
 | 
			
		||||
		// write content at URL to temp file
 | 
			
		||||
		await downloadUrl(url, path);
 | 
			
		||||
 | 
			
		||||
		const driveFile = await addFile({ user, path, name, comment, folderId, force, isLink, url, uri, sensitive });
 | 
			
		||||
		logger.succ(`Got: ${driveFile.id}`);
 | 
			
		||||
		return driveFile!;
 | 
			
		||||
	} catch (e) {
 | 
			
		||||
		error = e;
 | 
			
		||||
		logger.error(`Failed to create drive file: ${e}`, {
 | 
			
		||||
			url: url,
 | 
			
		||||
			e: e,
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// clean-up
 | 
			
		||||
	cleanup();
 | 
			
		||||
 | 
			
		||||
	if (error) {
 | 
			
		||||
		throw error;
 | 
			
		||||
	} else {
 | 
			
		||||
		return driveFile!;
 | 
			
		||||
		throw e;
 | 
			
		||||
	} finally {
 | 
			
		||||
		cleanup();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import { DOMWindow, JSDOM } from 'jsdom';
 | 
			
		||||
import fetch from 'node-fetch';
 | 
			
		||||
import tinycolor from 'tinycolor2';
 | 
			
		||||
import { getJson, getHtml, getAgentByUrl } from '@/misc/fetch.js';
 | 
			
		||||
import { Instance } from '@/models/entities/instance.js';
 | 
			
		||||
import { Instances } from '@/models/index.js';
 | 
			
		||||
@@ -208,16 +209,11 @@ async function fetchIconUrl(instance: Instance, doc: DOMWindow['document'] | nul
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function getThemeColor(doc: DOMWindow['document'] | null, manifest: Record<string, any> | null): Promise<string | null> {
 | 
			
		||||
	if (doc) {
 | 
			
		||||
		const themeColor = doc.querySelector('meta[name="theme-color"]')?.getAttribute('content');
 | 
			
		||||
	const themeColor = doc?.querySelector('meta[name="theme-color"]')?.getAttribute('content') || manifest?.theme_color;
 | 
			
		||||
 | 
			
		||||
		if (themeColor) {
 | 
			
		||||
			return themeColor;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (manifest) {
 | 
			
		||||
		return manifest.theme_color;
 | 
			
		||||
	if (themeColor) {
 | 
			
		||||
		const color = new tinycolor(themeColor);
 | 
			
		||||
		if (color.isValid()) return color.toHexString();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return null;
 | 
			
		||||
 
 | 
			
		||||
@@ -187,6 +187,8 @@ export default async (user: { id: User['id']; username: User['username']; host:
 | 
			
		||||
 | 
			
		||||
	if (data.text) {
 | 
			
		||||
		data.text = data.text.trim();
 | 
			
		||||
	} else {
 | 
			
		||||
		data.text = null;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	let tags = data.apHashtags;
 | 
			
		||||
 
 | 
			
		||||
@@ -27,6 +27,11 @@ export default async (user: { id: User['id']; host: User['host']; }, note: Note,
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// check visibility
 | 
			
		||||
	if (!await Notes.isVisibleForMe(note, user.id)) {
 | 
			
		||||
		throw new IdentifiableError('68e9d2d1-48bf-42c2-b90a-b20e09fd3d48', 'Note not accessible for you.');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// TODO: cache
 | 
			
		||||
	reaction = await toDbReaction(reaction, user.host);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
	"env": {
 | 
			
		||||
		"node": true,
 | 
			
		||||
		"mocha": true,
 | 
			
		||||
		"commonjs": true
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										11
									
								
								packages/backend/test/.eslintrc.cjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								packages/backend/test/.eslintrc.cjs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
module.exports = {
 | 
			
		||||
	parserOptions: {
 | 
			
		||||
		tsconfigRootDir: __dirname,
 | 
			
		||||
		project: ['./tsconfig.json'],
 | 
			
		||||
	},
 | 
			
		||||
	extends: ['../.eslintrc.cjs'],
 | 
			
		||||
	env: {
 | 
			
		||||
		node: true,
 | 
			
		||||
		mocha: true,
 | 
			
		||||
	},
 | 
			
		||||
};
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
process.env.NODE_ENV = 'test';
 | 
			
		||||
 | 
			
		||||
import rndstr from 'rndstr';
 | 
			
		||||
import * as assert from 'assert';
 | 
			
		||||
import rndstr from 'rndstr';
 | 
			
		||||
import { initTestDb } from './utils.js';
 | 
			
		||||
 | 
			
		||||
describe('ActivityPub', () => {
 | 
			
		||||
@@ -57,8 +57,8 @@ describe('ActivityPub', () => {
 | 
			
		||||
			const note = await createNote(post.id, resolver, true);
 | 
			
		||||
 | 
			
		||||
			assert.deepStrictEqual(note?.uri, post.id);
 | 
			
		||||
			assert.deepStrictEqual(note?.visibility, 'public');
 | 
			
		||||
			assert.deepStrictEqual(note?.text, post.content);
 | 
			
		||||
			assert.deepStrictEqual(note.visibility, 'public');
 | 
			
		||||
			assert.deepStrictEqual(note.text, post.content);
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import * as assert from 'assert';
 | 
			
		||||
import httpSignature from 'http-signature';
 | 
			
		||||
import { genRsaKeyPair } from '../src/misc/gen-key-pair.js';
 | 
			
		||||
import { createSignedPost, createSignedGet } from '../src/remote/activitypub/ap-request.js';
 | 
			
		||||
import httpSignature from 'http-signature';
 | 
			
		||||
 | 
			
		||||
export const buildParsedSignature = (signingString: string, signature: string, algorithm: string) => {
 | 
			
		||||
	return {
 | 
			
		||||
@@ -13,7 +13,7 @@ export const buildParsedSignature = (signingString: string, signature: string, a
 | 
			
		||||
			signature: signature,
 | 
			
		||||
		},
 | 
			
		||||
		signingString: signingString,
 | 
			
		||||
		algorithm: algorithm?.toUpperCase(),
 | 
			
		||||
		algorithm: algorithm.toUpperCase(),
 | 
			
		||||
		keyId: 'KeyID',	// dummy, not used for verify
 | 
			
		||||
	};
 | 
			
		||||
};
 | 
			
		||||
@@ -26,7 +26,7 @@ describe('ap-request', () => {
 | 
			
		||||
		const activity = { a: 1 };
 | 
			
		||||
		const body = JSON.stringify(activity);
 | 
			
		||||
		const headers = {
 | 
			
		||||
			'User-Agent': 'UA'
 | 
			
		||||
			'User-Agent': 'UA',
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		const req = createSignedPost({ key, url, body, additionalHeaders: headers });
 | 
			
		||||
@@ -42,7 +42,7 @@ describe('ap-request', () => {
 | 
			
		||||
		const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey };
 | 
			
		||||
		const url = 'https://example.com/outbox';
 | 
			
		||||
		const headers = {
 | 
			
		||||
			'User-Agent': 'UA'
 | 
			
		||||
			'User-Agent': 'UA',
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		const req = createSignedGet({ key, url, additionalHeaders: headers });
 | 
			
		||||
 
 | 
			
		||||
@@ -61,40 +61,40 @@ describe('API visibility', () => {
 | 
			
		||||
 | 
			
		||||
		const show = async (noteId: any, by: any) => {
 | 
			
		||||
			return await request('/notes/show', {
 | 
			
		||||
				noteId
 | 
			
		||||
				noteId,
 | 
			
		||||
			}, by);
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		before(async () => {
 | 
			
		||||
			//#region prepare
 | 
			
		||||
			// signup
 | 
			
		||||
			alice    = await signup({ username: 'alice' });
 | 
			
		||||
			alice = await signup({ username: 'alice' });
 | 
			
		||||
			follower = await signup({ username: 'follower' });
 | 
			
		||||
			other    = await signup({ username: 'other' });
 | 
			
		||||
			target   = await signup({ username: 'target' });
 | 
			
		||||
			target2  = await signup({ username: 'target2' });
 | 
			
		||||
			other = await signup({ username: 'other' });
 | 
			
		||||
			target = await signup({ username: 'target' });
 | 
			
		||||
			target2 = await signup({ username: 'target2' });
 | 
			
		||||
 | 
			
		||||
			// follow alice <= follower
 | 
			
		||||
			await request('/following/create', { userId: alice.id }, follower);
 | 
			
		||||
 | 
			
		||||
			// normal posts
 | 
			
		||||
			pub  = await post(alice, { text: 'x', visibility: 'public' });
 | 
			
		||||
			pub = await post(alice, { text: 'x', visibility: 'public' });
 | 
			
		||||
			home = await post(alice, { text: 'x', visibility: 'home' });
 | 
			
		||||
			fol  = await post(alice, { text: 'x', visibility: 'followers' });
 | 
			
		||||
			spe  = await post(alice, { text: 'x', visibility: 'specified', visibleUserIds: [target.id] });
 | 
			
		||||
			fol = await post(alice, { text: 'x', visibility: 'followers' });
 | 
			
		||||
			spe = await post(alice, { text: 'x', visibility: 'specified', visibleUserIds: [target.id] });
 | 
			
		||||
 | 
			
		||||
			// replies
 | 
			
		||||
			tgt = await post(target, { text: 'y', visibility: 'public' });
 | 
			
		||||
			pubR  = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'public' });
 | 
			
		||||
			pubR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'public' });
 | 
			
		||||
			homeR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'home' });
 | 
			
		||||
			folR  = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'followers' });
 | 
			
		||||
			speR  = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'specified' });
 | 
			
		||||
			folR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'followers' });
 | 
			
		||||
			speR = await post(alice, { text: 'x', replyId: tgt.id, visibility: 'specified' });
 | 
			
		||||
 | 
			
		||||
			// mentions
 | 
			
		||||
			pubM  = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'public' });
 | 
			
		||||
			pubM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'public' });
 | 
			
		||||
			homeM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'home' });
 | 
			
		||||
			folM  = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'followers' });
 | 
			
		||||
			speM  = await post(alice, { text: '@target2 x', replyId: tgt.id, visibility: 'specified' });
 | 
			
		||||
			folM = await post(alice, { text: '@target x', replyId: tgt.id, visibility: 'followers' });
 | 
			
		||||
			speM = await post(alice, { text: '@target2 x', replyId: tgt.id, visibility: 'specified' });
 | 
			
		||||
			//#endregion
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,7 @@ describe('Block', () => {
 | 
			
		||||
 | 
			
		||||
	it('Block作成', async(async () => {
 | 
			
		||||
		const res = await request('/blocking/create', {
 | 
			
		||||
			userId: bob.id
 | 
			
		||||
			userId: bob.id,
 | 
			
		||||
		}, alice);
 | 
			
		||||
 | 
			
		||||
		assert.strictEqual(res.status, 200);
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,6 @@ process.env.NODE_ENV = 'test';
 | 
			
		||||
 | 
			
		||||
import * as assert from 'assert';
 | 
			
		||||
import * as lolex from '@sinonjs/fake-timers';
 | 
			
		||||
import { async, initTestDb } from './utils.js';
 | 
			
		||||
import TestChart from '../src/services/chart/charts/test.js';
 | 
			
		||||
import TestGroupedChart from '../src/services/chart/charts/test-grouped.js';
 | 
			
		||||
import TestUniqueChart from '../src/services/chart/charts/test-unique.js';
 | 
			
		||||
@@ -11,6 +10,7 @@ import * as _TestChart from '../src/services/chart/charts/entities/test.js';
 | 
			
		||||
import * as _TestGroupedChart from '../src/services/chart/charts/entities/test-grouped.js';
 | 
			
		||||
import * as _TestUniqueChart from '../src/services/chart/charts/entities/test-unique.js';
 | 
			
		||||
import * as _TestIntersectionChart from '../src/services/chart/charts/entities/test-intersection.js';
 | 
			
		||||
import { async, initTestDb } from './utils.js';
 | 
			
		||||
 | 
			
		||||
describe('Chart', () => {
 | 
			
		||||
	let testChart: TestChart;
 | 
			
		||||
@@ -33,7 +33,7 @@ describe('Chart', () => {
 | 
			
		||||
		testIntersectionChart = new TestIntersectionChart();
 | 
			
		||||
 | 
			
		||||
		clock = lolex.install({
 | 
			
		||||
			now: new Date(Date.UTC(2000, 0, 1, 0, 0, 0))
 | 
			
		||||
			now: new Date(Date.UTC(2000, 0, 1, 0, 0, 0)),
 | 
			
		||||
		});
 | 
			
		||||
	}));
 | 
			
		||||
 | 
			
		||||
@@ -52,7 +52,7 @@ describe('Chart', () => {
 | 
			
		||||
			foo: {
 | 
			
		||||
				dec: [0, 0, 0],
 | 
			
		||||
				inc: [1, 0, 0],
 | 
			
		||||
				total: [1, 0, 0]
 | 
			
		||||
				total: [1, 0, 0],
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
@@ -60,7 +60,7 @@ describe('Chart', () => {
 | 
			
		||||
			foo: {
 | 
			
		||||
				dec: [0, 0, 0],
 | 
			
		||||
				inc: [1, 0, 0],
 | 
			
		||||
				total: [1, 0, 0]
 | 
			
		||||
				total: [1, 0, 0],
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
	}));
 | 
			
		||||
@@ -76,7 +76,7 @@ describe('Chart', () => {
 | 
			
		||||
			foo: {
 | 
			
		||||
				dec: [1, 0, 0],
 | 
			
		||||
				inc: [0, 0, 0],
 | 
			
		||||
				total: [-1, 0, 0]
 | 
			
		||||
				total: [-1, 0, 0],
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
@@ -84,7 +84,7 @@ describe('Chart', () => {
 | 
			
		||||
			foo: {
 | 
			
		||||
				dec: [1, 0, 0],
 | 
			
		||||
				inc: [0, 0, 0],
 | 
			
		||||
				total: [-1, 0, 0]
 | 
			
		||||
				total: [-1, 0, 0],
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
	}));
 | 
			
		||||
@@ -97,7 +97,7 @@ describe('Chart', () => {
 | 
			
		||||
			foo: {
 | 
			
		||||
				dec: [0, 0, 0],
 | 
			
		||||
				inc: [0, 0, 0],
 | 
			
		||||
				total: [0, 0, 0]
 | 
			
		||||
				total: [0, 0, 0],
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
@@ -105,7 +105,7 @@ describe('Chart', () => {
 | 
			
		||||
			foo: {
 | 
			
		||||
				dec: [0, 0, 0],
 | 
			
		||||
				inc: [0, 0, 0],
 | 
			
		||||
				total: [0, 0, 0]
 | 
			
		||||
				total: [0, 0, 0],
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
	}));
 | 
			
		||||
@@ -123,7 +123,7 @@ describe('Chart', () => {
 | 
			
		||||
			foo: {
 | 
			
		||||
				dec: [0, 0, 0],
 | 
			
		||||
				inc: [3, 0, 0],
 | 
			
		||||
				total: [3, 0, 0]
 | 
			
		||||
				total: [3, 0, 0],
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
@@ -131,7 +131,7 @@ describe('Chart', () => {
 | 
			
		||||
			foo: {
 | 
			
		||||
				dec: [0, 0, 0],
 | 
			
		||||
				inc: [3, 0, 0],
 | 
			
		||||
				total: [3, 0, 0]
 | 
			
		||||
				total: [3, 0, 0],
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
	}));
 | 
			
		||||
@@ -149,7 +149,7 @@ describe('Chart', () => {
 | 
			
		||||
			foo: {
 | 
			
		||||
				dec: [0, 0, 0],
 | 
			
		||||
				inc: [1, 0, 0],
 | 
			
		||||
				total: [1, 0, 0]
 | 
			
		||||
				total: [1, 0, 0],
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
@@ -157,7 +157,7 @@ describe('Chart', () => {
 | 
			
		||||
			foo: {
 | 
			
		||||
				dec: [0, 0, 0],
 | 
			
		||||
				inc: [1, 0, 0],
 | 
			
		||||
				total: [1, 0, 0]
 | 
			
		||||
				total: [1, 0, 0],
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
	}));
 | 
			
		||||
@@ -178,7 +178,7 @@ describe('Chart', () => {
 | 
			
		||||
			foo: {
 | 
			
		||||
				dec: [0, 0, 0],
 | 
			
		||||
				inc: [1, 1, 0],
 | 
			
		||||
				total: [2, 1, 0]
 | 
			
		||||
				total: [2, 1, 0],
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
@@ -186,7 +186,7 @@ describe('Chart', () => {
 | 
			
		||||
			foo: {
 | 
			
		||||
				dec: [0, 0, 0],
 | 
			
		||||
				inc: [2, 0, 0],
 | 
			
		||||
				total: [2, 0, 0]
 | 
			
		||||
				total: [2, 0, 0],
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
	}));
 | 
			
		||||
@@ -238,7 +238,7 @@ describe('Chart', () => {
 | 
			
		||||
			foo: {
 | 
			
		||||
				dec: [0, 0, 0],
 | 
			
		||||
				inc: [1, 0, 1],
 | 
			
		||||
				total: [2, 1, 1]
 | 
			
		||||
				total: [2, 1, 1],
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
@@ -246,7 +246,7 @@ describe('Chart', () => {
 | 
			
		||||
			foo: {
 | 
			
		||||
				dec: [0, 0, 0],
 | 
			
		||||
				inc: [2, 0, 0],
 | 
			
		||||
				total: [2, 0, 0]
 | 
			
		||||
				total: [2, 0, 0],
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
	}));
 | 
			
		||||
@@ -265,7 +265,7 @@ describe('Chart', () => {
 | 
			
		||||
			foo: {
 | 
			
		||||
				dec: [0, 0, 0],
 | 
			
		||||
				inc: [0, 0, 0],
 | 
			
		||||
				total: [1, 1, 1]
 | 
			
		||||
				total: [1, 1, 1],
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
@@ -273,7 +273,7 @@ describe('Chart', () => {
 | 
			
		||||
			foo: {
 | 
			
		||||
				dec: [0, 0, 0],
 | 
			
		||||
				inc: [1, 0, 0],
 | 
			
		||||
				total: [1, 0, 0]
 | 
			
		||||
				total: [1, 0, 0],
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
	}));
 | 
			
		||||
@@ -296,7 +296,7 @@ describe('Chart', () => {
 | 
			
		||||
			foo: {
 | 
			
		||||
				dec: [0, 0, 0],
 | 
			
		||||
				inc: [1, 0, 0],
 | 
			
		||||
				total: [2, 1, 1]
 | 
			
		||||
				total: [2, 1, 1],
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
@@ -304,7 +304,7 @@ describe('Chart', () => {
 | 
			
		||||
			foo: {
 | 
			
		||||
				dec: [0, 0, 0],
 | 
			
		||||
				inc: [2, 0, 0],
 | 
			
		||||
				total: [2, 0, 0]
 | 
			
		||||
				total: [2, 0, 0],
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
	}));
 | 
			
		||||
@@ -325,7 +325,7 @@ describe('Chart', () => {
 | 
			
		||||
			foo: {
 | 
			
		||||
				dec: [0, 0, 0],
 | 
			
		||||
				inc: [1, 0, 0],
 | 
			
		||||
				total: [1, 0, 0]
 | 
			
		||||
				total: [1, 0, 0],
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
@@ -333,7 +333,7 @@ describe('Chart', () => {
 | 
			
		||||
			foo: {
 | 
			
		||||
				dec: [0, 0, 0],
 | 
			
		||||
				inc: [2, 0, 0],
 | 
			
		||||
				total: [2, 0, 0]
 | 
			
		||||
				total: [2, 0, 0],
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
	}));
 | 
			
		||||
@@ -356,7 +356,7 @@ describe('Chart', () => {
 | 
			
		||||
			foo: {
 | 
			
		||||
				dec: [0, 0, 0],
 | 
			
		||||
				inc: [1, 0, 0],
 | 
			
		||||
				total: [1, 0, 0]
 | 
			
		||||
				total: [1, 0, 0],
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
@@ -364,7 +364,7 @@ describe('Chart', () => {
 | 
			
		||||
			foo: {
 | 
			
		||||
				dec: [0, 0, 0],
 | 
			
		||||
				inc: [2, 0, 0],
 | 
			
		||||
				total: [2, 0, 0]
 | 
			
		||||
				total: [2, 0, 0],
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
	}));
 | 
			
		||||
@@ -383,7 +383,7 @@ describe('Chart', () => {
 | 
			
		||||
				foo: {
 | 
			
		||||
					dec: [0, 0, 0],
 | 
			
		||||
					inc: [1, 0, 0],
 | 
			
		||||
					total: [1, 0, 0]
 | 
			
		||||
					total: [1, 0, 0],
 | 
			
		||||
				},
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
@@ -391,7 +391,7 @@ describe('Chart', () => {
 | 
			
		||||
				foo: {
 | 
			
		||||
					dec: [0, 0, 0],
 | 
			
		||||
					inc: [1, 0, 0],
 | 
			
		||||
					total: [1, 0, 0]
 | 
			
		||||
					total: [1, 0, 0],
 | 
			
		||||
				},
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
@@ -399,7 +399,7 @@ describe('Chart', () => {
 | 
			
		||||
				foo: {
 | 
			
		||||
					dec: [0, 0, 0],
 | 
			
		||||
					inc: [0, 0, 0],
 | 
			
		||||
					total: [0, 0, 0]
 | 
			
		||||
					total: [0, 0, 0],
 | 
			
		||||
				},
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
@@ -407,7 +407,7 @@ describe('Chart', () => {
 | 
			
		||||
				foo: {
 | 
			
		||||
					dec: [0, 0, 0],
 | 
			
		||||
					inc: [0, 0, 0],
 | 
			
		||||
					total: [0, 0, 0]
 | 
			
		||||
					total: [0, 0, 0],
 | 
			
		||||
				},
 | 
			
		||||
			});
 | 
			
		||||
		}));
 | 
			
		||||
@@ -493,7 +493,7 @@ describe('Chart', () => {
 | 
			
		||||
				foo: {
 | 
			
		||||
					dec: [0, 0, 0],
 | 
			
		||||
					inc: [0, 0, 0],
 | 
			
		||||
					total: [1, 0, 0]
 | 
			
		||||
					total: [1, 0, 0],
 | 
			
		||||
				},
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
@@ -501,7 +501,7 @@ describe('Chart', () => {
 | 
			
		||||
				foo: {
 | 
			
		||||
					dec: [0, 0, 0],
 | 
			
		||||
					inc: [0, 0, 0],
 | 
			
		||||
					total: [1, 0, 0]
 | 
			
		||||
					total: [1, 0, 0],
 | 
			
		||||
				},
 | 
			
		||||
			});
 | 
			
		||||
		}));
 | 
			
		||||
@@ -523,7 +523,7 @@ describe('Chart', () => {
 | 
			
		||||
				foo: {
 | 
			
		||||
					dec: [0, 0, 0],
 | 
			
		||||
					inc: [0, 1, 0],
 | 
			
		||||
					total: [100, 1, 0]
 | 
			
		||||
					total: [100, 1, 0],
 | 
			
		||||
				},
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
@@ -531,7 +531,7 @@ describe('Chart', () => {
 | 
			
		||||
				foo: {
 | 
			
		||||
					dec: [0, 0, 0],
 | 
			
		||||
					inc: [1, 0, 0],
 | 
			
		||||
					total: [100, 0, 0]
 | 
			
		||||
					total: [100, 0, 0],
 | 
			
		||||
				},
 | 
			
		||||
			});
 | 
			
		||||
		}));
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import * as assert from 'assert';
 | 
			
		||||
 | 
			
		||||
import { extractMentions } from '../src/misc/extract-mentions.js';
 | 
			
		||||
import { parse } from 'mfm-js';
 | 
			
		||||
import { extractMentions } from '../src/misc/extract-mentions.js';
 | 
			
		||||
 | 
			
		||||
describe('Extract mentions', () => {
 | 
			
		||||
	it('simple', () => {
 | 
			
		||||
@@ -10,15 +10,15 @@ describe('Extract mentions', () => {
 | 
			
		||||
		assert.deepStrictEqual(mentions, [{
 | 
			
		||||
			username: 'foo',
 | 
			
		||||
			acct: '@foo',
 | 
			
		||||
			host: null
 | 
			
		||||
			host: null,
 | 
			
		||||
		}, {
 | 
			
		||||
			username: 'bar',
 | 
			
		||||
			acct: '@bar',
 | 
			
		||||
			host: null
 | 
			
		||||
			host: null,
 | 
			
		||||
		}, {
 | 
			
		||||
			username: 'baz',
 | 
			
		||||
			acct: '@baz',
 | 
			
		||||
			host: null
 | 
			
		||||
			host: null,
 | 
			
		||||
		}]);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
@@ -28,15 +28,15 @@ describe('Extract mentions', () => {
 | 
			
		||||
		assert.deepStrictEqual(mentions, [{
 | 
			
		||||
			username: 'foo',
 | 
			
		||||
			acct: '@foo',
 | 
			
		||||
			host: null
 | 
			
		||||
			host: null,
 | 
			
		||||
		}, {
 | 
			
		||||
			username: 'bar',
 | 
			
		||||
			acct: '@bar',
 | 
			
		||||
			host: null
 | 
			
		||||
			host: null,
 | 
			
		||||
		}, {
 | 
			
		||||
			username: 'baz',
 | 
			
		||||
			acct: '@baz',
 | 
			
		||||
			host: null
 | 
			
		||||
			host: null,
 | 
			
		||||
		}]);
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,8 @@ process.env.NODE_ENV = 'test';
 | 
			
		||||
 | 
			
		||||
import * as assert from 'assert';
 | 
			
		||||
import * as childProcess from 'child_process';
 | 
			
		||||
import { async, startServer, signup, post, request, simpleGet, port, shutdownServer } from './utils.js';
 | 
			
		||||
import * as openapi from '@redocly/openapi-core';
 | 
			
		||||
import { async, startServer, signup, post, request, simpleGet, port, shutdownServer } from './utils.js';
 | 
			
		||||
 | 
			
		||||
// Request Accept
 | 
			
		||||
const ONLY_AP = 'application/activity+json';
 | 
			
		||||
@@ -26,7 +26,7 @@ describe('Fetch resource', () => {
 | 
			
		||||
		p = await startServer();
 | 
			
		||||
		alice = await signup({ username: 'alice' });
 | 
			
		||||
		alicesPost = await post(alice, {
 | 
			
		||||
			text: 'test'
 | 
			
		||||
			text: 'test',
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
@@ -70,7 +70,7 @@ describe('Fetch resource', () => {
 | 
			
		||||
			const config = await openapi.loadConfig();
 | 
			
		||||
			const result = await openapi.bundle({
 | 
			
		||||
				config,
 | 
			
		||||
				ref: `http://localhost:${port}/api.json`
 | 
			
		||||
				ref: `http://localhost:${port}/api.json`,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			for (const problem of result.problems) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,15 @@
 | 
			
		||||
import * as assert from 'assert';
 | 
			
		||||
import { async } from './utils.js';
 | 
			
		||||
import { fileURLToPath } from 'node:url';
 | 
			
		||||
import { dirname } from 'node:path';
 | 
			
		||||
import { getFileInfo } from '../src/misc/get-file-info.js';
 | 
			
		||||
import { async } from './utils.js';
 | 
			
		||||
 | 
			
		||||
const _filename = fileURLToPath(import.meta.url);
 | 
			
		||||
const _dirname = dirname(_filename);
 | 
			
		||||
 | 
			
		||||
describe('Get file info', () => {
 | 
			
		||||
	it('Empty file', async (async () => {
 | 
			
		||||
		const path = `${__dirname}/resources/emptyfile`;
 | 
			
		||||
		const path = `${_dirname}/resources/emptyfile`;
 | 
			
		||||
		const info = await getFileInfo(path) as any;
 | 
			
		||||
		delete info.warnings;
 | 
			
		||||
		delete info.blurhash;
 | 
			
		||||
@@ -13,7 +18,7 @@ describe('Get file info', () => {
 | 
			
		||||
			md5: 'd41d8cd98f00b204e9800998ecf8427e',
 | 
			
		||||
			type: {
 | 
			
		||||
				mime: 'application/octet-stream',
 | 
			
		||||
				ext: null
 | 
			
		||||
				ext: null,
 | 
			
		||||
			},
 | 
			
		||||
			width: undefined,
 | 
			
		||||
			height: undefined,
 | 
			
		||||
@@ -22,7 +27,7 @@ describe('Get file info', () => {
 | 
			
		||||
	}));
 | 
			
		||||
 | 
			
		||||
	it('Generic JPEG', async (async () => {
 | 
			
		||||
		const path = `${__dirname}/resources/Lenna.jpg`;
 | 
			
		||||
		const path = `${_dirname}/resources/Lenna.jpg`;
 | 
			
		||||
		const info = await getFileInfo(path) as any;
 | 
			
		||||
		delete info.warnings;
 | 
			
		||||
		delete info.blurhash;
 | 
			
		||||
@@ -31,7 +36,7 @@ describe('Get file info', () => {
 | 
			
		||||
			md5: '091b3f259662aa31e2ffef4519951168',
 | 
			
		||||
			type: {
 | 
			
		||||
				mime: 'image/jpeg',
 | 
			
		||||
				ext: 'jpg'
 | 
			
		||||
				ext: 'jpg',
 | 
			
		||||
			},
 | 
			
		||||
			width: 512,
 | 
			
		||||
			height: 512,
 | 
			
		||||
@@ -40,7 +45,7 @@ describe('Get file info', () => {
 | 
			
		||||
	}));
 | 
			
		||||
 | 
			
		||||
	it('Generic APNG', async (async () => {
 | 
			
		||||
		const path = `${__dirname}/resources/anime.png`;
 | 
			
		||||
		const path = `${_dirname}/resources/anime.png`;
 | 
			
		||||
		const info = await getFileInfo(path) as any;
 | 
			
		||||
		delete info.warnings;
 | 
			
		||||
		delete info.blurhash;
 | 
			
		||||
@@ -49,7 +54,7 @@ describe('Get file info', () => {
 | 
			
		||||
			md5: '08189c607bea3b952704676bb3c979e0',
 | 
			
		||||
			type: {
 | 
			
		||||
				mime: 'image/apng',
 | 
			
		||||
				ext: 'apng'
 | 
			
		||||
				ext: 'apng',
 | 
			
		||||
			},
 | 
			
		||||
			width: 256,
 | 
			
		||||
			height: 256,
 | 
			
		||||
@@ -58,7 +63,7 @@ describe('Get file info', () => {
 | 
			
		||||
	}));
 | 
			
		||||
 | 
			
		||||
	it('Generic AGIF', async (async () => {
 | 
			
		||||
		const path = `${__dirname}/resources/anime.gif`;
 | 
			
		||||
		const path = `${_dirname}/resources/anime.gif`;
 | 
			
		||||
		const info = await getFileInfo(path) as any;
 | 
			
		||||
		delete info.warnings;
 | 
			
		||||
		delete info.blurhash;
 | 
			
		||||
@@ -67,7 +72,7 @@ describe('Get file info', () => {
 | 
			
		||||
			md5: '32c47a11555675d9267aee1a86571e7e',
 | 
			
		||||
			type: {
 | 
			
		||||
				mime: 'image/gif',
 | 
			
		||||
				ext: 'gif'
 | 
			
		||||
				ext: 'gif',
 | 
			
		||||
			},
 | 
			
		||||
			width: 256,
 | 
			
		||||
			height: 256,
 | 
			
		||||
@@ -76,7 +81,7 @@ describe('Get file info', () => {
 | 
			
		||||
	}));
 | 
			
		||||
 | 
			
		||||
	it('PNG with alpha', async (async () => {
 | 
			
		||||
		const path = `${__dirname}/resources/with-alpha.png`;
 | 
			
		||||
		const path = `${_dirname}/resources/with-alpha.png`;
 | 
			
		||||
		const info = await getFileInfo(path) as any;
 | 
			
		||||
		delete info.warnings;
 | 
			
		||||
		delete info.blurhash;
 | 
			
		||||
@@ -85,7 +90,7 @@ describe('Get file info', () => {
 | 
			
		||||
			md5: 'f73535c3e1e27508885b69b10cf6e991',
 | 
			
		||||
			type: {
 | 
			
		||||
				mime: 'image/png',
 | 
			
		||||
				ext: 'png'
 | 
			
		||||
				ext: 'png',
 | 
			
		||||
			},
 | 
			
		||||
			width: 256,
 | 
			
		||||
			height: 256,
 | 
			
		||||
@@ -94,7 +99,7 @@ describe('Get file info', () => {
 | 
			
		||||
	}));
 | 
			
		||||
 | 
			
		||||
	it('Generic SVG', async (async () => {
 | 
			
		||||
		const path = `${__dirname}/resources/image.svg`;
 | 
			
		||||
		const path = `${_dirname}/resources/image.svg`;
 | 
			
		||||
		const info = await getFileInfo(path) as any;
 | 
			
		||||
		delete info.warnings;
 | 
			
		||||
		delete info.blurhash;
 | 
			
		||||
@@ -103,7 +108,7 @@ describe('Get file info', () => {
 | 
			
		||||
			md5: 'b6f52b4b021e7b92cdd04509c7267965',
 | 
			
		||||
			type: {
 | 
			
		||||
				mime: 'image/svg+xml',
 | 
			
		||||
				ext: 'svg'
 | 
			
		||||
				ext: 'svg',
 | 
			
		||||
			},
 | 
			
		||||
			width: 256,
 | 
			
		||||
			height: 256,
 | 
			
		||||
@@ -113,7 +118,7 @@ describe('Get file info', () => {
 | 
			
		||||
 | 
			
		||||
	it('SVG with XML definition', async (async () => {
 | 
			
		||||
		// https://github.com/misskey-dev/misskey/issues/4413
 | 
			
		||||
		const path = `${__dirname}/resources/with-xml-def.svg`;
 | 
			
		||||
		const path = `${_dirname}/resources/with-xml-def.svg`;
 | 
			
		||||
		const info = await getFileInfo(path) as any;
 | 
			
		||||
		delete info.warnings;
 | 
			
		||||
		delete info.blurhash;
 | 
			
		||||
@@ -122,7 +127,7 @@ describe('Get file info', () => {
 | 
			
		||||
			md5: '4b7a346cde9ccbeb267e812567e33397',
 | 
			
		||||
			type: {
 | 
			
		||||
				mime: 'image/svg+xml',
 | 
			
		||||
				ext: 'svg'
 | 
			
		||||
				ext: 'svg',
 | 
			
		||||
			},
 | 
			
		||||
			width: 256,
 | 
			
		||||
			height: 256,
 | 
			
		||||
@@ -131,7 +136,7 @@ describe('Get file info', () => {
 | 
			
		||||
	}));
 | 
			
		||||
 | 
			
		||||
	it('Dimension limit', async (async () => {
 | 
			
		||||
		const path = `${__dirname}/resources/25000x25000.png`;
 | 
			
		||||
		const path = `${_dirname}/resources/25000x25000.png`;
 | 
			
		||||
		const info = await getFileInfo(path) as any;
 | 
			
		||||
		delete info.warnings;
 | 
			
		||||
		delete info.blurhash;
 | 
			
		||||
@@ -140,7 +145,7 @@ describe('Get file info', () => {
 | 
			
		||||
			md5: '268c5dde99e17cf8fe09f1ab3f97df56',
 | 
			
		||||
			type: {
 | 
			
		||||
				mime: 'application/octet-stream',	// do not treat as image
 | 
			
		||||
				ext: null
 | 
			
		||||
				ext: null,
 | 
			
		||||
			},
 | 
			
		||||
			width: 25000,
 | 
			
		||||
			height: 25000,
 | 
			
		||||
@@ -149,7 +154,7 @@ describe('Get file info', () => {
 | 
			
		||||
	}));
 | 
			
		||||
 | 
			
		||||
	it('Rotate JPEG', async (async () => {
 | 
			
		||||
		const path = `${__dirname}/resources/rotate.jpg`;
 | 
			
		||||
		const path = `${_dirname}/resources/rotate.jpg`;
 | 
			
		||||
		const info = await getFileInfo(path) as any;
 | 
			
		||||
		delete info.warnings;
 | 
			
		||||
		delete info.blurhash;
 | 
			
		||||
@@ -158,7 +163,7 @@ describe('Get file info', () => {
 | 
			
		||||
			md5: '68d5b2d8d1d1acbbce99203e3ec3857e',
 | 
			
		||||
			type: {
 | 
			
		||||
				mime: 'image/jpeg',
 | 
			
		||||
				ext: 'jpg'
 | 
			
		||||
				ext: 'jpg',
 | 
			
		||||
			},
 | 
			
		||||
			width: 512,
 | 
			
		||||
			height: 256,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,37 +1,34 @@
 | 
			
		||||
import path from 'path'
 | 
			
		||||
import typescript from 'typescript'
 | 
			
		||||
import { createMatchPath } from 'tsconfig-paths'
 | 
			
		||||
import { resolve as BaseResolve, getFormat, transformSource } from 'ts-node/esm'
 | 
			
		||||
/**
 | 
			
		||||
 * ts-node/esmローダーに投げる前にpath mappingを解決する
 | 
			
		||||
 * 参考
 | 
			
		||||
 * - https://github.com/TypeStrong/ts-node/discussions/1450#discussioncomment-1806115
 | 
			
		||||
 * - https://nodejs.org/api/esm.html#loaders
 | 
			
		||||
 * ※ https://github.com/TypeStrong/ts-node/pull/1585 が取り込まれたらこのカスタムローダーは必要なくなる
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
const { readConfigFile, parseJsonConfigFileContent, sys } = typescript
 | 
			
		||||
import { resolve as resolveTs, load } from 'ts-node/esm';
 | 
			
		||||
import { loadConfig, createMatchPath } from 'tsconfig-paths';
 | 
			
		||||
import { pathToFileURL } from 'url';
 | 
			
		||||
 | 
			
		||||
const __dirname = path.dirname(new URL(import.meta.url).pathname)
 | 
			
		||||
const tsconfig = loadConfig();
 | 
			
		||||
const matchPath = createMatchPath(tsconfig.absoluteBaseUrl, tsconfig.paths);
 | 
			
		||||
 | 
			
		||||
const configFile = readConfigFile('./test/tsconfig.json', sys.readFile)
 | 
			
		||||
if (typeof configFile.error !== 'undefined') {
 | 
			
		||||
  throw new Error(`Failed to load tsconfig: ${configFile.error}`)
 | 
			
		||||
export function resolve(specifier, ctx, defaultResolve) {
 | 
			
		||||
	let resolvedSpecifier;
 | 
			
		||||
	if (specifier.endsWith('.js')) {
 | 
			
		||||
		// maybe transpiled
 | 
			
		||||
		const specifierWithoutExtension = specifier.substring(0, specifier.length - '.js'.length);
 | 
			
		||||
		const matchedSpecifier = matchPath(specifierWithoutExtension);
 | 
			
		||||
		if (matchedSpecifier) {
 | 
			
		||||
			resolvedSpecifier = pathToFileURL(`${matchedSpecifier}.js`).href;
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		const matchedSpecifier = matchPath(specifier);
 | 
			
		||||
		if (matchedSpecifier) {
 | 
			
		||||
			resolvedSpecifier = pathToFileURL(matchedSpecifier).href;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return resolveTs(resolvedSpecifier ?? specifier, ctx, defaultResolve);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const { options } = parseJsonConfigFileContent(
 | 
			
		||||
  configFile.config,
 | 
			
		||||
  {
 | 
			
		||||
    fileExists: sys.fileExists,
 | 
			
		||||
    readFile: sys.readFile,
 | 
			
		||||
    readDirectory: sys.readDirectory,
 | 
			
		||||
    useCaseSensitiveFileNames: true,
 | 
			
		||||
  },
 | 
			
		||||
  __dirname
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
export { getFormat, transformSource }  // こいつらはそのまま使ってほしいので re-export する
 | 
			
		||||
 | 
			
		||||
const matchPath = createMatchPath(options.baseUrl, options.paths)
 | 
			
		||||
 | 
			
		||||
export async function resolve(specifier, context, defaultResolve) {
 | 
			
		||||
  const matchedSpecifier = matchPath(specifier.replace('.js', '.ts'))
 | 
			
		||||
  return BaseResolve(  // ts-node/esm の resolve に tsconfig-paths で解決したパスを渡す
 | 
			
		||||
    matchedSpecifier ? `${matchedSpecifier}.ts` : specifier,
 | 
			
		||||
    context,
 | 
			
		||||
    defaultResolve
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
export { load };
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ export class MockResolver extends Resolver {
 | 
			
		||||
	public async _register(uri: string, content: string | Record<string, any>, type = 'application/activity+json') {
 | 
			
		||||
		this._rs.set(uri, {
 | 
			
		||||
			type,
 | 
			
		||||
			content: typeof content === 'string' ? content : JSON.stringify(content)
 | 
			
		||||
			content: typeof content === 'string' ? content : JSON.stringify(content),
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -22,9 +22,9 @@ export class MockResolver extends Resolver {
 | 
			
		||||
 | 
			
		||||
		if (!r) {
 | 
			
		||||
			throw {
 | 
			
		||||
				name: `StatusError`,
 | 
			
		||||
				name: 'StatusError',
 | 
			
		||||
				statusCode: 404,
 | 
			
		||||
				message: `Not registed for mock`
 | 
			
		||||
				message: 'Not registed for mock',
 | 
			
		||||
			};
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,7 @@ describe('Mute', () => {
 | 
			
		||||
 | 
			
		||||
	it('ミュート作成', async(async () => {
 | 
			
		||||
		const res = await request('/mute/create', {
 | 
			
		||||
			userId: carol.id
 | 
			
		||||
			userId: carol.id,
 | 
			
		||||
		}, alice);
 | 
			
		||||
 | 
			
		||||
		assert.strictEqual(res.status, 204);
 | 
			
		||||
@@ -117,7 +117,7 @@ describe('Mute', () => {
 | 
			
		||||
			const aliceNote = await post(alice);
 | 
			
		||||
			const carolNote = await post(carol);
 | 
			
		||||
			const bobNote = await post(bob, {
 | 
			
		||||
				renoteId: carolNote.id
 | 
			
		||||
				renoteId: carolNote.id,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			const res = await request('/notes/local-timeline', {}, alice);
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,8 @@ process.env.NODE_ENV = 'test';
 | 
			
		||||
 | 
			
		||||
import * as assert from 'assert';
 | 
			
		||||
import * as childProcess from 'child_process';
 | 
			
		||||
import { async, signup, request, post, uploadFile, startServer, shutdownServer, initTestDb } from './utils.js';
 | 
			
		||||
import { Note } from '../src/models/entities/note.js';
 | 
			
		||||
import { async, signup, request, post, uploadFile, startServer, shutdownServer, initTestDb } from './utils.js';
 | 
			
		||||
 | 
			
		||||
describe('Note', () => {
 | 
			
		||||
	let p: childProcess.ChildProcess;
 | 
			
		||||
@@ -26,7 +26,7 @@ describe('Note', () => {
 | 
			
		||||
 | 
			
		||||
	it('投稿できる', async(async () => {
 | 
			
		||||
		const post = {
 | 
			
		||||
			text: 'test'
 | 
			
		||||
			text: 'test',
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		const res = await request('/notes/create', post, alice);
 | 
			
		||||
@@ -40,7 +40,7 @@ describe('Note', () => {
 | 
			
		||||
		const file = await uploadFile(alice);
 | 
			
		||||
 | 
			
		||||
		const res = await request('/notes/create', {
 | 
			
		||||
			fileIds: [file.id]
 | 
			
		||||
			fileIds: [file.id],
 | 
			
		||||
		}, alice);
 | 
			
		||||
 | 
			
		||||
		assert.strictEqual(res.status, 200);
 | 
			
		||||
@@ -53,7 +53,7 @@ describe('Note', () => {
 | 
			
		||||
 | 
			
		||||
		const res = await request('/notes/create', {
 | 
			
		||||
			text: 'test',
 | 
			
		||||
			fileIds: [file.id]
 | 
			
		||||
			fileIds: [file.id],
 | 
			
		||||
		}, alice);
 | 
			
		||||
 | 
			
		||||
		assert.strictEqual(res.status, 200);
 | 
			
		||||
@@ -64,7 +64,7 @@ describe('Note', () => {
 | 
			
		||||
	it('存在しないファイルは無視', async(async () => {
 | 
			
		||||
		const res = await request('/notes/create', {
 | 
			
		||||
			text: 'test',
 | 
			
		||||
			fileIds: ['000000000000000000000000']
 | 
			
		||||
			fileIds: ['000000000000000000000000'],
 | 
			
		||||
		}, alice);
 | 
			
		||||
 | 
			
		||||
		assert.strictEqual(res.status, 200);
 | 
			
		||||
@@ -74,19 +74,19 @@ describe('Note', () => {
 | 
			
		||||
 | 
			
		||||
	it('不正なファイルIDで怒られる', async(async () => {
 | 
			
		||||
		const res = await request('/notes/create', {
 | 
			
		||||
			fileIds: ['kyoppie']
 | 
			
		||||
			fileIds: ['kyoppie'],
 | 
			
		||||
		}, alice);
 | 
			
		||||
		assert.strictEqual(res.status, 400);
 | 
			
		||||
	}));
 | 
			
		||||
 | 
			
		||||
	it('返信できる', async(async () => {
 | 
			
		||||
		const bobPost = await post(bob, {
 | 
			
		||||
			text: 'foo'
 | 
			
		||||
			text: 'foo',
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		const alicePost = {
 | 
			
		||||
			text: 'bar',
 | 
			
		||||
			replyId: bobPost.id
 | 
			
		||||
			replyId: bobPost.id,
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		const res = await request('/notes/create', alicePost, alice);
 | 
			
		||||
@@ -100,11 +100,11 @@ describe('Note', () => {
 | 
			
		||||
 | 
			
		||||
	it('renoteできる', async(async () => {
 | 
			
		||||
		const bobPost = await post(bob, {
 | 
			
		||||
			text: 'test'
 | 
			
		||||
			text: 'test',
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		const alicePost = {
 | 
			
		||||
			renoteId: bobPost.id
 | 
			
		||||
			renoteId: bobPost.id,
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		const res = await request('/notes/create', alicePost, alice);
 | 
			
		||||
@@ -117,12 +117,12 @@ describe('Note', () => {
 | 
			
		||||
 | 
			
		||||
	it('引用renoteできる', async(async () => {
 | 
			
		||||
		const bobPost = await post(bob, {
 | 
			
		||||
			text: 'test'
 | 
			
		||||
			text: 'test',
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		const alicePost = {
 | 
			
		||||
			text: 'test',
 | 
			
		||||
			renoteId: bobPost.id
 | 
			
		||||
			renoteId: bobPost.id,
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		const res = await request('/notes/create', alicePost, alice);
 | 
			
		||||
@@ -136,7 +136,7 @@ describe('Note', () => {
 | 
			
		||||
 | 
			
		||||
	it('文字数ぎりぎりで怒られない', async(async () => {
 | 
			
		||||
		const post = {
 | 
			
		||||
			text: '!'.repeat(500)
 | 
			
		||||
			text: '!'.repeat(500),
 | 
			
		||||
		};
 | 
			
		||||
		const res = await request('/notes/create', post, alice);
 | 
			
		||||
		assert.strictEqual(res.status, 200);
 | 
			
		||||
@@ -144,7 +144,7 @@ describe('Note', () => {
 | 
			
		||||
 | 
			
		||||
	it('文字数オーバーで怒られる', async(async () => {
 | 
			
		||||
		const post = {
 | 
			
		||||
			text: '!'.repeat(501)
 | 
			
		||||
			text: '!'.repeat(501),
 | 
			
		||||
		};
 | 
			
		||||
		const res = await request('/notes/create', post, alice);
 | 
			
		||||
		assert.strictEqual(res.status, 400);
 | 
			
		||||
@@ -153,7 +153,7 @@ describe('Note', () => {
 | 
			
		||||
	it('存在しないリプライ先で怒られる', async(async () => {
 | 
			
		||||
		const post = {
 | 
			
		||||
			text: 'test',
 | 
			
		||||
			replyId: '000000000000000000000000'
 | 
			
		||||
			replyId: '000000000000000000000000',
 | 
			
		||||
		};
 | 
			
		||||
		const res = await request('/notes/create', post, alice);
 | 
			
		||||
		assert.strictEqual(res.status, 400);
 | 
			
		||||
@@ -161,7 +161,7 @@ describe('Note', () => {
 | 
			
		||||
 | 
			
		||||
	it('存在しないrenote対象で怒られる', async(async () => {
 | 
			
		||||
		const post = {
 | 
			
		||||
			renoteId: '000000000000000000000000'
 | 
			
		||||
			renoteId: '000000000000000000000000',
 | 
			
		||||
		};
 | 
			
		||||
		const res = await request('/notes/create', post, alice);
 | 
			
		||||
		assert.strictEqual(res.status, 400);
 | 
			
		||||
@@ -170,7 +170,7 @@ describe('Note', () => {
 | 
			
		||||
	it('不正なリプライ先IDで怒られる', async(async () => {
 | 
			
		||||
		const post = {
 | 
			
		||||
			text: 'test',
 | 
			
		||||
			replyId: 'foo'
 | 
			
		||||
			replyId: 'foo',
 | 
			
		||||
		};
 | 
			
		||||
		const res = await request('/notes/create', post, alice);
 | 
			
		||||
		assert.strictEqual(res.status, 400);
 | 
			
		||||
@@ -178,7 +178,7 @@ describe('Note', () => {
 | 
			
		||||
 | 
			
		||||
	it('不正なrenote対象IDで怒られる', async(async () => {
 | 
			
		||||
		const post = {
 | 
			
		||||
			renoteId: 'foo'
 | 
			
		||||
			renoteId: 'foo',
 | 
			
		||||
		};
 | 
			
		||||
		const res = await request('/notes/create', post, alice);
 | 
			
		||||
		assert.strictEqual(res.status, 400);
 | 
			
		||||
@@ -186,7 +186,7 @@ describe('Note', () => {
 | 
			
		||||
 | 
			
		||||
	it('存在しないユーザーにメンションできる', async(async () => {
 | 
			
		||||
		const post = {
 | 
			
		||||
			text: '@ghost yo'
 | 
			
		||||
			text: '@ghost yo',
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		const res = await request('/notes/create', post, alice);
 | 
			
		||||
@@ -198,7 +198,7 @@ describe('Note', () => {
 | 
			
		||||
 | 
			
		||||
	it('同じユーザーに複数メンションしても内部的にまとめられる', async(async () => {
 | 
			
		||||
		const post = {
 | 
			
		||||
			text: '@bob @bob @bob yo'
 | 
			
		||||
			text: '@bob @bob @bob yo',
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		const res = await request('/notes/create', post, alice);
 | 
			
		||||
@@ -216,8 +216,8 @@ describe('Note', () => {
 | 
			
		||||
			const res = await request('/notes/create', {
 | 
			
		||||
				text: 'test',
 | 
			
		||||
				poll: {
 | 
			
		||||
					choices: ['foo', 'bar']
 | 
			
		||||
				}
 | 
			
		||||
					choices: ['foo', 'bar'],
 | 
			
		||||
				},
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(res.status, 200);
 | 
			
		||||
@@ -227,7 +227,7 @@ describe('Note', () => {
 | 
			
		||||
 | 
			
		||||
		it('投票の選択肢が無くて怒られる', async(async () => {
 | 
			
		||||
			const res = await request('/notes/create', {
 | 
			
		||||
				poll: {}
 | 
			
		||||
				poll: {},
 | 
			
		||||
			}, alice);
 | 
			
		||||
			assert.strictEqual(res.status, 400);
 | 
			
		||||
		}));
 | 
			
		||||
@@ -235,8 +235,8 @@ describe('Note', () => {
 | 
			
		||||
		it('投票の選択肢が無くて怒られる (空の配列)', async(async () => {
 | 
			
		||||
			const res = await request('/notes/create', {
 | 
			
		||||
				poll: {
 | 
			
		||||
					choices: []
 | 
			
		||||
				}
 | 
			
		||||
					choices: [],
 | 
			
		||||
				},
 | 
			
		||||
			}, alice);
 | 
			
		||||
			assert.strictEqual(res.status, 400);
 | 
			
		||||
		}));
 | 
			
		||||
@@ -244,8 +244,8 @@ describe('Note', () => {
 | 
			
		||||
		it('投票の選択肢が1つで怒られる', async(async () => {
 | 
			
		||||
			const res = await request('/notes/create', {
 | 
			
		||||
				poll: {
 | 
			
		||||
					choices: ['Strawberry Pasta']
 | 
			
		||||
				}
 | 
			
		||||
					choices: ['Strawberry Pasta'],
 | 
			
		||||
				},
 | 
			
		||||
			}, alice);
 | 
			
		||||
			assert.strictEqual(res.status, 400);
 | 
			
		||||
		}));
 | 
			
		||||
@@ -254,13 +254,13 @@ describe('Note', () => {
 | 
			
		||||
			const { body } = await request('/notes/create', {
 | 
			
		||||
				text: 'test',
 | 
			
		||||
				poll: {
 | 
			
		||||
					choices: ['sakura', 'izumi', 'ako']
 | 
			
		||||
				}
 | 
			
		||||
					choices: ['sakura', 'izumi', 'ako'],
 | 
			
		||||
				},
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const res = await request('/notes/polls/vote', {
 | 
			
		||||
				noteId: body.createdNote.id,
 | 
			
		||||
				choice: 1
 | 
			
		||||
				choice: 1,
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(res.status, 204);
 | 
			
		||||
@@ -270,18 +270,18 @@ describe('Note', () => {
 | 
			
		||||
			const { body } = await request('/notes/create', {
 | 
			
		||||
				text: 'test',
 | 
			
		||||
				poll: {
 | 
			
		||||
					choices: ['sakura', 'izumi', 'ako']
 | 
			
		||||
				}
 | 
			
		||||
					choices: ['sakura', 'izumi', 'ako'],
 | 
			
		||||
				},
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			await request('/notes/polls/vote', {
 | 
			
		||||
				noteId: body.createdNote.id,
 | 
			
		||||
				choice: 0
 | 
			
		||||
				choice: 0,
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const res = await request('/notes/polls/vote', {
 | 
			
		||||
				noteId: body.createdNote.id,
 | 
			
		||||
				choice: 2
 | 
			
		||||
				choice: 2,
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(res.status, 400);
 | 
			
		||||
@@ -292,23 +292,23 @@ describe('Note', () => {
 | 
			
		||||
				text: 'test',
 | 
			
		||||
				poll: {
 | 
			
		||||
					choices: ['sakura', 'izumi', 'ako'],
 | 
			
		||||
					multiple: true
 | 
			
		||||
				}
 | 
			
		||||
					multiple: true,
 | 
			
		||||
				},
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			await request('/notes/polls/vote', {
 | 
			
		||||
				noteId: body.createdNote.id,
 | 
			
		||||
				choice: 0
 | 
			
		||||
				choice: 0,
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			await request('/notes/polls/vote', {
 | 
			
		||||
				noteId: body.createdNote.id,
 | 
			
		||||
				choice: 1
 | 
			
		||||
				choice: 1,
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const res = await request('/notes/polls/vote', {
 | 
			
		||||
				noteId: body.createdNote.id,
 | 
			
		||||
				choice: 2
 | 
			
		||||
				choice: 2,
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(res.status, 204);
 | 
			
		||||
@@ -319,15 +319,15 @@ describe('Note', () => {
 | 
			
		||||
				text: 'test',
 | 
			
		||||
				poll: {
 | 
			
		||||
					choices: ['sakura', 'izumi', 'ako'],
 | 
			
		||||
					expiredAfter: 1
 | 
			
		||||
				}
 | 
			
		||||
					expiredAfter: 1,
 | 
			
		||||
				},
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			await new Promise(x => setTimeout(x, 2));
 | 
			
		||||
 | 
			
		||||
			const res = await request('/notes/polls/vote', {
 | 
			
		||||
				noteId: body.createdNote.id,
 | 
			
		||||
				choice: 1
 | 
			
		||||
				choice: 1,
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(res.status, 400);
 | 
			
		||||
@@ -341,11 +341,11 @@ describe('Note', () => {
 | 
			
		||||
			}, alice);
 | 
			
		||||
			const replyOneRes = await request('/notes/create', {
 | 
			
		||||
				text: 'reply one',
 | 
			
		||||
				replyId: mainNoteRes.body.createdNote.id
 | 
			
		||||
				replyId: mainNoteRes.body.createdNote.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
			const replyTwoRes = await request('/notes/create', {
 | 
			
		||||
				text: 'reply two',
 | 
			
		||||
				replyId: mainNoteRes.body.createdNote.id
 | 
			
		||||
				replyId: mainNoteRes.body.createdNote.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const deleteOneRes = await request('/notes/delete', {
 | 
			
		||||
@@ -353,7 +353,7 @@ describe('Note', () => {
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(deleteOneRes.status, 204);
 | 
			
		||||
			let mainNote = await Notes.findOne({id: mainNoteRes.body.createdNote.id});
 | 
			
		||||
			let mainNote = await Notes.findOne({ id: mainNoteRes.body.createdNote.id });
 | 
			
		||||
			assert.strictEqual(mainNote.repliesCount, 1);
 | 
			
		||||
 | 
			
		||||
			const deleteTwoRes = await request('/notes/delete', {
 | 
			
		||||
@@ -361,7 +361,7 @@ describe('Note', () => {
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			assert.strictEqual(deleteTwoRes.status, 204);
 | 
			
		||||
			mainNote = await Notes.findOne({id: mainNoteRes.body.createdNote.id});
 | 
			
		||||
			mainNote = await Notes.findOne({ id: mainNoteRes.body.createdNote.id });
 | 
			
		||||
			assert.strictEqual(mainNote.repliesCount, 0);
 | 
			
		||||
		}));
 | 
			
		||||
	});
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ describe('url', () => {
 | 
			
		||||
		const s = query({
 | 
			
		||||
			foo: 'ふぅ',
 | 
			
		||||
			bar: 'b a r',
 | 
			
		||||
			baz: undefined
 | 
			
		||||
			baz: undefined,
 | 
			
		||||
		});
 | 
			
		||||
		assert.deepStrictEqual(s, 'foo=%E3%81%B5%E3%81%85&bar=b%20a%20r');
 | 
			
		||||
	});
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,8 @@ process.env.NODE_ENV = 'test';
 | 
			
		||||
 | 
			
		||||
import * as assert from 'assert';
 | 
			
		||||
import * as childProcess from 'child_process';
 | 
			
		||||
import { connectStream, signup, request, post, startServer, shutdownServer, initTestDb } from './utils.js';
 | 
			
		||||
import { Following } from '../src/models/entities/following.js';
 | 
			
		||||
import { connectStream, signup, request, post, startServer, shutdownServer, initTestDb } from './utils.js';
 | 
			
		||||
 | 
			
		||||
describe('Streaming', () => {
 | 
			
		||||
	let p: childProcess.ChildProcess;
 | 
			
		||||
@@ -30,7 +30,7 @@ describe('Streaming', () => {
 | 
			
		||||
			followerSharedInbox: null,
 | 
			
		||||
			followeeHost: followee.host,
 | 
			
		||||
			followeeInbox: null,
 | 
			
		||||
			followeeSharedInbox: null
 | 
			
		||||
			followeeSharedInbox: null,
 | 
			
		||||
		});
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
@@ -47,7 +47,7 @@ describe('Streaming', () => {
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		post(alice, {
 | 
			
		||||
			text: 'foo @bob bar'
 | 
			
		||||
			text: 'foo @bob bar',
 | 
			
		||||
		});
 | 
			
		||||
	}));
 | 
			
		||||
 | 
			
		||||
@@ -55,7 +55,7 @@ describe('Streaming', () => {
 | 
			
		||||
		const alice = await signup({ username: 'alice' });
 | 
			
		||||
		const bob = await signup({ username: 'bob' });
 | 
			
		||||
		const bobNote = await post(bob, {
 | 
			
		||||
			text: 'foo'
 | 
			
		||||
			text: 'foo',
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		const ws = await connectStream(bob, 'main', ({ type, body }) => {
 | 
			
		||||
@@ -67,14 +67,14 @@ describe('Streaming', () => {
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		post(alice, {
 | 
			
		||||
			renoteId: bobNote.id
 | 
			
		||||
			renoteId: bobNote.id,
 | 
			
		||||
		});
 | 
			
		||||
	}));
 | 
			
		||||
 | 
			
		||||
	describe('Home Timeline', () => {
 | 
			
		||||
		it('自分の投稿が流れる', () => new Promise(async done => {
 | 
			
		||||
			const post = {
 | 
			
		||||
				text: 'foo'
 | 
			
		||||
				text: 'foo',
 | 
			
		||||
			};
 | 
			
		||||
 | 
			
		||||
			const me = await signup();
 | 
			
		||||
@@ -96,7 +96,7 @@ describe('Streaming', () => {
 | 
			
		||||
 | 
			
		||||
			// Alice が Bob をフォロー
 | 
			
		||||
			await request('/following/create', {
 | 
			
		||||
				userId: bob.id
 | 
			
		||||
				userId: bob.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const ws = await connectStream(alice, 'homeTimeline', ({ type, body }) => {
 | 
			
		||||
@@ -108,7 +108,7 @@ describe('Streaming', () => {
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			post(bob, {
 | 
			
		||||
				text: 'foo'
 | 
			
		||||
				text: 'foo',
 | 
			
		||||
			});
 | 
			
		||||
		}));
 | 
			
		||||
 | 
			
		||||
@@ -125,7 +125,7 @@ describe('Streaming', () => {
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			post(bob, {
 | 
			
		||||
				text: 'foo'
 | 
			
		||||
				text: 'foo',
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			setTimeout(() => {
 | 
			
		||||
@@ -141,7 +141,7 @@ describe('Streaming', () => {
 | 
			
		||||
 | 
			
		||||
			// Alice が Bob をフォロー
 | 
			
		||||
			await request('/following/create', {
 | 
			
		||||
				userId: bob.id
 | 
			
		||||
				userId: bob.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const ws = await connectStream(alice, 'homeTimeline', ({ type, body }) => {
 | 
			
		||||
@@ -157,7 +157,7 @@ describe('Streaming', () => {
 | 
			
		||||
			post(bob, {
 | 
			
		||||
				text: 'foo',
 | 
			
		||||
				visibility: 'specified',
 | 
			
		||||
				visibleUserIds: [alice.id]
 | 
			
		||||
				visibleUserIds: [alice.id],
 | 
			
		||||
			});
 | 
			
		||||
		}));
 | 
			
		||||
 | 
			
		||||
@@ -168,7 +168,7 @@ describe('Streaming', () => {
 | 
			
		||||
 | 
			
		||||
			// Alice が Bob をフォロー
 | 
			
		||||
			await request('/following/create', {
 | 
			
		||||
				userId: bob.id
 | 
			
		||||
				userId: bob.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			let fired = false;
 | 
			
		||||
@@ -183,7 +183,7 @@ describe('Streaming', () => {
 | 
			
		||||
			post(bob, {
 | 
			
		||||
				text: 'foo',
 | 
			
		||||
				visibility: 'specified',
 | 
			
		||||
				visibleUserIds: [carol.id]
 | 
			
		||||
				visibleUserIds: [carol.id],
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			setTimeout(() => {
 | 
			
		||||
@@ -207,7 +207,7 @@ describe('Streaming', () => {
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			post(me, {
 | 
			
		||||
				text: 'foo'
 | 
			
		||||
				text: 'foo',
 | 
			
		||||
			});
 | 
			
		||||
		}));
 | 
			
		||||
 | 
			
		||||
@@ -224,7 +224,7 @@ describe('Streaming', () => {
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			post(bob, {
 | 
			
		||||
				text: 'foo'
 | 
			
		||||
				text: 'foo',
 | 
			
		||||
			});
 | 
			
		||||
		}));
 | 
			
		||||
 | 
			
		||||
@@ -241,7 +241,7 @@ describe('Streaming', () => {
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			post(bob, {
 | 
			
		||||
				text: 'foo'
 | 
			
		||||
				text: 'foo',
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			setTimeout(() => {
 | 
			
		||||
@@ -257,7 +257,7 @@ describe('Streaming', () => {
 | 
			
		||||
 | 
			
		||||
			// Alice が Bob をフォロー
 | 
			
		||||
			await request('/following/create', {
 | 
			
		||||
				userId: bob.id
 | 
			
		||||
				userId: bob.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			let fired = false;
 | 
			
		||||
@@ -269,7 +269,7 @@ describe('Streaming', () => {
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			post(bob, {
 | 
			
		||||
				text: 'foo'
 | 
			
		||||
				text: 'foo',
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			setTimeout(() => {
 | 
			
		||||
@@ -294,7 +294,7 @@ describe('Streaming', () => {
 | 
			
		||||
			// ホーム指定
 | 
			
		||||
			post(bob, {
 | 
			
		||||
				text: 'foo',
 | 
			
		||||
				visibility: 'home'
 | 
			
		||||
				visibility: 'home',
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			setTimeout(() => {
 | 
			
		||||
@@ -310,7 +310,7 @@ describe('Streaming', () => {
 | 
			
		||||
 | 
			
		||||
			// Alice が Bob をフォロー
 | 
			
		||||
			await request('/following/create', {
 | 
			
		||||
				userId: bob.id
 | 
			
		||||
				userId: bob.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			let fired = false;
 | 
			
		||||
@@ -325,7 +325,7 @@ describe('Streaming', () => {
 | 
			
		||||
			post(bob, {
 | 
			
		||||
				text: 'foo',
 | 
			
		||||
				visibility: 'specified',
 | 
			
		||||
				visibleUserIds: [alice.id]
 | 
			
		||||
				visibleUserIds: [alice.id],
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			setTimeout(() => {
 | 
			
		||||
@@ -350,7 +350,7 @@ describe('Streaming', () => {
 | 
			
		||||
			// フォロワー宛て投稿
 | 
			
		||||
			post(bob, {
 | 
			
		||||
				text: 'foo',
 | 
			
		||||
				visibility: 'followers'
 | 
			
		||||
				visibility: 'followers',
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			setTimeout(() => {
 | 
			
		||||
@@ -374,7 +374,7 @@ describe('Streaming', () => {
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			post(me, {
 | 
			
		||||
				text: 'foo'
 | 
			
		||||
				text: 'foo',
 | 
			
		||||
			});
 | 
			
		||||
		}));
 | 
			
		||||
 | 
			
		||||
@@ -391,7 +391,7 @@ describe('Streaming', () => {
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			post(bob, {
 | 
			
		||||
				text: 'foo'
 | 
			
		||||
				text: 'foo',
 | 
			
		||||
			});
 | 
			
		||||
		}));
 | 
			
		||||
 | 
			
		||||
@@ -411,7 +411,7 @@ describe('Streaming', () => {
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			post(bob, {
 | 
			
		||||
				text: 'foo'
 | 
			
		||||
				text: 'foo',
 | 
			
		||||
			});
 | 
			
		||||
		}));
 | 
			
		||||
 | 
			
		||||
@@ -428,7 +428,7 @@ describe('Streaming', () => {
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			post(bob, {
 | 
			
		||||
				text: 'foo'
 | 
			
		||||
				text: 'foo',
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			setTimeout(() => {
 | 
			
		||||
@@ -444,7 +444,7 @@ describe('Streaming', () => {
 | 
			
		||||
 | 
			
		||||
			// Alice が Bob をフォロー
 | 
			
		||||
			await request('/following/create', {
 | 
			
		||||
				userId: bob.id
 | 
			
		||||
				userId: bob.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => {
 | 
			
		||||
@@ -460,7 +460,7 @@ describe('Streaming', () => {
 | 
			
		||||
			post(bob, {
 | 
			
		||||
				text: 'foo',
 | 
			
		||||
				visibility: 'specified',
 | 
			
		||||
				visibleUserIds: [alice.id]
 | 
			
		||||
				visibleUserIds: [alice.id],
 | 
			
		||||
			});
 | 
			
		||||
		}));
 | 
			
		||||
 | 
			
		||||
@@ -470,7 +470,7 @@ describe('Streaming', () => {
 | 
			
		||||
 | 
			
		||||
			// Alice が Bob をフォロー
 | 
			
		||||
			await request('/following/create', {
 | 
			
		||||
				userId: bob.id
 | 
			
		||||
				userId: bob.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const ws = await connectStream(alice, 'hybridTimeline', ({ type, body }) => {
 | 
			
		||||
@@ -485,7 +485,7 @@ describe('Streaming', () => {
 | 
			
		||||
			// ホーム投稿
 | 
			
		||||
			post(bob, {
 | 
			
		||||
				text: 'foo',
 | 
			
		||||
				visibility: 'home'
 | 
			
		||||
				visibility: 'home',
 | 
			
		||||
			});
 | 
			
		||||
		}));
 | 
			
		||||
 | 
			
		||||
@@ -504,7 +504,7 @@ describe('Streaming', () => {
 | 
			
		||||
			// ホーム投稿
 | 
			
		||||
			post(bob, {
 | 
			
		||||
				text: 'foo',
 | 
			
		||||
				visibility: 'home'
 | 
			
		||||
				visibility: 'home',
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			setTimeout(() => {
 | 
			
		||||
@@ -529,7 +529,7 @@ describe('Streaming', () => {
 | 
			
		||||
			// フォロワー宛て投稿
 | 
			
		||||
			post(bob, {
 | 
			
		||||
				text: 'foo',
 | 
			
		||||
				visibility: 'followers'
 | 
			
		||||
				visibility: 'followers',
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			setTimeout(() => {
 | 
			
		||||
@@ -554,7 +554,7 @@ describe('Streaming', () => {
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			post(bob, {
 | 
			
		||||
				text: 'foo'
 | 
			
		||||
				text: 'foo',
 | 
			
		||||
			});
 | 
			
		||||
		}));
 | 
			
		||||
 | 
			
		||||
@@ -571,7 +571,7 @@ describe('Streaming', () => {
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			post(bob, {
 | 
			
		||||
				text: 'foo'
 | 
			
		||||
				text: 'foo',
 | 
			
		||||
			});
 | 
			
		||||
		}));
 | 
			
		||||
 | 
			
		||||
@@ -590,7 +590,7 @@ describe('Streaming', () => {
 | 
			
		||||
			// ホーム投稿
 | 
			
		||||
			post(bob, {
 | 
			
		||||
				text: 'foo',
 | 
			
		||||
				visibility: 'home'
 | 
			
		||||
				visibility: 'home',
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			setTimeout(() => {
 | 
			
		||||
@@ -608,13 +608,13 @@ describe('Streaming', () => {
 | 
			
		||||
 | 
			
		||||
			// リスト作成
 | 
			
		||||
			const list = await request('/users/lists/create', {
 | 
			
		||||
				name: 'my list'
 | 
			
		||||
				name: 'my list',
 | 
			
		||||
			}, alice).then(x => x.body);
 | 
			
		||||
 | 
			
		||||
			// Alice が Bob をリスイン
 | 
			
		||||
			await request('/users/lists/push', {
 | 
			
		||||
				listId: list.id,
 | 
			
		||||
				userId: bob.id
 | 
			
		||||
				userId: bob.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const ws = await connectStream(alice, 'userList', ({ type, body }) => {
 | 
			
		||||
@@ -624,11 +624,11 @@ describe('Streaming', () => {
 | 
			
		||||
					done();
 | 
			
		||||
				}
 | 
			
		||||
			}, {
 | 
			
		||||
				listId: list.id
 | 
			
		||||
				listId: list.id,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			post(bob, {
 | 
			
		||||
				text: 'foo'
 | 
			
		||||
				text: 'foo',
 | 
			
		||||
			});
 | 
			
		||||
		}));
 | 
			
		||||
 | 
			
		||||
@@ -638,7 +638,7 @@ describe('Streaming', () => {
 | 
			
		||||
 | 
			
		||||
			// リスト作成
 | 
			
		||||
			const list = await request('/users/lists/create', {
 | 
			
		||||
				name: 'my list'
 | 
			
		||||
				name: 'my list',
 | 
			
		||||
			}, alice).then(x => x.body);
 | 
			
		||||
 | 
			
		||||
			let fired = false;
 | 
			
		||||
@@ -648,11 +648,11 @@ describe('Streaming', () => {
 | 
			
		||||
					fired = true;
 | 
			
		||||
				}
 | 
			
		||||
			}, {
 | 
			
		||||
				listId: list.id
 | 
			
		||||
				listId: list.id,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			post(bob, {
 | 
			
		||||
				text: 'foo'
 | 
			
		||||
				text: 'foo',
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			setTimeout(() => {
 | 
			
		||||
@@ -669,13 +669,13 @@ describe('Streaming', () => {
 | 
			
		||||
 | 
			
		||||
			// リスト作成
 | 
			
		||||
			const list = await request('/users/lists/create', {
 | 
			
		||||
				name: 'my list'
 | 
			
		||||
				name: 'my list',
 | 
			
		||||
			}, alice).then(x => x.body);
 | 
			
		||||
 | 
			
		||||
			// Alice が Bob をリスイン
 | 
			
		||||
			await request('/users/lists/push', {
 | 
			
		||||
				listId: list.id,
 | 
			
		||||
				userId: bob.id
 | 
			
		||||
				userId: bob.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			const ws = await connectStream(alice, 'userList', ({ type, body }) => {
 | 
			
		||||
@@ -686,14 +686,14 @@ describe('Streaming', () => {
 | 
			
		||||
					done();
 | 
			
		||||
				}
 | 
			
		||||
			}, {
 | 
			
		||||
				listId: list.id
 | 
			
		||||
				listId: list.id,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			// Bob が Alice 宛てのダイレクト投稿
 | 
			
		||||
			post(bob, {
 | 
			
		||||
				text: 'foo',
 | 
			
		||||
				visibility: 'specified',
 | 
			
		||||
				visibleUserIds: [alice.id]
 | 
			
		||||
				visibleUserIds: [alice.id],
 | 
			
		||||
			});
 | 
			
		||||
		}));
 | 
			
		||||
 | 
			
		||||
@@ -704,13 +704,13 @@ describe('Streaming', () => {
 | 
			
		||||
 | 
			
		||||
			// リスト作成
 | 
			
		||||
			const list = await request('/users/lists/create', {
 | 
			
		||||
				name: 'my list'
 | 
			
		||||
				name: 'my list',
 | 
			
		||||
			}, alice).then(x => x.body);
 | 
			
		||||
 | 
			
		||||
			// Alice が Bob をリスイン
 | 
			
		||||
			await request('/users/lists/push', {
 | 
			
		||||
				listId: list.id,
 | 
			
		||||
				userId: bob.id
 | 
			
		||||
				userId: bob.id,
 | 
			
		||||
			}, alice);
 | 
			
		||||
 | 
			
		||||
			let fired = false;
 | 
			
		||||
@@ -720,13 +720,13 @@ describe('Streaming', () => {
 | 
			
		||||
					fired = true;
 | 
			
		||||
				}
 | 
			
		||||
			}, {
 | 
			
		||||
				listId: list.id
 | 
			
		||||
				listId: list.id,
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			// フォロワー宛て投稿
 | 
			
		||||
			post(bob, {
 | 
			
		||||
				text: 'foo',
 | 
			
		||||
				visibility: 'followers'
 | 
			
		||||
				visibility: 'followers',
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			setTimeout(() => {
 | 
			
		||||
@@ -749,12 +749,12 @@ describe('Streaming', () => {
 | 
			
		||||
				}
 | 
			
		||||
			}, {
 | 
			
		||||
				q: [
 | 
			
		||||
					['foo']
 | 
			
		||||
				]
 | 
			
		||||
					['foo'],
 | 
			
		||||
				],
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			post(me, {
 | 
			
		||||
				text: '#foo'
 | 
			
		||||
				text: '#foo',
 | 
			
		||||
			});
 | 
			
		||||
		}));
 | 
			
		||||
 | 
			
		||||
@@ -773,20 +773,20 @@ describe('Streaming', () => {
 | 
			
		||||
				}
 | 
			
		||||
			}, {
 | 
			
		||||
				q: [
 | 
			
		||||
					['foo', 'bar']
 | 
			
		||||
				]
 | 
			
		||||
					['foo', 'bar'],
 | 
			
		||||
				],
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			post(me, {
 | 
			
		||||
				text: '#foo'
 | 
			
		||||
				text: '#foo',
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			post(me, {
 | 
			
		||||
				text: '#bar'
 | 
			
		||||
				text: '#bar',
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			post(me, {
 | 
			
		||||
				text: '#foo #bar'
 | 
			
		||||
				text: '#foo #bar',
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			setTimeout(() => {
 | 
			
		||||
@@ -816,24 +816,24 @@ describe('Streaming', () => {
 | 
			
		||||
			}, {
 | 
			
		||||
				q: [
 | 
			
		||||
					['foo'],
 | 
			
		||||
					['bar']
 | 
			
		||||
				]
 | 
			
		||||
					['bar'],
 | 
			
		||||
				],
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			post(me, {
 | 
			
		||||
				text: '#foo'
 | 
			
		||||
				text: '#foo',
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			post(me, {
 | 
			
		||||
				text: '#bar'
 | 
			
		||||
				text: '#bar',
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			post(me, {
 | 
			
		||||
				text: '#foo #bar'
 | 
			
		||||
				text: '#foo #bar',
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			post(me, {
 | 
			
		||||
				text: '#piyo'
 | 
			
		||||
				text: '#piyo',
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			setTimeout(() => {
 | 
			
		||||
@@ -866,28 +866,28 @@ describe('Streaming', () => {
 | 
			
		||||
			}, {
 | 
			
		||||
				q: [
 | 
			
		||||
					['foo', 'bar'],
 | 
			
		||||
					['piyo']
 | 
			
		||||
				]
 | 
			
		||||
					['piyo'],
 | 
			
		||||
				],
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			post(me, {
 | 
			
		||||
				text: '#foo'
 | 
			
		||||
				text: '#foo',
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			post(me, {
 | 
			
		||||
				text: '#bar'
 | 
			
		||||
				text: '#bar',
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			post(me, {
 | 
			
		||||
				text: '#foo #bar'
 | 
			
		||||
				text: '#foo #bar',
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			post(me, {
 | 
			
		||||
				text: '#piyo'
 | 
			
		||||
				text: '#piyo',
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			post(me, {
 | 
			
		||||
				text: '#waaa'
 | 
			
		||||
				text: '#waaa',
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			setTimeout(() => {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,13 @@ process.env.NODE_ENV = 'test';
 | 
			
		||||
 | 
			
		||||
import * as assert from 'assert';
 | 
			
		||||
import * as childProcess from 'child_process';
 | 
			
		||||
import { dirname } from 'node:path';
 | 
			
		||||
import { fileURLToPath } from 'node:url';
 | 
			
		||||
import { async, signup, request, post, uploadFile, startServer, shutdownServer } from './utils.js';
 | 
			
		||||
 | 
			
		||||
const _filename = fileURLToPath(import.meta.url);
 | 
			
		||||
const _dirname = dirname(_filename);
 | 
			
		||||
 | 
			
		||||
describe('users/notes', () => {
 | 
			
		||||
	let p: childProcess.ChildProcess;
 | 
			
		||||
 | 
			
		||||
@@ -15,16 +20,16 @@ describe('users/notes', () => {
 | 
			
		||||
	before(async () => {
 | 
			
		||||
		p = await startServer();
 | 
			
		||||
		alice = await signup({ username: 'alice' });
 | 
			
		||||
		const jpg = await uploadFile(alice, __dirname + '/resources/Lenna.jpg');
 | 
			
		||||
		const png = await uploadFile(alice, __dirname + '/resources/Lenna.png');
 | 
			
		||||
		const jpg = await uploadFile(alice, _dirname + '/resources/Lenna.jpg');
 | 
			
		||||
		const png = await uploadFile(alice, _dirname + '/resources/Lenna.png');
 | 
			
		||||
		jpgNote = await post(alice, {
 | 
			
		||||
			fileIds: [jpg.id]
 | 
			
		||||
			fileIds: [jpg.id],
 | 
			
		||||
		});
 | 
			
		||||
		pngNote = await post(alice, {
 | 
			
		||||
			fileIds: [png.id]
 | 
			
		||||
			fileIds: [png.id],
 | 
			
		||||
		});
 | 
			
		||||
		jpgPngNote = await post(alice, {
 | 
			
		||||
			fileIds: [jpg.id, png.id]
 | 
			
		||||
			fileIds: [jpg.id, png.id],
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
@@ -35,7 +40,7 @@ describe('users/notes', () => {
 | 
			
		||||
	it('ファイルタイプ指定 (jpg)', async(async () => {
 | 
			
		||||
		const res = await request('/users/notes', {
 | 
			
		||||
			userId: alice.id,
 | 
			
		||||
			fileType: ['image/jpeg']
 | 
			
		||||
			fileType: ['image/jpeg'],
 | 
			
		||||
		}, alice);
 | 
			
		||||
 | 
			
		||||
		assert.strictEqual(res.status, 200);
 | 
			
		||||
@@ -48,7 +53,7 @@ describe('users/notes', () => {
 | 
			
		||||
	it('ファイルタイプ指定 (jpg or png)', async(async () => {
 | 
			
		||||
		const res = await request('/users/notes', {
 | 
			
		||||
			userId: alice.id,
 | 
			
		||||
			fileType: ['image/jpeg', 'image/png']
 | 
			
		||||
			fileType: ['image/jpeg', 'image/png'],
 | 
			
		||||
		}, alice);
 | 
			
		||||
 | 
			
		||||
		assert.strictEqual(res.status, 200);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,20 @@
 | 
			
		||||
import * as fs from 'node:fs';
 | 
			
		||||
import { fileURLToPath } from 'node:url';
 | 
			
		||||
import { dirname } from 'node:path';
 | 
			
		||||
import * as childProcess from 'child_process';
 | 
			
		||||
import * as http from 'node:http';
 | 
			
		||||
import { SIGKILL } from 'constants';
 | 
			
		||||
import * as WebSocket from 'ws';
 | 
			
		||||
import * as misskey from 'misskey-js';
 | 
			
		||||
import fetch from 'node-fetch';
 | 
			
		||||
import FormData from 'form-data';
 | 
			
		||||
import * as childProcess from 'child_process';
 | 
			
		||||
import * as http from 'node:http';
 | 
			
		||||
import { DataSource } from 'typeorm';
 | 
			
		||||
import loadConfig from '../src/config/load.js';
 | 
			
		||||
import { SIGKILL } from 'constants';
 | 
			
		||||
import { entities } from '../src/db/postgre.js';
 | 
			
		||||
 | 
			
		||||
const _filename = fileURLToPath(import.meta.url);
 | 
			
		||||
const _dirname = dirname(_filename);
 | 
			
		||||
 | 
			
		||||
const config = loadConfig();
 | 
			
		||||
export const port = config.port;
 | 
			
		||||
 | 
			
		||||
@@ -22,29 +28,29 @@ export const async = (fn: Function) => (done: Function) => {
 | 
			
		||||
 | 
			
		||||
export const request = async (endpoint: string, params: any, me?: any): Promise<{ body: any, status: number }> => {
 | 
			
		||||
	const auth = me ? {
 | 
			
		||||
		i: me.token
 | 
			
		||||
		i: me.token,
 | 
			
		||||
	} : {};
 | 
			
		||||
 | 
			
		||||
	const res = await fetch(`http://localhost:${port}/api${endpoint}`, {
 | 
			
		||||
		method: 'POST',
 | 
			
		||||
		headers: {
 | 
			
		||||
			'Content-Type': 'application/json'
 | 
			
		||||
			'Content-Type': 'application/json',
 | 
			
		||||
		},
 | 
			
		||||
		body: JSON.stringify(Object.assign(auth, params))
 | 
			
		||||
		body: JSON.stringify(Object.assign(auth, params)),
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	const status = res.status;
 | 
			
		||||
	const body = res.status !== 204 ? await res.json().catch() : null;
 | 
			
		||||
 | 
			
		||||
	return {
 | 
			
		||||
		body, status
 | 
			
		||||
		body, status,
 | 
			
		||||
	};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const signup = async (params?: any): Promise<any> => {
 | 
			
		||||
	const q = Object.assign({
 | 
			
		||||
		username: 'test',
 | 
			
		||||
		password: 'test'
 | 
			
		||||
		password: 'test',
 | 
			
		||||
	}, params);
 | 
			
		||||
 | 
			
		||||
	const res = await request('/signup', q);
 | 
			
		||||
@@ -54,7 +60,7 @@ export const signup = async (params?: any): Promise<any> => {
 | 
			
		||||
 | 
			
		||||
export const post = async (user: any, params?: misskey.Endpoints['notes/create']['req']): Promise<misskey.entities.Note> => {
 | 
			
		||||
	const q = Object.assign({
 | 
			
		||||
		text: 'test'
 | 
			
		||||
		text: 'test',
 | 
			
		||||
	}, params);
 | 
			
		||||
 | 
			
		||||
	const res = await request('/notes/create', q, user);
 | 
			
		||||
@@ -65,26 +71,26 @@ export const post = async (user: any, params?: misskey.Endpoints['notes/create']
 | 
			
		||||
export const react = async (user: any, note: any, reaction: string): Promise<any> => {
 | 
			
		||||
	await request('/notes/reactions/create', {
 | 
			
		||||
		noteId: note.id,
 | 
			
		||||
		reaction: reaction
 | 
			
		||||
		reaction: reaction,
 | 
			
		||||
	}, user);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const uploadFile = (user: any, path?: string): Promise<any> => {
 | 
			
		||||
		const formData = new FormData();
 | 
			
		||||
		formData.append('i', user.token);
 | 
			
		||||
		formData.append('file', fs.createReadStream(path || __dirname + '/resources/Lenna.png'));
 | 
			
		||||
	const formData = new FormData();
 | 
			
		||||
	formData.append('i', user.token);
 | 
			
		||||
	formData.append('file', fs.createReadStream(path || _dirname + '/resources/Lenna.png'));
 | 
			
		||||
 | 
			
		||||
		return fetch(`http://localhost:${port}/api/drive/files/create`, {
 | 
			
		||||
			method: 'post',
 | 
			
		||||
			body: formData,
 | 
			
		||||
			timeout: 30 * 1000,
 | 
			
		||||
		}).then(res => {
 | 
			
		||||
			if (!res.ok) {
 | 
			
		||||
				throw `${res.status} ${res.statusText}`;
 | 
			
		||||
			} else {
 | 
			
		||||
				return res.json();
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	return fetch(`http://localhost:${port}/api/drive/files/create`, {
 | 
			
		||||
		method: 'post',
 | 
			
		||||
		body: formData,
 | 
			
		||||
		timeout: 30 * 1000,
 | 
			
		||||
	}).then(res => {
 | 
			
		||||
		if (!res.ok) {
 | 
			
		||||
			throw `${res.status} ${res.statusText}`;
 | 
			
		||||
		} else {
 | 
			
		||||
			return res.json();
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function connectStream(user: any, channel: string, listener: (message: Record<string, any>) => any, params?: any): Promise<WebSocket> {
 | 
			
		||||
@@ -94,9 +100,9 @@ export function connectStream(user: any, channel: string, listener: (message: Re
 | 
			
		||||
		ws.on('open', () => {
 | 
			
		||||
			ws.on('message', data => {
 | 
			
		||||
				const msg = JSON.parse(data.toString());
 | 
			
		||||
				if (msg.type == 'channel' && msg.body.id == 'a') {
 | 
			
		||||
				if (msg.type === 'channel' && msg.body.id === 'a') {
 | 
			
		||||
					listener(msg.body);
 | 
			
		||||
				} else if (msg.type == 'connected' && msg.body.id == 'a') {
 | 
			
		||||
				} else if (msg.type === 'connected' && msg.body.id === 'a') {
 | 
			
		||||
					res(ws);
 | 
			
		||||
				}
 | 
			
		||||
			});
 | 
			
		||||
@@ -107,8 +113,8 @@ export function connectStream(user: any, channel: string, listener: (message: Re
 | 
			
		||||
					channel: channel,
 | 
			
		||||
					id: 'a',
 | 
			
		||||
					pong: true,
 | 
			
		||||
					params: params
 | 
			
		||||
				}
 | 
			
		||||
					params: params,
 | 
			
		||||
				},
 | 
			
		||||
			}));
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
@@ -119,8 +125,8 @@ export const simpleGet = async (path: string, accept = '*/*'): Promise<{ status?
 | 
			
		||||
	return await new Promise((resolve, reject) => {
 | 
			
		||||
		const req = http.request(`http://localhost:${port}${path}`, {
 | 
			
		||||
			headers: {
 | 
			
		||||
				Accept: accept
 | 
			
		||||
			}
 | 
			
		||||
				Accept: accept,
 | 
			
		||||
			},
 | 
			
		||||
		}, res => {
 | 
			
		||||
			if (res.statusCode! >= 400) {
 | 
			
		||||
				reject(res);
 | 
			
		||||
@@ -139,9 +145,9 @@ export const simpleGet = async (path: string, accept = '*/*'): Promise<{ status?
 | 
			
		||||
 | 
			
		||||
export function launchServer(callbackSpawnedProcess: (p: childProcess.ChildProcess) => void, moreProcess: () => Promise<void> = async () => {}) {
 | 
			
		||||
	return (done: (err?: Error) => any) => {
 | 
			
		||||
		const p = childProcess.spawn('node', [__dirname + '/../index.js'], {
 | 
			
		||||
		const p = childProcess.spawn('node', [_dirname + '/../index.js'], {
 | 
			
		||||
			stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
 | 
			
		||||
			env: { NODE_ENV: 'test', PATH: process.env.PATH }
 | 
			
		||||
			env: { NODE_ENV: 'test', PATH: process.env.PATH },
 | 
			
		||||
		});
 | 
			
		||||
		callbackSpawnedProcess(p);
 | 
			
		||||
		p.on('message', message => {
 | 
			
		||||
@@ -153,12 +159,7 @@ export function launchServer(callbackSpawnedProcess: (p: childProcess.ChildProce
 | 
			
		||||
export async function initTestDb(justBorrow = false, initEntities?: any[]) {
 | 
			
		||||
	if (process.env.NODE_ENV !== 'test') throw 'NODE_ENV is not a test';
 | 
			
		||||
 | 
			
		||||
	try {
 | 
			
		||||
		const conn = await getConnection();
 | 
			
		||||
		await conn.close();
 | 
			
		||||
	} catch (e) {}
 | 
			
		||||
 | 
			
		||||
	return await createConnection({
 | 
			
		||||
	const db = new DataSource({
 | 
			
		||||
		type: 'postgres',
 | 
			
		||||
		host: config.db.host,
 | 
			
		||||
		port: config.db.port,
 | 
			
		||||
@@ -167,8 +168,12 @@ export async function initTestDb(justBorrow = false, initEntities?: any[]) {
 | 
			
		||||
		database: config.db.db,
 | 
			
		||||
		synchronize: true && !justBorrow,
 | 
			
		||||
		dropSchema: true && !justBorrow,
 | 
			
		||||
		entities: initEntities || entities
 | 
			
		||||
		entities: initEntities || entities,
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	await db.initialize();
 | 
			
		||||
 | 
			
		||||
	return db;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function startServer(timeout = 30 * 1000): Promise<childProcess.ChildProcess> {
 | 
			
		||||
@@ -178,9 +183,9 @@ export function startServer(timeout = 30 * 1000): Promise<childProcess.ChildProc
 | 
			
		||||
			rej('timeout to start');
 | 
			
		||||
		}, timeout);
 | 
			
		||||
 | 
			
		||||
		const p = childProcess.spawn('node', [__dirname + '/../built/index.js'], {
 | 
			
		||||
		const p = childProcess.spawn('node', [_dirname + '/../built/index.js'], {
 | 
			
		||||
			stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
 | 
			
		||||
			env: { NODE_ENV: 'test', PATH: process.env.PATH }
 | 
			
		||||
			env: { NODE_ENV: 'test', PATH: process.env.PATH },
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		p.on('error', e => rej(e));
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										5
									
								
								packages/client/@types/theme.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								packages/client/@types/theme.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
import { Theme } from '../src/scripts/theme';
 | 
			
		||||
 | 
			
		||||
declare module '@/themes/*.json5' {
 | 
			
		||||
	export = Theme;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
{
 | 
			
		||||
	"private": true,
 | 
			
		||||
	"scripts": {
 | 
			
		||||
		"watch": "webpack --watch",
 | 
			
		||||
		"build": "webpack",
 | 
			
		||||
		"watch": "vite build --watch --mode development",
 | 
			
		||||
		"build": "vite build",
 | 
			
		||||
		"lint": "eslint --quiet \"src/**/*.{ts,vue}\""
 | 
			
		||||
	},
 | 
			
		||||
	"resolutions": {
 | 
			
		||||
@@ -10,50 +10,38 @@
 | 
			
		||||
		"lodash": "^4.17.21"
 | 
			
		||||
	},
 | 
			
		||||
	"dependencies": {
 | 
			
		||||
		"@discordapp/twemoji": "13.1.1",
 | 
			
		||||
		"@discordapp/twemoji": "14.0.2",
 | 
			
		||||
		"@fortawesome/fontawesome-free": "6.1.1",
 | 
			
		||||
		"@syuilo/aiscript": "0.11.1",
 | 
			
		||||
		"@typescript-eslint/parser": "5.20.0",
 | 
			
		||||
		"@vue/compiler-sfc": "3.2.33",
 | 
			
		||||
		"abort-controller": "3.0.0",
 | 
			
		||||
		"autobind-decorator": "2.4.0",
 | 
			
		||||
		"autosize": "5.0.1",
 | 
			
		||||
		"autwh": "0.1.0",
 | 
			
		||||
		"blurhash": "1.1.5",
 | 
			
		||||
		"broadcast-channel": "4.11.0",
 | 
			
		||||
		"broadcast-channel": "4.12.0",
 | 
			
		||||
		"browser-image-resizer": "git+https://github.com/misskey-dev/browser-image-resizer#v2.2.1-misskey.2",
 | 
			
		||||
		"chart.js": "3.7.1",
 | 
			
		||||
		"chart.js": "3.8.0",
 | 
			
		||||
		"chartjs-adapter-date-fns": "2.0.0",
 | 
			
		||||
		"chartjs-plugin-gradient": "0.2.2",
 | 
			
		||||
		"chartjs-plugin-gradient": "0.5.0",
 | 
			
		||||
		"chartjs-plugin-zoom": "1.2.1",
 | 
			
		||||
		"compare-versions": "4.1.3",
 | 
			
		||||
		"content-disposition": "0.5.4",
 | 
			
		||||
		"css-loader": "6.7.1",
 | 
			
		||||
		"cssnano": "5.1.7",
 | 
			
		||||
		"date-fns": "2.28.0",
 | 
			
		||||
		"deepcopy": "2.1.0",
 | 
			
		||||
		"escape-regexp": "0.0.1",
 | 
			
		||||
		"eslint": "8.14.0",
 | 
			
		||||
		"eslint-plugin-vue": "8.7.1",
 | 
			
		||||
		"eventemitter3": "4.0.7",
 | 
			
		||||
		"feed": "4.2.2",
 | 
			
		||||
		"glob": "7.2.0",
 | 
			
		||||
		"idb-keyval": "6.1.0",
 | 
			
		||||
		"insert-text-at-cursor": "0.3.0",
 | 
			
		||||
		"json5": "2.2.1",
 | 
			
		||||
		"json5-loader": "4.0.1",
 | 
			
		||||
		"katex": "0.15.3",
 | 
			
		||||
		"katex": "0.15.6",
 | 
			
		||||
		"matter-js": "0.18.0",
 | 
			
		||||
		"mfm-js": "0.21.0",
 | 
			
		||||
		"mfm-js": "0.22.1",
 | 
			
		||||
		"misskey-js": "0.0.14",
 | 
			
		||||
		"mocha": "9.2.2",
 | 
			
		||||
		"mocha": "10.0.0",
 | 
			
		||||
		"ms": "2.1.3",
 | 
			
		||||
		"nested-property": "4.0.0",
 | 
			
		||||
		"parse5": "6.0.1",
 | 
			
		||||
		"photoswipe": "5.2.4",
 | 
			
		||||
		"portscanner": "2.2.0",
 | 
			
		||||
		"postcss": "8.4.12",
 | 
			
		||||
		"postcss-loader": "6.2.1",
 | 
			
		||||
		"photoswipe": "5.2.7",
 | 
			
		||||
		"prismjs": "1.28.0",
 | 
			
		||||
		"private-ip": "2.3.3",
 | 
			
		||||
		"promise-limit": "2.7.0",
 | 
			
		||||
@@ -65,36 +53,34 @@
 | 
			
		||||
		"reflect-metadata": "0.1.13",
 | 
			
		||||
		"rndstr": "1.0.0",
 | 
			
		||||
		"s-age": "1.1.2",
 | 
			
		||||
		"sass": "1.50.1",
 | 
			
		||||
		"sass-loader": "12.6.0",
 | 
			
		||||
		"sass": "1.52.1",
 | 
			
		||||
		"seedrandom": "3.0.5",
 | 
			
		||||
		"strict-event-emitter-types": "2.0.0",
 | 
			
		||||
		"stringz": "2.1.0",
 | 
			
		||||
		"style-loader": "3.3.1",
 | 
			
		||||
		"syuilo-password-strength": "0.0.1",
 | 
			
		||||
		"textarea-caret": "3.1.0",
 | 
			
		||||
		"three": "0.139.2",
 | 
			
		||||
		"throttle-debounce": "4.0.1",
 | 
			
		||||
		"three": "0.140.2",
 | 
			
		||||
		"throttle-debounce": "5.0.0",
 | 
			
		||||
		"tinycolor2": "1.4.2",
 | 
			
		||||
		"ts-loader": "9.2.8",
 | 
			
		||||
		"tsc-alias": "1.5.0",
 | 
			
		||||
		"tsconfig-paths": "3.14.1",
 | 
			
		||||
		"tsc-alias": "1.6.7",
 | 
			
		||||
		"tsconfig-paths": "4.0.0",
 | 
			
		||||
		"twemoji-parser": "14.0.0",
 | 
			
		||||
		"typescript": "4.6.3",
 | 
			
		||||
		"uuid": "8.3.2",
 | 
			
		||||
		"v-debounce": "0.1.2",
 | 
			
		||||
		"vanilla-tilt": "1.7.2",
 | 
			
		||||
		"vue": "3.2.33",
 | 
			
		||||
		"vue-loader": "17.0.0",
 | 
			
		||||
		"vue": "3.2.36",
 | 
			
		||||
		"vue-prism-editor": "2.0.0-alpha.2",
 | 
			
		||||
		"vue-router": "4.0.14",
 | 
			
		||||
		"vue-style-loader": "4.1.3",
 | 
			
		||||
		"vue-svg-loader": "0.17.0-beta.2",
 | 
			
		||||
		"vue-router": "4.0.15",
 | 
			
		||||
		"vuedraggable": "4.0.1",
 | 
			
		||||
		"webpack": "5.72.0",
 | 
			
		||||
		"webpack-cli": "4.9.2",
 | 
			
		||||
		"websocket": "1.0.34",
 | 
			
		||||
		"ws": "8.5.0"
 | 
			
		||||
		"@vitejs/plugin-vue": "2.3.3",
 | 
			
		||||
		"@vue/compiler-sfc": "3.2.36",
 | 
			
		||||
		"@rollup/plugin-alias": "3.1.9",
 | 
			
		||||
		"@rollup/plugin-json": "4.1.0",
 | 
			
		||||
		"rollup": "2.74.1",
 | 
			
		||||
		"typescript": "4.7.2",
 | 
			
		||||
		"vite": "2.9.9",
 | 
			
		||||
		"ws": "8.6.0"
 | 
			
		||||
	},
 | 
			
		||||
	"devDependencies": {
 | 
			
		||||
		"@types/escape-regexp": "0.0.1",
 | 
			
		||||
@@ -106,21 +92,21 @@
 | 
			
		||||
		"@types/matter-js": "0.17.7",
 | 
			
		||||
		"@types/mocha": "9.1.1",
 | 
			
		||||
		"@types/oauth": "0.9.1",
 | 
			
		||||
		"@types/parse5": "6.0.3",
 | 
			
		||||
		"@types/punycode": "2.1.0",
 | 
			
		||||
		"@types/qrcode": "1.4.2",
 | 
			
		||||
		"@types/random-seed": "0.3.3",
 | 
			
		||||
		"@types/seedrandom": "3.0.2",
 | 
			
		||||
		"@types/throttle-debounce": "4.0.0",
 | 
			
		||||
		"@types/throttle-debounce": "5.0.0",
 | 
			
		||||
		"@types/tinycolor2": "1.4.3",
 | 
			
		||||
		"@types/uuid": "8.3.4",
 | 
			
		||||
		"@types/webpack": "5.28.0",
 | 
			
		||||
		"@types/webpack-stream": "3.2.12",
 | 
			
		||||
		"@types/websocket": "1.0.5",
 | 
			
		||||
		"@types/ws": "8.5.3",
 | 
			
		||||
		"@typescript-eslint/eslint-plugin": "5.20.0",
 | 
			
		||||
		"@typescript-eslint/eslint-plugin": "5.26.0",
 | 
			
		||||
		"@typescript-eslint/parser": "5.26.0",
 | 
			
		||||
		"eslint": "8.16.0",
 | 
			
		||||
		"eslint-plugin-vue": "9.0.1",
 | 
			
		||||
		"cross-env": "7.0.3",
 | 
			
		||||
		"cypress": "9.5.4",
 | 
			
		||||
		"cypress": "9.7.0",
 | 
			
		||||
		"eslint-plugin-import": "2.26.0",
 | 
			
		||||
		"start-server-and-test": "1.14.0"
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import { del, get, set } from '@/scripts/idb-proxy';
 | 
			
		||||
import { reactive } from 'vue';
 | 
			
		||||
import { defineAsyncComponent, reactive } from 'vue';
 | 
			
		||||
import * as misskey from 'misskey-js';
 | 
			
		||||
import { apiUrl } from '@/config';
 | 
			
		||||
import { waiting, api, popup, popupMenu, success, alert } from '@/os';
 | 
			
		||||
@@ -11,10 +11,10 @@ import { i18n } from './i18n';
 | 
			
		||||
 | 
			
		||||
type Account = misskey.entities.MeDetailed;
 | 
			
		||||
 | 
			
		||||
const data = localStorage.getItem('account');
 | 
			
		||||
const accountData = localStorage.getItem('account');
 | 
			
		||||
 | 
			
		||||
// TODO: 外部からはreadonlyに
 | 
			
		||||
export const $i = data ? reactive(JSON.parse(data) as Account) : null;
 | 
			
		||||
export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null;
 | 
			
		||||
 | 
			
		||||
export const iAmModerator = $i != null && ($i.isAdmin || $i.isModerator);
 | 
			
		||||
 | 
			
		||||
@@ -52,7 +52,7 @@ export async function signout() {
 | 
			
		||||
					return Promise.all(registrations.map(registration => registration.unregister()));
 | 
			
		||||
				});
 | 
			
		||||
		}
 | 
			
		||||
	} catch (e) {}
 | 
			
		||||
	} catch (err) {}
 | 
			
		||||
	//#endregion
 | 
			
		||||
 | 
			
		||||
	document.cookie = `igi=; path=/`;
 | 
			
		||||
@@ -104,8 +104,8 @@ function fetchAccount(token: string): Promise<Account> {
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function updateAccount(data) {
 | 
			
		||||
	for (const [key, value] of Object.entries(data)) {
 | 
			
		||||
export function updateAccount(accountData) {
 | 
			
		||||
	for (const [key, value] of Object.entries(accountData)) {
 | 
			
		||||
		$i[key] = value;
 | 
			
		||||
	}
 | 
			
		||||
	localStorage.setItem('account', JSON.stringify($i));
 | 
			
		||||
@@ -141,7 +141,7 @@ export async function openAccountMenu(opts: {
 | 
			
		||||
	onChoose?: (account: misskey.entities.UserDetailed) => void;
 | 
			
		||||
}, ev: MouseEvent) {
 | 
			
		||||
	function showSigninDialog() {
 | 
			
		||||
		popup(import('@/components/signin-dialog.vue'), {}, {
 | 
			
		||||
		popup(defineAsyncComponent(() => import('@/components/signin-dialog.vue')), {}, {
 | 
			
		||||
			done: res => {
 | 
			
		||||
				addAccount(res.id, res.i);
 | 
			
		||||
				success();
 | 
			
		||||
@@ -150,7 +150,7 @@ export async function openAccountMenu(opts: {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function createAccount() {
 | 
			
		||||
		popup(import('@/components/signup-dialog.vue'), {}, {
 | 
			
		||||
		popup(defineAsyncComponent(() => import('@/components/signup-dialog.vue')), {}, {
 | 
			
		||||
			done: res => {
 | 
			
		||||
				addAccount(res.id, res.i);
 | 
			
		||||
				switchAccountWithToken(res.i);
 | 
			
		||||
 
 | 
			
		||||
@@ -37,7 +37,7 @@ const props = defineProps<{
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(e: 'closed'): void;
 | 
			
		||||
	(ev: 'closed'): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const window = ref<InstanceType<typeof XWindow>>();
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
<div class="bcekxzvu _card _gap">
 | 
			
		||||
	<div class="_content target">
 | 
			
		||||
		<MkAvatar class="avatar" :user="report.targetUser" :show-indicator="true"/>
 | 
			
		||||
		<MkA class="info" :to="userPage(report.targetUser)" v-user-preview="report.targetUserId">
 | 
			
		||||
		<MkA v-user-preview="report.targetUserId" class="info" :to="userPage(report.targetUser)">
 | 
			
		||||
			<MkUserName class="name" :user="report.targetUser"/>
 | 
			
		||||
			<MkAcct class="acct" :user="report.targetUser" style="display: block;"/>
 | 
			
		||||
		</MkA>
 | 
			
		||||
@@ -43,20 +43,20 @@ export default defineComponent({
 | 
			
		||||
		MkSwitch,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	emits: ['resolved'],
 | 
			
		||||
 | 
			
		||||
	props: {
 | 
			
		||||
		report: {
 | 
			
		||||
			type: Object,
 | 
			
		||||
			required: true,
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	emits: ['resolved'],
 | 
			
		||||
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			forward: this.report.forwarded,
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		acct,
 | 
			
		||||
 
 | 
			
		||||
@@ -42,7 +42,7 @@
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
 | 
			
		||||
import * as tinycolor from 'tinycolor2';
 | 
			
		||||
import tinycolor from 'tinycolor2';
 | 
			
		||||
 | 
			
		||||
withDefaults(defineProps<{
 | 
			
		||||
	thickness: number;
 | 
			
		||||
 
 | 
			
		||||
@@ -48,8 +48,8 @@ async function onClick() {
 | 
			
		||||
			});
 | 
			
		||||
			isFollowing.value = true;
 | 
			
		||||
		}
 | 
			
		||||
	} catch (e) {
 | 
			
		||||
		console.error(e);
 | 
			
		||||
	} catch (err) {
 | 
			
		||||
		console.error(err);
 | 
			
		||||
	} finally {
 | 
			
		||||
		wait.value = false;
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -18,7 +18,7 @@ const props = defineProps<{
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(e: 'update:modelValue', v: boolean): void;
 | 
			
		||||
	(ev: 'update:modelValue', v: boolean): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const label = computed(() => {
 | 
			
		||||
 
 | 
			
		||||
@@ -90,8 +90,8 @@ const props = withDefaults(defineProps<{
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(e: 'done', v: { canceled: boolean; result: any }): void;
 | 
			
		||||
	(e: 'closed'): void;
 | 
			
		||||
	(ev: 'done', v: { canceled: boolean; result: any }): void;
 | 
			
		||||
	(ev: 'closed'): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const modal = ref<InstanceType<typeof MkModal>>();
 | 
			
		||||
@@ -122,14 +122,14 @@ function onBgClick() {
 | 
			
		||||
	if (props.cancelableByBgClick) cancel();
 | 
			
		||||
}
 | 
			
		||||
*/
 | 
			
		||||
function onKeydown(e: KeyboardEvent) {
 | 
			
		||||
	if (e.key === 'Escape') cancel();
 | 
			
		||||
function onKeydown(evt: KeyboardEvent) {
 | 
			
		||||
	if (evt.key === 'Escape') cancel();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onInputKeydown(e: KeyboardEvent) {
 | 
			
		||||
	if (e.key === 'Enter') {
 | 
			
		||||
		e.preventDefault();
 | 
			
		||||
		e.stopPropagation();
 | 
			
		||||
function onInputKeydown(evt: KeyboardEvent) {
 | 
			
		||||
	if (evt.key === 'Enter') {
 | 
			
		||||
		evt.preventDefault();
 | 
			
		||||
		evt.stopPropagation();
 | 
			
		||||
		ok();
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -42,7 +42,7 @@ const is = computed(() => {
 | 
			
		||||
			"application/x-tar",
 | 
			
		||||
			"application/gzip",
 | 
			
		||||
			"application/x-7z-compressed"
 | 
			
		||||
		].some(e => e === props.file.type)) return 'archive';
 | 
			
		||||
		].some(archiveType => archiveType === props.file.type)) return 'archive';
 | 
			
		||||
	return 'unknown';
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -33,8 +33,8 @@ withDefaults(defineProps<{
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(e: 'done', r?: Misskey.entities.DriveFile[]): void;
 | 
			
		||||
	(e: 'closed'): void;
 | 
			
		||||
	(ev: 'done', r?: Misskey.entities.DriveFile[]): void;
 | 
			
		||||
	(ev: 'closed'): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const dialog = ref<InstanceType<typeof XModalWindow>>();
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,6 @@ defineProps<{
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(e: 'closed'): void;
 | 
			
		||||
	(ev: 'closed'): void;
 | 
			
		||||
}>();
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -31,7 +31,7 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { computed, ref } from 'vue';
 | 
			
		||||
import { computed, defineAsyncComponent, ref } from 'vue';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
 | 
			
		||||
import MkDriveFileThumbnail from './drive-file-thumbnail.vue';
 | 
			
		||||
@@ -50,9 +50,9 @@ const props = withDefaults(defineProps<{
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(e: 'chosen', r: Misskey.entities.DriveFile): void;
 | 
			
		||||
	(e: 'dragstart'): void;
 | 
			
		||||
	(e: 'dragend'): void;
 | 
			
		||||
	(ev: 'chosen', r: Misskey.entities.DriveFile): void;
 | 
			
		||||
	(ev: 'dragstart'): void;
 | 
			
		||||
	(ev: 'dragend'): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const isDragging = ref(false);
 | 
			
		||||
@@ -99,14 +99,14 @@ function onClick(ev: MouseEvent) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onContextmenu(e: MouseEvent) {
 | 
			
		||||
	os.contextMenu(getMenu(), e);
 | 
			
		||||
function onContextmenu(ev: MouseEvent) {
 | 
			
		||||
	os.contextMenu(getMenu(), ev);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onDragstart(e: DragEvent) {
 | 
			
		||||
	if (e.dataTransfer) {
 | 
			
		||||
		e.dataTransfer.effectAllowed = 'move';
 | 
			
		||||
		e.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FILE_, JSON.stringify(props.file));
 | 
			
		||||
function onDragstart(ev: DragEvent) {
 | 
			
		||||
	if (ev.dataTransfer) {
 | 
			
		||||
		ev.dataTransfer.effectAllowed = 'move';
 | 
			
		||||
		ev.dataTransfer.setData(_DATA_TRANSFER_DRIVE_FILE_, JSON.stringify(props.file));
 | 
			
		||||
	}
 | 
			
		||||
	isDragging.value = true;
 | 
			
		||||
 | 
			
		||||
@@ -133,11 +133,11 @@ function rename() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function describe() {
 | 
			
		||||
	os.popup(import('@/components/media-caption.vue'), {
 | 
			
		||||
	os.popup(defineAsyncComponent(() => import('@/components/media-caption.vue')), {
 | 
			
		||||
		title: i18n.ts.describeFile,
 | 
			
		||||
		input: {
 | 
			
		||||
			placeholder: i18n.ts.inputNewDescription,
 | 
			
		||||
			default: props.file.comment !== null ? props.file.comment : '',
 | 
			
		||||
			default: props.file.comment != null ? props.file.comment : '',
 | 
			
		||||
		},
 | 
			
		||||
		image: props.file
 | 
			
		||||
	}, {
 | 
			
		||||
@@ -146,7 +146,7 @@ function describe() {
 | 
			
		||||
			let comment = result.result;
 | 
			
		||||
			os.api('drive/files/update', {
 | 
			
		||||
				fileId: props.file.id,
 | 
			
		||||
				comment: comment.length == 0 ? null : comment
 | 
			
		||||
				comment: comment.length === 0 ? null : comment
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}, 'closed');
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,7 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { computed, ref } from 'vue';
 | 
			
		||||
import { computed, defineAsyncComponent, ref } from 'vue';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
@@ -84,12 +84,12 @@ function onDragover(ev: DragEvent) {
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const isFile = ev.dataTransfer.items[0].kind == 'file';
 | 
			
		||||
	const isDriveFile = ev.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
 | 
			
		||||
	const isDriveFolder = ev.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
 | 
			
		||||
	const isFile = ev.dataTransfer.items[0].kind === 'file';
 | 
			
		||||
	const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_;
 | 
			
		||||
	const isDriveFolder = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FOLDER_;
 | 
			
		||||
 | 
			
		||||
	if (isFile || isDriveFile || isDriveFolder) {
 | 
			
		||||
		ev.dataTransfer.dropEffect = ev.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
 | 
			
		||||
		ev.dataTransfer.dropEffect = ev.dataTransfer.effectAllowed === 'all' ? 'copy' : 'move';
 | 
			
		||||
	} else {
 | 
			
		||||
		ev.dataTransfer.dropEffect = 'none';
 | 
			
		||||
	}
 | 
			
		||||
@@ -118,7 +118,7 @@ function onDrop(ev: DragEvent) {
 | 
			
		||||
 | 
			
		||||
	//#region ドライブのファイル
 | 
			
		||||
	const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
 | 
			
		||||
	if (driveFile != null && driveFile != '') {
 | 
			
		||||
	if (driveFile != null && driveFile !== '') {
 | 
			
		||||
		const file = JSON.parse(driveFile);
 | 
			
		||||
		emit('removeFile', file.id);
 | 
			
		||||
		os.api('drive/files/update', {
 | 
			
		||||
@@ -130,11 +130,11 @@ function onDrop(ev: DragEvent) {
 | 
			
		||||
 | 
			
		||||
	//#region ドライブのフォルダ
 | 
			
		||||
	const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
 | 
			
		||||
	if (driveFolder != null && driveFolder != '') {
 | 
			
		||||
	if (driveFolder != null && driveFolder !== '') {
 | 
			
		||||
		const folder = JSON.parse(driveFolder);
 | 
			
		||||
 | 
			
		||||
		// 移動先が自分自身ならreject
 | 
			
		||||
		if (folder.id == props.folder.id) return;
 | 
			
		||||
		if (folder.id === props.folder.id) return;
 | 
			
		||||
 | 
			
		||||
		emit('removeFolder', folder.id);
 | 
			
		||||
		os.api('drive/folders/update', {
 | 
			
		||||
@@ -230,7 +230,7 @@ function onContextmenu(ev: MouseEvent) {
 | 
			
		||||
		text: i18n.ts.openInWindow,
 | 
			
		||||
		icon: 'fas fa-window-restore',
 | 
			
		||||
		action: () => {
 | 
			
		||||
			os.popup(import('./drive-window.vue'), {
 | 
			
		||||
			os.popup(defineAsyncComponent(() => import('./drive-window.vue')), {
 | 
			
		||||
				initialFolder: props.folder
 | 
			
		||||
			}, {
 | 
			
		||||
			}, 'closed');
 | 
			
		||||
 
 | 
			
		||||
@@ -24,10 +24,10 @@ const props = defineProps<{
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(e: 'move', v?: Misskey.entities.DriveFolder): void;
 | 
			
		||||
	(e: 'upload', file: File, folder?: Misskey.entities.DriveFolder | null): void;
 | 
			
		||||
	(e: 'removeFile', v: Misskey.entities.DriveFile['id']): void;
 | 
			
		||||
	(e: 'removeFolder', v: Misskey.entities.DriveFolder['id']): void;
 | 
			
		||||
	(ev: 'move', v?: Misskey.entities.DriveFolder): void;
 | 
			
		||||
	(ev: 'upload', file: File, folder?: Misskey.entities.DriveFolder | null): void;
 | 
			
		||||
	(ev: 'removeFile', v: Misskey.entities.DriveFile['id']): void;
 | 
			
		||||
	(ev: 'removeFolder', v: Misskey.entities.DriveFolder['id']): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const hover = ref(false);
 | 
			
		||||
@@ -45,22 +45,22 @@ function onMouseout() {
 | 
			
		||||
	hover.value = false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onDragover(e: DragEvent) {
 | 
			
		||||
	if (!e.dataTransfer) return;
 | 
			
		||||
function onDragover(ev: DragEvent) {
 | 
			
		||||
	if (!ev.dataTransfer) return;
 | 
			
		||||
 | 
			
		||||
	// このフォルダがルートかつカレントディレクトリならドロップ禁止
 | 
			
		||||
	if (props.folder == null && props.parentFolder == null) {
 | 
			
		||||
		e.dataTransfer.dropEffect = 'none';
 | 
			
		||||
		ev.dataTransfer.dropEffect = 'none';
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const isFile = e.dataTransfer.items[0].kind == 'file';
 | 
			
		||||
	const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
 | 
			
		||||
	const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
 | 
			
		||||
	const isFile = ev.dataTransfer.items[0].kind === 'file';
 | 
			
		||||
	const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_;
 | 
			
		||||
	const isDriveFolder = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FOLDER_;
 | 
			
		||||
 | 
			
		||||
	if (isFile || isDriveFile || isDriveFolder) {
 | 
			
		||||
		e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
 | 
			
		||||
		ev.dataTransfer.dropEffect = ev.dataTransfer.effectAllowed === 'all' ? 'copy' : 'move';
 | 
			
		||||
	} else {
 | 
			
		||||
		e.dataTransfer.dropEffect = 'none';
 | 
			
		||||
		ev.dataTransfer.dropEffect = 'none';
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return false;
 | 
			
		||||
@@ -74,22 +74,22 @@ function onDragleave() {
 | 
			
		||||
	if (props.folder || props.parentFolder) draghover.value = false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onDrop(e: DragEvent) {
 | 
			
		||||
function onDrop(ev: DragEvent) {
 | 
			
		||||
	draghover.value = false;
 | 
			
		||||
 | 
			
		||||
	if (!e.dataTransfer) return;
 | 
			
		||||
	if (!ev.dataTransfer) return;
 | 
			
		||||
 | 
			
		||||
	// ファイルだったら
 | 
			
		||||
	if (e.dataTransfer.files.length > 0) {
 | 
			
		||||
		for (const file of Array.from(e.dataTransfer.files)) {
 | 
			
		||||
	if (ev.dataTransfer.files.length > 0) {
 | 
			
		||||
		for (const file of Array.from(ev.dataTransfer.files)) {
 | 
			
		||||
			emit('upload', file, props.folder);
 | 
			
		||||
		}
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	//#region ドライブのファイル
 | 
			
		||||
	const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
 | 
			
		||||
	if (driveFile != null && driveFile != '') {
 | 
			
		||||
	const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
 | 
			
		||||
	if (driveFile != null && driveFile !== '') {
 | 
			
		||||
		const file = JSON.parse(driveFile);
 | 
			
		||||
		emit('removeFile', file.id);
 | 
			
		||||
		os.api('drive/files/update', {
 | 
			
		||||
@@ -100,11 +100,11 @@ function onDrop(e: DragEvent) {
 | 
			
		||||
	//#endregion
 | 
			
		||||
 | 
			
		||||
	//#region ドライブのフォルダ
 | 
			
		||||
	const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
 | 
			
		||||
	if (driveFolder != null && driveFolder != '') {
 | 
			
		||||
	const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
 | 
			
		||||
	if (driveFolder != null && driveFolder !== '') {
 | 
			
		||||
		const folder = JSON.parse(driveFolder);
 | 
			
		||||
		// 移動先が自分自身ならreject
 | 
			
		||||
		if (props.folder && folder.id == props.folder.id) return;
 | 
			
		||||
		if (props.folder && folder.id === props.folder.id) return;
 | 
			
		||||
		emit('removeFolder', folder.id);
 | 
			
		||||
		os.api('drive/folders/update', {
 | 
			
		||||
			folderId: folder.id,
 | 
			
		||||
 
 | 
			
		||||
@@ -110,11 +110,11 @@ const props = withDefaults(defineProps<{
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(e: 'selected', v: Misskey.entities.DriveFile | Misskey.entities.DriveFolder): void;
 | 
			
		||||
	(e: 'change-selection', v: Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]): void;
 | 
			
		||||
	(e: 'move-root'): void;
 | 
			
		||||
	(e: 'cd', v: Misskey.entities.DriveFolder | null): void;
 | 
			
		||||
	(e: 'open-folder', v: Misskey.entities.DriveFolder): void;
 | 
			
		||||
	(ev: 'selected', v: Misskey.entities.DriveFile | Misskey.entities.DriveFolder): void;
 | 
			
		||||
	(ev: 'change-selection', v: Misskey.entities.DriveFile[] | Misskey.entities.DriveFolder[]): void;
 | 
			
		||||
	(ev: 'move-root'): void;
 | 
			
		||||
	(ev: 'cd', v: Misskey.entities.DriveFolder | null): void;
 | 
			
		||||
	(ev: 'open-folder', v: Misskey.entities.DriveFolder): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const loadMoreFiles = ref<InstanceType<typeof MkButton>>();
 | 
			
		||||
@@ -153,7 +153,7 @@ function onStreamDriveFileCreated(file: Misskey.entities.DriveFile) {
 | 
			
		||||
 | 
			
		||||
function onStreamDriveFileUpdated(file: Misskey.entities.DriveFile) {
 | 
			
		||||
	const current = folder.value ? folder.value.id : null;
 | 
			
		||||
	if (current != file.folderId) {
 | 
			
		||||
	if (current !== file.folderId) {
 | 
			
		||||
		removeFile(file);
 | 
			
		||||
	} else {
 | 
			
		||||
		addFile(file, true);
 | 
			
		||||
@@ -170,7 +170,7 @@ function onStreamDriveFolderCreated(createdFolder: Misskey.entities.DriveFolder)
 | 
			
		||||
 | 
			
		||||
function onStreamDriveFolderUpdated(updatedFolder: Misskey.entities.DriveFolder) {
 | 
			
		||||
	const current = folder.value ? folder.value.id : null;
 | 
			
		||||
	if (current != updatedFolder.parentId) {
 | 
			
		||||
	if (current !== updatedFolder.parentId) {
 | 
			
		||||
		removeFolder(updatedFolder);
 | 
			
		||||
	} else {
 | 
			
		||||
		addFolder(updatedFolder, true);
 | 
			
		||||
@@ -181,23 +181,23 @@ function onStreamDriveFolderDeleted(folderId: string) {
 | 
			
		||||
	removeFolder(folderId);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onDragover(e: DragEvent): any {
 | 
			
		||||
	if (!e.dataTransfer) return;
 | 
			
		||||
function onDragover(ev: DragEvent): any {
 | 
			
		||||
	if (!ev.dataTransfer) return;
 | 
			
		||||
 | 
			
		||||
	// ドラッグ元が自分自身の所有するアイテムだったら
 | 
			
		||||
	if (isDragSource.value) {
 | 
			
		||||
		// 自分自身にはドロップさせない
 | 
			
		||||
		e.dataTransfer.dropEffect = 'none';
 | 
			
		||||
		ev.dataTransfer.dropEffect = 'none';
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const isFile = e.dataTransfer.items[0].kind == 'file';
 | 
			
		||||
	const isDriveFile = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FILE_;
 | 
			
		||||
	const isDriveFolder = e.dataTransfer.types[0] == _DATA_TRANSFER_DRIVE_FOLDER_;
 | 
			
		||||
	const isFile = ev.dataTransfer.items[0].kind === 'file';
 | 
			
		||||
	const isDriveFile = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FILE_;
 | 
			
		||||
	const isDriveFolder = ev.dataTransfer.types[0] === _DATA_TRANSFER_DRIVE_FOLDER_;
 | 
			
		||||
	if (isFile || isDriveFile || isDriveFolder) {
 | 
			
		||||
		e.dataTransfer.dropEffect = e.dataTransfer.effectAllowed == 'all' ? 'copy' : 'move';
 | 
			
		||||
		ev.dataTransfer.dropEffect = ev.dataTransfer.effectAllowed === 'all' ? 'copy' : 'move';
 | 
			
		||||
	} else {
 | 
			
		||||
		e.dataTransfer.dropEffect = 'none';
 | 
			
		||||
		ev.dataTransfer.dropEffect = 'none';
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return false;
 | 
			
		||||
@@ -211,24 +211,24 @@ function onDragleave() {
 | 
			
		||||
	draghover.value = false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onDrop(e: DragEvent): any {
 | 
			
		||||
function onDrop(ev: DragEvent): any {
 | 
			
		||||
	draghover.value = false;
 | 
			
		||||
 | 
			
		||||
	if (!e.dataTransfer) return;
 | 
			
		||||
	if (!ev.dataTransfer) return;
 | 
			
		||||
 | 
			
		||||
	// ドロップされてきたものがファイルだったら
 | 
			
		||||
	if (e.dataTransfer.files.length > 0) {
 | 
			
		||||
		for (const file of Array.from(e.dataTransfer.files)) {
 | 
			
		||||
	if (ev.dataTransfer.files.length > 0) {
 | 
			
		||||
		for (const file of Array.from(ev.dataTransfer.files)) {
 | 
			
		||||
			upload(file, folder.value);
 | 
			
		||||
		}
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	//#region ドライブのファイル
 | 
			
		||||
	const driveFile = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
 | 
			
		||||
	if (driveFile != null && driveFile != '') {
 | 
			
		||||
	const driveFile = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FILE_);
 | 
			
		||||
	if (driveFile != null && driveFile !== '') {
 | 
			
		||||
		const file = JSON.parse(driveFile);
 | 
			
		||||
		if (files.value.some(f => f.id == file.id)) return;
 | 
			
		||||
		if (files.value.some(f => f.id === file.id)) return;
 | 
			
		||||
		removeFile(file.id);
 | 
			
		||||
		os.api('drive/files/update', {
 | 
			
		||||
			fileId: file.id,
 | 
			
		||||
@@ -238,13 +238,13 @@ function onDrop(e: DragEvent): any {
 | 
			
		||||
	//#endregion
 | 
			
		||||
 | 
			
		||||
	//#region ドライブのフォルダ
 | 
			
		||||
	const driveFolder = e.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
 | 
			
		||||
	if (driveFolder != null && driveFolder != '') {
 | 
			
		||||
	const driveFolder = ev.dataTransfer.getData(_DATA_TRANSFER_DRIVE_FOLDER_);
 | 
			
		||||
	if (driveFolder != null && driveFolder !== '') {
 | 
			
		||||
		const droppedFolder = JSON.parse(driveFolder);
 | 
			
		||||
 | 
			
		||||
		// 移動先が自分自身ならreject
 | 
			
		||||
		if (folder.value && droppedFolder.id == folder.value.id) return false;
 | 
			
		||||
		if (folders.value.some(f => f.id == droppedFolder.id)) return false;
 | 
			
		||||
		if (folder.value && droppedFolder.id === folder.value.id) return false;
 | 
			
		||||
		if (folders.value.some(f => f.id === droppedFolder.id)) return false;
 | 
			
		||||
		removeFolder(droppedFolder.id);
 | 
			
		||||
		os.api('drive/folders/update', {
 | 
			
		||||
			folderId: droppedFolder.id,
 | 
			
		||||
@@ -357,16 +357,16 @@ function onChangeFileInput() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function upload(file: File, folderToUpload?: Misskey.entities.DriveFolder | null) {
 | 
			
		||||
	uploadFile(file, (folderToUpload && typeof folderToUpload == 'object') ? folderToUpload.id : null, undefined, keepOriginal.value).then(res => {
 | 
			
		||||
	uploadFile(file, (folderToUpload && typeof folderToUpload === 'object') ? folderToUpload.id : null, undefined, keepOriginal.value).then(res => {
 | 
			
		||||
		addFile(res, true);
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function chooseFile(file: Misskey.entities.DriveFile) {
 | 
			
		||||
	const isAlreadySelected = selectedFiles.value.some(f => f.id == file.id);
 | 
			
		||||
	const isAlreadySelected = selectedFiles.value.some(f => f.id === file.id);
 | 
			
		||||
	if (props.multiple) {
 | 
			
		||||
		if (isAlreadySelected) {
 | 
			
		||||
			selectedFiles.value = selectedFiles.value.filter(f => f.id != file.id);
 | 
			
		||||
			selectedFiles.value = selectedFiles.value.filter(f => f.id !== file.id);
 | 
			
		||||
		} else {
 | 
			
		||||
			selectedFiles.value.push(file);
 | 
			
		||||
		}
 | 
			
		||||
@@ -382,10 +382,10 @@ function chooseFile(file: Misskey.entities.DriveFile) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function chooseFolder(folderToChoose: Misskey.entities.DriveFolder) {
 | 
			
		||||
	const isAlreadySelected = selectedFolders.value.some(f => f.id == folderToChoose.id);
 | 
			
		||||
	const isAlreadySelected = selectedFolders.value.some(f => f.id === folderToChoose.id);
 | 
			
		||||
	if (props.multiple) {
 | 
			
		||||
		if (isAlreadySelected) {
 | 
			
		||||
			selectedFolders.value = selectedFolders.value.filter(f => f.id != folderToChoose.id);
 | 
			
		||||
			selectedFolders.value = selectedFolders.value.filter(f => f.id !== folderToChoose.id);
 | 
			
		||||
		} else {
 | 
			
		||||
			selectedFolders.value.push(folderToChoose);
 | 
			
		||||
		}
 | 
			
		||||
@@ -404,7 +404,7 @@ function move(target?: Misskey.entities.DriveFolder) {
 | 
			
		||||
	if (!target) {
 | 
			
		||||
		goRoot();
 | 
			
		||||
		return;
 | 
			
		||||
	} else if (typeof target == 'object') {
 | 
			
		||||
	} else if (typeof target === 'object') {
 | 
			
		||||
		target = target.id;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -430,9 +430,9 @@ function move(target?: Misskey.entities.DriveFolder) {
 | 
			
		||||
 | 
			
		||||
function addFolder(folderToAdd: Misskey.entities.DriveFolder, unshift = false) {
 | 
			
		||||
	const current = folder.value ? folder.value.id : null;
 | 
			
		||||
	if (current != folderToAdd.parentId) return;
 | 
			
		||||
	if (current !== folderToAdd.parentId) return;
 | 
			
		||||
 | 
			
		||||
	if (folders.value.some(f => f.id == folderToAdd.id)) {
 | 
			
		||||
	if (folders.value.some(f => f.id === folderToAdd.id)) {
 | 
			
		||||
		const exist = folders.value.map(f => f.id).indexOf(folderToAdd.id);
 | 
			
		||||
		folders.value[exist] = folderToAdd;
 | 
			
		||||
		return;
 | 
			
		||||
@@ -447,9 +447,9 @@ function addFolder(folderToAdd: Misskey.entities.DriveFolder, unshift = false) {
 | 
			
		||||
 | 
			
		||||
function addFile(fileToAdd: Misskey.entities.DriveFile, unshift = false) {
 | 
			
		||||
	const current = folder.value ? folder.value.id : null;
 | 
			
		||||
	if (current != fileToAdd.folderId) return;
 | 
			
		||||
	if (current !== fileToAdd.folderId) return;
 | 
			
		||||
 | 
			
		||||
	if (files.value.some(f => f.id == fileToAdd.id)) {
 | 
			
		||||
	if (files.value.some(f => f.id === fileToAdd.id)) {
 | 
			
		||||
		const exist = files.value.map(f => f.id).indexOf(fileToAdd.id);
 | 
			
		||||
		files.value[exist] = fileToAdd;
 | 
			
		||||
		return;
 | 
			
		||||
@@ -464,12 +464,12 @@ function addFile(fileToAdd: Misskey.entities.DriveFile, unshift = false) {
 | 
			
		||||
 | 
			
		||||
function removeFolder(folderToRemove: Misskey.entities.DriveFolder | string) {
 | 
			
		||||
	const folderIdToRemove = typeof folderToRemove === 'object' ? folderToRemove.id : folderToRemove;
 | 
			
		||||
	folders.value = folders.value.filter(f => f.id != folderIdToRemove);
 | 
			
		||||
	folders.value = folders.value.filter(f => f.id !== folderIdToRemove);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function removeFile(file: Misskey.entities.DriveFile | string) {
 | 
			
		||||
	const fileId = typeof file === 'object' ? file.id : file;
 | 
			
		||||
	files.value = files.value.filter(f => f.id != fileId);
 | 
			
		||||
	files.value = files.value.filter(f => f.id !== fileId);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function appendFile(file: Misskey.entities.DriveFile) {
 | 
			
		||||
@@ -512,7 +512,7 @@ async function fetch() {
 | 
			
		||||
		folderId: folder.value ? folder.value.id : null,
 | 
			
		||||
		limit: foldersMax + 1
 | 
			
		||||
	}).then(fetchedFolders => {
 | 
			
		||||
		if (fetchedFolders.length == foldersMax + 1) {
 | 
			
		||||
		if (fetchedFolders.length === foldersMax + 1) {
 | 
			
		||||
			moreFolders.value = true;
 | 
			
		||||
			fetchedFolders.pop();
 | 
			
		||||
		}
 | 
			
		||||
@@ -524,7 +524,7 @@ async function fetch() {
 | 
			
		||||
		type: props.type,
 | 
			
		||||
		limit: filesMax + 1
 | 
			
		||||
	}).then(fetchedFiles => {
 | 
			
		||||
		if (fetchedFiles.length == filesMax + 1) {
 | 
			
		||||
		if (fetchedFiles.length === filesMax + 1) {
 | 
			
		||||
			moreFiles.value = true;
 | 
			
		||||
			fetchedFiles.pop();
 | 
			
		||||
		}
 | 
			
		||||
@@ -551,7 +551,7 @@ function fetchMoreFiles() {
 | 
			
		||||
		untilId: files.value[files.value.length - 1].id,
 | 
			
		||||
		limit: max + 1
 | 
			
		||||
	}).then(files => {
 | 
			
		||||
		if (files.length == max + 1) {
 | 
			
		||||
		if (files.length === max + 1) {
 | 
			
		||||
			moreFiles.value = true;
 | 
			
		||||
			files.pop();
 | 
			
		||||
		} else {
 | 
			
		||||
 
 | 
			
		||||
@@ -25,8 +25,8 @@ withDefaults(defineProps<{
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(e: 'chosen', v: any): void;
 | 
			
		||||
	(e: 'closed'): void;
 | 
			
		||||
	(ev: 'chosen', v: any): void;
 | 
			
		||||
	(ev: 'closed'): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
function chosen(emoji: any) {
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ const props = defineProps<{
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(e: 'chosen', v: string, ev: MouseEvent): void;
 | 
			
		||||
	(ev: 'chosen', v: string, event: MouseEvent): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const shown = ref(!!props.initialShown);
 | 
			
		||||
 
 | 
			
		||||
@@ -61,7 +61,7 @@
 | 
			
		||||
		</div>
 | 
			
		||||
		<div>
 | 
			
		||||
			<header class="_acrylic">{{ i18n.ts.emoji }}</header>
 | 
			
		||||
			<XSection v-for="category in categories" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)" @chosen="chosen">{{ category }}</XSection>
 | 
			
		||||
			<XSection v-for="category in categories" :key="category" :emojis="emojilist.filter(e => e.category === category).map(e => e.char)" @chosen="chosen">{{ category }}</XSection>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div class="tabs">
 | 
			
		||||
@@ -97,7 +97,7 @@ const props = withDefaults(defineProps<{
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(e: 'chosen', v: string): void;
 | 
			
		||||
	(ev: 'chosen', v: string): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const search = ref<HTMLInputElement>();
 | 
			
		||||
@@ -138,7 +138,7 @@ watch(q, () => {
 | 
			
		||||
		const emojis = customEmojis;
 | 
			
		||||
		const matches = new Set<Misskey.entities.CustomEmoji>();
 | 
			
		||||
 | 
			
		||||
		const exactMatch = emojis.find(e => e.name === newQ);
 | 
			
		||||
		const exactMatch = emojis.find(emoji => emoji.name === newQ);
 | 
			
		||||
		if (exactMatch) matches.add(exactMatch);
 | 
			
		||||
 | 
			
		||||
		if (newQ.includes(' ')) { // AND検索
 | 
			
		||||
@@ -201,7 +201,7 @@ watch(q, () => {
 | 
			
		||||
		const emojis = emojilist;
 | 
			
		||||
		const matches = new Set<UnicodeEmojiDef>();
 | 
			
		||||
 | 
			
		||||
		const exactMatch = emojis.find(e => e.name === newQ);
 | 
			
		||||
		const exactMatch = emojis.find(emoji => emoji.name === newQ);
 | 
			
		||||
		if (exactMatch) matches.add(exactMatch);
 | 
			
		||||
 | 
			
		||||
		if (newQ.includes(' ')) { // AND検索
 | 
			
		||||
@@ -295,7 +295,7 @@ function chosen(emoji: any, ev?: MouseEvent) {
 | 
			
		||||
	// 最近使った絵文字更新
 | 
			
		||||
	if (!pinned.value.includes(key)) {
 | 
			
		||||
		let recents = defaultStore.state.recentlyUsedEmojis;
 | 
			
		||||
		recents = recents.filter((e: any) => e !== key);
 | 
			
		||||
		recents = recents.filter((emoji: any) => emoji !== key);
 | 
			
		||||
		recents.unshift(key);
 | 
			
		||||
		defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32));
 | 
			
		||||
	}
 | 
			
		||||
@@ -313,12 +313,12 @@ function done(query?: any): boolean | void {
 | 
			
		||||
	if (query == null || typeof query !== 'string') return;
 | 
			
		||||
 | 
			
		||||
	const q2 = query.replace(/:/g, '');
 | 
			
		||||
	const exactMatchCustom = customEmojis.find(e => e.name === q2);
 | 
			
		||||
	const exactMatchCustom = customEmojis.find(emoji => emoji.name === q2);
 | 
			
		||||
	if (exactMatchCustom) {
 | 
			
		||||
		chosen(exactMatchCustom);
 | 
			
		||||
		return true;
 | 
			
		||||
	}
 | 
			
		||||
	const exactMatchUnicode = emojilist.find(e => e.char === q2 || e.name === q2);
 | 
			
		||||
	const exactMatchUnicode = emojilist.find(emoji => emoji.char === q2 || emoji.name === q2);
 | 
			
		||||
	if (exactMatchUnicode) {
 | 
			
		||||
		chosen(exactMatchUnicode);
 | 
			
		||||
		return true;
 | 
			
		||||
 
 | 
			
		||||
@@ -58,7 +58,7 @@ if (props.user.isFollowing == null) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onFollowChange(user: Misskey.entities.UserDetailed) {
 | 
			
		||||
	if (user.id == props.user.id) {
 | 
			
		||||
	if (user.id === props.user.id) {
 | 
			
		||||
		isFollowing.value = user.isFollowing;
 | 
			
		||||
		hasPendingFollowRequestFromYou.value = user.hasPendingFollowRequestFromYou;
 | 
			
		||||
	}
 | 
			
		||||
@@ -96,8 +96,8 @@ async function onClick() {
 | 
			
		||||
				hasPendingFollowRequestFromYou.value = true;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	} catch (e) {
 | 
			
		||||
		console.error(e);
 | 
			
		||||
	} catch (err) {
 | 
			
		||||
		console.error(err);
 | 
			
		||||
	} finally {
 | 
			
		||||
		wait.value = false;
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -41,8 +41,8 @@ import { instance } from '@/instance';
 | 
			
		||||
import { i18n } from '@/i18n';
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(e: 'done'): void;
 | 
			
		||||
	(e: 'closed'): void;
 | 
			
		||||
	(ev: 'done'): void;
 | 
			
		||||
	(ev: 'closed'): void;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
let dialog: InstanceType<typeof XModalWindow> = $ref();
 | 
			
		||||
 
 | 
			
		||||
@@ -44,7 +44,7 @@
 | 
			
		||||
					<template #label><span v-text="form[item].label || item"></span><span v-if="form[item].required === false"> ({{ $ts.optional }})</span></template>
 | 
			
		||||
					<template v-if="form[item].description" #caption>{{ form[item].description }}</template>
 | 
			
		||||
				</FormRange>
 | 
			
		||||
				<MkButton v-else-if="form[item].type === 'button'" @click="form[item].action($event, values)" class="_formBlock">
 | 
			
		||||
				<MkButton v-else-if="form[item].type === 'button'" class="_formBlock" @click="form[item].action($event, values)">
 | 
			
		||||
					<span v-text="form[item].content || item"></span>
 | 
			
		||||
				</MkButton>
 | 
			
		||||
			</template>
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { computed, defineComponent, onMounted, onUnmounted, ref, watch } from 'vue';
 | 
			
		||||
import { computed, defineAsyncComponent, defineComponent, onMounted, onUnmounted, ref, watch } from 'vue';
 | 
			
		||||
import * as os from '@/os';
 | 
			
		||||
 | 
			
		||||
export default defineComponent({
 | 
			
		||||
@@ -112,7 +112,7 @@ export default defineComponent({
 | 
			
		||||
			ev.preventDefault();
 | 
			
		||||
 | 
			
		||||
			const tooltipShowing = ref(true);
 | 
			
		||||
			os.popup(import('@/components/ui/tooltip.vue'), {
 | 
			
		||||
			os.popup(defineAsyncComponent(() => import('@/components/ui/tooltip.vue')), {
 | 
			
		||||
				showing: tooltipShowing,
 | 
			
		||||
				text: computed(() => {
 | 
			
		||||
					return props.textConverter(finalValue.value);
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user