Merge pull request #5 from transfem-org/feature/masto-api

Feature/masto api
This commit is contained in:
Amelia Yukii 2023-09-24 17:48:55 +02:00 committed by GitHub
commit 018280e3df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
251 changed files with 25148 additions and 129 deletions

4
.gitignore vendored
View File

@ -58,6 +58,10 @@ ormconfig.json
temp temp
/packages/frontend/src/**/*.stories.ts /packages/frontend/src/**/*.stories.ts
# Sharkey
/packages/megalodon/lib
/packages/megalodon-bk
# blender backups # blender backups
*.blend1 *.blend1
*.blend2 *.blend2

View File

@ -24,6 +24,7 @@ COPY --link ["packages/backend/package.json", "./packages/backend/"]
COPY --link ["packages/frontend/package.json", "./packages/frontend/"] COPY --link ["packages/frontend/package.json", "./packages/frontend/"]
COPY --link ["packages/sw/package.json", "./packages/sw/"] COPY --link ["packages/sw/package.json", "./packages/sw/"]
COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"] COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
COPY --link ["packages/megalodon/package.json", "./packages/megalodon/"]
RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \ RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \
pnpm i --frozen-lockfile --aggregate-output pnpm i --frozen-lockfile --aggregate-output

View File

@ -1,9 +1,9 @@
<div align="center"> <div align="center">
<a href="https://dev.transfem.social"> <a href="https://test.transfem.social">
<img src="https://cdn.transfem.social/files/06eb1052-fd80-448a-803e-3adf7a2d03a3.png" alt="Sharky logo" style="border-radius:50%" width="400"/> <img src="https://cdn.transfem.social/files/06eb1052-fd80-448a-803e-3adf7a2d03a3.png" alt="Sharky logo" style="border-radius:50%" width="400"/>
</a> </a>
**🌎 **[Sharky](https://dev.transfem.social/)** is an open source, decentralized social media platform that's free forever! 🚀** **🌎 **[Sharky](https://test.transfem.social/)** is an open source, decentralized social media platform that's free forever! 🚀**
--- ---
@ -15,13 +15,19 @@
<div> <div>
<a href="https://dev.transfem.social/"><img src="https://cdn.transfem.social/files/dce7b668-fa9e-44e9-8e53-60fd743681aa.png" align="right" height="320px"/></a> <a href="https://test.transfem.social/"><img src="https://cdn.transfem.social/files/dce7b668-fa9e-44e9-8e53-60fd743681aa.png" align="right" height="320px"/></a>
## ✨ Features ## ✨ Features
- **ActivityPub support**\ - **ActivityPub support**\
Not on Sharky? No problem! Not only can Sharky instances talk to each other, but you can make friends with people on other networks like Mastodon and Pixelfed! Not on Sharky? No problem! Not only can Sharky instances talk to each other, but you can make friends with people on other networks like Mastodon and Pixelfed!
- **Reactions**\ - **Reactions**\
You can add emoji reactions to any post! No longer are you bound by a like button, show everyone exactly how you feel with the tap of a button. You can add emoji reactions to any post! No longer are you bound by a like button, show everyone exactly how you feel with the tap of a button.
- **Post Editing**\
In Sharkey you can edit your post, this is not possible in normal Misskey
- **Mastodon API**\
Sharkey implements the mastodon api unlike normal Misskey
- **UI/UX Improvements**\
Sharkey makes some Ui/UX improvements to make it easier to navigate
- **Drive**\ - **Drive**\
With Sharky's built in drive, you get cloud storage right in your social media, where you can upload any files, make folders, and find media from posts you've made! With Sharky's built in drive, you get cloud storage right in your social media, where you can upload any files, make folders, and find media from posts you've made!
- **Rich Web UI**\ - **Rich Web UI**\

View File

@ -1,10 +1,10 @@
{ {
"name": "misskey", "name": "sharkey",
"version": "2023.9.0-beta.10", "version": "2023.9.0-beta.10",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/misskey-dev/misskey.git" "url": "https://github.com/transfem-org/sharkey.git"
}, },
"packageManager": "pnpm@8.7.6", "packageManager": "pnpm@8.7.6",
"workspaces": [ "workspaces": [

View File

@ -15,7 +15,7 @@
"watch:swc": "swc src -d built -D -w", "watch:swc": "swc src -d built -D -w",
"build:tsc": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json", "build:tsc": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json",
"watch": "node watch.mjs", "watch": "node watch.mjs",
"typecheck": "tsc --noEmit", "typecheck": "pnpm --filter megalodon build && tsc --noEmit",
"eslint": "eslint --quiet \"src/**/*.ts\"", "eslint": "eslint --quiet \"src/**/*.ts\"",
"lint": "pnpm typecheck && pnpm eslint", "lint": "pnpm typecheck && pnpm eslint",
"jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit", "jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit",
@ -99,6 +99,7 @@
"date-fns": "2.30.0", "date-fns": "2.30.0",
"deep-email-validator": "0.1.21", "deep-email-validator": "0.1.21",
"fastify": "4.23.2", "fastify": "4.23.2",
"fastify-multer": "^2.0.3",
"feed": "4.2.2", "feed": "4.2.2",
"file-type": "18.5.0", "file-type": "18.5.0",
"fluent-ffmpeg": "2.1.2", "fluent-ffmpeg": "2.1.2",
@ -116,6 +117,7 @@
"json5": "2.2.3", "json5": "2.2.3",
"jsonld": "8.3.1", "jsonld": "8.3.1",
"jsrsasign": "10.8.6", "jsrsasign": "10.8.6",
"megalodon": "workspace:*",
"meilisearch": "0.34.2", "meilisearch": "0.34.2",
"mfm-js": "0.23.3", "mfm-js": "0.23.3",
"microformats-parser": "1.5.2", "microformats-parser": "1.5.2",

File diff suppressed because one or more lines are too long

View File

@ -39,6 +39,7 @@ import { QueueStatsChannelService } from './api/stream/channels/queue-stats.js';
import { ServerStatsChannelService } from './api/stream/channels/server-stats.js'; import { ServerStatsChannelService } from './api/stream/channels/server-stats.js';
import { UserListChannelService } from './api/stream/channels/user-list.js'; import { UserListChannelService } from './api/stream/channels/user-list.js';
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js'; import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
import { MastodonApiServerService } from './api/mastodon/MastodonApiServerService.js';
import { ClientLoggerService } from './web/ClientLoggerService.js'; import { ClientLoggerService } from './web/ClientLoggerService.js';
import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js'; import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js';
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js'; import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
@ -84,6 +85,7 @@ import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
ServerStatsChannelService, ServerStatsChannelService,
UserListChannelService, UserListChannelService,
OpenApiServerService, OpenApiServerService,
MastodonApiServerService,
OAuth2ProviderService, OAuth2ProviderService,
], ],
exports: [ exports: [

View File

@ -30,6 +30,7 @@ import { WellKnownServerService } from './WellKnownServerService.js';
import { FileServerService } from './FileServerService.js'; import { FileServerService } from './FileServerService.js';
import { ClientServerService } from './web/ClientServerService.js'; import { ClientServerService } from './web/ClientServerService.js';
import { OpenApiServerService } from './api/openapi/OpenApiServerService.js'; import { OpenApiServerService } from './api/openapi/OpenApiServerService.js';
import { MastodonApiServerService } from './api/mastodon/MastodonApiServerService.js';
import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js'; import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
const _dirname = fileURLToPath(new URL('.', import.meta.url)); const _dirname = fileURLToPath(new URL('.', import.meta.url));
@ -56,6 +57,7 @@ export class ServerService implements OnApplicationShutdown {
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private apiServerService: ApiServerService, private apiServerService: ApiServerService,
private openApiServerService: OpenApiServerService, private openApiServerService: OpenApiServerService,
private mastodonApiServerService: MastodonApiServerService,
private streamingApiServerService: StreamingApiServerService, private streamingApiServerService: StreamingApiServerService,
private activityPubServerService: ActivityPubServerService, private activityPubServerService: ActivityPubServerService,
private wellKnownServerService: WellKnownServerService, private wellKnownServerService: WellKnownServerService,
@ -95,6 +97,7 @@ export class ServerService implements OnApplicationShutdown {
fastify.register(this.apiServerService.createServer, { prefix: '/api' }); fastify.register(this.apiServerService.createServer, { prefix: '/api' });
fastify.register(this.openApiServerService.createServer); fastify.register(this.openApiServerService.createServer);
fastify.register(this.mastodonApiServerService.createServer, { prefix: '/api' });
fastify.register(this.fileServerService.createServer); fastify.register(this.fileServerService.createServer);
fastify.register(this.activityPubServerService.createServer); fastify.register(this.activityPubServerService.createServer);
fastify.register(this.nodeinfoServerService.createServer); fastify.register(this.nodeinfoServerService.createServer);

View File

@ -0,0 +1,783 @@
import { Inject, Injectable } from '@nestjs/common';
import megalodon, { Entity, MegalodonInterface } from 'megalodon';
import { IsNull } from 'typeorm';
import multer from 'fastify-multer';
import type { UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import type { Config } from '@/config.js';
import { MetaService } from '@/core/MetaService.js';
import { convertId, IdConvertType as IdType, convertAccount, convertAnnouncement, convertFilter, convertAttachment, convertFeaturedTag, convertList } from './converters.js';
import { getInstance } from './endpoints/meta.js';
import { ApiAuthMastodon, ApiAccountMastodon, ApiFilterMastodon, ApiNotifyMastodon, ApiSearchMastodon, ApiTimelineMastodon, ApiStatusMastodon } from './endpoints.js';
import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
export function getClient(BASE_URL: string, authorization: string | undefined): MegalodonInterface {
const accessTokenArr = authorization?.split(' ') ?? [null];
const accessToken = accessTokenArr[accessTokenArr.length - 1];
const generator = (megalodon as any).default;
const client = generator('misskey', BASE_URL, accessToken) as MegalodonInterface;
return client;
}
@Injectable()
export class MastodonApiServerService {
constructor(
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.config)
private config: Config,
private metaService: MetaService,
) { }
@bindThis
public createServer(fastify: FastifyInstance, _options: FastifyPluginOptions, done: (err?: Error) => void) {
const upload = multer({
storage: multer.diskStorage({}),
limits: {
fileSize: this.config.maxFileSize || 262144000,
files: 1,
},
});
fastify.register(multer.contentParser);
fastify.get('/v1/custom_emojis', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getInstanceCustomEmojis();
reply.send(data.data);
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
}
});
fastify.get('/v1/instance', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
// displayed without being logged in
try {
const data = await client.getInstance();
const admin = await this.usersRepository.findOne({
where: {
host: IsNull(),
isRoot: true,
isDeleted: false,
isSuspended: false,
},
order: { id: 'ASC' },
});
const contact = admin == null ? null : convertAccount((await client.getAccount(admin.id)).data);
reply.send(await getInstance(data.data, contact, this.config, await this.metaService.fetch()));
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
}
});
fastify.get('/v1/announcements', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getInstanceAnnouncements();
reply.send(data.data.map((announcement) => convertAnnouncement(announcement)));
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
}
});
fastify.post<{ Body: { id: string } }>('/v1/announcements/:id/dismiss', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.dismissInstanceAnnouncement(
convertId(_request.body['id'], IdType.SharkeyId),
);
reply.send(data.data);
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
}
},
);
fastify.post('/v1/media', { preHandler: upload.single('file') }, async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const multipartData = await _request.file;
if (!multipartData) {
reply.code(401).send({ error: 'No image' });
return;
}
const data = await client.uploadMedia(multipartData);
reply.send(convertAttachment(data.data as Entity.Attachment));
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
}
});
fastify.post('/v2/media', { preHandler: upload.single('file') }, async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const multipartData = await _request.file;
if (!multipartData) {
reply.code(401).send({ error: 'No image' });
return;
}
const data = await client.uploadMedia(multipartData, _request.body!);
reply.send(convertAttachment(data.data as Entity.Attachment));
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
}
});
fastify.get('/v1/filters', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
// displayed without being logged in
try {
const data = await client.getFilters();
reply.send(data.data.map((filter) => convertFilter(filter)));
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
}
});
fastify.get('/v1/trends', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
// displayed without being logged in
try {
const data = await client.getInstanceTrends();
reply.send(data.data);
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
}
});
fastify.post('/v1/apps', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const client = getClient(BASE_URL, ''); // we are using this here, because in private mode some info isnt
// displayed without being logged in
try {
const data = await ApiAuthMastodon(_request, client);
reply.send(data);
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
}
});
fastify.get('/v1/preferences', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
// displayed without being logged in
try {
const data = await client.getPreferences();
reply.send(data.data);
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
}
});
//#region Accounts
fastify.get('/v1/accounts/verify_credentials', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
// displayed without being logged in
try {
const account = new ApiAccountMastodon(_request, client, BASE_URL);
reply.send(await account.verifyCredentials());
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
}
});
fastify.patch('/v1/accounts/update_credentials', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
// displayed without being logged in
try {
const account = new ApiAccountMastodon(_request, client, BASE_URL);
reply.send(await account.updateCredentials());
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
}
});
fastify.get('/v1/accounts/lookup', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
// displayed without being logged in
try {
const account = new ApiAccountMastodon(_request, client, BASE_URL);
reply.send(await account.lookup());
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
}
});
fastify.get('/v1/accounts/relationships', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens); // we are using this here, because in private mode some info isnt
// displayed without being logged in
let users;
try {
let ids = _request.query ? (_request.query as any)['id[]'] : null;
if (typeof ids === 'string') {
ids = [ids];
}
users = ids;
const account = new ApiAccountMastodon(_request, client, BASE_URL);
reply.send(await account.getRelationships(users));
} catch (e: any) {
console.error(e);
const data = e.response.data;
data.users = users;
console.error(data);
reply.code(401).send(data);
}
});
fastify.get<{ Params: { id: string } }>('/v1/accounts/:id', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const sharkId = convertId(_request.params.id, IdType.SharkeyId);
const data = await client.getAccount(sharkId);
reply.send(convertAccount(data.data));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
fastify.get<{ Params: { id: string } }>('/v1/accounts/:id/statuses', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const account = new ApiAccountMastodon(_request, client, BASE_URL);
reply.send(await account.getStatuses());
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
fastify.get<{ Params: { id: string } }>('/v1/accounts/:id/featured_tags', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getFeaturedTags();
reply.send(data.data.map((tag) => convertFeaturedTag(tag)));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
fastify.get<{ Params: { id: string } }>('/v1/accounts/:id/followers', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const account = new ApiAccountMastodon(_request, client, BASE_URL);
reply.send(await account.getFollowers());
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
fastify.get<{ Params: { id: string } }>('/v1/accounts/:id/following', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const account = new ApiAccountMastodon(_request, client, BASE_URL);
reply.send(await account.getFollowing());
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
fastify.get<{ Params: { id: string } }>('/v1/accounts/:id/lists', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getAccountLists(convertId(_request.params.id, IdType.SharkeyId));
reply.send(data.data.map((list) => convertList(list)));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
fastify.post<{ Params: { id: string } }>('/v1/accounts/:id/follow', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const account = new ApiAccountMastodon(_request, client, BASE_URL);
reply.send(await account.addFollow());
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
fastify.post<{ Params: { id: string } }>('/v1/accounts/:id/unfollow', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const account = new ApiAccountMastodon(_request, client, BASE_URL);
reply.send(await account.rmFollow());
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
fastify.post<{ Params: { id: string } }>('/v1/accounts/:id/block', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const account = new ApiAccountMastodon(_request, client, BASE_URL);
reply.send(await account.addBlock());
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
fastify.post<{ Params: { id: string } }>('/v1/accounts/:id/unblock', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const account = new ApiAccountMastodon(_request, client, BASE_URL);
reply.send(await account.rmBlock());
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
fastify.post<{ Params: { id: string } }>('/v1/accounts/:id/mute', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const account = new ApiAccountMastodon(_request, client, BASE_URL);
reply.send(await account.addMute());
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
fastify.post<{ Params: { id: string } }>('/v1/accounts/:id/unmute', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const account = new ApiAccountMastodon(_request, client, BASE_URL);
reply.send(await account.rmMute());
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
fastify.get('/v1/followed_tags', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getFollowedTags();
reply.send(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
fastify.get('/v1/bookmarks', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const account = new ApiAccountMastodon(_request, client, BASE_URL);
reply.send(await account.getBookmarks());
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
fastify.get('/v1/favourites', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const account = new ApiAccountMastodon(_request, client, BASE_URL);
reply.send(await account.getFavourites());
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
fastify.get('/v1/mutes', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const account = new ApiAccountMastodon(_request, client, BASE_URL);
reply.send(await account.getMutes());
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
fastify.get('/v1/blocks', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const account = new ApiAccountMastodon(_request, client, BASE_URL);
reply.send(await account.getBlocks());
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
fastify.get('/v1/follow_requests', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getFollowRequests( ((_request.query as any) || { limit: 20 }).limit );
reply.send(data.data.map((account) => convertAccount(account as Entity.Account)));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
fastify.post<{ Params: { id: string } }>('/v1/follow_requests/:id/authorize', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const account = new ApiAccountMastodon(_request, client, BASE_URL);
reply.send(await account.acceptFollow());
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
fastify.post<{ Params: { id: string } }>('/v1/follow_requests/:id/reject', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const account = new ApiAccountMastodon(_request, client, BASE_URL);
reply.send(await account.rejectFollow());
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
//#endregion
//#region Search
fastify.get('/v1/search', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const search = new ApiSearchMastodon(_request, client, BASE_URL);
reply.send(await search.SearchV1());
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
fastify.get('/v2/search', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const search = new ApiSearchMastodon(_request, client, BASE_URL);
reply.send(await search.SearchV2());
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
fastify.get('/v1/trends/statuses', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const search = new ApiSearchMastodon(_request, client, BASE_URL);
reply.send(await search.getStatusTrends());
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
fastify.get('/v2/suggestions', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const search = new ApiSearchMastodon(_request, client, BASE_URL);
reply.send(await search.getSuggestions());
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
//#endregion
//#region Notifications
fastify.get('/v1/notifications', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const notify = new ApiNotifyMastodon(_request, client);
reply.send(await notify.getNotifications());
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
fastify.get<{ Params: { id: string } }>('/v1/notification/:id', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const notify = new ApiNotifyMastodon(_request, client);
reply.send(await notify.getNotification());
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
fastify.post<{ Params: { id: string } }>('/v1/notification/:id/dismiss', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const notify = new ApiNotifyMastodon(_request, client);
reply.send(await notify.rmNotification());
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
fastify.post('/v1/notifications/clear', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const notify = new ApiNotifyMastodon(_request, client);
reply.send(await notify.rmNotifications());
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
//#endregion
//#region Filters
fastify.get<{ Params: { id: string } }>('/v1/filters/:id', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const filter = new ApiFilterMastodon(_request, client);
!_request.params.id ? reply.send(await filter.getFilters()) : reply.send(await filter.getFilter());
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
fastify.post('/v1/filters', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const filter = new ApiFilterMastodon(_request, client);
reply.send(await filter.createFilter());
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
fastify.post<{ Params: { id: string } }>('/v1/filters/:id', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const filter = new ApiFilterMastodon(_request, client);
reply.send(await filter.updateFilter());
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
fastify.delete<{ Params: { id: string } }>('/v1/filters/:id', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const filter = new ApiFilterMastodon(_request, client);
reply.send(await filter.rmFilter());
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
//#endregion
//#region Timelines
const TLEndpoint = new ApiTimelineMastodon(fastify);
// GET Endpoints
TLEndpoint.getTL();
TLEndpoint.getHomeTl();
TLEndpoint.getListTL();
TLEndpoint.getTagTl();
TLEndpoint.getConversations();
TLEndpoint.getList();
TLEndpoint.getLists();
TLEndpoint.getListAccounts();
// POST Endpoints
TLEndpoint.createList();
TLEndpoint.addListAccount();
// PUT Endpoint
TLEndpoint.updateList();
// DELETE Endpoints
TLEndpoint.deleteList();
TLEndpoint.rmListAccount();
//#endregion
//#region Status
const NoteEndpoint = new ApiStatusMastodon(fastify);
// GET Endpoints
NoteEndpoint.getStatus();
NoteEndpoint.getContext();
NoteEndpoint.getHistory();
NoteEndpoint.getReblogged();
NoteEndpoint.getFavourites();
NoteEndpoint.getMedia();
NoteEndpoint.getPoll();
//POST Endpoints
NoteEndpoint.postStatus();
NoteEndpoint.addFavourite();
NoteEndpoint.rmFavourite();
NoteEndpoint.reblogStatus();
NoteEndpoint.unreblogStatus();
NoteEndpoint.bookmarkStatus();
NoteEndpoint.unbookmarkStatus();
NoteEndpoint.pinStatus();
NoteEndpoint.unpinStatus();
NoteEndpoint.reactStatus();
NoteEndpoint.unreactStatus();
NoteEndpoint.votePoll();
// PUT Endpoint
NoteEndpoint.updateMedia();
// DELETE Endpoint
NoteEndpoint.deleteStatus();
//#endregion
done();
}
}

View File

@ -0,0 +1,132 @@
import { Entity } from 'megalodon';
const CHAR_COLLECTION = '0123456789abcdefghijklmnopqrstuvwxyz';
export enum IdConvertType {
MastodonId,
SharkeyId,
}
export function convertId(in_id: string, id_convert_type: IdConvertType): string {
switch (id_convert_type) {
case IdConvertType.MastodonId: {
let out = BigInt(0);
const lowerCaseId = in_id.toLowerCase();
for (let i = 0; i < lowerCaseId.length; i++) {
const charValue = numFromChar(lowerCaseId.charAt(i));
out += BigInt(charValue) * BigInt(36) ** BigInt(i);
}
return out.toString();
}
case IdConvertType.SharkeyId: {
let input = BigInt(in_id);
let outStr = '';
while (input > BigInt(0)) {
const remainder = Number(input % BigInt(36));
outStr = charFromNum(remainder) + outStr;
input /= BigInt(36);
}
const ReversedoutStr = outStr.split('').reduce((acc, char) => char + acc, '');
return ReversedoutStr;
}
default:
throw new Error('Invalid ID conversion type');
}
}
function numFromChar(character: string): number {
for (let i = 0; i < CHAR_COLLECTION.length; i++) {
if (CHAR_COLLECTION.charAt(i) === character) {
return i;
}
}
throw new Error('Invalid character in parsed base36 id');
}
function charFromNum(number: number): string {
if (number >= 0 && number < CHAR_COLLECTION.length) {
return CHAR_COLLECTION.charAt(number);
} else {
throw new Error('Invalid number for base-36 encoding');
}
}
function simpleConvert(data: any) {
// copy the object to bypass weird pass by reference bugs
const result = Object.assign({}, data);
result.id = convertId(data.id, IdConvertType.MastodonId);
return result;
}
export function convertAccount(account: Entity.Account) {
return simpleConvert(account);
}
export function convertAnnouncement(announcement: Entity.Announcement) {
return simpleConvert(announcement);
}
export function convertAttachment(attachment: Entity.Attachment) {
return simpleConvert(attachment);
}
export function convertFilter(filter: Entity.Filter) {
return simpleConvert(filter);
}
export function convertList(list: Entity.List) {
return simpleConvert(list);
}
export function convertFeaturedTag(tag: Entity.FeaturedTag) {
return simpleConvert(tag);
}
export function convertNotification(notification: Entity.Notification) {
notification.account = convertAccount(notification.account);
notification.id = convertId(notification.id, IdConvertType.MastodonId);
if (notification.status) notification.status = convertStatus(notification.status);
return notification;
}
export function convertPoll(poll: Entity.Poll) {
return simpleConvert(poll);
}
export function convertReaction(reaction: Entity.Reaction) {
if (reaction.accounts) {
reaction.accounts = reaction.accounts.map(convertAccount);
}
return reaction;
}
export function convertRelationship(relationship: Entity.Relationship) {
return simpleConvert(relationship);
}
export function convertStatus(status: Entity.Status) {
status.account = convertAccount(status.account);
status.id = convertId(status.id, IdConvertType.MastodonId);
if (status.in_reply_to_account_id) status.in_reply_to_account_id = convertId(
status.in_reply_to_account_id,
IdConvertType.MastodonId,
);
if (status.in_reply_to_id) status.in_reply_to_id = convertId(status.in_reply_to_id, IdConvertType.MastodonId);
status.media_attachments = status.media_attachments.map((attachment) =>
convertAttachment(attachment),
);
status.mentions = status.mentions.map((mention) => ({
...mention,
id: convertId(mention.id, IdConvertType.MastodonId),
}));
if (status.poll) status.poll = convertPoll(status.poll);
if (status.reblog) status.reblog = convertStatus(status.reblog);
return status;
}
export function convertConversation(conversation: Entity.Conversation) {
conversation.id = convertId(conversation.id, IdConvertType.MastodonId);
conversation.accounts = conversation.accounts.map(convertAccount);
if (conversation.last_status) {
conversation.last_status = convertStatus(conversation.last_status);
}
return conversation;
}

View File

@ -0,0 +1,17 @@
import { ApiAuthMastodon } from './endpoints/auth.js';
import { ApiAccountMastodon } from './endpoints/account.js';
import { ApiSearchMastodon } from './endpoints/search.js';
import { ApiNotifyMastodon } from './endpoints/notifications.js';
import { ApiFilterMastodon } from './endpoints/filter.js';
import { ApiTimelineMastodon } from './endpoints/timeline.js';
import { ApiStatusMastodon } from './endpoints/status.js';
export {
ApiAccountMastodon,
ApiAuthMastodon,
ApiSearchMastodon,
ApiNotifyMastodon,
ApiFilterMastodon,
ApiTimelineMastodon,
ApiStatusMastodon,
};

View File

@ -0,0 +1,285 @@
import { convertId, IdConvertType as IdType, convertAccount, convertRelationship, convertStatus } from '../converters.js';
import { argsToBools, convertTimelinesArgsId, limitToInt } from './timeline.js';
import type { MegalodonInterface } from 'megalodon';
import type { FastifyRequest } from 'fastify';
const relationshipModel = {
id: '',
following: false,
followed_by: false,
delivery_following: false,
blocking: false,
blocked_by: false,
muting: false,
muting_notifications: false,
requested: false,
domain_blocking: false,
showing_reblogs: false,
endorsed: false,
notifying: false,
note: '',
};
export class ApiAccountMastodon {
private request: FastifyRequest;
private client: MegalodonInterface;
private BASE_URL: string;
constructor(request: FastifyRequest, client: MegalodonInterface, BASE_URL: string) {
this.request = request;
this.client = client;
this.BASE_URL = BASE_URL;
}
public async verifyCredentials() {
try {
const data = await this.client.verifyAccountCredentials();
const acct = data.data;
acct.id = convertId(acct.id, IdType.MastodonId);
acct.display_name = acct.display_name || acct.username;
acct.url = `${this.BASE_URL}/@${acct.url}`;
acct.note = acct.note || '';
acct.avatar_static = acct.avatar;
acct.header = acct.header || '/static-assets/transparent.png';
acct.header_static = acct.header || '/static-assets/transparent.png';
acct.source = {
note: acct.note,
fields: acct.fields,
privacy: '',
sensitive: false,
language: '',
};
console.log(acct);
return acct;
} catch (e: any) {
console.error(e);
console.error(e.response.data);
return e.response.data;
}
}
public async updateCredentials() {
try {
const data = await this.client.updateCredentials(this.request.body as any);
return convertAccount(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
return e.response.data;
}
}
public async lookup() {
try {
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);
console.error(e.response.data);
return e.response.data;
}
}
public async getRelationships(users: [string]) {
try {
relationshipModel.id = users.toString() || '1';
if (!(users.length > 0)) {
return [relationshipModel];
}
const reqIds = [];
for (let i = 0; i < users.length; i++) {
reqIds.push(convertId(users[i], IdType.SharkeyId));
}
const data = await this.client.getRelationships(reqIds);
return data.data.map((relationship) => convertRelationship(relationship));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
return e.response.data;
}
}
public async getStatuses() {
try {
const data = await this.client.getAccountStatuses(
convertId((this.request.params as any).id, IdType.SharkeyId),
convertTimelinesArgsId(argsToBools(limitToInt(this.request.query as any))),
);
return data.data.map((status) => convertStatus(status));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
return e.response.data;
}
}
public async getFollowers() {
try {
const data = await this.client.getAccountFollowers(
convertId((this.request.params as any).id, IdType.SharkeyId),
convertTimelinesArgsId(limitToInt(this.request.query as any)),
);
return data.data.map((account) => convertAccount(account));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
return e.response.data;
}
}
public async getFollowing() {
try {
const data = await this.client.getAccountFollowing(
convertId((this.request.params as any).id, IdType.SharkeyId),
convertTimelinesArgsId(limitToInt(this.request.query as any)),
);
return data.data.map((account) => convertAccount(account));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
return e.response.data;
}
}
public async addFollow() {
try {
const data = await this.client.followAccount( convertId((this.request.params as any).id, IdType.SharkeyId) );
const acct = convertRelationship(data.data);
acct.following = true;
return acct;
} catch (e: any) {
console.error(e);
console.error(e.response.data);
return e.response.data;
}
}
public async rmFollow() {
try {
const data = await this.client.unfollowAccount( convertId((this.request.params as any).id, IdType.SharkeyId) );
const acct = convertRelationship(data.data);
acct.following = false;
return acct;
} catch (e: any) {
console.error(e);
console.error(e.response.data);
return e.response.data;
}
}
public async addBlock() {
try {
const data = await this.client.blockAccount( convertId((this.request.params as any).id, IdType.SharkeyId) );
return convertRelationship(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
return e.response.data;
}
}
public async rmBlock() {
try {
const data = await this.client.unblockAccount( convertId((this.request.params as any).id, IdType.SharkeyId) );
return convertRelationship(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
return e.response.data;
}
}
public async addMute() {
try {
const data = await this.client.muteAccount(
convertId((this.request.params as any).id, IdType.SharkeyId),
this.request.body as any,
);
return convertRelationship(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
return e.response.data;
}
}
public async rmMute() {
try {
const data = await this.client.unmuteAccount( convertId((this.request.params as any).id, IdType.SharkeyId) );
return convertRelationship(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
return e.response.data;
}
}
public async getBookmarks() {
try {
const data = await this.client.getBookmarks( convertTimelinesArgsId(limitToInt(this.request.query as any)) );
return data.data.map((status) => convertStatus(status));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
return e.response.data;
}
}
public async getFavourites() {
try {
const data = await this.client.getFavourites( convertTimelinesArgsId(limitToInt(this.request.query as any)) );
return data.data.map((status) => convertStatus(status));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
return e.response.data;
}
}
public async getMutes() {
try {
const data = await this.client.getMutes( convertTimelinesArgsId(limitToInt(this.request.query as any)) );
return data.data.map((account) => convertAccount(account));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
return e.response.data;
}
}
public async getBlocks() {
try {
const data = await this.client.getBlocks( convertTimelinesArgsId(limitToInt(this.request.query as any)) );
return data.data.map((account) => convertAccount(account));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
return e.response.data;
}
}
public async acceptFollow() {
try {
const data = await this.client.acceptFollowRequest( convertId((this.request.params as any).id, IdType.SharkeyId) );
return convertRelationship(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
return e.response.data;
}
}
public async rejectFollow() {
try {
const data = await this.client.rejectFollowRequest( convertId((this.request.params as any).id, IdType.SharkeyId) );
return convertRelationship(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
return e.response.data;
}
}
}

View File

@ -0,0 +1,74 @@
import type { MegalodonInterface } from 'megalodon';
import type { FastifyRequest } from 'fastify';
const readScope = [
'read:account',
'read:drive',
'read:blocks',
'read:favorites',
'read:following',
'read:messaging',
'read:mutes',
'read:notifications',
'read:reactions',
'read:pages',
'read:page-likes',
'read:user-groups',
'read:channels',
'read:gallery',
'read:gallery-likes',
];
const writeScope = [
'write:account',
'write:drive',
'write:blocks',
'write:favorites',
'write:following',
'write:messaging',
'write:mutes',
'write:notes',
'write:notifications',
'write:reactions',
'write:votes',
'write:pages',
'write:page-likes',
'write:user-groups',
'write:channels',
'write:gallery',
'write:gallery-likes',
];
export async function ApiAuthMastodon(request: FastifyRequest, client: MegalodonInterface) {
const body: any = request.body || request.query;
try {
let scope = body.scopes;
if (typeof scope === 'string') scope = scope.split(' ');
const pushScope = new Set<string>();
for (const s of scope) {
if (s.match(/^read/)) for (const r of readScope) pushScope.add(r);
if (s.match(/^write/)) for (const r of writeScope) pushScope.add(r);
}
const scopeArr = Array.from(pushScope);
const red = body.redirect_uris;
const appData = await client.registerApp(body.client_name, {
scopes: scopeArr,
redirect_uris: red,
website: body.website,
});
const returns = {
id: Math.floor(Math.random() * 100).toString(),
name: appData.name,
website: body.website,
redirect_uri: red,
client_id: Buffer.from(appData.url || '').toString('base64'),
client_secret: appData.clientSecret,
};
return returns;
} catch (e: any) {
console.error(e);
return e.response.data;
}
}

View File

@ -0,0 +1,65 @@
import { IdConvertType as IdType, convertId, convertFilter } from '../converters.js';
import type { MegalodonInterface } from 'megalodon';
import type { FastifyRequest } from 'fastify';
export class ApiFilterMastodon {
private request: FastifyRequest;
private client: MegalodonInterface;
constructor(request: FastifyRequest, client: MegalodonInterface) {
this.request = request;
this.client = client;
}
public async getFilters() {
try {
const data = await this.client.getFilters();
return data.data.map((filter) => convertFilter(filter));
} catch (e: any) {
console.error(e);
return e.response.data;
}
}
public async getFilter() {
try {
const data = await this.client.getFilter( convertId((this.request.params as any).id, IdType.SharkeyId) );
return convertFilter(data.data);
} catch (e: any) {
console.error(e);
return e.response.data;
}
}
public async createFilter() {
try {
const body: any = this.request.body;
const data = await this.client.createFilter(body.pharse, body.context, body);
return convertFilter(data.data);
} catch (e: any) {
console.error(e);
return e.response.data;
}
}
public async updateFilter() {
try {
const body: any = this.request.body;
const data = await this.client.updateFilter(convertId((this.request.params as any).id, IdType.SharkeyId), body.pharse, body.context);
return convertFilter(data.data);
} catch (e: any) {
console.error(e);
return e.response.data;
}
}
public async rmFilter() {
try {
const data = await this.client.deleteFilter( convertId((this.request.params as any).id, IdType.SharkeyId) );
return data.data;
} catch (e: any) {
console.error(e);
return e.response.data;
}
}
}

View File

@ -0,0 +1,63 @@
import { Entity } from 'megalodon';
import { MAX_NOTE_TEXT_LENGTH, FILE_TYPE_BROWSERSAFE } from '@/const.js';
import type { Config } from '@/config.js';
import type { MiMeta } from '@/models/Meta.js';
export async function getInstance(
response: Entity.Instance,
contact: Entity.Account,
config: Config,
meta: MiMeta,
) {
return {
uri: config.url,
title: meta.name || 'Sharkey',
short_description:
meta.description?.substring(0, 50) || 'See real server website',
description:
meta.description ||
'This is a vanilla Sharkey Instance. It doesn\'t seem to have a description.',
email: response.email || '',
version: `3.0.0 (compatible; Sharkey ${config.version})`,
urls: response.urls,
stats: {
user_count: response.stats.user_count,
status_count: response.stats.status_count,
domain_count: response.stats.domain_count,
},
thumbnail: meta.backgroundImageUrl || '/static-assets/transparent.png',
languages: meta.langs,
registrations: !meta.disableRegistration || response.registrations,
approval_required: !response.registrations,
invites_enabled: response.registrations,
configuration: {
accounts: {
max_featured_tags: 20,
},
statuses: {
max_characters: MAX_NOTE_TEXT_LENGTH,
max_media_attachments: 16,
characters_reserved_per_url: response.uri.length,
},
media_attachments: {
supported_mime_types: FILE_TYPE_BROWSERSAFE,
image_size_limit: 10485760,
image_matrix_limit: 16777216,
video_size_limit: 41943040,
video_frame_rate_limit: 60,
video_matrix_limit: 2304000,
},
polls: {
max_options: 10,
max_characters_per_option: 50,
min_expiration: 50,
max_expiration: 2629746,
},
reactions: {
max_reactions: 1,
},
},
contact_account: contact,
rules: [],
};
}

View File

@ -0,0 +1,71 @@
import { IdConvertType as IdType, convertId, convertNotification } from '../converters.js';
import { convertTimelinesArgsId } from './timeline.js';
import type { MegalodonInterface, Entity } from 'megalodon';
import type { FastifyRequest } from 'fastify';
function toLimitToInt(q: any) {
if (q.limit) if (typeof q.limit === 'string') q.limit = parseInt(q.limit, 10);
return q;
}
export class ApiNotifyMastodon {
private request: FastifyRequest;
private client: MegalodonInterface;
constructor(request: FastifyRequest, client: MegalodonInterface) {
this.request = request;
this.client = client;
}
public async getNotifications() {
try {
const data = await this.client.getNotifications( convertTimelinesArgsId(toLimitToInt(this.request.query)) );
const notifs = data.data;
const processed = notifs.map((n: Entity.Notification) => {
const convertedn = convertNotification(n);
if (convertedn.type !== 'follow' && convertedn.type !== 'follow_request') {
if (convertedn.type === 'reaction') convertedn.type = 'favourite';
return convertedn;
} else {
return convertedn;
}
});
return processed;
} catch (e: any) {
console.error(e);
return e.response.data;
}
}
public async getNotification() {
try {
const data = await this.client.getNotification( convertId((this.request.params as any).id, IdType.SharkeyId) );
const notif = convertNotification(data.data);
if (notif.type !== 'follow' && notif.type !== 'follow_request' && notif.type === 'reaction') notif.type = 'favourite';
return notif;
} catch (e: any) {
console.error(e);
return e.response.data;
}
}
public async rmNotification() {
try {
const data = await this.client.dismissNotification( convertId((this.request.params as any).id, IdType.SharkeyId) );
return data.data;
} catch (e: any) {
console.error(e);
return e.response.data;
}
}
public async rmNotifications() {
try {
const data = await this.client.dismissNotifications();
return data.data;
} catch (e: any) {
console.error(e);
return e.response.data;
}
}
}

View File

@ -0,0 +1,131 @@
import { Converter } from 'megalodon';
import { convertAccount, convertStatus } from '../converters.js';
import { convertTimelinesArgsId, limitToInt } from './timeline.js';
import type { MegalodonInterface } from 'megalodon';
import type { FastifyRequest } from 'fastify';
async function getHighlight(
BASE_URL: string,
domain: string,
accessTokens: string | undefined,
) {
const accessTokenArr = accessTokens?.split(' ') ?? [null];
const accessToken = accessTokenArr[accessTokenArr.length - 1];
try {
const apicall = await fetch(`${BASE_URL}/api/notes/featured`,
{
method: 'POST',
headers: {
'Accept': 'application/json, text/plain, */*',
'Content-Type': 'application/json',
},
body: JSON.stringify({ i: accessToken }),
});
const api = await apicall.json();
const data: MisskeyEntity.Note[] = api;
return data.map((note) => Converter.note(note, domain));
} catch (e: any) {
console.log(e);
console.log(e.response.data);
return [];
}
}
async function getFeaturedUser( BASE_URL: string, host: string, accessTokens: string | undefined, limit: number ) {
const accessTokenArr = accessTokens?.split(' ') ?? [null];
const accessToken = accessTokenArr[accessTokenArr.length - 1];
try {
const apicall = await fetch(`${BASE_URL}/api/users`,
{
method: 'POST',
headers: {
'Accept': 'application/json, text/plain, */*',
'Content-Type': 'application/json',
},
body: JSON.stringify({ i: accessToken, limit, origin: 'local', sort: '+follower', state: 'alive' }),
});
const api = await apicall.json();
const data: MisskeyEntity.UserDetail[] = api;
return data.map((u) => {
return {
source: 'past_interactions',
account: Converter.userDetail(u, host),
};
});
} catch (e: any) {
console.log(e);
console.log(e.response.data);
return [];
}
}
export class ApiSearchMastodon {
private request: FastifyRequest;
private client: MegalodonInterface;
private BASE_URL: string;
constructor(request: FastifyRequest, client: MegalodonInterface, BASE_URL: string) {
this.request = request;
this.client = client;
this.BASE_URL = BASE_URL;
}
public async SearchV1() {
try {
const query: any = convertTimelinesArgsId(limitToInt(this.request.query as any));
const type = query.type || '';
const data = await this.client.search(query.q, { type: type, ...query });
return data.data;
} catch (e: any) {
console.error(e);
return e.response.data;
}
}
public async SearchV2() {
try {
const query: any = convertTimelinesArgsId(limitToInt(this.request.query as any));
const type = query.type;
const acct = !type || type === 'accounts' ? await this.client.search(query.q, { type: 'accounts', ...query }) : null;
const stat = !type || type === 'statuses' ? await this.client.search(query.q, { type: 'statuses', ...query }) : null;
const tags = !type || type === 'hashtags' ? await this.client.search(query.q, { type: 'hashtags', ...query }) : null;
const data = {
accounts: acct?.data.accounts.map((account) => convertAccount(account)) ?? [],
statuses: stat?.data.statuses.map((status) => convertStatus(status)) ?? [],
hashtags: tags?.data.hashtags ?? [],
};
return data;
} catch (e: any) {
console.error(e);
return e.response.data;
}
}
public async getStatusTrends() {
try {
const data = await getHighlight(
this.BASE_URL,
this.request.hostname,
this.request.headers.authorization,
);
return data.map((status) => convertStatus(status));
} catch (e: any) {
console.error(e);
return e.response.data;
}
}
public async getSuggestions() {
try {
const data = await getFeaturedUser(
this.BASE_URL,
this.request.hostname,
this.request.headers.authorization,
(this.request.query as any).limit || 20,
);
return data.map((suggestion) => { suggestion.account = convertAccount(suggestion.account); return suggestion; });
} catch (e: any) {
console.error(e);
return e.response.data;
}
}
}

View File

@ -0,0 +1,400 @@
import querystring from 'querystring';
import { emojiRegexAtStartToEnd } from '@/misc/emoji-regex.js';
import { convertId, IdConvertType as IdType, convertAccount, convertAttachment, convertPoll, convertStatus } from '../converters.js';
import { getClient } from '../MastodonApiServerService.js';
import { convertTimelinesArgsId, limitToInt } from './timeline.js';
import type { Entity } from 'megalodon';
import type { FastifyInstance } from 'fastify';
function normalizeQuery(data: any) {
const str = querystring.stringify(data);
return querystring.parse(str);
}
export class ApiStatusMastodon {
private fastify: FastifyInstance;
constructor(fastify: FastifyInstance) {
this.fastify = fastify;
}
public async getStatus() {
this.fastify.get<{ Params: { id: string } }>('/v1/statuses/:id', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getStatus(convertId(_request.params.id, IdType.SharkeyId));
reply.send(convertStatus(data.data));
} catch (e: any) {
console.error(e);
reply.code(_request.is404 ? 404 : 401).send(e.response.data);
}
});
}
public async getContext() {
this.fastify.get<{ Params: { id: string } }>('/v1/statuses/:id/context', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
const query: any = _request.query;
try {
const data = await client.getStatusContext(
convertId(_request.params.id, IdType.SharkeyId),
convertTimelinesArgsId(limitToInt(query)),
);
data.data.ancestors = data.data.ancestors.map((status: Entity.Status) => convertStatus(status));
data.data.descendants = data.data.descendants.map((status: Entity.Status) => convertStatus(status));
reply.send(data.data);
} catch (e: any) {
console.error(e);
reply.code(_request.is404 ? 404 : 401).send(e.response.data);
}
});
}
public async getHistory() {
this.fastify.get<{ Params: { id: string } }>('/v1/statuses/:id/history', async (_request, reply) => {
try {
reply.code(401).send({ message: 'Not Implemented' });
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
}
});
}
public async getReblogged() {
this.fastify.get<{ Params: { id: string } }>('/v1/statuses/:id/reblogged_by', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getStatusRebloggedBy(convertId(_request.params.id, IdType.SharkeyId));
reply.send(data.data.map((account: Entity.Account) => convertAccount(account)));
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
}
});
}
public async getFavourites() {
this.fastify.get<{ Params: { id: string } }>('/v1/statuses/:id/favourited_by', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getStatusFavouritedBy(convertId(_request.params.id, IdType.SharkeyId));
reply.send(data.data.map((account: Entity.Account) => convertAccount(account)));
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
}
});
}
public async getMedia() {
this.fastify.get<{ Params: { id: string } }>('/v1/media/:id', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getMedia(convertId(_request.params.id, IdType.SharkeyId));
reply.send(convertAttachment(data.data));
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
}
});
}
public async getPoll() {
this.fastify.get<{ Params: { id: string } }>('/v1/polls/:id', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.getPoll(convertId(_request.params.id, IdType.SharkeyId));
reply.send(convertPoll(data.data));
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
}
});
}
public async votePoll() {
this.fastify.post<{ Params: { id: string } }>('/v1/polls/:id/votes', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
const body: any = _request.body;
try {
const data = await client.votePoll(convertId(_request.params.id, IdType.SharkeyId), body.choices);
reply.send(convertPoll(data.data));
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
}
});
}
public async postStatus() {
this.fastify.post('/v1/statuses', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
let body: any = _request.body;
try {
if (body.in_reply_to_id) body.in_reply_to_id = convertId(body.in_reply_to_id, IdType.SharkeyId);
if (body.quote_id) body.quote_id = convertId(body.quote_id, IdType.SharkeyId);
if (
(!body.poll && body['poll[options][]']) ||
(!body.media_ids && body['media_ids[]'])
) {
body = normalizeQuery(body);
}
const text = body.status;
const removed = text.replace(/@\S+/g, '').replace(/\s|/g, '');
const isDefaultEmoji = emojiRegexAtStartToEnd.test(removed);
const isCustomEmoji = /^:[a-zA-Z0-9@_]+:$/.test(removed);
if ((body.in_reply_to_id && isDefaultEmoji) || isCustomEmoji) {
const a = await client.createEmojiReaction(
body.in_reply_to_id,
removed,
);
reply.send(a.data);
}
if (body.in_reply_to_id && removed === '/unreact') {
try {
const id = body.in_reply_to_id;
const post = await client.getStatus(id);
const react = post.data.emoji_reactions.filter((e: any) => e.me)[0].name;
const data = await client.deleteEmojiReaction(id, react);
reply.send(data.data);
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
}
}
if (!body.media_ids) body.media_ids = undefined;
if (body.media_ids && !body.media_ids.length) body.media_ids = undefined;
if (body.media_ids) {
body.media_ids = (body.media_ids as string[]).map((p) => convertId(p, IdType.SharkeyId));
}
const { sensitive } = body;
body.sensitive = typeof sensitive === 'string' ? sensitive === 'true' : sensitive;
if (body.poll) {
if (
body.poll.expires_in != null &&
typeof body.poll.expires_in === 'string'
) body.poll.expires_in = parseInt(body.poll.expires_in);
if (
body.poll.multiple != null &&
typeof body.poll.multiple === 'string'
) body.poll.multiple = body.poll.multiple === 'true';
if (
body.poll.hide_totals != null &&
typeof body.poll.hide_totals === 'string'
) body.poll.hide_totals = body.poll.hide_totals === 'true';
}
const data = await client.postStatus(text, body);
reply.send(convertStatus(data.data as Entity.Status));
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
}
});
}
public async addFavourite() {
this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/favourite', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = (await client.createEmojiReaction(
convertId(_request.params.id, IdType.SharkeyId),
'⭐',
)) as any;
reply.send(convertStatus(data.data));
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
}
});
}
public async rmFavourite() {
this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/unfavourite', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.deleteEmojiReaction(
convertId(_request.params.id, IdType.SharkeyId),
'⭐',
);
reply.send(convertStatus(data.data));
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
}
});
}
public async reblogStatus() {
this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/reblog', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.reblogStatus(convertId(_request.params.id, IdType.SharkeyId));
reply.send(convertStatus(data.data));
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
}
});
}
public async unreblogStatus() {
this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/unreblog', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.unreblogStatus(convertId(_request.params.id, IdType.SharkeyId));
reply.send(convertStatus(data.data));
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
}
});
}
public async bookmarkStatus() {
this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/bookmark', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.bookmarkStatus(convertId(_request.params.id, IdType.SharkeyId));
reply.send(convertStatus(data.data));
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
}
});
}
public async unbookmarkStatus() {
this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/unbookmark', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.unbookmarkStatus(convertId(_request.params.id, IdType.SharkeyId));
reply.send(convertStatus(data.data));
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
}
});
}
public async pinStatus() {
this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/pin', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.pinStatus(convertId(_request.params.id, IdType.SharkeyId));
reply.send(convertStatus(data.data));
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
}
});
}
public async unpinStatus() {
this.fastify.post<{ Params: { id: string } }>('/v1/statuses/:id/unpin', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.unpinStatus(convertId(_request.params.id, IdType.SharkeyId));
reply.send(convertStatus(data.data));
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
}
});
}
public async reactStatus() {
this.fastify.post<{ Params: { id: string, name: string } }>('/v1/statuses/:id/react/:name', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.createEmojiReaction(convertId(_request.params.id, IdType.SharkeyId), _request.params.name);
reply.send(convertStatus(data.data));
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
}
});
}
public async unreactStatus() {
this.fastify.post<{ Params: { id: string, name: string } }>('/v1/statuses/:id/unreact/:name', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.deleteEmojiReaction(convertId(_request.params.id, IdType.SharkeyId), _request.params.name);
reply.send(convertStatus(data.data));
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
}
});
}
public async updateMedia() {
this.fastify.put<{ Params: { id: string } }>('/v1/media/:id', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.updateMedia(convertId(_request.params.id, IdType.SharkeyId), _request.body as any);
reply.send(convertAttachment(data.data));
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
}
});
}
public async deleteStatus() {
this.fastify.delete<{ Params: { id: string } }>('/v1/statuses/:id', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const data = await client.deleteStatus(convertId(_request.params.id, IdType.SharkeyId));
reply.send(data.data);
} catch (e: any) {
console.error(e);
reply.code(401).send(e.response.data);
}
});
}
}

