Merge branch 'develop' into swn
This commit is contained in:
21
packages/backend/.eslintrc.cjs
Normal file
21
packages/backend/.eslintrc.cjs
Normal 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'
|
||||
}
|
||||
],
|
||||
}]
|
||||
},
|
||||
};
|
@@ -1,9 +0,0 @@
|
||||
module.exports = {
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: ['./tsconfig.json'],
|
||||
},
|
||||
extends: [
|
||||
'../shared/.eslintrc.js',
|
||||
],
|
||||
};
|
4
packages/backend/.vscode/settings.json
vendored
4
packages/backend/.vscode/settings.json
vendored
@@ -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
|
||||
}
|
||||
}
|
||||
|
19
packages/backend/migration/1648548247382-webhook.js
Normal file
19
packages/backend/migration/1648548247382-webhook.js
Normal 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"`);
|
||||
}
|
||||
}
|
14
packages/backend/migration/1648816172177-webhook-2.js
Normal file
14
packages/backend/migration/1648816172177-webhook-2.js
Normal 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"`);
|
||||
}
|
||||
}
|
@@ -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"
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
declare module 'http-signature' {
|
||||
import { IncomingMessage, ClientRequest } from 'http';
|
||||
import { IncomingMessage, ClientRequest } from 'node:http';
|
||||
|
||||
interface ISignature {
|
||||
keyId: string;
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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() {
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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}`;
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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})`;
|
||||
}
|
||||
|
||||
|
49
packages/backend/src/misc/webhook-cache.ts
Normal file
49
packages/backend/src/misc/webhook-cache.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
});
|
@@ -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;
|
||||
|
||||
|
73
packages/backend/src/models/entities/webhook.ts
Normal file
73
packages/backend/src/models/entities/webhook.ts
Normal 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;
|
||||
}
|
@@ -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);
|
||||
|
@@ -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);
|
||||
},
|
||||
});
|
||||
|
@@ -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,
|
||||
});
|
||||
|
@@ -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)));
|
||||
},
|
||||
|
@@ -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',
|
||||
|
@@ -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',
|
||||
|
@@ -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);
|
||||
|
||||
|
59
packages/backend/src/queue/processors/webhook-deliver.ts
Normal file
59
packages/backend/src/queue/processors/webhook-deliver.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
};
|
@@ -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,
|
||||
];
|
||||
|
@@ -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'];
|
||||
};
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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);
|
||||
|
@@ -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`;
|
||||
}
|
||||
|
||||
|
@@ -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';
|
||||
|
@@ -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) {
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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}`,
|
||||
|
@@ -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[];
|
||||
|
@@ -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');
|
||||
|
@@ -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 {
|
||||
|
@@ -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) {
|
||||
|
@@ -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);
|
||||
|
@@ -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);
|
||||
|
@@ -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');
|
||||
|
@@ -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');
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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],
|
||||
|
@@ -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)!,
|
||||
}));
|
||||
});
|
||||
|
@@ -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;
|
||||
|
@@ -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
|
||||
|
@@ -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' },
|
||||
|
@@ -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',
|
||||
|
@@ -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;
|
||||
|
@@ -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',
|
||||
} },
|
||||
|
@@ -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'],
|
||||
|
@@ -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;
|
||||
|
@@ -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 {
|
||||
|
@@ -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);
|
||||
|
@@ -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'],
|
||||
|
@@ -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);
|
||||
|
@@ -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;
|
||||
|
@@ -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'],
|
||||
|
@@ -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) {
|
||||
|
@@ -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 },
|
||||
|
@@ -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');
|
||||
}
|
||||
|
||||
|
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
|
@@ -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;
|
||||
});
|
@@ -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);
|
||||
});
|
25
packages/backend/src/server/api/endpoints/i/webhooks/list.ts
Normal file
25
packages/backend/src/server/api/endpoints/i/webhooks/list.ts
Normal 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;
|
||||
});
|
41
packages/backend/src/server/api/endpoints/i/webhooks/show.ts
Normal file
41
packages/backend/src/server/api/endpoints/i/webhooks/show.ts
Normal 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;
|
||||
});
|
@@ -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);
|
||||
});
|
@@ -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();
|
||||
}
|
||||
});
|
||||
|
@@ -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
|
||||
|
@@ -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',
|
||||
|
@@ -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;
|
||||
|
@@ -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');
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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 });
|
||||
|
||||
|
@@ -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' },
|
||||
|
@@ -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;
|
||||
|
@@ -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',
|
||||
} },
|
||||
|
@@ -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 {
|
||||
|
@@ -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) {
|
||||
// ローカルリアクションはホスト名が . とされているが
|
||||
|
@@ -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
|
||||
|
@@ -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 },
|
||||
},
|
||||
|
@@ -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;
|
||||
|
@@ -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,
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -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 });
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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したいけどエラーになる
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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
Reference in New Issue
Block a user