Merge branch 'develop' into pizzax-indexeddb

This commit is contained in:
tamaina 2022-05-28 01:31:23 +09:00
commit fa99d9c6fe
340 changed files with 9589 additions and 9298 deletions

View file

@ -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.'
}
]
},
};

View file

@ -5,6 +5,6 @@
"loader=./test/loader.js"
],
"slow": 1000,
"timeout": 35000,
"timeout": 3000,
"exit": true
}

View 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"`),
]);
}
}

View file

@ -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.
}
}

View file

@ -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"
}

View file

@ -1,4 +1,4 @@
declare module 'http-signature' {
declare module '@peertube/http-signature' {
import { IncomingMessage, ClientRequest } from 'node:http';
interface ISignature {

View file

@ -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();
});

View file

@ -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;

View file

@ -80,6 +80,7 @@ export type Mixin = {
authUrl: string;
driveUrl: string;
userAgent: string;
clientEntry: string;
};
export type Config = Source & Mixin;

View file

@ -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();
}
}

View file

@ -48,6 +48,7 @@ export class Cache<T> {
// Cache MISS
const value = await fetcher();
this.set(key, value);
return value;
}

View file

@ -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]);
});
});
}

View file

@ -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;

View file

@ -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;

View file

@ -23,7 +23,7 @@ export class AuthSession {
...id(),
nullable: true,
})
public userId: User['id'];
public userId: User['id'] | null;
@ManyToOne(type => User, {
onDelete: 'CASCADE',

View file

@ -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;

View file

@ -79,7 +79,6 @@ export class DriveFile {
})
public properties: { width?: number; height?: number; orientation?: number; avgColor?: string };
@Index()
@Column('boolean')
public storedInternal: boolean;

View file

@ -36,6 +36,7 @@ export class Emoji {
@Column('varchar', {
length: 512,
default: '',
})
public publicUrl: string;

View file

@ -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;

View file

@ -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;

View file

@ -17,7 +17,6 @@ export class Muting {
@Index()
@Column('timestamp with time zone', {
nullable: true,
default: null,
})
public expiresAt: Date | null;

View file

@ -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;

View file

@ -192,6 +192,7 @@ export class UserProfile {
@Column('jsonb', {
default: [],
comment: 'List of instances muted by the user.',
})
public mutedInstances: string[];

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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';

View file

@ -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();
}

View file

@ -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 },

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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}`);

View file

@ -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';

View file

@ -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 */

View file

@ -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);

View file

@ -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}`;
}
};

View file

@ -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';

View file

@ -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';

View file

@ -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 },

View file

@ -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,

View file

@ -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;

View file

@ -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 });

View file

@ -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,
},
},
},

View file

@ -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'),
});

View file

@ -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);

View file

@ -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);
});

View file

@ -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

View file

@ -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,
},
}));

View file

@ -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;
}

View file

@ -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

View file

@ -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}`,
} });

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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);

View file

@ -1,7 +0,0 @@
{
"env": {
"node": true,
"mocha": true,
"commonjs": true
}
}

View file

@ -0,0 +1,11 @@
module.exports = {
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
extends: ['../.eslintrc.cjs'],
env: {
node: true,
mocha: true,
},
};

View file

@ -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);
});
});

View file

@ -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 });

View file

@ -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
});

View file

@ -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);

View file

@ -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],
},
});
}));

View file

@ -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,
}]);
});
});

View file

@ -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) {

View file

@ -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,

View file

@ -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 };

View file

@ -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',
};
}

View file

@ -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);

View file

@ -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);
}));
});

View file

@ -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');
});

View file

@ -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(() => {

View file

@ -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);

View file

@ -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
View file

@ -0,0 +1,5 @@
import { Theme } from '../src/scripts/theme';
declare module '@/themes/*.json5' {
export = Theme;
}

View file

@ -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"
}

View file

@ -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);

View file

@ -37,7 +37,7 @@ const props = defineProps<{
}>();
const emit = defineEmits<{
(e: 'closed'): void;
(ev: 'closed'): void;
}>();
const window = ref<InstanceType<typeof XWindow>>();

View file

@ -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,

View file

@ -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;

View file

@ -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

View file

@ -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(() => {

View file

@ -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();
}
}

View file

@ -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';
});

View file

@ -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>>();

View file

@ -24,6 +24,6 @@ defineProps<{
}>();
const emit = defineEmits<{
(e: 'closed'): void;
(ev: 'closed'): void;
}>();
</script>

View file

@ -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');

View file

@ -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');

View file

@ -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,

View file

@ -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 {

View file

@ -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) {

View file

@ -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);

View file

@ -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;

View file

@ -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;
}

View file

@ -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();

View file

@ -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>

View file

@ -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