Merge remote-tracking branch 'upstream/stable' into nodeinfostats

This commit is contained in:
Kinetix 2025-03-03 12:13:25 -08:00
commit 9adbb114c8
508 changed files with 30187 additions and 14010 deletions

View file

@ -30,14 +30,14 @@ import type { MiNote } from '@/models/Note.js';
import { QueryService } from '@/core/QueryService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type Logger from '@/logger.js';
import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
import { IActivity } from '@/core/activitypub/type.js';
import { isQuote, isRenote } from '@/misc/is-renote.js';
import * as Acct from '@/misc/acct.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify';
import type { FindOptionsWhere } from 'typeorm';
import type Logger from '@/logger.js';
import { LoggerService } from '@/core/LoggerService.js';
const ACTIVITY_JSON = 'application/activity+json; charset=utf-8';
const LD_JSON = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"; charset=utf-8';
@ -103,15 +103,16 @@ export class ActivityPubServerService {
/**
* Pack Create<Note> or Announce Activity
* @param note Note
* @param author Author of the note
*/
@bindThis
private async packActivity(note: MiNote): Promise<any> {
private async packActivity(note: MiNote, author: MiUser): Promise<any> {
if (isRenote(note) && !isQuote(note)) {
const renote = await this.notesRepository.findOneByOrFail({ id: note.renoteId });
return this.apRendererService.renderAnnounce(renote.uri ? renote.uri : `${this.config.url}/notes/${renote.id}`, note);
}
return this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, false), note);
return this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, author, false), note);
}
@bindThis
@ -506,7 +507,7 @@ export class ActivityPubServerService {
this.notesRepository.findOneByOrFail({ id: pining.noteId }))))
.filter(note => !note.localOnly && ['public', 'home'].includes(note.visibility));
const renderedNotes = await Promise.all(pinnedNotes.map(note => this.apRendererService.renderNote(note)));
const renderedNotes = await Promise.all(pinnedNotes.map(note => this.apRendererService.renderNote(note, user)));
const rendered = this.apRendererService.renderOrderedCollection(
`${this.config.url}/users/${userId}/collections/featured`,
@ -579,7 +580,7 @@ export class ActivityPubServerService {
if (sinceId) notes.reverse();
const activities = await Promise.all(notes.map(note => this.packActivity(note)));
const activities = await Promise.all(notes.map(note => this.packActivity(note, user)));
const rendered = this.apRendererService.renderOrderedCollectionPage(
`${partOf}?${url.query({
page: 'true',
@ -653,8 +654,8 @@ export class ActivityPubServerService {
},
deriveConstraint(request: IncomingMessage) {
const accepted = accepts(request).type(['html', ACTIVITY_JSON, LD_JSON]);
const isAp = typeof accepted === 'string' && !accepted.match(/html/);
return isAp ? 'ap' : 'html';
if (accepted === false) return null;
return accepted !== 'html' ? 'ap' : 'html';
},
});
@ -723,7 +724,9 @@ export class ActivityPubServerService {
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
this.setResponseType(request, reply);
return this.apRendererService.addContext(await this.apRendererService.renderNote(note, false));
const author = await this.usersRepository.findOneByOrFail({ id: note.userId });
return this.apRendererService.addContext(await this.apRendererService.renderNote(note, author, false));
});
// note activity
@ -746,7 +749,9 @@ export class ActivityPubServerService {
if (!this.config.checkActivityPubGetSignature) reply.header('Cache-Control', 'public, max-age=180');
this.setResponseType(request, reply);
return (this.apRendererService.addContext(await this.packActivity(note)));
const author = await this.usersRepository.findOneByOrFail({ id: note.userId });
return (this.apRendererService.addContext(await this.packActivity(note, author)));
});
// outbox

View file

@ -11,7 +11,7 @@ import rename from 'rename';
import sharp from 'sharp';
import { sharpBmp } from '@misskey-dev/sharp-read-bmp';
import type { Config } from '@/config.js';
import type { MiDriveFile, DriveFilesRepository } from '@/models/_.js';
import type { MiDriveFile, DriveFilesRepository, MiUser } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { createTemp } from '@/misc/create-temp.js';
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
@ -30,8 +30,7 @@ import { correctFilename } from '@/misc/correct-filename.js';
import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js';
import { getIpHash } from '@/misc/get-ip-hash.js';
import { AuthenticateService } from '@/server/api/AuthenticateService.js';
import { RoleService } from '@/core/RoleService.js';
import { SkRateLimiterService } from '@/server/api/SkRateLimiterService.js';
import { SkRateLimiterService } from '@/server/SkRateLimiterService.js';
import { Keyed, RateLimit, sendRateLimitHeaders } from '@/misc/rate-limit-utils.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions } from 'fastify';
@ -59,7 +58,6 @@ export class FileServerService {
private loggerService: LoggerService,
private authenticateService: AuthenticateService,
private rateLimiterService: SkRateLimiterService,
private roleService: RoleService,
) {
this.logger = this.loggerService.getLogger('server', 'gray');
@ -625,14 +623,13 @@ export class FileServerService {
// koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app.
const [user] = await this.authenticateService.authenticate(token);
const actor = user?.id ?? getIpHash(request.ip);
const factor = user ? (await this.roleService.getUserPolicies(user.id)).rateLimitFactor : 1;
const actor = user ?? getIpHash(request.ip);
// Call both limits: the per-resource limit and the shared cross-resource limit
return await this.checkResourceLimit(reply, actor, group, resource, factor) && await this.checkSharedLimit(reply, actor, group, factor);
return await this.checkResourceLimit(reply, actor, group, resource) && await this.checkSharedLimit(reply, actor, group);
}
private async checkResourceLimit(reply: FastifyReply, actor: string, group: string, resource: string, factor = 1): Promise<boolean> {
private async checkResourceLimit(reply: FastifyReply, actor: string | MiUser, group: string, resource: string): Promise<boolean> {
const limit: Keyed<RateLimit> = {
// Group by resource
key: `${group}${resource}`,
@ -643,10 +640,10 @@ export class FileServerService {
dripRate: 1000 * 60,
};
return await this.checkLimit(reply, actor, limit, factor);
return await this.checkLimit(reply, actor, limit);
}
private async checkSharedLimit(reply: FastifyReply, actor: string, group: string, factor = 1): Promise<boolean> {
private async checkSharedLimit(reply: FastifyReply, actor: string | MiUser, group: string): Promise<boolean> {
const limit: Keyed<RateLimit> = {
key: group,
type: 'bucket',
@ -655,11 +652,11 @@ export class FileServerService {
size: 3600,
};
return await this.checkLimit(reply, actor, limit, factor);
return await this.checkLimit(reply, actor, limit);
}
private async checkLimit(reply: FastifyReply, actor: string, limit: Keyed<RateLimit>, factor = 1): Promise<boolean> {
const info = await this.rateLimiterService.limit(limit, actor, factor);
private async checkLimit(reply: FastifyReply, actor: string | MiUser, limit: Keyed<RateLimit>): Promise<boolean> {
const info = await this.rateLimiterService.limit(limit, actor);
sendRateLimitHeaders(reply, info);

View file

@ -6,7 +6,7 @@
import { Module } from '@nestjs/common';
import { EndpointsModule } from '@/server/api/EndpointsModule.js';
import { CoreModule } from '@/core/CoreModule.js';
import { SkRateLimiterService } from '@/server/api/SkRateLimiterService.js';
import { SkRateLimiterService } from '@/server/SkRateLimiterService.js';
import { ApiCallService } from './api/ApiCallService.js';
import { FileServerService } from './FileServerService.js';
import { HealthServerService } from './HealthServerService.js';
@ -27,6 +27,8 @@ import { StreamingApiServerService } from './api/StreamingApiServerService.js';
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
import { ClientServerService } from './web/ClientServerService.js';
import { MastoConverters } from './api/mastodon/converters.js';
import { MastodonLogger } from './api/mastodon/MastodonLogger.js';
import { MastodonDataService } from './api/mastodon/MastodonDataService.js';
import { FeedService } from './web/FeedService.js';
import { UrlPreviewService } from './web/UrlPreviewService.js';
import { ClientLoggerService } from './web/ClientLoggerService.js';
@ -103,6 +105,8 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j
MastodonApiServerService,
OAuth2ProviderService,
MastoConverters,
MastodonLogger,
MastodonDataService,
],
exports: [
ServerService,

View file

@ -262,7 +262,7 @@ export class ServerService implements OnApplicationShutdown {
}
});
} else {
fastify.listen({ port: this.config.port, host: '0.0.0.0' });
fastify.listen({ port: this.config.port, host: this.config.address });
}
await fastify.ready();

View file

@ -12,6 +12,11 @@ SkRateLimiterService is not quite plug-and-play compatible with existing call si
Instead, the returned LimitInfo object will have `blocked` set to true.
Callers are responsible for checking this property and taking any desired action, such as rejecting a request or returning limit details.
Rate limit factors are also handled differently.
Instead of providing an optional parameter for callers, SkRateLimiterServer accepts an `MiUser` parameter that is used to compute the factor directly.
If a user is not available (such as for unauthenticated callers), then the Role Template factor is used instead.
To avoid confusion, the `factor` parameter has been removed entirely and is now an implementation detail.
## Headers
LimitInfo objects (returned by `SkRateLimitService.limit()`) can be passed to `rate-limit-utils.sendRateLimitHeaders()` to send standard rate limit headers with an HTTP response.
@ -34,6 +39,7 @@ The first call is read-only, while the others perform at least one write operati
Two integer keys are stored per client/subject, and both expire together after the maximum duration of the limit.
While performance has not been formally tested, it's expected that SkRateLimiterService has an impact roughly on par with the legacy RateLimiterService.
Redis memory usage should be notably lower due to the reduced number of keys and avoidance of set / array constructions.
If redis load does become a concern, then a dedicated node can be assigned via the `redisForRateLimit` config setting.
## Concurrency and Multi-Node Correctness

View file

@ -5,36 +5,67 @@
import { Inject, Injectable } from '@nestjs/common';
import Redis from 'ioredis';
import { TimeService } from '@/core/TimeService.js';
import { EnvService } from '@/core/EnvService.js';
import type { TimeService } from '@/core/TimeService.js';
import type { EnvService } from '@/core/EnvService.js';
import { BucketRateLimit, LegacyRateLimit, LimitInfo, RateLimit, hasMinLimit, isLegacyRateLimit, Keyed, hasMaxLimit, disabledLimitInfo, MaxLegacyLimit, MinLegacyLimit } from '@/misc/rate-limit-utils.js';
import { DI } from '@/di-symbols.js';
import { MemoryKVCache } from '@/misc/cache.js';
import type { MiUser } from '@/models/_.js';
import type { RoleService } from '@/core/RoleService.js';
// Sentinel value used for caching the default role template.
// Required because MemoryKVCache doesn't support null keys.
const defaultUserKey = '';
@Injectable()
export class SkRateLimiterService {
// 1-minute cache interval
private readonly factorCache = new MemoryKVCache<number>(1000 * 60);
private readonly disabled: boolean;
constructor(
@Inject(TimeService)
@Inject('TimeService')
private readonly timeService: TimeService,
@Inject(DI.redis)
@Inject(DI.redisForRateLimit)
private readonly redisClient: Redis.Redis,
@Inject(EnvService)
@Inject('RoleService')
private readonly roleService: RoleService,
@Inject('EnvService')
envService: EnvService,
) {
this.disabled = envService.env.NODE_ENV === 'test';
}
/**
* Check & increment a rate limit
* Check & increment a rate limit for a client.
*
* If the client (actorOrUser) is passed as a string, then it uses the default rate limit factor from the role template.
* If the client (actorOrUser) is passed as an MiUser, then it queries the user's actual rate limit factor from their assigned roles.
*
* A factor of zero (0) will disable the limit, while any negative number will produce an error.
* A factor between zero (0) and one (1) will increase the limit from its default values (allowing more actions per time interval).
* A factor greater than one (1) will decrease the limit from its default values (allowing fewer actions per time interval).
*
* @param limit The limit definition
* @param actor Client who is calling this limit
* @param factor Scaling factor - smaller = larger limit (less restrictive)
* @param actorOrUser authenticated client user or IP hash
*/
public async limit(limit: Keyed<RateLimit>, actor: string, factor = 1): Promise<LimitInfo> {
if (this.disabled || factor === 0) {
public async limit(limit: Keyed<RateLimit>, actorOrUser: string | MiUser): Promise<LimitInfo> {
if (this.disabled) {
return disabledLimitInfo;
}
const actor = typeof(actorOrUser) === 'object' ? actorOrUser.id : actorOrUser;
const userCacheKey = typeof(actorOrUser) === 'object' ? actorOrUser.id : defaultUserKey;
const userRoleKey = typeof(actorOrUser) === 'object' ? actorOrUser.id : null;
const factor = this.factorCache.get(userCacheKey) ?? await this.factorCache.fetch(userCacheKey, async () => {
const role = await this.roleService.getUserPolicies(userRoleKey);
return role.rateLimitFactor;
});
if (factor === 0) {
return disabledLimitInfo;
}
@ -42,10 +73,6 @@ export class SkRateLimiterService {
throw new Error(`Rate limit factor is zero or negative: ${factor}`);
}
return await this.tryLimit(limit, actor, factor);
}
private async tryLimit(limit: Keyed<RateLimit>, actor: string, factor: number): Promise<LimitInfo> {
if (isLegacyRateLimit(limit)) {
return await this.limitLegacy(limit, actor, factor);
} else {

View file

@ -19,7 +19,7 @@ import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import type { Config } from '@/config.js';
import { sendRateLimitHeaders } from '@/misc/rate-limit-utils.js';
import { SkRateLimiterService } from '@/server/api/SkRateLimiterService.js';
import { SkRateLimiterService } from '@/server/SkRateLimiterService.js';
import { ApiError } from './error.js';
import { ApiLoggerService } from './ApiLoggerService.js';
import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
@ -313,35 +313,30 @@ export class ApiCallService implements OnApplicationShutdown {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (endpointLimit) {
// koa will automatically load the `X-Forwarded-For` header if `proxy: true` is configured in the app.
let limitActor: string;
let limitActor: string | MiLocalUser;
if (user) {
limitActor = user.id;
limitActor = user;
} else {
limitActor = getIpHash(request.ip);
}
// TODO: 毎リクエスト計算するのもあれだしキャッシュしたい
const factor = user ? (await this.roleService.getUserPolicies(user.id)).rateLimitFactor : 1;
const limit = {
key: ep.name,
...endpointLimit,
};
if (factor > 0) {
const limit = {
key: ep.name,
...endpointLimit,
};
// Rate limit
const info = await this.rateLimiterService.limit(limit, limitActor);
// Rate limit
const info = await this.rateLimiterService.limit(limit, limitActor, factor);
sendRateLimitHeaders(reply, info);
sendRateLimitHeaders(reply, info);
if (info.blocked) {
throw new ApiError({
message: 'Rate limit exceeded. Please try again later.',
code: 'RATE_LIMIT_EXCEEDED',
id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
httpStatusCode: 429,
}, info);
}
if (info.blocked) {
throw new ApiError({
message: 'Rate limit exceeded. Please try again later.',
code: 'RATE_LIMIT_EXCEEDED',
id: 'd5826d14-3982-4d2e-8011-b9e9f02499ef',
httpStatusCode: 429,
}, info);
}
}

File diff suppressed because it is too large Load diff

View file

@ -26,12 +26,19 @@ import { UserAuthService } from '@/core/UserAuthService.js';
import { CaptchaService } from '@/core/CaptchaService.js';
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
import { isSystemAccount } from '@/misc/is-system-account.js';
import { SkRateLimiterService } from '@/server/api/SkRateLimiterService.js';
import { sendRateLimitHeaders } from '@/misc/rate-limit-utils.js';
import { SkRateLimiterService } from '@/server/SkRateLimiterService.js';
import { Keyed, RateLimit, sendRateLimitHeaders } from '@/misc/rate-limit-utils.js';
import { SigninService } from './SigninService.js';
import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
import type { FastifyReply, FastifyRequest } from 'fastify';
// Up to 10 attempts, then 1 per minute
const signinRateLimit: Keyed<RateLimit> = {
key: 'signin',
max: 10,
dripRate: 1000 * 60,
};
@Injectable()
export class SigninApiService {
constructor(
@ -94,7 +101,7 @@ export class SigninApiService {
}
// not more than 1 attempt per second and not more than 10 attempts per hour
const rateLimit = await this.rateLimiterService.limit({ key: 'signin', duration: 60 * 60 * 1000, max: 10, minInterval: 1000 }, getIpHash(request.ip));
const rateLimit = await this.rateLimiterService.limit(signinRateLimit, getIpHash(request.ip));
sendRateLimitHeaders(reply, rateLimit);

View file

@ -21,7 +21,7 @@ import { WebAuthnService } from '@/core/WebAuthnService.js';
import Logger from '@/logger.js';
import { LoggerService } from '@/core/LoggerService.js';
import type { IdentifiableError } from '@/misc/identifiable-error.js';
import { SkRateLimiterService } from '@/server/api/SkRateLimiterService.js';
import { SkRateLimiterService } from '@/server/SkRateLimiterService.js';
import { sendRateLimitHeaders } from '@/misc/rate-limit-utils.js';
import { SigninService } from './SigninService.js';
import type { AuthenticationResponseJSON } from '@simplewebauthn/types';

View file

@ -19,10 +19,9 @@ import { MiLocalUser } from '@/models/User.js';
import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
import { bindThis } from '@/decorators.js';
import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js';
import { RoleService } from '@/core/RoleService.js';
import { SigninService } from './SigninService.js';
import type { FastifyRequest, FastifyReply } from 'fastify';
import instance from './endpoints/charts/instance.js';
import { RoleService } from '@/core/RoleService.js';
@Injectable()
export class SignupApiService {

View file

@ -18,10 +18,9 @@ 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 { RoleService } from '@/core/RoleService.js';
import { getIpHash } from '@/misc/get-ip-hash.js';
import { LoggerService } from '@/core/LoggerService.js';
import { SkRateLimiterService } from '@/server/api/SkRateLimiterService.js';
import { SkRateLimiterService } from '@/server/SkRateLimiterService.js';
import { AuthenticateService, AuthenticationError } from './AuthenticateService.js';
import MainStreamConnection from './stream/Connection.js';
import { ChannelsService } from './stream/ChannelsService.js';
@ -49,7 +48,6 @@ export class StreamingApiServerService {
private usersService: UserService,
private channelFollowingService: ChannelFollowingService,
private rateLimiterService: SkRateLimiterService,
private roleService: RoleService,
private loggerService: LoggerService,
) {
}
@ -57,22 +55,18 @@ export class StreamingApiServerService {
@bindThis
private async rateLimitThis(
user: MiLocalUser | null | undefined,
requestIp: string | undefined,
requestIp: string,
limit: IEndpointMeta['limit'] & { key: NonNullable<string> },
) : Promise<boolean> {
let limitActor: string;
let limitActor: string | MiLocalUser;
if (user) {
limitActor = user.id;
limitActor = user;
} else {
limitActor = getIpHash(requestIp || 'wtf');
limitActor = getIpHash(requestIp);
}
const factor = user ? (await this.roleService.getUserPolicies(user.id)).rateLimitFactor : 1;
if (factor <= 0) return false;
// Rate limit
const rateLimit = await this.rateLimiterService.limit(limit, limitActor, factor);
const rateLimit = await this.rateLimiterService.limit(limit, limitActor);
return rateLimit.blocked;
}

View file

@ -0,0 +1,421 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/*
* This file contains list of all endpoints exported as pathname of API endpoint
*
* When you add new endpoint, you should add it to this file.
* This file is used to generate API documentation and EndpointsModule.
*/
export * as 'admin/abuse-report/notification-recipient/create' from './endpoints/admin/abuse-report/notification-recipient/create.js';
export * as 'admin/abuse-report/notification-recipient/delete' from './endpoints/admin/abuse-report/notification-recipient/delete.js';
export * as 'admin/abuse-report/notification-recipient/list' from './endpoints/admin/abuse-report/notification-recipient/list.js';
export * as 'admin/abuse-report/notification-recipient/show' from './endpoints/admin/abuse-report/notification-recipient/show.js';
export * as 'admin/abuse-report/notification-recipient/update' from './endpoints/admin/abuse-report/notification-recipient/update.js';
export * as 'admin/abuse-user-reports' from './endpoints/admin/abuse-user-reports.js';
export * as 'admin/accounts/create' from './endpoints/admin/accounts/create.js';
export * as 'admin/accounts/delete' from './endpoints/admin/accounts/delete.js';
export * as 'admin/accounts/find-by-email' from './endpoints/admin/accounts/find-by-email.js';
export * as 'admin/ad/create' from './endpoints/admin/ad/create.js';
export * as 'admin/ad/delete' from './endpoints/admin/ad/delete.js';
export * as 'admin/ad/list' from './endpoints/admin/ad/list.js';
export * as 'admin/ad/update' from './endpoints/admin/ad/update.js';
export * as 'admin/announcements/create' from './endpoints/admin/announcements/create.js';
export * as 'admin/announcements/delete' from './endpoints/admin/announcements/delete.js';
export * as 'admin/announcements/list' from './endpoints/admin/announcements/list.js';
export * as 'admin/announcements/update' from './endpoints/admin/announcements/update.js';
export * as 'admin/approve-user' from './endpoints/admin/approve-user.js';
export * as 'admin/avatar-decorations/create' from './endpoints/admin/avatar-decorations/create.js';
export * as 'admin/avatar-decorations/delete' from './endpoints/admin/avatar-decorations/delete.js';
export * as 'admin/avatar-decorations/list' from './endpoints/admin/avatar-decorations/list.js';
export * as 'admin/avatar-decorations/update' from './endpoints/admin/avatar-decorations/update.js';
export * as 'admin/captcha/current' from './endpoints/admin/captcha/current.js';
export * as 'admin/captcha/save' from './endpoints/admin/captcha/save.js';
export * as 'admin/cw-user' from './endpoints/admin/cw-user.js';
export * as 'admin/decline-user' from './endpoints/admin/decline-user.js';
export * as 'admin/delete-account' from './endpoints/admin/delete-account.js';
export * as 'admin/delete-all-files-of-a-user' from './endpoints/admin/delete-all-files-of-a-user.js';
export * as 'admin/drive/clean-remote-files' from './endpoints/admin/drive/clean-remote-files.js';
export * as 'admin/drive/cleanup' from './endpoints/admin/drive/cleanup.js';
export * as 'admin/drive/files' from './endpoints/admin/drive/files.js';
export * as 'admin/drive/show-file' from './endpoints/admin/drive/show-file.js';
export * as 'admin/emoji/add' from './endpoints/admin/emoji/add.js';
export * as 'admin/emoji/add-aliases-bulk' from './endpoints/admin/emoji/add-aliases-bulk.js';
export * as 'admin/emoji/copy' from './endpoints/admin/emoji/copy.js';
export * as 'admin/emoji/delete' from './endpoints/admin/emoji/delete.js';
export * as 'admin/emoji/delete-bulk' from './endpoints/admin/emoji/delete-bulk.js';
export * as 'admin/emoji/import-zip' from './endpoints/admin/emoji/import-zip.js';
export * as 'admin/emoji/list' from './endpoints/admin/emoji/list.js';
export * as 'admin/emoji/list-remote' from './endpoints/admin/emoji/list-remote.js';
export * as 'admin/emoji/remove-aliases-bulk' from './endpoints/admin/emoji/remove-aliases-bulk.js';
export * as 'admin/emoji/set-aliases-bulk' from './endpoints/admin/emoji/set-aliases-bulk.js';
export * as 'admin/emoji/set-category-bulk' from './endpoints/admin/emoji/set-category-bulk.js';
export * as 'admin/emoji/set-license-bulk' from './endpoints/admin/emoji/set-license-bulk.js';
export * as 'admin/emoji/update' from './endpoints/admin/emoji/update.js';
export * as 'admin/federation/delete-all-files' from './endpoints/admin/federation/delete-all-files.js';
export * as 'admin/federation/refresh-remote-instance-metadata' from './endpoints/admin/federation/refresh-remote-instance-metadata.js';
export * as 'admin/federation/remove-all-following' from './endpoints/admin/federation/remove-all-following.js';
export * as 'admin/federation/update-instance' from './endpoints/admin/federation/update-instance.js';
export * as 'admin/forward-abuse-user-report' from './endpoints/admin/forward-abuse-user-report.js';
export * as 'admin/gen-vapid-keys' from './endpoints/admin/gen-vapid-keys.js';
export * as 'admin/get-index-stats' from './endpoints/admin/get-index-stats.js';
export * as 'admin/get-table-stats' from './endpoints/admin/get-table-stats.js';
export * as 'admin/get-user-ips' from './endpoints/admin/get-user-ips.js';
export * as 'admin/invite/create' from './endpoints/admin/invite/create.js';
export * as 'admin/invite/list' from './endpoints/admin/invite/list.js';
export * as 'admin/meta' from './endpoints/admin/meta.js';
export * as 'admin/nsfw-user' from './endpoints/admin/nsfw-user.js';
export * as 'admin/promo/create' from './endpoints/admin/promo/create.js';
export * as 'admin/queue/clear' from './endpoints/admin/queue/clear.js';
export * as 'admin/queue/deliver-delayed' from './endpoints/admin/queue/deliver-delayed.js';
export * as 'admin/queue/inbox-delayed' from './endpoints/admin/queue/inbox-delayed.js';
export * as 'admin/queue/promote' from './endpoints/admin/queue/promote.js';
export * as 'admin/queue/stats' from './endpoints/admin/queue/stats.js';
export * as 'admin/reject-quotes' from './endpoints/admin/reject-quotes.js';
export * as 'admin/relays/add' from './endpoints/admin/relays/add.js';
export * as 'admin/relays/list' from './endpoints/admin/relays/list.js';
export * as 'admin/relays/remove' from './endpoints/admin/relays/remove.js';
export * as 'admin/reset-password' from './endpoints/admin/reset-password.js';
export * as 'admin/resolve-abuse-user-report' from './endpoints/admin/resolve-abuse-user-report.js';
export * as 'admin/roles/assign' from './endpoints/admin/roles/assign.js';
export * as 'admin/roles/create' from './endpoints/admin/roles/create.js';
export * as 'admin/roles/delete' from './endpoints/admin/roles/delete.js';
export * as 'admin/roles/list' from './endpoints/admin/roles/list.js';
export * as 'admin/roles/show' from './endpoints/admin/roles/show.js';
export * as 'admin/roles/unassign' from './endpoints/admin/roles/unassign.js';
export * as 'admin/roles/update' from './endpoints/admin/roles/update.js';
export * as 'admin/roles/update-default-policies' from './endpoints/admin/roles/update-default-policies.js';
export * as 'admin/roles/users' from './endpoints/admin/roles/users.js';
export * as 'admin/send-email' from './endpoints/admin/send-email.js';
export * as 'admin/server-info' from './endpoints/admin/server-info.js';
export * as 'admin/show-moderation-logs' from './endpoints/admin/show-moderation-logs.js';
export * as 'admin/show-user' from './endpoints/admin/show-user.js';
export * as 'admin/show-users' from './endpoints/admin/show-users.js';
export * as 'admin/silence-user' from './endpoints/admin/silence-user.js';
export * as 'admin/suspend-user' from './endpoints/admin/suspend-user.js';
export * as 'admin/system-webhook/create' from './endpoints/admin/system-webhook/create.js';
export * as 'admin/system-webhook/delete' from './endpoints/admin/system-webhook/delete.js';
export * as 'admin/system-webhook/list' from './endpoints/admin/system-webhook/list.js';
export * as 'admin/system-webhook/show' from './endpoints/admin/system-webhook/show.js';
export * as 'admin/system-webhook/test' from './endpoints/admin/system-webhook/test.js';
export * as 'admin/system-webhook/update' from './endpoints/admin/system-webhook/update.js';
export * as 'admin/unnsfw-user' from './endpoints/admin/unnsfw-user.js';
export * as 'admin/unset-user-avatar' from './endpoints/admin/unset-user-avatar.js';
export * as 'admin/unset-user-banner' from './endpoints/admin/unset-user-banner.js';
export * as 'admin/unsilence-user' from './endpoints/admin/unsilence-user.js';
export * as 'admin/unsuspend-user' from './endpoints/admin/unsuspend-user.js';
export * as 'admin/update-abuse-user-report' from './endpoints/admin/update-abuse-user-report.js';
export * as 'admin/update-meta' from './endpoints/admin/update-meta.js';
export * as 'admin/update-user-note' from './endpoints/admin/update-user-note.js';
export * as 'announcements' from './endpoints/announcements.js';
export * as 'announcements/show' from './endpoints/announcements/show.js';
export * as 'antennas/create' from './endpoints/antennas/create.js';
export * as 'antennas/delete' from './endpoints/antennas/delete.js';
export * as 'antennas/list' from './endpoints/antennas/list.js';
export * as 'antennas/notes' from './endpoints/antennas/notes.js';
export * as 'antennas/show' from './endpoints/antennas/show.js';
export * as 'antennas/update' from './endpoints/antennas/update.js';
export * as 'ap/get' from './endpoints/ap/get.js';
export * as 'ap/show' from './endpoints/ap/show.js';
export * as 'app/create' from './endpoints/app/create.js';
export * as 'app/show' from './endpoints/app/show.js';
export * as 'auth/accept' from './endpoints/auth/accept.js';
export * as 'auth/session/generate' from './endpoints/auth/session/generate.js';
export * as 'auth/session/show' from './endpoints/auth/session/show.js';
export * as 'auth/session/userkey' from './endpoints/auth/session/userkey.js';
export * as 'blocking/create' from './endpoints/blocking/create.js';
export * as 'blocking/delete' from './endpoints/blocking/delete.js';
export * as 'blocking/list' from './endpoints/blocking/list.js';
export * as 'bubble-game/ranking' from './endpoints/bubble-game/ranking.js';
export * as 'bubble-game/register' from './endpoints/bubble-game/register.js';
export * as 'channels/create' from './endpoints/channels/create.js';
export * as 'channels/favorite' from './endpoints/channels/favorite.js';
export * as 'channels/featured' from './endpoints/channels/featured.js';
export * as 'channels/follow' from './endpoints/channels/follow.js';
export * as 'channels/followed' from './endpoints/channels/followed.js';
export * as 'channels/my-favorites' from './endpoints/channels/my-favorites.js';
export * as 'channels/owned' from './endpoints/channels/owned.js';
export * as 'channels/search' from './endpoints/channels/search.js';
export * as 'channels/show' from './endpoints/channels/show.js';
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 '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';
export * as 'charts/federation' from './endpoints/charts/federation.js';
export * as 'charts/instance' from './endpoints/charts/instance.js';
export * as 'charts/notes' from './endpoints/charts/notes.js';
export * as 'charts/user/drive' from './endpoints/charts/user/drive.js';
export * as 'charts/user/following' from './endpoints/charts/user/following.js';
export * as 'charts/user/notes' from './endpoints/charts/user/notes.js';
export * as 'charts/user/pv' from './endpoints/charts/user/pv.js';
export * as 'charts/user/reactions' from './endpoints/charts/user/reactions.js';
export * as 'charts/users' from './endpoints/charts/users.js';
export * as 'clips/add-note' from './endpoints/clips/add-note.js';
export * as 'clips/create' from './endpoints/clips/create.js';
export * as 'clips/delete' from './endpoints/clips/delete.js';
export * as 'clips/favorite' from './endpoints/clips/favorite.js';
export * as 'clips/list' from './endpoints/clips/list.js';
export * as 'clips/my-favorites' from './endpoints/clips/my-favorites.js';
export * as 'clips/notes' from './endpoints/clips/notes.js';
export * as 'clips/remove-note' from './endpoints/clips/remove-note.js';
export * as 'clips/show' from './endpoints/clips/show.js';
export * as 'clips/unfavorite' from './endpoints/clips/unfavorite.js';
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/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';
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/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';
export * as 'drive/folders/delete' from './endpoints/drive/folders/delete.js';
export * as 'drive/folders/find' from './endpoints/drive/folders/find.js';
export * as 'drive/folders/show' from './endpoints/drive/folders/show.js';
export * as 'drive/folders/update' from './endpoints/drive/folders/update.js';
export * as 'drive/stream' from './endpoints/drive/stream.js';
export * as 'email-address/available' from './endpoints/email-address/available.js';
export * as 'emoji' from './endpoints/emoji.js';
export * as 'emojis' from './endpoints/emojis.js';
export * as 'endpoint' from './endpoints/endpoint.js';
export * as 'endpoints' from './endpoints/endpoints.js';
export * as 'export-custom-emojis' from './endpoints/export-custom-emojis.js';
export * as 'federation/followers' from './endpoints/federation/followers.js';
export * as 'federation/following' from './endpoints/federation/following.js';
export * as 'federation/instances' from './endpoints/federation/instances.js';
export * as 'federation/show-instance' from './endpoints/federation/show-instance.js';
export * as 'federation/stats' from './endpoints/federation/stats.js';
export * as 'federation/update-remote-user' from './endpoints/federation/update-remote-user.js';
export * as 'federation/users' from './endpoints/federation/users.js';
export * as 'fetch-external-resources' from './endpoints/fetch-external-resources.js';
export * as 'fetch-rss' from './endpoints/fetch-rss.js';
export * as 'flash/create' from './endpoints/flash/create.js';
export * as 'flash/delete' from './endpoints/flash/delete.js';
export * as 'flash/featured' from './endpoints/flash/featured.js';
export * as 'flash/like' from './endpoints/flash/like.js';
export * as 'flash/my' from './endpoints/flash/my.js';
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 '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';
export * as 'following/requests/accept' from './endpoints/following/requests/accept.js';
export * as 'following/requests/cancel' from './endpoints/following/requests/cancel.js';
export * as 'following/requests/list' from './endpoints/following/requests/list.js';
export * as 'following/requests/reject' from './endpoints/following/requests/reject.js';
export * as 'following/requests/sent' from './endpoints/following/requests/sent.js';
export * as 'following/update' from './endpoints/following/update.js';
export * as 'following/update-all' from './endpoints/following/update-all.js';
export * as 'gallery/featured' from './endpoints/gallery/featured.js';
export * as 'gallery/popular' from './endpoints/gallery/popular.js';
export * as 'gallery/posts' from './endpoints/gallery/posts.js';
export * as 'gallery/posts/create' from './endpoints/gallery/posts/create.js';
export * as 'gallery/posts/delete' from './endpoints/gallery/posts/delete.js';
export * as 'gallery/posts/like' from './endpoints/gallery/posts/like.js';
export * as 'gallery/posts/show' from './endpoints/gallery/posts/show.js';
export * as 'gallery/posts/unlike' from './endpoints/gallery/posts/unlike.js';
export * as 'gallery/posts/update' from './endpoints/gallery/posts/update.js';
export * as 'get-avatar-decorations' from './endpoints/get-avatar-decorations.js';
export * as 'get-online-users-count' from './endpoints/get-online-users-count.js';
export * as 'hashtags/list' from './endpoints/hashtags/list.js';
export * as 'hashtags/search' from './endpoints/hashtags/search.js';
export * as 'hashtags/show' from './endpoints/hashtags/show.js';
export * as 'hashtags/trend' from './endpoints/hashtags/trend.js';
export * as 'hashtags/users' from './endpoints/hashtags/users.js';
export * as 'i' from './endpoints/i.js';
export * as 'i/2fa/done' from './endpoints/i/2fa/done.js';
export * as 'i/2fa/key-done' from './endpoints/i/2fa/key-done.js';
export * as 'i/2fa/password-less' from './endpoints/i/2fa/password-less.js';
export * as 'i/2fa/register' from './endpoints/i/2fa/register.js';
export * as 'i/2fa/register-key' from './endpoints/i/2fa/register-key.js';
export * as 'i/2fa/remove-key' from './endpoints/i/2fa/remove-key.js';
export * as 'i/2fa/unregister' from './endpoints/i/2fa/unregister.js';
export * as 'i/2fa/update-key' from './endpoints/i/2fa/update-key.js';
export * as 'i/apps' from './endpoints/i/apps.js';
export * as 'i/authorized-apps' from './endpoints/i/authorized-apps.js';
export * as 'i/change-password' from './endpoints/i/change-password.js';
export * as 'i/claim-achievement' from './endpoints/i/claim-achievement.js';
export * as 'i/delete-account' from './endpoints/i/delete-account.js';
export * as 'i/export-antennas' from './endpoints/i/export-antennas.js';
export * as 'i/export-blocking' from './endpoints/i/export-blocking.js';
export * as 'i/export-clips' from './endpoints/i/export-clips.js';
export * as 'i/export-data' from './endpoints/i/export-data.js';
export * as 'i/export-favorites' from './endpoints/i/export-favorites.js';
export * as 'i/export-following' from './endpoints/i/export-following.js';
export * as 'i/export-mute' from './endpoints/i/export-mute.js';
export * as 'i/export-notes' from './endpoints/i/export-notes.js';
export * as 'i/export-user-lists' from './endpoints/i/export-user-lists.js';
export * as 'i/favorites' from './endpoints/i/favorites.js';
export * as 'i/gallery/likes' from './endpoints/i/gallery/likes.js';
export * as 'i/gallery/posts' from './endpoints/i/gallery/posts.js';
export * as 'i/import-antennas' from './endpoints/i/import-antennas.js';
export * as 'i/import-blocking' from './endpoints/i/import-blocking.js';
export * as 'i/import-following' from './endpoints/i/import-following.js';
export * as 'i/import-muting' from './endpoints/i/import-muting.js';
export * as 'i/import-notes' from './endpoints/i/import-notes.js';
export * as 'i/import-user-lists' from './endpoints/i/import-user-lists.js';
export * as 'i/move' from './endpoints/i/move.js';
export * as 'i/notifications' from './endpoints/i/notifications.js';
export * as 'i/notifications-grouped' from './endpoints/i/notifications-grouped.js';
export * as 'i/page-likes' from './endpoints/i/page-likes.js';
export * as 'i/pages' from './endpoints/i/pages.js';
export * as 'i/pin' from './endpoints/i/pin.js';
export * as 'i/read-all-unread-notes' from './endpoints/i/read-all-unread-notes.js';
export * as 'i/read-announcement' from './endpoints/i/read-announcement.js';
export * as 'i/regenerate-token' from './endpoints/i/regenerate-token.js';
export * as 'i/registry/get' from './endpoints/i/registry/get.js';
export * as 'i/registry/get-all' from './endpoints/i/registry/get-all.js';
export * as 'i/registry/get-detail' from './endpoints/i/registry/get-detail.js';
export * as 'i/registry/get-unsecure' from './endpoints/i/registry/get-unsecure.js';
export * as 'i/registry/keys' from './endpoints/i/registry/keys.js';
export * as 'i/registry/keys-with-type' from './endpoints/i/registry/keys-with-type.js';
export * as 'i/registry/remove' from './endpoints/i/registry/remove.js';
export * as 'i/registry/scopes-with-domain' from './endpoints/i/registry/scopes-with-domain.js';
export * as 'i/registry/set' from './endpoints/i/registry/set.js';
export * as 'i/revoke-token' from './endpoints/i/revoke-token.js';
export * as 'i/signin-history' from './endpoints/i/signin-history.js';
export * as 'i/unpin' from './endpoints/i/unpin.js';
export * as 'i/update' from './endpoints/i/update.js';
export * as 'i/update-email' from './endpoints/i/update-email.js';
export * as 'i/webhooks/create' from './endpoints/i/webhooks/create.js';
export * as 'i/webhooks/delete' from './endpoints/i/webhooks/delete.js';
export * as 'i/webhooks/list' from './endpoints/i/webhooks/list.js';
export * as 'i/webhooks/show' from './endpoints/i/webhooks/show.js';
export * as 'i/webhooks/test' from './endpoints/i/webhooks/test.js';
export * as 'i/webhooks/update' from './endpoints/i/webhooks/update.js';
export * as 'invite/create' from './endpoints/invite/create.js';
export * as 'invite/delete' from './endpoints/invite/delete.js';
export * as 'invite/limit' from './endpoints/invite/limit.js';
export * as 'invite/list' from './endpoints/invite/list.js';
export * as 'meta' from './endpoints/meta.js';
export * as 'miauth/gen-token' from './endpoints/miauth/gen-token.js';
export * as 'mute/create' from './endpoints/mute/create.js';
export * as 'mute/delete' from './endpoints/mute/delete.js';
export * as 'mute/list' from './endpoints/mute/list.js';
export * as 'my/apps' from './endpoints/my/apps.js';
export * as 'notes' from './endpoints/notes.js';
export * as 'notes/bubble-timeline' from './endpoints/notes/bubble-timeline.js';
export * as 'notes/children' from './endpoints/notes/children.js';
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/edit' from './endpoints/notes/edit.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';
export * as 'notes/following' from './endpoints/notes/following.js';
export * as 'notes/global-timeline' from './endpoints/notes/global-timeline.js';
export * as 'notes/hybrid-timeline' from './endpoints/notes/hybrid-timeline.js';
export * as 'notes/like' from './endpoints/notes/like.js';
export * as 'notes/local-timeline' from './endpoints/notes/local-timeline.js';
export * as 'notes/mentions' from './endpoints/notes/mentions.js';
export * as 'notes/polls/recommendation' from './endpoints/notes/polls/recommendation.js';
export * as 'notes/polls/refresh' from './endpoints/notes/polls/refresh.js';
export * as 'notes/polls/vote' from './endpoints/notes/polls/vote.js';
export * as 'notes/reactions' from './endpoints/notes/reactions.js';
export * as 'notes/reactions/create' from './endpoints/notes/reactions/create.js';
export * as 'notes/reactions/delete' from './endpoints/notes/reactions/delete.js';
export * as 'notes/renotes' from './endpoints/notes/renotes.js';
export * as 'notes/replies' from './endpoints/notes/replies.js';
export * as 'notes/search' from './endpoints/notes/search.js';
export * as 'notes/schedule/create' from './endpoints/notes/schedule/create.js';
export * as 'notes/schedule/delete' from './endpoints/notes/schedule/delete.js';
export * as 'notes/schedule/list' from './endpoints/notes/schedule/list.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/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';
export * as 'notes/timeline' from './endpoints/notes/timeline.js';
export * as 'notes/translate' from './endpoints/notes/translate.js';
export * as 'notes/unrenote' from './endpoints/notes/unrenote.js';
export * as 'notes/user-list-timeline' from './endpoints/notes/user-list-timeline.js';
export * as 'notes/versions' from './endpoints/notes/versions.js';
export * as 'notifications/create' from './endpoints/notifications/create.js';
export * as 'notifications/flush' from './endpoints/notifications/flush.js';
export * as 'notifications/mark-all-as-read' from './endpoints/notifications/mark-all-as-read.js';
export * as 'notifications/test-notification' from './endpoints/notifications/test-notification.js';
export * as 'page-push' from './endpoints/page-push.js';
export * as 'pages/create' from './endpoints/pages/create.js';
export * as 'pages/delete' from './endpoints/pages/delete.js';
export * as 'pages/featured' from './endpoints/pages/featured.js';
export * as 'pages/like' from './endpoints/pages/like.js';
export * as 'pages/show' from './endpoints/pages/show.js';
export * as 'pages/unlike' from './endpoints/pages/unlike.js';
export * as 'pages/update' from './endpoints/pages/update.js';
export * as 'ping' from './endpoints/ping.js';
export * as 'pinned-users' from './endpoints/pinned-users.js';
export * as 'promo/read' from './endpoints/promo/read.js';
export * as 'renote-mute/create' from './endpoints/renote-mute/create.js';
export * as 'renote-mute/delete' from './endpoints/renote-mute/delete.js';
export * as 'renote-mute/list' from './endpoints/renote-mute/list.js';
export * as 'request-reset-password' from './endpoints/request-reset-password.js';
export * as 'reset-db' from './endpoints/reset-db.js';
export * as 'reset-password' from './endpoints/reset-password.js';
export * as 'retention' from './endpoints/retention.js';
export * as 'reversi/cancel-match' from './endpoints/reversi/cancel-match.js';
export * as 'reversi/games' from './endpoints/reversi/games.js';
export * as 'reversi/invitations' from './endpoints/reversi/invitations.js';
export * as 'reversi/match' from './endpoints/reversi/match.js';
export * as 'reversi/show-game' from './endpoints/reversi/show-game.js';
export * as 'reversi/surrender' from './endpoints/reversi/surrender.js';
export * as 'reversi/verify' from './endpoints/reversi/verify.js';
export * as 'roles/list' from './endpoints/roles/list.js';
export * as 'roles/notes' from './endpoints/roles/notes.js';
export * as 'roles/show' from './endpoints/roles/show.js';
export * as 'roles/users' from './endpoints/roles/users.js';
export * as 'server-info' from './endpoints/server-info.js';
export * as 'sponsors' from './endpoints/sponsors.js';
export * as 'stats' from './endpoints/stats.js';
export * as 'sw/register' from './endpoints/sw/register.js';
export * as 'sw/show-registration' from './endpoints/sw/show-registration.js';
export * as 'sw/unregister' from './endpoints/sw/unregister.js';
export * as 'sw/update-registration' from './endpoints/sw/update-registration.js';
export * as 'test' from './endpoints/test.js';
export * as 'username/available' from './endpoints/username/available.js';
export * as 'users' from './endpoints/users.js';
export * as 'users/achievements' from './endpoints/users/achievements.js';
export * as 'users/clips' from './endpoints/users/clips.js';
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/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';
export * as 'users/lists/create-from-public' from './endpoints/users/lists/create-from-public.js';
export * as 'users/lists/delete' from './endpoints/users/lists/delete.js';
export * as 'users/lists/favorite' from './endpoints/users/lists/favorite.js';
export * as 'users/lists/get-memberships' from './endpoints/users/lists/get-memberships.js';
export * as 'users/lists/list' from './endpoints/users/lists/list.js';
export * as 'users/lists/pull' from './endpoints/users/lists/pull.js';
export * as 'users/lists/push' from './endpoints/users/lists/push.js';
export * as 'users/lists/show' from './endpoints/users/lists/show.js';
export * as 'users/lists/unfavorite' from './endpoints/users/lists/unfavorite.js';
export * as 'users/lists/update' from './endpoints/users/lists/update.js';
export * as 'users/lists/update-membership' from './endpoints/users/lists/update-membership.js';
export * as 'users/notes' from './endpoints/users/notes.js';
export * as 'users/pages' from './endpoints/users/pages.js';
export * as 'users/reactions' from './endpoints/users/reactions.js';
export * as 'users/recommendation' from './endpoints/users/recommendation.js';
export * as 'users/relation' from './endpoints/users/relation.js';
export * as 'users/report-abuse' from './endpoints/users/report-abuse.js';
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 'v2/admin/emoji/list' from './endpoints/v2/admin/emoji/list.js';

View file

@ -7,821 +7,7 @@ import { permissions } from 'misskey-js';
import type { KeyOf, Schema } from '@/misc/json-schema.js';
import type { RateLimit } from '@/misc/rate-limit-utils.js';
import * as ep___admin_abuseReport_notificationRecipient_list
from '@/server/api/endpoints/admin/abuse-report/notification-recipient/list.js';
import * as ep___admin_abuseReport_notificationRecipient_show
from '@/server/api/endpoints/admin/abuse-report/notification-recipient/show.js';
import * as ep___admin_abuseReport_notificationRecipient_create
from '@/server/api/endpoints/admin/abuse-report/notification-recipient/create.js';
import * as ep___admin_abuseReport_notificationRecipient_update
from '@/server/api/endpoints/admin/abuse-report/notification-recipient/update.js';
import * as ep___admin_abuseReport_notificationRecipient_delete
from '@/server/api/endpoints/admin/abuse-report/notification-recipient/delete.js';
import * as ep___admin_abuseUserReports from './endpoints/admin/abuse-user-reports.js';
import * as ep___admin_meta from './endpoints/admin/meta.js';
import * as ep___admin_accounts_create from './endpoints/admin/accounts/create.js';
import * as ep___admin_accounts_delete from './endpoints/admin/accounts/delete.js';
import * as ep___admin_accounts_findByEmail from './endpoints/admin/accounts/find-by-email.js';
import * as ep___admin_ad_create from './endpoints/admin/ad/create.js';
import * as ep___admin_ad_delete from './endpoints/admin/ad/delete.js';
import * as ep___admin_ad_list from './endpoints/admin/ad/list.js';
import * as ep___admin_ad_update from './endpoints/admin/ad/update.js';
import * as ep___admin_announcements_create from './endpoints/admin/announcements/create.js';
import * as ep___admin_announcements_delete from './endpoints/admin/announcements/delete.js';
import * as ep___admin_announcements_list from './endpoints/admin/announcements/list.js';
import * as ep___admin_announcements_update from './endpoints/admin/announcements/update.js';
import * as ep___admin_avatarDecorations_create from './endpoints/admin/avatar-decorations/create.js';
import * as ep___admin_avatarDecorations_delete from './endpoints/admin/avatar-decorations/delete.js';
import * as ep___admin_avatarDecorations_list from './endpoints/admin/avatar-decorations/list.js';
import * as ep___admin_avatarDecorations_update from './endpoints/admin/avatar-decorations/update.js';
import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js';
import * as ep___admin_unsetUserAvatar from './endpoints/admin/unset-user-avatar.js';
import * as ep___admin_unsetUserBanner from './endpoints/admin/unset-user-banner.js';
import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js';
import * as ep___admin_drive_files from './endpoints/admin/drive/files.js';
import * as ep___admin_drive_showFile from './endpoints/admin/drive/show-file.js';
import * as ep___admin_emoji_addAliasesBulk from './endpoints/admin/emoji/add-aliases-bulk.js';
import * as ep___admin_emoji_add from './endpoints/admin/emoji/add.js';
import * as ep___admin_emoji_copy from './endpoints/admin/emoji/copy.js';
import * as ep___admin_emoji_deleteBulk from './endpoints/admin/emoji/delete-bulk.js';
import * as ep___admin_emoji_delete from './endpoints/admin/emoji/delete.js';
import * as ep___admin_emoji_importZip from './endpoints/admin/emoji/import-zip.js';
import * as ep___admin_emoji_listRemote from './endpoints/admin/emoji/list-remote.js';
import * as ep___admin_emoji_list from './endpoints/admin/emoji/list.js';
import * as ep___admin_emoji_removeAliasesBulk from './endpoints/admin/emoji/remove-aliases-bulk.js';
import * as ep___admin_emoji_setAliasesBulk from './endpoints/admin/emoji/set-aliases-bulk.js';
import * as ep___admin_emoji_setCategoryBulk from './endpoints/admin/emoji/set-category-bulk.js';
import * as ep___admin_emoji_setLicenseBulk from './endpoints/admin/emoji/set-license-bulk.js';
import * as ep___admin_emoji_update from './endpoints/admin/emoji/update.js';
import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federation/delete-all-files.js';
import * as ep___admin_federation_refreshRemoteInstanceMetadata
from './endpoints/admin/federation/refresh-remote-instance-metadata.js';
import * as ep___admin_federation_removeAllFollowing from './endpoints/admin/federation/remove-all-following.js';
import * as ep___admin_federation_updateInstance from './endpoints/admin/federation/update-instance.js';
import * as ep___admin_getIndexStats from './endpoints/admin/get-index-stats.js';
import * as ep___admin_getTableStats from './endpoints/admin/get-table-stats.js';
import * as ep___admin_getUserIps from './endpoints/admin/get-user-ips.js';
import * as ep___admin_invite_create from './endpoints/admin/invite/create.js';
import * as ep___admin_invite_list from './endpoints/admin/invite/list.js';
import * as ep___admin_promo_create from './endpoints/admin/promo/create.js';
import * as ep___admin_queue_clear from './endpoints/admin/queue/clear.js';
import * as ep___admin_queue_deliverDelayed from './endpoints/admin/queue/deliver-delayed.js';
import * as ep___admin_queue_inboxDelayed from './endpoints/admin/queue/inbox-delayed.js';
import * as ep___admin_queue_promote from './endpoints/admin/queue/promote.js';
import * as ep___admin_queue_stats from './endpoints/admin/queue/stats.js';
import * as ep___admin_relays_add from './endpoints/admin/relays/add.js';
import * as ep___admin_relays_list from './endpoints/admin/relays/list.js';
import * as ep___admin_relays_remove from './endpoints/admin/relays/remove.js';
import * as ep___admin_resetPassword from './endpoints/admin/reset-password.js';
import * as ep___admin_resolveAbuseUserReport from './endpoints/admin/resolve-abuse-user-report.js';
import * as ep___admin_forwardAbuseUserReport from './endpoints/admin/forward-abuse-user-report.js';
import * as ep___admin_updateAbuseUserReport from './endpoints/admin/update-abuse-user-report.js';
import * as ep___admin_sendEmail from './endpoints/admin/send-email.js';
import * as ep___admin_serverInfo from './endpoints/admin/server-info.js';
import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js';
import * as ep___admin_showUser from './endpoints/admin/show-user.js';
import * as ep___admin_showUsers from './endpoints/admin/show-users.js';
import * as ep___admin_nsfwUser from './endpoints/admin/nsfw-user.js';
import * as ep___admin_unnsfwUser from './endpoints/admin/unnsfw-user.js';
import * as ep___admin_silenceUser from './endpoints/admin/silence-user.js';
import * as ep___admin_unsilenceUser from './endpoints/admin/unsilence-user.js';
import * as ep___admin_suspendUser from './endpoints/admin/suspend-user.js';
import * as ep___admin_approveUser from './endpoints/admin/approve-user.js';
import * as ep___admin_declineUser from './endpoints/admin/decline-user.js';
import * as ep___admin_unsuspendUser from './endpoints/admin/unsuspend-user.js';
import * as ep___admin_updateMeta from './endpoints/admin/update-meta.js';
import * as ep___admin_deleteAccount from './endpoints/admin/delete-account.js';
import * as ep___admin_updateUserNote from './endpoints/admin/update-user-note.js';
import * as ep___admin_roles_create from './endpoints/admin/roles/create.js';
import * as ep___admin_roles_delete from './endpoints/admin/roles/delete.js';
import * as ep___admin_roles_list from './endpoints/admin/roles/list.js';
import * as ep___admin_roles_show from './endpoints/admin/roles/show.js';
import * as ep___admin_roles_update from './endpoints/admin/roles/update.js';
import * as ep___admin_roles_assign from './endpoints/admin/roles/assign.js';
import * as ep___admin_roles_unassign from './endpoints/admin/roles/unassign.js';
import * as ep___admin_roles_updateDefaultPolicies from './endpoints/admin/roles/update-default-policies.js';
import * as ep___admin_roles_users from './endpoints/admin/roles/users.js';
import * as ep___admin_systemWebhook_create from './endpoints/admin/system-webhook/create.js';
import * as ep___admin_systemWebhook_delete from './endpoints/admin/system-webhook/delete.js';
import * as ep___admin_systemWebhook_list from './endpoints/admin/system-webhook/list.js';
import * as ep___admin_systemWebhook_show from './endpoints/admin/system-webhook/show.js';
import * as ep___admin_systemWebhook_update from './endpoints/admin/system-webhook/update.js';
import * as ep___admin_systemWebhook_test from './endpoints/admin/system-webhook/test.js';
import * as ep___announcements from './endpoints/announcements.js';
import * as ep___announcements_show from './endpoints/announcements/show.js';
import * as ep___antennas_create from './endpoints/antennas/create.js';
import * as ep___antennas_delete from './endpoints/antennas/delete.js';
import * as ep___antennas_list from './endpoints/antennas/list.js';
import * as ep___antennas_notes from './endpoints/antennas/notes.js';
import * as ep___antennas_show from './endpoints/antennas/show.js';
import * as ep___antennas_update from './endpoints/antennas/update.js';
import * as ep___ap_get from './endpoints/ap/get.js';
import * as ep___ap_show from './endpoints/ap/show.js';
import * as ep___app_create from './endpoints/app/create.js';
import * as ep___app_show from './endpoints/app/show.js';
import * as ep___auth_accept from './endpoints/auth/accept.js';
import * as ep___auth_session_generate from './endpoints/auth/session/generate.js';
import * as ep___auth_session_show from './endpoints/auth/session/show.js';
import * as ep___auth_session_userkey from './endpoints/auth/session/userkey.js';
import * as ep___blocking_create from './endpoints/blocking/create.js';
import * as ep___blocking_delete from './endpoints/blocking/delete.js';
import * as ep___blocking_list from './endpoints/blocking/list.js';
import * as ep___channels_create from './endpoints/channels/create.js';
import * as ep___channels_featured from './endpoints/channels/featured.js';
import * as ep___channels_follow from './endpoints/channels/follow.js';
import * as ep___channels_followed from './endpoints/channels/followed.js';
import * as ep___channels_owned from './endpoints/channels/owned.js';
import * as ep___channels_show from './endpoints/channels/show.js';
import * as ep___channels_timeline from './endpoints/channels/timeline.js';
import * as ep___channels_unfollow from './endpoints/channels/unfollow.js';
import * as ep___channels_update from './endpoints/channels/update.js';
import * as ep___channels_favorite from './endpoints/channels/favorite.js';
import * as ep___channels_unfavorite from './endpoints/channels/unfavorite.js';
import * as ep___channels_myFavorites from './endpoints/channels/my-favorites.js';
import * as ep___channels_search from './endpoints/channels/search.js';
import * as ep___charts_activeUsers from './endpoints/charts/active-users.js';
import * as ep___charts_apRequest from './endpoints/charts/ap-request.js';
import * as ep___charts_drive from './endpoints/charts/drive.js';
import * as ep___charts_federation from './endpoints/charts/federation.js';
import * as ep___charts_instance from './endpoints/charts/instance.js';
import * as ep___charts_notes from './endpoints/charts/notes.js';
import * as ep___charts_user_drive from './endpoints/charts/user/drive.js';
import * as ep___charts_user_following from './endpoints/charts/user/following.js';
import * as ep___charts_user_notes from './endpoints/charts/user/notes.js';
import * as ep___charts_user_pv from './endpoints/charts/user/pv.js';
import * as ep___charts_user_reactions from './endpoints/charts/user/reactions.js';
import * as ep___charts_users from './endpoints/charts/users.js';
import * as ep___clips_addNote from './endpoints/clips/add-note.js';
import * as ep___clips_removeNote from './endpoints/clips/remove-note.js';
import * as ep___clips_create from './endpoints/clips/create.js';
import * as ep___clips_delete from './endpoints/clips/delete.js';
import * as ep___clips_list from './endpoints/clips/list.js';
import * as ep___clips_notes from './endpoints/clips/notes.js';
import * as ep___clips_show from './endpoints/clips/show.js';
import * as ep___clips_update from './endpoints/clips/update.js';
import * as ep___clips_favorite from './endpoints/clips/favorite.js';
import * as ep___clips_unfavorite from './endpoints/clips/unfavorite.js';
import * as ep___clips_myFavorites from './endpoints/clips/my-favorites.js';
import * as ep___drive from './endpoints/drive.js';
import * as ep___drive_files from './endpoints/drive/files.js';
import * as ep___drive_files_attachedNotes from './endpoints/drive/files/attached-notes.js';
import * as ep___drive_files_checkExistence from './endpoints/drive/files/check-existence.js';
import * as ep___drive_files_create from './endpoints/drive/files/create.js';
import * as ep___drive_files_delete from './endpoints/drive/files/delete.js';
import * as ep___drive_files_findByHash from './endpoints/drive/files/find-by-hash.js';
import * as ep___drive_files_find from './endpoints/drive/files/find.js';
import * as ep___drive_files_show from './endpoints/drive/files/show.js';
import * as ep___drive_files_update from './endpoints/drive/files/update.js';
import * as ep___drive_files_uploadFromUrl from './endpoints/drive/files/upload-from-url.js';
import * as ep___drive_folders from './endpoints/drive/folders.js';
import * as ep___drive_folders_create from './endpoints/drive/folders/create.js';
import * as ep___drive_folders_delete from './endpoints/drive/folders/delete.js';
import * as ep___drive_folders_find from './endpoints/drive/folders/find.js';
import * as ep___drive_folders_show from './endpoints/drive/folders/show.js';
import * as ep___drive_folders_update from './endpoints/drive/folders/update.js';
import * as ep___drive_stream from './endpoints/drive/stream.js';
import * as ep___emailAddress_available from './endpoints/email-address/available.js';
import * as ep___endpoint from './endpoints/endpoint.js';
import * as ep___endpoints from './endpoints/endpoints.js';
import * as ep___exportCustomEmojis from './endpoints/export-custom-emojis.js';
import * as ep___federation_followers from './endpoints/federation/followers.js';
import * as ep___federation_following from './endpoints/federation/following.js';
import * as ep___federation_instances from './endpoints/federation/instances.js';
import * as ep___federation_showInstance from './endpoints/federation/show-instance.js';
import * as ep___federation_updateRemoteUser from './endpoints/federation/update-remote-user.js';
import * as ep___federation_users from './endpoints/federation/users.js';
import * as ep___federation_stats from './endpoints/federation/stats.js';
import * as ep___following_create from './endpoints/following/create.js';
import * as ep___following_delete from './endpoints/following/delete.js';
import * as ep___following_update from './endpoints/following/update.js';
import * as ep___following_update_all from './endpoints/following/update-all.js';
import * as ep___following_invalidate from './endpoints/following/invalidate.js';
import * as ep___following_requests_accept from './endpoints/following/requests/accept.js';
import * as ep___following_requests_cancel from './endpoints/following/requests/cancel.js';
import * as ep___following_requests_list from './endpoints/following/requests/list.js';
import * as ep___following_requests_sent from './endpoints/following/requests/sent.js';
import * as ep___following_requests_reject from './endpoints/following/requests/reject.js';
import * as ep___gallery_featured from './endpoints/gallery/featured.js';
import * as ep___gallery_popular from './endpoints/gallery/popular.js';
import * as ep___gallery_posts from './endpoints/gallery/posts.js';
import * as ep___gallery_posts_create from './endpoints/gallery/posts/create.js';
import * as ep___gallery_posts_delete from './endpoints/gallery/posts/delete.js';
import * as ep___gallery_posts_like from './endpoints/gallery/posts/like.js';
import * as ep___gallery_posts_show from './endpoints/gallery/posts/show.js';
import * as ep___gallery_posts_unlike from './endpoints/gallery/posts/unlike.js';
import * as ep___gallery_posts_update from './endpoints/gallery/posts/update.js';
import * as ep___getOnlineUsersCount from './endpoints/get-online-users-count.js';
import * as ep___getAvatarDecorations from './endpoints/get-avatar-decorations.js';
import * as ep___hashtags_list from './endpoints/hashtags/list.js';
import * as ep___hashtags_search from './endpoints/hashtags/search.js';
import * as ep___hashtags_show from './endpoints/hashtags/show.js';
import * as ep___hashtags_trend from './endpoints/hashtags/trend.js';
import * as ep___hashtags_users from './endpoints/hashtags/users.js';
import * as ep___i from './endpoints/i.js';
import * as ep___i_2fa_done from './endpoints/i/2fa/done.js';
import * as ep___i_2fa_keyDone from './endpoints/i/2fa/key-done.js';
import * as ep___i_2fa_passwordLess from './endpoints/i/2fa/password-less.js';
import * as ep___i_2fa_registerKey from './endpoints/i/2fa/register-key.js';
import * as ep___i_2fa_register from './endpoints/i/2fa/register.js';
import * as ep___i_2fa_updateKey from './endpoints/i/2fa/update-key.js';
import * as ep___i_2fa_removeKey from './endpoints/i/2fa/remove-key.js';
import * as ep___i_2fa_unregister from './endpoints/i/2fa/unregister.js';
import * as ep___i_apps from './endpoints/i/apps.js';
import * as ep___i_authorizedApps from './endpoints/i/authorized-apps.js';
import * as ep___i_claimAchievement from './endpoints/i/claim-achievement.js';
import * as ep___i_changePassword from './endpoints/i/change-password.js';
import * as ep___i_deleteAccount from './endpoints/i/delete-account.js';
import * as ep___i_exportData from './endpoints/i/export-data.js';
import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js';
import * as ep___i_exportFollowing from './endpoints/i/export-following.js';
import * as ep___i_exportMute from './endpoints/i/export-mute.js';
import * as ep___i_exportNotes from './endpoints/i/export-notes.js';
import * as ep___i_exportClips from './endpoints/i/export-clips.js';
import * as ep___i_exportFavorites from './endpoints/i/export-favorites.js';
import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js';
import * as ep___i_exportAntennas from './endpoints/i/export-antennas.js';
import * as ep___i_favorites from './endpoints/i/favorites.js';
import * as ep___i_gallery_likes from './endpoints/i/gallery/likes.js';
import * as ep___i_gallery_posts from './endpoints/i/gallery/posts.js';
import * as ep___i_importBlocking from './endpoints/i/import-blocking.js';
import * as ep___i_importFollowing from './endpoints/i/import-following.js';
import * as ep___i_importNotes from './endpoints/i/import-notes.js';
import * as ep___i_importMuting from './endpoints/i/import-muting.js';
import * as ep___i_importUserLists from './endpoints/i/import-user-lists.js';
import * as ep___i_importAntennas from './endpoints/i/import-antennas.js';
import * as ep___i_notifications from './endpoints/i/notifications.js';
import * as ep___i_notificationsGrouped from './endpoints/i/notifications-grouped.js';
import * as ep___i_pageLikes from './endpoints/i/page-likes.js';
import * as ep___i_pages from './endpoints/i/pages.js';
import * as ep___i_pin from './endpoints/i/pin.js';
import * as ep___i_readAllUnreadNotes from './endpoints/i/read-all-unread-notes.js';
import * as ep___i_readAnnouncement from './endpoints/i/read-announcement.js';
import * as ep___i_regenerateToken from './endpoints/i/regenerate-token.js';
import * as ep___i_registry_getAll from './endpoints/i/registry/get-all.js';
import * as ep___i_registry_getUnsecure from './endpoints/i/registry/get-unsecure.js';
import * as ep___i_registry_getDetail from './endpoints/i/registry/get-detail.js';
import * as ep___i_registry_get from './endpoints/i/registry/get.js';
import * as ep___i_registry_keysWithType from './endpoints/i/registry/keys-with-type.js';
import * as ep___i_registry_keys from './endpoints/i/registry/keys.js';
import * as ep___i_registry_remove from './endpoints/i/registry/remove.js';
import * as ep___i_registry_scopesWithDomain from './endpoints/i/registry/scopes-with-domain.js';
import * as ep___i_registry_set from './endpoints/i/registry/set.js';
import * as ep___i_revokeToken from './endpoints/i/revoke-token.js';
import * as ep___i_signinHistory from './endpoints/i/signin-history.js';
import * as ep___i_unpin from './endpoints/i/unpin.js';
import * as ep___i_updateEmail from './endpoints/i/update-email.js';
import * as ep___i_update from './endpoints/i/update.js';
import * as ep___i_move from './endpoints/i/move.js';
import * as ep___i_webhooks_create from './endpoints/i/webhooks/create.js';
import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js';
import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js';
import * as ep___i_webhooks_test from './endpoints/i/webhooks/test.js';
import * as ep___invite_create from './endpoints/invite/create.js';
import * as ep___invite_delete from './endpoints/invite/delete.js';
import * as ep___invite_list from './endpoints/invite/list.js';
import * as ep___invite_limit from './endpoints/invite/limit.js';
import * as ep___meta from './endpoints/meta.js';
import * as ep___emojis from './endpoints/emojis.js';
import * as ep___emoji from './endpoints/emoji.js';
import * as ep___miauth_genToken from './endpoints/miauth/gen-token.js';
import * as ep___mute_create from './endpoints/mute/create.js';
import * as ep___mute_delete from './endpoints/mute/delete.js';
import * as ep___mute_list from './endpoints/mute/list.js';
import * as ep___renoteMute_create from './endpoints/renote-mute/create.js';
import * as ep___renoteMute_delete from './endpoints/renote-mute/delete.js';
import * as ep___renoteMute_list from './endpoints/renote-mute/list.js';
import * as ep___my_apps from './endpoints/my/apps.js';
import * as ep___notes from './endpoints/notes.js';
import * as ep___notes_children from './endpoints/notes/children.js';
import * as ep___notes_clips from './endpoints/notes/clips.js';
import * as ep___notes_conversation from './endpoints/notes/conversation.js';
import * as ep___notes_create from './endpoints/notes/create.js';
import * as ep___notes_delete from './endpoints/notes/delete.js';
import * as ep___notes_favorites_create from './endpoints/notes/favorites/create.js';
import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete.js';
import * as ep___notes_featured from './endpoints/notes/featured.js';
import * as ep___notes_following from './endpoints/notes/following.js';
import * as ep___notes_globalTimeline from './endpoints/notes/global-timeline.js';
import * as ep___notes_bubbleTimeline from './endpoints/notes/bubble-timeline.js';
import * as ep___notes_hybridTimeline from './endpoints/notes/hybrid-timeline.js';
import * as ep___notes_localTimeline from './endpoints/notes/local-timeline.js';
import * as ep___notes_mentions from './endpoints/notes/mentions.js';
import * as ep___notes_polls_recommendation from './endpoints/notes/polls/recommendation.js';
import * as ep___notes_polls_vote from './endpoints/notes/polls/vote.js';
import * as ep___notes_polls_refresh from './endpoints/notes/polls/refresh.js';
import * as ep___notes_reactions from './endpoints/notes/reactions.js';
import * as ep___notes_reactions_create from './endpoints/notes/reactions/create.js';
import * as ep___notes_reactions_delete from './endpoints/notes/reactions/delete.js';
import * as ep___notes_like from './endpoints/notes/like.js';
import * as ep___notes_renotes from './endpoints/notes/renotes.js';
import * as ep___notes_replies from './endpoints/notes/replies.js';
import * as ep___notes_schedule_create from './endpoints/notes/schedule/create.js';
import * as ep___notes_schedule_delete from './endpoints/notes/schedule/delete.js';
import * as ep___notes_schedule_list from './endpoints/notes/schedule/list.js';
import * as ep___notes_searchByTag from './endpoints/notes/search-by-tag.js';
import * as ep___notes_search from './endpoints/notes/search.js';
import * as ep___notes_show from './endpoints/notes/show.js';
import * as ep___notes_state from './endpoints/notes/state.js';
import * as ep___notes_threadMuting_create from './endpoints/notes/thread-muting/create.js';
import * as ep___notes_threadMuting_delete from './endpoints/notes/thread-muting/delete.js';
import * as ep___notes_timeline from './endpoints/notes/timeline.js';
import * as ep___notes_translate from './endpoints/notes/translate.js';
import * as ep___notes_unrenote from './endpoints/notes/unrenote.js';
import * as ep___notes_userListTimeline from './endpoints/notes/user-list-timeline.js';
import * as ep___notes_edit from './endpoints/notes/edit.js';
import * as ep___notes_versions from './endpoints/notes/versions.js';
import * as ep___notifications_create from './endpoints/notifications/create.js';
import * as ep___notifications_flush from './endpoints/notifications/flush.js';
import * as ep___notifications_markAllAsRead from './endpoints/notifications/mark-all-as-read.js';
import * as ep___notifications_testNotification from './endpoints/notifications/test-notification.js';
import * as ep___pagePush from './endpoints/page-push.js';
import * as ep___pages_create from './endpoints/pages/create.js';
import * as ep___pages_delete from './endpoints/pages/delete.js';
import * as ep___pages_featured from './endpoints/pages/featured.js';
import * as ep___pages_like from './endpoints/pages/like.js';
import * as ep___pages_show from './endpoints/pages/show.js';
import * as ep___pages_unlike from './endpoints/pages/unlike.js';
import * as ep___pages_update from './endpoints/pages/update.js';
import * as ep___flash_create from './endpoints/flash/create.js';
import * as ep___flash_delete from './endpoints/flash/delete.js';
import * as ep___flash_featured from './endpoints/flash/featured.js';
import * as ep___flash_like from './endpoints/flash/like.js';
import * as ep___flash_show from './endpoints/flash/show.js';
import * as ep___flash_unlike from './endpoints/flash/unlike.js';
import * as ep___flash_update from './endpoints/flash/update.js';
import * as ep___flash_my from './endpoints/flash/my.js';
import * as ep___flash_myLikes from './endpoints/flash/my-likes.js';
import * as ep___ping from './endpoints/ping.js';
import * as ep___pinnedUsers from './endpoints/pinned-users.js';
import * as ep___promo_read from './endpoints/promo/read.js';
import * as ep___roles_list from './endpoints/roles/list.js';
import * as ep___roles_show from './endpoints/roles/show.js';
import * as ep___roles_users from './endpoints/roles/users.js';
import * as ep___roles_notes from './endpoints/roles/notes.js';
import * as ep___requestResetPassword from './endpoints/request-reset-password.js';
import * as ep___resetDb from './endpoints/reset-db.js';
import * as ep___resetPassword from './endpoints/reset-password.js';
import * as ep___serverInfo from './endpoints/server-info.js';
import * as ep___stats from './endpoints/stats.js';
import * as ep___sw_show_registration from './endpoints/sw/show-registration.js';
import * as ep___sw_update_registration from './endpoints/sw/update-registration.js';
import * as ep___sw_register from './endpoints/sw/register.js';
import * as ep___sw_unregister from './endpoints/sw/unregister.js';
import * as ep___test from './endpoints/test.js';
import * as ep___username_available from './endpoints/username/available.js';
import * as ep___users from './endpoints/users.js';
import * as ep___users_clips from './endpoints/users/clips.js';
import * as ep___users_followers from './endpoints/users/followers.js';
import * as ep___users_following from './endpoints/users/following.js';
import * as ep___users_gallery_posts from './endpoints/users/gallery/posts.js';
import * as ep___users_getFrequentlyRepliedUsers from './endpoints/users/get-frequently-replied-users.js';
import * as ep___users_featuredNotes from './endpoints/users/featured-notes.js';
import * as ep___users_lists_create from './endpoints/users/lists/create.js';
import * as ep___users_lists_delete from './endpoints/users/lists/delete.js';
import * as ep___users_lists_list from './endpoints/users/lists/list.js';
import * as ep___users_lists_pull from './endpoints/users/lists/pull.js';
import * as ep___users_lists_push from './endpoints/users/lists/push.js';
import * as ep___users_lists_show from './endpoints/users/lists/show.js';
import * as ep___users_lists_favorite from './endpoints/users/lists/favorite.js';
import * as ep___users_lists_unfavorite from './endpoints/users/lists/unfavorite.js';
import * as ep___users_lists_createFromPublic from './endpoints/users/lists/create-from-public.js';
import * as ep___users_lists_update from './endpoints/users/lists/update.js';
import * as ep___users_lists_updateMembership from './endpoints/users/lists/update-membership.js';
import * as ep___users_lists_getMemberships from './endpoints/users/lists/get-memberships.js';
import * as ep___users_notes from './endpoints/users/notes.js';
import * as ep___users_pages from './endpoints/users/pages.js';
import * as ep___users_flashs from './endpoints/users/flashs.js';
import * as ep___users_reactions from './endpoints/users/reactions.js';
import * as ep___users_recommendation from './endpoints/users/recommendation.js';
import * as ep___users_relation from './endpoints/users/relation.js';
import * as ep___users_reportAbuse from './endpoints/users/report-abuse.js';
import * as ep___users_searchByUsernameAndHost from './endpoints/users/search-by-username-and-host.js';
import * as ep___users_search from './endpoints/users/search.js';
import * as ep___users_show from './endpoints/users/show.js';
import * as ep___users_achievements from './endpoints/users/achievements.js';
import * as ep___users_updateMemo from './endpoints/users/update-memo.js';
import * as ep___fetchRss from './endpoints/fetch-rss.js';
import * as ep___fetchExternalResources from './endpoints/fetch-external-resources.js';
import * as ep___retention from './endpoints/retention.js';
import * as ep___sponsors from './endpoints/sponsors.js';
import * as ep___bubbleGame_register from './endpoints/bubble-game/register.js';
import * as ep___bubbleGame_ranking from './endpoints/bubble-game/ranking.js';
import * as ep___reversi_cancelMatch from './endpoints/reversi/cancel-match.js';
import * as ep___reversi_games from './endpoints/reversi/games.js';
import * as ep___reversi_match from './endpoints/reversi/match.js';
import * as ep___reversi_invitations from './endpoints/reversi/invitations.js';
import * as ep___reversi_showGame from './endpoints/reversi/show-game.js';
import * as ep___reversi_surrender from './endpoints/reversi/surrender.js';
import * as ep___reversi_verify from './endpoints/reversi/verify.js';
const eps = [
['admin/meta', ep___admin_meta],
['admin/abuse-user-reports', ep___admin_abuseUserReports],
['admin/abuse-report/notification-recipient/list', ep___admin_abuseReport_notificationRecipient_list],
['admin/abuse-report/notification-recipient/show', ep___admin_abuseReport_notificationRecipient_show],
['admin/abuse-report/notification-recipient/create', ep___admin_abuseReport_notificationRecipient_create],
['admin/abuse-report/notification-recipient/update', ep___admin_abuseReport_notificationRecipient_update],
['admin/abuse-report/notification-recipient/delete', ep___admin_abuseReport_notificationRecipient_delete],
['admin/accounts/create', ep___admin_accounts_create],
['admin/accounts/delete', ep___admin_accounts_delete],
['admin/accounts/find-by-email', ep___admin_accounts_findByEmail],
['admin/ad/create', ep___admin_ad_create],
['admin/ad/delete', ep___admin_ad_delete],
['admin/ad/list', ep___admin_ad_list],
['admin/ad/update', ep___admin_ad_update],
['admin/announcements/create', ep___admin_announcements_create],
['admin/announcements/delete', ep___admin_announcements_delete],
['admin/announcements/list', ep___admin_announcements_list],
['admin/announcements/update', ep___admin_announcements_update],
['admin/avatar-decorations/create', ep___admin_avatarDecorations_create],
['admin/avatar-decorations/delete', ep___admin_avatarDecorations_delete],
['admin/avatar-decorations/list', ep___admin_avatarDecorations_list],
['admin/avatar-decorations/update', ep___admin_avatarDecorations_update],
['admin/delete-all-files-of-a-user', ep___admin_deleteAllFilesOfAUser],
['admin/unset-user-avatar', ep___admin_unsetUserAvatar],
['admin/unset-user-banner', ep___admin_unsetUserBanner],
['admin/drive/clean-remote-files', ep___admin_drive_cleanRemoteFiles],
['admin/drive/cleanup', ep___admin_drive_cleanup],
['admin/drive/files', ep___admin_drive_files],
['admin/drive/show-file', ep___admin_drive_showFile],
['admin/emoji/add-aliases-bulk', ep___admin_emoji_addAliasesBulk],
['admin/emoji/add', ep___admin_emoji_add],
['admin/emoji/copy', ep___admin_emoji_copy],
['admin/emoji/delete-bulk', ep___admin_emoji_deleteBulk],
['admin/emoji/delete', ep___admin_emoji_delete],
['admin/emoji/import-zip', ep___admin_emoji_importZip],
['admin/emoji/list-remote', ep___admin_emoji_listRemote],
['admin/emoji/list', ep___admin_emoji_list],
['admin/emoji/remove-aliases-bulk', ep___admin_emoji_removeAliasesBulk],
['admin/emoji/set-aliases-bulk', ep___admin_emoji_setAliasesBulk],
['admin/emoji/set-category-bulk', ep___admin_emoji_setCategoryBulk],
['admin/emoji/set-license-bulk', ep___admin_emoji_setLicenseBulk],
['admin/emoji/update', ep___admin_emoji_update],
['admin/federation/delete-all-files', ep___admin_federation_deleteAllFiles],
['admin/federation/refresh-remote-instance-metadata', ep___admin_federation_refreshRemoteInstanceMetadata],
['admin/federation/remove-all-following', ep___admin_federation_removeAllFollowing],
['admin/federation/update-instance', ep___admin_federation_updateInstance],
['admin/get-index-stats', ep___admin_getIndexStats],
['admin/get-table-stats', ep___admin_getTableStats],
['admin/get-user-ips', ep___admin_getUserIps],
['admin/invite/create', ep___admin_invite_create],
['admin/invite/list', ep___admin_invite_list],
['admin/promo/create', ep___admin_promo_create],
['admin/queue/clear', ep___admin_queue_clear],
['admin/queue/deliver-delayed', ep___admin_queue_deliverDelayed],
['admin/queue/inbox-delayed', ep___admin_queue_inboxDelayed],
['admin/queue/promote', ep___admin_queue_promote],
['admin/queue/stats', ep___admin_queue_stats],
['admin/relays/add', ep___admin_relays_add],
['admin/relays/list', ep___admin_relays_list],
['admin/relays/remove', ep___admin_relays_remove],
['admin/reset-password', ep___admin_resetPassword],
['admin/resolve-abuse-user-report', ep___admin_resolveAbuseUserReport],
['admin/forward-abuse-user-report', ep___admin_forwardAbuseUserReport],
['admin/update-abuse-user-report', ep___admin_updateAbuseUserReport],
['admin/send-email', ep___admin_sendEmail],
['admin/server-info', ep___admin_serverInfo],
['admin/show-moderation-logs', ep___admin_showModerationLogs],
['admin/show-user', ep___admin_showUser],
['admin/show-users', ep___admin_showUsers],
['admin/nsfw-user', ep___admin_nsfwUser],
['admin/unnsfw-user', ep___admin_unnsfwUser],
['admin/silence-user', ep___admin_silenceUser],
['admin/unsilence-user', ep___admin_unsilenceUser],
['admin/suspend-user', ep___admin_suspendUser],
['admin/approve-user', ep___admin_approveUser],
['admin/decline-user', ep___admin_declineUser],
['admin/unsuspend-user', ep___admin_unsuspendUser],
['admin/update-meta', ep___admin_updateMeta],
['admin/delete-account', ep___admin_deleteAccount],
['admin/update-user-note', ep___admin_updateUserNote],
['admin/roles/create', ep___admin_roles_create],
['admin/roles/delete', ep___admin_roles_delete],
['admin/roles/list', ep___admin_roles_list],
['admin/roles/show', ep___admin_roles_show],
['admin/roles/update', ep___admin_roles_update],
['admin/roles/assign', ep___admin_roles_assign],
['admin/roles/unassign', ep___admin_roles_unassign],
['admin/roles/update-default-policies', ep___admin_roles_updateDefaultPolicies],
['admin/roles/users', ep___admin_roles_users],
['admin/system-webhook/create', ep___admin_systemWebhook_create],
['admin/system-webhook/delete', ep___admin_systemWebhook_delete],
['admin/system-webhook/list', ep___admin_systemWebhook_list],
['admin/system-webhook/show', ep___admin_systemWebhook_show],
['admin/system-webhook/update', ep___admin_systemWebhook_update],
['admin/system-webhook/test', ep___admin_systemWebhook_test],
['announcements', ep___announcements],
['announcements/show', ep___announcements_show],
['antennas/create', ep___antennas_create],
['antennas/delete', ep___antennas_delete],
['antennas/list', ep___antennas_list],
['antennas/notes', ep___antennas_notes],
['antennas/show', ep___antennas_show],
['antennas/update', ep___antennas_update],
['ap/get', ep___ap_get],
['ap/show', ep___ap_show],
['app/create', ep___app_create],
['app/show', ep___app_show],
['auth/accept', ep___auth_accept],
['auth/session/generate', ep___auth_session_generate],
['auth/session/show', ep___auth_session_show],
['auth/session/userkey', ep___auth_session_userkey],
['blocking/create', ep___blocking_create],
['blocking/delete', ep___blocking_delete],
['blocking/list', ep___blocking_list],
['channels/create', ep___channels_create],
['channels/featured', ep___channels_featured],
['channels/follow', ep___channels_follow],
['channels/followed', ep___channels_followed],
['channels/owned', ep___channels_owned],
['channels/show', ep___channels_show],
['channels/timeline', ep___channels_timeline],
['channels/unfollow', ep___channels_unfollow],
['channels/update', ep___channels_update],
['channels/favorite', ep___channels_favorite],
['channels/unfavorite', ep___channels_unfavorite],
['channels/my-favorites', ep___channels_myFavorites],
['channels/search', ep___channels_search],
['charts/active-users', ep___charts_activeUsers],
['charts/ap-request', ep___charts_apRequest],
['charts/drive', ep___charts_drive],
['charts/federation', ep___charts_federation],
['charts/instance', ep___charts_instance],
['charts/notes', ep___charts_notes],
['charts/user/drive', ep___charts_user_drive],
['charts/user/following', ep___charts_user_following],
['charts/user/notes', ep___charts_user_notes],
['charts/user/pv', ep___charts_user_pv],
['charts/user/reactions', ep___charts_user_reactions],
['charts/users', ep___charts_users],
['clips/add-note', ep___clips_addNote],
['clips/remove-note', ep___clips_removeNote],
['clips/create', ep___clips_create],
['clips/delete', ep___clips_delete],
['clips/list', ep___clips_list],
['clips/notes', ep___clips_notes],
['clips/show', ep___clips_show],
['clips/update', ep___clips_update],
['clips/favorite', ep___clips_favorite],
['clips/unfavorite', ep___clips_unfavorite],
['clips/my-favorites', ep___clips_myFavorites],
['drive', ep___drive],
['drive/files', ep___drive_files],
['drive/files/attached-notes', ep___drive_files_attachedNotes],
['drive/files/check-existence', ep___drive_files_checkExistence],
['drive/files/create', ep___drive_files_create],
['drive/files/delete', ep___drive_files_delete],
['drive/files/find-by-hash', ep___drive_files_findByHash],
['drive/files/find', ep___drive_files_find],
['drive/files/show', ep___drive_files_show],
['drive/files/update', ep___drive_files_update],
['drive/files/upload-from-url', ep___drive_files_uploadFromUrl],
['drive/folders', ep___drive_folders],
['drive/folders/create', ep___drive_folders_create],
['drive/folders/delete', ep___drive_folders_delete],
['drive/folders/find', ep___drive_folders_find],
['drive/folders/show', ep___drive_folders_show],
['drive/folders/update', ep___drive_folders_update],
['drive/stream', ep___drive_stream],
['email-address/available', ep___emailAddress_available],
['endpoint', ep___endpoint],
['endpoints', ep___endpoints],
['export-custom-emojis', ep___exportCustomEmojis],
['federation/followers', ep___federation_followers],
['federation/following', ep___federation_following],
['federation/instances', ep___federation_instances],
['federation/show-instance', ep___federation_showInstance],
['federation/update-remote-user', ep___federation_updateRemoteUser],
['federation/users', ep___federation_users],
['federation/stats', ep___federation_stats],
['following/create', ep___following_create],
['following/delete', ep___following_delete],
['following/update', ep___following_update],
['following/update-all', ep___following_update_all],
['following/invalidate', ep___following_invalidate],
['following/requests/accept', ep___following_requests_accept],
['following/requests/cancel', ep___following_requests_cancel],
['following/requests/list', ep___following_requests_list],
['following/requests/sent', ep___following_requests_sent],
['following/requests/reject', ep___following_requests_reject],
['gallery/featured', ep___gallery_featured],
['gallery/popular', ep___gallery_popular],
['gallery/posts', ep___gallery_posts],
['gallery/posts/create', ep___gallery_posts_create],
['gallery/posts/delete', ep___gallery_posts_delete],
['gallery/posts/like', ep___gallery_posts_like],
['gallery/posts/show', ep___gallery_posts_show],
['gallery/posts/unlike', ep___gallery_posts_unlike],
['gallery/posts/update', ep___gallery_posts_update],
['get-online-users-count', ep___getOnlineUsersCount],
['get-avatar-decorations', ep___getAvatarDecorations],
['hashtags/list', ep___hashtags_list],
['hashtags/search', ep___hashtags_search],
['hashtags/show', ep___hashtags_show],
['hashtags/trend', ep___hashtags_trend],
['hashtags/users', ep___hashtags_users],
['i', ep___i],
['i/2fa/done', ep___i_2fa_done],
['i/2fa/key-done', ep___i_2fa_keyDone],
['i/2fa/password-less', ep___i_2fa_passwordLess],
['i/2fa/register-key', ep___i_2fa_registerKey],
['i/2fa/register', ep___i_2fa_register],
['i/2fa/update-key', ep___i_2fa_updateKey],
['i/2fa/remove-key', ep___i_2fa_removeKey],
['i/2fa/unregister', ep___i_2fa_unregister],
['i/apps', ep___i_apps],
['i/authorized-apps', ep___i_authorizedApps],
['i/claim-achievement', ep___i_claimAchievement],
['i/change-password', ep___i_changePassword],
['i/delete-account', ep___i_deleteAccount],
['i/export-data', ep___i_exportData],
['i/export-blocking', ep___i_exportBlocking],
['i/export-following', ep___i_exportFollowing],
['i/export-mute', ep___i_exportMute],
['i/export-notes', ep___i_exportNotes],
['i/export-clips', ep___i_exportClips],
['i/export-favorites', ep___i_exportFavorites],
['i/export-user-lists', ep___i_exportUserLists],
['i/export-antennas', ep___i_exportAntennas],
['i/favorites', ep___i_favorites],
['i/gallery/likes', ep___i_gallery_likes],
['i/gallery/posts', ep___i_gallery_posts],
['i/import-blocking', ep___i_importBlocking],
['i/import-following', ep___i_importFollowing],
['i/import-notes', ep___i_importNotes],
['i/import-muting', ep___i_importMuting],
['i/import-user-lists', ep___i_importUserLists],
['i/import-antennas', ep___i_importAntennas],
['i/notifications', ep___i_notifications],
['i/notifications-grouped', ep___i_notificationsGrouped],
['i/page-likes', ep___i_pageLikes],
['i/pages', ep___i_pages],
['i/pin', ep___i_pin],
['i/read-all-unread-notes', ep___i_readAllUnreadNotes],
['i/read-announcement', ep___i_readAnnouncement],
['i/regenerate-token', ep___i_regenerateToken],
['i/registry/get-all', ep___i_registry_getAll],
['i/registry/get-unsecure', ep___i_registry_getUnsecure],
['i/registry/get-detail', ep___i_registry_getDetail],
['i/registry/get', ep___i_registry_get],
['i/registry/keys-with-type', ep___i_registry_keysWithType],
['i/registry/keys', ep___i_registry_keys],
['i/registry/remove', ep___i_registry_remove],
['i/registry/scopes-with-domain', ep___i_registry_scopesWithDomain],
['i/registry/set', ep___i_registry_set],
['i/revoke-token', ep___i_revokeToken],
['i/signin-history', ep___i_signinHistory],
['i/unpin', ep___i_unpin],
['i/update-email', ep___i_updateEmail],
['i/update', ep___i_update],
['i/move', ep___i_move],
['i/webhooks/create', ep___i_webhooks_create],
['i/webhooks/list', ep___i_webhooks_list],
['i/webhooks/show', ep___i_webhooks_show],
['i/webhooks/update', ep___i_webhooks_update],
['i/webhooks/delete', ep___i_webhooks_delete],
['i/webhooks/test', ep___i_webhooks_test],
['invite/create', ep___invite_create],
['invite/delete', ep___invite_delete],
['invite/list', ep___invite_list],
['invite/limit', ep___invite_limit],
['meta', ep___meta],
['emojis', ep___emojis],
['emoji', ep___emoji],
['miauth/gen-token', ep___miauth_genToken],
['mute/create', ep___mute_create],
['mute/delete', ep___mute_delete],
['mute/list', ep___mute_list],
['renote-mute/create', ep___renoteMute_create],
['renote-mute/delete', ep___renoteMute_delete],
['renote-mute/list', ep___renoteMute_list],
['my/apps', ep___my_apps],
['notes', ep___notes],
['notes/children', ep___notes_children],
['notes/clips', ep___notes_clips],
['notes/conversation', ep___notes_conversation],
['notes/create', ep___notes_create],
['notes/delete', ep___notes_delete],
['notes/favorites/create', ep___notes_favorites_create],
['notes/favorites/delete', ep___notes_favorites_delete],
['notes/featured', ep___notes_featured],
['notes/following', ep___notes_following],
['notes/global-timeline', ep___notes_globalTimeline],
['notes/bubble-timeline', ep___notes_bubbleTimeline],
['notes/hybrid-timeline', ep___notes_hybridTimeline],
['notes/local-timeline', ep___notes_localTimeline],
['notes/mentions', ep___notes_mentions],
['notes/polls/recommendation', ep___notes_polls_recommendation],
['notes/polls/vote', ep___notes_polls_vote],
['notes/polls/refresh', ep___notes_polls_refresh],
['notes/reactions', ep___notes_reactions],
['notes/reactions/create', ep___notes_reactions_create],
['notes/reactions/delete', ep___notes_reactions_delete],
['notes/like', ep___notes_like],
['notes/renotes', ep___notes_renotes],
['notes/replies', ep___notes_replies],
['notes/schedule/create', ep___notes_schedule_create],
['notes/schedule/delete', ep___notes_schedule_delete],
['notes/schedule/list', ep___notes_schedule_list],
['notes/search-by-tag', ep___notes_searchByTag],
['notes/search', ep___notes_search],
['notes/show', ep___notes_show],
['notes/state', ep___notes_state],
['notes/thread-muting/create', ep___notes_threadMuting_create],
['notes/thread-muting/delete', ep___notes_threadMuting_delete],
['notes/timeline', ep___notes_timeline],
['notes/translate', ep___notes_translate],
['notes/unrenote', ep___notes_unrenote],
['notes/user-list-timeline', ep___notes_userListTimeline],
['notes/edit', ep___notes_edit],
['notes/versions', ep___notes_versions],
['notifications/create', ep___notifications_create],
['notifications/flush', ep___notifications_flush],
['notifications/mark-all-as-read', ep___notifications_markAllAsRead],
['notifications/test-notification', ep___notifications_testNotification],
['page-push', ep___pagePush],
['pages/create', ep___pages_create],
['pages/delete', ep___pages_delete],
['pages/featured', ep___pages_featured],
['pages/like', ep___pages_like],
['pages/show', ep___pages_show],
['pages/unlike', ep___pages_unlike],
['pages/update', ep___pages_update],
['flash/create', ep___flash_create],
['flash/delete', ep___flash_delete],
['flash/featured', ep___flash_featured],
['flash/like', ep___flash_like],
['flash/show', ep___flash_show],
['flash/unlike', ep___flash_unlike],
['flash/update', ep___flash_update],
['flash/my', ep___flash_my],
['flash/my-likes', ep___flash_myLikes],
['ping', ep___ping],
['pinned-users', ep___pinnedUsers],
['promo/read', ep___promo_read],
['roles/list', ep___roles_list],
['roles/show', ep___roles_show],
['roles/users', ep___roles_users],
['roles/notes', ep___roles_notes],
['request-reset-password', ep___requestResetPassword],
['reset-db', ep___resetDb],
['reset-password', ep___resetPassword],
['server-info', ep___serverInfo],
['stats', ep___stats],
['sw/show-registration', ep___sw_show_registration],
['sw/update-registration', ep___sw_update_registration],
['sw/register', ep___sw_register],
['sw/unregister', ep___sw_unregister],
['test', ep___test],
['username/available', ep___username_available],
['users', ep___users],
['users/clips', ep___users_clips],
['users/followers', ep___users_followers],
['users/following', ep___users_following],
['users/gallery/posts', ep___users_gallery_posts],
['users/get-frequently-replied-users', ep___users_getFrequentlyRepliedUsers],
['users/featured-notes', ep___users_featuredNotes],
['users/lists/create', ep___users_lists_create],
['users/lists/delete', ep___users_lists_delete],
['users/lists/list', ep___users_lists_list],
['users/lists/pull', ep___users_lists_pull],
['users/lists/push', ep___users_lists_push],
['users/lists/show', ep___users_lists_show],
['users/lists/favorite', ep___users_lists_favorite],
['users/lists/unfavorite', ep___users_lists_unfavorite],
['users/lists/update', ep___users_lists_update],
['users/lists/create-from-public', ep___users_lists_createFromPublic],
['users/lists/update-membership', ep___users_lists_updateMembership],
['users/lists/get-memberships', ep___users_lists_getMemberships],
['users/notes', ep___users_notes],
['users/pages', ep___users_pages],
['users/flashs', ep___users_flashs],
['users/reactions', ep___users_reactions],
['users/recommendation', ep___users_recommendation],
['users/relation', ep___users_relation],
['users/report-abuse', ep___users_reportAbuse],
['users/search-by-username-and-host', ep___users_searchByUsernameAndHost],
['users/search', ep___users_search],
['users/show', ep___users_show],
['users/achievements', ep___users_achievements],
['users/update-memo', ep___users_updateMemo],
['fetch-rss', ep___fetchRss],
['fetch-external-resources', ep___fetchExternalResources],
['retention', ep___retention],
['sponsors', ep___sponsors],
['bubble-game/register', ep___bubbleGame_register],
['bubble-game/ranking', ep___bubbleGame_ranking],
['reversi/cancel-match', ep___reversi_cancelMatch],
['reversi/games', ep___reversi_games],
['reversi/match', ep___reversi_match],
['reversi/invitations', ep___reversi_invitations],
['reversi/show-game', ep___reversi_showGame],
['reversi/surrender', ep___reversi_surrender],
['reversi/verify', ep___reversi_verify],
];
import * as endpointsObject from './endpoint-list.js';
interface IEndpointMetaBase {
readonly stability?: 'deprecated' | 'experimental' | 'stable';
@ -922,7 +108,7 @@ export interface IEndpoint {
params: Schema;
}
const endpoints: IEndpoint[] = (eps as [string, any]).map(([name, ep]) => {
const endpoints: IEndpoint[] = Object.entries(endpointsObject).map(([name, ep]) => {
return {
name: name,
get meta() {

View file

@ -6,7 +6,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository } from '@/models/_.js';
import { MiAccessToken, MiUser } from '@/models/_.js';
import { SignupService } from '@/core/SignupService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
@ -16,6 +15,7 @@ import type { Config } from '@/config.js';
import { ApiError } from '@/server/api/error.js';
import { Packed } from '@/misc/json-schema.js';
import { RoleService } from '@/core/RoleService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@ -97,6 +97,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private userEntityService: UserEntityService,
private signupService: SignupService,
private instanceActorService: InstanceActorService,
private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, _me, token) => {
const me = _me ? await this.usersRepository.findOneByOrFail({ id: _me.id }) : null;
@ -138,6 +139,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
approved: true,
});
if (me) {
await this.moderationLogService.log(me, 'createAccount', {
userId: account.id,
userUsername: account.username,
});
}
const res = await this.userEntityService.pack(account, account, {
schema: 'MeDetailed',
includeSecrets: true,

View file

@ -0,0 +1,77 @@
/*
* 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 { CaptchaService, supportedCaptchaProviders } from '@/core/CaptchaService.js';
export const meta = {
tags: ['admin', 'captcha'],
requireCredential: true,
requireAdmin: true,
// 実態はmetaの取得であるため
kind: 'read:admin:meta',
res: {
type: 'object',
properties: {
provider: {
type: 'string',
enum: supportedCaptchaProviders,
},
hcaptcha: {
type: 'object',
properties: {
siteKey: { type: 'string', nullable: true },
secretKey: { type: 'string', nullable: true },
},
},
mcaptcha: {
type: 'object',
properties: {
siteKey: { type: 'string', nullable: true },
secretKey: { type: 'string', nullable: true },
instanceUrl: { type: 'string', nullable: true },
},
},
recaptcha: {
type: 'object',
properties: {
siteKey: { type: 'string', nullable: true },
secretKey: { type: 'string', nullable: true },
},
},
turnstile: {
type: 'object',
properties: {
siteKey: { type: 'string', nullable: true },
secretKey: { type: 'string', nullable: true },
},
},
fc: {
type: 'object',
properties: {
siteKey: { type: 'string', nullable: true },
secretKey: { type: 'string', nullable: true },
},
},
},
},
} as const;
export const paramDef = {} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private captchaService: CaptchaService,
) {
super(meta, paramDef, async () => {
return this.captchaService.get();
});
}
}

View file

@ -0,0 +1,129 @@
/*
* 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 { captchaErrorCodes, CaptchaService, supportedCaptchaProviders } from '@/core/CaptchaService.js';
import { ApiError } from '@/server/api/error.js';
export const meta = {
tags: ['admin', 'captcha'],
requireCredential: true,
requireAdmin: true,
// 実態はmetaの更新であるため
kind: 'write:admin:meta',
errors: {
invalidProvider: {
message: 'Invalid provider.',
code: 'INVALID_PROVIDER',
id: '14bf7ae1-80cc-4363-acb2-4fd61d086af0',
httpStatusCode: 400,
},
invalidParameters: {
message: 'Invalid parameters.',
code: 'INVALID_PARAMETERS',
id: '26654194-410e-44e2-b42e-460ff6f92476',
httpStatusCode: 400,
},
noResponseProvided: {
message: 'No response provided.',
code: 'NO_RESPONSE_PROVIDED',
id: '40acbba8-0937-41fb-bb3f-474514d40afe',
httpStatusCode: 400,
},
requestFailed: {
message: 'Request failed.',
code: 'REQUEST_FAILED',
id: '0f4fe2f1-2c15-4d6e-b714-efbfcde231cd',
httpStatusCode: 500,
},
verificationFailed: {
message: 'Verification failed.',
code: 'VERIFICATION_FAILED',
id: 'c41c067f-24f3-4150-84b2-b5a3ae8c2214',
httpStatusCode: 400,
},
unknown: {
message: 'unknown',
code: 'UNKNOWN',
id: 'f868d509-e257-42a9-99c1-42614b031a97',
httpStatusCode: 500,
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
provider: {
type: 'string',
enum: supportedCaptchaProviders,
},
captchaResult: {
type: 'string', nullable: true,
},
sitekey: {
type: 'string', nullable: true,
},
secret: {
type: 'string', nullable: true,
},
instanceUrl: {
type: 'string', nullable: true,
},
},
required: ['provider'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private captchaService: CaptchaService,
) {
super(meta, paramDef, async (ps) => {
const result = await this.captchaService.save(ps.provider, {
sitekey: ps.sitekey,
secret: ps.secret,
instanceUrl: ps.instanceUrl,
captchaResult: ps.captchaResult,
});
if (!result.success) {
switch (result.error.code) {
case captchaErrorCodes.invalidProvider:
throw new ApiError({
...meta.errors.invalidProvider,
message: result.error.message,
});
case captchaErrorCodes.invalidParameters:
throw new ApiError({
...meta.errors.invalidParameters,
message: result.error.message,
});
case captchaErrorCodes.noResponseProvided:
throw new ApiError({
...meta.errors.noResponseProvided,
message: result.error.message,
});
case captchaErrorCodes.requestFailed:
throw new ApiError({
...meta.errors.requestFailed,
message: result.error.message,
});
case captchaErrorCodes.verificationFailed:
throw new ApiError({
...meta.errors.verificationFailed,
message: result.error.message,
});
default:
throw new ApiError(meta.errors.unknown);
}
}
});
}
}

View file

@ -0,0 +1,67 @@
/*
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { CacheService } from '@/core/CacheService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
kind: 'write:admin:cw-user',
} as const;
export const paramDef = {
type: 'object',
properties: {
userId: { type: 'string', format: 'misskey:id' },
cw: { type: 'string', nullable: true },
},
required: ['userId', 'cw'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.usersRepository)
private readonly usersRepository: UsersRepository,
private readonly globalEventService: GlobalEventService,
private readonly cacheService: CacheService,
private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
const user = await this.cacheService.findUserById(ps.userId);
// Skip if there's nothing to do
if (user.mandatoryCW === ps.cw) return;
// Log event first.
// This ensures that we don't "lose" the log if an error occurs
await this.moderationLogService.log(me, 'setMandatoryCW', {
newCW: ps.cw,
oldCW: user.mandatoryCW,
userId: user.id,
userUsername: user.username,
userHost: user.host,
});
await this.usersRepository.update(ps.userId, {
// Collapse empty strings to null
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
mandatoryCW: ps.cw || null,
});
// Synchronize caches and other processes
this.globalEventService.publishInternalEvent('localUserUpdated', { id: ps.userId });
});
}
}

View file

@ -8,6 +8,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { DriveFilesRepository } from '@/models/_.js';
import { DriveService } from '@/core/DriveService.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { CacheService } from '@/core/CacheService.js';
export const meta = {
tags: ['admin'],
@ -30,14 +32,23 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
private readonly cacheService: CacheService,
private readonly moderationLogService: ModerationLogService,
private driveService: DriveService,
) {
super(meta, paramDef, async (ps, me) => {
const user = await this.cacheService.findUserById(ps.userId);
const files = await this.driveFilesRepository.findBy({
userId: ps.userId,
});
await this.moderationLogService.log(me, 'clearUserFiles', {
userId: ps.userId,
userUsername: user.username,
userHost: user.host,
count: files.length,
});
for (const file of files) {
this.driveService.deleteFile(file, false, me);
}

View file

@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueueService } from '@/core/QueueService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@ -25,9 +26,11 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private queueService: QueueService,
private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
this.queueService.createCleanRemoteFilesJob();
await this.moderationLogService.log(me, 'clearRemoteFiles', {});
await this.queueService.createCleanRemoteFilesJob();
});
}
}

View file

@ -9,6 +9,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { DriveFilesRepository } from '@/models/_.js';
import { DriveService } from '@/core/DriveService.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@ -29,7 +30,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
private readonly moderationLogService: ModerationLogService,
private driveService: DriveService,
) {
super(meta, paramDef, async (ps, me) => {
@ -37,6 +38,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
userId: IsNull(),
});
await this.moderationLogService.log(me, 'clearOwnerlessFiles', {
count: files.length,
});
for (const file of files) {
this.driveService.deleteFile(file);
}

View file

@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@ -32,8 +33,13 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private customEmojiService: CustomEmojiService,
private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
await this.moderationLogService.log(me, 'updateCustomEmojis', {
ids: ps.ids,
addAliases: ps.aliases,
});
await this.customEmojiService.addAliasesBulk(ps.ids, ps.aliases.map(a => a.normalize('NFC')));
});
}

View file

@ -9,6 +9,7 @@ import type { DriveFilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { FILE_TYPE_IMAGE } from '@/const.js';
import { ApiError } from '../../../error.js';
export const meta = {
@ -24,6 +25,11 @@ export const meta = {
code: 'NO_SUCH_FILE',
id: 'fc46b5a4-6b92-4c33-ac66-b806659bb5cf',
},
unsupportedFileType: {
message: 'Unsupported file type.',
code: 'UNSUPPORTED_FILE_TYPE',
id: 'f7599d96-8750-af68-1633-9575d625c1a7',
},
duplicateName: {
message: 'Duplicate name.',
code: 'DUPLICATE_NAME',
@ -47,15 +53,21 @@ export const paramDef = {
nullable: true,
description: 'Use `null` to reset the category.',
},
aliases: { 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',
} },
roleIdsThatCanBeUsedThisEmojiAsReaction: {
type: 'array',
items: {
type: 'string',
},
},
},
required: ['name', 'fileId'],
} as const;
@ -67,9 +79,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
private customEmojiService: CustomEmojiService,
private emojiEntityService: EmojiEntityService,
) {
super(meta, paramDef, async (ps, me) => {
@ -78,11 +88,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
const isDuplicate = await this.customEmojiService.checkDuplicate(nameNfc);
if (isDuplicate) throw new ApiError(meta.errors.duplicateName);
if (!FILE_TYPE_IMAGE.includes(driveFile.type)) throw new ApiError(meta.errors.unsupportedFileType);
if (driveFile.user !== null) await this.driveFilesRepository.update(driveFile.id, { user: null });
const emoji = await this.customEmojiService.add({
driveFile,
originalUrl: driveFile.url,
publicUrl: driveFile.webpublicUrl ?? driveFile.url,
fileType: driveFile.webpublicType ?? driveFile.type,
name: nameNfc,
category: ps.category?.normalize('NFC') ?? null,
aliases: ps.aliases?.map(a => a.normalize('NFC')) ?? [],

View file

@ -4,7 +4,6 @@
*/
import { Inject, Injectable } from '@nestjs/common';
import { IsNull } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { EmojisRepository } from '@/models/_.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
@ -88,10 +87,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (isDuplicate) throw new ApiError(meta.errors.duplicateName);
const addedEmoji = await this.customEmojiService.add({
driveFile,
originalUrl: driveFile.url,
publicUrl: driveFile.webpublicUrl ?? driveFile.url,
fileType: driveFile.webpublicType ?? driveFile.type,
name: nameNfc,
category: emoji.category?.normalize('NFC') ?? null,
aliases: emoji.aliases?.map(a => a.normalize('NFC')),
aliases: emoji.aliases.map(a => a.normalize('NFC')),
host: null,
license: emoji.license,
isSensitive: emoji.isSensitive,

View file

@ -3,9 +3,12 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueueService } from '@/core/QueueService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import type { DriveFilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
export const meta = {
secure: true,
@ -25,9 +28,16 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private queueService: QueueService,
private readonly moderationLogService: ModerationLogService,
@Inject(DI.driveFilesRepository)
private readonly driveFilesRepository: DriveFilesRepository,
) {
super(meta, paramDef, async (ps, me) => {
this.queueService.createImportCustomEmojisJob(me, ps.fileId);
const file = await driveFilesRepository.findOneByOrFail({ id: ps.fileId });
await this.moderationLogService.log(me, 'importCustomEmojis', {
fileName: file.name,
});
await this.queueService.createImportCustomEmojisJob(me, ps.fileId);
});
}
}

View file

@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@ -32,8 +33,13 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private customEmojiService: CustomEmojiService,
private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
await this.moderationLogService.log(me, 'updateCustomEmojis', {
ids: ps.ids,
delAliases: ps.aliases,
});
await this.customEmojiService.removeAliasesBulk(ps.ids, ps.aliases.map(a => a.normalize('NFC')));
});
}

View file

@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@ -32,8 +33,13 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private customEmojiService: CustomEmojiService,
private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
await this.moderationLogService.log(me, 'updateCustomEmojis', {
ids: ps.ids,
setAliases: ps.aliases,
});
await this.customEmojiService.setAliasesBulk(ps.ids, ps.aliases.map(a => a.normalize('NFC')));
});
}

View file

@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@ -34,8 +35,13 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private customEmojiService: CustomEmojiService,
private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
await this.moderationLogService.log(me, 'updateCustomEmojis', {
ids: ps.ids,
category: ps.category,
});
await this.customEmojiService.setCategoryBulk(ps.ids, ps.category?.normalize('NFC') ?? null);
});
}

View file

@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@ -34,8 +35,13 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private customEmojiService: CustomEmojiService,
private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
await this.moderationLogService.log(me, 'updateCustomEmojis', {
ids: ps.ids,
license: ps.license,
});
await this.customEmojiService.setLicenseBulk(ps.ids, ps.license ?? null);
});
}

View file

@ -86,7 +86,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const error = await this.customEmojiService.update({
...required,
driveFile,
originalUrl: driveFile != null ? driveFile.url : undefined,
publicUrl: driveFile != null ? (driveFile.webpublicUrl ?? driveFile.url) : undefined,
fileType: driveFile != null ? (driveFile.webpublicType ?? driveFile.type) : undefined,
category: ps.category?.normalize('NFC'),
aliases: ps.aliases?.map(a => a.normalize('NFC')),
license: ps.license,

View file

@ -8,6 +8,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { DriveFilesRepository } from '@/models/_.js';
import { DriveService } from '@/core/DriveService.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@ -30,7 +31,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
private readonly moderationLogService: ModerationLogService,
private driveService: DriveService,
) {
super(meta, paramDef, async (ps, me) => {
@ -38,6 +39,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
userHost: ps.host,
});
await this.moderationLogService.log(me, 'clearInstanceFiles', {
host: ps.host,
count: files.length,
});
for (const file of files) {
this.driveService.deleteFile(file);
}

View file

@ -8,6 +8,7 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { FollowingsRepository, UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { QueueService } from '@/core/QueueService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@ -35,6 +36,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private followingsRepository: FollowingsRepository,
private queueService: QueueService,
private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
const followings = await this.followingsRepository.findBy([
@ -51,6 +53,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.usersRepository.findOneByOrFail({ id: f.followeeId }),
]).then(([from, to]) => [{ id: from.id }, { id: to.id }])));
await this.moderationLogService.log(me, 'severFollowRelations', {
host: ps.host,
});
this.queueService.createUnfollowJob(pairs.map(p => ({ from: p[0], to: p[1], silent: true })));
});
}

View file

@ -27,6 +27,7 @@ export const paramDef = {
isNSFW: { type: 'boolean' },
rejectReports: { type: 'boolean' },
moderationNote: { type: 'string' },
rejectQuotes: { type: 'boolean' },
},
required: ['host'],
} as const;
@ -59,6 +60,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
suspensionState,
isNSFW: ps.isNSFW,
rejectReports: ps.rejectReports,
rejectQuotes: ps.rejectQuotes,
moderationNote: ps.moderationNote,
});
@ -92,6 +94,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
});
}
if (ps.rejectQuotes != null && instance.rejectQuotes !== ps.rejectQuotes) {
const message = ps.rejectReports ? 'rejectQuotesInstance' : 'acceptQuotesInstance';
this.moderationLogService.log(me, message, {
id: instance.id,
host: instance.host,
});
}
if (ps.moderationNote != null && instance.moderationNote !== ps.moderationNote) {
this.moderationLogService.log(me, 'updateRemoteInstanceNote', {
id: instance.id,

View file

@ -0,0 +1,33 @@
/*
* SPDX-FileCopyrightText: marie and sharkey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import webpush from 'web-push';
const { generateVAPIDKeys } = webpush;
import { Endpoint } from '@/server/api/endpoint-base.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
kind: 'write:admin:meta',
} as const;
export const paramDef = {} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
const keys = await generateVAPIDKeys();
return { public: keys.publicKey, private: keys.privateKey };
});
}
}

View file

@ -391,6 +391,10 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
robotsTxt: {
type: 'string',
optional: false, nullable: true,
},
enableIdenticonGeneration: {
type: 'boolean',
optional: false, nullable: false,
@ -708,6 +712,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
enableStatsForFederatedInstances: instance.enableStatsForFederatedInstances,
enableServerMachineStats: instance.enableServerMachineStats,
enableAchievements: instance.enableAchievements,
robotsTxt: instance.robotsTxt,
enableIdenticonGeneration: instance.enableIdenticonGeneration,
bannedEmailDomains: instance.bannedEmailDomains,
policies: { ...DEFAULT_POLICIES, ...instance.policies },

View file

@ -5,8 +5,10 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository, UserProfilesRepository } from '@/models/_.js';
import type { UserProfilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { CacheService } from '@/core/CacheService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@ -27,22 +29,25 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
private readonly userProfilesRepository: UserProfilesRepository,
private readonly moderationLogService: ModerationLogService,
private readonly cacheService: CacheService,
) {
super(meta, paramDef, async (ps, me) => {
const user = await this.usersRepository.findOneBy({ id: ps.userId });
const user = await this.cacheService.findUserById(ps.userId);
if (user == null) {
throw new Error('user not found');
}
await this.moderationLogService.log(me, 'nsfwUser', {
userId: ps.userId,
userUsername: user.username,
userHost: user.host,
});
await this.userProfilesRepository.update(user.id, {
alwaysMarkNsfw: true,
});
await this.cacheService.userProfileCache.refresh(ps.userId);
});
}
}

View file

@ -8,6 +8,8 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { PromoNotesRepository } from '@/models/_.js';
import { GetterService } from '@/server/api/GetterService.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { CacheService } from '@/core/CacheService.js';
import { ApiError } from '../../../error.js';
export const meta = {
@ -46,7 +48,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.promoNotesRepository)
private promoNotesRepository: PromoNotesRepository,
private readonly moderationLogService: ModerationLogService,
private readonly cacheService: CacheService,
private getterService: GetterService,
) {
super(meta, paramDef, async (ps, me) => {
@ -61,6 +64,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.alreadyPromoted);
}
const user = await this.cacheService.findUserById(note.userId);
await this.moderationLogService.log(me, 'createPromo', {
noteId: note.id,
noteUserId: user.id,
noteUserUsername: user.username,
noteUserHost: user.host,
});
await this.promoNotesRepository.insert({
noteId: note.id,
expiresAt: new Date(ps.expiresAt),

View file

@ -0,0 +1,63 @@
/*
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { CacheService } from '@/core/CacheService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireModerator: true,
kind: 'write:admin:reject-quotes',
} as const;
export const paramDef = {
type: 'object',
properties: {
userId: { type: 'string', format: 'misskey:id' },
rejectQuotes: { type: 'boolean', nullable: false },
},
required: ['userId', 'rejectQuotes'],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.usersRepository)
private readonly usersRepository: UsersRepository,
private readonly globalEventService: GlobalEventService,
private readonly cacheService: CacheService,
private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
const user = await this.cacheService.findUserById(ps.userId);
// Skip if there's nothing to do
if (user.rejectQuotes === ps.rejectQuotes) return;
// Log event first.
// This ensures that we don't "lose" the log if an error occurs
await this.moderationLogService.log(me, ps.rejectQuotes ? 'rejectQuotesUser' : 'acceptQuotesUser', {
userId: user.id,
userUsername: user.username,
userHost: user.host,
});
await this.usersRepository.update(ps.userId, {
rejectQuotes: ps.rejectQuotes,
});
// Synchronize caches and other processes
this.globalEventService.publishInternalEvent('localUserUpdated', { id: ps.userId });
});
}
}

View file

@ -8,6 +8,7 @@ import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { RelayService } from '@/core/RelayService.js';
import { ApiError } from '../../../error.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@ -64,6 +65,7 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private relayService: RelayService,
private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
try {
@ -72,6 +74,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.invalidUrl);
}
await this.moderationLogService.log(me, 'addRelay', {
inbox: ps.inbox,
});
return await this.relayService.addRelay(ps.inbox);
});
}

View file

@ -6,6 +6,7 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { RelayService } from '@/core/RelayService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@ -27,9 +28,13 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private relayService: RelayService,
private readonly moderationLogService: ModerationLogService,
) {
super(meta, paramDef, async (ps, me) => {
return await this.relayService.removeRelay(ps.inbox);
await this.moderationLogService.log(me, 'removeRelay', {
inbox: ps.inbox,
});
await this.relayService.removeRelay(ps.inbox);
});
}
}

View file

@ -8,6 +8,9 @@ import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { CacheService } from '@/core/CacheService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
export const meta = {
tags: ['admin'],
@ -29,24 +32,32 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private roleService: RoleService,
private readonly usersRepository: UsersRepository,
private readonly cacheService: CacheService,
private readonly moderationLogService: ModerationLogService,
private readonly roleService: RoleService,
private readonly globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
const user = await this.usersRepository.findOneBy({ id: ps.userId });
if (user == null) {
throw new Error('user not found');
}
const user = await this.cacheService.findUserById(ps.userId);
if (await this.roleService.isModerator(user)) {
throw new Error('cannot silence moderator account');
}
await this.moderationLogService.log(me, 'silenceUser', {
userId: ps.userId,
userUsername: user.username,
userHost: user.host,
});
await this.usersRepository.update(user.id, {
isSilenced: true,
});
this.globalEventService.publishInternalEvent(user.host == null ? 'localUserUpdated' : 'remoteUserUpdated', {
id: user.id,
});
});
}
}

View file

@ -5,8 +5,10 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository, UserProfilesRepository } from '@/models/_.js';
import type { UserProfilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { CacheService } from '@/core/CacheService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
export const meta = {
tags: ['admin'],
@ -27,18 +29,19 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private readonly cacheService: CacheService,
private readonly moderationLogService: ModerationLogService,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
private readonly userProfilesRepository: UserProfilesRepository,
) {
super(meta, paramDef, async (ps, me) => {
const user = await this.usersRepository.findOneBy({ id: ps.userId });
const user = await this.cacheService.findUserById(ps.userId);
if (user == null) {
throw new Error('user not found');
}
await this.moderationLogService.log(me, 'unNsfwUser', {
userId: ps.userId,
userUsername: user.username,
userHost: user.host,
});
await this.userProfilesRepository.update(user.id, {
alwaysMarkNsfw: false,

View file

@ -7,6 +7,9 @@ import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { CacheService } from '@/core/CacheService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
export const meta = {
tags: ['admin'],
@ -28,18 +31,27 @@ export const paramDef = {
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private readonly usersRepository: UsersRepository,
private readonly cacheService: CacheService,
private readonly moderationLogService: ModerationLogService,
private readonly globalEventService: GlobalEventService,
) {
super(meta, paramDef, async (ps, me) => {
const user = await this.usersRepository.findOneBy({ id: ps.userId });
const user = await this.cacheService.findUserById(ps.userId);
if (user == null) {
throw new Error('user not found');
}
await this.moderationLogService.log(me, 'unSilenceUser', {
userId: ps.userId,
userUsername: user.username,
userHost: user.host,
});
await this.usersRepository.update(user.id, {
isSilenced: false,
});
this.globalEventService.publishInternalEvent(user.host == null ? 'localUserUpdated' : 'remoteUserUpdated', {
id: user.id,
});
});
}
}

View file

@ -149,6 +149,7 @@ export const paramDef = {
enableStatsForFederatedInstances: { type: 'boolean' },
enableServerMachineStats: { type: 'boolean' },
enableAchievements: { type: 'boolean' },
robotsTxt: { type: 'string', nullable: true },
enableIdenticonGeneration: { type: 'boolean' },
serverRules: { type: 'array', items: { type: 'string' } },
bannedEmailDomains: { type: 'array', items: { type: 'string' } },
@ -636,6 +637,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.enableAchievements = ps.enableAchievements;
}
if (ps.robotsTxt !== undefined) {
set.robotsTxt = ps.robotsTxt;
}
if (ps.enableIdenticonGeneration !== undefined) {
set.enableIdenticonGeneration = ps.enableIdenticonGeneration;
}

View file

@ -4,11 +4,10 @@
*/
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { MiNote } from '@/models/Note.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
import { isActor, isPost, getApId } from '@/core/activitypub/type.js';
import { isActor, isPost, getApId, getNullableApId } from '@/core/activitypub/type.js';
import type { SchemaType } from '@/misc/json-schema.js';
import { ApResolverService } from '@/core/activitypub/ApResolverService.js';
import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
@ -18,7 +17,10 @@ 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 { ApRequestService } from '@/core/activitypub/ApRequestService.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
import { ApiError } from '../../error.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
export const meta = {
tags: ['federation'],
@ -26,12 +28,38 @@ export const meta = {
requireCredential: true,
kind: 'read:account',
// Up to 30 calls, then 1 per 1/2 second
limit: {
duration: ms('1minute'),
max: 30,
dripRate: 500,
},
errors: {
federationNotAllowed: {
message: 'Federation for this host is not allowed.',
code: 'FEDERATION_NOT_ALLOWED',
id: '974b799e-1a29-4889-b706-18d4dd93e266',
},
uriInvalid: {
message: 'URI is invalid.',
code: 'URI_INVALID',
id: '1a5eab56-e47b-48c2-8d5e-217b897d70db',
},
requestFailed: {
message: 'Request failed.',
code: 'REQUEST_FAILED',
id: '81b539cf-4f57-4b29-bc98-032c33c0792e',
},
responseInvalid: {
message: 'Response from remote server is invalid.',
code: 'RESPONSE_INVALID',
id: '70193c39-54f3-4813-82f0-70a680f7495b',
},
responseInvalidIdHostNotMatch: {
message: 'Requested URI and response URI host does not match.',
code: 'RESPONSE_INVALID_ID_HOST_NOT_MATCH',
id: 'a2c9c61a-cb72-43ab-a964-3ca5fddb410a',
},
noSuchObject: {
message: 'No such object.',
code: 'NO_SUCH_OBJECT',
@ -94,6 +122,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private apDbResolverService: ApDbResolverService,
private apPersonService: ApPersonService,
private apNoteService: ApNoteService,
private readonly apRequestService: ApRequestService,
private readonly instanceActorService: InstanceActorService,
) {
super(meta, paramDef, async (ps, me) => {
const object = await this.fetchAny(ps.uri, me);
@ -110,7 +140,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
*/
@bindThis
private async fetchAny(uri: string, me: MiLocalUser | null | undefined): Promise<SchemaType<typeof meta['res']> | null> {
if (!this.utilityService.isFederationAllowedUri(uri)) return null;
if (!this.utilityService.isFederationAllowedUri(uri)) {
throw new ApiError(meta.errors.federationNotAllowed);
}
let local = await this.mergePack(me, ...await Promise.all([
this.apDbResolverService.getUserFromApId(uri),
@ -118,6 +150,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
]));
if (local != null) return local;
// No local object found with that uri.
// Before we fetch, resolve the URI in case it has a cross-origin redirect or anything like that.
// Resolver.resolve() uses strict verification, which is overly paranoid for a user-provided lookup.
uri = await this.resolveCanonicalUri(uri); // eslint-disable-line no-param-reassign
if (!this.utilityService.isFederationAllowedUri(uri)) {
throw new ApiError(meta.errors.federationNotAllowed);
}
const host = this.utilityService.extractDbHost(uri);
// local object, not found in db? fail
@ -125,7 +165,40 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// リモートから一旦オブジェクトフェッチ
const resolver = this.apResolverService.createResolver();
const object = await resolver.resolve(uri) as any;
const object = await resolver.resolve(uri).catch((err) => {
if (err instanceof IdentifiableError) {
switch (err.id) {
// resolve
case 'b94fd5b1-0e3b-4678-9df2-dad4cd515ab2':
throw new ApiError(meta.errors.uriInvalid);
case '0dc86cf6-7cd6-4e56-b1e6-5903d62d7ea5':
case 'd592da9f-822f-4d91-83d7-4ceefabcf3d2':
throw new ApiError(meta.errors.requestFailed);
case '09d79f9e-64f1-4316-9cfa-e75c4d091574':
throw new ApiError(meta.errors.federationNotAllowed);
case '72180409-793c-4973-868e-5a118eb5519b':
case 'ad2dc287-75c1-44c4-839d-3d2e64576675':
throw new ApiError(meta.errors.responseInvalid);
case 'fd93c2fa-69a8-440f-880b-bf178e0ec877':
throw new ApiError(meta.errors.responseInvalidIdHostNotMatch);
// resolveLocal
case '02b40cd0-fa92-4b0c-acc9-fb2ada952ab8':
throw new ApiError(meta.errors.uriInvalid);
case 'a9d946e5-d276-47f8-95fb-f04230289bb0':
case '06ae3170-1796-4d93-a697-2611ea6d83b6':
throw new ApiError(meta.errors.noSuchObject);
case '7a5d2fc0-94bc-4db6-b8b8-1bf24a2e23d0':
throw new ApiError(meta.errors.responseInvalid);
}
}
throw new ApiError(meta.errors.requestFailed);
});
if (object.id == null) {
throw new ApiError(meta.errors.responseInvalid);
}
// /@user のような正規id以外で取得できるURIが指定されていた場合、ここで初めて正規URIが確定する
// これはDBに存在する可能性があるため再度DB検索
@ -167,4 +240,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return null;
}
/**
* Resolves an arbitrary URI to its canonical, post-redirect form.
*/
private async resolveCanonicalUri(uri: string): Promise<string> {
const user = await this.instanceActorService.getInstanceActor();
const res = await this.apRequestService.signedGet(uri, user, true);
return getNullableApId(res) ?? uri;
}
}

View file

@ -4,13 +4,13 @@
*/
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { DriveFilesRepository, ChannelsRepository } from '@/models/_.js';
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
import ms from 'ms';
export const meta = {
tags: ['channels'],

View file

@ -4,13 +4,13 @@
*/
import { Inject, Injectable } from '@nestjs/common';
import { Brackets } from 'typeorm';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { DriveFilesRepository } from '@/models/_.js';
import { QueryService } from '@/core/QueryService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { DI } from '@/di-symbols.js';
import { sqlLikeEscape } from '@/misc/sql-like-escape.js';
import { Brackets } from 'typeorm';
export const meta = {
tags: ['drive'],
@ -45,7 +45,7 @@ export const paramDef = {
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] },
searchQuery: { type: 'string', default: '' }
searchQuery: { type: 'string', default: '' },
},
required: [],
} as const;

View file

@ -11,7 +11,6 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../../error.js';
import ms from 'ms';
export const meta = {
tags: ['drive'],

View file

@ -11,7 +11,6 @@ import { RoleService } from '@/core/RoleService.js';
import { DriveService } from '@/core/DriveService.js';
import type { Config } from '@/config.js';
import { ApiError } from '../../../error.js';
import ms from 'ms';
export const meta = {
tags: ['drive'],

View file

@ -42,7 +42,7 @@ export const paramDef = {
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
folderId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
searchQuery: { type: 'string', default: '' }
searchQuery: { type: 'string', default: '' },
},
required: [],
} as const;

View file

@ -10,7 +10,6 @@ import { DriveFolderEntityService } from '@/core/entities/DriveFolderEntityServi
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
import ms from 'ms';
export const meta = {
tags: ['drive'],

View file

@ -13,10 +13,11 @@ export const meta = {
requireCredential: false,
// 2 calls per second
// Up to 10 calls, then 4 / second.
// This allows for reliable automation.
limit: {
duration: 1000,
max: 2,
max: 10,
dripRate: 250,
},
} as const;

View file

@ -4,13 +4,13 @@
*/
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import type { FlashsRepository, UsersRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
import ms from 'ms';
export const meta = {
tags: ['flashs'],
@ -78,7 +78,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
flashId: flash.id,
flashUserId: flash.userId,
flashUserUsername: user.username,
flash,
});
}
});

View file

@ -4,13 +4,13 @@
*/
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { GalleryPostsRepository, UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../../error.js';
import ms from 'ms';
export const meta = {
tags: ['gallery'],
@ -78,7 +78,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
postId: post.id,
postUserId: post.userId,
postUserUsername: user.username,
post,
});
}
});

View file

@ -31,10 +31,12 @@ export const meta = {
},
},
// 3 calls per second
// up to 20 calls, then 1 per second.
// This handles bursty traffic when all tabs reload as a group
limit: {
duration: 1000,
max: 3,
max: 20,
dripSize: 1,
dripRate: 1000,
},
} as const;

View file

@ -5,12 +5,12 @@
import * as OTPAuth from 'otpauth';
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { UserProfilesRepository } from '@/models/_.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import ms from 'ms';
export const meta = {
requireCredential: true,

View file

@ -6,6 +6,7 @@
//import bcrypt from 'bcryptjs';
import * as argon2 from 'argon2';
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DI } from '@/di-symbols.js';
@ -14,7 +15,6 @@ import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/model
import { WebAuthnService } from '@/core/WebAuthnService.js';
import { ApiError } from '@/server/api/error.js';
import { UserAuthService } from '@/core/UserAuthService.js';
import ms from 'ms';
export const meta = {
requireCredential: true,
@ -63,8 +63,8 @@ export const paramDef = {
required: ['password', 'name', 'credential'],
} as const;
// eslint-disable-next-line import/no-default-export
@Injectable()
// eslint-disable-next-line import/no-default-export
export default class extends Endpoint<typeof meta, typeof paramDef> {
constructor(
@Inject(DI.userProfilesRepository)

View file

@ -4,13 +4,13 @@
*/
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/_.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
import ms from 'ms';
export const meta = {
requireCredential: true,

View file

@ -6,13 +6,13 @@
//import bcrypt from 'bcryptjs';
import * as argon2 from 'argon2';
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UserProfilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { WebAuthnService } from '@/core/WebAuthnService.js';
import { ApiError } from '@/server/api/error.js';
import { UserAuthService } from '@/core/UserAuthService.js';
import ms from 'ms';
export const meta = {
requireCredential: true,

View file

@ -8,13 +8,13 @@ import * as argon2 from 'argon2';
import * as OTPAuth from 'otpauth';
import * as QRCode from 'qrcode';
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import type { UserProfilesRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
import { ApiError } from '@/server/api/error.js';
import { UserAuthService } from '@/core/UserAuthService.js';
import ms from 'ms';
export const meta = {
requireCredential: true,

View file

@ -6,6 +6,7 @@
//import bcrypt from 'bcryptjs';
import * as argon2 from 'argon2';
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UserProfilesRepository, UserSecurityKeysRepository } from '@/models/_.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
@ -13,7 +14,6 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '@/server/api/error.js';
import { UserAuthService } from '@/core/UserAuthService.js';
import ms from 'ms';
export const meta = {
requireCredential: true,

View file

@ -6,6 +6,7 @@
//import bcrypt from 'bcryptjs';
import * as argon2 from 'argon2';
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { UserProfilesRepository } from '@/models/_.js';
@ -13,7 +14,6 @@ import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '@/server/api/error.js';
import { UserAuthService } from '@/core/UserAuthService.js';
import ms from 'ms';
export const meta = {
requireCredential: true,

View file

@ -5,13 +5,13 @@
//import bcrypt from 'bcryptjs';
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UserSecurityKeysRepository } from '@/models/_.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
import ms from 'ms';
export const meta = {
requireCredential: true,

View file

@ -93,7 +93,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
name: token.name ?? token.app?.name,
createdAt: this.idService.parse(token.id).date.toISOString(),
lastUsedAt: token.lastUsedAt?.toISOString(),
permission: token.permission,
permission: token.app ? token.app.permission : token.permission,
})));
});
}

View file

@ -6,11 +6,11 @@
//import bcrypt from 'bcryptjs';
import * as argon2 from 'argon2';
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UserProfilesRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { UserAuthService } from '@/core/UserAuthService.js';
import ms from 'ms';
export const meta = {
requireCredential: true,

View file

@ -3,8 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { DI } from '@/di-symbols.js';
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { AchievementService, ACHIEVEMENT_TYPES } from '@/core/AchievementService.js';
import type { MiMeta } from '@/models/_.js';

View file

@ -6,12 +6,12 @@
//import bcrypt from 'bcryptjs';
import * as argon2 from 'argon2';
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import type { UsersRepository, UserProfilesRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DeleteAccountService } from '@/core/DeleteAccountService.js';
import { DI } from '@/di-symbols.js';
import { UserAuthService } from '@/core/UserAuthService.js';
import ms from 'ms';
export const meta = {
requireCredential: true,

View file

@ -6,12 +6,12 @@
//import bcrypt from 'bcryptjs';
import * as argon2 from 'argon2';
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { UsersRepository, UserProfilesRepository } from '@/models/_.js';
import generateUserToken from '@/misc/generate-native-user-token.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import ms from 'ms';
export const meta = {
requireCredential: true,

View file

@ -133,6 +133,12 @@ export const meta = {
id: '0b3f9f6a-2f4d-4b1f-9fb4-49d3a2fd7191',
httpStatusCode: 422,
},
maxCwLength: {
message: 'You tried setting a default content warning which is too long.',
code: 'MAX_CW_LENGTH',
id: '7004c478-bda3-4b4f-acb2-4316398c9d52',
},
},
res: {
@ -243,6 +249,12 @@ export const paramDef = {
uniqueItems: true,
items: { type: 'string' },
},
defaultCW: { type: 'string', nullable: true },
defaultCWPriority: {
type: 'string',
enum: ['default', 'parent', 'defaultParent', 'parentDefault'],
nullable: false,
},
},
} as const;
@ -494,6 +506,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
updates.alsoKnownAs = newAlsoKnownAs.size > 0 ? Array.from(newAlsoKnownAs) : null;
}
let defaultCW = ps.defaultCW;
if (defaultCW !== undefined) {
if (defaultCW === '') defaultCW = null;
if (defaultCW && defaultCW.length > this.config.maxCwLength) {
throw new ApiError(meta.errors.maxCwLength);
}
profileUpdates.defaultCW = defaultCW;
}
if (ps.defaultCWPriority !== undefined) {
profileUpdates.defaultCWPriority = ps.defaultCWPriority;
}
//#region emojis/tags
let emojis = [] as string[];
@ -592,7 +617,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const html = await this.httpRequestService.getHtml(url);
const { window } = new JSDOM(html);
const doc = window.document;
const doc: Document = window.document;
const myLink = `${this.config.url}/@${user.username}`;

View file

@ -4,13 +4,13 @@
*/
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { MutingsRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { GetterService } from '@/server/api/GetterService.js';
import { UserMutingService } from '@/core/UserMutingService.js';
import { ApiError } from '../../error.js';
import ms from 'ms';
export const meta = {
tags: ['account'],

View file

@ -143,6 +143,12 @@ export const meta = {
code: 'CONTAINS_TOO_MANY_MENTIONS',
id: '4de0363a-3046-481b-9b0f-feff3e211025',
},
quoteDisabledForUser: {
message: 'You do not have permission to create quote posts.',
code: 'QUOTE_DISABLED_FOR_USER',
id: '1c0ea108-d1e3-4e8e-aa3f-4d2487626153',
},
},
} as const;
@ -415,6 +421,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.containsProhibitedWords);
} else if (e.id === '9f466dab-c856-48cd-9e65-ff90ff750580') {
throw new ApiError(meta.errors.containsTooManyMentions);
} else if (e.id === '1c0ea108-d1e3-4e8e-aa3f-4d2487626153') {
throw new ApiError(meta.errors.quoteDisabledForUser);
}
}
throw e;

View file

@ -176,6 +176,12 @@ export const meta = {
id: '33510210-8452-094c-6227-4a6c05d99f02',
},
quoteDisabledForUser: {
message: 'You do not have permission to create quote posts.',
code: 'QUOTE_DISABLED_FOR_USER',
id: '1c0ea108-d1e3-4e8e-aa3f-4d2487626153',
},
containsProhibitedWords: {
message: 'Cannot post because it contains prohibited words.',
code: 'CONTAINS_PROHIBITED_WORDS',
@ -469,6 +475,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.containsProhibitedWords);
} else if (e.id === '9f466dab-c856-48cd-9e65-ff90ff750580') {
throw new ApiError(meta.errors.containsTooManyMentions);
} else if (e.id === '1c0ea108-d1e3-4e8e-aa3f-4d2487626153') {
throw new ApiError(meta.errors.quoteDisabledForUser);
}
}
throw e;

View file

@ -4,12 +4,12 @@
*/
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { GetterService } from '@/server/api/GetterService.js';
import { DI } from '@/di-symbols.js';
import type { NoteFavoritesRepository } from '@/models/_.js';
import { ApiError } from '../../../error.js';
import ms from 'ms';
export const meta = {
tags: ['notes', 'favorites'],

View file

@ -12,8 +12,8 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
import { CacheService } from '@/core/CacheService.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['notes'],

View file

@ -1,5 +1,5 @@
import { DI } from '@/di-symbols.js';
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { GetterService } from '@/server/api/GetterService.js';
import { ReactionService } from '@/core/ReactionService.js';

View file

@ -14,12 +14,10 @@ import type {
NotesRepository,
BlockingsRepository,
DriveFilesRepository,
ChannelsRepository,
NoteScheduleRepository,
} from '@/models/_.js';
import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiNote } from '@/models/Note.js';
import type { MiChannel } from '@/models/Channel.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { QueueService } from '@/core/QueueService.js';
@ -210,9 +208,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
@Inject(DI.channelsRepository)
private channelsRepository: ChannelsRepository,
private queueService: QueueService,
private roleService: RoleService,
private idService: IdService,

View file

@ -7,12 +7,14 @@ import ms from 'ms';
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import type { MiNote, MiNoteSchedule, NoteScheduleRepository } from '@/models/_.js';
import type { MiNote, MiUser, MiNoteSchedule, NoteScheduleRepository, NotesRepository } from '@/models/_.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { QueryService } from '@/core/QueryService.js';
import { Packed } from '@/misc/json-schema.js';
import { noteVisibilities } from '@/types.js';
import { bindThis } from '@/decorators.js';
export const meta = {
tags: ['notes'],
@ -81,7 +83,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.noteScheduleRepository)
private noteScheduleRepository: NoteScheduleRepository,
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService,
private driveFileEntityService: DriveFileEntityService,
private queryService: QueryService,
) {
@ -106,6 +112,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
userId: string;
scheduledAt: string;
}[] = await Promise.all(scheduleNotes.map(async (item: MiNoteSchedule) => {
const renote = await this.fetchNote(item.note.renote, me);
const reply = await this.fetchNote(item.note.reply, me);
return {
...item,
scheduledAt: item.scheduledAt.toISOString(),
@ -115,12 +124,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
user: user,
visibility: item.note.visibility ?? 'public',
reactionAcceptance: item.note.reactionAcceptance ?? null,
visibleUsers: item.note.visibleUsers ? await userEntityService.packMany(item.note.visibleUsers.map(u => u.id), me) : [],
visibleUsers: item.note.visibleUsers ? await this.userEntityService.packMany(item.note.visibleUsers.map(u => u.id), me) : [],
fileIds: item.note.files ? item.note.files : [],
files: await this.driveFileEntityService.packManyByIds(item.note.files),
createdAt: item.scheduledAt.toISOString(),
isSchedule: true,
id: item.id,
renote, reply,
renoteId: item.note.renote,
replyId: item.note.reply,
},
};
}));
@ -128,4 +140,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return scheduleNotesPack;
});
}
@bindThis
private async fetchNote(
id: MiNote['id'] | null | undefined,
me: MiUser,
): Promise<Packed<'Note'> | null> {
if (id) {
const note = await this.notesRepository.findOneBy({ id });
if (note) {
note.reactionAndUserPairCache ??= [];
return this.noteEntityService.pack(note, me);
}
}
return null;
}
}

View file

@ -87,7 +87,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'), ps.sinceId, ps.untilId)
.andWhere('note.visibility = \'public\'')
.andWhere("note.visibility IN ('public', 'home')") // keep in sync with NoteCreateService call to `hashtagService.updateHashtags()`
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')

View file

@ -5,7 +5,7 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { SearchService } from '@/core/SearchService.js';
import { fileTypeCategories, SearchService } from '@/core/SearchService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
@ -52,7 +52,11 @@ export const paramDef = {
type: 'string',
description: 'The local host is represented with `.`.',
},
filetype: { type: 'string', nullable: true },
filetype: {
type: 'string',
nullable: true,
enum: fileTypeCategories,
},
userId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
channelId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
order: { type: 'string' },

View file

@ -7,7 +7,7 @@ import ms from 'ms';
import { Inject, Injectable } from '@nestjs/common';
import type { DriveFilesRepository, PagesRepository } from '@/models/_.js';
import { IdService } from '@/core/IdService.js';
import { MiPage } from '@/models/Page.js';
import { MiPage, pageNameSchema } from '@/models/Page.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { PageEntityService } from '@/core/entities/PageEntityService.js';
import { DI } from '@/di-symbols.js';
@ -51,7 +51,7 @@ export const paramDef = {
type: 'object',
properties: {
title: { type: 'string' },
name: { type: 'string', minLength: 1 },
name: { ...pageNameSchema, minLength: 1 },
summary: { type: 'string', nullable: true },
content: { type: 'array', items: {
type: 'object', additionalProperties: true,

View file

@ -4,13 +4,13 @@
*/
import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import type { PagesRepository, UsersRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
import ms from 'ms';
export const meta = {
tags: ['pages'],
@ -79,7 +79,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
pageId: page.id,
pageUserId: page.userId,
pageUserUsername: user.username,
page,
});
}
});

View file

@ -10,6 +10,7 @@ import type { PagesRepository, DriveFilesRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
import { pageNameSchema } from '@/models/Page.js';
export const meta = {
tags: ['pages'],
@ -31,13 +32,11 @@ export const meta = {
code: 'NO_SUCH_PAGE',
id: '21149b9e-3616-4778-9592-c4ce89f5a864',
},
accessDenied: {
message: 'Access denied.',
code: 'ACCESS_DENIED',
id: '3c15cd52-3b4b-4274-967d-6456fc4f792b',
},
noSuchFile: {
message: 'No such file.',
code: 'NO_SUCH_FILE',
@ -56,7 +55,7 @@ export const paramDef = {
properties: {
pageId: { type: 'string', format: 'misskey:id' },
title: { type: 'string' },
name: { type: 'string', minLength: 1 },
name: { ...pageNameSchema, minLength: 1 },
summary: { type: 'string', nullable: true },
content: { type: 'array', items: {
type: 'object', additionalProperties: true,
@ -102,15 +101,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
}
await this.pagesRepository.findBy({
id: Not(ps.pageId),
userId: me.id,
name: ps.name,
}).then(result => {
if (result.length > 0) {
throw new ApiError(meta.errors.nameAlreadyExists);
}
});
if (ps.name != null) {
await this.pagesRepository.findBy({
id: Not(ps.pageId),
userId: me.id,
name: ps.name,
}).then(result => {
if (result.length > 0) {
throw new ApiError(meta.errors.nameAlreadyExists);
}
});
}
await this.pagesRepository.update(page.id, {
updatedAt: new Date(),

View file

@ -64,10 +64,11 @@ export const meta = {
},
},
// 2 calls per second
// 24 calls, then 7 per second-ish (1 for each type of server info graph)
limit: {
duration: 1000,
max: 2,
max: 24,
dripSize: 7,
dripRate: 900,
},
} as const;

View file

@ -58,6 +58,14 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
isInstanceMuted: {
type: 'boolean',
optional: true, nullable: false,
},
memo: {
type: 'string',
optional: true, nullable: true,
},
},
},
{
@ -103,6 +111,14 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
isInstanceMuted: {
type: 'boolean',
optional: true, nullable: false,
},
memo: {
type: 'string',
optional: true, nullable: true,
},
},
},
},

View file

@ -57,10 +57,10 @@ export const meta = {
},
},
// 5 calls per 2 seconds
// up to 50 calls @ 4 per second
limit: {
duration: 1000 * 2,
max: 5,
max: 50,
dripRate: 250,
},
} as const;

View file

@ -0,0 +1,126 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { CustomEmojiService, fetchEmojisHostTypes, fetchEmojisSortKeys } from '@/core/CustomEmojiService.js';
export const meta = {
tags: ['admin'],
requireCredential: true,
requireRolePolicy: 'canManageCustomEmojis',
kind: 'read:admin:emoji',
res: {
type: 'object',
properties: {
emojis: {
type: 'array',
items: {
type: 'object',
ref: 'EmojiDetailedAdmin',
},
},
count: { type: 'integer' },
allCount: { type: 'integer' },
allPages: { type: 'integer' },
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
query: {
type: 'object',
nullable: true,
properties: {
updatedAtFrom: { type: 'string' },
updatedAtTo: { type: 'string' },
name: { type: 'string' },
host: { type: 'string' },
uri: { type: 'string' },
publicUrl: { type: 'string' },
originalUrl: { type: 'string' },
type: { type: 'string' },
aliases: { type: 'string' },
category: { type: 'string' },
license: { type: 'string' },
isSensitive: { type: 'boolean' },
localOnly: { type: 'boolean' },
hostType: {
type: 'string',
enum: fetchEmojisHostTypes,
default: 'all',
},
roleIds: {
type: 'array',
items: { type: 'string', format: 'misskey:id' },
},
},
},
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
page: { type: 'integer' },
sortKeys: {
type: 'array',
default: ['-id'],
items: {
type: 'string',
enum: fetchEmojisSortKeys,
},
},
},
required: [],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private customEmojiService: CustomEmojiService,
private emojiEntityService: EmojiEntityService,
) {
super(meta, paramDef, async (ps, me) => {
const q = ps.query;
const result = await this.customEmojiService.fetchEmojis(
{
query: {
updatedAtFrom: q?.updatedAtFrom,
updatedAtTo: q?.updatedAtTo,
name: q?.name,
host: q?.host,
uri: q?.uri,
publicUrl: q?.publicUrl,
type: q?.type,
aliases: q?.aliases,
category: q?.category,
license: q?.license,
isSensitive: q?.isSensitive,
localOnly: q?.localOnly,
hostType: q?.hostType,
roleIds: q?.roleIds,
},
sinceId: ps.sinceId,
untilId: ps.untilId,
},
{
limit: ps.limit,
page: ps.page,
sortKeys: ps.sortKeys,
},
);
return {
emojis: await this.emojiEntityService.packDetailedAdminMany(result.emojis),
count: result.count,
allCount: result.allCount,
allPages: result.allPages,
};
});
}
}

View file

@ -0,0 +1,84 @@
/*
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Inject, Injectable } from '@nestjs/common';
import { IsNull } from 'typeorm';
import { DI } from '@/di-symbols.js';
import { QueryService } from '@/core/QueryService.js';
import type { MiNote, NotesRepository } from '@/models/_.js';
import type { MiLocalUser } from '@/models/User.js';
import { ApiError } from '../error.js';
/**
* Utility service for accessing data with Mastodon semantics
*/
@Injectable()
export class MastodonDataService {
constructor(
@Inject(DI.notesRepository)
private readonly notesRepository: NotesRepository,
@Inject(QueryService)
private readonly queryService: QueryService,
) {}
/**
* Fetches a note in the context of the current user, and throws an exception if not found.
*/
public async requireNote(noteId: string, me?: MiLocalUser | null): Promise<MiNote> {
const note = await this.getNote(noteId, me);
if (!note) {
throw new ApiError({
message: 'No such note.',
code: 'NO_SUCH_NOTE',
id: '24fcbfc6-2e37-42b6-8388-c29b3861a08d',
kind: 'client',
httpStatusCode: 404,
});
}
return note;
}
/**
* Fetches a note in the context of the current user.
*/
public async getNote(noteId: string, me?: MiLocalUser | null): Promise<MiNote | null> {
// Root query: note + required dependencies
const query = this.notesRepository
.createQueryBuilder('note')
.where('note.id = :noteId', { noteId })
.innerJoinAndSelect('note.user', 'user');
// Restrict visibility
this.queryService.generateVisibilityQuery(query, me);
if (me) {
this.queryService.generateBlockedUserQuery(query, me);
}
return await query.getOne();
}
/**
* Checks where the current user has made a reblog / boost / pure renote of a given target note.
*/
public async hasReblog(noteId: string, me: MiLocalUser | null | undefined): Promise<boolean> {
if (!me) return false;
return await this.notesRepository.existsBy({
// Reblog of the target note by me
userId: me.id,
renoteId: noteId,
// That is pure (not a quote)
text: IsNull(),
cw: IsNull(),
replyId: IsNull(),
hasPoll: false,
fileIds: '{}',
});
}
}

View file

@ -0,0 +1,39 @@
/*
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import Logger, { Data } from '@/logger.js';
import { LoggerService } from '@/core/LoggerService.js';
@Injectable()
export class MastodonLogger {
public readonly logger: Logger;
constructor(loggerService: LoggerService) {
this.logger = loggerService.getLogger('masto-api');
}
public error(endpoint: string, error: Data): void {
this.logger.error(`Error in mastodon API endpoint ${endpoint}:`, error);
}
}
export function getErrorData(error: unknown): Data {
if (error == null) return {};
if (typeof(error) === 'string') return error;
if (typeof(error) === 'object') {
if ('response' in error) {
if (typeof(error.response) === 'object' && error.response) {
if ('data' in error.response) {
if (typeof(error.response.data) === 'object' && error.response.data) {
return error.response.data as Record<string, unknown>;
}
}
}
}
return error as Record<string, unknown>;
}
return { error };
}

View file

@ -9,18 +9,32 @@ import mfm from '@transfem-org/sfm-js';
import { DI } from '@/di-symbols.js';
import { MfmService } from '@/core/MfmService.js';
import type { Config } from '@/config.js';
import type { IMentionedRemoteUsers } from '@/models/Note.js';
import type { MiUser } from '@/models/User.js';
import type { NoteEditRepository, NotesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import { IMentionedRemoteUsers, MiNote } from '@/models/Note.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
import type { NoteEditRepository, UserProfilesRepository } from '@/models/_.js';
import { awaitAll } from '@/misc/prelude/await-all.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { IdService } from '@/core/IdService.js';
import { GetterService } from '../GetterService.js';
import type { Packed } from '@/misc/json-schema.js';
import { MastodonDataService } from '@/server/api/mastodon/MastodonDataService.js';
import { GetterService } from '@/server/api/GetterService.js';
export enum IdConvertType {
MastodonId,
SharkeyId,
// Missing from Megalodon apparently
// https://docs.joinmastodon.org/entities/StatusEdit/
export interface StatusEdit {
content: string;
spoiler_text: string;
sensitive: boolean;
created_at: string;
account: MastodonEntity.Account;
poll?: {
options: {
title: string;
}[]
},
media_attachments: MastodonEntity.Attachment[],
emojis: MastodonEntity.Emoji[],
}
export const escapeMFM = (text: string): string => text
@ -36,27 +50,25 @@ export const escapeMFM = (text: string): string => text
export class MastoConverters {
constructor(
@Inject(DI.config)
private config: Config,
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
private readonly config: Config,
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
private readonly userProfilesRepository: UserProfilesRepository,
@Inject(DI.noteEditRepository)
private noteEditRepository: NoteEditRepository,
private readonly noteEditRepository: NoteEditRepository,
private mfmService: MfmService,
private getterService: GetterService,
private customEmojiService: CustomEmojiService,
private idService: IdService,
private driveFileEntityService: DriveFileEntityService,
) {
}
private readonly mfmService: MfmService,
private readonly getterService: GetterService,
private readonly customEmojiService: CustomEmojiService,
private readonly idService: IdService,
private readonly driveFileEntityService: DriveFileEntityService,
private readonly mastodonDataService: MastodonDataService,
) {}
private encode(u: MiUser, m: IMentionedRemoteUsers): Entity.Mention {
private encode(u: MiUser, m: IMentionedRemoteUsers): MastodonEntity.Mention {
let acct = u.username;
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
let acctUrl = `https://${u.host || this.config.host}/@${u.username}`;
let url: string | null = null;
if (u.host) {
@ -89,7 +101,11 @@ export class MastoConverters {
return 'unknown';
}
public encodeFile(f: any): Entity.Attachment {
public encodeFile(f: Packed<'DriveFile'>): MastodonEntity.Attachment {
const { width, height } = f.properties;
const size = (width && height) ? `${width}x${height}` : undefined;
const aspect = (width && height) ? (width / height) : undefined;
return {
id: f.id,
type: this.fileType(f.type),
@ -98,11 +114,19 @@ export class MastoConverters {
preview_url: f.thumbnailUrl,
text_url: f.url,
meta: {
width: f.properties.width,
height: f.properties.height,
original: {
width,
height,
size,
aspect,
},
width,
height,
size,
aspect,
},
description: f.comment ? f.comment : null,
blurhash: f.blurhash ? f.blurhash : null,
description: f.comment ?? null,
blurhash: f.blurhash ?? null,
};
}
@ -112,7 +136,7 @@ export class MastoConverters {
});
}
private async encodeField(f: Entity.Field): Promise<Entity.Field> {
private async encodeField(f: Entity.Field): Promise<MastodonEntity.Field> {
return {
name: f.name,
value: await this.mfmService.toMastoApiHtml(mfm.parse(f.value), [], true) ?? escapeMFM(f.value),
@ -120,7 +144,7 @@ export class MastoConverters {
};
}
public async convertAccount(account: Entity.Account | MiUser) {
public async convertAccount(account: Entity.Account | MiUser): Promise<MastodonEntity.Account> {
const user = await this.getUser(account.id);
const profile = await this.userProfilesRepository.findOneBy({ userId: user.id });
const emojis = await this.customEmojiService.populateEmojis(user.emojis, user.host ? user.host : this.config.host);
@ -137,6 +161,7 @@ export class MastoConverters {
});
const fqn = `${user.username}@${user.host ?? this.config.hostname}`;
let acct = user.username;
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
let acctUrl = `https://${user.host || this.config.host}/@${user.username}`;
const acctUri = `https://${this.config.host}/users/${user.id}`;
if (user.host) {
@ -166,19 +191,23 @@ export class MastoConverters {
fields: Promise.all(profile?.fields.map(async p => this.encodeField(p)) ?? []),
bot: user.isBot,
discoverable: user.isExplorable,
noindex: user.noindex,
group: null,
suspended: user.isSuspended,
limited: user.isSilenced,
});
}
public async getEdits(id: string) {
const note = await this.getterService.getNote(id);
public async getEdits(id: string, me?: MiLocalUser | null) {
const note = await this.mastodonDataService.getNote(id, me);
if (!note) {
return {};
return [];
}
const noteUser = await this.getUser(note.userId).then(async (p) => await this.convertAccount(p));
const edits = await this.noteEditRepository.find({ where: { noteId: note.id }, order: { id: 'ASC' } });
const history: Promise<any>[] = [];
const history: Promise<StatusEdit>[] = [];
// TODO this looks wrong, according to mastodon docs
let lastDate = this.idService.parse(note.id).date;
for (const edit of edits) {
const files = this.driveFileEntityService.packManyByIds(edit.fileIds);
@ -187,9 +216,8 @@ export class MastoConverters {
content: this.mfmService.toMastoApiHtml(mfm.parse(edit.newText ?? ''), JSON.parse(note.mentionedRemoteUsers)).then(p => p ?? ''),
created_at: lastDate.toISOString(),
emojis: [],
sensitive: files.then(files => files.length > 0 ? files.some((f) => f.isSensitive) : false),
sensitive: edit.cw != null && edit.cw.length > 0,
spoiler_text: edit.cw ?? '',
poll: null,
media_attachments: files.then(files => files.length > 0 ? files.map((f) => this.encodeFile(f)) : []),
};
lastDate = edit.updatedAt;
@ -199,15 +227,16 @@ export class MastoConverters {
return await Promise.all(history);
}
private async convertReblog(status: Entity.Status | null): Promise<any> {
private async convertReblog(status: Entity.Status | null, me?: MiLocalUser | null): Promise<MastodonEntity.Status | null> {
if (!status) return null;
return await this.convertStatus(status);
return await this.convertStatus(status, me);
}
public async convertStatus(status: Entity.Status) {
public async convertStatus(status: Entity.Status, me?: MiLocalUser | null): Promise<MastodonEntity.Status> {
const convertedAccount = this.convertAccount(status.account);
const note = await this.getterService.getNote(status.id);
const note = await this.mastodonDataService.requireNote(status.id, me);
const noteUser = await this.getUser(status.account.id);
const mentionedRemoteUsers = JSON.parse(note.mentionedRemoteUsers);
const emojis = await this.customEmojiService.populateEmojis(note.emojis, noteUser.host ? noteUser.host : this.config.host);
const emoji: Entity.Emoji[] = [];
@ -224,7 +253,7 @@ export class MastoConverters {
const mentions = Promise.all(note.mentions.map(p =>
this.getUser(p)
.then(u => this.encode(u, JSON.parse(note.mentionedRemoteUsers)))
.then(u => this.encode(u, mentionedRemoteUsers))
.catch(() => null)))
.then(p => p.filter(m => m)) as Promise<Entity.Mention[]>;
@ -235,20 +264,26 @@ export class MastoConverters {
} as Entity.Tag;
});
const isQuote = note.renoteId && note.text ? true : false;
// This must mirror the usual isQuote / isPureRenote logic used elsewhere.
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const isQuote = note.renoteId && (note.text || note.cw || note.fileIds.length > 0 || note.hasPoll || note.replyId);
const renote = note.renoteId ? this.getterService.getNote(note.renoteId) : null;
const renote: Promise<MiNote> | null = note.renoteId ? this.mastodonDataService.requireNote(note.renoteId, me) : null;
const quoteUri = Promise.resolve(renote).then(renote => {
if (!renote || !isQuote) return null;
return renote.url ?? renote.uri ?? `${this.config.url}/notes/${renote.id}`;
});
const content = note.text !== null
? quoteUri.then(quoteUri => this.mfmService.toMastoApiHtml(mfm.parse(note.text!), JSON.parse(note.mentionedRemoteUsers), false, quoteUri))
.then(p => p ?? escapeMFM(note.text!))
const text = note.text;
const content = text !== null
? quoteUri
.then(quoteUri => this.mfmService.toMastoApiHtml(mfm.parse(text), mentionedRemoteUsers, false, quoteUri))
.then(p => p ?? escapeMFM(text))
: '';
const reblogged = await this.mastodonDataService.hasReblog(note.id, me);
// noinspection ES6MissingAwait
return await awaitAll({
id: note.id,
@ -257,7 +292,7 @@ export class MastoConverters {
account: convertedAccount,
in_reply_to_id: note.replyId,
in_reply_to_account_id: note.replyUserId,
reblog: !isQuote ? await this.convertReblog(status.reblog) : null,
reblog: !isQuote ? await this.convertReblog(status.reblog, me) : null,
content: content,
content_type: 'text/x.misskeymarkdown',
text: note.text,
@ -266,34 +301,51 @@ export class MastoConverters {
replies_count: note.repliesCount,
reblogs_count: note.renoteCount,
favourites_count: status.favourites_count,
reblogged: false,
reblogged,
favourited: status.favourited,
muted: status.muted,
sensitive: status.sensitive,
spoiler_text: note.cw ? note.cw : '',
spoiler_text: note.cw ?? '',
visibility: status.visibility,
media_attachments: status.media_attachments,
media_attachments: status.media_attachments.map(a => convertAttachment(a)),
mentions: mentions,
tags: tags,
card: null, //FIXME
poll: status.poll ?? null,
application: null, //FIXME
language: null, //FIXME
pinned: false,
pinned: false, //FIXME
reactions: status.emoji_reactions,
emoji_reactions: status.emoji_reactions,
bookmarked: false,
quote: isQuote ? await this.convertReblog(status.reblog) : null,
// optional chaining cannot be used, as it evaluates to undefined, not null
edited_at: note.updatedAt ? note.updatedAt.toISOString() : null,
bookmarked: false, //FIXME
quote: isQuote ? await this.convertReblog(status.reblog, me) : null,
edited_at: note.updatedAt?.toISOString() ?? null,
});
}
public async convertConversation(conversation: Entity.Conversation, me?: MiLocalUser | null): Promise<MastodonEntity.Conversation> {
return {
id: conversation.id,
accounts: await Promise.all(conversation.accounts.map(a => this.convertAccount(a))),
last_status: conversation.last_status ? await this.convertStatus(conversation.last_status, me) : null,
unread: conversation.unread,
};
}
public async convertNotification(notification: Entity.Notification, me?: MiLocalUser | null): Promise<MastodonEntity.Notification> {
return {
account: await this.convertAccount(notification.account),
created_at: notification.created_at,
id: notification.id,
status: notification.status ? await this.convertStatus(notification.status, me) : undefined,
type: notification.type,
};
}
}
function simpleConvert(data: any) {
function simpleConvert<T>(data: T): T {
// copy the object to bypass weird pass by reference bugs
const result = Object.assign({}, data);
return result;
return Object.assign({}, data);
}
export function convertAccount(account: Entity.Account) {
@ -302,8 +354,30 @@ export function convertAccount(account: Entity.Account) {
export function convertAnnouncement(announcement: Entity.Announcement) {
return simpleConvert(announcement);
}
export function convertAttachment(attachment: Entity.Attachment) {
return simpleConvert(attachment);
export function convertAttachment(attachment: Entity.Attachment): MastodonEntity.Attachment {
const { width, height } = attachment.meta?.original ?? attachment.meta ?? {};
const size = (width && height) ? `${width}x${height}` : undefined;
const aspect = (width && height) ? (width / height) : undefined;
return {
...attachment,
meta: attachment.meta ? {
...attachment.meta,
original: {
...attachment.meta.original,
width,
height,
size,
aspect,
frame_rate: String(attachment.meta.fps),
duration: attachment.meta.duration,
bitrate: attachment.meta.audio_bitrate ? parseInt(attachment.meta.audio_bitrate) : undefined,
},
width,
height,
size,
aspect,
} : null,
};
}
export function convertFilter(filter: Entity.Filter) {
return simpleConvert(filter);
@ -315,45 +389,40 @@ export function convertFeaturedTag(tag: Entity.FeaturedTag) {
return simpleConvert(tag);
}
export function convertNotification(notification: Entity.Notification) {
notification.account = convertAccount(notification.account);
if (notification.status) notification.status = convertStatus(notification.status);
return notification;
}
export function convertPoll(poll: Entity.Poll) {
return simpleConvert(poll);
}
// noinspection JSUnusedGlobalSymbols
export function convertReaction(reaction: Entity.Reaction) {
if (reaction.accounts) {
reaction.accounts = reaction.accounts.map(convertAccount);
}
return reaction;
}
export function convertRelationship(relationship: Entity.Relationship) {
return simpleConvert(relationship);
}
export function convertStatus(status: Entity.Status) {
status.account = convertAccount(status.account);
status.media_attachments = status.media_attachments.map((attachment) =>
convertAttachment(attachment),
);
if (status.poll) status.poll = convertPoll(status.poll);
if (status.reblog) status.reblog = convertStatus(status.reblog);
return status;
// Megalodon sometimes returns broken / stubbed relationship data
export function convertRelationship(relationship: Partial<Entity.Relationship> & { id: string }): MastodonEntity.Relationship {
return {
id: relationship.id,
following: relationship.following ?? false,
showing_reblogs: relationship.showing_reblogs ?? true,
notifying: relationship.notifying ?? true,
languages: [],
followed_by: relationship.followed_by ?? false,
blocking: relationship.blocking ?? false,
blocked_by: relationship.blocked_by ?? false,
muting: relationship.muting ?? false,
muting_notifications: relationship.muting_notifications ?? false,
requested: relationship.requested ?? false,
requested_by: relationship.requested_by ?? false,
domain_blocking: relationship.domain_blocking ?? false,
endorsed: relationship.endorsed ?? false,
note: relationship.note ?? '',
};
}
// noinspection JSUnusedGlobalSymbols
export function convertStatusSource(status: Entity.StatusSource) {
return simpleConvert(status);
}
export function convertConversation(conversation: Entity.Conversation) {
conversation.accounts = conversation.accounts.map(convertAccount);
if (conversation.last_status) {
conversation.last_status = convertStatus(conversation.last_status);
}
return conversation;
}

View file

@ -3,273 +3,149 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { parseTimelineArgs, TimelineArgs } from '@/server/api/mastodon/timelineArgs.js';
import { MiLocalUser } from '@/models/User.js';
import { MastoConverters, convertRelationship } from '../converters.js';
import { argsToBools, limitToInt } from './timeline.js';
import type { MegalodonInterface } from 'megalodon';
import type { FastifyRequest } from 'fastify';
import { NoteEditRepository, NotesRepository, UsersRepository } from '@/models/_.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { Config } from '@/config.js';
import { Injectable } from '@nestjs/common';
const relationshipModel = {
id: '',
following: false,
followed_by: false,
delivery_following: false,
blocking: false,
blocked_by: false,
muting: false,
muting_notifications: false,
requested: false,
domain_blocking: false,
showing_reblogs: false,
endorsed: false,
notifying: false,
note: '',
};
export interface ApiAccountMastodonRoute {
Params: { id?: string },
Querystring: TimelineArgs & { acct?: string },
Body: { notifications?: boolean }
}
@Injectable()
export class ApiAccountMastodon {
private request: FastifyRequest;
private client: MegalodonInterface;
private BASE_URL: string;
constructor(request: FastifyRequest, client: MegalodonInterface, BASE_URL: string, private mastoconverter: MastoConverters) {
this.request = request;
this.client = client;
this.BASE_URL = BASE_URL;
}
constructor(
private readonly request: FastifyRequest<ApiAccountMastodonRoute>,
private readonly client: MegalodonInterface,
private readonly me: MiLocalUser | null,
private readonly mastoConverters: MastoConverters,
) {}
public async verifyCredentials() {
try {
const data = await this.client.verifyAccountCredentials();
const acct = await this.mastoconverter.convertAccount(data.data);
const newAcct = Object.assign({}, acct, {
source: {
note: acct.note,
fields: acct.fields,
privacy: '',
sensitive: false,
language: '',
},
});
return newAcct;
} catch (e: any) {
/* console.error(e);
console.error(e.response.data); */
return e.response;
}
const data = await this.client.verifyAccountCredentials();
const acct = await this.mastoConverters.convertAccount(data.data);
return Object.assign({}, acct, {
source: {
note: acct.note,
fields: acct.fields,
privacy: '',
sensitive: false,
language: '',
},
});
}
public async lookup() {
try {
const data = await this.client.search((this.request.query as any).acct, { type: 'accounts' });
return this.mastoconverter.convertAccount(data.data.accounts[0]);
} catch (e: any) {
/* console.error(e)
console.error(e.response.data); */
return e.response;
}
if (!this.request.query.acct) throw new Error('Missing required property "acct"');
const data = await this.client.search(this.request.query.acct, { type: 'accounts' });
return this.mastoConverters.convertAccount(data.data.accounts[0]);
}
public async getRelationships(users: [string]) {
try {
relationshipModel.id = users.toString() || '1';
if (!(users.length > 0)) {
return [relationshipModel];
}
const reqIds = [];
for (let i = 0; i < users.length; i++) {
reqIds.push(users[i]);
}
const data = await this.client.getRelationships(reqIds);
return data.data.map((relationship) => convertRelationship(relationship));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
return e.response.data;
}
public async getRelationships(reqIds: string[]) {
const data = await this.client.getRelationships(reqIds);
return data.data.map(relationship => convertRelationship(relationship));
}
public async getStatuses() {
try {
const data = await this.client.getAccountStatuses((this.request.params as any).id, argsToBools(limitToInt(this.request.query as any)));
return await Promise.all(data.data.map(async (status) => await this.mastoconverter.convertStatus(status)));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
return e.response.data;
}
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
const data = await this.client.getAccountStatuses(this.request.params.id, parseTimelineArgs(this.request.query));
return await Promise.all(data.data.map(async (status) => await this.mastoConverters.convertStatus(status, this.me)));
}
public async getFollowers() {
try {
const data = await this.client.getAccountFollowers(
(this.request.params as any).id,
limitToInt(this.request.query as any),
);
return await Promise.all(data.data.map(async (account) => await this.mastoconverter.convertAccount(account)));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
return e.response.data;
}
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
const data = await this.client.getAccountFollowers(
this.request.params.id,
parseTimelineArgs(this.request.query),
);
return await Promise.all(data.data.map(async (account) => await this.mastoConverters.convertAccount(account)));
}
public async getFollowing() {
try {
const data = await this.client.getAccountFollowing(
(this.request.params as any).id,
limitToInt(this.request.query as any),
);
return await Promise.all(data.data.map(async (account) => await this.mastoconverter.convertAccount(account)));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
return e.response.data;
}
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
const data = await this.client.getAccountFollowing(
this.request.params.id,
parseTimelineArgs(this.request.query),
);
return await Promise.all(data.data.map(async (account) => await this.mastoConverters.convertAccount(account)));
}
public async addFollow() {
try {
const data = await this.client.followAccount( (this.request.params as any).id );
const acct = convertRelationship(data.data);
acct.following = true;
return acct;
} catch (e: any) {
console.error(e);
console.error(e.response.data);
return e.response.data;
}
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
const data = await this.client.followAccount(this.request.params.id);
const acct = convertRelationship(data.data);
acct.following = true;
return acct;
}
public async rmFollow() {
try {
const data = await this.client.unfollowAccount( (this.request.params as any).id );
const acct = convertRelationship(data.data);
acct.following = false;
return acct;
} catch (e: any) {
console.error(e);
console.error(e.response.data);
return e.response.data;
}
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
const data = await this.client.unfollowAccount(this.request.params.id);
const acct = convertRelationship(data.data);
acct.following = false;
return acct;
}
public async addBlock() {
try {
const data = await this.client.blockAccount( (this.request.params as any).id );
return convertRelationship(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
return e.response.data;
}
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
const data = await this.client.blockAccount(this.request.params.id);
return convertRelationship(data.data);
}
public async rmBlock() {
try {
const data = await this.client.unblockAccount( (this.request.params as any).id );
return convertRelationship(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
return e.response.data;
}
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
const data = await this.client.unblockAccount(this.request.params.id);
return convertRelationship(data.data);
}
public async addMute() {
try {
const data = await this.client.muteAccount(
(this.request.params as any).id,
this.request.body as any,
);
return convertRelationship(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
return e.response.data;
}
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
const data = await this.client.muteAccount(
this.request.params.id,
this.request.body.notifications ?? true,
);
return convertRelationship(data.data);
}
public async rmMute() {
try {
const data = await this.client.unmuteAccount( (this.request.params as any).id );
return convertRelationship(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
return e.response.data;
}
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
const data = await this.client.unmuteAccount(this.request.params.id);
return convertRelationship(data.data);
}
public async getBookmarks() {
try {
const data = await this.client.getBookmarks( limitToInt(this.request.query as any) );
return data.data.map((status) => this.mastoconverter.convertStatus(status));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
return e.response.data;
}
const data = await this.client.getBookmarks(parseTimelineArgs(this.request.query));
return Promise.all(data.data.map((status) => this.mastoConverters.convertStatus(status, this.me)));
}
public async getFavourites() {
try {
const data = await this.client.getFavourites( limitToInt(this.request.query as any) );
return data.data.map((status) => this.mastoconverter.convertStatus(status));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
return e.response.data;
}
const data = await this.client.getFavourites(parseTimelineArgs(this.request.query));
return Promise.all(data.data.map((status) => this.mastoConverters.convertStatus(status, this.me)));
}
public async getMutes() {
try {
const data = await this.client.getMutes( limitToInt(this.request.query as any) );
return data.data.map((account) => this.mastoconverter.convertAccount(account));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
return e.response.data;
}
const data = await this.client.getMutes(parseTimelineArgs(this.request.query));
return Promise.all(data.data.map((account) => this.mastoConverters.convertAccount(account)));
}
public async getBlocks() {
try {
const data = await this.client.getBlocks( limitToInt(this.request.query as any) );
return data.data.map((account) => this.mastoconverter.convertAccount(account));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
return e.response.data;
}
const data = await this.client.getBlocks(parseTimelineArgs(this.request.query));
return Promise.all(data.data.map((account) => this.mastoConverters.convertAccount(account)));
}
public async acceptFollow() {
try {
const data = await this.client.acceptFollowRequest( (this.request.params as any).id );
return convertRelationship(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
return e.response.data;
}
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
const data = await this.client.acceptFollowRequest(this.request.params.id);
return convertRelationship(data.data);
}
public async rejectFollow() {
try {
const data = await this.client.rejectFollowRequest( (this.request.params as any).id );
return convertRelationship(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
return e.response.data;
}
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
const data = await this.client.rejectFollowRequest(this.request.params.id);
return convertRelationship(data.data);
}
}

View file

@ -44,36 +44,54 @@ const writeScope = [
'write:gallery-likes',
];
export async function ApiAuthMastodon(request: FastifyRequest, client: MegalodonInterface) {
const body: any = request.body || request.query;
try {
let scope = body.scopes;
if (typeof scope === 'string') scope = scope.split(' ') || scope.split('+');
const pushScope = new Set<string>();
for (const s of scope) {
if (s.match(/^read/)) for (const r of readScope) pushScope.add(r);
if (s.match(/^write/)) for (const r of writeScope) pushScope.add(r);
}
const scopeArr = Array.from(pushScope);
const red = body.redirect_uris;
const appData = await client.registerApp(body.client_name, {
scopes: scopeArr,
redirect_uris: red,
website: body.website,
});
const returns = {
id: Math.floor(Math.random() * 100).toString(),
name: appData.name,
website: body.website,
redirect_uri: red,
client_id: Buffer.from(appData.url || '').toString('base64'),
client_secret: appData.clientSecret,
};
return returns;
} catch (e: any) {
console.error(e);
return e.response.data;
}
export interface AuthPayload {
scopes?: string | string[],
redirect_uris?: string,
client_name?: string,
website?: string,
}
// Not entirely right, but it gets TypeScript to work so *shrug*
export type AuthMastodonRoute = { Body?: AuthPayload, Querystring: AuthPayload };
export async function ApiAuthMastodon(request: FastifyRequest<AuthMastodonRoute>, client: MegalodonInterface) {
const body = request.body ?? request.query;
if (!body.scopes) throw new Error('Missing required payload "scopes"');
if (!body.redirect_uris) throw new Error('Missing required payload "redirect_uris"');
if (!body.client_name) throw new Error('Missing required payload "client_name"');
let scope = body.scopes;
if (typeof scope === 'string') {
scope = scope.split(/[ +]/g);
}
const pushScope = new Set<string>();
for (const s of scope) {
if (s.match(/^read/)) {
for (const r of readScope) {
pushScope.add(r);
}
}
if (s.match(/^write/)) {
for (const r of writeScope) {
pushScope.add(r);
}
}
}
const red = body.redirect_uris;
const appData = await client.registerApp(body.client_name, {
scopes: Array.from(pushScope),
redirect_uris: red,
website: body.website,
});
return {
id: Math.floor(Math.random() * 100).toString(),
name: appData.name,
website: body.website,
redirect_uri: red,
client_id: Buffer.from(appData.url || '').toString('base64'), // eslint-disable-line @typescript-eslint/prefer-nullish-coalescing
client_secret: appData.clientSecret,
};
}

View file

@ -3,68 +3,73 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { toBoolean } from '@/server/api/mastodon/timelineArgs.js';
import { convertFilter } from '../converters.js';
import type { MegalodonInterface } from 'megalodon';
import type { FastifyRequest } from 'fastify';
export class ApiFilterMastodon {
private request: FastifyRequest;
private client: MegalodonInterface;
constructor(request: FastifyRequest, client: MegalodonInterface) {
this.request = request;
this.client = client;
export interface ApiFilterMastodonRoute {
Params: {
id?: string,
},
Body: {
phrase?: string,
context?: string[],
irreversible?: string,
whole_word?: string,
expires_in?: string,
}
}
export class ApiFilterMastodon {
constructor(
private readonly request: FastifyRequest<ApiFilterMastodonRoute>,
private readonly client: MegalodonInterface,
) {}
public async getFilters() {
try {
const data = await this.client.getFilters();
return data.data.map((filter) => convertFilter(filter));
} catch (e: any) {
console.error(e);
return e.response.data;
}
const data = await this.client.getFilters();
return data.data.map((filter) => convertFilter(filter));
}
public async getFilter() {
try {
const data = await this.client.getFilter( (this.request.params as any).id );
return convertFilter(data.data);
} catch (e: any) {
console.error(e);
return e.response.data;
}
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
const data = await this.client.getFilter(this.request.params.id);
return convertFilter(data.data);
}
public async createFilter() {
try {
const body: any = this.request.body;
const data = await this.client.createFilter(body.pharse, body.context, body);
return convertFilter(data.data);
} catch (e: any) {
console.error(e);
return e.response.data;
}
if (!this.request.body.phrase) throw new Error('Missing required payload "phrase"');
if (!this.request.body.context) throw new Error('Missing required payload "context"');
const options = {
phrase: this.request.body.phrase,
context: this.request.body.context,
irreversible: toBoolean(this.request.body.irreversible),
whole_word: toBoolean(this.request.body.whole_word),
expires_in: this.request.body.expires_in,
};
const data = await this.client.createFilter(this.request.body.phrase, this.request.body.context, options);
return convertFilter(data.data);
}
public async updateFilter() {
try {
const body: any = this.request.body;
const data = await this.client.updateFilter((this.request.params as any).id, body.pharse, body.context);
return convertFilter(data.data);
} catch (e: any) {
console.error(e);
return e.response.data;
}
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
if (!this.request.body.phrase) throw new Error('Missing required payload "phrase"');
if (!this.request.body.context) throw new Error('Missing required payload "context"');
const options = {
phrase: this.request.body.phrase,
context: this.request.body.context,
irreversible: toBoolean(this.request.body.irreversible),
whole_word: toBoolean(this.request.body.whole_word),
expires_in: this.request.body.expires_in,
};
const data = await this.client.updateFilter(this.request.params.id, this.request.body.phrase, this.request.body.context, options);
return convertFilter(data.data);
}
public async rmFilter() {
try {
const data = await this.client.deleteFilter( (this.request.params as any).id );
return data.data;
} catch (e: any) {
console.error(e);
return e.response.data;
}
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
const data = await this.client.deleteFilter(this.request.params.id);
return data.data;
}
}

View file

@ -8,6 +8,7 @@ import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
import type { Config } from '@/config.js';
import type { MiMeta } from '@/models/Meta.js';
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
export async function getInstance(
response: Entity.Instance,
contact: Entity.Account,
@ -17,11 +18,8 @@ export async function getInstance(
return {
uri: config.url,
title: meta.name || 'Sharkey',
short_description:
meta.description || 'This is a vanilla Sharkey Instance. It doesn\'t seem to have a description.',
description:
meta.description ||
'This is a vanilla Sharkey Instance. It doesn\'t seem to have a description.',
short_description: meta.description || 'This is a vanilla Sharkey Instance. It doesn\'t seem to have a description.',
description: meta.description || 'This is a vanilla Sharkey Instance. It doesn\'t seem to have a description.',
email: response.email || '',
version: `3.0.0 (compatible; Sharkey ${config.version})`,
urls: response.urls,

View file

@ -3,73 +3,56 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { convertNotification } from '../converters.js';
import type { MegalodonInterface, Entity } from 'megalodon';
import { parseTimelineArgs, TimelineArgs } from '@/server/api/mastodon/timelineArgs.js';
import { MiLocalUser } from '@/models/User.js';
import { MastoConverters } from '@/server/api/mastodon/converters.js';
import type { MegalodonInterface } from 'megalodon';
import type { FastifyRequest } from 'fastify';
function toLimitToInt(q: any) {
if (q.limit) if (typeof q.limit === 'string') q.limit = parseInt(q.limit, 10);
return q;
export interface ApiNotifyMastodonRoute {
Params: {
id?: string,
},
Querystring: TimelineArgs,
}
export class ApiNotifyMastodon {
private request: FastifyRequest;
private client: MegalodonInterface;
constructor(request: FastifyRequest, client: MegalodonInterface) {
this.request = request;
this.client = client;
}
constructor(
private readonly request: FastifyRequest<ApiNotifyMastodonRoute>,
private readonly client: MegalodonInterface,
private readonly me: MiLocalUser | null,
private readonly mastoConverters: MastoConverters,
) {}
public async getNotifications() {
try {
const data = await this.client.getNotifications( toLimitToInt(this.request.query) );
const notifs = data.data;
const processed = notifs.map((n: Entity.Notification) => {
const convertedn = convertNotification(n);
if (convertedn.type !== 'follow' && convertedn.type !== 'follow_request') {
if (convertedn.type === 'reaction') convertedn.type = 'favourite';
return convertedn;
} else {
return convertedn;
}
});
return processed;
} catch (e: any) {
console.error(e);
return e.response.data;
}
const data = await this.client.getNotifications(parseTimelineArgs(this.request.query));
return Promise.all(data.data.map(async n => {
const converted = await this.mastoConverters.convertNotification(n, this.me);
if (converted.type === 'reaction') {
converted.type = 'favourite';
}
return converted;
}));
}
public async getNotification() {
try {
const data = await this.client.getNotification( (this.request.params as any).id );
const notif = convertNotification(data.data);
if (notif.type !== 'follow' && notif.type !== 'follow_request' && notif.type === 'reaction') notif.type = 'favourite';
return notif;
} catch (e: any) {
console.error(e);
return e.response.data;
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
const data = await this.client.getNotification(this.request.params.id);
const converted = await this.mastoConverters.convertNotification(data.data, this.me);
if (converted.type === 'reaction') {
converted.type = 'favourite';
}
return converted;
}
public async rmNotification() {
try {
const data = await this.client.dismissNotification( (this.request.params as any).id );
return data.data;
} catch (e: any) {
console.error(e);
return e.response.data;
}
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
const data = await this.client.dismissNotification(this.request.params.id);
return data.data;
}
public async rmNotifications() {
try {
const data = await this.client.dismissNotifications();
return data.data;
} catch (e: any) {
console.error(e);
return e.response.data;
}
const data = await this.client.dismissNotifications();
return data.data;
}
}

View file

@ -3,88 +3,92 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { MiLocalUser } from '@/models/User.js';
import { MastoConverters } from '../converters.js';
import { limitToInt } from './timeline.js';
import { parseTimelineArgs, TimelineArgs } from '../timelineArgs.js';
import Account = Entity.Account;
import Status = Entity.Status;
import type { MegalodonInterface } from 'megalodon';
import type { FastifyRequest } from 'fastify';
export class ApiSearchMastodon {
private request: FastifyRequest;
private client: MegalodonInterface;
private BASE_URL: string;
constructor(request: FastifyRequest, client: MegalodonInterface, BASE_URL: string, private mastoConverter: MastoConverters) {
this.request = request;
this.client = client;
this.BASE_URL = BASE_URL;
export interface ApiSearchMastodonRoute {
Querystring: TimelineArgs & {
type?: 'accounts' | 'hashtags' | 'statuses';
q?: string;
}
}
export class ApiSearchMastodon {
constructor(
private readonly request: FastifyRequest<ApiSearchMastodonRoute>,
private readonly client: MegalodonInterface,
private readonly me: MiLocalUser | null,
private readonly BASE_URL: string,
private readonly mastoConverters: MastoConverters,
) {}
public async SearchV1() {
try {
const query: any = limitToInt(this.request.query as any);
const type = query.type || '';
const data = await this.client.search(query.q, { type: type, ...query });
return data.data;
} catch (e: any) {
console.error(e);
return e.response.data;
}
if (!this.request.query.q) throw new Error('Missing required property "q"');
const query = parseTimelineArgs(this.request.query);
const data = await this.client.search(this.request.query.q, { type: this.request.query.type, ...query });
return data.data;
}
public async SearchV2() {
try {
const query: any = limitToInt(this.request.query as any);
const type = query.type;
const acct = !type || type === 'accounts' ? await this.client.search(query.q, { type: 'accounts', ...query }) : null;
const stat = !type || type === 'statuses' ? await this.client.search(query.q, { type: 'statuses', ...query }) : null;
const tags = !type || type === 'hashtags' ? await this.client.search(query.q, { type: 'hashtags', ...query }) : null;
const data = {
accounts: await Promise.all(acct?.data.accounts.map(async (account: any) => await this.mastoConverter.convertAccount(account)) ?? []),
statuses: await Promise.all(stat?.data.statuses.map(async (status: any) => await this.mastoConverter.convertStatus(status)) ?? []),
hashtags: tags?.data.hashtags ?? [],
};
return data;
} catch (e: any) {
console.error(e);
return e.response.data;
}
if (!this.request.query.q) throw new Error('Missing required property "q"');
const query = parseTimelineArgs(this.request.query);
const type = this.request.query.type;
const acct = !type || type === 'accounts' ? await this.client.search(this.request.query.q, { type: 'accounts', ...query }) : null;
const stat = !type || type === 'statuses' ? await this.client.search(this.request.query.q, { type: 'statuses', ...query }) : null;
const tags = !type || type === 'hashtags' ? await this.client.search(this.request.query.q, { type: 'hashtags', ...query }) : null;
return {
accounts: await Promise.all(acct?.data.accounts.map(async (account: Account) => await this.mastoConverters.convertAccount(account)) ?? []),
statuses: await Promise.all(stat?.data.statuses.map(async (status: Status) => await this.mastoConverters.convertStatus(status, this.me)) ?? []),
hashtags: tags?.data.hashtags ?? [],
};
}
public async getStatusTrends() {
try {
const data = await fetch(`${this.BASE_URL}/api/notes/featured`,
{
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
})
.then(res => res.json())
.then(data => data.map((status: any) => this.mastoConverter.convertStatus(status)));
return data;
} catch (e: any) {
console.error(e);
return [];
}
const data = await fetch(`${this.BASE_URL}/api/notes/featured`,
{
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
i: this.request.headers.authorization?.replace('Bearer ', ''),
}),
})
.then(res => res.json() as Promise<Status[]>)
.then(data => data.map(status => this.mastoConverters.convertStatus(status, this.me)));
return Promise.all(data);
}
public async getSuggestions() {
try {
const data = await fetch(`${this.BASE_URL}/api/users`,
{
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ i: this.request.headers.authorization?.replace('Bearer ', ''), limit: parseInt((this.request.query as any).limit) || 20, origin: 'local', sort: '+follower', state: 'alive' }),
}).then((res) => res.json()).then(data => data.map(((entry: any) => { return { source: 'global', account: entry }; })));
return Promise.all(data.map(async (suggestion: any) => { suggestion.account = await this.mastoConverter.convertAccount(suggestion.account); return suggestion; }));
} catch (e: any) {
console.error(e);
return [];
}
const data = await fetch(`${this.BASE_URL}/api/users`,
{
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
i: this.request.headers.authorization?.replace('Bearer ', ''),
limit: parseTimelineArgs(this.request.query).limit ?? 20,
origin: 'local',
sort: '+follower',
state: 'alive',
}),
})
.then(res => res.json() as Promise<Account[]>)
.then(data => data.map((entry => ({
source: 'global',
account: entry,
}))));
return Promise.all(data.map(async suggestion => {
suggestion.account = await this.mastoConverters.convertAccount(suggestion.account);
return suggestion;
}));
}
}

View file

@ -3,181 +3,212 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import querystring from 'querystring';
import querystring, { ParsedUrlQueryInput } from 'querystring';
import { emojiRegexAtStartToEnd } from '@/misc/emoji-regex.js';
import { convertAttachment, convertPoll, convertStatusSource, MastoConverters } from '../converters.js';
import { getClient } from '../MastodonApiServerService.js';
import { limitToInt } from './timeline.js';
import { getErrorData, MastodonLogger } from '@/server/api/mastodon/MastodonLogger.js';
import { parseTimelineArgs, TimelineArgs, toBoolean, toInt } from '@/server/api/mastodon/timelineArgs.js';
import { AuthenticateService } from '@/server/api/AuthenticateService.js';
import { convertAttachment, convertPoll, MastoConverters } from '../converters.js';
import { getAccessToken, getClient, MastodonApiServerService } from '../MastodonApiServerService.js';
import type { Entity } from 'megalodon';
import type { FastifyInstance } from 'fastify';
import type { Config } from '@/config.js';
import { NoteEditRepository, NotesRepository, UsersRepository } from '@/models/_.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
function normalizeQuery(data: any) {
const str = querystring.stringify(data);
function normalizeQuery(data: Record<string, unknown>) {
const str = querystring.stringify(data as ParsedUrlQueryInput);
return querystring.parse(str);
}
export class ApiStatusMastodon {
private fastify: FastifyInstance;
private mastoconverter: MastoConverters;
constructor(
private readonly fastify: FastifyInstance,
private readonly mastoConverters: MastoConverters,
private readonly logger: MastodonLogger,
private readonly authenticateService: AuthenticateService,
private readonly mastodon: MastodonApiServerService,
) {}
constructor(fastify: FastifyInstance, mastoconverter: MastoConverters) {
this.fastify = fastify;
this.mastoconverter = mastoconverter;
}
public async getStatus() {
this.fastify.get<{ Params: { id: string } }>('/v1/statuses/:id', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
public getStatus() {
this.fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id', async (_request, reply) => {
try {
const { client, me } = await this.mastodon.getAuthClient(_request);
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const data = await client.getStatus(_request.params.id);
reply.send(await this.mastoconverter.convertStatus(data.data));
} catch (e: any) {
console.error(e);
reply.code(_request.is404 ? 404 : 401).send(e.response.data);
reply.send(await this.mastoConverters.convertStatus(data.data, me));
} catch (e) {
const data = getErrorData(e);
this.logger.error(`GET /v1/statuses/${_request.params.id}`, data);
reply.code(_request.is404 ? 404 : 401).send(data);
}
});
}
public async getStatusSource() {
this.fastify.get<{ Params: { id: string } }>('/v1/statuses/:id/source', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
public getStatusSource() {
this.fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/source', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const data = await client.getStatusSource(_request.params.id);
reply.send(data.data);
} catch (e: any) {
console.error(e);
reply.code(_request.is404 ? 404 : 401).send(e.response.data);
} catch (e) {
const data = getErrorData(e);
this.logger.error(`GET /v1/statuses/${_request.params.id}/source`, data);
reply.code(_request.is404 ? 404 : 401).send(data);
}
});
}
public async getContext() {
this.fastify.get<{ Params: { id: string } }>('/v1/statuses/:id/context', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
const query: any = _request.query;
public getContext() {
this.fastify.get<{ Params: { id?: string }, Querystring: TimelineArgs }>('/v1/statuses/:id/context', async (_request, reply) => {
try {
const data = await client.getStatusContext(_request.params.id, limitToInt(query));
data.data.ancestors = await Promise.all(data.data.ancestors.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status)));
data.data.descendants = await Promise.all(data.data.descendants.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status)));
reply.send(data.data);
} catch (e: any) {
console.error(e);
reply.code(_request.is404 ? 404 : 401).send(e.response.data);
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const { client, me } = await this.mastodon.getAuthClient(_request);
const { data } = await client.getStatusContext(_request.params.id, parseTimelineArgs(_request.query));
const ancestors = await Promise.all(data.ancestors.map(async status => await this.mastoConverters.convertStatus(status, me)));
const descendants = await Promise.all(data.descendants.map(async status => await this.mastoConverters.convertStatus(status, me)));
reply.send({ ancestors, descendants });
} catch (e) {
const data = getErrorData(e);
this.logger.error(`GET /v1/statuses/${_request.params.id}/context`, data);
reply.code(_request.is404 ? 404 : 401).send(data);
}
});
}
public async getHistory() {
this.fastify.get<{ Params: { id: string } }>('/v1/statuses/:id/history', async (_request, reply) => {
public getHistory() {
this.fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/history', async (_request, reply) => {
try {
const edits = await this.mastoconverter.getEdits(_request.params.id);
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const [user] = await this.authenticateService.authenticate(getAccessToken(_request.headers.authorization));
const edits = await this.mastoConverters.getEdits(_request.params.id, user);
reply.send(edits);
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
} catch (e) {
const data = getErrorData(e);
this.logger.error(`GET /v1/statuses/${_request.params.id}/history`, data);
reply.code(401).send(data);
}
});
}
public async getReblogged() {
this.fastify.get<{ Params: { id: string } }>('/v1/statuses/:id/reblogged_by', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
public getReblogged() {
this.fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/reblogged_by', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const data = await client.getStatusRebloggedBy(_request.params.id);
reply.send(await Promise.all(data.data.map(async (account: Entity.Account) => await this.mastoconverter.convertAccount(account))));
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
reply.send(await Promise.all(data.data.map(async (account: Entity.Account) => await this.mastoConverters.convertAccount(account))));
} catch (e) {
const data = getErrorData(e);
this.logger.error(`GET /v1/statuses/${_request.params.id}/reblogged_by`, data);
reply.code(401).send(data);
}
});
}
public async getFavourites() {
this.fastify.get<{ Params: { id: string } }>('/v1/statuses/:id/favourited_by', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
public getFavourites() {
this.fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/favourited_by', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const data = await client.getStatusFavouritedBy(_request.params.id);
reply.send(await Promise.all(data.data.map(async (account: Entity.Account) => await this.mastoconverter.convertAccount(account))));
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
reply.send(await Promise.all(data.data.map(async (account: Entity.Account) => await this.mastoConverters.convertAccount(account))));
} catch (e) {
const data = getErrorData(e);
this.logger.error(`GET /v1/statuses/${_request.params.id}/favourited_by`, data);
reply.code(401).send(data);
}
});
}
public async getMedia() {
this.fastify.get<{ Params: { id: string } }>('/v1/media/:id', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
public getMedia() {
this.fastify.get<{ Params: { id?: string } }>('/v1/media/:id', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const data = await client.getMedia(_request.params.id);
reply.send(convertAttachment(data.data));
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
} catch (e) {
const data = getErrorData(e);
this.logger.error(`GET /v1/media/${_request.params.id}`, data);
reply.code(401).send(data);
}
});
}
public async getPoll() {
this.fastify.get<{ Params: { id: string } }>('/v1/polls/:id', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
public getPoll() {
this.fastify.get<{ Params: { id?: string } }>('/v1/polls/:id', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const data = await client.getPoll(_request.params.id);
reply.send(convertPoll(data.data));
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
} catch (e) {
const data = getErrorData(e);
this.logger.error(`GET /v1/polls/${_request.params.id}`, data);
reply.code(401).send(data);
}
});
}
public async votePoll() {
this.fastify.post<{ Params: { id: string } }>('/v1/polls/:id/votes', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
public votePoll() {
this.fastify.post<{ Params: { id?: string }, Body: { choices?: number[] } }>('/v1/polls/:id/votes', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
const body: any = _request.body;
try {
const data = await client.votePoll(_request.params.id, body.choices);
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
if (!_request.body.choices) return reply.code(400).send({ error: 'Missing required payload "choices"' });
const data = await client.votePoll(_request.params.id, _request.body.choices);
reply.send(convertPoll(data.data));
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
} catch (e) {
const data = getErrorData(e);
this.logger.error(`GET /v1/polls/${_request.params.id}/votes`, data);
reply.code(401).send(data);
}
});
}
public async postStatus() {
this.fastify.post('/v1/statuses', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
let body: any = _request.body;
public postStatus() {
this.fastify.post<{
Body: {
media_ids?: string[],
poll?: {
options?: string[],
expires_in?: string,
multiple?: string,
hide_totals?: string,
},
in_reply_to_id?: string,
sensitive?: string,
spoiler_text?: string,
visibility?: 'public' | 'unlisted' | 'private' | 'direct',
scheduled_at?: string,
language?: string,
quote_id?: string,
status?: string,
// Broken clients
'poll[options][]'?: string[],
'media_ids[]'?: string[],
}
}>('/v1/statuses', async (_request, reply) => {
let body = _request.body;
try {
if (
(!body.poll && body['poll[options][]']) ||
(!body.media_ids && body['media_ids[]'])
const { client, me } = await this.mastodon.getAuthClient(_request);
if ((!body.poll && body['poll[options][]']) || (!body.media_ids && body['media_ids[]'])
) {
body = normalizeQuery(body);
}
const text = body.status ? body.status : ' ';
const text = body.status ??= ' ';
const removed = text.replace(/@\S+/g, '').replace(/\s|/g, '');
const isDefaultEmoji = emojiRegexAtStartToEnd.test(removed);
const isCustomEmoji = /^:[a-zA-Z0-9@_]+:$/.test(removed);
@ -189,226 +220,253 @@ export class ApiStatusMastodon {
reply.send(a.data);
}
if (body.in_reply_to_id && removed === '/unreact') {
try {
const id = body.in_reply_to_id;
const post = await client.getStatus(id);
const react = post.data.emoji_reactions.filter((e: any) => e.me)[0].name;
const data = await client.deleteEmojiReaction(id, react);
reply.send(data.data);
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
}
const id = body.in_reply_to_id;
const post = await client.getStatus(id);
const react = post.data.emoji_reactions.filter(e => e.me)[0].name;
const data = await client.deleteEmojiReaction(id, react);
reply.send(data.data);
}
if (!body.media_ids) body.media_ids = undefined;
if (body.media_ids && !body.media_ids.length) body.media_ids = undefined;
const { sensitive } = body;
body.sensitive = typeof sensitive === 'string' ? sensitive === 'true' : sensitive;
if (body.poll) {
if (
body.poll.expires_in != null &&
typeof body.poll.expires_in === 'string'
) body.poll.expires_in = parseInt(body.poll.expires_in);
if (
body.poll.multiple != null &&
typeof body.poll.multiple === 'string'
) body.poll.multiple = body.poll.multiple === 'true';
if (
body.poll.hide_totals != null &&
typeof body.poll.hide_totals === 'string'
) body.poll.hide_totals = body.poll.hide_totals === 'true';
if (body.poll && !body.poll.options) {
return reply.code(400).send({ error: 'Missing required payload "poll.options"' });
}
if (body.poll && !body.poll.expires_in) {
return reply.code(400).send({ error: 'Missing required payload "poll.expires_in"' });
}
const data = await client.postStatus(text, body);
reply.send(await this.mastoconverter.convertStatus(data.data as Entity.Status));
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
const options = {
...body,
sensitive: toBoolean(body.sensitive),
poll: body.poll ? {
options: body.poll.options!, // eslint-disable-line @typescript-eslint/no-non-null-assertion
expires_in: toInt(body.poll.expires_in)!, // eslint-disable-line @typescript-eslint/no-non-null-assertion
multiple: toBoolean(body.poll.multiple),
hide_totals: toBoolean(body.poll.hide_totals),
} : undefined,
};
const data = await client.postStatus(text, options);
reply.send(await this.mastoConverters.convertStatus(data.data as Entity.Status, me));
} catch (e) {
const data = getErrorData(e);
this.logger.error('POST /v1/statuses', data);
reply.code(401).send(data);
}
});
}
public async updateStatus() {
this.fastify.put<{ Params: { id: string } }>('/v1/statuses/:id', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
const body: any = _request.body;
public updateStatus() {
this.fastify.put<{
Params: { id: string },
Body: {
status?: string,
spoiler_text?: string,
sensitive?: string,
media_ids?: string[],
poll?: {
options?: string[],
expires_in?: string,
multiple?: string,
hide_totals?: string,
},
}
}>('/v1/statuses/:id', async (_request, reply) => {
try {
if (!body.media_ids) body.media_ids = undefined;
if (body.media_ids && !body.media_ids.length) body.media_ids = undefined;
const data = await client.editStatus(_request.params.id, body);
reply.send(await this.mastoconverter.convertStatus(data.data));
} catch (e: any) {
console.error(e);
reply.code(_request.is404 ? 404 : 401).send(e.response.data);
const { client, me } = await this.mastodon.getAuthClient(_request);
const body = _request.body;
if (!body.media_ids || !body.media_ids.length) {
body.media_ids = undefined;
}
const options = {
...body,
sensitive: toBoolean(body.sensitive),
poll: body.poll ? {
options: body.poll.options,
expires_in: toInt(body.poll.expires_in),
multiple: toBoolean(body.poll.multiple),
hide_totals: toBoolean(body.poll.hide_totals),
} : undefined,
};
const data = await client.editStatus(_request.params.id, options);
reply.send(await this.mastoConverters.convertStatus(data.data, me));
} catch (e) {
const data = getErrorData(e);
this.logger.error(`POST /v1/statuses/${_request.params.id}`, data);
reply.code(401).send(data);
}
});
}
public async addFavourite() {
this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/favourite', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
public addFavourite() {
this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/favourite', async (_request, reply) => {
try {
const data = (await client.createEmojiReaction(_request.params.id, '❤')) as any;
reply.send(await this.mastoconverter.convertStatus(data.data));
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const { client, me } = await this.mastodon.getAuthClient(_request);
const data = await client.createEmojiReaction(_request.params.id, '❤');
reply.send(await this.mastoConverters.convertStatus(data.data, me));
} catch (e) {
const data = getErrorData(e);
this.logger.error(`POST /v1/statuses/${_request.params.id}/favorite`, data);
reply.code(401).send(data);
}
});
}
public async rmFavourite() {
this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/unfavourite', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
public rmFavourite() {
this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unfavourite', async (_request, reply) => {
try {
const { client, me } = await this.mastodon.getAuthClient(_request);
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const data = await client.deleteEmojiReaction(_request.params.id, '❤');
reply.send(await this.mastoconverter.convertStatus(data.data));
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
reply.send(await this.mastoConverters.convertStatus(data.data, me));
} catch (e) {
const data = getErrorData(e);
this.logger.error(`GET /v1/statuses/${_request.params.id}/unfavorite`, data);
reply.code(401).send(data);
}
});
}
public async reblogStatus() {
this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/reblog', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
public reblogStatus() {
this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/reblog', async (_request, reply) => {
try {
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const { client, me } = await this.mastodon.getAuthClient(_request);
const data = await client.reblogStatus(_request.params.id);
reply.send(await this.mastoconverter.convertStatus(data.data));
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
reply.send(await this.mastoConverters.convertStatus(data.data, me));
} catch (e) {
const data = getErrorData(e);
this.logger.error(`POST /v1/statuses/${_request.params.id}/reblog`, data);
reply.code(401).send(data);
}
});
}
public async unreblogStatus() {
this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/unreblog', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
public unreblogStatus() {
this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unreblog', async (_request, reply) => {
try {
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const { client, me } = await this.mastodon.getAuthClient(_request);
const data = await client.unreblogStatus(_request.params.id);
reply.send(await this.mastoconverter.convertStatus(data.data));
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
reply.send(await this.mastoConverters.convertStatus(data.data, me));
} catch (e) {
const data = getErrorData(e);
this.logger.error(`POST /v1/statuses/${_request.params.id}/unreblog`, data);
reply.code(401).send(data);
}
});
}
public async bookmarkStatus() {
this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/bookmark', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
public bookmarkStatus() {
this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/bookmark', async (_request, reply) => {
try {
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const { client, me } = await this.mastodon.getAuthClient(_request);
const data = await client.bookmarkStatus(_request.params.id);
reply.send(await this.mastoconverter.convertStatus(data.data));
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
reply.send(await this.mastoConverters.convertStatus(data.data, me));
} catch (e) {
const data = getErrorData(e);
this.logger.error(`POST /v1/statuses/${_request.params.id}/bookmark`, data);
reply.code(401).send(data);
}
});
}
public async unbookmarkStatus() {
this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/unbookmark', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
public unbookmarkStatus() {
this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unbookmark', async (_request, reply) => {
try {
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const { client, me } = await this.mastodon.getAuthClient(_request);
const data = await client.unbookmarkStatus(_request.params.id);
reply.send(await this.mastoconverter.convertStatus(data.data));
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
reply.send(await this.mastoConverters.convertStatus(data.data, me));
} catch (e) {
const data = getErrorData(e);
this.logger.error(`POST /v1/statuses/${_request.params.id}/unbookmark`, data);
reply.code(401).send(data);
}
});
}
public async pinStatus() {
this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/pin', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
public pinStatus() {
this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/pin', async (_request, reply) => {
try {
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const { client, me } = await this.mastodon.getAuthClient(_request);
const data = await client.pinStatus(_request.params.id);
reply.send(await this.mastoconverter.convertStatus(data.data));
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
reply.send(await this.mastoConverters.convertStatus(data.data, me));
} catch (e) {
const data = getErrorData(e);
this.logger.error(`POST /v1/statuses/${_request.params.id}/pin`, data);
reply.code(401).send(data);
}
});
}
public async unpinStatus() {
this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/unpin', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
public unpinStatus() {
this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unpin', async (_request, reply) => {
try {
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const { client, me } = await this.mastodon.getAuthClient(_request);
const data = await client.unpinStatus(_request.params.id);
reply.send(await this.mastoconverter.convertStatus(data.data));
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
reply.send(await this.mastoConverters.convertStatus(data.data, me));
} catch (e) {
const data = getErrorData(e);
this.logger.error(`POST /v1/statuses/${_request.params.id}/unpin`, data);
reply.code(401).send(data);
}
});
}
public async reactStatus() {
this.fastify.post<{ Params: { id: string, name: string } }>('/v1/statuses/:id/react/:name', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
public reactStatus() {
this.fastify.post<{ Params: { id?: string, name?: string } }>('/v1/statuses/:id/react/:name', async (_request, reply) => {
try {
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
if (!_request.params.name) return reply.code(400).send({ error: 'Missing required parameter "name"' });
const { client, me } = await this.mastodon.getAuthClient(_request);
const data = await client.createEmojiReaction(_request.params.id, _request.params.name);
reply.send(await this.mastoconverter.convertStatus(data.data));
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
reply.send(await this.mastoConverters.convertStatus(data.data, me));
} catch (e) {
const data = getErrorData(e);
this.logger.error(`POST /v1/statuses/${_request.params.id}/react/${_request.params.name}`, data);
reply.code(401).send(data);
}
});
}
public async unreactStatus() {
this.fastify.post<{ Params: { id: string, name: string } }>('/v1/statuses/:id/unreact/:name', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
public unreactStatus() {
this.fastify.post<{ Params: { id?: string, name?: string } }>('/v1/statuses/:id/unreact/:name', async (_request, reply) => {
try {
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
if (!_request.params.name) return reply.code(400).send({ error: 'Missing required parameter "name"' });
const { client, me } = await this.mastodon.getAuthClient(_request);
const data = await client.deleteEmojiReaction(_request.params.id, _request.params.name);
reply.send(await this.mastoconverter.convertStatus(data.data));
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
reply.send(await this.mastoConverters.convertStatus(data.data, me));
} catch (e) {
const data = getErrorData(e);
this.logger.error(`POST /v1/statuses/${_request.params.id}/unreact/${_request.params.name}`, data);
reply.code(401).send(data);
}
});
}
public async deleteStatus() {
this.fastify.delete<{ Params: { id: string } }>('/v1/statuses/:id', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
public deleteStatus() {
this.fastify.delete<{ Params: { id?: string } }>('/v1/statuses/:id', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const data = await client.deleteStatus(_request.params.id);
reply.send(data.data);
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
} catch (e) {
const data = getErrorData(e);
this.logger.error(`DELETE /v1/statuses/${_request.params.id}`, data);
reply.code(401).send(data);
}
});
}

View file

@ -3,270 +3,231 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ParsedUrlQuery } from 'querystring';
import { convertConversation, convertList, MastoConverters } from '../converters.js';
import { getClient } from '../MastodonApiServerService.js';
import { getErrorData, MastodonLogger } from '@/server/api/mastodon/MastodonLogger.js';
import { convertList, MastoConverters } from '../converters.js';
import { getClient, MastodonApiServerService } from '../MastodonApiServerService.js';
import { parseTimelineArgs, TimelineArgs, toBoolean } from '../timelineArgs.js';
import type { Entity } from 'megalodon';
import type { FastifyInstance } from 'fastify';
import type { Config } from '@/config.js';
import { NoteEditRepository, NotesRepository, UsersRepository } from '@/models/_.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
export function limitToInt(q: ParsedUrlQuery) {
const object: any = q;
if (q.limit) if (typeof q.limit === 'string') object.limit = parseInt(q.limit, 10);
if (q.offset) if (typeof q.offset === 'string') object.offset = parseInt(q.offset, 10);
return object;
}
export function argsToBools(q: ParsedUrlQuery) {
// Values taken from https://docs.joinmastodon.org/client/intro/#boolean
const toBoolean = (value: string) =>
!['0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].includes(value);
// Keys taken from:
// - https://docs.joinmastodon.org/methods/accounts/#statuses
// - https://docs.joinmastodon.org/methods/timelines/#public
// - https://docs.joinmastodon.org/methods/timelines/#tag
const object: any = q;
if (q.only_media) if (typeof q.only_media === 'string') object.only_media = toBoolean(q.only_media);
if (q.exclude_replies) if (typeof q.exclude_replies === 'string') object.exclude_replies = toBoolean(q.exclude_replies);
if (q.exclude_reblogs) if (typeof q.exclude_reblogs === 'string') object.exclude_reblogs = toBoolean(q.exclude_reblogs);
if (q.pinned) if (typeof q.pinned === 'string') object.pinned = toBoolean(q.pinned);
if (q.local) if (typeof q.local === 'string') object.local = toBoolean(q.local);
return q;
}
export class ApiTimelineMastodon {
private fastify: FastifyInstance;
constructor(
private readonly fastify: FastifyInstance,
private readonly mastoConverters: MastoConverters,
private readonly logger: MastodonLogger,
private readonly mastodon: MastodonApiServerService,
) {}
constructor(fastify: FastifyInstance, config: Config, private mastoconverter: MastoConverters) {
this.fastify = fastify;
}
public async getTL() {
this.fastify.get('/v1/timelines/public', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
public getTL() {
this.fastify.get<{ Querystring: TimelineArgs }>('/v1/timelines/public', async (_request, reply) => {
try {
const query: any = _request.query;
const data = query.local === 'true'
? await client.getLocalTimeline(argsToBools(limitToInt(query)))
: await client.getPublicTimeline(argsToBools(limitToInt(query)));
reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status))));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
const { client, me } = await this.mastodon.getAuthClient(_request);
const data = toBoolean(_request.query.local)
? await client.getLocalTimeline(parseTimelineArgs(_request.query))
: await client.getPublicTimeline(parseTimelineArgs(_request.query));
reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoConverters.convertStatus(status, me))));
} catch (e) {
const data = getErrorData(e);
this.logger.error('GET /v1/timelines/public', data);
reply.code(401).send(data);
}
});
}
public async getHomeTl() {
this.fastify.get('/v1/timelines/home', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
public getHomeTl() {
this.fastify.get<{ Querystring: TimelineArgs }>('/v1/timelines/home', async (_request, reply) => {
try {
const query: any = _request.query;
const data = await client.getHomeTimeline(limitToInt(query));
reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status))));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
const { client, me } = await this.mastodon.getAuthClient(_request);
const data = await client.getHomeTimeline(parseTimelineArgs(_request.query));
reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoConverters.convertStatus(status, me))));
} catch (e) {
const data = getErrorData(e);
this.logger.error('GET /v1/timelines/home', data);
reply.code(401).send(data);
}
});
}
public async getTagTl() {
this.fastify.get<{ Params: { hashtag: string } }>('/v1/timelines/tag/:hashtag', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
public getTagTl() {
this.fastify.get<{ Params: { hashtag?: string }, Querystring: TimelineArgs }>('/v1/timelines/tag/:hashtag', async (_request, reply) => {
try {
const query: any = _request.query;
const params: any = _request.params;
const data = await client.getTagTimeline(params.hashtag, limitToInt(query));
reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status))));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
if (!_request.params.hashtag) return reply.code(400).send({ error: 'Missing required parameter "hashtag"' });
const { client, me } = await this.mastodon.getAuthClient(_request);
const data = await client.getTagTimeline(_request.params.hashtag, parseTimelineArgs(_request.query));
reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoConverters.convertStatus(status, me))));
} catch (e) {
const data = getErrorData(e);
this.logger.error(`GET /v1/timelines/tag/${_request.params.hashtag}`, data);
reply.code(401).send(data);
}
});
}
public async getListTL() {
this.fastify.get<{ Params: { id: string } }>('/v1/timelines/list/:id', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
public getListTL() {
this.fastify.get<{ Params: { id?: string }, Querystring: TimelineArgs }>('/v1/timelines/list/:id', async (_request, reply) => {
try {
const query: any = _request.query;
const params: any = _request.params;
const data = await client.getListTimeline(params.id, limitToInt(query));
reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status))));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const { client, me } = await this.mastodon.getAuthClient(_request);
const data = await client.getListTimeline(_request.params.id, parseTimelineArgs(_request.query));
reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoConverters.convertStatus(status, me))));
} catch (e) {
const data = getErrorData(e);
this.logger.error(`GET /v1/timelines/list/${_request.params.id}`, data);
reply.code(401).send(data);
}
});
}
public async getConversations() {
this.fastify.get('/v1/conversations', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
public getConversations() {
this.fastify.get<{ Querystring: TimelineArgs }>('/v1/conversations', async (_request, reply) => {
try {
const query: any = _request.query;
const data = await client.getConversationTimeline(limitToInt(query));
reply.send(data.data.map((conversation: Entity.Conversation) => convertConversation(conversation)));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
const { client, me } = await this.mastodon.getAuthClient(_request);
const data = await client.getConversationTimeline(parseTimelineArgs(_request.query));
const conversations = await Promise.all(data.data.map(async (conversation: Entity.Conversation) => await this.mastoConverters.convertConversation(conversation, me)));
reply.send(conversations);
} catch (e) {
const data = getErrorData(e);
this.logger.error('GET /v1/conversations', data);
reply.code(401).send(data);
}
});
}
public async getList() {
this.fastify.get<{ Params: { id: string } }>('/v1/lists/:id', async (_request, reply) => {
public getList() {
this.fastify.get<{ Params: { id?: string } }>('/v1/lists/:id', async (_request, reply) => {
try {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
const params: any = _request.params;
const data = await client.getList(params.id);
const data = await client.getList(_request.params.id);
reply.send(convertList(data.data));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
} catch (e) {
const data = getErrorData(e);
this.logger.error(`GET /v1/lists/${_request.params.id}`, data);
reply.code(401).send(data);
}
});
}
public async getLists() {
public getLists() {
this.fastify.get('/v1/lists', async (_request, reply) => {
try {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
const data = await client.getLists();
reply.send(data.data.map((list: Entity.List) => convertList(list)));
} catch (e: any) {
console.error(e);
return e.response.data;
} catch (e) {
const data = getErrorData(e);
this.logger.error('GET /v1/lists', data);
reply.code(401).send(data);
}
});
}
public async getListAccounts() {
this.fastify.get<{ Params: { id: string } }>('/v1/lists/:id/accounts', async (_request, reply) => {
public getListAccounts() {
this.fastify.get<{ Params: { id?: string }, Querystring: { limit?: number, max_id?: string, since_id?: string } }>('/v1/lists/:id/accounts', async (_request, reply) => {
try {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
const params: any = _request.params;
const query: any = _request.query;
const data = await client.getAccountsInList(params.id, query);
reply.send(data.data.map((account: Entity.Account) => this.mastoconverter.convertAccount(account)));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
const data = await client.getAccountsInList(_request.params.id, _request.query);
const accounts = await Promise.all(data.data.map((account: Entity.Account) => this.mastoConverters.convertAccount(account)));
reply.send(accounts);
} catch (e) {
const data = getErrorData(e);
this.logger.error(`GET /v1/lists/${_request.params.id}/accounts`, data);
reply.code(401).send(data);
}
});
}
public async addListAccount() {
this.fastify.post<{ Params: { id: string } }>('/v1/lists/:id/accounts', async (_request, reply) => {
public addListAccount() {
this.fastify.post<{ Params: { id?: string }, Querystring: { accounts_id?: string[] } }>('/v1/lists/:id/accounts', async (_request, reply) => {
try {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
if (!_request.query.accounts_id) return reply.code(400).send({ error: 'Missing required property "accounts_id"' });
const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
const params: any = _request.params;
const query: any = _request.query;
const data = await client.addAccountsToList(params.id, query.accounts_id);
const data = await client.addAccountsToList(_request.params.id, _request.query.accounts_id);
reply.send(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
} catch (e) {
const data = getErrorData(e);
this.logger.error(`POST /v1/lists/${_request.params.id}/accounts`, data);
reply.code(401).send(data);
}
});
}
public async rmListAccount() {
this.fastify.delete<{ Params: { id: string } }>('/v1/lists/:id/accounts', async (_request, reply) => {
public rmListAccount() {
this.fastify.delete<{ Params: { id?: string }, Querystring: { accounts_id?: string[] } }>('/v1/lists/:id/accounts', async (_request, reply) => {
try {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
if (!_request.query.accounts_id) return reply.code(400).send({ error: 'Missing required property "accounts_id"' });
const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
const params: any = _request.params;
const query: any = _request.query;
const data = await client.deleteAccountsFromList(params.id, query.accounts_id);
const data = await client.deleteAccountsFromList(_request.params.id, _request.query.accounts_id);
reply.send(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
} catch (e) {
const data = getErrorData(e);
this.logger.error(`DELETE /v1/lists/${_request.params.id}/accounts`, data);
reply.code(401).send(data);
}
});
}
public async createList() {
this.fastify.post('/v1/lists', async (_request, reply) => {
public createList() {
this.fastify.post<{ Body: { title?: string } }>('/v1/lists', async (_request, reply) => {
try {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
if (!_request.body.title) return reply.code(400).send({ error: 'Missing required payload "title"' });
const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
const body: any = _request.body;
const data = await client.createList(body.title);
const data = await client.createList(_request.body.title);
reply.send(convertList(data.data));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
} catch (e) {
const data = getErrorData(e);
this.logger.error('POST /v1/lists', data);
reply.code(401).send(data);
}
});
}
public async updateList() {
this.fastify.put<{ Params: { id: string } }>('/v1/lists/:id', async (_request, reply) => {
public updateList() {
this.fastify.put<{ Params: { id?: string }, Body: { title?: string } }>('/v1/lists/:id', async (_request, reply) => {
try {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
if (!_request.body.title) return reply.code(400).send({ error: 'Missing required payload "title"' });
const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
const body: any = _request.body;
const params: any = _request.params;
const data = await client.updateList(params.id, body.title);
const data = await client.updateList(_request.params.id, _request.body.title);
reply.send(convertList(data.data));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
} catch (e) {
const data = getErrorData(e);
this.logger.error(`PUT /v1/lists/${_request.params.id}`, data);
reply.code(401).send(data);
}
});
}
public async deleteList() {
this.fastify.delete<{ Params: { id: string } }>('/v1/lists/:id', async (_request, reply) => {
public deleteList() {
this.fastify.delete<{ Params: { id?: string } }>('/v1/lists/:id', async (_request, reply) => {
try {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
const BASE_URL = `${_request.protocol}://${_request.host}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
const params: any = _request.params;
const data = await client.deleteList(params.id);
await client.deleteList(_request.params.id);
reply.send({});
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
} catch (e) {
const data = getErrorData(e);
this.logger.error(`DELETE /v1/lists/${_request.params.id}`, data);
reply.code(401).send(data);
}
});
}

View file

@ -0,0 +1,47 @@
/*
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
// Keys taken from:
// - https://docs.joinmastodon.org/methods/accounts/#statuses
// - https://docs.joinmastodon.org/methods/timelines/#public
// - https://docs.joinmastodon.org/methods/timelines/#tag
export interface TimelineArgs {
max_id?: string;
min_id?: string;
since_id?: string;
limit?: string;
offset?: string;
local?: string;
pinned?: string;
exclude_reblogs?: string;
exclude_replies?: string;
only_media?: string;
}
// Values taken from https://docs.joinmastodon.org/client/intro/#boolean
export function toBoolean(value: string | undefined): boolean | undefined {
if (value === undefined) return undefined;
return !['0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].includes(value);
}
export function toInt(value: string | undefined): number | undefined {
if (value === undefined) return undefined;
return parseInt(value);
}
export function parseTimelineArgs(q: TimelineArgs) {
return {
max_id: q.max_id,
min_id: q.min_id,
since_id: q.since_id,
limit: typeof(q.limit) === 'string' ? parseInt(q.limit, 10) : undefined,
offset: typeof(q.offset) === 'string' ? parseInt(q.offset, 10) : undefined,
local: typeof(q.local) === 'string' ? toBoolean(q.local) : undefined,
pinned: typeof(q.pinned) === 'string' ? toBoolean(q.pinned) : undefined,
exclude_reblogs: typeof(q.exclude_reblogs) === 'string' ? toBoolean(q.exclude_reblogs) : undefined,
exclude_replies: typeof(q.exclude_replies) === 'string' ? toBoolean(q.exclude_replies) : undefined,
only_media: typeof(q.only_media) === 'string' ? toBoolean(q.only_media) : undefined,
};
}

View file

@ -3,13 +3,15 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { deepClone } from '@/misc/clone.js';
import type { Schema } from '@/misc/json-schema.js';
import { refs } from '@/misc/json-schema.js';
export function convertSchemaToOpenApiSchema(schema: Schema, type: 'param' | 'res', includeSelfRef: boolean): any {
// optional, nullable, refはスキーマ定義に含まれないので分離しておく
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { optional, nullable, ref, selfRef, ...res }: any = schema;
const { optional, nullable, ref, selfRef, ..._res }: any = schema;
const res = deepClone(_res);
if (schema.type === 'object' && schema.properties) {
if (type === 'res') {

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