mirror of
https://codeberg.org/yeentown/barkey.git
synced 2025-10-24 02:04:51 +00:00
302 lines
13 KiB
TypeScript
302 lines
13 KiB
TypeScript
/*
|
|
* SPDX-FileCopyrightText: marie and other Sharkey contributors
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|
*/
|
|
|
|
import { Inject, Injectable } from '@nestjs/common';
|
|
import { parseTimelineArgs, TimelineArgs, toBoolean } from '@/server/api/mastodon/argsUtils.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 { attachMinMaxPagination } from '@/server/api/mastodon/pagination.js';
|
|
import { MastodonConverters, convertRelationship, convertFeaturedTag, convertList } from '../MastodonConverters.js';
|
|
import type multer from 'fastify-multer';
|
|
import type { FastifyInstance } from 'fastify';
|
|
|
|
interface ApiAccountMastodonRoute {
|
|
Params: { id?: string },
|
|
Querystring: TimelineArgs & { acct?: string },
|
|
Body: { notifications?: boolean }
|
|
}
|
|
|
|
@Injectable()
|
|
export class ApiAccountMastodon {
|
|
constructor(
|
|
@Inject(DI.userProfilesRepository)
|
|
private readonly userProfilesRepository: UserProfilesRepository,
|
|
|
|
@Inject(DI.accessTokensRepository)
|
|
private readonly accessTokensRepository: AccessTokensRepository,
|
|
|
|
private readonly clientService: MastodonClientService,
|
|
private readonly mastoConverters: MastodonConverters,
|
|
private readonly driveService: DriveService,
|
|
) {}
|
|
|
|
public register(fastify: FastifyInstance, upload: ReturnType<typeof multer>): void {
|
|
fastify.get<ApiAccountMastodonRoute>('/v1/accounts/verify_credentials', async (_request, reply) => {
|
|
const client = this.clientService.getClient(_request);
|
|
const data = await client.verifyAccountCredentials();
|
|
const acct = await this.mastoConverters.convertAccount(data.data);
|
|
const response = Object.assign({}, acct, {
|
|
source: {
|
|
note: acct.note,
|
|
fields: acct.fields,
|
|
privacy: 'public',
|
|
sensitive: false,
|
|
language: '',
|
|
},
|
|
});
|
|
reply.send(response);
|
|
});
|
|
|
|
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;
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
const response = await this.mastoConverters.convertAccount(data.data);
|
|
|
|
reply.send(response);
|
|
});
|
|
|
|
fastify.get<{ Querystring: { acct?: string }}>('/v1/accounts/lookup', async (_request, reply) => {
|
|
if (!_request.query.acct) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: '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);
|
|
});
|
|
|
|
fastify.get<ApiAccountMastodonRoute & { Querystring: { id?: string | string[] }}>('/v1/accounts/relationships', async (_request, reply) => {
|
|
if (!_request.query.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required property "id"' });
|
|
|
|
const client = this.clientService.getClient(_request);
|
|
const data = await client.getRelationships(_request.query.id);
|
|
const response = data.data.map(relationship => convertRelationship(relationship));
|
|
|
|
reply.send(response);
|
|
});
|
|
|
|
fastify.get<{ Params: { id?: string } }>('/v1/accounts/:id', async (_request, reply) => {
|
|
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
|
|
|
|
const client = this.clientService.getClient(_request);
|
|
const data = await client.getAccount(_request.params.id);
|
|
const account = await this.mastoConverters.convertAccount(data.data);
|
|
|
|
reply.send(account);
|
|
});
|
|
|
|
fastify.get<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/statuses', async (request, reply) => {
|
|
if (!request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
|
|
|
|
const { client, me } = await this.clientService.getAuthClient(request);
|
|
const args = parseTimelineArgs(request.query);
|
|
const data = await client.getAccountStatuses(request.params.id, args);
|
|
const response = await Promise.all(data.data.map(async (status) => await this.mastoConverters.convertStatus(status, me)));
|
|
|
|
attachMinMaxPagination(request, reply, response);
|
|
reply.send(response);
|
|
});
|
|
|
|
fastify.get<{ Params: { id?: string } }>('/v1/accounts/:id/featured_tags', async (_request, reply) => {
|
|
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
|
|
|
|
const client = this.clientService.getClient(_request);
|
|
const data = await client.getFeaturedTags();
|
|
const response = data.data.map((tag) => convertFeaturedTag(tag));
|
|
|
|
reply.send(response);
|
|
});
|
|
|
|
fastify.get<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/followers', async (request, reply) => {
|
|
if (!request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
|
|
|
|
const client = this.clientService.getClient(request);
|
|
const data = await client.getAccountFollowers(
|
|
request.params.id,
|
|
parseTimelineArgs(request.query),
|
|
);
|
|
const response = await Promise.all(data.data.map(async (account) => await this.mastoConverters.convertAccount(account)));
|
|
|
|
attachMinMaxPagination(request, reply, response);
|
|
reply.send(response);
|
|
});
|
|
|
|
fastify.get<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/following', async (request, reply) => {
|
|
if (!request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
|
|
|
|
const client = this.clientService.getClient(request);
|
|
const data = await client.getAccountFollowing(
|
|
request.params.id,
|
|
parseTimelineArgs(request.query),
|
|
);
|
|
const response = await Promise.all(data.data.map(async (account) => await this.mastoConverters.convertAccount(account)));
|
|
|
|
attachMinMaxPagination(request, reply, response);
|
|
reply.send(response);
|
|
});
|
|
|
|
fastify.get<{ Params: { id?: string } }>('/v1/accounts/:id/lists', async (_request, reply) => {
|
|
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
|
|
|
|
const client = this.clientService.getClient(_request);
|
|
const data = await client.getAccountLists(_request.params.id);
|
|
const response = data.data.map((list) => convertList(list));
|
|
|
|
reply.send(response);
|
|
});
|
|
|
|
fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/follow', { preHandler: upload.single('none') }, async (_request, reply) => {
|
|
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
|
|
|
|
const client = this.clientService.getClient(_request);
|
|
const data = await client.followAccount(_request.params.id);
|
|
const acct = convertRelationship(data.data);
|
|
acct.following = true; // TODO this is wrong, follow may not have processed immediately
|
|
|
|
reply.send(acct);
|
|
});
|
|
|
|
fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/unfollow', { preHandler: upload.single('none') }, async (_request, reply) => {
|
|
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
|
|
|
|
const client = this.clientService.getClient(_request);
|
|
const data = await client.unfollowAccount(_request.params.id);
|
|
const acct = convertRelationship(data.data);
|
|
acct.following = false;
|
|
|
|
reply.send(acct);
|
|
});
|
|
|
|
fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/block', { preHandler: upload.single('none') }, async (_request, reply) => {
|
|
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
|
|
|
|
const client = this.clientService.getClient(_request);
|
|
const data = await client.blockAccount(_request.params.id);
|
|
const response = convertRelationship(data.data);
|
|
|
|
reply.send(response);
|
|
});
|
|
|
|
fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/unblock', { preHandler: upload.single('none') }, async (_request, reply) => {
|
|
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
|
|
|
|
const client = this.clientService.getClient(_request);
|
|
const data = await client.unblockAccount(_request.params.id);
|
|
const response = convertRelationship(data.data);
|
|
|
|
return reply.send(response);
|
|
});
|
|
|
|
fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/mute', { preHandler: upload.single('none') }, async (_request, reply) => {
|
|
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
|
|
|
|
const client = this.clientService.getClient(_request);
|
|
const data = await client.muteAccount(
|
|
_request.params.id,
|
|
_request.body.notifications ?? true,
|
|
);
|
|
const response = convertRelationship(data.data);
|
|
|
|
reply.send(response);
|
|
});
|
|
|
|
fastify.post<ApiAccountMastodonRoute & { Params: { id?: string } }>('/v1/accounts/:id/unmute', { preHandler: upload.single('none') }, async (_request, reply) => {
|
|
if (!_request.params.id) return reply.code(400).send({ error: 'BAD_REQUEST', error_description: 'Missing required parameter "id"' });
|
|
|
|
const client = this.clientService.getClient(_request);
|
|
const data = await client.unmuteAccount(_request.params.id);
|
|
const response = convertRelationship(data.data);
|
|
|
|
reply.send(response);
|
|
});
|
|
}
|
|
}
|