1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-05-18 02:35:32 +02:00

Merge branch 'develop' into mahjong

This commit is contained in:
syuilo
2026-01-22 11:47:13 +09:00
1550 changed files with 116596 additions and 65108 deletions

View File

@@ -7,7 +7,6 @@ import { randomUUID } from 'node:crypto';
import * as fs from 'node:fs';
import * as stream from 'node:stream/promises';
import { Inject, Injectable } from '@nestjs/common';
import * as Sentry from '@sentry/node';
import { DI } from '@/di-symbols.js';
import { getIpHash } from '@/misc/get-ip-hash.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
@@ -37,6 +36,7 @@ export class ApiCallService implements OnApplicationShutdown {
private logger: Logger;
private userIpHistories: Map<MiUser['id'], Set<string>>;
private userIpHistoriesClearIntervalId: NodeJS.Timeout;
private Sentry: typeof import('@sentry/node') | null = null;
constructor(
@Inject(DI.meta)
@@ -59,6 +59,12 @@ export class ApiCallService implements OnApplicationShutdown {
this.userIpHistoriesClearIntervalId = setInterval(() => {
this.userIpHistories.clear();
}, 1000 * 60 * 60);
if (this.config.sentryForBackend) {
import('@sentry/node').then((Sentry) => {
this.Sentry = Sentry;
});
}
}
#sendApiError(reply: FastifyReply, err: ApiError): void {
@@ -120,8 +126,8 @@ export class ApiCallService implements OnApplicationShutdown {
},
});
if (this.config.sentryForBackend) {
Sentry.captureMessage(`Internal error occurred in ${ep.name}: ${err.message}`, {
if (this.Sentry != null) {
this.Sentry.captureMessage(`Internal error occurred in ${ep.name}: ${err.message}`, {
level: 'error',
user: {
id: userId,
@@ -307,11 +313,14 @@ export class ApiCallService implements OnApplicationShutdown {
}
if (ep.meta.limit) {
// koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app.
let limitActor: string;
let limitActor: string | null = null;
if (user) {
limitActor = user.id;
} else {
} else if (this.config.enableIpRateLimit) {
if (process.env.NODE_ENV === 'production' && (request.ip === '::1' || request.ip === '127.0.0.1')) {
this.logger.warn('Recieved API request from localhost IP address for rate limiting in production environment. This is likely due to an improper trustProxy setting in the config file.');
}
limitActor = getIpHash(request.ip);
}
@@ -324,21 +333,17 @@ export class ApiCallService implements OnApplicationShutdown {
// TODO: 毎リクエスト計算するのもあれだしキャッシュしたい
const factor = user ? (await this.roleService.getUserPolicies(user.id)).rateLimitFactor : 1;
if (factor > 0) {
if (limitActor != null && factor > 0) {
// Rate limit
await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor).catch(err => {
if ('info' in err) {
// errはLimiter.LimiterInfoであることが期待される
throw new ApiError({
message: 'Rate limit exceeded. Please try again later.',
code: 'RATE_LIMIT_EXCEEDED',
id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
httpStatusCode: 429,
}, err.info);
} else {
throw new TypeError('information must be a rate-limiter information.');
}
});
const rateLimit = await this.rateLimiterService.limit(limit as IEndpointMeta['limit'] & { key: NonNullable<string> }, limitActor, factor);
if (rateLimit != null) {
throw new ApiError({
message: 'Rate limit exceeded. Please try again later.',
code: 'RATE_LIMIT_EXCEEDED',
id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
httpStatusCode: 429,
}, rateLimit.info);
}
}
}
@@ -421,7 +426,7 @@ export class ApiCallService implements OnApplicationShutdown {
if (['boolean', 'number', 'integer'].includes(param.type ?? '') && typeof data[k] === 'string') {
try {
data[k] = JSON.parse(data[k]);
} catch (e) {
} catch (_) {
throw new ApiError({
message: 'Invalid param.',
code: 'INVALID_PARAM',
@@ -436,8 +441,8 @@ export class ApiCallService implements OnApplicationShutdown {
}
// API invoking
if (this.config.sentryForBackend) {
return await Sentry.startSpan({
if (this.Sentry != null) {
return await this.Sentry.startSpan({
name: 'API: ' + ep.name,
}, () => ep.exec(data, user, token, file, request.ip, request.headers)
.catch((err: Error) => this.#onExecError(ep, data, err, user?.id)));

View File

@@ -176,6 +176,17 @@ export class ApiServerService {
}
});
fastify.all('/clear-browser-cache', (request, reply) => {
if (['GET', 'POST'].includes(request.method)) {
reply.header('Clear-Site-Data', '"cache", "prefetchCache", "prerenderCache", "executionContexts"');
reply.code(204);
reply.send();
} else {
reply.code(405);
reply.send();
}
});
// Make sure any unknown path under /api returns HTTP 404 Not Found,
// because otherwise ClientServerService will return the base client HTML
// page with HTTP 200.

View File

@@ -40,8 +40,8 @@ export class GetterService {
}
@bindThis
public async getNoteWithUser(noteId: MiNote['id']) {
const note = await this.notesRepository.findOne({ where: { id: noteId }, relations: ['user'] });
public async getNoteWithRelations(noteId: MiNote['id']) {
const note = await this.notesRepository.findOne({ where: { id: noteId }, relations: ['user', 'reply', 'renote', 'reply.user', 'renote.user'] });
if (note == null) {
throw new IdentifiableError('9725d0ce-ba28-4dde-95a7-2cbb2c15de24', 'No such note.');

View File

@@ -12,6 +12,14 @@ import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
import type { IEndpointMeta } from './endpoints.js';
type RateLimitInfo = {
code: 'BRIEF_REQUEST_INTERVAL',
info: Limiter.LimiterInfo,
} | {
code: 'RATE_LIMIT_EXCEEDED',
info: Limiter.LimiterInfo,
};
@Injectable()
export class RateLimiterService {
private logger: Logger;
@@ -31,77 +39,55 @@ export class RateLimiterService {
}
@bindThis
public limit(limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string, factor = 1) {
{
if (this.disabled) {
return Promise.resolve();
}
private checkLimiter(options: Limiter.LimiterOption): Promise<Limiter.LimiterInfo> {
return new Promise<Limiter.LimiterInfo>((resolve, reject) => {
new Limiter(options).get((err, info) => {
if (err) {
return reject(err);
}
resolve(info);
});
});
}
// Short-term limit
const min = new Promise<void>((ok, reject) => {
const minIntervalLimiter = new Limiter({
id: `${actor}:${limitation.key}:min`,
duration: limitation.minInterval! * factor,
max: 1,
db: this.redisClient,
});
@bindThis
public async limit(limitation: IEndpointMeta['limit'] & { key: NonNullable<string> }, actor: string, factor = 1): Promise<RateLimitInfo | null> {
if (this.disabled) {
return null;
}
minIntervalLimiter.get((err, info) => {
if (err) {
return reject({ code: 'ERR', info });
}
this.logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`);
if (info.remaining === 0) {
return reject({ code: 'BRIEF_REQUEST_INTERVAL', info });
} else {
if (hasLongTermLimit) {
return max.then(ok, reject);
} else {
return ok();
}
}
});
// Short-term limit
if (limitation.minInterval != null) {
const info = await this.checkLimiter({
id: `${actor}:${limitation.key}:min`,
duration: limitation.minInterval * factor,
max: 1,
db: this.redisClient,
});
// Long term limit
const max = new Promise<void>((ok, reject) => {
const limiter = new Limiter({
id: `${actor}:${limitation.key}`,
duration: limitation.duration! * factor,
max: limitation.max! / factor,
db: this.redisClient,
});
this.logger.debug(`${actor} ${limitation.key} min remaining: ${info.remaining}`);
limiter.get((err, info) => {
if (err) {
return reject({ code: 'ERR', info });
}
this.logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`);
if (info.remaining === 0) {
return reject({ code: 'RATE_LIMIT_EXCEEDED', info });
} else {
return ok();
}
});
});
const hasShortTermLimit = typeof limitation.minInterval === 'number';
const hasLongTermLimit =
typeof limitation.duration === 'number' &&
typeof limitation.max === 'number';
if (hasShortTermLimit) {
return min;
} else if (hasLongTermLimit) {
return max;
} else {
return Promise.resolve();
if (info.remaining === 0) {
return { code: 'BRIEF_REQUEST_INTERVAL', info };
}
}
// Long term limit
if (limitation.duration != null && limitation.max != null) {
const info = await this.checkLimiter({
id: `${actor}:${limitation.key}`,
duration: limitation.duration,
max: limitation.max / factor,
db: this.redisClient,
});
this.logger.debug(`${actor} ${limitation.key} max remaining: ${info.remaining}`);
if (info.remaining === 0) {
return { code: 'RATE_LIMIT_EXCEEDED', info };
}
}
return null;
}
}

View File

@@ -15,6 +15,7 @@ import type {
UserSecurityKeysRepository,
UsersRepository,
} from '@/models/_.js';
import type Logger from '@/logger.js';
import type { Config } from '@/config.js';
import { getIpHash } from '@/misc/get-ip-hash.js';
import type { MiLocalUser } from '@/models/User.js';
@@ -23,6 +24,7 @@ import { bindThis } from '@/decorators.js';
import { WebAuthnService } from '@/core/WebAuthnService.js';
import { UserAuthService } from '@/core/UserAuthService.js';
import { CaptchaService } from '@/core/CaptchaService.js';
import { LoggerService } from '@/core/LoggerService.js';
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
import { RateLimiterService } from './RateLimiterService.js';
import { SigninService } from './SigninService.js';
@@ -31,6 +33,8 @@ import type { FastifyReply, FastifyRequest } from 'fastify';
@Injectable()
export class SigninApiService {
private logger: Logger;
constructor(
@Inject(DI.config)
private config: Config,
@@ -50,6 +54,7 @@ export class SigninApiService {
@Inject(DI.signinsRepository)
private signinsRepository: SigninsRepository,
private loggerService: LoggerService,
private idService: IdService,
private rateLimiterService: RateLimiterService,
private signinService: SigninService,
@@ -57,6 +62,7 @@ export class SigninApiService {
private webAuthnService: WebAuthnService,
private captchaService: CaptchaService,
) {
this.logger = this.loggerService.getLogger('Signin');
}
@bindThis
@@ -89,18 +95,22 @@ export class SigninApiService {
return { error };
}
try {
// not more than 1 attempt per second and not more than 10 attempts per hour
await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip));
} catch (err) {
reply.code(429);
return {
error: {
message: 'Too many failed attempts to sign in. Try again later.',
code: 'TOO_MANY_AUTHENTICATION_FAILURES',
id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
},
};
if (this.config.enableIpRateLimit) {
if (process.env.NODE_ENV === 'production' && (request.ip === '::1' || request.ip === '127.0.0.1')) {
this.logger.warn('Recieved signin request from localhost IP address for rate limiting in production environment. This is likely due to an improper trustProxy setting in the config file.');
}
const rateLimit = await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip));
if (rateLimit != null) {
reply.code(429);
return {
error: {
message: 'Too many failed attempts to sign in. Try again later.',
code: 'TOO_MANY_AUTHENTICATION_FAILURES',
id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
},
};
}
}
if (typeof username !== 'string') {
@@ -221,7 +231,7 @@ export class SigninApiService {
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
} catch (e) {
} catch (_) {
return await fail(403, {
id: 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f',
});

View File

@@ -84,19 +84,25 @@ export class SigninWithPasskeyApiService {
return error(status ?? 500, failure ?? { id: '4e30e80c-e338-45a0-8c8f-44455efa3b76' });
};
try {
if (this.config.enableIpRateLimit) {
if (process.env.NODE_ENV === 'production' && (request.ip === '::1' || request.ip === '127.0.0.1')) {
this.logger.warn('Recieved signin with passkey request from localhost IP address for rate limiting in production environment. This is likely due to an improper trustProxy setting in the config file.');
}
try {
// Not more than 1 API call per 250ms and not more than 100 attempts per 30min
// NOTE: 1 Sign-in require 2 API calls
await this.rateLimiterService.limit({ key: 'signin-with-passkey', duration: 60 * 30 * 1000, max: 200, minInterval: 250 }, getIpHash(request.ip));
} catch (err) {
reply.code(429);
return {
error: {
message: 'Too many failed attempts to sign in. Try again later.',
code: 'TOO_MANY_AUTHENTICATION_FAILURES',
id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
},
};
await this.rateLimiterService.limit({ key: 'signin-with-passkey', duration: 60 * 30 * 1000, max: 200, minInterval: 250 }, getIpHash(request.ip));
} catch (_) {
reply.code(429);
return {
error: {
message: 'Too many failed attempts to sign in. Try again later.',
code: 'TOO_MANY_AUTHENTICATION_FAILURES',
id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
},
};
}
}
// Initiate Passkey Auth challenge with context

View File

@@ -129,7 +129,8 @@ export class SignupApiService {
let ticket: MiRegistrationTicket | null = null;
if (this.meta.disableRegistration) {
// テスト時はこの機構は障害となるため無効にする
if (process.env.NODE_ENV !== 'test' && this.meta.disableRegistration) {
if (invitationCode == null || typeof invitationCode !== 'string') {
reply.code(400);
return;
@@ -254,7 +255,7 @@ export class SignupApiService {
throw new FastifyReplyError(400, 'EXPIRED');
}
const { account, secret } = await this.signupService.signup({
const { account } = await this.signupService.signup({
username: pendingUser.username,
passwordHash: pendingUser.password,
});

View File

@@ -8,17 +8,14 @@ import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import * as WebSocket from 'ws';
import { DI } from '@/di-symbols.js';
import type { UsersRepository, MiAccessToken } from '@/models/_.js';
import { NotificationService } from '@/core/NotificationService.js';
import type { MiAccessToken } from '@/models/_.js';
import { bindThis } from '@/decorators.js';
import { CacheService } from '@/core/CacheService.js';
import { MiLocalUser } from '@/models/User.js';
import { UserService } from '@/core/UserService.js';
import { ChannelFollowingService } from '@/core/ChannelFollowingService.js';
import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
import MainStreamConnection from './stream/Connection.js';
import { ChannelsService } from './stream/ChannelsService.js';
import MainStreamConnection, { ConnectionRequest } from './stream/Connection.js';
import type * as http from 'node:http';
import { ContextIdFactory, ModuleRef } from '@nestjs/core';
@Injectable()
export class StreamingApiServerService {
@@ -30,15 +27,9 @@ export class StreamingApiServerService {
@Inject(DI.redisForSub)
private redisForSub: Redis.Redis,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private cacheService: CacheService,
private moduleRef: ModuleRef,
private authenticateService: AuthenticateService,
private channelsService: ChannelsService,
private notificationService: NotificationService,
private usersService: UserService,
private channelFollowingService: ChannelFollowingService,
) {
}
@@ -92,13 +83,12 @@ export class StreamingApiServerService {
return;
}
const stream = new MainStreamConnection(
this.channelsService,
this.notificationService,
this.cacheService,
this.channelFollowingService,
user, app,
);
const contextId = ContextIdFactory.create();
this.moduleRef.registerRequestByContextId<ConnectionRequest>({
user,
token: app,
}, contextId);
const stream = await this.moduleRef.create(MainStreamConnection, contextId);
await stream.init();
@@ -121,7 +111,7 @@ export class StreamingApiServerService {
user: MiLocalUser | null;
app: MiAccessToken | null
}) => {
const { stream, user, app } = ctx;
const { stream, user } = ctx;
const ev = new EventEmitter();

View File

@@ -70,6 +70,7 @@ export * as 'admin/queue/inbox-delayed' from './endpoints/admin/queue/inbox-dela
export * as 'admin/queue/retry-job' from './endpoints/admin/queue/retry-job.js';
export * as 'admin/queue/remove-job' from './endpoints/admin/queue/remove-job.js';
export * as 'admin/queue/show-job' from './endpoints/admin/queue/show-job.js';
export * as 'admin/queue/show-job-logs' from './endpoints/admin/queue/show-job-logs.js';
export * as 'admin/queue/promote-jobs' from './endpoints/admin/queue/promote-jobs.js';
export * as 'admin/queue/jobs' from './endpoints/admin/queue/jobs.js';
export * as 'admin/queue/stats' from './endpoints/admin/queue/stats.js';
@@ -142,6 +143,9 @@ export * as 'channels/timeline' from './endpoints/channels/timeline.js';
export * as 'channels/unfavorite' from './endpoints/channels/unfavorite.js';
export * as 'channels/unfollow' from './endpoints/channels/unfollow.js';
export * as 'channels/update' from './endpoints/channels/update.js';
export * as 'channels/mute/create' from './endpoints/channels/mute/create.js';
export * as 'channels/mute/delete' from './endpoints/channels/mute/delete.js';
export * as 'channels/mute/list' from './endpoints/channels/mute/list.js';
export * as 'charts/active-users' from './endpoints/charts/active-users.js';
export * as 'charts/ap-request' from './endpoints/charts/ap-request.js';
export * as 'charts/drive' from './endpoints/charts/drive.js';
@@ -168,6 +172,7 @@ export * as 'clips/update' from './endpoints/clips/update.js';
export * as 'drive' from './endpoints/drive.js';
export * as 'drive/files' from './endpoints/drive/files.js';
export * as 'drive/files/attached-notes' from './endpoints/drive/files/attached-notes.js';
export * as 'drive/files/attached-chat-messages' from './endpoints/drive/files/attached-chat-messages.js';
export * as 'drive/files/check-existence' from './endpoints/drive/files/check-existence.js';
export * as 'drive/files/create' from './endpoints/drive/files/create.js';
export * as 'drive/files/delete' from './endpoints/drive/files/delete.js';
@@ -175,6 +180,7 @@ export * as 'drive/files/find' from './endpoints/drive/files/find.js';
export * as 'drive/files/find-by-hash' from './endpoints/drive/files/find-by-hash.js';
export * as 'drive/files/show' from './endpoints/drive/files/show.js';
export * as 'drive/files/update' from './endpoints/drive/files/update.js';
export * as 'drive/files/move-bulk' from './endpoints/drive/files/move-bulk.js';
export * as 'drive/files/upload-from-url' from './endpoints/drive/files/upload-from-url.js';
export * as 'drive/folders' from './endpoints/drive/folders.js';
export * as 'drive/folders/create' from './endpoints/drive/folders/create.js';
@@ -207,6 +213,7 @@ export * as 'flash/my-likes' from './endpoints/flash/my-likes.js';
export * as 'flash/show' from './endpoints/flash/show.js';
export * as 'flash/unlike' from './endpoints/flash/unlike.js';
export * as 'flash/update' from './endpoints/flash/update.js';
export * as 'flash/search' from './endpoints/flash/search.js';
export * as 'following/create' from './endpoints/following/create.js';
export * as 'following/delete' from './endpoints/following/delete.js';
export * as 'following/invalidate' from './endpoints/following/invalidate.js';
@@ -312,6 +319,11 @@ export * as 'notes/clips' from './endpoints/notes/clips.js';
export * as 'notes/conversation' from './endpoints/notes/conversation.js';
export * as 'notes/create' from './endpoints/notes/create.js';
export * as 'notes/delete' from './endpoints/notes/delete.js';
export * as 'notes/drafts/list' from './endpoints/notes/drafts/list.js';
export * as 'notes/drafts/create' from './endpoints/notes/drafts/create.js';
export * as 'notes/drafts/delete' from './endpoints/notes/drafts/delete.js';
export * as 'notes/drafts/update' from './endpoints/notes/drafts/update.js';
export * as 'notes/drafts/count' from './endpoints/notes/drafts/count.js';
export * as 'notes/favorites/create' from './endpoints/notes/favorites/create.js';
export * as 'notes/favorites/delete' from './endpoints/notes/favorites/delete.js';
export * as 'notes/featured' from './endpoints/notes/featured.js';
@@ -329,6 +341,7 @@ export * as 'notes/replies' from './endpoints/notes/replies.js';
export * as 'notes/search' from './endpoints/notes/search.js';
export * as 'notes/search-by-tag' from './endpoints/notes/search-by-tag.js';
export * as 'notes/show' from './endpoints/notes/show.js';
export * as 'notes/show-partial-bulk' from './endpoints/notes/show-partial-bulk.js';
export * as 'notes/state' from './endpoints/notes/state.js';
export * as 'notes/thread-muting/create' from './endpoints/notes/thread-muting/create.js';
export * as 'notes/thread-muting/delete' from './endpoints/notes/thread-muting/delete.js';
@@ -384,6 +397,7 @@ export * as 'users/featured-notes' from './endpoints/users/featured-notes.js';
export * as 'users/flashs' from './endpoints/users/flashs.js';
export * as 'users/followers' from './endpoints/users/followers.js';
export * as 'users/following' from './endpoints/users/following.js';
export * as 'users/get-following-birthday-users' from './endpoints/users/get-following-birthday-users.js';
export * as 'users/gallery/posts' from './endpoints/users/gallery/posts.js';
export * as 'users/get-frequently-replied-users' from './endpoints/users/get-frequently-replied-users.js';
export * as 'users/lists/create' from './endpoints/users/lists/create.js';
@@ -408,6 +422,7 @@ export * as 'users/search' from './endpoints/users/search.js';
export * as 'users/search-by-username-and-host' from './endpoints/users/search-by-username-and-host.js';
export * as 'users/show' from './endpoints/users/show.js';
export * as 'users/update-memo' from './endpoints/users/update-memo.js';
export * as 'verify-email' from './endpoints/verify-email.js';
export * as 'chat/messages/create-to-user' from './endpoints/chat/messages/create-to-user.js';
export * as 'chat/messages/create-to-room' from './endpoints/chat/messages/create-to-room.js';
export * as 'chat/messages/delete' from './endpoints/chat/messages/delete.js';
@@ -432,4 +447,5 @@ export * as 'chat/rooms/invitations/ignore' from './endpoints/chat/rooms/invitat
export * as 'chat/rooms/invitations/inbox' from './endpoints/chat/rooms/invitations/inbox.js';
export * as 'chat/rooms/invitations/outbox' from './endpoints/chat/rooms/invitations/outbox.js';
export * as 'chat/history' from './endpoints/chat/history.js';
export * as 'chat/read-all' from './endpoints/chat/read-all.js';
export * as 'v2/admin/emoji/list' from './endpoints/v2/admin/emoji/list.js';

View File

@@ -98,6 +98,8 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
state: { type: 'string', nullable: true, default: null },
reporterOrigin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' },
targetUserOrigin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' },
@@ -115,7 +117,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.abuseUserReportsRepository.createQueryBuilder('report'), ps.sinceId, ps.untilId);
const query = this.queryService.makePaginationQuery(this.abuseUserReportsRepository.createQueryBuilder('report'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate);
switch (ps.state) {
case 'resolved': query.andWhere('report.resolved = TRUE'); break;

View File

@@ -34,13 +34,22 @@ export const meta = {
res: {
type: 'object',
optional: false, nullable: false,
ref: 'MeDetailed',
properties: {
token: {
type: 'string',
optional: false, nullable: false,
allOf: [
{
type: 'object',
ref: 'MeDetailed',
},
},
{
type: 'object',
optional: false, nullable: false,
properties: {
token: {
type: 'string',
optional: false, nullable: false,
},
},
}
],
},
} as const;

View File

@@ -36,6 +36,7 @@ export const paramDef = {
startsAt: { type: 'integer' },
imageUrl: { type: 'string', minLength: 1 },
dayOfWeek: { type: 'integer' },
isSensitive: { type: 'boolean' },
},
required: ['url', 'memo', 'place', 'priority', 'ratio', 'expiresAt', 'startsAt', 'imageUrl', 'dayOfWeek'],
} as const;
@@ -55,6 +56,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
expiresAt: new Date(ps.expiresAt),
startsAt: new Date(ps.startsAt),
dayOfWeek: ps.dayOfWeek,
isSensitive: ps.isSensitive,
url: ps.url,
imageUrl: ps.imageUrl,
priority: ps.priority,
@@ -73,6 +75,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
expiresAt: ad.expiresAt.toISOString(),
startsAt: ad.startsAt.toISOString(),
dayOfWeek: ad.dayOfWeek,
isSensitive: ad.isSensitive,
url: ad.url,
imageUrl: ad.imageUrl,
priority: ad.priority,

View File

@@ -34,6 +34,8 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
publishing: { type: 'boolean', default: null, nullable: true },
},
required: [],
@@ -48,7 +50,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.adsRepository.createQueryBuilder('ad'), ps.sinceId, ps.untilId);
const query = this.queryService.makePaginationQuery(this.adsRepository.createQueryBuilder('ad'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate);
if (ps.publishing === true) {
query.andWhere('ad.expiresAt > :now', { now: new Date() }).andWhere('ad.startsAt <= :now', { now: new Date() });
} else if (ps.publishing === false) {
@@ -61,6 +63,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
expiresAt: ad.expiresAt.toISOString(),
startsAt: ad.startsAt.toISOString(),
dayOfWeek: ad.dayOfWeek,
isSensitive: ad.isSensitive,
url: ad.url,
imageUrl: ad.imageUrl,
memo: ad.memo,

View File

@@ -39,6 +39,7 @@ export const paramDef = {
expiresAt: { type: 'integer' },
startsAt: { type: 'integer' },
dayOfWeek: { type: 'integer' },
isSensitive: { type: 'boolean' },
},
required: ['id'],
} as const;
@@ -66,6 +67,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : undefined,
startsAt: ps.startsAt ? new Date(ps.startsAt) : undefined,
dayOfWeek: ps.dayOfWeek,
isSensitive: ps.isSensitive,
});
const updatedAd = await this.adsRepository.findOneByOrFail({ id: ad.id });

View File

@@ -72,7 +72,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private announcementService: AnnouncementService,
) {
super(meta, paramDef, async (ps, me) => {
const { raw, packed } = await this.announcementService.create({
const { packed } = await this.announcementService.create({
updatedAt: null,
title: ps.title,
text: ps.text,

View File

@@ -49,6 +49,36 @@ export const meta = {
type: 'string',
optional: false, nullable: false,
},
icon: {
type: 'string',
optional: false, nullable: false,
enum: ['info', 'warning', 'error', 'success'],
},
display: {
type: 'string',
optional: false, nullable: false,
enum: ['normal', 'banner', 'dialog'],
},
isActive: {
type: 'boolean',
optional: false, nullable: false,
},
forExistingUsers: {
type: 'boolean',
optional: false, nullable: false,
},
silence: {
type: 'boolean',
optional: false, nullable: false,
},
needConfirmationToRead: {
type: 'boolean',
optional: false, nullable: false,
},
userId: {
type: 'string',
optional: false, nullable: true,
},
imageUrl: {
type: 'string',
optional: false, nullable: true,
@@ -68,6 +98,8 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
userId: { type: 'string', format: 'misskey:id', nullable: true },
status: { type: 'string', enum: ['all', 'active', 'archived'], default: 'active' },
},
@@ -87,7 +119,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId);
const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate);
if (ps.status === 'archived') {
query.andWhere('announcement.isActive = false');

View File

@@ -71,6 +71,8 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
userId: { type: 'string', format: 'misskey:id', nullable: true },
},
required: [],

View File

@@ -34,6 +34,8 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
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' },
@@ -57,7 +59,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.driveFilesRepository.createQueryBuilder('file'), ps.sinceId, ps.untilId);
const query = this.queryService.makePaginationQuery(this.driveFilesRepository.createQueryBuilder('file'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate);
if (ps.userId) {
query.andWhere('file.userId = :userId', { userId: ps.userId });

View File

@@ -157,19 +157,42 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
maybeSensitive: {
type: 'boolean',
optional: false, nullable: false,
},
maybePorn: {
type: 'boolean',
optional: false, nullable: false,
},
requestIp: {
type: 'string',
optional: false, nullable: true,
},
requestHeaders: {
type: 'object',
optional: false, nullable: true,
},
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
fileId: { type: 'string', format: 'misskey:id' },
url: { type: 'string' },
},
anyOf: [
{ required: ['fileId'] },
{ required: ['url'] },
{
type: 'object',
properties: {
fileId: { type: 'string', format: 'misskey:id' },
},
required: ['fileId'],
},
{
type: 'object',
properties: {
url: { type: 'string' },
},
required: ['url'],
},
],
} as const;
@@ -186,15 +209,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const file = ps.fileId ? await this.driveFilesRepository.findOneBy({ id: ps.fileId }) : await this.driveFilesRepository.findOne({
where: [{
url: ps.url,
}, {
thumbnailUrl: ps.url,
}, {
webpublicUrl: ps.url,
}],
});
const file = await this.driveFilesRepository.findOneBy(
'fileId' in ps
? { id: ps.fileId }
: [{ url: ps.url }, { thumbnailUrl: ps.url }, { webpublicUrl: ps.url }],
);
if (file == null) {
throw new ApiError(meta.errors.noSuchFile);

View File

@@ -76,7 +76,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
try {
// Create file
driveFile = await this.driveService.uploadFromUrl({ url: emoji.originalUrl, user: null, force: true });
} catch (e) {
} catch (_) {
// TODO: need to return Drive Error
throw new ApiError();
}

View File

@@ -24,39 +24,7 @@ export const meta = {
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
properties: {
id: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
aliases: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
},
},
name: {
type: 'string',
optional: false, nullable: false,
},
category: {
type: 'string',
optional: false, nullable: true,
},
host: {
type: 'string',
optional: false, nullable: true,
description: 'The local host is represented with `null`.',
},
url: {
type: 'string',
optional: false, nullable: false,
},
},
ref: 'EmojiDetailed',
},
},
} as const;
@@ -74,6 +42,8 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
},
required: [],
} as const;
@@ -89,7 +59,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private emojiEntityService: EmojiEntityService,
) {
super(meta, paramDef, async (ps, me) => {
const q = this.queryService.makePaginationQuery(this.emojisRepository.createQueryBuilder('emoji'), ps.sinceId, ps.untilId);
const q = this.queryService.makePaginationQuery(this.emojisRepository.createQueryBuilder('emoji'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate);
if (ps.host == null) {
q.andWhere('emoji.host IS NOT NULL');

View File

@@ -24,39 +24,7 @@ export const meta = {
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
properties: {
id: {
type: 'string',
optional: false, nullable: false,
format: 'id',
},
aliases: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
},
},
name: {
type: 'string',
optional: false, nullable: false,
},
category: {
type: 'string',
optional: false, nullable: true,
},
host: {
type: 'string',
optional: false, nullable: true,
description: 'The local host is represented with `null`. The field exists for compatibility with other API endpoints that return files.',
},
url: {
type: 'string',
optional: false, nullable: false,
},
},
ref: 'EmojiDetailed',
},
},
} as const;
@@ -68,6 +36,8 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
},
required: [],
} as const;
@@ -82,7 +52,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const q = this.queryService.makePaginationQuery(this.emojisRepository.createQueryBuilder('emoji'), ps.sinceId, ps.untilId)
const q = this.queryService.makePaginationQuery(this.emojisRepository.createQueryBuilder('emoji'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('emoji.host IS NULL');
let emojis: MiEmoji[];

View File

@@ -37,29 +37,45 @@ export const meta = {
} as const;
export const paramDef = {
type: 'object',
properties: {
id: { type: 'string', format: 'misskey:id' },
name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' },
fileId: { type: 'string', format: 'misskey:id' },
category: {
type: 'string',
nullable: true,
description: 'Use `null` to reset the category.',
allOf: [
{
anyOf: [
{
type: 'object',
properties: {
id: { type: 'string', format: 'misskey:id' },
},
required: ['id'],
},
{
type: 'object',
properties: {
name: { type: 'string', pattern: '^[a-zA-Z0-9_]+$' },
},
required: ['name'],
},
],
},
{
type: 'object',
properties: {
fileId: { type: 'string', format: 'misskey:id' },
category: {
type: 'string',
nullable: true,
description: 'Use `null` to reset the category.',
},
aliases: { type: 'array', items: {
type: 'string',
} },
license: { type: 'string', nullable: true },
isSensitive: { type: 'boolean' },
localOnly: { type: 'boolean' },
roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: {
type: 'string',
} },
},
},
aliases: { type: 'array', items: {
type: 'string',
} },
license: { type: 'string', nullable: true },
isSensitive: { type: 'boolean' },
localOnly: { type: 'boolean' },
roleIdsThatCanBeUsedThisEmojiAsReaction: { type: 'array', items: {
type: 'string',
} },
},
anyOf: [
{ required: ['id'] },
{ required: ['name'] },
],
} as const;
@@ -78,10 +94,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
}
// JSON schemeのanyOfの型変換がうまくいっていないらしい
const required = { id: ps.id, name: ps.name } as
| { id: MiEmoji['id']; name?: string }
| { id?: MiEmoji['id']; name: string };
const required = 'id' in ps
? { id: ps.id, name: 'name' in ps ? ps.name as string : undefined }
: { name: ps.name };
const error = await this.customEmojiService.update({
...required,
@@ -102,7 +117,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
case 'SAME_NAME_EMOJI_EXISTS': throw new ApiError(meta.errors.sameNameEmojiExists);
}
// 網羅性チェック
const mustBeNever: never = error;
const _mustBeNever: never = error;
});
}
}

View File

@@ -68,6 +68,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
for (let i = 0; i < ps.count; i++) {
ticketsPromises.push(this.registrationTicketsRepository.insertOne({
id: this.idService.gen(),
createdBy: me,
createdById: me.id,
expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null,
code: generateInviteCode(),
}));

View File

@@ -223,10 +223,12 @@ export const meta = {
sensitiveMediaDetection: {
type: 'string',
optional: false, nullable: false,
enum: ['none', 'all', 'local', 'remote'],
},
sensitiveMediaDetectionSensitivity: {
type: 'string',
optional: false, nullable: false,
enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'],
},
setSensitiveFlagAutomatically: {
type: 'boolean',
@@ -425,6 +427,9 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
clientOptions: {
ref: 'MetaClientOptions',
},
description: {
type: 'string',
optional: false, nullable: true,
@@ -469,6 +474,10 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
feedbackUrl: {
type: 'string',
optional: false, nullable: true,
},
summalyProxy: {
type: 'string',
optional: false, nullable: true,
@@ -495,6 +504,10 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
urlPreviewAllowRedirect: {
type: 'boolean',
optional: false, nullable: false,
},
urlPreviewTimeout: {
type: 'number',
optional: false, nullable: false,
@@ -528,6 +541,61 @@ export const meta = {
optional: false, nullable: false,
},
},
deliverSuspendedSoftware: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
properties: {
software: {
type: 'string',
optional: false, nullable: false,
},
versionRange: {
type: 'string',
optional: false, nullable: false,
},
},
},
},
singleUserMode: {
type: 'boolean',
optional: false, nullable: false,
},
ugcVisibilityForVisitor: {
type: 'string',
enum: ['all', 'local', 'none'],
optional: false, nullable: false,
},
proxyRemoteFiles: {
type: 'boolean',
optional: false, nullable: false,
},
signToActivityPubGet: {
type: 'boolean',
optional: false, nullable: false,
},
allowExternalApRedirect: {
type: 'boolean',
optional: false, nullable: false,
},
enableRemoteNotesCleaning: {
type: 'boolean',
optional: false, nullable: false,
},
remoteNotesCleaningExpiryDaysForEachNotes: {
type: 'number',
optional: false, nullable: false,
},
remoteNotesCleaningMaxProcessingDurationInMinutes: {
type: 'number',
optional: false, nullable: false,
},
showRoleBadgesOfRemoteUsers: {
type: 'boolean',
optional: false, nullable: false,
},
},
},
} as const;
@@ -595,6 +663,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
logoImageUrl: instance.logoImageUrl,
defaultLightTheme: instance.defaultLightTheme,
defaultDarkTheme: instance.defaultDarkTheme,
clientOptions: instance.clientOptions,
enableEmail: instance.enableEmail,
enableServiceWorker: instance.enableServiceWorker,
translatorAvailable: instance.deeplAuthKey != null,
@@ -665,6 +734,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
notesPerOneAd: instance.notesPerOneAd,
summalyProxy: instance.urlPreviewSummaryProxyUrl,
urlPreviewEnabled: instance.urlPreviewEnabled,
urlPreviewAllowRedirect: instance.urlPreviewAllowRedirect,
urlPreviewTimeout: instance.urlPreviewTimeout,
urlPreviewMaximumContentLength: instance.urlPreviewMaximumContentLength,
urlPreviewRequireContentLength: instance.urlPreviewRequireContentLength,
@@ -672,6 +742,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
urlPreviewSummaryProxyUrl: instance.urlPreviewSummaryProxyUrl,
federation: instance.federation,
federationHosts: instance.federationHosts,
deliverSuspendedSoftware: instance.deliverSuspendedSoftware,
singleUserMode: instance.singleUserMode,
ugcVisibilityForVisitor: instance.ugcVisibilityForVisitor,
proxyRemoteFiles: instance.proxyRemoteFiles,
signToActivityPubGet: instance.signToActivityPubGet,
allowExternalApRedirect: instance.allowExternalApRedirect,
enableRemoteNotesCleaning: instance.enableRemoteNotesCleaning,
remoteNotesCleaningExpiryDaysForEachNotes: instance.remoteNotesCleaningExpiryDaysForEachNotes,
remoteNotesCleaningMaxProcessingDurationInMinutes: instance.remoteNotesCleaningMaxProcessingDurationInMinutes,
showRoleBadgesOfRemoteUsers: instance.showRoleBadgesOfRemoteUsers,
};
});
}

View File

@@ -52,18 +52,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
super(meta, paramDef, async (ps, me) => {
const jobs = await this.deliverQueue.getJobs(['delayed']);
const res = [] as [string, number][];
const counts = new Map<string, number>();
for (const job of jobs) {
const host = new URL(job.data.to).host;
if (res.find(x => x[0] === host)) {
res.find(x => x[0] === host)![1]++;
} else {
res.push([host, 1]);
}
counts.set(host, (counts.get(host) ?? 0) + 1);
}
res.sort((a, b) => b[1] - a[1]);
const res = [...counts.entries()].sort((a, b) => b[1] - a[1]);
return res;
});

View File

@@ -52,18 +52,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
super(meta, paramDef, async (ps, me) => {
const jobs = await this.inboxQueue.getJobs(['delayed']);
const res = [] as [string, number][];
const counts = new Map<string, number>();
for (const job of jobs) {
const host = new URL(job.data.signature.keyId).host;
if (res.find(x => x[0] === host)) {
res.find(x => x[0] === host)![1]++;
} else {
res.push([host, 1]);
}
counts.set(host, (counts.get(host) ?? 0) + 1);
}
res.sort((a, b) => b[1] - a[1]);
const res = [...counts.entries()].sort((a, b) => b[1] - a[1]);
return res;
});

View File

@@ -5,7 +5,6 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js';
export const meta = {
@@ -14,13 +13,22 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'read:admin:queue',
res: {
type: 'array',
optional: false, nullable: false,
items: {
optional: false, nullable: false,
ref: 'QueueJob',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
queue: { type: 'string', enum: QUEUE_TYPES },
state: { type: 'array', items: { type: 'string', enum: ['active', 'wait', 'delayed', 'completed', 'failed'] } },
state: { type: 'array', items: { type: 'string', enum: ['active', 'wait', 'delayed', 'completed', 'failed', 'paused'] } },
search: { type: 'string' },
},
required: ['queue', 'state'],

View File

@@ -5,7 +5,6 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js';
export const meta = {
@@ -14,6 +13,118 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'read:admin:queue',
res: {
type: 'object',
optional: false, nullable: false,
properties: {
name: {
type: 'string',
optional: false, nullable: false,
enum: QUEUE_TYPES,
},
qualifiedName: {
type: 'string',
optional: false, nullable: false,
},
counts: {
type: 'object',
optional: false, nullable: false,
additionalProperties: {
type: 'number',
},
},
isPaused: {
type: 'boolean',
optional: false, nullable: false,
},
metrics: {
type: 'object',
optional: false, nullable: false,
properties: {
completed: {
optional: false, nullable: false,
ref: 'QueueMetrics',
},
failed: {
optional: false, nullable: false,
ref: 'QueueMetrics',
},
},
},
db: {
type: 'object',
optional: false, nullable: false,
properties: {
version: {
type: 'string',
optional: false, nullable: false,
},
mode: {
type: 'string',
optional: false, nullable: false,
enum: ['cluster', 'standalone', 'sentinel'],
},
runId: {
type: 'string',
optional: false, nullable: false,
},
processId: {
type: 'string',
optional: false, nullable: false,
},
port: {
type: 'number',
optional: false, nullable: false,
},
os: {
type: 'string',
optional: false, nullable: false,
},
uptime: {
type: 'number',
optional: false, nullable: false,
},
memory: {
type: 'object',
optional: false, nullable: false,
properties: {
total: {
type: 'number',
optional: false, nullable: false,
},
used: {
type: 'number',
optional: false, nullable: false,
},
fragmentationRatio: {
type: 'number',
optional: false, nullable: false,
},
peak: {
type: 'number',
optional: false, nullable: false,
},
},
},
clients: {
type: 'object',
optional: false, nullable: false,
properties: {
blocked: {
type: 'number',
optional: false, nullable: false,
},
connected: {
type: 'number',
optional: false, nullable: false,
},
},
},
},
}
},
},
} as const;
export const paramDef = {

View File

@@ -5,7 +5,6 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js';
export const meta = {
@@ -14,6 +13,47 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'read:admin:queue',
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
properties: {
name: {
type: 'string',
optional: false, nullable: false,
enum: QUEUE_TYPES,
},
counts: {
type: 'object',
optional: false, nullable: false,
additionalProperties: {
type: 'number',
},
},
isPaused: {
type: 'boolean',
optional: false, nullable: false,
},
metrics: {
type: 'object',
optional: false, nullable: false,
properties: {
completed: {
optional: false, nullable: false,
ref: 'QueueMetrics',
},
failed: {
optional: false, nullable: false,
ref: 'QueueMetrics',
},
},
},
},
},
},
} as const;
export const paramDef = {

View File

@@ -0,0 +1,45 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
kind: 'read:admin:queue',
res: {
type: 'array',
optional: false, nullable: false,
items: {
optional: false, nullable: false,
type: 'string',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
queue: { type: 'string', enum: QUEUE_TYPES },
jobId: { type: 'string' },
},
required: ['queue', 'jobId'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private queueService: QueueService,
) {
super(meta, paramDef, async (ps, me) => {
return this.queueService.queueGetJobLogs(ps.queue, ps.jobId);
});
}
}

View File

@@ -5,7 +5,6 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js';
export const meta = {
@@ -14,6 +13,11 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'read:admin:queue',
res: {
optional: false, nullable: false,
ref: 'QueueJob',
},
} as const;
export const paramDef = {
@@ -28,7 +32,6 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private moderationLogService: ModerationLogService,
private queueService: QueueService,
) {
super(meta, paramDef, async (ps, me) => {

View File

@@ -5,7 +5,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue } from '@/core/QueueModule.js';
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, PostScheduledNoteQueue, InboxQueue, ObjectStorageQueue, SystemQueue, UserWebhookDeliverQueue, SystemWebhookDeliverQueue } from '@/core/QueueModule.js';
export const meta = {
tags: ['admin'],
@@ -49,6 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject('queue:system') public systemQueue: SystemQueue,
@Inject('queue:endedPollNotification') public endedPollNotificationQueue: EndedPollNotificationQueue,
@Inject('queue:postScheduledNote') public postScheduledNoteQueue: PostScheduledNoteQueue,
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
@Inject('queue:inbox') public inboxQueue: InboxQueue,
@Inject('queue:db') public dbQueue: DbQueue,

View File

@@ -49,6 +49,8 @@ export const paramDef = {
roleId: { type: 'string', format: 'misskey:id' },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
},
required: ['roleId'],
@@ -76,7 +78,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchRole);
}
const query = this.queryService.makePaginationQuery(this.roleAssignmentsRepository.createQueryBuilder('assign'), ps.sinceId, ps.untilId)
const query = this.queryService.makePaginationQuery(this.roleAssignmentsRepository.createQueryBuilder('assign'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('assign.roleId = :roleId', { roleId: role.id })
.andWhere(new Brackets(qb => {
qb

View File

@@ -4,7 +4,6 @@
*/
import * as os from 'node:os';
import si from 'systeminformation';
import { Inject, Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
import * as Redis from 'ioredis';
@@ -112,6 +111,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
) {
super(meta, paramDef, async () => {
const si = await import('systeminformation');
const memStats = await si.mem();
const fsStats = await si.fsSize();
const netInterface = await si.networkInterfaceDefault();

View File

@@ -9,6 +9,7 @@ import type { ModerationLogsRepository } from '@/models/_.js';
import { QueryService } from '@/core/QueryService.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogEntityService } from '@/core/entities/ModerationLogEntityService.js';
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
export const meta = {
tags: ['admin'],
@@ -63,8 +64,11 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
type: { type: 'string', nullable: true },
userId: { type: 'string', format: 'misskey:id', nullable: true },
search: { type: 'string', nullable: true },
},
required: [],
} as const;
@@ -79,19 +83,24 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.moderationLogsRepository.createQueryBuilder('report'), ps.sinceId, ps.untilId);
const query = this.queryService.makePaginationQuery(this.moderationLogsRepository.createQueryBuilder('log'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate);
if (ps.type != null) {
query.andWhere('report.type = :type', { type: ps.type });
query.andWhere('log.type = :type', { type: ps.type });
}
if (ps.userId != null) {
query.andWhere('report.userId = :userId', { userId: ps.userId });
query.andWhere('log.userId = :userId', { userId: ps.userId });
}
const reports = await query.limit(ps.limit).getMany();
if (ps.search != null) {
const escapedSearch = sqlLikeEscape(ps.search);
query.andWhere('log.info::text ILIKE :search', { search: `%${escapedSearch}%` });
}
return await this.moderationLogEntityService.packMany(reports);
const logs = await query.limit(ps.limit).getMany();
return await this.moderationLogEntityService.packMany(logs);
});
}
}

View File

@@ -103,6 +103,8 @@ export const meta = {
quote: { optional: true, ...notificationRecieveConfig },
reaction: { optional: true, ...notificationRecieveConfig },
pollEnded: { optional: true, ...notificationRecieveConfig },
scheduledNotePosted: { optional: true, ...notificationRecieveConfig },
scheduledNotePostFailed: { optional: true, ...notificationRecieveConfig },
receiveFollowRequest: { optional: true, ...notificationRecieveConfig },
followRequestAccepted: { optional: true, ...notificationRecieveConfig },
roleAssigned: { optional: true, ...notificationRecieveConfig },

View File

@@ -48,8 +48,8 @@ export const paramDef = {
},
secret: {
type: 'string',
minLength: 1,
maxLength: 1024,
default: '',
},
},
required: [
@@ -57,7 +57,6 @@ export const paramDef = {
'name',
'on',
'url',
'secret',
],
} as const;

View File

@@ -52,8 +52,8 @@ export const paramDef = {
},
secret: {
type: 'string',
minLength: 1,
maxLength: 1024,
default: '',
},
},
required: [
@@ -62,7 +62,6 @@ export const paramDef = {
'name',
'on',
'url',
'secret',
],
} as const;

View File

@@ -3,7 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Injectable, Inject } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import type { MiMeta } from '@/models/Meta.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
@@ -67,6 +68,14 @@ export const paramDef = {
description: { type: 'string', nullable: true },
defaultLightTheme: { type: 'string', nullable: true },
defaultDarkTheme: { type: 'string', nullable: true },
clientOptions: {
type: 'object', nullable: false,
properties: {
entrancePageStyle: { type: 'string', nullable: false, enum: ['classic', 'simple'] },
showTimelineForVisitor: { type: 'boolean', nullable: false },
showActivitiesForVisitor: { type: 'boolean', nullable: false },
},
},
cacheRemoteFiles: { type: 'boolean' },
cacheRemoteSensitiveFiles: { type: 'boolean' },
emailRequiredForSignup: { type: 'boolean' },
@@ -170,6 +179,7 @@ export const paramDef = {
description: '[Deprecated] Use "urlPreviewSummaryProxyUrl" instead.',
},
urlPreviewEnabled: { type: 'boolean' },
urlPreviewAllowRedirect: { type: 'boolean' },
urlPreviewTimeout: { type: 'integer' },
urlPreviewMaximumContentLength: { type: 'integer' },
urlPreviewRequireContentLength: { type: 'boolean' },
@@ -185,6 +195,29 @@ export const paramDef = {
type: 'string',
},
},
deliverSuspendedSoftware: {
type: 'array',
items: {
type: 'object',
properties: {
software: { type: 'string' },
versionRange: { type: 'string' },
},
required: ['software', 'versionRange'],
},
},
singleUserMode: { type: 'boolean' },
ugcVisibilityForVisitor: {
type: 'string',
enum: ['all', 'local', 'none'],
},
proxyRemoteFiles: { type: 'boolean' },
signToActivityPubGet: { type: 'boolean' },
allowExternalApRedirect: { type: 'boolean' },
enableRemoteNotesCleaning: { type: 'boolean' },
remoteNotesCleaningExpiryDaysForEachNotes: { type: 'number' },
remoteNotesCleaningMaxProcessingDurationInMinutes: { type: 'number' },
showRoleBadgesOfRemoteUsers: { type: 'boolean' },
},
required: [],
} as const;
@@ -192,6 +225,9 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.meta)
private serverSettings: MiMeta,
private metaService: MetaService,
private moderationLogService: ModerationLogService,
) {
@@ -303,6 +339,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.defaultDarkTheme = ps.defaultDarkTheme;
}
if (ps.clientOptions !== undefined) {
set.clientOptions = {
...serverSettings.clientOptions,
...ps.clientOptions,
};
}
if (ps.cacheRemoteFiles !== undefined) {
set.cacheRemoteFiles = ps.cacheRemoteFiles;
}
@@ -645,6 +688,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.urlPreviewEnabled = ps.urlPreviewEnabled;
}
if (ps.urlPreviewAllowRedirect !== undefined) {
set.urlPreviewAllowRedirect = ps.urlPreviewAllowRedirect;
}
if (ps.urlPreviewTimeout !== undefined) {
set.urlPreviewTimeout = ps.urlPreviewTimeout;
}
@@ -671,10 +718,50 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.federation = ps.federation;
}
if (ps.deliverSuspendedSoftware !== undefined) {
set.deliverSuspendedSoftware = ps.deliverSuspendedSoftware;
}
if (Array.isArray(ps.federationHosts)) {
set.federationHosts = ps.federationHosts.filter(Boolean).map(x => x.toLowerCase());
}
if (ps.singleUserMode !== undefined) {
set.singleUserMode = ps.singleUserMode;
}
if (ps.ugcVisibilityForVisitor !== undefined) {
set.ugcVisibilityForVisitor = ps.ugcVisibilityForVisitor;
}
if (ps.proxyRemoteFiles !== undefined) {
set.proxyRemoteFiles = ps.proxyRemoteFiles;
}
if (ps.signToActivityPubGet !== undefined) {
set.signToActivityPubGet = ps.signToActivityPubGet;
}
if (ps.allowExternalApRedirect !== undefined) {
set.allowExternalApRedirect = ps.allowExternalApRedirect;
}
if (ps.enableRemoteNotesCleaning !== undefined) {
set.enableRemoteNotesCleaning = ps.enableRemoteNotesCleaning;
}
if (ps.remoteNotesCleaningExpiryDaysForEachNotes !== undefined) {
set.remoteNotesCleaningExpiryDaysForEachNotes = ps.remoteNotesCleaningExpiryDaysForEachNotes;
}
if (ps.remoteNotesCleaningMaxProcessingDurationInMinutes !== undefined) {
set.remoteNotesCleaningMaxProcessingDurationInMinutes = ps.remoteNotesCleaningMaxProcessingDurationInMinutes;
}
if (ps.showRoleBadgesOfRemoteUsers !== undefined) {
set.showRoleBadgesOfRemoteUsers = ps.showRoleBadgesOfRemoteUsers;
}
const before = await this.metaService.fetch(true);
await this.metaService.update(set);

View File

@@ -33,6 +33,8 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
isActive: { type: 'boolean', default: true },
},
required: [],
@@ -48,7 +50,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private announcementEntityService: AnnouncementEntityService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId)
const query = this.queryService.makePaginationQuery(this.announcementsRepository.createQueryBuilder('announcement'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('announcement.isActive = :isActive', { isActive: ps.isActive })
.andWhere(new Brackets(qb => {
if (me) qb.orWhere('announcement.userId = :meId', { meId: me.id });

View File

@@ -5,6 +5,7 @@
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
import { Brackets } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { NotesRepository, AntennasRepository } from '@/models/_.js';
import { QueryService } from '@/core/QueryService.js';
@@ -14,6 +15,7 @@ import { IdService } from '@/core/IdService.js';
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { trackPromise } from '@/misc/promise-tracker.js';
import { ChannelMutingService } from '@/core/ChannelMutingService.js';
import { ApiError } from '../../error.js';
export const meta = {
@@ -69,6 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private queryService: QueryService,
private fanoutTimelineService: FanoutTimelineService,
private globalEventService: GlobalEventService,
private channelMutingService: ChannelMutingService,
) {
super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
@@ -108,12 +111,26 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser');
// -- ミュートされたチャンネル対策
const mutingChannelIds = await this.channelMutingService
.list({ requestUserId: me.id }, { idOnly: true })
.then(x => x.map(x => x.id));
if (mutingChannelIds.length > 0) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.channelId IS NULL');
qb.orWhere('note.channelId NOT IN (:...mutingChannelIds)', { mutingChannelIds });
}));
query.andWhere(new Brackets(qb => {
qb.orWhere('note.renoteChannelId IS NULL');
qb.orWhere('note.renoteChannelId NOT IN (:...mutingChannelIds)', { mutingChannelIds });
}));
}
// NOTE: センシティブ除外の設定はこのエンドポイントでは無視する。
// https://github.com/misskey-dev/misskey/pull/15346#discussion_r1929950255
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
this.queryService.generateBaseNoteFilteringQuery(query, me);
const notes = await query.getMany();
if (sinceId != null && untilId == null) {

View File

@@ -43,7 +43,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private apResolverService: ApResolverService,
) {
super(meta, paramDef, async (ps, me) => {
const resolver = this.apResolverService.createResolver();
const resolver = await this.apResolverService.createResolver();
const object = await resolver.resolve(ps.uri);
return object;
});

View File

@@ -18,9 +18,9 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { bindThis } from '@/decorators.js';
import { ApiError } from '../../error.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['federation'],
@@ -148,7 +148,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (this.utilityService.isSelfHost(host)) return null;
// リモートから一旦オブジェクトフェッチ
const resolver = this.apResolverService.createResolver();
const resolver = await this.apResolverService.createResolver();
// allow ap/show exclusively to lookup URLs that are cross-origin or non-canonical (like https://alice.example.com/@bob@bob.example.com -> https://bob.example.com/@bob)
const object = await resolver.resolve(uri, FetchAllowSoftFailMask.CrossOrigin | FetchAllowSoftFailMask.NonCanonicalId).catch((err) => {
if (err instanceof IdentifiableError) {
@@ -215,7 +215,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
type: 'Note',
object,
};
} catch (e) {
} catch (_) {
return null;
}
}

View File

@@ -34,6 +34,8 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
},
required: [],
} as const;
@@ -48,7 +50,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.blockingsRepository.createQueryBuilder('blocking'), ps.sinceId, ps.untilId)
const query = this.queryService.makePaginationQuery(this.blockingsRepository.createQueryBuilder('blocking'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('blocking.blockerId = :meId', { meId: me.id });
const blockings = await query

View File

@@ -46,7 +46,7 @@ export const paramDef = {
type: 'object',
properties: {
name: { type: 'string', minLength: 1, maxLength: 128 },
description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 },
description: { type: 'string', nullable: true, maxLength: 2048 },
bannerId: { type: 'string', format: 'misskey:id', nullable: true },
color: { type: 'string', minLength: 1, maxLength: 16 },
isSensitive: { type: 'boolean', nullable: true },

View File

@@ -33,6 +33,8 @@ export const paramDef = {
properties: {
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 5 },
},
required: [],
@@ -48,7 +50,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.channelFollowingsRepository.createQueryBuilder(), ps.sinceId, ps.untilId)
const query = this.queryService
.makePaginationQuery(
this.channelFollowingsRepository.createQueryBuilder(),
ps.sinceId,
ps.untilId,
ps.sinceDate,
ps.untilDate,
'followeeId',
)
.andWhere({ followerId: me.id });
const followings = await query

View File

@@ -0,0 +1,90 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { ChannelsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '@/server/api/error.js';
import { ChannelMutingService } from '@/core/ChannelMutingService.js';
export const meta = {
tags: ['channels', 'mute'],
requireCredential: true,
prohibitMoved: true,
kind: 'write:channels',
errors: {
noSuchChannel: {
message: 'No such Channel.',
code: 'NO_SUCH_CHANNEL',
id: '7174361e-d58f-31d6-2e7c-6fb830786a3f',
},
alreadyMuting: {
message: 'You are already muting that user.',
code: 'ALREADY_MUTING_CHANNEL',
id: '5a251978-769a-da44-3e89-3931e43bb592',
},
expiresAtIsPast: {
message: 'Cannot set past date to "expiresAt".',
code: 'EXPIRES_AT_IS_PAST',
id: '42b32236-df2c-a45f-fdbf-def67268f749',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
channelId: { type: 'string', format: 'misskey:id' },
expiresAt: {
type: 'integer',
nullable: true,
description: 'A Unix Epoch timestamp that must lie in the future. `null` means an indefinite mute.',
},
},
required: ['channelId'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.channelsRepository)
private channelsRepository: ChannelsRepository,
private channelMutingService: ChannelMutingService,
) {
super(meta, paramDef, async (ps, me) => {
// Check if exists the channel
const targetChannel = await this.channelsRepository.findOneBy({ id: ps.channelId });
if (!targetChannel) {
throw new ApiError(meta.errors.noSuchChannel);
}
// Check if already muting
const exist = await this.channelMutingService.isMuted({
requestUserId: me.id,
targetChannelId: targetChannel.id,
});
if (exist) {
throw new ApiError(meta.errors.alreadyMuting);
}
// Check if expiresAt is past
if (ps.expiresAt && ps.expiresAt <= Date.now()) {
throw new ApiError(meta.errors.expiresAtIsPast);
}
await this.channelMutingService.mute({
requestUserId: me.id,
targetChannelId: targetChannel.id,
expiresAt: ps.expiresAt ? new Date(ps.expiresAt) : null,
});
});
}
}

View File

@@ -0,0 +1,73 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { ChannelsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { ChannelMutingService } from '@/core/ChannelMutingService.js';
import { ApiError } from '@/server/api/error.js';
export const meta = {
tags: ['channels', 'mute'],
requireCredential: true,
prohibitMoved: true,
kind: 'write:channels',
errors: {
noSuchChannel: {
message: 'No such Channel.',
code: 'NO_SUCH_CHANNEL',
id: 'e7998769-6e94-d9c2-6b8f-94a527314aba',
},
notMuting: {
message: 'You are not muting that channel.',
code: 'NOT_MUTING_CHANNEL',
id: '14d55962-6ea8-d990-1333-d6bef78dc2ab',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
channelId: { type: 'string', format: 'misskey:id' },
},
required: ['channelId'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.channelsRepository)
private channelsRepository: ChannelsRepository,
private channelMutingService: ChannelMutingService,
) {
super(meta, paramDef, async (ps, me) => {
// Check if exists the channel
const targetChannel = await this.channelsRepository.findOneBy({ id: ps.channelId });
if (!targetChannel) {
throw new ApiError(meta.errors.noSuchChannel);
}
// Check muting
const exist = await this.channelMutingService.isMuted({
requestUserId: me.id,
targetChannelId: targetChannel.id,
});
if (!exist) {
throw new ApiError(meta.errors.notMuting);
}
await this.channelMutingService.unmute({
requestUserId: me.id,
targetChannelId: targetChannel.id,
});
});
}
}

View File

@@ -0,0 +1,49 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { ChannelMutingService } from '@/core/ChannelMutingService.js';
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
export const meta = {
tags: ['channels', 'mute'],
requireCredential: true,
prohibitMoved: true,
kind: 'read:channels',
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
ref: 'Channel',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {},
required: [],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private channelMutingService: ChannelMutingService,
private channelEntityService: ChannelEntityService,
) {
super(meta, paramDef, async (ps, me) => {
const mutings = await this.channelMutingService.list({
requestUserId: me.id,
});
return await this.channelEntityService.packMany(mutings, me);
});
}
}

View File

@@ -33,6 +33,8 @@ export const paramDef = {
properties: {
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 5 },
},
required: [],
@@ -48,7 +50,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.channelsRepository.createQueryBuilder('channel'), ps.sinceId, ps.untilId)
const query = this.queryService.makePaginationQuery(this.channelsRepository.createQueryBuilder('channel'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('channel.isArchived = FALSE')
.andWhere({ userId: me.id });

View File

@@ -35,6 +35,8 @@ export const paramDef = {
type: { type: 'string', enum: ['nameAndDescription', 'nameOnly'], default: 'nameAndDescription' },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 5 },
},
required: ['query'],
@@ -50,7 +52,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.channelsRepository.createQueryBuilder('channel'), ps.sinceId, ps.untilId)
const query = this.queryService.makePaginationQuery(this.channelsRepository.createQueryBuilder('channel'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('channel.isArchived = FALSE');
if (ps.query !== '') {

View File

@@ -13,6 +13,7 @@ import { DI } from '@/di-symbols.js';
import { IdService } from '@/core/IdService.js';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
import { MiLocalUser } from '@/models/User.js';
import { ChannelMutingService } from '@/core/ChannelMutingService.js';
import { ApiError } from '../../error.js';
export const meta = {
@@ -70,6 +71,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private queryService: QueryService,
private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
private activeUsersChart: ActiveUsersChart,
private channelMutingService: ChannelMutingService,
) {
super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
@@ -98,6 +100,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
useDbFallback: true,
redisTimelines: [`channelTimeline:${channel.id}`],
excludePureRenotes: false,
ignoreAuthorChannelFromMute: true,
dbFallback: async (untilId, sinceId, limit) => {
return await this.getFromDb({ untilId, sinceId, limit, channelId: channel.id }, me);
},
@@ -121,9 +124,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser')
.leftJoinAndSelect('note.channel', 'channel');
this.queryService.generateBaseNoteFilteringQuery(query, me);
if (me) {
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
const mutingChannelIds = await this.channelMutingService
.list({ requestUserId: me.id }, { idOnly: true })
.then(x => x.map(x => x.id).filter(x => x !== ps.channelId));
if (mutingChannelIds.length > 0) {
query.andWhere('note.channelId NOT IN (:...mutingChannelIds)', { mutingChannelIds });
query.andWhere('note.renoteChannelId NOT IN (:...mutingChannelIds)', { mutingChannelIds });
}
}
//#endregion

View File

@@ -50,7 +50,7 @@ export const paramDef = {
properties: {
channelId: { type: 'string', format: 'misskey:id' },
name: { type: 'string', minLength: 1, maxLength: 128 },
description: { type: 'string', nullable: true, minLength: 1, maxLength: 2048 },
description: { type: 'string', nullable: true, maxLength: 2048 },
bannerId: { type: 'string', format: 'misskey:id', nullable: true },
isArchived: { type: 'boolean', nullable: true },
pinnedNoteIds: {

View File

@@ -16,9 +16,6 @@ export const meta = {
kind: 'write:chat',
res: {
},
errors: {
noSuchMessage: {
message: 'No such message.',

View File

@@ -16,9 +16,6 @@ export const meta = {
kind: 'write:chat',
res: {
},
errors: {
noSuchMessage: {
message: 'No such message.',

View File

@@ -9,6 +9,7 @@ import { DI } from '@/di-symbols.js';
import { ChatService } from '@/core/ChatService.js';
import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
import { ApiError } from '@/server/api/error.js';
import { IdService } from '@/core/IdService.js';
export const meta = {
tags: ['chat'],
@@ -42,6 +43,8 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
roomId: { type: 'string', format: 'misskey:id' },
},
required: ['roomId'],
@@ -52,8 +55,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
private chatEntityService: ChatEntityService,
private chatService: ChatService,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null);
await this.chatService.checkChatAvailability(me.id, 'read');
const room = await this.chatService.findRoomById(ps.roomId);
@@ -65,7 +72,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchRoom);
}
const messages = await this.chatService.roomTimeline(room.id, ps.limit, ps.sinceId, ps.untilId);
const messages = await this.chatService.roomTimeline(room.id, ps.limit, sinceId, untilId);
this.chatService.readRoomChatMessage(me.id, room.id);

View File

@@ -16,9 +16,6 @@ export const meta = {
kind: 'write:chat',
res: {
},
errors: {
noSuchMessage: {
message: 'No such message.',

View File

@@ -10,6 +10,7 @@ import { GetterService } from '@/server/api/GetterService.js';
import { ChatService } from '@/core/ChatService.js';
import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
import { ApiError } from '@/server/api/error.js';
import { IdService } from '@/core/IdService.js';
export const meta = {
tags: ['chat'],
@@ -43,6 +44,8 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
userId: { type: 'string', format: 'misskey:id' },
},
required: ['userId'],
@@ -54,8 +57,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private chatEntityService: ChatEntityService,
private chatService: ChatService,
private getterService: GetterService,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null);
await this.chatService.checkChatAvailability(me.id, 'read');
const other = await this.getterService.getUser(ps.userId).catch(err => {
@@ -63,7 +70,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw err;
});
const messages = await this.chatService.userTimeline(me.id, other.id, ps.limit, ps.sinceId, ps.untilId);
const messages = await this.chatService.userTimeline(me.id, other.id, ps.limit, sinceId, untilId);
this.chatService.readUserChatMessage(me.id, other.id);

View File

@@ -0,0 +1,40 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { ChatService } from '@/core/ChatService.js';
import { ApiError } from '@/server/api/error.js';
export const meta = {
tags: ['chat'],
requireCredential: true,
kind: 'write:chat',
errors: {
},
} as const;
export const paramDef = {
type: 'object',
properties: {
},
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private chatService: ChatService,
) {
super(meta, paramDef, async (ps, me) => {
await this.chatService.checkChatAvailability(me.id, 'read');
await this.chatService.readAllChatMessages(me.id);
});
}
}

View File

@@ -16,9 +16,6 @@ export const meta = {
kind: 'write:chat',
res: {
},
errors: {
noSuchRoom: {
message: 'No such room.',

View File

@@ -16,9 +16,6 @@ export const meta = {
kind: 'write:chat',
res: {
},
errors: {
noSuchRoom: {
message: 'No such room.',

View File

@@ -9,6 +9,7 @@ import { DI } from '@/di-symbols.js';
import { ChatService } from '@/core/ChatService.js';
import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
import { ApiError } from '@/server/api/error.js';
import { IdService } from '@/core/IdService.js';
export const meta = {
tags: ['chat'],
@@ -37,6 +38,8 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
},
} as const;
@@ -45,11 +48,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
private chatEntityService: ChatEntityService,
private chatService: ChatService,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null);
await this.chatService.checkChatAvailability(me.id, 'read');
const invitations = await this.chatService.getReceivedRoomInvitationsWithPagination(me.id, ps.limit, ps.sinceId, ps.untilId);
const invitations = await this.chatService.getReceivedRoomInvitationsWithPagination(me.id, ps.limit, sinceId, untilId);
return this.chatEntityService.packRoomInvitations(invitations, me);
});
}

View File

@@ -10,6 +10,7 @@ import { DI } from '@/di-symbols.js';
import { ApiError } from '@/server/api/error.js';
import { ChatService } from '@/core/ChatService.js';
import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
import { IdService } from '@/core/IdService.js';
export const meta = {
tags: ['chat'],
@@ -44,6 +45,8 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
},
required: ['roomId'],
} as const;
@@ -53,8 +56,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
private chatService: ChatService,
private chatEntityService: ChatEntityService,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null);
await this.chatService.checkChatAvailability(me.id, 'read');
const room = await this.chatService.findMyRoomById(me.id, ps.roomId);
@@ -62,7 +69,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchRoom);
}
const invitations = await this.chatService.getSentRoomInvitationsWithPagination(ps.roomId, ps.limit, ps.sinceId, ps.untilId);
const invitations = await this.chatService.getSentRoomInvitationsWithPagination(ps.roomId, ps.limit, sinceId, untilId);
return this.chatEntityService.packRoomInvitations(invitations, me);
});
}

View File

@@ -16,9 +16,6 @@ export const meta = {
kind: 'write:chat',
res: {
},
errors: {
noSuchRoom: {
message: 'No such room.',

View File

@@ -9,6 +9,7 @@ import { DI } from '@/di-symbols.js';
import { ChatService } from '@/core/ChatService.js';
import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
import { ApiError } from '@/server/api/error.js';
import { IdService } from '@/core/IdService.js';
export const meta = {
tags: ['chat'],
@@ -37,6 +38,8 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
},
} as const;
@@ -45,11 +48,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
private chatService: ChatService,
private chatEntityService: ChatEntityService,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null);
await this.chatService.checkChatAvailability(me.id, 'read');
const memberships = await this.chatService.getMyMemberships(me.id, ps.limit, ps.sinceId, ps.untilId);
const memberships = await this.chatService.getMyMemberships(me.id, ps.limit, sinceId, untilId);
return this.chatEntityService.packRoomMemberships(memberships, me, {
populateUser: false,

View File

@@ -16,9 +16,6 @@ export const meta = {
kind: 'write:chat',
res: {
},
errors: {
noSuchRoom: {
message: 'No such room.',

View File

@@ -9,6 +9,7 @@ import { DI } from '@/di-symbols.js';
import { ChatService } from '@/core/ChatService.js';
import { ApiError } from '@/server/api/error.js';
import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
import { IdService } from '@/core/IdService.js';
export const meta = {
tags: ['chat'],
@@ -43,6 +44,8 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
},
required: ['roomId'],
} as const;
@@ -52,8 +55,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
private chatService: ChatService,
private chatEntityService: ChatEntityService,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null);
await this.chatService.checkChatAvailability(me.id, 'read');
const room = await this.chatService.findRoomById(ps.roomId);
@@ -65,7 +72,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchRoom);
}
const memberships = await this.chatService.getRoomMembershipsWithPagination(room.id, ps.limit, ps.sinceId, ps.untilId);
const memberships = await this.chatService.getRoomMembershipsWithPagination(room.id, ps.limit, sinceId, untilId);
return this.chatEntityService.packRoomMemberships(memberships, me, {
populateUser: true,

View File

@@ -16,9 +16,6 @@ export const meta = {
kind: 'write:chat',
res: {
},
errors: {
noSuchRoom: {
message: 'No such room.',

View File

@@ -9,6 +9,7 @@ import { DI } from '@/di-symbols.js';
import { ChatService } from '@/core/ChatService.js';
import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
import { ApiError } from '@/server/api/error.js';
import { IdService } from '@/core/IdService.js';
export const meta = {
tags: ['chat'],
@@ -37,6 +38,8 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 30 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
},
} as const;
@@ -45,11 +48,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
private chatEntityService: ChatEntityService,
private chatService: ChatService,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null);
await this.chatService.checkChatAvailability(me.id, 'read');
const rooms = await this.chatService.getOwnedRoomsWithPagination(me.id, ps.limit, ps.sinceId, ps.untilId);
const rooms = await this.chatService.getOwnedRoomsWithPagination(me.id, ps.limit, sinceId, untilId);
return this.chatEntityService.packRooms(rooms, me);
});
}

View File

@@ -5,6 +5,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
import type { ClipsRepository } from '@/models/_.js';
import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
import { DI } from '@/di-symbols.js';
@@ -29,7 +30,13 @@ export const meta = {
export const paramDef = {
type: 'object',
properties: {},
properties: {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
},
required: [],
} as const;
@@ -39,12 +46,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.clipsRepository)
private clipsRepository: ClipsRepository,
private queryService: QueryService,
private clipEntityService: ClipEntityService,
) {
super(meta, paramDef, async (ps, me) => {
const clips = await this.clipsRepository.findBy({
userId: me.id,
});
const query = this.queryService.makePaginationQuery(this.clipsRepository.createQueryBuilder('clip'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('clip.userId = :userId', { userId: me.id });
const clips = await query.limit(ps.limit).getMany();
return await this.clipEntityService.packMany(clips, me);
});

View File

@@ -4,11 +4,13 @@
*/
import { Inject, Injectable } from '@nestjs/common';
import { Brackets } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { NotesRepository, ClipsRepository, ClipNotesRepository } from '@/models/_.js';
import { QueryService } from '@/core/QueryService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { DI } from '@/di-symbols.js';
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
import { ApiError } from '../../error.js';
export const meta = {
@@ -44,6 +46,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' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
search: { type: 'string', minLength: 1, maxLength: 100, nullable: true },
},
required: ['clipId'],
} as const;
@@ -76,7 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchClip);
}
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.innerJoin(this.clipNotesRepository.metadata.targetName, 'clipNote', 'clipNote.noteId = note.id')
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
@@ -85,10 +90,23 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
.leftJoinAndSelect('renote.user', 'renoteUser')
.andWhere('clipNote.clipId = :clipId', { clipId: clip.id });
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateBlockedHostQueryForNote(query);
// this.queryService.generateSuspendedUserQueryForNote(query); // To avoid problems with removing notes, ignoring suspended user for now
if (me) {
this.queryService.generateVisibilityQuery(query, me);
this.queryService.generateMutedUserQueryForNotes(query, me);
this.queryService.generateBlockedUserQueryForNotes(query, me);
this.queryService.generateMutedUserQueryForNotes(query, me, { noteColumn: 'renote' });
this.queryService.generateBlockedUserQueryForNotes(query, me, { noteColumn: 'renote' });
}
if (ps.search != null) {
for (const word of ps.search.trim().split(' ')) {
query.andWhere(new Brackets(qb => {
qb.orWhere('note.text ILIKE :search', { search: `%${sqlLikeEscape(word)}%` });
qb.orWhere('note.cw ILIKE :search', { search: `%${sqlLikeEscape(word)}%` });
}));
}
}
const notes = await query

View File

@@ -34,6 +34,8 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
type: { type: 'string', nullable: true, pattern: /^[a-zA-Z\/\-*]+$/.toString().slice(1, -1) },
sort: { type: 'string', nullable: true, enum: ['+createdAt', '-createdAt', '+name', '-name', '+size', '-size', null] },
@@ -51,7 +53,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.driveFilesRepository.createQueryBuilder('file'), ps.sinceId, ps.untilId)
const query = this.queryService.makePaginationQuery(this.driveFilesRepository.createQueryBuilder('file'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('file.userId = :userId', { userId: me.id });
if (ps.folderId) {

View File

@@ -0,0 +1,93 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { DriveFilesRepository, ChatMessagesRepository } from '@/models/_.js';
import { QueryService } from '@/core/QueryService.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { ChatEntityService } from '@/core/entities/ChatEntityService.js';
import { ChatService } from '@/core/ChatService.js';
import { ApiError } from '../../../error.js';
export const meta = {
tags: ['drive', 'chat'],
requireCredential: true,
kind: 'read:drive',
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
ref: 'ChatMessage',
},
},
errors: {
noSuchFile: {
message: 'No such file.',
code: 'NO_SUCH_FILE',
id: '485ce26d-f5d2-4313-9783-e689d131eafb',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
fileId: { type: 'string', format: 'misskey:id' },
},
required: ['fileId'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
@Inject(DI.chatMessagesRepository)
private chatMessagesRepository: ChatMessagesRepository,
private chatService: ChatService,
private chatEntityService: ChatEntityService,
private queryService: QueryService,
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const isModerator = await this.roleService.isModerator(me);
if (!isModerator) {
await this.chatService.checkChatAvailability(me.id, 'read');
}
const file = await this.driveFilesRepository.findOneBy({
id: ps.fileId,
userId: isModerator ? undefined : me.id,
});
if (file == null) {
throw new ApiError(meta.errors.noSuchFile);
}
const query = this.queryService.makePaginationQuery(this.chatMessagesRepository.createQueryBuilder('message'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate);
query.andWhere('message.fileId = :fileId', { fileId: file.id });
const messages = await query.limit(ps.limit).getMany();
return await this.chatEntityService.packMessagesDetailed(messages, me);
});
}
}

View File

@@ -9,8 +9,8 @@ import type { NotesRepository, DriveFilesRepository } from '@/models/_.js';
import { QueryService } from '@/core/QueryService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../../error.js';
export const meta = {
tags: ['drive', 'notes'],
@@ -45,6 +45,8 @@ export const paramDef = {
properties: {
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
fileId: { type: 'string', format: 'misskey:id' },
},
@@ -75,7 +77,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchFile);
}
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId);
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate);
query.andWhere(':file <@ note.fileIds', { file: [file.id] });
const notes = await query.limit(ps.limit).getMany();

View File

@@ -10,9 +10,9 @@ import { IdentifiableError } from '@/misc/identifiable-error.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { DriveService } from '@/core/DriveService.js';
import { ApiError } from '../../../error.js';
import { MiMeta } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
export const meta = {
tags: ['drive'],
@@ -56,6 +56,19 @@ export const meta = {
code: 'NO_FREE_SPACE',
id: 'd08dbc37-a6a9-463a-8c47-96c32ab5f064',
},
maxFileSizeExceeded: {
message: 'Cannot upload the file because it exceeds the maximum file size.',
code: 'MAX_FILE_SIZE_EXCEEDED',
id: 'b9d8c348-33f0-4673-b9a9-5d4da058977a',
httpStatusCode: 413,
},
unallowedFileType: {
message: 'Cannot upload the file because it is an unallowed file type.',
code: 'UNALLOWED_FILE_TYPE',
id: '4becd248-7f2c-48c4-a9f0-75edc4f9a1ea',
},
},
} as const;
@@ -115,6 +128,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (err instanceof IdentifiableError) {
if (err.id === '282f77bf-5816-4f72-9264-aa14d8261a21') throw new ApiError(meta.errors.inappropriate);
if (err.id === 'c6244ed2-a39a-4e1c-bf93-f0fbd7764fa6') throw new ApiError(meta.errors.noFreeSpace);
if (err.id === 'f9e4e5f3-4df4-40b5-b400-f236945f7073') throw new ApiError(meta.errors.maxFileSizeExceeded);
if (err.id === 'bd71c601-f9b0-4808-9137-a330647ced9b') throw new ApiError(meta.errors.unallowedFileType);
}
throw new ApiError();
} finally {

View File

@@ -0,0 +1,41 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { DriveService } from '@/core/DriveService.js';
import { ApiError } from '../../../error.js';
export const meta = {
tags: ['drive'],
requireCredential: true,
kind: 'write:drive',
errors: {
},
} as const;
export const paramDef = {
type: 'object',
properties: {
fileIds: { type: 'array', uniqueItems: true, minItems: 1, maxItems: 100, items: { type: 'string', format: 'misskey:id' } },
folderId: { type: 'string', format: 'misskey:id', nullable: true },
},
required: ['fileIds'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private driveService: DriveService,
) {
super(meta, paramDef, async (ps, me) => {
await this.driveService.moveFiles(ps.fileIds, ps.folderId ?? null, me.id);
});
}
}

View File

@@ -43,14 +43,21 @@ export const meta = {
} as const;
export const paramDef = {
type: 'object',
properties: {
fileId: { type: 'string', format: 'misskey:id' },
url: { type: 'string' },
},
anyOf: [
{ required: ['fileId'] },
{ required: ['url'] },
{
type: 'object',
properties: {
fileId: { type: 'string', format: 'misskey:id' },
},
required: ['fileId'],
},
{
type: 'object',
properties: {
url: { type: 'string' },
},
required: ['url'],
},
],
} as const;
@@ -64,21 +71,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
let file: MiDriveFile | null = null;
if (ps.fileId) {
file = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
} else if (ps.url) {
file = await this.driveFilesRepository.findOne({
where: [{
url: ps.url,
}, {
webpublicUrl: ps.url,
}, {
thumbnailUrl: ps.url,
}],
});
}
const file = await this.driveFilesRepository.findOneBy(
'fileId' in ps
? { id: ps.fileId }
: [{ url: ps.url }, { webpublicUrl: ps.url }, { thumbnailUrl: ps.url }],
);
if (file == null) {
throw new ApiError(meta.errors.noSuchFile);

View File

@@ -34,6 +34,8 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
},
required: [],
@@ -49,7 +51,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.driveFoldersRepository.createQueryBuilder('folder'), ps.sinceId, ps.untilId)
const query = this.queryService.makePaginationQuery(this.driveFoldersRepository.createQueryBuilder('folder'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('folder.userId = :userId', { userId: me.id });
if (ps.folderId) {

View File

@@ -34,6 +34,8 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
type: { type: 'string', pattern: /^[a-zA-Z\/\-*]+$/.toString().slice(1, -1) },
},
required: [],
@@ -49,7 +51,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.driveFilesRepository.createQueryBuilder('file'), ps.sinceId, ps.untilId)
const query = this.queryService.makePaginationQuery(this.driveFilesRepository.createQueryBuilder('file'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('file.userId = :userId', { userId: me.id });
if (ps.type) {

View File

@@ -32,6 +32,8 @@ export const paramDef = {
host: { type: 'string' },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
},
required: ['host'],
@@ -47,7 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.followingsRepository.createQueryBuilder('following'), ps.sinceId, ps.untilId)
const query = this.queryService.makePaginationQuery(this.followingsRepository.createQueryBuilder('following'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('following.followeeHost = :host', { host: ps.host });
const followings = await query

View File

@@ -32,6 +32,8 @@ export const paramDef = {
host: { type: 'string' },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
},
required: ['host'],
@@ -47,7 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.followingsRepository.createQueryBuilder('following'), ps.sinceId, ps.untilId)
const query = this.queryService.makePaginationQuery(this.followingsRepository.createQueryBuilder('following'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('following.followerHost = :host', { host: ps.host });
const followings = await query

View File

@@ -32,6 +32,8 @@ export const paramDef = {
host: { type: 'string' },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
},
required: ['host'],
@@ -47,7 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.usersRepository.createQueryBuilder('user'), ps.sinceId, ps.untilId)
const query = this.queryService.makePaginationQuery(this.usersRepository.createQueryBuilder('user'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('user.host = :host', { host: ps.host });
const users = await query

View File

@@ -5,10 +5,9 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { FlashLikesRepository } from '@/models/_.js';
import { QueryService } from '@/core/QueryService.js';
import { FlashLikeEntityService } from '@/core/entities/FlashLikeEntityService.js';
import { DI } from '@/di-symbols.js';
import { FlashService } from '@/core/FlashService.js';
export const meta = {
tags: ['account', 'flash'],
@@ -44,6 +43,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' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
search: { type: 'string', minLength: 1, maxLength: 100, nullable: true },
},
required: [],
} as const;
@@ -51,20 +53,18 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.flashLikesRepository)
private flashLikesRepository: FlashLikesRepository,
private flashLikeEntityService: FlashLikeEntityService,
private queryService: QueryService,
private flashService: FlashService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.flashLikesRepository.createQueryBuilder('like'), ps.sinceId, ps.untilId)
.andWhere('like.userId = :meId', { meId: me.id })
.leftJoinAndSelect('like.flash', 'flash');
const likes = await query
.limit(ps.limit)
.getMany();
const likes = await this.flashService.myLikes(me.id, {
sinceId: ps.sinceId,
untilId: ps.untilId,
sinceDate: ps.sinceDate,
untilDate: ps.untilDate,
limit: ps.limit,
search: ps.search,
});
return this.flashLikeEntityService.packMany(likes, me);
});

View File

@@ -34,6 +34,8 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
},
required: [],
} as const;
@@ -48,7 +50,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.flashsRepository.createQueryBuilder('flash'), ps.sinceId, ps.untilId)
const query = this.queryService.makePaginationQuery(this.flashsRepository.createQueryBuilder('flash'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('flash.userId = :meId', { meId: me.id });
const flashs = await query

View File

@@ -0,0 +1,59 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
import { DI } from '@/di-symbols.js';
import { FlashService } from '@/core/FlashService.js';
export const meta = {
tags: ['flash'],
requireCredential: false,
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
ref: 'Flash',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
query: { type: 'string', minLength: 1, maxLength: 100 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 5 },
},
required: ['query'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private flashService: FlashService,
private flashEntityService: FlashEntityService,
) {
super(meta, paramDef, async (ps, me) => {
const result = await this.flashService.search(ps.query, {
sinceId: ps.sinceId,
untilId: ps.untilId,
sinceDate: ps.sinceDate,
untilDate: ps.untilDate,
limit: ps.limit,
});
return await this.flashEntityService.packMany(result, me);
});
}
}

View File

@@ -73,8 +73,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
updatedAt: new Date(),
...Object.fromEntries(
Object.entries(ps).filter(
([key, val]) => (key !== 'flashId') && Object.hasOwn(paramDef.properties, key)
)
([key, val]) => (key !== 'flashId') && Object.hasOwn(paramDef.properties, key),
),
),
});
});

View File

@@ -49,6 +49,8 @@ export const paramDef = {
properties: {
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
},
required: [],
@@ -64,7 +66,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.followRequestsRepository.createQueryBuilder('request'), ps.sinceId, ps.untilId)
const query = this.queryService.makePaginationQuery(this.followRequestsRepository.createQueryBuilder('request'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('request.followeeId = :meId', { meId: me.id });
const requests = await query

View File

@@ -49,6 +49,8 @@ export const paramDef = {
properties: {
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
},
required: [],
@@ -64,7 +66,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.followRequestsRepository.createQueryBuilder('request'), ps.sinceId, ps.untilId)
const query = this.queryService.makePaginationQuery(this.followRequestsRepository.createQueryBuilder('request'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('request.followerId = :meId', { meId: me.id });
const requests = await query

View File

@@ -30,6 +30,8 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
},
required: [],
} as const;
@@ -44,7 +46,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.galleryPostsRepository.createQueryBuilder('post'), ps.sinceId, ps.untilId)
const query = this.queryService.makePaginationQuery(this.galleryPostsRepository.createQueryBuilder('post'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.innerJoinAndSelect('post.user', 'user');
const posts = await query.limit(ps.limit).getMany();

View File

@@ -81,7 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
} catch (e) {
} catch (_) {
throw new Error('authentication failed');
}
}

View File

@@ -212,7 +212,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
} catch (e) {
} catch (_) {
throw new Error('authentication failed');
}
}

View File

@@ -72,7 +72,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
} catch (e) {
} catch (_) {
throw new Error('authentication failed');
}
}

View File

@@ -61,7 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
} catch (e) {
} catch (_) {
throw new Error('authentication failed');
}
}

View File

@@ -57,7 +57,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
} catch (e) {
} catch (_) {
throw new Error('authentication failed');
}
}

View File

@@ -46,6 +46,14 @@ export const meta = {
type: 'string',
},
},
iconUrl: {
type: 'string',
optional: true, nullable: true,
},
description: {
type: 'string',
optional: true, nullable: true,
},
},
},
},
@@ -88,6 +96,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
createdAt: this.idService.parse(token.id).date.toISOString(),
lastUsedAt: token.lastUsedAt?.toISOString(),
permission: token.app ? token.app.permission : token.permission,
iconUrl: token.iconUrl,
description: token.description ?? token.app?.description ?? null,
})));
});
}

View File

@@ -45,7 +45,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
} catch (e) {
} catch (_) {
throw new Error('authentication failed');
}
}

View File

@@ -5,7 +5,8 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { AchievementService, ACHIEVEMENT_TYPES } from '@/core/AchievementService.js';
import { AchievementService } from '@/core/AchievementService.js';
import { ACHIEVEMENT_TYPES } from '@/models/UserProfile.js';
export const meta = {
requireCredential: true,

View File

@@ -49,7 +49,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
try {
await this.userAuthService.twoFactorAuthenticate(profile, token);
} catch (e) {
} catch (_) {
throw new Error('authentication failed');
}
}

View File

@@ -34,6 +34,8 @@ export const paramDef = {
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
},
required: [],
} as const;
@@ -48,7 +50,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.noteFavoritesRepository.createQueryBuilder('favorite'), ps.sinceId, ps.untilId)
const query = this.queryService.makePaginationQuery(this.noteFavoritesRepository.createQueryBuilder('favorite'), ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('favorite.userId = :meId', { meId: me.id })
.leftJoinAndSelect('favorite.note', 'note');

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