Merge branch 'develop' into pr/ThatOneCalculator/8764
|
|
@ -5,6 +5,6 @@
|
|||
"loader=./test/loader.js"
|
||||
],
|
||||
"slow": 1000,
|
||||
"timeout": 10000,
|
||||
"timeout": 30000,
|
||||
"exit": true
|
||||
}
|
||||
|
|
|
|||
5
packages/backend/assets/notification-badges/LICENSE
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
Font Awesome Icons
|
||||
-------------------------
|
||||
|
||||
Ⓒ Font Awesome
|
||||
CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/)
|
||||
BIN
packages/backend/assets/notification-badges/at.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
packages/backend/assets/notification-badges/check.png
Normal file
|
After Width: | Height: | Size: 577 B |
|
After Width: | Height: | Size: 1.4 KiB |
BIN
packages/backend/assets/notification-badges/clock.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
packages/backend/assets/notification-badges/comments.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
packages/backend/assets/notification-badges/id-card-alt.png
Normal file
|
After Width: | Height: | Size: 844 B |
BIN
packages/backend/assets/notification-badges/null.png
Normal file
|
After Width: | Height: | Size: 174 B |
BIN
packages/backend/assets/notification-badges/plus.png
Normal file
|
After Width: | Height: | Size: 507 B |
BIN
packages/backend/assets/notification-badges/poll-h.png
Normal file
|
After Width: | Height: | Size: 689 B |
BIN
packages/backend/assets/notification-badges/quote-right.png
Normal file
|
After Width: | Height: | Size: 772 B |
BIN
packages/backend/assets/notification-badges/reply.png
Normal file
|
After Width: | Height: | Size: 930 B |
BIN
packages/backend/assets/notification-badges/retweet.png
Normal file
|
After Width: | Height: | Size: 798 B |
BIN
packages/backend/assets/notification-badges/user-plus.png
Normal file
|
After Width: | Height: | Size: 991 B |
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export type Source = {
|
|||
redis: {
|
||||
host: string;
|
||||
port: number;
|
||||
family?: number;
|
||||
pass: string;
|
||||
db?: number;
|
||||
prefix?: string;
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
8
packages/backend/src/misc/is-mime-image.ts
Normal 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);
|
||||
|
|
@ -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;
|
||||
}
|
||||
15
packages/backend/src/misc/is-user-related.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
|
@ -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),
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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 != \'{}\'');
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 != \'{}\'');
|
||||
|
|
|
|||
|
|
@ -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; });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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テーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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テーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||
|
|
|
|||
|
|
@ -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テーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||
|
|
|
|||
|
|
@ -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テーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`, {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
23
packages/backend/src/services/delete-account.ts
Normal 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', {});
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}));
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||