Merge branch 'develop' into swn

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
import { DriveFile } from '@/models/entities/drive-file.js';
import { Note } from '@/models/entities/note';
import { User } from '@/models/entities/user.js';
import { Webhook } from '@/models/entities/webhook';
import { IActivity } from '@/remote/activitypub/type.js';
import httpSignature from 'http-signature';
@@ -46,6 +47,17 @@ export type EndedPollNotificationJobData = {
noteId: Note['id'];
};
export type WebhookDeliverJobData = {
type: string;
content: unknown;
webhookId: Webhook['id'];
userId: User['id'];
to: string;
secret: string;
createdAt: number;
eventId: string;
};
export type ThinUser = {
id: User['id'];
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,27 +1,25 @@
import * as websocket from 'websocket';
import { readNotification } from '../common/read-notification.js';
import call from '../call.js';
import readNote from '@/services/note/read.js';
import Channel from './channel.js';
import channels from './channels/index.js';
import { EventEmitter } from 'events';
import * as websocket from 'websocket';
import readNote from '@/services/note/read.js';
import { User } from '@/models/entities/user.js';
import { Channel as ChannelModel } from '@/models/entities/channel.js';
import { Users, Followings, Mutings, UserProfiles, ChannelFollowings, Blockings } from '@/models/index.js';
import { ApiError } from '../error.js';
import { AccessToken } from '@/models/entities/access-token.js';
import { UserProfile } from '@/models/entities/user-profile.js';
import { publishChannelStream, publishGroupMessagingStream, publishMessagingStream } from '@/services/stream.js';
import { UserGroup } from '@/models/entities/user-group.js';
import { StreamEventEmitter, StreamMessages } from './types.js';
import { Packed } from '@/misc/schema.js';
import { readNotification } from '../common/read-notification.js';
import channels from './channels/index.js';
import Channel from './channel.js';
import { StreamEventEmitter, StreamMessages } from './types.js';
/**
* Main stream connection
*/
export default class Connection {
public user?: User;
public userProfile?: UserProfile;
public userProfile?: UserProfile | null;
public following: Set<User['id']> = new Set();
public muting: Set<User['id']> = new Set();
public blocking: Set<User['id']> = new Set(); // "被"blocking
@@ -84,7 +82,7 @@ export default class Connection {
this.muting.delete(data.body.id);
break;
// TODO: block events
// TODO: block events
case 'followChannel':
this.followingChannels.add(data.body.id);
@@ -126,7 +124,6 @@ export default class Connection {
const { type, body } = obj;
switch (type) {
case 'api': this.onApiRequest(body); break;
case 'readNotification': this.onReadNotification(body); break;
case 'subNote': this.onSubscribeNote(body); break;
case 's': this.onSubscribeNote(body); break; // alias
@@ -183,31 +180,6 @@ export default class Connection {
}
}
/**
* APIリクエスト要求時
*/
private async onApiRequest(payload: any) {
// 新鮮なデータを利用するためにユーザーをフェッチ
const user = this.user ? await Users.findOneBy({ id: this.user.id }) : null;
const endpoint = payload.endpoint || payload.ep; // alias
// 呼び出し
call(endpoint, user, this.token, payload.data).then(res => {
this.sendMessageToWs(`api:${payload.id}`, { res });
}).catch((e: ApiError) => {
this.sendMessageToWs(`api:${payload.id}`, {
error: {
message: e.message,
code: e.code,
id: e.id,
kind: e.kind,
...(e.info ? { info: e.info } : {}),
},
});
});
}
private onReadNotification(payload: any) {
if (!payload.id) return;
readNotification(this.user!.id, [payload.id]);

View File

@@ -15,6 +15,7 @@ import { AbuseUserReport } from '@/models/entities/abuse-user-report.js';
import { Signin } from '@/models/entities/signin.js';
import { Page } from '@/models/entities/page.js';
import { Packed } from '@/misc/schema.js';
import { Webhook } from '@/models/entities/webhook';
//#region Stream type-body definitions
export interface InternalStreamTypes {
@@ -23,6 +24,9 @@ export interface InternalStreamTypes {
userChangeModeratorState: { id: User['id']; isModerator: User['isModerator']; };
userTokenRegenerated: { id: User['id']; oldToken: User['token']; newToken: User['token']; };
remoteUserUpdated: { id: User['id']; };
webhookCreated: Webhook;
webhookDeleted: Webhook;
webhookUpdated: Webhook;
antennaCreated: Antenna;
antennaDeleted: Antenna;
antennaUpdated: Antenna;

View File

@@ -1,4 +1,4 @@
import * as http from 'http';
import * as http from 'node:http';
import * as websocket from 'websocket';
import MainStreamConnection from './stream/index.js';

View File

@@ -3,30 +3,30 @@
*/
import * as fs from 'node:fs';
import * as http from 'http';
import * as http from 'node:http';
import Koa from 'koa';
import Router from '@koa/router';
import mount from 'koa-mount';
import koaLogger from 'koa-logger';
import * as slow from 'koa-slow';
import activityPub from './activitypub.js';
import nodeinfo from './nodeinfo.js';
import wellKnown from './well-known.js';
import { IsNull } from 'typeorm';
import config from '@/config/index.js';
import apiServer from './api/index.js';
import fileServer from './file/index.js';
import proxyServer from './proxy/index.js';
import webServer from './web/index.js';
import Logger from '@/services/logger.js';
import { envOption } from '../env.js';
import { UserProfiles, Users } from '@/models/index.js';
import { genIdenticon } from '@/misc/gen-identicon.js';
import { createTemp } from '@/misc/create-temp.js';
import { publishMainStream } from '@/services/stream.js';
import * as Acct from '@/misc/acct.js';
import { envOption } from '../env.js';
import activityPub from './activitypub.js';
import nodeinfo from './nodeinfo.js';
import wellKnown from './well-known.js';
import apiServer from './api/index.js';
import fileServer from './file/index.js';
import proxyServer from './proxy/index.js';
import webServer from './web/index.js';
import { initializeStreamingServer } from './api/streaming.js';
import { IsNull } from 'typeorm';
export const serverLogger = new Logger('server', 'gray', false);
@@ -81,7 +81,7 @@ router.get('/avatar/@:acct', async ctx => {
});
if (user) {
ctx.redirect(Users.getAvatarUrl(user));
ctx.redirect(Users.getAvatarUrlSync(user));
} else {
ctx.redirect('/static-assets/user-unknown.png');
}

View File

@@ -1,8 +1,8 @@
import { Feed } from 'feed';
import { In, IsNull } from 'typeorm';
import config from '@/config/index.js';
import { User } from '@/models/entities/user.js';
import { Notes, DriveFiles, UserProfiles } from '@/models/index.js';
import { In, IsNull } from 'typeorm';
import { Notes, DriveFiles, UserProfiles, Users } from '@/models/index.js';
export default async function(user: User) {
const author = {
@@ -29,7 +29,7 @@ export default async function(user: User) {
generator: 'Misskey',
description: `${user.notesCount} Notes, ${profile.ffVisibility === 'public' ? user.followingCount : '?'} Following, ${profile.ffVisibility === 'public' ? user.followersCount : '?'} Followers${profile.description ? ` · ${profile.description}` : ''}`,
link: author.link,
image: user.avatarUrl ? user.avatarUrl : undefined,
image: await Users.getAvatarUrl(user),
feedLinks: {
json: `${author.link}.json`,
atom: `${author.link}.atom`,

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