Merge branch 'develop' into pr/ThatOneCalculator/8764

This commit is contained in:
tamaina 2022-06-28 05:08:57 +00:00
commit b9154cda2f
309 changed files with 11129 additions and 9851 deletions

View file

@ -5,6 +5,6 @@
"loader=./test/loader.js"
],
"slow": 1000,
"timeout": 10000,
"timeout": 30000,
"exit": true
}

View file

@ -0,0 +1,5 @@
Font Awesome Icons
-------------------------
Ⓒ Font Awesome
CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 844 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 507 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 689 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 772 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 930 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 798 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 991 B

View file

@ -53,6 +53,7 @@
"fluent-ffmpeg": "2.1.2",
"got": "12.1.0",
"hpagent": "0.1.2",
"ioredis": "4.28.5",
"ip-cidr": "3.0.10",
"is-svg": "4.3.2",
"js-yaml": "4.1.0",
@ -60,7 +61,7 @@
"json5": "2.2.1",
"json5-loader": "4.0.1",
"jsonld": "6.0.0",
"jsrsasign": "10.5.24",
"jsrsasign": "10.5.25",
"koa": "2.13.4",
"koa-bodyparser": "4.3.0",
"koa-favicon": "2.1.0",
@ -93,7 +94,6 @@
"random-seed": "0.3.0",
"ratelimiter": "3.4.1",
"re2": "1.17.4",
"redis": "3.1.2",
"redis-lock": "0.1.4",
"reflect-metadata": "0.1.13",
"rename": "1.0.4",
@ -107,7 +107,7 @@
"strict-event-emitter-types": "2.0.0",
"stringz": "2.1.0",
"style-loader": "3.3.1",
"summaly": "2.5.1",
"summaly": "2.6.0",
"syslog-pro": "1.0.0",
"systeminformation": "5.11.16",
"tinycolor2": "1.4.2",

View file

