Merge branch 'develop' into swn

This commit is contained in:
tamaina
2022-04-23 00:59:00 +09:00
156 changed files with 2393 additions and 2154 deletions

View File

@@ -0,0 +1,21 @@
module.exports = {
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
extends: [
'../shared/.eslintrc.js',
],
rules: {
'import/order': ['warn', {
'groups': ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
'pathGroups': [
{
'pattern': '@/**',
'group': 'external',
'position': 'after'
}
],
}]
},
};

View File

@@ -1,9 +0,0 @@
module.exports = {
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
},
extends: [
'../shared/.eslintrc.js',
],
};

View File

@@ -2,5 +2,9 @@
"typescript.tsdk": "node_modules\\typescript\\lib",
"path-intellisense.mappings": {
"@": "${workspaceRoot}/packages/backend/src/"
},
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": true
}
}

View File

@@ -0,0 +1,19 @@
export class webhook1648548247382 {
name = 'webhook1648548247382'
async up(queryRunner) {
await queryRunner.query(`CREATE TABLE "webhook" ("id" character varying(32) NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "userId" character varying(32) NOT NULL, "name" character varying(128) NOT NULL, "on" character varying(128) array NOT NULL DEFAULT '{}', "url" character varying(1024) NOT NULL, "secret" character varying(1024) NOT NULL, "active" boolean NOT NULL DEFAULT true, CONSTRAINT "PK_e6765510c2d078db49632b59020" PRIMARY KEY ("id")); COMMENT ON COLUMN "webhook"."createdAt" IS 'The created date of the Antenna.'; COMMENT ON COLUMN "webhook"."userId" IS 'The owner ID.'; COMMENT ON COLUMN "webhook"."name" IS 'The name of the Antenna.'`);
await queryRunner.query(`CREATE INDEX "IDX_f272c8c8805969e6a6449c77b3" ON "webhook" ("userId") `);
await queryRunner.query(`CREATE INDEX "IDX_8063a0586ed1dfbe86e982d961" ON "webhook" ("on") `);
await queryRunner.query(`CREATE INDEX "IDX_5a056076f76b2efe08216ba655" ON "webhook" ("active") `);
await queryRunner.query(`ALTER TABLE "webhook" ADD CONSTRAINT "FK_f272c8c8805969e6a6449c77b3c" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "webhook" DROP CONSTRAINT "FK_f272c8c8805969e6a6449c77b3c"`);
await queryRunner.query(`DROP INDEX "public"."IDX_5a056076f76b2efe08216ba655"`);
await queryRunner.query(`DROP INDEX "public"."IDX_8063a0586ed1dfbe86e982d961"`);
await queryRunner.query(`DROP INDEX "public"."IDX_f272c8c8805969e6a6449c77b3"`);
await queryRunner.query(`DROP TABLE "webhook"`);
}
}

View File

@@ -0,0 +1,14 @@
export class webhook21648816172177 {
name = 'webhook21648816172177'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "webhook" ADD "latestSentAt" TIMESTAMP WITH TIME ZONE`);
await queryRunner.query(`ALTER TABLE "webhook" ADD "latestStatus" integer`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "webhook" DROP COLUMN "latestStatus"`);
await queryRunner.query(`ALTER TABLE "webhook" DROP COLUMN "latestSentAt"`);
}
}

View File

@@ -5,7 +5,7 @@
"scripts": {
"build": "tsc -p tsconfig.json || echo done. && tsc-alias -p tsconfig.json",
"watch": "node watch.mjs",
"lint": "eslint --quiet src/**/*.ts",
"lint": "eslint --quiet \"src/**/*.ts\"",
"mocha": "cross-env TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true TS_NODE_PROJECT=\"./test/tsconfig.json\" mocha",
"test": "npm run mocha"
},
@@ -14,6 +14,7 @@
"lodash": "^4.17.21"
},
"dependencies": {
"@bull-board/koa": "3.10.3",
"@discordapp/twemoji": "13.1.1",
"@elastic/elasticsearch": "7.11.0",
"@koa/cors": "3.1.0",
@@ -21,65 +22,19 @@
"@koa/router": "9.0.1",
"@sinonjs/fake-timers": "9.1.1",
"@syuilo/aiscript": "0.11.1",
"@types/bcryptjs": "2.4.2",
"@types/bull": "3.15.8",
"@types/cbor": "6.0.0",
"@types/escape-regexp": "0.0.1",
"@types/is-url": "1.2.30",
"@types/js-yaml": "4.0.5",
"@types/jsdom": "16.2.14",
"@types/jsonld": "1.5.6",
"@types/koa": "2.13.4",
"@types/koa-bodyparser": "4.3.7",
"@types/koa-cors": "0.0.2",
"@types/koa-favicon": "2.0.21",
"@types/koa-logger": "3.1.2",
"@types/koa-mount": "4.0.1",
"@types/koa-send": "4.1.3",
"@types/koa-views": "7.0.0",
"@types/koa__cors": "3.1.1",
"@types/koa__multer": "2.0.4",
"@types/koa__router": "8.0.11",
"@types/mocha": "9.1.0",
"@types/node": "17.0.23",
"@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",
"@types/random-seed": "0.3.3",
"@types/ratelimiter": "3.4.3",
"@types/redis": "4.0.11",
"@types/rename": "1.0.4",
"@types/sanitize-html": "2.6.2",
"@types/sharp": "0.30.0",
"@types/sinonjs__fake-timers": "8.1.2",
"@types/speakeasy": "2.0.7",
"@types/throttle-debounce": "2.1.0",
"@types/tinycolor2": "1.4.3",
"@types/tmp": "0.2.3",
"@types/uuid": "8.3.4",
"@types/web-push": "3.3.2",
"@types/websocket": "1.0.5",
"@types/ws": "8.5.3",
"@typescript-eslint/eslint-plugin": "5.16.0",
"@typescript-eslint/parser": "5.16.0",
"@bull-board/koa": "3.10.1",
"@typescript-eslint/eslint-plugin": "5.18.0",
"@typescript-eslint/parser": "5.18.0",
"abort-controller": "3.0.0",
"ajv": "8.11.0",
"archiver": "5.3.0",
"autobind-decorator": "2.4.0",
"autwh": "0.1.0",
"aws-sdk": "2.1100.0",
"aws-sdk": "2.1111.0",
"bcryptjs": "2.4.3",
"blurhash": "1.1.5",
"broadcast-channel": "4.10.0",
"bull": "4.8.1",
"cacheable-lookup": "6.0.4",
"cafy": "15.2.1",
"cbor": "8.1.0",
"chalk": "5.0.1",
"chalk-template": "0.4.0",
@@ -89,8 +44,8 @@
"date-fns": "2.28.0",
"deep-email-validator": "0.1.21",
"escape-regexp": "0.0.1",
"eslint": "8.12.0",
"eslint-plugin-import": "2.25.4",
"eslint": "8.13.0",
"eslint-plugin-import": "2.26.0",
"feed": "4.2.2",
"file-type": "17.1.1",
"fluent-ffmpeg": "2.1.2",
@@ -145,7 +100,7 @@
"rndstr": "1.0.0",
"s-age": "1.1.2",
"sanitize-html": "2.7.0",
"semver": "7.3.5",
"semver": "7.3.6",
"sharp": "0.30.3",
"speakeasy": "2.0.0",
"strict-event-emitter-types": "2.0.0",
@@ -154,7 +109,6 @@
"summaly": "2.5.0",
"syslog-pro": "1.0.0",
"systeminformation": "5.11.9",
"throttle-debounce": "3.0.1",
"tinycolor2": "1.4.2",
"tmp": "0.2.1",
"ts-loader": "9.2.8",
@@ -162,7 +116,7 @@
"tsc-alias": "1.4.1",
"tsconfig-paths": "3.14.1",
"twemoji-parser": "14.0.0",
"typeorm": "0.3.4",
"typeorm": "0.3.5",
"typescript": "4.6.3",
"ulid": "2.3.0",
"unzipper": "0.10.11",
@@ -170,11 +124,55 @@
"web-push": "3.4.5",
"websocket": "1.0.34",
"ws": "8.5.0",
"xev": "2.0.1"
"xev": "3.0.2"
},
"devDependencies": {
"@redocly/openapi-core": "1.0.0-beta.90",
"@redocly/openapi-core": "1.0.0-beta.93",
"@types/semver": "7.3.9",
"@types/bcryptjs": "2.4.2",
"@types/bull": "3.15.8",
"@types/cbor": "6.0.0",
"@types/escape-regexp": "0.0.1",
"@types/fluent-ffmpeg": "2.1.20",
"@types/is-url": "1.2.30",
"@types/js-yaml": "4.0.5",
"@types/jsdom": "16.2.14",
"@types/jsonld": "1.5.6",
"@types/koa": "2.13.4",
"@types/koa-bodyparser": "4.3.7",
"@types/koa-cors": "0.0.2",
"@types/koa-favicon": "2.0.21",
"@types/koa-logger": "3.1.2",
"@types/koa-mount": "4.0.1",
"@types/koa-send": "4.1.3",
"@types/koa-views": "7.0.0",
"@types/koa__cors": "3.1.1",
"@types/koa__multer": "2.0.4",
"@types/koa__router": "8.0.11",
"@types/mocha": "9.1.0",
"@types/node": "17.0.23",
"@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",
"@types/random-seed": "0.3.3",
"@types/ratelimiter": "3.4.3",
"@types/redis": "4.0.11",
"@types/rename": "1.0.4",
"@types/sanitize-html": "2.6.2",
"@types/sharp": "0.30.1",
"@types/sinonjs__fake-timers": "8.1.2",
"@types/speakeasy": "2.0.7",
"@types/tinycolor2": "1.4.3",
"@types/tmp": "0.2.3",
"@types/uuid": "8.3.4",
"@types/web-push": "3.3.2",
"@types/websocket": "1.0.5",
"@types/ws": "8.5.3",
"cross-env": "7.0.3",
"execa": "6.1.0"
}

View File

