* Fix TS errors and warnings * Fix ESLint errors and warnings * Fix property typos in various places * Fix property data conversion * Add missing entity properties * Normalize logging and reduce spam * Check for missing request parameters * Allow mastodon API to work with local debugging * Safer error handling * Fix quote-post detection
This commit is contained in:
@@ -3,14 +3,11 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { parseTimelineArgs, TimelineArgs } from '@/server/api/mastodon/timelineArgs.js';
|
||||
import { MastoConverters, convertRelationship } from '../converters.js';
|
||||
import { argsToBools, limitToInt } from './timeline.js';
|
||||
import type { MegalodonInterface } from 'megalodon';
|
||||
import type { FastifyRequest } from 'fastify';
|
||||
import { NoteEditRepository, NotesRepository, UsersRepository } from '@/models/_.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
const relationshipModel = {
|
||||
id: '',
|
||||
@@ -29,247 +26,152 @@ const relationshipModel = {
|
||||
note: '',
|
||||
};
|
||||
|
||||
export interface ApiAccountMastodonRoute {
|
||||
Params: { id?: string },
|
||||
Querystring: TimelineArgs & { acct?: string },
|
||||
Body: { notifications?: boolean }
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ApiAccountMastodon {
|
||||
private request: FastifyRequest;
|
||||
private client: MegalodonInterface;
|
||||
private BASE_URL: string;
|
||||
|
||||
constructor(request: FastifyRequest, client: MegalodonInterface, BASE_URL: string, private mastoconverter: MastoConverters) {
|
||||
this.request = request;
|
||||
this.client = client;
|
||||
this.BASE_URL = BASE_URL;
|
||||
}
|
||||
constructor(
|
||||
private readonly request: FastifyRequest<ApiAccountMastodonRoute>,
|
||||
private readonly client: MegalodonInterface,
|
||||
private readonly mastoConverters: MastoConverters,
|
||||
) {}
|
||||
|
||||
public async verifyCredentials() {
|
||||
try {
|
||||
const data = await this.client.verifyAccountCredentials();
|
||||
const acct = await this.mastoconverter.convertAccount(data.data);
|
||||
const newAcct = Object.assign({}, acct, {
|
||||
source: {
|
||||
note: acct.note,
|
||||
fields: acct.fields,
|
||||
privacy: '',
|
||||
sensitive: false,
|
||||
language: '',
|
||||
},
|
||||
});
|
||||
return newAcct;
|
||||
} catch (e: any) {
|
||||
/* console.error(e);
|
||||
console.error(e.response.data); */
|
||||
return e.response;
|
||||
}
|
||||
const data = await this.client.verifyAccountCredentials();
|
||||
const acct = await this.mastoConverters.convertAccount(data.data);
|
||||
return Object.assign({}, acct, {
|
||||
source: {
|
||||
note: acct.note,
|
||||
fields: acct.fields,
|
||||
privacy: '',
|
||||
sensitive: false,
|
||||
language: '',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async lookup() {
|
||||
try {
|
||||
const data = await this.client.search((this.request.query as any).acct, { type: 'accounts' });
|
||||
return this.mastoconverter.convertAccount(data.data.accounts[0]);
|
||||
} catch (e: any) {
|
||||
/* console.error(e)
|
||||
console.error(e.response.data); */
|
||||
return e.response;
|
||||
}
|
||||
if (!this.request.query.acct) throw new Error('Missing required property "acct"');
|
||||
const data = await this.client.search(this.request.query.acct, { type: 'accounts' });
|
||||
return this.mastoConverters.convertAccount(data.data.accounts[0]);
|
||||
}
|
||||
|
||||
public async getRelationships(users: [string]) {
|
||||
try {
|
||||
relationshipModel.id = users.toString() || '1';
|
||||
public async getRelationships(users: string[]) {
|
||||
relationshipModel.id = users.toString() || '1';
|
||||
|
||||
if (!(users.length > 0)) {
|
||||
return [relationshipModel];
|
||||
}
|
||||
|
||||
const reqIds = [];
|
||||
for (let i = 0; i < users.length; i++) {
|
||||
reqIds.push(users[i]);
|
||||
}
|
||||
|
||||
const data = await this.client.getRelationships(reqIds);
|
||||
return data.data.map((relationship) => convertRelationship(relationship));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
return e.response.data;
|
||||
if (!(users.length > 0)) {
|
||||
return [relationshipModel];
|
||||
}
|
||||
|
||||
const reqIds = [];
|
||||
for (let i = 0; i < users.length; i++) {
|
||||
reqIds.push(users[i]);
|
||||
}
|
||||
|
||||
const data = await this.client.getRelationships(reqIds);
|
||||
return data.data.map((relationship) => convertRelationship(relationship));
|
||||
}
|
||||
|
||||
public async getStatuses() {
|
||||
try {
|
||||
const data = await this.client.getAccountStatuses((this.request.params as any).id, argsToBools(limitToInt(this.request.query as any)));
|
||||
return await Promise.all(data.data.map(async (status) => await this.mastoconverter.convertStatus(status)));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
return e.response.data;
|
||||
}
|
||||
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||
const data = await this.client.getAccountStatuses(this.request.params.id, parseTimelineArgs(this.request.query));
|
||||
return await Promise.all(data.data.map(async (status) => await this.mastoConverters.convertStatus(status)));
|
||||
}
|
||||
|
||||
public async getFollowers() {
|
||||
try {
|
||||
const data = await this.client.getAccountFollowers(
|
||||
(this.request.params as any).id,
|
||||
limitToInt(this.request.query as any),
|
||||
);
|
||||
return await Promise.all(data.data.map(async (account) => await this.mastoconverter.convertAccount(account)));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
return e.response.data;
|
||||
}
|
||||
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||
const data = await this.client.getAccountFollowers(
|
||||
this.request.params.id,
|
||||
parseTimelineArgs(this.request.query),
|
||||
);
|
||||
return await Promise.all(data.data.map(async (account) => await this.mastoConverters.convertAccount(account)));
|
||||
}
|
||||
|
||||
public async getFollowing() {
|
||||
try {
|
||||
const data = await this.client.getAccountFollowing(
|
||||
(this.request.params as any).id,
|
||||
limitToInt(this.request.query as any),
|
||||
);
|
||||
return await Promise.all(data.data.map(async (account) => await this.mastoconverter.convertAccount(account)));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
return e.response.data;
|
||||
}
|
||||
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||
const data = await this.client.getAccountFollowing(
|
||||
this.request.params.id,
|
||||
parseTimelineArgs(this.request.query),
|
||||
);
|
||||
return await Promise.all(data.data.map(async (account) => await this.mastoConverters.convertAccount(account)));
|
||||
}
|
||||
|
||||
public async addFollow() {
|
||||
try {
|
||||
const data = await this.client.followAccount( (this.request.params as any).id );
|
||||
const acct = convertRelationship(data.data);
|
||||
acct.following = true;
|
||||
return acct;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
return e.response.data;
|
||||
}
|
||||
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||
const data = await this.client.followAccount( this.request.params.id );
|
||||
const acct = convertRelationship(data.data);
|
||||
acct.following = true;
|
||||
return acct;
|
||||
}
|
||||
|
||||
public async rmFollow() {
|
||||
try {
|
||||
const data = await this.client.unfollowAccount( (this.request.params as any).id );
|
||||
const acct = convertRelationship(data.data);
|
||||
acct.following = false;
|
||||
return acct;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
return e.response.data;
|
||||
}
|
||||
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||
const data = await this.client.unfollowAccount( this.request.params.id );
|
||||
const acct = convertRelationship(data.data);
|
||||
acct.following = false;
|
||||
return acct;
|
||||
}
|
||||
|
||||
public async addBlock() {
|
||||
try {
|
||||
const data = await this.client.blockAccount( (this.request.params as any).id );
|
||||
return convertRelationship(data.data);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
return e.response.data;
|
||||
}
|
||||
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||
const data = await this.client.blockAccount( this.request.params.id );
|
||||
return convertRelationship(data.data);
|
||||
}
|
||||
|
||||
public async rmBlock() {
|
||||
try {
|
||||
const data = await this.client.unblockAccount( (this.request.params as any).id );
|
||||
return convertRelationship(data.data);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
return e.response.data;
|
||||
}
|
||||
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||
const data = await this.client.unblockAccount( this.request.params.id );
|
||||
return convertRelationship(data.data);
|
||||
}
|
||||
|
||||
public async addMute() {
|
||||
try {
|
||||
const data = await this.client.muteAccount(
|
||||
(this.request.params as any).id,
|
||||
this.request.body as any,
|
||||
);
|
||||
return convertRelationship(data.data);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
return e.response.data;
|
||||
}
|
||||
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||
const data = await this.client.muteAccount(
|
||||
this.request.params.id,
|
||||
this.request.body.notifications ?? true,
|
||||
);
|
||||
return convertRelationship(data.data);
|
||||
}
|
||||
|
||||
public async rmMute() {
|
||||
try {
|
||||
const data = await this.client.unmuteAccount( (this.request.params as any).id );
|
||||
return convertRelationship(data.data);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
return e.response.data;
|
||||
}
|
||||
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||
const data = await this.client.unmuteAccount( this.request.params.id );
|
||||
return convertRelationship(data.data);
|
||||
}
|
||||
|
||||
public async getBookmarks() {
|
||||
try {
|
||||
const data = await this.client.getBookmarks( limitToInt(this.request.query as any) );
|
||||
return data.data.map((status) => this.mastoconverter.convertStatus(status));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
return e.response.data;
|
||||
}
|
||||
const data = await this.client.getBookmarks(parseTimelineArgs(this.request.query));
|
||||
return data.data.map((status) => this.mastoConverters.convertStatus(status));
|
||||
}
|
||||
|
||||
public async getFavourites() {
|
||||
try {
|
||||
const data = await this.client.getFavourites( limitToInt(this.request.query as any) );
|
||||
return data.data.map((status) => this.mastoconverter.convertStatus(status));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
return e.response.data;
|
||||
}
|
||||
const data = await this.client.getFavourites(parseTimelineArgs(this.request.query));
|
||||
return data.data.map((status) => this.mastoConverters.convertStatus(status));
|
||||
}
|
||||
|
||||
public async getMutes() {
|
||||
try {
|
||||
const data = await this.client.getMutes( limitToInt(this.request.query as any) );
|
||||
return data.data.map((account) => this.mastoconverter.convertAccount(account));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
return e.response.data;
|
||||
}
|
||||
const data = await this.client.getMutes(parseTimelineArgs(this.request.query));
|
||||
return data.data.map((account) => this.mastoConverters.convertAccount(account));
|
||||
}
|
||||
|
||||
public async getBlocks() {
|
||||
try {
|
||||
const data = await this.client.getBlocks( limitToInt(this.request.query as any) );
|
||||
return data.data.map((account) => this.mastoconverter.convertAccount(account));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
return e.response.data;
|
||||
}
|
||||
const data = await this.client.getBlocks(parseTimelineArgs(this.request.query));
|
||||
return data.data.map((account) => this.mastoConverters.convertAccount(account));
|
||||
}
|
||||
|
||||
public async acceptFollow() {
|
||||
try {
|
||||
const data = await this.client.acceptFollowRequest( (this.request.params as any).id );
|
||||
return convertRelationship(data.data);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
return e.response.data;
|
||||
}
|
||||
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||
const data = await this.client.acceptFollowRequest(this.request.params.id);
|
||||
return convertRelationship(data.data);
|
||||
}
|
||||
|
||||
public async rejectFollow() {
|
||||
try {
|
||||
const data = await this.client.rejectFollowRequest( (this.request.params as any).id );
|
||||
return convertRelationship(data.data);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
return e.response.data;
|
||||
}
|
||||
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||
const data = await this.client.rejectFollowRequest(this.request.params.id);
|
||||
return convertRelationship(data.data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,36 +44,54 @@ const writeScope = [
|
||||
'write:gallery-likes',
|
||||
];
|
||||
|
||||
export async function ApiAuthMastodon(request: FastifyRequest, client: MegalodonInterface) {
|
||||
const body: any = request.body || request.query;
|
||||
try {
|
||||
let scope = body.scopes;
|
||||
if (typeof scope === 'string') scope = scope.split(' ') || scope.split('+');
|
||||
const pushScope = new Set<string>();
|
||||
for (const s of scope) {
|
||||
if (s.match(/^read/)) for (const r of readScope) pushScope.add(r);
|
||||
if (s.match(/^write/)) for (const r of writeScope) pushScope.add(r);
|
||||
}
|
||||
const scopeArr = Array.from(pushScope);
|
||||
|
||||
const red = body.redirect_uris;
|
||||
const appData = await client.registerApp(body.client_name, {
|
||||
scopes: scopeArr,
|
||||
redirect_uris: red,
|
||||
website: body.website,
|
||||
});
|
||||
const returns = {
|
||||
id: Math.floor(Math.random() * 100).toString(),
|
||||
name: appData.name,
|
||||
website: body.website,
|
||||
redirect_uri: red,
|
||||
client_id: Buffer.from(appData.url || '').toString('base64'),
|
||||
client_secret: appData.clientSecret,
|
||||
};
|
||||
|
||||
return returns;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
return e.response.data;
|
||||
}
|
||||
export interface AuthPayload {
|
||||
scopes?: string | string[],
|
||||
redirect_uris?: string,
|
||||
client_name?: string,
|
||||
website?: string,
|
||||
}
|
||||
|
||||
// Not entirely right, but it gets TypeScript to work so *shrug*
|
||||
export type AuthMastodonRoute = { Body?: AuthPayload, Querystring: AuthPayload };
|
||||
|
||||
export async function ApiAuthMastodon(request: FastifyRequest<AuthMastodonRoute>, client: MegalodonInterface) {
|
||||
const body = request.body ?? request.query;
|
||||
if (!body.scopes) throw new Error('Missing required payload "scopes"');
|
||||
if (!body.redirect_uris) throw new Error('Missing required payload "redirect_uris"');
|
||||
if (!body.client_name) throw new Error('Missing required payload "client_name"');
|
||||
|
||||
let scope = body.scopes;
|
||||
if (typeof scope === 'string') {
|
||||
scope = scope.split(/[ +]/g);
|
||||
}
|
||||
|
||||
const pushScope = new Set<string>();
|
||||
for (const s of scope) {
|
||||
if (s.match(/^read/)) {
|
||||
for (const r of readScope) {
|
||||
pushScope.add(r);
|
||||
}
|
||||
}
|
||||
if (s.match(/^write/)) {
|
||||
for (const r of writeScope) {
|
||||
pushScope.add(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const red = body.redirect_uris;
|
||||
const appData = await client.registerApp(body.client_name, {
|
||||
scopes: Array.from(pushScope),
|
||||
redirect_uris: red,
|
||||
website: body.website,
|
||||
});
|
||||
|
||||
return {
|
||||
id: Math.floor(Math.random() * 100).toString(),
|
||||
name: appData.name,
|
||||
website: body.website,
|
||||
redirect_uri: red,
|
||||
client_id: Buffer.from(appData.url || '').toString('base64'), // eslint-disable-line @typescript-eslint/prefer-nullish-coalescing
|
||||
client_secret: appData.clientSecret,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,68 +3,73 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { toBoolean } from '@/server/api/mastodon/timelineArgs.js';
|
||||
import { convertFilter } from '../converters.js';
|
||||
import type { MegalodonInterface } from 'megalodon';
|
||||
import type { FastifyRequest } from 'fastify';
|
||||
|
||||
export class ApiFilterMastodon {
|
||||
private request: FastifyRequest;
|
||||
private client: MegalodonInterface;
|
||||
|
||||
constructor(request: FastifyRequest, client: MegalodonInterface) {
|
||||
this.request = request;
|
||||
this.client = client;
|
||||
export interface ApiFilterMastodonRoute {
|
||||
Params: {
|
||||
id?: string,
|
||||
},
|
||||
Body: {
|
||||
phrase?: string,
|
||||
context?: string[],
|
||||
irreversible?: string,
|
||||
whole_word?: string,
|
||||
expires_in?: string,
|
||||
}
|
||||
}
|
||||
|
||||
export class ApiFilterMastodon {
|
||||
constructor(
|
||||
private readonly request: FastifyRequest<ApiFilterMastodonRoute>,
|
||||
private readonly client: MegalodonInterface,
|
||||
) {}
|
||||
|
||||
public async getFilters() {
|
||||
try {
|
||||
const data = await this.client.getFilters();
|
||||
return data.data.map((filter) => convertFilter(filter));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
return e.response.data;
|
||||
}
|
||||
const data = await this.client.getFilters();
|
||||
return data.data.map((filter) => convertFilter(filter));
|
||||
}
|
||||
|
||||
public async getFilter() {
|
||||
try {
|
||||
const data = await this.client.getFilter( (this.request.params as any).id );
|
||||
return convertFilter(data.data);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
return e.response.data;
|
||||
}
|
||||
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||
const data = await this.client.getFilter(this.request.params.id);
|
||||
return convertFilter(data.data);
|
||||
}
|
||||
|
||||
public async createFilter() {
|
||||
try {
|
||||
const body: any = this.request.body;
|
||||
const data = await this.client.createFilter(body.pharse, body.context, body);
|
||||
return convertFilter(data.data);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
return e.response.data;
|
||||
}
|
||||
if (!this.request.body.phrase) throw new Error('Missing required payload "phrase"');
|
||||
if (!this.request.body.context) throw new Error('Missing required payload "context"');
|
||||
const options = {
|
||||
phrase: this.request.body.phrase,
|
||||
context: this.request.body.context,
|
||||
irreversible: toBoolean(this.request.body.irreversible),
|
||||
whole_word: toBoolean(this.request.body.whole_word),
|
||||
expires_in: this.request.body.expires_in,
|
||||
};
|
||||
const data = await this.client.createFilter(this.request.body.phrase, this.request.body.context, options);
|
||||
return convertFilter(data.data);
|
||||
}
|
||||
|
||||
public async updateFilter() {
|
||||
try {
|
||||
const body: any = this.request.body;
|
||||
const data = await this.client.updateFilter((this.request.params as any).id, body.pharse, body.context);
|
||||
return convertFilter(data.data);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
return e.response.data;
|
||||
}
|
||||
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||
if (!this.request.body.phrase) throw new Error('Missing required payload "phrase"');
|
||||
if (!this.request.body.context) throw new Error('Missing required payload "context"');
|
||||
const options = {
|
||||
phrase: this.request.body.phrase,
|
||||
context: this.request.body.context,
|
||||
irreversible: toBoolean(this.request.body.irreversible),
|
||||
whole_word: toBoolean(this.request.body.whole_word),
|
||||
expires_in: this.request.body.expires_in,
|
||||
};
|
||||
const data = await this.client.updateFilter(this.request.params.id, this.request.body.phrase, this.request.body.context, options);
|
||||
return convertFilter(data.data);
|
||||
}
|
||||
|
||||
public async rmFilter() {
|
||||
try {
|
||||
const data = await this.client.deleteFilter( (this.request.params as any).id );
|
||||
return data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
return e.response.data;
|
||||
}
|
||||
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||
const data = await this.client.deleteFilter(this.request.params.id);
|
||||
return data.data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { FILE_TYPE_BROWSERSAFE } from '@/const.js';
|
||||
import type { Config } from '@/config.js';
|
||||
import type { MiMeta } from '@/models/Meta.js';
|
||||
|
||||
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
|
||||
export async function getInstance(
|
||||
response: Entity.Instance,
|
||||
contact: Entity.Account,
|
||||
@@ -17,11 +18,8 @@ export async function getInstance(
|
||||
return {
|
||||
uri: config.url,
|
||||
title: meta.name || 'Sharkey',
|
||||
short_description:
|
||||
meta.description || 'This is a vanilla Sharkey Instance. It doesn\'t seem to have a description.',
|
||||
description:
|
||||
meta.description ||
|
||||
'This is a vanilla Sharkey Instance. It doesn\'t seem to have a description.',
|
||||
short_description: meta.description || 'This is a vanilla Sharkey Instance. It doesn\'t seem to have a description.',
|
||||
description: meta.description || 'This is a vanilla Sharkey Instance. It doesn\'t seem to have a description.',
|
||||
email: response.email || '',
|
||||
version: `3.0.0 (compatible; Sharkey ${config.version})`,
|
||||
urls: response.urls,
|
||||
|
||||
@@ -3,73 +3,53 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { parseTimelineArgs, TimelineArgs } from '@/server/api/mastodon/timelineArgs.js';
|
||||
import { convertNotification } from '../converters.js';
|
||||
import type { MegalodonInterface, Entity } from 'megalodon';
|
||||
import type { MegalodonInterface } from 'megalodon';
|
||||
import type { FastifyRequest } from 'fastify';
|
||||
|
||||
function toLimitToInt(q: any) {
|
||||
if (q.limit) if (typeof q.limit === 'string') q.limit = parseInt(q.limit, 10);
|
||||
return q;
|
||||
export interface ApiNotifyMastodonRoute {
|
||||
Params: {
|
||||
id?: string,
|
||||
},
|
||||
Querystring: TimelineArgs,
|
||||
}
|
||||
|
||||
export class ApiNotifyMastodon {
|
||||
private request: FastifyRequest;
|
||||
private client: MegalodonInterface;
|
||||
|
||||
constructor(request: FastifyRequest, client: MegalodonInterface) {
|
||||
this.request = request;
|
||||
this.client = client;
|
||||
}
|
||||
constructor(
|
||||
private readonly request: FastifyRequest<ApiNotifyMastodonRoute>,
|
||||
private readonly client: MegalodonInterface,
|
||||
) {}
|
||||
|
||||
public async getNotifications() {
|
||||
try {
|
||||
const data = await this.client.getNotifications( toLimitToInt(this.request.query) );
|
||||
const notifs = data.data;
|
||||
const processed = notifs.map((n: Entity.Notification) => {
|
||||
const convertedn = convertNotification(n);
|
||||
if (convertedn.type !== 'follow' && convertedn.type !== 'follow_request') {
|
||||
if (convertedn.type === 'reaction') convertedn.type = 'favourite';
|
||||
return convertedn;
|
||||
} else {
|
||||
return convertedn;
|
||||
}
|
||||
});
|
||||
return processed;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
return e.response.data;
|
||||
}
|
||||
const data = await this.client.getNotifications(parseTimelineArgs(this.request.query));
|
||||
return data.data.map(n => {
|
||||
const converted = convertNotification(n);
|
||||
if (converted.type === 'reaction') {
|
||||
converted.type = 'favourite';
|
||||
}
|
||||
return converted;
|
||||
});
|
||||
}
|
||||
|
||||
public async getNotification() {
|
||||
try {
|
||||
const data = await this.client.getNotification( (this.request.params as any).id );
|
||||
const notif = convertNotification(data.data);
|
||||
if (notif.type !== 'follow' && notif.type !== 'follow_request' && notif.type === 'reaction') notif.type = 'favourite';
|
||||
return notif;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
return e.response.data;
|
||||
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||
const data = await this.client.getNotification(this.request.params.id);
|
||||
const converted = convertNotification(data.data);
|
||||
if (converted.type === 'reaction') {
|
||||
converted.type = 'favourite';
|
||||
}
|
||||
return converted;
|
||||
}
|
||||
|
||||
public async rmNotification() {
|
||||
try {
|
||||
const data = await this.client.dismissNotification( (this.request.params as any).id );
|
||||
return data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
return e.response.data;
|
||||
}
|
||||
if (!this.request.params.id) throw new Error('Missing required parameter "id"');
|
||||
const data = await this.client.dismissNotification(this.request.params.id);
|
||||
return data.data;
|
||||
}
|
||||
|
||||
public async rmNotifications() {
|
||||
try {
|
||||
const data = await this.client.dismissNotifications();
|
||||
return data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
return e.response.data;
|
||||
}
|
||||
const data = await this.client.dismissNotifications();
|
||||
return data.data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,87 +4,86 @@
|
||||
*/
|
||||
|
||||
import { MastoConverters } from '../converters.js';
|
||||
import { limitToInt } from './timeline.js';
|
||||
import { parseTimelineArgs, TimelineArgs } from '../timelineArgs.js';
|
||||
import Account = Entity.Account;
|
||||
import Status = Entity.Status;
|
||||
import type { MegalodonInterface } from 'megalodon';
|
||||
import type { FastifyRequest } from 'fastify';
|
||||
|
||||
export class ApiSearchMastodon {
|
||||
private request: FastifyRequest;
|
||||
private client: MegalodonInterface;
|
||||
private BASE_URL: string;
|
||||
|
||||
constructor(request: FastifyRequest, client: MegalodonInterface, BASE_URL: string, private mastoConverter: MastoConverters) {
|
||||
this.request = request;
|
||||
this.client = client;
|
||||
this.BASE_URL = BASE_URL;
|
||||
export interface ApiSearchMastodonRoute {
|
||||
Querystring: TimelineArgs & {
|
||||
type?: 'accounts' | 'hashtags' | 'statuses';
|
||||
q?: string;
|
||||
}
|
||||
}
|
||||
|
||||
export class ApiSearchMastodon {
|
||||
constructor(
|
||||
private readonly request: FastifyRequest<ApiSearchMastodonRoute>,
|
||||
private readonly client: MegalodonInterface,
|
||||
private readonly BASE_URL: string,
|
||||
private readonly mastoConverter: MastoConverters,
|
||||
) {}
|
||||
|
||||
public async SearchV1() {
|
||||
try {
|
||||
const query: any = limitToInt(this.request.query as any);
|
||||
const type = query.type || '';
|
||||
const data = await this.client.search(query.q, { type: type, ...query });
|
||||
return data.data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
return e.response.data;
|
||||
}
|
||||
if (!this.request.query.q) throw new Error('Missing required property "q"');
|
||||
const query = parseTimelineArgs(this.request.query);
|
||||
const data = await this.client.search(this.request.query.q, { type: this.request.query.type, ...query });
|
||||
return data.data;
|
||||
}
|
||||
|
||||
public async SearchV2() {
|
||||
try {
|
||||
const query: any = limitToInt(this.request.query as any);
|
||||
const type = query.type;
|
||||
const acct = !type || type === 'accounts' ? await this.client.search(query.q, { type: 'accounts', ...query }) : null;
|
||||
const stat = !type || type === 'statuses' ? await this.client.search(query.q, { type: 'statuses', ...query }) : null;
|
||||
const tags = !type || type === 'hashtags' ? await this.client.search(query.q, { type: 'hashtags', ...query }) : null;
|
||||
const data = {
|
||||
accounts: await Promise.all(acct?.data.accounts.map(async (account: any) => await this.mastoConverter.convertAccount(account)) ?? []),
|
||||
statuses: await Promise.all(stat?.data.statuses.map(async (status: any) => await this.mastoConverter.convertStatus(status)) ?? []),
|
||||
hashtags: tags?.data.hashtags ?? [],
|
||||
};
|
||||
return data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
return e.response.data;
|
||||
}
|
||||
if (!this.request.query.q) throw new Error('Missing required property "q"');
|
||||
const query = parseTimelineArgs(this.request.query);
|
||||
const type = this.request.query.type;
|
||||
const acct = !type || type === 'accounts' ? await this.client.search(this.request.query.q, { type: 'accounts', ...query }) : null;
|
||||
const stat = !type || type === 'statuses' ? await this.client.search(this.request.query.q, { type: 'statuses', ...query }) : null;
|
||||
const tags = !type || type === 'hashtags' ? await this.client.search(this.request.query.q, { type: 'hashtags', ...query }) : null;
|
||||
return {
|
||||
accounts: await Promise.all(acct?.data.accounts.map(async (account: Account) => await this.mastoConverter.convertAccount(account)) ?? []),
|
||||
statuses: await Promise.all(stat?.data.statuses.map(async (status: Status) => await this.mastoConverter.convertStatus(status)) ?? []),
|
||||
hashtags: tags?.data.hashtags ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
public async getStatusTrends() {
|
||||
try {
|
||||
const data = await fetch(`${this.BASE_URL}/api/notes/featured`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => data.map((status: any) => this.mastoConverter.convertStatus(status)));
|
||||
return data;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
return [];
|
||||
}
|
||||
return await fetch(`${this.BASE_URL}/api/notes/featured`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => data.map((status: Status) => this.mastoConverter.convertStatus(status)));
|
||||
}
|
||||
|
||||
public async getSuggestions() {
|
||||
try {
|
||||
const data = await fetch(`${this.BASE_URL}/api/users`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ i: this.request.headers.authorization?.replace('Bearer ', ''), limit: parseInt((this.request.query as any).limit) || 20, origin: 'local', sort: '+follower', state: 'alive' }),
|
||||
}).then((res) => res.json()).then(data => data.map(((entry: any) => { return { source: 'global', account: entry }; })));
|
||||
return Promise.all(data.map(async (suggestion: any) => { suggestion.account = await this.mastoConverter.convertAccount(suggestion.account); return suggestion; }));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
return [];
|
||||
}
|
||||
const data = await fetch(`${this.BASE_URL}/api/users`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
i: this.request.headers.authorization?.replace('Bearer ', ''),
|
||||
limit: parseTimelineArgs(this.request.query).limit ?? 20,
|
||||
origin: 'local',
|
||||
sort: '+follower',
|
||||
state: 'alive',
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data: Account[]) => data.map((entry => ({
|
||||
source: 'global',
|
||||
account: entry,
|
||||
}))));
|
||||
return Promise.all(data.map(async suggestion => {
|
||||
suggestion.account = await this.mastoConverter.convertAccount(suggestion.account);
|
||||
return suggestion;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,181 +3,214 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import querystring from 'querystring';
|
||||
import querystring, { ParsedUrlQueryInput } from 'querystring';
|
||||
import { emojiRegexAtStartToEnd } from '@/misc/emoji-regex.js';
|
||||
import { convertAttachment, convertPoll, convertStatusSource, MastoConverters } from '../converters.js';
|
||||
import { getErrorData, MastodonLogger } from '@/server/api/mastodon/MastodonLogger.js';
|
||||
import { parseTimelineArgs, TimelineArgs, toBoolean, toInt } from '@/server/api/mastodon/timelineArgs.js';
|
||||
import { convertAttachment, convertPoll, MastoConverters } from '../converters.js';
|
||||
import { getClient } from '../MastodonApiServerService.js';
|
||||
import { limitToInt } from './timeline.js';
|
||||
import type { Entity } from 'megalodon';
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import type { Config } from '@/config.js';
|
||||
import { NoteEditRepository, NotesRepository, UsersRepository } from '@/models/_.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
|
||||
function normalizeQuery(data: any) {
|
||||
const str = querystring.stringify(data);
|
||||
function normalizeQuery(data: Record<string, unknown>) {
|
||||
const str = querystring.stringify(data as ParsedUrlQueryInput);
|
||||
return querystring.parse(str);
|
||||
}
|
||||
|
||||
export class ApiStatusMastodon {
|
||||
private fastify: FastifyInstance;
|
||||
private mastoconverter: MastoConverters;
|
||||
constructor(
|
||||
private readonly fastify: FastifyInstance,
|
||||
private readonly mastoConverters: MastoConverters,
|
||||
private readonly logger: MastodonLogger,
|
||||
) {}
|
||||
|
||||
constructor(fastify: FastifyInstance, mastoconverter: MastoConverters) {
|
||||
this.fastify = fastify;
|
||||
this.mastoconverter = mastoconverter;
|
||||
}
|
||||
|
||||
public async getStatus() {
|
||||
this.fastify.get<{ Params: { id: string } }>('/v1/statuses/:id', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
||||
public getStatus() {
|
||||
this.fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const data = await client.getStatus(_request.params.id);
|
||||
reply.send(await this.mastoconverter.convertStatus(data.data));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
reply.code(_request.is404 ? 404 : 401).send(e.response.data);
|
||||
reply.send(await this.mastoConverters.convertStatus(data.data));
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/statuses/${_request.params.id}`, data);
|
||||
reply.code(_request.is404 ? 404 : 401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async getStatusSource() {
|
||||
this.fastify.get<{ Params: { id: string } }>('/v1/statuses/:id/source', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
||||
public getStatusSource() {
|
||||
this.fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/source', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const data = await client.getStatusSource(_request.params.id);
|
||||
reply.send(data.data);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
reply.code(_request.is404 ? 404 : 401).send(e.response.data);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/statuses/${_request.params.id}/source`, data);
|
||||
reply.code(_request.is404 ? 404 : 401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async getContext() {
|
||||
this.fastify.get<{ Params: { id: string } }>('/v1/statuses/:id/context', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
||||
public getContext() {
|
||||
this.fastify.get<{ Params: { id?: string }, Querystring: TimelineArgs }>('/v1/statuses/:id/context', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
const query: any = _request.query;
|
||||
try {
|
||||
const data = await client.getStatusContext(_request.params.id, limitToInt(query));
|
||||
data.data.ancestors = await Promise.all(data.data.ancestors.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status)));
|
||||
data.data.descendants = await Promise.all(data.data.descendants.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status)));
|
||||
reply.send(data.data);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
reply.code(_request.is404 ? 404 : 401).send(e.response.data);
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const { 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)));
|
||||
const descendants = await Promise.all(data.descendants.map(async status => await this.mastoConverters.convertStatus(status)));
|
||||
reply.send({ ancestors, descendants });
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/statuses/${_request.params.id}/context`, data);
|
||||
reply.code(_request.is404 ? 404 : 401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async getHistory() {
|
||||
this.fastify.get<{ Params: { id: string } }>('/v1/statuses/:id/history', async (_request, reply) => {
|
||||
public getHistory() {
|
||||
this.fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/history', async (_request, reply) => {
|
||||
try {
|
||||
const edits = await this.mastoconverter.getEdits(_request.params.id);
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const edits = await this.mastoConverters.getEdits(_request.params.id);
|
||||
reply.send(edits);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
reply.code(401).send(e.response.data);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/statuses/${_request.params.id}/history`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async getReblogged() {
|
||||
this.fastify.get<{ Params: { id: string } }>('/v1/statuses/:id/reblogged_by', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
||||
public getReblogged() {
|
||||
this.fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/reblogged_by', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const data = await client.getStatusRebloggedBy(_request.params.id);
|
||||
reply.send(await Promise.all(data.data.map(async (account: Entity.Account) => await this.mastoconverter.convertAccount(account))));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
reply.code(401).send(e.response.data);
|
||||
reply.send(await Promise.all(data.data.map(async (account: Entity.Account) => await this.mastoConverters.convertAccount(account))));
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/statuses/${_request.params.id}/reblogged_by`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async getFavourites() {
|
||||
this.fastify.get<{ Params: { id: string } }>('/v1/statuses/:id/favourited_by', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
||||
public getFavourites() {
|
||||
this.fastify.get<{ Params: { id?: string } }>('/v1/statuses/:id/favourited_by', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const data = await client.getStatusFavouritedBy(_request.params.id);
|
||||
reply.send(await Promise.all(data.data.map(async (account: Entity.Account) => await this.mastoconverter.convertAccount(account))));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
reply.code(401).send(e.response.data);
|
||||
reply.send(await Promise.all(data.data.map(async (account: Entity.Account) => await this.mastoConverters.convertAccount(account))));
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/statuses/${_request.params.id}/favourited_by`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async getMedia() {
|
||||
this.fastify.get<{ Params: { id: string } }>('/v1/media/:id', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
||||
public getMedia() {
|
||||
this.fastify.get<{ Params: { id?: string } }>('/v1/media/:id', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const data = await client.getMedia(_request.params.id);
|
||||
reply.send(convertAttachment(data.data));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
reply.code(401).send(e.response.data);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/media/${_request.params.id}`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async getPoll() {
|
||||
this.fastify.get<{ Params: { id: string } }>('/v1/polls/:id', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
||||
public getPoll() {
|
||||
this.fastify.get<{ Params: { id?: string } }>('/v1/polls/:id', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const data = await client.getPoll(_request.params.id);
|
||||
reply.send(convertPoll(data.data));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
reply.code(401).send(e.response.data);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/polls/${_request.params.id}`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async votePoll() {
|
||||
this.fastify.post<{ Params: { id: string } }>('/v1/polls/:id/votes', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
||||
public votePoll() {
|
||||
this.fastify.post<{ Params: { id?: string }, Body: { choices?: number[] } }>('/v1/polls/:id/votes', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
const body: any = _request.body;
|
||||
try {
|
||||
const data = await client.votePoll(_request.params.id, body.choices);
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
if (!_request.body.choices) return reply.code(400).send({ error: 'Missing required payload "choices"' });
|
||||
const data = await client.votePoll(_request.params.id, _request.body.choices);
|
||||
reply.send(convertPoll(data.data));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
reply.code(401).send(e.response.data);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/polls/${_request.params.id}/votes`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async postStatus() {
|
||||
this.fastify.post('/v1/statuses', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
||||
public postStatus() {
|
||||
this.fastify.post<{
|
||||
Body: {
|
||||
media_ids?: string[],
|
||||
poll?: {
|
||||
options?: string[],
|
||||
expires_in?: string,
|
||||
multiple?: string,
|
||||
hide_totals?: string,
|
||||
},
|
||||
in_reply_to_id?: string,
|
||||
sensitive?: string,
|
||||
spoiler_text?: string,
|
||||
visibility?: 'public' | 'unlisted' | 'private' | 'direct',
|
||||
scheduled_at?: string,
|
||||
language?: string,
|
||||
quote_id?: string,
|
||||
status?: string,
|
||||
|
||||
// Broken clients
|
||||
'poll[options][]'?: string[],
|
||||
'media_ids[]'?: string[],
|
||||
}
|
||||
}>('/v1/statuses', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
let body: any = _request.body;
|
||||
let body = _request.body;
|
||||
try {
|
||||
if (
|
||||
(!body.poll && body['poll[options][]']) ||
|
||||
(!body.media_ids && body['media_ids[]'])
|
||||
if ((!body.poll && body['poll[options][]']) || (!body.media_ids && body['media_ids[]'])
|
||||
) {
|
||||
body = normalizeQuery(body);
|
||||
}
|
||||
const text = body.status ? body.status : ' ';
|
||||
const text = body.status ??= ' ';
|
||||
const removed = text.replace(/@\S+/g, '').replace(/\s|/g, '');
|
||||
const isDefaultEmoji = emojiRegexAtStartToEnd.test(removed);
|
||||
const isCustomEmoji = /^:[a-zA-Z0-9@_]+:$/.test(removed);
|
||||
@@ -189,226 +222,275 @@ export class ApiStatusMastodon {
|
||||
reply.send(a.data);
|
||||
}
|
||||
if (body.in_reply_to_id && removed === '/unreact') {
|
||||
try {
|
||||
const id = body.in_reply_to_id;
|
||||
const post = await client.getStatus(id);
|
||||
const react = post.data.emoji_reactions.filter((e: any) => e.me)[0].name;
|
||||
const data = await client.deleteEmojiReaction(id, react);
|
||||
reply.send(data.data);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
reply.code(401).send(e.response.data);
|
||||
}
|
||||
const id = body.in_reply_to_id;
|
||||
const post = await client.getStatus(id);
|
||||
const react = post.data.emoji_reactions.filter(e => e.me)[0].name;
|
||||
const data = await client.deleteEmojiReaction(id, react);
|
||||
reply.send(data.data);
|
||||
}
|
||||
if (!body.media_ids) body.media_ids = undefined;
|
||||
if (body.media_ids && !body.media_ids.length) body.media_ids = undefined;
|
||||
|
||||
const { sensitive } = body;
|
||||
body.sensitive = typeof sensitive === 'string' ? sensitive === 'true' : sensitive;
|
||||
|
||||
if (body.poll) {
|
||||
if (
|
||||
body.poll.expires_in != null &&
|
||||
typeof body.poll.expires_in === 'string'
|
||||
) body.poll.expires_in = parseInt(body.poll.expires_in);
|
||||
if (
|
||||
body.poll.multiple != null &&
|
||||
typeof body.poll.multiple === 'string'
|
||||
) body.poll.multiple = body.poll.multiple === 'true';
|
||||
if (
|
||||
body.poll.hide_totals != null &&
|
||||
typeof body.poll.hide_totals === 'string'
|
||||
) body.poll.hide_totals = body.poll.hide_totals === 'true';
|
||||
if (body.poll && !body.poll.options) {
|
||||
return reply.code(400).send({ error: 'Missing required payload "poll.options"' });
|
||||
}
|
||||
if (body.poll && !body.poll.expires_in) {
|
||||
return reply.code(400).send({ error: 'Missing required payload "poll.expires_in"' });
|
||||
}
|
||||
|
||||
const data = await client.postStatus(text, body);
|
||||
reply.send(await this.mastoconverter.convertStatus(data.data as Entity.Status));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
reply.code(401).send(e.response.data);
|
||||
const options = {
|
||||
...body,
|
||||
sensitive: toBoolean(body.sensitive),
|
||||
poll: body.poll ? {
|
||||
options: body.poll.options!, // eslint-disable-line @typescript-eslint/no-non-null-assertion
|
||||
expires_in: toInt(body.poll.expires_in)!, // eslint-disable-line @typescript-eslint/no-non-null-assertion
|
||||
multiple: toBoolean(body.poll.multiple),
|
||||
hide_totals: toBoolean(body.poll.hide_totals),
|
||||
} : undefined,
|
||||
};
|
||||
|
||||
const data = await client.postStatus(text, options);
|
||||
reply.send(await this.mastoConverters.convertStatus(data.data as Entity.Status));
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('POST /v1/statuses', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async updateStatus() {
|
||||
this.fastify.put<{ Params: { id: string } }>('/v1/statuses/:id', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
||||
public updateStatus() {
|
||||
this.fastify.put<{
|
||||
Params: { id: string },
|
||||
Body: {
|
||||
status?: string,
|
||||
spoiler_text?: string,
|
||||
sensitive?: string,
|
||||
media_ids?: string[],
|
||||
poll?: {
|
||||
options?: string[],
|
||||
expires_in?: string,
|
||||
multiple?: string,
|
||||
hide_totals?: string,
|
||||
},
|
||||
}
|
||||
}>('/v1/statuses/:id', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
const body: any = _request.body;
|
||||
try {
|
||||
if (!body.media_ids) body.media_ids = undefined;
|
||||
if (body.media_ids && !body.media_ids.length) body.media_ids = undefined;
|
||||
const data = await client.editStatus(_request.params.id, body);
|
||||
reply.send(await this.mastoconverter.convertStatus(data.data));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
reply.code(_request.is404 ? 404 : 401).send(e.response.data);
|
||||
const body = _request.body;
|
||||
|
||||
if (!body.media_ids || !body.media_ids.length) {
|
||||
body.media_ids = undefined;
|
||||
}
|
||||
|
||||
const options = {
|
||||
...body,
|
||||
sensitive: toBoolean(body.sensitive),
|
||||
poll: body.poll ? {
|
||||
options: body.poll.options,
|
||||
expires_in: toInt(body.poll.expires_in),
|
||||
multiple: toBoolean(body.poll.multiple),
|
||||
hide_totals: toBoolean(body.poll.hide_totals),
|
||||
} : undefined,
|
||||
};
|
||||
|
||||
const data = await client.editStatus(_request.params.id, options);
|
||||
reply.send(await this.mastoConverters.convertStatus(data.data));
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/statuses/${_request.params.id}`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async addFavourite() {
|
||||
this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/favourite', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
||||
public addFavourite() {
|
||||
this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/favourite', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = (await client.createEmojiReaction(_request.params.id, '❤')) as any;
|
||||
reply.send(await this.mastoconverter.convertStatus(data.data));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
reply.code(401).send(e.response.data);
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const data = await client.createEmojiReaction(_request.params.id, '❤');
|
||||
reply.send(await this.mastoConverters.convertStatus(data.data));
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/statuses/${_request.params.id}/favorite`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async rmFavourite() {
|
||||
this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/unfavourite', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
||||
public rmFavourite() {
|
||||
this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unfavourite', 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.deleteEmojiReaction(_request.params.id, '❤');
|
||||
reply.send(await this.mastoconverter.convertStatus(data.data));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
reply.code(401).send(e.response.data);
|
||||
reply.send(await this.mastoConverters.convertStatus(data.data));
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/statuses/${_request.params.id}/unfavorite`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async reblogStatus() {
|
||||
this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/reblog', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
||||
public reblogStatus() {
|
||||
this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/reblog', 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.reblogStatus(_request.params.id);
|
||||
reply.send(await this.mastoconverter.convertStatus(data.data));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
reply.code(401).send(e.response.data);
|
||||
reply.send(await this.mastoConverters.convertStatus(data.data));
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/statuses/${_request.params.id}/reblog`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async unreblogStatus() {
|
||||
this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/unreblog', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
||||
public unreblogStatus() {
|
||||
this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unreblog', 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.unreblogStatus(_request.params.id);
|
||||
reply.send(await this.mastoconverter.convertStatus(data.data));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
reply.code(401).send(e.response.data);
|
||||
reply.send(await this.mastoConverters.convertStatus(data.data));
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/statuses/${_request.params.id}/unreblog`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async bookmarkStatus() {
|
||||
this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/bookmark', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
||||
public bookmarkStatus() {
|
||||
this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/bookmark', 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.bookmarkStatus(_request.params.id);
|
||||
reply.send(await this.mastoconverter.convertStatus(data.data));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
reply.code(401).send(e.response.data);
|
||||
reply.send(await this.mastoConverters.convertStatus(data.data));
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/statuses/${_request.params.id}/bookmark`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async unbookmarkStatus() {
|
||||
this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/unbookmark', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
||||
public unbookmarkStatus() {
|
||||
this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unbookmark', 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.unbookmarkStatus(_request.params.id);
|
||||
reply.send(await this.mastoconverter.convertStatus(data.data));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
reply.code(401).send(e.response.data);
|
||||
reply.send(await this.mastoConverters.convertStatus(data.data));
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/statuses/${_request.params.id}/unbookmark`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async pinStatus() {
|
||||
this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/pin', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
||||
public pinStatus() {
|
||||
this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/pin', 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.pinStatus(_request.params.id);
|
||||
reply.send(await this.mastoconverter.convertStatus(data.data));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
reply.code(401).send(e.response.data);
|
||||
reply.send(await this.mastoConverters.convertStatus(data.data));
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/statuses/${_request.params.id}/pin`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async unpinStatus() {
|
||||
this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/unpin', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
||||
public unpinStatus() {
|
||||
this.fastify.post<{ Params: { id?: string } }>('/v1/statuses/:id/unpin', 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.unpinStatus(_request.params.id);
|
||||
reply.send(await this.mastoconverter.convertStatus(data.data));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
reply.code(401).send(e.response.data);
|
||||
reply.send(await this.mastoConverters.convertStatus(data.data));
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/statuses/${_request.params.id}/unpin`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async reactStatus() {
|
||||
this.fastify.post<{ Params: { id: string, name: string } }>('/v1/statuses/:id/react/:name', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
||||
public reactStatus() {
|
||||
this.fastify.post<{ Params: { id?: string, name?: string } }>('/v1/statuses/:id/react/:name', 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"' });
|
||||
if (!_request.params.name) return reply.code(400).send({ error: 'Missing required parameter "name"' });
|
||||
const data = await client.createEmojiReaction(_request.params.id, _request.params.name);
|
||||
reply.send(await this.mastoconverter.convertStatus(data.data));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
reply.code(401).send(e.response.data);
|
||||
reply.send(await this.mastoConverters.convertStatus(data.data));
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/statuses/${_request.params.id}/react/${_request.params.name}`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async unreactStatus() {
|
||||
this.fastify.post<{ Params: { id: string, name: string } }>('/v1/statuses/:id/unreact/:name', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
||||
public unreactStatus() {
|
||||
this.fastify.post<{ Params: { id?: string, name?: string } }>('/v1/statuses/:id/unreact/:name', 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"' });
|
||||
if (!_request.params.name) return reply.code(400).send({ error: 'Missing required parameter "name"' });
|
||||
const data = await client.deleteEmojiReaction(_request.params.id, _request.params.name);
|
||||
reply.send(await this.mastoconverter.convertStatus(data.data));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
reply.code(401).send(e.response.data);
|
||||
reply.send(await this.mastoConverters.convertStatus(data.data));
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/statuses/${_request.params.id}/unreact/${_request.params.name}`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async deleteStatus() {
|
||||
this.fastify.delete<{ Params: { id: string } }>('/v1/statuses/:id', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
||||
public deleteStatus() {
|
||||
this.fastify.delete<{ Params: { id?: string } }>('/v1/statuses/:id', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const data = await client.deleteStatus(_request.params.id);
|
||||
reply.send(data.data);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
reply.code(401).send(e.response.data);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`DELETE /v1/statuses/${_request.params.id}`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,270 +3,238 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { ParsedUrlQuery } from 'querystring';
|
||||
import { getErrorData, MastodonLogger } from '@/server/api/mastodon/MastodonLogger.js';
|
||||
import { convertConversation, convertList, MastoConverters } from '../converters.js';
|
||||
import { getClient } from '../MastodonApiServerService.js';
|
||||
import { parseTimelineArgs, TimelineArgs, toBoolean } from '../timelineArgs.js';
|
||||
import type { Entity } from 'megalodon';
|
||||
import type { FastifyInstance } from 'fastify';
|
||||
import type { Config } from '@/config.js';
|
||||
import { NoteEditRepository, NotesRepository, UsersRepository } from '@/models/_.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
|
||||
export function limitToInt(q: ParsedUrlQuery) {
|
||||
const object: any = q;
|
||||
if (q.limit) if (typeof q.limit === 'string') object.limit = parseInt(q.limit, 10);
|
||||
if (q.offset) if (typeof q.offset === 'string') object.offset = parseInt(q.offset, 10);
|
||||
return object;
|
||||
}
|
||||
|
||||
export function argsToBools(q: ParsedUrlQuery) {
|
||||
// Values taken from https://docs.joinmastodon.org/client/intro/#boolean
|
||||
const toBoolean = (value: string) =>
|
||||
!['0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].includes(value);
|
||||
|
||||
// Keys taken from:
|
||||
// - https://docs.joinmastodon.org/methods/accounts/#statuses
|
||||
// - https://docs.joinmastodon.org/methods/timelines/#public
|
||||
// - https://docs.joinmastodon.org/methods/timelines/#tag
|
||||
const object: any = q;
|
||||
if (q.only_media) if (typeof q.only_media === 'string') object.only_media = toBoolean(q.only_media);
|
||||
if (q.exclude_replies) if (typeof q.exclude_replies === 'string') object.exclude_replies = toBoolean(q.exclude_replies);
|
||||
if (q.exclude_reblogs) if (typeof q.exclude_reblogs === 'string') object.exclude_reblogs = toBoolean(q.exclude_reblogs);
|
||||
if (q.pinned) if (typeof q.pinned === 'string') object.pinned = toBoolean(q.pinned);
|
||||
if (q.local) if (typeof q.local === 'string') object.local = toBoolean(q.local);
|
||||
return q;
|
||||
}
|
||||
|
||||
export class ApiTimelineMastodon {
|
||||
private fastify: FastifyInstance;
|
||||
constructor(
|
||||
private readonly fastify: FastifyInstance,
|
||||
private readonly mastoConverters: MastoConverters,
|
||||
private readonly logger: MastodonLogger,
|
||||
) {}
|
||||
|
||||
constructor(fastify: FastifyInstance, config: Config, private mastoconverter: MastoConverters) {
|
||||
this.fastify = fastify;
|
||||
}
|
||||
|
||||
public async getTL() {
|
||||
this.fastify.get('/v1/timelines/public', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
||||
public getTL() {
|
||||
this.fastify.get<{ Querystring: TimelineArgs }>('/v1/timelines/public', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const query: any = _request.query;
|
||||
const data = query.local === 'true'
|
||||
? await client.getLocalTimeline(argsToBools(limitToInt(query)))
|
||||
: await client.getPublicTimeline(argsToBools(limitToInt(query)));
|
||||
reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status))));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
reply.code(401).send(e.response.data);
|
||||
const 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))));
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('GET /v1/timelines/public', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async getHomeTl() {
|
||||
this.fastify.get('/v1/timelines/home', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
||||
public getHomeTl() {
|
||||
this.fastify.get<{ Querystring: TimelineArgs }>('/v1/timelines/home', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const query: any = _request.query;
|
||||
const data = await client.getHomeTimeline(limitToInt(query));
|
||||
reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status))));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
reply.code(401).send(e.response.data);
|
||||
const data = await client.getHomeTimeline(parseTimelineArgs(_request.query));
|
||||
reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoConverters.convertStatus(status))));
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('GET /v1/timelines/home', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async getTagTl() {
|
||||
this.fastify.get<{ Params: { hashtag: string } }>('/v1/timelines/tag/:hashtag', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
||||
public getTagTl() {
|
||||
this.fastify.get<{ Params: { hashtag?: string }, Querystring: TimelineArgs }>('/v1/timelines/tag/:hashtag', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const query: any = _request.query;
|
||||
const params: any = _request.params;
|
||||
const data = await client.getTagTimeline(params.hashtag, limitToInt(query));
|
||||
reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status))));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
reply.code(401).send(e.response.data);
|
||||
if (!_request.params.hashtag) return reply.code(400).send({ error: 'Missing required parameter "hashtag"' });
|
||||
const 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))));
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/timelines/tag/${_request.params.hashtag}`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async getListTL() {
|
||||
this.fastify.get<{ Params: { id: string } }>('/v1/timelines/list/:id', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
||||
public getListTL() {
|
||||
this.fastify.get<{ Params: { id?: string }, Querystring: TimelineArgs }>('/v1/timelines/list/:id', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const query: any = _request.query;
|
||||
const params: any = _request.params;
|
||||
const data = await client.getListTimeline(params.id, limitToInt(query));
|
||||
reply.send(await Promise.all(data.data.map(async (status: Entity.Status) => await this.mastoconverter.convertStatus(status))));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
reply.code(401).send(e.response.data);
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const 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))));
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/timelines/list/${_request.params.id}`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async getConversations() {
|
||||
this.fastify.get('/v1/conversations', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
||||
public getConversations() {
|
||||
this.fastify.get<{ Querystring: TimelineArgs }>('/v1/conversations', async (_request, reply) => {
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const query: any = _request.query;
|
||||
const data = await client.getConversationTimeline(limitToInt(query));
|
||||
const data = await client.getConversationTimeline(parseTimelineArgs(_request.query));
|
||||
reply.send(data.data.map((conversation: Entity.Conversation) => convertConversation(conversation)));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
reply.code(401).send(e.response.data);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('GET /v1/conversations', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async getList() {
|
||||
this.fastify.get<{ Params: { id: string } }>('/v1/lists/:id', async (_request, reply) => {
|
||||
public getList() {
|
||||
this.fastify.get<{ Params: { id?: string } }>('/v1/lists/:id', async (_request, reply) => {
|
||||
try {
|
||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
const params: any = _request.params;
|
||||
const data = await client.getList(params.id);
|
||||
const data = await client.getList(_request.params.id);
|
||||
reply.send(convertList(data.data));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
reply.code(401).send(e.response.data);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/lists/${_request.params.id}`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async getLists() {
|
||||
public getLists() {
|
||||
this.fastify.get('/v1/lists', async (_request, reply) => {
|
||||
try {
|
||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
const data = await client.getLists();
|
||||
reply.send(data.data.map((list: Entity.List) => convertList(list)));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
return e.response.data;
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('GET /v1/lists', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async getListAccounts() {
|
||||
this.fastify.get<{ Params: { id: string } }>('/v1/lists/:id/accounts', async (_request, reply) => {
|
||||
public getListAccounts() {
|
||||
this.fastify.get<{ Params: { id?: string }, Querystring: { limit?: number, max_id?: string, since_id?: string } }>('/v1/lists/:id/accounts', async (_request, reply) => {
|
||||
try {
|
||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
const params: any = _request.params;
|
||||
const query: any = _request.query;
|
||||
const data = await client.getAccountsInList(params.id, query);
|
||||
reply.send(data.data.map((account: Entity.Account) => this.mastoconverter.convertAccount(account)));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
reply.code(401).send(e.response.data);
|
||||
const data = await client.getAccountsInList(_request.params.id, _request.query);
|
||||
reply.send(data.data.map((account: Entity.Account) => this.mastoConverters.convertAccount(account)));
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`GET /v1/lists/${_request.params.id}/accounts`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async addListAccount() {
|
||||
this.fastify.post<{ Params: { id: string } }>('/v1/lists/:id/accounts', async (_request, reply) => {
|
||||
public addListAccount() {
|
||||
this.fastify.post<{ Params: { id?: string }, Querystring: { accounts_id?: string[] } }>('/v1/lists/:id/accounts', async (_request, reply) => {
|
||||
try {
|
||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
if (!_request.query.accounts_id) return reply.code(400).send({ error: 'Missing required property "accounts_id"' });
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
const params: any = _request.params;
|
||||
const query: any = _request.query;
|
||||
const data = await client.addAccountsToList(params.id, query.accounts_id);
|
||||
const data = await client.addAccountsToList(_request.params.id, _request.query.accounts_id);
|
||||
reply.send(data.data);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
reply.code(401).send(e.response.data);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`POST /v1/lists/${_request.params.id}/accounts`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async rmListAccount() {
|
||||
this.fastify.delete<{ Params: { id: string } }>('/v1/lists/:id/accounts', async (_request, reply) => {
|
||||
public rmListAccount() {
|
||||
this.fastify.delete<{ Params: { id?: string }, Querystring: { accounts_id?: string[] } }>('/v1/lists/:id/accounts', async (_request, reply) => {
|
||||
try {
|
||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
if (!_request.query.accounts_id) return reply.code(400).send({ error: 'Missing required property "accounts_id"' });
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
const params: any = _request.params;
|
||||
const query: any = _request.query;
|
||||
const data = await client.deleteAccountsFromList(params.id, query.accounts_id);
|
||||
const data = await client.deleteAccountsFromList(_request.params.id, _request.query.accounts_id);
|
||||
reply.send(data.data);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
reply.code(401).send(e.response.data);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`DELETE /v1/lists/${_request.params.id}/accounts`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async createList() {
|
||||
this.fastify.post('/v1/lists', async (_request, reply) => {
|
||||
public createList() {
|
||||
this.fastify.post<{ Body: { title?: string } }>('/v1/lists', async (_request, reply) => {
|
||||
try {
|
||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
||||
if (!_request.body.title) return reply.code(400).send({ error: 'Missing required payload "title"' });
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
const body: any = _request.body;
|
||||
const data = await client.createList(body.title);
|
||||
const data = await client.createList(_request.body.title);
|
||||
reply.send(convertList(data.data));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
reply.code(401).send(e.response.data);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error('POST /v1/lists', data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async updateList() {
|
||||
this.fastify.put<{ Params: { id: string } }>('/v1/lists/:id', async (_request, reply) => {
|
||||
public updateList() {
|
||||
this.fastify.put<{ Params: { id?: string }, Body: { title?: string } }>('/v1/lists/:id', async (_request, reply) => {
|
||||
try {
|
||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
if (!_request.body.title) return reply.code(400).send({ error: 'Missing required payload "title"' });
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
const body: any = _request.body;
|
||||
const params: any = _request.params;
|
||||
const data = await client.updateList(params.id, body.title);
|
||||
const data = await client.updateList(_request.params.id, _request.body.title);
|
||||
reply.send(convertList(data.data));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
reply.code(401).send(e.response.data);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`PUT /v1/lists/${_request.params.id}`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async deleteList() {
|
||||
this.fastify.delete<{ Params: { id: string } }>('/v1/lists/:id', async (_request, reply) => {
|
||||
public deleteList() {
|
||||
this.fastify.delete<{ Params: { id?: string } }>('/v1/lists/:id', async (_request, reply) => {
|
||||
try {
|
||||
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
|
||||
if (!_request.params.id) return reply.code(400).send({ error: 'Missing required parameter "id"' });
|
||||
const BASE_URL = `${_request.protocol}://${_request.host}`;
|
||||
const accessTokens = _request.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
const params: any = _request.params;
|
||||
const data = await client.deleteList(params.id);
|
||||
await client.deleteList(_request.params.id);
|
||||
reply.send({});
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
console.error(e.response.data);
|
||||
reply.code(401).send(e.response.data);
|
||||
} catch (e) {
|
||||
const data = getErrorData(e);
|
||||
this.logger.error(`DELETE /v1/lists/${_request.params.id}`, data);
|
||||
reply.code(401).send(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user