@ -19,6 +19,7 @@ export type Source = {
redis: {
host: string;
port: number;
family?: number;
pass: string;
db?: number;
prefix?: string;

View file

@ -192,12 +192,13 @@ export const db = new DataSource({
synchronize: process.env.NODE_ENV === 'test',
dropSchema: process.env.NODE_ENV === 'test',
cache: !config.db.disableCache ? {
type: 'redis',
type: 'ioredis',
options: {
host: config.redis.host,
port: config.redis.port,
family: config.redis.family == null ? 0 : config.redis.family,
password: config.redis.pass,
prefix: `${config.redis.prefix}:query:`,
keyPrefix: `${config.redis.prefix}:query:`,
db: config.redis.db || 0,
},
} : false,
@ -226,7 +227,7 @@ export async function initDb(force = false) {
export async function resetDb() {
const reset = async () => {
await redisClient.FLUSHDB();
await redisClient.flushdb();
const tables = await db.query(`SELECT relname AS "table"
FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace)
WHERE nspname NOT IN ('pg_catalog', 'information_schema')

View file

@ -1,16 +1,15 @@
import * as redis from 'redis';
import Redis from 'ioredis';
import config from '@/config/index.js';
export function createConnection() {
return redis.createClient(
config.redis.port,
config.redis.host,
{
password: config.redis.pass,
prefix: config.redis.prefix,
db: config.redis.db || 0,
}
);
return new Redis({
port: config.redis.port,
host: config.redis.host,
family: config.redis.family == null ? 0 : config.redis.family,
password: config.redis.pass,
keyPrefix: `${config.redis.prefix}:`,
db: config.redis.db || 0,
});
}
export const subsdcriber = createConnection();

View file

@ -16,11 +16,13 @@ export async function checkWordMute(note: NoteLike, me: UserLike | null | undefi
if (me && (note.userId === me.id)) return false;
if (mutedWords.length > 0) {
if (note.text == null) return false;
const text = ((note.cw ?? '') + '\n' + (note.text ?? '')).trim();
if (text === '') return false;
const matched = mutedWords.some(filter => {
if (Array.isArray(filter)) {
return filter.every(keyword => note.text!.includes(keyword));
return filter.every(keyword => text.includes(keyword));
} else {
// represents RegExp
const regexp = filter.match(/^\/(.+)\/(.*)$/);
@ -29,7 +31,7 @@ export async function checkWordMute(note: NoteLike, me: UserLike | null | undefi
if (!regexp) return false;
try {
return new RE2(regexp[1], regexp[2]).test(note.text!);
return new RE2(regexp[1], regexp[2]).test(text);
} catch (err) {
// This should never happen due to input sanitisation.
return false;

View file

@ -1,15 +0,0 @@
export function isBlockerUserRelated(note: any, blockerUserIds: Set<string>): boolean {
if (blockerUserIds.has(note.userId)) {
return true;
}
if (note.reply != null && blockerUserIds.has(note.reply.userId)) {
return true;
}
if (note.renote != null && blockerUserIds.has(note.renote.userId)) {
return true;
}
return false;
}

View file

@ -0,0 +1,8 @@
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
const dictionary = {
'safe-file': FILE_TYPE_BROWSERSAFE,
'sharp-convertible-image': ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/svg+xml'],
};
export const isMimeImage = (mime: string, type: keyof typeof dictionary): boolean => dictionary[type].includes(mime);

View file

@ -1,15 +0,0 @@
export function isMutedUserRelated(note: any, mutedUserIds: Set<string>): boolean {
if (mutedUserIds.has(note.userId)) {
return true;
}
if (note.reply != null && mutedUserIds.has(note.reply.userId)) {
return true;
}
if (note.renote != null && mutedUserIds.has(note.renote.userId)) {
return true;
}
return false;
}

View file

@ -0,0 +1,15 @@
export function isUserRelated(note: any, userIds: Set<string>): boolean {
if (userIds.has(note.userId)) {
return true;
}
if (note.reply != null && userIds.has(note.reply.userId)) {
return true;
}
if (note.renote != null && userIds.has(note.renote.userId)) {
return true;
}
return false;
}

View file

@ -1,11 +1,13 @@
import { db } from '@/db/postgre.js';
import { Instance } from '@/models/entities/instance.js';
import { Packed } from '@/misc/schema.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
export const InstanceRepository = db.getRepository(Instance).extend({
async pack(
instance: Instance,
): Promise<Packed<'FederationInstance'>> {
const meta = await fetchMeta();
return {
id: instance.id,
caughtAt: instance.caughtAt.toISOString(),
@ -18,6 +20,7 @@ export const InstanceRepository = db.getRepository(Instance).extend({
lastCommunicatedAt: instance.lastCommunicatedAt.toISOString(),
isNotResponding: instance.isNotResponding,
isSuspended: instance.isSuspended,
isBlocked: meta.blockedHosts.includes(instance.host),
softwareName: instance.softwareName,
softwareVersion: instance.softwareVersion,
openRegistrations: instance.openRegistrations,
@ -26,6 +29,8 @@ export const InstanceRepository = db.getRepository(Instance).extend({
maintainerName: instance.maintainerName,
maintainerEmail: instance.maintainerEmail,
iconUrl: instance.iconUrl,
faviconUrl: instance.faviconUrl,
themeColor: instance.themeColor,
infoUpdatedAt: instance.infoUpdatedAt ? instance.infoUpdatedAt.toISOString() : null,
};
},

View file

@ -52,6 +52,10 @@ export const packedFederationInstanceSchema = {
type: 'boolean',
optional: false, nullable: false,
},
isBlocked: {
type: 'boolean',
optional: false, nullable: false,
},
softwareName: {
type: 'string',
optional: false, nullable: true,
@ -88,6 +92,15 @@ export const packedFederationInstanceSchema = {
optional: false, nullable: true,
format: 'url',
},
faviconUrl: {
type: 'string',
optional: false, nullable: true,
format: 'url',
},
themeColor: {
type: 'string',
optional: false, nullable: true,
},
infoUpdatedAt: {
type: 'string',
optional: false, nullable: true,

View file

@ -6,6 +6,7 @@ export function initialize<T>(name: string, limitPerSec = -1) {
redis: {
port: config.redis.port,
host: config.redis.host,
family: config.redis.family == null ? 0 : config.redis.family,
password: config.redis.pass,
db: config.redis.db || 0,
},

View file

@ -201,7 +201,7 @@ export interface IApMention extends IObject {
href: string;
}
export const isMention = (object: IObject): object is IApMention=>
export const isMention = (object: IObject): object is IApMention =>
getApType(object) === 'Mention' &&
typeof object.href === 'string';

View file

@ -6,7 +6,11 @@ import call from './call.js';
import { ApiError } from './error.js';
export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise<void>((res) => {
const body = ctx.request.body;
const body = ctx.is('multipart/form-data')
? (ctx.request as any).body
: ctx.method === 'GET'
? ctx.query
: ctx.request.body;
const reply = (x?: any, y?: ApiError) => {
if (x == null) {
@ -33,6 +37,9 @@ export default (endpoint: IEndpoint, ctx: Koa.Context) => new Promise<void>((res
authenticate(body['i']).then(([user, app]) => {
// API invoking
call(endpoint.name, user, app, body, ctx).then((res: any) => {
if (ctx.method === 'GET' && endpoint.meta.cacheSec && !body['i'] && !user) {
ctx.set('Cache-Control', `public, max-age=${endpoint.meta.cacheSec}`);
}
reply(res);
}).catch((e: ApiError) => {
reply(e.httpStatusCode ? e.httpStatusCode : e.kind === 'client' ? 400 : 500, e);

View file

@ -94,7 +94,7 @@ export default async (endpoint: string, user: CacheableLocalUser | null | undefi
}
// Cast non JSON input
if (ep.meta.requireFile && ep.params.properties) {
if ((ep.meta.requireFile || ctx?.method === 'GET') && ep.params.properties) {
for (const k of Object.keys(ep.params.properties)) {
const param = ep.params.properties![k];
if (['boolean', 'number', 'integer'].includes(param.type ?? '') && typeof data[k] === 'string') {

View file

@ -1,40 +0,0 @@
import { User } from '@/models/entities/user.js';
import { id } from '@/models/id.js';
import { UserProfiles } from '@/models/index.js';
import { SelectQueryBuilder, Brackets } from 'typeorm';
function createMutesQuery(id: string) {
return UserProfiles.createQueryBuilder('user_profile')
.select('user_profile.mutedInstances')
.where('user_profile.userId = :muterId', { muterId: id });
}
export function generateMutedInstanceQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }) {
const mutingQuery = createMutesQuery(me.id);
q
.andWhere(new Brackets(qb => { qb
.andWhere('note.userHost IS NULL')
.orWhere(`NOT((${ mutingQuery.getQuery() })::jsonb ? note.userHost)`);
}))
.andWhere(new Brackets(qb => { qb
.where(`note.replyUserHost IS NULL`)
.orWhere(`NOT ((${ mutingQuery.getQuery() })::jsonb ? note.replyUserHost)`);
}))
.andWhere(new Brackets(qb => { qb
.where(`note.renoteUserHost IS NULL`)
.orWhere(`NOT ((${ mutingQuery.getQuery() })::jsonb ? note.renoteUserHost)`);
}));
q.setParameters(mutingQuery.getParameters());
}
export function generateMutedInstanceNotificationQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }) {
const mutingQuery = createMutesQuery(me.id);
q.andWhere(new Brackets(qb => { qb
.andWhere('notifier.host IS NULL')
.orWhere(`NOT (( ${mutingQuery.getQuery()} )::jsonb ? notifier.host)`);
}));
q.setParameters(mutingQuery.getParameters());
}

View file

@ -1,6 +1,6 @@
import { User } from '@/models/entities/user.js';
import { Mutings } from '@/models/index.js';
import { SelectQueryBuilder, Brackets } from 'typeorm';
import { User } from '@/models/entities/user.js';
import { Mutings, UserProfiles } from '@/models/index.js';
export function generateMutedUserQuery(q: SelectQueryBuilder<any>, me: { id: User['id'] }, exclude?: User) {
const mutingQuery = Mutings.createQueryBuilder('muting')
@ -11,21 +11,39 @@ export function generateMutedUserQuery(q: SelectQueryBuilder<any>, me: { id: Use
mutingQuery.andWhere('muting.muteeId != :excludeId', { excludeId: exclude.id });
}
const mutingInstanceQuery = UserProfiles.createQueryBuilder('user_profile')
.select('user_profile.mutedInstances')
.where('user_profile.userId = :muterId', { muterId: me.id });
// 投稿の作者をミュートしていない かつ
// 投稿の返信先の作者をミュートしていない かつ
// 投稿の引用元の作者をミュートしていない
q
.andWhere(`note.userId NOT IN (${ mutingQuery.getQuery() })`)
.andWhere(new Brackets(qb => { qb
.where(`note.replyUserId IS NULL`)
.where('note.replyUserId IS NULL')
.orWhere(`note.replyUserId NOT IN (${ mutingQuery.getQuery() })`);
}))
.andWhere(new Brackets(qb => { qb
.where(`note.renoteUserId IS NULL`)
.where('note.renoteUserId IS NULL')
.orWhere(`note.renoteUserId NOT IN (${ mutingQuery.getQuery() })`);
}))
// mute instances
.andWhere(new Brackets(qb => { qb
.andWhere('note.userHost IS NULL')
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.userHost)`);
}))
.andWhere(new Brackets(qb => { qb
.where('note.replyUserHost IS NULL')
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.replyUserHost)`);
}))
.andWhere(new Brackets(qb => { qb
.where('note.renoteUserHost IS NULL')
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.renoteUserHost)`);
}));
q.setParameters(mutingQuery.getParameters());
q.setParameters(mutingInstanceQuery.getParameters());
}
export function generateMutedUserQueryForUsers(q: SelectQueryBuilder<any>, me: { id: User['id'] }) {
@ -33,8 +51,26 @@ export function generateMutedUserQueryForUsers(q: SelectQueryBuilder<any>, me: {
.select('muting.muteeId')
.where('muting.muterId = :muterId', { muterId: me.id });
const mutingInstanceQuery = UserProfiles.createQueryBuilder('user_profile')
.select('user_profile.mutedInstances')
.where('user_profile.userId = :muterId', { muterId: me.id });
q
.andWhere(`user.id NOT IN (${ mutingQuery.getQuery() })`);
.andWhere(`user.id NOT IN (${ mutingQuery.getQuery() })`)
// mute instances
.andWhere(new Brackets(qb => { qb
.andWhere('note.userHost IS NULL')
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.userHost)`);
}))
.andWhere(new Brackets(qb => { qb
.where('note.replyUserHost IS NULL')
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.replyUserHost)`);
}))
.andWhere(new Brackets(qb => { qb
.where('note.renoteUserHost IS NULL')
.orWhere(`NOT ((${ mutingInstanceQuery.getQuery() })::jsonb ? note.renoteUserHost)`);
}));
q.setParameters(mutingQuery.getParameters());
q.setParameters(mutingInstanceQuery.getParameters());
}

View file

@ -21,7 +21,6 @@ ajv.addFormat('misskey:id', /^[a-zA-Z0-9]+$/);
export default function <T extends IEndpointMeta, Ps extends Schema>(meta: T, paramDef: Ps, cb: executor<T, Ps>)
: (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any) => Promise<any> {
const validate = ajv.compile(paramDef);
return (params: any, user: T['requireCredential'] extends true ? CacheableLocalUser : CacheableLocalUser | null, token: AccessToken | null, file?: any) => {

View file

@ -59,6 +59,7 @@ import * as ep___admin_unsilenceUser from './endpoints/admin/unsilence-user.js';
import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js';
import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js';
import * as ep___admin_vacuum from './endpoints/admin/vacuum.js';
import * as ep___admin_deleteAccount from './endpoints/admin/delete-account.js';
import * as ep___announcements from './endpoints/announcements.js';
import * as ep___antennas_create from './endpoints/antennas/create.js';
import * as ep___antennas_delete from './endpoints/antennas/delete.js';
@ -99,6 +100,7 @@ import * as ep___charts_user_notes from './endpoints/charts/user/notes.js';
import * as ep___charts_user_reactions from './endpoints/charts/user/reactions.js';
import * as ep___charts_users from './endpoints/charts/users.js';
import * as ep___clips_addNote from './endpoints/clips/add-note.js';
import * as ep___clips_removeNote from './endpoints/clips/remove-note.js';
import * as ep___clips_create from './endpoints/clips/create.js';
import * as ep___clips_delete from './endpoints/clips/delete.js';
import * as ep___clips_list from './endpoints/clips/list.js';
@ -133,6 +135,7 @@ import * as ep___federation_instances from './endpoints/federation/instances.js'
import * as ep___federation_showInstance from './endpoints/federation/show-instance.js';
import * as ep___federation_updateRemoteUser from './endpoints/federation/update-remote-user.js';
import * as ep___federation_users from './endpoints/federation/users.js';
import * as ep___federation_stats from './endpoints/federation/stats.js';
import * as ep___following_create from './endpoints/following/create.js';
import * as ep___following_delete from './endpoints/following/delete.js';
import * as ep___following_invalidate from './endpoints/following/invalidate.js';
@ -369,6 +372,7 @@ const eps = [
['admin/unsuspend-user', ep___admin_unsuspendUser],
['admin/update-meta', ep___admin_updateMeta],
['admin/vacuum', ep___admin_vacuum],
['admin/delete-account', ep___admin_deleteAccount],
['announcements', ep___announcements],
['antennas/create', ep___antennas_create],
['antennas/delete', ep___antennas_delete],
@ -409,6 +413,7 @@ const eps = [
['charts/user/reactions', ep___charts_user_reactions],
['charts/users', ep___charts_users],
['clips/add-note', ep___clips_addNote],
['clips/remove-note', ep___clips_removeNote],
['clips/create', ep___clips_create],
['clips/delete', ep___clips_delete],
['clips/list', ep___clips_list],
@ -443,6 +448,7 @@ const eps = [
['federation/show-instance', ep___federation_showInstance],
['federation/update-remote-user', ep___federation_updateRemoteUser],
['federation/users', ep___federation_users],
['federation/stats', ep___federation_stats],
['following/create', ep___following_create],
['following/delete', ep___following_delete],
['following/invalidate', ep___following_invalidate],
@ -699,6 +705,16 @@ export interface IEndpointMeta {
readonly kind?: string;
readonly description?: string;
/**
* GETでのリクエストを許容するか否か
*/
readonly allowGet?: boolean;
/**
* (Cache-Control: public)
*/
readonly cacheSec?: number;
}
export interface IEndpoint {

View file

@ -0,0 +1,31 @@
import { Users } from '@/models/index.js';
import { deleteAccount } from '@/services/delete-account.js';
import define from '../../define.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireAdmin: true,
res: {
},
} as const;
export const paramDef = {
type: 'object',
properties: {
userId: { type: 'string', format: 'misskey:id' },
},
required: ['userId'],
} as const;
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps) => {
const user = await Users.findOneByOrFail({ id: ps.userId });
if (user.isDeleted) {
return;
}
await deleteAccount(user);
});

View file

@ -1,5 +1,5 @@
import define from '../../../define.js';
import { DriveFiles } from '@/models/index.js';
import define from '../../../define.js';
import { makePaginationQuery } from '../../../common/make-pagination-query.js';
export const meta = {
@ -25,8 +25,9 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
userId: { type: 'string', format: 'misskey:id', nullable: true },
type: { type: 'string', nullable: true, pattern: /^[a-zA-Z0-9\/\-*]+$/.toString().slice(1, -1) },
origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: "local" },
origin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'local' },
hostname: {
type: 'string',
nullable: true,
@ -41,14 +42,18 @@ export const paramDef = {
export default define(meta, paramDef, async (ps, me) => {
const query = makePaginationQuery(DriveFiles.createQueryBuilder('file'), ps.sinceId, ps.untilId);
if (ps.origin === 'local') {
query.andWhere('file.userHost IS NULL');
} else if (ps.origin === 'remote') {
query.andWhere('file.userHost IS NOT NULL');
}
if (ps.userId) {
query.andWhere('file.userId = :userId', { userId: ps.userId });
} else {
if (ps.origin === 'local') {
query.andWhere('file.userHost IS NULL');
} else if (ps.origin === 'remote') {
query.andWhere('file.userHost IS NOT NULL');
}
if (ps.hostname) {
query.andWhere('file.userHost = :hostname', { hostname: ps.hostname });
if (ps.hostname) {
query.andWhere('file.userHost = :hostname', { hostname: ps.hostname });
}
}
if (ps.type) {

View file

@ -99,12 +99,16 @@ export default define(meta, paramDef, async () => {
const fsStats = await si.fsSize();
const netInterface = await si.networkInterfaceDefault();
const redisServerInfo = await redisClient.info('Server');
const m = redisServerInfo.match(new RegExp('^redis_version:(.*)', 'm'));
const redis_version = m?.[1];
return {
machine: os.hostname(),
os: os.platform(),
node: process.version,
psql: await db.query('SHOW server_version').then(x => x[0].server_version),
redis: redisClient.server_info.redis_version,
redis: redis_version,
cpu: {
model: os.cpus()[0].model,
cores: os.cpus().length,

View file

@ -2,12 +2,13 @@ import define from '../../define.js';
import config from '@/config/index.js';
import { createPerson } from '@/remote/activitypub/models/person.js';
import { createNote } from '@/remote/activitypub/models/note.js';
import DbResolver from '@/remote/activitypub/db-resolver.js';
import Resolver from '@/remote/activitypub/resolver.js';
import { ApiError } from '../../error.js';
import { extractDbHost } from '@/misc/convert-host.js';
import { Users, Notes } from '@/models/index.js';
import { Note } from '@/models/entities/note.js';
import { User } from '@/models/entities/user.js';
import { CacheableLocalUser, User } from '@/models/entities/user.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { isActor, isPost, getApId } from '@/remote/activitypub/type.js';
import ms from 'ms';
@ -77,8 +78,8 @@ export const paramDef = {
} as const;
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps) => {
const object = await fetchAny(ps.uri);
export default define(meta, paramDef, async (ps, me) => {
const object = await fetchAny(ps.uri, me);
if (object) {
return object;
} else {
@ -89,48 +90,18 @@ export default define(meta, paramDef, async (ps) => {
/***
* URIからUserかNoteを解決する
*/
async function fetchAny(uri: string): Promise<SchemaType<typeof meta['res']> | null> {
// URIがこのサーバーを指しているなら、ローカルユーザーIDとしてDBからフェッチ
if (uri.startsWith(config.url + '/')) {
const parts = uri.split('/');
const id = parts.pop();
const type = parts.pop();
if (type === 'notes') {
const note = await Notes.findOneBy({ id });
if (note) {
return {
type: 'Note',
object: await Notes.pack(note, null, { detail: true }),
};
}
} else if (type === 'users') {
const user = await Users.findOneBy({ id });
if (user) {
return {
type: 'User',
object: await Users.pack(user, null, { detail: true }),
};
}
}
}
async function fetchAny(uri: string, me: CacheableLocalUser | null | undefined): Promise<SchemaType<typeof meta['res']> | null> {
// ブロックしてたら中断
const fetchedMeta = await fetchMeta();
if (fetchedMeta.blockedHosts.includes(extractDbHost(uri))) return null;
// URI(AP Object id)としてDB検索
{
const [user, note] = await Promise.all([
Users.findOneBy({ uri: uri }),
Notes.findOneBy({ uri: uri }),
]);
const dbResolver = new DbResolver();
const packed = await mergePack(user, note);
if (packed !== null) return packed;
}
let local = await mergePack(me, ...await Promise.all([
dbResolver.getUserFromApId(uri),
dbResolver.getNoteFromApId(uri),
]));
if (local != null) return local;
// リモートから一旦オブジェクトフェッチ
const resolver = new Resolver();
@ -139,74 +110,37 @@ async function fetchAny(uri: string): Promise<SchemaType<typeof meta['res']> | n
// /@user のような正規id以外で取得できるURIが指定されていた場合、ここで初めて正規URIが確定する
// これはDBに存在する可能性があるため再度DB検索
if (uri !== object.id) {
if (object.id.startsWith(config.url + '/')) {
const parts = object.id.split('/');
const id = parts.pop();
const type = parts.pop();
if (type === 'notes') {
const note = await Notes.findOneBy({ id });
if (note) {
return {
type: 'Note',
object: await Notes.pack(note, null, { detail: true }),
};
}
} else if (type === 'users') {
const user = await Users.findOneBy({ id });
if (user) {
return {
type: 'User',
object: await Users.pack(user, null, { detail: true }),
};
}
}
}
const [user, note] = await Promise.all([
Users.findOneBy({ uri: object.id }),
Notes.findOneBy({ uri: object.id }),
]);
const packed = await mergePack(user, note);
if (packed !== null) return packed;
local = await mergePack(me, ...await Promise.all([
dbResolver.getUserFromApId(object.id),
dbResolver.getNoteFromApId(object.id),
]));
if (local != null) return local;
}
// それでもみつからなければ新規であるため登録
if (isActor(object)) {
const user = await createPerson(getApId(object));
return {
type: 'User',
object: await Users.pack(user, null, { detail: true }),
};
}
if (isPost(object)) {
const note = await createNote(getApId(object), undefined, true);
return {
type: 'Note',
object: await Notes.pack(note!, null, { detail: true }),
};
}
return null;
return await mergePack(
me,
isActor(object) ? await createPerson(getApId(object)) : null,
isPost(object) ? await createNote(getApId(object), undefined, true) : null,
);
}
async function mergePack(user: User | null | undefined, note: Note | null | undefined): Promise<SchemaType<typeof meta.res> | null> {
async function mergePack(me: CacheableLocalUser | null | undefined, user: User | null | undefined, note: Note | null | undefined): Promise<SchemaType<typeof meta.res> | null> {
if (user != null) {
return {
type: 'User',
object: await Users.pack(user, null, { detail: true }),
object: await Users.pack(user, me, { detail: true }),
};
}
} else if (note != null) {
try {
const object = await Notes.pack(note, me, { detail: true });
if (note != null) {
return {
type: 'Note',
object: await Notes.pack(note, null, { detail: true }),
};
return {
type: 'Note',
object,
};
} catch (e) {
return null;
}
}
return null;

View file

@ -1,11 +1,14 @@
import define from '../../define.js';
import { getJsonSchema } from '@/services/chart/core.js';
import { activeUsersChart } from '@/services/chart/index.js';
import define from '../../define.js';
export const meta = {
tags: ['charts', 'users'],
res: getJsonSchema(activeUsersChart.schema),
allowGet: true,
cacheSec: 60 * 60,
} as const;
export const paramDef = {

View file

@ -1,11 +1,14 @@
import define from '../../define.js';
import { getJsonSchema } from '@/services/chart/core.js';
import { apRequestChart } from '@/services/chart/index.js';
import define from '../../define.js';
export const meta = {
tags: ['charts'],
res: getJsonSchema(apRequestChart.schema),
allowGet: true,
cacheSec: 60 * 60,
} as const;
export const paramDef = {

View file

@ -1,11 +1,14 @@
import define from '../../define.js';
import { getJsonSchema } from '@/services/chart/core.js';
import { driveChart } from '@/services/chart/index.js';
import define from '../../define.js';
export const meta = {
tags: ['charts', 'drive'],
res: getJsonSchema(driveChart.schema),
allowGet: true,
cacheSec: 60 * 60,
} as const;
export const paramDef = {

View file

@ -1,11 +1,14 @@
import define from '../../define.js';
import { getJsonSchema } from '@/services/chart/core.js';
import { federationChart } from '@/services/chart/index.js';
import define from '../../define.js';
export const meta = {
tags: ['charts'],
res: getJsonSchema(federationChart.schema),
allowGet: true,
cacheSec: 60 * 60,
} as const;
export const paramDef = {

View file

@ -1,11 +1,14 @@
import define from '../../define.js';
import { getJsonSchema } from '@/services/chart/core.js';
import { hashtagChart } from '@/services/chart/index.js';
import define from '../../define.js';
export const meta = {
tags: ['charts', 'hashtags'],
res: getJsonSchema(hashtagChart.schema),
allowGet: true,
cacheSec: 60 * 60,
} as const;
export const paramDef = {

View file

@ -1,11 +1,14 @@
import define from '../../define.js';
import { getJsonSchema } from '@/services/chart/core.js';
import { instanceChart } from '@/services/chart/index.js';
import define from '../../define.js';
export const meta = {
tags: ['charts'],
res: getJsonSchema(instanceChart.schema),
allowGet: true,
cacheSec: 60 * 60,
} as const;
export const paramDef = {

View file

@ -1,11 +1,14 @@
import define from '../../define.js';
import { getJsonSchema } from '@/services/chart/core.js';
import { notesChart } from '@/services/chart/index.js';
import define from '../../define.js';
export const meta = {
tags: ['charts', 'notes'],
res: getJsonSchema(notesChart.schema),
allowGet: true,
cacheSec: 60 * 60,
} as const;
export const paramDef = {

View file

@ -1,11 +1,14 @@
import define from '../../../define.js';
import { getJsonSchema } from '@/services/chart/core.js';
import { perUserDriveChart } from '@/services/chart/index.js';
import define from '../../../define.js';
export const meta = {
tags: ['charts', 'drive', 'users'],
res: getJsonSchema(perUserDriveChart.schema),
allowGet: true,
cacheSec: 60 * 60,
} as const;
export const paramDef = {

View file

@ -6,6 +6,9 @@ export const meta = {
tags: ['charts', 'users', 'following'],
res: getJsonSchema(perUserFollowingChart.schema),
allowGet: true,
cacheSec: 60 * 60,
} as const;
export const paramDef = {

View file

@ -1,11 +1,14 @@
import define from '../../../define.js';
import { getJsonSchema } from '@/services/chart/core.js';
import { perUserNotesChart } from '@/services/chart/index.js';
import define from '../../../define.js';
export const meta = {
tags: ['charts', 'users', 'notes'],
res: getJsonSchema(perUserNotesChart.schema),
allowGet: true,
cacheSec: 60 * 60,
} as const;
export const paramDef = {

View file

@ -1,11 +1,14 @@
import define from '../../../define.js';
import { getJsonSchema } from '@/services/chart/core.js';
import { perUserReactionsChart } from '@/services/chart/index.js';
import define from '../../../define.js';
export const meta = {
tags: ['charts', 'users', 'reactions'],
res: getJsonSchema(perUserReactionsChart.schema),
allowGet: true,
cacheSec: 60 * 60,
} as const;
export const paramDef = {

View file

@ -1,11 +1,14 @@
import define from '../../define.js';
import { getJsonSchema } from '@/services/chart/core.js';
import { usersChart } from '@/services/chart/index.js';
import define from '../../define.js';
export const meta = {
tags: ['charts', 'users'],
res: getJsonSchema(usersChart.schema),
allowGet: true,
cacheSec: 60 * 60,
} as const;
export const paramDef = {

View file

@ -0,0 +1,57 @@
import define from '../../define.js';
import { ClipNotes, Clips } from '@/models/index.js';
import { ApiError } from '../../error.js';
import { getNote } from '../../common/getters.js';
export const meta = {
tags: ['account', 'notes', 'clips'],
requireCredential: true,
kind: 'write:account',
errors: {
noSuchClip: {
message: 'No such clip.',
code: 'NO_SUCH_CLIP',
id: 'b80525c6-97f7-49d7-a42d-ebccd49cfd52',
},
noSuchNote: {
message: 'No such note.',
code: 'NO_SUCH_NOTE',
id: 'aff017de-190e-434b-893e-33a9ff5049d8',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
clipId: { type: 'string', format: 'misskey:id' },
noteId: { type: 'string', format: 'misskey:id' },
},
required: ['clipId', 'noteId'],
} as const;
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, user) => {
const clip = await Clips.findOneBy({
id: ps.clipId,
userId: user.id,
});
if (clip == null) {
throw new ApiError(meta.errors.noSuchClip);
}
const note = await getNote(ps.noteId).catch(e => {
if (e.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
throw e;
});
await ClipNotes.delete({
noteId: note.id,
clipId: clip.id,
});
});

View file

@ -0,0 +1,64 @@
import { IsNull, MoreThan, Not } from 'typeorm';
import { Followings, Instances } from '@/models/index.js';
import { awaitAll } from '@/prelude/await-all.js';
import define from '../../define.js';
export const meta = {
tags: ['federation'],
requireCredential: false,
allowGet: true,
cacheSec: 60 * 60,
} 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) => {
const [topSubInstances, topPubInstances, allSubCount, allPubCount] = await Promise.all([
Instances.find({
where: {
followersCount: MoreThan(0),
},
order: {
followersCount: 'DESC',
},
take: 10,
}),
Instances.find({
where: {
followingCount: MoreThan(0),
},
order: {
followingCount: 'DESC',
},
take: 10,
}),
Followings.count({
where: {
followeeHost: Not(IsNull()),
},
}),
Followings.count({
where: {
followerHost: Not(IsNull()),
},
}),
]);
const gotSubCount = topSubInstances.map(x => x.followersCount).reduce((a, b) => a + b, 0);
const gotPubCount = topSubInstances.map(x => x.followingCount).reduce((a, b) => a + b, 0);
return await awaitAll({
topSubInstances: Instances.packMany(topSubInstances),
otherFollowersCount: Math.max(0, allSubCount - gotSubCount),
topPubInstances: Instances.packMany(topPubInstances),
otherFollowingCount: Math.max(0, allPubCount - gotPubCount),
});
});

View file

@ -1,9 +1,7 @@
import bcrypt from 'bcryptjs';
import define from '../../define.js';
import { UserProfiles, Users } from '@/models/index.js';
import { doPostSuspend } from '@/services/suspend-user.js';
import { publishUserEvent } from '@/services/stream.js';
import { createDeleteAccountJob } from '@/queue/index.js';
import { deleteAccount } from '@/services/delete-account.js';
import define from '../../define.js';
export const meta = {
requireCredential: true,
@ -34,17 +32,5 @@ export default define(meta, paramDef, async (ps, user) => {
throw new Error('incorrect password');
}
// 物理削除する前にDelete activityを送信する
await doPostSuspend(user).catch(e => {});
createDeleteAccountJob(user, {
soft: false,
});
await Users.update(user.id, {
isDeleted: true,
});
// Terminate streaming
publishUserEvent(user.id, 'terminate', {});
await deleteAccount(user);
});

View file

@ -1,11 +1,10 @@
import { Brackets } from 'typeorm';
import { Notifications, Followings, Mutings, Users } from '@/models/index.js';
import { Notifications, Followings, Mutings, Users, UserProfiles } from '@/models/index.js';
import { notificationTypes } from '@/types.js';
import read from '@/services/note/read.js';
import { readNotification } from '../../common/read-notification.js';
import define from '../../define.js';
import { makePaginationQuery } from '../../common/make-pagination-query.js';
import { generateMutedInstanceNotificationQuery } from '../../common/generate-muted-instance-query.js';
export const meta = {
tags: ['account', 'notifications'],
@ -67,6 +66,10 @@ export default define(meta, paramDef, async (ps, user) => {
.select('muting.muteeId')
.where('muting.muterId = :muterId', { muterId: user.id });
const mutingInstanceQuery = UserProfiles.createQueryBuilder('user_profile')
.select('user_profile.mutedInstances')
.where('user_profile.userId = :muterId', { muterId: user.id });
const suspendedQuery = Users.createQueryBuilder('users')
.select('users.id')
.where('users.isSuspended = TRUE');
@ -89,14 +92,21 @@ export default define(meta, paramDef, async (ps, user) => {
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
// muted users
query.andWhere(new Brackets(qb => { qb
.where(`notification.notifierId NOT IN (${ mutingQuery.getQuery() })`)
.orWhere('notification.notifierId IS NULL');
}));
query.setParameters(mutingQuery.getParameters());
generateMutedInstanceNotificationQuery(query, user);
// muted instances
query.andWhere(new Brackets(qb => { qb
.andWhere('notifier.host IS NULL')
.orWhere(`NOT (( ${mutingInstanceQuery.getQuery()} )::jsonb ? notifier.host)`);
}));
query.setParameters(mutingInstanceQuery.getParameters());
// suspended users
query.andWhere(new Brackets(qb => { qb
.where(`notification.notifierId NOT IN (${ suspendedQuery.getQuery() })`)
.orWhere('notification.notifierId IS NULL');

View file

@ -5,7 +5,6 @@ import { makePaginationQuery } from '../../common/make-pagination-query.js';
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query.js';
export const meta = {
tags: ['notes'],
@ -61,9 +60,10 @@ export default define(meta, paramDef, async (ps, user) => {
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
generateVisibilityQuery(query, user);
if (user) generateMutedUserQuery(query, user);
if (user) generateBlockedUserQuery(query, user);
if (user) generateMutedInstanceQuery(query, user);
if (user) {
generateMutedUserQuery(query, user);
generateBlockedUserQuery(query, user);
}
const notes = await query.take(ps.limit).getMany();

View file

@ -1,11 +1,10 @@
import { fetchMeta } from '@/misc/fetch-meta.js';
import { Notes, Users } from '@/models/index.js';
import { Notes } from '@/models/index.js';
import { activeUsersChart } from '@/services/chart/index.js';
import define from '../../define.js';
import { ApiError } from '../../error.js';
import { makePaginationQuery } from '../../common/make-pagination-query.js';
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query.js';
import { generateRepliesQuery } from '../../common/generate-replies-query.js';
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js';
import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
@ -76,10 +75,11 @@ export default define(meta, paramDef, async (ps, user) => {
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
generateRepliesQuery(query, user);
if (user) generateMutedUserQuery(query, user);
if (user) generateMutedNoteQuery(query, user);
if (user) generateBlockedUserQuery(query, user);
if (user) generateMutedInstanceQuery(query, user);
if (user) {
generateMutedUserQuery(query, user);
generateMutedNoteQuery(query, user);
generateBlockedUserQuery(query, user);
}
if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');

View file

@ -1,13 +1,12 @@
import { Brackets } from 'typeorm';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { Followings, Notes, Users } from '@/models/index.js';
import { Followings, Notes } from '@/models/index.js';
import { activeUsersChart } from '@/services/chart/index.js';
import define from '../../define.js';
import { ApiError } from '../../error.js';
import { makePaginationQuery } from '../../common/make-pagination-query.js';
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query.js';
import { generateRepliesQuery } from '../../common/generate-replies-query.js';
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js';
import { generateChannelQuery } from '../../common/generate-channel-query.js';
@ -92,7 +91,6 @@ export default define(meta, paramDef, async (ps, user) => {
generateRepliesQuery(query, user);
generateVisibilityQuery(query, user);
generateMutedUserQuery(query, user);
generateMutedInstanceQuery(query, user);
generateMutedNoteQuery(query, user);
generateBlockedUserQuery(query, user);
@ -134,9 +132,7 @@ export default define(meta, paramDef, async (ps, user) => {
const timeline = await query.take(ps.limit).getMany();
process.nextTick(() => {
if (user) {
activeUsersChart.read(user);
}
activeUsersChart.read(user);
});
return await Notes.packMany(timeline, user);

View file

@ -5,7 +5,6 @@ import define from '../../define.js';
import { makePaginationQuery } from '../../common/make-pagination-query.js';
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query.js';
import { generateRepliesQuery } from '../../common/generate-replies-query.js';
import { generateMutedNoteQuery } from '../../common/generate-muted-note-query.js';
import { generateChannelQuery } from '../../common/generate-channel-query.js';
@ -84,7 +83,6 @@ export default define(meta, paramDef, async (ps, user) => {
generateRepliesQuery(query, user);
generateVisibilityQuery(query, user);
generateMutedUserQuery(query, user);
generateMutedInstanceQuery(query, user);
generateMutedNoteQuery(query, user);
generateBlockedUserQuery(query, user);
@ -126,9 +124,7 @@ export default define(meta, paramDef, async (ps, user) => {
const timeline = await query.take(ps.limit).getMany();
process.nextTick(() => {
if (user) {
activeUsersChart.read(user);
}
activeUsersChart.read(user);
});
return await Notes.packMany(timeline, user);

View file

@ -7,7 +7,6 @@ import { makePaginationQuery } from '../../common/make-pagination-query.js';
import { generateVisibilityQuery } from '../../common/generate-visibility-query.js';
import { generateMutedUserQuery } from '../../common/generate-muted-user-query.js';
import { generateBlockedUserQuery } from '../../common/generate-block-query.js';
import { generateMutedInstanceQuery } from '../../common/generate-muted-instance-query.js';
export const meta = {
tags: ['users', 'notes'],
@ -77,9 +76,10 @@ export default define(meta, paramDef, async (ps, me) => {
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
generateVisibilityQuery(query, me);
if (me) generateMutedUserQuery(query, me, user);
if (me) generateBlockedUserQuery(query, me);
if (me) generateMutedInstanceQuery(query, me);
if (me) {
generateMutedUserQuery(query, me, user);
generateBlockedUserQuery(query, me);
}
if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');

View file

@ -8,6 +8,8 @@ import multer from '@koa/multer';
import bodyParser from 'koa-bodyparser';
import cors from '@koa/cors';
import { Instances, AccessTokens, Users } from '@/models/index.js';
import config from '@/config/index.js';
import endpoints from './endpoints.js';
import handler from './api-handler.js';
import signup from './private/signup.js';
@ -16,8 +18,6 @@ import signupPending from './private/signup-pending.js';
import discord from './service/discord.js';
import github from './service/github.js';
import twitter from './service/twitter.js';
import { Instances, AccessTokens, Users } from '@/models/index.js';
import config from '@/config/index.js';
// Init app
const app = new Koa();
@ -56,11 +56,24 @@ for (const endpoint of endpoints) {
if (endpoint.meta.requireFile) {
router.post(`/${endpoint.name}`, upload.single('file'), handler.bind(null, endpoint));
} else {
// 後方互換性のため
if (endpoint.name.includes('-')) {
// 後方互換性のため
router.post(`/${endpoint.name.replace(/-/g, '_')}`, handler.bind(null, endpoint));
if (endpoint.meta.allowGet) {
router.get(`/${endpoint.name.replace(/-/g, '_')}`, handler.bind(null, endpoint));
} else {
router.get(`/${endpoint.name.replace(/-/g, '_')}`, async ctx => { ctx.status = 405; });
}
}
router.post(`/${endpoint.name}`, handler.bind(null, endpoint));
if (endpoint.meta.allowGet) {
router.get(`/${endpoint.name}`, handler.bind(null, endpoint));
} else {
router.get(`/${endpoint.name}`, async ctx => { ctx.status = 405; });
}
}
}

View file

@ -7,6 +7,8 @@ import { IEndpointMeta } from './endpoints.js';
const logger = new Logger('limiter');
export const limiter = (limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string) => new Promise<void>((ok, reject) => {
if (process.env.NODE_ENV === 'test') ok();
const hasShortTermLimit = typeof limitation.minInterval === 'number';
const hasLongTermLimit =

View file

@ -1,7 +1,6 @@
import Channel from '../channel.js';
import { Notes } from '@/models/index.js';
import { isMutedUserRelated } from '@/misc/is-muted-user-related.js';
import { isBlockerUserRelated } from '@/misc/is-blocker-user-related.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { StreamMessages } from '../types.js';
export default class extends Channel {
@ -27,9 +26,9 @@ export default class extends Channel {
const note = await Notes.pack(data.body.id, this.user, { detail: true });
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isMutedUserRelated(note, this.muting)) return;
if (isUserRelated(note, this.muting)) return;
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
if (isBlockerUserRelated(note, this.blocking)) return;
if (isUserRelated(note, this.blocking)) return;
this.connection.cacheNote(note);

View file

@ -1,7 +1,6 @@
import Channel from '../channel.js';
import { Notes, Users } from '@/models/index.js';
import { isMutedUserRelated } from '@/misc/is-muted-user-related.js';
import { isBlockerUserRelated } from '@/misc/is-blocker-user-related.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { User } from '@/models/entities/user.js';
import { StreamMessages } from '../types.js';
import { Packed } from '@/misc/schema.js';
@ -45,9 +44,9 @@ export default class extends Channel {
}
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isMutedUserRelated(note, this.muting)) return;
if (isUserRelated(note, this.muting)) return;
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
if (isBlockerUserRelated(note, this.blocking)) return;
if (isUserRelated(note, this.blocking)) return;
this.connection.cacheNote(note);

View file

@ -1,10 +1,9 @@
import { isMutedUserRelated } from '@/misc/is-muted-user-related.js';
import Channel from '../channel.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { Notes } from '@/models/index.js';
import { checkWordMute } from '@/misc/check-word-mute.js';
import { isBlockerUserRelated } from '@/misc/is-blocker-user-related.js';
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { Packed } from '@/misc/schema.js';
export default class extends Channel {
@ -55,9 +54,9 @@ export default class extends Channel {
if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? []))) return;
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isMutedUserRelated(note, this.muting)) return;
if (isUserRelated(note, this.muting)) return;
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
if (isBlockerUserRelated(note, this.blocking)) return;
if (isUserRelated(note, this.blocking)) return;
// 流れてきたNoteがミュートすべきNoteだったら無視する
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)

View file

@ -1,8 +1,7 @@
import { isMutedUserRelated } from '@/misc/is-muted-user-related.js';
import Channel from '../channel.js';
import { Notes } from '@/models/index.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { isBlockerUserRelated } from '@/misc/is-blocker-user-related.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { Packed } from '@/misc/schema.js';
export default class extends Channel {
@ -38,9 +37,9 @@ export default class extends Channel {
}
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isMutedUserRelated(note, this.muting)) return;
if (isUserRelated(note, this.muting)) return;
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
if (isBlockerUserRelated(note, this.blocking)) return;
if (isUserRelated(note, this.blocking)) return;
this.connection.cacheNote(note);

View file

@ -1,8 +1,7 @@
import { isMutedUserRelated } from '@/misc/is-muted-user-related.js';
import Channel from '../channel.js';
import { Notes } from '@/models/index.js';
import { checkWordMute } from '@/misc/check-word-mute.js';
import { isBlockerUserRelated } from '@/misc/is-blocker-user-related.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
import { Packed } from '@/misc/schema.js';
@ -63,9 +62,9 @@ export default class extends Channel {
}
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isMutedUserRelated(note, this.muting)) return;
if (isUserRelated(note, this.muting)) return;
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
if (isBlockerUserRelated(note, this.blocking)) return;
if (isUserRelated(note, this.blocking)) return;
// 流れてきたNoteがミュートすべきNoteだったら無視する
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)

View file

@ -1,9 +1,8 @@
import { isMutedUserRelated } from '@/misc/is-muted-user-related.js';
import Channel from '../channel.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { Notes } from '@/models/index.js';
import { checkWordMute } from '@/misc/check-word-mute.js';
import { isBlockerUserRelated } from '@/misc/is-blocker-user-related.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { isInstanceMuted } from '@/misc/is-instance-muted.js';
import { Packed } from '@/misc/schema.js';
@ -71,9 +70,9 @@ export default class extends Channel {
}
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isMutedUserRelated(note, this.muting)) return;
if (isUserRelated(note, this.muting)) return;
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
if (isBlockerUserRelated(note, this.blocking)) return;
if (isUserRelated(note, this.blocking)) return;
// 流れてきたNoteがミュートすべきNoteだったら無視する
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)

View file

@ -1,9 +1,8 @@
import { isMutedUserRelated } from '@/misc/is-muted-user-related.js';
import Channel from '../channel.js';
import { fetchMeta } from '@/misc/fetch-meta.js';
import { Notes } from '@/models/index.js';
import { checkWordMute } from '@/misc/check-word-mute.js';
import { isBlockerUserRelated } from '@/misc/is-blocker-user-related.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { Packed } from '@/misc/schema.js';
export default class extends Channel {
@ -52,9 +51,9 @@ export default class extends Channel {
}
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isMutedUserRelated(note, this.muting)) return;
if (iUserRelated(note, this.muting)) return;
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
if (isBlockerUserRelated(note, this.blocking)) return;
if (isUserRelated(note, this.blocking)) return;
// 流れてきたNoteがミュートすべきNoteだったら無視する
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)

View file

@ -1,8 +1,7 @@
import Channel from '../channel.js';
import { Notes, UserListJoinings, UserLists } from '@/models/index.js';
import { isMutedUserRelated } from '@/misc/is-muted-user-related.js';
import { User } from '@/models/entities/user.js';
import { isBlockerUserRelated } from '@/misc/is-blocker-user-related.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { Packed } from '@/misc/schema.js';
export default class extends Channel {
@ -76,9 +75,9 @@ export default class extends Channel {
}
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isMutedUserRelated(note, this.muting)) return;
if (isUserRelated(note, this.muting)) return;
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
if (isBlockerUserRelated(note, this.blocking)) return;
if (isUserRelated(note, this.blocking)) return;
this.send('note', note);
}

View file

@ -1,13 +1,16 @@
import * as fs from 'node:fs';
import Koa from 'koa';
import { serverLogger } from '../index.js';
import sharp from 'sharp';
import { IImage, convertToWebp } from '@/services/drive/image-processor.js';
import { createTemp } from '@/misc/create-temp.js';
import { downloadUrl } from '@/misc/download-url.js';
import { detectType } from '@/misc/get-file-info.js';
import { StatusError } from '@/misc/fetch.js';
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
import { serverLogger } from '../index.js';
import { isMimeImage } from '@/misc/is-mime-image.js';
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export async function proxyMedia(ctx: Koa.Context) {
const url = 'url' in ctx.query ? ctx.query.url : 'https://' + ctx.params.url;
@ -23,14 +26,50 @@ export async function proxyMedia(ctx: Koa.Context) {
await downloadUrl(url, path);
const { mime, ext } = await detectType(path);
const isConvertibleImage = isMimeImage(mime, 'sharp-convertible-image');
let image: IImage;
if ('static' in ctx.query && ['image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/webp', 'image/svg+xml'].includes(mime)) {
if ('static' in ctx.query && isConvertibleImage) {
image = await convertToWebp(path, 498, 280);
} else if ('preview' in ctx.query && ['image/jpeg', 'image/png', 'image/gif', 'image/apng', 'image/vnd.mozilla.apng', 'image/svg+xml'].includes(mime)) {
} else if ('preview' in ctx.query && isConvertibleImage) {
image = await convertToWebp(path, 200, 200);
} else if (['image/svg+xml'].includes(mime)) {
} else if ('badge' in ctx.query) {
if (!isConvertibleImage) {
// 画像でないなら404でお茶を濁す
throw new StatusError('Unexpected mime', 404);
}
const mask = sharp(path)
.resize(96, 96, {
fit: 'inside',
withoutEnlargement: false,
})
.greyscale()
.normalise()
.linear(1.75, -(128 * 1.75) + 128) // 1.75x contrast
.flatten({ background: '#000' })
.toColorspace('b-w');
const stats = await mask.clone().stats();
if (stats.entropy < 0.1) {
// エントロピーがあまりない場合は404にする
throw new StatusError('Skip to provide badge', 404);
}
const data = sharp({
create: { width: 96, height: 96, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } },
})
.pipelineColorspace('b-w')
.boolean(await mask.png().toBuffer(), 'eor');
image = {
data: await data.png().toBuffer(),
ext: 'png',
type: 'image/png',
};
} else if (mime === 'image/svg+xml') {
image = await convertToWebp(path, 2048, 2048, 1);
} else if (!mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(mime)) {
throw new StatusError('Rejected type', 403, 'Rejected type');
@ -48,7 +87,7 @@ export async function proxyMedia(ctx: Koa.Context) {
} catch (e) {
serverLogger.error(`${e}`);
if (e instanceof StatusError && e.isClientError) {
if (e instanceof StatusError && (e.statusCode === 302 || e.isClientError)) {
ctx.status = e.statusCode;
} else {
ctx.status = 500;

View file

@ -14,10 +14,10 @@
// ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので
(async () => {
window.onerror = (e) => {
renderError('SOMETHING_HAPPENED', e.toString());
renderError('SOMETHING_HAPPENED', e);
};
window.onunhandledrejection = (e) => {
renderError('SOMETHING_HAPPENED_IN_PROMISE', e.toString());
renderError('SOMETHING_HAPPENED_IN_PROMISE', e);
};
const v = localStorage.getItem('v') || VERSION;
@ -57,7 +57,7 @@
import(`/assets/${CLIENT_ENTRY}`)
.catch(async e => {
await checkUpdate();
renderError('APP_FETCH_FAILED', JSON.stringify(e));
renderError('APP_FETCH_FAILED', e);
})
//#endregion
@ -104,20 +104,27 @@
// eslint-disable-next-line no-inner-declarations
function renderError(code, details) {
document.documentElement.innerHTML = `
<h1>エラーが発生しました</h1>
<p>問題が解決しない場合は管理者までお問い合わせください以下のオプションを試すこともできます:</p>
let errorsElement = document.getElementById('errors');
if (!errorsElement) {
document.documentElement.innerHTML = `
<h1> An error has occurred. </h1>
<p>If the problem persists, please contact the administrator. You may also try the following options:</p>
<ul>
<li><a href="/cli">簡易クライアント</a></li>
<li><a href="/bios">BIOS</a></li>
<li><a href="/flush">キャッシュをクリア</a></li>
<li>Start <a href="/cli">the simple client</a></li>
<li>Attempt to repair in <a href="/bios">BIOS</a></li>
<li><a href="/flush">Flush preferences and cache</a></li>
</ul>
<hr>
<code>ERROR CODE: ${code}</code>
<details>
${details}
</details>
`;
<div id="errors"></div>
`;
errorsElement = document.getElementById('errors');
}
const detailsElement = document.createElement('details');
detailsElement.innerHTML = `<summary><code>ERROR CODE: ${code}</code></summary>${JSON.stringify(details)}`;
errorsElement.appendChild(detailsElement);
}
// eslint-disable-next-line no-inner-declarations

View file

@ -11,6 +11,7 @@ import Router from '@koa/router';
import send from 'koa-send';
import favicon from 'koa-favicon';
import views from 'koa-views';
import sharp from 'sharp';
import { createBullBoard } from '@bull-board/api';
import { BullAdapter } from '@bull-board/api/bullAdapter.js';
import { KoaAdapter } from '@bull-board/koa';
@ -140,6 +141,49 @@ router.get('/twemoji/(.*)', async ctx => {
});
});
router.get('/twemoji-badge/(.*)', async ctx => {
const path = ctx.path.replace('/twemoji-badge/', '');
if (!path.match(/^[0-9a-f-]+\.png$/)) {
ctx.status = 404;
return;
}
const mask = await sharp(
`${_dirname}/../../../node_modules/@discordapp/twemoji/dist/svg/${path.replace('.png', '')}.svg`,
{ density: 1000 },
)
.resize(488, 488)
.greyscale()
.normalise()
.linear(1.75, -(128 * 1.75) + 128) // 1.75x contrast
.flatten({ background: '#000' })
.extend({
top: 12,
bottom: 12,
left: 12,
right: 12,
background: '#000',
})
.toColorspace('b-w')
.png()
.toBuffer();
const buffer = await sharp({
create: { width: 512, height: 512, channels: 4, background: { r: 0, g: 0, b: 0, alpha: 0 } },
})
.pipelineColorspace('b-w')
.boolean(mask, 'eor')
.resize(96, 96)
.png()
.toBuffer();
ctx.set('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\'');
ctx.set('Cache-Control', 'max-age=2592000');
ctx.set('Content-Type', 'image/png');
ctx.body = buffer;
});
// ServiceWorker
router.get(`/sw.js`, async ctx => {
await send(ctx as any, `/sw.js`, {

View file

@ -2,7 +2,7 @@ import { Antenna } from '@/models/entities/antenna.js';
import { Note } from '@/models/entities/note.js';
import { AntennaNotes, Mutings, Notes } from '@/models/index.js';
import { genId } from '@/misc/gen-id.js';
import { isMutedUserRelated } from '@/misc/is-muted-user-related.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import { publishAntennaStream, publishMainStream } from '@/services/stream.js';
import { User } from '@/models/entities/user.js';
@ -39,7 +39,7 @@ export async function addNoteToAntenna(antenna: Antenna, note: Note, noteUser: {
_note.renote = await Notes.findOneByOrFail({ id: note.renoteId });
}
if (isMutedUserRelated(_note, new Set<string>(mutings.map(x => x.muteeId)))) {
if (isUserRelated(_note, new Set<string>(mutings.map(x => x.muteeId)))) {
return;
}

View file

@ -0,0 +1,23 @@
import { Users } from '@/models/index.js';
import { createDeleteAccountJob } from '@/queue/index.js';
import { publishUserEvent } from './stream.js';
import { doPostSuspend } from './suspend-user.js';
export async function deleteAccount(user: {
id: string;
host: string | null;
}): Promise<void> {
// 物理削除する前にDelete activityを送信する
await doPostSuspend(user).catch(e => {});
createDeleteAccountJob(user, {
soft: false,
});
await Users.update(user.id, {
isDeleted: true,
});
// Terminate streaming
publishUserEvent(user.id, 'terminate', {});
}

View file

@ -14,7 +14,6 @@ export async function deliverQuestionUpdate(noteId: Note['id']) {
if (user == null) throw new Error('note not found');
if (Users.isLocalUser(user)) {
const content = renderActivity(renderUpdate(await renderNote(note, false), user));
deliverToFollowers(user, content);
deliverToRelays(user, content);

View file

@ -2,7 +2,7 @@ process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import * as childProcess from 'child_process';
import { async, signup, request, post, react, connectStream, startServer, shutdownServer } from './utils.js';
import { async, signup, request, post, react, startServer, shutdownServer, waitFire } from './utils.js';
describe('Mute', () => {
let p: childProcess.ChildProcess;
@ -55,48 +55,24 @@ describe('Mute', () => {
assert.strictEqual(res.body.hasUnreadMentions, false);
}));
it('ミュートしているユーザーからメンションされても、ストリームに unreadMention イベントが流れてこない', () => new Promise(async done => {
it('ミュートしているユーザーからメンションされても、ストリームに unreadMention イベントが流れてこない', async () => {
// 状態リセット
await request('/i/read-all-unread-notes', {}, alice);
let fired = false;
const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadMention');
const ws = await connectStream(alice, 'main', ({ type }) => {
if (type == 'unreadMention') {
fired = true;
}
});
assert.strictEqual(fired, false);
});
post(carol, { text: '@alice hi' });
setTimeout(() => {
assert.strictEqual(fired, false);
ws.close();
done();
}, 5000);
}));
it('ミュートしているユーザーからメンションされても、ストリームに unreadNotification イベントが流れてこない', () => new Promise(async done => {
it('ミュートしているユーザーからメンションされても、ストリームに unreadNotification イベントが流れてこない', async () => {
// 状態リセット
await request('/i/read-all-unread-notes', {}, alice);
await request('/notifications/mark-all-as-read', {}, alice);
let fired = false;
const fired = await waitFire(alice, 'main', () => post(carol, { text: '@alice hi' }), msg => msg.type === 'unreadNotification');
const ws = await connectStream(alice, 'main', ({ type }) => {
if (type == 'unreadNotification') {
fired = true;
}
});
post(carol, { text: '@alice hi' });
setTimeout(() => {
assert.strictEqual(fired, false);
ws.close();
done();
}, 5000);
}));
assert.strictEqual(fired, false);
});
describe('Timeline', () => {
it('タイムラインにミュートしているユーザーの投稿が含まれない', async(async () => {

View file

@ -3,7 +3,7 @@ process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import * as childProcess from 'child_process';
import { Note } from '../src/models/entities/note.js';
import { async, signup, request, post, uploadFile, startServer, shutdownServer, initTestDb } from './utils.js';
import { async, signup, request, post, uploadUrl, startServer, shutdownServer, initTestDb, api } from './utils.js';
describe('Note', () => {
let p: childProcess.ChildProcess;
@ -37,7 +37,7 @@ describe('Note', () => {
}));
it('ファイルを添付できる', async(async () => {
const file = await uploadFile(alice);
const file = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg');
const res = await request('/notes/create', {
fileIds: [file.id],
@ -49,7 +49,7 @@ describe('Note', () => {
}));
it('他人のファイルは無視', async(async () => {
const file = await uploadFile(bob);
const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg');
const res = await request('/notes/create', {
text: 'test',
@ -72,11 +72,13 @@ describe('Note', () => {
assert.deepStrictEqual(res.body.createdNote.fileIds, []);
}));
it('不正なファイルIDで怒られる', async(async () => {
it('不正なファイルIDは無視', async(async () => {
const res = await request('/notes/create', {
fileIds: ['kyoppie'],
}, alice);
assert.strictEqual(res.status, 400);
assert.strictEqual(res.status, 200);
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.deepStrictEqual(res.body.createdNote.fileIds, []);
}));
it('返信できる', async(async () => {
@ -136,7 +138,7 @@ describe('Note', () => {
it('文字数ぎりぎりで怒られない', async(async () => {
const post = {
text: '!'.repeat(500),
text: '!'.repeat(3000),
};
const res = await request('/notes/create', post, alice);
assert.strictEqual(res.status, 200);
@ -144,7 +146,7 @@ describe('Note', () => {
it('文字数オーバーで怒られる', async(async () => {
const post = {
text: '!'.repeat(501),
text: '!'.repeat(3001),
};
const res = await request('/notes/create', post, alice);
assert.strictEqual(res.status, 400);
@ -207,7 +209,7 @@ describe('Note', () => {
assert.strictEqual(typeof res.body === 'object' && !Array.isArray(res.body), true);
assert.strictEqual(res.body.createdNote.text, post.text);
const noteDoc = await Notes.findOne(res.body.createdNote.id);
const noteDoc = await Notes.findOneBy({ id: res.body.createdNote.id });
assert.deepStrictEqual(noteDoc.mentions, [bob.id]);
}));
@ -336,32 +338,32 @@ describe('Note', () => {
describe('notes/delete', () => {
it('delete a reply', async(async () => {
const mainNoteRes = await request('/notes/create', {
const mainNoteRes = await api('notes/create', {
text: 'main post',
}, alice);
const replyOneRes = await request('/notes/create', {
const replyOneRes = await api('notes/create', {
text: 'reply one',
replyId: mainNoteRes.body.createdNote.id,
}, alice);
const replyTwoRes = await request('/notes/create', {
const replyTwoRes = await api('notes/create', {
text: 'reply two',
replyId: mainNoteRes.body.createdNote.id,
}, alice);
const deleteOneRes = await request('/notes/delete', {
const deleteOneRes = await api('notes/delete', {
noteId: replyOneRes.body.createdNote.id,
}, alice);
assert.strictEqual(deleteOneRes.status, 204);
let mainNote = await Notes.findOne({ id: mainNoteRes.body.createdNote.id });
let mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id });
assert.strictEqual(mainNote.repliesCount, 1);
const deleteTwoRes = await request('/notes/delete', {
const deleteTwoRes = await api('notes/delete', {
noteId: replyTwoRes.body.createdNote.id,
}, alice);
assert.strictEqual(deleteTwoRes.status, 204);
mainNote = await Notes.findOne({ id: mainNoteRes.body.createdNote.id });
mainNote = await Notes.findOneBy({ id: mainNoteRes.body.createdNote.id });
assert.strictEqual(mainNote.repliesCount, 0);
}));
});

View file

@ -2,12 +2,7 @@ process.env.NODE_ENV = 'test';
import * as assert from 'assert';
import * as childProcess from 'child_process';
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { async, signup, request, post, uploadFile, startServer, shutdownServer } from './utils.js';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
import { async, signup, request, post, uploadUrl, startServer, shutdownServer } from './utils.js';
describe('users/notes', () => {
let p: childProcess.ChildProcess;
@ -20,8 +15,8 @@ describe('users/notes', () => {
before(async () => {
p = await startServer();
alice = await signup({ username: 'alice' });
const jpg = await uploadFile(alice, _dirname + '/resources/Lenna.jpg');
const png = await uploadFile(alice, _dirname + '/resources/Lenna.png');
const jpg = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.jpg');
const png = await uploadUrl(alice, 'https://raw.githubusercontent.com/misskey-dev/misskey/develop/packages/backend/test/resources/Lenna.png');
jpgNote = await post(alice, {
fileIds: [jpg.id],
});

View file

@ -1,16 +1,18 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
import * as childProcess from 'child_process';
import * as http from 'node:http';
import { SIGKILL } from 'constants';
import * as WebSocket from 'ws';
import WebSocket from 'ws';
import * as misskey from 'misskey-js';
import fetch from 'node-fetch';
import FormData from 'form-data';
import { DataSource } from 'typeorm';
import loadConfig from '../src/config/load.js';
import { entities } from '../src/db/postgre.js';
import got from 'got';
const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
@ -26,6 +28,42 @@ export const async = (fn: Function) => (done: Function) => {
});
};
export const api = async (endpoint: string, params: any, me?: any) => {
endpoint = endpoint.replace(/^\//, '');
const auth = me ? {
i: me.token
} : {};
const res = await got<string>(`http://localhost:${port}/api/${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(Object.assign(auth, params)),
retry: {
limit: 0,
},
hooks: {
beforeError: [
error => {
const { response } = error;
if (response && response.body) console.warn(response.body);
return error;
}
]
},
});
const status = res.statusCode;
const body = res.statusCode !== 204 ? await JSON.parse(res.body) : null;
return {
status,
body
};
};
export const request = async (endpoint: string, params: any, me?: any): Promise<{ body: any, status: number }> => {
const auth = me ? {
i: me.token,
@ -53,7 +91,7 @@ export const signup = async (params?: any): Promise<any> => {
password: 'test',
}, params);
const res = await request('/signup', q);
const res = await api('signup', q);
return res.body;
};
@ -63,34 +101,62 @@ export const post = async (user: any, params?: misskey.Endpoints['notes/create']
text: 'test',
}, params);
const res = await request('/notes/create', q, user);
const res = await api('notes/create', q, user);
return res.body ? res.body.createdNote : null;
};
export const react = async (user: any, note: any, reaction: string): Promise<any> => {
await request('/notes/reactions/create', {
await api('notes/reactions/create', {
noteId: note.id,
reaction: reaction,
}, user);
};
export const uploadFile = (user: any, path?: string): Promise<any> => {
const formData = new FormData();
formData.append('i', user.token);
formData.append('file', fs.createReadStream(path || _dirname + '/resources/Lenna.png'));
/**
* Upload file
* @param user User
* @param _path Optional, absolute path or relative from ./resources/
*/
export const uploadFile = async (user: any, _path?: string): Promise<any> => {
const absPath = _path == null ? `${_dirname}/resources/Lenna.jpg` : path.isAbsolute(_path) ? _path : `${_dirname}/resources/${_path}`;
return fetch(`http://localhost:${port}/api/drive/files/create`, {
method: 'post',
const formData = new FormData() as any;
formData.append('i', user.token);
formData.append('file', fs.createReadStream(absPath));
formData.append('force', 'true');
const res = await got<string>(`http://localhost:${port}/api/drive/files/create`, {
method: 'POST',
body: formData,
timeout: 30 * 1000,
}).then(res => {
if (!res.ok) {
throw `${res.status} ${res.statusText}`;
} else {
return res.json();
retry: {
limit: 0,
},
});
const body = res.statusCode !== 204 ? await JSON.parse(res.body) : null;
return body;
};
export const uploadUrl = async (user: any, url: string) => {
let file: any;
const ws = await connectStream(user, 'main', (msg) => {
if (msg.type === 'driveFileCreated') {
file = msg.body;
}
});
await api('drive/files/upload-from-url', {
url,
force: true,
}, user);
await sleep(5000);
ws.close();
return file;
};
export function connectStream(user: any, channel: string, listener: (message: Record<string, any>) => any, params?: any): Promise<WebSocket> {
@ -120,6 +186,40 @@ export function connectStream(user: any, channel: string, listener: (message: Re
});
}
export const waitFire = async (user: any, channel: string, trgr: () => any, cond: (msg: Record<string, any>) => boolean) => {
return new Promise<boolean>(async (res, rej) => {
let timer: NodeJS.Timeout;
let ws: WebSocket;
try {
ws = await connectStream(user, channel, msg => {
if (cond(msg)) {
ws.close();
if (timer) clearTimeout(timer);
res(true);
}
});
} catch (e) {
rej(e);
}
if (!ws!) return;
timer = setTimeout(() => {
ws.close();
res(false);
}, 5000);
try {
await trgr();
} catch (e) {
ws.close();
if (timer) clearTimeout(timer);
rej(e);
}
})
};
export const simpleGet = async (path: string, accept = '*/*'): Promise<{ status?: number, type?: string, location?: string }> => {
// node-fetchだと3xxを取れない
return await new Promise((resolve, reject) => {
@ -176,7 +276,7 @@ export async function initTestDb(justBorrow = false, initEntities?: any[]) {
return db;
}
export function startServer(timeout = 30 * 1000): Promise<childProcess.ChildProcess> {
export function startServer(timeout = 60 * 1000): Promise<childProcess.ChildProcess> {
return new Promise((res, rej) => {
const t = setTimeout(() => {
p.kill(SIGKILL);
@ -214,3 +314,11 @@ export function shutdownServer(p: childProcess.ChildProcess, timeout = 20 * 1000
p.kill();
});
}
export function sleep(msec: number) {
return new Promise<void>(res => {
setTimeout(() => {
res();
}, msec);
});
}