@@ -1,5 +1,5 @@
declare module 'http-signature' {
import { IncomingMessage, ClientRequest } from 'http';
import { IncomingMessage, ClientRequest } from 'node:http';
interface ISignature {
keyId: string;

View File

@@ -1,6 +1,6 @@
import cluster from 'node:cluster';
import chalk from 'chalk';
import { default as Xev } from 'xev';
import Xev from 'xev';
import Logger from '@/services/logger.js';
import { envOption } from '../env.js';
@@ -12,7 +12,7 @@ import { workerMain } from './worker.js';
const logger = new Logger('core', 'cyan');
const clusterLogger = logger.createSubLogger('cluster', 'orange', false);
const ev = new Xev.default();
const ev = new Xev();
/**
* Init process

View File

@@ -1,7 +1,7 @@
import { default as Xev } from 'xev';
import Xev from 'xev';
import { deliverQueue, inboxQueue } from '../queue/queues.js';
const ev = new Xev.default();
const ev = new Xev();
const interval = 10000;

View File

@@ -1,8 +1,8 @@
import si from 'systeminformation';
import { default as Xev } from 'xev';
import Xev from 'xev';
import * as osUtils from 'os-utils';
const ev = new Xev.default();
const ev = new Xev();
const interval = 2000;

View File

@@ -73,6 +73,7 @@ import { PasswordResetRequest } from '@/models/entities/password-reset-request.j
import { UserPending } from '@/models/entities/user-pending.js';
import { entities as charts } from '@/services/chart/entities.js';
import { Webhook } from '@/models/entities/webhook.js';
const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false);
@@ -171,6 +172,7 @@ export const entities = [
Ad,
PasswordResetRequest,
UserPending,
Webhook,
...charts,
];
@@ -207,7 +209,11 @@ export const db = new DataSource({
});
export async function initDb() {
await db.connect();
if (db.isInitialized) {
// nop
} else {
await db.connect();
}
}
export async function resetDb() {

View File

@@ -1,33 +0,0 @@
import { Context } from 'cafy';
// eslint-disable-next-line @typescript-eslint/ban-types
export class ID<Maybe = string> extends Context<string | (Maybe extends {} ? string : Maybe)> {
public readonly name = 'ID';
constructor(optional = false, nullable = false) {
super(optional, nullable);
this.push((v: any) => {
if (typeof v !== 'string') {
return new Error('must-be-an-id');
}
return true;
});
}
public getType() {
return super.getType('String');
}
public makeOptional(): ID<undefined> {
return new ID(true, false);
}
public makeNullable(): ID<null> {
return new ID(false, true);
}
public makeOptionalNullable(): ID<undefined | null> {
return new ID(true, true);
}
}

View File

@@ -42,7 +42,8 @@ async function getCaptchaResponse(url: string, secret: string, response: string)
headers: {
'User-Agent': config.userAgent,
},
timeout: 10 * 1000,
// TODO
//timeout: 10 * 1000,
agent: getAgentByUrl,
}).catch(e => {
throw `${e.message || e}`;

View File

@@ -1,10 +1,10 @@
import * as http from 'http';
import * as https from 'https';
import * as http from 'node:http';
import * as https from 'node:https';
import { URL } from 'node:url';
import CacheableLookup from 'cacheable-lookup';
import fetch from 'node-fetch';
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
import config from '@/config/index.js';
import { URL } from 'node:url';
export async function getJson(url: string, accept = 'application/json, */*', timeout = 10000, headers?: Record<string, string>) {
const res = await getResponse({
@@ -35,7 +35,7 @@ export async function getHtml(url: string, accept = 'text/html, */*', timeout =
}
export async function getResponse(args: { url: string, method: string, body?: string, headers: Record<string, string>, timeout?: number, size?: number }) {
const timeout = args?.timeout || 10 * 1000;
const timeout = args.timeout || 10 * 1000;
const controller = new AbortController();
setTimeout(() => {
@@ -47,7 +47,7 @@ export async function getResponse(args: { url: string, method: string, body?: st
headers: args.headers,
body: args.body,
timeout,
size: args?.size || 10 * 1024 * 1024,
size: args.size || 10 * 1024 * 1024,
agent: getAgentByUrl,
signal: controller.signal,
});
@@ -120,9 +120,9 @@ export const httpsAgent = config.proxy
*/
export function getAgentByUrl(url: URL, bypassProxy = false) {
if (bypassProxy || (config.proxyBypassHosts || []).includes(url.hostname)) {
return url.protocol == 'http:' ? _http : _https;
return url.protocol === 'http:' ? _http : _https;
} else {
return url.protocol == 'http:' ? httpAgent : httpsAgent;
return url.protocol === 'http:' ? httpAgent : httpsAgent;
}
}

View File

@@ -23,7 +23,7 @@ export const getNoteSummary = (note: Packed<'Note'>): string => {
}
// ファイルが添付されているとき
if ((note.files || []).length != 0) {
if ((note.files || []).length !== 0) {
summary += ` (📎${note.files!.length})`;
}

View File

@@ -0,0 +1,49 @@
import { Webhooks } from '@/models/index.js';
import { Webhook } from '@/models/entities/webhook.js';
import { subsdcriber } from '../db/redis.js';
let webhooksFetched = false;
let webhooks: Webhook[] = [];
export async function getActiveWebhooks() {
if (!webhooksFetched) {
webhooks = await Webhooks.findBy({
active: true,
});
webhooksFetched = true;
}
return webhooks;
}
subsdcriber.on('message', async (_, data) => {
const obj = JSON.parse(data);
if (obj.channel === 'internal') {
const { type, body } = obj.message;
switch (type) {
case 'webhookCreated':
if (body.active) {
webhooks.push(body);
}
break;
case 'webhookUpdated':
if (body.active) {
const i = webhooks.findIndex(a => a.id === body.id);
if (i > -1) {
webhooks[i] = body;
} else {
webhooks.push(body);
}
} else {
webhooks = webhooks.filter(a => a.id !== body.id);
}
break;
case 'webhookDeleted':
webhooks = webhooks.filter(a => a.id !== body.id);
break;
default:
break;
}
}
});

View File

@@ -1,6 +1,6 @@
import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
import { DriveFile } from './drive-file.js';
import { id } from '../id.js';
import { DriveFile } from './drive-file.js';
@Entity()
@Index(['usernameLower', 'host'], { unique: true })
@@ -207,7 +207,7 @@ export class User {
@Column('boolean', {
default: false,
comment: 'Whether to show users replying to other users in the timeline'
comment: 'Whether to show users replying to other users in the timeline',
})
public showTimelineReplies: boolean;

View File

@@ -0,0 +1,73 @@
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
import { User } from './user.js';
import { id } from '../id.js';
export const webhookEventTypes = ['mention', 'unfollow', 'follow', 'followed', 'note', 'reply', 'renote', 'reaction'] as const;
@Entity()
export class Webhook {
@PrimaryColumn(id())
public id: string;
@Column('timestamp with time zone', {
comment: 'The created date of the Antenna.',
})
public createdAt: Date;
@Index()
@Column({
...id(),
comment: 'The owner ID.',
})
public userId: User['id'];
@ManyToOne(type => User, {
onDelete: 'CASCADE',
})
@JoinColumn()
public user: User | null;
@Column('varchar', {
length: 128,
comment: 'The name of the Antenna.',
})
public name: string;
@Index()
@Column('varchar', {
length: 128, array: true, default: '{}',
})
public on: (typeof webhookEventTypes)[number][];
@Column('varchar', {
length: 1024,
})
public url: string;
@Column('varchar', {
length: 1024,
})
public secret: string;
@Index()
@Column('boolean', {
default: true,
})
public active: boolean;
/**
* 直近のリクエスト送信日時
*/
@Column('timestamp with time zone', {
nullable: true,
})
public latestSentAt: Date | null;
/**
* 直近のリクエスト送信時のHTTPステータスコード
*/
@Column('integer', {
nullable: true,
})
public latestStatus: number | null;
}

View File

@@ -64,6 +64,7 @@ import { Ad } from './entities/ad.js';
import { PasswordResetRequest } from './entities/password-reset-request.js';
import { UserPending } from './entities/user-pending.js';
import { InstanceRepository } from './repositories/instance.js';
import { Webhook } from './entities/webhook.js';
export const Announcements = db.getRepository(Announcement);
export const AnnouncementReads = db.getRepository(AnnouncementRead);
@@ -125,5 +126,6 @@ export const Channels = (ChannelRepository);
export const ChannelFollowings = db.getRepository(ChannelFollowing);
export const ChannelNotePinings = db.getRepository(ChannelNotePining);
export const RegistryItems = db.getRepository(RegistryItem);
export const Webhooks = db.getRepository(Webhook);
export const Ads = db.getRepository(Ad);
export const PasswordResetRequests = db.getRepository(PasswordResetRequest);

View File

@@ -1,6 +1,5 @@
import { db } from '@/db/postgre.js';
import { DriveFile } from '@/models/entities/drive-file.js';
import { Users, DriveFolders } from '../index.js';
import { User } from '@/models/entities/user.js';
import { toPuny } from '@/misc/convert-host.js';
import { awaitAll, Promiseable } from '@/prelude/await-all.js';
@@ -9,6 +8,7 @@ import config from '@/config/index.js';
import { query, appendQuery } from '@/prelude/url.js';
import { Meta } from '@/models/entities/meta.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { Users, DriveFolders } from '../index.js';
type PackOptions = {
detail?: boolean,
@@ -111,7 +111,40 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({
async pack(
src: DriveFile['id'] | DriveFile,
options?: PackOptions
options?: PackOptions,
): Promise<Packed<'DriveFile'>> {
const opts = Object.assign({
detail: false,
self: false,
}, options);
const file = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
return await awaitAll<Packed<'DriveFile'>>({
id: file.id,
createdAt: file.createdAt.toISOString(),
name: file.name,
type: file.type,
md5: file.md5,
size: file.size,
isSensitive: file.isSensitive,
blurhash: file.blurhash,
properties: opts.self ? file.properties : this.getPublicProperties(file),
url: opts.self ? file.url : this.getPublicUrl(file, false),
thumbnailUrl: this.getPublicUrl(file, true),
comment: file.comment,
folderId: file.folderId,
folder: opts.detail && file.folderId ? DriveFolders.pack(file.folderId, {
detail: true,
}) : null,
userId: opts.withUser ? file.userId : null,
user: (opts.withUser && file.userId) ? Users.pack(file.userId) : null,
});
},
async packNullable(
src: DriveFile['id'] | DriveFile,
options?: PackOptions,
): Promise<Packed<'DriveFile'> | null> {
const opts = Object.assign({
detail: false,
@@ -145,9 +178,9 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({
async packMany(
files: (DriveFile['id'] | DriveFile)[],
options?: PackOptions
) {
const items = await Promise.all(files.map(f => this.pack(f, options)));
return items.filter(x => x != null);
options?: PackOptions,
): Promise<Packed<'DriveFile'>[]> {
const items = await Promise.all(files.map(f => this.packNullable(f, options)));
return items.filter((x): x is Packed<'DriveFile'> => x != null);
},
});

View File

@@ -1,10 +1,10 @@
import { db } from '@/db/postgre.js';
import { Page } from '@/models/entities/page.js';
import { Packed } from '@/misc/schema.js';
import { Users, DriveFiles, PageLikes } from '../index.js';
import { awaitAll } from '@/prelude/await-all.js';
import { DriveFile } from '@/models/entities/drive-file.js';
import { User } from '@/models/entities/user.js';
import { Users, DriveFiles, PageLikes } from '../index.js';
export const PageRepository = db.getRepository(Page).extend({
async pack(
@@ -14,7 +14,7 @@ export const PageRepository = db.getRepository(Page).extend({
const meId = me ? me.id : null;
const page = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
const attachedFiles: Promise<DriveFile | undefined>[] = [];
const attachedFiles: Promise<DriveFile | null>[] = [];
const collectFile = (xs: any[]) => {
for (const x of xs) {
if (x.type === 'image') {
@@ -73,7 +73,7 @@ export const PageRepository = db.getRepository(Page).extend({
script: page.script,
eyeCatchingImageId: page.eyeCatchingImageId,
eyeCatchingImage: page.eyeCatchingImageId ? await DriveFiles.pack(page.eyeCatchingImageId) : null,
attachedFiles: DriveFiles.packMany(await Promise.all(attachedFiles)),
attachedFiles: DriveFiles.packMany((await Promise.all(attachedFiles)).filter((x): x is DriveFile => x != null)),
likedCount: page.likedCount,
isLiked: meId ? await PageLikes.findOneBy({ pageId: page.id, userId: meId }).then(x => x != null) : undefined,
});

View File

@@ -1,7 +1,6 @@
import { EntityRepository, Repository, In, Not } from 'typeorm';
import Ajv from 'ajv';
import { User, ILocalUser, IRemoteUser } from '@/models/entities/user.js';
import { Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, Antennas, AntennaNotes, ChannelFollowings, Instances, DriveFiles } from '../index.js';
import config from '@/config/index.js';
import { Packed } from '@/misc/schema.js';
import { awaitAll, Promiseable } from '@/prelude/await-all.js';
@@ -9,8 +8,9 @@ import { populateEmojis } from '@/misc/populate-emojis.js';
import { getAntennas } from '@/misc/antenna-cache.js';
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
import { Cache } from '@/misc/cache.js';
import { Instance } from '../entities/instance.js';
import { db } from '@/db/postgre.js';
import { Instance } from '../entities/instance.js';
import { Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, Antennas, AntennaNotes, ChannelFollowings, Instances, DriveFiles } from '../index.js';
const userInstanceCache = new Cache<Instance | null>(1000 * 60 * 60 * 3);
@@ -112,7 +112,7 @@ export const UserRepository = db.getRepository(User).extend({
const joinings = await UserGroupJoinings.findBy({ userId: userId });
const groupQs = Promise.all(joinings.map(j => MessagingMessages.createQueryBuilder('message')
.where(`message.groupId = :groupId`, { groupId: j.userGroupId })
.where('message.groupId = :groupId', { groupId: j.userGroupId })
.andWhere('message.userId != :userId', { userId: userId })
.andWhere('NOT (:userId = ANY(message.reads))', { userId: userId })
.andWhere('message.createdAt > :joinedAt', { joinedAt: j.createdAt }) // 自分が加入する前の会話については、未読扱いしない
@@ -204,8 +204,18 @@ export const UserRepository = db.getRepository(User).extend({
);
},
getAvatarUrl(user: User): string {
// TODO: avatarIdがあるがavatarがない(JOINされてない)場合のハンドリング
async getAvatarUrl(user: User): Promise<string> {
if (user.avatar) {
return DriveFiles.getPublicUrl(user.avatar, true) || this.getIdenticonUrl(user.id);
} else if (user.avatarId) {
const avatar = await DriveFiles.findOneByOrFail({ id: user.avatarId });
return DriveFiles.getPublicUrl(avatar, true) || this.getIdenticonUrl(user.id);
} else {
return this.getIdenticonUrl(user.id);
}
},
getAvatarUrlSync(user: User): string {
if (user.avatar) {
return DriveFiles.getPublicUrl(user.avatar, true) || this.getIdenticonUrl(user.id);
} else {
@@ -223,7 +233,7 @@ export const UserRepository = db.getRepository(User).extend({
options?: {
detail?: D,
includeSecrets?: boolean,
}
},
): Promise<IsMeAndIsUserDetailed<ExpectsMe, D>> {
const opts = Object.assign({
detail: false,
@@ -274,7 +284,7 @@ export const UserRepository = db.getRepository(User).extend({
name: user.name,
username: user.username,
host: user.host,
avatarUrl: this.getAvatarUrl(user),
avatarUrl: this.getAvatarUrlSync(user),
avatarBlurhash: user.avatar?.blurhash || null,
avatarColor: null, // 後方互換性のため
isAdmin: user.isAdmin || falsy,
@@ -283,7 +293,7 @@ export const UserRepository = db.getRepository(User).extend({
isCat: user.isCat || falsy,
instance: user.host ? userInstanceCache.fetch(user.host,
() => Instances.findOneBy({ host: user.host! }),
v => v != null
v => v != null,
).then(instance => instance ? {
name: instance.name,
softwareName: instance.softwareName,
@@ -403,7 +413,7 @@ export const UserRepository = db.getRepository(User).extend({
options?: {
detail?: D,
includeSecrets?: boolean,
}
},
): Promise<IsUserDetailed<D>[]> {
return Promise.all(users.map(u => this.pack(u, me, options)));
},

View File

@@ -27,6 +27,7 @@ export const packedEmojiSchema = {
host: {
type: 'string',
optional: false, nullable: true,
description: 'The local host is represented with `null`.',
},
url: {
type: 'string',

View File

@@ -21,6 +21,7 @@ export const packedUserLiteSchema = {
type: 'string',
nullable: true, optional: false,
example: 'misskey.example.com',
description: 'The local host is represented with `null`.',
},
avatarUrl: {
type: 'string',

View File

@@ -1,4 +1,5 @@
import httpSignature from 'http-signature';
import { v4 as uuid } from 'uuid';
import config from '@/config/index.js';
import { envOption } from '../env.js';
@@ -8,13 +9,15 @@ import processInbox from './processors/inbox.js';
import processDb from './processors/db/index.js';
import processObjectStorage from './processors/object-storage/index.js';
import processSystemQueue from './processors/system/index.js';
import processWebhookDeliver from './processors/webhook-deliver.js';
import { endedPollNotification } from './processors/ended-poll-notification.js';
import { queueLogger } from './logger.js';
import { DriveFile } from '@/models/entities/drive-file.js';
import { getJobInfo } from './get-job-info.js';
import { systemQueue, dbQueue, deliverQueue, inboxQueue, objectStorageQueue, endedPollNotificationQueue } from './queues.js';
import { systemQueue, dbQueue, deliverQueue, inboxQueue, objectStorageQueue, endedPollNotificationQueue, webhookDeliverQueue } from './queues.js';
import { ThinUser } from './types.js';
import { IActivity } from '@/remote/activitypub/type.js';
import { Webhook, webhookEventTypes } from '@/models/entities/webhook.js';
function renderError(e: Error): any {
return {
@@ -26,6 +29,7 @@ function renderError(e: Error): any {
const systemLogger = queueLogger.createSubLogger('system');
const deliverLogger = queueLogger.createSubLogger('deliver');
const webhookLogger = queueLogger.createSubLogger('webhook');
const inboxLogger = queueLogger.createSubLogger('inbox');
const dbLogger = queueLogger.createSubLogger('db');
const objectStorageLogger = queueLogger.createSubLogger('objectStorage');
@@ -70,6 +74,14 @@ objectStorageQueue
.on('error', (job: any, err: Error) => objectStorageLogger.error(`error ${err}`, { job, e: renderError(err) }))
.on('stalled', (job) => objectStorageLogger.warn(`stalled id=${job.id}`));
webhookDeliverQueue
.on('waiting', (jobId) => webhookLogger.debug(`waiting id=${jobId}`))
.on('active', (job) => webhookLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
.on('completed', (job, result) => webhookLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
.on('failed', (job, err) => webhookLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`))
.on('error', (job: any, err: Error) => webhookLogger.error(`error ${err}`, { job, e: renderError(err) }))
.on('stalled', (job) => webhookLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`));
export function deliver(user: ThinUser, content: unknown, to: string | null) {
if (content == null) return null;
if (to == null) return null;
@@ -251,12 +263,36 @@ export function createCleanRemoteFilesJob() {
});
}
export function webhookDeliver(webhook: Webhook, type: typeof webhookEventTypes[number], content: unknown) {
const data = {
type,
content,
webhookId: webhook.id,
userId: webhook.userId,
to: webhook.url,
secret: webhook.secret,
createdAt: Date.now(),
eventId: uuid(),
};
return webhookDeliverQueue.add(data, {
attempts: 4,
timeout: 1 * 60 * 1000, // 1min
backoff: {
type: 'apBackoff',
},
removeOnComplete: true,
removeOnFail: true,
});
}
export default function() {
if (envOption.onlyServer) return;
deliverQueue.process(config.deliverJobConcurrency || 128, processDeliver);
inboxQueue.process(config.inboxJobConcurrency || 16, processInbox);
endedPollNotificationQueue.process(endedPollNotification);
webhookDeliverQueue.process(64, processWebhookDeliver);
processDb(dbQueue);
processObjectStorage(objectStorageQueue);

View File

@@ -0,0 +1,59 @@
import { URL } from 'node:url';
import Bull from 'bull';
import Logger from '@/services/logger.js';
import { WebhookDeliverJobData } from '../types.js';
import { getResponse, StatusError } from '@/misc/fetch.js';
import { Webhooks } from '@/models/index.js';
import config from '@/config/index.js';
const logger = new Logger('webhook');
export default async (job: Bull.Job<WebhookDeliverJobData>) => {
try {
logger.debug(`delivering ${job.data.webhookId}`);
const res = await getResponse({
url: job.data.to,
method: 'POST',
headers: {
'User-Agent': 'Misskey-Hooks',
'X-Misskey-Host': config.host,
'X-Misskey-Hook-Id': job.data.webhookId,
'X-Misskey-Hook-Secret': job.data.secret,
},
body: JSON.stringify({
hookId: job.data.webhookId,
userId: job.data.userId,
eventId: job.data.eventId,
createdAt: job.data.createdAt,
type: job.data.type,
body: job.data.content,
}),
});
Webhooks.update({ id: job.data.webhookId }, {
latestSentAt: new Date(),
latestStatus: res.status,
});
return 'Success';
} catch (res) {
Webhooks.update({ id: job.data.webhookId }, {
latestSentAt: new Date(),
latestStatus: res instanceof StatusError ? res.statusCode : 1,
});
if (res instanceof StatusError) {
// 4xx
if (res.isClientError) {
return `${res.statusCode} ${res.statusMessage}`;
}
// 5xx etc.
throw `${res.statusCode} ${res.statusMessage}`;
} else {
// DNS error, socket error, timeout ...
throw res;
}
}
};

View File

@@ -1,6 +1,6 @@
import config from '@/config/index.js';
import { initialize as initializeQueue } from './initialize.js';
import { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData, EndedPollNotificationJobData } from './types.js';
import { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData, EndedPollNotificationJobData, WebhookDeliverJobData } from './types.js';
export const systemQueue = initializeQueue<Record<string, unknown>>('system');
export const endedPollNotificationQueue = initializeQueue<EndedPollNotificationJobData>('endedPollNotification');
@@ -8,6 +8,7 @@ export const deliverQueue = initializeQueue<DeliverJobData>('deliver', config.de
export const inboxQueue = initializeQueue<InboxJobData>('inbox', config.inboxJobPerSec || 16);
export const dbQueue = initializeQueue<DbJobData>('db');
export const objectStorageQueue = initializeQueue<ObjectStorageJobData>('objectStorage');
export const webhookDeliverQueue = initializeQueue<WebhookDeliverJobData>('webhookDeliver', 64);
export const queues = [
systemQueue,
@@ -16,4 +17,5 @@ export const queues = [
inboxQueue,
dbQueue,
objectStorageQueue,
webhookDeliverQueue,
];

View File

@@ -1,6 +1,7 @@
import { DriveFile } from '@/models/entities/drive-file.js';
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';
@@ -46,6 +47,17 @@ export type EndedPollNotificationJobData = {
noteId: Note['id'];
};
export type WebhookDeliverJobData = {
type: string;
content: unknown;
webhookId: Webhook['id'];
userId: User['id'];
to: string;
secret: string;
createdAt: number;
eventId: string;
};
export type ThinUser = {
id: User['id'];
};

View File

@@ -95,7 +95,7 @@ function genSigningString(request: Request, includeHeaders: string[]) {
function lcObjectKey(src: Record<string, string>) {
const dst: Record<string, string> = {};
for (const key of Object.keys(src).filter(x => x != '__proto__' && typeof src[x] === 'string')) dst[key.toLowerCase()] = src[key];
for (const key of Object.keys(src).filter(x => x !== '__proto__' && typeof src[x] === 'string')) dst[key.toLowerCase()] = src[key];
return dst;
}

View File

@@ -79,37 +79,46 @@ export default class DeliverManager {
const inboxes = new Set<string>();
// build inbox list
for (const recipe of this.recipes) {
if (isFollowers(recipe)) {
// followers deliver
// TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう
// ただ、sharedInboxがnullなリモートユーザーも稀におり、その対応ができなさそう
const followers = await Followings.find({
where: {
followeeId: this.actor.id,
followerHost: Not(IsNull()),
},
select: {
followerSharedInbox: true,
followerInbox: true,
},
}) as {
followerSharedInbox: string | null;
followerInbox: string;
}[];
/*
build inbox list
for (const following of followers) {
const inbox = following.followerSharedInbox || following.followerInbox;
inboxes.add(inbox);
}
} else if (isDirect(recipe)) {
// direct deliver
const inbox = recipe.to.inbox;
if (inbox) inboxes.add(inbox);
Process follower recipes first to avoid duplication when processing
direct recipes later.
*/
if (this.recipes.some(r => isFollowers(r))) {
// followers deliver
// TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう
// ただ、sharedInboxがnullなリモートユーザーも稀におり、その対応ができなさそう
const followers = await Followings.find({
where: {
followeeId: this.actor.id,
followerHost: Not(IsNull()),
},
select: {
followerSharedInbox: true,
followerInbox: true,
},
}) as {
followerSharedInbox: string | null;
followerInbox: string;
}[];
for (const following of followers) {
const inbox = following.followerSharedInbox || following.followerInbox;
inboxes.add(inbox);
}
}
this.recipes.filter((recipe): recipe is IDirectRecipe =>
// followers recipes have already been processed
isDirect(recipe)
// check that shared inbox has not been added yet
&& !(recipe.to.sharedInbox && inboxes.has(recipe.to.sharedInbox))
// check that they actually have an inbox
&& recipe.to.inbox != null,
)
.forEach(recipe => inboxes.add(recipe.to.inbox!));
// deliver
for (const inbox of inboxes) {
deliver(this.actor, this.activity, inbox);

View File

@@ -18,7 +18,7 @@ export const performReadActivity = async (actor: CacheableRemoteUser, activity:
return `skip: message not found`;
}
if (actor.id != message.recipientId) {
if (actor.id !== message.recipientId) {
return `skip: actor is not a message recipient`;
}

View File

@@ -1,6 +1,6 @@
import unfollow from '@/services/following/delete.js';
import cancelRequest from '@/services/following/requests/cancel.js';
import {IAccept} from '../../type.js';
import { IAccept } from '../../type.js';
import { CacheableRemoteUser } from '@/models/entities/user.js';
import { Followings } from '@/models/index.js';
import DbResolver from '../../db-resolver.js';

View File

@@ -113,7 +113,8 @@ export class LdSignature {
headers: {
Accept: 'application/ld+json, application/json',
},
timeout: this.loderTimeout,
// TODO
//timeout: this.loderTimeout,
agent: u => u.protocol === 'http:' ? httpAgent : httpsAgent,
}).then(res => {
if (!res.ok) {

View File

@@ -1,9 +1,9 @@
import { toArray, unique } from '@/prelude/array.js';
import { IObject, isMention, IApMention } from '../type.js';
import { resolvePerson } from './person.js';
import promiseLimit from 'promise-limit';
import Resolver from '../resolver.js';
import { toArray, unique } from '@/prelude/array.js';
import { CacheableUser, User } from '@/models/entities/user.js';
import { IObject, isMention, IApMention } from '../type.js';
import Resolver from '../resolver.js';
import { resolvePerson } from './person.js';
export async function extractApMentions(tags: IObject | IObject[] | null | undefined) {
const hrefs = unique(extractApMentionObjects(tags).map(x => x.href as string));
@@ -12,7 +12,7 @@ export async function extractApMentions(tags: IObject | IObject[] | null | undef
const limit = promiseLimit<CacheableUser | null>(2);
const mentionedUsers = (await Promise.all(
hrefs.map(x => limit(() => resolvePerson(x, resolver).catch(() => null)))
hrefs.map(x => limit(() => resolvePerson(x, resolver).catch(() => null))),
)).filter((x): x is CacheableUser => x != null);
return mentionedUsers;

View File

@@ -1,17 +1,8 @@
import { URL } from 'node:url';
import promiseLimit from 'promise-limit';
import $, { Context } from 'cafy';
import config from '@/config/index.js';
import Resolver from '../resolver.js';
import { resolveImage } from './image.js';
import { isCollectionOrOrderedCollection, isCollection, IActor, getApId, getOneApHrefNullable, IObject, isPropertyValue, IApPropertyValue, getApType, isActor } from '../type.js';
import { fromHtml } from '../../../mfm/from-html.js';
import { htmlToMfm } from '../misc/html-to-mfm.js';
import { resolveNote, extractEmojis } from './note.js';
import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc.js';
import { extractApHashtags } from './tag.js';
import { apLogger } from '../logger.js';
import { Note } from '@/models/entities/note.js';
import { updateUsertags } from '@/services/update-hashtag.js';
import { Users, Instances, DriveFiles, Followings, UserProfiles, UserPublickeys } from '@/models/index.js';
@@ -32,6 +23,14 @@ import { StatusError } from '@/misc/fetch.js';
import { uriPersonCache } from '@/services/user-cache.js';
import { publishInternalEvent } from '@/services/stream.js';
import { db } from '@/db/postgre.js';
import { apLogger } from '../logger.js';
import { htmlToMfm } from '../misc/html-to-mfm.js';
import { fromHtml } from '../../../mfm/from-html.js';
import { isCollectionOrOrderedCollection, isCollection, IActor, getApId, getOneApHrefNullable, IObject, isPropertyValue, IApPropertyValue, getApType, isActor } from '../type.js';
import Resolver from '../resolver.js';
import { extractApHashtags } from './tag.js';
import { resolveNote, extractEmojis } from './note.js';
import { resolveImage } from './image.js';
const logger = apLogger;
@@ -54,20 +53,33 @@ function validateActor(x: IObject, uri: string): IActor {
throw new Error(`invalid Actor type '${x.type}'`);
}
const validate = (name: string, value: any, validater: Context) => {
const e = validater.test(value);
if (e) throw new Error(`invalid Actor: ${name} ${e.message}`);
};
if (!(typeof x.id === 'string' && x.id.length > 0)) {
throw new Error('invalid Actor: wrong id');
}
validate('id', x.id, $.default.str.min(1));
validate('inbox', x.inbox, $.default.str.min(1));
validate('preferredUsername', x.preferredUsername, $.default.str.min(1).max(128).match(/^\w([\w-.]*\w)?$/));
if (!(typeof x.inbox === 'string' && x.inbox.length > 0)) {
throw new Error('invalid Actor: wrong inbox');
}
if (!(typeof x.preferredUsername === 'string' && x.preferredUsername.length > 0 && x.preferredUsername.length <= 128 && /^\w([\w-.]*\w)?$/.test(x.preferredUsername))) {
throw new Error('invalid Actor: wrong username');
}
// These fields are only informational, and some AP software allows these
// fields to be very long. If they are too long, we cut them off. This way
// we can at least see these users and their activities.
validate('name', truncate(x.name, nameLength), $.default.optional.nullable.str);
validate('summary', truncate(x.summary, summaryLength), $.default.optional.nullable.str);
if (x.name) {
if (!(typeof x.name === 'string' && x.name.length > 0)) {
throw new Error('invalid Actor: wrong name');
}
x.name = truncate(x.name, nameLength);
}
if (x.summary) {
if (!(typeof x.summary === 'string' && x.summary.length > 0)) {
throw new Error('invalid Actor: wrong summary');
}
x.summary = truncate(x.summary, summaryLength);
}
const idHost = toPuny(new URL(x.id!).hostname);
if (idHost !== expectHost) {
@@ -271,7 +283,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
* @param resolver Resolver
* @param hint Hint of Person object (この値が正当なPersonの場合、Remote resolveをせずに更新に利用します)
*/
export async function updatePerson(uri: string, resolver?: Resolver | null, hint?: Record<string, unknown>): Promise<void> {
export async function updatePerson(uri: string, resolver?: Resolver | null, hint?: IObject): Promise<void> {
if (typeof uri !== 'string') throw new Error('uri is not string');
// URIがこのサーバーを指しているならスキップ
@@ -289,7 +301,7 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
if (resolver == null) resolver = new Resolver();
const object = hint || await resolver.resolve(uri) as any;
const object = hint || await resolver.resolve(uri);
const person = validateActor(object, uri);
@@ -400,10 +412,10 @@ export async function resolvePerson(uri: string, resolver?: Resolver): Promise<C
const services: {
[x: string]: (id: string, username: string) => any
} = {
'misskey:authentication:twitter': (userId, screenName) => ({ userId, screenName }),
'misskey:authentication:github': (id, login) => ({ id, login }),
'misskey:authentication:discord': (id, name) => $discord(id, name),
};
'misskey:authentication:twitter': (userId, screenName) => ({ userId, screenName }),
'misskey:authentication:github': (id, login) => ({ id, login }),
'misskey:authentication:discord': (id, name) => $discord(id, name),
};
const $discord = (id: string, name: string) => {
if (typeof name !== 'string') {
@@ -461,7 +473,7 @@ export async function updateFeatured(userId: User['id']) {
// Resolve to (Ordered)Collection Object
const collection = await resolver.resolveCollection(user.featured);
if (!isCollectionOrOrderedCollection(collection)) throw new Error(`Object is not Collection or OrderedCollection`);
if (!isCollectionOrOrderedCollection(collection)) throw new Error('Object is not Collection or OrderedCollection');
// Resolve to Object(may be Note) arrays
const unresolvedItems = isCollection(collection) ? collection.items : collection.orderedItems;

View File

@@ -69,7 +69,7 @@ export async function updateQuestion(value: any) {
const oldCount = poll.votes[poll.choices.indexOf(choice)];
const newCount = apChoices!.filter(ap => ap.name === choice)[0].replies!.totalItems;
if (oldCount != newCount) {
if (oldCount !== newCount) {
changed = true;
poll.votes[poll.choices.indexOf(choice)] = newCount;
}

View File

@@ -5,7 +5,7 @@ import { getInstanceActor } from '@/services/instance-actor.js';
// to anonymise reporters, the reporting actor must be a system user
// object has to be a uri or array of uris
export const renderFlag = (user: ILocalUser, object: [string], content: string): IActivity => {
export const renderFlag = (user: ILocalUser, object: [string], content: string) => {
return {
type: 'Flag',
actor: `${config.url}/users/${user.id}`,

View File

@@ -1,15 +1,15 @@
import renderDocument from './document.js';
import renderHashtag from './hashtag.js';
import renderMention from './mention.js';
import renderEmoji from './emoji.js';
import { In, IsNull } from 'typeorm';
import config from '@/config/index.js';
import toHtml from '../misc/get-note-html.js';
import { Note, IMentionedRemoteUsers } from '@/models/entities/note.js';
import { DriveFile } from '@/models/entities/drive-file.js';
import { DriveFiles, Notes, Users, Emojis, Polls } from '@/models/index.js';
import { In, IsNull } from 'typeorm';
import { Emoji } from '@/models/entities/emoji.js';
import { Poll } from '@/models/entities/poll.js';
import toHtml from '../misc/get-note-html.js';
import renderEmoji from './emoji.js';
import renderMention from './mention.js';
import renderHashtag from './hashtag.js';
import renderDocument from './document.js';
export default async function renderNote(note: Note, dive = true, isTalk = false): Promise<Record<string, unknown>> {
const getPromisedFiles = async (ids: string[]) => {
@@ -83,7 +83,7 @@ export default async function renderNote(note: Note, dive = true, isTalk = false
const files = await getPromisedFiles(note.fileIds);
const text = note.text;
let poll: Poll | null;
let poll: Poll | null = null;
if (note.hasPoll) {
poll = await Polls.findOneBy({ noteId: note.id });
@@ -159,7 +159,7 @@ export async function getEmojis(names: string[]): Promise<Emoji[]> {
names.map(name => Emojis.findOneBy({
name,
host: IsNull(),
}))
})),
);
return emojis.filter(emoji => emoji != null) as Emoji[];

View File

@@ -2,10 +2,10 @@ import config from '@/config/index.js';
import { getJson } from '@/misc/fetch.js';
import { ILocalUser } from '@/models/entities/user.js';
import { getInstanceActor } from '@/services/instance-actor.js';
import { signedGet } from './request.js';
import { IObject, isCollectionOrOrderedCollection, ICollection, IOrderedCollection } from './type.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { extractDbHost } from '@/misc/convert-host.js';
import { signedGet } from './request.js';
import { IObject, isCollectionOrOrderedCollection, ICollection, IOrderedCollection } from './type.js';
export default class Resolver {
private history: Set<string>;
@@ -56,13 +56,13 @@ export default class Resolver {
this.user = await getInstanceActor();
}
const object = this.user
const object = (this.user
? await signedGet(value, this.user)
: await getJson(value, 'application/activity+json, application/ld+json');
: await getJson(value, 'application/activity+json, application/ld+json')) as IObject;
if (object == null || (
Array.isArray(object['@context']) ?
!object['@context'].includes('https://www.w3.org/ns/activitystreams') :
!(object['@context'] as unknown[]).includes('https://www.w3.org/ns/activitystreams') :
object['@context'] !== 'https://www.w3.org/ns/activitystreams'
)) {
throw new Error('invalid response');

View File

@@ -2,7 +2,7 @@ export type obj = { [x: string]: any };
export type ApObject = IObject | string | (IObject | string)[];
export interface IObject {
'@context': string | obj | obj[];
'@context': string | string[] | obj | obj[];
type: string | string[];
id?: string;
summary?: string;
@@ -48,7 +48,7 @@ export function getOneApId(value: ApObject): string {
export function getApId(value: string | IObject): string {
if (typeof value === 'string') return value;
if (typeof value.id === 'string') return value.id;
throw new Error(`cannot detemine id`);
throw new Error('cannot detemine id');
}
/**
@@ -57,7 +57,7 @@ export function getApId(value: string | IObject): string {
export function getApType(value: IObject): string {
if (typeof value.type === 'string') return value.type;
if (Array.isArray(value.type) && typeof value.type[0] === 'string') return value.type[0];
throw new Error(`cannot detect type`);
throw new Error('cannot detect type');
}
export function getOneApHrefNullable(value: ApObject | undefined): string | undefined {

View File

@@ -15,7 +15,7 @@ type IWebFinger = {
export default async function(query: string): Promise<IWebFinger> {
const url = genUrl(query);
return await getJson(url, 'application/jrd+json, application/json');
return await getJson(url, 'application/jrd+json, application/json') as IWebFinger;
}
function genUrl(query: string) {

View File

@@ -1,32 +1,26 @@
import Router from '@koa/router';
import { FindOptionsWhere, IsNull, LessThan } from 'typeorm';
import config from '@/config/index.js';
import $ from 'cafy';
import { ID } from '@/misc/cafy-id.js';
import * as url from '@/prelude/url.js';
import { renderActivity } from '@/remote/activitypub/renderer/index.js';
import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection.js';
import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page.js';
import renderFollowUser from '@/remote/activitypub/renderer/follow-user.js';
import { setResponseType } from '../activitypub.js';
import { Users, Followings, UserProfiles } from '@/models/index.js';
import { IsNull, LessThan } from 'typeorm';
import { Following } from '@/models/entities/following.js';
import { setResponseType } from '../activitypub.js';
export default async (ctx: Router.RouterContext) => {
const userId = ctx.params.user;
// Get 'cursor' parameter
const [cursor, cursorErr] = $.default.optional.type(ID).get(ctx.request.query.cursor);
// Get 'page' parameter
const pageErr = !$.default.optional.str.or(['true', 'false']).ok(ctx.request.query.page);
const page: boolean = ctx.request.query.page === 'true';
// Validate parameters
if (cursorErr || pageErr) {
const cursor = ctx.request.query.cursor;
if (cursor != null && typeof cursor !== 'string') {
ctx.status = 400;
return;
}
const page = ctx.request.query.page === 'true';
const user = await Users.findOneBy({
id: userId,
host: IsNull(),
@@ -57,7 +51,7 @@ export default async (ctx: Router.RouterContext) => {
if (page) {
const query = {
followeeId: user.id,
} as any;
} as FindOptionsWhere<Following>;
// カーソルが指定されている場合
if (cursor) {
@@ -86,7 +80,7 @@ export default async (ctx: Router.RouterContext) => {
inStock ? `${partOf}?${url.query({
page: 'true',
cursor: followings[followings.length - 1].id,
})}` : undefined
})}` : undefined,
);
ctx.body = renderActivity(rendered);

View File

@@ -1,33 +1,26 @@
import Router from '@koa/router';
import { LessThan, IsNull, FindOptionsWhere } from 'typeorm';
import config from '@/config/index.js';
import $ from 'cafy';
import { ID } from '@/misc/cafy-id.js';
import * as url from '@/prelude/url.js';
import { renderActivity } from '@/remote/activitypub/renderer/index.js';
import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection.js';
import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page.js';
import renderFollowUser from '@/remote/activitypub/renderer/follow-user.js';
import { setResponseType } from '../activitypub.js';
import { Users, Followings, UserProfiles } from '@/models/index.js';
import { LessThan, IsNull, FindOptionsWhere } from 'typeorm';
import { Following } from '@/models/entities/following.js';
import { setResponseType } from '../activitypub.js';
export default async (ctx: Router.RouterContext) => {
const userId = ctx.params.user;
// Get 'cursor' parameter
const [cursor, cursorErr] = $.default.optional.type(ID).get(ctx.request.query.cursor);
// Get 'page' parameter
const pageErr = !$.default.optional.str.or(['true', 'false']).ok(ctx.request.query.page);
const page: boolean = ctx.request.query.page === 'true';
// Validate parameters
if (cursorErr || pageErr) {
const cursor = ctx.request.query.cursor;
if (cursor != null && typeof cursor !== 'string') {
ctx.status = 400;
return;
}
const page = ctx.request.query.page === 'true';
const user = await Users.findOneBy({
id: userId,
host: IsNull(),
@@ -87,7 +80,7 @@ export default async (ctx: Router.RouterContext) => {
inStock ? `${partOf}?${url.query({
page: 'true',
cursor: followings[followings.length - 1].id,
})}` : undefined
})}` : undefined,
);
ctx.body = renderActivity(rendered);

View File

@@ -1,36 +1,37 @@
import Router from '@koa/router';
import { Brackets, IsNull } from 'typeorm';
import config from '@/config/index.js';
import $ from 'cafy';
import { ID } from '@/misc/cafy-id.js';
import { renderActivity } from '@/remote/activitypub/renderer/index.js';
import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection.js';
import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page.js';
import { setResponseType } from '../activitypub.js';
import renderNote from '@/remote/activitypub/renderer/note.js';
import renderCreate from '@/remote/activitypub/renderer/create.js';
import renderAnnounce from '@/remote/activitypub/renderer/announce.js';
import { countIf } from '@/prelude/array.js';
import * as url from '@/prelude/url.js';
import { Users, Notes } from '@/models/index.js';
import { makePaginationQuery } from '../api/common/make-pagination-query.js';
import { Brackets, IsNull } from 'typeorm';
import { Note } from '@/models/entities/note.js';
import { makePaginationQuery } from '../api/common/make-pagination-query.js';
import { setResponseType } from '../activitypub.js';
export default async (ctx: Router.RouterContext) => {
const userId = ctx.params.user;
// Get 'sinceId' parameter
const [sinceId, sinceIdErr] = $.default.optional.type(ID).get(ctx.request.query.since_id);
const sinceId = ctx.request.query.since_id;
if (sinceId != null && typeof sinceId !== 'string') {
ctx.status = 400;
return;
}
// Get 'untilId' parameter
const [untilId, untilIdErr] = $.default.optional.type(ID).get(ctx.request.query.until_id);
const untilId = ctx.request.query.until_id;
if (untilId != null && typeof untilId !== 'string') {
ctx.status = 400;
return;
}
// Get 'page' parameter
const pageErr = !$.default.optional.str.or(['true', 'false']).ok(ctx.request.query.page);
const page: boolean = ctx.request.query.page === 'true';
const page = ctx.request.query.page === 'true';
// Validate parameters
if (sinceIdErr || untilIdErr || pageErr || countIf(x => x != null, [sinceId, untilId]) > 1) {
if (countIf(x => x != null, [sinceId, untilId]) > 1) {
ctx.status = 400;
return;
}
@@ -52,8 +53,8 @@ export default async (ctx: Router.RouterContext) => {
const query = makePaginationQuery(Notes.createQueryBuilder('note'), sinceId, untilId)
.andWhere('note.userId = :userId', { userId: user.id })
.andWhere(new Brackets(qb => { qb
.where(`note.visibility = 'public'`)
.orWhere(`note.visibility = 'home'`);
.where('note.visibility = \'public\'')
.orWhere('note.visibility = \'home\'');
}))
.andWhere('note.localOnly = FALSE');
@@ -76,7 +77,7 @@ export default async (ctx: Router.RouterContext) => {
notes.length ? `${partOf}?${url.query({
page: 'true',
until_id: notes[notes.length - 1].id,
})}` : undefined
})}` : undefined,
);
ctx.body = renderActivity(rendered);
@@ -85,7 +86,7 @@ export default async (ctx: Router.RouterContext) => {
// index page
const rendered = renderOrderedCollection(partOf, user.notesCount,
`${partOf}?page=true`,
`${partOf}?page=true&since_id=000000000000000000000000`
`${partOf}?page=true&since_id=000000000000000000000000`,
);
ctx.body = renderActivity(rendered);
ctx.set('Cache-Control', 'public, max-age=180');

View File

@@ -121,14 +121,14 @@ export function verifyLogin({
signature: Buffer,
challenge: string
}) {
if (clientData.type != 'webauthn.get') {
if (clientData.type !== 'webauthn.get') {
throw new Error('type is not webauthn.get');
}
if (hash(clientData.challenge).toString('hex') != challenge) {
if (hash(clientData.challenge).toString('hex') !== challenge) {
throw new Error('challenge mismatch');
}
if (clientData.origin != config.scheme + '://' + config.host) {
if (clientData.origin !== config.scheme + '://' + config.host) {
throw new Error('origin mismatch');
}
@@ -148,11 +148,11 @@ export const procedures = {
verify({ publicKey }: {publicKey: Map<number, Buffer>}) {
const negTwo = publicKey.get(-2);
if (!negTwo || negTwo.length != 32) {
if (!negTwo || negTwo.length !== 32) {
throw new Error('invalid or no -2 key given');
}
const negThree = publicKey.get(-3);
if (!negThree || negThree.length != 32) {
if (!negThree || negThree.length !== 32) {
throw new Error('invalid or no -3 key given');
}
@@ -183,7 +183,7 @@ export const procedures = {
rpIdHash: Buffer,
credentialId: Buffer,
}) {
if (attStmt.alg != -7) {
if (attStmt.alg !== -7) {
throw new Error('alg mismatch');
}
@@ -196,11 +196,11 @@ export const procedures = {
const negTwo = publicKey.get(-2);
if (!negTwo || negTwo.length != 32) {
if (!negTwo || negTwo.length !== 32) {
throw new Error('invalid or no -2 key given');
}
const negThree = publicKey.get(-3);
if (!negThree || negThree.length != 32) {
if (!negThree || negThree.length !== 32) {
throw new Error('invalid or no -3 key given');
}
@@ -263,7 +263,7 @@ export const procedures = {
.map((key: any) => PEMString(key))
.concat([GSR2]);
if (getCertSubject(certificateChain[0]).CN != 'attest.android.com') {
if (getCertSubject(certificateChain[0]).CN !== 'attest.android.com') {
throw new Error('invalid common name');
}
@@ -283,11 +283,11 @@ export const procedures = {
const negTwo = publicKey.get(-2);
if (!negTwo || negTwo.length != 32) {
if (!negTwo || negTwo.length !== 32) {
throw new Error('invalid or no -2 key given');
}
const negThree = publicKey.get(-3);
if (!negThree || negThree.length != 32) {
if (!negThree || negThree.length !== 32) {
throw new Error('invalid or no -3 key given');
}
@@ -332,11 +332,11 @@ export const procedures = {
const negTwo = publicKey.get(-2);
if (!negTwo || negTwo.length != 32) {
if (!negTwo || negTwo.length !== 32) {
throw new Error('invalid or no -2 key given');
}
const negThree = publicKey.get(-3);
if (!negThree || negThree.length != 32) {
if (!negThree || negThree.length !== 32) {
throw new Error('invalid or no -3 key given');
}
@@ -353,7 +353,7 @@ export const procedures = {
// https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-ecdaa-algorithm-v2.0-id-20180227.html#ecdaa-verify-operation
throw new Error('ECDAA-Verify is not supported');
} else {
if (attStmt.alg != -7) throw new Error('alg mismatch');
if (attStmt.alg !== -7) throw new Error('alg mismatch');
throw new Error('self attestation is not supported');
}
@@ -377,7 +377,7 @@ export const procedures = {
credentialId: Buffer
}) {
const x5c: Buffer[] = attStmt.x5c;
if (x5c.length != 1) {
if (x5c.length !== 1) {
throw new Error('x5c length does not match expectation');
}
@@ -387,11 +387,11 @@ export const procedures = {
const negTwo: Buffer = publicKey.get(-2);
if (!negTwo || negTwo.length != 32) {
if (!negTwo || negTwo.length !== 32) {
throw new Error('invalid or no -2 key given');
}
const negThree: Buffer = publicKey.get(-3);
if (!negThree || negThree.length != 32) {
if (!negThree || negThree.length !== 32) {
throw new Error('invalid or no -3 key given');
}

View File

@@ -9,7 +9,7 @@ import { publishMainStream } from '@/services/stream.js';
export default function(ctx: Koa.Context, user: ILocalUser, redirect = false) {
if (redirect) {
//#region Cookie
ctx.cookies.set('igi', user.token, {
ctx.cookies.set('igi', user.token!, {
path: '/',
// SEE: https://github.com/koajs/koa/issues/974
// When using a SSL proxy it should be configured to add the "X-Forwarded-Proto: https" header

View File

@@ -1,5 +1,6 @@
import { Schema } from '@/misc/schema.js';
import * as ep___admin_meta from './endpoints/admin/meta.js';
import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js';
import * as ep___admin_accounts_create from './endpoints/admin/accounts/create.js';
import * as ep___admin_accounts_delete from './endpoints/admin/accounts/delete.js';
@@ -201,6 +202,11 @@ import * as ep___i_unpin from './endpoints/i/unpin.js';
import * as ep___i_updateEmail from './endpoints/i/update-email.js';
import * as ep___i_update from './endpoints/i/update.js';
import * as ep___i_userGroupInvites from './endpoints/i/user-group-invites.js';
import * as ep___i_webhooks_create from './endpoints/i/webhooks/create.js';
import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js';
import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js';
import * as ep___messaging_history from './endpoints/messaging/history.js';
import * as ep___messaging_messages from './endpoints/messaging/messages.js';
import * as ep___messaging_messages_create from './endpoints/messaging/messages/create.js';
@@ -304,6 +310,7 @@ import * as ep___users_show from './endpoints/users/show.js';
import * as ep___users_stats from './endpoints/users/stats.js';
const eps = [
['admin/meta', ep___admin_meta],
['admin/abuse-user-reports', ep___admin_abuseUserReports],
['admin/accounts/create', ep___admin_accounts_create],
['admin/accounts/delete', ep___admin_accounts_delete],
@@ -505,6 +512,11 @@ const eps = [
['i/update-email', ep___i_updateEmail],
['i/update', ep___i_update],
['i/user-group-invites', ep___i_userGroupInvites],
['i/webhooks/create', ep___i_webhooks_create],
['i/webhooks/list', ep___i_webhooks_list],
['i/webhooks/show', ep___i_webhooks_show],
['i/webhooks/update', ep___i_webhooks_update],
['i/webhooks/delete', ep___i_webhooks_delete],
['messaging/history', ep___messaging_history],
['messaging/messages', ep___messaging_messages],
['messaging/messages/create', ep___messaging_messages_create],

View File

@@ -1,5 +1,6 @@
import define from '../../../define.js';
import { Announcements, AnnouncementReads } from '@/models/index.js';
import { Announcement } from '@/models/entities/announcement.js';
import define from '../../../define.js';
import { makePaginationQuery } from '../../../common/make-pagination-query.js';
export const meta = {
@@ -68,11 +69,21 @@ export default define(meta, paramDef, async (ps) => {
const announcements = await query.take(ps.limit).getMany();
const reads = new Map<Announcement, number>();
for (const announcement of announcements) {
(announcement as any).reads = await AnnouncementReads.countBy({
reads.set(announcement, await AnnouncementReads.countBy({
announcementId: announcement.id,
});
}));
}
return announcements;
return announcements.map(announcement => ({
id: announcement.id,
createdAt: announcement.createdAt.toISOString(),
updatedAt: announcement.updatedAt?.toISOString() ?? null,
title: announcement.title,
text: announcement.text,
imageUrl: announcement.imageUrl,
reads: reads.get(announcement)!,
}));
});

View File

@@ -27,7 +27,12 @@ export const paramDef = {
untilId: { type: 'string', format: 'misskey:id' },
type: { type: 'string', nullable: true, pattern: /^[a-zA-Z0-9\/\-*]+$/.toString().slice(1, -1) },
origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: "local" },
hostname: { type: 'string', nullable: true, default: null },
hostname: {
type: 'string',
nullable: true,
default: null,
description: 'The local host is represented with `null`.',
},
},
required: [],
} as const;

View File

@@ -40,6 +40,7 @@ export const meta = {
userHost: {
type: 'string',
optional: false, nullable: true,
description: 'The local host is represented with `null`.',
},
md5: {
type: 'string',
@@ -151,11 +152,20 @@ export const meta = {
export const paramDef = {
type: 'object',
properties: {
fileId: { type: 'string', format: 'misskey:id' },
url: { type: 'string' },
},
required: [],
anyOf: [
{
properties: {
fileId: { type: 'string', format: 'misskey:id' },
},
required: ['fileId'],
},
{
properties: {
url: { type: 'string' },
},
required: ['url'],
},
],
} as const;
// eslint-disable-next-line import/no-default-export

View File

@@ -40,6 +40,7 @@ export const meta = {
host: {
type: 'string',
optional: false, nullable: true,
description: 'The local host is represented with `null`.',
},
url: {
type: 'string',
@@ -54,7 +55,12 @@ export const paramDef = {
type: 'object',
properties: {
query: { type: 'string', nullable: true, default: null },
host: { type: 'string', nullable: true, default: null },
host: {
type: 'string',
nullable: true,
default: null,
description: 'Use `null` to represent the local host.',
},
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },

View File

@@ -38,8 +38,9 @@ export const meta = {
optional: false, nullable: true,
},
host: {
type: 'string',
optional: false, nullable: true,
type: 'null',
optional: false,
description: 'The local host is represented with `null`. The field exists for compatibility with other API endpoints that return files.',
},
url: {
type: 'string',

View File

@@ -17,7 +17,11 @@ export const paramDef = {
ids: { type: 'array', items: {
type: 'string', format: 'misskey:id',
} },
category: { type: 'string', nullable: true },
category: {
type: 'string',
nullable: true,
description: 'Use `null` to reset the category.',
},
},
required: ['ids'],
} as const;

View File

@@ -23,7 +23,11 @@ export const paramDef = {
properties: {
id: { type: 'string', format: 'misskey:id' },
name: { type: 'string' },
category: { type: 'string', nullable: true },
category: {
type: 'string',
nullable: true,
description: 'Use `null` to reset the category.',
},
aliases: { type: 'array', items: {
type: 'string',
} },

View File

@@ -1,5 +1,6 @@
import define from '../../../define.js';
import { Users } from '@/models/index.js';
import { publishInternalEvent } from '@/services/stream.js';
export const meta = {
tags: ['admin'],

View File

@@ -1,5 +1,5 @@
import define from '../../define.js';
import { Users } from '@/models/index.js';
import define from '../../define.js';
export const meta = {
tags: ['admin'],
@@ -24,10 +24,15 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
offset: { type: 'integer', default: 0 },
sort: { type: 'string', enum: ['+follower', '-follower', '+createdAt', '-createdAt', '+updatedAt', '-updatedAt'] },
state: { type: 'string', enum: ['all', 'available', 'admin', 'moderator', 'adminOrModerator', 'silenced', 'suspended'], default: "all" },
origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: "local" },
username: { type: 'string', default: null },
hostname: { type: 'string', default: null },
state: { type: 'string', enum: ['all', 'alive', 'available', 'admin', 'moderator', 'adminOrModerator', 'silenced', 'suspended'], default: 'all' },
origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'local' },
username: { type: 'string', nullable: true, default: null },
hostname: {
type: 'string',
nullable: true,
default: null,
description: 'The local host is represented with `null`.',
},
},
required: [],
} as const;

View File

@@ -397,12 +397,14 @@ export default define(meta, paramDef, async (ps, me) => {
}
await db.transaction(async transactionalEntityManager => {
const meta = await transactionalEntityManager.findOne(Meta, {
const metas = await transactionalEntityManager.find(Meta, {
order: {
id: 'DESC',
},
});
const meta = metas[0];
if (meta) {
await transactionalEntityManager.update(Meta, meta.id, set);
} else {

View File

@@ -57,13 +57,9 @@ export default define(meta, paramDef, async (ps, user) => {
throw new ApiError(meta.errors.noSuchAntenna);
}
const antennaQuery = AntennaNotes.createQueryBuilder('joining')
.select('joining.noteId')
.where('joining.antennaId = :antennaId', { antennaId: antenna.id });
const query = makePaginationQuery(Notes.createQueryBuilder('note'),
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere(`note.id IN (${ antennaQuery.getQuery() })`)
.innerJoin(AntennaNotes.metadata.targetName, 'antennaNote', 'antennaNote.noteId = note.id')
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('user.avatar', 'avatar')
.leftJoinAndSelect('user.banner', 'banner')
@@ -75,7 +71,7 @@ export default define(meta, paramDef, async (ps, user) => {
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner')
.setParameters(antennaQuery.getParameters());
.andWhere('antennaNote.antennaId = :antennaId', { antennaId: antenna.id });
generateVisibilityQuery(query, user);
generateMutedUserQuery(query, user);

View File

@@ -20,7 +20,7 @@ export const paramDef = {
type: 'object',
properties: {
name: { type: 'string', minLength: 1, maxLength: 100 },
isPublic: { type: 'boolean' },
isPublic: { type: 'boolean', default: false },
description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 },
},
required: ['name'],

View File

@@ -57,12 +57,8 @@ export default define(meta, paramDef, async (ps, user) => {
throw new ApiError(meta.errors.noSuchClip);
}
const clipQuery = ClipNotes.createQueryBuilder('joining')
.select('joining.noteId')
.where('joining.clipId = :clipId', { clipId: clip.id });
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.andWhere(`note.id IN (${ clipQuery.getQuery() })`)
.innerJoin(ClipNotes.metadata.targetName, 'clipNote', 'clipNote.noteId = note.id')
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('user.avatar', 'avatar')
.leftJoinAndSelect('user.banner', 'banner')
@@ -74,7 +70,7 @@ export default define(meta, paramDef, async (ps, user) => {
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner')
.setParameters(clipQuery.getParameters());
.andWhere('clipNote.clipId = :clipId', { clipId: clip.id });
if (user) {
generateVisibilityQuery(query, user);

View File

@@ -48,7 +48,6 @@ export const paramDef = {
} as const;
// eslint-disable-next-line import/no-default-export
// @ts-ignore
export default define(meta, paramDef, async (ps, user, _, file, cleanup) => {
// Get 'name' parameter
let name = ps.name || file.originalname;

View File

@@ -1,5 +1,5 @@
import define from '../../../define.js';
import { DriveFiles } from '@/models/index.js';
import define from '../../../define.js';
export const meta = {
tags: ['drive'],

View File

@@ -1,7 +1,7 @@
import define from '../../../define.js';
import { ApiError } from '../../../error.js';
import { DriveFile } from '@/models/entities/drive-file.js';
import { DriveFiles, Users } from '@/models/index.js';
import define from '../../../define.js';
import { ApiError } from '../../../error.js';
export const meta = {
tags: ['drive'],
@@ -28,27 +28,30 @@ export const meta = {
code: 'ACCESS_DENIED',
id: '25b73c73-68b1-41d0-bad1-381cfdf6579f',
},
fileIdOrUrlRequired: {
message: 'fileId or url required.',
code: 'INVALID_PARAM',
id: '89674805-722c-440c-8d88-5641830dc3e4',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
fileId: { type: 'string', format: 'misskey:id' },
url: { type: 'string' },
},
required: [],
anyOf: [
{
properties: {
fileId: { type: 'string', format: 'misskey:id' },
},
required: ['fileId'],
},
{
properties: {
url: { type: 'string' },
},
required: ['url'],
},
],
} as const;
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, user) => {
let file: DriveFile | undefined;
let file: DriveFile | null = null;
if (ps.fileId) {
file = await DriveFiles.findOneBy({ id: ps.fileId });
@@ -62,8 +65,6 @@ export default define(meta, paramDef, async (ps, user) => {
thumbnailUrl: ps.url,
}],
});
} else {
throw new ApiError(meta.errors.fileIdOrUrlRequired);
}
if (file == null) {

View File

@@ -22,7 +22,7 @@ export const meta = {
export const paramDef = {
type: 'object',
properties: {
host: { type: 'string', nullable: true },
host: { type: 'string', nullable: true, description: 'Omit or use `null` to not filter by host.' },
blocked: { type: 'boolean', nullable: true },
notResponding: { type: 'boolean', nullable: true },
suspended: { type: 'boolean', nullable: true },

View File

@@ -50,10 +50,10 @@ export default define(meta, paramDef, async (ps, user) => {
const clientData = JSON.parse(ps.clientDataJSON);
if (clientData.type != 'webauthn.create') {
if (clientData.type !== 'webauthn.create') {
throw new Error('not a creation attestation');
}
if (clientData.origin != config.scheme + '://' + config.host) {
if (clientData.origin !== config.scheme + '://' + config.host) {
throw new Error('origin mismatch');
}
@@ -78,7 +78,7 @@ export default define(meta, paramDef, async (ps, user) => {
const credentialId = authData.slice(55, 55 + credentialIdLength);
const publicKeyData = authData.slice(55 + credentialIdLength);
const publicKey: Map<number, any> = await cborDecodeFirst(publicKeyData);
if (publicKey.get(3) != -7) {
if (publicKey.get(3) !== -7) {
throw new Error('alg mismatch');
}

View File

@@ -27,7 +27,7 @@ export default define(meta, paramDef, async (ps, user) => {
take: ps.limit,
skip: ps.offset,
order: {
id: ps.sort == 'asc' ? 1 : -1,
id: ps.sort === 'asc' ? 1 : -1,
},
});

View File

@@ -0,0 +1,43 @@
import define from '../../../define.js';
import { genId } from '@/misc/gen-id.js';
import { Webhooks } from '@/models/index.js';
import { publishInternalEvent } from '@/services/stream.js';
import { webhookEventTypes } from '@/models/entities/webhook.js';
export const meta = {
tags: ['webhooks'],
requireCredential: true,
kind: 'write:account',
} as const;
export const paramDef = {
type: 'object',
properties: {
name: { type: 'string', minLength: 1, maxLength: 100 },
url: { type: 'string', minLength: 1, maxLength: 1024 },
secret: { type: 'string', minLength: 1, maxLength: 1024 },
on: { type: 'array', items: {
type: 'string', enum: webhookEventTypes,
} },
},
required: ['name', 'url', 'secret', 'on'],
} as const;
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, user) => {
const webhook = await Webhooks.insert({
id: genId(),
createdAt: new Date(),
userId: user.id,
name: ps.name,
url: ps.url,
secret: ps.secret,
on: ps.on,
}).then(x => Webhooks.findOneByOrFail(x.identifiers[0]));
publishInternalEvent('webhookCreated', webhook);
return webhook;
});

View File

@@ -0,0 +1,44 @@
import define from '../../../define.js';
import { ApiError } from '../../../error.js';
import { Webhooks } from '@/models/index.js';
import { publishInternalEvent } from '@/services/stream.js';
export const meta = {
tags: ['webhooks'],
requireCredential: true,
kind: 'write:account',
errors: {
noSuchWebhook: {
message: 'No such webhook.',
code: 'NO_SUCH_WEBHOOK',
id: 'bae73e5a-5522-4965-ae19-3a8688e71d82',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
webhookId: { type: 'string', format: 'misskey:id' },
},
required: ['webhookId'],
} as const;
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, user) => {
const webhook = await Webhooks.findOneBy({
id: ps.webhookId,
userId: user.id,
});
if (webhook == null) {
throw new ApiError(meta.errors.noSuchWebhook);
}
await Webhooks.delete(webhook.id);
publishInternalEvent('webhookDeleted', webhook);
});

View File

@@ -0,0 +1,25 @@
import define from '../../../define.js';
import { Webhooks } from '@/models/index.js';
export const meta = {
tags: ['webhooks', 'account'],
requireCredential: true,
kind: 'read:account',
} as const;
export const paramDef = {
type: 'object',
properties: {},
required: [],
} as const;
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, me) => {
const webhooks = await Webhooks.findBy({
userId: me.id,
});
return webhooks;
});

View File

@@ -0,0 +1,41 @@
import define from '../../../define.js';
import { ApiError } from '../../../error.js';
import { Webhooks } from '@/models/index.js';
export const meta = {
tags: ['webhooks'],
requireCredential: true,
kind: 'read:account',
errors: {
noSuchWebhook: {
message: 'No such webhook.',
code: 'NO_SUCH_WEBHOOK',
id: '50f614d9-3047-4f7e-90d8-ad6b2d5fb098',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
webhookId: { type: 'string', format: 'misskey:id' },
},
required: ['webhookId'],
} as const;
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, user) => {
const webhook = await Webhooks.findOneBy({
id: ps.webhookId,
userId: user.id,
});
if (webhook == null) {
throw new ApiError(meta.errors.noSuchWebhook);
}
return webhook;
});

View File

@@ -0,0 +1,59 @@
import define from '../../../define.js';
import { ApiError } from '../../../error.js';
import { Webhooks } from '@/models/index.js';
import { publishInternalEvent } from '@/services/stream.js';
import { webhookEventTypes } from '@/models/entities/webhook.js';
export const meta = {
tags: ['webhooks'],
requireCredential: true,
kind: 'write:account',
errors: {
noSuchWebhook: {
message: 'No such webhook.',
code: 'NO_SUCH_WEBHOOK',
id: 'fb0fea69-da18-45b1-828d-bd4fd1612518',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
webhookId: { type: 'string', format: 'misskey:id' },
name: { type: 'string', minLength: 1, maxLength: 100 },
url: { type: 'string', minLength: 1, maxLength: 1024 },
secret: { type: 'string', minLength: 1, maxLength: 1024 },
on: { type: 'array', items: {
type: 'string', enum: webhookEventTypes,
} },
active: { type: 'boolean' },
},
required: ['webhookId', 'name', 'url', 'secret', 'on', 'active'],
} as const;
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, user) => {
const webhook = await Webhooks.findOneBy({
id: ps.webhookId,
userId: user.id,
});
if (webhook == null) {
throw new ApiError(meta.errors.noSuchWebhook);
}
await Webhooks.update(webhook.id, {
name: ps.name,
url: ps.url,
secret: ps.secret,
on: ps.on,
active: ps.active,
});
publishInternalEvent('webhookUpdated', webhook);
});

View File

@@ -47,14 +47,25 @@ export const meta = {
export const paramDef = {
type: 'object',
properties: {
userId: { type: 'string', format: 'misskey:id' },
groupId: { type: 'string', format: 'misskey:id' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
markAsRead: { type: 'boolean', default: true },
},
required: [],
anyOf: [
{
properties: {
userId: { type: 'string', format: 'misskey:id' },
},
required: ['userId'],
},
{
properties: {
groupId: { type: 'string', format: 'misskey:id' },
},
required: ['groupId'],
},
],
} as const;
// eslint-disable-next-line import/no-default-export
@@ -126,7 +137,5 @@ export default define(meta, paramDef, async (ps, user) => {
return await Promise.all(messages.map(message => MessagingMessages.pack(message, user, {
populateGroup: false,
})));
} else {
throw new Error();
}
});

View File

@@ -67,12 +67,23 @@ export const meta = {
export const paramDef = {
type: 'object',
properties: {
userId: { type: 'string', format: 'misskey:id' },
groupId: { type: 'string', format: 'misskey:id' },
text: { type: 'string', nullable: true, maxLength: 3000 },
fileId: { type: 'string', format: 'misskey:id' },
},
required: [],
anyOf: [
{
properties: {
userId: { type: 'string', format: 'misskey:id' },
},
required: ['userId'],
},
{
properties: {
groupId: { type: 'string', format: 'misskey:id' },
},
required: ['groupId'],
},
],
} as const;
// eslint-disable-next-line import/no-default-export

View File

@@ -169,6 +169,7 @@ export const meta = {
host: {
type: 'string',
optional: false, nullable: true,
description: 'The local host is represented with `null`.',
},
url: {
type: 'string',

View File

@@ -38,7 +38,11 @@ export const paramDef = {
type: 'object',
properties: {
userId: { type: 'string', format: 'misskey:id' },
expiresAt: { type: 'integer', nullable: true },
expiresAt: {
type: 'integer',
nullable: true,
description: 'A Unix Epoch timestamp that must lie in the future. `null` means an indefinite mute.',
},
},
required: ['userId'],
} as const;

View File

@@ -19,7 +19,7 @@ export const meta = {
export const paramDef = {
type: 'object',
properties: {
local: { type: 'boolean' },
local: { type: 'boolean', default: false },
reply: { type: 'boolean' },
renote: { type: 'boolean' },
withFiles: { type: 'boolean' },
@@ -52,19 +52,19 @@ export default define(meta, paramDef, async (ps) => {
query.andWhere('note.userHost IS NULL');
}
if (ps.reply != undefined) {
if (ps.reply !== undefined) {
query.andWhere(ps.reply ? 'note.replyId IS NOT NULL' : 'note.replyId IS NULL');
}
if (ps.renote != undefined) {
if (ps.renote !== undefined) {
query.andWhere(ps.renote ? 'note.renoteId IS NOT NULL' : 'note.renoteId IS NULL');
}
if (ps.withFiles != undefined) {
if (ps.withFiles !== undefined) {
query.andWhere(ps.withFiles ? `note.fileIds != '{}'` : `note.fileIds = '{}'`);
}
if (ps.poll != undefined) {
if (ps.poll !== undefined) {
query.andWhere(ps.poll ? 'note.hasPoll = TRUE' : 'note.hasPoll = FALSE');
}

View File

@@ -57,7 +57,7 @@ export default define(meta, paramDef, async (ps, user) => {
conversation.push(p);
}
if (conversation.length == ps.limit) {
if (conversation.length === ps.limit) {
return;
}

View File

@@ -1,14 +1,15 @@
import ms from 'ms';
import { In } from 'typeorm';
import create from '@/services/note/create.js';
import define from '../../define.js';
import { ApiError } from '../../error.js';
import { User } from '@/models/entities/user.js';
import { Users, DriveFiles, Notes, Channels, Blockings } from '@/models/index.js';
import { DriveFile } from '@/models/entities/drive-file.js';
import { Note } from '@/models/entities/note.js';
import { noteVisibilities } from '../../../../types.js';
import { Channel } from '@/models/entities/channel.js';
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { noteVisibilities } from '../../../../types.js';
import { ApiError } from '../../error.js';
import define from '../../define.js';
export const meta = {
tags: ['notes'],
@@ -59,12 +60,6 @@ export const meta = {
id: '3ac74a84-8fd5-4bb0-870f-01804f82ce15',
},
contentRequired: {
message: 'Content required. You need to set text, fileIds, renoteId or poll.',
code: 'CONTENT_REQUIRED',
id: '6f57e42b-c348-439b-bc45-993995cc515a',
},
cannotCreateAlreadyExpiredPoll: {
message: 'Poll is already expired.',
code: 'CANNOT_CREATE_ALREADY_EXPIRED_POLL',
@@ -88,33 +83,45 @@ export const meta = {
export const paramDef = {
type: 'object',
properties: {
visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'], default: "public" },
visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'], default: 'public' },
visibleUserIds: { type: 'array', uniqueItems: true, items: {
type: 'string', format: 'misskey:id',
} },
text: { type: 'string', nullable: true, maxLength: MAX_NOTE_TEXT_LENGTH, default: null },
text: { type: 'string', maxLength: MAX_NOTE_TEXT_LENGTH, nullable: true },
cw: { type: 'string', nullable: true, maxLength: 100 },
localOnly: { type: 'boolean', default: false },
noExtractMentions: { type: 'boolean', default: false },
noExtractHashtags: { type: 'boolean', default: false },
noExtractEmojis: { type: 'boolean', default: false },
fileIds: { type: 'array', uniqueItems: true, minItems: 1, maxItems: 16, items: {
type: 'string', format: 'misskey:id',
} },
mediaIds: { type: 'array', uniqueItems: true, minItems: 1, maxItems: 16, items: {
type: 'string', format: 'misskey:id',
} },
fileIds: {
type: 'array',
uniqueItems: true,
minItems: 1,
maxItems: 16,
items: { type: 'string', format: 'misskey:id' },
},
mediaIds: {
deprecated: true,
description: 'Use `fileIds` instead. If both are specified, this property is discarded.',
type: 'array',
uniqueItems: true,
minItems: 1,
maxItems: 16,
items: { type: 'string', format: 'misskey:id' },
},
replyId: { type: 'string', format: 'misskey:id', nullable: true },
renoteId: { type: 'string', format: 'misskey:id', nullable: true },
channelId: { type: 'string', format: 'misskey:id', nullable: true },
poll: {
type: 'object', nullable: true,
type: 'object',
nullable: true,
properties: {
choices: {
type: 'array', uniqueItems: true, minItems: 2, maxItems: 10,
items: {
type: 'string', minLength: 1, maxLength: 50,
},
type: 'array',
uniqueItems: true,
minItems: 2,
maxItems: 10,
items: { type: 'string', minLength: 1, maxLength: 50 },
},
multiple: { type: 'boolean', default: false },
expiresAt: { type: 'integer', nullable: true },
@@ -123,36 +130,62 @@ export const paramDef = {
required: ['choices'],
},
},
required: [],
anyOf: [
{
// (re)note with text, files and poll are optional
properties: {
text: { type: 'string', maxLength: MAX_NOTE_TEXT_LENGTH, nullable: false },
},
required: ['text'],
},
{
// (re)note with files, text and poll are optional
required: ['fileIds'],
},
{
// (re)note with files, text and poll are optional
required: ['mediaIds'],
},
{
// (re)note with poll, text and files are optional
properties: {
poll: { type: 'object', nullable: false },
},
required: ['poll'],
},
{
// pure renote
required: ['renoteId'],
},
],
} as const;
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, user) => {
let visibleUsers: User[] = [];
if (ps.visibleUserIds) {
visibleUsers = (await Promise.all(ps.visibleUserIds.map(id => Users.findOneBy({ id }))))
.filter(x => x != null) as User[];
visibleUsers = await Users.findBy({
id: In(ps.visibleUserIds),
});
}
let files: DriveFile[] = [];
const fileIds = ps.fileIds != null ? ps.fileIds : ps.mediaIds != null ? ps.mediaIds : null;
if (fileIds != null) {
files = (await Promise.all(fileIds.map(fileId =>
DriveFiles.findOneBy({
id: fileId,
userId: user.id,
})
))).filter(file => file != null) as DriveFile[];
files = await DriveFiles.findBy({
userId: user.id,
id: In(fileIds),
});
}
let renote: Note | null;
let renote: Note | null = null;
if (ps.renoteId != null) {
// Fetch renote to note
renote = await Notes.findOneBy({ id: ps.renoteId });
if (renote == null) {
throw new ApiError(meta.errors.noSuchRenoteTarget);
} else if (renote.renoteId && !renote.text && !renote.fileIds) {
} else if (renote.renoteId && !renote.text && !renote.fileIds && !renote.hasPoll) {
throw new ApiError(meta.errors.cannotReRenote);
}
@@ -168,17 +201,14 @@ export default define(meta, paramDef, async (ps, user) => {
}
}
let reply: Note | null;
let reply: Note | null = null;
if (ps.replyId != null) {
// Fetch reply
reply = await Notes.findOneBy({ id: ps.replyId });
if (reply == null) {
throw new ApiError(meta.errors.noSuchReplyTarget);
}
// 返信対象が引用でないRenoteだったらエラー
if (reply.renoteId && !reply.text && !reply.fileIds) {
} else if (reply.renoteId && !reply.text && !reply.fileIds && !reply.hasPoll) {
throw new ApiError(meta.errors.cannotReplyToPureRenote);
}
@@ -204,12 +234,7 @@ export default define(meta, paramDef, async (ps, user) => {
}
}
// テキストが無いかつ添付ファイルが無いかつRenoteも無いかつ投票も無かったらエラー
if (!(ps.text || files.length || renote || ps.poll)) {
throw new ApiError(meta.errors.contentRequired);
}
let channel: Channel | undefined;
let channel: Channel | null = null;
if (ps.channelId != null) {
channel = await Channels.findOneBy({ id: ps.channelId });

View File

@@ -35,7 +35,11 @@ export const meta = {
export const paramDef = {
type: 'object',
properties: {
withFiles: { type: 'boolean' },
withFiles: {
type: 'boolean',
default: false,
description: 'Only show notes that have attached files.',
},
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },

View File

@@ -48,7 +48,11 @@ export const paramDef = {
includeMyRenotes: { type: 'boolean', default: true },
includeRenotedMyNotes: { type: 'boolean', default: true },
includeLocalRenotes: { type: 'boolean', default: true },
withFiles: { type: 'boolean' },
withFiles: {
type: 'boolean',
default: false,
description: 'Only show notes that have attached files.',
},
},
required: [],
} as const;

View File

@@ -37,7 +37,11 @@ export const meta = {
export const paramDef = {
type: 'object',
properties: {
withFiles: { type: 'boolean' },
withFiles: {
type: 'boolean',
default: false,
description: 'Only show notes that have attached files.',
},
fileType: { type: 'array', items: {
type: 'string',
} },

View File

@@ -110,7 +110,7 @@ export default define(meta, paramDef, async (ps, user) => {
if (exist.length) {
if (poll.multiple) {
if (exist.some(x => x.choice == ps.choice)) {
if (exist.some(x => x.choice === ps.choice)) {
throw new ApiError(meta.errors.alreadyVoted);
}
} else {

View File

@@ -1,8 +1,8 @@
import { DeepPartial, FindOptionsWhere } from 'typeorm';
import { NoteReactions } from '@/models/index.js';
import { NoteReaction } from '@/models/entities/note-reaction.js';
import define from '../../define.js';
import { ApiError } from '../../error.js';
import { NoteReactions } from '@/models/index.js';
import { DeepPartial } from 'typeorm';
import { NoteReaction } from '@/models/entities/note-reaction.js';
export const meta = {
tags: ['notes', 'reactions'],
@@ -45,7 +45,7 @@ export const paramDef = {
export default define(meta, paramDef, async (ps, user) => {
const query = {
noteId: ps.noteId,
} as DeepPartial<NoteReaction>;
} as FindOptionsWhere<NoteReaction>;
if (ps.type) {
// ローカルリアクションはホスト名が . とされているが

View File

@@ -25,21 +25,44 @@ export const meta = {
export const paramDef = {
type: 'object',
properties: {
tag: { type: 'string' },
query: { type: 'array', items: {
type: 'array', items: {
type: 'string',
},
} },
reply: { type: 'boolean', nullable: true, default: null },
renote: { type: 'boolean', nullable: true, default: null },
withFiles: { type: 'boolean' },
withFiles: {
type: 'boolean',
default: false,
description: 'Only show notes that have attached files.',
},
poll: { type: 'boolean', nullable: true, default: null },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
},
required: [],
anyOf: [
{
properties: {
tag: { type: 'string', minLength: 1 },
},
required: ['tag'],
},
{
properties: {
query: {
type: 'array',
description: 'The outer arrays are chained with OR, the inner arrays are chained with AND.',
items: {
type: 'array',
items: {
type: 'string',
minLength: 1,
},
minItems: 1,
},
minItems: 1,
},
},
required: ['query'],
},
],
} as const;
// eslint-disable-next-line import/no-default-export

View File

@@ -35,7 +35,11 @@ export const paramDef = {
untilId: { type: 'string', format: 'misskey:id' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
offset: { type: 'integer', default: 0 },
host: { type: 'string', nullable: true },
host: {
type: 'string',
nullable: true,
description: 'The local host is represented with `null`.',
},
userId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
channelId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
},

View File

@@ -38,7 +38,11 @@ export const paramDef = {
includeMyRenotes: { type: 'boolean', default: true },
includeRenotedMyNotes: { type: 'boolean', default: true },
includeLocalRenotes: { type: 'boolean', default: true },
withFiles: { type: 'boolean' },
withFiles: {
type: 'boolean',
default: false,
description: 'Only show notes that have attached files.',
},
},
required: [],
} as const;

View File

@@ -1,12 +1,12 @@
import define from '../../define.js';
import { getNote } from '../../common/getters.js';
import { ApiError } from '../../error.js';
import { URLSearchParams } from 'node:url';
import fetch from 'node-fetch';
import config from '@/config/index.js';
import { getAgentByUrl } from '@/misc/fetch.js';
import { URLSearchParams } from 'node:url';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { Notes } from '@/models/index.js';
import { ApiError } from '../../error.js';
import { getNote } from '../../common/getters.js';
import define from '../../define.js';
export const meta = {
tags: ['notes'],
@@ -75,11 +75,17 @@ export default define(meta, paramDef, async (ps, user) => {
Accept: 'application/json, */*',
},
body: params,
timeout: 10000,
// TODO
//timeout: 10000,
agent: getAgentByUrl,
});
const json = await res.json();
const json = (await res.json()) as {
translations: {
detected_source_language: string;
text: string;
}[];
};
return {
sourceLang: json.translations[0].detected_source_language,

View File

@@ -42,7 +42,11 @@ export const paramDef = {
includeMyRenotes: { type: 'boolean', default: true },
includeRenotedMyNotes: { type: 'boolean', default: true },
includeLocalRenotes: { type: 'boolean', default: true },
withFiles: { type: 'boolean' },
withFiles: {
type: 'boolean',
default: false,
description: 'Only show notes that have attached files.',
},
},
required: ['listId'],
} as const;
@@ -59,12 +63,8 @@ export default define(meta, paramDef, async (ps, user) => {
}
//#region Construct query
const listQuery = UserListJoinings.createQueryBuilder('joining')
.select('joining.userId')
.where('joining.userListId = :userListId', { userListId: list.id });
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.andWhere(`note.userId IN (${ listQuery.getQuery() })`)
.innerJoin(UserListJoinings.metadata.targetName, 'userListJoining', 'userListJoining.userId = note.userId')
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('user.avatar', 'avatar')
.leftJoinAndSelect('user.banner', 'banner')
@@ -76,7 +76,7 @@ export default define(meta, paramDef, async (ps, user) => {
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner')
.setParameters(listQuery.getParameters());
.andWhere('userListJoining.userListId = :userListId', { userListId: list.id });
generateVisibilityQuery(query, user);

View File

@@ -1,8 +1,8 @@
import define from '../../define.js';
import { ApiError } from '../../error.js';
import { IsNull } from 'typeorm';
import { Pages, Users } from '@/models/index.js';
import { Page } from '@/models/entities/page.js';
import { IsNull } from 'typeorm';
import define from '../../define.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['pages'],
@@ -26,17 +26,26 @@ export const meta = {
export const paramDef = {
type: 'object',
properties: {
pageId: { type: 'string', format: 'misskey:id' },
name: { type: 'string' },
username: { type: 'string' },
},
required: [],
anyOf: [
{
properties: {
pageId: { type: 'string', format: 'misskey:id' },
},
required: ['pageId'],
},
{
properties: {
name: { type: 'string' },
username: { type: 'string' },
},
required: ['name', 'username'],
},
],
} as const;
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, user) => {
let page: Page | undefined;
let page: Page | null = null;
if (ps.pageId) {
page = await Pages.findOneBy({ id: ps.pageId });

View File

@@ -38,14 +38,29 @@ export const meta = {
export const paramDef = {
type: 'object',
properties: {
userId: { type: 'string', format: 'misskey:id' },
username: { type: 'string' },
host: { type: 'string', nullable: true },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
},
required: [],
anyOf: [
{
properties: {
userId: { type: 'string', format: 'misskey:id' },
},
required: ['userId'],
},
{
properties: {
username: { type: 'string' },
host: {
type: 'string',
nullable: true,
description: 'The local host is represented with `null`.',
},
},
required: ['username', 'host'],
},
],
} as const;
// eslint-disable-next-line import/no-default-export

View File

@@ -38,14 +38,29 @@ export const meta = {
export const paramDef = {
type: 'object',
properties: {
userId: { type: 'string', format: 'misskey:id' },
username: { type: 'string' },
host: { type: 'string', nullable: true },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
},
required: [],
anyOf: [
{
properties: {
userId: { type: 'string', format: 'misskey:id' },
},
required: ['userId'],
},
{
properties: {
username: { type: 'string' },
host: {
type: 'string',
nullable: true,
description: 'The local host is represented with `null`.',
},
},
required: ['username', 'host'],
},
],
} as const;
// eslint-disable-next-line import/no-default-export

View File

@@ -28,7 +28,10 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
detail: { type: 'boolean', default: true },
},
required: [],
anyOf: [
{ required: ['username'] },
{ required: ['host'] },
],
} as const;
// TODO: avatar,bannerをJOINしたいけどエラーになる

View File

@@ -23,9 +23,9 @@ export const meta = {
items: {
type: 'object',
ref: 'UserDetailed',
}
},
},
]
],
},
errors: {
@@ -46,15 +46,33 @@ export const meta = {
export const paramDef = {
type: 'object',
properties: {
userId: { type: 'string', format: 'misskey:id' },
userIds: { type: 'array', uniqueItems: true, items: {
type: 'string', format: 'misskey:id',
} },
username: { type: 'string' },
host: { type: 'string', nullable: true },
},
required: [],
anyOf: [
{
properties: {
userId: { type: 'string', format: 'misskey:id' },
},
required: ['userId'],
},
{
properties: {
userIds: { type: 'array', uniqueItems: true, items: {
type: 'string', format: 'misskey:id',
} },
},
required: ['userIds'],
},
{
properties: {
username: { type: 'string' },
host: {
type: 'string',
nullable: true,
description: 'The local host is represented with `null`.',
},
},
required: ['username'],
},
],
} as const;
// eslint-disable-next-line import/no-default-export

View File

@@ -24,17 +24,17 @@ export default async (ctx: Koa.Context) => {
ctx.body = { error };
}
if (typeof username != 'string') {
if (typeof username !== 'string') {
ctx.status = 400;
return;
}
if (typeof password != 'string') {
if (typeof password !== 'string') {
ctx.status = 400;
return;
}
if (token != null && typeof token != 'string') {
if (token != null && typeof token !== 'string') {
ctx.status = 400;
return;
}

View File

@@ -1,16 +1,16 @@
import Koa from 'koa';
import Router from '@koa/router';
import { getJson } from '@/misc/fetch.js';
import { OAuth2 } from 'oauth';
import { v4 as uuid } from 'uuid';
import { IsNull } from 'typeorm';
import { getJson } from '@/misc/fetch.js';
import config from '@/config/index.js';
import { publishMainStream } from '@/services/stream.js';
import { redisClient } from '../../../db/redis.js';
import { v4 as uuid } from 'uuid';
import signin from '../common/signin.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { Users, UserProfiles } from '@/models/index.js';
import { ILocalUser } from '@/models/entities/user.js';
import { IsNull } from 'typeorm';
import { redisClient } from '../../../db/redis.js';
import signin from '../common/signin.js';
function getUserToken(ctx: Koa.BaseContext): string | null {
return ((ctx.headers['cookie'] || '').match(/igi=(\w+)/) || [null, null])[1];
@@ -54,7 +54,7 @@ router.get('/disconnect/discord', async ctx => {
integrations: profile.integrations,
});
ctx.body = `Discordの連携を解除しました :v:`;
ctx.body = 'Discordの連携を解除しました :v:';
// Publish i updated event
publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, {
@@ -140,7 +140,7 @@ router.get('/dc/cb', async ctx => {
const code = ctx.query.code;
if (!code) {
if (!code || typeof code !== 'string') {
ctx.throw(400, 'invalid session');
return;
}
@@ -174,17 +174,17 @@ router.get('/dc/cb', async ctx => {
}
}));
const { id, username, discriminator } = await getJson('https://discord.com/api/users/@me', '*/*', 10 * 1000, {
const { id, username, discriminator } = (await getJson('https://discord.com/api/users/@me', '*/*', 10 * 1000, {
'Authorization': `Bearer ${accessToken}`,
});
})) as Record<string, unknown>;
if (!id || !username || !discriminator) {
if (typeof id !== 'string' || typeof username !== 'string' || typeof discriminator !== 'string') {
ctx.throw(400, 'invalid session');
return;
}
const profile = await UserProfiles.createQueryBuilder()
.where(`"integrations"->'discord'->>'id' = :id`, { id: id })
.where('"integrations"->\'discord\'->>\'id\' = :id', { id: id })
.andWhere('"userHost" IS NULL')
.getOne();
@@ -211,7 +211,7 @@ router.get('/dc/cb', async ctx => {
} else {
const code = ctx.query.code;
if (!code) {
if (!code || typeof code !== 'string') {
ctx.throw(400, 'invalid session');
return;
}
@@ -245,10 +245,10 @@ router.get('/dc/cb', async ctx => {
}
}));
const { id, username, discriminator } = await getJson('https://discord.com/api/users/@me', '*/*', 10 * 1000, {
const { id, username, discriminator } = (await getJson('https://discord.com/api/users/@me', '*/*', 10 * 1000, {
'Authorization': `Bearer ${accessToken}`,
});
if (!id || !username || !discriminator) {
})) as Record<string, unknown>;
if (typeof id !== 'string' || typeof username !== 'string' || typeof discriminator !== 'string') {
ctx.throw(400, 'invalid session');
return;
}

View File

@@ -1,16 +1,16 @@
import Koa from 'koa';
import Router from '@koa/router';
import { getJson } from '@/misc/fetch.js';
import { OAuth2 } from 'oauth';
import { v4 as uuid } from 'uuid';
import { IsNull } from 'typeorm';
import { getJson } from '@/misc/fetch.js';
import config from '@/config/index.js';
import { publishMainStream } from '@/services/stream.js';
import { redisClient } from '../../../db/redis.js';
import { v4 as uuid } from 'uuid';
import signin from '../common/signin.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { Users, UserProfiles } from '@/models/index.js';
import { ILocalUser } from '@/models/entities/user.js';
import { IsNull } from 'typeorm';
import { redisClient } from '../../../db/redis.js';
import signin from '../common/signin.js';
function getUserToken(ctx: Koa.BaseContext): string | null {
return ((ctx.headers['cookie'] || '').match(/igi=(\w+)/) || [null, null])[1];
@@ -54,7 +54,7 @@ router.get('/disconnect/github', async ctx => {
integrations: profile.integrations,
});
ctx.body = `GitHubの連携を解除しました :v:`;
ctx.body = 'GitHubの連携を解除しました :v:';
// Publish i updated event
publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, {
@@ -138,7 +138,7 @@ router.get('/gh/cb', async ctx => {
const code = ctx.query.code;
if (!code) {
if (!code || typeof code !== 'string') {
ctx.throw(400, 'invalid session');
return;
}
@@ -167,16 +167,16 @@ router.get('/gh/cb', async ctx => {
}
}));
const { login, id } = await getJson('https://api.github.com/user', 'application/vnd.github.v3+json', 10 * 1000, {
const { login, id } = (await getJson('https://api.github.com/user', 'application/vnd.github.v3+json', 10 * 1000, {
'Authorization': `bearer ${accessToken}`,
});
if (!login || !id) {
})) as Record<string, unknown>;
if (typeof login !== 'string' || typeof id !== 'string') {
ctx.throw(400, 'invalid session');
return;
}
const link = await UserProfiles.createQueryBuilder()
.where(`"integrations"->'github'->>'id' = :id`, { id: id })
.where('"integrations"->\'github\'->>\'id\' = :id', { id: id })
.andWhere('"userHost" IS NULL')
.getOne();
@@ -189,7 +189,7 @@ router.get('/gh/cb', async ctx => {
} else {
const code = ctx.query.code;
if (!code) {
if (!code || typeof code !== 'string') {
ctx.throw(400, 'invalid session');
return;
}
@@ -219,11 +219,11 @@ router.get('/gh/cb', async ctx => {
}
}));
const { login, id } = await getJson('https://api.github.com/user', 'application/vnd.github.v3+json', 10 * 1000, {
const { login, id } = (await getJson('https://api.github.com/user', 'application/vnd.github.v3+json', 10 * 1000, {
'Authorization': `bearer ${accessToken}`,
});
})) as Record<string, unknown>;
if (!login || !id) {
if (typeof login !== 'string' || typeof id !== 'string') {
ctx.throw(400, 'invalid session');
return;
}

View File

@@ -2,14 +2,14 @@ import Koa from 'koa';
import Router from '@koa/router';
import { v4 as uuid } from 'uuid';
import autwh from 'autwh';
import { redisClient } from '../../../db/redis.js';
import { IsNull } from 'typeorm';
import { publishMainStream } from '@/services/stream.js';
import config from '@/config/index.js';
import signin from '../common/signin.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { Users, UserProfiles } from '@/models/index.js';
import { ILocalUser } from '@/models/entities/user.js';
import { IsNull } from 'typeorm';
import signin from '../common/signin.js';
import { redisClient } from '../../../db/redis.js';
function getUserToken(ctx: Koa.BaseContext): string | null {
return ((ctx.headers['cookie'] || '').match(/igi=(\w+)/) || [null, null])[1];
@@ -53,7 +53,7 @@ router.get('/disconnect/twitter', async ctx => {
integrations: profile.integrations,
});
ctx.body = `Twitterの連携を解除しました :v:`;
ctx.body = 'Twitterの連携を解除しました :v:';
// Publish i updated event
publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, {
@@ -132,10 +132,16 @@ router.get('/tw/cb', async ctx => {
const twCtx = await get;
const result = await twAuth!.done(JSON.parse(twCtx), ctx.query.oauth_verifier);
const verifier = ctx.query.oauth_verifier;
if (!verifier || typeof verifier !== 'string') {
ctx.throw(400, 'invalid session');
return;
}
const result = await twAuth!.done(JSON.parse(twCtx), verifier);
const link = await UserProfiles.createQueryBuilder()
.where(`"integrations"->'twitter'->>'userId' = :id`, { id: result.userId })
.where('"integrations"->\'twitter\'->>\'userId\' = :id', { id: result.userId })
.andWhere('"userHost" IS NULL')
.getOne();
@@ -148,7 +154,7 @@ router.get('/tw/cb', async ctx => {
} else {
const verifier = ctx.query.oauth_verifier;
if (verifier == null) {
if (!verifier || typeof verifier !== 'string') {
ctx.throw(400, 'invalid session');
return;
}

View File

@@ -1,7 +1,7 @@
import { default as Xev } from 'xev';
import Xev from 'xev';
import Channel from '../channel.js';
const ev = new Xev.default();
const ev = new Xev();
export default class extends Channel {
public readonly chName = 'queueStats';

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