Merge branch 'develop' into swn
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
declare module 'http-signature' {
|
||||
import { IncomingMessage, ClientRequest } from 'http';
|
||||
import { IncomingMessage, ClientRequest } from 'node:http';
|
||||
|
||||
interface ISignature {
|
||||
keyId: string;
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import cluster from 'node:cluster';
|
||||
import chalk from 'chalk';
|
||||
import { default as Xev } from 'xev';
|
||||
import Xev from 'xev';
|
||||
|
||||
import Logger from '@/services/logger.js';
|
||||
import { envOption } from '../env.js';
|
||||
@@ -12,7 +12,7 @@ import { workerMain } from './worker.js';
|
||||
|
||||
const logger = new Logger('core', 'cyan');
|
||||
const clusterLogger = logger.createSubLogger('cluster', 'orange', false);
|
||||
const ev = new Xev.default();
|
||||
const ev = new Xev();
|
||||
|
||||
/**
|
||||
* Init process
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { default as Xev } from 'xev';
|
||||
import Xev from 'xev';
|
||||
import { deliverQueue, inboxQueue } from '../queue/queues.js';
|
||||
|
||||
const ev = new Xev.default();
|
||||
const ev = new Xev();
|
||||
|
||||
const interval = 10000;
|
||||
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import si from 'systeminformation';
|
||||
import { default as Xev } from 'xev';
|
||||
import Xev from 'xev';
|
||||
import * as osUtils from 'os-utils';
|
||||
|
||||
const ev = new Xev.default();
|
||||
const ev = new Xev();
|
||||
|
||||
const interval = 2000;
|
||||
|
||||
|
@@ -73,6 +73,7 @@ import { PasswordResetRequest } from '@/models/entities/password-reset-request.j
|
||||
import { UserPending } from '@/models/entities/user-pending.js';
|
||||
|
||||
import { entities as charts } from '@/services/chart/entities.js';
|
||||
import { Webhook } from '@/models/entities/webhook.js';
|
||||
|
||||
const sqlLogger = dbLogger.createSubLogger('sql', 'gray', false);
|
||||
|
||||
@@ -171,6 +172,7 @@ export const entities = [
|
||||
Ad,
|
||||
PasswordResetRequest,
|
||||
UserPending,
|
||||
Webhook,
|
||||
...charts,
|
||||
];
|
||||
|
||||
@@ -207,7 +209,11 @@ export const db = new DataSource({
|
||||
});
|
||||
|
||||
export async function initDb() {
|
||||
await db.connect();
|
||||
if (db.isInitialized) {
|
||||
// nop
|
||||
} else {
|
||||
await db.connect();
|
||||
}
|
||||
}
|
||||
|
||||
export async function resetDb() {
|
||||
|
@@ -1,33 +0,0 @@
|
||||
import { Context } from 'cafy';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
export class ID<Maybe = string> extends Context<string | (Maybe extends {} ? string : Maybe)> {
|
||||
public readonly name = 'ID';
|
||||
|
||||
constructor(optional = false, nullable = false) {
|
||||
super(optional, nullable);
|
||||
|
||||
this.push((v: any) => {
|
||||
if (typeof v !== 'string') {
|
||||
return new Error('must-be-an-id');
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public getType() {
|
||||
return super.getType('String');
|
||||
}
|
||||
|
||||
public makeOptional(): ID<undefined> {
|
||||
return new ID(true, false);
|
||||
}
|
||||
|
||||
public makeNullable(): ID<null> {
|
||||
return new ID(false, true);
|
||||
}
|
||||
|
||||
public makeOptionalNullable(): ID<undefined | null> {
|
||||
return new ID(true, true);
|
||||
}
|
||||
}
|
@@ -42,7 +42,8 @@ async function getCaptchaResponse(url: string, secret: string, response: string)
|
||||
headers: {
|
||||
'User-Agent': config.userAgent,
|
||||
},
|
||||
timeout: 10 * 1000,
|
||||
// TODO
|
||||
//timeout: 10 * 1000,
|
||||
agent: getAgentByUrl,
|
||||
}).catch(e => {
|
||||
throw `${e.message || e}`;
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import * as http from 'node:http';
|
||||
import * as https from 'node:https';
|
||||
import { URL } from 'node:url';
|
||||
import CacheableLookup from 'cacheable-lookup';
|
||||
import fetch from 'node-fetch';
|
||||
import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent';
|
||||
import config from '@/config/index.js';
|
||||
import { URL } from 'node:url';
|
||||
|
||||
export async function getJson(url: string, accept = 'application/json, */*', timeout = 10000, headers?: Record<string, string>) {
|
||||
const res = await getResponse({
|
||||
@@ -35,7 +35,7 @@ export async function getHtml(url: string, accept = 'text/html, */*', timeout =
|
||||
}
|
||||
|
||||
export async function getResponse(args: { url: string, method: string, body?: string, headers: Record<string, string>, timeout?: number, size?: number }) {
|
||||
const timeout = args?.timeout || 10 * 1000;
|
||||
const timeout = args.timeout || 10 * 1000;
|
||||
|
||||
const controller = new AbortController();
|
||||
setTimeout(() => {
|
||||
@@ -47,7 +47,7 @@ export async function getResponse(args: { url: string, method: string, body?: st
|
||||
headers: args.headers,
|
||||
body: args.body,
|
||||
timeout,
|
||||
size: args?.size || 10 * 1024 * 1024,
|
||||
size: args.size || 10 * 1024 * 1024,
|
||||
agent: getAgentByUrl,
|
||||
signal: controller.signal,
|
||||
});
|
||||
@@ -120,9 +120,9 @@ export const httpsAgent = config.proxy
|
||||
*/
|
||||
export function getAgentByUrl(url: URL, bypassProxy = false) {
|
||||
if (bypassProxy || (config.proxyBypassHosts || []).includes(url.hostname)) {
|
||||
return url.protocol == 'http:' ? _http : _https;
|
||||
return url.protocol === 'http:' ? _http : _https;
|
||||
} else {
|
||||
return url.protocol == 'http:' ? httpAgent : httpsAgent;
|
||||
return url.protocol === 'http:' ? httpAgent : httpsAgent;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -23,7 +23,7 @@ export const getNoteSummary = (note: Packed<'Note'>): string => {
|
||||
}
|
||||
|
||||
// ファイルが添付されているとき
|
||||
if ((note.files || []).length != 0) {
|
||||
if ((note.files || []).length !== 0) {
|
||||
summary += ` (📎${note.files!.length})`;
|
||||
}
|
||||
|
||||
|
49
packages/backend/src/misc/webhook-cache.ts
Normal file
49
packages/backend/src/misc/webhook-cache.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Webhooks } from '@/models/index.js';
|
||||
import { Webhook } from '@/models/entities/webhook.js';
|
||||
import { subsdcriber } from '../db/redis.js';
|
||||
|
||||
let webhooksFetched = false;
|
||||
let webhooks: Webhook[] = [];
|
||||
|
||||
export async function getActiveWebhooks() {
|
||||
if (!webhooksFetched) {
|
||||
webhooks = await Webhooks.findBy({
|
||||
active: true,
|
||||
});
|
||||
webhooksFetched = true;
|
||||
}
|
||||
|
||||
return webhooks;
|
||||
}
|
||||
|
||||
subsdcriber.on('message', async (_, data) => {
|
||||
const obj = JSON.parse(data);
|
||||
|
||||
if (obj.channel === 'internal') {
|
||||
const { type, body } = obj.message;
|
||||
switch (type) {
|
||||
case 'webhookCreated':
|
||||
if (body.active) {
|
||||
webhooks.push(body);
|
||||
}
|
||||
break;
|
||||
case 'webhookUpdated':
|
||||
if (body.active) {
|
||||
const i = webhooks.findIndex(a => a.id === body.id);
|
||||
if (i > -1) {
|
||||
webhooks[i] = body;
|
||||
} else {
|
||||
webhooks.push(body);
|
||||
}
|
||||
} else {
|
||||
webhooks = webhooks.filter(a => a.id !== body.id);
|
||||
}
|
||||
break;
|
||||
case 'webhookDeleted':
|
||||
webhooks = webhooks.filter(a => a.id !== body.id);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
@@ -1,6 +1,6 @@
|
||||
import { Entity, Column, Index, OneToOne, JoinColumn, PrimaryColumn } from 'typeorm';
|
||||
import { DriveFile } from './drive-file.js';
|
||||
import { id } from '../id.js';
|
||||
import { DriveFile } from './drive-file.js';
|
||||
|
||||
@Entity()
|
||||
@Index(['usernameLower', 'host'], { unique: true })
|
||||
@@ -207,7 +207,7 @@ export class User {
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
comment: 'Whether to show users replying to other users in the timeline'
|
||||
comment: 'Whether to show users replying to other users in the timeline',
|
||||
})
|
||||
public showTimelineReplies: boolean;
|
||||
|
||||
|
73
packages/backend/src/models/entities/webhook.ts
Normal file
73
packages/backend/src/models/entities/webhook.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { PrimaryColumn, Entity, Index, JoinColumn, Column, ManyToOne } from 'typeorm';
|
||||
import { User } from './user.js';
|
||||
import { id } from '../id.js';
|
||||
|
||||
export const webhookEventTypes = ['mention', 'unfollow', 'follow', 'followed', 'note', 'reply', 'renote', 'reaction'] as const;
|
||||
|
||||
@Entity()
|
||||
export class Webhook {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
comment: 'The created date of the Antenna.',
|
||||
})
|
||||
public createdAt: Date;
|
||||
|
||||
@Index()
|
||||
@Column({
|
||||
...id(),
|
||||
comment: 'The owner ID.',
|
||||
})
|
||||
public userId: User['id'];
|
||||
|
||||
@ManyToOne(type => User, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn()
|
||||
public user: User | null;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128,
|
||||
comment: 'The name of the Antenna.',
|
||||
})
|
||||
public name: string;
|
||||
|
||||
@Index()
|
||||
@Column('varchar', {
|
||||
length: 128, array: true, default: '{}',
|
||||
})
|
||||
public on: (typeof webhookEventTypes)[number][];
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024,
|
||||
})
|
||||
public url: string;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 1024,
|
||||
})
|
||||
public secret: string;
|
||||
|
||||
@Index()
|
||||
@Column('boolean', {
|
||||
default: true,
|
||||
})
|
||||
public active: boolean;
|
||||
|
||||
/**
|
||||
* 直近のリクエスト送信日時
|
||||
*/
|
||||
@Column('timestamp with time zone', {
|
||||
nullable: true,
|
||||
})
|
||||
public latestSentAt: Date | null;
|
||||
|
||||
/**
|
||||
* 直近のリクエスト送信時のHTTPステータスコード
|
||||
*/
|
||||
@Column('integer', {
|
||||
nullable: true,
|
||||
})
|
||||
public latestStatus: number | null;
|
||||
}
|
@@ -64,6 +64,7 @@ import { Ad } from './entities/ad.js';
|
||||
import { PasswordResetRequest } from './entities/password-reset-request.js';
|
||||
import { UserPending } from './entities/user-pending.js';
|
||||
import { InstanceRepository } from './repositories/instance.js';
|
||||
import { Webhook } from './entities/webhook.js';
|
||||
|
||||
export const Announcements = db.getRepository(Announcement);
|
||||
export const AnnouncementReads = db.getRepository(AnnouncementRead);
|
||||
@@ -125,5 +126,6 @@ export const Channels = (ChannelRepository);
|
||||
export const ChannelFollowings = db.getRepository(ChannelFollowing);
|
||||
export const ChannelNotePinings = db.getRepository(ChannelNotePining);
|
||||
export const RegistryItems = db.getRepository(RegistryItem);
|
||||
export const Webhooks = db.getRepository(Webhook);
|
||||
export const Ads = db.getRepository(Ad);
|
||||
export const PasswordResetRequests = db.getRepository(PasswordResetRequest);
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { DriveFile } from '@/models/entities/drive-file.js';
|
||||
import { Users, DriveFolders } from '../index.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
import { toPuny } from '@/misc/convert-host.js';
|
||||
import { awaitAll, Promiseable } from '@/prelude/await-all.js';
|
||||
@@ -9,6 +8,7 @@ import config from '@/config/index.js';
|
||||
import { query, appendQuery } from '@/prelude/url.js';
|
||||
import { Meta } from '@/models/entities/meta.js';
|
||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
import { Users, DriveFolders } from '../index.js';
|
||||
|
||||
type PackOptions = {
|
||||
detail?: boolean,
|
||||
@@ -111,7 +111,40 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({
|
||||
|
||||
async pack(
|
||||
src: DriveFile['id'] | DriveFile,
|
||||
options?: PackOptions
|
||||
options?: PackOptions,
|
||||
): Promise<Packed<'DriveFile'>> {
|
||||
const opts = Object.assign({
|
||||
detail: false,
|
||||
self: false,
|
||||
}, options);
|
||||
|
||||
const file = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
|
||||
|
||||
return await awaitAll<Packed<'DriveFile'>>({
|
||||
id: file.id,
|
||||
createdAt: file.createdAt.toISOString(),
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
md5: file.md5,
|
||||
size: file.size,
|
||||
isSensitive: file.isSensitive,
|
||||
blurhash: file.blurhash,
|
||||
properties: opts.self ? file.properties : this.getPublicProperties(file),
|
||||
url: opts.self ? file.url : this.getPublicUrl(file, false),
|
||||
thumbnailUrl: this.getPublicUrl(file, true),
|
||||
comment: file.comment,
|
||||
folderId: file.folderId,
|
||||
folder: opts.detail && file.folderId ? DriveFolders.pack(file.folderId, {
|
||||
detail: true,
|
||||
}) : null,
|
||||
userId: opts.withUser ? file.userId : null,
|
||||
user: (opts.withUser && file.userId) ? Users.pack(file.userId) : null,
|
||||
});
|
||||
},
|
||||
|
||||
async packNullable(
|
||||
src: DriveFile['id'] | DriveFile,
|
||||
options?: PackOptions,
|
||||
): Promise<Packed<'DriveFile'> | null> {
|
||||
const opts = Object.assign({
|
||||
detail: false,
|
||||
@@ -145,9 +178,9 @@ export const DriveFileRepository = db.getRepository(DriveFile).extend({
|
||||
|
||||
async packMany(
|
||||
files: (DriveFile['id'] | DriveFile)[],
|
||||
options?: PackOptions
|
||||
) {
|
||||
const items = await Promise.all(files.map(f => this.pack(f, options)));
|
||||
return items.filter(x => x != null);
|
||||
options?: PackOptions,
|
||||
): Promise<Packed<'DriveFile'>[]> {
|
||||
const items = await Promise.all(files.map(f => this.packNullable(f, options)));
|
||||
return items.filter((x): x is Packed<'DriveFile'> => x != null);
|
||||
},
|
||||
});
|
||||
|
@@ -1,10 +1,10 @@
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { Page } from '@/models/entities/page.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
import { Users, DriveFiles, PageLikes } from '../index.js';
|
||||
import { awaitAll } from '@/prelude/await-all.js';
|
||||
import { DriveFile } from '@/models/entities/drive-file.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
import { Users, DriveFiles, PageLikes } from '../index.js';
|
||||
|
||||
export const PageRepository = db.getRepository(Page).extend({
|
||||
async pack(
|
||||
@@ -14,7 +14,7 @@ export const PageRepository = db.getRepository(Page).extend({
|
||||
const meId = me ? me.id : null;
|
||||
const page = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
|
||||
|
||||
const attachedFiles: Promise<DriveFile | undefined>[] = [];
|
||||
const attachedFiles: Promise<DriveFile | null>[] = [];
|
||||
const collectFile = (xs: any[]) => {
|
||||
for (const x of xs) {
|
||||
if (x.type === 'image') {
|
||||
@@ -73,7 +73,7 @@ export const PageRepository = db.getRepository(Page).extend({
|
||||
script: page.script,
|
||||
eyeCatchingImageId: page.eyeCatchingImageId,
|
||||
eyeCatchingImage: page.eyeCatchingImageId ? await DriveFiles.pack(page.eyeCatchingImageId) : null,
|
||||
attachedFiles: DriveFiles.packMany(await Promise.all(attachedFiles)),
|
||||
attachedFiles: DriveFiles.packMany((await Promise.all(attachedFiles)).filter((x): x is DriveFile => x != null)),
|
||||
likedCount: page.likedCount,
|
||||
isLiked: meId ? await PageLikes.findOneBy({ pageId: page.id, userId: meId }).then(x => x != null) : undefined,
|
||||
});
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { EntityRepository, Repository, In, Not } from 'typeorm';
|
||||
import Ajv from 'ajv';
|
||||
import { User, ILocalUser, IRemoteUser } from '@/models/entities/user.js';
|
||||
import { Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, Antennas, AntennaNotes, ChannelFollowings, Instances, DriveFiles } from '../index.js';
|
||||
import config from '@/config/index.js';
|
||||
import { Packed } from '@/misc/schema.js';
|
||||
import { awaitAll, Promiseable } from '@/prelude/await-all.js';
|
||||
@@ -9,8 +8,9 @@ import { populateEmojis } from '@/misc/populate-emojis.js';
|
||||
import { getAntennas } from '@/misc/antenna-cache.js';
|
||||
import { USER_ACTIVE_THRESHOLD, USER_ONLINE_THRESHOLD } from '@/const.js';
|
||||
import { Cache } from '@/misc/cache.js';
|
||||
import { Instance } from '../entities/instance.js';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { Instance } from '../entities/instance.js';
|
||||
import { Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings, Pages, Announcements, AnnouncementReads, Antennas, AntennaNotes, ChannelFollowings, Instances, DriveFiles } from '../index.js';
|
||||
|
||||
const userInstanceCache = new Cache<Instance | null>(1000 * 60 * 60 * 3);
|
||||
|
||||
@@ -112,7 +112,7 @@ export const UserRepository = db.getRepository(User).extend({
|
||||
const joinings = await UserGroupJoinings.findBy({ userId: userId });
|
||||
|
||||
const groupQs = Promise.all(joinings.map(j => MessagingMessages.createQueryBuilder('message')
|
||||
.where(`message.groupId = :groupId`, { groupId: j.userGroupId })
|
||||
.where('message.groupId = :groupId', { groupId: j.userGroupId })
|
||||
.andWhere('message.userId != :userId', { userId: userId })
|
||||
.andWhere('NOT (:userId = ANY(message.reads))', { userId: userId })
|
||||
.andWhere('message.createdAt > :joinedAt', { joinedAt: j.createdAt }) // 自分が加入する前の会話については、未読扱いしない
|
||||
@@ -204,8 +204,18 @@ export const UserRepository = db.getRepository(User).extend({
|
||||
);
|
||||
},
|
||||
|
||||
getAvatarUrl(user: User): string {
|
||||
// TODO: avatarIdがあるがavatarがない(JOINされてない)場合のハンドリング
|
||||
async getAvatarUrl(user: User): Promise<string> {
|
||||
if (user.avatar) {
|
||||
return DriveFiles.getPublicUrl(user.avatar, true) || this.getIdenticonUrl(user.id);
|
||||
} else if (user.avatarId) {
|
||||
const avatar = await DriveFiles.findOneByOrFail({ id: user.avatarId });
|
||||
return DriveFiles.getPublicUrl(avatar, true) || this.getIdenticonUrl(user.id);
|
||||
} else {
|
||||
return this.getIdenticonUrl(user.id);
|
||||
}
|
||||
},
|
||||
|
||||
getAvatarUrlSync(user: User): string {
|
||||
if (user.avatar) {
|
||||
return DriveFiles.getPublicUrl(user.avatar, true) || this.getIdenticonUrl(user.id);
|
||||
} else {
|
||||
@@ -223,7 +233,7 @@ export const UserRepository = db.getRepository(User).extend({
|
||||
options?: {
|
||||
detail?: D,
|
||||
includeSecrets?: boolean,
|
||||
}
|
||||
},
|
||||
): Promise<IsMeAndIsUserDetailed<ExpectsMe, D>> {
|
||||
const opts = Object.assign({
|
||||
detail: false,
|
||||
@@ -274,7 +284,7 @@ export const UserRepository = db.getRepository(User).extend({
|
||||
name: user.name,
|
||||
username: user.username,
|
||||
host: user.host,
|
||||
avatarUrl: this.getAvatarUrl(user),
|
||||
avatarUrl: this.getAvatarUrlSync(user),
|
||||
avatarBlurhash: user.avatar?.blurhash || null,
|
||||
avatarColor: null, // 後方互換性のため
|
||||
isAdmin: user.isAdmin || falsy,
|
||||
@@ -283,7 +293,7 @@ export const UserRepository = db.getRepository(User).extend({
|
||||
isCat: user.isCat || falsy,
|
||||
instance: user.host ? userInstanceCache.fetch(user.host,
|
||||
() => Instances.findOneBy({ host: user.host! }),
|
||||
v => v != null
|
||||
v => v != null,
|
||||
).then(instance => instance ? {
|
||||
name: instance.name,
|
||||
softwareName: instance.softwareName,
|
||||
@@ -403,7 +413,7 @@ export const UserRepository = db.getRepository(User).extend({
|
||||
options?: {
|
||||
detail?: D,
|
||||
includeSecrets?: boolean,
|
||||
}
|
||||
},
|
||||
): Promise<IsUserDetailed<D>[]> {
|
||||
return Promise.all(users.map(u => this.pack(u, me, options)));
|
||||
},
|
||||
|
@@ -27,6 +27,7 @@ export const packedEmojiSchema = {
|
||||
host: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
description: 'The local host is represented with `null`.',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
|
@@ -21,6 +21,7 @@ export const packedUserLiteSchema = {
|
||||
type: 'string',
|
||||
nullable: true, optional: false,
|
||||
example: 'misskey.example.com',
|
||||
description: 'The local host is represented with `null`.',
|
||||
},
|
||||
avatarUrl: {
|
||||
type: 'string',
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import httpSignature from 'http-signature';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import config from '@/config/index.js';
|
||||
import { envOption } from '../env.js';
|
||||
@@ -8,13 +9,15 @@ import processInbox from './processors/inbox.js';
|
||||
import processDb from './processors/db/index.js';
|
||||
import processObjectStorage from './processors/object-storage/index.js';
|
||||
import processSystemQueue from './processors/system/index.js';
|
||||
import processWebhookDeliver from './processors/webhook-deliver.js';
|
||||
import { endedPollNotification } from './processors/ended-poll-notification.js';
|
||||
import { queueLogger } from './logger.js';
|
||||
import { DriveFile } from '@/models/entities/drive-file.js';
|
||||
import { getJobInfo } from './get-job-info.js';
|
||||
import { systemQueue, dbQueue, deliverQueue, inboxQueue, objectStorageQueue, endedPollNotificationQueue } from './queues.js';
|
||||
import { systemQueue, dbQueue, deliverQueue, inboxQueue, objectStorageQueue, endedPollNotificationQueue, webhookDeliverQueue } from './queues.js';
|
||||
import { ThinUser } from './types.js';
|
||||
import { IActivity } from '@/remote/activitypub/type.js';
|
||||
import { Webhook, webhookEventTypes } from '@/models/entities/webhook.js';
|
||||
|
||||
function renderError(e: Error): any {
|
||||
return {
|
||||
@@ -26,6 +29,7 @@ function renderError(e: Error): any {
|
||||
|
||||
const systemLogger = queueLogger.createSubLogger('system');
|
||||
const deliverLogger = queueLogger.createSubLogger('deliver');
|
||||
const webhookLogger = queueLogger.createSubLogger('webhook');
|
||||
const inboxLogger = queueLogger.createSubLogger('inbox');
|
||||
const dbLogger = queueLogger.createSubLogger('db');
|
||||
const objectStorageLogger = queueLogger.createSubLogger('objectStorage');
|
||||
@@ -70,6 +74,14 @@ objectStorageQueue
|
||||
.on('error', (job: any, err: Error) => objectStorageLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
||||
.on('stalled', (job) => objectStorageLogger.warn(`stalled id=${job.id}`));
|
||||
|
||||
webhookDeliverQueue
|
||||
.on('waiting', (jobId) => webhookLogger.debug(`waiting id=${jobId}`))
|
||||
.on('active', (job) => webhookLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||
.on('completed', (job, result) => webhookLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
|
||||
.on('failed', (job, err) => webhookLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job.data.to}`))
|
||||
.on('error', (job: any, err: Error) => webhookLogger.error(`error ${err}`, { job, e: renderError(err) }))
|
||||
.on('stalled', (job) => webhookLogger.warn(`stalled ${getJobInfo(job)} to=${job.data.to}`));
|
||||
|
||||
export function deliver(user: ThinUser, content: unknown, to: string | null) {
|
||||
if (content == null) return null;
|
||||
if (to == null) return null;
|
||||
@@ -251,12 +263,36 @@ export function createCleanRemoteFilesJob() {
|
||||
});
|
||||
}
|
||||
|
||||
export function webhookDeliver(webhook: Webhook, type: typeof webhookEventTypes[number], content: unknown) {
|
||||
const data = {
|
||||
type,
|
||||
content,
|
||||
webhookId: webhook.id,
|
||||
userId: webhook.userId,
|
||||
to: webhook.url,
|
||||
secret: webhook.secret,
|
||||
createdAt: Date.now(),
|
||||
eventId: uuid(),
|
||||
};
|
||||
|
||||
return webhookDeliverQueue.add(data, {
|
||||
attempts: 4,
|
||||
timeout: 1 * 60 * 1000, // 1min
|
||||
backoff: {
|
||||
type: 'apBackoff',
|
||||
},
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
});
|
||||
}
|
||||
|
||||
export default function() {
|
||||
if (envOption.onlyServer) return;
|
||||
|
||||
deliverQueue.process(config.deliverJobConcurrency || 128, processDeliver);
|
||||
inboxQueue.process(config.inboxJobConcurrency || 16, processInbox);
|
||||
endedPollNotificationQueue.process(endedPollNotification);
|
||||
webhookDeliverQueue.process(64, processWebhookDeliver);
|
||||
processDb(dbQueue);
|
||||
processObjectStorage(objectStorageQueue);
|
||||
|
||||
|
59
packages/backend/src/queue/processors/webhook-deliver.ts
Normal file
59
packages/backend/src/queue/processors/webhook-deliver.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { URL } from 'node:url';
|
||||
import Bull from 'bull';
|
||||
import Logger from '@/services/logger.js';
|
||||
import { WebhookDeliverJobData } from '../types.js';
|
||||
import { getResponse, StatusError } from '@/misc/fetch.js';
|
||||
import { Webhooks } from '@/models/index.js';
|
||||
import config from '@/config/index.js';
|
||||
|
||||
const logger = new Logger('webhook');
|
||||
|
||||
export default async (job: Bull.Job<WebhookDeliverJobData>) => {
|
||||
try {
|
||||
logger.debug(`delivering ${job.data.webhookId}`);
|
||||
|
||||
const res = await getResponse({
|
||||
url: job.data.to,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'User-Agent': 'Misskey-Hooks',
|
||||
'X-Misskey-Host': config.host,
|
||||
'X-Misskey-Hook-Id': job.data.webhookId,
|
||||
'X-Misskey-Hook-Secret': job.data.secret,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
hookId: job.data.webhookId,
|
||||
userId: job.data.userId,
|
||||
eventId: job.data.eventId,
|
||||
createdAt: job.data.createdAt,
|
||||
type: job.data.type,
|
||||
body: job.data.content,
|
||||
}),
|
||||
});
|
||||
|
||||
Webhooks.update({ id: job.data.webhookId }, {
|
||||
latestSentAt: new Date(),
|
||||
latestStatus: res.status,
|
||||
});
|
||||
|
||||
return 'Success';
|
||||
} catch (res) {
|
||||
Webhooks.update({ id: job.data.webhookId }, {
|
||||
latestSentAt: new Date(),
|
||||
latestStatus: res instanceof StatusError ? res.statusCode : 1,
|
||||
});
|
||||
|
||||
if (res instanceof StatusError) {
|
||||
// 4xx
|
||||
if (res.isClientError) {
|
||||
return `${res.statusCode} ${res.statusMessage}`;
|
||||
}
|
||||
|
||||
// 5xx etc.
|
||||
throw `${res.statusCode} ${res.statusMessage}`;
|
||||
} else {
|
||||
// DNS error, socket error, timeout ...
|
||||
throw res;
|
||||
}
|
||||
}
|
||||
};
|
@@ -1,6 +1,6 @@
|
||||
import config from '@/config/index.js';
|
||||
import { initialize as initializeQueue } from './initialize.js';
|
||||
import { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData, EndedPollNotificationJobData } from './types.js';
|
||||
import { DeliverJobData, InboxJobData, DbJobData, ObjectStorageJobData, EndedPollNotificationJobData, WebhookDeliverJobData } from './types.js';
|
||||
|
||||
export const systemQueue = initializeQueue<Record<string, unknown>>('system');
|
||||
export const endedPollNotificationQueue = initializeQueue<EndedPollNotificationJobData>('endedPollNotification');
|
||||
@@ -8,6 +8,7 @@ export const deliverQueue = initializeQueue<DeliverJobData>('deliver', config.de
|
||||
export const inboxQueue = initializeQueue<InboxJobData>('inbox', config.inboxJobPerSec || 16);
|
||||
export const dbQueue = initializeQueue<DbJobData>('db');
|
||||
export const objectStorageQueue = initializeQueue<ObjectStorageJobData>('objectStorage');
|
||||
export const webhookDeliverQueue = initializeQueue<WebhookDeliverJobData>('webhookDeliver', 64);
|
||||
|
||||
export const queues = [
|
||||
systemQueue,
|
||||
@@ -16,4 +17,5 @@ export const queues = [
|
||||
inboxQueue,
|
||||
dbQueue,
|
||||
objectStorageQueue,
|
||||
webhookDeliverQueue,
|
||||
];
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { DriveFile } from '@/models/entities/drive-file.js';
|
||||
import { Note } from '@/models/entities/note';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
import { Webhook } from '@/models/entities/webhook';
|
||||
import { IActivity } from '@/remote/activitypub/type.js';
|
||||
import httpSignature from 'http-signature';
|
||||
|
||||
@@ -46,6 +47,17 @@ export type EndedPollNotificationJobData = {
|
||||
noteId: Note['id'];
|
||||
};
|
||||
|
||||
export type WebhookDeliverJobData = {
|
||||
type: string;
|
||||
content: unknown;
|
||||
webhookId: Webhook['id'];
|
||||
userId: User['id'];
|
||||
to: string;
|
||||
secret: string;
|
||||
createdAt: number;
|
||||
eventId: string;
|
||||
};
|
||||
|
||||
export type ThinUser = {
|
||||
id: User['id'];
|
||||
};
|
||||
|
@@ -95,7 +95,7 @@ function genSigningString(request: Request, includeHeaders: string[]) {
|
||||
|
||||
function lcObjectKey(src: Record<string, string>) {
|
||||
const dst: Record<string, string> = {};
|
||||
for (const key of Object.keys(src).filter(x => x != '__proto__' && typeof src[x] === 'string')) dst[key.toLowerCase()] = src[key];
|
||||
for (const key of Object.keys(src).filter(x => x !== '__proto__' && typeof src[x] === 'string')) dst[key.toLowerCase()] = src[key];
|
||||
return dst;
|
||||
}
|
||||
|
||||
|
@@ -79,37 +79,46 @@ export default class DeliverManager {
|
||||
|
||||
const inboxes = new Set<string>();
|
||||
|
||||
// build inbox list
|
||||
for (const recipe of this.recipes) {
|
||||
if (isFollowers(recipe)) {
|
||||
// followers deliver
|
||||
// TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう
|
||||
// ただ、sharedInboxがnullなリモートユーザーも稀におり、その対応ができなさそう?
|
||||
const followers = await Followings.find({
|
||||
where: {
|
||||
followeeId: this.actor.id,
|
||||
followerHost: Not(IsNull()),
|
||||
},
|
||||
select: {
|
||||
followerSharedInbox: true,
|
||||
followerInbox: true,
|
||||
},
|
||||
}) as {
|
||||
followerSharedInbox: string | null;
|
||||
followerInbox: string;
|
||||
}[];
|
||||
/*
|
||||
build inbox list
|
||||
|
||||
for (const following of followers) {
|
||||
const inbox = following.followerSharedInbox || following.followerInbox;
|
||||
inboxes.add(inbox);
|
||||
}
|
||||
} else if (isDirect(recipe)) {
|
||||
// direct deliver
|
||||
const inbox = recipe.to.inbox;
|
||||
if (inbox) inboxes.add(inbox);
|
||||
Process follower recipes first to avoid duplication when processing
|
||||
direct recipes later.
|
||||
*/
|
||||
if (this.recipes.some(r => isFollowers(r))) {
|
||||
// followers deliver
|
||||
// TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう
|
||||
// ただ、sharedInboxがnullなリモートユーザーも稀におり、その対応ができなさそう?
|
||||
const followers = await Followings.find({
|
||||
where: {
|
||||
followeeId: this.actor.id,
|
||||
followerHost: Not(IsNull()),
|
||||
},
|
||||
select: {
|
||||
followerSharedInbox: true,
|
||||
followerInbox: true,
|
||||
},
|
||||
}) as {
|
||||
followerSharedInbox: string | null;
|
||||
followerInbox: string;
|
||||
}[];
|
||||
|
||||
for (const following of followers) {
|
||||
const inbox = following.followerSharedInbox || following.followerInbox;
|
||||
inboxes.add(inbox);
|
||||
}
|
||||
}
|
||||
|
||||
this.recipes.filter((recipe): recipe is IDirectRecipe =>
|
||||
// followers recipes have already been processed
|
||||
isDirect(recipe)
|
||||
// check that shared inbox has not been added yet
|
||||
&& !(recipe.to.sharedInbox && inboxes.has(recipe.to.sharedInbox))
|
||||
// check that they actually have an inbox
|
||||
&& recipe.to.inbox != null,
|
||||
)
|
||||
.forEach(recipe => inboxes.add(recipe.to.inbox!));
|
||||
|
||||
// deliver
|
||||
for (const inbox of inboxes) {
|
||||
deliver(this.actor, this.activity, inbox);
|
||||
|
@@ -18,7 +18,7 @@ export const performReadActivity = async (actor: CacheableRemoteUser, activity:
|
||||
return `skip: message not found`;
|
||||
}
|
||||
|
||||
if (actor.id != message.recipientId) {
|
||||
if (actor.id !== message.recipientId) {
|
||||
return `skip: actor is not a message recipient`;
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import unfollow from '@/services/following/delete.js';
|
||||
import cancelRequest from '@/services/following/requests/cancel.js';
|
||||
import {IAccept} from '../../type.js';
|
||||
import { IAccept } from '../../type.js';
|
||||
import { CacheableRemoteUser } from '@/models/entities/user.js';
|
||||
import { Followings } from '@/models/index.js';
|
||||
import DbResolver from '../../db-resolver.js';
|
||||
|
@@ -113,7 +113,8 @@ export class LdSignature {
|
||||
headers: {
|
||||
Accept: 'application/ld+json, application/json',
|
||||
},
|
||||
timeout: this.loderTimeout,
|
||||
// TODO
|
||||
//timeout: this.loderTimeout,
|
||||
agent: u => u.protocol === 'http:' ? httpAgent : httpsAgent,
|
||||
}).then(res => {
|
||||
if (!res.ok) {
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { toArray, unique } from '@/prelude/array.js';
|
||||
import { IObject, isMention, IApMention } from '../type.js';
|
||||
import { resolvePerson } from './person.js';
|
||||
import promiseLimit from 'promise-limit';
|
||||
import Resolver from '../resolver.js';
|
||||
import { toArray, unique } from '@/prelude/array.js';
|
||||
import { CacheableUser, User } from '@/models/entities/user.js';
|
||||
import { IObject, isMention, IApMention } from '../type.js';
|
||||
import Resolver from '../resolver.js';
|
||||
import { resolvePerson } from './person.js';
|
||||
|
||||
export async function extractApMentions(tags: IObject | IObject[] | null | undefined) {
|
||||
const hrefs = unique(extractApMentionObjects(tags).map(x => x.href as string));
|
||||
@@ -12,7 +12,7 @@ export async function extractApMentions(tags: IObject | IObject[] | null | undef
|
||||
|
||||
const limit = promiseLimit<CacheableUser | null>(2);
|
||||
const mentionedUsers = (await Promise.all(
|
||||
hrefs.map(x => limit(() => resolvePerson(x, resolver).catch(() => null)))
|
||||
hrefs.map(x => limit(() => resolvePerson(x, resolver).catch(() => null))),
|
||||
)).filter((x): x is CacheableUser => x != null);
|
||||
|
||||
return mentionedUsers;
|
||||
|
@@ -1,17 +1,8 @@
|
||||
import { URL } from 'node:url';
|
||||
import promiseLimit from 'promise-limit';
|
||||
|
||||
import $, { Context } from 'cafy';
|
||||
import config from '@/config/index.js';
|
||||
import Resolver from '../resolver.js';
|
||||
import { resolveImage } from './image.js';
|
||||
import { isCollectionOrOrderedCollection, isCollection, IActor, getApId, getOneApHrefNullable, IObject, isPropertyValue, IApPropertyValue, getApType, isActor } from '../type.js';
|
||||
import { fromHtml } from '../../../mfm/from-html.js';
|
||||
import { htmlToMfm } from '../misc/html-to-mfm.js';
|
||||
import { resolveNote, extractEmojis } from './note.js';
|
||||
import { registerOrFetchInstanceDoc } from '@/services/register-or-fetch-instance-doc.js';
|
||||
import { extractApHashtags } from './tag.js';
|
||||
import { apLogger } from '../logger.js';
|
||||
import { Note } from '@/models/entities/note.js';
|
||||
import { updateUsertags } from '@/services/update-hashtag.js';
|
||||
import { Users, Instances, DriveFiles, Followings, UserProfiles, UserPublickeys } from '@/models/index.js';
|
||||
@@ -32,6 +23,14 @@ import { StatusError } from '@/misc/fetch.js';
|
||||
import { uriPersonCache } from '@/services/user-cache.js';
|
||||
import { publishInternalEvent } from '@/services/stream.js';
|
||||
import { db } from '@/db/postgre.js';
|
||||
import { apLogger } from '../logger.js';
|
||||
import { htmlToMfm } from '../misc/html-to-mfm.js';
|
||||
import { fromHtml } from '../../../mfm/from-html.js';
|
||||
import { isCollectionOrOrderedCollection, isCollection, IActor, getApId, getOneApHrefNullable, IObject, isPropertyValue, IApPropertyValue, getApType, isActor } from '../type.js';
|
||||
import Resolver from '../resolver.js';
|
||||
import { extractApHashtags } from './tag.js';
|
||||
import { resolveNote, extractEmojis } from './note.js';
|
||||
import { resolveImage } from './image.js';
|
||||
|
||||
const logger = apLogger;
|
||||
|
||||
@@ -54,20 +53,33 @@ function validateActor(x: IObject, uri: string): IActor {
|
||||
throw new Error(`invalid Actor type '${x.type}'`);
|
||||
}
|
||||
|
||||
const validate = (name: string, value: any, validater: Context) => {
|
||||
const e = validater.test(value);
|
||||
if (e) throw new Error(`invalid Actor: ${name} ${e.message}`);
|
||||
};
|
||||
if (!(typeof x.id === 'string' && x.id.length > 0)) {
|
||||
throw new Error('invalid Actor: wrong id');
|
||||
}
|
||||
|
||||
validate('id', x.id, $.default.str.min(1));
|
||||
validate('inbox', x.inbox, $.default.str.min(1));
|
||||
validate('preferredUsername', x.preferredUsername, $.default.str.min(1).max(128).match(/^\w([\w-.]*\w)?$/));
|
||||
if (!(typeof x.inbox === 'string' && x.inbox.length > 0)) {
|
||||
throw new Error('invalid Actor: wrong inbox');
|
||||
}
|
||||
|
||||
if (!(typeof x.preferredUsername === 'string' && x.preferredUsername.length > 0 && x.preferredUsername.length <= 128 && /^\w([\w-.]*\w)?$/.test(x.preferredUsername))) {
|
||||
throw new Error('invalid Actor: wrong username');
|
||||
}
|
||||
|
||||
// These fields are only informational, and some AP software allows these
|
||||
// fields to be very long. If they are too long, we cut them off. This way
|
||||
// we can at least see these users and their activities.
|
||||
validate('name', truncate(x.name, nameLength), $.default.optional.nullable.str);
|
||||
validate('summary', truncate(x.summary, summaryLength), $.default.optional.nullable.str);
|
||||
if (x.name) {
|
||||
if (!(typeof x.name === 'string' && x.name.length > 0)) {
|
||||
throw new Error('invalid Actor: wrong name');
|
||||
}
|
||||
x.name = truncate(x.name, nameLength);
|
||||
}
|
||||
if (x.summary) {
|
||||
if (!(typeof x.summary === 'string' && x.summary.length > 0)) {
|
||||
throw new Error('invalid Actor: wrong summary');
|
||||
}
|
||||
x.summary = truncate(x.summary, summaryLength);
|
||||
}
|
||||
|
||||
const idHost = toPuny(new URL(x.id!).hostname);
|
||||
if (idHost !== expectHost) {
|
||||
@@ -271,7 +283,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
|
||||
* @param resolver Resolver
|
||||
* @param hint Hint of Person object (この値が正当なPersonの場合、Remote resolveをせずに更新に利用します)
|
||||
*/
|
||||
export async function updatePerson(uri: string, resolver?: Resolver | null, hint?: Record<string, unknown>): Promise<void> {
|
||||
export async function updatePerson(uri: string, resolver?: Resolver | null, hint?: IObject): Promise<void> {
|
||||
if (typeof uri !== 'string') throw new Error('uri is not string');
|
||||
|
||||
// URIがこのサーバーを指しているならスキップ
|
||||
@@ -289,7 +301,7 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
|
||||
|
||||
if (resolver == null) resolver = new Resolver();
|
||||
|
||||
const object = hint || await resolver.resolve(uri) as any;
|
||||
const object = hint || await resolver.resolve(uri);
|
||||
|
||||
const person = validateActor(object, uri);
|
||||
|
||||
@@ -400,10 +412,10 @@ export async function resolvePerson(uri: string, resolver?: Resolver): Promise<C
|
||||
const services: {
|
||||
[x: string]: (id: string, username: string) => any
|
||||
} = {
|
||||
'misskey:authentication:twitter': (userId, screenName) => ({ userId, screenName }),
|
||||
'misskey:authentication:github': (id, login) => ({ id, login }),
|
||||
'misskey:authentication:discord': (id, name) => $discord(id, name),
|
||||
};
|
||||
'misskey:authentication:twitter': (userId, screenName) => ({ userId, screenName }),
|
||||
'misskey:authentication:github': (id, login) => ({ id, login }),
|
||||
'misskey:authentication:discord': (id, name) => $discord(id, name),
|
||||
};
|
||||
|
||||
const $discord = (id: string, name: string) => {
|
||||
if (typeof name !== 'string') {
|
||||
@@ -461,7 +473,7 @@ export async function updateFeatured(userId: User['id']) {
|
||||
|
||||
// Resolve to (Ordered)Collection Object
|
||||
const collection = await resolver.resolveCollection(user.featured);
|
||||
if (!isCollectionOrOrderedCollection(collection)) throw new Error(`Object is not Collection or OrderedCollection`);
|
||||
if (!isCollectionOrOrderedCollection(collection)) throw new Error('Object is not Collection or OrderedCollection');
|
||||
|
||||
// Resolve to Object(may be Note) arrays
|
||||
const unresolvedItems = isCollection(collection) ? collection.items : collection.orderedItems;
|
||||
|
@@ -69,7 +69,7 @@ export async function updateQuestion(value: any) {
|
||||
const oldCount = poll.votes[poll.choices.indexOf(choice)];
|
||||
const newCount = apChoices!.filter(ap => ap.name === choice)[0].replies!.totalItems;
|
||||
|
||||
if (oldCount != newCount) {
|
||||
if (oldCount !== newCount) {
|
||||
changed = true;
|
||||
poll.votes[poll.choices.indexOf(choice)] = newCount;
|
||||
}
|
||||
|
@@ -5,7 +5,7 @@ import { getInstanceActor } from '@/services/instance-actor.js';
|
||||
|
||||
// to anonymise reporters, the reporting actor must be a system user
|
||||
// object has to be a uri or array of uris
|
||||
export const renderFlag = (user: ILocalUser, object: [string], content: string): IActivity => {
|
||||
export const renderFlag = (user: ILocalUser, object: [string], content: string) => {
|
||||
return {
|
||||
type: 'Flag',
|
||||
actor: `${config.url}/users/${user.id}`,
|
||||
|
@@ -1,15 +1,15 @@
|
||||
import renderDocument from './document.js';
|
||||
import renderHashtag from './hashtag.js';
|
||||
import renderMention from './mention.js';
|
||||
import renderEmoji from './emoji.js';
|
||||
import { In, IsNull } from 'typeorm';
|
||||
import config from '@/config/index.js';
|
||||
import toHtml from '../misc/get-note-html.js';
|
||||
import { Note, IMentionedRemoteUsers } from '@/models/entities/note.js';
|
||||
import { DriveFile } from '@/models/entities/drive-file.js';
|
||||
import { DriveFiles, Notes, Users, Emojis, Polls } from '@/models/index.js';
|
||||
import { In, IsNull } from 'typeorm';
|
||||
import { Emoji } from '@/models/entities/emoji.js';
|
||||
import { Poll } from '@/models/entities/poll.js';
|
||||
import toHtml from '../misc/get-note-html.js';
|
||||
import renderEmoji from './emoji.js';
|
||||
import renderMention from './mention.js';
|
||||
import renderHashtag from './hashtag.js';
|
||||
import renderDocument from './document.js';
|
||||
|
||||
export default async function renderNote(note: Note, dive = true, isTalk = false): Promise<Record<string, unknown>> {
|
||||
const getPromisedFiles = async (ids: string[]) => {
|
||||
@@ -83,7 +83,7 @@ export default async function renderNote(note: Note, dive = true, isTalk = false
|
||||
const files = await getPromisedFiles(note.fileIds);
|
||||
|
||||
const text = note.text;
|
||||
let poll: Poll | null;
|
||||
let poll: Poll | null = null;
|
||||
|
||||
if (note.hasPoll) {
|
||||
poll = await Polls.findOneBy({ noteId: note.id });
|
||||
@@ -159,7 +159,7 @@ export async function getEmojis(names: string[]): Promise<Emoji[]> {
|
||||
names.map(name => Emojis.findOneBy({
|
||||
name,
|
||||
host: IsNull(),
|
||||
}))
|
||||
})),
|
||||
);
|
||||
|
||||
return emojis.filter(emoji => emoji != null) as Emoji[];
|
||||
|
@@ -2,10 +2,10 @@ import config from '@/config/index.js';
|
||||
import { getJson } from '@/misc/fetch.js';
|
||||
import { ILocalUser } from '@/models/entities/user.js';
|
||||
import { getInstanceActor } from '@/services/instance-actor.js';
|
||||
import { signedGet } from './request.js';
|
||||
import { IObject, isCollectionOrOrderedCollection, ICollection, IOrderedCollection } from './type.js';
|
||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
import { extractDbHost } from '@/misc/convert-host.js';
|
||||
import { signedGet } from './request.js';
|
||||
import { IObject, isCollectionOrOrderedCollection, ICollection, IOrderedCollection } from './type.js';
|
||||
|
||||
export default class Resolver {
|
||||
private history: Set<string>;
|
||||
@@ -56,13 +56,13 @@ export default class Resolver {
|
||||
this.user = await getInstanceActor();
|
||||
}
|
||||
|
||||
const object = this.user
|
||||
const object = (this.user
|
||||
? await signedGet(value, this.user)
|
||||
: await getJson(value, 'application/activity+json, application/ld+json');
|
||||
: await getJson(value, 'application/activity+json, application/ld+json')) as IObject;
|
||||
|
||||
if (object == null || (
|
||||
Array.isArray(object['@context']) ?
|
||||
!object['@context'].includes('https://www.w3.org/ns/activitystreams') :
|
||||
!(object['@context'] as unknown[]).includes('https://www.w3.org/ns/activitystreams') :
|
||||
object['@context'] !== 'https://www.w3.org/ns/activitystreams'
|
||||
)) {
|
||||
throw new Error('invalid response');
|
||||
|
@@ -2,7 +2,7 @@ export type obj = { [x: string]: any };
|
||||
export type ApObject = IObject | string | (IObject | string)[];
|
||||
|
||||
export interface IObject {
|
||||
'@context': string | obj | obj[];
|
||||
'@context': string | string[] | obj | obj[];
|
||||
type: string | string[];
|
||||
id?: string;
|
||||
summary?: string;
|
||||
@@ -48,7 +48,7 @@ export function getOneApId(value: ApObject): string {
|
||||
export function getApId(value: string | IObject): string {
|
||||
if (typeof value === 'string') return value;
|
||||
if (typeof value.id === 'string') return value.id;
|
||||
throw new Error(`cannot detemine id`);
|
||||
throw new Error('cannot detemine id');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,7 +57,7 @@ export function getApId(value: string | IObject): string {
|
||||
export function getApType(value: IObject): string {
|
||||
if (typeof value.type === 'string') return value.type;
|
||||
if (Array.isArray(value.type) && typeof value.type[0] === 'string') return value.type[0];
|
||||
throw new Error(`cannot detect type`);
|
||||
throw new Error('cannot detect type');
|
||||
}
|
||||
|
||||
export function getOneApHrefNullable(value: ApObject | undefined): string | undefined {
|
||||
|
@@ -15,7 +15,7 @@ type IWebFinger = {
|
||||
export default async function(query: string): Promise<IWebFinger> {
|
||||
const url = genUrl(query);
|
||||
|
||||
return await getJson(url, 'application/jrd+json, application/json');
|
||||
return await getJson(url, 'application/jrd+json, application/json') as IWebFinger;
|
||||
}
|
||||
|
||||
function genUrl(query: string) {
|
||||
|
@@ -1,32 +1,26 @@
|
||||
import Router from '@koa/router';
|
||||
import { FindOptionsWhere, IsNull, LessThan } from 'typeorm';
|
||||
import config from '@/config/index.js';
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id.js';
|
||||
import * as url from '@/prelude/url.js';
|
||||
import { renderActivity } from '@/remote/activitypub/renderer/index.js';
|
||||
import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection.js';
|
||||
import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page.js';
|
||||
import renderFollowUser from '@/remote/activitypub/renderer/follow-user.js';
|
||||
import { setResponseType } from '../activitypub.js';
|
||||
import { Users, Followings, UserProfiles } from '@/models/index.js';
|
||||
import { IsNull, LessThan } from 'typeorm';
|
||||
import { Following } from '@/models/entities/following.js';
|
||||
import { setResponseType } from '../activitypub.js';
|
||||
|
||||
export default async (ctx: Router.RouterContext) => {
|
||||
const userId = ctx.params.user;
|
||||
|
||||
// Get 'cursor' parameter
|
||||
const [cursor, cursorErr] = $.default.optional.type(ID).get(ctx.request.query.cursor);
|
||||
|
||||
// Get 'page' parameter
|
||||
const pageErr = !$.default.optional.str.or(['true', 'false']).ok(ctx.request.query.page);
|
||||
const page: boolean = ctx.request.query.page === 'true';
|
||||
|
||||
// Validate parameters
|
||||
if (cursorErr || pageErr) {
|
||||
const cursor = ctx.request.query.cursor;
|
||||
if (cursor != null && typeof cursor !== 'string') {
|
||||
ctx.status = 400;
|
||||
return;
|
||||
}
|
||||
|
||||
const page = ctx.request.query.page === 'true';
|
||||
|
||||
const user = await Users.findOneBy({
|
||||
id: userId,
|
||||
host: IsNull(),
|
||||
@@ -57,7 +51,7 @@ export default async (ctx: Router.RouterContext) => {
|
||||
if (page) {
|
||||
const query = {
|
||||
followeeId: user.id,
|
||||
} as any;
|
||||
} as FindOptionsWhere<Following>;
|
||||
|
||||
// カーソルが指定されている場合
|
||||
if (cursor) {
|
||||
@@ -86,7 +80,7 @@ export default async (ctx: Router.RouterContext) => {
|
||||
inStock ? `${partOf}?${url.query({
|
||||
page: 'true',
|
||||
cursor: followings[followings.length - 1].id,
|
||||
})}` : undefined
|
||||
})}` : undefined,
|
||||
);
|
||||
|
||||
ctx.body = renderActivity(rendered);
|
||||
|
@@ -1,33 +1,26 @@
|
||||
import Router from '@koa/router';
|
||||
import { LessThan, IsNull, FindOptionsWhere } from 'typeorm';
|
||||
import config from '@/config/index.js';
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id.js';
|
||||
import * as url from '@/prelude/url.js';
|
||||
import { renderActivity } from '@/remote/activitypub/renderer/index.js';
|
||||
import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection.js';
|
||||
import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page.js';
|
||||
import renderFollowUser from '@/remote/activitypub/renderer/follow-user.js';
|
||||
import { setResponseType } from '../activitypub.js';
|
||||
import { Users, Followings, UserProfiles } from '@/models/index.js';
|
||||
import { LessThan, IsNull, FindOptionsWhere } from 'typeorm';
|
||||
import { Following } from '@/models/entities/following.js';
|
||||
import { setResponseType } from '../activitypub.js';
|
||||
|
||||
export default async (ctx: Router.RouterContext) => {
|
||||
const userId = ctx.params.user;
|
||||
|
||||
// Get 'cursor' parameter
|
||||
const [cursor, cursorErr] = $.default.optional.type(ID).get(ctx.request.query.cursor);
|
||||
|
||||
// Get 'page' parameter
|
||||
const pageErr = !$.default.optional.str.or(['true', 'false']).ok(ctx.request.query.page);
|
||||
const page: boolean = ctx.request.query.page === 'true';
|
||||
|
||||
// Validate parameters
|
||||
if (cursorErr || pageErr) {
|
||||
const cursor = ctx.request.query.cursor;
|
||||
if (cursor != null && typeof cursor !== 'string') {
|
||||
ctx.status = 400;
|
||||
return;
|
||||
}
|
||||
|
||||
const page = ctx.request.query.page === 'true';
|
||||
|
||||
const user = await Users.findOneBy({
|
||||
id: userId,
|
||||
host: IsNull(),
|
||||
@@ -87,7 +80,7 @@ export default async (ctx: Router.RouterContext) => {
|
||||
inStock ? `${partOf}?${url.query({
|
||||
page: 'true',
|
||||
cursor: followings[followings.length - 1].id,
|
||||
})}` : undefined
|
||||
})}` : undefined,
|
||||
);
|
||||
|
||||
ctx.body = renderActivity(rendered);
|
||||
|
@@ -1,36 +1,37 @@
|
||||
import Router from '@koa/router';
|
||||
import { Brackets, IsNull } from 'typeorm';
|
||||
import config from '@/config/index.js';
|
||||
import $ from 'cafy';
|
||||
import { ID } from '@/misc/cafy-id.js';
|
||||
import { renderActivity } from '@/remote/activitypub/renderer/index.js';
|
||||
import renderOrderedCollection from '@/remote/activitypub/renderer/ordered-collection.js';
|
||||
import renderOrderedCollectionPage from '@/remote/activitypub/renderer/ordered-collection-page.js';
|
||||
import { setResponseType } from '../activitypub.js';
|
||||
import renderNote from '@/remote/activitypub/renderer/note.js';
|
||||
import renderCreate from '@/remote/activitypub/renderer/create.js';
|
||||
import renderAnnounce from '@/remote/activitypub/renderer/announce.js';
|
||||
import { countIf } from '@/prelude/array.js';
|
||||
import * as url from '@/prelude/url.js';
|
||||
import { Users, Notes } from '@/models/index.js';
|
||||
import { makePaginationQuery } from '../api/common/make-pagination-query.js';
|
||||
import { Brackets, IsNull } from 'typeorm';
|
||||
import { Note } from '@/models/entities/note.js';
|
||||
import { makePaginationQuery } from '../api/common/make-pagination-query.js';
|
||||
import { setResponseType } from '../activitypub.js';
|
||||
|
||||
export default async (ctx: Router.RouterContext) => {
|
||||
const userId = ctx.params.user;
|
||||
|
||||
// Get 'sinceId' parameter
|
||||
const [sinceId, sinceIdErr] = $.default.optional.type(ID).get(ctx.request.query.since_id);
|
||||
const sinceId = ctx.request.query.since_id;
|
||||
if (sinceId != null && typeof sinceId !== 'string') {
|
||||
ctx.status = 400;
|
||||
return;
|
||||
}
|
||||
|
||||
// Get 'untilId' parameter
|
||||
const [untilId, untilIdErr] = $.default.optional.type(ID).get(ctx.request.query.until_id);
|
||||
const untilId = ctx.request.query.until_id;
|
||||
if (untilId != null && typeof untilId !== 'string') {
|
||||
ctx.status = 400;
|
||||
return;
|
||||
}
|
||||
|
||||
// Get 'page' parameter
|
||||
const pageErr = !$.default.optional.str.or(['true', 'false']).ok(ctx.request.query.page);
|
||||
const page: boolean = ctx.request.query.page === 'true';
|
||||
const page = ctx.request.query.page === 'true';
|
||||
|
||||
// Validate parameters
|
||||
if (sinceIdErr || untilIdErr || pageErr || countIf(x => x != null, [sinceId, untilId]) > 1) {
|
||||
if (countIf(x => x != null, [sinceId, untilId]) > 1) {
|
||||
ctx.status = 400;
|
||||
return;
|
||||
}
|
||||
@@ -52,8 +53,8 @@ export default async (ctx: Router.RouterContext) => {
|
||||
const query = makePaginationQuery(Notes.createQueryBuilder('note'), sinceId, untilId)
|
||||
.andWhere('note.userId = :userId', { userId: user.id })
|
||||
.andWhere(new Brackets(qb => { qb
|
||||
.where(`note.visibility = 'public'`)
|
||||
.orWhere(`note.visibility = 'home'`);
|
||||
.where('note.visibility = \'public\'')
|
||||
.orWhere('note.visibility = \'home\'');
|
||||
}))
|
||||
.andWhere('note.localOnly = FALSE');
|
||||
|
||||
@@ -76,7 +77,7 @@ export default async (ctx: Router.RouterContext) => {
|
||||
notes.length ? `${partOf}?${url.query({
|
||||
page: 'true',
|
||||
until_id: notes[notes.length - 1].id,
|
||||
})}` : undefined
|
||||
})}` : undefined,
|
||||
);
|
||||
|
||||
ctx.body = renderActivity(rendered);
|
||||
@@ -85,7 +86,7 @@ export default async (ctx: Router.RouterContext) => {
|
||||
// index page
|
||||
const rendered = renderOrderedCollection(partOf, user.notesCount,
|
||||
`${partOf}?page=true`,
|
||||
`${partOf}?page=true&since_id=000000000000000000000000`
|
||||
`${partOf}?page=true&since_id=000000000000000000000000`,
|
||||
);
|
||||
ctx.body = renderActivity(rendered);
|
||||
ctx.set('Cache-Control', 'public, max-age=180');
|
||||
|
@@ -121,14 +121,14 @@ export function verifyLogin({
|
||||
signature: Buffer,
|
||||
challenge: string
|
||||
}) {
|
||||
if (clientData.type != 'webauthn.get') {
|
||||
if (clientData.type !== 'webauthn.get') {
|
||||
throw new Error('type is not webauthn.get');
|
||||
}
|
||||
|
||||
if (hash(clientData.challenge).toString('hex') != challenge) {
|
||||
if (hash(clientData.challenge).toString('hex') !== challenge) {
|
||||
throw new Error('challenge mismatch');
|
||||
}
|
||||
if (clientData.origin != config.scheme + '://' + config.host) {
|
||||
if (clientData.origin !== config.scheme + '://' + config.host) {
|
||||
throw new Error('origin mismatch');
|
||||
}
|
||||
|
||||
@@ -148,11 +148,11 @@ export const procedures = {
|
||||
verify({ publicKey }: {publicKey: Map<number, Buffer>}) {
|
||||
const negTwo = publicKey.get(-2);
|
||||
|
||||
if (!negTwo || negTwo.length != 32) {
|
||||
if (!negTwo || negTwo.length !== 32) {
|
||||
throw new Error('invalid or no -2 key given');
|
||||
}
|
||||
const negThree = publicKey.get(-3);
|
||||
if (!negThree || negThree.length != 32) {
|
||||
if (!negThree || negThree.length !== 32) {
|
||||
throw new Error('invalid or no -3 key given');
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ export const procedures = {
|
||||
rpIdHash: Buffer,
|
||||
credentialId: Buffer,
|
||||
}) {
|
||||
if (attStmt.alg != -7) {
|
||||
if (attStmt.alg !== -7) {
|
||||
throw new Error('alg mismatch');
|
||||
}
|
||||
|
||||
@@ -196,11 +196,11 @@ export const procedures = {
|
||||
|
||||
const negTwo = publicKey.get(-2);
|
||||
|
||||
if (!negTwo || negTwo.length != 32) {
|
||||
if (!negTwo || negTwo.length !== 32) {
|
||||
throw new Error('invalid or no -2 key given');
|
||||
}
|
||||
const negThree = publicKey.get(-3);
|
||||
if (!negThree || negThree.length != 32) {
|
||||
if (!negThree || negThree.length !== 32) {
|
||||
throw new Error('invalid or no -3 key given');
|
||||
}
|
||||
|
||||
@@ -263,7 +263,7 @@ export const procedures = {
|
||||
.map((key: any) => PEMString(key))
|
||||
.concat([GSR2]);
|
||||
|
||||
if (getCertSubject(certificateChain[0]).CN != 'attest.android.com') {
|
||||
if (getCertSubject(certificateChain[0]).CN !== 'attest.android.com') {
|
||||
throw new Error('invalid common name');
|
||||
}
|
||||
|
||||
@@ -283,11 +283,11 @@ export const procedures = {
|
||||
|
||||
const negTwo = publicKey.get(-2);
|
||||
|
||||
if (!negTwo || negTwo.length != 32) {
|
||||
if (!negTwo || negTwo.length !== 32) {
|
||||
throw new Error('invalid or no -2 key given');
|
||||
}
|
||||
const negThree = publicKey.get(-3);
|
||||
if (!negThree || negThree.length != 32) {
|
||||
if (!negThree || negThree.length !== 32) {
|
||||
throw new Error('invalid or no -3 key given');
|
||||
}
|
||||
|
||||
@@ -332,11 +332,11 @@ export const procedures = {
|
||||
|
||||
const negTwo = publicKey.get(-2);
|
||||
|
||||
if (!negTwo || negTwo.length != 32) {
|
||||
if (!negTwo || negTwo.length !== 32) {
|
||||
throw new Error('invalid or no -2 key given');
|
||||
}
|
||||
const negThree = publicKey.get(-3);
|
||||
if (!negThree || negThree.length != 32) {
|
||||
if (!negThree || negThree.length !== 32) {
|
||||
throw new Error('invalid or no -3 key given');
|
||||
}
|
||||
|
||||
@@ -353,7 +353,7 @@ export const procedures = {
|
||||
// https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-ecdaa-algorithm-v2.0-id-20180227.html#ecdaa-verify-operation
|
||||
throw new Error('ECDAA-Verify is not supported');
|
||||
} else {
|
||||
if (attStmt.alg != -7) throw new Error('alg mismatch');
|
||||
if (attStmt.alg !== -7) throw new Error('alg mismatch');
|
||||
|
||||
throw new Error('self attestation is not supported');
|
||||
}
|
||||
@@ -377,7 +377,7 @@ export const procedures = {
|
||||
credentialId: Buffer
|
||||
}) {
|
||||
const x5c: Buffer[] = attStmt.x5c;
|
||||
if (x5c.length != 1) {
|
||||
if (x5c.length !== 1) {
|
||||
throw new Error('x5c length does not match expectation');
|
||||
}
|
||||
|
||||
@@ -387,11 +387,11 @@ export const procedures = {
|
||||
|
||||
const negTwo: Buffer = publicKey.get(-2);
|
||||
|
||||
if (!negTwo || negTwo.length != 32) {
|
||||
if (!negTwo || negTwo.length !== 32) {
|
||||
throw new Error('invalid or no -2 key given');
|
||||
}
|
||||
const negThree: Buffer = publicKey.get(-3);
|
||||
if (!negThree || negThree.length != 32) {
|
||||
if (!negThree || negThree.length !== 32) {
|
||||
throw new Error('invalid or no -3 key given');
|
||||
}
|
||||
|
||||
|
@@ -9,7 +9,7 @@ import { publishMainStream } from '@/services/stream.js';
|
||||
export default function(ctx: Koa.Context, user: ILocalUser, redirect = false) {
|
||||
if (redirect) {
|
||||
//#region Cookie
|
||||
ctx.cookies.set('igi', user.token, {
|
||||
ctx.cookies.set('igi', user.token!, {
|
||||
path: '/',
|
||||
// SEE: https://github.com/koajs/koa/issues/974
|
||||
// When using a SSL proxy it should be configured to add the "X-Forwarded-Proto: https" header
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { Schema } from '@/misc/schema.js';
|
||||
|
||||
import * as ep___admin_meta from './endpoints/admin/meta.js';
|
||||
import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js';
|
||||
import * as ep___admin_accounts_create from './endpoints/admin/accounts/create.js';
|
||||
import * as ep___admin_accounts_delete from './endpoints/admin/accounts/delete.js';
|
||||
@@ -201,6 +202,11 @@ import * as ep___i_unpin from './endpoints/i/unpin.js';
|
||||
import * as ep___i_updateEmail from './endpoints/i/update-email.js';
|
||||
import * as ep___i_update from './endpoints/i/update.js';
|
||||
import * as ep___i_userGroupInvites from './endpoints/i/user-group-invites.js';
|
||||
import * as ep___i_webhooks_create from './endpoints/i/webhooks/create.js';
|
||||
import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
|
||||
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
|
||||
import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js';
|
||||
import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js';
|
||||
import * as ep___messaging_history from './endpoints/messaging/history.js';
|
||||
import * as ep___messaging_messages from './endpoints/messaging/messages.js';
|
||||
import * as ep___messaging_messages_create from './endpoints/messaging/messages/create.js';
|
||||
@@ -304,6 +310,7 @@ import * as ep___users_show from './endpoints/users/show.js';
|
||||
import * as ep___users_stats from './endpoints/users/stats.js';
|
||||
|
||||
const eps = [
|
||||
['admin/meta', ep___admin_meta],
|
||||
['admin/abuse-user-reports', ep___admin_abuseUserReports],
|
||||
['admin/accounts/create', ep___admin_accounts_create],
|
||||
['admin/accounts/delete', ep___admin_accounts_delete],
|
||||
@@ -505,6 +512,11 @@ const eps = [
|
||||
['i/update-email', ep___i_updateEmail],
|
||||
['i/update', ep___i_update],
|
||||
['i/user-group-invites', ep___i_userGroupInvites],
|
||||
['i/webhooks/create', ep___i_webhooks_create],
|
||||
['i/webhooks/list', ep___i_webhooks_list],
|
||||
['i/webhooks/show', ep___i_webhooks_show],
|
||||
['i/webhooks/update', ep___i_webhooks_update],
|
||||
['i/webhooks/delete', ep___i_webhooks_delete],
|
||||
['messaging/history', ep___messaging_history],
|
||||
['messaging/messages', ep___messaging_messages],
|
||||
['messaging/messages/create', ep___messaging_messages_create],
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import define from '../../../define.js';
|
||||
import { Announcements, AnnouncementReads } from '@/models/index.js';
|
||||
import { Announcement } from '@/models/entities/announcement.js';
|
||||
import define from '../../../define.js';
|
||||
import { makePaginationQuery } from '../../../common/make-pagination-query.js';
|
||||
|
||||
export const meta = {
|
||||
@@ -68,11 +69,21 @@ export default define(meta, paramDef, async (ps) => {
|
||||
|
||||
const announcements = await query.take(ps.limit).getMany();
|
||||
|
||||
const reads = new Map<Announcement, number>();
|
||||
|
||||
for (const announcement of announcements) {
|
||||
(announcement as any).reads = await AnnouncementReads.countBy({
|
||||
reads.set(announcement, await AnnouncementReads.countBy({
|
||||
announcementId: announcement.id,
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
return announcements;
|
||||
return announcements.map(announcement => ({
|
||||
id: announcement.id,
|
||||
createdAt: announcement.createdAt.toISOString(),
|
||||
updatedAt: announcement.updatedAt?.toISOString() ?? null,
|
||||
title: announcement.title,
|
||||
text: announcement.text,
|
||||
imageUrl: announcement.imageUrl,
|
||||
reads: reads.get(announcement)!,
|
||||
}));
|
||||
});
|
||||
|
@@ -27,7 +27,12 @@ export const paramDef = {
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
type: { type: 'string', nullable: true, pattern: /^[a-zA-Z0-9\/\-*]+$/.toString().slice(1, -1) },
|
||||
origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: "local" },
|
||||
hostname: { type: 'string', nullable: true, default: null },
|
||||
hostname: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
default: null,
|
||||
description: 'The local host is represented with `null`.',
|
||||
},
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
@@ -40,6 +40,7 @@ export const meta = {
|
||||
userHost: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
description: 'The local host is represented with `null`.',
|
||||
},
|
||||
md5: {
|
||||
type: 'string',
|
||||
@@ -151,11 +152,20 @@ export const meta = {
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
fileId: { type: 'string', format: 'misskey:id' },
|
||||
url: { type: 'string' },
|
||||
},
|
||||
required: [],
|
||||
anyOf: [
|
||||
{
|
||||
properties: {
|
||||
fileId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['fileId'],
|
||||
},
|
||||
{
|
||||
properties: {
|
||||
url: { type: 'string' },
|
||||
},
|
||||
required: ['url'],
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
|
@@ -40,6 +40,7 @@ export const meta = {
|
||||
host: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
description: 'The local host is represented with `null`.',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
@@ -54,7 +55,12 @@ export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: { type: 'string', nullable: true, default: null },
|
||||
host: { type: 'string', nullable: true, default: null },
|
||||
host: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
default: null,
|
||||
description: 'Use `null` to represent the local host.',
|
||||
},
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
|
@@ -38,8 +38,9 @@ export const meta = {
|
||||
optional: false, nullable: true,
|
||||
},
|
||||
host: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
type: 'null',
|
||||
optional: false,
|
||||
description: 'The local host is represented with `null`. The field exists for compatibility with other API endpoints that return files.',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
|
@@ -17,7 +17,11 @@ export const paramDef = {
|
||||
ids: { type: 'array', items: {
|
||||
type: 'string', format: 'misskey:id',
|
||||
} },
|
||||
category: { type: 'string', nullable: true },
|
||||
category: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
description: 'Use `null` to reset the category.',
|
||||
},
|
||||
},
|
||||
required: ['ids'],
|
||||
} as const;
|
||||
|
@@ -23,7 +23,11 @@ export const paramDef = {
|
||||
properties: {
|
||||
id: { type: 'string', format: 'misskey:id' },
|
||||
name: { type: 'string' },
|
||||
category: { type: 'string', nullable: true },
|
||||
category: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
description: 'Use `null` to reset the category.',
|
||||
},
|
||||
aliases: { type: 'array', items: {
|
||||
type: 'string',
|
||||
} },
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import define from '../../../define.js';
|
||||
import { Users } from '@/models/index.js';
|
||||
import { publishInternalEvent } from '@/services/stream.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import define from '../../define.js';
|
||||
import { Users } from '@/models/index.js';
|
||||
import define from '../../define.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
@@ -24,10 +24,15 @@ export const paramDef = {
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
offset: { type: 'integer', default: 0 },
|
||||
sort: { type: 'string', enum: ['+follower', '-follower', '+createdAt', '-createdAt', '+updatedAt', '-updatedAt'] },
|
||||
state: { type: 'string', enum: ['all', 'available', 'admin', 'moderator', 'adminOrModerator', 'silenced', 'suspended'], default: "all" },
|
||||
origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: "local" },
|
||||
username: { type: 'string', default: null },
|
||||
hostname: { type: 'string', default: null },
|
||||
state: { type: 'string', enum: ['all', 'alive', 'available', 'admin', 'moderator', 'adminOrModerator', 'silenced', 'suspended'], default: 'all' },
|
||||
origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'local' },
|
||||
username: { type: 'string', nullable: true, default: null },
|
||||
hostname: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
default: null,
|
||||
description: 'The local host is represented with `null`.',
|
||||
},
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
@@ -397,12 +397,14 @@ export default define(meta, paramDef, async (ps, me) => {
|
||||
}
|
||||
|
||||
await db.transaction(async transactionalEntityManager => {
|
||||
const meta = await transactionalEntityManager.findOne(Meta, {
|
||||
const metas = await transactionalEntityManager.find(Meta, {
|
||||
order: {
|
||||
id: 'DESC',
|
||||
},
|
||||
});
|
||||
|
||||
const meta = metas[0];
|
||||
|
||||
if (meta) {
|
||||
await transactionalEntityManager.update(Meta, meta.id, set);
|
||||
} else {
|
||||
|
@@ -57,13 +57,9 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||
throw new ApiError(meta.errors.noSuchAntenna);
|
||||
}
|
||||
|
||||
const antennaQuery = AntennaNotes.createQueryBuilder('joining')
|
||||
.select('joining.noteId')
|
||||
.where('joining.antennaId = :antennaId', { antennaId: antenna.id });
|
||||
|
||||
const query = makePaginationQuery(Notes.createQueryBuilder('note'),
|
||||
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
|
||||
.andWhere(`note.id IN (${ antennaQuery.getQuery() })`)
|
||||
.innerJoin(AntennaNotes.metadata.targetName, 'antennaNote', 'antennaNote.noteId = note.id')
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('user.avatar', 'avatar')
|
||||
.leftJoinAndSelect('user.banner', 'banner')
|
||||
@@ -75,7 +71,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
|
||||
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner')
|
||||
.setParameters(antennaQuery.getParameters());
|
||||
.andWhere('antennaNote.antennaId = :antennaId', { antennaId: antenna.id });
|
||||
|
||||
generateVisibilityQuery(query, user);
|
||||
generateMutedUserQuery(query, user);
|
||||
|
@@ -20,7 +20,7 @@ export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', minLength: 1, maxLength: 100 },
|
||||
isPublic: { type: 'boolean' },
|
||||
isPublic: { type: 'boolean', default: false },
|
||||
description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 },
|
||||
},
|
||||
required: ['name'],
|
||||
|
@@ -57,12 +57,8 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||
throw new ApiError(meta.errors.noSuchClip);
|
||||
}
|
||||
|
||||
const clipQuery = ClipNotes.createQueryBuilder('joining')
|
||||
.select('joining.noteId')
|
||||
.where('joining.clipId = :clipId', { clipId: clip.id });
|
||||
|
||||
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||
.andWhere(`note.id IN (${ clipQuery.getQuery() })`)
|
||||
.innerJoin(ClipNotes.metadata.targetName, 'clipNote', 'clipNote.noteId = note.id')
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('user.avatar', 'avatar')
|
||||
.leftJoinAndSelect('user.banner', 'banner')
|
||||
@@ -74,7 +70,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
|
||||
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner')
|
||||
.setParameters(clipQuery.getParameters());
|
||||
.andWhere('clipNote.clipId = :clipId', { clipId: clip.id });
|
||||
|
||||
if (user) {
|
||||
generateVisibilityQuery(query, user);
|
||||
|
@@ -48,7 +48,6 @@ export const paramDef = {
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
// @ts-ignore
|
||||
export default define(meta, paramDef, async (ps, user, _, file, cleanup) => {
|
||||
// Get 'name' parameter
|
||||
let name = ps.name || file.originalname;
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import define from '../../../define.js';
|
||||
import { DriveFiles } from '@/models/index.js';
|
||||
import define from '../../../define.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['drive'],
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import define from '../../../define.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
import { DriveFile } from '@/models/entities/drive-file.js';
|
||||
import { DriveFiles, Users } from '@/models/index.js';
|
||||
import define from '../../../define.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['drive'],
|
||||
@@ -28,27 +28,30 @@ export const meta = {
|
||||
code: 'ACCESS_DENIED',
|
||||
id: '25b73c73-68b1-41d0-bad1-381cfdf6579f',
|
||||
},
|
||||
|
||||
fileIdOrUrlRequired: {
|
||||
message: 'fileId or url required.',
|
||||
code: 'INVALID_PARAM',
|
||||
id: '89674805-722c-440c-8d88-5641830dc3e4',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
fileId: { type: 'string', format: 'misskey:id' },
|
||||
url: { type: 'string' },
|
||||
},
|
||||
required: [],
|
||||
anyOf: [
|
||||
{
|
||||
properties: {
|
||||
fileId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['fileId'],
|
||||
},
|
||||
{
|
||||
properties: {
|
||||
url: { type: 'string' },
|
||||
},
|
||||
required: ['url'],
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
let file: DriveFile | undefined;
|
||||
let file: DriveFile | null = null;
|
||||
|
||||
if (ps.fileId) {
|
||||
file = await DriveFiles.findOneBy({ id: ps.fileId });
|
||||
@@ -62,8 +65,6 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||
thumbnailUrl: ps.url,
|
||||
}],
|
||||
});
|
||||
} else {
|
||||
throw new ApiError(meta.errors.fileIdOrUrlRequired);
|
||||
}
|
||||
|
||||
if (file == null) {
|
||||
|
@@ -22,7 +22,7 @@ export const meta = {
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
host: { type: 'string', nullable: true },
|
||||
host: { type: 'string', nullable: true, description: 'Omit or use `null` to not filter by host.' },
|
||||
blocked: { type: 'boolean', nullable: true },
|
||||
notResponding: { type: 'boolean', nullable: true },
|
||||
suspended: { type: 'boolean', nullable: true },
|
||||
|
@@ -50,10 +50,10 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||
|
||||
const clientData = JSON.parse(ps.clientDataJSON);
|
||||
|
||||
if (clientData.type != 'webauthn.create') {
|
||||
if (clientData.type !== 'webauthn.create') {
|
||||
throw new Error('not a creation attestation');
|
||||
}
|
||||
if (clientData.origin != config.scheme + '://' + config.host) {
|
||||
if (clientData.origin !== config.scheme + '://' + config.host) {
|
||||
throw new Error('origin mismatch');
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||
const credentialId = authData.slice(55, 55 + credentialIdLength);
|
||||
const publicKeyData = authData.slice(55 + credentialIdLength);
|
||||
const publicKey: Map<number, any> = await cborDecodeFirst(publicKeyData);
|
||||
if (publicKey.get(3) != -7) {
|
||||
if (publicKey.get(3) !== -7) {
|
||||
throw new Error('alg mismatch');
|
||||
}
|
||||
|
||||
|
@@ -27,7 +27,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||
take: ps.limit,
|
||||
skip: ps.offset,
|
||||
order: {
|
||||
id: ps.sort == 'asc' ? 1 : -1,
|
||||
id: ps.sort === 'asc' ? 1 : -1,
|
||||
},
|
||||
});
|
||||
|
||||
|
@@ -0,0 +1,43 @@
|
||||
import define from '../../../define.js';
|
||||
import { genId } from '@/misc/gen-id.js';
|
||||
import { Webhooks } from '@/models/index.js';
|
||||
import { publishInternalEvent } from '@/services/stream.js';
|
||||
import { webhookEventTypes } from '@/models/entities/webhook.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['webhooks'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:account',
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', minLength: 1, maxLength: 100 },
|
||||
url: { type: 'string', minLength: 1, maxLength: 1024 },
|
||||
secret: { type: 'string', minLength: 1, maxLength: 1024 },
|
||||
on: { type: 'array', items: {
|
||||
type: 'string', enum: webhookEventTypes,
|
||||
} },
|
||||
},
|
||||
required: ['name', 'url', 'secret', 'on'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const webhook = await Webhooks.insert({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
userId: user.id,
|
||||
name: ps.name,
|
||||
url: ps.url,
|
||||
secret: ps.secret,
|
||||
on: ps.on,
|
||||
}).then(x => Webhooks.findOneByOrFail(x.identifiers[0]));
|
||||
|
||||
publishInternalEvent('webhookCreated', webhook);
|
||||
|
||||
return webhook;
|
||||
});
|
@@ -0,0 +1,44 @@
|
||||
import define from '../../../define.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
import { Webhooks } from '@/models/index.js';
|
||||
import { publishInternalEvent } from '@/services/stream.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['webhooks'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
errors: {
|
||||
noSuchWebhook: {
|
||||
message: 'No such webhook.',
|
||||
code: 'NO_SUCH_WEBHOOK',
|
||||
id: 'bae73e5a-5522-4965-ae19-3a8688e71d82',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
webhookId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['webhookId'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const webhook = await Webhooks.findOneBy({
|
||||
id: ps.webhookId,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (webhook == null) {
|
||||
throw new ApiError(meta.errors.noSuchWebhook);
|
||||
}
|
||||
|
||||
await Webhooks.delete(webhook.id);
|
||||
|
||||
publishInternalEvent('webhookDeleted', webhook);
|
||||
});
|
25
packages/backend/src/server/api/endpoints/i/webhooks/list.ts
Normal file
25
packages/backend/src/server/api/endpoints/i/webhooks/list.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import define from '../../../define.js';
|
||||
import { Webhooks } from '@/models/index.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['webhooks', 'account'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:account',
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, me) => {
|
||||
const webhooks = await Webhooks.findBy({
|
||||
userId: me.id,
|
||||
});
|
||||
|
||||
return webhooks;
|
||||
});
|
41
packages/backend/src/server/api/endpoints/i/webhooks/show.ts
Normal file
41
packages/backend/src/server/api/endpoints/i/webhooks/show.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import define from '../../../define.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
import { Webhooks } from '@/models/index.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['webhooks'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'read:account',
|
||||
|
||||
errors: {
|
||||
noSuchWebhook: {
|
||||
message: 'No such webhook.',
|
||||
code: 'NO_SUCH_WEBHOOK',
|
||||
id: '50f614d9-3047-4f7e-90d8-ad6b2d5fb098',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
webhookId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['webhookId'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const webhook = await Webhooks.findOneBy({
|
||||
id: ps.webhookId,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (webhook == null) {
|
||||
throw new ApiError(meta.errors.noSuchWebhook);
|
||||
}
|
||||
|
||||
return webhook;
|
||||
});
|
@@ -0,0 +1,59 @@
|
||||
import define from '../../../define.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
import { Webhooks } from '@/models/index.js';
|
||||
import { publishInternalEvent } from '@/services/stream.js';
|
||||
import { webhookEventTypes } from '@/models/entities/webhook.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['webhooks'],
|
||||
|
||||
requireCredential: true,
|
||||
|
||||
kind: 'write:account',
|
||||
|
||||
errors: {
|
||||
noSuchWebhook: {
|
||||
message: 'No such webhook.',
|
||||
code: 'NO_SUCH_WEBHOOK',
|
||||
id: 'fb0fea69-da18-45b1-828d-bd4fd1612518',
|
||||
},
|
||||
},
|
||||
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
webhookId: { type: 'string', format: 'misskey:id' },
|
||||
name: { type: 'string', minLength: 1, maxLength: 100 },
|
||||
url: { type: 'string', minLength: 1, maxLength: 1024 },
|
||||
secret: { type: 'string', minLength: 1, maxLength: 1024 },
|
||||
on: { type: 'array', items: {
|
||||
type: 'string', enum: webhookEventTypes,
|
||||
} },
|
||||
active: { type: 'boolean' },
|
||||
},
|
||||
required: ['webhookId', 'name', 'url', 'secret', 'on', 'active'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const webhook = await Webhooks.findOneBy({
|
||||
id: ps.webhookId,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (webhook == null) {
|
||||
throw new ApiError(meta.errors.noSuchWebhook);
|
||||
}
|
||||
|
||||
await Webhooks.update(webhook.id, {
|
||||
name: ps.name,
|
||||
url: ps.url,
|
||||
secret: ps.secret,
|
||||
on: ps.on,
|
||||
active: ps.active,
|
||||
});
|
||||
|
||||
publishInternalEvent('webhookUpdated', webhook);
|
||||
});
|
@@ -47,14 +47,25 @@ export const meta = {
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
groupId: { type: 'string', format: 'misskey:id' },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
markAsRead: { type: 'boolean', default: true },
|
||||
},
|
||||
required: [],
|
||||
anyOf: [
|
||||
{
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['userId'],
|
||||
},
|
||||
{
|
||||
properties: {
|
||||
groupId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['groupId'],
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@@ -126,7 +137,5 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||
return await Promise.all(messages.map(message => MessagingMessages.pack(message, user, {
|
||||
populateGroup: false,
|
||||
})));
|
||||
} else {
|
||||
throw new Error();
|
||||
}
|
||||
});
|
||||
|
@@ -67,12 +67,23 @@ export const meta = {
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
groupId: { type: 'string', format: 'misskey:id' },
|
||||
text: { type: 'string', nullable: true, maxLength: 3000 },
|
||||
fileId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: [],
|
||||
anyOf: [
|
||||
{
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['userId'],
|
||||
},
|
||||
{
|
||||
properties: {
|
||||
groupId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['groupId'],
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
|
@@ -169,6 +169,7 @@ export const meta = {
|
||||
host: {
|
||||
type: 'string',
|
||||
optional: false, nullable: true,
|
||||
description: 'The local host is represented with `null`.',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
|
@@ -38,7 +38,11 @@ export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
expiresAt: { type: 'integer', nullable: true },
|
||||
expiresAt: {
|
||||
type: 'integer',
|
||||
nullable: true,
|
||||
description: 'A Unix Epoch timestamp that must lie in the future. `null` means an indefinite mute.',
|
||||
},
|
||||
},
|
||||
required: ['userId'],
|
||||
} as const;
|
||||
|
@@ -19,7 +19,7 @@ export const meta = {
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
local: { type: 'boolean' },
|
||||
local: { type: 'boolean', default: false },
|
||||
reply: { type: 'boolean' },
|
||||
renote: { type: 'boolean' },
|
||||
withFiles: { type: 'boolean' },
|
||||
@@ -52,19 +52,19 @@ export default define(meta, paramDef, async (ps) => {
|
||||
query.andWhere('note.userHost IS NULL');
|
||||
}
|
||||
|
||||
if (ps.reply != undefined) {
|
||||
if (ps.reply !== undefined) {
|
||||
query.andWhere(ps.reply ? 'note.replyId IS NOT NULL' : 'note.replyId IS NULL');
|
||||
}
|
||||
|
||||
if (ps.renote != undefined) {
|
||||
if (ps.renote !== undefined) {
|
||||
query.andWhere(ps.renote ? 'note.renoteId IS NOT NULL' : 'note.renoteId IS NULL');
|
||||
}
|
||||
|
||||
if (ps.withFiles != undefined) {
|
||||
if (ps.withFiles !== undefined) {
|
||||
query.andWhere(ps.withFiles ? `note.fileIds != '{}'` : `note.fileIds = '{}'`);
|
||||
}
|
||||
|
||||
if (ps.poll != undefined) {
|
||||
if (ps.poll !== undefined) {
|
||||
query.andWhere(ps.poll ? 'note.hasPoll = TRUE' : 'note.hasPoll = FALSE');
|
||||
}
|
||||
|
||||
|
@@ -57,7 +57,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||
conversation.push(p);
|
||||
}
|
||||
|
||||
if (conversation.length == ps.limit) {
|
||||
if (conversation.length === ps.limit) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@@ -1,14 +1,15 @@
|
||||
import ms from 'ms';
|
||||
import { In } from 'typeorm';
|
||||
import create from '@/services/note/create.js';
|
||||
import define from '../../define.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { User } from '@/models/entities/user.js';
|
||||
import { Users, DriveFiles, Notes, Channels, Blockings } from '@/models/index.js';
|
||||
import { DriveFile } from '@/models/entities/drive-file.js';
|
||||
import { Note } from '@/models/entities/note.js';
|
||||
import { noteVisibilities } from '../../../../types.js';
|
||||
import { Channel } from '@/models/entities/channel.js';
|
||||
import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
|
||||
import { noteVisibilities } from '../../../../types.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import define from '../../define.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
@@ -59,12 +60,6 @@ export const meta = {
|
||||
id: '3ac74a84-8fd5-4bb0-870f-01804f82ce15',
|
||||
},
|
||||
|
||||
contentRequired: {
|
||||
message: 'Content required. You need to set text, fileIds, renoteId or poll.',
|
||||
code: 'CONTENT_REQUIRED',
|
||||
id: '6f57e42b-c348-439b-bc45-993995cc515a',
|
||||
},
|
||||
|
||||
cannotCreateAlreadyExpiredPoll: {
|
||||
message: 'Poll is already expired.',
|
||||
code: 'CANNOT_CREATE_ALREADY_EXPIRED_POLL',
|
||||
@@ -88,33 +83,45 @@ export const meta = {
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'], default: "public" },
|
||||
visibility: { type: 'string', enum: ['public', 'home', 'followers', 'specified'], default: 'public' },
|
||||
visibleUserIds: { type: 'array', uniqueItems: true, items: {
|
||||
type: 'string', format: 'misskey:id',
|
||||
} },
|
||||
text: { type: 'string', nullable: true, maxLength: MAX_NOTE_TEXT_LENGTH, default: null },
|
||||
text: { type: 'string', maxLength: MAX_NOTE_TEXT_LENGTH, nullable: true },
|
||||
cw: { type: 'string', nullable: true, maxLength: 100 },
|
||||
localOnly: { type: 'boolean', default: false },
|
||||
noExtractMentions: { type: 'boolean', default: false },
|
||||
noExtractHashtags: { type: 'boolean', default: false },
|
||||
noExtractEmojis: { type: 'boolean', default: false },
|
||||
fileIds: { type: 'array', uniqueItems: true, minItems: 1, maxItems: 16, items: {
|
||||
type: 'string', format: 'misskey:id',
|
||||
} },
|
||||
mediaIds: { type: 'array', uniqueItems: true, minItems: 1, maxItems: 16, items: {
|
||||
type: 'string', format: 'misskey:id',
|
||||
} },
|
||||
fileIds: {
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
minItems: 1,
|
||||
maxItems: 16,
|
||||
items: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
mediaIds: {
|
||||
deprecated: true,
|
||||
description: 'Use `fileIds` instead. If both are specified, this property is discarded.',
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
minItems: 1,
|
||||
maxItems: 16,
|
||||
items: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
replyId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
renoteId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
channelId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||
poll: {
|
||||
type: 'object', nullable: true,
|
||||
type: 'object',
|
||||
nullable: true,
|
||||
properties: {
|
||||
choices: {
|
||||
type: 'array', uniqueItems: true, minItems: 2, maxItems: 10,
|
||||
items: {
|
||||
type: 'string', minLength: 1, maxLength: 50,
|
||||
},
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
minItems: 2,
|
||||
maxItems: 10,
|
||||
items: { type: 'string', minLength: 1, maxLength: 50 },
|
||||
},
|
||||
multiple: { type: 'boolean', default: false },
|
||||
expiresAt: { type: 'integer', nullable: true },
|
||||
@@ -123,36 +130,62 @@ export const paramDef = {
|
||||
required: ['choices'],
|
||||
},
|
||||
},
|
||||
required: [],
|
||||
anyOf: [
|
||||
{
|
||||
// (re)note with text, files and poll are optional
|
||||
properties: {
|
||||
text: { type: 'string', maxLength: MAX_NOTE_TEXT_LENGTH, nullable: false },
|
||||
},
|
||||
required: ['text'],
|
||||
},
|
||||
{
|
||||
// (re)note with files, text and poll are optional
|
||||
required: ['fileIds'],
|
||||
},
|
||||
{
|
||||
// (re)note with files, text and poll are optional
|
||||
required: ['mediaIds'],
|
||||
},
|
||||
{
|
||||
// (re)note with poll, text and files are optional
|
||||
properties: {
|
||||
poll: { type: 'object', nullable: false },
|
||||
},
|
||||
required: ['poll'],
|
||||
},
|
||||
{
|
||||
// pure renote
|
||||
required: ['renoteId'],
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
let visibleUsers: User[] = [];
|
||||
if (ps.visibleUserIds) {
|
||||
visibleUsers = (await Promise.all(ps.visibleUserIds.map(id => Users.findOneBy({ id }))))
|
||||
.filter(x => x != null) as User[];
|
||||
visibleUsers = await Users.findBy({
|
||||
id: In(ps.visibleUserIds),
|
||||
});
|
||||
}
|
||||
|
||||
let files: DriveFile[] = [];
|
||||
const fileIds = ps.fileIds != null ? ps.fileIds : ps.mediaIds != null ? ps.mediaIds : null;
|
||||
if (fileIds != null) {
|
||||
files = (await Promise.all(fileIds.map(fileId =>
|
||||
DriveFiles.findOneBy({
|
||||
id: fileId,
|
||||
userId: user.id,
|
||||
})
|
||||
))).filter(file => file != null) as DriveFile[];
|
||||
files = await DriveFiles.findBy({
|
||||
userId: user.id,
|
||||
id: In(fileIds),
|
||||
});
|
||||
}
|
||||
|
||||
let renote: Note | null;
|
||||
let renote: Note | null = null;
|
||||
if (ps.renoteId != null) {
|
||||
// Fetch renote to note
|
||||
renote = await Notes.findOneBy({ id: ps.renoteId });
|
||||
|
||||
if (renote == null) {
|
||||
throw new ApiError(meta.errors.noSuchRenoteTarget);
|
||||
} else if (renote.renoteId && !renote.text && !renote.fileIds) {
|
||||
} else if (renote.renoteId && !renote.text && !renote.fileIds && !renote.hasPoll) {
|
||||
throw new ApiError(meta.errors.cannotReRenote);
|
||||
}
|
||||
|
||||
@@ -168,17 +201,14 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||
}
|
||||
}
|
||||
|
||||
let reply: Note | null;
|
||||
let reply: Note | null = null;
|
||||
if (ps.replyId != null) {
|
||||
// Fetch reply
|
||||
reply = await Notes.findOneBy({ id: ps.replyId });
|
||||
|
||||
if (reply == null) {
|
||||
throw new ApiError(meta.errors.noSuchReplyTarget);
|
||||
}
|
||||
|
||||
// 返信対象が引用でないRenoteだったらエラー
|
||||
if (reply.renoteId && !reply.text && !reply.fileIds) {
|
||||
} else if (reply.renoteId && !reply.text && !reply.fileIds && !reply.hasPoll) {
|
||||
throw new ApiError(meta.errors.cannotReplyToPureRenote);
|
||||
}
|
||||
|
||||
@@ -204,12 +234,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||
}
|
||||
}
|
||||
|
||||
// テキストが無いかつ添付ファイルが無いかつRenoteも無いかつ投票も無かったらエラー
|
||||
if (!(ps.text || files.length || renote || ps.poll)) {
|
||||
throw new ApiError(meta.errors.contentRequired);
|
||||
}
|
||||
|
||||
let channel: Channel | undefined;
|
||||
let channel: Channel | null = null;
|
||||
if (ps.channelId != null) {
|
||||
channel = await Channels.findOneBy({ id: ps.channelId });
|
||||
|
||||
|
@@ -35,7 +35,11 @@ export const meta = {
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
withFiles: { type: 'boolean' },
|
||||
withFiles: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Only show notes that have attached files.',
|
||||
},
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
|
@@ -48,7 +48,11 @@ export const paramDef = {
|
||||
includeMyRenotes: { type: 'boolean', default: true },
|
||||
includeRenotedMyNotes: { type: 'boolean', default: true },
|
||||
includeLocalRenotes: { type: 'boolean', default: true },
|
||||
withFiles: { type: 'boolean' },
|
||||
withFiles: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Only show notes that have attached files.',
|
||||
},
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
@@ -37,7 +37,11 @@ export const meta = {
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
withFiles: { type: 'boolean' },
|
||||
withFiles: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Only show notes that have attached files.',
|
||||
},
|
||||
fileType: { type: 'array', items: {
|
||||
type: 'string',
|
||||
} },
|
||||
|
@@ -110,7 +110,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||
|
||||
if (exist.length) {
|
||||
if (poll.multiple) {
|
||||
if (exist.some(x => x.choice == ps.choice)) {
|
||||
if (exist.some(x => x.choice === ps.choice)) {
|
||||
throw new ApiError(meta.errors.alreadyVoted);
|
||||
}
|
||||
} else {
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { DeepPartial, FindOptionsWhere } from 'typeorm';
|
||||
import { NoteReactions } from '@/models/index.js';
|
||||
import { NoteReaction } from '@/models/entities/note-reaction.js';
|
||||
import define from '../../define.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { NoteReactions } from '@/models/index.js';
|
||||
import { DeepPartial } from 'typeorm';
|
||||
import { NoteReaction } from '@/models/entities/note-reaction.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes', 'reactions'],
|
||||
@@ -45,7 +45,7 @@ export const paramDef = {
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
const query = {
|
||||
noteId: ps.noteId,
|
||||
} as DeepPartial<NoteReaction>;
|
||||
} as FindOptionsWhere<NoteReaction>;
|
||||
|
||||
if (ps.type) {
|
||||
// ローカルリアクションはホスト名が . とされているが
|
||||
|
@@ -25,21 +25,44 @@ export const meta = {
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tag: { type: 'string' },
|
||||
query: { type: 'array', items: {
|
||||
type: 'array', items: {
|
||||
type: 'string',
|
||||
},
|
||||
} },
|
||||
reply: { type: 'boolean', nullable: true, default: null },
|
||||
renote: { type: 'boolean', nullable: true, default: null },
|
||||
withFiles: { type: 'boolean' },
|
||||
withFiles: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Only show notes that have attached files.',
|
||||
},
|
||||
poll: { type: 'boolean', nullable: true, default: null },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
},
|
||||
required: [],
|
||||
anyOf: [
|
||||
{
|
||||
properties: {
|
||||
tag: { type: 'string', minLength: 1 },
|
||||
},
|
||||
required: ['tag'],
|
||||
},
|
||||
{
|
||||
properties: {
|
||||
query: {
|
||||
type: 'array',
|
||||
description: 'The outer arrays are chained with OR, the inner arrays are chained with AND.',
|
||||
items: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
minLength: 1,
|
||||
},
|
||||
minItems: 1,
|
||||
},
|
||||
minItems: 1,
|
||||
},
|
||||
},
|
||||
required: ['query'],
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
|
@@ -35,7 +35,11 @@ export const paramDef = {
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
offset: { type: 'integer', default: 0 },
|
||||
host: { type: 'string', nullable: true },
|
||||
host: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
description: 'The local host is represented with `null`.',
|
||||
},
|
||||
userId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
|
||||
channelId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
|
||||
},
|
||||
|
@@ -38,7 +38,11 @@ export const paramDef = {
|
||||
includeMyRenotes: { type: 'boolean', default: true },
|
||||
includeRenotedMyNotes: { type: 'boolean', default: true },
|
||||
includeLocalRenotes: { type: 'boolean', default: true },
|
||||
withFiles: { type: 'boolean' },
|
||||
withFiles: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Only show notes that have attached files.',
|
||||
},
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import define from '../../define.js';
|
||||
import { getNote } from '../../common/getters.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { URLSearchParams } from 'node:url';
|
||||
import fetch from 'node-fetch';
|
||||
import config from '@/config/index.js';
|
||||
import { getAgentByUrl } from '@/misc/fetch.js';
|
||||
import { URLSearchParams } from 'node:url';
|
||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
import { Notes } from '@/models/index.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { getNote } from '../../common/getters.js';
|
||||
import define from '../../define.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['notes'],
|
||||
@@ -75,11 +75,17 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||
Accept: 'application/json, */*',
|
||||
},
|
||||
body: params,
|
||||
timeout: 10000,
|
||||
// TODO
|
||||
//timeout: 10000,
|
||||
agent: getAgentByUrl,
|
||||
});
|
||||
|
||||
const json = await res.json();
|
||||
const json = (await res.json()) as {
|
||||
translations: {
|
||||
detected_source_language: string;
|
||||
text: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
return {
|
||||
sourceLang: json.translations[0].detected_source_language,
|
||||
|
@@ -42,7 +42,11 @@ export const paramDef = {
|
||||
includeMyRenotes: { type: 'boolean', default: true },
|
||||
includeRenotedMyNotes: { type: 'boolean', default: true },
|
||||
includeLocalRenotes: { type: 'boolean', default: true },
|
||||
withFiles: { type: 'boolean' },
|
||||
withFiles: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Only show notes that have attached files.',
|
||||
},
|
||||
},
|
||||
required: ['listId'],
|
||||
} as const;
|
||||
@@ -59,12 +63,8 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||
}
|
||||
|
||||
//#region Construct query
|
||||
const listQuery = UserListJoinings.createQueryBuilder('joining')
|
||||
.select('joining.userId')
|
||||
.where('joining.userListId = :userListId', { userListId: list.id });
|
||||
|
||||
const query = makePaginationQuery(Notes.createQueryBuilder('note'), ps.sinceId, ps.untilId)
|
||||
.andWhere(`note.userId IN (${ listQuery.getQuery() })`)
|
||||
.innerJoin(UserListJoinings.metadata.targetName, 'userListJoining', 'userListJoining.userId = note.userId')
|
||||
.innerJoinAndSelect('note.user', 'user')
|
||||
.leftJoinAndSelect('user.avatar', 'avatar')
|
||||
.leftJoinAndSelect('user.banner', 'banner')
|
||||
@@ -76,7 +76,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||
.leftJoinAndSelect('renote.user', 'renoteUser')
|
||||
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
|
||||
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner')
|
||||
.setParameters(listQuery.getParameters());
|
||||
.andWhere('userListJoining.userListId = :userListId', { userListId: list.id });
|
||||
|
||||
generateVisibilityQuery(query, user);
|
||||
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import define from '../../define.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { Pages, Users } from '@/models/index.js';
|
||||
import { Page } from '@/models/entities/page.js';
|
||||
import { IsNull } from 'typeorm';
|
||||
import define from '../../define.js';
|
||||
import { ApiError } from '../../error.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['pages'],
|
||||
@@ -26,17 +26,26 @@ export const meta = {
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
pageId: { type: 'string', format: 'misskey:id' },
|
||||
name: { type: 'string' },
|
||||
username: { type: 'string' },
|
||||
},
|
||||
required: [],
|
||||
anyOf: [
|
||||
{
|
||||
properties: {
|
||||
pageId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['pageId'],
|
||||
},
|
||||
{
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
username: { type: 'string' },
|
||||
},
|
||||
required: ['name', 'username'],
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
let page: Page | undefined;
|
||||
let page: Page | null = null;
|
||||
|
||||
if (ps.pageId) {
|
||||
page = await Pages.findOneBy({ id: ps.pageId });
|
||||
|
@@ -38,14 +38,29 @@ export const meta = {
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
username: { type: 'string' },
|
||||
host: { type: 'string', nullable: true },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
},
|
||||
required: [],
|
||||
anyOf: [
|
||||
{
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['userId'],
|
||||
},
|
||||
{
|
||||
properties: {
|
||||
username: { type: 'string' },
|
||||
host: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
description: 'The local host is represented with `null`.',
|
||||
},
|
||||
},
|
||||
required: ['username', 'host'],
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
|
@@ -38,14 +38,29 @@ export const meta = {
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
username: { type: 'string' },
|
||||
host: { type: 'string', nullable: true },
|
||||
sinceId: { type: 'string', format: 'misskey:id' },
|
||||
untilId: { type: 'string', format: 'misskey:id' },
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
},
|
||||
required: [],
|
||||
anyOf: [
|
||||
{
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['userId'],
|
||||
},
|
||||
{
|
||||
properties: {
|
||||
username: { type: 'string' },
|
||||
host: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
description: 'The local host is represented with `null`.',
|
||||
},
|
||||
},
|
||||
required: ['username', 'host'],
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
|
@@ -28,7 +28,10 @@ export const paramDef = {
|
||||
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
|
||||
detail: { type: 'boolean', default: true },
|
||||
},
|
||||
required: [],
|
||||
anyOf: [
|
||||
{ required: ['username'] },
|
||||
{ required: ['host'] },
|
||||
],
|
||||
} as const;
|
||||
|
||||
// TODO: avatar,bannerをJOINしたいけどエラーになる
|
||||
|
@@ -23,9 +23,9 @@ export const meta = {
|
||||
items: {
|
||||
type: 'object',
|
||||
ref: 'UserDetailed',
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
],
|
||||
},
|
||||
|
||||
errors: {
|
||||
@@ -46,15 +46,33 @@ export const meta = {
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
userIds: { type: 'array', uniqueItems: true, items: {
|
||||
type: 'string', format: 'misskey:id',
|
||||
} },
|
||||
username: { type: 'string' },
|
||||
host: { type: 'string', nullable: true },
|
||||
},
|
||||
required: [],
|
||||
anyOf: [
|
||||
{
|
||||
properties: {
|
||||
userId: { type: 'string', format: 'misskey:id' },
|
||||
},
|
||||
required: ['userId'],
|
||||
},
|
||||
{
|
||||
properties: {
|
||||
userIds: { type: 'array', uniqueItems: true, items: {
|
||||
type: 'string', format: 'misskey:id',
|
||||
} },
|
||||
},
|
||||
required: ['userIds'],
|
||||
},
|
||||
{
|
||||
properties: {
|
||||
username: { type: 'string' },
|
||||
host: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
description: 'The local host is represented with `null`.',
|
||||
},
|
||||
},
|
||||
required: ['username'],
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
|
@@ -24,17 +24,17 @@ export default async (ctx: Koa.Context) => {
|
||||
ctx.body = { error };
|
||||
}
|
||||
|
||||
if (typeof username != 'string') {
|
||||
if (typeof username !== 'string') {
|
||||
ctx.status = 400;
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof password != 'string') {
|
||||
if (typeof password !== 'string') {
|
||||
ctx.status = 400;
|
||||
return;
|
||||
}
|
||||
|
||||
if (token != null && typeof token != 'string') {
|
||||
if (token != null && typeof token !== 'string') {
|
||||
ctx.status = 400;
|
||||
return;
|
||||
}
|
||||
|
@@ -1,16 +1,16 @@
|
||||
import Koa from 'koa';
|
||||
import Router from '@koa/router';
|
||||
import { getJson } from '@/misc/fetch.js';
|
||||
import { OAuth2 } from 'oauth';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { getJson } from '@/misc/fetch.js';
|
||||
import config from '@/config/index.js';
|
||||
import { publishMainStream } from '@/services/stream.js';
|
||||
import { redisClient } from '../../../db/redis.js';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import signin from '../common/signin.js';
|
||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
import { Users, UserProfiles } from '@/models/index.js';
|
||||
import { ILocalUser } from '@/models/entities/user.js';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { redisClient } from '../../../db/redis.js';
|
||||
import signin from '../common/signin.js';
|
||||
|
||||
function getUserToken(ctx: Koa.BaseContext): string | null {
|
||||
return ((ctx.headers['cookie'] || '').match(/igi=(\w+)/) || [null, null])[1];
|
||||
@@ -54,7 +54,7 @@ router.get('/disconnect/discord', async ctx => {
|
||||
integrations: profile.integrations,
|
||||
});
|
||||
|
||||
ctx.body = `Discordの連携を解除しました :v:`;
|
||||
ctx.body = 'Discordの連携を解除しました :v:';
|
||||
|
||||
// Publish i updated event
|
||||
publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, {
|
||||
@@ -140,7 +140,7 @@ router.get('/dc/cb', async ctx => {
|
||||
|
||||
const code = ctx.query.code;
|
||||
|
||||
if (!code) {
|
||||
if (!code || typeof code !== 'string') {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
}
|
||||
@@ -174,17 +174,17 @@ router.get('/dc/cb', async ctx => {
|
||||
}
|
||||
}));
|
||||
|
||||
const { id, username, discriminator } = await getJson('https://discord.com/api/users/@me', '*/*', 10 * 1000, {
|
||||
const { id, username, discriminator } = (await getJson('https://discord.com/api/users/@me', '*/*', 10 * 1000, {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
});
|
||||
})) as Record<string, unknown>;
|
||||
|
||||
if (!id || !username || !discriminator) {
|
||||
if (typeof id !== 'string' || typeof username !== 'string' || typeof discriminator !== 'string') {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
}
|
||||
|
||||
const profile = await UserProfiles.createQueryBuilder()
|
||||
.where(`"integrations"->'discord'->>'id' = :id`, { id: id })
|
||||
.where('"integrations"->\'discord\'->>\'id\' = :id', { id: id })
|
||||
.andWhere('"userHost" IS NULL')
|
||||
.getOne();
|
||||
|
||||
@@ -211,7 +211,7 @@ router.get('/dc/cb', async ctx => {
|
||||
} else {
|
||||
const code = ctx.query.code;
|
||||
|
||||
if (!code) {
|
||||
if (!code || typeof code !== 'string') {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
}
|
||||
@@ -245,10 +245,10 @@ router.get('/dc/cb', async ctx => {
|
||||
}
|
||||
}));
|
||||
|
||||
const { id, username, discriminator } = await getJson('https://discord.com/api/users/@me', '*/*', 10 * 1000, {
|
||||
const { id, username, discriminator } = (await getJson('https://discord.com/api/users/@me', '*/*', 10 * 1000, {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
});
|
||||
if (!id || !username || !discriminator) {
|
||||
})) as Record<string, unknown>;
|
||||
if (typeof id !== 'string' || typeof username !== 'string' || typeof discriminator !== 'string') {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
}
|
||||
|
@@ -1,16 +1,16 @@
|
||||
import Koa from 'koa';
|
||||
import Router from '@koa/router';
|
||||
import { getJson } from '@/misc/fetch.js';
|
||||
import { OAuth2 } from 'oauth';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { getJson } from '@/misc/fetch.js';
|
||||
import config from '@/config/index.js';
|
||||
import { publishMainStream } from '@/services/stream.js';
|
||||
import { redisClient } from '../../../db/redis.js';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import signin from '../common/signin.js';
|
||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
import { Users, UserProfiles } from '@/models/index.js';
|
||||
import { ILocalUser } from '@/models/entities/user.js';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { redisClient } from '../../../db/redis.js';
|
||||
import signin from '../common/signin.js';
|
||||
|
||||
function getUserToken(ctx: Koa.BaseContext): string | null {
|
||||
return ((ctx.headers['cookie'] || '').match(/igi=(\w+)/) || [null, null])[1];
|
||||
@@ -54,7 +54,7 @@ router.get('/disconnect/github', async ctx => {
|
||||
integrations: profile.integrations,
|
||||
});
|
||||
|
||||
ctx.body = `GitHubの連携を解除しました :v:`;
|
||||
ctx.body = 'GitHubの連携を解除しました :v:';
|
||||
|
||||
// Publish i updated event
|
||||
publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, {
|
||||
@@ -138,7 +138,7 @@ router.get('/gh/cb', async ctx => {
|
||||
|
||||
const code = ctx.query.code;
|
||||
|
||||
if (!code) {
|
||||
if (!code || typeof code !== 'string') {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
}
|
||||
@@ -167,16 +167,16 @@ router.get('/gh/cb', async ctx => {
|
||||
}
|
||||
}));
|
||||
|
||||
const { login, id } = await getJson('https://api.github.com/user', 'application/vnd.github.v3+json', 10 * 1000, {
|
||||
const { login, id } = (await getJson('https://api.github.com/user', 'application/vnd.github.v3+json', 10 * 1000, {
|
||||
'Authorization': `bearer ${accessToken}`,
|
||||
});
|
||||
if (!login || !id) {
|
||||
})) as Record<string, unknown>;
|
||||
if (typeof login !== 'string' || typeof id !== 'string') {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
}
|
||||
|
||||
const link = await UserProfiles.createQueryBuilder()
|
||||
.where(`"integrations"->'github'->>'id' = :id`, { id: id })
|
||||
.where('"integrations"->\'github\'->>\'id\' = :id', { id: id })
|
||||
.andWhere('"userHost" IS NULL')
|
||||
.getOne();
|
||||
|
||||
@@ -189,7 +189,7 @@ router.get('/gh/cb', async ctx => {
|
||||
} else {
|
||||
const code = ctx.query.code;
|
||||
|
||||
if (!code) {
|
||||
if (!code || typeof code !== 'string') {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
}
|
||||
@@ -219,11 +219,11 @@ router.get('/gh/cb', async ctx => {
|
||||
}
|
||||
}));
|
||||
|
||||
const { login, id } = await getJson('https://api.github.com/user', 'application/vnd.github.v3+json', 10 * 1000, {
|
||||
const { login, id } = (await getJson('https://api.github.com/user', 'application/vnd.github.v3+json', 10 * 1000, {
|
||||
'Authorization': `bearer ${accessToken}`,
|
||||
});
|
||||
})) as Record<string, unknown>;
|
||||
|
||||
if (!login || !id) {
|
||||
if (typeof login !== 'string' || typeof id !== 'string') {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
}
|
||||
|
@@ -2,14 +2,14 @@ import Koa from 'koa';
|
||||
import Router from '@koa/router';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import autwh from 'autwh';
|
||||
import { redisClient } from '../../../db/redis.js';
|
||||
import { IsNull } from 'typeorm';
|
||||
import { publishMainStream } from '@/services/stream.js';
|
||||
import config from '@/config/index.js';
|
||||
import signin from '../common/signin.js';
|
||||
import { fetchMeta } from '@/misc/fetch-meta.js';
|
||||
import { Users, UserProfiles } from '@/models/index.js';
|
||||
import { ILocalUser } from '@/models/entities/user.js';
|
||||
import { IsNull } from 'typeorm';
|
||||
import signin from '../common/signin.js';
|
||||
import { redisClient } from '../../../db/redis.js';
|
||||
|
||||
function getUserToken(ctx: Koa.BaseContext): string | null {
|
||||
return ((ctx.headers['cookie'] || '').match(/igi=(\w+)/) || [null, null])[1];
|
||||
@@ -53,7 +53,7 @@ router.get('/disconnect/twitter', async ctx => {
|
||||
integrations: profile.integrations,
|
||||
});
|
||||
|
||||
ctx.body = `Twitterの連携を解除しました :v:`;
|
||||
ctx.body = 'Twitterの連携を解除しました :v:';
|
||||
|
||||
// Publish i updated event
|
||||
publishMainStream(user.id, 'meUpdated', await Users.pack(user, user, {
|
||||
@@ -132,10 +132,16 @@ router.get('/tw/cb', async ctx => {
|
||||
|
||||
const twCtx = await get;
|
||||
|
||||
const result = await twAuth!.done(JSON.parse(twCtx), ctx.query.oauth_verifier);
|
||||
const verifier = ctx.query.oauth_verifier;
|
||||
if (!verifier || typeof verifier !== 'string') {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await twAuth!.done(JSON.parse(twCtx), verifier);
|
||||
|
||||
const link = await UserProfiles.createQueryBuilder()
|
||||
.where(`"integrations"->'twitter'->>'userId' = :id`, { id: result.userId })
|
||||
.where('"integrations"->\'twitter\'->>\'userId\' = :id', { id: result.userId })
|
||||
.andWhere('"userHost" IS NULL')
|
||||
.getOne();
|
||||
|
||||
@@ -148,7 +154,7 @@ router.get('/tw/cb', async ctx => {
|
||||
} else {
|
||||
const verifier = ctx.query.oauth_verifier;
|
||||
|
||||
if (verifier == null) {
|
||||
if (!verifier || typeof verifier !== 'string') {
|
||||
ctx.throw(400, 'invalid session');
|
||||
return;
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { default as Xev } from 'xev';
|
||||
import Xev from 'xev';
|
||||
import Channel from '../channel.js';
|
||||
|
||||
const ev = new Xev.default();
|
||||
const ev = new Xev();
|
||||
|
||||
export default class extends Channel {
|
||||
public readonly chName = 'queueStats';
|
||||
|
@@ -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';
|
||||
|
@@ -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]);
|
||||
|
@@ -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;
|
||||
|
@@ -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';
|
||||
|
@@ -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');
|
||||
}
|
||||
|
@@ -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
Reference in New Issue
Block a user