diff --git a/packages/backend/src/server/api/mastodon/endpoints/account.ts b/packages/backend/src/server/api/mastodon/endpoints/account.ts
index f72b4657d..36b49c900 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/account.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/account.ts
@@ -71,7 +71,7 @@ export class ApiAccountMastodon {
public async lookup() {
try {
- const data = await this.client.search((this.request.query as any).acct, 'accounts');
+ const data = await this.client.search((this.request.query as any).acct, { type: 'accounts' });
return convertAccount(data.data.accounts[0]);
} catch (e: any) {
/* console.error(e);
diff --git a/packages/backend/src/server/api/mastodon/endpoints/search.ts b/packages/backend/src/server/api/mastodon/endpoints/search.ts
index 79195ee9a..5c68402ed 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/search.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/search.ts
@@ -23,7 +23,7 @@ async function getHighlight(
});
const api = await apicall.json();
const data: MisskeyEntity.Note[] = api;
- return data.map((note) => new Converter(BASE_URL).note(note, domain));
+ return data.map((note) => Converter.note(note, domain));
} catch (e: any) {
console.log(e);
console.log(e.response.data);
@@ -49,7 +49,7 @@ async function getFeaturedUser( BASE_URL: string, host: string, accessTokens: st
return data.map((u) => {
return {
source: 'past_interactions',
- account: new Converter(BASE_URL).userDetail(u, host),
+ account: Converter.userDetail(u, host),
};
});
} catch (e: any) {
diff --git a/packages/backend/src/server/api/mastodon/endpoints/status.ts b/packages/backend/src/server/api/mastodon/endpoints/status.ts
index b3faa7f0e..5ce0c8941 100644
--- a/packages/backend/src/server/api/mastodon/endpoints/status.ts
+++ b/packages/backend/src/server/api/mastodon/endpoints/status.ts
@@ -171,7 +171,7 @@ export class ApiStatusMastodon {
try {
const id = body.in_reply_to_id;
const post = await client.getStatus(id);
- const react = post.data.reactions.filter((e: any) => e.me)[0].name;
+ 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) {
diff --git a/packages/backend/src/server/oauth/oauth2.txt b/packages/backend/src/server/oauth/oauth2.txt
deleted file mode 100644
index cd96cda12..000000000
--- a/packages/backend/src/server/oauth/oauth2.txt
+++ /dev/null
@@ -1,489 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and other misskey contributors
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import dns from 'node:dns/promises';
-import { fileURLToPath } from 'node:url';
-import { Inject, Injectable } from '@nestjs/common';
-import { JSDOM } from 'jsdom';
-import httpLinkHeader from 'http-link-header';
-import ipaddr from 'ipaddr.js';
-import oauth2orize, { type OAuth2, AuthorizationError, ValidateFunctionArity2, OAuth2Req, MiddlewareRequest } from 'oauth2orize';
-import oauth2Pkce from 'oauth2orize-pkce';
-import fastifyView from '@fastify/view';
-import pug from 'pug';
-import bodyParser from 'body-parser';
-import fastifyExpress from '@fastify/express';
-import { verifyChallenge } from 'pkce-challenge';
-import { mf2 } from 'microformats-parser';
-import { secureRndstr } from '@/misc/secure-rndstr.js';
-import { HttpRequestService } from '@/core/HttpRequestService.js';
-import { kinds } from '@/misc/api-permissions.js';
-import type { Config } from '@/config.js';
-import { DI } from '@/di-symbols.js';
-import { bindThis } from '@/decorators.js';
-import type { AccessTokensRepository, UsersRepository } from '@/models/_.js';
-import { IdService } from '@/core/IdService.js';
-import { CacheService } from '@/core/CacheService.js';
-import type { MiLocalUser } from '@/models/User.js';
-import { MemoryKVCache } from '@/misc/cache.js';
-import { LoggerService } from '@/core/LoggerService.js';
-import Logger from '@/logger.js';
-import { StatusError } from '@/misc/status-error.js';
-import type { ServerResponse } from 'node:http';
-import type { FastifyInstance } from 'fastify';
-const base64regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/;
-
-// TODO: Consider migrating to @node-oauth/oauth2-server once
-// https://github.com/node-oauth/node-oauth2-server/issues/180 is figured out.
-// Upstream the various validations and RFC9207 implementation in that case.
-
-// Follows https://indieauth.spec.indieweb.org/#client-identifier
-// This is also mostly similar to https://developers.google.com/identity/protocols/oauth2/web-server#uri-validation
-// although Google has stricter rule.
-function validateClientId(raw: string): URL {
- // "Clients are identified by a [URL]."
- const url = ((): URL => {
- try {
- if (base64regex.test(raw)) return new URL(atob(raw));
- return new URL(raw);
- } catch { throw new AuthorizationError('client_id must be a valid URL', 'invalid_request'); }
- })();
-
- // "Client identifier URLs MUST have either an https or http scheme"
- // But then again:
- // https://datatracker.ietf.org/doc/html/rfc6749.html#section-3.1.2.1
- // 'The redirection endpoint SHOULD require the use of TLS as described
- // in Section 1.6 when the requested response type is "code" or "token"'
- const allowedProtocols = process.env.NODE_ENV === 'test' ? ['http:', 'https:'] : ['https:'];
- if (!allowedProtocols.includes(url.protocol)) {
- throw new AuthorizationError('client_id must be a valid HTTPS URL', 'invalid_request');
- }
-
- // "MUST contain a path component (new URL() implicitly adds one)"
-
- // "MUST NOT contain single-dot or double-dot path segments,"
- const segments = url.pathname.split('/');
- if (segments.includes('.') || segments.includes('..')) {
- throw new AuthorizationError('client_id must not contain dot path segments', 'invalid_request');
- }
-
- // ("MAY contain a query string component")
-
- // "MUST NOT contain a fragment component"
- if (url.hash) {
- throw new AuthorizationError('client_id must not contain a fragment component', 'invalid_request');
- }
-
- // "MUST NOT contain a username or password component"
- if (url.username || url.password) {
- throw new AuthorizationError('client_id must not contain a username or a password', 'invalid_request');
- }
-
- // ("MAY contain a port")
-
- // "host names MUST be domain names or a loopback interface and MUST NOT be
- // IPv4 or IPv6 addresses except for IPv4 127.0.0.1 or IPv6 [::1]."
- if (!url.hostname.match(/\.\w+$/) && !['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)) {
- throw new AuthorizationError('client_id must have a domain name as a host name', 'invalid_request');
- }
-
- return url;
-}
-
-interface ClientInformation {
- id: string;
- redirectUris: string[];
- name: string;
-}
-
-// https://indieauth.spec.indieweb.org/#client-information-discovery
-// "Authorization servers SHOULD support parsing the [h-app] Microformat from the client_id,
-// and if there is an [h-app] with a url property matching the client_id URL,
-// then it should use the name and icon and display them on the authorization prompt."
-// (But we don't display any icon for now)
-// https://indieauth.spec.indieweb.org/#redirect-url
-// "The client SHOULD publish one or more tags or Link HTTP headers with a rel attribute
-// of redirect_uri at the client_id URL.
-// Authorization endpoints verifying that a redirect_uri is allowed for use by a client MUST
-// look for an exact match of the given redirect_uri in the request against the list of
-// redirect_uris discovered after resolving any relative URLs."
-async function discoverClientInformation(logger: Logger, httpRequestService: HttpRequestService, id: string): Promise {
- try {
- const res = await httpRequestService.send(id);
- const redirectUris: string[] = [];
-
- const linkHeader = res.headers.get('link');
- if (linkHeader) {
- redirectUris.push(...httpLinkHeader.parse(linkHeader).get('rel', 'redirect_uri').map(r => r.uri));
- }
-
- const text = await res.text();
- const fragment = JSDOM.fragment(text);
-
- redirectUris.push(...[...fragment.querySelectorAll('link[rel=redirect_uri][href]')].map(el => el.href));
-
- let name = id;
- if (text) {
- const microformats = mf2(text, { baseUrl: res.url });
- const nameProperty = microformats.items.find(item => item.type?.includes('h-app') && item.properties.url.includes(id))?.properties.name[0];
- if (typeof nameProperty === 'string') {
- name = nameProperty;
- }
- }
-
- return {
- id,
- redirectUris: redirectUris.map(uri => new URL(uri, res.url).toString()),
- name: typeof name === 'string' ? name : id,
- };
- } catch (err) {
- console.error(err);
- logger.error('Error while fetching client information', { err });
- if (err instanceof StatusError) {
- throw new AuthorizationError('Failed to fetch client information', 'invalid_request');
- } else {
- throw new AuthorizationError('Failed to parse client information', 'server_error');
- }
- }
-}
-
-type OmitFirstElement = T extends [unknown, ...(infer R)]
- ? R
- : [];
-
-interface OAuthParsedRequest extends OAuth2Req {
- codeChallenge: string;
- codeChallengeMethod: string;
-}
-
-interface OAuthHttpResponse extends ServerResponse {
- redirect(location: string): void;
-}
-
-interface OAuth2DecisionRequest extends MiddlewareRequest {
- body: {
- transaction_id: string;
- cancel: boolean;
- login_token: string;
- }
-}
-
-function getQueryMode(issuerUrl: string): oauth2orize.grant.Options['modes'] {
- return {
- query: (txn, res, params): void => {
- // https://datatracker.ietf.org/doc/html/rfc9207#name-response-parameter-iss
- // "In authorization responses to the client, including error responses,
- // an authorization server supporting this specification MUST indicate its
- // identity by including the iss parameter in the response."
- params.iss = issuerUrl;
-
- const parsed = new URL(txn.redirectURI);
- for (const [key, value] of Object.entries(params)) {
- parsed.searchParams.append(key, value as string);
- }
-
- return (res as OAuthHttpResponse).redirect(parsed.toString());
- },
- };
-}
-
-/**
- * Maps the transaction ID and the oauth/authorize parameters.
- *
- * Flow:
- * 1. oauth/authorize endpoint will call store() to store the parameters
- * and puts the generated transaction ID to the dialog page
- * 2. oauth/decision will call load() to retrieve the parameters and then remove()
- */
-class OAuth2Store {
- #cache = new MemoryKVCache(1000 * 60 * 5); // expires after 5min
-
- load(req: OAuth2DecisionRequest, cb: (err: Error | null, txn?: OAuth2) => void): void {
- const { transaction_id } = req.body;
- if (!transaction_id) {
- cb(new AuthorizationError('Missing transaction ID', 'invalid_request'));
- return;
- }
- const loaded = this.#cache.get(transaction_id);
- if (!loaded) {
- cb(new AuthorizationError('Invalid or expired transaction ID', 'access_denied'));
- return;
- }
- cb(null, loaded);
- }
-
- store(req: OAuth2DecisionRequest, oauth2: OAuth2, cb: (err: Error | null, transactionID?: string) => void): void {
- const transactionId = secureRndstr(128);
- this.#cache.set(transactionId, oauth2);
- cb(null, transactionId);
- }
-
- remove(req: OAuth2DecisionRequest, tid: string, cb: () => void): void {
- this.#cache.delete(tid);
- cb();
- }
-}
-
-@Injectable()
-export class OAuth2ProviderService {
- #server = oauth2orize.createServer({
- store: new OAuth2Store(),
- });
- #logger: Logger;
-
- constructor(
- @Inject(DI.config)
- private config: Config,
- private httpRequestService: HttpRequestService,
- @Inject(DI.accessTokensRepository)
- accessTokensRepository: AccessTokensRepository,
- idService: IdService,
- @Inject(DI.usersRepository)
- private usersRepository: UsersRepository,
- private cacheService: CacheService,
- loggerService: LoggerService,
- ) {
- this.#logger = loggerService.getLogger('oauth');
-
- const grantCodeCache = new MemoryKVCache<{
- clientId: string,
- userId: string,
- redirectUri: string,
- codeChallenge: string,
- scopes: string[],
-
- // fields to prevent multiple code use
- grantedToken?: string,
- revoked?: boolean,
- used?: boolean,
- }>(1000 * 60 * 5); // expires after 5m
-
- // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics
- // "Authorization servers MUST support PKCE [RFC7636]."
- this.#server.grant(oauth2Pkce.extensions());
- this.#server.grant(oauth2orize.grant.code({
- modes: getQueryMode(config.url),
- }, (client, redirectUri, token, ares, areq, locals, done) => {
- (async (): Promise>> => {
- this.#logger.info(`Checking the user before sending authorization code to ${client.id}`);
-
- if (!token) {
- throw new AuthorizationError('No user', 'invalid_request');
- }
- const user = await this.cacheService.localUserByNativeTokenCache.fetch(token,
- () => this.usersRepository.findOneBy({ token }) as Promise);
- if (!user) {
- throw new AuthorizationError('No such user', 'invalid_request');
- }
-
- this.#logger.info(`Sending authorization code on behalf of user ${user.id} to ${client.id} through ${redirectUri}, with scope: [${areq.scope}]`);
-
- const code = secureRndstr(128);
- grantCodeCache.set(code, {
- clientId: client.id,
- userId: user.id,
- redirectUri,
- codeChallenge: (areq as OAuthParsedRequest).codeChallenge,
- scopes: areq.scope,
- });
- return [code];
- })().then(args => done(null, ...args), err => done(err));
- }));
- this.#server.exchange(oauth2orize.exchange.authorizationCode((client, code, redirectUri, body, authInfo, done) => {
- (async (): Promise> | undefined> => {
- this.#logger.info('Checking the received authorization code for the exchange');
- const granted = grantCodeCache.get(code);
- if (!granted) {
- return;
- }
-
- // https://datatracker.ietf.org/doc/html/rfc6749.html#section-4.1.2
- // "If an authorization code is used more than once, the authorization server
- // MUST deny the request and SHOULD revoke (when possible) all tokens
- // previously issued based on that authorization code."
- if (granted.used) {
- this.#logger.info(`Detected multiple code use from ${granted.clientId} for user ${granted.userId}. Revoking the code.`);
- grantCodeCache.delete(code);
- granted.revoked = true;
- if (granted.grantedToken) {
- await accessTokensRepository.delete({ token: granted.grantedToken });
- }
- return;
- }
- granted.used = true;
-
- // https://datatracker.ietf.org/doc/html/rfc6749.html#section-4.1.3
- if (body.client_id !== granted.clientId) return;
- if (redirectUri !== granted.redirectUri) return;
-
- // https://datatracker.ietf.org/doc/html/rfc7636.html#section-4.6
- if (!body.code_verifier) return;
- if (!(await verifyChallenge(body.code_verifier as string, granted.codeChallenge))) return;
-
- const accessToken = secureRndstr(128);
- const now = new Date();
-
- // NOTE: we don't have a setup for automatic token expiration
- await accessTokensRepository.insert({
- id: idService.genId(),
- createdAt: now,
- lastUsedAt: now,
- userId: granted.userId,
- token: accessToken,
- hash: accessToken,
- name: granted.clientId,
- permission: granted.scopes,
- });
-
- if (granted.revoked) {
- this.#logger.info('Canceling the token as the authorization code was revoked in parallel during the process.');
- await accessTokensRepository.delete({ token: accessToken });
- return;
- }
-
- granted.grantedToken = accessToken;
- this.#logger.info(`Generated access token for ${granted.clientId} for user ${granted.userId}, with scope: [${granted.scopes}]`);
-
- return [accessToken, undefined, { scope: granted.scopes.join(' ') }];
- })().then(args => done(null, ...args ?? []), err => done(err));
- }));
- }
-
- @bindThis
- public async createServer(fastify: FastifyInstance): Promise {
- // https://datatracker.ietf.org/doc/html/rfc8414.html
- // https://indieauth.spec.indieweb.org/#indieauth-server-metadata
- fastify.get('/.well-known/oauth-authorization-server', async (_request, reply) => {
- reply.send({
- issuer: this.config.url,
- authorization_endpoint: new URL('/oauth/authorize', this.config.url),
- token_endpoint: new URL('/oauth/token', this.config.url),
- scopes_supported: kinds,
- response_types_supported: ['code'],
- grant_types_supported: ['authorization_code'],
- service_documentation: 'https://misskey-hub.net',
- code_challenge_methods_supported: ['S256'],
- authorization_response_iss_parameter_supported: true,
- });
- });
-
- fastify.get('/oauth/authorize', async (request, reply) => {
- const oauth2 = (request.raw as MiddlewareRequest).oauth2;
- if (!oauth2) {
- throw new Error('Unexpected lack of authorization information');
- }
-
- this.#logger.info(`Rendering authorization page for "${oauth2.client.name}"`);
-
- reply.header('Cache-Control', 'no-store');
- return await reply.view('oauth', {
- transactionId: oauth2.transactionID,
- clientName: oauth2.client.name,
- scope: oauth2.req.scope.join(' '),
- });
- });
- fastify.post('/oauth/decision', async () => { });
- fastify.post('/oauth/token', async () => { });
-
- fastify.register(fastifyView, {
- root: fileURLToPath(new URL('../web/views', import.meta.url)),
- engine: { pug },
- defaultContext: {
- version: this.config.version,
- config: this.config,
- },
- });
-
- await fastify.register(fastifyExpress);
- fastify.use('/oauth/authorize', this.#server.authorize(((areq, done) => {
- (async (): Promise> => {
- // This should return client/redirectURI AND the error, or
- // the handler can't send error to the redirection URI
-
- const { codeChallenge, codeChallengeMethod, clientID, redirectURI, scope } = areq as OAuthParsedRequest;
-
- this.#logger.info(`Validating authorization parameters, with client_id: ${clientID}, redirect_uri: ${redirectURI}, scope: ${scope}`);
-
- const clientUrl = validateClientId(clientID);
-
- // https://indieauth.spec.indieweb.org/#client-information-discovery
- // "the server may want to resolve the domain name first and avoid fetching the document
- // if the IP address is within the loopback range defined by [RFC5735]
- // or any other implementation-specific internal IP address."
- if (process.env.NODE_ENV !== 'test' || process.env.MISSKEY_TEST_CHECK_IP_RANGE === '1') {
- const lookup = await dns.lookup(clientUrl.hostname);
- if (ipaddr.parse(lookup.address).range() !== 'unicast') {
- throw new AuthorizationError('client_id resolves to disallowed IP range.', 'invalid_request');
- }
- }
-
- // Find client information from the remote.
- const clientInfo = await discoverClientInformation(this.#logger, this.httpRequestService, clientUrl.href);
-
- // Require the redirect URI to be included in an explicit list, per
- // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-4.1.3
- /* if (!clientInfo.redirectUris.includes(redirectURI)) {
- throw new AuthorizationError('Invalid redirect_uri', 'invalid_request');
- } */
-
- try {
- const scopes = [...new Set(scope)].filter(s => kinds.includes(s));
- if (!scopes.length) {
- throw new AuthorizationError('`scope` parameter has no known scope', 'invalid_scope');
- }
- areq.scope = scopes;
-
- // Require PKCE parameters.
- // Recommended by https://indieauth.spec.indieweb.org/#authorization-request, but also prevents downgrade attack:
- // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#name-pkce-downgrade-attack
- if (typeof codeChallenge !== 'string') {
- throw new AuthorizationError('`code_challenge` parameter is required', 'invalid_request');
- }
- if (codeChallengeMethod !== 'S256') {
- throw new AuthorizationError('`code_challenge_method` parameter must be set as S256', 'invalid_request');
- }
- } catch (err) {
- return [err as Error, clientInfo, redirectURI];
- }
-
- return [null, clientInfo, redirectURI];
- })().then(args => done(...args), err => done(err));
- }) as ValidateFunctionArity2));
- fastify.use('/oauth/authorize', this.#server.errorHandler({
- mode: 'indirect',
- modes: getQueryMode(this.config.url),
- }));
- fastify.use('/oauth/authorize', this.#server.errorHandler());
-
- fastify.use('/oauth/decision', bodyParser.urlencoded({ extended: false }));
- fastify.use('/oauth/decision', this.#server.decision((req, done) => {
- const { body } = req as OAuth2DecisionRequest;
- this.#logger.info(`Received the decision. Cancel: ${!!body.cancel}`);
- req.user = body.login_token;
- done(null, undefined);
- }));
- fastify.use('/oauth/decision', this.#server.errorHandler());
-
- // Clients may use JSON or urlencoded
- fastify.use('/oauth/token', bodyParser.urlencoded({ extended: false }));
- fastify.use('/oauth/token', bodyParser.json({ strict: true }));
- fastify.use('/oauth/token', this.#server.token());
- fastify.use('/oauth/token', this.#server.errorHandler());
-
- // Return 404 for any unknown paths under /oauth so that clients can know
- // whether a certain endpoint is supported or not.
- fastify.all('/oauth/*', async (_request, reply) => {
- reply.code(404);
- reply.send({
- error: {
- message: 'Unknown OAuth endpoint.',
- code: 'UNKNOWN_OAUTH_ENDPOINT',
- id: 'aa49e620-26cb-4e28-aad6-8cbcb58db147',
- kind: 'client',
- },
- });
- });
- }
-}
diff --git a/packages/megalodon/.npmignore b/packages/megalodon/.npmignore
new file mode 100644
index 000000000..fd54d1deb
--- /dev/null
+++ b/packages/megalodon/.npmignore
@@ -0,0 +1,3 @@
+node_modules
+./src
+tsconfig.json
diff --git a/packages/megalodon/package.json b/packages/megalodon/package.json
index 3403b94b4..ebd958834 100644
--- a/packages/megalodon/package.json
+++ b/packages/megalodon/package.json
@@ -1,16 +1,35 @@
{
"name": "megalodon",
- "private": true,
+ "version": "7.0.1",
+ "description": "Mastodon API client for node.js and browser",
"main": "./lib/src/index.js",
"typings": "./lib/src/index.d.ts",
"scripts": {
"build": "tsc -p ./",
- "build:debug": "pnpm run build",
- "lint": "pnpm biome check **/*.ts --apply",
- "format": "pnpm biome format --write src/**/*.ts",
+ "lint": "eslint --ext .js,.ts src",
"doc": "typedoc --out ../docs ./src",
"test": "NODE_ENV=test jest -u --maxWorkers=3"
},
+ "engines": {
+ "node": ">=15.0.0"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/h3poteto/megalodon.git"
+ },
+ "keywords": [
+ "mastodon",
+ "client",
+ "api",
+ "streaming",
+ "rest",
+ "proxy"
+ ],
+ "author": "h3poteto",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/h3poteto/megalodon/issues"
+ },
"jest": {
"moduleFileExtensions": [
"ts",
@@ -25,59 +44,44 @@
],
"preset": "ts-jest/presets/default",
"transform": {
- "^.+\\.(ts|tsx)$": "ts-jest"
- },
- "globals": {
- "ts-jest": {
+ "^.+\\.(ts|tsx)$": ["ts-jest", {
"tsconfig": "tsconfig.json"
- }
+ }]
},
"testEnvironment": "node"
},
+ "homepage": "https://github.com/h3poteto/megalodon#readme",
"dependencies": {
- "@types/oauth": "^0.9.0",
- "@types/ws": "^8.5.4",
- "axios": "1.2.2",
- "dayjs": "^1.11.7",
+ "@types/oauth": "^0.9.2",
+ "@types/ws": "^8.5.5",
+ "axios": "1.5.0",
+ "dayjs": "^1.11.9",
"form-data": "^4.0.0",
- "https-proxy-agent": "^5.0.1",
+ "https-proxy-agent": "^7.0.2",
"oauth": "^0.10.0",
"object-assign-deep": "^0.4.0",
"parse-link-header": "^2.0.0",
- "socks-proxy-agent": "^7.0.0",
- "typescript": "4.9.4",
- "uuid": "^9.0.0",
- "ws": "8.12.0",
- "async-lock": "1.4.0"
+ "socks-proxy-agent": "^8.0.2",
+ "typescript": "5.1.6",
+ "uuid": "^9.0.1",
+ "ws": "8.14.2"
},
"devDependencies": {
- "@types/core-js": "^2.5.0",
+ "@types/core-js": "^2.5.6",
"@types/form-data": "^2.5.0",
- "@types/jest": "^29.4.0",
- "@types/object-assign-deep": "^0.4.0",
- "@types/parse-link-header": "^2.0.0",
- "@types/uuid": "^9.0.0",
- "@types/node": "18.11.18",
- "@typescript-eslint/eslint-plugin": "^5.49.0",
- "@typescript-eslint/parser": "^5.49.0",
- "@types/async-lock": "1.4.0",
- "eslint": "^8.32.0",
- "eslint-config-prettier": "^8.6.0",
- "eslint-config-standard": "^16.0.3",
- "eslint-plugin-import": "^2.27.5",
- "eslint-plugin-node": "^11.0.0",
- "eslint-plugin-prettier": "^4.2.1",
- "eslint-plugin-promise": "^6.1.1",
- "eslint-plugin-standard": "^5.0.0",
- "jest": "^29.4.0",
- "jest-worker": "^29.4.0",
+ "@types/jest": "^29.5.5",
+ "@types/object-assign-deep": "^0.4.1",
+ "@types/parse-link-header": "^2.0.1",
+ "@types/uuid": "^9.0.4",
+ "@typescript-eslint/eslint-plugin": "^6.7.2",
+ "@typescript-eslint/parser": "^6.7.2",
+ "eslint": "^8.49.0",
+ "eslint-config-prettier": "^9.0.0",
+ "jest": "^29.7.0",
+ "jest-worker": "^29.7.0",
"lodash": "^4.17.14",
- "prettier": "^2.8.3",
- "ts-jest": "^29.0.5",
- "typedoc": "^0.23.24"
- },
- "directories": {
- "lib": "lib",
- "test": "test"
+ "prettier": "^3.0.3",
+ "ts-jest": "^29.1.1",
+ "typedoc": "^0.25.1"
}
}
diff --git a/packages/megalodon/src/axios.d.ts b/packages/megalodon/src/axios.d.ts
index f19fe38a2..114cb06aa 100644
--- a/packages/megalodon/src/axios.d.ts
+++ b/packages/megalodon/src/axios.d.ts
@@ -1 +1 @@
-declare module "axios/lib/adapters/http";
+declare module 'axios/lib/adapters/http'
diff --git a/packages/megalodon/src/cancel.ts b/packages/megalodon/src/cancel.ts
index f8e4729b8..3b905a492 100644
--- a/packages/megalodon/src/cancel.ts
+++ b/packages/megalodon/src/cancel.ts
@@ -1,13 +1,13 @@
export class RequestCanceledError extends Error {
- public isCancel: boolean;
+ public isCancel: boolean
- constructor(msg: string) {
- super(msg);
- this.isCancel = true;
- Object.setPrototypeOf(this, RequestCanceledError);
- }
+ constructor(msg: string) {
+ super(msg)
+ this.isCancel = true
+ Object.setPrototypeOf(this, RequestCanceledError)
+ }
}
export const isCancel = (value: any): boolean => {
- return value && value.isCancel;
-};
+ return value && value.isCancel
+}
diff --git a/packages/megalodon/src/converter.ts b/packages/megalodon/src/converter.ts
index 93d669fa7..f768fc930 100644
--- a/packages/megalodon/src/converter.ts
+++ b/packages/megalodon/src/converter.ts
@@ -1,3 +1,3 @@
import MisskeyAPI from "./misskey/api_client";
-export default MisskeyAPI.Converter;
+export default MisskeyAPI.Converter;
\ No newline at end of file
diff --git a/packages/megalodon/src/default.ts b/packages/megalodon/src/default.ts
index 45bce13e2..0194b3dcc 100644
--- a/packages/megalodon/src/default.ts
+++ b/packages/megalodon/src/default.ts
@@ -1,3 +1,3 @@
-export const NO_REDIRECT = "urn:ietf:wg:oauth:2.0:oob";
-export const DEFAULT_SCOPE = ["read", "write", "follow"];
-export const DEFAULT_UA = "megalodon";
+export const NO_REDIRECT = 'urn:ietf:wg:oauth:2.0:oob'
+export const DEFAULT_SCOPE = ['read', 'write', 'follow']
+export const DEFAULT_UA = 'megalodon'
diff --git a/packages/megalodon/src/detector.ts b/packages/megalodon/src/detector.ts
new file mode 100644
index 000000000..31f34d72f
--- /dev/null
+++ b/packages/megalodon/src/detector.ts
@@ -0,0 +1,137 @@
+import axios, { AxiosRequestConfig } from 'axios'
+import proxyAgent, { ProxyConfig } from './proxy_config'
+import { NodeinfoError } from './megalodon'
+
+const NODEINFO_10 = 'http://nodeinfo.diaspora.software/ns/schema/1.0'
+const NODEINFO_20 = 'http://nodeinfo.diaspora.software/ns/schema/2.0'
+const NODEINFO_21 = 'http://nodeinfo.diaspora.software/ns/schema/2.1'
+
+type Links = {
+ links: Array
+}
+
+type Link = {
+ href: string
+ rel: string
+}
+
+type Nodeinfo10 = {
+ software: Software
+ metadata: Metadata
+}
+
+type Nodeinfo20 = {
+ software: Software
+ metadata: Metadata
+}
+
+type Nodeinfo21 = {
+ software: Software
+ metadata: Metadata
+}
+
+type Software = {
+ name: string
+}
+
+type Metadata = {
+ upstream?: {
+ name: string
+ }
+}
+
+/**
+ * Detect SNS type.
+ * Now support Mastodon, Pleroma and Pixelfed. Throws an error when no known platform can be detected.
+ *
+ * @param url Base URL of SNS.
+ * @param proxyConfig Proxy setting, or set false if don't use proxy.
+ * @return SNS name.
+ */
+export const detector = async (
+ url: string,
+ proxyConfig: ProxyConfig | false = false
+): Promise<'mastodon' | 'pleroma' | 'misskey' | 'friendica'> => {
+ let options: AxiosRequestConfig = {
+ timeout: 20000
+ }
+ if (proxyConfig) {
+ options = Object.assign(options, {
+ httpsAgent: proxyAgent(proxyConfig)
+ })
+ }
+
+ const res = await axios.get(url + '/.well-known/nodeinfo', options)
+ const link = res.data.links.find(l => l.rel === NODEINFO_20 || l.rel === NODEINFO_21)
+ if (!link) throw new NodeinfoError('Could not find nodeinfo')
+ switch (link.rel) {
+ case NODEINFO_10: {
+ const res = await axios.get(link.href, options)
+ switch (res.data.software.name) {
+ case 'pleroma':
+ return 'pleroma'
+ case 'akkoma':
+ return 'pleroma'
+ case 'mastodon':
+ return 'mastodon'
+ case "wildebeest":
+ return "mastodon"
+ case 'misskey':
+ return 'misskey'
+ case 'friendica':
+ return 'friendica'
+ default:
+ if (res.data.metadata.upstream?.name && res.data.metadata.upstream.name === 'mastodon') {
+ return 'mastodon'
+ }
+ throw new NodeinfoError('Unknown SNS')
+ }
+ }
+ case NODEINFO_20: {
+ const res = await axios.get(link.href, options)
+ switch (res.data.software.name) {
+ case 'pleroma':
+ return 'pleroma'
+ case 'akkoma':
+ return 'pleroma'
+ case 'mastodon':
+ return 'mastodon'
+ case "wildebeest":
+ return "mastodon"
+ case 'misskey':
+ return 'misskey'
+ case 'friendica':
+ return 'friendica'
+ default:
+ if (res.data.metadata.upstream?.name && res.data.metadata.upstream.name === 'mastodon') {
+ return 'mastodon'
+ }
+ throw new NodeinfoError('Unknown SNS')
+ }
+ }
+ case NODEINFO_21: {
+ const res = await axios.get(link.href, options)
+ switch (res.data.software.name) {
+ case 'pleroma':
+ return 'pleroma'
+ case 'akkoma':
+ return 'pleroma'
+ case 'mastodon':
+ return 'mastodon'
+ case "wildebeest":
+ return "mastodon"
+ case 'misskey':
+ return 'misskey'
+ case 'friendica':
+ return 'friendica'
+ default:
+ if (res.data.metadata.upstream?.name && res.data.metadata.upstream.name === 'mastodon') {
+ return 'mastodon'
+ }
+ throw new NodeinfoError('Unknown SNS')
+ }
+ }
+ default:
+ throw new NodeinfoError('Could not find nodeinfo')
+ }
+}
diff --git a/packages/megalodon/src/entities/account.ts b/packages/megalodon/src/entities/account.ts
index 06a85eb98..89c0f17c4 100644
--- a/packages/megalodon/src/entities/account.ts
+++ b/packages/megalodon/src/entities/account.ts
@@ -1,27 +1,35 @@
///
///
///
+///
namespace Entity {
- export type Account = {
- id: string;
- username: string;
- acct: string;
- display_name: string;
- locked: boolean;
- created_at: string;
- followers_count: number;
- following_count: number;
- statuses_count: number;
- note: string;
- url: string;
- avatar: string;
- avatar_static: string;
- header: string;
- header_static: string;
- emojis: Array;
- moved: Account | null;
- fields: Array;
- bot: boolean | null;
- source?: Source;
- };
+ export type Account = {
+ id: string
+ username: string
+ acct: string
+ display_name: string
+ locked: boolean
+ discoverable?: boolean
+ group: boolean | null
+ noindex: boolean | null
+ suspended: boolean | null
+ limited: boolean | null
+ created_at: string
+ followers_count: number
+ following_count: number
+ statuses_count: number
+ note: string
+ url: string
+ avatar: string
+ avatar_static: string
+ header: string
+ header_static: string
+ emojis: Array
+ moved: Account | null
+ fields: Array
+ bot: boolean | null
+ source?: Source
+ role?: Role
+ mute_expires_at?: string
+ }
}
diff --git a/packages/megalodon/src/entities/activity.ts b/packages/megalodon/src/entities/activity.ts
index 6bc0b6d80..2494916a9 100644
--- a/packages/megalodon/src/entities/activity.ts
+++ b/packages/megalodon/src/entities/activity.ts
@@ -1,8 +1,8 @@
namespace Entity {
- export type Activity = {
- week: string;
- statuses: string;
- logins: string;
- registrations: string;
- };
+ export type Activity = {
+ week: string
+ statuses: string
+ logins: string
+ registrations: string
+ }
}
diff --git a/packages/megalodon/src/entities/announcement.ts b/packages/megalodon/src/entities/announcement.ts
index 7c7983163..0db9c23bb 100644
--- a/packages/megalodon/src/entities/announcement.ts
+++ b/packages/megalodon/src/entities/announcement.ts
@@ -1,34 +1,40 @@
-///
///
-///
namespace Entity {
- export type Announcement = {
- id: string;
- content: string;
- starts_at: string | null;
- ends_at: string | null;
- published: boolean;
- all_day: boolean;
- published_at: string;
- updated_at: string;
- read?: boolean;
- mentions: Array;
- statuses: Array;
- tags: Array;
- emojis: Array;
- reactions: Array;
- };
+ export type Announcement = {
+ id: string
+ content: string
+ starts_at: string | null
+ ends_at: string | null
+ published: boolean
+ all_day: boolean
+ published_at: string
+ updated_at: string | null
+ read: boolean | null
+ mentions: Array
+ statuses: Array
+ tags: Array
+ emojis: Array
+ reactions: Array
+ }
- export type AnnouncementAccount = {
- id: string;
- username: string;
- url: string;
- acct: string;
- };
+ export type AnnouncementAccount = {
+ id: string
+ username: string
+ url: string
+ acct: string
+ }
- export type AnnouncementStatus = {
- id: string;
- url: string;
- };
+ export type AnnouncementStatus = {
+ id: string
+ url: string
+ }
+
+ export type AnnouncementReaction = {
+ name: string
+ count: number
+ me: boolean | null
+ url: string | null
+ static_url: string | null
+ }
}
diff --git a/packages/megalodon/src/entities/application.ts b/packages/megalodon/src/entities/application.ts
index 9b98b1277..3af64fcf9 100644
--- a/packages/megalodon/src/entities/application.ts
+++ b/packages/megalodon/src/entities/application.ts
@@ -1,7 +1,7 @@
namespace Entity {
- export type Application = {
- name: string;
- website?: string | null;
- vapid_key?: string | null;
- };
+ export type Application = {
+ name: string
+ website?: string | null
+ vapid_key?: string | null
+ }
}
diff --git a/packages/megalodon/src/entities/async_attachment.ts b/packages/megalodon/src/entities/async_attachment.ts
index 9cc17acc5..b383f90c5 100644
--- a/packages/megalodon/src/entities/async_attachment.ts
+++ b/packages/megalodon/src/entities/async_attachment.ts
@@ -1,14 +1,14 @@
///
namespace Entity {
- export type AsyncAttachment = {
- id: string;
- type: "unknown" | "image" | "gifv" | "video" | "audio";
- url: string | null;
- remote_url: string | null;
- preview_url: string;
- text_url: string | null;
- meta: Meta | null;
- description: string | null;
- blurhash: string | null;
- };
+ export type AsyncAttachment = {
+ id: string
+ type: 'unknown' | 'image' | 'gifv' | 'video' | 'audio'
+ url: string | null
+ remote_url: string | null
+ preview_url: string
+ text_url: string | null
+ meta: Meta | null
+ description: string | null
+ blurhash: string | null
+ }
}
diff --git a/packages/megalodon/src/entities/attachment.ts b/packages/megalodon/src/entities/attachment.ts
index 082c79edd..aab1deade 100644
--- a/packages/megalodon/src/entities/attachment.ts
+++ b/packages/megalodon/src/entities/attachment.ts
@@ -1,49 +1,49 @@
namespace Entity {
- export type Sub = {
- // For Image, Gifv, and Video
- width?: number;
- height?: number;
- size?: string;
- aspect?: number;
+ export type Sub = {
+ // For Image, Gifv, and Video
+ width?: number
+ height?: number
+ size?: string
+ aspect?: number
- // For Gifv and Video
- frame_rate?: string;
+ // For Gifv and Video
+ frame_rate?: string
- // For Audio, Gifv, and Video
- duration?: number;
- bitrate?: number;
- };
+ // For Audio, Gifv, and Video
+ duration?: number
+ bitrate?: number
+ }
- export type Focus = {
- x: number;
- y: number;
- };
+ export type Focus = {
+ x: number
+ y: number
+ }
- export type Meta = {
- original?: Sub;
- small?: Sub;
- focus?: Focus;
- length?: string;
- duration?: number;
- fps?: number;
- size?: string;
- width?: number;
- height?: number;
- aspect?: number;
- audio_encode?: string;
- audio_bitrate?: string;
- audio_channel?: string;
- };
+ export type Meta = {
+ original?: Sub
+ small?: Sub
+ focus?: Focus
+ length?: string
+ duration?: number
+ fps?: number
+ size?: string
+ width?: number
+ height?: number
+ aspect?: number
+ audio_encode?: string
+ audio_bitrate?: string
+ audio_channel?: string
+ }
- export type Attachment = {
- id: string;
- type: "unknown" | "image" | "gifv" | "video" | "audio";
- url: string;
- remote_url: string | null;
- preview_url: string | null;
- text_url: string | null;
- meta: Meta | null;
- description: string | null;
- blurhash: string | null;
- };
+ export type Attachment = {
+ id: string
+ type: 'unknown' | 'image' | 'gifv' | 'video' | 'audio'
+ url: string
+ remote_url: string | null
+ preview_url: string | null
+ text_url: string | null
+ meta: Meta | null
+ description: string | null
+ blurhash: string | null
+ }
}
diff --git a/packages/megalodon/src/entities/card.ts b/packages/megalodon/src/entities/card.ts
index 356d99aee..1ef6f5e4d 100644
--- a/packages/megalodon/src/entities/card.ts
+++ b/packages/megalodon/src/entities/card.ts
@@ -1,16 +1,18 @@
namespace Entity {
- export type Card = {
- url: string;
- title: string;
- description: string;
- type: "link" | "photo" | "video" | "rich";
- image?: string;
- author_name?: string;
- author_url?: string;
- provider_name?: string;
- provider_url?: string;
- html?: string;
- width?: number;
- height?: number;
- };
+ export type Card = {
+ url: string
+ title: string
+ description: string
+ type: 'link' | 'photo' | 'video' | 'rich'
+ image: string | null
+ author_name: string | null
+ author_url: string | null
+ provider_name: string | null
+ provider_url: string | null
+ html: string | null
+ width: number | null
+ height: number | null
+ embed_url: string | null
+ blurhash: string | null
+ }
}
diff --git a/packages/megalodon/src/entities/context.ts b/packages/megalodon/src/entities/context.ts
index a794a7c5a..3f2eda58f 100644
--- a/packages/megalodon/src/entities/context.ts
+++ b/packages/megalodon/src/entities/context.ts
@@ -1,8 +1,8 @@
///
namespace Entity {
- export type Context = {
- ancestors: Array;
- descendants: Array;
- };
+ export type Context = {
+ ancestors: Array
+ descendants: Array
+ }
}
diff --git a/packages/megalodon/src/entities/conversation.ts b/packages/megalodon/src/entities/conversation.ts
index 2bdc19666..cdadf1e0f 100644
--- a/packages/megalodon/src/entities/conversation.ts
+++ b/packages/megalodon/src/entities/conversation.ts
@@ -2,10 +2,10 @@
///
namespace Entity {
- export type Conversation = {
- id: string;
- accounts: Array;
- last_status: Status | null;
- unread: boolean;
- };
+ export type Conversation = {
+ id: string
+ accounts: Array
+ last_status: Status | null
+ unread: boolean
+ }
}
diff --git a/packages/megalodon/src/entities/emoji.ts b/packages/megalodon/src/entities/emoji.ts
index 10c32ab0b..546ef818f 100644
--- a/packages/megalodon/src/entities/emoji.ts
+++ b/packages/megalodon/src/entities/emoji.ts
@@ -1,9 +1,9 @@
namespace Entity {
- export type Emoji = {
- shortcode: string;
- static_url: string;
- url: string;
- visible_in_picker: boolean;
- category: string;
- };
+ export type Emoji = {
+ shortcode: string
+ static_url: string
+ url: string
+ visible_in_picker: boolean
+ category?: string
+ }
}
diff --git a/packages/megalodon/src/entities/featured_tag.ts b/packages/megalodon/src/entities/featured_tag.ts
index fc9f8c69c..06ae6d7a9 100644
--- a/packages/megalodon/src/entities/featured_tag.ts
+++ b/packages/megalodon/src/entities/featured_tag.ts
@@ -1,8 +1,8 @@
namespace Entity {
- export type FeaturedTag = {
- id: string;
- name: string;
- statuses_count: number;
- last_status_at: string;
- };
+ export type FeaturedTag = {
+ id: string
+ name: string
+ statuses_count: number
+ last_status_at: string
+ }
}
diff --git a/packages/megalodon/src/entities/field.ts b/packages/megalodon/src/entities/field.ts
index de4b6b2b7..03e4604b0 100644
--- a/packages/megalodon/src/entities/field.ts
+++ b/packages/megalodon/src/entities/field.ts
@@ -1,7 +1,7 @@
namespace Entity {
- export type Field = {
- name: string;
- value: string;
- verified_at: string | null;
- };
+ export type Field = {
+ name: string
+ value: string
+ verified_at: string | null
+ }
}
diff --git a/packages/megalodon/src/entities/filter.ts b/packages/megalodon/src/entities/filter.ts
index 55b7305cc..ffbacb728 100644
--- a/packages/megalodon/src/entities/filter.ts
+++ b/packages/megalodon/src/entities/filter.ts
@@ -1,12 +1,12 @@
namespace Entity {
- export type Filter = {
- id: string;
- phrase: string;
- context: Array;
- expires_at: string | null;
- irreversible: boolean;
- whole_word: boolean;
- };
+ export type Filter = {
+ id: string
+ phrase: string
+ context: Array
+ expires_at: string | null
+ irreversible: boolean
+ whole_word: boolean
+ }
- export type FilterContext = string;
+ export type FilterContext = string
}
diff --git a/packages/megalodon/src/entities/follow_request.ts b/packages/megalodon/src/entities/follow_request.ts
new file mode 100644
index 000000000..84ea4d02c
--- /dev/null
+++ b/packages/megalodon/src/entities/follow_request.ts
@@ -0,0 +1,27 @@
+///
+///
+
+namespace Entity {
+ export type FollowRequest = {
+ id: number
+ username: string
+ acct: string
+ display_name: string
+ locked: boolean
+ bot: boolean
+ discoverable?: boolean
+ group: boolean
+ created_at: string
+ note: string
+ url: string
+ avatar: string
+ avatar_static: string
+ header: string
+ header_static: string
+ followers_count: number
+ following_count: number
+ statuses_count: number
+ emojis: Array
+ fields: Array
+ }
+}
diff --git a/packages/megalodon/src/entities/history.ts b/packages/megalodon/src/entities/history.ts
index 4676357d6..070969426 100644
--- a/packages/megalodon/src/entities/history.ts
+++ b/packages/megalodon/src/entities/history.ts
@@ -1,7 +1,7 @@
namespace Entity {
- export type History = {
- day: string;
- uses: number;
- accounts: number;
- };
+ export type History = {
+ day: string
+ uses: number
+ accounts: number
+ }
}
diff --git a/packages/megalodon/src/entities/identity_proof.ts b/packages/megalodon/src/entities/identity_proof.ts
index 3b42e6f41..ff857addb 100644
--- a/packages/megalodon/src/entities/identity_proof.ts
+++ b/packages/megalodon/src/entities/identity_proof.ts
@@ -1,9 +1,9 @@
namespace Entity {
- export type IdentityProof = {
- provider: string;
- provider_username: string;
- updated_at: string;
- proof_url: string;
- profile_url: string;
- };
+ export type IdentityProof = {
+ provider: string
+ provider_username: string
+ updated_at: string
+ proof_url: string
+ profile_url: string
+ }
}
diff --git a/packages/megalodon/src/entities/instance.ts b/packages/megalodon/src/entities/instance.ts
index 9c0f572db..8f4808be8 100644
--- a/packages/megalodon/src/entities/instance.ts
+++ b/packages/megalodon/src/entities/instance.ts
@@ -3,39 +3,38 @@
///
namespace Entity {
- export type Instance = {
- uri: string;
- title: string;
- description: string;
- email: string;
- version: string;
- thumbnail: string | null;
- urls: URLs;
- stats: Stats;
- languages: Array;
- contact_account: Account | null;
- max_toot_chars?: number;
- registrations?: boolean;
- configuration?: {
- statuses: {
- max_characters: number;
- max_media_attachments: number;
- characters_reserved_per_url: number;
- };
- media_attachments: {
- supported_mime_types: Array;
- image_size_limit: number;
- image_matrix_limit: number;
- video_size_limit: number;
- video_frame_limit: number;
- video_matrix_limit: number;
- };
- polls: {
- max_options: number;
- max_characters_per_option: number;
- min_expiration: number;
- max_expiration: number;
- };
- };
- };
+ export type Instance = {
+ uri: string
+ title: string
+ description: string
+ email: string
+ version: string
+ thumbnail: string | null
+ urls: URLs | null
+ stats: Stats
+ languages: Array
+ registrations: boolean
+ approval_required: boolean
+ invites_enabled?: boolean
+ configuration: {
+ statuses: {
+ max_characters: number
+ max_media_attachments?: number
+ characters_reserved_per_url?: number
+ }
+ polls?: {
+ max_options: number
+ max_characters_per_option: number
+ min_expiration: number
+ max_expiration: number
+ }
+ }
+ contact_account?: Account
+ rules?: Array
+ }
+
+ export type InstanceRule = {
+ id: string
+ text: string
+ }
}
diff --git a/packages/megalodon/src/entities/list.ts b/packages/megalodon/src/entities/list.ts
index 97e75286b..58c264aba 100644
--- a/packages/megalodon/src/entities/list.ts
+++ b/packages/megalodon/src/entities/list.ts
@@ -1,6 +1,9 @@
namespace Entity {
- export type List = {
- id: string;
- title: string;
- };
+ export type List = {
+ id: string
+ title: string
+ replies_policy: RepliesPolicy | null
+ }
+
+ export type RepliesPolicy = 'followed' | 'list' | 'none'
}
diff --git a/packages/megalodon/src/entities/marker.ts b/packages/megalodon/src/entities/marker.ts
index 7ee99282c..33cb98a10 100644
--- a/packages/megalodon/src/entities/marker.ts
+++ b/packages/megalodon/src/entities/marker.ts
@@ -1,15 +1,15 @@
namespace Entity {
- export type Marker = {
- home?: {
- last_read_id: string;
- version: number;
- updated_at: string;
- };
- notifications?: {
- last_read_id: string;
- version: number;
- updated_at: string;
- unread_count?: number;
- };
- };
+ export type Marker = {
+ home?: {
+ last_read_id: string
+ version: number
+ updated_at: string
+ }
+ notifications?: {
+ last_read_id: string
+ version: number
+ updated_at: string
+ unread_count?: number
+ }
+ }
}
diff --git a/packages/megalodon/src/entities/mention.ts b/packages/megalodon/src/entities/mention.ts
index 4fe36a655..046912971 100644
--- a/packages/megalodon/src/entities/mention.ts
+++ b/packages/megalodon/src/entities/mention.ts
@@ -1,8 +1,8 @@
namespace Entity {
- export type Mention = {
- id: string;
- username: string;
- url: string;
- acct: string;
- };
+ export type Mention = {
+ id: string
+ username: string
+ url: string
+ acct: string
+ }
}
diff --git a/packages/megalodon/src/entities/notification.ts b/packages/megalodon/src/entities/notification.ts
index 68eff3347..653d235d9 100644
--- a/packages/megalodon/src/entities/notification.ts
+++ b/packages/megalodon/src/entities/notification.ts
@@ -2,14 +2,15 @@
///
namespace Entity {
- export type Notification = {
- account: Account;
- created_at: string;
- id: string;
- status?: Status;
- reaction?: Reaction;
- type: NotificationType;
- };
+ export type Notification = {
+ account: Account
+ created_at: string
+ id: string
+ status?: Status
+ emoji?: string
+ type: NotificationType
+ target?: Account
+ }
- export type NotificationType = string;
+ export type NotificationType = string
}
diff --git a/packages/megalodon/src/entities/poll.ts b/packages/megalodon/src/entities/poll.ts
index 2539d68b2..69706e8ae 100644
--- a/packages/megalodon/src/entities/poll.ts
+++ b/packages/megalodon/src/entities/poll.ts
@@ -1,14 +1,13 @@
///
namespace Entity {
- export type Poll = {
- id: string;
- expires_at: string | null;
- expired: boolean;
- multiple: boolean;
- votes_count: number;
- options: Array;
- voted: boolean;
- own_votes: Array;
- };
+ export type Poll = {
+ id: string
+ expires_at: string | null
+ expired: boolean
+ multiple: boolean
+ votes_count: number
+ options: Array
+ voted: boolean
+ }
}
diff --git a/packages/megalodon/src/entities/poll_option.ts b/packages/megalodon/src/entities/poll_option.ts
index e818a8607..ae4c63849 100644
--- a/packages/megalodon/src/entities/poll_option.ts
+++ b/packages/megalodon/src/entities/poll_option.ts
@@ -1,6 +1,6 @@
namespace Entity {
- export type PollOption = {
- title: string;
- votes_count: number | null;
- };
+ export type PollOption = {
+ title: string
+ votes_count: number | null
+ }
}
diff --git a/packages/megalodon/src/entities/preferences.ts b/packages/megalodon/src/entities/preferences.ts
index 7994dc568..cb5797c4c 100644
--- a/packages/megalodon/src/entities/preferences.ts
+++ b/packages/megalodon/src/entities/preferences.ts
@@ -1,9 +1,9 @@
namespace Entity {
- export type Preferences = {
- "posting:default:visibility": "public" | "unlisted" | "private" | "direct";
- "posting:default:sensitive": boolean;
- "posting:default:language": string | null;
- "reading:expand:media": "default" | "show_all" | "hide_all";
- "reading:expand:spoilers": boolean;
- };
+ export type Preferences = {
+ 'posting:default:visibility': 'public' | 'unlisted' | 'private' | 'direct'
+ 'posting:default:sensitive': boolean
+ 'posting:default:language': string | null
+ 'reading:expand:media': 'default' | 'show_all' | 'hide_all'
+ 'reading:expand:spoilers': boolean
+ }
}
diff --git a/packages/megalodon/src/entities/push_subscription.ts b/packages/megalodon/src/entities/push_subscription.ts
index ad1146a24..fe7464e8e 100644
--- a/packages/megalodon/src/entities/push_subscription.ts
+++ b/packages/megalodon/src/entities/push_subscription.ts
@@ -1,16 +1,16 @@
namespace Entity {
- export type Alerts = {
- follow: boolean;
- favourite: boolean;
- mention: boolean;
- reblog: boolean;
- poll: boolean;
- };
+ export type Alerts = {
+ follow: boolean
+ favourite: boolean
+ mention: boolean
+ reblog: boolean
+ poll: boolean
+ }
- export type PushSubscription = {
- id: string;
- endpoint: string;
- server_key: string;
- alerts: Alerts;
- };
+ export type PushSubscription = {
+ id: string
+ endpoint: string
+ server_key: string
+ alerts: Alerts
+ }
}
diff --git a/packages/megalodon/src/entities/reaction.ts b/packages/megalodon/src/entities/reaction.ts
index 4edbec6a7..8c626f9e8 100644
--- a/packages/megalodon/src/entities/reaction.ts
+++ b/packages/megalodon/src/entities/reaction.ts
@@ -1,12 +1,10 @@
///
namespace Entity {
- export type Reaction = {
- count: number;
- me: boolean;
- name: string;
- url?: string;
- static_url?: string;
- accounts?: Array;
- };
+ export type Reaction = {
+ count: number
+ me: boolean
+ name: string
+ accounts?: Array
+ }
}
diff --git a/packages/megalodon/src/entities/relationship.ts b/packages/megalodon/src/entities/relationship.ts
index 91802d5c8..283a1158c 100644
--- a/packages/megalodon/src/entities/relationship.ts
+++ b/packages/megalodon/src/entities/relationship.ts
@@ -1,17 +1,17 @@
namespace Entity {
- export type Relationship = {
- id: string;
- following: boolean;
- followed_by: boolean;
- delivery_following?: boolean;
- blocking: boolean;
- blocked_by: boolean;
- muting: boolean;
- muting_notifications: boolean;
- requested: boolean;
- domain_blocking: boolean;
- showing_reblogs: boolean;
- endorsed: boolean;
- notifying: boolean;
- };
+ export type Relationship = {
+ id: string
+ following: boolean
+ followed_by: boolean
+ blocking: boolean
+ blocked_by: boolean
+ muting: boolean
+ muting_notifications: boolean
+ requested: boolean
+ domain_blocking: boolean
+ showing_reblogs: boolean
+ endorsed: boolean
+ notifying: boolean
+ note: string | null
+ }
}
diff --git a/packages/megalodon/src/entities/report.ts b/packages/megalodon/src/entities/report.ts
index 6862a5fab..353886a34 100644
--- a/packages/megalodon/src/entities/report.ts
+++ b/packages/megalodon/src/entities/report.ts
@@ -1,9 +1,18 @@
+///
+
namespace Entity {
- export type Report = {
- id: string;
- action_taken: string;
- comment: string;
- account_id: string;
- status_ids: Array;
- };
+ export type Report = {
+ id: string
+ action_taken: boolean
+ action_taken_at: string | null
+ status_ids: Array | null
+ rule_ids: Array | null
+ // These parameters don't exist in Pleroma
+ category: Category | null
+ comment: string | null
+ forwarded: boolean | null
+ target_account?: Account | null
+ }
+
+ export type Category = 'spam' | 'violation' | 'other'
}
diff --git a/packages/megalodon/src/entities/results.ts b/packages/megalodon/src/entities/results.ts
index 4448e5335..fe168de67 100644
--- a/packages/megalodon/src/entities/results.ts
+++ b/packages/megalodon/src/entities/results.ts
@@ -3,9 +3,9 @@
///
namespace Entity {
- export type Results = {
- accounts: Array;
- statuses: Array;
- hashtags: Array;
- };
+ export type Results = {
+ accounts: Array
+ statuses: Array
+ hashtags: Array
+ }
}
diff --git a/packages/megalodon/src/entities/role.ts b/packages/megalodon/src/entities/role.ts
new file mode 100644
index 000000000..caaae9ea1
--- /dev/null
+++ b/packages/megalodon/src/entities/role.ts
@@ -0,0 +1,5 @@
+namespace Entity {
+ export type Role = {
+ name: string
+ }
+}
diff --git a/packages/megalodon/src/entities/scheduled_status.ts b/packages/megalodon/src/entities/scheduled_status.ts
index 78dfb8ed2..561a5b9f2 100644
--- a/packages/megalodon/src/entities/scheduled_status.ts
+++ b/packages/megalodon/src/entities/scheduled_status.ts
@@ -1,10 +1,10 @@
///
///
namespace Entity {
- export type ScheduledStatus = {
- id: string;
- scheduled_at: string;
- params: StatusParams;
- media_attachments: Array;
- };
+ export type ScheduledStatus = {
+ id: string
+ scheduled_at: string
+ params: StatusParams
+ media_attachments: Array | null
+ }
}
diff --git a/packages/megalodon/src/entities/source.ts b/packages/megalodon/src/entities/source.ts
index 913b02fda..d87cf55d8 100644
--- a/packages/megalodon/src/entities/source.ts
+++ b/packages/megalodon/src/entities/source.ts
@@ -1,10 +1,10 @@
///
namespace Entity {
- export type Source = {
- privacy: string | null;
- sensitive: boolean | null;
- language: string | null;
- note: string;
- fields: Array;
- };
+ export type Source = {
+ privacy: string | null
+ sensitive: boolean | null
+ language: string | null
+ note: string
+ fields: Array
+ }
}
diff --git a/packages/megalodon/src/entities/stats.ts b/packages/megalodon/src/entities/stats.ts
index 6471df039..76f0bad34 100644
--- a/packages/megalodon/src/entities/stats.ts
+++ b/packages/megalodon/src/entities/stats.ts
@@ -1,7 +1,7 @@
namespace Entity {
- export type Stats = {
- user_count: number;
- status_count: number;
- domain_count: number;
- };
+ export type Stats = {
+ user_count: number
+ status_count: number
+ domain_count: number
+ }
}
diff --git a/packages/megalodon/src/entities/status.ts b/packages/megalodon/src/entities/status.ts
index f27f728b5..295703e57 100644
--- a/packages/megalodon/src/entities/status.ts
+++ b/packages/megalodon/src/entities/status.ts
@@ -1,7 +1,6 @@
///
///
///
-///
///
///
///
@@ -9,37 +8,42 @@
///
namespace Entity {
- export type Status = {
- id: string;
- uri: string;
- url: string;
- account: Account;
- in_reply_to_id: string | null;
- in_reply_to_account_id: string | null;
- reblog: Status | null;
- content: string;
- plain_content: string | null;
- created_at: string;
- emojis: Emoji[];
- replies_count: number;
- reblogs_count: number;
- favourites_count: number;
- reblogged: boolean | null;
- favourited: boolean | null;
- muted: boolean | null;
- sensitive: boolean;
- spoiler_text: string;
- visibility: "public" | "unlisted" | "private" | "direct";
- media_attachments: Array;
- mentions: Array;
- tags: Array;
- card: Card | null;
- poll: Poll | null;
- application: Application | null;
- language: string | null;
- pinned: boolean | null;
- reactions: Array;
- quote: Status | null;
- bookmarked: boolean;
- };
+ export type Status = {
+ id: string
+ uri: string
+ url: string
+ account: Account
+ in_reply_to_id: string | null
+ in_reply_to_account_id: string | null
+ reblog: Status | null
+ content: string
+ plain_content: string | null
+ created_at: string
+ emojis: Emoji[]
+ replies_count: number
+ reblogs_count: number
+ favourites_count: number
+ reblogged: boolean | null
+ favourited: boolean | null
+ muted: boolean | null
+ sensitive: boolean
+ spoiler_text: string
+ visibility: 'public' | 'unlisted' | 'private' | 'direct'
+ media_attachments: Array
+ mentions: Array
+ tags: Array
+ card: Card | null
+ poll: Poll | null
+ application: Application | null
+ language: string | null
+ pinned: boolean | null
+ emoji_reactions: Array
+ quote: boolean
+ bookmarked: boolean
+ }
+
+ export type StatusTag = {
+ name: string
+ url: string
+ }
}
diff --git a/packages/megalodon/src/entities/status_params.ts b/packages/megalodon/src/entities/status_params.ts
index 18908c01c..82d789086 100644
--- a/packages/megalodon/src/entities/status_params.ts
+++ b/packages/megalodon/src/entities/status_params.ts
@@ -1,12 +1,12 @@
namespace Entity {
- export type StatusParams = {
- text: string;
- in_reply_to_id: string | null;
- media_ids: Array | null;
- sensitive: boolean | null;
- spoiler_text: string | null;
- visibility: "public" | "unlisted" | "private" | "direct";
- scheduled_at: string | null;
- application_id: string;
- };
+ export type StatusParams = {
+ text: string
+ in_reply_to_id: string | null
+ media_ids: Array | null
+ sensitive: boolean | null
+ spoiler_text: string | null
+ visibility: 'public' | 'unlisted' | 'private' | 'direct' | null
+ scheduled_at: string | null
+ application_id: number | null
+ }
}
diff --git a/packages/megalodon/src/entities/status_source.ts b/packages/megalodon/src/entities/status_source.ts
new file mode 100644
index 000000000..0de7030ed
--- /dev/null
+++ b/packages/megalodon/src/entities/status_source.ts
@@ -0,0 +1,7 @@
+namespace Entity {
+ export type StatusSource = {
+ id: string
+ text: string
+ spoiler_text: string
+ }
+}
diff --git a/packages/megalodon/src/entities/tag.ts b/packages/megalodon/src/entities/tag.ts
index ccc88aece..ddc5fe92b 100644
--- a/packages/megalodon/src/entities/tag.ts
+++ b/packages/megalodon/src/entities/tag.ts
@@ -1,10 +1,10 @@
///
namespace Entity {
- export type Tag = {
- name: string;
- url: string;
- history: Array | null;
- following?: boolean;
- };
+ export type Tag = {
+ name: string
+ url: string
+ history: Array
+ following?: boolean
+ }
}
diff --git a/packages/megalodon/src/entities/token.ts b/packages/megalodon/src/entities/token.ts
index 1583edafb..6fa28e39b 100644
--- a/packages/megalodon/src/entities/token.ts
+++ b/packages/megalodon/src/entities/token.ts
@@ -1,8 +1,8 @@
namespace Entity {
- export type Token = {
- access_token: string;
- token_type: string;
- scope: string;
- created_at: number;
- };
+ export type Token = {
+ access_token: string
+ token_type: string
+ scope: string
+ created_at: number
+ }
}
diff --git a/packages/megalodon/src/entities/urls.ts b/packages/megalodon/src/entities/urls.ts
index 1ee9ed67c..4a980d589 100644
--- a/packages/megalodon/src/entities/urls.ts
+++ b/packages/megalodon/src/entities/urls.ts
@@ -1,5 +1,5 @@
namespace Entity {
- export type URLs = {
- streaming_api: string;
- };
+ export type URLs = {
+ streaming_api: string
+ }
}
diff --git a/packages/megalodon/src/entity.ts b/packages/megalodon/src/entity.ts
index b73d2b359..387981cec 100644
--- a/packages/megalodon/src/entity.ts
+++ b/packages/megalodon/src/entity.ts
@@ -11,6 +11,7 @@
///
///
///
+///
///
///
///
@@ -31,8 +32,9 @@
///
///
///
+///
///
///
///
-export default Entity;
+export default Entity
diff --git a/packages/megalodon/src/filter_context.ts b/packages/megalodon/src/filter_context.ts
index 4c83cb15f..c69be98cd 100644
--- a/packages/megalodon/src/filter_context.ts
+++ b/packages/megalodon/src/filter_context.ts
@@ -1,11 +1,11 @@
-import Entity from "./entity";
+import Entity from './entity'
namespace FilterContext {
- export const Home: Entity.FilterContext = "home";
- export const Notifications: Entity.FilterContext = "notifications";
- export const Public: Entity.FilterContext = "public";
- export const Thread: Entity.FilterContext = "thread";
- export const Account: Entity.FilterContext = "account";
+ export const Home: Entity.FilterContext = 'home'
+ export const Notifications: Entity.FilterContext = 'notifications'
+ export const Public: Entity.FilterContext = 'public'
+ export const Thread: Entity.FilterContext = 'thread'
+ export const Account: Entity.FilterContext = 'account'
}
-export default FilterContext;
+export default FilterContext
diff --git a/packages/megalodon/src/friendica.ts b/packages/megalodon/src/friendica.ts
new file mode 100644
index 000000000..c5ee9d59c
--- /dev/null
+++ b/packages/megalodon/src/friendica.ts
@@ -0,0 +1,2868 @@
+import { OAuth2 } from 'oauth'
+import FormData from 'form-data'
+import parseLinkHeader from 'parse-link-header'
+
+import FriendicaAPI from './friendica/api_client'
+import WebSocket from './friendica/web_socket'
+import { MegalodonInterface, NoImplementedError } from './megalodon'
+import Response from './response'
+import Entity from './entity'
+import { NO_REDIRECT, DEFAULT_SCOPE, DEFAULT_UA } from './default'
+import { ProxyConfig } from './proxy_config'
+import OAuth from './oauth'
+import { UnknownNotificationTypeError } from './notification'
+
+export default class Friendica implements MegalodonInterface {
+ public client: FriendicaAPI.Interface
+ public baseUrl: string
+
+ /**
+ * @param baseUrl hostname or base URL
+ * @param accessToken access token from OAuth2 authorization
+ * @param userAgent UserAgent is specified in header on request.
+ * @param proxyConfig Proxy setting, or set false if don't use proxy.
+ */
+ constructor(
+ baseUrl: string,
+ accessToken: string | null = null,
+ userAgent: string | null = DEFAULT_UA,
+ proxyConfig: ProxyConfig | false = false
+ ) {
+ let token = ''
+ if (accessToken) {
+ token = accessToken
+ }
+ let agent: string = DEFAULT_UA
+ if (userAgent) {
+ agent = userAgent
+ }
+ this.client = new FriendicaAPI.Client(baseUrl, token, agent, proxyConfig)
+ this.baseUrl = baseUrl
+ }
+
+ public cancel(): void {
+ return this.client.cancel()
+ }
+
+ /**
+ * First, call createApp to get client_id and client_secret.
+ * Next, call generateAuthUrl to get authorization url.
+ * @param client_name Form Data, which is sent to /api/v1/apps
+ * @param options Form Data, which is sent to /api/v1/apps. and properties should be **snake_case**
+ */
+ public async registerApp(
+ client_name: string,
+ options: Partial<{ scopes: Array; redirect_uris: string; website: string }>
+ ): Promise {
+ const scopes = options.scopes || DEFAULT_SCOPE
+ return this.createApp(client_name, options).then(async appData => {
+ return this.generateAuthUrl(appData.client_id, appData.client_secret, {
+ scope: scopes,
+ redirect_uri: appData.redirect_uri
+ }).then(url => {
+ appData.url = url
+ return appData
+ })
+ })
+ }
+
+ /**
+ * Call /api/v1/apps
+ *
+ * Create an application.
+ * @param client_name your application's name
+ * @param options Form Data
+ */
+ public async createApp(
+ client_name: string,
+ options: Partial<{ scopes: Array; redirect_uris: string; website: string }>
+ ): Promise {
+ const scopes = options.scopes || DEFAULT_SCOPE
+ const redirect_uris = options.redirect_uris || NO_REDIRECT
+
+ const params: {
+ client_name: string
+ redirect_uris: string
+ scopes: string
+ website?: string
+ } = {
+ client_name: client_name,
+ redirect_uris: redirect_uris,
+ scopes: scopes.join(' ')
+ }
+ if (options.website) params.website = options.website
+
+ return this.client
+ .post('/api/v1/apps', params)
+ .then((res: Response) => OAuth.AppData.from(res.data))
+ }
+
+ /**
+ * Generate authorization url using OAuth2.
+ *
+ * @param clientId your OAuth app's client ID
+ * @param clientSecret your OAuth app's client Secret
+ * @param options as property, redirect_uri and scope are available, and must be the same as when you register your app
+ */
+ public generateAuthUrl(
+ clientId: string,
+ clientSecret: string,
+ options: Partial<{ scope: Array; redirect_uri: string }>
+ ): Promise {
+ const scope = options.scope || DEFAULT_SCOPE
+ const redirect_uri = options.redirect_uri || NO_REDIRECT
+ return new Promise(resolve => {
+ const oauth = new OAuth2(clientId, clientSecret, this.baseUrl, undefined, '/oauth/token')
+ const url = oauth.getAuthorizeUrl({
+ redirect_uri: redirect_uri,
+ response_type: 'code',
+ client_id: clientId,
+ scope: scope.join(' ')
+ })
+ resolve(url)
+ })
+ }
+
+ // ======================================
+ // apps
+ // ======================================
+ /**
+ * GET /api/v1/apps/verify_credentials
+ *
+ * @return An Application
+ */
+ public verifyAppCredentials(): Promise> {
+ return this.client.get('/api/v1/apps/verify_credentials')
+ }
+
+ // ======================================
+ // apps/oauth
+ // ======================================
+ /**
+ * POST /oauth/token
+ *
+ * Fetch OAuth access token.
+ * Get an access token based client_id and client_secret and authorization code.
+ * @param client_id will be generated by #createApp or #registerApp
+ * @param client_secret will be generated by #createApp or #registerApp
+ * @param code will be generated by the link of #generateAuthUrl or #registerApp
+ * @param redirect_uri must be the same uri as the time when you register your OAuth application
+ */
+ public async fetchAccessToken(
+ client_id: string | null,
+ client_secret: string,
+ code: string,
+ redirect_uri: string = NO_REDIRECT
+ ): Promise {
+ if (!client_id) {
+ throw new Error('client_id is required')
+ }
+ return this.client
+ .post('/oauth/token', {
+ client_id,
+ client_secret,
+ code,
+ redirect_uri,
+ grant_type: 'authorization_code'
+ })
+ .then((res: Response) => OAuth.TokenData.from(res.data))
+ }
+
+ /**
+ * POST /oauth/token
+ *
+ * Refresh OAuth access token.
+ * Send refresh token and get new access token.
+ * @param client_id will be generated by #createApp or #registerApp
+ * @param client_secret will be generated by #createApp or #registerApp
+ * @param refresh_token will be get #fetchAccessToken
+ */
+ public async refreshToken(client_id: string, client_secret: string, refresh_token: string): Promise {
+ return this.client
+ .post('/oauth/token', {
+ client_id,
+ client_secret,
+ refresh_token,
+ grant_type: 'refresh_token'
+ })
+ .then((res: Response) => OAuth.TokenData.from(res.data))
+ }
+
+ /**
+ * POST /oauth/revoke
+ *
+ * Revoke an OAuth token.
+ * @param client_id will be generated by #createApp or #registerApp
+ * @param client_secret will be generated by #createApp or #registerApp
+ * @param token will be get #fetchAccessToken
+ */
+ public async revokeToken(client_id: string, client_secret: string, token: string): Promise>> {
+ return this.client.post>('/oauth/revoke', {
+ client_id,
+ client_secret,
+ token
+ })
+ }
+
+ // ======================================
+ // accounts
+ // ======================================
+ public async registerAccount(
+ _username: string,
+ _email: string,
+ _password: string,
+ _agreement: boolean,
+ _locale: string,
+ _reason?: string | null
+ ): Promise> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError('friendica does not support')
+ reject(err)
+ })
+ }
+
+ /**
+ * GET /api/v1/accounts/verify_credentials
+ *
+ * @return Account.
+ */
+ public async verifyAccountCredentials(): Promise> {
+ return this.client.get('/api/v1/accounts/verify_credentials').then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.account(res.data)
+ })
+ })
+ }
+
+ public async updateCredentials(_options?: {
+ discoverable?: boolean
+ bot?: boolean
+ display_name?: string
+ note?: string
+ avatar?: string
+ header?: string
+ locked?: boolean
+ source?: {
+ privacy?: string
+ sensitive?: boolean
+ language?: string
+ }
+ fields_attributes?: Array<{ name: string; value: string }>
+ }): Promise> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError('friendica does not support')
+ reject(err)
+ })
+ }
+
+ /**
+ * GET /api/v1/accounts/:id
+ *
+ * @param id The account ID.
+ * @return An account.
+ */
+ public async getAccount(id: string): Promise> {
+ return this.client.get(`/api/v1/accounts/${id}`).then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.account(res.data)
+ })
+ })
+ }
+
+ /**
+ * GET /api/v1/accounts/:id/statuses
+ *
+ * @param id The account ID.
+
+ * @param options.limit Max number of results to return. Defaults to 20.
+ * @param options.max_id Return results older than ID.
+ * @param options.since_id Return results newer than ID but starting with most recent.
+ * @param options.min_id Return results newer than ID.
+ * @param options.pinned Return statuses which include pinned statuses.
+ * @param options.exclude_replies Return statuses which exclude replies.
+ * @param options.exclude_reblogs Return statuses which exclude reblogs.
+ * @param options.only_media Show only statuses with media attached? Defaults to false.
+ * @return Account's statuses.
+ */
+ public async getAccountStatuses(
+ id: string,
+ options?: {
+ limit?: number
+ max_id?: string
+ since_id?: string
+ min_id?: string
+ pinned?: boolean
+ exclude_replies?: boolean
+ exclude_reblogs?: boolean
+ only_media: boolean
+ }
+ ): Promise>> {
+ let params = {}
+ if (options) {
+ if (options.limit) {
+ params = Object.assign(params, {
+ limit: options.limit
+ })
+ }
+ if (options.max_id) {
+ params = Object.assign(params, {
+ max_id: options.max_id
+ })
+ }
+ if (options.since_id) {
+ params = Object.assign(params, {
+ since_id: options.since_id
+ })
+ }
+ if (options.min_id) {
+ params = Object.assign(params, {
+ min_id: options.min_id
+ })
+ }
+ if (options.pinned) {
+ params = Object.assign(params, {
+ pinned: options.pinned
+ })
+ }
+ if (options.exclude_replies) {
+ params = Object.assign(params, {
+ exclude_replies: options.exclude_replies
+ })
+ }
+ if (options.exclude_reblogs) {
+ params = Object.assign(params, {
+ exclude_reblogs: options.exclude_reblogs
+ })
+ }
+ if (options.only_media) {
+ params = Object.assign(params, {
+ only_media: options.only_media
+ })
+ }
+ }
+
+ return this.client.get>(`/api/v1/accounts/${id}/statuses`, params).then(res => {
+ return Object.assign(res, {
+ data: res.data.map(s => FriendicaAPI.Converter.status(s))
+ })
+ })
+ }
+
+ /**
+ * POST /api/v1/accounts/:id/follow
+ *
+ * @param id Target account ID.
+ * @return Relationship.
+ */
+ public async subscribeAccount(id: string): Promise> {
+ const params = {
+ notify: true
+ }
+ return this.client.post(`/api/v1/accounts/${id}/follow`, params).then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.relationship(res.data)
+ })
+ })
+ }
+
+ /**
+ * POST /api/v1/accounts/:id/follow
+ *
+ * @param id Target account ID.
+ * @return Relationship.
+ */
+ public async unsubscribeAccount(id: string): Promise> {
+ const params = {
+ notify: false
+ }
+ return this.client.post(`/api/v1/accounts/${id}/follow`, params).then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.relationship(res.data)
+ })
+ })
+ }
+
+ public getAccountFavourites(
+ _id: string,
+ _options?: {
+ limit?: number
+ max_id?: string
+ since_id?: string
+ }
+ ): Promise>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError('friendica does not support')
+ reject(err)
+ })
+ }
+
+ /**
+ * GET /api/v1/accounts/:id/followers
+ *
+ * @param id The account ID.
+ * @param options.limit Max number of results to return. Defaults to 40.
+ * @param options.max_id Return results older than ID.
+ * @param options.since_id Return results newer than ID.
+ * @return The array of accounts.
+ */
+ public async getAccountFollowers(
+ id: string,
+ options?: {
+ limit?: number
+ max_id?: string
+ since_id?: string
+ get_all?: boolean
+ sleep_ms?: number
+ }
+ ): Promise>> {
+ let params = {}
+ if (options) {
+ if (options.max_id) {
+ params = Object.assign(params, {
+ max_id: options.max_id
+ })
+ }
+ if (options.since_id) {
+ params = Object.assign(params, {
+ since_id: options.since_id
+ })
+ }
+ if (options.limit) {
+ params = Object.assign(params, {
+ limit: options.limit
+ })
+ }
+ }
+ return this.urlToAccounts(`/api/v1/accounts/${id}/followers`, params, options?.get_all || false, options?.sleep_ms || 0)
+ }
+
+ /**
+ * GET /api/v1/accounts/:id/following
+ *
+ * @param id The account ID.
+ * @param options.limit Max number of results to return. Defaults to 40.
+ * @param options.max_id Return results older than ID.
+ * @param options.since_id Return results newer than ID.
+ * @return The array of accounts.
+ */
+ public async getAccountFollowing(
+ id: string,
+ options?: {
+ limit?: number
+ max_id?: string
+ since_id?: string
+ get_all?: boolean
+ sleep_ms?: number
+ }
+ ): Promise>> {
+ let params = {}
+ if (options) {
+ if (options.max_id) {
+ params = Object.assign(params, {
+ max_id: options.max_id
+ })
+ }
+ if (options.since_id) {
+ params = Object.assign(params, {
+ since_id: options.since_id
+ })
+ }
+ if (options.limit) {
+ params = Object.assign(params, {
+ limit: options.limit
+ })
+ }
+ }
+ return this.urlToAccounts(`/api/v1/accounts/${id}/following`, params, options?.get_all || false, options?.sleep_ms || 0)
+ }
+
+ /** Helper function to optionally follow Link headers as pagination */
+ private async urlToAccounts(url: string, params: Record, get_all: boolean, sleep_ms: number) {
+ const res = await this.client.get>(url, params)
+ let converted = Object.assign({}, res, {
+ data: res.data.map(a => FriendicaAPI.Converter.account(a))
+ })
+ if (get_all && converted.headers.link) {
+ let parsed = parseLinkHeader(converted.headers.link)
+ while (parsed?.next) {
+ const nextRes = await this.client.get>(parsed?.next.url, undefined, undefined, true)
+ converted = Object.assign({}, converted, {
+ data: [...converted.data, ...nextRes.data.map(a => FriendicaAPI.Converter.account(a))]
+ })
+ parsed = parseLinkHeader(nextRes.headers.link)
+ if (sleep_ms) {
+ await new Promise(converted => setTimeout(converted, sleep_ms))
+ }
+ }
+ }
+ return converted
+ }
+
+ /**
+ * GET /api/v1/accounts/:id/lists
+ *
+ * @param id The account ID.
+ * @return The array of lists.
+ */
+ public async getAccountLists(id: string): Promise>> {
+ return this.client.get>(`/api/v1/accounts/${id}/lists`).then(res => {
+ return Object.assign(res, {
+ data: res.data.map(l => FriendicaAPI.Converter.list(l))
+ })
+ })
+ }
+
+ /**
+ * GET /api/v1/accounts/:id/identity_proofs
+ *
+ * @param id The account ID.
+ * @return Array of IdentityProof
+ */
+ public async getIdentityProof(id: string): Promise>> {
+ return this.client.get>(`/api/v1/accounts/${id}/identity_proofs`).then(res => {
+ return Object.assign(res, {
+ data: res.data.map(i => FriendicaAPI.Converter.identity_proof(i))
+ })
+ })
+ }
+
+ /**
+ * POST /api/v1/accounts/:id/follow
+ *
+ * @param id The account ID.
+ * @param reblog Receive this account's reblogs in home timeline.
+ * @return Relationship
+ */
+ public async followAccount(id: string, options?: { reblog?: boolean }): Promise> {
+ let params = {}
+ if (options) {
+ if (options.reblog !== undefined) {
+ params = Object.assign(params, {
+ reblog: options.reblog
+ })
+ }
+ }
+ return this.client.post(`/api/v1/accounts/${id}/follow`, params).then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.relationship(res.data)
+ })
+ })
+ }
+
+ /**
+ * POST /api/v1/accounts/:id/unfollow
+ *
+ * @param id The account ID.
+ * @return Relationship
+ */
+ public async unfollowAccount(id: string): Promise> {
+ return this.client.post(`/api/v1/accounts/${id}/unfollow`).then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.relationship(res.data)
+ })
+ })
+ }
+
+ /**
+ * POST /api/v1/accounts/:id/block
+ *
+ * @param id The account ID.
+ * @return Relationship
+ */
+ public async blockAccount(id: string): Promise> {
+ return this.client.post(`/api/v1/accounts/${id}/block`).then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.relationship(res.data)
+ })
+ })
+ }
+
+ /**
+ * POST /api/v1/accounts/:id/unblock
+ *
+ * @param id The account ID.
+ * @return RElationship
+ */
+ public async unblockAccount(id: string): Promise> {
+ return this.client.post(`/api/v1/accounts/${id}/unblock`).then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.relationship(res.data)
+ })
+ })
+ }
+
+ /**
+ * POST /api/v1/accounts/:id/mute
+ *
+ * @param id The account ID.
+ * @param notifications Mute notifications in addition to statuses.
+ * @return Relationship
+ */
+ public async muteAccount(id: string, notifications = true): Promise> {
+ return this.client
+ .post(`/api/v1/accounts/${id}/mute`, {
+ notifications: notifications
+ })
+ .then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.relationship(res.data)
+ })
+ })
+ }
+
+ /**
+ * POST /api/v1/accounts/:id/unmute
+ *
+ * @param id The account ID.
+ * @return Relationship
+ */
+ public async unmuteAccount(id: string): Promise> {
+ return this.client.post(`/api/v1/accounts/${id}/unmute`).then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.relationship(res.data)
+ })
+ })
+ }
+
+ /**
+ * POST /api/v1/accounts/:id/pin
+ *
+ * @param id The account ID.
+ * @return Relationship
+ */
+ public async pinAccount(_id: string): Promise> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError('friendica does not support')
+ reject(err)
+ })
+ }
+
+ /**
+ * POST /api/v1/accounts/:id/unpin
+ *
+ * @param id The account ID.
+ * @return Relationship
+ */
+ public async unpinAccount(_id: string): Promise> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError('friendica does not support')
+ reject(err)
+ })
+ }
+
+ /**
+ * GET /api/v1/accounts/relationships
+ *
+ * @param id The account ID.
+ * @return Relationship
+ */
+ public async getRelationship(id: string): Promise> {
+ return this.client
+ .get>('/api/v1/accounts/relationships', {
+ id: [id]
+ })
+ .then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.relationship(res.data[0])
+ })
+ })
+ }
+
+ /**
+ * Get multiple relationships in one method
+ *
+ * @param ids Array of account IDs.
+ * @return Array of Relationship.
+ */
+ public async getRelationships(ids: Array): Promise>> {
+ return this.client
+ .get>('/api/v1/accounts/relationships', {
+ id: ids
+ })
+ .then(res => {
+ return Object.assign(res, {
+ data: res.data.map(r => FriendicaAPI.Converter.relationship(r))
+ })
+ })
+ }
+
+ /**
+ * GET /api/v1/accounts/search
+ *
+ * @param q Search query.
+ * @param options.limit Max number of results to return. Defaults to 40.
+ * @param options.max_id Return results older than ID.
+ * @param options.since_id Return results newer than ID.
+ * @return The array of accounts.
+ */
+ public async searchAccount(
+ q: string,
+ options?: {
+ following?: boolean
+ resolve?: boolean
+ limit?: number
+ max_id?: string
+ since_id?: string
+ }
+ ): Promise>> {
+ let params = { q: q }
+ if (options) {
+ if (options.following !== undefined && options.following !== null) {
+ params = Object.assign(params, {
+ following: options.following
+ })
+ }
+ if (options.resolve !== undefined && options.resolve !== null) {
+ params = Object.assign(params, {
+ resolve: options.resolve
+ })
+ }
+ if (options.max_id) {
+ params = Object.assign(params, {
+ max_id: options.max_id
+ })
+ }
+ if (options.since_id) {
+ params = Object.assign(params, {
+ since_id: options.since_id
+ })
+ }
+ if (options.limit) {
+ params = Object.assign(params, {
+ limit: options.limit
+ })
+ }
+ }
+ return this.client.get>('/api/v1/accounts/search', params).then(res => {
+ return Object.assign(res, {
+ data: res.data.map(a => FriendicaAPI.Converter.account(a))
+ })
+ })
+ }
+
+ // ======================================
+ // accounts/bookmarks
+ // ======================================
+ /**
+ * GET /api/v1/bookmarks
+ *
+ * @param options.limit Max number of results to return. Defaults to 40.
+ * @param options.max_id Return results older than ID.
+ * @param options.since_id Return results newer than ID.
+ * @param options.min_id Return results immediately newer than ID.
+ * @return Array of statuses.
+ */
+ public async getBookmarks(options?: {
+ limit?: number
+ max_id?: string
+ since_id?: string
+ min_id?: string
+ }): Promise>> {
+ let params = {}
+ if (options) {
+ if (options.max_id) {
+ params = Object.assign(params, {
+ max_id: options.max_id
+ })
+ }
+ if (options.since_id) {
+ params = Object.assign(params, {
+ since_id: options.since_id
+ })
+ }
+ if (options.limit) {
+ params = Object.assign(params, {
+ limit: options.limit
+ })
+ }
+ if (options.min_id) {
+ params = Object.assign(params, {
+ min_id: options.min_id
+ })
+ }
+ }
+ return this.client.get>('/api/v1/bookmarks', params).then(res => {
+ return Object.assign(res, {
+ data: res.data.map(s => FriendicaAPI.Converter.status(s))
+ })
+ })
+ }
+
+ // ======================================
+ // accounts/favourites
+ // ======================================
+ /**
+ * GET /api/v1/favourites
+ *
+ * @param options.limit Max number of results to return. Defaults to 40.
+ * @param options.max_id Return results older than ID.
+ * @param options.min_id Return results immediately newer than ID.
+ * @return Array of statuses.
+ */
+ public async getFavourites(options?: { limit?: number; max_id?: string; min_id?: string }): Promise>> {
+ let params = {}
+ if (options) {
+ if (options.min_id) {
+ params = Object.assign(params, {
+ min_id: options.min_id
+ })
+ }
+ if (options.max_id) {
+ params = Object.assign(params, {
+ max_id: options.max_id
+ })
+ }
+ if (options.limit) {
+ params = Object.assign(params, {
+ limit: options.limit
+ })
+ }
+ }
+ return this.client.get>('/api/v1/favourites', params).then(res => {
+ return Object.assign(res, {
+ data: res.data.map(s => FriendicaAPI.Converter.status(s))
+ })
+ })
+ }
+
+ // ======================================
+ // accounts/mutes
+ // ======================================
+ /**
+ * GET /api/v1/mutes
+ *
+ * @param options.limit Max number of results to return. Defaults to 40.
+ * @param options.max_id Return results older than ID.
+ * @param options.min_id Return results immediately newer than ID.
+ * @return Array of accounts.
+ */
+ public async getMutes(options?: { limit?: number; max_id?: string; min_id?: string }): Promise>> {
+ let params = {}
+ if (options) {
+ if (options.min_id) {
+ params = Object.assign(params, {
+ min_id: options.min_id
+ })
+ }
+ if (options.max_id) {
+ params = Object.assign(params, {
+ max_id: options.max_id
+ })
+ }
+ if (options.limit) {
+ params = Object.assign(params, {
+ limit: options.limit
+ })
+ }
+ }
+ return this.client.get>('/api/v1/mutes', params).then(res => {
+ return Object.assign(res, {
+ data: res.data.map(a => FriendicaAPI.Converter.account(a))
+ })
+ })
+ }
+
+ // ======================================
+ // accounts/blocks
+ // ======================================
+ /**
+ * GET /api/v1/blocks
+ *
+ * @param options.limit Max number of results to return. Defaults to 40.
+ * @param options.max_id Return results older than ID.
+ * @param options.min_id Return results immediately newer than ID.
+ * @return Array of accounts.
+ */
+ public async getBlocks(options?: { limit?: number; max_id?: string; min_id?: string }): Promise>> {
+ let params = {}
+ if (options) {
+ if (options.min_id) {
+ params = Object.assign(params, {
+ min_id: options.min_id
+ })
+ }
+ if (options.max_id) {
+ params = Object.assign(params, {
+ max_id: options.max_id
+ })
+ }
+ if (options.limit) {
+ params = Object.assign(params, {
+ limit: options.limit
+ })
+ }
+ }
+ return this.client.get>('/api/v1/blocks', params).then(res => {
+ return Object.assign(res, {
+ data: res.data.map(a => FriendicaAPI.Converter.account(a))
+ })
+ })
+ }
+
+ // ======================================
+ // accounts/domain_blocks
+ // ======================================
+ public async getDomainBlocks(_options?: { limit?: number; max_id?: string; min_id?: string }): Promise>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError('friendica does not support')
+ reject(err)
+ })
+ }
+
+ public blockDomain(_domain: string): Promise>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError('friendica does not support')
+ reject(err)
+ })
+ }
+
+ public unblockDomain(_domain: string): Promise>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError('friendica does not support')
+ reject(err)
+ })
+ }
+
+ // ======================================
+ // accounts/filters
+ // ======================================
+ /**
+ * GET /api/v1/filters
+ *
+ * @return Array of filters.
+ */
+ public async getFilters(): Promise>> {
+ return this.client.get>('/api/v1/filters').then(res => {
+ return Object.assign(res, {
+ data: res.data.map(f => FriendicaAPI.Converter.filter(f))
+ })
+ })
+ }
+
+ public async getFilter(_id: string): Promise> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError('friendica does not support')
+ reject(err)
+ })
+ }
+
+ public async createFilter(
+ _phrase: string,
+ _context: Array,
+ _options?: {
+ irreversible?: boolean
+ whole_word?: boolean
+ expires_in?: string
+ }
+ ): Promise> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError('friendica does not support')
+ reject(err)
+ })
+ }
+
+ public async updateFilter(
+ _id: string,
+ _phrase: string,
+ _context: Array,
+ _options?: {
+ irreversible?: boolean
+ whole_word?: boolean
+ expires_in?: string
+ }
+ ): Promise> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError('friendica does not support')
+ reject(err)
+ })
+ }
+
+ public async deleteFilter(_id: string): Promise> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError('friendica does not support')
+ reject(err)
+ })
+ }
+
+ // ======================================
+ // accounts/reports
+ // ======================================
+ public async report(
+ _account_id: string,
+ _options?: {
+ status_ids?: Array
+ comment: string
+ forward?: boolean
+ category?: Entity.Category
+ rule_ids?: Array
+ }
+ ): Promise> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError('friendica does not support')
+ reject(err)
+ })
+ }
+
+ // ======================================
+ // accounts/follow_requests
+ // ======================================
+ /**
+ * GET /api/v1/follow_requests
+ *
+ * @param limit Maximum number of results.
+ * @return Array of FollowRequest.
+ */
+ public async getFollowRequests(limit?: number): Promise>> {
+ if (limit) {
+ return this.client
+ .get>('/api/v1/follow_requests', {
+ limit: limit
+ })
+ .then(res => {
+ return Object.assign(res, {
+ data: res.data.map(a => FriendicaAPI.Converter.follow_request(a))
+ })
+ })
+ } else {
+ return this.client.get>('/api/v1/follow_requests').then(res => {
+ return Object.assign(res, {
+ data: res.data.map(a => FriendicaAPI.Converter.follow_request(a))
+ })
+ })
+ }
+ }
+
+ /**
+ * POST /api/v1/follow_requests/:id/authorize
+ *
+ * @param id The FollowRequest ID.
+ * @return Relationship.
+ */
+ public async acceptFollowRequest(id: string): Promise> {
+ return this.client.post(`/api/v1/follow_requests/${id}/authorize`).then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.relationship(res.data)
+ })
+ })
+ }
+
+ /**
+ * POST /api/v1/follow_requests/:id/reject
+ *
+ * @param id The FollowRequest ID.
+ * @return Relationship.
+ */
+ public async rejectFollowRequest(id: string): Promise> {
+ return this.client.post(`/api/v1/follow_requests/${id}/reject`).then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.relationship(res.data)
+ })
+ })
+ }
+
+ // ======================================
+ // accounts/endorsements
+ // ======================================
+ /**
+ * GET /api/v1/endorsements
+ *
+ * @param options.limit Max number of results to return. Defaults to 40.
+ * @param options.max_id Return results older than ID.
+ * @param options.since_id Return results newer than ID.
+ * @return Array of accounts.
+ */
+ public async getEndorsements(options?: { limit?: number; max_id?: string; since_id?: string }): Promise>> {
+ let params = {}
+ if (options) {
+ if (options.limit) {
+ params = Object.assign(params, {
+ limit: options.limit
+ })
+ }
+ if (options.max_id) {
+ params = Object.assign(params, {
+ max_id: options.max_id
+ })
+ }
+ if (options.since_id) {
+ params = Object.assign(params, {
+ since_id: options.since_id
+ })
+ }
+ }
+ return this.client.get>('/api/v1/endorsements', params).then(res => {
+ return Object.assign(res, {
+ data: res.data.map(a => FriendicaAPI.Converter.account(a))
+ })
+ })
+ }
+
+ // ======================================
+ // accounts/featured_tags
+ // ======================================
+ public async getFeaturedTags(): Promise>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError('friendica does not support')
+ reject(err)
+ })
+ }
+
+ public async createFeaturedTag(_name: string): Promise> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError('friendica does not support')
+ reject(err)
+ })
+ }
+
+ public deleteFeaturedTag(_id: string): Promise>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError('friendica does not support')
+ reject(err)
+ })
+ }
+
+ public async getSuggestedTags(): Promise>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError('friendica does not support')
+ reject(err)
+ })
+ }
+
+ // ======================================
+ // accounts/preferences
+ // ======================================
+ /**
+ * GET /api/v1/preferences
+ *
+ * @return Preferences.
+ */
+ public async getPreferences(): Promise> {
+ return this.client.get('/api/v1/preferences').then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.preferences(res.data)
+ })
+ })
+ }
+
+ // ======================================
+ // accounts/followed_tags
+ // ======================================
+ public async getFollowedTags(): Promise>> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError('friendica does not support')
+ reject(err)
+ })
+ }
+
+ // ======================================
+ // accounts/suggestions
+ // ======================================
+ /**
+ * GET /api/v1/suggestions
+ *
+ * @param limit Maximum number of results.
+ * @return Array of accounts.
+ */
+ public async getSuggestions(limit?: number): Promise>> {
+ if (limit) {
+ return this.client
+ .get>('/api/v1/suggestions', {
+ limit: limit
+ })
+ .then(res => {
+ return Object.assign(res, {
+ data: res.data.map(a => FriendicaAPI.Converter.account(a))
+ })
+ })
+ } else {
+ return this.client.get>('/api/v1/suggestions').then(res => {
+ return Object.assign(res, {
+ data: res.data.map(a => FriendicaAPI.Converter.account(a))
+ })
+ })
+ }
+ }
+
+ // ======================================
+ // accounts/tags
+ // ======================================
+ /**
+ * GET /api/v1/tags/:id
+ *
+ * @param id Target hashtag id.
+ * @return Tag
+ */
+ public async getTag(id: string): Promise> {
+ return this.client.get(`/api/v1/tags/${id}`).then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.tag(res.data)
+ })
+ })
+ }
+
+ /**
+ * POST /api/v1/tags/:id/follow
+ *
+ * @param id Target hashtag id.
+ * @return Tag
+ */
+ public async followTag(id: string): Promise> {
+ return this.client.post(`/api/v1/tags/${id}/follow`).then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.tag(res.data)
+ })
+ })
+ }
+
+ /**
+ * POST /api/v1/tags/:id/unfollow
+ *
+ * @param id Target hashtag id.
+ * @return Tag
+ */
+ public async unfollowTag(id: string): Promise> {
+ return this.client.post(`/api/v1/tags/${id}/unfollow`).then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.tag(res.data)
+ })
+ })
+ }
+
+ // ======================================
+ // statuses
+ // ======================================
+ /**
+ * POST /api/v1/statuses
+ *
+ * @param status Text content of status.
+ * @param options.media_ids Array of Attachment ids.
+ * @param options.poll Poll object.
+ * @param options.in_reply_to_id ID of the status being replied to, if status is a reply.
+ * @param options.sensitive Mark status and attached media as sensitive?
+ * @param options.spoiler_text Text to be shown as a warning or subject before the actual content.
+ * @param options.visibility Visibility of the posted status.
+ * @param options.scheduled_at ISO 8601 Datetime at which to schedule a status.
+ * @param options.language ISO 639 language code for this status.
+ * @param options.quote_id ID of the status being quoted to, if status is a quote.
+ * @return Status. When options.scheduled_at is present, ScheduledStatus is returned instead.
+ */
+ public async postStatus(
+ status: string,
+ options: {
+ media_ids?: Array
+ poll?: { options: Array; expires_in: number; multiple?: boolean; hide_totals?: boolean }
+ in_reply_to_id?: string
+ sensitive?: boolean
+ spoiler_text?: string
+ visibility?: 'public' | 'unlisted' | 'private' | 'direct'
+ scheduled_at?: string
+ language?: string
+ quote_id?: string
+ }
+ ): Promise> {
+ let params = {
+ status: status
+ }
+ if (options) {
+ if (options.media_ids) {
+ params = Object.assign(params, {
+ media_ids: options.media_ids
+ })
+ }
+ if (options.poll) {
+ let pollParam = {
+ options: options.poll.options,
+ expires_in: options.poll.expires_in
+ }
+ if (options.poll.multiple !== undefined) {
+ pollParam = Object.assign(pollParam, {
+ multiple: options.poll.multiple
+ })
+ }
+ if (options.poll.hide_totals !== undefined) {
+ pollParam = Object.assign(pollParam, {
+ hide_totals: options.poll.hide_totals
+ })
+ }
+ params = Object.assign(params, {
+ poll: pollParam
+ })
+ }
+ if (options.in_reply_to_id) {
+ params = Object.assign(params, {
+ in_reply_to_id: options.in_reply_to_id
+ })
+ }
+ if (options.sensitive !== undefined) {
+ params = Object.assign(params, {
+ sensitive: options.sensitive
+ })
+ }
+ if (options.spoiler_text) {
+ params = Object.assign(params, {
+ spoiler_text: options.spoiler_text
+ })
+ }
+ if (options.visibility) {
+ params = Object.assign(params, {
+ visibility: options.visibility
+ })
+ }
+ if (options.scheduled_at) {
+ params = Object.assign(params, {
+ scheduled_at: options.scheduled_at
+ })
+ }
+ if (options.language) {
+ params = Object.assign(params, {
+ language: options.language
+ })
+ }
+ if (options.quote_id) {
+ params = Object.assign(params, {
+ quote_id: options.quote_id
+ })
+ }
+ }
+ if (options.scheduled_at) {
+ return this.client.post('/api/v1/statuses', params).then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.scheduled_status(res.data)
+ })
+ })
+ }
+ return this.client.post('/api/v1/statuses', params).then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.status(res.data)
+ })
+ })
+ }
+ /**
+ * GET /api/v1/statuses/:id
+ *
+ * @param id The target status id.
+ * @return Status
+ */
+ public async getStatus(id: string): Promise> {
+ return this.client.get(`/api/v1/statuses/${id}`).then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.status(res.data)
+ })
+ })
+ }
+
+ /**
+ PUT /api/v1/statuses/:id
+ *
+ * @param id The target status id.
+ * @return Status
+ */
+ public async editStatus(
+ id: string,
+ options: {
+ status?: string
+ spoiler_text?: string
+ sensitive?: boolean
+ media_ids?: Array
+ poll?: { options?: Array; expires_in?: number; multiple?: boolean; hide_totals?: boolean }
+ }
+ ): Promise> {
+ let params = {}
+ if (options.status) {
+ params = Object.assign(params, {
+ status: options.status
+ })
+ }
+ if (options.spoiler_text) {
+ params = Object.assign(params, {
+ spoiler_text: options.spoiler_text
+ })
+ }
+ if (options.sensitive) {
+ params = Object.assign(params, {
+ sensitive: options.sensitive
+ })
+ }
+ if (options.media_ids) {
+ params = Object.assign(params, {
+ media_ids: options.media_ids
+ })
+ }
+ if (options.poll) {
+ let pollParam = {}
+ if (options.poll.options !== undefined) {
+ pollParam = Object.assign(pollParam, {
+ options: options.poll.options
+ })
+ }
+ if (options.poll.expires_in !== undefined) {
+ pollParam = Object.assign(pollParam, {
+ expires_in: options.poll.expires_in
+ })
+ }
+ if (options.poll.multiple !== undefined) {
+ pollParam = Object.assign(pollParam, {
+ multiple: options.poll.multiple
+ })
+ }
+ if (options.poll.hide_totals !== undefined) {
+ pollParam = Object.assign(pollParam, {
+ hide_totals: options.poll.hide_totals
+ })
+ }
+ params = Object.assign(params, {
+ poll: pollParam
+ })
+ }
+ return this.client.put(`/api/v1/statuses/${id}`, params).then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.status(res.data)
+ })
+ })
+ }
+
+ /**
+ * DELETE /api/v1/statuses/:id
+ *
+ * @param id The target status id.
+ * @return Status
+ */
+ public async deleteStatus(id: string): Promise> {
+ return this.client.del(`/api/v1/statuses/${id}`).then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.status(res.data)
+ })
+ })
+ }
+
+ /**
+ * GET /api/v1/statuses/:id/context
+ *
+ * Get parent and child statuses.
+ * @param id The target status id.
+ * @return Context
+ */
+ public async getStatusContext(
+ id: string,
+ options?: { limit?: number; max_id?: string; since_id?: string }
+ ): Promise> {
+ let params = {}
+ if (options) {
+ if (options.limit) {
+ params = Object.assign(params, {
+ limit: options.limit
+ })
+ }
+ if (options.max_id) {
+ params = Object.assign(params, {
+ max_id: options.max_id
+ })
+ }
+ if (options.since_id) {
+ params = Object.assign(params, {
+ since_id: options.since_id
+ })
+ }
+ }
+ return this.client.get(`/api/v1/statuses/${id}/context`, params).then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.context(res.data)
+ })
+ })
+ }
+
+ /**
+ * GET /api/v1/statuses/:id/source
+ *
+ * Obtain the source properties for a status so that it can be edited.
+ * @param id The target status id.
+ * @return StatusSource
+ */
+ public async getStatusSource(id: string): Promise> {
+ return this.client.get(`/api/v1/statuses/${id}/source`).then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.status_source(res.data)
+ })
+ })
+ }
+
+ /**
+ * GET /api/v1/statuses/:id/reblogged_by
+ *
+ * @param id The target status id.
+ * @return Array of accounts.
+ */
+ public async getStatusRebloggedBy(id: string): Promise>> {
+ return this.client.get>(`/api/v1/statuses/${id}/reblogged_by`).then(res => {
+ return Object.assign(res, {
+ data: res.data.map(a => FriendicaAPI.Converter.account(a))
+ })
+ })
+ }
+
+ /**
+ * GET /api/v1/statuses/:id/favourited_by
+ *
+ * @param id The target status id.
+ * @return Array of accounts.
+ */
+ public async getStatusFavouritedBy(id: string): Promise>> {
+ return this.client.get>(`/api/v1/statuses/${id}/favourited_by`).then(res => {
+ return Object.assign(res, {
+ data: res.data.map(a => FriendicaAPI.Converter.account(a))
+ })
+ })
+ }
+
+ /**
+ * POST /api/v1/statuses/:id/favourite
+ *
+ * @param id The target status id.
+ * @return Status.
+ */
+ public async favouriteStatus(id: string): Promise> {
+ return this.client.post(`/api/v1/statuses/${id}/favourite`).then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.status(res.data)
+ })
+ })
+ }
+
+ /**
+ * POST /api/v1/statuses/:id/unfavourite
+ *
+ * @param id The target status id.
+ * @return Status.
+ */
+ public async unfavouriteStatus(id: string): Promise> {
+ return this.client.post(`/api/v1/statuses/${id}/unfavourite`).then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.status(res.data)
+ })
+ })
+ }
+
+ /**
+ * POST /api/v1/statuses/:id/reblog
+ *
+ * @param id The target status id.
+ * @return Status.
+ */
+ public async reblogStatus(id: string): Promise> {
+ return this.client.post(`/api/v1/statuses/${id}/reblog`).then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.status(res.data)
+ })
+ })
+ }
+
+ /**
+ * POST /api/v1/statuses/:id/unreblog
+ *
+ * @param id The target status id.
+ * @return Status.
+ */
+ public async unreblogStatus(id: string): Promise> {
+ return this.client.post(`/api/v1/statuses/${id}/unreblog`).then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.status(res.data)
+ })
+ })
+ }
+
+ /**
+ * POST /api/v1/statuses/:id/bookmark
+ *
+ * @param id The target status id.
+ * @return Status.
+ */
+ public async bookmarkStatus(id: string): Promise> {
+ return this.client.post(`/api/v1/statuses/${id}/bookmark`).then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.status(res.data)
+ })
+ })
+ }
+
+ /**
+ * POST /api/v1/statuses/:id/unbookmark
+ *
+ * @param id The target status id.
+ * @return Status.
+ */
+ public async unbookmarkStatus(id: string): Promise> {
+ return this.client.post(`/api/v1/statuses/${id}/unbookmark`).then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.status(res.data)
+ })
+ })
+ }
+
+ /**
+ * POST /api/v1/statuses/:id/mute
+ *
+ * @param id The target status id.
+ * @return Status
+ */
+ public async muteStatus(id: string): Promise> {
+ return this.client.post(`/api/v1/statuses/${id}/mute`).then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.status(res.data)
+ })
+ })
+ }
+
+ /**
+ * POST /api/v1/statuses/:id/unmute
+ *
+ * @param id The target status id.
+ * @return Status
+ */
+ public async unmuteStatus(id: string): Promise> {
+ return this.client.post(`/api/v1/statuses/${id}/unmute`).then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.status(res.data)
+ })
+ })
+ }
+
+ /**
+ * POST /api/v1/statuses/:id/pin
+ * @param id The target status id.
+ * @return Status
+ */
+ public async pinStatus(id: string): Promise> {
+ return this.client.post(`/api/v1/statuses/${id}/pin`).then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.status(res.data)
+ })
+ })
+ }
+
+ /**
+ * POST /api/v1/statuses/:id/unpin
+ *
+ * @param id The target status id.
+ * @return Status
+ */
+ public async unpinStatus(id: string): Promise> {
+ return this.client.post(`/api/v1/statuses/${id}/unpin`).then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.status(res.data)
+ })
+ })
+ }
+
+ // ======================================
+ // statuses/media
+ // ======================================
+ /**
+ * POST /api/v2/media
+ *
+ * @param file The file to be attached, using multipart form data.
+ * @param options.description A plain-text description of the media.
+ * @param options.focus Two floating points (x,y), comma-delimited, ranging from -1.0 to 1.0.
+ * @return Attachment
+ */
+ public async uploadMedia(
+ file: any,
+ options?: { description?: string; focus?: string }
+ ): Promise> {
+ const formData = new FormData()
+ formData.append('file', file)
+ if (options) {
+ if (options.description) {
+ formData.append('description', options.description)
+ }
+ if (options.focus) {
+ formData.append('focus', options.focus)
+ }
+ }
+ return this.client.postForm('/api/v2/media', formData).then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.async_attachment(res.data)
+ })
+ })
+ }
+
+ /**
+ * GET /api/v1/media/:id
+ *
+ * @param id Target media ID.
+ * @return Attachment
+ */
+ public async getMedia(id: string): Promise> {
+ const res = await this.client.get(`/api/v1/media/${id}`)
+
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.attachment(res.data)
+ })
+ }
+
+ /**
+ * PUT /api/v1/media/:id
+ *
+ * @param id Target media ID.
+ * @param options.file The file to be attached, using multipart form data.
+ * @param options.description A plain-text description of the media.
+ * @param options.focus Two floating points (x,y), comma-delimited, ranging from -1.0 to 1.0.
+ * @param options.is_sensitive Whether the media is sensitive.
+ * @return Attachment
+ */
+ public async updateMedia(
+ id: string,
+ options?: {
+ file?: any
+ description?: string
+ focus?: string
+ }
+ ): Promise> {
+ const formData = new FormData()
+ if (options) {
+ if (options.file) {
+ formData.append('file', options.file)
+ }
+ if (options.description) {
+ formData.append('description', options.description)
+ }
+ if (options.focus) {
+ formData.append('focus', options.focus)
+ }
+ }
+ return this.client.putForm(`/api/v1/media/${id}`, formData).then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.attachment(res.data)
+ })
+ })
+ }
+
+ // ======================================
+ // statuses/polls
+ // ======================================
+ /**
+ * GET /api/v1/polls/:id
+ *
+ * @param id Target poll ID.
+ * @return Poll
+ */
+ public async getPoll(id: string): Promise> {
+ return this.client.get(`/api/v1/polls/${id}`).then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.poll(res.data)
+ })
+ })
+ }
+
+ public async votePoll(_id: string, _choices: Array): Promise> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError('friendica does not support')
+ reject(err)
+ })
+ }
+
+ // ======================================
+ // statuses/scheduled_statuses
+ // ======================================
+ /**
+ * GET /api/v1/scheduled_statuses
+ *
+ * @param options.limit Max number of results to return. Defaults to 20.
+ * @param options.max_id Return results older than ID.
+ * @param options.since_id Return results newer than ID.
+ * @param options.min_id Return results immediately newer than ID.
+ * @return Array of scheduled statuses.
+ */
+ public async getScheduledStatuses(options?: {
+ limit?: number | null
+ max_id?: string | null
+ since_id?: string | null
+ min_id?: string | null
+ }): Promise>> {
+ let params = {}
+ if (options) {
+ if (options.limit) {
+ params = Object.assign(params, {
+ limit: options.limit
+ })
+ }
+ if (options.max_id) {
+ params = Object.assign(params, {
+ max_id: options.max_id
+ })
+ }
+ if (options.since_id) {
+ params = Object.assign(params, {
+ since_id: options.since_id
+ })
+ }
+ if (options.min_id) {
+ params = Object.assign(params, {
+ min_id: options.min_id
+ })
+ }
+ }
+ return this.client.get>('/api/v1/scheduled_statuses', params).then(res => {
+ return Object.assign(res, {
+ data: res.data.map(s => FriendicaAPI.Converter.scheduled_status(s))
+ })
+ })
+ }
+
+ /**
+ * GET /api/v1/scheduled_statuses/:id
+ *
+ * @param id Target status ID.
+ * @return ScheduledStatus.
+ */
+ public async getScheduledStatus(id: string): Promise> {
+ return this.client.get(`/api/v1/scheduled_statuses/${id}`).then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.scheduled_status(res.data)
+ })
+ })
+ }
+
+ public async scheduleStatus(_id: string, _scheduled_at?: string | null): Promise> {
+ return new Promise((_, reject) => {
+ const err = new NoImplementedError('friendica does not support')
+ reject(err)
+ })
+ }
+
+ /**
+ * DELETE /api/v1/scheduled_statuses/:id
+ *
+ * @param id Target scheduled status ID.
+ */
+ public cancelScheduledStatus(id: string): Promise>> {
+ return this.client.del>(`/api/v1/scheduled_statuses/${id}`)
+ }
+
+ // ======================================
+ // timelines
+ // ======================================
+ /**
+ * GET /api/v1/timelines/public
+ *
+ * @param options.only_media Show only statuses with media attached? Defaults to false.
+ * @param options.limit Max number of results to return. Defaults to 20.
+ * @param options.max_id Return results older than ID.
+ * @param options.since_id Return results newer than ID.
+ * @param options.min_id Return results immediately newer than ID.
+ * @return Array of statuses.
+ */
+ public async getPublicTimeline(options?: {
+ only_media?: boolean
+ limit?: number
+ max_id?: string
+ since_id?: string
+ min_id?: string
+ }): Promise>> {
+ let params = {
+ local: false
+ }
+ if (options) {
+ if (options.only_media !== undefined) {
+ params = Object.assign(params, {
+ only_media: options.only_media
+ })
+ }
+ if (options.max_id) {
+ params = Object.assign(params, {
+ max_id: options.max_id
+ })
+ }
+ if (options.since_id) {
+ params = Object.assign(params, {
+ since_id: options.since_id
+ })
+ }
+ if (options.min_id) {
+ params = Object.assign(params, {
+ min_id: options.min_id
+ })
+ }
+ if (options.limit) {
+ params = Object.assign(params, {
+ limit: options.limit
+ })
+ }
+ }
+ return this.client.get>('/api/v1/timelines/public', params).then(res => {
+ return Object.assign(res, {
+ data: res.data.map(s => FriendicaAPI.Converter.status(s))
+ })
+ })
+ }
+
+ /**
+ * GET /api/v1/timelines/public
+ *
+ * @param options.only_media Show only statuses with media attached? Defaults to false.
+ * @param options.limit Max number of results to return. Defaults to 20.
+ * @param options.max_id Return results older than ID.
+ * @param options.since_id Return results newer than ID.
+ * @param options.min_id Return results immediately newer than ID.
+ * @return Array of statuses.
+ */
+ public async getLocalTimeline(options?: {
+ only_media?: boolean
+ limit?: number
+ max_id?: string
+ since_id?: string
+ min_id?: string
+ }): Promise>> {
+ let params = {
+ local: true
+ }
+ if (options) {
+ if (options.only_media !== undefined) {
+ params = Object.assign(params, {
+ only_media: options.only_media
+ })
+ }
+ if (options.max_id) {
+ params = Object.assign(params, {
+ max_id: options.max_id
+ })
+ }
+ if (options.since_id) {
+ params = Object.assign(params, {
+ since_id: options.since_id
+ })
+ }
+ if (options.min_id) {
+ params = Object.assign(params, {
+ min_id: options.min_id
+ })
+ }
+ if (options.limit) {
+ params = Object.assign(params, {
+ limit: options.limit
+ })
+ }
+ }
+ return this.client.get>('/api/v1/timelines/public', params).then(res => {
+ return Object.assign(res, {
+ data: res.data.map(s => FriendicaAPI.Converter.status(s))
+ })
+ })
+ }
+
+ /**
+ * GET /api/v1/timelines/tag/:hashtag
+ *
+ * @param hashtag Content of a #hashtag, not including # symbol.
+ * @param options.local Show only local statuses? Defaults to false.
+ * @param options.only_media Show only statuses with media attached? Defaults to false.
+ * @param options.limit Max number of results to return. Defaults to 20.
+ * @param options.max_id Return results older than ID.
+ * @param options.since_id Return results newer than ID.
+ * @param options.min_id Return results immediately newer than ID.
+ * @return Array of statuses.
+ */
+ public async getTagTimeline(
+ hashtag: string,
+ options?: {
+ local?: boolean
+ only_media?: boolean
+ limit?: number
+ max_id?: string
+ since_id?: string
+ min_id?: string
+ }
+ ): Promise>> {
+ let params = {}
+ if (options) {
+ if (options.local !== undefined) {
+ params = Object.assign(params, {
+ local: options.local
+ })
+ }
+ if (options.only_media !== undefined) {
+ params = Object.assign(params, {
+ only_media: options.only_media
+ })
+ }
+ if (options.max_id) {
+ params = Object.assign(params, {
+ max_id: options.max_id
+ })
+ }
+ if (options.since_id) {
+ params = Object.assign(params, {
+ since_id: options.since_id
+ })
+ }
+ if (options.min_id) {
+ params = Object.assign(params, {
+ min_id: options.min_id
+ })
+ }
+ if (options.limit) {
+ params = Object.assign(params, {
+ limit: options.limit
+ })
+ }
+ }
+ return this.client.get>(`/api/v1/timelines/tag/${hashtag}`, params).then(res => {
+ return Object.assign(res, {
+ data: res.data.map(s => FriendicaAPI.Converter.status(s))
+ })
+ })
+ }
+
+ /**
+ * GET /api/v1/timelines/home
+ *
+ * @param options.local Show only local statuses? Defaults to false.
+ * @param options.limit Max number of results to return. Defaults to 20.
+ * @param options.max_id Return results older than ID.
+ * @param options.since_id Return results newer than ID.
+ * @param options.min_id Return results immediately newer than ID.
+ * @return Array of statuses.
+ */
+ public async getHomeTimeline(options?: {
+ local?: boolean
+ limit?: number
+ max_id?: string
+ since_id?: string
+ min_id?: string
+ }): Promise>> {
+ let params = {}
+ if (options) {
+ if (options.local !== undefined) {
+ params = Object.assign(params, {
+ local: options.local
+ })
+ }
+ if (options.max_id) {
+ params = Object.assign(params, {
+ max_id: options.max_id
+ })
+ }
+ if (options.since_id) {
+ params = Object.assign(params, {
+ since_id: options.since_id
+ })
+ }
+ if (options.min_id) {
+ params = Object.assign(params, {
+ min_id: options.min_id
+ })
+ }
+ if (options.limit) {
+ params = Object.assign(params, {
+ limit: options.limit
+ })
+ }
+ }
+ return this.client.get>('/api/v1/timelines/home', params).then(res => {
+ return Object.assign(res, {
+ data: res.data.map(s => FriendicaAPI.Converter.status(s))
+ })
+ })
+ }
+
+ /**
+ * GET /api/v1/timelines/list/:list_id
+ *
+ * @param list_id Local ID of the list in the database.
+ * @param options.limit Max number of results to return. Defaults to 20.
+ * @param options.max_id Return results older than ID.
+ * @param options.since_id Return results newer than ID.
+ * @param options.min_id Return results immediately newer than ID.
+ * @return Array of statuses.
+ */
+ public async getListTimeline(
+ list_id: string,
+ options?: {
+ limit?: number
+ max_id?: string
+ since_id?: string
+ min_id?: string
+ }
+ ): Promise>> {
+ let params = {}
+ if (options) {
+ if (options.max_id) {
+ params = Object.assign(params, {
+ max_id: options.max_id
+ })
+ }
+ if (options.since_id) {
+ params = Object.assign(params, {
+ since_id: options.since_id
+ })
+ }
+ if (options.min_id) {
+ params = Object.assign(params, {
+ min_id: options.min_id
+ })
+ }
+ if (options.limit) {
+ params = Object.assign(params, {
+ limit: options.limit
+ })
+ }
+ }
+ return this.client.get>(`/api/v1/timelines/list/${list_id}`, params).then(res => {
+ return Object.assign(res, {
+ data: res.data.map(s => FriendicaAPI.Converter.status(s))
+ })
+ })
+ }
+
+ // ======================================
+ // timelines/conversations
+ // ======================================
+ /**
+ * GET /api/v1/conversations
+ *
+ * @param options.limit Max number of results to return. Defaults to 20.
+ * @param options.max_id Return results older than ID.
+ * @param options.since_id Return results newer than ID.
+ * @param options.min_id Return results immediately newer than ID.
+ * @return Array of statuses.
+ */
+ public async getConversationTimeline(options?: {
+ limit?: number
+ max_id?: string
+ since_id?: string
+ min_id?: string
+ }): Promise>> {
+ let params = {}
+ if (options) {
+ if (options.max_id) {
+ params = Object.assign(params, {
+ max_id: options.max_id
+ })
+ }
+ if (options.since_id) {
+ params = Object.assign(params, {
+ since_id: options.since_id
+ })
+ }
+ if (options.min_id) {
+ params = Object.assign(params, {
+ min_id: options.min_id
+ })
+ }
+ if (options.limit) {
+ params = Object.assign(params, {
+ limit: options.limit
+ })
+ }
+ }
+ return this.client.get>('/api/v1/conversations', params).then(res => {
+ return Object.assign(res, {
+ data: res.data.map(c => FriendicaAPI.Converter.conversation(c))
+ })
+ })
+ }
+
+ /**
+ * DELETE /api/v1/conversations/:id
+ *
+ * @param id Target conversation ID.
+ */
+ public deleteConversation(id: string): Promise>> {
+ return this.client.del>(`/api/v1/conversations/${id}`)
+ }
+
+ /**
+ * POST /api/v1/conversations/:id/read
+ *
+ * @param id Target conversation ID.
+ * @return Conversation.
+ */
+ public async readConversation(id: string): Promise> {
+ return this.client.post(`/api/v1/conversations/${id}/read`).then(res => {
+ return Object.assign(res, {
+ data: FriendicaAPI.Converter.conversation(res.data)
+ })
+ })
+ }
+
+ // ======================================
+ // timelines/lists
+ // ======================================
+ /**
+ * GET /api/v1/lists
+ *
+ * @return Array of lists.
+ */
+ public async getLists(): Promise