Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop

This commit is contained in:
tamaina 2022-05-30 05:53:40 +00:00
commit 465531d56c
266 changed files with 7852 additions and 5442 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,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

@ -46,7 +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_\//, '');
mixin.clientEntry = clientManifest['src/init.ts'];
if (!config.redis.prefix) config.redis.prefix = mixin.host;

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

@ -144,13 +144,7 @@ export const NoteRepository = db.getRepository(Note).extend({
return true;
} else {
// 指定されているかどうか
const specified = note.visibleUserIds.some((id: any) => meId === id);
if (specified) {
return true;
} else {
return false;
}
return note.visibleUserIds.some((id: any) => meId === id);
}
}
@ -168,16 +162,25 @@ 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.count({
where: {
followeeId: note.userId,
followerId: meId,
},
take: 1,
}),
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 > 0 || (note.userHost != null && user.host != null);
}
}

View file

@ -61,47 +61,58 @@ export const UserRepository = db.getRepository(User).extend({
//#endregion
async getRelation(me: User['id'], target: User['id']) {
const [following1, following2, followReq1, followReq2, toBlocking, fromBlocked, mute] = await Promise.all([
Followings.findOneBy({
followerId: me,
followeeId: target,
}),
Followings.findOneBy({
followerId: target,
followeeId: me,
}),
FollowRequests.findOneBy({
followerId: me,
followeeId: target,
}),
FollowRequests.findOneBy({
followerId: target,
followeeId: me,
}),
Blockings.findOneBy({
blockerId: me,
blockeeId: target,
}),
Blockings.findOneBy({
blockerId: target,
blockeeId: me,
}),
Mutings.findOneBy({
muterId: me,
muteeId: target,
}),
]);
return {
return awaitAll({
id: target,
isFollowing: following1 != null,
hasPendingFollowRequestFromYou: followReq1 != null,
hasPendingFollowRequestToYou: followReq2 != null,
isFollowed: following2 != null,
isBlocking: toBlocking != null,
isBlocked: fromBlocked != null,
isMuted: mute != null,
};
isFollowing: Followings.count({
where: {
followerId: me,
followeeId: target,
},
take: 1,
}).then(n => n > 0),
isFollowed: Followings.count({
where: {
followerId: target,
followeeId: me,
},
take: 1,
}).then(n => n > 0),
hasPendingFollowRequestFromYou: FollowRequests.count({
where: {
followerId: me,
followeeId: target,
},
take: 1,
}).then(n => n > 0),
hasPendingFollowRequestToYou: FollowRequests.count({
where: {
followerId: target,
followeeId: me,
},
take: 1,
}).then(n => n > 0),
isBlocking: Blockings.count({
where: {
blockerId: me,
blockeeId: target,
},
take: 1,
}).then(n => n > 0),
isBlocked: Blockings.count({
where: {
blockerId: target,
blockeeId: me,
},
take: 1,
}).then(n => n > 0),
isMuted: Mutings.count({
where: {
muterId: me,
muteeId: target,
},
take: 1,
}).then(n => n > 0),
});
},
async getHasUnreadMessagingMessage(userId: User['id']): Promise<boolean> {

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 { createTemp, 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

@ -3,9 +3,9 @@ import promiseLimit from 'promise-limit';
import config from '@/config/index.js';
import Resolver from '../resolver.js';
import post from '@/services/note/create.js';
import { resolvePerson, updatePerson } from './person.js';
import { resolvePerson } from './person.js';
import { resolveImage } from './image.js';
import { CacheableRemoteUser, IRemoteUser } from '@/models/entities/user.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import { htmlToMfm } from '../misc/html-to-mfm.js';
import { extractApHashtags } from './tag.js';
import { unique, toArray, toSingle } from '@/prelude/array.js';
@ -15,7 +15,7 @@ import { apLogger } from '../logger.js';
import { DriveFile } from '@/models/entities/drive-file.js';
import { deliverQuestionUpdate } from '@/services/note/polls/update.js';
import { extractDbHost, toPuny } from '@/misc/convert-host.js';
import { Emojis, Polls, MessagingMessages, Users } from '@/models/index.js';
import { Emojis, Polls, MessagingMessages } from '@/models/index.js';
import { Note } from '@/models/entities/note.js';
import { IObject, getOneApId, getApId, getOneApHrefNullable, validPost, IPost, isEmoji, getApType } from '../type.js';
import { Emoji } from '@/models/entities/emoji.js';

View file

@ -8,7 +8,7 @@ import { User } from '@/models/entities/user.js';
export const renderActivity = (x: any): IActivity | null => {
if (x == null) return null;
if (x !== null && typeof x === 'object' && x.id == null) {
if (typeof x === 'object' && x.id == null) {
x.id = `${config.url}/${uuid()}`;
}

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

@ -2,10 +2,11 @@ import Koa from 'koa';
import { performance } from 'perf_hooks';
import { limiter } from './limiter.js';
import { CacheableLocalUser, User } from '@/models/entities/user.js';
import endpoints, { IEndpoint } from './endpoints.js';
import endpoints, { IEndpointMeta } from './endpoints.js';
import { ApiError } from './error.js';
import { apiLogger } from './logger.js';
import { AccessToken } from '@/models/entities/access-token.js';
import IPCIDR from 'ip-cidr';
const accessDenied = {
message: 'Access denied.',
@ -15,6 +16,7 @@ const accessDenied = {
export default async (endpoint: string, user: CacheableLocalUser | null | undefined, token: AccessToken | null | undefined, data: any, ctx?: Koa.Context) => {
const isSecure = user != null && token == null;
const isModerator = user != null && (user.isModerator || user.isAdmin);
const ep = endpoints.find(e => e.name === endpoint);
@ -31,6 +33,37 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
throw new ApiError(accessDenied);
}
if (ep.meta.requireCredential && ep.meta.limit && !isModerator) {
// koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app.
let limitActor: string;
if (user) {
limitActor = user.id;
} else {
// because a single person may control many IPv6 addresses,
// only a /64 subnet prefix of any IP will be taken into account.
// (this means for IPv4 the entire address is used)
const ip = IPCIDR.createAddress(ctx.ip).mask(64);
limitActor = 'ip-' + parseInt(ip, 2).toString(36);
}
const limit = Object.assign({}, ep.meta.limit);
if (!limit.key) {
limit.key = ep.name;
}
// Rate limit
await limiter(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor).catch(e => {
throw new ApiError({
message: 'Rate limit exceeded. Please try again later.',
code: 'RATE_LIMIT_EXCEEDED',
id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
httpStatusCode: 429,
});
});
}
if (ep.meta.requireCredential && user == null) {
throw new ApiError({
message: 'Credential required.',
@ -53,7 +86,7 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
throw new ApiError(accessDenied, { reason: 'You are not the admin.' });
}
if (ep.meta.requireModerator && !user!.isAdmin && !user!.isModerator) {
if (ep.meta.requireModerator && !isModerator) {
throw new ApiError(accessDenied, { reason: 'You are not a moderator.' });
}
@ -65,18 +98,6 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
});
}
if (ep.meta.requireCredential && ep.meta.limit && !user!.isAdmin && !user!.isModerator) {
// Rate limit
await limiter(ep as IEndpoint & { meta: { limit: NonNullable<IEndpoint['meta']['limit']> } }, user!).catch(e => {
throw new ApiError({
message: 'Rate limit exceeded. Please try again later.',
code: 'RATE_LIMIT_EXCEEDED',
id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
httpStatusCode: 429,
});
});
}
// Cast non JSON input
if (ep.meta.requireFile && ep.params.properties) {
for (const k of Object.keys(ep.params.properties)) {

View file

@ -654,7 +654,6 @@ export interface IEndpointMeta {
/**
*
*
* withCredential false
*/
readonly limit?: {

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

@ -134,7 +134,7 @@ export const paramDef = {
{
// (re)note with text, files and poll are optional
properties: {
text: { type: 'string', maxLength: MAX_NOTE_TEXT_LENGTH, nullable: false },
text: { type: 'string', minLength: 1, maxLength: MAX_NOTE_TEXT_LENGTH, nullable: false },
},
required: ['text'],
},
@ -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

@ -1,25 +1,17 @@
import Limiter from 'ratelimiter';
import { redisClient } from '../../db/redis.js';
import { IEndpoint } from './endpoints.js';
import * as Acct from '@/misc/acct.js';
import { IEndpointMeta } from './endpoints.js';
import { CacheableLocalUser, User } from '@/models/entities/user.js';
import Logger from '@/services/logger.js';
const logger = new Logger('limiter');
export const limiter = (endpoint: IEndpoint & { meta: { limit: NonNullable<IEndpoint['meta']['limit']> } }, user: CacheableLocalUser) => new Promise<void>((ok, reject) => {
const limitation = endpoint.meta.limit;
const key = Object.prototype.hasOwnProperty.call(limitation, 'key')
? limitation.key
: endpoint.name;
const hasShortTermLimit =
Object.prototype.hasOwnProperty.call(limitation, 'minInterval');
export const limiter = (limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string) => new Promise<void>((ok, reject) => {
const hasShortTermLimit = typeof limitation.minInterval === 'number';
const hasLongTermLimit =
Object.prototype.hasOwnProperty.call(limitation, 'duration') &&
Object.prototype.hasOwnProperty.call(limitation, 'max');
typeof limitation.duration === 'number' &&
typeof limitation.max === 'number';
if (hasShortTermLimit) {
min();
@ -32,7 +24,7 @@ export const limiter = (endpoint: IEndpoint & { meta: { limit: NonNullable<IEndp
// Short-term limit
function min(): void {
const minIntervalLimiter = new Limiter({
id: `${user.id}:${key}:min`,
id: `${actor}:${limitation.key}:min`,
duration: limitation.minInterval,
max: 1,
db: redisClient,
@ -43,7 +35,7 @@ export const limiter = (endpoint: IEndpoint & { meta: { limit: NonNullable<IEndp
return reject('ERR');
}
logger.debug(`@${Acct.toString(user)} ${endpoint.name} min remaining: ${info.remaining}`);
logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`);
if (info.remaining === 0) {
reject('BRIEF_REQUEST_INTERVAL');
@ -60,7 +52,7 @@ export const limiter = (endpoint: IEndpoint & { meta: { limit: NonNullable<IEndp
// Long term limit
function max(): void {
const limiter = new Limiter({
id: `${user.id}:${key}`,
id: `${actor}:${limitation.key}`,
duration: limitation.duration,
max: limitation.max,
db: redisClient,
@ -71,7 +63,7 @@ export const limiter = (endpoint: IEndpoint & { meta: { limit: NonNullable<IEndp
return reject('ERR');
}
logger.debug(`@${Acct.toString(user)} ${endpoint.name} max remaining: ${info.remaining}`);
logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`);
if (info.remaining === 0) {
reject('RATE_LIMIT_EXCEEDED');

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

@ -9,6 +9,7 @@ import { genId } from '@/misc/gen-id.js';
import { verifyLogin, hash } from '../2fa.js';
import { randomBytes } from 'node:crypto';
import { IsNull } from 'typeorm';
import { limiter } from '../limiter.js';
export default async (ctx: Koa.Context) => {
ctx.set('Access-Control-Allow-Origin', config.url);
@ -24,6 +25,21 @@ export default async (ctx: Koa.Context) => {
ctx.body = { error };
}
try {
// not more than 1 attempt per second and not more than 10 attempts per hour
await limiter({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, ctx.ip);
} catch (err) {
ctx.status = 429;
ctx.body = {
error: {
message: 'Too many failed attempts to sign in. Try again later.',
code: 'TOO_MANY_AUTHENTICATION_FAILURES',
id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
},
};
return;
}
if (typeof username !== 'string') {
ctx.status = 400;
return;

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

@ -54,14 +54,10 @@
//#endregion
//#region Script
const salt = localStorage.getItem('salt')
? `?salt=${localStorage.getItem('salt')}`
: '';
import(`/assets/${CLIENT_ENTRY}${salt}`)
.catch(async () => {
import(`/assets/${CLIENT_ENTRY}`)
.catch(async e => {
await checkUpdate();
renderError('APP_FETCH_FAILED');
renderError('APP_FETCH_FAILED', JSON.stringify(e));
})
//#endregion
@ -142,9 +138,6 @@
// eslint-disable-next-line no-inner-declarations
function refresh() {
// Random
localStorage.setItem('salt', Math.random().toString().substr(2, 8));
// Clear cache (service worker)
try {
navigator.serviceWorker.controller.postMessage('clear');

View file

@ -74,9 +74,9 @@ app.use(views(_dirname + '/views', {
extension: 'pug',
options: {
version: config.version,
clientEntry: () => process.env.NODE_ENV === 'production' ?
getClientEntry: () => 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_\//, ''),
JSON.parse(readFileSync(`${_dirname}/../../../../../built/_client_dist_/manifest.json`, 'utf-8'))['src/init.ts'],
config,
},
}));
@ -247,7 +247,7 @@ router.get(['/@:user', '/@:user/:sub'], async (ctx, next) => {
icon: meta.iconUrl,
themeColor: meta.themeColor,
});
ctx.set('Cache-Control', 'public, max-age=30');
ctx.set('Cache-Control', 'public, max-age=15');
} else {
// リモートユーザーなので
// モデレータがAPI経由で参照可能にするために404にはしない
@ -292,7 +292,7 @@ router.get('/notes/:note', async (ctx, next) => {
themeColor: meta.themeColor,
});
ctx.set('Cache-Control', 'public, max-age=180');
ctx.set('Cache-Control', 'public, max-age=15');
return;
}
@ -329,7 +329,7 @@ router.get('/@:user/pages/:page', async (ctx, next) => {
});
if (['public'].includes(page.visibility)) {
ctx.set('Cache-Control', 'public, max-age=180');
ctx.set('Cache-Control', 'public, max-age=15');
} else {
ctx.set('Cache-Control', 'private, max-age=0, must-revalidate');
}
@ -360,7 +360,7 @@ router.get('/clips/:clip', async (ctx, next) => {
themeColor: meta.themeColor,
});
ctx.set('Cache-Control', 'public, max-age=180');
ctx.set('Cache-Control', 'public, max-age=15');
return;
}
@ -385,7 +385,7 @@ router.get('/gallery/:post', async (ctx, next) => {
themeColor: meta.themeColor,
});
ctx.set('Cache-Control', 'public, max-age=180');
ctx.set('Cache-Control', 'public, max-age=15');
return;
}
@ -409,7 +409,7 @@ router.get('/channels/:channel', async (ctx, next) => {
themeColor: meta.themeColor,
});
ctx.set('Cache-Control', 'public, max-age=180');
ctx.set('Cache-Control', 'public, max-age=15');
return;
}
@ -468,7 +468,7 @@ router.get('(.*)', async ctx => {
icon: meta.iconUrl,
themeColor: meta.themeColor,
});
ctx.set('Cache-Control', 'public, max-age=300');
ctx.set('Cache-Control', 'public, max-age=15');
});
// Register router

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

@ -1,17 +1,23 @@
block vars
block loadClientEntry
- const clientEntry = getClientEntry();
doctype html
!= '<!--\n'
!= ' _____ _ _ \n'
!= ' | |_|___ ___| |_ ___ _ _ \n'
!= ' | | | | |_ -|_ -| \'_| -_| | |\n'
!= ' |_|_|_|_|___|___|_,_|___|_ |\n'
!= ' |___|\n'
!= ' Thank you for using Misskey!\n'
!= ' If you are reading this message... how about joining the development?\n'
!= ' https://github.com/misskey-dev/misskey'
!= '\n-->\n'
//
-
_____ _ _
| |_|___ ___| |_ ___ _ _
| | | | |_ -|_ -| \'_| -_| | |
|_|_|_|_|___|___|_,_|___|_ |
|___|
Thank you for using Misskey!
If you are reading this message... how about joining the development?
https://github.com/misskey-dev/misskey
html
@ -30,8 +36,14 @@ html
link(rel='prefetch' href='https://xn--931a.moe/assets/info.jpg')
link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg')
link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg')
link(rel='preload' href='/assets/fontawesome/css/all.css' as='style')
link(rel='stylesheet' href='/assets/fontawesome/css/all.css')
link(rel='modulepreload' href=`/assets/${clientEntry.file}`)
each href in clientEntry.css
link(rel='preload' href=`/assets/${href}` as='style')
each href in clientEntry.css
link(rel='preload' href=`/assets/${href}` as='style')
title
block title
@ -52,7 +64,7 @@ html
script.
var VERSION = "#{version}";
var CLIENT_ENTRY = "#{clientEntry()}";
var CLIENT_ENTRY = "#{clientEntry.file}";
script
include ../boot.js
@ -65,4 +77,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

@ -91,27 +91,20 @@ type ToJsonSchema<S> = {
};
export function getJsonSchema<S extends Schema>(schema: S): ToJsonSchema<Unflatten<ChartResult<S>>> {
const object = {};
for (const [k, v] of Object.entries(schema)) {
nestedProperty.set(object, k, null);
}
const jsonSchema = {
type: 'object',
properties: {} as Record<string, unknown>,
required: [],
};
function f(obj: Record<string, null | Record<string, unknown>>) {
const jsonSchema = {
type: 'object',
properties: {} as Record<string, unknown>,
required: [],
for (const k in schema) {
jsonSchema.properties[k] = {
type: 'array',
items: { type: 'number' },
};
for (const [k, v] of Object.entries(obj)) {
jsonSchema.properties[k] = v === null ? {
type: 'array',
items: { type: 'number' },
} : f(v as Record<string, null | Record<string, unknown>>);
}
return jsonSchema;
}
return f(object) as ToJsonSchema<Unflatten<ChartResult<S>>>;
return jsonSchema as ToJsonSchema<Unflatten<ChartResult<S>>>;
}
/**

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

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

View file

@ -1,68 +1,79 @@
module.exports = {
root: true,
env: {
"node": false
'node': false,
},
parser: "vue-eslint-parser",
parser: 'vue-eslint-parser',
parserOptions: {
"parser": "@typescript-eslint/parser",
'parser': '@typescript-eslint/parser',
tsconfigRootDir: __dirname,
//project: ['./tsconfig.json'],
project: ['./tsconfig.json'],
extraFileExtensions: ['.vue'],
},
extends: [
//"../shared/.eslintrc.js",
"plugin:vue/vue3-recommended"
'../shared/.eslintrc.js',
'plugin:vue/vue3-recommended',
],
rules: {
// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
// data の禁止理由: 抽象的すぎるため
// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
"id-denylist": ["error", "window", "data", "e"],
'id-denylist': ['error', 'window', 'data', 'e'],
'eqeqeq': ['error', 'always', { 'null': 'ignore' }],
"no-shadow": ["warn"],
"vue/attributes-order": ["error", {
"alphabetical": false
'no-shadow': ['warn'],
'vue/attributes-order': ['error', {
'alphabetical': false,
}],
"vue/no-use-v-if-with-v-for": ["error", {
"allowUsingIterationVar": false
'vue/no-use-v-if-with-v-for': ['error', {
'allowUsingIterationVar': false,
}],
"vue/no-ref-as-operand": "error",
"vue/no-multi-spaces": ["error", {
"ignoreProperties": false
'vue/no-ref-as-operand': 'error',
'vue/no-multi-spaces': ['error', {
'ignoreProperties': false,
}],
"vue/no-v-html": "error",
"vue/order-in-components": "error",
"vue/html-indent": ["warn", "tab", {
"attribute": 1,
"baseIndent": 0,
"closeBracket": 0,
"alignAttributesVertically": true,
"ignores": []
'vue/no-v-html': 'error',
'vue/order-in-components': 'error',
'vue/html-indent': ['warn', 'tab', {
'attribute': 1,
'baseIndent': 0,
'closeBracket': 0,
'alignAttributesVertically': true,
'ignores': [],
}],
"vue/html-closing-bracket-spacing": ["warn", {
"startTag": "never",
"endTag": "never",
"selfClosingTag": "never"
'vue/html-closing-bracket-spacing': ['warn', {
'startTag': 'never',
'endTag': 'never',
'selfClosingTag': 'never',
}],
"vue/multi-word-component-names": "warn",
"vue/require-v-for-key": "warn",
"vue/no-unused-components": "warn",
"vue/valid-v-for": "warn",
"vue/return-in-computed-property": "warn",
"vue/no-setup-props-destructure": "warn",
"vue/max-attributes-per-line": "off",
"vue/html-self-closing": "off",
"vue/singleline-html-element-content-newline": "off",
'vue/multi-word-component-names': 'warn',
'vue/require-v-for-key': 'warn',
'vue/no-unused-components': 'warn',
'vue/valid-v-for': 'warn',
'vue/return-in-computed-property': 'warn',
'vue/no-setup-props-destructure': 'warn',
'vue/max-attributes-per-line': 'off',
'vue/html-self-closing': 'off',
'vue/singleline-html-element-content-newline': 'off',
},
globals: {
"require": false,
"_DEV_": false,
"_LANGS_": false,
"_VERSION_": false,
"_ENV_": false,
"_PERF_PREFIX_": false,
"_DATA_TRANSFER_DRIVE_FILE_": false,
"_DATA_TRANSFER_DRIVE_FOLDER_": false,
"_DATA_TRANSFER_DECK_COLUMN_": false
}
}
// Node.js
'module': false,
'require': false,
'__dirname': false,
// Vue
'$$': false,
'$ref': false,
'$computed': false,
// Misskey
'_DEV_': false,
'_LANGS_': false,
'_VERSION_': false,
'_ENV_': false,
'_PERF_PREFIX_': false,
'_DATA_TRANSFER_DRIVE_FILE_': false,
'_DATA_TRANSFER_DRIVE_FOLDER_': false,
'_DATA_TRANSFER_DECK_COLUMN_': false,
},
};

View file

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

View file

@ -10,47 +10,37 @@
"lodash": "^4.17.21"
},
"dependencies": {
"@discordapp/twemoji": "13.1.1",
"@discordapp/twemoji": "14.0.2",
"@fortawesome/fontawesome-free": "6.1.1",
"@rollup/plugin-alias": "3.1.9",
"@rollup/plugin-json": "4.1.0",
"@syuilo/aiscript": "0.11.1",
"@typescript-eslint/parser": "5.20.0",
"@vitejs/plugin-vue": "2.3.1",
"@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",
"date-fns": "2.28.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",
"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",
"photoswipe": "5.2.7",
"prismjs": "1.28.0",
"private-ip": "2.3.3",
"promise-limit": "2.7.0",
@ -61,31 +51,35 @@
"random-seed": "0.3.0",
"reflect-metadata": "0.1.13",
"rndstr": "1.0.0",
"rollup": "2.70.2",
"s-age": "1.1.2",
"sass": "1.50.1",
"sass": "1.52.1",
"seedrandom": "3.0.5",
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
"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",
"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",
"vite": "2.9.6",
"vue": "3.2.33",
"vue": "3.2.36",
"vue-prism-editor": "2.0.0-alpha.2",
"vue-router": "4.0.14",
"vue-router": "4.0.15",
"vuedraggable": "4.0.1",
"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",
@ -97,19 +91,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/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

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

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>

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

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

@ -31,7 +31,7 @@ const props = defineProps<{
}>();
const emit = defineEmits<{
(e: 'update:modelValue', v: boolean): void;
(ev: 'update:modelValue', v: boolean): void;
}>();
let button = $ref<HTMLElement>();

View file

@ -32,7 +32,7 @@ const props = withDefaults(defineProps<{
});
const emit = defineEmits<{
(e: 'click', ev: MouseEvent): void;
(ev: 'click', v: MouseEvent): void;
}>();
const url = $computed(() => defaultStore.state.disableShowingAnimatedImages

View file

@ -46,7 +46,7 @@ export default defineComponent({
const url = computed(() => {
if (char.value) {
let codes = Array.from(char.value).map(x => x.codePointAt(0).toString(16));
if (!codes.includes('200d')) codes = codes.filter(x => x != 'fe0f');
if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f');
codes = codes.filter(x => x && x.length);
return `${twemojiSvgBase}/${codes.join('-')}.svg`;
} else {

View file

@ -1,11 +1,24 @@
<template>
<div class="yxspomdl" :class="{ inline, colored, mini }">
<div class="ring"></div>
<div :class="[$style.root, { [$style.inline]: inline, [$style.colored]: colored, [$style.mini]: mini }]">
<div :class="$style.container">
<svg :class="[$style.spinner, $style.bg]" viewBox="0 0 168 168" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1.125,0,0,1.125,12,12)">
<circle cx="64" cy="64" r="64" style="fill:none;stroke:currentColor;stroke-width:21.33px;"/>
</g>
</svg>
<svg :class="[$style.spinner, $style.fg]" viewBox="0 0 168 168" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1.125,0,0,1.125,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:21.33px;"/>
</g>
</svg>
</div>
</div>
</template>
<script lang="ts" setup>
import { } from 'vue';
import { useCssModule } from 'vue';
useCssModule();
const props = withDefaults(defineProps<{
inline?: boolean;
@ -18,8 +31,8 @@ const props = withDefaults(defineProps<{
});
</script>
<style lang="scss" scoped>
@keyframes ring {
<style lang="scss" module>
@keyframes spinner {
0% {
transform: rotate(0deg);
}
@ -28,12 +41,12 @@ const props = withDefaults(defineProps<{
}
}
.yxspomdl {
.root {
padding: 32px;
text-align: center;
cursor: wait;
--size: 48px;
--size: 40px;
&.colored {
color: var(--accent);
@ -49,34 +62,33 @@ const props = withDefaults(defineProps<{
padding: 16px;
--size: 32px;
}
}
> .ring {
position: relative;
display: inline-block;
vertical-align: middle;
.container {
position: relative;
width: var(--size);
height: var(--size);
margin: 0 auto;
}
&:before,
&:after {
content: " ";
display: block;
box-sizing: border-box;
width: var(--size);
height: var(--size);
border-radius: 50%;
border: solid 4px;
}
.spinner {
position: absolute;
top: 0;
left: 0;
width: var(--size);
height: var(--size);
fill-rule: evenodd;
clip-rule: evenodd;
stroke-linecap: round;
stroke-linejoin: round;
stroke-miterlimit: 1.5;
}
&:before {
border-color: currentColor;
opacity: 0.3;
}
.bg {
opacity: 0.275;
}
&:after {
position: absolute;
top: 0;
border-color: currentColor transparent transparent transparent;
animation: ring 0.5s linear infinite;
}
}
.fg {
animation: spinner 0.5s linear infinite;
}
</style>

View file

@ -31,6 +31,32 @@ const props = withDefaults(defineProps<{
}
}
.mfm-x2 {
--mfm-zoom-size: 200%;
}
.mfm-x3 {
--mfm-zoom-size: 400%;
}
.mfm-x4 {
--mfm-zoom-size: 600%;
}
.mfm-x2, .mfm-x3, .mfm-x4 {
font-size: var(--mfm-zoom-size);
.mfm-x2, .mfm-x3, .mfm-x4 {
/* only half effective */
font-size: calc(var(--mfm-zoom-size) / 2 + 50%);
.mfm-x2, .mfm-x3, .mfm-x4 {
/* disabled */
font-size: 100%;
}
}
}
@keyframes mfm-spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }

View file

@ -17,7 +17,7 @@ const props = withDefaults(defineProps<{
mode: 'relative',
});
const _time = typeof props.time == 'string' ? new Date(props.time) : props.time;
const _time = typeof props.time === 'string' ? new Date(props.time) : props.time;
const absolute = _time.toLocaleString();
let now = $ref(new Date());
@ -32,8 +32,7 @@ const relative = $computed(() => {
ago >= 60 ? i18n.t('_ago.minutesAgo', { n: (~~(ago / 60)).toString() }) :
ago >= 10 ? i18n.t('_ago.secondsAgo', { n: (~~(ago % 60)).toString() }) :
ago >= -1 ? i18n.ts._ago.justNow :
ago < -1 ? i18n.ts._ago.future :
i18n.ts._ago.unknown);
i18n.ts._ago.future);
});
function tick() {

View file

@ -25,7 +25,7 @@ const props = withDefaults(defineProps<{
});
const emit = defineEmits<{
(e: 'closed'): void;
(ev: 'closed'): void;
}>();
const modal = $ref<InstanceType<typeof MkModal>>();

View file

@ -39,6 +39,19 @@ const bg = {
border-radius: 4px 0 0 4px;
overflow: hidden;
color: #fff;
text-shadow: /* .866 ≈ sin(60deg) */
1px 0 1px #000,
.866px .5px 1px #000,
.5px .866px 1px #000,
0 1px 1px #000,
-.5px .866px 1px #000,
-.866px .5px 1px #000,
-1px 0 1px #000,
-.866px -.5px 1px #000,
-.5px -.866px 1px #000,
0 -1px 1px #000,
.5px -.866px 1px #000,
.866px -.5px 1px #000;
> .icon {
height: 100%;

View file

@ -77,7 +77,7 @@ export default defineComponent({
computed: {
remainingLength(): number {
if (typeof this.inputValue != "string") return 512;
if (typeof this.inputValue !== "string") return 512;
return 512 - length(this.inputValue);
}
},
@ -116,17 +116,17 @@ export default defineComponent({
}
},
onKeydown(e) {
if (e.which === 27) { // ESC
onKeydown(evt) {
if (evt.which === 27) { // ESC
this.cancel();
}
},
onInputKeydown(e) {
if (e.which === 13) { // Enter
if (e.ctrlKey) {
e.preventDefault();
e.stopPropagation();
onInputKeydown(evt) {
if (evt.which === 13) { // Enter
if (evt.ctrlKey) {
evt.preventDefault();
evt.stopPropagation();
this.ok();
}
}

View file

@ -142,16 +142,19 @@ export default defineComponent({
break;
}
case 'x2': {
style = `font-size: 200%;`;
break;
return h('span', {
class: 'mfm-x2',
}, genEl(token.children));
}
case 'x3': {
style = `font-size: 400%;`;
break;
return h('span', {
class: 'mfm-x3',
}, genEl(token.children));
}
case 'x4': {
style = `font-size: 600%;`;
break;
return h('span', {
class: 'mfm-x4',
}, genEl(token.children));
}
case 'font': {
const family =

View file

@ -2,9 +2,9 @@
<div
v-if="!muted"
v-show="!isDeleted"
ref="el"
v-hotkey="keymap"
v-size="{ max: [500, 450, 350, 300] }"
ref="el"
class="lxwezrsl _block"
:tabindex="!isDeleted ? '-1' : null"
:class="{ renote: isRenote }"
@ -197,7 +197,7 @@ const keymap = {
'q': () => renoteButton.value.renote(true),
'esc': blur,
'm|o': () => menu(true),
's': () => showContent.value != showContent.value,
's': () => showContent.value !== showContent.value,
};
useNoteCapture({

View file

@ -185,7 +185,7 @@ const keymap = {
'down|j|tab': focusAfter,
'esc': blur,
'm|o': () => menu(true),
's': () => showContent.value != showContent.value,
's': () => showContent.value !== showContent.value,
};
useNoteCapture({

View file

@ -1,34 +1,22 @@
<template>
<div class="lzyxtsnt">
<img v-if="image" :src="image.url"/>
<ImgWithBlurhash v-if="image" :hash="image.blurhash" :src="image.url" :alt="image.comment" :title="image.comment" :cover="false"/>
</div>
</template>
<script lang="ts">
<script lang="ts" setup>
import { defineComponent, PropType } from 'vue';
import ImgWithBlurhash from '@/components/img-with-blurhash.vue';
import * as os from '@/os';
import { ImageBlock } from '@/scripts/hpml/block';
import { Hpml } from '@/scripts/hpml/evaluator';
export default defineComponent({
props: {
block: {
type: Object as PropType<ImageBlock>,
required: true
},
hpml: {
type: Object as PropType<Hpml>,
required: true
}
},
setup(props, ctx) {
const image = props.hpml.page.attachedFiles.find(x => x.id === props.block.fileId);
const props = defineProps<{
block: PropType<ImageBlock>,
hpml: PropType<Hpml>,
}>();
return {
image
};
}
});
const image = props.hpml.page.attachedFiles.find(x => x.id === props.block.fileId);
</script>
<style lang="scss" scoped>

View file

@ -52,16 +52,16 @@ export default defineComponent({
const promise = new Promise((ok) => {
const canvas = this.hpml.canvases[this.block.canvasId];
canvas.toBlob(blob => {
const data = new FormData();
data.append('file', blob);
data.append('i', this.$i.token);
const formData = new FormData();
formData.append('file', blob);
formData.append('i', this.$i.token);
if (this.$store.state.uploadFolder) {
data.append('folderId', this.$store.state.uploadFolder);
formData.append('folderId', this.$store.state.uploadFolder);
}
fetch(apiUrl + '/drive/files/create', {
method: 'POST',
body: data
body: formData,
})
.then(response => response.json())
.then(f => {

View file

@ -38,8 +38,8 @@ export default defineComponent({
let ast;
try {
ast = parse(props.page.script);
} catch (e) {
console.error(e);
} catch (err) {
console.error(err);
/*os.alert({
type: 'error',
text: 'Syntax error :('
@ -48,11 +48,11 @@ export default defineComponent({
}
hpml.aiscript.exec(ast).then(() => {
hpml.eval();
}).catch(e => {
console.error(e);
}).catch(err => {
console.error(err);
/*os.alert({
type: 'error',
text: e
text: err
});*/
});
} else {

View file

@ -104,7 +104,7 @@ function add() {
}
function remove(i) {
choices.value = choices.value.filter((_, _i) => _i != i);
choices.value = choices.value.filter((_, _i) => _i !== i);
}
function get() {

View file

@ -98,7 +98,7 @@ export default defineComponent({
}, {
done: result => {
if (!result || result.canceled) return;
let comment = result.result.length == 0 ? null : result.result;
let comment = result.result.length === 0 ? null : result.result;
os.api('drive/files/update', {
fileId: file.id,
comment: comment,

View file

@ -107,7 +107,7 @@ const props = withDefaults(defineProps<{
fixed?: boolean;
autofocus?: boolean;
}>(), {
initialVisibleUsers: [],
initialVisibleUsers: () => [],
autofocus: true,
});

View file

@ -7,8 +7,8 @@
:class="{ reacted: note.myReaction == reaction, canToggle }"
@click="toggleReaction()"
>
<XReactionIcon :reaction="reaction" :custom-emojis="note.emojis"/>
<span>{{ count }}</span>
<XReactionIcon class="icon" :reaction="reaction" :custom-emojis="note.emojis"/>
<span class="count">{{ count }}</span>
</button>
</template>
@ -141,12 +141,16 @@ export default defineComponent({
background: var(--accent);
}
> span {
> .count {
color: var(--fgOnAccent);
}
> .icon {
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.5));
}
}
> span {
> .count {
font-size: 0.9em;
line-height: 32px;
margin: 0 0 0 4px;

Some files were not shown because too many files have changed in this diff Show more