View File

@ -0,0 +1,282 @@
import { ParsedUrlQuery } from 'querystring';
import { convertId, IdConvertType as IdType, convertAccount, convertConversation, convertList, convertStatus } from '../converters.js';
import { getClient } from '../MastodonApiServerService.js';
import type { Entity } from 'megalodon';
import type { FastifyInstance } from 'fastify';
export function limitToInt(q: ParsedUrlQuery) {
const object: any = q;
if (q.limit) if (typeof q.limit === 'string') object.limit = parseInt(q.limit, 10);
if (q.offset) if (typeof q.offset === 'string') object.offset = parseInt(q.offset, 10);
return object;
}
export function argsToBools(q: ParsedUrlQuery) {
// Values taken from https://docs.joinmastodon.org/client/intro/#boolean
const toBoolean = (value: string) =>
!['0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].includes(value);
// Keys taken from:
// - https://docs.joinmastodon.org/methods/accounts/#statuses
// - https://docs.joinmastodon.org/methods/timelines/#public
// - https://docs.joinmastodon.org/methods/timelines/#tag
const object: any = q;
if (q.only_media) if (typeof q.only_media === 'string') object.only_media = toBoolean(q.only_media);
if (q.exclude_replies) if (typeof q.exclude_replies === 'string') object.exclude_replies = toBoolean(q.exclude_replies);
if (q.exclude_reblogs) if (typeof q.exclude_reblogs === 'string') object.exclude_reblogs = toBoolean(q.exclude_reblogs);
if (q.pinned) if (typeof q.pinned === 'string') object.pinned = toBoolean(q.pinned);
if (q.local) if (typeof q.local === 'string') object.local = toBoolean(q.local);
return q;
}
export function convertTimelinesArgsId(q: ParsedUrlQuery) {
if (typeof q.min_id === 'string') q.min_id = convertId(q.min_id, IdType.SharkeyId);
if (typeof q.max_id === 'string') q.max_id = convertId(q.max_id, IdType.SharkeyId);
if (typeof q.since_id === 'string') q.since_id = convertId(q.since_id, IdType.SharkeyId);
return q;
}
export class ApiTimelineMastodon {
private fastify: FastifyInstance;
constructor(fastify: FastifyInstance) {
this.fastify = fastify;
}
public async getTL() {
this.fastify.get('/v1/timelines/public', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const query: any = _request.query;
const data = query.local === 'true'
? await client.getLocalTimeline(convertTimelinesArgsId(argsToBools(limitToInt(query))))
: await client.getPublicTimeline(convertTimelinesArgsId(argsToBools(limitToInt(query))));
reply.send(data.data.map((status: Entity.Status) => convertStatus(status)));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
}
public async getHomeTl() {
this.fastify.get('/v1/timelines/home', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const query: any = _request.query;
const data = await client.getHomeTimeline(convertTimelinesArgsId(limitToInt(query)));
reply.send(data.data.map((status: Entity.Status) => convertStatus(status)));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
}
public async getTagTl() {
this.fastify.get<{ Params: { hashtag: string } }>('/v1/timelines/tag/:hashtag', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const query: any = _request.query;
const params: any = _request.params;
const data = await client.getTagTimeline(params.hashtag, convertTimelinesArgsId(limitToInt(query)));
reply.send(data.data.map((status: Entity.Status) => convertStatus(status)));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
}
public async getListTL() {
this.fastify.get<{ Params: { id: string } }>('/v1/timelines/list/:id', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const query: any = _request.query;
const params: any = _request.params;
const data = await client.getListTimeline(convertId(params.id, IdType.SharkeyId), convertTimelinesArgsId(limitToInt(query)));
reply.send(data.data.map((status: Entity.Status) => convertStatus(status)));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
}
public async getConversations() {
this.fastify.get('/v1/conversations', async (_request, reply) => {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
try {
const query: any = _request.query;
const data = await client.getConversationTimeline(convertTimelinesArgsId(limitToInt(query)));
reply.send(data.data.map((conversation: Entity.Conversation) => convertConversation(conversation)));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
}
public async getList() {
this.fastify.get<{ Params: { id: string } }>('/v1/lists/:id', async (_request, reply) => {
try {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
const params: any = _request.params;
const data = await client.getList(convertId(params.id, IdType.SharkeyId));
reply.send(convertList(data.data));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
}
public async getLists() {
this.fastify.get('/v1/lists', async (_request, reply) => {
try {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
const account = await client.verifyAccountCredentials();
const data = await client.getLists(account.data.id);
reply.send(data.data.map((list: Entity.List) => convertList(list)));
} catch (e: any) {
console.error(e);
return e.response.data;
}
});
}
public async getListAccounts() {
this.fastify.get<{ Params: { id: string } }>('/v1/lists/:id/accounts', async (_request, reply) => {
try {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
const params: any = _request.params;
const query: any = _request.query;
const data = await client.getAccountsInList(
convertId(params.id, IdType.SharkeyId),
convertTimelinesArgsId(query),
);
reply.send(data.data.map((account: Entity.Account) => convertAccount(account)));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
}
public async addListAccount() {
this.fastify.post<{ Params: { id: string } }>('/v1/lists/:id/accounts', async (_request, reply) => {
try {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
const params: any = _request.params;
const query: any = _request.query;
const data = await client.addAccountsToList(
convertId(params.id, IdType.SharkeyId),
(query.accounts_id as string[]).map((id) => convertId(id, IdType.SharkeyId)),
);
reply.send(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
}
public async rmListAccount() {
this.fastify.delete<{ Params: { id: string } }>('/v1/lists/:id/accounts', async (_request, reply) => {
try {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
const params: any = _request.params;
const query: any = _request.query;
const data = await client.deleteAccountsFromList(
convertId(params.id, IdType.SharkeyId),
(query.accounts_id as string[]).map((id) => convertId(id, IdType.SharkeyId)),
);
reply.send(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
}
public async createList() {
this.fastify.post('/v1/lists', async (_request, reply) => {
try {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
const body: any = _request.body;
const data = await client.createList(body.title);
reply.send(convertList(data.data));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
}
public async updateList() {
this.fastify.put<{ Params: { id: string } }>('/v1/lists/:id', async (_request, reply) => {
try {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
const body: any = _request.body;
const params: any = _request.params;
const data = await client.updateList(convertId(params.id, IdType.SharkeyId), body.title);
reply.send(convertList(data.data));
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
}
public async deleteList() {
this.fastify.delete<{ Params: { id: string } }>('/v1/lists/:id', async (_request, reply) => {
try {
const BASE_URL = `${_request.protocol}://${_request.hostname}`;
const accessTokens = _request.headers.authorization;
const client = getClient(BASE_URL, accessTokens);
const params: any = _request.params;
const data = await client.deleteList(convertId(params.id, IdType.SharkeyId));
reply.send(data.data);
} catch (e: any) {
console.error(e);
console.error(e.response.data);
reply.code(401).send(e.response.data);
}
});
}
}

View File

@ -0,0 +1,3 @@
node_modules
./src
tsconfig.json

View File

@ -0,0 +1,87 @@
{
"name": "megalodon",
"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 ./",
"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",
"js"
],
"moduleNameMapper": {
"^@/(.+)": "<rootDir>/src/$1",
"^~/(.+)": "<rootDir>/$1"
},
"testMatch": [
"**/test/**/*.spec.ts"
],
"preset": "ts-jest/presets/default",
"transform": {
"^.+\\.(ts|tsx)$": ["ts-jest", {
"tsconfig": "tsconfig.json"
}]
},
"testEnvironment": "node"
},
"homepage": "https://github.com/h3poteto/megalodon#readme",
"dependencies": {
"@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": "^7.0.2",
"oauth": "^0.10.0",
"object-assign-deep": "^0.4.0",
"parse-link-header": "^2.0.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.6",
"@types/form-data": "^2.5.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": "^3.0.3",
"ts-jest": "^29.1.1",
"typedoc": "^0.25.1"
}
}

1
packages/megalodon/src/axios.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module 'axios/lib/adapters/http'

View File

@ -0,0 +1,13 @@
export class RequestCanceledError extends Error {
public isCancel: boolean
constructor(msg: string) {
super(msg)
this.isCancel = true
Object.setPrototypeOf(this, RequestCanceledError)
}
}
export const isCancel = (value: any): boolean => {
return value && value.isCancel
}

View File

@ -0,0 +1,3 @@
import MisskeyAPI from "./misskey/api_client";
export default MisskeyAPI.Converter;

View File

@ -0,0 +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'

View File

@ -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<Link>
}
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<Links>(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<Nodeinfo10>(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<Nodeinfo20>(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<Nodeinfo21>(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')
}
}

View File

@ -0,0 +1,35 @@
/// <reference path="emoji.ts" />
/// <reference path="source.ts" />
/// <reference path="field.ts" />
/// <reference path="role.ts" />
namespace Entity {
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<Emoji>
moved: Account | null
fields: Array<Field>
bot: boolean | null
source?: Source
role?: Role
mute_expires_at?: string
}
}

View File

@ -0,0 +1,8 @@
namespace Entity {
export type Activity = {
week: string
statuses: string
logins: string
registrations: string
}
}

View File

@ -0,0 +1,40 @@
/// <reference path="emoji.ts" />
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 | null
read: boolean | null
mentions: Array<AnnouncementAccount>
statuses: Array<AnnouncementStatus>
tags: Array<StatusTag>
emojis: Array<Emoji>
reactions: Array<AnnouncementReaction>
}
export type AnnouncementAccount = {
id: string
username: string
url: string
acct: 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
}
}

View File

@ -0,0 +1,7 @@
namespace Entity {
export type Application = {
name: string
website?: string | null
vapid_key?: string | null
}
}

View File

@ -0,0 +1,14 @@
/// <reference path="attachment.ts" />
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
}
}

View File

@ -0,0 +1,49 @@
namespace Entity {
export type Sub = {
// For Image, Gifv, and Video
width?: number
height?: number
size?: string
aspect?: number
// For Gifv and Video
frame_rate?: string
// For Audio, Gifv, and Video
duration?: number
bitrate?: 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 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
}
}

View File

@ -0,0 +1,18 @@
namespace Entity {
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
}
}

View File

@ -0,0 +1,8 @@
/// <reference path="status.ts" />
namespace Entity {
export type Context = {
ancestors: Array<Status>
descendants: Array<Status>
}
}

View File

@ -0,0 +1,11 @@
/// <reference path="account.ts" />
/// <reference path="status.ts" />
namespace Entity {
export type Conversation = {
id: string
accounts: Array<Account>
last_status: Status | null
unread: boolean
}
}

View File

@ -0,0 +1,9 @@
namespace Entity {
export type Emoji = {
shortcode: string
static_url: string
url: string
visible_in_picker: boolean
category?: string
}
}

View File

@ -0,0 +1,8 @@
namespace Entity {
export type FeaturedTag = {
id: string
name: string
statuses_count: number
last_status_at: string
}
}

View File

@ -0,0 +1,7 @@
namespace Entity {
export type Field = {
name: string
value: string
verified_at: string | null
}
}

View File

@ -0,0 +1,12 @@
namespace Entity {
export type Filter = {
id: string
phrase: string
context: Array<FilterContext>
expires_at: string | null
irreversible: boolean
whole_word: boolean
}
export type FilterContext = string
}

View File

@ -0,0 +1,27 @@
/// <reference path="emoji.ts" />
/// <reference path="field.ts" />
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<Emoji>
fields: Array<Field>
}
}

View File

@ -0,0 +1,7 @@
namespace Entity {
export type History = {
day: string
uses: number
accounts: number
}
}

View File

@ -0,0 +1,9 @@
namespace Entity {
export type IdentityProof = {
provider: string
provider_username: string
updated_at: string
proof_url: string
profile_url: string
}
}

View File

@ -0,0 +1,40 @@
/// <reference path="account.ts" />
/// <reference path="urls.ts" />
/// <reference path="stats.ts" />
namespace Entity {
export type Instance = {
uri: string
title: string
description: string
email: string
version: string
thumbnail: string | null
urls: URLs | null
stats: Stats
languages: Array<string>
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<InstanceRule>
}
export type InstanceRule = {
id: string
text: string
}
}

View File

@ -0,0 +1,9 @@
namespace Entity {
export type List = {
id: string
title: string
replies_policy: RepliesPolicy | null
}
export type RepliesPolicy = 'followed' | 'list' | 'none'
}

View File

@ -0,0 +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
}
}
}

View File

@ -0,0 +1,8 @@
namespace Entity {
export type Mention = {
id: string
username: string
url: string
acct: string
}
}

View File

@ -0,0 +1,16 @@
/// <reference path="account.ts" />
/// <reference path="status.ts" />
namespace Entity {
export type Notification = {
account: Account
created_at: string
id: string
status?: Status
emoji?: string
type: NotificationType
target?: Account
}
export type NotificationType = string
}

View File

@ -0,0 +1,13 @@
/// <reference path="poll_option.ts" />
namespace Entity {
export type Poll = {
id: string
expires_at: string | null
expired: boolean
multiple: boolean
votes_count: number
options: Array<PollOption>
voted: boolean
}
}

View File

@ -0,0 +1,6 @@
namespace Entity {
export type PollOption = {
title: string
votes_count: number | null
}
}

View File

@ -0,0 +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
}
}

View File

@ -0,0 +1,16 @@
namespace Entity {
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
}
}

View File

@ -0,0 +1,10 @@
/// <reference path="account.ts" />
namespace Entity {
export type Reaction = {
count: number
me: boolean
name: string
accounts?: Array<Account>
}
}

View File

@ -0,0 +1,17 @@
namespace Entity {
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
}
}

View File

@ -0,0 +1,18 @@
/// <reference path="account.ts" />
namespace Entity {
export type Report = {
id: string
action_taken: boolean
action_taken_at: string | null
status_ids: Array<string> | null
rule_ids: Array<string> | 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'
}

View File

@ -0,0 +1,11 @@
/// <reference path="account.ts" />
/// <reference path="status.ts" />
/// <reference path="tag.ts" />
namespace Entity {
export type Results = {
accounts: Array<Account>
statuses: Array<Status>
hashtags: Array<Tag>
}
}

View File

@ -0,0 +1,5 @@
namespace Entity {
export type Role = {
name: string
}
}

View File

@ -0,0 +1,10 @@
/// <reference path="attachment.ts" />
/// <reference path="status_params.ts" />
namespace Entity {
export type ScheduledStatus = {
id: string
scheduled_at: string
params: StatusParams
media_attachments: Array<Attachment> | null
}
}

View File

@ -0,0 +1,10 @@
/// <reference path="field.ts" />
namespace Entity {
export type Source = {
privacy: string | null
sensitive: boolean | null
language: string | null
note: string
fields: Array<Field>
}
}

View File

@ -0,0 +1,7 @@
namespace Entity {
export type Stats = {
user_count: number
status_count: number
domain_count: number
}
}

View File

@ -0,0 +1,49 @@
/// <reference path="account.ts" />
/// <reference path="application.ts" />
/// <reference path="mention.ts" />
/// <reference path="attachment.ts" />
/// <reference path="emoji.ts" />
/// <reference path="card.ts" />
/// <reference path="poll.ts" />
/// <reference path="reaction.ts" />
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<Attachment>
mentions: Array<Mention>
tags: Array<StatusTag>
card: Card | null
poll: Poll | null
application: Application | null
language: string | null
pinned: boolean | null
emoji_reactions: Array<Reaction>
quote: boolean
bookmarked: boolean
}
export type StatusTag = {
name: string
url: string
}
}

View File

@ -0,0 +1,23 @@
/// <reference path="account.ts" />
/// <reference path="application.ts" />
/// <reference path="mention.ts" />
/// <reference path="tag.ts" />
/// <reference path="attachment.ts" />
/// <reference path="emoji.ts" />
/// <reference path="card.ts" />
/// <reference path="poll.ts" />
/// <reference path="reaction.ts" />
namespace Entity {
export type StatusEdit = {
account: Account;
content: string;
plain_content: string | null;
created_at: string;
emojis: Emoji[];
sensitive: boolean;
spoiler_text: string;
media_attachments: Array<Attachment>;
poll: Poll | null;
};
}

View File

@ -0,0 +1,12 @@
namespace Entity {
export type StatusParams = {
text: string
in_reply_to_id: string | null
media_ids: Array<string> | null
sensitive: boolean | null
spoiler_text: string | null
visibility: 'public' | 'unlisted' | 'private' | 'direct' | null
scheduled_at: string | null
application_id: number | null
}
}

View File

@ -0,0 +1,7 @@
namespace Entity {
export type StatusSource = {
id: string
text: string
spoiler_text: string
}
}

View File

@ -0,0 +1,10 @@
/// <reference path="history.ts" />
namespace Entity {
export type Tag = {
name: string
url: string
history: Array<History>
following?: boolean
}
}

View File

@ -0,0 +1,8 @@
namespace Entity {
export type Token = {
access_token: string
token_type: string
scope: string
created_at: number
}
}

View File

@ -0,0 +1,5 @@
namespace Entity {
export type URLs = {
streaming_api: string
}
}

View File

@ -0,0 +1,40 @@
/// <reference path="./entities/account.ts" />
/// <reference path="./entities/activity.ts" />
/// <reference path="./entities/announcement.ts" />
/// <reference path="./entities/application.ts" />
/// <reference path="./entities/async_attachment.ts" />
/// <reference path="./entities/attachment.ts" />
/// <reference path="./entities/card.ts" />
/// <reference path="./entities/context.ts" />
/// <reference path="./entities/conversation.ts" />
/// <reference path="./entities/emoji.ts" />
/// <reference path="./entities/featured_tag.ts" />
/// <reference path="./entities/field.ts" />
/// <reference path="./entities/filter.ts" />
/// <reference path="./entities/follow_request.ts" />
/// <reference path="./entities/history.ts" />
/// <reference path="./entities/identity_proof.ts" />
/// <reference path="./entities/instance.ts" />
/// <reference path="./entities/list.ts" />
/// <reference path="./entities/marker.ts" />
/// <reference path="./entities/mention.ts" />
/// <reference path="./entities/notification.ts" />
/// <reference path="./entities/poll.ts" />
/// <reference path="./entities/poll_option.ts" />
/// <reference path="./entities/preferences.ts" />
/// <reference path="./entities/push_subscription.ts" />
/// <reference path="./entities/reaction.ts" />
/// <reference path="./entities/relationship.ts" />
/// <reference path="./entities/report.ts" />
/// <reference path="./entities/results.ts" />
/// <reference path="./entities/scheduled_status.ts" />
/// <reference path="./entities/source.ts" />
/// <reference path="./entities/stats.ts" />
/// <reference path="./entities/status.ts" />
/// <reference path="./entities/status_params.ts" />
/// <reference path="./entities/status_source.ts" />
/// <reference path="./entities/tag.ts" />
/// <reference path="./entities/token.ts" />
/// <reference path="./entities/urls.ts" />
export default Entity

View File

@ -0,0 +1,11 @@
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 default FilterContext

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,768 @@
import axios, { AxiosResponse, AxiosRequestConfig } from 'axios'
import objectAssignDeep from 'object-assign-deep'
import WebSocket from './web_socket'
import Response from '../response'
import { RequestCanceledError } from '../cancel'
import proxyAgent, { ProxyConfig } from '../proxy_config'
import { NO_REDIRECT, DEFAULT_SCOPE, DEFAULT_UA } from '../default'
import FriendicaEntity from './entity'
import MegalodonEntity from '../entity'
import NotificationType, { UnknownNotificationTypeError } from '../notification'
import FriendicaNotificationType from './notification'
namespace FriendicaAPI {
/**
* Interface
*/
export interface Interface {
get<T = any>(path: string, params?: any, headers?: { [key: string]: string }, pathIsFullyQualified?: boolean): Promise<Response<T>>
put<T = any>(path: string, params?: any, headers?: { [key: string]: string }): Promise<Response<T>>
putForm<T = any>(path: string, params?: any, headers?: { [key: string]: string }): Promise<Response<T>>
patch<T = any>(path: string, params?: any, headers?: { [key: string]: string }): Promise<Response<T>>
patchForm<T = any>(path: string, params?: any, headers?: { [key: string]: string }): Promise<Response<T>>
post<T = any>(path: string, params?: any, headers?: { [key: string]: string }): Promise<Response<T>>
postForm<T = any>(path: string, params?: any, headers?: { [key: string]: string }): Promise<Response<T>>
del<T = any>(path: string, params?: any, headers?: { [key: string]: string }): Promise<Response<T>>
cancel(): void
socket(path: string, stream: string, params?: string): WebSocket
}
/**
* Friendica API client.
*
* Using axios for request, you will handle promises.
*/
export class Client implements Interface {
static DEFAULT_SCOPE = DEFAULT_SCOPE
static DEFAULT_URL = 'https://mastodon.social'
static NO_REDIRECT = NO_REDIRECT
private accessToken: string | null
private baseUrl: string
private userAgent: string
private abortController: AbortController
private proxyConfig: ProxyConfig | false = false
/**
* @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 = DEFAULT_UA,
proxyConfig: ProxyConfig | false = false
) {
this.accessToken = accessToken
this.baseUrl = baseUrl
this.userAgent = userAgent
this.proxyConfig = proxyConfig
this.abortController = new AbortController()
axios.defaults.signal = this.abortController.signal
}
/**
* GET request to mastodon REST API.
* @param path relative path from baseUrl
* @param params Query parameters
* @param headers Request header object
*/
public async get<T>(
path: string,
params = {},
headers: { [key: string]: string } = {},
pathIsFullyQualified = false
): Promise<Response<T>> {
let options: AxiosRequestConfig = {
params: params,
headers: headers,
maxContentLength: Infinity,
maxBodyLength: Infinity
}
if (this.accessToken) {
options = objectAssignDeep({}, options, {
headers: {
Authorization: `Bearer ${this.accessToken}`
}
})
}
if (this.proxyConfig) {
options = Object.assign(options, {
httpAgent: proxyAgent(this.proxyConfig),
httpsAgent: proxyAgent(this.proxyConfig)
})
}
return axios
.get<T>((pathIsFullyQualified ? '' : this.baseUrl) + path, options)
.catch((err: Error) => {
if (axios.isCancel(err)) {
throw new RequestCanceledError(err.message)
} else {
throw err
}
})
.then((resp: AxiosResponse<T>) => {
const res: Response<T> = {
data: resp.data,
status: resp.status,
statusText: resp.statusText,
headers: resp.headers
}
return res
})
}
/**
* PUT request to mastodon REST API.
* @param path relative path from baseUrl
* @param params Form data. If you want to post file, please use FormData()
* @param headers Request header object
*/
public async put<T>(path: string, params = {}, headers: { [key: string]: string } = {}): Promise<Response<T>> {
let options: AxiosRequestConfig = {
headers: headers,
maxContentLength: Infinity,
maxBodyLength: Infinity
}
if (this.accessToken) {
options = objectAssignDeep({}, options, {
headers: {
Authorization: `Bearer ${this.accessToken}`
}
})
}
if (this.proxyConfig) {
options = Object.assign(options, {
httpAgent: proxyAgent(this.proxyConfig),
httpsAgent: proxyAgent(this.proxyConfig)
})
}
return axios
.put<T>(this.baseUrl + path, params, options)
.catch((err: Error) => {
if (axios.isCancel(err)) {
throw new RequestCanceledError(err.message)
} else {
throw err
}
})
.then((resp: AxiosResponse<T>) => {
const res: Response<T> = {
data: resp.data,
status: resp.status,
statusText: resp.statusText,
headers: resp.headers
}
return res
})
}
/**
* PUT request to mastodon REST API for multipart.
* @param path relative path from baseUrl
* @param params Form data. If you want to post file, please use FormData()
* @param headers Request header object
*/
public async putForm<T>(path: string, params = {}, headers: { [key: string]: string } = {}): Promise<Response<T>> {
let options: AxiosRequestConfig = {
headers: headers,
maxContentLength: Infinity,
maxBodyLength: Infinity
}
if (this.accessToken) {
options = objectAssignDeep({}, options, {
headers: {
Authorization: `Bearer ${this.accessToken}`
}
})
}
if (this.proxyConfig) {
options = Object.assign(options, {
httpAgent: proxyAgent(this.proxyConfig),
httpsAgent: proxyAgent(this.proxyConfig)
})
}
return axios
.putForm<T>(this.baseUrl + path, params, options)
.catch((err: Error) => {
if (axios.isCancel(err)) {
throw new RequestCanceledError(err.message)
} else {
throw err
}
})
.then((resp: AxiosResponse<T>) => {
const res: Response<T> = {
data: resp.data,
status: resp.status,
statusText: resp.statusText,
headers: resp.headers
}
return res
})
}
/**
* PATCH request to mastodon REST API.
* @param path relative path from baseUrl
* @param params Form data. If you want to post file, please use FormData()
* @param headers Request header object
*/
public async patch<T>(path: string, params = {}, headers: { [key: string]: string } = {}): Promise<Response<T>> {
let options: AxiosRequestConfig = {
headers: headers,
maxContentLength: Infinity,
maxBodyLength: Infinity
}
if (this.accessToken) {
options = objectAssignDeep({}, options, {
headers: {
Authorization: `Bearer ${this.accessToken}`
}
})
}
if (this.proxyConfig) {
options = Object.assign(options, {
httpAgent: proxyAgent(this.proxyConfig),
httpsAgent: proxyAgent(this.proxyConfig)
})
}
return axios
.patch<T>(this.baseUrl + path, params, options)
.catch((err: Error) => {
if (axios.isCancel(err)) {
throw new RequestCanceledError(err.message)
} else {
throw err
}
})
.then((resp: AxiosResponse<T>) => {
const res: Response<T> = {
data: resp.data,
status: resp.status,
statusText: resp.statusText,
headers: resp.headers
}
return res
})
}
/**
* PATCH request to mastodon REST API for multipart.
* @param path relative path from baseUrl
* @param params Form data. If you want to post file, please use FormData()
* @param headers Request header object
*/
public async patchForm<T>(path: string, params = {}, headers: { [key: string]: string } = {}): Promise<Response<T>> {
let options: AxiosRequestConfig = {
headers: headers,
maxContentLength: Infinity,
maxBodyLength: Infinity
}
if (this.accessToken) {
options = objectAssignDeep({}, options, {
headers: {
Authorization: `Bearer ${this.accessToken}`
}
})
}
if (this.proxyConfig) {
options = Object.assign(options, {
httpAgent: proxyAgent(this.proxyConfig),
httpsAgent: proxyAgent(this.proxyConfig)
})
}
return axios
.patchForm<T>(this.baseUrl + path, params, options)
.catch((err: Error) => {
if (axios.isCancel(err)) {
throw new RequestCanceledError(err.message)
} else {
throw err
}
})
.then((resp: AxiosResponse<T>) => {
const res: Response<T> = {
data: resp.data,
status: resp.status,
statusText: resp.statusText,
headers: resp.headers
}
return res
})
}
/**
* POST request to mastodon REST API.
* @param path relative path from baseUrl
* @param params Form data
* @param headers Request header object
*/
public async post<T>(path: string, params = {}, headers: { [key: string]: string } = {}): Promise<Response<T>> {
let options: AxiosRequestConfig = {
headers: headers,
maxContentLength: Infinity,
maxBodyLength: Infinity
}
if (this.accessToken) {
options = objectAssignDeep({}, options, {
headers: {
Authorization: `Bearer ${this.accessToken}`
}
})
}
if (this.proxyConfig) {
options = Object.assign(options, {
httpAgent: proxyAgent(this.proxyConfig),
httpsAgent: proxyAgent(this.proxyConfig)
})
}
return axios.post<T>(this.baseUrl + path, params, options).then((resp: AxiosResponse<T>) => {
const res: Response<T> = {
data: resp.data,
status: resp.status,
statusText: resp.statusText,
headers: resp.headers
}
return res
})
}
/**
* POST request to mastodon REST API for multipart.
* @param path relative path from baseUrl
* @param params Form data
* @param headers Request header object
*/
public async postForm<T>(path: string, params = {}, headers: { [key: string]: string } = {}): Promise<Response<T>> {
let options: AxiosRequestConfig = {
headers: headers,
maxContentLength: Infinity,
maxBodyLength: Infinity
}
if (this.accessToken) {
options = objectAssignDeep({}, options, {
headers: {
Authorization: `Bearer ${this.accessToken}`
}
})
}
if (this.proxyConfig) {
options = Object.assign(options, {
httpAgent: proxyAgent(this.proxyConfig),
httpsAgent: proxyAgent(this.proxyConfig)
})
}
return axios.postForm<T>(this.baseUrl + path, params, options).then((resp: AxiosResponse<T>) => {
const res: Response<T> = {
data: resp.data,
status: resp.status,
statusText: resp.statusText,
headers: resp.headers
}
return res
})
}
/**
* DELETE request to mastodon REST API.
* @param path relative path from baseUrl
* @param params Form data
* @param headers Request header object
*/
public async del<T>(path: string, params = {}, headers: { [key: string]: string } = {}): Promise<Response<T>> {
let options: AxiosRequestConfig = {
data: params,
headers: headers,
maxContentLength: Infinity,
maxBodyLength: Infinity
}
if (this.accessToken) {
options = objectAssignDeep({}, options, {
headers: {
Authorization: `Bearer ${this.accessToken}`
}
})
}
if (this.proxyConfig) {
options = Object.assign(options, {
httpAgent: proxyAgent(this.proxyConfig),
httpsAgent: proxyAgent(this.proxyConfig)
})
}
return axios
.delete(this.baseUrl + path, options)
.catch((err: Error) => {
if (axios.isCancel(err)) {
throw new RequestCanceledError(err.message)
} else {
throw err
}
})
.then((resp: AxiosResponse) => {
const res: Response<T> = {
data: resp.data,
status: resp.status,
statusText: resp.statusText,
headers: resp.headers
}
return res
})
}
/**
* Cancel all requests in this instance.
* @returns void
*/
public cancel(): void {
return this.abortController.abort()
}
/**
* Get connection and receive websocket connection for Pleroma API.
*
* @param path relative path from baseUrl: normally it is `/streaming`.
* @param stream Stream name, please refer: https://git.pleroma.social/pleroma/pleroma/blob/develop/lib/pleroma/web/mastodon_api/mastodon_socket.ex#L19-28
* @returns WebSocket, which inherits from EventEmitter
*/
public socket(path: string, stream: string, params?: string): WebSocket {
if (!this.accessToken) {
throw new Error('accessToken is required')
}
const url = this.baseUrl + path
const streaming = new WebSocket(url, stream, params, this.accessToken, this.userAgent, this.proxyConfig)
process.nextTick(() => {
streaming.start()
})
return streaming
}
}
export namespace Entity {
export type Account = FriendicaEntity.Account
export type Activity = FriendicaEntity.Activity
export type Application = FriendicaEntity.Application
export type AsyncAttachment = FriendicaEntity.AsyncAttachment
export type Attachment = FriendicaEntity.Attachment
export type Card = FriendicaEntity.Card
export type Context = FriendicaEntity.Context
export type Conversation = FriendicaEntity.Conversation
export type Emoji = FriendicaEntity.Emoji
export type FeaturedTag = FriendicaEntity.FeaturedTag
export type Field = FriendicaEntity.Field
export type Filter = FriendicaEntity.Filter
export type FollowRequest = FriendicaEntity.FollowRequest
export type History = FriendicaEntity.History
export type IdentityProof = FriendicaEntity.IdentityProof
export type Instance = FriendicaEntity.Instance
export type List = FriendicaEntity.List
export type Marker = FriendicaEntity.Marker
export type Mention = FriendicaEntity.Mention
export type Notification = FriendicaEntity.Notification
export type Poll = FriendicaEntity.Poll
export type PollOption = FriendicaEntity.PollOption
export type Preferences = FriendicaEntity.Preferences
export type PushSubscription = FriendicaEntity.PushSubscription
export type Relationship = FriendicaEntity.Relationship
export type Report = FriendicaEntity.Report
export type Results = FriendicaEntity.Results
export type ScheduledStatus = FriendicaEntity.ScheduledStatus
export type Source = FriendicaEntity.Source
export type Stats = FriendicaEntity.Stats
export type Status = FriendicaEntity.Status
export type StatusParams = FriendicaEntity.StatusParams
export type StatusSource = FriendicaEntity.StatusSource
export type Tag = FriendicaEntity.Tag
export type Token = FriendicaEntity.Token
export type URLs = FriendicaEntity.URLs
}
export namespace Converter {
export const encodeNotificationType = (
t: MegalodonEntity.NotificationType
): FriendicaEntity.NotificationType | UnknownNotificationTypeError => {
switch (t) {
case NotificationType.Follow:
return FriendicaNotificationType.Follow
case NotificationType.Favourite:
return FriendicaNotificationType.Favourite
case NotificationType.Reblog:
return FriendicaNotificationType.Reblog
case NotificationType.Mention:
return FriendicaNotificationType.Mention
case NotificationType.FollowRequest:
return FriendicaNotificationType.FollowRequest
case NotificationType.Status:
return FriendicaNotificationType.Status
case NotificationType.PollExpired:
return FriendicaNotificationType.Poll
case NotificationType.Update:
return FriendicaNotificationType.Update
default:
return new UnknownNotificationTypeError()
}
}
export const decodeNotificationType = (
t: FriendicaEntity.NotificationType
): MegalodonEntity.NotificationType | UnknownNotificationTypeError => {
switch (t) {
case FriendicaNotificationType.Follow:
return NotificationType.Follow
case FriendicaNotificationType.Favourite:
return NotificationType.Favourite
case FriendicaNotificationType.Mention:
return NotificationType.Mention
case FriendicaNotificationType.Reblog:
return NotificationType.Reblog
case FriendicaNotificationType.FollowRequest:
return NotificationType.FollowRequest
case FriendicaNotificationType.Status:
return NotificationType.Status
case FriendicaNotificationType.Poll:
return NotificationType.PollExpired
case FriendicaNotificationType.Update:
return NotificationType.Update
default:
return new UnknownNotificationTypeError()
}
}
export const account = (a: Entity.Account): MegalodonEntity.Account => ({
id: a.id,
username: a.username,
acct: a.acct,
display_name: a.display_name,
locked: a.locked,
discoverable: a.discoverable,
group: a.group,
noindex: null,
suspended: null,
limited: null,
created_at: a.created_at,
followers_count: a.followers_count,
following_count: a.following_count,
statuses_count: a.statuses_count,
note: a.note,
url: a.url,
avatar: a.avatar,
avatar_static: a.avatar_static,
header: a.header,
header_static: a.header_static,
emojis: a.emojis.map(e => emoji(e)),
moved: a.moved ? account(a.moved) : null,
fields: a.fields.map(f => field(f)),
bot: a.bot,
source: a.source ? source(a.source) : undefined
})
export const activity = (a: Entity.Activity): MegalodonEntity.Activity => a
export const application = (a: Entity.Application): MegalodonEntity.Application => a
export const attachment = (a: Entity.Attachment): MegalodonEntity.Attachment => a
export const async_attachment = (a: Entity.AsyncAttachment) => {
if (a.url) {
return {
id: a.id,
type: a.type,
url: a.url,
remote_url: a.remote_url,
preview_url: a.preview_url,
text_url: a.text_url,
meta: a.meta,
description: a.description,
blurhash: a.blurhash
} as MegalodonEntity.Attachment
} else {
return a as MegalodonEntity.AsyncAttachment
}
}
export const card = (c: Entity.Card): MegalodonEntity.Card => ({
url: c.url,
title: c.title,
description: c.description,
type: c.type,
image: c.image,
author_name: c.author_name,
author_url: c.author_url,
provider_name: c.provider_name,
provider_url: c.provider_url,
html: c.html,
width: c.width,
height: c.height,
embed_url: null,
blurhash: c.blurhash
})
export const context = (c: Entity.Context): MegalodonEntity.Context => ({
ancestors: Array.isArray(c.ancestors) ? c.ancestors.map(a => status(a)) : [],
descendants: Array.isArray(c.descendants) ? c.descendants.map(d => status(d)) : []
})
export const conversation = (c: Entity.Conversation): MegalodonEntity.Conversation => ({
id: c.id,
accounts: Array.isArray(c.accounts) ? c.accounts.map(a => account(a)) : [],
last_status: c.last_status ? status(c.last_status) : null,
unread: c.unread
})
export const emoji = (e: Entity.Emoji): MegalodonEntity.Emoji => ({
shortcode: e.shortcode,
static_url: e.static_url,
url: e.url,
visible_in_picker: e.visible_in_picker
})
export const featured_tag = (e: Entity.FeaturedTag): MegalodonEntity.FeaturedTag => e
export const field = (f: Entity.Field): MegalodonEntity.Field => f
export const filter = (f: Entity.Filter): MegalodonEntity.Filter => f
export const follow_request = (f: Entity.FollowRequest): MegalodonEntity.FollowRequest => ({
id: f.id,
username: f.username,
acct: f.acct,
display_name: f.display_name,
locked: f.locked,
bot: f.bot,
discoverable: f.discoverable,
group: f.group,
created_at: f.created_at,
note: f.note,
url: f.url,
avatar: f.avatar,
avatar_static: f.avatar_static,
header: f.header,
header_static: f.header_static,
followers_count: f.followers_count,
following_count: f.following_count,
statuses_count: f.statuses_count,
emojis: f.emojis.map(e => emoji(e)),
fields: f.fields.map(f => field(f))
})
export const history = (h: Entity.History): MegalodonEntity.History => h
export const identity_proof = (i: Entity.IdentityProof): MegalodonEntity.IdentityProof => i
export const instance = (i: Entity.Instance): MegalodonEntity.Instance => {
return {
uri: i.uri,
title: i.title,
description: i.description,
email: i.email,
version: i.version,
thumbnail: i.thumbnail,
urls: i.urls ? urls(i.urls) : null,
stats: stats(i.stats),
languages: i.languages,
registrations: i.registrations,
approval_required: i.approval_required,
invites_enabled: i.invites_enabled,
configuration: {
statuses: {
max_characters: i.max_toot_chars
}
},
contact_account: account(i.contact_account),
rules: i.rules
}
}
export const list = (l: Entity.List): MegalodonEntity.List => l
export const marker = (m: Entity.Marker): MegalodonEntity.Marker => m
export const mention = (m: Entity.Mention): MegalodonEntity.Mention => m
export const notification = (n: Entity.Notification): MegalodonEntity.Notification | UnknownNotificationTypeError => {
const notificationType = decodeNotificationType(n.type)
if (notificationType instanceof UnknownNotificationTypeError) return notificationType
if (n.status) {
return {
account: account(n.account),
created_at: n.created_at,
id: n.id,
status: status(n.status),
type: notificationType
}
} else {
return {
account: account(n.account),
created_at: n.created_at,
id: n.id,
type: notificationType
}
}
}
export const poll = (p: Entity.Poll): MegalodonEntity.Poll => p
export const poll_option = (p: Entity.PollOption): MegalodonEntity.PollOption => p
export const preferences = (p: Entity.Preferences): MegalodonEntity.Preferences => p
export const push_subscription = (p: Entity.PushSubscription): MegalodonEntity.PushSubscription => p
export const relationship = (r: Entity.Relationship): MegalodonEntity.Relationship => r
export const report = (r: Entity.Report): MegalodonEntity.Report => ({
id: r.id,
action_taken: r.action_taken,
action_taken_at: null,
category: r.category,
comment: r.comment,
forwarded: r.forwarded,
status_ids: r.status_ids,
rule_ids: r.rule_ids,
target_account: account(r.target_account)
})
export const results = (r: Entity.Results): MegalodonEntity.Results => ({
accounts: Array.isArray(r.accounts) ? r.accounts.map(a => account(a)) : [],
statuses: Array.isArray(r.statuses) ? r.statuses.map(s => status(s)) : [],
hashtags: Array.isArray(r.hashtags) ? r.hashtags.map(h => tag(h)) : []
})
export const scheduled_status = (s: Entity.ScheduledStatus): MegalodonEntity.ScheduledStatus => {
return {
id: s.id,
scheduled_at: s.scheduled_at,
params: status_params(s.params),
media_attachments: s.media_attachments ? s.media_attachments.map(a => attachment(a)) : null
}
}
export const source = (s: Entity.Source): MegalodonEntity.Source => s
export const stats = (s: Entity.Stats): MegalodonEntity.Stats => s
export const status = (s: Entity.Status): MegalodonEntity.Status => ({
id: s.id,
uri: s.uri,
url: s.url,
account: account(s.account),
in_reply_to_id: s.in_reply_to_id,
in_reply_to_account_id: s.in_reply_to_account_id,
reblog: s.reblog ? status(s.reblog) : s.quote ? status(s.quote) : null,
content: s.content,
plain_content: null,
created_at: s.created_at,
emojis: Array.isArray(s.emojis) ? s.emojis.map(e => emoji(e)) : [],
replies_count: s.replies_count,
reblogs_count: s.reblogs_count,
favourites_count: s.favourites_count,
reblogged: s.reblogged,
favourited: s.favourited,
muted: s.muted,
sensitive: s.sensitive,
spoiler_text: s.spoiler_text,
visibility: s.visibility,
media_attachments: Array.isArray(s.media_attachments) ? s.media_attachments.map(m => attachment(m)) : [],
mentions: Array.isArray(s.mentions) ? s.mentions.map(m => mention(m)) : [],
tags: s.tags,
card: s.card ? card(s.card) : null,
poll: s.poll ? poll(s.poll) : null,
application: s.application ? application(s.application) : null,
language: s.language,
pinned: s.pinned,
emoji_reactions: [],
bookmarked: s.bookmarked ? s.bookmarked : false,
quote: false
})
export const status_params = (s: Entity.StatusParams): MegalodonEntity.StatusParams => {
return {
text: s.text,
in_reply_to_id: s.in_reply_to_id,
media_ids: s.media_ids,
sensitive: s.sensitive,
spoiler_text: s.spoiler_text,
visibility: s.visibility,
scheduled_at: s.scheduled_at,
application_id: parseInt(s.application_id)
}
}
export const status_source = (s: Entity.StatusSource): MegalodonEntity.StatusSource => s
export const tag = (t: Entity.Tag): MegalodonEntity.Tag => t
export const token = (t: Entity.Token): MegalodonEntity.Token => t
export const urls = (u: Entity.URLs): MegalodonEntity.URLs => u
}
}
export default FriendicaAPI

View File

@ -0,0 +1,29 @@
/// <reference path="emoji.ts" />
/// <reference path="source.ts" />
/// <reference path="field.ts" />
namespace FriendicaEntity {
export type Account = {
id: string
username: string
acct: string
display_name: string
locked: boolean
discoverable?: boolean
group: 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<Emoji>
moved: Account | null
fields: Array<Field>
bot: boolean
source?: Source
}
}

View File

@ -0,0 +1,8 @@
namespace FriendicaEntity {
export type Activity = {
week: string
statuses: string
logins: string
registrations: string
}
}

View File

@ -0,0 +1,7 @@
namespace FriendicaEntity {
export type Application = {
name: string
website?: string | null
vapid_key?: string | null
}
}

View File

@ -0,0 +1,14 @@
/// <reference path="attachment.ts" />
namespace FriendicaEntity {
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
}
}

View File

@ -0,0 +1,49 @@
namespace FriendicaEntity {
export type Sub = {
// For Image, Gifv, and Video
width?: number
height?: number
size?: string
aspect?: number
// For Gifv and Video
frame_rate?: string
// For Audio, Gifv, and Video
duration?: number
bitrate?: 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 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
}
}

View File

@ -0,0 +1,17 @@
namespace FriendicaEntity {
export type Card = {
url: string
title: string
description: string
type: 'link' | 'photo' | 'video' | 'rich'
image: string | null
author_name: string
author_url: string
provider_name: string
provider_url: string
html: string
width: number
height: number
blurhash: string | null
}
}

View File

@ -0,0 +1,8 @@
/// <reference path="status.ts" />
namespace FriendicaEntity {
export type Context = {
ancestors: Array<Status>
descendants: Array<Status>
}
}

View File

@ -0,0 +1,11 @@
/// <reference path="account.ts" />
/// <reference path="status.ts" />
namespace FriendicaEntity {
export type Conversation = {
id: string
accounts: Array<Account>
last_status: Status | null
unread: boolean
}
}

View File

@ -0,0 +1,8 @@
namespace FriendicaEntity {
export type Emoji = {
shortcode: string
static_url: string
url: string
visible_in_picker: boolean
}
}

View File

@ -0,0 +1,8 @@
namespace FriendicaEntity {
export type FeaturedTag = {
id: string
name: string
statuses_count: number
last_status_at: string
}
}

View File

@ -0,0 +1,7 @@
namespace FriendicaEntity {
export type Field = {
name: string
value: string
verified_at: string | null
}
}

View File

@ -0,0 +1,12 @@
namespace FriendicaEntity {
export type Filter = {
id: string
phrase: string
context: Array<FilterContext>
expires_at: string | null
irreversible: boolean
whole_word: boolean
}
export type FilterContext = string
}

View File

@ -0,0 +1,27 @@
/// <reference path="emoji.ts" />
/// <reference path="field.ts" />
namespace FriendicaEntity {
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<Emoji>
fields: Array<Field>
}
}

View File

@ -0,0 +1,7 @@
namespace FriendicaEntity {
export type History = {
day: string
uses: number
accounts: number
}
}

View File

@ -0,0 +1,9 @@
namespace FriendicaEntity {
export type IdentityProof = {
provider: string
provider_username: string
updated_at: string
proof_url: string
profile_url: string
}
}

View File

@ -0,0 +1,28 @@
/// <reference path="account.ts" />
/// <reference path="urls.ts" />
/// <reference path="stats.ts" />
namespace FriendicaEntity {
export type Instance = {
uri: string
title: string
description: string
email: string
version: string
thumbnail: string | null
urls: URLs | null
stats: Stats
languages: Array<string>
registrations: boolean
approval_required: boolean
invites_enabled: boolean
max_toot_chars: number
contact_account: Account
rules: Array<InstanceRule>
}
export type InstanceRule = {
id: string
text: string
}
}

View File

@ -0,0 +1,9 @@
namespace FriendicaEntity {
export type List = {
id: string
title: string
replies_policy: RepliesPolicy
}
export type RepliesPolicy = 'followed' | 'list' | 'none'
}

View File

@ -0,0 +1,14 @@
namespace FriendicaEntity {
export type Marker = {
home: {
last_read_id: string
version: number
updated_at: string
}
notifications: {
last_read_id: string
version: number
updated_at: string
}
}
}

View File

@ -0,0 +1,8 @@
namespace FriendicaEntity {
export type Mention = {
id: string
username: string
url: string
acct: string
}
}

View File

@ -0,0 +1,14 @@
/// <reference path="account.ts" />
/// <reference path="status.ts" />
namespace FriendicaEntity {
export type Notification = {
account: Account
created_at: string
id: string
status?: Status
type: NotificationType
}
export type NotificationType = string
}

View File

@ -0,0 +1,13 @@
/// <reference path="poll_option.ts" />
namespace FriendicaEntity {
export type Poll = {
id: string
expires_at: string | null
expired: boolean
multiple: boolean
votes_count: number
options: Array<PollOption>
voted: boolean
}
}

View File

@ -0,0 +1,6 @@
namespace FriendicaEntity {
export type PollOption = {
title: string
votes_count: number | null
}
}

View File

@ -0,0 +1,9 @@
namespace FriendicaEntity {
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
}
}

View File

@ -0,0 +1,16 @@
namespace FriendicaEntity {
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
}
}

View File

@ -0,0 +1,17 @@
namespace FriendicaEntity {
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
}
}

View File

@ -0,0 +1,16 @@
/// <reference path="account.ts" />
namespace FriendicaEntity {
export type Report = {
id: string
action_taken: boolean
category: Category
comment: string
forwarded: boolean
status_ids: Array<string> | null
rule_ids: Array<string> | null
target_account: Account
}
export type Category = 'spam' | 'violation' | 'other'
}

View File

@ -0,0 +1,11 @@
/// <reference path="account.ts" />
/// <reference path="status.ts" />
/// <reference path="tag.ts" />
namespace FriendicaEntity {
export type Results = {
accounts: Array<Account>
statuses: Array<Status>
hashtags: Array<Tag>
}
}

View File

@ -0,0 +1,10 @@
/// <reference path="attachment.ts" />
/// <reference path="status_params.ts" />
namespace FriendicaEntity {
export type ScheduledStatus = {
id: string
scheduled_at: string
params: StatusParams
media_attachments: Array<Attachment>
}
}

View File

@ -0,0 +1,10 @@
/// <reference path="field.ts" />
namespace FriendicaEntity {
export type Source = {
privacy: string | null
sensitive: boolean | null
language: string | null
note: string
fields: Array<Field>
}
}

View File

@ -0,0 +1,7 @@
namespace FriendicaEntity {
export type Stats = {
user_count: number
status_count: number
domain_count: number
}
}

Some files were not shown because too many files have changed in this diff Show More