mirror of
https://codeberg.org/yeentown/barkey.git
synced 2025-07-07 20:44:34 +00:00
refactor mastodon API and preserve remote user agent for requests
This commit is contained in:
parent
92382b2ed4
commit
f61d71ac8c
17 changed files with 1319 additions and 1447 deletions
|
@ -7,6 +7,15 @@ import { Module } from '@nestjs/common';
|
|||
import { EndpointsModule } from '@/server/api/EndpointsModule.js';
|
||||
import { CoreModule } from '@/core/CoreModule.js';
|
||||
import { SkRateLimiterService } from '@/server/SkRateLimiterService.js';
|
||||
import { MastodonClientService } from '@/server/api/mastodon/MastodonClientService.js';
|
||||
import { ApiNotificationsMastodon } from '@/server/api/mastodon/endpoints/notifications.js';
|
||||
import { ApiAccountMastodon } from '@/server/api/mastodon/endpoints/account.js';
|
||||
import { ApiFilterMastodon } from '@/server/api/mastodon/endpoints/filter.js';
|
||||
import { ApiSearchMastodon } from '@/server/api/mastodon/endpoints/search.js';
|
||||
import { ApiTimelineMastodon } from '@/server/api/mastodon/endpoints/timeline.js';
|
||||
import { ApiAppsMastodon } from '@/server/api/mastodon/endpoints/apps.js';
|
||||
import { ApiInstanceMastodon } from '@/server/api/mastodon/endpoints/instance.js';
|
||||
import { ApiStatusMastodon } from '@/server/api/mastodon/endpoints/status.js';
|
||||
import { ApiCallService } from './api/ApiCallService.js';
|
||||
import { FileServerService } from './FileServerService.js';
|
||||
import { HealthServerService } from './HealthServerService.js';
|
||||
|
@ -107,6 +116,15 @@ import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.j
|
|||
MastoConverters,
|
||||
MastodonLogger,
|
||||
MastodonDataService,
|
||||
MastodonClientService,
|
||||
ApiAccountMastodon,
|
||||
ApiAppsMastodon,
|
||||
ApiFilterMastodon,
|
||||
ApiInstanceMastodon,
|
||||
ApiNotificationsMastodon,
|
||||
ApiSearchMastodon,
|
||||
ApiStatusMastodon,
|
||||
ApiTimelineMastodon,
|
||||
],
|
||||
exports: [
|
||||
ServerService,
|
||||
|
|
|
@ -4,76 +4,45 @@
|
|||
*/
|
||||
|
||||
import querystring from 'querystring';
|
||||
import { megalodon, Entity, MegalodonInterface } from 'megalodon';
|
||||
import { IsNull } from 'typeorm';
|
||||
import multer from 'fastify-multer';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { AccessTokensRepository, UserProfilesRepository, UsersRepository, MiMeta } from '@/models/_.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { DriveService } from '@/core/DriveService.js';
|
||||
import { getErrorData, MastodonLogger } from '@/server/api/mastodon/MastodonLogger.js';
|
||||
import { ApiAccountMastodonRoute } from '@/server/api/mastodon/endpoints/account.js';
|
||||
import { ApiSearchMastodonRoute } from '@/server/api/mastodon/endpoints/search.js';
|
||||
import { ApiFilterMastodonRoute } from '@/server/api/mastodon/endpoints/filter.js';
|
||||
import { ApiNotifyMastodonRoute } from '@/server/api/mastodon/endpoints/notifications.js';
|
||||
import { AuthenticateService } from '@/server/api/AuthenticateService.js';
|
||||
import { MiLocalUser } from '@/models/User.js';
|
||||
import { AuthMastodonRoute } from './endpoints/auth.js';
|
||||
import { toBoolean } from './timelineArgs.js';
|
||||
import { convertAnnouncement, convertFilter, convertAttachment, convertFeaturedTag, convertList, MastoConverters } from './converters.js';
|
||||
import { getInstance } from './endpoints/meta.js';
|
||||
import { ApiAuthMastodon, ApiAccountMastodon, ApiFilterMastodon, ApiNotifyMastodon, ApiSearchMastodon, ApiTimelineMastodon, ApiStatusMastodon } from './endpoints.js';
|
||||
import type { FastifyInstance, FastifyPluginOptions, FastifyRequest } from 'fastify';
|
||||
|
||||
export function getAccessToken(authorization: string | undefined): string | null {
|
||||
const accessTokenArr = authorization?.split(' ') ?? [null];
|
||||
return accessTokenArr[accessTokenArr.length - 1];
|
||||
}
|
||||
|
||||
export function getClient(BASE_URL: string, authorization: string | undefined): MegalodonInterface {
|
||||
const accessToken = getAccessToken(authorization);
|
||||
return megalodon('misskey', BASE_URL, accessToken);
|
||||
}
|
||||
import { MastodonClientService } from '@/server/api/mastodon/MastodonClientService.js';
|
||||
import { ApiAccountMastodon } from '@/server/api/mastodon/endpoints/account.js';
|
||||
import { ApiAppsMastodon } from '@/server/api/mastodon/endpoints/apps.js';
|
||||
import { ApiFilterMastodon } from '@/server/api/mastodon/endpoints/filter.js';
|
||||
import { ApiInstanceMastodon } from '@/server/api/mastodon/endpoints/instance.js';
|
||||
import { ApiStatusMastodon } from '@/server/api/mastodon/endpoints/status.js';
|
||||
import { ApiNotificationsMastodon } from '@/server/api/mastodon/endpoints/notifications.js';
|
||||
import { ApiTimelineMastodon } from '@/server/api/mastodon/endpoints/timeline.js';
|
||||
import { ApiSearchMastodon } from '@/server/api/mastodon/endpoints/search.js';
|
||||
import { parseTimelineArgs, TimelineArgs, toBoolean } from './argsUtils.js';
|
||||
import { convertAnnouncement, convertAttachment, MastoConverters, convertRelationship } from './converters.js';
|
||||
import type { Entity } from 'megalodon';
|
||||
import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
|
||||
|
||||
@Injectable()
|
||||
export class MastodonApiServerService {
|
||||
constructor(
|
||||
@Inject(DI.meta)
|
||||
private readonly serverSettings: MiMeta,
|
||||
@Inject(DI.usersRepository)
|
||||
private readonly usersRepository: UsersRepository,
|
||||
@Inject(DI.userProfilesRepository)
|
||||
private readonly userProfilesRepository: UserProfilesRepository,
|
||||
@Inject(DI.accessTokensRepository)
|
||||
private readonly accessTokensRepository: AccessTokensRepository,
|
||||
@Inject(DI.config)
|
||||
private readonly config: Config,
|
||||
private readonly driveService: DriveService,
|
||||
|
||||
private readonly mastoConverters: MastoConverters,
|
||||
private readonly logger: MastodonLogger,
|
||||
private readonly authenticateService: AuthenticateService,
|
||||
private readonly clientService: MastodonClientService,
|
||||
private readonly apiAccountMastodon: ApiAccountMastodon,
|
||||
private readonly apiAppsMastodon: ApiAppsMastodon,
|
||||
private readonly apiFilterMastodon: ApiFilterMastodon,
|
||||
private readonly apiInstanceMastodon: ApiInstanceMastodon,
|
||||
private readonly apiNotificationsMastodon: ApiNotificationsMastodon,
|
||||
private readonly apiSearchMastodon: ApiSearchMastodon,
|
||||
private readonly apiStatusMastodon: ApiStatusMastodon,
|
||||
private readonly apiTimelineMastodon: ApiTimelineMastodon,
|
||||
) {}
|
||||
|
||||
@bindThis
|
||||
public async getAuthClient(request: FastifyRequest): Promise<{ client: MegalodonInterface, me: MiLocalUser | null }> {
|
||||
const accessToken = getAccessToken(request.headers.authorization);
|
||||
const [me] = await this.authenticateService.authenticate(accessToken);
|
||||
|
||||
const baseUrl = `${request.protocol}://${request.host}`;
|
||||
const client = megalodon('misskey', baseUrl, accessToken);
|
||||
|
||||
return { client, me };
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getAuthOnly(request: FastifyRequest): Promise<MiLocalUser | null> {
|
||||
const accessToken = getAccessToken(request.headers.authorization);
|
||||
const [me] = await this.authenticateService.authenticate(accessToken);
|
||||
return me;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createServer(fastify: FastifyInstance, _options: FastifyPluginOptions, done: (err?: Error) => void) {
|
||||
const upload = multer({
|
||||
|
@ -107,11 +76,19 @@ export class MastodonApiServerService {
|
|||
|
||||
fastify.register(multer.contentParser);
|
||||
|
||||
// External endpoints
|
||||
this.apiAccountMastodon.register(fastify, upload);
|
||||
this.apiAppsMastodon.register(fastify, upload);
|
||||
this.apiFilterMastodon.register(fastify, upload);
|
||||
this.apiInstanceMastodon.register(fastify);
|
||||
this.apiNotificationsMastodon.register(fastify, upload);
|
||||
this.apiSearchMastodon.register(fastify);
|
||||
this.apiStatusMastodon.register(fastify);
|
||||
this.apiTimelineMastodon.register(fastify);
|
||||
|
||||
fastify.get('/v1/custom_emojis', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.getInstanceCustomEmojis();
|
||||
reply.send(data.data);
|
||||
} catch (e) {
|
||||
|
@ -121,36 +98,9 @@ export class MastodonApiServerService {
|
|||
}
|
||||
});
|
||||
|
||||
fastify.get('/v1/instance', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
|
||||
// displayed without being logged in
|
||||
try {
|
||||
const data = await client.getInstance();
|
||||
const admin = await this.usersRepository.findOne({
|
||||
where: {
|
||||
host: IsNull(),
|
||||
isRoot: true,
|
||||
isDeleted: false,
|
||||
isSuspended: false,
|
||||
},
|
||||
order: { id: 'ASC' },
|
||||
});
|
||||
const contact = admin == null ? null : await this.mastoConverters.convertAccount((await client.getAccount(admin.id)).data);
|
||||
reply.send(await getInstance(data.data, contact as Entity.Account, this.config, this.serverSettings));
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('GET /v1/instance', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.get('/v1/announcements', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.getInstanceAnnouncements();
|
||||
reply.send(data.data.map((announcement) => convertAnnouncement(announcement)));
|
||||
} catch (e) {
|
||||
|
@ -161,11 +111,9 @@ export class MastodonApiServerService {
|
|||
});
|
||||
|
||||
fastify.post<{ Body: { id?: string } }>('/v1/announcements/:id/dismiss', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
if (!_request.body.id) return reply.code(400).send({ error: 'Missing required payload "id"' });
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.dismissInstanceAnnouncement(_request.body['id']);
|
||||
reply.send(data.data);
|
||||
} catch (e) {
|
||||
|
@ -176,15 +124,13 @@ export class MastodonApiServerService {
|
|||
});
|
||||
|
||||
fastify.post('/v1/media', { preHandler: upload.single('file') }, async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const multipartData = await _request.file();
|
||||
if (!multipartData) {
|
||||
reply.code(401).send({ error: 'No image' });
|
||||
return;
|
||||
}
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.uploadMedia(multipartData);
|
||||
reply.send(convertAttachment(data.data as Entity.Attachment));
|
||||
} catch (e) {
|
||||
|
@ -195,15 +141,13 @@ export class MastodonApiServerService {
|
|||
});
|
||||
|
||||
fastify.post<{ Body: { description?: string; focus?: string }}>('/v2/media', { preHandler: upload.single('file') }, async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const multipartData = await _request.file();
|
||||
if (!multipartData) {
|
||||
reply.code(401).send({ error: 'No image' });
|
||||
return;
|
||||
}
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.uploadMedia(multipartData, _request.body);
|
||||
reply.send(convertAttachment(data.data as Entity.Attachment));
|
||||
} catch (e) {
|
||||
|
@ -213,27 +157,9 @@ export class MastodonApiServerService {
|
|||
}
|
||||
});
|
||||
|
||||
fastify.get('/v1/filters', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
|
||||
// displayed without being logged in
|
||||
try {
|
||||
const data = await client.getFilters();
|
||||
reply.send(data.data.map((filter) => convertFilter(filter)));
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('GET /v1/filters', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.get('/v1/trends', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
|
||||
// displayed without being logged in
|
||||
try {
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.getInstanceTrends();
|
||||
reply.send(data.data);
|
||||
} catch (e) {
|
||||
|
@ -244,11 +170,8 @@ export class MastodonApiServerService {
|
|||
});
|
||||
|
||||
fastify.get('/v1/trends/tags', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
|
||||
// displayed without being logged in
|
||||
try {
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.getInstanceTrends();
|
||||
reply.send(data.data);
|
||||
} catch (e) {
|
||||
|
@ -263,26 +186,9 @@ export class MastodonApiServerService {
|
|||
reply.send([]);
|
||||
});
|
||||
|
||||
fastify.post<AuthMastodonRoute>('/v1/apps', { preHandler: upload.single('none') }, async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const client = getClient(BASE_URL, ''); // we are using this here, because in private mode some info isnt
|
||||
// displayed without being logged in
|
||||
try {
|
||||
const data = await ApiAuthMastodon(_request, client);
|
||||
reply.send(data);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('GET /v1/apps', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.get('/v1/preferences', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
|
||||
// displayed without being logged in
|
||||
try {
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.getPreferences();
|
||||
reply.send(data.data);
|
||||
} catch (e) {
|
||||
|
@ -292,317 +198,9 @@ export class MastodonApiServerService {
|
|||
}
|
||||
});
|
||||
|
||||
//#region Accounts
|
||||
fastify.get<ApiAccountMastodonRoute>('/v1/accounts/verify_credentials', async (_request, reply) => {
|
||||
try {
|
||||
const { client, me } = await this.getAuthClient(_request);
|
||||
const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
|
||||
reply.send(await account.verifyCredentials());
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('GET /v1/accounts/verify_credentials', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.patch<{
|
||||
Body: {
|
||||
discoverable?: string,
|
||||
bot?: string,
|
||||
display_name?: string,
|
||||
note?: string,
|
||||
avatar?: string,
|
||||
header?: string,
|
||||
locked?: string,
|
||||
source?: {
|
||||
privacy?: string,
|
||||
sensitive?: string,
|
||||
language?: string,
|
||||
},
|
||||
fields_attributes?: {
|
||||
name: string,
|
||||
value: string,
|
||||
}[],
|
||||
},
|
||||
}>('/v1/accounts/update_credentials', { preHandler: upload.any() }, async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
|
||||
// displayed without being logged in
|
||||
try {
|
||||
// Check if there is an 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) {
|
||||
const tokeninfo = await this.accessTokensRepository.findOneBy({ token: accessTokens.replace('Bearer ', '') });
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const avatar = (_request.files as any).find((obj: any) => {
|
||||
return obj.fieldname === 'avatar';
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const header = (_request.files as any).find((obj: any) => {
|
||||
return obj.fieldname === 'header';
|
||||
});
|
||||
|
||||
if (tokeninfo && avatar) {
|
||||
const upload = await this.driveService.addFile({
|
||||
user: { id: tokeninfo.userId, host: null },
|
||||
path: avatar.path,
|
||||
name: avatar.originalname !== null && avatar.originalname !== 'file' ? avatar.originalname : undefined,
|
||||
sensitive: false,
|
||||
});
|
||||
if (upload.type.startsWith('image/')) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(_request.body as any).avatar = upload.id;
|
||||
}
|
||||
} else if (tokeninfo && header) {
|
||||
const upload = await this.driveService.addFile({
|
||||
user: { id: tokeninfo.userId, host: null },
|
||||
path: header.path,
|
||||
name: header.originalname !== null && header.originalname !== 'file' ? header.originalname : undefined,
|
||||
sensitive: false,
|
||||
});
|
||||
if (upload.type.startsWith('image/')) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(_request.body as any).header = upload.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if ((_request.body as any).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() === '') 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');
|
||||
}
|
||||
return {
|
||||
...field,
|
||||
};
|
||||
});
|
||||
// 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 = {
|
||||
..._request.body,
|
||||
discoverable: toBoolean(_request.body.discoverable),
|
||||
bot: toBoolean(_request.body.bot),
|
||||
locked: toBoolean(_request.body.locked),
|
||||
source: _request.body.source ? {
|
||||
..._request.body.source,
|
||||
sensitive: toBoolean(_request.body.source.sensitive),
|
||||
} : undefined,
|
||||
};
|
||||
const data = await client.updateCredentials(options);
|
||||
reply.send(await this.mastoConverters.convertAccount(data.data));
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('PATCH /v1/accounts/update_credentials', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.get<{ Querystring: { acct?: string }}>('/v1/accounts/lookup', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isn't displayed without being logged in
|
||||
try {
|
||||
if (!_request.query.acct) return reply.code(400).send({ error: 'Missing required property "acct"' });
|
||||
const data = await client.search(_request.query.acct, { type: 'accounts' });
|
||||
const profile = await this.userProfilesRepository.findOneBy({ userId: data.data.accounts[0].id });
|
||||
data.data.accounts[0].fields = profile?.fields.map(f => ({ ...f, verified_at: null })) ?? [];
|
||||
reply.send(await this.mastoConverters.convertAccount(data.data.accounts[0]));
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('GET /v1/accounts/lookup', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.get<ApiAccountMastodonRoute & { Querystring: { id?: string | string[], 'id[]'?: string | string[] }}>('/v1/accounts/relationships', async (_request, reply) => {
|
||||
try {
|
||||
const { client, me } = await this.getAuthClient(_request);
|
||||
let ids = _request.query['id[]'] ?? _request.query['id'] ?? [];
|
||||
if (typeof ids === 'string') {
|
||||
ids = [ids];
|
||||
}
|
||||
const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
|
||||
reply.send(await account.getRelationships(ids));
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('GET /v1/accounts/relationships', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.get<{ Params: { id?: string } }>('/v1/accounts/:id', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const data = await client.getAccount(_request.params.id);
|
||||
const account = await this.mastoConverters.convertAccount(data.data);
|
||||
reply.send(account);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/accounts/${_request.params.id}`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.get<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/statuses', async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const { client, me } = await this.getAuthClient(_request);
|
||||
const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
|
||||
reply.send(await account.getStatuses());
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/accounts/${_request.params.id}/statuses`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.get<{ Params: { id?: string } }>('/v1/accounts/:id/featured_tags', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const data = await client.getFeaturedTags();
|
||||
reply.send(data.data.map((tag) => convertFeaturedTag(tag)));
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/accounts/${_request.params.id}/featured_tags`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.get<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/followers', async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const { client, me } = await this.getAuthClient(_request);
|
||||
const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
|
||||
reply.send(await account.getFollowers());
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/accounts/${_request.params.id}/followers`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.get<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/following', async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const { client, me } = await this.getAuthClient(_request);
|
||||
const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
|
||||
reply.send(await account.getFollowing());
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/accounts/${_request.params.id}/following`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.get<{ Params: { id?: string } }>('/v1/accounts/:id/lists', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const data = await client.getAccountLists(_request.params.id);
|
||||
reply.send(data.data.map((list) => convertList(list)));
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/accounts/${_request.params.id}/lists`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/follow', { preHandler: upload.single('none') }, async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const { client, me } = await this.getAuthClient(_request);
|
||||
const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
|
||||
reply.send(await account.addFollow());
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/accounts/${_request.params.id}/follow`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/unfollow', { preHandler: upload.single('none') }, async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const { client, me } = await this.getAuthClient(_request);
|
||||
const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
|
||||
reply.send(await account.rmFollow());
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/accounts/${_request.params.id}/unfollow`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/block', { preHandler: upload.single('none') }, async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const { client, me } = await this.getAuthClient(_request);
|
||||
const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
|
||||
reply.send(await account.addBlock());
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/accounts/${_request.params.id}/block`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/unblock', { preHandler: upload.single('none') }, async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const { client, me } = await this.getAuthClient(_request);
|
||||
const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
|
||||
reply.send(await account.rmBlock());
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/accounts/${_request.params.id}/unblock`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/mute', { preHandler: upload.single('none') }, async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const { client, me } = await this.getAuthClient(_request);
|
||||
const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
|
||||
reply.send(await account.addMute());
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/accounts/${_request.params.id}/mute`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/unmute', { preHandler: upload.single('none') }, async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const { client, me } = await this.getAuthClient(_request);
|
||||
const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
|
||||
reply.send(await account.rmMute());
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/accounts/${_request.params.id}/unmute`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.get('/v1/followed_tags', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.getFollowedTags();
|
||||
reply.send(data.data);
|
||||
} catch (e) {
|
||||
|
@ -612,11 +210,14 @@ export class MastodonApiServerService {
|
|||
}
|
||||
});
|
||||
|
||||
fastify.get<ApiAccountMastodonRoute>('/v1/bookmarks', async (_request, reply) => {
|
||||
fastify.get<{ Querystring: TimelineArgs }>('/v1/bookmarks', async (_request, reply) => {
|
||||
try {
|
||||
const { client, me } = await this.getAuthClient(_request);
|
||||
const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
|
||||
reply.send(await account.getBookmarks());
|
||||
const { client, me } = await this.clientService.getAuthClient(_request);
|
||||
|
||||
const data = await client.getBookmarks(parseTimelineArgs(_request.query));
|
||||
const response = await Promise.all(data.data.map((status) => this.mastoConverters.convertStatus(status, me)));
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('GET /v1/bookmarks', data);
|
||||
|
@ -624,11 +225,14 @@ export class MastodonApiServerService {
|
|||
}
|
||||
});
|
||||
|
||||
fastify.get<ApiAccountMastodonRoute>('/v1/favourites', async (_request, reply) => {
|
||||
fastify.get<{ Querystring: TimelineArgs }>('/v1/favourites', async (_request, reply) => {
|
||||
try {
|
||||
const { client, me } = await this.getAuthClient(_request);
|
||||
const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
|
||||
reply.send(await account.getFavourites());
|
||||
const { client, me } = await this.clientService.getAuthClient(_request);
|
||||
|
||||
const data = await client.getFavourites(parseTimelineArgs(_request.query));
|
||||
const response = Promise.all(data.data.map((status) => this.mastoConverters.convertStatus(status, me)));
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('GET /v1/favourites', data);
|
||||
|
@ -636,11 +240,14 @@ export class MastodonApiServerService {
|
|||
}
|
||||
});
|
||||
|
||||
fastify.get<ApiAccountMastodonRoute>('/v1/mutes', async (_request, reply) => {
|
||||
fastify.get<{ Querystring: TimelineArgs }>('/v1/mutes', async (_request, reply) => {
|
||||
try {
|
||||
const { client, me } = await this.getAuthClient(_request);
|
||||
const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
|
||||
reply.send(await account.getMutes());
|
||||
const client = this.clientService.getClient(_request);
|
||||
|
||||
const data = await client.getMutes(parseTimelineArgs(_request.query));
|
||||
const response = Promise.all(data.data.map((account) => this.mastoConverters.convertAccount(account)));
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('GET /v1/mutes', data);
|
||||
|
@ -648,11 +255,14 @@ export class MastodonApiServerService {
|
|||
}
|
||||
});
|
||||
|
||||
fastify.get<ApiAccountMastodonRoute>('/v1/blocks', async (_request, reply) => {
|
||||
fastify.get<{ Querystring: TimelineArgs }>('/v1/blocks', async (_request, reply) => {
|
||||
try {
|
||||
const { client, me } = await this.getAuthClient(_request);
|
||||
const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
|
||||
reply.send(await account.getBlocks());
|
||||
const client = this.clientService.getClient(_request);
|
||||
|
||||
const data = await client.getBlocks(parseTimelineArgs(_request.query));
|
||||
const response = Promise.all(data.data.map((account) => this.mastoConverters.convertAccount(account)));
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('GET /v1/blocks', data);
|
||||
|
@ -661,10 +271,8 @@ export class MastodonApiServerService {
|
|||
});
|
||||
|
||||
fastify.get<{ Querystring: { limit?: string }}>('/v1/follow_requests', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const client = this.clientService.getClient(_request);
|
||||
const limit = _request.query.limit ? parseInt(_request.query.limit) : 20;
|
||||
const data = await client.getFollowRequests(limit);
|
||||
reply.send(await Promise.all(data.data.map(async (account) => await this.mastoConverters.convertAccount(account as Entity.Account))));
|
||||
|
@ -675,12 +283,15 @@ export class MastodonApiServerService {
|
|||
}
|
||||
});
|
||||
|
||||
fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/follow_requests/:id/authorize', { preHandler: upload.single('none') }, async (_request, reply) => {
|
||||
fastify.post<{ Querystring: TimelineArgs, Params: { id?: string } }>('/v1/follow_requests/:id/authorize', { preHandler: upload.single('none') }, async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const { client, me } = await this.getAuthClient(_request);
|
||||
const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
|
||||
reply.send(await account.acceptFollow());
|
||||
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.acceptFollowRequest(_request.params.id);
|
||||
const response = convertRelationship(data.data);
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/follow_requests/${_request.params.id}/authorize`, data);
|
||||
|
@ -688,12 +299,15 @@ export class MastodonApiServerService {
|
|||
}
|
||||
});
|
||||
|
||||
fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/follow_requests/:id/reject', { preHandler: upload.single('none') }, async (_request, reply) => {
|
||||
fastify.post<{ Querystring: TimelineArgs, Params: { id?: string } }>('/v1/follow_requests/:id/reject', { preHandler: upload.single('none') }, async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const { client, me } = await this.getAuthClient(_request);
|
||||
const account = new ApiAccountMastodon(_request, client, me, this.mastoConverters);
|
||||
reply.send(await account.rejectFollow());
|
||||
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.rejectFollowRequest(_request.params.id);
|
||||
const response = convertRelationship(data.data);
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/follow_requests/${_request.params.id}/reject`, data);
|
||||
|
@ -702,227 +316,6 @@ export class MastodonApiServerService {
|
|||
});
|
||||
//#endregion
|
||||
|
||||
//#region Search
|
||||
fastify.get<ApiSearchMastodonRoute>('/v1/search', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
try {
|
||||
const { client, me } = await this.getAuthClient(_request);
|
||||
const search = new ApiSearchMastodon(_request, client, me, BASE_URL, this.mastoConverters);
|
||||
reply.send(await search.SearchV1());
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('GET /v1/search', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.get<ApiSearchMastodonRoute>('/v2/search', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
try {
|
||||
const { client, me } = await this.getAuthClient(_request);
|
||||
const search = new ApiSearchMastodon(_request, client, me, BASE_URL, this.mastoConverters);
|
||||
reply.send(await search.SearchV2());
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('GET /v2/search', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.get<ApiSearchMastodonRoute>('/v1/trends/statuses', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
try {
|
||||
const { client, me } = await this.getAuthClient(_request);
|
||||
const search = new ApiSearchMastodon(_request, client, me, BASE_URL, this.mastoConverters);
|
||||
reply.send(await search.getStatusTrends());
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('GET /v1/trends/statuses', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.get<ApiSearchMastodonRoute>('/v2/suggestions', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
try {
|
||||
const { client, me } = await this.getAuthClient(_request);
|
||||
const search = new ApiSearchMastodon(_request, client, me, BASE_URL, this.mastoConverters);
|
||||
reply.send(await search.getSuggestions());
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('GET /v2/suggestions', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
//#endregion
|
||||
|
||||
//#region Notifications
|
||||
fastify.get<ApiNotifyMastodonRoute>('/v1/notifications', async (_request, reply) => {
|
||||
try {
|
||||
const { client, me } = await this.getAuthClient(_request);
|
||||
const notify = new ApiNotifyMastodon(_request, client, me, this.mastoConverters);
|
||||
reply.send(await notify.getNotifications());
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('GET /v1/notifications', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.get<ApiNotifyMastodonRoute & { Params: { id?: string } }>('/v1/notification/:id', async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const { client, me } = await this.getAuthClient(_request);
|
||||
const notify = new ApiNotifyMastodon(_request, client, me, this.mastoConverters);
|
||||
reply.send(await notify.getNotification());
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/notification/${_request.params.id}`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.post<ApiNotifyMastodonRoute & { Params: { id?: string } }>('/v1/notification/:id/dismiss', { preHandler: upload.single('none') }, async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const { client, me } = await this.getAuthClient(_request);
|
||||
const notify = new ApiNotifyMastodon(_request, client, me, this.mastoConverters);
|
||||
reply.send(await notify.rmNotification());
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/notification/${_request.params.id}/dismiss`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.post<ApiNotifyMastodonRoute>('/v1/notifications/clear', { preHandler: upload.single('none') }, async (_request, reply) => {
|
||||
try {
|
||||
const { client, me } = await this.getAuthClient(_request);
|
||||
const notify = new ApiNotifyMastodon(_request, client, me, this.mastoConverters);
|
||||
reply.send(await notify.rmNotifications());
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('POST /v1/notifications/clear', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
//#endregion
|
||||
|
||||
//#region Filters
|
||||
fastify.get<ApiFilterMastodonRoute & { Params: { id?: string } }>('/v1/filters/:id', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const filter = new ApiFilterMastodon(_request, client);
|
||||
_request.params.id
|
||||
? reply.send(await filter.getFilter())
|
||||
: reply.send(await filter.getFilters());
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/filters/${_request.params.id}`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.post<ApiFilterMastodonRoute>('/v1/filters', { preHandler: upload.single('none') }, async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const filter = new ApiFilterMastodon(_request, client);
|
||||
reply.send(await filter.createFilter());
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('POST /v1/filters', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.post<ApiFilterMastodonRoute & { Params: { id?: string } }>('/v1/filters/:id', { preHandler: upload.single('none') }, async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const filter = new ApiFilterMastodon(_request, client);
|
||||
reply.send(await filter.updateFilter());
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/filters/${_request.params.id}`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.delete<ApiFilterMastodonRoute & { Params: { id?: string } }>('/v1/filters/:id', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const filter = new ApiFilterMastodon(_request, client);
|
||||
reply.send(await filter.rmFilter());
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`DELETE /v1/filters/${_request.params.id}`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
//#endregion
|
||||
|
||||
//#region Timelines
|
||||
const TLEndpoint = new ApiTimelineMastodon(fastify, this.mastoConverters, this.logger, this);
|
||||
|
||||
// GET Endpoints
|
||||
TLEndpoint.getTL();
|
||||
TLEndpoint.getHomeTl();
|
||||
TLEndpoint.getListTL();
|
||||
TLEndpoint.getTagTl();
|
||||
TLEndpoint.getConversations();
|
||||
TLEndpoint.getList();
|
||||
TLEndpoint.getLists();
|
||||
TLEndpoint.getListAccounts();
|
||||
|
||||
// POST Endpoints
|
||||
TLEndpoint.createList();
|
||||
TLEndpoint.addListAccount();
|
||||
|
||||
// PUT Endpoint
|
||||
TLEndpoint.updateList();
|
||||
|
||||
// DELETE Endpoints
|
||||
TLEndpoint.deleteList();
|
||||
TLEndpoint.rmListAccount();
|
||||
//#endregion
|
||||
|
||||
//#region Status
|
||||
const NoteEndpoint = new ApiStatusMastodon(fastify, this.mastoConverters, this.logger, this.authenticateService, this);
|
||||
|
||||
// GET Endpoints
|
||||
NoteEndpoint.getStatus();
|
||||
NoteEndpoint.getStatusSource();
|
||||
NoteEndpoint.getContext();
|
||||
NoteEndpoint.getHistory();
|
||||
NoteEndpoint.getReblogged();
|
||||
NoteEndpoint.getFavourites();
|
||||
NoteEndpoint.getMedia();
|
||||
NoteEndpoint.getPoll();
|
||||
|
||||
//POST Endpoints
|
||||
NoteEndpoint.postStatus();
|
||||
NoteEndpoint.addFavourite();
|
||||
NoteEndpoint.rmFavourite();
|
||||
NoteEndpoint.reblogStatus();
|
||||
NoteEndpoint.unreblogStatus();
|
||||
NoteEndpoint.bookmarkStatus();
|
||||
NoteEndpoint.unbookmarkStatus();
|
||||
NoteEndpoint.pinStatus();
|
||||
NoteEndpoint.unpinStatus();
|
||||
NoteEndpoint.reactStatus();
|
||||
NoteEndpoint.unreactStatus();
|
||||
NoteEndpoint.votePoll();
|
||||
|
||||
// PUT Endpoint
|
||||
fastify.put<{
|
||||
Params: {
|
||||
id?: string,
|
||||
|
@ -934,28 +327,25 @@ export class MastodonApiServerService {
|
|||
is_sensitive?: string,
|
||||
},
|
||||
}>('/v1/media/:id', { preHandler: upload.none() }, async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
|
||||
const options = {
|
||||
..._request.body,
|
||||
is_sensitive: toBoolean(_request.body.is_sensitive),
|
||||
};
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.updateMedia(_request.params.id, options);
|
||||
reply.send(convertAttachment(data.data));
|
||||
const response = convertAttachment(data.data);
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`PUT /v1/media/${_request.params.id}`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
NoteEndpoint.updateStatus();
|
||||
|
||||
// DELETE Endpoint
|
||||
NoteEndpoint.deleteStatus();
|
||||
//#endregion
|
||||
done();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { megalodon, MegalodonInterface } from 'megalodon';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { MiLocalUser } from '@/models/User.js';
|
||||
import { AuthenticateService } from '@/server/api/AuthenticateService.js';
|
||||
import type { FastifyRequest } from 'fastify';
|
||||
|
||||
@Injectable()
|
||||
export class MastodonClientService {
|
||||
constructor(
|
||||
private readonly authenticateService: AuthenticateService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Gets the authenticated user and API client for a request.
|
||||
*/
|
||||
public async getAuthClient(request: FastifyRequest, accessToken?: string | null): Promise<{ client: MegalodonInterface, me: MiLocalUser | null }> {
|
||||
const authorization = request.headers.authorization;
|
||||
accessToken = accessToken !== undefined ? accessToken : getAccessToken(authorization);
|
||||
|
||||
const me = await this.getAuth(request, accessToken);
|
||||
const client = this.getClient(request, accessToken);
|
||||
|
||||
return { client, me };
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the authenticated client user for a request.
|
||||
*/
|
||||
public async getAuth(request: FastifyRequest, accessToken?: string | null): Promise<MiLocalUser | null> {
|
||||
const authorization = request.headers.authorization;
|
||||
accessToken = accessToken !== undefined ? accessToken : getAccessToken(authorization);
|
||||
const [me] = await this.authenticateService.authenticate(accessToken);
|
||||
return me;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an authenticated API client for a request.
|
||||
*/
|
||||
public getClient(request: FastifyRequest, accessToken?: string | null): MegalodonInterface {
|
||||
const authorization = request.headers.authorization;
|
||||
accessToken = accessToken !== undefined ? accessToken : getAccessToken(authorization);
|
||||
|
||||
// TODO pass agent?
|
||||
const baseUrl = this.getBaseUrl(request);
|
||||
const userAgent = request.headers['user-agent'];
|
||||
return megalodon('misskey', baseUrl, accessToken, userAgent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the base URL (origin) of the incoming request
|
||||
*/
|
||||
public getBaseUrl(request: FastifyRequest): string {
|
||||
return `${request.protocol}://${request.host}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the first access token from an authorization header
|
||||
* Returns null if none were found.
|
||||
*/
|
||||
function getAccessToken(authorization: string | undefined): string | null {
|
||||
const accessTokenArr = authorization?.split(' ') ?? [null];
|
||||
return accessTokenArr[accessTokenArr.length - 1];
|
||||
}
|
|
@ -68,7 +68,6 @@ export class MastoConverters {
|
|||
|
||||
private encode(u: MiUser, m: IMentionedRemoteUsers): MastodonEntity.Mention {
|
||||
let acct = u.username;
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
let acctUrl = `https://${u.host || this.config.host}/@${u.username}`;
|
||||
let url: string | null = null;
|
||||
if (u.host) {
|
||||
|
@ -161,7 +160,6 @@ export class MastoConverters {
|
|||
});
|
||||
const fqn = `${user.username}@${user.host ?? this.config.hostname}`;
|
||||
let acct = user.username;
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
let acctUrl = `https://${user.host || this.config.host}/@${user.username}`;
|
||||
const acctUri = `https://${this.config.host}/users/${user.id}`;
|
||||
if (user.host) {
|
||||
|
@ -265,7 +263,6 @@ export class MastoConverters {
|
|||
});
|
||||
|
||||
// This must mirror the usual isQuote / isPureRenote logic used elsewhere.
|
||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
||||
const isQuote = note.renoteId && (note.text || note.cw || note.fileIds.length > 0 || note.hasPoll || note.replyId);
|
||||
|
||||
const renote: Promise<MiNote> | null = note.renoteId ? this.mastodonDataService.requireNote(note.renoteId, me) : null;
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: marie and other Sharkey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { ApiAuthMastodon } from './endpoints/auth.js';
|
||||
import { ApiAccountMastodon } from './endpoints/account.js';
|
||||
import { ApiSearchMastodon } from './endpoints/search.js';
|
||||
import { ApiNotifyMastodon } from './endpoints/notifications.js';
|
||||
import { ApiFilterMastodon } from './endpoints/filter.js';
|
||||
import { ApiTimelineMastodon } from './endpoints/timeline.js';
|
||||
import { ApiStatusMastodon } from './endpoints/status.js';
|
||||
|
||||
export {
|
||||
ApiAccountMastodon,
|
||||
ApiAuthMastodon,
|
||||
ApiSearchMastodon,
|
||||
ApiNotifyMastodon,
|
||||
ApiFilterMastodon,
|
||||
ApiTimelineMastodon,
|
||||
ApiStatusMastodon,
|
||||
};
|
|
@ -3,14 +3,18 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { parseTimelineArgs, TimelineArgs } from '@/server/api/mastodon/timelineArgs.js';
|
||||
import { MiLocalUser } from '@/models/User.js';
|
||||
import { MastoConverters, convertRelationship } from '../converters.js';
|
||||
import type { MegalodonInterface } from 'megalodon';
|
||||
import type { FastifyRequest } from 'fastify';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { parseTimelineArgs, TimelineArgs, toBoolean } from '@/server/api/mastodon/argsUtils.js';
|
||||
import { getErrorData, MastodonLogger } from '@/server/api/mastodon/MastodonLogger.js';
|
||||
import { MastodonClientService } from '@/server/api/mastodon/MastodonClientService.js';
|
||||
import { DriveService } from '@/core/DriveService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { AccessTokensRepository, UserProfilesRepository } from '@/models/_.js';
|
||||
import { MastoConverters, convertRelationship, convertFeaturedTag, convertList } from '../converters.js';
|
||||
import type multer from 'fastify-multer';
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
|
||||
export interface ApiAccountMastodonRoute {
|
||||
interface ApiAccountMastodonRoute {
|
||||
Params: { id?: string },
|
||||
Querystring: TimelineArgs & { acct?: string },
|
||||
Body: { notifications?: boolean }
|
||||
|
@ -19,17 +23,27 @@ export interface ApiAccountMastodonRoute {
|
|||
@Injectable()
|
||||
export class ApiAccountMastodon {
|
||||
constructor(
|
||||
private readonly request: FastifyRequest<ApiAccountMastodonRoute>,
|
||||
private readonly client: MegalodonInterface,
|
||||
private readonly me: MiLocalUser | null,
|
||||
@Inject(DI.userProfilesRepository)
|
||||
private readonly userProfilesRepository: UserProfilesRepository,
|
||||
|
||||
@Inject(DI.accessTokensRepository)
|
||||
private readonly accessTokensRepository: AccessTokensRepository,
|
||||
|
||||
private readonly clientService: MastodonClientService,
|
||||
private readonly mastoConverters: MastoConverters,
|
||||
private readonly logger: MastodonLogger,
|
||||
private readonly driveService: DriveService,
|
||||
) {}
|
||||
|
||||
public async verifyCredentials() {
|
||||
const data = await this.client.verifyAccountCredentials();
|
||||
public register(fastify: FastifyInstance, upload: ReturnType<typeof multer>): void {
|
||||
fastify.get<ApiAccountMastodonRoute>('/v1/accounts/verify_credentials', async (_request, reply) => {
|
||||
try {
|
||||
const client = await this.clientService.getClient(_request);
|
||||
const data = await client.verifyAccountCredentials();
|
||||
const acct = await this.mastoConverters.convertAccount(data.data);
|
||||
return Object.assign({}, acct, {
|
||||
const response = Object.assign({}, acct, {
|
||||
source: {
|
||||
// TODO move these into the convertAccount logic directly
|
||||
note: acct.note,
|
||||
fields: acct.fields,
|
||||
privacy: '',
|
||||
|
@ -37,115 +51,347 @@ export class ApiAccountMastodon {
|
|||
language: '',
|
||||
},
|
||||
});
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('GET /v1/accounts/verify_credentials', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.patch<{
|
||||
Body: {
|
||||
discoverable?: string,
|
||||
bot?: string,
|
||||
display_name?: string,
|
||||
note?: string,
|
||||
avatar?: string,
|
||||
header?: string,
|
||||
locked?: string,
|
||||
source?: {
|
||||
privacy?: string,
|
||||
sensitive?: string,
|
||||
language?: string,
|
||||
},
|
||||
fields_attributes?: {
|
||||
name: string,
|
||||
value: string,
|
||||
}[],
|
||||
},
|
||||
}>('/v1/accounts/update_credentials', { preHandler: upload.any() }, async (_request, reply) => {
|
||||
const accessTokens = _request.headers.authorization;
|
||||
try {
|
||||
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.
|
||||
if (_request.files.length > 0 && accessTokens) {
|
||||
const tokeninfo = await this.accessTokensRepository.findOneBy({ token: accessTokens.replace('Bearer ', '') });
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const avatar = (_request.files as any).find((obj: any) => {
|
||||
return obj.fieldname === 'avatar';
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const header = (_request.files as any).find((obj: any) => {
|
||||
return obj.fieldname === 'header';
|
||||
});
|
||||
|
||||
if (tokeninfo && avatar) {
|
||||
const upload = await this.driveService.addFile({
|
||||
user: { id: tokeninfo.userId, host: null },
|
||||
path: avatar.path,
|
||||
name: avatar.originalname !== null && avatar.originalname !== 'file' ? avatar.originalname : undefined,
|
||||
sensitive: false,
|
||||
});
|
||||
if (upload.type.startsWith('image/')) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(_request.body as any).avatar = upload.id;
|
||||
}
|
||||
} else if (tokeninfo && header) {
|
||||
const upload = await this.driveService.addFile({
|
||||
user: { id: tokeninfo.userId, host: null },
|
||||
path: header.path,
|
||||
name: header.originalname !== null && header.originalname !== 'file' ? header.originalname : undefined,
|
||||
sensitive: false,
|
||||
});
|
||||
if (upload.type.startsWith('image/')) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(_request.body as any).header = upload.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async lookup() {
|
||||
if (!this.request.query.acct) throw new Error('Missing required property "acct"');
|
||||
const data = await this.client.search(this.request.query.acct, { type: 'accounts' });
|
||||
return this.mastoConverters.convertAccount(data.data.accounts[0]);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if ((_request.body as any).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() === '') 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');
|
||||
}
|
||||
return {
|
||||
...field,
|
||||
};
|
||||
});
|
||||
// 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);
|
||||
}
|
||||
|
||||
public async getRelationships(reqIds: string[]) {
|
||||
const data = await this.client.getRelationships(reqIds);
|
||||
return data.data.map(relationship => convertRelationship(relationship));
|
||||
const options = {
|
||||
..._request.body,
|
||||
discoverable: toBoolean(_request.body.discoverable),
|
||||
bot: toBoolean(_request.body.bot),
|
||||
locked: toBoolean(_request.body.locked),
|
||||
source: _request.body.source ? {
|
||||
..._request.body.source,
|
||||
sensitive: toBoolean(_request.body.source.sensitive),
|
||||
} : undefined,
|
||||
};
|
||||
const data = await client.updateCredentials(options);
|
||||
reply.send(await this.mastoConverters.convertAccount(data.data));
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('PATCH /v1/accounts/update_credentials', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.get<{ Querystring: { acct?: string }}>('/v1/accounts/lookup', async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.query.acct) return reply.code(400).send({ error: 'Missing required property "acct"' });
|
||||
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.search(_request.query.acct, { type: 'accounts' });
|
||||
const profile = await this.userProfilesRepository.findOneBy({ userId: data.data.accounts[0].id });
|
||||
data.data.accounts[0].fields = profile?.fields.map(f => ({ ...f, verified_at: null })) ?? [];
|
||||
const response = await this.mastoConverters.convertAccount(data.data.accounts[0]);
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('GET /v1/accounts/lookup', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.get<ApiAccountMastodonRoute & { Querystring: { id?: string | string[], 'id[]'?: string | string[] }}>('/v1/accounts/relationships', async (_request, reply) => {
|
||||
try {
|
||||
let ids = _request.query['id[]'] ?? _request.query['id'] ?? [];
|
||||
if (typeof ids === 'string') {
|
||||
ids = [ids];
|
||||
}
|
||||
|
||||
public async getStatuses() {
|
||||
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||
const data = await this.client.getAccountStatuses(this.request.params.id, parseTimelineArgs(this.request.query));
|
||||
return await Promise.all(data.data.map(async (status) => await this.mastoConverters.convertStatus(status, this.me)));
|
||||
}
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.getRelationships(ids);
|
||||
const response = data.data.map(relationship => convertRelationship(relationship));
|
||||
|
||||
public async getFollowers() {
|
||||
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||
const data = await this.client.getAccountFollowers(
|
||||
this.request.params.id,
|
||||
parseTimelineArgs(this.request.query),
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('GET /v1/accounts/relationships', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.get<{ Params: { id?: string } }>('/v1/accounts/:id', async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.getAccount(_request.params.id);
|
||||
const account = await this.mastoConverters.convertAccount(data.data);
|
||||
|
||||
reply.send(account);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/accounts/${_request.params.id}`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.get<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/statuses', async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
|
||||
const { client, me } = await this.clientService.getAuthClient(_request);
|
||||
const data = await client.getAccountStatuses(_request.params.id, parseTimelineArgs(_request.query));
|
||||
const response = await Promise.all(data.data.map(async (status) => await this.mastoConverters.convertStatus(status, me)));
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/accounts/${_request.params.id}/statuses`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.get<{ Params: { id?: string } }>('/v1/accounts/:id/featured_tags', async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.getFeaturedTags();
|
||||
const response = data.data.map((tag) => convertFeaturedTag(tag));
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/accounts/${_request.params.id}/featured_tags`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.get<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/followers', async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.getAccountFollowers(
|
||||
_request.params.id,
|
||||
parseTimelineArgs(_request.query),
|
||||
);
|
||||
return 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)));
|
||||
|
||||
public async getFollowing() {
|
||||
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||
const data = await this.client.getAccountFollowing(
|
||||
this.request.params.id,
|
||||
parseTimelineArgs(this.request.query),
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/accounts/${_request.params.id}/followers`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.get<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/following', async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.getAccountFollowing(
|
||||
_request.params.id,
|
||||
parseTimelineArgs(_request.query),
|
||||
);
|
||||
return 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)));
|
||||
|
||||
public async addFollow() {
|
||||
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||
const data = await this.client.followAccount(this.request.params.id);
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/accounts/${_request.params.id}/following`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.get<{ Params: { id?: string } }>('/v1/accounts/:id/lists', async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.getAccountLists(_request.params.id);
|
||||
const response = data.data.map((list) => convertList(list));
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/accounts/${_request.params.id}/lists`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/follow', { preHandler: upload.single('none') }, async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.followAccount(_request.params.id);
|
||||
const acct = convertRelationship(data.data);
|
||||
acct.following = true;
|
||||
return acct;
|
||||
}
|
||||
|
||||
public async rmFollow() {
|
||||
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||
const data = await this.client.unfollowAccount(this.request.params.id);
|
||||
reply.send(acct);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/accounts/${_request.params.id}/follow`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/unfollow', { preHandler: upload.single('none') }, async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.unfollowAccount(_request.params.id);
|
||||
const acct = convertRelationship(data.data);
|
||||
acct.following = false;
|
||||
return acct;
|
||||
}
|
||||
|
||||
public async addBlock() {
|
||||
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||
const data = await this.client.blockAccount(this.request.params.id);
|
||||
return convertRelationship(data.data);
|
||||
reply.send(acct);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/accounts/${_request.params.id}/unfollow`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
public async rmBlock() {
|
||||
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||
const data = await this.client.unblockAccount(this.request.params.id);
|
||||
return convertRelationship(data.data);
|
||||
fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/block', { preHandler: upload.single('none') }, async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.blockAccount(_request.params.id);
|
||||
const response = convertRelationship(data.data);
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/accounts/${_request.params.id}/block`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
public async addMute() {
|
||||
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||
const data = await this.client.muteAccount(
|
||||
this.request.params.id,
|
||||
this.request.body.notifications ?? true,
|
||||
fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/unblock', { preHandler: upload.single('none') }, async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.unblockAccount(_request.params.id);
|
||||
const response = convertRelationship(data.data);
|
||||
|
||||
return reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/accounts/${_request.params.id}/unblock`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/mute', { preHandler: upload.single('none') }, async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.muteAccount(
|
||||
_request.params.id,
|
||||
_request.body.notifications ?? true,
|
||||
);
|
||||
return convertRelationship(data.data);
|
||||
}
|
||||
const response = convertRelationship(data.data);
|
||||
|
||||
public async rmMute() {
|
||||
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||
const data = await this.client.unmuteAccount(this.request.params.id);
|
||||
return convertRelationship(data.data);
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/accounts/${_request.params.id}/mute`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
public async getBookmarks() {
|
||||
const data = await this.client.getBookmarks(parseTimelineArgs(this.request.query));
|
||||
return Promise.all(data.data.map((status) => this.mastoConverters.convertStatus(status, this.me)));
|
||||
}
|
||||
fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/unmute', { preHandler: upload.single('none') }, async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
|
||||
public async getFavourites() {
|
||||
const data = await this.client.getFavourites(parseTimelineArgs(this.request.query));
|
||||
return Promise.all(data.data.map((status) => this.mastoConverters.convertStatus(status, this.me)));
|
||||
}
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.unmuteAccount(_request.params.id);
|
||||
const response = convertRelationship(data.data);
|
||||
|
||||
public async getMutes() {
|
||||
const data = await this.client.getMutes(parseTimelineArgs(this.request.query));
|
||||
return Promise.all(data.data.map((account) => this.mastoConverters.convertAccount(account)));
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/accounts/${_request.params.id}/unmute`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
|
||||
public async getBlocks() {
|
||||
const data = await this.client.getBlocks(parseTimelineArgs(this.request.query));
|
||||
return Promise.all(data.data.map((account) => this.mastoConverters.convertAccount(account)));
|
||||
}
|
||||
|
||||
public async acceptFollow() {
|
||||
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||
const data = await this.client.acceptFollowRequest(this.request.params.id);
|
||||
return convertRelationship(data.data);
|
||||
}
|
||||
|
||||
public async rejectFollow() {
|
||||
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||
const data = await this.client.rejectFollowRequest(this.request.params.id);
|
||||
return convertRelationship(data.data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
121
packages/backend/src/server/api/mastodon/endpoints/apps.ts
Normal file
121
packages/backend/src/server/api/mastodon/endpoints/apps.ts
Normal file
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: marie and other Sharkey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { getErrorData, MastodonLogger } from '@/server/api/mastodon/MastodonLogger.js';
|
||||
import { MastodonClientService } from '@/server/api/mastodon/MastodonClientService.js';
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import type multer from 'fastify-multer';
|
||||
|
||||
const readScope = [
|
||||
'read:account',
|
||||
'read:drive',
|
||||
'read:blocks',
|
||||
'read:favorites',
|
||||
'read:following',
|
||||
'read:messaging',
|
||||
'read:mutes',
|
||||
'read:notifications',
|
||||
'read:reactions',
|
||||
'read:pages',
|
||||
'read:page-likes',
|
||||
'read:user-groups',
|
||||
'read:channels',
|
||||
'read:gallery',
|
||||
'read:gallery-likes',
|
||||
];
|
||||
|
||||
const writeScope = [
|
||||
'write:account',
|
||||
'write:drive',
|
||||
'write:blocks',
|
||||
'write:favorites',
|
||||
'write:following',
|
||||
'write:messaging',
|
||||
'write:mutes',
|
||||
'write:notes',
|
||||
'write:notifications',
|
||||
'write:reactions',
|
||||
'write:votes',
|
||||
'write:pages',
|
||||
'write:page-likes',
|
||||
'write:user-groups',
|
||||
'write:channels',
|
||||
'write:gallery',
|
||||
'write:gallery-likes',
|
||||
];
|
||||
|
||||
export interface AuthPayload {
|
||||
scopes?: string | string[],
|
||||
redirect_uris?: string,
|
||||
client_name?: string,
|
||||
website?: string,
|
||||
}
|
||||
|
||||
// Not entirely right, but it gets TypeScript to work so *shrug*
|
||||
type AuthMastodonRoute = { Body?: AuthPayload, Querystring: AuthPayload };
|
||||
|
||||
@Injectable()
|
||||
export class ApiAppsMastodon {
|
||||
constructor(
|
||||
private readonly clientService: MastodonClientService,
|
||||
private readonly logger: MastodonLogger,
|
||||
) {}
|
||||
|
||||
public register(fastify: FastifyInstance, upload: ReturnType<typeof multer>): void {
|
||||
fastify.post<AuthMastodonRoute>('/v1/apps', { preHandler: upload.single('none') }, async (_request, reply) => {
|
||||
try {
|
||||
const body = _request.body ?? _request.query;
|
||||
if (!body.scopes) return reply.code(400).send({ error: 'Missing required payload "scopes"' });
|
||||
if (!body.redirect_uris) return reply.code(400).send({ error: 'Missing required payload "redirect_uris"' });
|
||||
if (!body.client_name) return reply.code(400).send({ error: 'Missing required payload "client_name"' });
|
||||
|
||||
let scope = body.scopes;
|
||||
if (typeof scope === 'string') {
|
||||
scope = scope.split(/[ +]/g);
|
||||
}
|
||||
|
||||
const pushScope = new Set<string>();
|
||||
for (const s of scope) {
|
||||
if (s.match(/^read/)) {
|
||||
for (const r of readScope) {
|
||||
pushScope.add(r);
|
||||
}
|
||||
}
|
||||
if (s.match(/^write/)) {
|
||||
for (const r of writeScope) {
|
||||
pushScope.add(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const red = body.redirect_uris;
|
||||
|
||||
const client = this.clientService.getClient(_request);
|
||||
const appData = await client.registerApp(body.client_name, {
|
||||
scopes: Array.from(pushScope),
|
||||
redirect_uris: red,
|
||||
website: body.website,
|
||||
});
|
||||
|
||||
const response = {
|
||||
id: Math.floor(Math.random() * 100).toString(),
|
||||
name: appData.name,
|
||||
website: body.website,
|
||||
redirect_uri: red,
|
||||
client_id: Buffer.from(appData.url || '').toString('base64'),
|
||||
client_secret: appData.clientSecret,
|
||||
};
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('GET /v1/apps', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: marie and other Sharkey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { MegalodonInterface } from 'megalodon';
|
||||
import type { FastifyRequest } from 'fastify';
|
||||
|
||||
const readScope = [
|
||||
'read:account',
|
||||
'read:drive',
|
||||
'read:blocks',
|
||||
'read:favorites',
|
||||
'read:following',
|
||||
'read:messaging',
|
||||
'read:mutes',
|
||||
'read:notifications',
|
||||
'read:reactions',
|
||||
'read:pages',
|
||||
'read:page-likes',
|
||||
'read:user-groups',
|
||||
'read:channels',
|
||||
'read:gallery',
|
||||
'read:gallery-likes',
|
||||
];
|
||||
|
||||
const writeScope = [
|
||||
'write:account',
|
||||
'write:drive',
|
||||
'write:blocks',
|
||||
'write:favorites',
|
||||
'write:following',
|
||||
'write:messaging',
|
||||
'write:mutes',
|
||||
'write:notes',
|
||||
'write:notifications',
|
||||
'write:reactions',
|
||||
'write:votes',
|
||||
'write:pages',
|
||||
'write:page-likes',
|
||||
'write:user-groups',
|
||||
'write:channels',
|
||||
'write:gallery',
|
||||
'write:gallery-likes',
|
||||
];
|
||||
|
||||
export interface AuthPayload {
|
||||
scopes?: string | string[],
|
||||
redirect_uris?: string,
|
||||
client_name?: string,
|
||||
website?: string,
|
||||
}
|
||||
|
||||
// Not entirely right, but it gets TypeScript to work so *shrug*
|
||||
export type AuthMastodonRoute = { Body?: AuthPayload, Querystring: AuthPayload };
|
||||
|
||||
export async function ApiAuthMastodon(request: FastifyRequest<AuthMastodonRoute>, client: MegalodonInterface) {
|
||||
const body = request.body ?? request.query;
|
||||
if (!body.scopes) throw new Error('Missing required payload "scopes"');
|
||||
if (!body.redirect_uris) throw new Error('Missing required payload "redirect_uris"');
|
||||
if (!body.client_name) throw new Error('Missing required payload "client_name"');
|
||||
|
||||
let scope = body.scopes;
|
||||
if (typeof scope === 'string') {
|
||||
scope = scope.split(/[ +]/g);
|
||||
}
|
||||
|
||||
const pushScope = new Set<string>();
|
||||
for (const s of scope) {
|
||||
if (s.match(/^read/)) {
|
||||
for (const r of readScope) {
|
||||
pushScope.add(r);
|
||||
}
|
||||
}
|
||||
if (s.match(/^write/)) {
|
||||
for (const r of writeScope) {
|
||||
pushScope.add(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const red = body.redirect_uris;
|
||||
const appData = await client.registerApp(body.client_name, {
|
||||
scopes: Array.from(pushScope),
|
||||
redirect_uris: red,
|
||||
website: body.website,
|
||||
});
|
||||
|
||||
return {
|
||||
id: Math.floor(Math.random() * 100).toString(),
|
||||
name: appData.name,
|
||||
website: body.website,
|
||||
redirect_uri: red,
|
||||
client_id: Buffer.from(appData.url || '').toString('base64'), // eslint-disable-line @typescript-eslint/prefer-nullish-coalescing
|
||||
client_secret: appData.clientSecret,
|
||||
};
|
||||
}
|
|
@ -3,12 +3,15 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { toBoolean } from '@/server/api/mastodon/timelineArgs.js';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { toBoolean } from '@/server/api/mastodon/argsUtils.js';
|
||||
import { MastodonClientService } from '@/server/api/mastodon/MastodonClientService.js';
|
||||
import { getErrorData, MastodonLogger } from '@/server/api/mastodon/MastodonLogger.js';
|
||||
import { convertFilter } from '../converters.js';
|
||||
import type { MegalodonInterface } from 'megalodon';
|
||||
import type { FastifyRequest } from 'fastify';
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import type multer from 'fastify-multer';
|
||||
|
||||
export interface ApiFilterMastodonRoute {
|
||||
interface ApiFilterMastodonRoute {
|
||||
Params: {
|
||||
id?: string,
|
||||
},
|
||||
|
@ -21,55 +24,109 @@ export interface ApiFilterMastodonRoute {
|
|||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ApiFilterMastodon {
|
||||
constructor(
|
||||
private readonly request: FastifyRequest<ApiFilterMastodonRoute>,
|
||||
private readonly client: MegalodonInterface,
|
||||
private readonly clientService: MastodonClientService,
|
||||
private readonly logger: MastodonLogger,
|
||||
) {}
|
||||
|
||||
public async getFilters() {
|
||||
const data = await this.client.getFilters();
|
||||
return data.data.map((filter) => convertFilter(filter));
|
||||
}
|
||||
public register(fastify: FastifyInstance, upload: ReturnType<typeof multer>): void {
|
||||
fastify.get('/v1/filters', async (_request, reply) => {
|
||||
try {
|
||||
const client = this.clientService.getClient(_request);
|
||||
|
||||
public async getFilter() {
|
||||
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||
const data = await this.client.getFilter(this.request.params.id);
|
||||
return convertFilter(data.data);
|
||||
}
|
||||
const data = await client.getFilters();
|
||||
const response = data.data.map((filter) => convertFilter(filter));
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('GET /v1/filters', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.get<ApiFilterMastodonRoute & { Params: { id?: string } }>('/v1/filters/:id', async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.getFilter(_request.params.id);
|
||||
const response = convertFilter(data.data);
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/filters/${_request.params.id}`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.post<ApiFilterMastodonRoute>('/v1/filters', { preHandler: upload.single('none') }, async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.body.phrase) return reply.code(400).send({ error: 'Missing required payload "phrase"' });
|
||||
if (!_request.body.context) return reply.code(400).send({ error: 'Missing required payload "context"' });
|
||||
|
||||
public async createFilter() {
|
||||
if (!this.request.body.phrase) throw new Error('Missing required payload "phrase"');
|
||||
if (!this.request.body.context) throw new Error('Missing required payload "context"');
|
||||
const options = {
|
||||
phrase: this.request.body.phrase,
|
||||
context: this.request.body.context,
|
||||
irreversible: toBoolean(this.request.body.irreversible),
|
||||
whole_word: toBoolean(this.request.body.whole_word),
|
||||
expires_in: this.request.body.expires_in,
|
||||
phrase: _request.body.phrase,
|
||||
context: _request.body.context,
|
||||
irreversible: toBoolean(_request.body.irreversible),
|
||||
whole_word: toBoolean(_request.body.whole_word),
|
||||
expires_in: _request.body.expires_in,
|
||||
};
|
||||
const data = await this.client.createFilter(this.request.body.phrase, this.request.body.context, options);
|
||||
return convertFilter(data.data);
|
||||
}
|
||||
|
||||
public async updateFilter() {
|
||||
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||
if (!this.request.body.phrase) throw new Error('Missing required payload "phrase"');
|
||||
if (!this.request.body.context) throw new Error('Missing required payload "context"');
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.createFilter(_request.body.phrase, _request.body.context, options);
|
||||
const response = convertFilter(data.data);
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('POST /v1/filters', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.post<ApiFilterMastodonRoute & { Params: { id?: string } }>('/v1/filters/:id', { preHandler: upload.single('none') }, async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
if (!_request.body.phrase) return reply.code(400).send({ error: 'Missing required payload "phrase"' });
|
||||
if (!_request.body.context) return reply.code(400).send({ error: 'Missing required payload "context"' });
|
||||
|
||||
const options = {
|
||||
phrase: this.request.body.phrase,
|
||||
context: this.request.body.context,
|
||||
irreversible: toBoolean(this.request.body.irreversible),
|
||||
whole_word: toBoolean(this.request.body.whole_word),
|
||||
expires_in: this.request.body.expires_in,
|
||||
phrase: _request.body.phrase,
|
||||
context: _request.body.context,
|
||||
irreversible: toBoolean(_request.body.irreversible),
|
||||
whole_word: toBoolean(_request.body.whole_word),
|
||||
expires_in: _request.body.expires_in,
|
||||
};
|
||||
const data = await this.client.updateFilter(this.request.params.id, this.request.body.phrase, this.request.body.context, options);
|
||||
return convertFilter(data.data);
|
||||
}
|
||||
|
||||
public async rmFilter() {
|
||||
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||
const data = await this.client.deleteFilter(this.request.params.id);
|
||||
return data.data;
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.updateFilter(_request.params.id, _request.body.phrase, _request.body.context, options);
|
||||
const response = convertFilter(data.data);
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/filters/${_request.params.id}`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.delete<ApiFilterMastodonRoute & { Params: { id?: string } }>('/v1/filters/:id', async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.deleteFilter(_request.params.id);
|
||||
|
||||
reply.send(data.data);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`DELETE /v1/filters/${_request.params.id}`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
110
packages/backend/src/server/api/mastodon/endpoints/instance.ts
Normal file
110
packages/backend/src/server/api/mastodon/endpoints/instance.ts
Normal file
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: marie and other Sharkey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { IsNull } from 'typeorm';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { MiMeta, UsersRepository } from '@/models/_.js';
|
||||
import { MastoConverters } from '@/server/api/mastodon/converters.js';
|
||||
import { getErrorData, MastodonLogger } from '@/server/api/mastodon/MastodonLogger.js';
|
||||
import { MastodonClientService } from '@/server/api/mastodon/MastodonClientService.js';
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
|
||||
// TODO rename to ApiInstanceMastodon
|
||||
|
||||
@Injectable()
|
||||
export class ApiInstanceMastodon {
|
||||
constructor(
|
||||
@Inject(DI.meta)
|
||||
private readonly meta: MiMeta,
|
||||
|
||||
@Inject(DI.usersRepository)
|
||||
private readonly usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.config)
|
||||
private readonly config: Config,
|
||||
|
||||
private readonly mastoConverters: MastoConverters,
|
||||
private readonly logger: MastodonLogger,
|
||||
private readonly clientService: MastodonClientService,
|
||||
) {}
|
||||
|
||||
public register(fastify: FastifyInstance): void {
|
||||
fastify.get('/v1/instance', async (_request, reply) => {
|
||||
try {
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.getInstance();
|
||||
const instance = data.data;
|
||||
const admin = await this.usersRepository.findOne({
|
||||
where: {
|
||||
host: IsNull(),
|
||||
isRoot: true,
|
||||
isDeleted: false,
|
||||
isSuspended: false,
|
||||
},
|
||||
order: { id: 'ASC' },
|
||||
});
|
||||
const contact = admin == null ? null : await this.mastoConverters.convertAccount((await client.getAccount(admin.id)).data);
|
||||
|
||||
const response = {
|
||||
uri: this.config.url,
|
||||
title: this.meta.name || 'Sharkey',
|
||||
short_description: this.meta.description || 'This is a vanilla Sharkey Instance. It doesn\'t seem to have a description.',
|
||||
description: this.meta.description || 'This is a vanilla Sharkey Instance. It doesn\'t seem to have a description.',
|
||||
email: instance.email || '',
|
||||
version: `3.0.0 (compatible; Sharkey ${this.config.version})`,
|
||||
urls: instance.urls,
|
||||
stats: {
|
||||
user_count: instance.stats.user_count,
|
||||
status_count: instance.stats.status_count,
|
||||
domain_count: instance.stats.domain_count,
|
||||
},
|
||||
thumbnail: this.meta.backgroundImageUrl || '/static-assets/transparent.png',
|
||||
languages: this.meta.langs,
|
||||
registrations: !this.meta.disableRegistration || instance.registrations,
|
||||
approval_required: this.meta.approvalRequiredForSignup,
|
||||
invites_enabled: instance.registrations,
|
||||
configuration: {
|
||||
accounts: {
|
||||
max_featured_tags: 20,
|
||||
},
|
||||
statuses: {
|
||||
max_characters: this.config.maxNoteLength,
|
||||
max_media_attachments: 16,
|
||||
characters_reserved_per_url: instance.uri.length,
|
||||
},
|
||||
media_attachments: {
|
||||
supported_mime_types: FILE_TYPE_BROWSERSAFE,
|
||||
image_size_limit: 10485760,
|
||||
image_matrix_limit: 16777216,
|
||||
video_size_limit: 41943040,
|
||||
video_frame_rate_limit: 60,
|
||||
video_matrix_limit: 2304000,
|
||||
},
|
||||
polls: {
|
||||
max_options: 10,
|
||||
max_characters_per_option: 150,
|
||||
min_expiration: 50,
|
||||
max_expiration: 2629746,
|
||||
},
|
||||
reactions: {
|
||||
max_reactions: 1,
|
||||
},
|
||||
},
|
||||
contact_account: contact,
|
||||
rules: [],
|
||||
};
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('GET /v1/instance', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: marie and other Sharkey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Entity } from 'megalodon';
|
||||
import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { MiMeta } from '@/models/Meta.js';
|
||||
|
||||
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
|
||||
export async function getInstance(
|
||||
response: Entity.Instance,
|
||||
contact: Entity.Account,
|
||||
config: Config,
|
||||
meta: MiMeta,
|
||||
) {
|
||||
return {
|
||||
uri: config.url,
|
||||
title: meta.name || 'Sharkey',
|
||||
short_description: meta.description || 'This is a vanilla Sharkey Instance. It doesn\'t seem to have a description.',
|
||||
description: meta.description || 'This is a vanilla Sharkey Instance. It doesn\'t seem to have a description.',
|
||||
email: response.email || '',
|
||||
version: `3.0.0 (compatible; Sharkey ${config.version})`,
|
||||
urls: response.urls,
|
||||
stats: {
|
||||
user_count: response.stats.user_count,
|
||||
status_count: response.stats.status_count,
|
||||
domain_count: response.stats.domain_count,
|
||||
},
|
||||
thumbnail: meta.backgroundImageUrl || '/static-assets/transparent.png',
|
||||
languages: meta.langs,
|
||||
registrations: !meta.disableRegistration || response.registrations,
|
||||
approval_required: meta.approvalRequiredForSignup,
|
||||
invites_enabled: response.registrations,
|
||||
configuration: {
|
||||
accounts: {
|
||||
max_featured_tags: 20,
|
||||
},
|
||||
statuses: {
|
||||
max_characters: config.maxNoteLength,
|
||||
max_media_attachments: 16,
|
||||
characters_reserved_per_url: response.uri.length,
|
||||
},
|
||||
media_attachments: {
|
||||
supported_mime_types: FILE_TYPE_BROWSERSAFE,
|
||||
image_size_limit: 10485760,
|
||||
image_matrix_limit: 16777216,
|
||||
video_size_limit: 41943040,
|
||||
video_frame_rate_limit: 60,
|
||||
video_matrix_limit: 2304000,
|
||||
},
|
||||
polls: {
|
||||
max_options: 10,
|
||||
max_characters_per_option: 150,
|
||||
min_expiration: 50,
|
||||
max_expiration: 2629746,
|
||||
},
|
||||
reactions: {
|
||||
max_reactions: 1,
|
||||
},
|
||||
},
|
||||
contact_account: contact,
|
||||
rules: [],
|
||||
};
|
||||
}
|
|
@ -3,56 +3,95 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { parseTimelineArgs, TimelineArgs } from '@/server/api/mastodon/timelineArgs.js';
|
||||
import { MiLocalUser } from '@/models/User.js';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { parseTimelineArgs, TimelineArgs } from '@/server/api/mastodon/argsUtils.js';
|
||||
import { MastoConverters } from '@/server/api/mastodon/converters.js';
|
||||
import type { MegalodonInterface } from 'megalodon';
|
||||
import type { FastifyRequest } from 'fastify';
|
||||
import { getErrorData, MastodonLogger } from '@/server/api/mastodon/MastodonLogger.js';
|
||||
import { MastodonClientService } from '../MastodonClientService.js';
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import type multer from 'fastify-multer';
|
||||
|
||||
export interface ApiNotifyMastodonRoute {
|
||||
interface ApiNotifyMastodonRoute {
|
||||
Params: {
|
||||
id?: string,
|
||||
},
|
||||
Querystring: TimelineArgs,
|
||||
}
|
||||
|
||||
export class ApiNotifyMastodon {
|
||||
@Injectable()
|
||||
export class ApiNotificationsMastodon {
|
||||
constructor(
|
||||
private readonly request: FastifyRequest<ApiNotifyMastodonRoute>,
|
||||
private readonly client: MegalodonInterface,
|
||||
private readonly me: MiLocalUser | null,
|
||||
private readonly mastoConverters: MastoConverters,
|
||||
private readonly clientService: MastodonClientService,
|
||||
private readonly logger: MastodonLogger,
|
||||
) {}
|
||||
|
||||
public async getNotifications() {
|
||||
const data = await this.client.getNotifications(parseTimelineArgs(this.request.query));
|
||||
return Promise.all(data.data.map(async n => {
|
||||
const converted = await this.mastoConverters.convertNotification(n, this.me);
|
||||
public register(fastify: FastifyInstance, upload: ReturnType<typeof multer>): void {
|
||||
fastify.get<ApiNotifyMastodonRoute>('/v1/notifications', async (_request, reply) => {
|
||||
try {
|
||||
const { client, me } = await this.clientService.getAuthClient(_request);
|
||||
const data = await client.getNotifications(parseTimelineArgs(_request.query));
|
||||
const response = Promise.all(data.data.map(async n => {
|
||||
const converted = await this.mastoConverters.convertNotification(n, me);
|
||||
if (converted.type === 'reaction') {
|
||||
converted.type = 'favourite';
|
||||
}
|
||||
return converted;
|
||||
}));
|
||||
}
|
||||
|
||||
public async getNotification() {
|
||||
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||
const data = await this.client.getNotification(this.request.params.id);
|
||||
const converted = await this.mastoConverters.convertNotification(data.data, this.me);
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('GET /v1/notifications', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.get<ApiNotifyMastodonRoute & { Params: { id?: string } }>('/v1/notification/:id', async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
|
||||
const { client, me } = await this.clientService.getAuthClient(_request);
|
||||
const data = await client.getNotification(_request.params.id);
|
||||
const converted = await this.mastoConverters.convertNotification(data.data, me);
|
||||
if (converted.type === 'reaction') {
|
||||
converted.type = 'favourite';
|
||||
}
|
||||
return converted;
|
||||
}
|
||||
|
||||
public async rmNotification() {
|
||||
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||
const data = await this.client.dismissNotification(this.request.params.id);
|
||||
return data.data;
|
||||
reply.send(converted);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/notification/${_request.params.id}`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
public async rmNotifications() {
|
||||
const data = await this.client.dismissNotifications();
|
||||
return data.data;
|
||||
fastify.post<ApiNotifyMastodonRoute & { Params: { id?: string } }>('/v1/notification/:id/dismiss', { preHandler: upload.single('none') }, async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.dismissNotification(_request.params.id);
|
||||
|
||||
reply.send(data.data);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/notification/${_request.params.id}/dismiss`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.post<ApiNotifyMastodonRoute>('/v1/notifications/clear', { preHandler: upload.single('none') }, async (_request, reply) => {
|
||||
try {
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.dismissNotifications();
|
||||
|
||||
reply.send(data.data);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('POST /v1/notifications/clear', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,92 +3,128 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { MiLocalUser } from '@/models/User.js';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { MastodonClientService } from '@/server/api/mastodon/MastodonClientService.js';
|
||||
import { getErrorData, MastodonLogger } from '@/server/api/mastodon/MastodonLogger.js';
|
||||
import { MastoConverters } from '../converters.js';
|
||||
import { parseTimelineArgs, TimelineArgs } from '../timelineArgs.js';
|
||||
import { parseTimelineArgs, TimelineArgs } from '../argsUtils.js';
|
||||
import Account = Entity.Account;
|
||||
import Status = Entity.Status;
|
||||
import type { MegalodonInterface } from 'megalodon';
|
||||
import type { FastifyRequest } from 'fastify';
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
|
||||
export interface ApiSearchMastodonRoute {
|
||||
interface ApiSearchMastodonRoute {
|
||||
Querystring: TimelineArgs & {
|
||||
type?: 'accounts' | 'hashtags' | 'statuses';
|
||||
q?: string;
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ApiSearchMastodon {
|
||||
constructor(
|
||||
private readonly request: FastifyRequest<ApiSearchMastodonRoute>,
|
||||
private readonly client: MegalodonInterface,
|
||||
private readonly me: MiLocalUser | null,
|
||||
private readonly BASE_URL: string,
|
||||
private readonly mastoConverters: MastoConverters,
|
||||
private readonly clientService: MastodonClientService,
|
||||
private readonly logger: MastodonLogger,
|
||||
) {}
|
||||
|
||||
public async SearchV1() {
|
||||
if (!this.request.query.q) throw new Error('Missing required property "q"');
|
||||
const query = parseTimelineArgs(this.request.query);
|
||||
const data = await this.client.search(this.request.query.q, { type: this.request.query.type, ...query });
|
||||
return data.data;
|
||||
}
|
||||
public register(fastify: FastifyInstance): void {
|
||||
fastify.get<ApiSearchMastodonRoute>('/v1/search', async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.query.q) return reply.code(400).send({ error: 'Missing required property "q"' });
|
||||
|
||||
public async SearchV2() {
|
||||
if (!this.request.query.q) throw new Error('Missing required property "q"');
|
||||
const query = parseTimelineArgs(this.request.query);
|
||||
const type = this.request.query.type;
|
||||
const acct = !type || type === 'accounts' ? await this.client.search(this.request.query.q, { type: 'accounts', ...query }) : null;
|
||||
const stat = !type || type === 'statuses' ? await this.client.search(this.request.query.q, { type: 'statuses', ...query }) : null;
|
||||
const tags = !type || type === 'hashtags' ? await this.client.search(this.request.query.q, { type: 'hashtags', ...query }) : null;
|
||||
return {
|
||||
const query = parseTimelineArgs(_request.query);
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.search(_request.query.q, { type: _request.query.type, ...query });
|
||||
|
||||
reply.send(data.data);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('GET /v1/search', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.get<ApiSearchMastodonRoute>('/v2/search', async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.query.q) return reply.code(400).send({ error: 'Missing required property "q"' });
|
||||
|
||||
const query = parseTimelineArgs(_request.query);
|
||||
const type = _request.query.type;
|
||||
const { client, me } = await this.clientService.getAuthClient(_request);
|
||||
const acct = !type || type === 'accounts' ? await client.search(_request.query.q, { type: 'accounts', ...query }) : null;
|
||||
const stat = !type || type === 'statuses' ? await client.search(_request.query.q, { type: 'statuses', ...query }) : null;
|
||||
const tags = !type || type === 'hashtags' ? await client.search(_request.query.q, { type: 'hashtags', ...query }) : null;
|
||||
const response = {
|
||||
accounts: await Promise.all(acct?.data.accounts.map(async (account: Account) => await this.mastoConverters.convertAccount(account)) ?? []),
|
||||
statuses: await Promise.all(stat?.data.statuses.map(async (status: Status) => await this.mastoConverters.convertStatus(status, this.me)) ?? []),
|
||||
statuses: await Promise.all(stat?.data.statuses.map(async (status: Status) => await this.mastoConverters.convertStatus(status, me)) ?? []),
|
||||
hashtags: tags?.data.hashtags ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
public async getStatusTrends() {
|
||||
const data = await fetch(`${this.BASE_URL}/api/notes/featured`,
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('GET /v2/search', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.get<ApiSearchMastodonRoute>('/v1/trends/statuses', async (_request, reply) => {
|
||||
try {
|
||||
const baseUrl = this.clientService.getBaseUrl(_request);
|
||||
const res = await fetch(`${baseUrl}/api/notes/featured`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
..._request.headers as HeadersInit,
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: '{}',
|
||||
});
|
||||
const data = await res.json() as Status[];
|
||||
const me = await this.clientService.getAuth(_request);
|
||||
const response = await Promise.all(data.map(status => this.mastoConverters.convertStatus(status, me)));
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('GET /v1/trends/statuses', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
|
||||
fastify.get<ApiSearchMastodonRoute>('/v2/suggestions', async (_request, reply) => {
|
||||
try {
|
||||
const baseUrl = this.clientService.getBaseUrl(_request);
|
||||
const res = await fetch(`${baseUrl}/api/users`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
..._request.headers as HeadersInit,
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
i: this.request.headers.authorization?.replace('Bearer ', ''),
|
||||
}),
|
||||
})
|
||||
.then(res => res.json() as Promise<Status[]>)
|
||||
.then(data => data.map(status => this.mastoConverters.convertStatus(status, this.me)));
|
||||
return Promise.all(data);
|
||||
}
|
||||
|
||||
public async getSuggestions() {
|
||||
const data = await fetch(`${this.BASE_URL}/api/users`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
i: this.request.headers.authorization?.replace('Bearer ', ''),
|
||||
limit: parseTimelineArgs(this.request.query).limit ?? 20,
|
||||
limit: parseTimelineArgs(_request.query).limit ?? 20,
|
||||
origin: 'local',
|
||||
sort: '+follower',
|
||||
state: 'alive',
|
||||
}),
|
||||
})
|
||||
.then(res => res.json() as Promise<Account[]>)
|
||||
.then(data => data.map((entry => ({
|
||||
});
|
||||
const data = await res.json() as Account[];
|
||||
const response = await Promise.all(data.map(async entry => {
|
||||
return {
|
||||
source: 'global',
|
||||
account: entry,
|
||||
}))));
|
||||
return Promise.all(data.map(async suggestion => {
|
||||
suggestion.account = await this.mastoConverters.convertAccount(suggestion.account);
|
||||
return suggestion;
|
||||
account: await this.mastoConverters.convertAccount(entry),
|
||||
};
|
||||
}));
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('GET /v2/suggestions', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,12 +4,12 @@
|
|||
*/
|
||||
|
||||
import querystring, { ParsedUrlQueryInput } from 'querystring';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { emojiRegexAtStartToEnd } from '@/misc/emoji-regex.js';
|
||||
import { getErrorData, MastodonLogger } from '@/server/api/mastodon/MastodonLogger.js';
|
||||
import { parseTimelineArgs, TimelineArgs, toBoolean, toInt } from '@/server/api/mastodon/timelineArgs.js';
|
||||
import { AuthenticateService } from '@/server/api/AuthenticateService.js';
|
||||
import { parseTimelineArgs, TimelineArgs, toBoolean, toInt } from '@/server/api/mastodon/argsUtils.js';
|
||||
import { MastodonClientService } from '@/server/api/mastodon/MastodonClientService.js';
|
||||
import { convertAttachment, convertPoll, MastoConverters } from '../converters.js';
|
||||
import { getAccessToken, getClient, MastodonApiServerService } from '../MastodonApiServerService.js';
|
||||
import type { Entity } from 'megalodon';
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
|
||||
|
@ -18,38 +18,38 @@ function normalizeQuery(data: Record<string, unknown>) {
|
|||
return querystring.parse(str);
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ApiStatusMastodon {
|
||||
constructor(
|
||||
private readonly fastify: FastifyInstance,
|
||||
private readonly mastoConverters: MastoConverters,
|
||||
private readonly logger: MastodonLogger,
|
||||
private readonly authenticateService: AuthenticateService,
|
||||
private readonly mastodon: MastodonApiServerService,
|
||||
private readonly clientService: MastodonClientService,
|
||||
) {}
|
||||
|
||||
public getStatus() {
|
||||
this.fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id', async (_request, reply) => {
|
||||
public register(fastify: FastifyInstance): void {
|
||||
fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id', async (_request, reply) => {
|
||||
try {
|
||||
const { client, me } = await this.mastodon.getAuthClient(_request);
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
|
||||
const { client, me } = await this.clientService.getAuthClient(_request);
|
||||
const data = await client.getStatus(_request.params.id);
|
||||
reply.send(await this.mastoConverters.convertStatus(data.data, me));
|
||||
const response = await this.mastoConverters.convertStatus(data.data, me);
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/statuses/${_request.params.id}`, data);
|
||||
reply.code(_request.is404 ? 404 : 401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public getStatusSource() {
|
||||
this.fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/source', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/source', async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.getStatusSource(_request.params.id);
|
||||
|
||||
reply.send(data.data);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
|
@ -57,31 +57,32 @@ export class ApiStatusMastodon {
|
|||
reply.code(_request.is404 ? 404 : 401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public getContext() {
|
||||
this.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) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const { client, me } = await this.mastodon.getAuthClient(_request);
|
||||
|
||||
const { client, me } = await this.clientService.getAuthClient(_request);
|
||||
const { data } = await client.getStatusContext(_request.params.id, parseTimelineArgs(_request.query));
|
||||
const ancestors = await Promise.all(data.ancestors.map(async status => await this.mastoConverters.convertStatus(status, me)));
|
||||
const descendants = await Promise.all(data.descendants.map(async status => await this.mastoConverters.convertStatus(status, me)));
|
||||
reply.send({ ancestors, descendants });
|
||||
const response = { ancestors, descendants };
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/statuses/${_request.params.id}/context`, data);
|
||||
reply.code(_request.is404 ? 404 : 401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public getHistory() {
|
||||
this.fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/history', async (_request, reply) => {
|
||||
fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/history', async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const [user] = await this.authenticateService.authenticate(getAccessToken(_request.headers.authorization));
|
||||
|
||||
const user = await this.clientService.getAuth(_request);
|
||||
const edits = await this.mastoConverters.getEdits(_request.params.id, user);
|
||||
|
||||
reply.send(edits);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
|
@ -89,96 +90,89 @@ export class ApiStatusMastodon {
|
|||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public getReblogged() {
|
||||
this.fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/reblogged_by', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/reblogged_by', async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.getStatusRebloggedBy(_request.params.id);
|
||||
reply.send(await Promise.all(data.data.map(async (account: Entity.Account) => await this.mastoConverters.convertAccount(account))));
|
||||
const response = await Promise.all(data.data.map((account: Entity.Account) => this.mastoConverters.convertAccount(account)));
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/statuses/${_request.params.id}/reblogged_by`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public getFavourites() {
|
||||
this.fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/favourited_by', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/favourited_by', async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.getStatusFavouritedBy(_request.params.id);
|
||||
reply.send(await Promise.all(data.data.map(async (account: Entity.Account) => await this.mastoConverters.convertAccount(account))));
|
||||
const response = await Promise.all(data.data.map((account: Entity.Account) => this.mastoConverters.convertAccount(account)));
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/statuses/${_request.params.id}/favourited_by`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public getMedia() {
|
||||
this.fastify.get<{ Params: { id?: string } }>('/v1/media/:id', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
fastify.get<{ Params: { id?: string } }>('/v1/media/:id', async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.getMedia(_request.params.id);
|
||||
reply.send(convertAttachment(data.data));
|
||||
const response = convertAttachment(data.data);
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/media/${_request.params.id}`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public getPoll() {
|
||||
this.fastify.get<{ Params: { id?: string } }>('/v1/polls/:id', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
fastify.get<{ Params: { id?: string } }>('/v1/polls/:id', async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.getPoll(_request.params.id);
|
||||
reply.send(convertPoll(data.data));
|
||||
const response = convertPoll(data.data);
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/polls/${_request.params.id}`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public votePoll() {
|
||||
this.fastify.post<{ Params: { id?: string }, Body: { choices?: number[] } }>('/v1/polls/:id/votes', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
fastify.post<{ Params: { id?: string }, Body: { choices?: number[] } }>('/v1/polls/:id/votes', async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
if (!_request.body.choices) return reply.code(400).send({ error: 'Missing required payload "choices"' });
|
||||
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.votePoll(_request.params.id, _request.body.choices);
|
||||
reply.send(convertPoll(data.data));
|
||||
const response = convertPoll(data.data);
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/polls/${_request.params.id}/votes`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public postStatus() {
|
||||
this.fastify.post<{
|
||||
fastify.post<{
|
||||
Body: {
|
||||
media_ids?: string[],
|
||||
poll?: {
|
||||
|
@ -203,7 +197,7 @@ export class ApiStatusMastodon {
|
|||
}>('/v1/statuses', async (_request, reply) => {
|
||||
let body = _request.body;
|
||||
try {
|
||||
const { client, me } = await this.mastodon.getAuthClient(_request);
|
||||
const { client, me } = await this.clientService.getAuthClient(_request);
|
||||
if ((!body.poll && body['poll[options][]']) || (!body.media_ids && body['media_ids[]'])
|
||||
) {
|
||||
body = normalizeQuery(body);
|
||||
|
@ -248,17 +242,17 @@ export class ApiStatusMastodon {
|
|||
};
|
||||
|
||||
const data = await client.postStatus(text, options);
|
||||
reply.send(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);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('POST /v1/statuses', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public updateStatus() {
|
||||
this.fastify.put<{
|
||||
fastify.put<{
|
||||
Params: { id: string },
|
||||
Body: {
|
||||
status?: string,
|
||||
|
@ -274,7 +268,7 @@ export class ApiStatusMastodon {
|
|||
}
|
||||
}>('/v1/statuses/:id', async (_request, reply) => {
|
||||
try {
|
||||
const { client, me } = await this.mastodon.getAuthClient(_request);
|
||||
const { client, me } = await this.clientService.getAuthClient(_request);
|
||||
const body = _request.body;
|
||||
|
||||
if (!body.media_ids || !body.media_ids.length) {
|
||||
|
@ -293,175 +287,184 @@ export class ApiStatusMastodon {
|
|||
};
|
||||
|
||||
const data = await client.editStatus(_request.params.id, options);
|
||||
reply.send(await this.mastoConverters.convertStatus(data.data, me));
|
||||
const response = await this.mastoConverters.convertStatus(data.data, me);
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/statuses/${_request.params.id}`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public addFavourite() {
|
||||
this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/favourite', async (_request, reply) => {
|
||||
fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/favourite', async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const { client, me } = await this.mastodon.getAuthClient(_request);
|
||||
|
||||
const { client, me } = await this.clientService.getAuthClient(_request);
|
||||
const data = await client.createEmojiReaction(_request.params.id, '❤');
|
||||
reply.send(await this.mastoConverters.convertStatus(data.data, me));
|
||||
const response = await this.mastoConverters.convertStatus(data.data, me);
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/statuses/${_request.params.id}/favorite`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public rmFavourite() {
|
||||
this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unfavourite', async (_request, reply) => {
|
||||
fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unfavourite', async (_request, reply) => {
|
||||
try {
|
||||
const { client, me } = await this.mastodon.getAuthClient(_request);
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
|
||||
const { client, me } = await this.clientService.getAuthClient(_request);
|
||||
const data = await client.deleteEmojiReaction(_request.params.id, '❤');
|
||||
reply.send(await this.mastoConverters.convertStatus(data.data, me));
|
||||
const response = await this.mastoConverters.convertStatus(data.data, me);
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/statuses/${_request.params.id}/unfavorite`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public reblogStatus() {
|
||||
this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/reblog', async (_request, reply) => {
|
||||
fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/reblog', async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const { client, me } = await this.mastodon.getAuthClient(_request);
|
||||
|
||||
const { client, me } = await this.clientService.getAuthClient(_request);
|
||||
const data = await client.reblogStatus(_request.params.id);
|
||||
reply.send(await this.mastoConverters.convertStatus(data.data, me));
|
||||
const response = await this.mastoConverters.convertStatus(data.data, me);
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/statuses/${_request.params.id}/reblog`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public unreblogStatus() {
|
||||
this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unreblog', async (_request, reply) => {
|
||||
fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unreblog', async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const { client, me } = await this.mastodon.getAuthClient(_request);
|
||||
|
||||
const { client, me } = await this.clientService.getAuthClient(_request);
|
||||
const data = await client.unreblogStatus(_request.params.id);
|
||||
reply.send(await this.mastoConverters.convertStatus(data.data, me));
|
||||
const response = await this.mastoConverters.convertStatus(data.data, me);
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/statuses/${_request.params.id}/unreblog`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public bookmarkStatus() {
|
||||
this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/bookmark', async (_request, reply) => {
|
||||
fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/bookmark', async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const { client, me } = await this.mastodon.getAuthClient(_request);
|
||||
|
||||
const { client, me } = await this.clientService.getAuthClient(_request);
|
||||
const data = await client.bookmarkStatus(_request.params.id);
|
||||
reply.send(await this.mastoConverters.convertStatus(data.data, me));
|
||||
const response = await this.mastoConverters.convertStatus(data.data, me);
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/statuses/${_request.params.id}/bookmark`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public unbookmarkStatus() {
|
||||
this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unbookmark', async (_request, reply) => {
|
||||
fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unbookmark', async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const { client, me } = await this.mastodon.getAuthClient(_request);
|
||||
|
||||
const { client, me } = await this.clientService.getAuthClient(_request);
|
||||
const data = await client.unbookmarkStatus(_request.params.id);
|
||||
reply.send(await this.mastoConverters.convertStatus(data.data, me));
|
||||
const response = await this.mastoConverters.convertStatus(data.data, me);
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/statuses/${_request.params.id}/unbookmark`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public pinStatus() {
|
||||
this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/pin', async (_request, reply) => {
|
||||
fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/pin', async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const { client, me } = await this.mastodon.getAuthClient(_request);
|
||||
|
||||
const { client, me } = await this.clientService.getAuthClient(_request);
|
||||
const data = await client.pinStatus(_request.params.id);
|
||||
reply.send(await this.mastoConverters.convertStatus(data.data, me));
|
||||
const response = await this.mastoConverters.convertStatus(data.data, me);
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/statuses/${_request.params.id}/pin`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public unpinStatus() {
|
||||
this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unpin', async (_request, reply) => {
|
||||
fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unpin', async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const { client, me } = await this.mastodon.getAuthClient(_request);
|
||||
|
||||
const { client, me } = await this.clientService.getAuthClient(_request);
|
||||
const data = await client.unpinStatus(_request.params.id);
|
||||
reply.send(await this.mastoConverters.convertStatus(data.data, me));
|
||||
const response = await this.mastoConverters.convertStatus(data.data, me);
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/statuses/${_request.params.id}/unpin`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public reactStatus() {
|
||||
this.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) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
if (!_request.params.name) return reply.code(400).send({ error: 'Missing required parameter "name"' });
|
||||
const { client, me } = await this.mastodon.getAuthClient(_request);
|
||||
|
||||
const { client, me } = await this.clientService.getAuthClient(_request);
|
||||
const data = await client.createEmojiReaction(_request.params.id, _request.params.name);
|
||||
reply.send(await this.mastoConverters.convertStatus(data.data, me));
|
||||
const response = await this.mastoConverters.convertStatus(data.data, me);
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/statuses/${_request.params.id}/react/${_request.params.name}`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public unreactStatus() {
|
||||
this.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) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
if (!_request.params.name) return reply.code(400).send({ error: 'Missing required parameter "name"' });
|
||||
const { client, me } = await this.mastodon.getAuthClient(_request);
|
||||
|
||||
const { client, me } = await this.clientService.getAuthClient(_request);
|
||||
const data = await client.deleteEmojiReaction(_request.params.id, _request.params.name);
|
||||
reply.send(await this.mastoConverters.convertStatus(data.data, me));
|
||||
const response = await this.mastoConverters.convertStatus(data.data, me);
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/statuses/${_request.params.id}/unreact/${_request.params.name}`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public deleteStatus() {
|
||||
this.fastify.delete<{ Params: { id?: string } }>('/v1/statuses/:id', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
fastify.delete<{ Params: { id?: string } }>('/v1/statuses/:id', async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.deleteStatus(_request.params.id);
|
||||
|
||||
reply.send(data.data);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
|
|
|
@ -3,87 +3,97 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { getErrorData, MastodonLogger } from '@/server/api/mastodon/MastodonLogger.js';
|
||||
import { MastodonClientService } from '@/server/api/mastodon/MastodonClientService.js';
|
||||
import { convertList, MastoConverters } from '../converters.js';
|
||||
import { getClient, MastodonApiServerService } from '../MastodonApiServerService.js';
|
||||
import { parseTimelineArgs, TimelineArgs, toBoolean } from '../timelineArgs.js';
|
||||
import { parseTimelineArgs, TimelineArgs, toBoolean } from '../argsUtils.js';
|
||||
import type { Entity } from 'megalodon';
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
|
||||
@Injectable()
|
||||
export class ApiTimelineMastodon {
|
||||
constructor(
|
||||
private readonly fastify: FastifyInstance,
|
||||
private readonly clientService: MastodonClientService,
|
||||
private readonly mastoConverters: MastoConverters,
|
||||
private readonly logger: MastodonLogger,
|
||||
private readonly mastodon: MastodonApiServerService,
|
||||
) {}
|
||||
|
||||
public getTL() {
|
||||
this.fastify.get<{ Querystring: TimelineArgs }>('/v1/timelines/public', async (_request, reply) => {
|
||||
public register(fastify: FastifyInstance): void {
|
||||
fastify.get<{ Querystring: TimelineArgs }>('/v1/timelines/public', async (_request, reply) => {
|
||||
try {
|
||||
const { client, me } = await this.mastodon.getAuthClient(_request);
|
||||
const { client, me } = await this.clientService.getAuthClient(_request);
|
||||
|
||||
const query = parseTimelineArgs(_request.query);
|
||||
const data = toBoolean(_request.query.local)
|
||||
? await client.getLocalTimeline(parseTimelineArgs(_request.query))
|
||||
: await client.getPublicTimeline(parseTimelineArgs(_request.query));
|
||||
reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoConverters.convertStatus(status, me))));
|
||||
? await client.getLocalTimeline(query)
|
||||
: await client.getPublicTimeline(query);
|
||||
const response = await Promise.all(data.data.map((status: Entity.Status) => this.mastoConverters.convertStatus(status, me)));
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('GET /v1/timelines/public', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public getHomeTl() {
|
||||
this.fastify.get<{ Querystring: TimelineArgs }>('/v1/timelines/home', async (_request, reply) => {
|
||||
fastify.get<{ Querystring: TimelineArgs }>('/v1/timelines/home', async (_request, reply) => {
|
||||
try {
|
||||
const { client, me } = await this.mastodon.getAuthClient(_request);
|
||||
const data = await client.getHomeTimeline(parseTimelineArgs(_request.query));
|
||||
reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoConverters.convertStatus(status, me))));
|
||||
const { client, me } = await this.clientService.getAuthClient(_request);
|
||||
const query = parseTimelineArgs(_request.query);
|
||||
const data = await client.getHomeTimeline(query);
|
||||
const response = await Promise.all(data.data.map((status: Entity.Status) => this.mastoConverters.convertStatus(status, me)));
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('GET /v1/timelines/home', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public getTagTl() {
|
||||
this.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) => {
|
||||
try {
|
||||
if (!_request.params.hashtag) return reply.code(400).send({ error: 'Missing required parameter "hashtag"' });
|
||||
const { client, me } = await this.mastodon.getAuthClient(_request);
|
||||
const data = await client.getTagTimeline(_request.params.hashtag, parseTimelineArgs(_request.query));
|
||||
reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoConverters.convertStatus(status, me))));
|
||||
|
||||
const { client, me } = await this.clientService.getAuthClient(_request);
|
||||
const query = parseTimelineArgs(_request.query);
|
||||
const data = await client.getTagTimeline(_request.params.hashtag, query);
|
||||
const response = await Promise.all(data.data.map((status: Entity.Status) => this.mastoConverters.convertStatus(status, me)));
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/timelines/tag/${_request.params.hashtag}`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public getListTL() {
|
||||
this.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) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const { client, me } = await this.mastodon.getAuthClient(_request);
|
||||
const data = await client.getListTimeline(_request.params.id, parseTimelineArgs(_request.query));
|
||||
reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoConverters.convertStatus(status, me))));
|
||||
|
||||
const { client, me } = await this.clientService.getAuthClient(_request);
|
||||
const query = parseTimelineArgs(_request.query);
|
||||
const data = await client.getListTimeline(_request.params.id, query);
|
||||
const response = await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoConverters.convertStatus(status, me)));
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/timelines/list/${_request.params.id}`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public getConversations() {
|
||||
this.fastify.get<{ Querystring: TimelineArgs }>('/v1/conversations', async (_request, reply) => {
|
||||
fastify.get<{ Querystring: TimelineArgs }>('/v1/conversations', async (_request, reply) => {
|
||||
try {
|
||||
const { client, me } = await this.mastodon.getAuthClient(_request);
|
||||
const data = await client.getConversationTimeline(parseTimelineArgs(_request.query));
|
||||
const conversations = await Promise.all(data.data.map(async (conversation: Entity.Conversation) => await this.mastoConverters.convertConversation(conversation, me)));
|
||||
const { client, me } = await this.clientService.getAuthClient(_request);
|
||||
const query = parseTimelineArgs(_request.query);
|
||||
const data = await client.getConversationTimeline(query);
|
||||
const conversations = await Promise.all(data.data.map((conversation: Entity.Conversation) => this.mastoConverters.convertConversation(conversation, me)));
|
||||
|
||||
reply.send(conversations);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
|
@ -91,50 +101,45 @@ export class ApiTimelineMastodon {
|
|||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public getList() {
|
||||
this.fastify.get<{ Params: { id?: string } }>('/v1/lists/:id', async (_request, reply) => {
|
||||
fastify.get<{ Params: { id?: string } }>('/v1/lists/:id', async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.getList(_request.params.id);
|
||||
reply.send(convertList(data.data));
|
||||
const response = convertList(data.data);
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/lists/${_request.params.id}`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public getLists() {
|
||||
this.fastify.get('/v1/lists', async (_request, reply) => {
|
||||
fastify.get('/v1/lists', async (_request, reply) => {
|
||||
try {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.getLists();
|
||||
reply.send(data.data.map((list: Entity.List) => convertList(list)));
|
||||
const response = data.data.map((list: Entity.List) => convertList(list));
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('GET /v1/lists', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public getListAccounts() {
|
||||
this.fastify.get<{ Params: { id?: string }, Querystring: { limit?: number, max_id?: string, since_id?: string } }>('/v1/lists/:id/accounts', async (_request, reply) => {
|
||||
fastify.get<{ Params: { id?: string }, Querystring: { limit?: number, max_id?: string, since_id?: string } }>('/v1/lists/:id/accounts', async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.getAccountsInList(_request.params.id, _request.query);
|
||||
const accounts = await Promise.all(data.data.map((account: Entity.Account) => this.mastoConverters.convertAccount(account)));
|
||||
|
||||
reply.send(accounts);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
|
@ -142,17 +147,15 @@ export class ApiTimelineMastodon {
|
|||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public addListAccount() {
|
||||
this.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) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
if (!_request.query.accounts_id) return reply.code(400).send({ error: 'Missing required property "accounts_id"' });
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.addAccountsToList(_request.params.id, _request.query.accounts_id);
|
||||
|
||||
reply.send(data.data);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
|
@ -160,17 +163,15 @@ export class ApiTimelineMastodon {
|
|||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public rmListAccount() {
|
||||
this.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) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
if (!_request.query.accounts_id) return reply.code(400).send({ error: 'Missing required property "accounts_id"' });
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.deleteAccountsFromList(_request.params.id, _request.query.accounts_id);
|
||||
|
||||
reply.send(data.data);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
|
@ -178,51 +179,47 @@ export class ApiTimelineMastodon {
|
|||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public createList() {
|
||||
this.fastify.post<{ Body: { title?: string } }>('/v1/lists', async (_request, reply) => {
|
||||
fastify.post<{ Body: { title?: string } }>('/v1/lists', async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.body.title) return reply.code(400).send({ error: 'Missing required payload "title"' });
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.createList(_request.body.title);
|
||||
reply.send(convertList(data.data));
|
||||
const response = convertList(data.data);
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('POST /v1/lists', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public updateList() {
|
||||
this.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) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
if (!_request.body.title) return reply.code(400).send({ error: 'Missing required payload "title"' });
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
|
||||
const client = this.clientService.getClient(_request);
|
||||
const data = await client.updateList(_request.params.id, _request.body.title);
|
||||
reply.send(convertList(data.data));
|
||||
const response = convertList(data.data);
|
||||
|
||||
reply.send(response);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`PUT /v1/lists/${_request.params.id}`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public deleteList() {
|
||||
this.fastify.delete<{ Params: { id?: string } }>('/v1/lists/:id', async (_request, reply) => {
|
||||
fastify.delete<{ Params: { id?: string } }>('/v1/lists/:id', async (_request, reply) => {
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
|
||||
const client = this.clientService.getClient(_request);
|
||||
await client.deleteList(_request.params.id);
|
||||
|
||||
reply.send({});
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
|
||||
import querystring from 'querystring';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import megalodon, { MegalodonInterface } from 'megalodon';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
/* import { kinds } from '@/misc/api-permissions.js';
|
||||
import type { Config } from '@/config.js';
|
||||
|
@ -14,6 +13,8 @@ import multer from 'fastify-multer';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { MastodonClientService } from '@/server/api/mastodon/MastodonClientService.js';
|
||||
import { getErrorData } from '@/server/api/mastodon/MastodonLogger.js';
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
|
||||
const kinds = [
|
||||
|
@ -51,19 +52,13 @@ const kinds = [
|
|||
'write:gallery-likes',
|
||||
];
|
||||
|
||||
function getClient(BASE_URL: string, authorization: string | undefined): MegalodonInterface {
|
||||
const accessTokenArr = authorization?.split(' ') ?? [null];
|
||||
const accessToken = accessTokenArr[accessTokenArr.length - 1];
|
||||
const generator = (megalodon as any).default;
|
||||
const client = generator('misskey', BASE_URL, accessToken) as MegalodonInterface;
|
||||
return client;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class OAuth2ProviderService {
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
private config: Config,
|
||||
|
||||
private readonly mastodonClientService: MastodonClientService,
|
||||
) { }
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc8414.html
|
||||
|
@ -122,8 +117,8 @@ export class OAuth2ProviderService {
|
|||
try {
|
||||
const parsed = querystring.parse(body);
|
||||
done(null, parsed);
|
||||
} catch (e: any) {
|
||||
done(e);
|
||||
} catch (e: unknown) {
|
||||
done(e instanceof Error ? e : new Error(String(e)));
|
||||
}
|
||||
});
|
||||
payload.on('error', done);
|
||||
|
@ -131,74 +126,53 @@ export class OAuth2ProviderService {
|
|||
|
||||
fastify.register(multer.contentParser);
|
||||
|
||||
fastify.get('/authorize', async (request, reply) => {
|
||||
const query: any = request.query;
|
||||
let param = "mastodon=true";
|
||||
if (query.state) param += `&state=${query.state}`;
|
||||
if (query.redirect_uri) param += `&redirect_uri=${query.redirect_uri}`;
|
||||
const client = query.client_id ? query.client_id : "";
|
||||
reply.redirect(
|
||||
`${Buffer.from(client.toString(), 'base64').toString()}?${param}`,
|
||||
);
|
||||
});
|
||||
for (const url of ['/authorize', '/authorize/']) {
|
||||
fastify.get<{ Querystring: Record<string, string | string[] | undefined> }>(url, async (request, reply) => {
|
||||
if (typeof(request.query.client_id) !== 'string') return reply.code(400).send({ error: 'Missing required query "client_id"' });
|
||||
|
||||
fastify.get('/authorize/', async (request, reply) => {
|
||||
const query: any = request.query;
|
||||
let param = "mastodon=true";
|
||||
if (query.state) param += `&state=${query.state}`;
|
||||
if (query.redirect_uri) param += `&redirect_uri=${query.redirect_uri}`;
|
||||
const client = query.client_id ? query.client_id : "";
|
||||
reply.redirect(
|
||||
`${Buffer.from(client.toString(), 'base64').toString()}?${param}`,
|
||||
);
|
||||
});
|
||||
const redirectUri = new URL(Buffer.from(request.query.client_id, 'base64').toString());
|
||||
redirectUri.searchParams.set('mastodon', 'true');
|
||||
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));
|
||||
|
||||
fastify.post('/token', { preHandler: upload.none() }, async (request, reply) => {
|
||||
const body: any = request.body || request.query;
|
||||
if (body.grant_type === "client_credentials") {
|
||||
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) => {
|
||||
const body = request.body ?? request.query;
|
||||
|
||||
if (body.grant_type === 'client_credentials') {
|
||||
const ret = {
|
||||
access_token: uuid(),
|
||||
token_type: "Bearer",
|
||||
scope: "read",
|
||||
token_type: 'Bearer',
|
||||
scope: 'read',
|
||||
created_at: Math.floor(new Date().getTime() / 1000),
|
||||
};
|
||||
reply.send(ret);
|
||||
}
|
||||
let client_id: any = body.client_id;
|
||||
const BASE_URL = `${request.protocol}://${request.hostname}`;
|
||||
const client = getClient(BASE_URL, '');
|
||||
let token = null;
|
||||
if (body.code) {
|
||||
//m = body.code.match(/^([a-zA-Z0-9]{8})([a-zA-Z0-9]{4})([a-zA-Z0-9]{4})([a-zA-Z0-9]{4})([a-zA-Z0-9]{12})/);
|
||||
//if (!m.length) {
|
||||
// ctx.body = { error: "Invalid code" };
|
||||
// return;
|
||||
//}
|
||||
//token = `${m[1]}-${m[2]}-${m[3]}-${m[4]}-${m[5]}`
|
||||
//console.log(body.code, token);
|
||||
token = body.code;
|
||||
}
|
||||
if (client_id instanceof Array) {
|
||||
client_id = client_id.toString();
|
||||
} else if (!client_id) {
|
||||
client_id = null;
|
||||
}
|
||||
|
||||
try {
|
||||
const atData = await client.fetchAccessToken(
|
||||
client_id,
|
||||
body.client_secret,
|
||||
token ? token : "",
|
||||
);
|
||||
if (!body.client_secret) return reply.code(400).send({ error: 'Missing required query "client_secret"' });
|
||||
|
||||
const clientId = body.client_id ? String(body.clientId) : null;
|
||||
const secret = String(body.client_secret);
|
||||
const code = body.code ? String(body.code) : '';
|
||||
|
||||
// TODO fetch the access token directly
|
||||
const client = this.mastodonClientService.getClient(request);
|
||||
const atData = await client.fetchAccessToken(clientId, secret, code);
|
||||
|
||||
const ret = {
|
||||
access_token: atData.accessToken,
|
||||
token_type: "Bearer",
|
||||
scope: body.scope || "read write follow push",
|
||||
token_type: 'Bearer',
|
||||
scope: body.scope || 'read write follow push',
|
||||
created_at: Math.floor(new Date().getTime() / 1000),
|
||||
};
|
||||
reply.send(ret);
|
||||
} catch (err: any) {
|
||||
/* console.error(err); */
|
||||
reply.code(401).send(err.response.data);
|
||||
} catch (e: unknown) {
|
||||
const data = getErrorData(e);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue