mirror of
https://codeberg.org/yeentown/barkey.git
synced 2025-07-07 20:44:34 +00:00
merge: Fix Mastodon API requests with multipart/form-data encoding (resolves #1024, #839, #699, #574, and #486) (!987)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/987 Closes #1024, #839, #699, #574, and #486 Approved-by: dakkar <dakkar@thenautilus.net> Approved-by: Marie <github@yuugi.dev>
This commit is contained in:
commit
b8ff441474
40 changed files with 724 additions and 438 deletions
|
@ -349,6 +349,9 @@ attachLdSignatureForRelays: true
|
||||||
# # Disable query truncation. If set to true, the full text of the query will be output to the log.
|
# # Disable query truncation. If set to true, the full text of the query will be output to the log.
|
||||||
# # default: false
|
# # default: false
|
||||||
# disableQueryTruncation: false
|
# disableQueryTruncation: false
|
||||||
|
# # Shows debug log messages after instance startup. To capture earlier debug logs, set the MK_VERBOSE environment variable.
|
||||||
|
# # default: false in production, true otherwise.
|
||||||
|
# #verbose: false
|
||||||
|
|
||||||
# Settings for the activity logger, which records inbound activities to the database.
|
# Settings for the activity logger, which records inbound activities to the database.
|
||||||
# Disabled by default due to the large volume of data it saves.
|
# Disabled by default due to the large volume of data it saves.
|
||||||
|
|
|
@ -295,6 +295,9 @@ allowedPrivateNetworks: [
|
||||||
# # Disable query truncation. If set to true, the full text of the query will be output to the log.
|
# # Disable query truncation. If set to true, the full text of the query will be output to the log.
|
||||||
# # default: false
|
# # default: false
|
||||||
# disableQueryTruncation: false
|
# disableQueryTruncation: false
|
||||||
|
# # Shows debug log messages after instance startup. To capture earlier debug logs, set the MK_VERBOSE environment variable.
|
||||||
|
# # default: false in production, true otherwise.
|
||||||
|
# #verbose: false
|
||||||
|
|
||||||
# Settings for the activity logger, which records inbound activities to the database.
|
# Settings for the activity logger, which records inbound activities to the database.
|
||||||
# Disabled by default due to the large volume of data it saves.
|
# Disabled by default due to the large volume of data it saves.
|
||||||
|
|
|
@ -411,6 +411,9 @@ attachLdSignatureForRelays: true
|
||||||
# # Disable query truncation. If set to true, the full text of the query will be output to the log.
|
# # Disable query truncation. If set to true, the full text of the query will be output to the log.
|
||||||
# # default: false
|
# # default: false
|
||||||
# disableQueryTruncation: false
|
# disableQueryTruncation: false
|
||||||
|
# # Shows debug log messages after instance startup. To capture earlier debug logs, set the MK_VERBOSE environment variable.
|
||||||
|
# # default: false in production, true otherwise.
|
||||||
|
# #verbose: false
|
||||||
|
|
||||||
# Settings for the activity logger, which records inbound activities to the database.
|
# Settings for the activity logger, which records inbound activities to the database.
|
||||||
# Disabled by default due to the large volume of data it saves.
|
# Disabled by default due to the large volume of data it saves.
|
||||||
|
|
|
@ -417,6 +417,9 @@ attachLdSignatureForRelays: true
|
||||||
# # Disable query truncation. If set to true, the full text of the query will be output to the log.
|
# # Disable query truncation. If set to true, the full text of the query will be output to the log.
|
||||||
# # default: false
|
# # default: false
|
||||||
# disableQueryTruncation: false
|
# disableQueryTruncation: false
|
||||||
|
# # Shows debug log messages after instance startup. To capture earlier debug logs, set the MK_VERBOSE environment variable.
|
||||||
|
# # default: false in production, true otherwise.
|
||||||
|
# #verbose: false
|
||||||
|
|
||||||
# Settings for the activity logger, which records inbound activities to the database.
|
# Settings for the activity logger, which records inbound activities to the database.
|
||||||
# Disabled by default due to the large volume of data it saves.
|
# Disabled by default due to the large volume of data it saves.
|
||||||
|
|
|
@ -97,6 +97,7 @@
|
||||||
"ajv": "8.17.1",
|
"ajv": "8.17.1",
|
||||||
"archiver": "7.0.1",
|
"archiver": "7.0.1",
|
||||||
"argon2": "^0.40.1",
|
"argon2": "^0.40.1",
|
||||||
|
"axios": "1.7.4",
|
||||||
"async-mutex": "0.5.0",
|
"async-mutex": "0.5.0",
|
||||||
"bcryptjs": "2.4.3",
|
"bcryptjs": "2.4.3",
|
||||||
"blurhash": "2.0.5",
|
"blurhash": "2.0.5",
|
||||||
|
@ -115,7 +116,6 @@
|
||||||
"deep-email-validator": "0.1.21",
|
"deep-email-validator": "0.1.21",
|
||||||
"fast-xml-parser": "4.4.1",
|
"fast-xml-parser": "4.4.1",
|
||||||
"fastify": "5.3.2",
|
"fastify": "5.3.2",
|
||||||
"fastify-multer": "^2.0.3",
|
|
||||||
"fastify-raw-body": "5.0.0",
|
"fastify-raw-body": "5.0.0",
|
||||||
"feed": "4.2.2",
|
"feed": "4.2.2",
|
||||||
"file-type": "19.6.0",
|
"file-type": "19.6.0",
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { fileURLToPath } from 'node:url';
|
||||||
import { dirname } from 'node:path';
|
import { dirname } from 'node:path';
|
||||||
import * as os from 'node:os';
|
import * as os from 'node:os';
|
||||||
import cluster from 'node:cluster';
|
import cluster from 'node:cluster';
|
||||||
|
import * as net from 'node:net';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import chalkTemplate from 'chalk-template';
|
import chalkTemplate from 'chalk-template';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
|
@ -18,7 +19,6 @@ import type { Config } from '@/config.js';
|
||||||
import { showMachineInfo } from '@/misc/show-machine-info.js';
|
import { showMachineInfo } from '@/misc/show-machine-info.js';
|
||||||
import { envOption } from '@/env.js';
|
import { envOption } from '@/env.js';
|
||||||
import { jobQueue, server } from './common.js';
|
import { jobQueue, server } from './common.js';
|
||||||
import * as net from 'node:net';
|
|
||||||
|
|
||||||
const _filename = fileURLToPath(import.meta.url);
|
const _filename = fileURLToPath(import.meta.url);
|
||||||
const _dirname = dirname(_filename);
|
const _dirname = dirname(_filename);
|
||||||
|
|
|
@ -135,7 +135,8 @@ type Source = {
|
||||||
sql?: {
|
sql?: {
|
||||||
disableQueryTruncation?: boolean,
|
disableQueryTruncation?: boolean,
|
||||||
enableQueryParamLogging?: boolean,
|
enableQueryParamLogging?: boolean,
|
||||||
}
|
};
|
||||||
|
verbose?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
activityLogging?: {
|
activityLogging?: {
|
||||||
|
@ -220,7 +221,8 @@ export type Config = {
|
||||||
sql?: {
|
sql?: {
|
||||||
disableQueryTruncation?: boolean,
|
disableQueryTruncation?: boolean,
|
||||||
enableQueryParamLogging?: boolean,
|
enableQueryParamLogging?: boolean,
|
||||||
}
|
};
|
||||||
|
verbose?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
version: string;
|
version: string;
|
||||||
|
@ -585,6 +587,7 @@ function applyEnvOverrides(config: Source) {
|
||||||
_apply_top(['import', ['downloadTimeout', 'maxFileSize']]);
|
_apply_top(['import', ['downloadTimeout', 'maxFileSize']]);
|
||||||
_apply_top([['signToActivityPubGet', 'checkActivityPubGetSignature', 'setupPassword', 'disallowExternalApRedirect']]);
|
_apply_top([['signToActivityPubGet', 'checkActivityPubGetSignature', 'setupPassword', 'disallowExternalApRedirect']]);
|
||||||
_apply_top(['logging', 'sql', ['disableQueryTruncation', 'enableQueryParamLogging']]);
|
_apply_top(['logging', 'sql', ['disableQueryTruncation', 'enableQueryParamLogging']]);
|
||||||
|
_apply_top(['logging', ['verbose']]);
|
||||||
_apply_top(['activityLogging', ['enabled', 'preSave', 'maxAge']]);
|
_apply_top(['activityLogging', ['enabled', 'preSave', 'maxAge']]);
|
||||||
_apply_top(['customHtml', ['head']]);
|
_apply_top(['customHtml', ['head']]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,7 @@ import { isMimeImage } from '@/misc/is-mime-image.js';
|
||||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import { BunnyService } from '@/core/BunnyService.js';
|
import { BunnyService } from '@/core/BunnyService.js';
|
||||||
|
import { LoggerService } from './LoggerService.js';
|
||||||
|
|
||||||
type AddFileArgs = {
|
type AddFileArgs = {
|
||||||
/** User who wish to add file */
|
/** User who wish to add file */
|
||||||
|
@ -133,8 +134,10 @@ export class DriveService {
|
||||||
private perUserDriveChart: PerUserDriveChart,
|
private perUserDriveChart: PerUserDriveChart,
|
||||||
private instanceChart: InstanceChart,
|
private instanceChart: InstanceChart,
|
||||||
private utilityService: UtilityService,
|
private utilityService: UtilityService,
|
||||||
|
|
||||||
|
loggerService: LoggerService,
|
||||||
) {
|
) {
|
||||||
const logger = new Logger('drive', 'blue');
|
const logger = loggerService.getLogger('drive', 'blue');
|
||||||
this.registerLogger = logger.createSubLogger('register', 'yellow');
|
this.registerLogger = logger.createSubLogger('register', 'yellow');
|
||||||
this.downloaderLogger = logger.createSubLogger('downloader');
|
this.downloaderLogger = logger.createSubLogger('downloader');
|
||||||
this.deleteLogger = logger.createSubLogger('delete');
|
this.deleteLogger = logger.createSubLogger('delete');
|
||||||
|
|
|
@ -3,19 +3,25 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import Logger from '@/logger.js';
|
import Logger from '@/logger.js';
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type { KEYWORD } from 'color-convert/conversions.js';
|
import type { KEYWORD } from 'color-convert/conversions.js';
|
||||||
|
import { envOption } from '@/env.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import type { Config } from '@/config.js';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LoggerService {
|
export class LoggerService {
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DI.config)
|
||||||
|
private config: Config,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public getLogger(domain: string, color?: KEYWORD | undefined) {
|
public getLogger(domain: string, color?: KEYWORD | undefined) {
|
||||||
return new Logger(domain, color);
|
const verbose = this.config.logging?.verbose || envOption.verbose;
|
||||||
|
return new Logger(domain, color, verbose);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,9 +28,8 @@ import type { Config } from '@/config.js';
|
||||||
import { AccountMoveService } from '@/core/AccountMoveService.js';
|
import { AccountMoveService } from '@/core/AccountMoveService.js';
|
||||||
import { UtilityService } from '@/core/UtilityService.js';
|
import { UtilityService } from '@/core/UtilityService.js';
|
||||||
import type { ThinUser } from '@/queue/types.js';
|
import type { ThinUser } from '@/queue/types.js';
|
||||||
import Logger from '../logger.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
|
import type Logger from '../logger.js';
|
||||||
const logger = new Logger('following/create');
|
|
||||||
|
|
||||||
type Local = MiLocalUser | {
|
type Local = MiLocalUser | {
|
||||||
id: MiLocalUser['id'];
|
id: MiLocalUser['id'];
|
||||||
|
@ -48,6 +47,7 @@ type Both = Local | Remote;
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserFollowingService implements OnModuleInit {
|
export class UserFollowingService implements OnModuleInit {
|
||||||
private userBlockingService: UserBlockingService;
|
private userBlockingService: UserBlockingService;
|
||||||
|
private readonly logger: Logger;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private moduleRef: ModuleRef,
|
private moduleRef: ModuleRef,
|
||||||
|
@ -86,7 +86,10 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
private accountMoveService: AccountMoveService,
|
private accountMoveService: AccountMoveService,
|
||||||
private perUserFollowingChart: PerUserFollowingChart,
|
private perUserFollowingChart: PerUserFollowingChart,
|
||||||
private instanceChart: InstanceChart,
|
private instanceChart: InstanceChart,
|
||||||
|
|
||||||
|
loggerService: LoggerService,
|
||||||
) {
|
) {
|
||||||
|
this.logger = loggerService.getLogger('following/create');
|
||||||
}
|
}
|
||||||
|
|
||||||
onModuleInit() {
|
onModuleInit() {
|
||||||
|
@ -254,7 +257,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
followeeSharedInbox: this.userEntityService.isRemoteUser(followee) ? followee.sharedInbox : null,
|
followeeSharedInbox: this.userEntityService.isRemoteUser(followee) ? followee.sharedInbox : null,
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
if (isDuplicateKeyValueError(err) && this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
if (isDuplicateKeyValueError(err) && this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
|
||||||
logger.info(`Insert duplicated ignore. ${follower.id} => ${followee.id}`);
|
this.logger.info(`Insert duplicated ignore. ${follower.id} => ${followee.id}`);
|
||||||
alreadyFollowed = true;
|
alreadyFollowed = true;
|
||||||
} else {
|
} else {
|
||||||
throw err;
|
throw err;
|
||||||
|
@ -372,7 +375,7 @@ export class UserFollowingService implements OnModuleInit {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (following === null || !following.follower || !following.followee) {
|
if (following === null || !following.follower || !following.followee) {
|
||||||
logger.warn('フォロー解除がリクエストされましたがフォローしていませんでした');
|
this.logger.warn('フォロー解除がリクエストされましたがフォローしていませんでした');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,17 +27,19 @@ export type DataObject = Record<string, unknown> | (object & { length?: never; }
|
||||||
export default class Logger {
|
export default class Logger {
|
||||||
private context: Context;
|
private context: Context;
|
||||||
private parentLogger: Logger | null = null;
|
private parentLogger: Logger | null = null;
|
||||||
|
public readonly verbose: boolean;
|
||||||
|
|
||||||
constructor(context: string, color?: KEYWORD) {
|
constructor(context: string, color?: KEYWORD, verbose?: boolean) {
|
||||||
this.context = {
|
this.context = {
|
||||||
name: context,
|
name: context,
|
||||||
color: color,
|
color: color,
|
||||||
};
|
};
|
||||||
|
this.verbose = verbose ?? envOption.verbose;
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public createSubLogger(context: string, color?: KEYWORD): Logger {
|
public createSubLogger(context: string, color?: KEYWORD): Logger {
|
||||||
const logger = new Logger(context, color);
|
const logger = new Logger(context, color, this.verbose);
|
||||||
logger.parentLogger = this;
|
logger.parentLogger = this;
|
||||||
return logger;
|
return logger;
|
||||||
}
|
}
|
||||||
|
@ -110,7 +112,7 @@ export default class Logger {
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public debug(message: string, data?: Data, important = false): void { // デバッグ用に使う(開発者に必要だが利用者に不要な情報)
|
public debug(message: string, data?: Data, important = false): void { // デバッグ用に使う(開発者に必要だが利用者に不要な情報)
|
||||||
if (process.env.NODE_ENV !== 'production' || envOption.verbose) {
|
if (process.env.NODE_ENV !== 'production' || this.verbose) {
|
||||||
this.log('debug', message, data, important);
|
this.log('debug', message, data, important);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { pipeline } from 'node:stream/promises';
|
||||||
|
import fs from 'node:fs';
|
||||||
import * as tmp from 'tmp';
|
import * as tmp from 'tmp';
|
||||||
|
|
||||||
export function createTemp(): Promise<[string, () => void]> {
|
export function createTemp(): Promise<[string, () => void]> {
|
||||||
|
@ -27,3 +29,14 @@ export function createTempDir(): Promise<[string, () => void]> {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function saveToTempFile(stream: NodeJS.ReadableStream): Promise<string> {
|
||||||
|
const [filepath, cleanup] = await createTemp();
|
||||||
|
try {
|
||||||
|
await pipeline(stream, fs.createWriteStream(filepath));
|
||||||
|
return filepath;
|
||||||
|
} catch (e) {
|
||||||
|
cleanup();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { ApiTimelineMastodon } from '@/server/api/mastodon/endpoints/timeline.js
|
||||||
import { ApiAppsMastodon } from '@/server/api/mastodon/endpoints/apps.js';
|
import { ApiAppsMastodon } from '@/server/api/mastodon/endpoints/apps.js';
|
||||||
import { ApiInstanceMastodon } from '@/server/api/mastodon/endpoints/instance.js';
|
import { ApiInstanceMastodon } from '@/server/api/mastodon/endpoints/instance.js';
|
||||||
import { ApiStatusMastodon } from '@/server/api/mastodon/endpoints/status.js';
|
import { ApiStatusMastodon } from '@/server/api/mastodon/endpoints/status.js';
|
||||||
|
import { ServerUtilityService } from '@/server/ServerUtilityService.js';
|
||||||
import { ApiCallService } from './api/ApiCallService.js';
|
import { ApiCallService } from './api/ApiCallService.js';
|
||||||
import { FileServerService } from './FileServerService.js';
|
import { FileServerService } from './FileServerService.js';
|
||||||
import { HealthServerService } from './HealthServerService.js';
|
import { HealthServerService } from './HealthServerService.js';
|
||||||
|
@ -126,6 +127,7 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j
|
||||||
ApiSearchMastodon,
|
ApiSearchMastodon,
|
||||||
ApiStatusMastodon,
|
ApiStatusMastodon,
|
||||||
ApiTimelineMastodon,
|
ApiTimelineMastodon,
|
||||||
|
ServerUtilityService,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
ServerService,
|
ServerService,
|
||||||
|
|
162
packages/backend/src/server/ServerUtilityService.ts
Normal file
162
packages/backend/src/server/ServerUtilityService.ts
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import querystring from 'querystring';
|
||||||
|
import multipart from '@fastify/multipart';
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { FastifyInstance } from 'fastify';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import type { Config } from '@/config.js';
|
||||||
|
import { saveToTempFile } from '@/misc/create-temp.js';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ServerUtilityService {
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.config)
|
||||||
|
private readonly config: Config,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public addMultipartFormDataContentType(fastify: FastifyInstance): void {
|
||||||
|
fastify.register(multipart, {
|
||||||
|
limits: {
|
||||||
|
fileSize: this.config.maxFileSize,
|
||||||
|
files: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Default behavior saves files to memory - we don't want that!
|
||||||
|
// Store to temporary file instead, and copy the body fields while we're at it.
|
||||||
|
fastify.addHook<{ Body?: Record<string, string | string[] | undefined> }>('preValidation', async request => {
|
||||||
|
if (request.isMultipart()) {
|
||||||
|
// We can't use saveRequestFiles() because it erases all the data fields.
|
||||||
|
// Instead, recreate it manually.
|
||||||
|
// https://github.com/fastify/fastify-multipart/issues/549
|
||||||
|
|
||||||
|
for await (const part of request.parts()) {
|
||||||
|
if (part.type === 'field') {
|
||||||
|
const k = part.fieldname;
|
||||||
|
const v = part.value;
|
||||||
|
const body = request.body ??= {};
|
||||||
|
|
||||||
|
// Value can be string, buffer, or undefined.
|
||||||
|
// We only support the first one.
|
||||||
|
if (typeof(v) !== 'string') continue;
|
||||||
|
|
||||||
|
// This is just progressive conversion from undefined -> string -> string[]
|
||||||
|
if (!body[k]) {
|
||||||
|
body[k] = v;
|
||||||
|
} else if (Array.isArray(body[k])) {
|
||||||
|
body[k].push(v);
|
||||||
|
} else {
|
||||||
|
body[k] = [body[k], v];
|
||||||
|
}
|
||||||
|
} else { // Otherwise it's a file
|
||||||
|
try {
|
||||||
|
const filepath = await saveToTempFile(part.file);
|
||||||
|
|
||||||
|
const tmpUploads = (request.tmpUploads ??= []);
|
||||||
|
tmpUploads.push(filepath);
|
||||||
|
|
||||||
|
const requestSavedFiles = (request.savedRequestFiles ??= []);
|
||||||
|
requestSavedFiles.push({
|
||||||
|
...part,
|
||||||
|
filepath,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// Cleanup to avoid file leak in case of errors
|
||||||
|
await request.cleanRequestFiles();
|
||||||
|
request.tmpUploads = null;
|
||||||
|
request.savedRequestFiles = null;
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public addFormUrlEncodedContentType(fastify: FastifyInstance) {
|
||||||
|
fastify.addContentTypeParser('application/x-www-form-urlencoded', (_, payload, done) => {
|
||||||
|
let body = '';
|
||||||
|
payload.on('data', (data) => {
|
||||||
|
body += data;
|
||||||
|
});
|
||||||
|
payload.on('end', () => {
|
||||||
|
try {
|
||||||
|
const parsed = querystring.parse(body);
|
||||||
|
done(null, parsed);
|
||||||
|
} catch (e) {
|
||||||
|
done(e as Error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
payload.on('error', done);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public addCORS(fastify: FastifyInstance) {
|
||||||
|
fastify.addHook('preHandler', (_, reply, done) => {
|
||||||
|
// Allow web-based clients to connect from other origins.
|
||||||
|
reply.header('Access-Control-Allow-Origin', '*');
|
||||||
|
|
||||||
|
// Mastodon uses all types of request methods.
|
||||||
|
reply.header('Access-Control-Allow-Methods', '*');
|
||||||
|
|
||||||
|
// Allow web-based clients to access Link header - required for mastodon pagination.
|
||||||
|
// https://stackoverflow.com/a/54928828
|
||||||
|
// https://docs.joinmastodon.org/api/guidelines/#pagination
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Expose-Headers
|
||||||
|
reply.header('Access-Control-Expose-Headers', 'Link');
|
||||||
|
|
||||||
|
// Cache to avoid extra pre-flight requests
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Max-Age
|
||||||
|
reply.header('Access-Control-Max-Age', 60 * 60 * 24); // 1 day in seconds
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public addFlattenedQueryType(fastify: FastifyInstance) {
|
||||||
|
// Remove trailing "[]" from query params
|
||||||
|
fastify.addHook<{ Querystring?: Record<string, string | string[] | undefined> }>('preValidation', (request, _reply, done) => {
|
||||||
|
if (!request.query || typeof(request.query) !== 'object') {
|
||||||
|
return done();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of Object.keys(request.query)) {
|
||||||
|
if (!key.endsWith('[]')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (request.query[key] == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newKey = key.substring(0, key.length - 2);
|
||||||
|
const newValue = request.query[key];
|
||||||
|
const oldValue = request.query[newKey];
|
||||||
|
|
||||||
|
// Move the value to the correct key
|
||||||
|
if (oldValue != null) {
|
||||||
|
if (Array.isArray(oldValue)) {
|
||||||
|
// Works for both array and single values
|
||||||
|
request.query[newKey] = oldValue.concat(newValue);
|
||||||
|
} else if (Array.isArray(newValue)) {
|
||||||
|
// Preserve order
|
||||||
|
request.query[newKey] = [oldValue, ...newValue];
|
||||||
|
} else {
|
||||||
|
// Preserve order
|
||||||
|
request.query[newKey] = [oldValue, newValue];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
request.query[newKey] = newValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the invalid key
|
||||||
|
delete request.query[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
return done();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -84,6 +84,8 @@ export class AuthenticateService implements OnApplicationShutdown {
|
||||||
return [user, {
|
return [user, {
|
||||||
id: accessToken.id,
|
id: accessToken.id,
|
||||||
permission: app.permission,
|
permission: app.permission,
|
||||||
|
appId: app.id,
|
||||||
|
app,
|
||||||
} as MiAccessToken];
|
} as MiAccessToken];
|
||||||
} else {
|
} else {
|
||||||
return [user, accessToken];
|
return [user, accessToken];
|
||||||
|
|
|
@ -128,6 +128,7 @@ export * as 'antennas/update' from './endpoints/antennas/update.js';
|
||||||
export * as 'ap/get' from './endpoints/ap/get.js';
|
export * as 'ap/get' from './endpoints/ap/get.js';
|
||||||
export * as 'ap/show' from './endpoints/ap/show.js';
|
export * as 'ap/show' from './endpoints/ap/show.js';
|
||||||
export * as 'app/create' from './endpoints/app/create.js';
|
export * as 'app/create' from './endpoints/app/create.js';
|
||||||
|
export * as 'app/current' from './endpoints/app/current.js';
|
||||||
export * as 'app/show' from './endpoints/app/show.js';
|
export * as 'app/show' from './endpoints/app/show.js';
|
||||||
export * as 'auth/accept' from './endpoints/auth/accept.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/generate' from './endpoints/auth/session/generate.js';
|
||||||
|
|
73
packages/backend/src/server/api/endpoints/app/current.ts
Normal file
73
packages/backend/src/server/api/endpoints/app/current.ts
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
* 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 { AppsRepository } from '@/models/_.js';
|
||||||
|
import { AppEntityService } from '@/core/entities/AppEntityService.js';
|
||||||
|
import { DI } from '@/di-symbols.js';
|
||||||
|
import { ApiError } from '../../error.js';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
tags: ['app'],
|
||||||
|
|
||||||
|
errors: {
|
||||||
|
credentialRequired: {
|
||||||
|
message: 'Credential required.',
|
||||||
|
code: 'CREDENTIAL_REQUIRED',
|
||||||
|
id: '1384574d-a912-4b81-8601-c7b1c4085df1',
|
||||||
|
httpStatusCode: 401,
|
||||||
|
},
|
||||||
|
noAppLogin: {
|
||||||
|
message: 'Not logged in with an app.',
|
||||||
|
code: 'NO_APP_LOGIN',
|
||||||
|
id: '339a4ad2-48c3-47fc-bd9d-2408f05120f8',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
res: {
|
||||||
|
type: 'object',
|
||||||
|
optional: false, nullable: false,
|
||||||
|
ref: 'App',
|
||||||
|
},
|
||||||
|
|
||||||
|
// 10 calls per 5 seconds
|
||||||
|
limit: {
|
||||||
|
duration: 1000 * 5,
|
||||||
|
max: 10,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const paramDef = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {},
|
||||||
|
required: [],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||||
|
constructor(
|
||||||
|
@Inject(DI.appsRepository)
|
||||||
|
private appsRepository: AppsRepository,
|
||||||
|
|
||||||
|
private appEntityService: AppEntityService,
|
||||||
|
) {
|
||||||
|
super(meta, paramDef, async (_, user, token) => {
|
||||||
|
if (!user) {
|
||||||
|
throw new ApiError(meta.errors.credentialRequired);
|
||||||
|
}
|
||||||
|
if (!token || !token.appId) {
|
||||||
|
throw new ApiError(meta.errors.noAppLogin);
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = token.app ?? await this.appsRepository.findOneByOrFail({ id: token.appId });
|
||||||
|
|
||||||
|
return await this.appEntityService.pack(app, user, {
|
||||||
|
detail: true,
|
||||||
|
includeSecret: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,13 +3,9 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import querystring from 'querystring';
|
import { Injectable } from '@nestjs/common';
|
||||||
import multer from 'fastify-multer';
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
|
||||||
import { DI } from '@/di-symbols.js';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type { Config } from '@/config.js';
|
import { getErrorData, getErrorException, getErrorStatus, MastodonLogger } from '@/server/api/mastodon/MastodonLogger.js';
|
||||||
import { getErrorData, getErrorStatus, MastodonLogger } from '@/server/api/mastodon/MastodonLogger.js';
|
|
||||||
import { MastodonClientService } from '@/server/api/mastodon/MastodonClientService.js';
|
import { MastodonClientService } from '@/server/api/mastodon/MastodonClientService.js';
|
||||||
import { ApiAccountMastodon } from '@/server/api/mastodon/endpoints/account.js';
|
import { ApiAccountMastodon } from '@/server/api/mastodon/endpoints/account.js';
|
||||||
import { ApiAppsMastodon } from '@/server/api/mastodon/endpoints/apps.js';
|
import { ApiAppsMastodon } from '@/server/api/mastodon/endpoints/apps.js';
|
||||||
|
@ -20,6 +16,7 @@ import { ApiNotificationsMastodon } from '@/server/api/mastodon/endpoints/notifi
|
||||||
import { ApiTimelineMastodon } from '@/server/api/mastodon/endpoints/timeline.js';
|
import { ApiTimelineMastodon } from '@/server/api/mastodon/endpoints/timeline.js';
|
||||||
import { ApiSearchMastodon } from '@/server/api/mastodon/endpoints/search.js';
|
import { ApiSearchMastodon } from '@/server/api/mastodon/endpoints/search.js';
|
||||||
import { ApiError } from '@/server/api/error.js';
|
import { ApiError } from '@/server/api/error.js';
|
||||||
|
import { ServerUtilityService } from '@/server/ServerUtilityService.js';
|
||||||
import { parseTimelineArgs, TimelineArgs, toBoolean } from './argsUtils.js';
|
import { parseTimelineArgs, TimelineArgs, toBoolean } from './argsUtils.js';
|
||||||
import { convertAnnouncement, convertAttachment, MastodonConverters, convertRelationship } from './MastodonConverters.js';
|
import { convertAnnouncement, convertAttachment, MastodonConverters, convertRelationship } from './MastodonConverters.js';
|
||||||
import type { Entity } from 'megalodon';
|
import type { Entity } from 'megalodon';
|
||||||
|
@ -28,9 +25,6 @@ import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MastodonApiServerService {
|
export class MastodonApiServerService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DI.config)
|
|
||||||
private readonly config: Config,
|
|
||||||
|
|
||||||
private readonly mastoConverters: MastodonConverters,
|
private readonly mastoConverters: MastodonConverters,
|
||||||
private readonly logger: MastodonLogger,
|
private readonly logger: MastodonLogger,
|
||||||
private readonly clientService: MastodonClientService,
|
private readonly clientService: MastodonClientService,
|
||||||
|
@ -42,115 +36,47 @@ export class MastodonApiServerService {
|
||||||
private readonly apiSearchMastodon: ApiSearchMastodon,
|
private readonly apiSearchMastodon: ApiSearchMastodon,
|
||||||
private readonly apiStatusMastodon: ApiStatusMastodon,
|
private readonly apiStatusMastodon: ApiStatusMastodon,
|
||||||
private readonly apiTimelineMastodon: ApiTimelineMastodon,
|
private readonly apiTimelineMastodon: ApiTimelineMastodon,
|
||||||
|
private readonly serverUtilityService: ServerUtilityService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public createServer(fastify: FastifyInstance, _options: FastifyPluginOptions, done: (err?: Error) => void) {
|
public createServer(fastify: FastifyInstance, _options: FastifyPluginOptions, done: (err?: Error) => void) {
|
||||||
const upload = multer({
|
this.serverUtilityService.addMultipartFormDataContentType(fastify);
|
||||||
storage: multer.diskStorage({}),
|
this.serverUtilityService.addFormUrlEncodedContentType(fastify);
|
||||||
limits: {
|
this.serverUtilityService.addCORS(fastify);
|
||||||
fileSize: this.config.maxFileSize || 262144000,
|
this.serverUtilityService.addFlattenedQueryType(fastify);
|
||||||
files: 1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
fastify.addHook('onRequest', (_, reply, done) => {
|
|
||||||
// Allow web-based clients to connect from other origins.
|
|
||||||
reply.header('Access-Control-Allow-Origin', '*');
|
|
||||||
|
|
||||||
// Mastodon uses all types of request methods.
|
|
||||||
reply.header('Access-Control-Allow-Methods', '*');
|
|
||||||
|
|
||||||
// Allow web-based clients to access Link header - required for mastodon pagination.
|
|
||||||
// https://stackoverflow.com/a/54928828
|
|
||||||
// https://docs.joinmastodon.org/api/guidelines/#pagination
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Expose-Headers
|
|
||||||
reply.header('Access-Control-Expose-Headers', 'Link');
|
|
||||||
|
|
||||||
// Cache to avoid extra pre-flight requests
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Max-Age
|
|
||||||
reply.header('Access-Control-Max-Age', 60 * 60 * 24); // 1 day in seconds
|
|
||||||
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
fastify.addContentTypeParser('application/x-www-form-urlencoded', (_, payload, done) => {
|
|
||||||
let body = '';
|
|
||||||
payload.on('data', (data) => {
|
|
||||||
body += data;
|
|
||||||
});
|
|
||||||
payload.on('end', () => {
|
|
||||||
try {
|
|
||||||
const parsed = querystring.parse(body);
|
|
||||||
done(null, parsed);
|
|
||||||
} catch (e) {
|
|
||||||
done(e as Error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
payload.on('error', done);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove trailing "[]" from query params
|
|
||||||
fastify.addHook('preValidation', (request, _reply, done) => {
|
|
||||||
if (!request.query || typeof(request.query) !== 'object') {
|
|
||||||
return done();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Same object aliased with a different type
|
|
||||||
const query = request.query as Record<string, string | string[] | undefined>;
|
|
||||||
|
|
||||||
for (const key of Object.keys(query)) {
|
|
||||||
if (!key.endsWith('[]')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (query[key] == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newKey = key.substring(0, key.length - 2);
|
|
||||||
const newValue = query[key];
|
|
||||||
const oldValue = query[newKey];
|
|
||||||
|
|
||||||
// Move the value to the correct key
|
|
||||||
if (oldValue != null) {
|
|
||||||
if (Array.isArray(oldValue)) {
|
|
||||||
// Works for both array and single values
|
|
||||||
query[newKey] = oldValue.concat(newValue);
|
|
||||||
} else if (Array.isArray(newValue)) {
|
|
||||||
// Preserve order
|
|
||||||
query[newKey] = [oldValue, ...newValue];
|
|
||||||
} else {
|
|
||||||
// Preserve order
|
|
||||||
query[newKey] = [oldValue, newValue];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
query[newKey] = newValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the invalid key
|
|
||||||
delete query[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
return done();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// Convert JS exceptions into error responses
|
||||||
fastify.setErrorHandler((error, request, reply) => {
|
fastify.setErrorHandler((error, request, reply) => {
|
||||||
const data = getErrorData(error);
|
const data = getErrorData(error);
|
||||||
const status = getErrorStatus(error);
|
const status = getErrorStatus(error);
|
||||||
|
const exception = getErrorException(error);
|
||||||
|
|
||||||
this.logger.error(request, data, status);
|
if (exception) {
|
||||||
|
this.logger.exception(request, exception);
|
||||||
|
}
|
||||||
|
|
||||||
reply.code(status).send(data);
|
return reply.code(status).send(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.register(multer.contentParser);
|
// Log error responses (including converted JSON exceptions)
|
||||||
|
fastify.addHook('onSend', (request, reply, payload, done) => {
|
||||||
|
if (reply.statusCode >= 400) {
|
||||||
|
if (typeof(payload) === 'string' && String(reply.getHeader('content-type')).toLowerCase().includes('application/json')) {
|
||||||
|
const body = JSON.parse(payload);
|
||||||
|
const data = getErrorData(body);
|
||||||
|
this.logger.error(request, data, reply.statusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
// External endpoints
|
// External endpoints
|
||||||
this.apiAccountMastodon.register(fastify, upload);
|
this.apiAccountMastodon.register(fastify);
|
||||||
this.apiAppsMastodon.register(fastify, upload);
|
this.apiAppsMastodon.register(fastify);
|
||||||
this.apiFilterMastodon.register(fastify, upload);
|
this.apiFilterMastodon.register(fastify);
|
||||||
this.apiInstanceMastodon.register(fastify);
|
this.apiInstanceMastodon.register(fastify);
|
||||||
this.apiNotificationsMastodon.register(fastify, upload);
|
this.apiNotificationsMastodon.register(fastify);
|
||||||
this.apiSearchMastodon.register(fastify);
|
this.apiSearchMastodon.register(fastify);
|
||||||
this.apiStatusMastodon.register(fastify);
|
this.apiStatusMastodon.register(fastify);
|
||||||
this.apiTimelineMastodon.register(fastify);
|
this.apiTimelineMastodon.register(fastify);
|
||||||
|
@ -158,7 +84,7 @@ export class MastodonApiServerService {
|
||||||
fastify.get('/v1/custom_emojis', async (_request, reply) => {
|
fastify.get('/v1/custom_emojis', async (_request, reply) => {
|
||||||
const client = this.clientService.getClient(_request);
|
const client = this.clientService.getClient(_request);
|
||||||
const data = await client.getInstanceCustomEmojis();
|
const data = await client.getInstanceCustomEmojis();
|
||||||
reply.send(data.data);
|
return reply.send(data.data);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get('/v1/announcements', async (_request, reply) => {
|
fastify.get('/v1/announcements', async (_request, reply) => {
|
||||||
|
@ -166,7 +92,7 @@ export class MastodonApiServerService {
|
||||||
const data = await client.getInstanceAnnouncements();
|
const data = await client.getInstanceAnnouncements();
|
||||||
const response = data.data.map((announcement) => convertAnnouncement(announcement));
|
const response = data.data.map((announcement) => convertAnnouncement(announcement));
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.post<{ Body: { id?: string } }>('/v1/announcements/:id/dismiss', async (_request, reply) => {
|
fastify.post<{ Body: { id?: string } }>('/v1/announcements/:id/dismiss', async (_request, reply) => {
|
||||||
|
@ -175,64 +101,62 @@ export class MastodonApiServerService {
|
||||||
const client = this.clientService.getClient(_request);
|
const client = this.clientService.getClient(_request);
|
||||||
const data = await client.dismissInstanceAnnouncement(_request.body.id);
|
const data = await client.dismissInstanceAnnouncement(_request.body.id);
|
||||||
|
|
||||||
reply.send(data.data);
|
return reply.send(data.data);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.post('/v1/media', { preHandler: upload.single('file') }, async (_request, reply) => {
|
fastify.post('/v1/media', async (_request, reply) => {
|
||||||
const multipartData = await _request.file();
|
const multipartData = _request.savedRequestFiles?.[0];
|
||||||
if (!multipartData) {
|
if (!multipartData) {
|
||||||
reply.code(401).send({ error: 'No image' });
|
return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'No image' });
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = this.clientService.getClient(_request);
|
const client = this.clientService.getClient(_request);
|
||||||
const data = await client.uploadMedia(multipartData);
|
const data = await client.uploadMedia(multipartData);
|
||||||
const response = convertAttachment(data.data as Entity.Attachment);
|
const response = convertAttachment(data.data as Entity.Attachment);
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.post<{ Body: { description?: string; focus?: string } }>('/v2/media', { preHandler: upload.single('file') }, async (_request, reply) => {
|
fastify.post<{ Body: { description?: string; focus?: string } }>('/v2/media', async (_request, reply) => {
|
||||||
const multipartData = await _request.file();
|
const multipartData = _request.savedRequestFiles?.[0];
|
||||||
if (!multipartData) {
|
if (!multipartData) {
|
||||||
reply.code(401).send({ error: 'No image' });
|
return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'No image' });
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const client = this.clientService.getClient(_request);
|
const client = this.clientService.getClient(_request);
|
||||||
const data = await client.uploadMedia(multipartData, _request.body);
|
const data = await client.uploadMedia(multipartData, _request.body);
|
||||||
const response = convertAttachment(data.data as Entity.Attachment);
|
const response = convertAttachment(data.data as Entity.Attachment);
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get('/v1/trends', async (_request, reply) => {
|
fastify.get('/v1/trends', async (_request, reply) => {
|
||||||
const client = this.clientService.getClient(_request);
|
const client = this.clientService.getClient(_request);
|
||||||
const data = await client.getInstanceTrends();
|
const data = await client.getInstanceTrends();
|
||||||
reply.send(data.data);
|
return reply.send(data.data);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get('/v1/trends/tags', async (_request, reply) => {
|
fastify.get('/v1/trends/tags', async (_request, reply) => {
|
||||||
const client = this.clientService.getClient(_request);
|
const client = this.clientService.getClient(_request);
|
||||||
const data = await client.getInstanceTrends();
|
const data = await client.getInstanceTrends();
|
||||||
reply.send(data.data);
|
return reply.send(data.data);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get('/v1/trends/links', async (_request, reply) => {
|
fastify.get('/v1/trends/links', async (_request, reply) => {
|
||||||
// As we do not have any system for news/links this will just return empty
|
// As we do not have any system for news/links this will just return empty
|
||||||
reply.send([]);
|
return reply.send([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get('/v1/preferences', async (_request, reply) => {
|
fastify.get('/v1/preferences', async (_request, reply) => {
|
||||||
const client = this.clientService.getClient(_request);
|
const client = this.clientService.getClient(_request);
|
||||||
const data = await client.getPreferences();
|
const data = await client.getPreferences();
|
||||||
reply.send(data.data);
|
return reply.send(data.data);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get('/v1/followed_tags', async (_request, reply) => {
|
fastify.get('/v1/followed_tags', async (_request, reply) => {
|
||||||
const client = this.clientService.getClient(_request);
|
const client = this.clientService.getClient(_request);
|
||||||
const data = await client.getFollowedTags();
|
const data = await client.getFollowedTags();
|
||||||
reply.send(data.data);
|
return reply.send(data.data);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<{ Querystring: TimelineArgs }>('/v1/bookmarks', async (_request, reply) => {
|
fastify.get<{ Querystring: TimelineArgs }>('/v1/bookmarks', async (_request, reply) => {
|
||||||
|
@ -241,7 +165,7 @@ export class MastodonApiServerService {
|
||||||
const data = await client.getBookmarks(parseTimelineArgs(_request.query));
|
const data = await client.getBookmarks(parseTimelineArgs(_request.query));
|
||||||
const response = await Promise.all(data.data.map((status) => this.mastoConverters.convertStatus(status, me)));
|
const response = await Promise.all(data.data.map((status) => this.mastoConverters.convertStatus(status, me)));
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<{ Querystring: TimelineArgs }>('/v1/favourites', async (_request, reply) => {
|
fastify.get<{ Querystring: TimelineArgs }>('/v1/favourites', async (_request, reply) => {
|
||||||
|
@ -263,7 +187,7 @@ export class MastodonApiServerService {
|
||||||
const data = await client.getFavourites(args);
|
const data = await client.getFavourites(args);
|
||||||
const response = await Promise.all(data.data.map((status) => this.mastoConverters.convertStatus(status, me)));
|
const response = await Promise.all(data.data.map((status) => this.mastoConverters.convertStatus(status, me)));
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<{ Querystring: TimelineArgs }>('/v1/mutes', async (_request, reply) => {
|
fastify.get<{ Querystring: TimelineArgs }>('/v1/mutes', async (_request, reply) => {
|
||||||
|
@ -272,7 +196,7 @@ export class MastodonApiServerService {
|
||||||
const data = await client.getMutes(parseTimelineArgs(_request.query));
|
const data = await client.getMutes(parseTimelineArgs(_request.query));
|
||||||
const response = await Promise.all(data.data.map((account) => this.mastoConverters.convertAccount(account)));
|
const response = await Promise.all(data.data.map((account) => this.mastoConverters.convertAccount(account)));
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<{ Querystring: TimelineArgs }>('/v1/blocks', async (_request, reply) => {
|
fastify.get<{ Querystring: TimelineArgs }>('/v1/blocks', async (_request, reply) => {
|
||||||
|
@ -281,7 +205,7 @@ export class MastodonApiServerService {
|
||||||
const data = await client.getBlocks(parseTimelineArgs(_request.query));
|
const data = await client.getBlocks(parseTimelineArgs(_request.query));
|
||||||
const response = await Promise.all(data.data.map((account) => this.mastoConverters.convertAccount(account)));
|
const response = await Promise.all(data.data.map((account) => this.mastoConverters.convertAccount(account)));
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<{ Querystring: { limit?: string } }>('/v1/follow_requests', async (_request, reply) => {
|
fastify.get<{ Querystring: { limit?: string } }>('/v1/follow_requests', async (_request, reply) => {
|
||||||
|
@ -291,27 +215,27 @@ export class MastodonApiServerService {
|
||||||
const data = await client.getFollowRequests(limit);
|
const data = await client.getFollowRequests(limit);
|
||||||
const response = await Promise.all(data.data.map((account) => this.mastoConverters.convertAccount(account as Entity.Account)));
|
const response = await Promise.all(data.data.map((account) => this.mastoConverters.convertAccount(account as Entity.Account)));
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.post<{ Querystring: TimelineArgs, Params: { id?: string } }>('/v1/follow_requests/:id/authorize', { preHandler: upload.single('none') }, async (_request, reply) => {
|
fastify.post<{ Params: { id?: string } }>('/v1/follow_requests/:id/authorize', async (_request, reply) => {
|
||||||
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
|
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
|
||||||
|
|
||||||
const client = this.clientService.getClient(_request);
|
const client = this.clientService.getClient(_request);
|
||||||
const data = await client.acceptFollowRequest(_request.params.id);
|
const data = await client.acceptFollowRequest(_request.params.id);
|
||||||
const response = convertRelationship(data.data);
|
const response = convertRelationship(data.data);
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.post<{ Querystring: TimelineArgs, Params: { id?: string } }>('/v1/follow_requests/:id/reject', { preHandler: upload.single('none') }, async (_request, reply) => {
|
fastify.post<{ Params: { id?: string } }>('/v1/follow_requests/:id/reject', async (_request, reply) => {
|
||||||
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
|
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
|
||||||
|
|
||||||
const client = this.clientService.getClient(_request);
|
const client = this.clientService.getClient(_request);
|
||||||
const data = await client.rejectFollowRequest(_request.params.id);
|
const data = await client.rejectFollowRequest(_request.params.id);
|
||||||
const response = convertRelationship(data.data);
|
const response = convertRelationship(data.data);
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
@ -325,7 +249,7 @@ export class MastodonApiServerService {
|
||||||
focus?: string,
|
focus?: string,
|
||||||
is_sensitive?: string,
|
is_sensitive?: string,
|
||||||
},
|
},
|
||||||
}>('/v1/media/:id', { preHandler: upload.none() }, async (_request, reply) => {
|
}>('/v1/media/:id', async (_request, reply) => {
|
||||||
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
|
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
|
@ -336,7 +260,7 @@ export class MastodonApiServerService {
|
||||||
const data = await client.updateMedia(_request.params.id, options);
|
const data = await client.updateMedia(_request.params.id, options);
|
||||||
const response = convertAttachment(data.data);
|
const response = convertAttachment(data.data);
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
done();
|
done();
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { Entity, MastodonEntity } from 'megalodon';
|
import { Entity, MastodonEntity, MisskeyEntity } from 'megalodon';
|
||||||
import mfm from '@transfem-org/sfm-js';
|
import mfm from '@transfem-org/sfm-js';
|
||||||
import { MastodonNotificationType } from 'megalodon/lib/src/mastodon/notification.js';
|
import { MastodonNotificationType } from 'megalodon/lib/src/mastodon/notification.js';
|
||||||
import { NotificationType } from 'megalodon/lib/src/notification.js';
|
import { NotificationType } from 'megalodon/lib/src/notification.js';
|
||||||
|
@ -369,6 +369,15 @@ export class MastodonConverters {
|
||||||
type: convertNotificationType(notification.type as NotificationType),
|
type: convertNotificationType(notification.type as NotificationType),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public convertApplication(app: MisskeyEntity.App): MastodonEntity.Application {
|
||||||
|
return {
|
||||||
|
name: app.name,
|
||||||
|
scopes: app.permission,
|
||||||
|
redirect_uri: app.callbackUrl,
|
||||||
|
redirect_uris: [app.callbackUrl],
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function simpleConvert<T>(data: T): T {
|
function simpleConvert<T>(data: T): T {
|
||||||
|
@ -459,4 +468,3 @@ export function convertRelationship(relationship: Partial<Entity.Relationship> &
|
||||||
note: relationship.note ?? '',
|
note: relationship.note ?? '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,33 +3,49 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { FastifyRequest } from 'fastify';
|
import { isAxiosError } from 'axios';
|
||||||
import Logger from '@/logger.js';
|
import type Logger from '@/logger.js';
|
||||||
import { LoggerService } from '@/core/LoggerService.js';
|
import { LoggerService } from '@/core/LoggerService.js';
|
||||||
import { ApiError } from '@/server/api/error.js';
|
import { ApiError } from '@/server/api/error.js';
|
||||||
import { EnvService } from '@/core/EnvService.js';
|
|
||||||
import { getBaseUrl } from '@/server/api/mastodon/MastodonClientService.js';
|
import { getBaseUrl } from '@/server/api/mastodon/MastodonClientService.js';
|
||||||
|
import { AuthenticationError } from '@/server/api/AuthenticateService.js';
|
||||||
|
import type { FastifyRequest } from 'fastify';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MastodonLogger {
|
export class MastodonLogger {
|
||||||
public readonly logger: Logger;
|
public readonly logger: Logger;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(EnvService)
|
|
||||||
private readonly envService: EnvService,
|
|
||||||
|
|
||||||
loggerService: LoggerService,
|
loggerService: LoggerService,
|
||||||
) {
|
) {
|
||||||
this.logger = loggerService.getLogger('masto-api');
|
this.logger = loggerService.getLogger('masto-api');
|
||||||
}
|
}
|
||||||
|
|
||||||
public error(request: FastifyRequest, error: MastodonError, status: number): void {
|
public error(request: FastifyRequest, error: MastodonError, status: number): void {
|
||||||
if ((status < 400 && status > 499) || this.envService.env.NODE_ENV === 'development') {
|
const path = getPath(request);
|
||||||
const path = new URL(request.url, getBaseUrl(request)).pathname;
|
|
||||||
|
if (status >= 400 && status <= 499) { // Client errors
|
||||||
|
this.logger.debug(`Error in mastodon endpoint ${request.method} ${path}:`, error);
|
||||||
|
} else { // Server errors
|
||||||
this.logger.error(`Error in mastodon endpoint ${request.method} ${path}:`, error);
|
this.logger.error(`Error in mastodon endpoint ${request.method} ${path}:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public exception(request: FastifyRequest, ex: Error): void {
|
||||||
|
const path = getPath(request);
|
||||||
|
|
||||||
|
// Exceptions are always server errors, and should therefore always be logged.
|
||||||
|
this.logger.error(`Exception in mastodon endpoint ${request.method} ${path}:`, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPath(request: FastifyRequest): string {
|
||||||
|
try {
|
||||||
|
return new URL(request.url, getBaseUrl(request)).pathname;
|
||||||
|
} catch {
|
||||||
|
return request.url;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO move elsewhere
|
// TODO move elsewhere
|
||||||
|
@ -38,6 +54,43 @@ export interface MastodonError {
|
||||||
error_description?: string;
|
error_description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getErrorException(error: unknown): Error | null {
|
||||||
|
if (!(error instanceof Error)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AxiosErrors need special decoding
|
||||||
|
if (isAxiosError(error)) {
|
||||||
|
// Axios errors with a response are from the remote
|
||||||
|
if (error.response) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is the inner exception, basically
|
||||||
|
if (error.cause && !isAxiosError(error.cause)) {
|
||||||
|
if (!error.cause.stack) {
|
||||||
|
error.cause.stack = error.stack;
|
||||||
|
}
|
||||||
|
|
||||||
|
return error.cause;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ex = new Error();
|
||||||
|
ex.name = error.name;
|
||||||
|
ex.stack = error.stack;
|
||||||
|
ex.message = error.message;
|
||||||
|
ex.cause = error.cause;
|
||||||
|
return ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuthenticationError is a client error
|
||||||
|
if (error instanceof AuthenticationError) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
export function getErrorData(error: unknown): MastodonError {
|
export function getErrorData(error: unknown): MastodonError {
|
||||||
// Axios wraps errors from the backend
|
// Axios wraps errors from the backend
|
||||||
error = unpackAxiosError(error);
|
error = unpackAxiosError(error);
|
||||||
|
@ -59,17 +112,33 @@ export function getErrorData(error: unknown): MastodonError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ('error' in error && typeof (error.error) === 'string') {
|
||||||
|
if ('message' in error && typeof (error.message) === 'string') {
|
||||||
|
return convertErrorMessageError(error as { error: string, message: string });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
return convertGenericError(error);
|
return convertGenericError(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return convertUnknownError(error);
|
if ('error' in error && typeof(error.error) === 'string') {
|
||||||
|
// "error_description" is string, undefined, or not present.
|
||||||
|
if (!('error_description' in error) || typeof(error.error_description) === 'string' || typeof(error.error_description) === 'undefined') {
|
||||||
|
return convertMastodonError(error as MastodonError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
error: 'INTERNAL_ERROR',
|
||||||
|
error_description: 'Internal error occurred. Please contact us if the error persists.',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function unpackAxiosError(error: unknown): unknown {
|
function unpackAxiosError(error: unknown): unknown {
|
||||||
if (error && typeof(error) === 'object') {
|
if (isAxiosError(error)) {
|
||||||
if ('response' in error && error.response && typeof (error.response) === 'object') {
|
if (error.response) {
|
||||||
if ('data' in error.response && error.response.data && typeof (error.response.data) === 'object') {
|
if (error.response.data && typeof(error.response.data) === 'object') {
|
||||||
if ('error' in error.response.data && error.response.data.error && typeof(error.response.data.error) === 'object') {
|
if ('error' in error.response.data && error.response.data.error && typeof(error.response.data.error) === 'object') {
|
||||||
return error.response.data.error;
|
return error.response.data.error;
|
||||||
}
|
}
|
||||||
|
@ -80,46 +149,48 @@ function unpackAxiosError(error: unknown): unknown {
|
||||||
// No data - this is a fallback to avoid leaking request/response details in the error
|
// No data - this is a fallback to avoid leaking request/response details in the error
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error.cause && !isAxiosError(error.cause)) {
|
||||||
|
if (!error.cause.stack) {
|
||||||
|
error.cause.stack = error.stack;
|
||||||
|
}
|
||||||
|
|
||||||
|
return error.cause;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No data - this is a fallback to avoid leaking request/response details in the error
|
||||||
|
return String(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertApiError(apiError: ApiError): MastodonError {
|
function convertApiError(apiError: ApiError): MastodonError {
|
||||||
const mastoError: MastodonError & Partial<ApiError> = {
|
return {
|
||||||
error: apiError.code,
|
error: apiError.code,
|
||||||
error_description: apiError.message,
|
error_description: apiError.message,
|
||||||
...apiError,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
delete mastoError.code;
|
|
||||||
delete mastoError.message;
|
|
||||||
delete mastoError.httpStatusCode;
|
|
||||||
|
|
||||||
return mastoError;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertUnknownError(data: object = {}): MastodonError {
|
function convertErrorMessageError(error: { error: string, message: string }): MastodonError {
|
||||||
return Object.assign({}, data, {
|
return {
|
||||||
error: 'INTERNAL_ERROR',
|
error: error.error,
|
||||||
error_description: 'Internal error occurred. Please contact us if the error persists.',
|
error_description: error.message,
|
||||||
id: '5d37dbcb-891e-41ca-a3d6-e690c97775ac',
|
};
|
||||||
kind: 'server',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertGenericError(error: Error): MastodonError {
|
function convertGenericError(error: Error): MastodonError {
|
||||||
const mastoError: MastodonError & Partial<Error> = {
|
return {
|
||||||
error: 'INTERNAL_ERROR',
|
error: 'INTERNAL_ERROR',
|
||||||
error_description: String(error),
|
error_description: String(error),
|
||||||
...error,
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
delete mastoError.name;
|
function convertMastodonError(error: MastodonError): MastodonError {
|
||||||
delete mastoError.message;
|
return {
|
||||||
delete mastoError.stack;
|
error: error.error,
|
||||||
|
error_description: error.error_description,
|
||||||
return mastoError;
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getErrorStatus(error: unknown): number {
|
export function getErrorStatus(error: unknown): number {
|
||||||
|
@ -134,6 +205,10 @@ export function getErrorStatus(error: unknown): number {
|
||||||
if ('httpStatusCode' in error && typeof(error.httpStatusCode) === 'number') {
|
if ('httpStatusCode' in error && typeof(error.httpStatusCode) === 'number') {
|
||||||
return error.httpStatusCode;
|
return error.httpStatusCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ('statusCode' in error && typeof(error.statusCode) === 'number') {
|
||||||
|
return error.statusCode;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return 500;
|
return 500;
|
||||||
|
|
|
@ -11,7 +11,6 @@ import { DI } from '@/di-symbols.js';
|
||||||
import type { AccessTokensRepository, UserProfilesRepository } from '@/models/_.js';
|
import type { AccessTokensRepository, UserProfilesRepository } from '@/models/_.js';
|
||||||
import { attachMinMaxPagination } from '@/server/api/mastodon/pagination.js';
|
import { attachMinMaxPagination } from '@/server/api/mastodon/pagination.js';
|
||||||
import { MastodonConverters, convertRelationship, convertFeaturedTag, convertList } from '../MastodonConverters.js';
|
import { MastodonConverters, convertRelationship, convertFeaturedTag, convertList } from '../MastodonConverters.js';
|
||||||
import type multer from 'fastify-multer';
|
|
||||||
import type { FastifyInstance } from 'fastify';
|
import type { FastifyInstance } from 'fastify';
|
||||||
|
|
||||||
interface ApiAccountMastodonRoute {
|
interface ApiAccountMastodonRoute {
|
||||||
|
@ -34,7 +33,7 @@ export class ApiAccountMastodon {
|
||||||
private readonly driveService: DriveService,
|
private readonly driveService: DriveService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public register(fastify: FastifyInstance, upload: ReturnType<typeof multer>): void {
|
public register(fastify: FastifyInstance): void {
|
||||||
fastify.get<ApiAccountMastodonRoute>('/v1/accounts/verify_credentials', async (_request, reply) => {
|
fastify.get<ApiAccountMastodonRoute>('/v1/accounts/verify_credentials', async (_request, reply) => {
|
||||||
const client = this.clientService.getClient(_request);
|
const client = this.clientService.getClient(_request);
|
||||||
const data = await client.verifyAccountCredentials();
|
const data = await client.verifyAccountCredentials();
|
||||||
|
@ -48,7 +47,7 @@ export class ApiAccountMastodon {
|
||||||
language: '',
|
language: '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.patch<{
|
fastify.patch<{
|
||||||
|
@ -70,60 +69,50 @@ export class ApiAccountMastodon {
|
||||||
value: string,
|
value: string,
|
||||||
}[],
|
}[],
|
||||||
},
|
},
|
||||||
}>('/v1/accounts/update_credentials', { preHandler: upload.any() }, async (_request, reply) => {
|
}>('/v1/accounts/update_credentials', async (_request, reply) => {
|
||||||
const accessTokens = _request.headers.authorization;
|
const accessTokens = _request.headers.authorization;
|
||||||
const client = this.clientService.getClient(_request);
|
const client = this.clientService.getClient(_request);
|
||||||
// Check if there is a Header or Avatar being uploaded, if there is proceed to upload it to the drive of the user and then set it.
|
// Check if there is a Header or Avatar being uploaded, if there is proceed to upload it to the drive of the user and then set it.
|
||||||
if (_request.files.length > 0 && accessTokens) {
|
if (_request.savedRequestFiles?.length && accessTokens) {
|
||||||
const tokeninfo = await this.accessTokensRepository.findOneBy({ token: accessTokens.replace('Bearer ', '') });
|
const tokeninfo = await this.accessTokensRepository.findOneBy({ token: accessTokens.replace('Bearer ', '') });
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
const avatar = _request.savedRequestFiles.find(obj => {
|
||||||
const avatar = (_request.files as any).find((obj: any) => {
|
|
||||||
return obj.fieldname === 'avatar';
|
return obj.fieldname === 'avatar';
|
||||||
});
|
});
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
const header = _request.savedRequestFiles.find(obj => {
|
||||||
const header = (_request.files as any).find((obj: any) => {
|
|
||||||
return obj.fieldname === 'header';
|
return obj.fieldname === 'header';
|
||||||
});
|
});
|
||||||
|
|
||||||
if (tokeninfo && avatar) {
|
if (tokeninfo && avatar) {
|
||||||
const upload = await this.driveService.addFile({
|
const upload = await this.driveService.addFile({
|
||||||
user: { id: tokeninfo.userId, host: null },
|
user: { id: tokeninfo.userId, host: null },
|
||||||
path: avatar.path,
|
path: avatar.filepath,
|
||||||
name: avatar.originalname !== null && avatar.originalname !== 'file' ? avatar.originalname : undefined,
|
name: avatar.filename && avatar.filename !== 'file' ? avatar.filename : undefined,
|
||||||
sensitive: false,
|
sensitive: false,
|
||||||
});
|
});
|
||||||
if (upload.type.startsWith('image/')) {
|
if (upload.type.startsWith('image/')) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
_request.body.avatar = upload.id;
|
||||||
(_request.body as any).avatar = upload.id;
|
|
||||||
}
|
}
|
||||||
} else if (tokeninfo && header) {
|
} else if (tokeninfo && header) {
|
||||||
const upload = await this.driveService.addFile({
|
const upload = await this.driveService.addFile({
|
||||||
user: { id: tokeninfo.userId, host: null },
|
user: { id: tokeninfo.userId, host: null },
|
||||||
path: header.path,
|
path: header.filepath,
|
||||||
name: header.originalname !== null && header.originalname !== 'file' ? header.originalname : undefined,
|
name: header.filename && header.filename !== 'file' ? header.filename : undefined,
|
||||||
sensitive: false,
|
sensitive: false,
|
||||||
});
|
});
|
||||||
if (upload.type.startsWith('image/')) {
|
if (upload.type.startsWith('image/')) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
_request.body.header = upload.id;
|
||||||
(_request.body as any).header = upload.id;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
if (_request.body.fields_attributes) {
|
||||||
if ((_request.body as any).fields_attributes) {
|
for (const field of _request.body.fields_attributes) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
const fields = (_request.body as any).fields_attributes.map((field: any) => {
|
|
||||||
if (!(field.name.trim() === '' && field.value.trim() === '')) {
|
if (!(field.name.trim() === '' && field.value.trim() === '')) {
|
||||||
if (field.name.trim() === '') return reply.code(400).send('Field name can not be empty');
|
if (field.name.trim() === '') return reply.code(400).send('Field name can not be empty');
|
||||||
if (field.value.trim() === '') return reply.code(400).send('Field value can not be empty');
|
if (field.value.trim() === '') return reply.code(400).send('Field value can not be empty');
|
||||||
}
|
}
|
||||||
return {
|
}
|
||||||
...field,
|
_request.body.fields_attributes = _request.body.fields_attributes.filter(field => field.name.trim().length > 0 && field.value.length > 0);
|
||||||
};
|
|
||||||
});
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
(_request.body as any).fields_attributes = fields.filter((field: any) => field.name.trim().length > 0 && field.value.length > 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
|
@ -139,7 +128,7 @@ export class ApiAccountMastodon {
|
||||||
const data = await client.updateCredentials(options);
|
const data = await client.updateCredentials(options);
|
||||||
const response = await this.mastoConverters.convertAccount(data.data);
|
const response = await this.mastoConverters.convertAccount(data.data);
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<{ Querystring: { acct?: string } }>('/v1/accounts/lookup', async (_request, reply) => {
|
fastify.get<{ Querystring: { acct?: string } }>('/v1/accounts/lookup', async (_request, reply) => {
|
||||||
|
@ -151,7 +140,7 @@ export class ApiAccountMastodon {
|
||||||
data.data.accounts[0].fields = profile?.fields.map(f => ({ ...f, verified_at: null })) ?? [];
|
data.data.accounts[0].fields = profile?.fields.map(f => ({ ...f, verified_at: null })) ?? [];
|
||||||
const response = await this.mastoConverters.convertAccount(data.data.accounts[0]);
|
const response = await this.mastoConverters.convertAccount(data.data.accounts[0]);
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<ApiAccountMastodonRoute & { Querystring: { id?: string | string[] } }>('/v1/accounts/relationships', async (_request, reply) => {
|
fastify.get<ApiAccountMastodonRoute & { Querystring: { id?: string | string[] } }>('/v1/accounts/relationships', async (_request, reply) => {
|
||||||
|
@ -161,7 +150,7 @@ export class ApiAccountMastodon {
|
||||||
const data = await client.getRelationships(_request.query.id);
|
const data = await client.getRelationships(_request.query.id);
|
||||||
const response = data.data.map(relationship => convertRelationship(relationship));
|
const response = data.data.map(relationship => convertRelationship(relationship));
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<{ Params: { id?: string } }>('/v1/accounts/:id', async (_request, reply) => {
|
fastify.get<{ Params: { id?: string } }>('/v1/accounts/:id', async (_request, reply) => {
|
||||||
|
@ -171,7 +160,7 @@ export class ApiAccountMastodon {
|
||||||
const data = await client.getAccount(_request.params.id);
|
const data = await client.getAccount(_request.params.id);
|
||||||
const account = await this.mastoConverters.convertAccount(data.data);
|
const account = await this.mastoConverters.convertAccount(data.data);
|
||||||
|
|
||||||
reply.send(account);
|
return reply.send(account);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/statuses', async (request, reply) => {
|
fastify.get<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/statuses', async (request, reply) => {
|
||||||
|
@ -183,7 +172,7 @@ export class ApiAccountMastodon {
|
||||||
const response = await Promise.all(data.data.map(async (status) => await this.mastoConverters.convertStatus(status, me)));
|
const response = await Promise.all(data.data.map(async (status) => await this.mastoConverters.convertStatus(status, me)));
|
||||||
|
|
||||||
attachMinMaxPagination(request, reply, response);
|
attachMinMaxPagination(request, reply, response);
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<{ Params: { id?: string } }>('/v1/accounts/:id/featured_tags', async (_request, reply) => {
|
fastify.get<{ Params: { id?: string } }>('/v1/accounts/:id/featured_tags', async (_request, reply) => {
|
||||||
|
@ -193,7 +182,7 @@ export class ApiAccountMastodon {
|
||||||
const data = await client.getFeaturedTags();
|
const data = await client.getFeaturedTags();
|
||||||
const response = data.data.map((tag) => convertFeaturedTag(tag));
|
const response = data.data.map((tag) => convertFeaturedTag(tag));
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/followers', async (request, reply) => {
|
fastify.get<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/followers', async (request, reply) => {
|
||||||
|
@ -207,7 +196,7 @@ export class ApiAccountMastodon {
|
||||||
const response = await Promise.all(data.data.map(async (account) => await this.mastoConverters.convertAccount(account)));
|
const response = await Promise.all(data.data.map(async (account) => await this.mastoConverters.convertAccount(account)));
|
||||||
|
|
||||||
attachMinMaxPagination(request, reply, response);
|
attachMinMaxPagination(request, reply, response);
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/following', async (request, reply) => {
|
fastify.get<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/following', async (request, reply) => {
|
||||||
|
@ -221,7 +210,7 @@ export class ApiAccountMastodon {
|
||||||
const response = await Promise.all(data.data.map(async (account) => await this.mastoConverters.convertAccount(account)));
|
const response = await Promise.all(data.data.map(async (account) => await this.mastoConverters.convertAccount(account)));
|
||||||
|
|
||||||
attachMinMaxPagination(request, reply, response);
|
attachMinMaxPagination(request, reply, response);
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<{ Params: { id?: string } }>('/v1/accounts/:id/lists', async (_request, reply) => {
|
fastify.get<{ Params: { id?: string } }>('/v1/accounts/:id/lists', async (_request, reply) => {
|
||||||
|
@ -231,10 +220,10 @@ export class ApiAccountMastodon {
|
||||||
const data = await client.getAccountLists(_request.params.id);
|
const data = await client.getAccountLists(_request.params.id);
|
||||||
const response = data.data.map((list) => convertList(list));
|
const response = data.data.map((list) => convertList(list));
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/follow', { preHandler: upload.single('none') }, async (_request, reply) => {
|
fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/follow', async (_request, reply) => {
|
||||||
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
|
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
|
||||||
|
|
||||||
const client = this.clientService.getClient(_request);
|
const client = this.clientService.getClient(_request);
|
||||||
|
@ -242,10 +231,10 @@ export class ApiAccountMastodon {
|
||||||
const acct = convertRelationship(data.data);
|
const acct = convertRelationship(data.data);
|
||||||
acct.following = true; // TODO this is wrong, follow may not have processed immediately
|
acct.following = true; // TODO this is wrong, follow may not have processed immediately
|
||||||
|
|
||||||
reply.send(acct);
|
return reply.send(acct);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/unfollow', { preHandler: upload.single('none') }, async (_request, reply) => {
|
fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/unfollow', async (_request, reply) => {
|
||||||
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
|
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
|
||||||
|
|
||||||
const client = this.clientService.getClient(_request);
|
const client = this.clientService.getClient(_request);
|
||||||
|
@ -253,20 +242,20 @@ export class ApiAccountMastodon {
|
||||||
const acct = convertRelationship(data.data);
|
const acct = convertRelationship(data.data);
|
||||||
acct.following = false;
|
acct.following = false;
|
||||||
|
|
||||||
reply.send(acct);
|
return reply.send(acct);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/block', { preHandler: upload.single('none') }, async (_request, reply) => {
|
fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/block', async (_request, reply) => {
|
||||||
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
|
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
|
||||||
|
|
||||||
const client = this.clientService.getClient(_request);
|
const client = this.clientService.getClient(_request);
|
||||||
const data = await client.blockAccount(_request.params.id);
|
const data = await client.blockAccount(_request.params.id);
|
||||||
const response = convertRelationship(data.data);
|
const response = convertRelationship(data.data);
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/unblock', { preHandler: upload.single('none') }, async (_request, reply) => {
|
fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/unblock', async (_request, reply) => {
|
||||||
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
|
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
|
||||||
|
|
||||||
const client = this.clientService.getClient(_request);
|
const client = this.clientService.getClient(_request);
|
||||||
|
@ -276,7 +265,7 @@ export class ApiAccountMastodon {
|
||||||
return reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/mute', { preHandler: upload.single('none') }, async (_request, reply) => {
|
fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/mute', async (_request, reply) => {
|
||||||
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
|
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
|
||||||
|
|
||||||
const client = this.clientService.getClient(_request);
|
const client = this.clientService.getClient(_request);
|
||||||
|
@ -286,17 +275,17 @@ export class ApiAccountMastodon {
|
||||||
);
|
);
|
||||||
const response = convertRelationship(data.data);
|
const response = convertRelationship(data.data);
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/unmute', { preHandler: upload.single('none') }, async (_request, reply) => {
|
fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/unmute', async (_request, reply) => {
|
||||||
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
|
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
|
||||||
|
|
||||||
const client = this.clientService.getClient(_request);
|
const client = this.clientService.getClient(_request);
|
||||||
const data = await client.unmuteAccount(_request.params.id);
|
const data = await client.unmuteAccount(_request.params.id);
|
||||||
const response = convertRelationship(data.data);
|
const response = convertRelationship(data.data);
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,8 @@
|
||||||
|
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { MastodonClientService } from '@/server/api/mastodon/MastodonClientService.js';
|
import { MastodonClientService } from '@/server/api/mastodon/MastodonClientService.js';
|
||||||
|
import { MastodonConverters } from '@/server/api/mastodon/MastodonConverters.js';
|
||||||
import type { FastifyInstance } from 'fastify';
|
import type { FastifyInstance } from 'fastify';
|
||||||
import type multer from 'fastify-multer';
|
|
||||||
|
|
||||||
const readScope = [
|
const readScope = [
|
||||||
'read:account',
|
'read:account',
|
||||||
|
@ -48,9 +48,9 @@ const writeScope = [
|
||||||
|
|
||||||
export interface AuthPayload {
|
export interface AuthPayload {
|
||||||
scopes?: string | string[],
|
scopes?: string | string[],
|
||||||
redirect_uris?: string,
|
redirect_uris?: string | string[],
|
||||||
client_name?: string,
|
client_name?: string | string[],
|
||||||
website?: string,
|
website?: string | string[],
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not entirely right, but it gets TypeScript to work so *shrug*
|
// Not entirely right, but it gets TypeScript to work so *shrug*
|
||||||
|
@ -60,14 +60,18 @@ type AuthMastodonRoute = { Body?: AuthPayload, Querystring: AuthPayload };
|
||||||
export class ApiAppsMastodon {
|
export class ApiAppsMastodon {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly clientService: MastodonClientService,
|
private readonly clientService: MastodonClientService,
|
||||||
|
private readonly mastoConverters: MastodonConverters,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public register(fastify: FastifyInstance, upload: ReturnType<typeof multer>): void {
|
public register(fastify: FastifyInstance): void {
|
||||||
fastify.post<AuthMastodonRoute>('/v1/apps', { preHandler: upload.single('none') }, async (_request, reply) => {
|
fastify.post<AuthMastodonRoute>('/v1/apps', async (_request, reply) => {
|
||||||
const body = _request.body ?? _request.query;
|
const body = _request.body ?? _request.query;
|
||||||
if (!body.scopes) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required payload "scopes"' });
|
if (!body.scopes) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required payload "scopes"' });
|
||||||
if (!body.redirect_uris) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required payload "redirect_uris"' });
|
if (!body.redirect_uris) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required payload "redirect_uris"' });
|
||||||
|
if (Array.isArray(body.redirect_uris)) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Invalid payload "redirect_uris": only one value is allowed' });
|
||||||
if (!body.client_name) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required payload "client_name"' });
|
if (!body.client_name) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required payload "client_name"' });
|
||||||
|
if (Array.isArray(body.client_name)) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Invalid payload "client_name": only one value is allowed' });
|
||||||
|
if (Array.isArray(body.website)) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Invalid payload "website": only one value is allowed' });
|
||||||
|
|
||||||
let scope = body.scopes;
|
let scope = body.scopes;
|
||||||
if (typeof scope === 'string') {
|
if (typeof scope === 'string') {
|
||||||
|
@ -88,12 +92,10 @@ export class ApiAppsMastodon {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const red = body.redirect_uris;
|
|
||||||
|
|
||||||
const client = this.clientService.getClient(_request);
|
const client = this.clientService.getClient(_request);
|
||||||
const appData = await client.registerApp(body.client_name, {
|
const appData = await client.registerApp(body.client_name, {
|
||||||
scopes: Array.from(pushScope),
|
scopes: Array.from(pushScope),
|
||||||
redirect_uris: red,
|
redirect_uri: body.redirect_uris,
|
||||||
website: body.website,
|
website: body.website,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -101,12 +103,19 @@ export class ApiAppsMastodon {
|
||||||
id: Math.floor(Math.random() * 100).toString(),
|
id: Math.floor(Math.random() * 100).toString(),
|
||||||
name: appData.name,
|
name: appData.name,
|
||||||
website: body.website,
|
website: body.website,
|
||||||
redirect_uri: red,
|
redirect_uri: body.redirect_uris,
|
||||||
client_id: Buffer.from(appData.url || '').toString('base64'),
|
client_id: Buffer.from(appData.url || '').toString('base64'),
|
||||||
client_secret: appData.clientSecret,
|
client_secret: appData.clientSecret,
|
||||||
};
|
};
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
|
});
|
||||||
|
|
||||||
|
fastify.get('/v1/apps/verify_credentials', async (_request, reply) => {
|
||||||
|
const client = this.clientService.getClient(_request);
|
||||||
|
const data = await client.verifyAppCredentials();
|
||||||
|
const response = this.mastoConverters.convertApplication(data.data);
|
||||||
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ import { toBoolean } from '@/server/api/mastodon/argsUtils.js';
|
||||||
import { MastodonClientService } from '@/server/api/mastodon/MastodonClientService.js';
|
import { MastodonClientService } from '@/server/api/mastodon/MastodonClientService.js';
|
||||||
import { convertFilter } from '../MastodonConverters.js';
|
import { convertFilter } from '../MastodonConverters.js';
|
||||||
import type { FastifyInstance } from 'fastify';
|
import type { FastifyInstance } from 'fastify';
|
||||||
import type multer from 'fastify-multer';
|
|
||||||
|
|
||||||
interface ApiFilterMastodonRoute {
|
interface ApiFilterMastodonRoute {
|
||||||
Params: {
|
Params: {
|
||||||
|
@ -29,14 +28,14 @@ export class ApiFilterMastodon {
|
||||||
private readonly clientService: MastodonClientService,
|
private readonly clientService: MastodonClientService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public register(fastify: FastifyInstance, upload: ReturnType<typeof multer>): void {
|
public register(fastify: FastifyInstance): void {
|
||||||
fastify.get('/v1/filters', async (_request, reply) => {
|
fastify.get('/v1/filters', async (_request, reply) => {
|
||||||
const client = this.clientService.getClient(_request);
|
const client = this.clientService.getClient(_request);
|
||||||
|
|
||||||
const data = await client.getFilters();
|
const data = await client.getFilters();
|
||||||
const response = data.data.map((filter) => convertFilter(filter));
|
const response = data.data.map((filter) => convertFilter(filter));
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<ApiFilterMastodonRoute & { Params: { id?: string } }>('/v1/filters/:id', async (_request, reply) => {
|
fastify.get<ApiFilterMastodonRoute & { Params: { id?: string } }>('/v1/filters/:id', async (_request, reply) => {
|
||||||
|
@ -46,10 +45,10 @@ export class ApiFilterMastodon {
|
||||||
const data = await client.getFilter(_request.params.id);
|
const data = await client.getFilter(_request.params.id);
|
||||||
const response = convertFilter(data.data);
|
const response = convertFilter(data.data);
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.post<ApiFilterMastodonRoute>('/v1/filters', { preHandler: upload.single('none') }, async (_request, reply) => {
|
fastify.post<ApiFilterMastodonRoute>('/v1/filters', async (_request, reply) => {
|
||||||
if (!_request.body.phrase) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required payload "phrase"' });
|
if (!_request.body.phrase) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required payload "phrase"' });
|
||||||
if (!_request.body.context) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required payload "context"' });
|
if (!_request.body.context) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required payload "context"' });
|
||||||
|
|
||||||
|
@ -65,10 +64,10 @@ export class ApiFilterMastodon {
|
||||||
const data = await client.createFilter(_request.body.phrase, _request.body.context, options);
|
const data = await client.createFilter(_request.body.phrase, _request.body.context, options);
|
||||||
const response = convertFilter(data.data);
|
const response = convertFilter(data.data);
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.post<ApiFilterMastodonRoute & { Params: { id?: string } }>('/v1/filters/:id', { preHandler: upload.single('none') }, async (_request, reply) => {
|
fastify.post<ApiFilterMastodonRoute & { Params: { id?: string } }>('/v1/filters/:id', async (_request, reply) => {
|
||||||
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
|
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
|
||||||
if (!_request.body.phrase) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required payload "phrase"' });
|
if (!_request.body.phrase) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required payload "phrase"' });
|
||||||
if (!_request.body.context) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required payload "context"' });
|
if (!_request.body.context) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required payload "context"' });
|
||||||
|
@ -85,7 +84,7 @@ export class ApiFilterMastodon {
|
||||||
const data = await client.updateFilter(_request.params.id, _request.body.phrase, _request.body.context, options);
|
const data = await client.updateFilter(_request.params.id, _request.body.phrase, _request.body.context, options);
|
||||||
const response = convertFilter(data.data);
|
const response = convertFilter(data.data);
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.delete<ApiFilterMastodonRoute & { Params: { id?: string } }>('/v1/filters/:id', async (_request, reply) => {
|
fastify.delete<ApiFilterMastodonRoute & { Params: { id?: string } }>('/v1/filters/:id', async (_request, reply) => {
|
||||||
|
@ -94,7 +93,7 @@ export class ApiFilterMastodon {
|
||||||
const client = this.clientService.getClient(_request);
|
const client = this.clientService.getClient(_request);
|
||||||
const data = await client.deleteFilter(_request.params.id);
|
const data = await client.deleteFilter(_request.params.id);
|
||||||
|
|
||||||
reply.send(data.data);
|
return reply.send(data.data);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,7 +87,7 @@ export class ApiInstanceMastodon {
|
||||||
rules: instance.rules ?? [],
|
rules: instance.rules ?? [],
|
||||||
};
|
};
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,6 @@ import { MastodonConverters } from '@/server/api/mastodon/MastodonConverters.js'
|
||||||
import { attachMinMaxPagination } from '@/server/api/mastodon/pagination.js';
|
import { attachMinMaxPagination } from '@/server/api/mastodon/pagination.js';
|
||||||
import { MastodonClientService } from '../MastodonClientService.js';
|
import { MastodonClientService } from '../MastodonClientService.js';
|
||||||
import type { FastifyInstance } from 'fastify';
|
import type { FastifyInstance } from 'fastify';
|
||||||
import type multer from 'fastify-multer';
|
|
||||||
|
|
||||||
interface ApiNotifyMastodonRoute {
|
interface ApiNotifyMastodonRoute {
|
||||||
Params: {
|
Params: {
|
||||||
|
@ -26,7 +25,7 @@ export class ApiNotificationsMastodon {
|
||||||
private readonly clientService: MastodonClientService,
|
private readonly clientService: MastodonClientService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public register(fastify: FastifyInstance, upload: ReturnType<typeof multer>): void {
|
public register(fastify: FastifyInstance): void {
|
||||||
fastify.get<ApiNotifyMastodonRoute>('/v1/notifications', async (request, reply) => {
|
fastify.get<ApiNotifyMastodonRoute>('/v1/notifications', async (request, reply) => {
|
||||||
const { client, me } = await this.clientService.getAuthClient(request);
|
const { client, me } = await this.clientService.getAuthClient(request);
|
||||||
const data = await client.getNotifications(parseTimelineArgs(request.query));
|
const data = await client.getNotifications(parseTimelineArgs(request.query));
|
||||||
|
@ -46,7 +45,7 @@ export class ApiNotificationsMastodon {
|
||||||
}
|
}
|
||||||
|
|
||||||
attachMinMaxPagination(request, reply, response);
|
attachMinMaxPagination(request, reply, response);
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<ApiNotifyMastodonRoute & { Params: { id?: string } }>('/v1/notification/:id', async (_request, reply) => {
|
fastify.get<ApiNotifyMastodonRoute & { Params: { id?: string } }>('/v1/notification/:id', async (_request, reply) => {
|
||||||
|
@ -63,23 +62,23 @@ export class ApiNotificationsMastodon {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.post<ApiNotifyMastodonRoute & { Params: { id?: string } }>('/v1/notification/:id/dismiss', { preHandler: upload.single('none') }, async (_request, reply) => {
|
fastify.post<ApiNotifyMastodonRoute & { Params: { id?: string } }>('/v1/notification/:id/dismiss', async (_request, reply) => {
|
||||||
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
|
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
|
||||||
|
|
||||||
const client = this.clientService.getClient(_request);
|
const client = this.clientService.getClient(_request);
|
||||||
const data = await client.dismissNotification(_request.params.id);
|
const data = await client.dismissNotification(_request.params.id);
|
||||||
|
|
||||||
reply.send(data.data);
|
return reply.send(data.data);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.post<ApiNotifyMastodonRoute>('/v1/notifications/clear', { preHandler: upload.single('none') }, async (_request, reply) => {
|
fastify.post<ApiNotifyMastodonRoute>('/v1/notifications/clear', async (_request, reply) => {
|
||||||
const client = this.clientService.getClient(_request);
|
const client = this.clientService.getClient(_request);
|
||||||
const data = await client.dismissNotifications();
|
const data = await client.dismissNotifications();
|
||||||
|
|
||||||
reply.send(data.data);
|
return reply.send(data.data);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,7 +62,7 @@ export class ApiSearchMastodon {
|
||||||
attachMinMaxPagination(request, reply, response[type]);
|
attachMinMaxPagination(request, reply, response[type]);
|
||||||
}
|
}
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<ApiSearchMastodonRoute>('/v2/search', async (request, reply) => {
|
fastify.get<ApiSearchMastodonRoute>('/v2/search', async (request, reply) => {
|
||||||
|
@ -103,7 +103,7 @@ export class ApiSearchMastodon {
|
||||||
// Offset pagination is the only possible option
|
// Offset pagination is the only possible option
|
||||||
attachOffsetPagination(request, reply, longestResult);
|
attachOffsetPagination(request, reply, longestResult);
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<ApiSearchMastodonRoute>('/v1/trends/statuses', async (request, reply) => {
|
fastify.get<ApiSearchMastodonRoute>('/v1/trends/statuses', async (request, reply) => {
|
||||||
|
@ -126,7 +126,7 @@ export class ApiSearchMastodon {
|
||||||
const response = await Promise.all(data.map(status => this.mastoConverters.convertStatus(status, me)));
|
const response = await Promise.all(data.map(status => this.mastoConverters.convertStatus(status, me)));
|
||||||
|
|
||||||
attachMinMaxPagination(request, reply, response);
|
attachMinMaxPagination(request, reply, response);
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<ApiSearchMastodonRoute>('/v2/suggestions', async (request, reply) => {
|
fastify.get<ApiSearchMastodonRoute>('/v2/suggestions', async (request, reply) => {
|
||||||
|
@ -158,7 +158,7 @@ export class ApiSearchMastodon {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
attachOffsetPagination(request, reply, response);
|
attachOffsetPagination(request, reply, response);
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ export class ApiStatusMastodon {
|
||||||
response.media_attachments = [];
|
response.media_attachments = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/source', async (_request, reply) => {
|
fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/source', async (_request, reply) => {
|
||||||
|
@ -47,7 +47,7 @@ export class ApiStatusMastodon {
|
||||||
const client = this.clientService.getClient(_request);
|
const client = this.clientService.getClient(_request);
|
||||||
const data = await client.getStatusSource(_request.params.id);
|
const data = await client.getStatusSource(_request.params.id);
|
||||||
|
|
||||||
reply.send(data.data);
|
return reply.send(data.data);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<{ Params: { id?: string }, Querystring: TimelineArgs }>('/v1/statuses/:id/context', async (_request, reply) => {
|
fastify.get<{ Params: { id?: string }, Querystring: TimelineArgs }>('/v1/statuses/:id/context', async (_request, reply) => {
|
||||||
|
@ -59,7 +59,7 @@ export class ApiStatusMastodon {
|
||||||
const descendants = await Promise.all(data.descendants.map(async (status: Entity.Status) => await this.mastoConverters.convertStatus(status, me)));
|
const descendants = await Promise.all(data.descendants.map(async (status: Entity.Status) => await this.mastoConverters.convertStatus(status, me)));
|
||||||
const response = { ancestors, descendants };
|
const response = { ancestors, descendants };
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/history', async (_request, reply) => {
|
fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/history', async (_request, reply) => {
|
||||||
|
@ -68,7 +68,7 @@ export class ApiStatusMastodon {
|
||||||
const user = await this.clientService.getAuth(_request);
|
const user = await this.clientService.getAuth(_request);
|
||||||
const edits = await this.mastoConverters.getEdits(_request.params.id, user);
|
const edits = await this.mastoConverters.getEdits(_request.params.id, user);
|
||||||
|
|
||||||
reply.send(edits);
|
return reply.send(edits);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/reblogged_by', async (_request, reply) => {
|
fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/reblogged_by', async (_request, reply) => {
|
||||||
|
@ -78,7 +78,7 @@ export class ApiStatusMastodon {
|
||||||
const data = await client.getStatusRebloggedBy(_request.params.id);
|
const data = await client.getStatusRebloggedBy(_request.params.id);
|
||||||
const response = await Promise.all(data.data.map((account: Entity.Account) => this.mastoConverters.convertAccount(account)));
|
const response = await Promise.all(data.data.map((account: Entity.Account) => this.mastoConverters.convertAccount(account)));
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/favourited_by', async (_request, reply) => {
|
fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/favourited_by', async (_request, reply) => {
|
||||||
|
@ -88,7 +88,7 @@ export class ApiStatusMastodon {
|
||||||
const data = await client.getStatusFavouritedBy(_request.params.id);
|
const data = await client.getStatusFavouritedBy(_request.params.id);
|
||||||
const response = await Promise.all(data.data.map((account: Entity.Account) => this.mastoConverters.convertAccount(account)));
|
const response = await Promise.all(data.data.map((account: Entity.Account) => this.mastoConverters.convertAccount(account)));
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<{ Params: { id?: string } }>('/v1/media/:id', async (_request, reply) => {
|
fastify.get<{ Params: { id?: string } }>('/v1/media/:id', async (_request, reply) => {
|
||||||
|
@ -98,7 +98,7 @@ export class ApiStatusMastodon {
|
||||||
const data = await client.getMedia(_request.params.id);
|
const data = await client.getMedia(_request.params.id);
|
||||||
const response = convertAttachment(data.data);
|
const response = convertAttachment(data.data);
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<{ Params: { id?: string } }>('/v1/polls/:id', async (_request, reply) => {
|
fastify.get<{ Params: { id?: string } }>('/v1/polls/:id', async (_request, reply) => {
|
||||||
|
@ -108,7 +108,7 @@ export class ApiStatusMastodon {
|
||||||
const data = await client.getPoll(_request.params.id);
|
const data = await client.getPoll(_request.params.id);
|
||||||
const response = convertPoll(data.data);
|
const response = convertPoll(data.data);
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.post<{ Params: { id?: string }, Body: { choices?: number[] } }>('/v1/polls/:id/votes', async (_request, reply) => {
|
fastify.post<{ Params: { id?: string }, Body: { choices?: number[] } }>('/v1/polls/:id/votes', async (_request, reply) => {
|
||||||
|
@ -119,7 +119,7 @@ export class ApiStatusMastodon {
|
||||||
const data = await client.votePoll(_request.params.id, _request.body.choices);
|
const data = await client.votePoll(_request.params.id, _request.body.choices);
|
||||||
const response = convertPoll(data.data);
|
const response = convertPoll(data.data);
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.post<{
|
fastify.post<{
|
||||||
|
@ -161,14 +161,14 @@ export class ApiStatusMastodon {
|
||||||
body.in_reply_to_id,
|
body.in_reply_to_id,
|
||||||
removed,
|
removed,
|
||||||
);
|
);
|
||||||
reply.send(a.data);
|
return reply.send(a.data);
|
||||||
}
|
}
|
||||||
if (body.in_reply_to_id && removed === '/unreact') {
|
if (body.in_reply_to_id && removed === '/unreact') {
|
||||||
const id = body.in_reply_to_id;
|
const id = body.in_reply_to_id;
|
||||||
const post = await client.getStatus(id);
|
const post = await client.getStatus(id);
|
||||||
const react = post.data.emoji_reactions.filter((e: Entity.Emoji) => e.me)[0].name;
|
const react = post.data.emoji_reactions.filter((e: Entity.Emoji) => e.me)[0].name;
|
||||||
const data = await client.deleteEmojiReaction(id, react);
|
const data = await client.deleteEmojiReaction(id, react);
|
||||||
reply.send(data.data);
|
return reply.send(data.data);
|
||||||
}
|
}
|
||||||
if (!body.media_ids) body.media_ids = undefined;
|
if (!body.media_ids) body.media_ids = undefined;
|
||||||
if (body.media_ids && !body.media_ids.length) body.media_ids = undefined;
|
if (body.media_ids && !body.media_ids.length) body.media_ids = undefined;
|
||||||
|
@ -194,7 +194,7 @@ export class ApiStatusMastodon {
|
||||||
const data = await client.postStatus(text, options);
|
const data = await client.postStatus(text, options);
|
||||||
const response = await this.mastoConverters.convertStatus(data.data as Entity.Status, me);
|
const response = await this.mastoConverters.convertStatus(data.data as Entity.Status, me);
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.put<{
|
fastify.put<{
|
||||||
|
@ -233,7 +233,7 @@ export class ApiStatusMastodon {
|
||||||
const data = await client.editStatus(_request.params.id, options);
|
const data = await client.editStatus(_request.params.id, options);
|
||||||
const response = await this.mastoConverters.convertStatus(data.data, me);
|
const response = await this.mastoConverters.convertStatus(data.data, me);
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/favourite', async (_request, reply) => {
|
fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/favourite', async (_request, reply) => {
|
||||||
|
@ -243,7 +243,7 @@ export class ApiStatusMastodon {
|
||||||
const data = await client.createEmojiReaction(_request.params.id, '❤');
|
const data = await client.createEmojiReaction(_request.params.id, '❤');
|
||||||
const response = await this.mastoConverters.convertStatus(data.data, me);
|
const response = await this.mastoConverters.convertStatus(data.data, me);
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unfavourite', async (_request, reply) => {
|
fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unfavourite', async (_request, reply) => {
|
||||||
|
@ -253,7 +253,7 @@ export class ApiStatusMastodon {
|
||||||
const data = await client.deleteEmojiReaction(_request.params.id, '❤');
|
const data = await client.deleteEmojiReaction(_request.params.id, '❤');
|
||||||
const response = await this.mastoConverters.convertStatus(data.data, me);
|
const response = await this.mastoConverters.convertStatus(data.data, me);
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/reblog', async (_request, reply) => {
|
fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/reblog', async (_request, reply) => {
|
||||||
|
@ -263,7 +263,7 @@ export class ApiStatusMastodon {
|
||||||
const data = await client.reblogStatus(_request.params.id);
|
const data = await client.reblogStatus(_request.params.id);
|
||||||
const response = await this.mastoConverters.convertStatus(data.data, me);
|
const response = await this.mastoConverters.convertStatus(data.data, me);
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unreblog', async (_request, reply) => {
|
fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unreblog', async (_request, reply) => {
|
||||||
|
@ -273,7 +273,7 @@ export class ApiStatusMastodon {
|
||||||
const data = await client.unreblogStatus(_request.params.id);
|
const data = await client.unreblogStatus(_request.params.id);
|
||||||
const response = await this.mastoConverters.convertStatus(data.data, me);
|
const response = await this.mastoConverters.convertStatus(data.data, me);
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/bookmark', async (_request, reply) => {
|
fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/bookmark', async (_request, reply) => {
|
||||||
|
@ -283,7 +283,7 @@ export class ApiStatusMastodon {
|
||||||
const data = await client.bookmarkStatus(_request.params.id);
|
const data = await client.bookmarkStatus(_request.params.id);
|
||||||
const response = await this.mastoConverters.convertStatus(data.data, me);
|
const response = await this.mastoConverters.convertStatus(data.data, me);
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unbookmark', async (_request, reply) => {
|
fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unbookmark', async (_request, reply) => {
|
||||||
|
@ -293,7 +293,7 @@ export class ApiStatusMastodon {
|
||||||
const data = await client.unbookmarkStatus(_request.params.id);
|
const data = await client.unbookmarkStatus(_request.params.id);
|
||||||
const response = await this.mastoConverters.convertStatus(data.data, me);
|
const response = await this.mastoConverters.convertStatus(data.data, me);
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/pin', async (_request, reply) => {
|
fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/pin', async (_request, reply) => {
|
||||||
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
|
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
|
||||||
|
@ -302,7 +302,7 @@ export class ApiStatusMastodon {
|
||||||
const data = await client.pinStatus(_request.params.id);
|
const data = await client.pinStatus(_request.params.id);
|
||||||
const response = await this.mastoConverters.convertStatus(data.data, me);
|
const response = await this.mastoConverters.convertStatus(data.data, me);
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unpin', async (_request, reply) => {
|
fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unpin', async (_request, reply) => {
|
||||||
|
@ -312,7 +312,7 @@ export class ApiStatusMastodon {
|
||||||
const data = await client.unpinStatus(_request.params.id);
|
const data = await client.unpinStatus(_request.params.id);
|
||||||
const response = await this.mastoConverters.convertStatus(data.data, me);
|
const response = await this.mastoConverters.convertStatus(data.data, me);
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.post<{ Params: { id?: string, name?: string } }>('/v1/statuses/:id/react/:name', async (_request, reply) => {
|
fastify.post<{ Params: { id?: string, name?: string } }>('/v1/statuses/:id/react/:name', async (_request, reply) => {
|
||||||
|
@ -323,7 +323,7 @@ export class ApiStatusMastodon {
|
||||||
const data = await client.createEmojiReaction(_request.params.id, _request.params.name);
|
const data = await client.createEmojiReaction(_request.params.id, _request.params.name);
|
||||||
const response = await this.mastoConverters.convertStatus(data.data, me);
|
const response = await this.mastoConverters.convertStatus(data.data, me);
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.post<{ Params: { id?: string, name?: string } }>('/v1/statuses/:id/unreact/:name', async (_request, reply) => {
|
fastify.post<{ Params: { id?: string, name?: string } }>('/v1/statuses/:id/unreact/:name', async (_request, reply) => {
|
||||||
|
@ -334,7 +334,7 @@ export class ApiStatusMastodon {
|
||||||
const data = await client.deleteEmojiReaction(_request.params.id, _request.params.name);
|
const data = await client.deleteEmojiReaction(_request.params.id, _request.params.name);
|
||||||
const response = await this.mastoConverters.convertStatus(data.data, me);
|
const response = await this.mastoConverters.convertStatus(data.data, me);
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.delete<{ Params: { id?: string } }>('/v1/statuses/:id', async (_request, reply) => {
|
fastify.delete<{ Params: { id?: string } }>('/v1/statuses/:id', async (_request, reply) => {
|
||||||
|
@ -343,7 +343,7 @@ export class ApiStatusMastodon {
|
||||||
const client = this.clientService.getClient(_request);
|
const client = this.clientService.getClient(_request);
|
||||||
const data = await client.deleteStatus(_request.params.id);
|
const data = await client.deleteStatus(_request.params.id);
|
||||||
|
|
||||||
reply.send(data.data);
|
return reply.send(data.data);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ export class ApiTimelineMastodon {
|
||||||
const response = await Promise.all(data.data.map((status: Entity.Status) => this.mastoConverters.convertStatus(status, me)));
|
const response = await Promise.all(data.data.map((status: Entity.Status) => this.mastoConverters.convertStatus(status, me)));
|
||||||
|
|
||||||
attachMinMaxPagination(request, reply, response);
|
attachMinMaxPagination(request, reply, response);
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<{ Querystring: TimelineArgs }>('/v1/timelines/home', async (request, reply) => {
|
fastify.get<{ Querystring: TimelineArgs }>('/v1/timelines/home', async (request, reply) => {
|
||||||
|
@ -38,7 +38,7 @@ export class ApiTimelineMastodon {
|
||||||
const response = await Promise.all(data.data.map((status: Entity.Status) => this.mastoConverters.convertStatus(status, me)));
|
const response = await Promise.all(data.data.map((status: Entity.Status) => this.mastoConverters.convertStatus(status, me)));
|
||||||
|
|
||||||
attachMinMaxPagination(request, reply, response);
|
attachMinMaxPagination(request, reply, response);
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<{ Params: { hashtag?: string }, Querystring: TimelineArgs }>('/v1/timelines/tag/:hashtag', async (request, reply) => {
|
fastify.get<{ Params: { hashtag?: string }, Querystring: TimelineArgs }>('/v1/timelines/tag/:hashtag', async (request, reply) => {
|
||||||
|
@ -50,7 +50,7 @@ export class ApiTimelineMastodon {
|
||||||
const response = await Promise.all(data.data.map((status: Entity.Status) => this.mastoConverters.convertStatus(status, me)));
|
const response = await Promise.all(data.data.map((status: Entity.Status) => this.mastoConverters.convertStatus(status, me)));
|
||||||
|
|
||||||
attachMinMaxPagination(request, reply, response);
|
attachMinMaxPagination(request, reply, response);
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<{ Params: { id?: string }, Querystring: TimelineArgs }>('/v1/timelines/list/:id', async (request, reply) => {
|
fastify.get<{ Params: { id?: string }, Querystring: TimelineArgs }>('/v1/timelines/list/:id', async (request, reply) => {
|
||||||
|
@ -62,7 +62,7 @@ export class ApiTimelineMastodon {
|
||||||
const response = await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoConverters.convertStatus(status, me)));
|
const response = await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoConverters.convertStatus(status, me)));
|
||||||
|
|
||||||
attachMinMaxPagination(request, reply, response);
|
attachMinMaxPagination(request, reply, response);
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<{ Querystring: TimelineArgs }>('/v1/conversations', async (request, reply) => {
|
fastify.get<{ Querystring: TimelineArgs }>('/v1/conversations', async (request, reply) => {
|
||||||
|
@ -72,7 +72,7 @@ export class ApiTimelineMastodon {
|
||||||
const response = await Promise.all(data.data.map((conversation: Entity.Conversation) => this.mastoConverters.convertConversation(conversation, me)));
|
const response = await Promise.all(data.data.map((conversation: Entity.Conversation) => this.mastoConverters.convertConversation(conversation, me)));
|
||||||
|
|
||||||
attachMinMaxPagination(request, reply, response);
|
attachMinMaxPagination(request, reply, response);
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<{ Params: { id?: string } }>('/v1/lists/:id', async (_request, reply) => {
|
fastify.get<{ Params: { id?: string } }>('/v1/lists/:id', async (_request, reply) => {
|
||||||
|
@ -82,7 +82,7 @@ export class ApiTimelineMastodon {
|
||||||
const data = await client.getList(_request.params.id);
|
const data = await client.getList(_request.params.id);
|
||||||
const response = convertList(data.data);
|
const response = convertList(data.data);
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get('/v1/lists', async (request, reply) => {
|
fastify.get('/v1/lists', async (request, reply) => {
|
||||||
|
@ -91,7 +91,7 @@ export class ApiTimelineMastodon {
|
||||||
const response = data.data.map((list: Entity.List) => convertList(list));
|
const response = data.data.map((list: Entity.List) => convertList(list));
|
||||||
|
|
||||||
attachMinMaxPagination(request, reply, response);
|
attachMinMaxPagination(request, reply, response);
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.get<{ Params: { id?: string }, Querystring: TimelineArgs }>('/v1/lists/:id/accounts', async (request, reply) => {
|
fastify.get<{ Params: { id?: string }, Querystring: TimelineArgs }>('/v1/lists/:id/accounts', async (request, reply) => {
|
||||||
|
@ -102,7 +102,7 @@ export class ApiTimelineMastodon {
|
||||||
const response = await Promise.all(data.data.map((account: Entity.Account) => this.mastoConverters.convertAccount(account)));
|
const response = await Promise.all(data.data.map((account: Entity.Account) => this.mastoConverters.convertAccount(account)));
|
||||||
|
|
||||||
attachMinMaxPagination(request, reply, response);
|
attachMinMaxPagination(request, reply, response);
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.post<{ Params: { id?: string }, Querystring: { accounts_id?: string[] } }>('/v1/lists/:id/accounts', async (_request, reply) => {
|
fastify.post<{ Params: { id?: string }, Querystring: { accounts_id?: string[] } }>('/v1/lists/:id/accounts', async (_request, reply) => {
|
||||||
|
@ -112,7 +112,7 @@ export class ApiTimelineMastodon {
|
||||||
const client = this.clientService.getClient(_request);
|
const client = this.clientService.getClient(_request);
|
||||||
const data = await client.addAccountsToList(_request.params.id, _request.query.accounts_id);
|
const data = await client.addAccountsToList(_request.params.id, _request.query.accounts_id);
|
||||||
|
|
||||||
reply.send(data.data);
|
return reply.send(data.data);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.delete<{ Params: { id?: string }, Querystring: { accounts_id?: string[] } }>('/v1/lists/:id/accounts', async (_request, reply) => {
|
fastify.delete<{ Params: { id?: string }, Querystring: { accounts_id?: string[] } }>('/v1/lists/:id/accounts', async (_request, reply) => {
|
||||||
|
@ -122,7 +122,7 @@ export class ApiTimelineMastodon {
|
||||||
const client = this.clientService.getClient(_request);
|
const client = this.clientService.getClient(_request);
|
||||||
const data = await client.deleteAccountsFromList(_request.params.id, _request.query.accounts_id);
|
const data = await client.deleteAccountsFromList(_request.params.id, _request.query.accounts_id);
|
||||||
|
|
||||||
reply.send(data.data);
|
return reply.send(data.data);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.post<{ Body: { title?: string } }>('/v1/lists', async (_request, reply) => {
|
fastify.post<{ Body: { title?: string } }>('/v1/lists', async (_request, reply) => {
|
||||||
|
@ -132,7 +132,7 @@ export class ApiTimelineMastodon {
|
||||||
const data = await client.createList(_request.body.title);
|
const data = await client.createList(_request.body.title);
|
||||||
const response = convertList(data.data);
|
const response = convertList(data.data);
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.put<{ Params: { id?: string }, Body: { title?: string } }>('/v1/lists/:id', async (_request, reply) => {
|
fastify.put<{ Params: { id?: string }, Body: { title?: string } }>('/v1/lists/:id', async (_request, reply) => {
|
||||||
|
@ -143,7 +143,7 @@ export class ApiTimelineMastodon {
|
||||||
const data = await client.updateList(_request.params.id, _request.body.title);
|
const data = await client.updateList(_request.params.id, _request.body.title);
|
||||||
const response = convertList(data.data);
|
const response = convertList(data.data);
|
||||||
|
|
||||||
reply.send(response);
|
return reply.send(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.delete<{ Params: { id?: string } }>('/v1/lists/:id', async (_request, reply) => {
|
fastify.delete<{ Params: { id?: string } }>('/v1/lists/:id', async (_request, reply) => {
|
||||||
|
@ -152,7 +152,7 @@ export class ApiTimelineMastodon {
|
||||||
const client = this.clientService.getClient(_request);
|
const client = this.clientService.getClient(_request);
|
||||||
await client.deleteList(_request.params.id);
|
await client.deleteList(_request.params.id);
|
||||||
|
|
||||||
reply.send({});
|
return reply.send({});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,15 +3,14 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import querystring from 'querystring';
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import multer from 'fastify-multer';
|
|
||||||
import { bindThis } from '@/decorators.js';
|
import { bindThis } from '@/decorators.js';
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import { DI } from '@/di-symbols.js';
|
import { DI } from '@/di-symbols.js';
|
||||||
import { MastodonClientService } from '@/server/api/mastodon/MastodonClientService.js';
|
import { MastodonClientService } from '@/server/api/mastodon/MastodonClientService.js';
|
||||||
import { getErrorData } from '@/server/api/mastodon/MastodonLogger.js';
|
import { getErrorData } from '@/server/api/mastodon/MastodonLogger.js';
|
||||||
|
import { ServerUtilityService } from '@/server/ServerUtilityService.js';
|
||||||
import type { FastifyInstance } from 'fastify';
|
import type { FastifyInstance } from 'fastify';
|
||||||
|
|
||||||
const kinds = [
|
const kinds = [
|
||||||
|
@ -56,6 +55,7 @@ export class OAuth2ProviderService {
|
||||||
private config: Config,
|
private config: Config,
|
||||||
|
|
||||||
private readonly mastodonClientService: MastodonClientService,
|
private readonly mastodonClientService: MastodonClientService,
|
||||||
|
private readonly serverUtilityService: ServerUtilityService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
// https://datatracker.ietf.org/doc/html/rfc8414.html
|
// https://datatracker.ietf.org/doc/html/rfc8414.html
|
||||||
|
@ -92,36 +92,10 @@ export class OAuth2ProviderService {
|
||||||
});
|
});
|
||||||
}); */
|
}); */
|
||||||
|
|
||||||
const upload = multer({
|
this.serverUtilityService.addMultipartFormDataContentType(fastify);
|
||||||
storage: multer.diskStorage({}),
|
this.serverUtilityService.addFormUrlEncodedContentType(fastify);
|
||||||
limits: {
|
this.serverUtilityService.addCORS(fastify);
|
||||||
fileSize: this.config.maxFileSize || 262144000,
|
this.serverUtilityService.addFlattenedQueryType(fastify);
|
||||||
files: 1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
fastify.addHook('onRequest', (request, reply, done) => {
|
|
||||||
reply.header('Access-Control-Allow-Origin', '*');
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
fastify.addContentTypeParser('application/x-www-form-urlencoded', (request, payload, done) => {
|
|
||||||
let body = '';
|
|
||||||
payload.on('data', (data) => {
|
|
||||||
body += data;
|
|
||||||
});
|
|
||||||
payload.on('end', () => {
|
|
||||||
try {
|
|
||||||
const parsed = querystring.parse(body);
|
|
||||||
done(null, parsed);
|
|
||||||
} catch (e: unknown) {
|
|
||||||
done(e instanceof Error ? e : new Error(String(e)));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
payload.on('error', done);
|
|
||||||
});
|
|
||||||
|
|
||||||
fastify.register(multer.contentParser);
|
|
||||||
|
|
||||||
for (const url of ['/authorize', '/authorize/']) {
|
for (const url of ['/authorize', '/authorize/']) {
|
||||||
fastify.get<{ Querystring: Record<string, string | string[] | undefined> }>(url, async (request, reply) => {
|
fastify.get<{ Querystring: Record<string, string | string[] | undefined> }>(url, async (request, reply) => {
|
||||||
|
@ -132,11 +106,11 @@ export class OAuth2ProviderService {
|
||||||
if (request.query.state) redirectUri.searchParams.set('state', String(request.query.state));
|
if (request.query.state) redirectUri.searchParams.set('state', String(request.query.state));
|
||||||
if (request.query.redirect_uri) redirectUri.searchParams.set('redirect_uri', String(request.query.redirect_uri));
|
if (request.query.redirect_uri) redirectUri.searchParams.set('redirect_uri', String(request.query.redirect_uri));
|
||||||
|
|
||||||
reply.redirect(redirectUri.toString());
|
return reply.redirect(redirectUri.toString());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fastify.post<{ Body?: Record<string, string | string[] | undefined>, Querystring: Record<string, string | string[] | undefined> }>('/token', { preHandler: upload.none() }, async (request, reply) => {
|
fastify.post<{ Body?: Record<string, string | string[] | undefined>, Querystring: Record<string, string | string[] | undefined> }>('/token', async (request, reply) => {
|
||||||
const body = request.body ?? request.query;
|
const body = request.body ?? request.query;
|
||||||
|
|
||||||
if (body.grant_type === 'client_credentials') {
|
if (body.grant_type === 'client_credentials') {
|
||||||
|
@ -146,7 +120,7 @@ export class OAuth2ProviderService {
|
||||||
scope: 'read',
|
scope: 'read',
|
||||||
created_at: Math.floor(new Date().getTime() / 1000),
|
created_at: Math.floor(new Date().getTime() / 1000),
|
||||||
};
|
};
|
||||||
reply.send(ret);
|
return reply.send(ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -163,13 +137,13 @@ export class OAuth2ProviderService {
|
||||||
const ret = {
|
const ret = {
|
||||||
access_token: atData.accessToken,
|
access_token: atData.accessToken,
|
||||||
token_type: 'Bearer',
|
token_type: 'Bearer',
|
||||||
scope: body.scope || 'read write follow push',
|
scope: atData.scope || body.scope || 'read write follow push',
|
||||||
created_at: Math.floor(new Date().getTime() / 1000),
|
created_at: atData.createdAt || Math.floor(new Date().getTime() / 1000),
|
||||||
};
|
};
|
||||||
reply.send(ret);
|
return reply.send(ret);
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
const data = getErrorData(e);
|
const data = getErrorData(e);
|
||||||
reply.code(401).send(data);
|
return reply.code(401).send(data);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import * as NotificationType from './notification'
|
||||||
import FilterContext from './filter_context'
|
import FilterContext from './filter_context'
|
||||||
import Converter from './converter'
|
import Converter from './converter'
|
||||||
import MastodonEntity from './mastodon/entity';
|
import MastodonEntity from './mastodon/entity';
|
||||||
|
import MisskeyEntity from './misskey/entity';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Response,
|
Response,
|
||||||
|
@ -23,4 +24,5 @@ export {
|
||||||
Entity,
|
Entity,
|
||||||
Converter,
|
Converter,
|
||||||
MastodonEntity,
|
MastodonEntity,
|
||||||
|
MisskeyEntity,
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,5 +3,8 @@ namespace MastodonEntity {
|
||||||
name: string
|
name: string
|
||||||
website?: string | null
|
website?: string | null
|
||||||
vapid_key?: string | null
|
vapid_key?: string | null
|
||||||
|
scopes: string[]
|
||||||
|
redirect_uris: string[]
|
||||||
|
redirect_uri?: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,9 +39,9 @@ export default class Misskey implements MegalodonInterface {
|
||||||
|
|
||||||
public async registerApp(
|
public async registerApp(
|
||||||
client_name: string,
|
client_name: string,
|
||||||
options: Partial<{ scopes: Array<string>; redirect_uris: string; website: string }> = {
|
options: Partial<{ scopes: Array<string>; redirect_uri: string; website?: string }> = {
|
||||||
scopes: MisskeyAPI.DEFAULT_SCOPE,
|
scopes: MisskeyAPI.DEFAULT_SCOPE,
|
||||||
redirect_uris: this.baseUrl
|
redirect_uri: this.baseUrl
|
||||||
}
|
}
|
||||||
): Promise<OAuth.AppData> {
|
): Promise<OAuth.AppData> {
|
||||||
return this.createApp(client_name, options).then(async appData => {
|
return this.createApp(client_name, options).then(async appData => {
|
||||||
|
@ -62,13 +62,14 @@ export default class Misskey implements MegalodonInterface {
|
||||||
*/
|
*/
|
||||||
public async createApp(
|
public async createApp(
|
||||||
client_name: string,
|
client_name: string,
|
||||||
options: Partial<{ scopes: Array<string>; redirect_uris: string; website: string }> = {
|
options: Partial<{ scopes: Array<string>; redirect_uri: string; website?: string }> = {
|
||||||
scopes: MisskeyAPI.DEFAULT_SCOPE,
|
scopes: MisskeyAPI.DEFAULT_SCOPE,
|
||||||
redirect_uris: this.baseUrl
|
redirect_uri: this.baseUrl
|
||||||
}
|
}
|
||||||
): Promise<OAuth.AppData> {
|
): Promise<OAuth.AppData> {
|
||||||
const redirect_uris = options.redirect_uris || this.baseUrl
|
const redirect_uri = options.redirect_uri || this.baseUrl
|
||||||
const scopes = options.scopes || MisskeyAPI.DEFAULT_SCOPE
|
const scopes = options.scopes || MisskeyAPI.DEFAULT_SCOPE
|
||||||
|
const website = options.website ?? '';
|
||||||
|
|
||||||
const params: {
|
const params: {
|
||||||
name: string
|
name: string
|
||||||
|
@ -77,9 +78,9 @@ export default class Misskey implements MegalodonInterface {
|
||||||
callbackUrl: string
|
callbackUrl: string
|
||||||
} = {
|
} = {
|
||||||
name: client_name,
|
name: client_name,
|
||||||
description: '',
|
description: website,
|
||||||
permission: scopes,
|
permission: scopes,
|
||||||
callbackUrl: redirect_uris
|
callbackUrl: redirect_uri
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -101,7 +102,7 @@ export default class Misskey implements MegalodonInterface {
|
||||||
website: null,
|
website: null,
|
||||||
redirect_uri: res.data.callbackUrl,
|
redirect_uri: res.data.callbackUrl,
|
||||||
client_id: '',
|
client_id: '',
|
||||||
client_secret: res.data.secret
|
client_secret: res.data.secret!
|
||||||
}
|
}
|
||||||
return OAuth.AppData.from(appData)
|
return OAuth.AppData.from(appData)
|
||||||
})
|
})
|
||||||
|
@ -121,11 +122,8 @@ export default class Misskey implements MegalodonInterface {
|
||||||
// ======================================
|
// ======================================
|
||||||
// apps
|
// apps
|
||||||
// ======================================
|
// ======================================
|
||||||
public async verifyAppCredentials(): Promise<Response<Entity.Application>> {
|
public async verifyAppCredentials(): Promise<Response<MisskeyAPI.Entity.App>> {
|
||||||
return new Promise((_, reject) => {
|
return await this.client.post<MisskeyAPI.Entity.App>('/api/app/current');
|
||||||
const err = new NoImplementedError('misskey does not support')
|
|
||||||
reject(err)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================================
|
// ======================================
|
||||||
|
@ -1502,13 +1500,13 @@ export default class Misskey implements MegalodonInterface {
|
||||||
/**
|
/**
|
||||||
* POST /api/drive/files/create
|
* POST /api/drive/files/create
|
||||||
*/
|
*/
|
||||||
public async uploadMedia(file: any, _options?: { description?: string; focus?: string }): Promise<Response<Entity.Attachment>> {
|
public async uploadMedia(file: { filepath: fs.PathLike, mimetype: string, filename: string }, _options?: { description?: string; focus?: string }): Promise<Response<Entity.Attachment>> {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', fs.createReadStream(file.path), {
|
formData.append('file', fs.createReadStream(file.filepath), {
|
||||||
contentType: file.mimetype,
|
contentType: file.mimetype,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (file.originalname != null && file.originalname !== "file") formData.append("name", file.originalname);
|
if (file.filename && file.filename !== "file") formData.append("name", file.filename);
|
||||||
|
|
||||||
if (_options?.description != null) formData.append("comment", _options.description);
|
if (_options?.description != null) formData.append("comment", _options.description);
|
||||||
let headers: { [key: string]: string } = {}
|
let headers: { [key: string]: string } = {}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import axios, { AxiosResponse, AxiosRequestConfig } from 'axios'
|
import axios, { AxiosResponse, AxiosRequestConfig } from 'axios'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import FormData from 'form-data'
|
|
||||||
|
|
||||||
import { DEFAULT_UA } from '../default'
|
import { DEFAULT_UA } from '../default'
|
||||||
import Response from '../response'
|
import Response from '../response'
|
||||||
|
@ -575,22 +574,26 @@ namespace MisskeyAPI {
|
||||||
this.accessToken = accessToken
|
this.accessToken = accessToken
|
||||||
this.baseUrl = baseUrl
|
this.baseUrl = baseUrl
|
||||||
this.userAgent = userAgent
|
this.userAgent = userAgent
|
||||||
this.abortController = new AbortController()
|
this.abortController = new AbortController();
|
||||||
axios.defaults.signal = this.abortController.signal
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET request to misskey API.
|
* GET request to misskey API.
|
||||||
**/
|
**/
|
||||||
public async get<T>(path: string, params: any = {}, headers: { [key: string]: string } = {}): Promise<Response<T>> {
|
public async get<T>(path: string, params: any = {}, headers: { [key: string]: string } = {}): Promise<Response<T>> {
|
||||||
|
if (!headers['Authorization'] && this.accessToken) {
|
||||||
|
headers['Authorization'] = `Bearer ${this.accessToken}`;
|
||||||
|
}
|
||||||
|
if (!headers['User-Agent']) {
|
||||||
|
headers['User-Agent'] = this.userAgent;
|
||||||
|
}
|
||||||
|
|
||||||
let options: AxiosRequestConfig = {
|
let options: AxiosRequestConfig = {
|
||||||
params: params,
|
params: params,
|
||||||
headers: {
|
headers,
|
||||||
'User-Agent': this.userAgent,
|
|
||||||
...headers,
|
|
||||||
},
|
|
||||||
maxContentLength: Infinity,
|
maxContentLength: Infinity,
|
||||||
maxBodyLength: Infinity
|
maxBodyLength: Infinity,
|
||||||
|
signal: this.abortController.signal,
|
||||||
}
|
}
|
||||||
return axios.get<T>(this.baseUrl + path, options).then((resp: AxiosResponse<T>) => {
|
return axios.get<T>(this.baseUrl + path, options).then((resp: AxiosResponse<T>) => {
|
||||||
const res: Response<T> = {
|
const res: Response<T> = {
|
||||||
|
@ -610,22 +613,21 @@ namespace MisskeyAPI {
|
||||||
* @param headers Request header object
|
* @param headers Request header object
|
||||||
*/
|
*/
|
||||||
public async post<T>(path: string, params: any = {}, headers: { [key: string]: string } = {}): Promise<Response<T>> {
|
public async post<T>(path: string, params: any = {}, headers: { [key: string]: string } = {}): Promise<Response<T>> {
|
||||||
|
if (!headers['Authorization'] && this.accessToken) {
|
||||||
|
headers['Authorization'] = `Bearer ${this.accessToken}`;
|
||||||
|
}
|
||||||
|
if (!headers['User-Agent']) {
|
||||||
|
headers['User-Agent'] = this.userAgent;
|
||||||
|
}
|
||||||
|
|
||||||
let options: AxiosRequestConfig = {
|
let options: AxiosRequestConfig = {
|
||||||
headers: headers,
|
headers: headers,
|
||||||
maxContentLength: Infinity,
|
maxContentLength: Infinity,
|
||||||
maxBodyLength: Infinity
|
maxBodyLength: Infinity,
|
||||||
|
signal: this.abortController.signal,
|
||||||
}
|
}
|
||||||
let bodyParams = params
|
|
||||||
if (this.accessToken) {
|
return axios.post<T>(this.baseUrl + path, params, options).then((resp: AxiosResponse<T>) => {
|
||||||
if (params instanceof FormData) {
|
|
||||||
bodyParams.append('i', this.accessToken)
|
|
||||||
} else {
|
|
||||||
bodyParams = Object.assign(params, {
|
|
||||||
i: this.accessToken
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return axios.post<T>(this.baseUrl + path, bodyParams, options).then((resp: AxiosResponse<T>) => {
|
|
||||||
const res: Response<T> = {
|
const res: Response<T> = {
|
||||||
data: resp.data,
|
data: resp.data,
|
||||||
status: resp.status,
|
status: resp.status,
|
||||||
|
|
|
@ -4,6 +4,6 @@ namespace MisskeyEntity {
|
||||||
name: string
|
name: string
|
||||||
callbackUrl: string
|
callbackUrl: string
|
||||||
permission: Array<string>
|
permission: Array<string>
|
||||||
secret: string
|
secret?: string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -552,6 +552,9 @@ type AppCreateRequest = operations['app___create']['requestBody']['content']['ap
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type AppCreateResponse = operations['app___create']['responses']['200']['content']['application/json'];
|
type AppCreateResponse = operations['app___create']['responses']['200']['content']['application/json'];
|
||||||
|
|
||||||
|
// @public (undocumented)
|
||||||
|
type AppCurrentResponse = operations['app___current']['responses']['200']['content']['application/json'];
|
||||||
|
|
||||||
// @public (undocumented)
|
// @public (undocumented)
|
||||||
type AppShowRequest = operations['app___show']['requestBody']['content']['application/json'];
|
type AppShowRequest = operations['app___show']['requestBody']['content']['application/json'];
|
||||||
|
|
||||||
|
@ -1643,6 +1646,7 @@ declare namespace entities {
|
||||||
ApShowResponse,
|
ApShowResponse,
|
||||||
AppCreateRequest,
|
AppCreateRequest,
|
||||||
AppCreateResponse,
|
AppCreateResponse,
|
||||||
|
AppCurrentResponse,
|
||||||
AppShowRequest,
|
AppShowRequest,
|
||||||
AppShowResponse,
|
AppShowResponse,
|
||||||
AuthAcceptRequest,
|
AuthAcceptRequest,
|
||||||
|
|
|
@ -1313,6 +1313,17 @@ declare module '../api.js' {
|
||||||
credential?: string | null,
|
credential?: string | null,
|
||||||
): Promise<SwitchCaseResponseType<E, P>>;
|
): Promise<SwitchCaseResponseType<E, P>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No description provided.
|
||||||
|
*
|
||||||
|
* **Credential required**: *No*
|
||||||
|
*/
|
||||||
|
request<E extends 'app/current', P extends Endpoints[E]['req']>(
|
||||||
|
endpoint: E,
|
||||||
|
params: P,
|
||||||
|
credential?: string | null,
|
||||||
|
): Promise<SwitchCaseResponseType<E, P>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* No description provided.
|
* No description provided.
|
||||||
*
|
*
|
||||||
|
|
|
@ -159,6 +159,7 @@ import type {
|
||||||
ApShowResponse,
|
ApShowResponse,
|
||||||
AppCreateRequest,
|
AppCreateRequest,
|
||||||
AppCreateResponse,
|
AppCreateResponse,
|
||||||
|
AppCurrentResponse,
|
||||||
AppShowRequest,
|
AppShowRequest,
|
||||||
AppShowResponse,
|
AppShowResponse,
|
||||||
AuthAcceptRequest,
|
AuthAcceptRequest,
|
||||||
|
@ -778,6 +779,7 @@ export type Endpoints = {
|
||||||
'ap/get': { req: ApGetRequest; res: ApGetResponse };
|
'ap/get': { req: ApGetRequest; res: ApGetResponse };
|
||||||
'ap/show': { req: ApShowRequest; res: ApShowResponse };
|
'ap/show': { req: ApShowRequest; res: ApShowResponse };
|
||||||
'app/create': { req: AppCreateRequest; res: AppCreateResponse };
|
'app/create': { req: AppCreateRequest; res: AppCreateResponse };
|
||||||
|
'app/current': { req: EmptyRequest; res: AppCurrentResponse };
|
||||||
'app/show': { req: AppShowRequest; res: AppShowResponse };
|
'app/show': { req: AppShowRequest; res: AppShowResponse };
|
||||||
'auth/accept': { req: AuthAcceptRequest; res: EmptyResponse };
|
'auth/accept': { req: AuthAcceptRequest; res: EmptyResponse };
|
||||||
'auth/session/generate': { req: AuthSessionGenerateRequest; res: AuthSessionGenerateResponse };
|
'auth/session/generate': { req: AuthSessionGenerateRequest; res: AuthSessionGenerateResponse };
|
||||||
|
|
|
@ -162,6 +162,7 @@ export type ApShowRequest = operations['ap___show']['requestBody']['content']['a
|
||||||
export type ApShowResponse = operations['ap___show']['responses']['200']['content']['application/json'];
|
export type ApShowResponse = operations['ap___show']['responses']['200']['content']['application/json'];
|
||||||
export type AppCreateRequest = operations['app___create']['requestBody']['content']['application/json'];
|
export type AppCreateRequest = operations['app___create']['requestBody']['content']['application/json'];
|
||||||
export type AppCreateResponse = operations['app___create']['responses']['200']['content']['application/json'];
|
export type AppCreateResponse = operations['app___create']['responses']['200']['content']['application/json'];
|
||||||
|
export type AppCurrentResponse = operations['app___current']['responses']['200']['content']['application/json'];
|
||||||
export type AppShowRequest = operations['app___show']['requestBody']['content']['application/json'];
|
export type AppShowRequest = operations['app___show']['requestBody']['content']['application/json'];
|
||||||
export type AppShowResponse = operations['app___show']['responses']['200']['content']['application/json'];
|
export type AppShowResponse = operations['app___show']['responses']['200']['content']['application/json'];
|
||||||
export type AuthAcceptRequest = operations['auth___accept']['requestBody']['content']['application/json'];
|
export type AuthAcceptRequest = operations['auth___accept']['requestBody']['content']['application/json'];
|
||||||
|
|
|
@ -1086,6 +1086,15 @@ export type paths = {
|
||||||
*/
|
*/
|
||||||
post: operations['app___create'];
|
post: operations['app___create'];
|
||||||
};
|
};
|
||||||
|
'/app/current': {
|
||||||
|
/**
|
||||||
|
* app/current
|
||||||
|
* @description No description provided.
|
||||||
|
*
|
||||||
|
* **Credential required**: *No*
|
||||||
|
*/
|
||||||
|
post: operations['app___current'];
|
||||||
|
};
|
||||||
'/app/show': {
|
'/app/show': {
|
||||||
/**
|
/**
|
||||||
* app/show
|
* app/show
|
||||||
|
@ -13071,6 +13080,58 @@ export type operations = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* app/current
|
||||||
|
* @description No description provided.
|
||||||
|
*
|
||||||
|
* **Credential required**: *No*
|
||||||
|
*/
|
||||||
|
app___current: {
|
||||||
|
responses: {
|
||||||
|
/** @description OK (with results) */
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['App'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Client error */
|
||||||
|
400: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Authentication error */
|
||||||
|
401: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Forbidden error */
|
||||||
|
403: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description I'm Ai */
|
||||||
|
418: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Too many requests */
|
||||||
|
429: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Internal server error */
|
||||||
|
500: {
|
||||||
|
content: {
|
||||||
|
'application/json': components['schemas']['Error'];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* app/show
|
* app/show
|
||||||
* @description No description provided.
|
* @description No description provided.
|
||||||
|
|
65
pnpm-lock.yaml
generated
65
pnpm-lock.yaml
generated
|
@ -182,6 +182,9 @@ importers:
|
||||||
async-mutex:
|
async-mutex:
|
||||||
specifier: 0.5.0
|
specifier: 0.5.0
|
||||||
version: 0.5.0
|
version: 0.5.0
|
||||||
|
axios:
|
||||||
|
specifier: 1.7.4
|
||||||
|
version: 1.7.4
|
||||||
bcryptjs:
|
bcryptjs:
|
||||||
specifier: 2.4.3
|
specifier: 2.4.3
|
||||||
version: 2.4.3
|
version: 2.4.3
|
||||||
|
@ -233,9 +236,6 @@ importers:
|
||||||
fastify:
|
fastify:
|
||||||
specifier: 5.3.2
|
specifier: 5.3.2
|
||||||
version: 5.3.2
|
version: 5.3.2
|
||||||
fastify-multer:
|
|
||||||
specifier: ^2.0.3
|
|
||||||
version: 2.0.3
|
|
||||||
fastify-raw-body:
|
fastify-raw-body:
|
||||||
specifier: 5.0.0
|
specifier: 5.0.0
|
||||||
version: 5.0.0
|
version: 5.0.0
|
||||||
|
@ -2274,10 +2274,6 @@ packages:
|
||||||
'@fastify/ajv-compiler@4.0.1':
|
'@fastify/ajv-compiler@4.0.1':
|
||||||
resolution: {integrity: sha512-DxrBdgsjNLP0YM6W5Hd6/Fmj43S8zMKiFJYgi+Ri3htTGAowPVG/tG1wpnWLMjufEnehRivUCKZ1pLDIoZdTuw==}
|
resolution: {integrity: sha512-DxrBdgsjNLP0YM6W5Hd6/Fmj43S8zMKiFJYgi+Ri3htTGAowPVG/tG1wpnWLMjufEnehRivUCKZ1pLDIoZdTuw==}
|
||||||
|
|
||||||
'@fastify/busboy@1.2.1':
|
|
||||||
resolution: {integrity: sha512-7PQA7EH43S0CxcOa9OeAnaeA0oQ+e/DHNPZwSQM9CQHW76jle5+OvLdibRp/Aafs9KXbLhxyjOTkRjWUbQEd3Q==}
|
|
||||||
engines: {node: '>=14'}
|
|
||||||
|
|
||||||
'@fastify/busboy@2.1.0':
|
'@fastify/busboy@2.1.0':
|
||||||
resolution: {integrity: sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==}
|
resolution: {integrity: sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
@ -5455,10 +5451,6 @@ packages:
|
||||||
resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==}
|
resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==}
|
||||||
engines: {'0': node >= 0.8}
|
engines: {'0': node >= 0.8}
|
||||||
|
|
||||||
concat-stream@2.0.0:
|
|
||||||
resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==}
|
|
||||||
engines: {'0': node >= 6.0}
|
|
||||||
|
|
||||||
config-chain@1.1.13:
|
config-chain@1.1.13:
|
||||||
resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==}
|
resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==}
|
||||||
|
|
||||||
|
@ -6313,13 +6305,6 @@ packages:
|
||||||
resolution: {integrity: sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==}
|
resolution: {integrity: sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
fastify-multer@2.0.3:
|
|
||||||
resolution: {integrity: sha512-QnFqrRgxmUwWHTgX9uyQSu0C/hmVCfcxopqjApZ4uaZD5W9MJ+nHUlW4+9q7Yd3BRxDIuHvgiM5mjrh6XG8cAA==}
|
|
||||||
engines: {node: '>=10.17.0'}
|
|
||||||
|
|
||||||
fastify-plugin@2.3.4:
|
|
||||||
resolution: {integrity: sha512-I+Oaj6p9oiRozbam30sh39BiuiqBda7yK2nmSPVwDCfIBlKnT8YB3MY+pRQc2Fcd07bf6KPGklHJaQ2Qu81TYQ==}
|
|
||||||
|
|
||||||
fastify-plugin@4.5.1:
|
fastify-plugin@4.5.1:
|
||||||
resolution: {integrity: sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==}
|
resolution: {integrity: sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==}
|
||||||
|
|
||||||
|
@ -6435,15 +6420,6 @@ packages:
|
||||||
resolution: {integrity: sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==}
|
resolution: {integrity: sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
follow-redirects@1.15.2:
|
|
||||||
resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
|
|
||||||
engines: {node: '>=4.0'}
|
|
||||||
peerDependencies:
|
|
||||||
debug: '*'
|
|
||||||
peerDependenciesMeta:
|
|
||||||
debug:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
follow-redirects@1.15.9:
|
follow-redirects@1.15.9:
|
||||||
resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
|
resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
|
||||||
engines: {node: '>=4.0'}
|
engines: {node: '>=4.0'}
|
||||||
|
@ -9960,9 +9936,6 @@ packages:
|
||||||
resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==}
|
resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
text-decoding@1.0.0:
|
|
||||||
resolution: {integrity: sha512-/0TJD42KDnVwKmDK6jj3xP7E2MG7SHAOG4tyTgyUCRPdHwvkquYNLEQltmdMa3owq3TkddCVcTsoctJI8VQNKA==}
|
|
||||||
|
|
||||||
textarea-caret@3.1.0:
|
textarea-caret@3.1.0:
|
||||||
resolution: {integrity: sha512-cXAvzO9pP5CGa6NKx0WYHl+8CHKZs8byMkt3PCJBCmq2a34YA9pO1NrQET5pzeqnBjBdToF5No4rrmkDUgQC2Q==}
|
resolution: {integrity: sha512-cXAvzO9pP5CGa6NKx0WYHl+8CHKZs8byMkt3PCJBCmq2a34YA9pO1NrQET5pzeqnBjBdToF5No4rrmkDUgQC2Q==}
|
||||||
|
|
||||||
|
@ -12060,10 +12033,6 @@ snapshots:
|
||||||
ajv-formats: 3.0.1(ajv@8.17.1)
|
ajv-formats: 3.0.1(ajv@8.17.1)
|
||||||
fast-uri: 3.0.1
|
fast-uri: 3.0.1
|
||||||
|
|
||||||
'@fastify/busboy@1.2.1':
|
|
||||||
dependencies:
|
|
||||||
text-decoding: 1.0.0
|
|
||||||
|
|
||||||
'@fastify/busboy@2.1.0': {}
|
'@fastify/busboy@2.1.0': {}
|
||||||
|
|
||||||
'@fastify/busboy@3.0.0': {}
|
'@fastify/busboy@3.0.0': {}
|
||||||
|
@ -15354,7 +15323,7 @@ snapshots:
|
||||||
|
|
||||||
axios@0.24.0:
|
axios@0.24.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
follow-redirects: 1.15.2
|
follow-redirects: 1.15.9(debug@4.4.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- debug
|
- debug
|
||||||
|
|
||||||
|
@ -15990,13 +15959,6 @@ snapshots:
|
||||||
readable-stream: 2.3.7
|
readable-stream: 2.3.7
|
||||||
typedarray: 0.0.6
|
typedarray: 0.0.6
|
||||||
|
|
||||||
concat-stream@2.0.0:
|
|
||||||
dependencies:
|
|
||||||
buffer-from: 1.1.2
|
|
||||||
inherits: 2.0.4
|
|
||||||
readable-stream: 3.6.0
|
|
||||||
typedarray: 0.0.6
|
|
||||||
|
|
||||||
config-chain@1.1.13:
|
config-chain@1.1.13:
|
||||||
dependencies:
|
dependencies:
|
||||||
ini: 1.3.8
|
ini: 1.3.8
|
||||||
|
@ -17122,21 +17084,6 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
strnum: 1.0.5
|
strnum: 1.0.5
|
||||||
|
|
||||||
fastify-multer@2.0.3:
|
|
||||||
dependencies:
|
|
||||||
'@fastify/busboy': 1.2.1
|
|
||||||
append-field: 1.0.0
|
|
||||||
concat-stream: 2.0.0
|
|
||||||
fastify-plugin: 2.3.4
|
|
||||||
mkdirp: 1.0.4
|
|
||||||
on-finished: 2.4.1
|
|
||||||
type-is: 1.6.18
|
|
||||||
xtend: 4.0.2
|
|
||||||
|
|
||||||
fastify-plugin@2.3.4:
|
|
||||||
dependencies:
|
|
||||||
semver: 7.6.0
|
|
||||||
|
|
||||||
fastify-plugin@4.5.1: {}
|
fastify-plugin@4.5.1: {}
|
||||||
|
|
||||||
fastify-plugin@5.0.1: {}
|
fastify-plugin@5.0.1: {}
|
||||||
|
@ -17298,8 +17245,6 @@ snapshots:
|
||||||
async: 0.2.10
|
async: 0.2.10
|
||||||
which: 1.3.1
|
which: 1.3.1
|
||||||
|
|
||||||
follow-redirects@1.15.2: {}
|
|
||||||
|
|
||||||
follow-redirects@1.15.9(debug@4.4.0):
|
follow-redirects@1.15.9(debug@4.4.0):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
debug: 4.4.0(supports-color@8.1.1)
|
debug: 4.4.0(supports-color@8.1.1)
|
||||||
|
@ -21438,8 +21383,6 @@ snapshots:
|
||||||
glob: 10.4.5
|
glob: 10.4.5
|
||||||
minimatch: 9.0.5
|
minimatch: 9.0.5
|
||||||
|
|
||||||
text-decoding@1.0.0: {}
|
|
||||||
|
|
||||||
textarea-caret@3.1.0: {}
|
textarea-caret@3.1.0: {}
|
||||||
|
|
||||||
thenify-all@1.6.0:
|
thenify-all@1.6.0:
|
||||||
|
|
Loading…
Add table
Reference in a new issue