hotfix: don't use upstream megalodon for now
This commit is contained in:
parent
b7125966d0
commit
d1bb6e02e9
3
.gitignore
vendored
3
.gitignore
vendored
@ -57,6 +57,9 @@ packages/backend/assets/LICENSE
|
||||
!/packages/backend/src/db
|
||||
!/packages/backend/src/server/api/endpoints/drive/files
|
||||
|
||||
packages/megalodon/lib
|
||||
packages/megalodon/.idea
|
||||
|
||||
# blender backups
|
||||
*.blend1
|
||||
*.blend2
|
||||
|
@ -26,6 +26,7 @@ COPY packages/backend/package.json packages/backend/package.json
|
||||
COPY packages/client/package.json packages/client/package.json
|
||||
COPY packages/sw/package.json packages/sw/package.json
|
||||
COPY packages/firefish-js/package.json packages/firefish-js/package.json
|
||||
COPY packages/megalodon/package.json packages/megalodon/package.json
|
||||
COPY packages/backend/native-utils/package.json packages/backend/native-utils/package.json
|
||||
COPY packages/backend/native-utils/npm/linux-x64-musl/package.json packages/backend/native-utils/npm/linux-x64-musl/package.json
|
||||
COPY packages/backend/native-utils/npm/linux-arm64-musl/package.json packages/backend/native-utils/npm/linux-arm64-musl/package.json
|
||||
@ -55,6 +56,8 @@ RUN apt-get update && apt-get install -y libvips-dev zip unzip tini ffmpeg
|
||||
|
||||
COPY . ./
|
||||
|
||||
COPY --from=build /firefish/packages/megalodon /firefish/packages/megalodon
|
||||
|
||||
# Copy node modules
|
||||
COPY --from=build /firefish/node_modules /firefish/node_modules
|
||||
COPY --from=build /firefish/packages/backend/node_modules /firefish/packages/backend/node_modules
|
||||
|
@ -7,3 +7,4 @@ This directory contains all of the packages Firefish uses.
|
||||
- `client`: Web interface written in Vue3 and TypeScript
|
||||
- `sw`: Web [Service Worker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) written in TypeScript
|
||||
- `firefish-js`: TypeScript SDK for both backend and client, also published on [NPM](https://www.npmjs.com/package/firefish-js) for public use
|
||||
- `megalodon`: TypeScript library used for partial Mastodon API compatibility
|
||||
|
@ -87,7 +87,7 @@
|
||||
"koa-send": "5.0.1",
|
||||
"koa-slow": "2.1.0",
|
||||
"koa-views": "7.0.2",
|
||||
"megalodon": "8.1.1",
|
||||
"megalodon": "workspace:*",
|
||||
"meilisearch": "0.34.1",
|
||||
"mfm-js": "0.23.3",
|
||||
"mime-types": "2.1.35",
|
||||
|
@ -24,11 +24,7 @@ export function getClient(
|
||||
const accessTokenArr = authorization?.split(" ") ?? [null];
|
||||
const accessToken = accessTokenArr[accessTokenArr.length - 1];
|
||||
const generator = (megalodon as any).default;
|
||||
const client = generator(
|
||||
"firefish",
|
||||
BASE_URL,
|
||||
accessToken,
|
||||
) as MegalodonInterface;
|
||||
const client = generator(BASE_URL, accessToken) as MegalodonInterface;
|
||||
return client;
|
||||
}
|
||||
|
||||
|
@ -68,7 +68,7 @@ export function apiAuthMastodon(router: Router): void {
|
||||
website: body.website,
|
||||
redirect_uri: red,
|
||||
client_id: Buffer.from(appData.url || "").toString("base64"),
|
||||
client_secret: appData.client_secret,
|
||||
client_secret: appData.clientSecret,
|
||||
};
|
||||
console.log(returns);
|
||||
ctx.body = returns;
|
||||
|
@ -1,7 +1,8 @@
|
||||
import megalodon, { MegalodonInterface } from "megalodon";
|
||||
import Router from "@koa/router";
|
||||
import { getClient } from "../ApiMastodonCompatibleService.js";
|
||||
import axios from "axios";
|
||||
import Converter from "megalodon";
|
||||
import { Converter } from "megalodon";
|
||||
import { convertTimelinesArgsId, limitToInt } from "./timeline.js";
|
||||
import { convertAccount, convertStatus } from "../converters.js";
|
||||
|
||||
|
@ -380,7 +380,7 @@ export function apiStatusMastodon(router: Router): void {
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.createEmojiReaction(
|
||||
const data = await client.reactStatus(
|
||||
convertId(ctx.params.id, IdType.FirefishId),
|
||||
ctx.params.name,
|
||||
);
|
||||
@ -400,7 +400,7 @@ export function apiStatusMastodon(router: Router): void {
|
||||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.deleteEmojiReaction(
|
||||
const data = await client.unreactStatus(
|
||||
convertId(ctx.params.id, IdType.FirefishId),
|
||||
ctx.params.name,
|
||||
);
|
||||
|
@ -25,7 +25,7 @@ import { readNotification } from "../common/read-notification.js";
|
||||
import channels from "./channels/index.js";
|
||||
import type Channel from "./channel.js";
|
||||
import type { StreamEventEmitter, StreamMessages } from "./types.js";
|
||||
import Converter from "megalodon";
|
||||
import { Converter } from "megalodon";
|
||||
import { getClient } from "../mastodon/ApiMastodonCompatibleService.js";
|
||||
|
||||
/**
|
||||
|
@ -163,10 +163,10 @@ mastoRouter.post("/oauth/token", async (ctx) => {
|
||||
ctx.body = ret;
|
||||
return;
|
||||
}
|
||||
let client_id: Array<string> | string | null = body.client_id;
|
||||
let client_id: any = body.client_id;
|
||||
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
|
||||
const generator = (megalodon as any).default;
|
||||
const client = generator("firefish", BASE_URL, null) as MegalodonInterface;
|
||||
const client = generator(BASE_URL, null) as MegalodonInterface;
|
||||
let token = null;
|
||||
if (body.code) {
|
||||
//m = body.code.match(/^([a-zA-Z0-9]{8})([a-zA-Z0-9]{4})([a-zA-Z0-9]{4})([a-zA-Z0-9]{4})([a-zA-Z0-9]{12})/);
|
||||
@ -190,7 +190,7 @@ mastoRouter.post("/oauth/token", async (ctx) => {
|
||||
token ? token : "",
|
||||
);
|
||||
const ret = {
|
||||
access_token: atData.access_token,
|
||||
access_token: atData.accessToken,
|
||||
token_type: "Bearer",
|
||||
scope: body.scope || "read write follow push",
|
||||
created_at: Math.floor(new Date().getTime() / 1000),
|
||||
|
83
packages/megalodon/package.json
Normal file
83
packages/megalodon/package.json
Normal file
@ -0,0 +1,83 @@
|
||||
{
|
||||
"name": "megalodon",
|
||||
"private": true,
|
||||
"main": "./lib/src/index.js",
|
||||
"typings": "./lib/src/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc -p ./",
|
||||
"build:debug": "pnpm run build",
|
||||
"lint": "pnpm biome check **/*.ts --apply",
|
||||
"format": "pnpm biome format --write src/**/*.ts",
|
||||
"doc": "typedoc --out ../docs ./src",
|
||||
"test": "NODE_ENV=test jest -u --maxWorkers=3"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"ts",
|
||||
"js"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"^@/(.+)": "<rootDir>/src/$1",
|
||||
"^~/(.+)": "<rootDir>/$1"
|
||||
},
|
||||
"testMatch": [
|
||||
"**/test/**/*.spec.ts"
|
||||
],
|
||||
"preset": "ts-jest/presets/default",
|
||||
"transform": {
|
||||
"^.+\\.(ts|tsx)$": "ts-jest"
|
||||
},
|
||||
"globals": {
|
||||
"ts-jest": {
|
||||
"tsconfig": "tsconfig.json"
|
||||
}
|
||||
},
|
||||
"testEnvironment": "node"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/oauth": "^0.9.0",
|
||||
"@types/ws": "^8.5.4",
|
||||
"axios": "1.2.2",
|
||||
"dayjs": "^1.11.7",
|
||||
"form-data": "^4.0.0",
|
||||
"https-proxy-agent": "^5.0.1",
|
||||
"oauth": "^0.10.0",
|
||||
"object-assign-deep": "^0.4.0",
|
||||
"parse-link-header": "^2.0.0",
|
||||
"socks-proxy-agent": "^7.0.0",
|
||||
"typescript": "4.9.4",
|
||||
"uuid": "^9.0.0",
|
||||
"ws": "8.12.0",
|
||||
"async-lock": "1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/core-js": "^2.5.0",
|
||||
"@types/form-data": "^2.5.0",
|
||||
"@types/jest": "^29.4.0",
|
||||
"@types/object-assign-deep": "^0.4.0",
|
||||
"@types/parse-link-header": "^2.0.0",
|
||||
"@types/uuid": "^9.0.0",
|
||||
"@types/node": "18.11.18",
|
||||
"@typescript-eslint/eslint-plugin": "^5.49.0",
|
||||
"@typescript-eslint/parser": "^5.49.0",
|
||||
"@types/async-lock": "1.4.0",
|
||||
"eslint": "^8.32.0",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-config-standard": "^16.0.3",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-node": "^11.0.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-standard": "^5.0.0",
|
||||
"jest": "^29.4.0",
|
||||
"jest-worker": "^29.4.0",
|
||||
"lodash": "^4.17.14",
|
||||
"prettier": "^2.8.3",
|
||||
"ts-jest": "^29.0.5",
|
||||
"typedoc": "^0.23.24"
|
||||
},
|
||||
"directories": {
|
||||
"lib": "lib",
|
||||
"test": "test"
|
||||
}
|
||||
}
|
1
packages/megalodon/src/axios.d.ts
vendored
Normal file
1
packages/megalodon/src/axios.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
declare module "axios/lib/adapters/http";
|
13
packages/megalodon/src/cancel.ts
Normal file
13
packages/megalodon/src/cancel.ts
Normal 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;
|
||||
};
|
3
packages/megalodon/src/converter.ts
Normal file
3
packages/megalodon/src/converter.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import MisskeyAPI from "./misskey/api_client";
|
||||
|
||||
export default MisskeyAPI.Converter;
|
3
packages/megalodon/src/default.ts
Normal file
3
packages/megalodon/src/default.ts
Normal 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";
|
27
packages/megalodon/src/entities/account.ts
Normal file
27
packages/megalodon/src/entities/account.ts
Normal file
@ -0,0 +1,27 @@
|
||||
/// <reference path="emoji.ts" />
|
||||
/// <reference path="source.ts" />
|
||||
/// <reference path="field.ts" />
|
||||
namespace Entity {
|
||||
export type Account = {
|
||||
id: string;
|
||||
username: string;
|
||||
acct: string;
|
||||
display_name: string;
|
||||
locked: boolean;
|
||||
created_at: string;
|
||||
followers_count: number;
|
||||
following_count: number;
|
||||
statuses_count: number;
|
||||
note: string;
|
||||
url: string;
|
||||
avatar: string;
|
||||
avatar_static: string;
|
||||
header: string;
|
||||
header_static: string;
|
||||
emojis: Array<Emoji>;
|
||||
moved: Account | null;
|
||||
fields: Array<Field>;
|
||||
bot: boolean | null;
|
||||
source?: Source;
|
||||
};
|
||||
}
|
8
packages/megalodon/src/entities/activity.ts
Normal file
8
packages/megalodon/src/entities/activity.ts
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Entity {
|
||||
export type Activity = {
|
||||
week: string;
|
||||
statuses: string;
|
||||
logins: string;
|
||||
registrations: string;
|
||||
};
|
||||
}
|
34
packages/megalodon/src/entities/announcement.ts
Normal file
34
packages/megalodon/src/entities/announcement.ts
Normal file
@ -0,0 +1,34 @@
|
||||
/// <reference path="tag.ts" />
|
||||
/// <reference path="emoji.ts" />
|
||||
/// <reference path="reaction.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;
|
||||
read?: boolean;
|
||||
mentions: Array<AnnouncementAccount>;
|
||||
statuses: Array<AnnouncementStatus>;
|
||||
tags: Array<Tag>;
|
||||
emojis: Array<Emoji>;
|
||||
reactions: Array<Reaction>;
|
||||
};
|
||||
|
||||
export type AnnouncementAccount = {
|
||||
id: string;
|
||||
username: string;
|
||||
url: string;
|
||||
acct: string;
|
||||
};
|
||||
|
||||
export type AnnouncementStatus = {
|
||||
id: string;
|
||||
url: string;
|
||||
};
|
||||
}
|
7
packages/megalodon/src/entities/application.ts
Normal file
7
packages/megalodon/src/entities/application.ts
Normal file
@ -0,0 +1,7 @@
|
||||
namespace Entity {
|
||||
export type Application = {
|
||||
name: string;
|
||||
website?: string | null;
|
||||
vapid_key?: string | null;
|
||||
};
|
||||
}
|
14
packages/megalodon/src/entities/async_attachment.ts
Normal file
14
packages/megalodon/src/entities/async_attachment.ts
Normal 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;
|
||||
};
|
||||
}
|
49
packages/megalodon/src/entities/attachment.ts
Normal file
49
packages/megalodon/src/entities/attachment.ts
Normal 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;
|
||||
};
|
||||
}
|
16
packages/megalodon/src/entities/card.ts
Normal file
16
packages/megalodon/src/entities/card.ts
Normal file
@ -0,0 +1,16 @@
|
||||
namespace Entity {
|
||||
export type Card = {
|
||||
url: string;
|
||||
title: string;
|
||||
description: string;
|
||||
type: "link" | "photo" | "video" | "rich";
|
||||
image?: string;
|
||||
author_name?: string;
|
||||
author_url?: string;
|
||||
provider_name?: string;
|
||||
provider_url?: string;
|
||||
html?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
}
|
8
packages/megalodon/src/entities/context.ts
Normal file
8
packages/megalodon/src/entities/context.ts
Normal file
@ -0,0 +1,8 @@
|
||||
/// <reference path="status.ts" />
|
||||
|
||||
namespace Entity {
|
||||
export type Context = {
|
||||
ancestors: Array<Status>;
|
||||
descendants: Array<Status>;
|
||||
};
|
||||
}
|
11
packages/megalodon/src/entities/conversation.ts
Normal file
11
packages/megalodon/src/entities/conversation.ts
Normal 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;
|
||||
};
|
||||
}
|
9
packages/megalodon/src/entities/emoji.ts
Normal file
9
packages/megalodon/src/entities/emoji.ts
Normal file
@ -0,0 +1,9 @@
|
||||
namespace Entity {
|
||||
export type Emoji = {
|
||||
shortcode: string;
|
||||
static_url: string;
|
||||
url: string;
|
||||
visible_in_picker: boolean;
|
||||
category: string;
|
||||
};
|
||||
}
|
8
packages/megalodon/src/entities/featured_tag.ts
Normal file
8
packages/megalodon/src/entities/featured_tag.ts
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Entity {
|
||||
export type FeaturedTag = {
|
||||
id: string;
|
||||
name: string;
|
||||
statuses_count: number;
|
||||
last_status_at: string;
|
||||
};
|
||||
}
|
7
packages/megalodon/src/entities/field.ts
Normal file
7
packages/megalodon/src/entities/field.ts
Normal file
@ -0,0 +1,7 @@
|
||||
namespace Entity {
|
||||
export type Field = {
|
||||
name: string;
|
||||
value: string;
|
||||
verified_at: string | null;
|
||||
};
|
||||
}
|
12
packages/megalodon/src/entities/filter.ts
Normal file
12
packages/megalodon/src/entities/filter.ts
Normal 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;
|
||||
}
|
7
packages/megalodon/src/entities/history.ts
Normal file
7
packages/megalodon/src/entities/history.ts
Normal file
@ -0,0 +1,7 @@
|
||||
namespace Entity {
|
||||
export type History = {
|
||||
day: string;
|
||||
uses: number;
|
||||
accounts: number;
|
||||
};
|
||||
}
|
9
packages/megalodon/src/entities/identity_proof.ts
Normal file
9
packages/megalodon/src/entities/identity_proof.ts
Normal file
@ -0,0 +1,9 @@
|
||||
namespace Entity {
|
||||
export type IdentityProof = {
|
||||
provider: string;
|
||||
provider_username: string;
|
||||
updated_at: string;
|
||||
proof_url: string;
|
||||
profile_url: string;
|
||||
};
|
||||
}
|
41
packages/megalodon/src/entities/instance.ts
Normal file
41
packages/megalodon/src/entities/instance.ts
Normal file
@ -0,0 +1,41 @@
|
||||
/// <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;
|
||||
stats: Stats;
|
||||
languages: Array<string>;
|
||||
contact_account: Account | null;
|
||||
max_toot_chars?: number;
|
||||
registrations?: boolean;
|
||||
configuration?: {
|
||||
statuses: {
|
||||
max_characters: number;
|
||||
max_media_attachments: number;
|
||||
characters_reserved_per_url: number;
|
||||
};
|
||||
media_attachments: {
|
||||
supported_mime_types: Array<string>;
|
||||
image_size_limit: number;
|
||||
image_matrix_limit: number;
|
||||
video_size_limit: number;
|
||||
video_frame_limit: number;
|
||||
video_matrix_limit: number;
|
||||
};
|
||||
polls: {
|
||||
max_options: number;
|
||||
max_characters_per_option: number;
|
||||
min_expiration: number;
|
||||
max_expiration: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
6
packages/megalodon/src/entities/list.ts
Normal file
6
packages/megalodon/src/entities/list.ts
Normal file
@ -0,0 +1,6 @@
|
||||
namespace Entity {
|
||||
export type List = {
|
||||
id: string;
|
||||
title: string;
|
||||
};
|
||||
}
|
15
packages/megalodon/src/entities/marker.ts
Normal file
15
packages/megalodon/src/entities/marker.ts
Normal 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;
|
||||
};
|
||||
};
|
||||
}
|
8
packages/megalodon/src/entities/mention.ts
Normal file
8
packages/megalodon/src/entities/mention.ts
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Entity {
|
||||
export type Mention = {
|
||||
id: string;
|
||||
username: string;
|
||||
url: string;
|
||||
acct: string;
|
||||
};
|
||||
}
|
15
packages/megalodon/src/entities/notification.ts
Normal file
15
packages/megalodon/src/entities/notification.ts
Normal file
@ -0,0 +1,15 @@
|
||||
/// <reference path="account.ts" />
|
||||
/// <reference path="status.ts" />
|
||||
|
||||
namespace Entity {
|
||||
export type Notification = {
|
||||
account: Account;
|
||||
created_at: string;
|
||||
id: string;
|
||||
status?: Status;
|
||||
reaction?: Reaction;
|
||||
type: NotificationType;
|
||||
};
|
||||
|
||||
export type NotificationType = string;
|
||||
}
|
14
packages/megalodon/src/entities/poll.ts
Normal file
14
packages/megalodon/src/entities/poll.ts
Normal file
@ -0,0 +1,14 @@
|
||||
/// <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;
|
||||
own_votes: Array<number>;
|
||||
};
|
||||
}
|
6
packages/megalodon/src/entities/poll_option.ts
Normal file
6
packages/megalodon/src/entities/poll_option.ts
Normal file
@ -0,0 +1,6 @@
|
||||
namespace Entity {
|
||||
export type PollOption = {
|
||||
title: string;
|
||||
votes_count: number | null;
|
||||
};
|
||||
}
|
9
packages/megalodon/src/entities/preferences.ts
Normal file
9
packages/megalodon/src/entities/preferences.ts
Normal 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;
|
||||
};
|
||||
}
|
16
packages/megalodon/src/entities/push_subscription.ts
Normal file
16
packages/megalodon/src/entities/push_subscription.ts
Normal 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;
|
||||
};
|
||||
}
|
12
packages/megalodon/src/entities/reaction.ts
Normal file
12
packages/megalodon/src/entities/reaction.ts
Normal file
@ -0,0 +1,12 @@
|
||||
/// <reference path="account.ts" />
|
||||
|
||||
namespace Entity {
|
||||
export type Reaction = {
|
||||
count: number;
|
||||
me: boolean;
|
||||
name: string;
|
||||
url?: string;
|
||||
static_url?: string;
|
||||
accounts?: Array<Account>;
|
||||
};
|
||||
}
|
17
packages/megalodon/src/entities/relationship.ts
Normal file
17
packages/megalodon/src/entities/relationship.ts
Normal file
@ -0,0 +1,17 @@
|
||||
namespace Entity {
|
||||
export type Relationship = {
|
||||
id: string;
|
||||
following: boolean;
|
||||
followed_by: boolean;
|
||||
delivery_following?: boolean;
|
||||
blocking: boolean;
|
||||
blocked_by: boolean;
|
||||
muting: boolean;
|
||||
muting_notifications: boolean;
|
||||
requested: boolean;
|
||||
domain_blocking: boolean;
|
||||
showing_reblogs: boolean;
|
||||
endorsed: boolean;
|
||||
notifying: boolean;
|
||||
};
|
||||
}
|
9
packages/megalodon/src/entities/report.ts
Normal file
9
packages/megalodon/src/entities/report.ts
Normal file
@ -0,0 +1,9 @@
|
||||
namespace Entity {
|
||||
export type Report = {
|
||||
id: string;
|
||||
action_taken: string;
|
||||
comment: string;
|
||||
account_id: string;
|
||||
status_ids: Array<string>;
|
||||
};
|
||||
}
|
11
packages/megalodon/src/entities/results.ts
Normal file
11
packages/megalodon/src/entities/results.ts
Normal 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>;
|
||||
};
|
||||
}
|
10
packages/megalodon/src/entities/scheduled_status.ts
Normal file
10
packages/megalodon/src/entities/scheduled_status.ts
Normal 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>;
|
||||
};
|
||||
}
|
10
packages/megalodon/src/entities/source.ts
Normal file
10
packages/megalodon/src/entities/source.ts
Normal 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>;
|
||||
};
|
||||
}
|
7
packages/megalodon/src/entities/stats.ts
Normal file
7
packages/megalodon/src/entities/stats.ts
Normal file
@ -0,0 +1,7 @@
|
||||
namespace Entity {
|
||||
export type Stats = {
|
||||
user_count: number;
|
||||
status_count: number;
|
||||
domain_count: number;
|
||||
};
|
||||
}
|
45
packages/megalodon/src/entities/status.ts
Normal file
45
packages/megalodon/src/entities/status.ts
Normal file
@ -0,0 +1,45 @@
|
||||
/// <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 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<Tag>;
|
||||
card: Card | null;
|
||||
poll: Poll | null;
|
||||
application: Application | null;
|
||||
language: string | null;
|
||||
pinned: boolean | null;
|
||||
reactions: Array<Reaction>;
|
||||
quote: Status | null;
|
||||
bookmarked: boolean;
|
||||
};
|
||||
}
|
23
packages/megalodon/src/entities/status_edit.ts
Normal file
23
packages/megalodon/src/entities/status_edit.ts
Normal 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;
|
||||
};
|
||||
}
|
12
packages/megalodon/src/entities/status_params.ts
Normal file
12
packages/megalodon/src/entities/status_params.ts
Normal 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";
|
||||
scheduled_at: string | null;
|
||||
application_id: string;
|
||||
};
|
||||
}
|
10
packages/megalodon/src/entities/tag.ts
Normal file
10
packages/megalodon/src/entities/tag.ts
Normal file
@ -0,0 +1,10 @@
|
||||
/// <reference path="history.ts" />
|
||||
|
||||
namespace Entity {
|
||||
export type Tag = {
|
||||
name: string;
|
||||
url: string;
|
||||
history: Array<History> | null;
|
||||
following?: boolean;
|
||||
};
|
||||
}
|
8
packages/megalodon/src/entities/token.ts
Normal file
8
packages/megalodon/src/entities/token.ts
Normal file
@ -0,0 +1,8 @@
|
||||
namespace Entity {
|
||||
export type Token = {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
scope: string;
|
||||
created_at: number;
|
||||
};
|
||||
}
|
5
packages/megalodon/src/entities/urls.ts
Normal file
5
packages/megalodon/src/entities/urls.ts
Normal file
@ -0,0 +1,5 @@
|
||||
namespace Entity {
|
||||
export type URLs = {
|
||||
streaming_api: string;
|
||||
};
|
||||
}
|
38
packages/megalodon/src/entity.ts
Normal file
38
packages/megalodon/src/entity.ts
Normal file
@ -0,0 +1,38 @@
|
||||
/// <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/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/tag.ts" />
|
||||
/// <reference path="./entities/token.ts" />
|
||||
/// <reference path="./entities/urls.ts" />
|
||||
|
||||
export default Entity;
|
11
packages/megalodon/src/filter_context.ts
Normal file
11
packages/megalodon/src/filter_context.ts
Normal 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;
|
32
packages/megalodon/src/index.ts
Normal file
32
packages/megalodon/src/index.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import Response from "./response";
|
||||
import OAuth from "./oauth";
|
||||
import { isCancel, RequestCanceledError } from "./cancel";
|
||||
import { ProxyConfig } from "./proxy_config";
|
||||
import generator, {
|
||||
detector,
|
||||
MegalodonInterface,
|
||||
WebSocketInterface,
|
||||
} from "./megalodon";
|
||||
import Misskey from "./misskey";
|
||||
import Entity from "./entity";
|
||||
import NotificationType from "./notification";
|
||||
import FilterContext from "./filter_context";
|
||||
import Converter from "./converter";
|
||||
|
||||
export {
|
||||
Response,
|
||||
OAuth,
|
||||
RequestCanceledError,
|
||||
isCancel,
|
||||
ProxyConfig,
|
||||
detector,
|
||||
MegalodonInterface,
|
||||
WebSocketInterface,
|
||||
NotificationType,
|
||||
FilterContext,
|
||||
Misskey,
|
||||
Entity,
|
||||
Converter,
|
||||
};
|
||||
|
||||
export default generator;
|
1532
packages/megalodon/src/megalodon.ts
Normal file
1532
packages/megalodon/src/megalodon.ts
Normal file
File diff suppressed because it is too large
Load Diff
3436
packages/megalodon/src/misskey.ts
Normal file
3436
packages/megalodon/src/misskey.ts
Normal file
File diff suppressed because it is too large
Load Diff
727
packages/megalodon/src/misskey/api_client.ts
Normal file
727
packages/megalodon/src/misskey/api_client.ts
Normal file
@ -0,0 +1,727 @@
|
||||
import axios, { AxiosResponse, AxiosRequestConfig } from "axios";
|
||||
import dayjs from "dayjs";
|
||||
import FormData from "form-data";
|
||||
|
||||
import { DEFAULT_UA } from "../default";
|
||||
import proxyAgent, { ProxyConfig } from "../proxy_config";
|
||||
import Response from "../response";
|
||||
import MisskeyEntity from "./entity";
|
||||
import MegalodonEntity from "../entity";
|
||||
import WebSocket from "./web_socket";
|
||||
import MisskeyNotificationType from "./notification";
|
||||
import NotificationType from "../notification";
|
||||
|
||||
namespace MisskeyAPI {
|
||||
export namespace Entity {
|
||||
export type App = MisskeyEntity.App;
|
||||
export type Announcement = MisskeyEntity.Announcement;
|
||||
export type Blocking = MisskeyEntity.Blocking;
|
||||
export type Choice = MisskeyEntity.Choice;
|
||||
export type CreatedNote = MisskeyEntity.CreatedNote;
|
||||
export type Emoji = MisskeyEntity.Emoji;
|
||||
export type Favorite = MisskeyEntity.Favorite;
|
||||
export type Field = MisskeyEntity.Field;
|
||||
export type File = MisskeyEntity.File;
|
||||
export type Follower = MisskeyEntity.Follower;
|
||||
export type Following = MisskeyEntity.Following;
|
||||
export type FollowRequest = MisskeyEntity.FollowRequest;
|
||||
export type Hashtag = MisskeyEntity.Hashtag;
|
||||
export type List = MisskeyEntity.List;
|
||||
export type Meta = MisskeyEntity.Meta;
|
||||
export type Mute = MisskeyEntity.Mute;
|
||||
export type Note = MisskeyEntity.Note;
|
||||
export type Notification = MisskeyEntity.Notification;
|
||||
export type Poll = MisskeyEntity.Poll;
|
||||
export type Reaction = MisskeyEntity.Reaction;
|
||||
export type Relation = MisskeyEntity.Relation;
|
||||
export type User = MisskeyEntity.User;
|
||||
export type UserDetail = MisskeyEntity.UserDetail;
|
||||
export type UserDetailMe = MisskeyEntity.UserDetailMe;
|
||||
export type GetAll = MisskeyEntity.GetAll;
|
||||
export type UserKey = MisskeyEntity.UserKey;
|
||||
export type Session = MisskeyEntity.Session;
|
||||
export type Stats = MisskeyEntity.Stats;
|
||||
export type State = MisskeyEntity.State;
|
||||
export type APIEmoji = { emojis: Emoji[] };
|
||||
}
|
||||
|
||||
export class Converter {
|
||||
private baseUrl: string;
|
||||
private instanceHost: string;
|
||||
private plcUrl: string;
|
||||
private modelOfAcct = {
|
||||
id: "1",
|
||||
username: "none",
|
||||
acct: "none",
|
||||
display_name: "none",
|
||||
locked: true,
|
||||
bot: true,
|
||||
discoverable: false,
|
||||
group: false,
|
||||
created_at: "1971-01-01T00:00:00.000Z",
|
||||
note: "",
|
||||
url: "plc",
|
||||
avatar: "plc",
|
||||
avatar_static: "plc",
|
||||
header: "plc",
|
||||
header_static: "plc",
|
||||
followers_count: -1,
|
||||
following_count: 0,
|
||||
statuses_count: 0,
|
||||
last_status_at: "1971-01-01T00:00:00.000Z",
|
||||
noindex: true,
|
||||
emojis: [],
|
||||
fields: [],
|
||||
moved: null,
|
||||
};
|
||||
|
||||
constructor(baseUrl: string) {
|
||||
this.baseUrl = baseUrl;
|
||||
this.instanceHost = baseUrl.substring(baseUrl.indexOf("//") + 2);
|
||||
this.plcUrl = `${baseUrl}/static-assets/transparent.png`;
|
||||
this.modelOfAcct.url = this.plcUrl;
|
||||
this.modelOfAcct.avatar = this.plcUrl;
|
||||
this.modelOfAcct.avatar_static = this.plcUrl;
|
||||
this.modelOfAcct.header = this.plcUrl;
|
||||
this.modelOfAcct.header_static = this.plcUrl;
|
||||
}
|
||||
|
||||
// FIXME: Properly render MFM instead of just escaping HTML characters.
|
||||
escapeMFM = (text: string): string =>
|
||||
text
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'")
|
||||
.replace(/`/g, "`")
|
||||
.replace(/\r?\n/g, "<br>");
|
||||
|
||||
emoji = (e: Entity.Emoji): MegalodonEntity.Emoji => {
|
||||
return {
|
||||
shortcode: e.name,
|
||||
static_url: e.url,
|
||||
url: e.url,
|
||||
visible_in_picker: true,
|
||||
category: e.category,
|
||||
};
|
||||
};
|
||||
|
||||
field = (f: Entity.Field): MegalodonEntity.Field => ({
|
||||
name: f.name,
|
||||
value: this.escapeMFM(f.value),
|
||||
verified_at: null,
|
||||
});
|
||||
|
||||
user = (u: Entity.User): MegalodonEntity.Account => {
|
||||
let acct = u.username;
|
||||
let acctUrl = `https://${u.host || this.instanceHost}/@${u.username}`;
|
||||
if (u.host) {
|
||||
acct = `${u.username}@${u.host}`;
|
||||
acctUrl = `https://${u.host}/@${u.username}`;
|
||||
}
|
||||
return {
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
acct: acct,
|
||||
display_name: u.name || u.username,
|
||||
locked: false,
|
||||
created_at: new Date().toISOString(),
|
||||
followers_count: 0,
|
||||
following_count: 0,
|
||||
statuses_count: 0,
|
||||
note: "",
|
||||
url: acctUrl,
|
||||
avatar: u.avatarUrl,
|
||||
avatar_static: u.avatarUrl,
|
||||
header: this.plcUrl,
|
||||
header_static: this.plcUrl,
|
||||
emojis: u.emojis.map((e) => this.emoji(e)),
|
||||
moved: null,
|
||||
fields: [],
|
||||
bot: false,
|
||||
};
|
||||
};
|
||||
|
||||
userDetail = (
|
||||
u: Entity.UserDetail,
|
||||
host: string,
|
||||
): MegalodonEntity.Account => {
|
||||
let acct = u.username;
|
||||
host = host.replace("https://", "");
|
||||
let acctUrl = `https://${host || u.host || this.instanceHost}/@${
|
||||
u.username
|
||||
}`;
|
||||
if (u.host) {
|
||||
acct = `${u.username}@${u.host}`;
|
||||
acctUrl = `https://${u.host}/@${u.username}`;
|
||||
}
|
||||
return {
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
acct: acct,
|
||||
display_name: u.name || u.username,
|
||||
locked: u.isLocked,
|
||||
created_at: u.createdAt,
|
||||
followers_count: u.followersCount,
|
||||
following_count: u.followingCount,
|
||||
statuses_count: u.notesCount,
|
||||
note: u.description?.replace(/\n|\\n/g, "<br>") ?? "",
|
||||
url: acctUrl,
|
||||
avatar: u.avatarUrl,
|
||||
avatar_static: u.avatarUrl,
|
||||
header: u.bannerUrl ?? this.plcUrl,
|
||||
header_static: u.bannerUrl ?? this.plcUrl,
|
||||
emojis: u.emojis.map((e) => this.emoji(e)),
|
||||
moved: null,
|
||||
fields: u.fields.map((f) => this.field(f)),
|
||||
bot: u.isBot,
|
||||
};
|
||||
};
|
||||
|
||||
userPreferences = (
|
||||
u: MisskeyAPI.Entity.UserDetailMe,
|
||||
v: "public" | "unlisted" | "private" | "direct",
|
||||
): MegalodonEntity.Preferences => {
|
||||
return {
|
||||
"reading:expand:media": "default",
|
||||
"reading:expand:spoilers": false,
|
||||
"posting:default:language": u.lang,
|
||||
"posting:default:sensitive": u.alwaysMarkNsfw,
|
||||
"posting:default:visibility": v,
|
||||
};
|
||||
};
|
||||
|
||||
visibility = (
|
||||
v: "public" | "home" | "followers" | "specified",
|
||||
): "public" | "unlisted" | "private" | "direct" => {
|
||||
switch (v) {
|
||||
case "public":
|
||||
return v;
|
||||
case "home":
|
||||
return "unlisted";
|
||||
case "followers":
|
||||
return "private";
|
||||
case "specified":
|
||||
return "direct";
|
||||
}
|
||||
};
|
||||
|
||||
encodeVisibility = (
|
||||
v: "public" | "unlisted" | "private" | "direct",
|
||||
): "public" | "home" | "followers" | "specified" => {
|
||||
switch (v) {
|
||||
case "public":
|
||||
return v;
|
||||
case "unlisted":
|
||||
return "home";
|
||||
case "private":
|
||||
return "followers";
|
||||
case "direct":
|
||||
return "specified";
|
||||
}
|
||||
};
|
||||
|
||||
fileType = (
|
||||
s: string,
|
||||
): "unknown" | "image" | "gifv" | "video" | "audio" => {
|
||||
if (s === "image/gif") {
|
||||
return "gifv";
|
||||
}
|
||||
if (s.includes("image")) {
|
||||
return "image";
|
||||
}
|
||||
if (s.includes("video")) {
|
||||
return "video";
|
||||
}
|
||||
if (s.includes("audio")) {
|
||||
return "audio";
|
||||
}
|
||||
return "unknown";
|
||||
};
|
||||
|
||||
file = (f: Entity.File): MegalodonEntity.Attachment => {
|
||||
return {
|
||||
id: f.id,
|
||||
type: this.fileType(f.type),
|
||||
url: f.url,
|
||||
remote_url: f.url,
|
||||
preview_url: f.thumbnailUrl,
|
||||
text_url: f.url,
|
||||
meta: {
|
||||
width: f.properties.width,
|
||||
height: f.properties.height,
|
||||
},
|
||||
description: f.comment,
|
||||
blurhash: f.blurhash,
|
||||
};
|
||||
};
|
||||
|
||||
follower = (f: Entity.Follower): MegalodonEntity.Account => {
|
||||
return this.user(f.follower);
|
||||
};
|
||||
|
||||
following = (f: Entity.Following): MegalodonEntity.Account => {
|
||||
return this.user(f.followee);
|
||||
};
|
||||
|
||||
relation = (r: Entity.Relation): MegalodonEntity.Relationship => {
|
||||
return {
|
||||
id: r.id,
|
||||
following: r.isFollowing,
|
||||
followed_by: r.isFollowed,
|
||||
blocking: r.isBlocking,
|
||||
blocked_by: r.isBlocked,
|
||||
muting: r.isMuted,
|
||||
muting_notifications: false,
|
||||
requested: r.hasPendingFollowRequestFromYou,
|
||||
domain_blocking: false,
|
||||
showing_reblogs: true,
|
||||
endorsed: false,
|
||||
notifying: false,
|
||||
};
|
||||
};
|
||||
|
||||
choice = (c: Entity.Choice): MegalodonEntity.PollOption => {
|
||||
return {
|
||||
title: c.text,
|
||||
votes_count: c.votes,
|
||||
};
|
||||
};
|
||||
|
||||
poll = (p: Entity.Poll, id: string): MegalodonEntity.Poll => {
|
||||
const now = dayjs();
|
||||
const expire = dayjs(p.expiresAt);
|
||||
const count = p.choices.reduce((sum, choice) => sum + choice.votes, 0);
|
||||
return {
|
||||
id: id,
|
||||
expires_at: p.expiresAt,
|
||||
expired: now.isAfter(expire),
|
||||
multiple: p.multiple,
|
||||
votes_count: count,
|
||||
options: p.choices.map((c) => this.choice(c)),
|
||||
voted: p.choices.some((c) => c.isVoted),
|
||||
own_votes: p.choices
|
||||
.filter((c) => c.isVoted)
|
||||
.map((c) => p.choices.indexOf(c)),
|
||||
};
|
||||
};
|
||||
|
||||
note = (n: Entity.Note, host: string): MegalodonEntity.Status => {
|
||||
host = host.replace("https://", "");
|
||||
|
||||
return {
|
||||
id: n.id,
|
||||
uri: n.uri ? n.uri : `https://${host}/notes/${n.id}`,
|
||||
url: n.uri ? n.uri : `https://${host}/notes/${n.id}`,
|
||||
account: this.user(n.user),
|
||||
in_reply_to_id: n.replyId,
|
||||
in_reply_to_account_id: n.reply?.userId ?? null,
|
||||
reblog: n.renote ? this.note(n.renote, host) : null,
|
||||
content: n.text ? this.escapeMFM(n.text) : "",
|
||||
plain_content: n.text ? n.text : null,
|
||||
created_at: n.createdAt,
|
||||
// Remove reaction emojis with names containing @ from the emojis list.
|
||||
emojis: n.emojis
|
||||
.filter((e) => e.name.indexOf("@") === -1)
|
||||
.map((e) => this.emoji(e)),
|
||||
replies_count: n.repliesCount,
|
||||
reblogs_count: n.renoteCount,
|
||||
favourites_count: this.getTotalReactions(n.reactions),
|
||||
reblogged: false,
|
||||
favourited: !!n.myReaction,
|
||||
muted: false,
|
||||
sensitive: n.files ? n.files.some((f) => f.isSensitive) : false,
|
||||
spoiler_text: n.cw ? n.cw : "",
|
||||
visibility: this.visibility(n.visibility),
|
||||
media_attachments: n.files ? n.files.map((f) => this.file(f)) : [],
|
||||
mentions: [],
|
||||
tags: [],
|
||||
card: null,
|
||||
poll: n.poll ? this.poll(n.poll, n.id) : null,
|
||||
application: null,
|
||||
language: null,
|
||||
pinned: null,
|
||||
// Use emojis list to provide URLs for emoji reactions.
|
||||
reactions: this.mapReactions(n.emojis, n.reactions, n.myReaction),
|
||||
bookmarked: false,
|
||||
quote: n.renote && n.text ? this.note(n.renote, host) : null,
|
||||
};
|
||||
};
|
||||
|
||||
mapReactions = (
|
||||
emojis: Array<MisskeyEntity.Emoji>,
|
||||
r: { [key: string]: number },
|
||||
myReaction?: string,
|
||||
): Array<MegalodonEntity.Reaction> => {
|
||||
// Map of emoji shortcodes to image URLs.
|
||||
const emojiUrls = new Map<string, string>(
|
||||
emojis.map((e) => [e.name, e.url]),
|
||||
);
|
||||
return Object.keys(r).map((key) => {
|
||||
// Strip colons from custom emoji reaction names to match emoji shortcodes.
|
||||
const shortcode = key.replaceAll(":", "");
|
||||
// If this is a custom emoji (vs. a Unicode emoji), find its image URL.
|
||||
const url = emojiUrls.get(shortcode);
|
||||
// Finally, remove trailing @. from local custom emoji reaction names.
|
||||
const name = shortcode.replace("@.", "");
|
||||
return {
|
||||
count: r[key],
|
||||
me: key === myReaction,
|
||||
name,
|
||||
url,
|
||||
// We don't actually have a static version of the asset, but clients expect one anyway.
|
||||
static_url: url,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
getTotalReactions = (r: { [key: string]: number }): number => {
|
||||
return Object.values(r).length > 0
|
||||
? Object.values(r).reduce(
|
||||
(previousValue, currentValue) => previousValue + currentValue,
|
||||
)
|
||||
: 0;
|
||||
};
|
||||
|
||||
reactions = (
|
||||
r: Array<Entity.Reaction>,
|
||||
): Array<MegalodonEntity.Reaction> => {
|
||||
const result: Array<MegalodonEntity.Reaction> = [];
|
||||
for (const e of r) {
|
||||
const i = result.findIndex((res) => res.name === e.type);
|
||||
if (i >= 0) {
|
||||
result[i].count++;
|
||||
} else {
|
||||
result.push({
|
||||
count: 1,
|
||||
me: false,
|
||||
name: e.type,
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
noteToConversation = (
|
||||
n: Entity.Note,
|
||||
host: string,
|
||||
): MegalodonEntity.Conversation => {
|
||||
const accounts: Array<MegalodonEntity.Account> = [this.user(n.user)];
|
||||
if (n.reply) {
|
||||
accounts.push(this.user(n.reply.user));
|
||||
}
|
||||
return {
|
||||
id: n.id,
|
||||
accounts: accounts,
|
||||
last_status: this.note(n, host),
|
||||
unread: false,
|
||||
};
|
||||
};
|
||||
|
||||
list = (l: Entity.List): MegalodonEntity.List => ({
|
||||
id: l.id,
|
||||
title: l.name,
|
||||
});
|
||||
|
||||
encodeNotificationType = (
|
||||
e: MegalodonEntity.NotificationType,
|
||||
): MisskeyEntity.NotificationType => {
|
||||
switch (e) {
|
||||
case NotificationType.Follow:
|
||||
return MisskeyNotificationType.Follow;
|
||||
case NotificationType.Mention:
|
||||
return MisskeyNotificationType.Reply;
|
||||
case NotificationType.Favourite:
|
||||
case NotificationType.Reaction:
|
||||
return MisskeyNotificationType.Reaction;
|
||||
case NotificationType.Reblog:
|
||||
return MisskeyNotificationType.Renote;
|
||||
case NotificationType.Poll:
|
||||
return MisskeyNotificationType.PollEnded;
|
||||
case NotificationType.FollowRequest:
|
||||
return MisskeyNotificationType.ReceiveFollowRequest;
|
||||
default:
|
||||
return e;
|
||||
}
|
||||
};
|
||||
|
||||
decodeNotificationType = (
|
||||
e: MisskeyEntity.NotificationType,
|
||||
): MegalodonEntity.NotificationType => {
|
||||
switch (e) {
|
||||
case MisskeyNotificationType.Follow:
|
||||
return NotificationType.Follow;
|
||||
case MisskeyNotificationType.Mention:
|
||||
case MisskeyNotificationType.Reply:
|
||||
return NotificationType.Mention;
|
||||
case MisskeyNotificationType.Renote:
|
||||
case MisskeyNotificationType.Quote:
|
||||
return NotificationType.Reblog;
|
||||
case MisskeyNotificationType.Reaction:
|
||||
return NotificationType.Reaction;
|
||||
case MisskeyNotificationType.PollEnded:
|
||||
return NotificationType.Poll;
|
||||
case MisskeyNotificationType.ReceiveFollowRequest:
|
||||
return NotificationType.FollowRequest;
|
||||
case MisskeyNotificationType.FollowRequestAccepted:
|
||||
return NotificationType.Follow;
|
||||
default:
|
||||
return e;
|
||||
}
|
||||
};
|
||||
|
||||
announcement = (a: Entity.Announcement): MegalodonEntity.Announcement => ({
|
||||
id: a.id,
|
||||
content: `<h1>${this.escapeMFM(a.title)}</h1>${this.escapeMFM(a.text)}`,
|
||||
starts_at: null,
|
||||
ends_at: null,
|
||||
published: true,
|
||||
all_day: false,
|
||||
published_at: a.createdAt,
|
||||
updated_at: a.updatedAt,
|
||||
read: a.isRead,
|
||||
mentions: [],
|
||||
statuses: [],
|
||||
tags: [],
|
||||
emojis: [],
|
||||
reactions: [],
|
||||
});
|
||||
|
||||
notification = (
|
||||
n: Entity.Notification,
|
||||
host: string,
|
||||
): MegalodonEntity.Notification => {
|
||||
let notification = {
|
||||
id: n.id,
|
||||
account: n.user ? this.user(n.user) : this.modelOfAcct,
|
||||
created_at: n.createdAt,
|
||||
type: this.decodeNotificationType(n.type),
|
||||
};
|
||||
if (n.note) {
|
||||
notification = Object.assign(notification, {
|
||||
status: this.note(n.note, host),
|
||||
});
|
||||
if (notification.type === NotificationType.Poll) {
|
||||
notification = Object.assign(notification, {
|
||||
account: this.note(n.note, host).account,
|
||||
});
|
||||
}
|
||||
if (n.reaction) {
|
||||
notification = Object.assign(notification, {
|
||||
reaction: this.mapReactions(n.note.emojis, { [n.reaction]: 1 })[0],
|
||||
});
|
||||
}
|
||||
}
|
||||
return notification;
|
||||
};
|
||||
|
||||
stats = (s: Entity.Stats): MegalodonEntity.Stats => {
|
||||
return {
|
||||
user_count: s.usersCount,
|
||||
status_count: s.notesCount,
|
||||
domain_count: s.instances,
|
||||
};
|
||||
};
|
||||
|
||||
meta = (m: Entity.Meta, s: Entity.Stats): MegalodonEntity.Instance => {
|
||||
const wss = m.uri.replace(/^https:\/\//, "wss://");
|
||||
return {
|
||||
uri: m.uri,
|
||||
title: m.name,
|
||||
description: m.description,
|
||||
email: m.maintainerEmail,
|
||||
version: m.version,
|
||||
thumbnail: m.bannerUrl,
|
||||
urls: {
|
||||
streaming_api: `${wss}/streaming`,
|
||||
},
|
||||
stats: this.stats(s),
|
||||
languages: m.langs,
|
||||
contact_account: null,
|
||||
max_toot_chars: m.maxNoteTextLength,
|
||||
registrations: !m.disableRegistration,
|
||||
};
|
||||
};
|
||||
|
||||
hashtag = (h: Entity.Hashtag): MegalodonEntity.Tag => {
|
||||
return {
|
||||
name: h.tag,
|
||||
url: h.tag,
|
||||
history: null,
|
||||
following: false,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export const DEFAULT_SCOPE = [
|
||||
"read:account",
|
||||
"write:account",
|
||||
"read:blocks",
|
||||
"write:blocks",
|
||||
"read:drive",
|
||||
"write:drive",
|
||||
"read:favorites",
|
||||
"write:favorites",
|
||||
"read:following",
|
||||
"write:following",
|
||||
"read:mutes",
|
||||
"write:mutes",
|
||||
"write:notes",
|
||||
"read:notifications",
|
||||
"write:notifications",
|
||||
"read:reactions",
|
||||
"write:reactions",
|
||||
"write:votes",
|
||||
];
|
||||
|
||||
/**
|
||||
* Interface
|
||||
*/
|
||||
export interface Interface {
|
||||
post<T = any>(
|
||||
path: string,
|
||||
params?: any,
|
||||
headers?: { [key: string]: string },
|
||||
): Promise<Response<T>>;
|
||||
cancel(): void;
|
||||
socket(
|
||||
channel:
|
||||
| "user"
|
||||
| "localTimeline"
|
||||
| "hybridTimeline"
|
||||
| "globalTimeline"
|
||||
| "conversation"
|
||||
| "list",
|
||||
listId?: string,
|
||||
): WebSocket;
|
||||
}
|
||||
|
||||
/**
|
||||
* Misskey API client.
|
||||
*
|
||||
* Usign axios for request, you will handle promises.
|
||||
*/
|
||||
export class Client implements Interface {
|
||||
private accessToken: string | null;
|
||||
private baseUrl: string;
|
||||
private userAgent: string;
|
||||
private abortController: AbortController;
|
||||
private proxyConfig: ProxyConfig | false = false;
|
||||
private converter: Converter;
|
||||
|
||||
/**
|
||||
* @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.
|
||||
* @param converter Converter instance.
|
||||
*/
|
||||
constructor(
|
||||
baseUrl: string,
|
||||
accessToken: string | null,
|
||||
userAgent: string = DEFAULT_UA,
|
||||
proxyConfig: ProxyConfig | false = false,
|
||||
converter: Converter,
|
||||
) {
|
||||
this.accessToken = accessToken;
|
||||
this.baseUrl = baseUrl;
|
||||
this.userAgent = userAgent;
|
||||
this.proxyConfig = proxyConfig;
|
||||
this.abortController = new AbortController();
|
||||
this.converter = converter;
|
||||
axios.defaults.signal = this.abortController.signal;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: any = {},
|
||||
headers: { [key: string]: string } = {},
|
||||
): Promise<Response<T>> {
|
||||
let options: AxiosRequestConfig = {
|
||||
headers: headers,
|
||||
maxContentLength: Infinity,
|
||||
maxBodyLength: Infinity,
|
||||
};
|
||||
if (this.proxyConfig) {
|
||||
options = Object.assign(options, {
|
||||
httpAgent: proxyAgent(this.proxyConfig),
|
||||
httpsAgent: proxyAgent(this.proxyConfig),
|
||||
});
|
||||
}
|
||||
let bodyParams = params;
|
||||
if (this.accessToken) {
|
||||
if (params instanceof FormData) {
|
||||
bodyParams.append("i", this.accessToken);
|
||||
} else {
|
||||
bodyParams = Object.assign(params, {
|
||||
i: this.accessToken,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return axios
|
||||
.post<T>(this.baseUrl + path, bodyParams, options)
|
||||
.then((resp: AxiosResponse<T>) => {
|
||||
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 Misskey API.
|
||||
*
|
||||
* @param channel Channel name is user, localTimeline, hybridTimeline, globalTimeline, conversation or list.
|
||||
* @param listId This parameter is required only list channel.
|
||||
*/
|
||||
public socket(
|
||||
channel:
|
||||
| "user"
|
||||
| "localTimeline"
|
||||
| "hybridTimeline"
|
||||
| "globalTimeline"
|
||||
| "conversation"
|
||||
| "list",
|
||||
listId?: string,
|
||||
): WebSocket {
|
||||
if (!this.accessToken) {
|
||||
throw new Error("accessToken is required");
|
||||
}
|
||||
const url = `${this.baseUrl}/streaming`;
|
||||
const streaming = new WebSocket(
|
||||
url,
|
||||
channel,
|
||||
this.accessToken,
|
||||
listId,
|
||||
this.userAgent,
|
||||
this.proxyConfig,
|
||||
this.converter,
|
||||
);
|
||||
process.nextTick(() => {
|
||||
streaming.start();
|
||||
});
|
||||
return streaming;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default MisskeyAPI;
|
6
packages/megalodon/src/misskey/entities/GetAll.ts
Normal file
6
packages/megalodon/src/misskey/entities/GetAll.ts
Normal file
@ -0,0 +1,6 @@
|
||||
namespace MisskeyEntity {
|
||||
export type GetAll = {
|
||||
tutorial: number;
|
||||
defaultNoteVisibility: "public" | "home" | "followers" | "specified";
|
||||
};
|
||||
}
|
10
packages/megalodon/src/misskey/entities/announcement.ts
Normal file
10
packages/megalodon/src/misskey/entities/announcement.ts
Normal file
@ -0,0 +1,10 @@
|
||||
namespace MisskeyEntity {
|
||||
export type Announcement = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
text: string;
|
||||
title: string;
|
||||
isRead?: boolean;
|
||||
};
|
||||
}
|
9
packages/megalodon/src/misskey/entities/app.ts
Normal file
9
packages/megalodon/src/misskey/entities/app.ts
Normal file
@ -0,0 +1,9 @@
|
||||
namespace MisskeyEntity {
|
||||
export type App = {
|
||||
id: string;
|
||||
name: string;
|
||||
callbackUrl: string;
|
||||
permission: Array<string>;
|
||||
secret: string;
|
||||
};
|
||||
}
|
10
packages/megalodon/src/misskey/entities/blocking.ts
Normal file
10
packages/megalodon/src/misskey/entities/blocking.ts
Normal file
@ -0,0 +1,10 @@
|
||||
/// <reference path="userDetail.ts" />
|
||||
|
||||
namespace MisskeyEntity {
|
||||
export type Blocking = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
blockeeId: string;
|
||||
blockee: UserDetail;
|
||||
};
|
||||
}
|
7
packages/megalodon/src/misskey/entities/createdNote.ts
Normal file
7
packages/megalodon/src/misskey/entities/createdNote.ts
Normal file
@ -0,0 +1,7 @@
|
||||
/// <reference path="note.ts" />
|
||||
|
||||
namespace MisskeyEntity {
|
||||
export type CreatedNote = {
|
||||
createdNote: Note;
|
||||
};
|
||||
}
|
9
packages/megalodon/src/misskey/entities/emoji.ts
Normal file
9
packages/megalodon/src/misskey/entities/emoji.ts
Normal file
@ -0,0 +1,9 @@
|
||||
namespace MisskeyEntity {
|
||||
export type Emoji = {
|
||||
name: string;
|
||||
host: string | null;
|
||||
url: string;
|
||||
aliases: Array<string>;
|
||||
category: string;
|
||||
};
|
||||
}
|
10
packages/megalodon/src/misskey/entities/favorite.ts
Normal file
10
packages/megalodon/src/misskey/entities/favorite.ts
Normal file
@ -0,0 +1,10 @@
|
||||
/// <reference path="note.ts" />
|
||||
|
||||
namespace MisskeyEntity {
|
||||
export type Favorite = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
noteId: string;
|
||||
note: Note;
|
||||
};
|
||||
}
|
7
packages/megalodon/src/misskey/entities/field.ts
Normal file
7
packages/megalodon/src/misskey/entities/field.ts
Normal file
@ -0,0 +1,7 @@
|
||||
namespace MisskeyEntity {
|
||||
export type Field = {
|
||||
name: string;
|
||||
value: string;
|
||||
verified?: string;
|
||||
};
|
||||
}
|
20
packages/megalodon/src/misskey/entities/file.ts
Normal file
20
packages/megalodon/src/misskey/entities/file.ts
Normal file
@ -0,0 +1,20 @@
|
||||
namespace MisskeyEntity {
|
||||
export type File = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
name: string;
|
||||
type: string;
|
||||
md5: string;
|
||||
size: number;
|
||||
isSensitive: boolean;
|
||||
properties: {
|
||||
width: number;
|
||||
height: number;
|
||||
avgColor: string;
|
||||
};
|
||||
url: string;
|
||||
thumbnailUrl: string;
|
||||
comment: string;
|
||||
blurhash: string;
|
||||
};
|
||||
}
|
9
packages/megalodon/src/misskey/entities/followRequest.ts
Normal file
9
packages/megalodon/src/misskey/entities/followRequest.ts
Normal file
@ -0,0 +1,9 @@
|
||||
/// <reference path="user.ts" />
|
||||
|
||||
namespace MisskeyEntity {
|
||||
export type FollowRequest = {
|
||||
id: string;
|
||||
follower: User;
|
||||
followee: User;
|
||||
};
|
||||
}
|
11
packages/megalodon/src/misskey/entities/follower.ts
Normal file
11
packages/megalodon/src/misskey/entities/follower.ts
Normal file
@ -0,0 +1,11 @@
|
||||
/// <reference path="userDetail.ts" />
|
||||
|
||||
namespace MisskeyEntity {
|
||||
export type Follower = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
followeeId: string;
|
||||
followerId: string;
|
||||
follower: UserDetail;
|
||||
};
|
||||
}
|
11
packages/megalodon/src/misskey/entities/following.ts
Normal file
11
packages/megalodon/src/misskey/entities/following.ts
Normal file
@ -0,0 +1,11 @@
|
||||
/// <reference path="userDetail.ts" />
|
||||
|
||||
namespace MisskeyEntity {
|
||||
export type Following = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
followeeId: string;
|
||||
followerId: string;
|
||||
followee: UserDetail;
|
||||
};
|
||||
}
|
7
packages/megalodon/src/misskey/entities/hashtag.ts
Normal file
7
packages/megalodon/src/misskey/entities/hashtag.ts
Normal file
@ -0,0 +1,7 @@
|
||||
namespace MisskeyEntity {
|
||||
export type Hashtag = {
|
||||
tag: string;
|
||||
chart: Array<number>;
|
||||
usersCount: number;
|
||||
};
|
||||
}
|
8
packages/megalodon/src/misskey/entities/list.ts
Normal file
8
packages/megalodon/src/misskey/entities/list.ts
Normal file
@ -0,0 +1,8 @@
|
||||
namespace MisskeyEntity {
|
||||
export type List = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
name: string;
|
||||
userIds: Array<string>;
|
||||
};
|
||||
}
|
18
packages/megalodon/src/misskey/entities/meta.ts
Normal file
18
packages/megalodon/src/misskey/entities/meta.ts
Normal file
@ -0,0 +1,18 @@
|
||||
/// <reference path="emoji.ts" />
|
||||
|
||||
namespace MisskeyEntity {
|
||||
export type Meta = {
|
||||
maintainerName: string;
|
||||
maintainerEmail: string;
|
||||
name: string;
|
||||
version: string;
|
||||
uri: string;
|
||||
description: string;
|
||||
langs: Array<string>;
|
||||
disableRegistration: boolean;
|
||||
disableLocalTimeline: boolean;
|
||||
bannerUrl: string;
|
||||
maxNoteTextLength: 3000;
|
||||
emojis: Array<Emoji>;
|
||||
};
|
||||
}
|
10
packages/megalodon/src/misskey/entities/mute.ts
Normal file
10
packages/megalodon/src/misskey/entities/mute.ts
Normal file
@ -0,0 +1,10 @@
|
||||
/// <reference path="userDetail.ts" />
|
||||
|
||||
namespace MisskeyEntity {
|
||||
export type Mute = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
muteeId: string;
|
||||
mutee: UserDetail;
|
||||
};
|
||||
}
|
32
packages/megalodon/src/misskey/entities/note.ts
Normal file
32
packages/megalodon/src/misskey/entities/note.ts
Normal file
@ -0,0 +1,32 @@
|
||||
/// <reference path="user.ts" />
|
||||
/// <reference path="emoji.ts" />
|
||||
/// <reference path="file.ts" />
|
||||
/// <reference path="poll.ts" />
|
||||
|
||||
namespace MisskeyEntity {
|
||||
export type Note = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
userId: string;
|
||||
user: User;
|
||||
text: string | null;
|
||||
cw: string | null;
|
||||
visibility: "public" | "home" | "followers" | "specified";
|
||||
renoteCount: number;
|
||||
repliesCount: number;
|
||||
reactions: { [key: string]: number };
|
||||
emojis: Array<Emoji>;
|
||||
fileIds: Array<string>;
|
||||
files: Array<File>;
|
||||
replyId: string | null;
|
||||
renoteId: string | null;
|
||||
uri?: string;
|
||||
reply?: Note;
|
||||
renote?: Note;
|
||||
viaMobile?: boolean;
|
||||
tags?: Array<string>;
|
||||
poll?: Poll;
|
||||
mentions?: Array<string>;
|
||||
myReaction?: string;
|
||||
};
|
||||
}
|
17
packages/megalodon/src/misskey/entities/notification.ts
Normal file
17
packages/megalodon/src/misskey/entities/notification.ts
Normal file
@ -0,0 +1,17 @@
|
||||
/// <reference path="user.ts" />
|
||||
/// <reference path="note.ts" />
|
||||
|
||||
namespace MisskeyEntity {
|
||||
export type Notification = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
// https://github.com/syuilo/misskey/blob/056942391aee135eb6c77aaa63f6ed5741d701a6/src/models/entities/notification.ts#L50-L62
|
||||
type: NotificationType;
|
||||
userId: string;
|
||||
user: User;
|
||||
note?: Note;
|
||||
reaction?: string;
|
||||
};
|
||||
|
||||
export type NotificationType = string;
|
||||
}
|
13
packages/megalodon/src/misskey/entities/poll.ts
Normal file
13
packages/megalodon/src/misskey/entities/poll.ts
Normal file
@ -0,0 +1,13 @@
|
||||
namespace MisskeyEntity {
|
||||
export type Choice = {
|
||||
text: string;
|
||||
votes: number;
|
||||
isVoted: boolean;
|
||||
};
|
||||
|
||||
export type Poll = {
|
||||
multiple: boolean;
|
||||
expiresAt: string;
|
||||
choices: Array<Choice>;
|
||||
};
|
||||
}
|
11
packages/megalodon/src/misskey/entities/reaction.ts
Normal file
11
packages/megalodon/src/misskey/entities/reaction.ts
Normal file
@ -0,0 +1,11 @@
|
||||
/// <reference path="user.ts" />
|
||||
|
||||
namespace MisskeyEntity {
|
||||
export type Reaction = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
user: User;
|
||||
url?: string;
|
||||
type: string;
|
||||
};
|
||||
}
|
12
packages/megalodon/src/misskey/entities/relation.ts
Normal file
12
packages/megalodon/src/misskey/entities/relation.ts
Normal file
@ -0,0 +1,12 @@
|
||||
namespace MisskeyEntity {
|
||||
export type Relation = {
|
||||
id: string;
|
||||
isFollowing: boolean;
|
||||
hasPendingFollowRequestFromYou: boolean;
|
||||
hasPendingFollowRequestToYou: boolean;
|
||||
isFollowed: boolean;
|
||||
isBlocking: boolean;
|
||||
isBlocked: boolean;
|
||||
isMuted: boolean;
|
||||
};
|
||||
}
|
6
packages/megalodon/src/misskey/entities/session.ts
Normal file
6
packages/megalodon/src/misskey/entities/session.ts
Normal file
@ -0,0 +1,6 @@
|
||||
namespace MisskeyEntity {
|
||||
export type Session = {
|
||||
token: string;
|
||||
url: string;
|
||||
};
|
||||
}
|
7
packages/megalodon/src/misskey/entities/state.ts
Normal file
7
packages/megalodon/src/misskey/entities/state.ts
Normal file
@ -0,0 +1,7 @@
|
||||
namespace MisskeyEntity {
|
||||
export type State = {
|
||||
isFavorited: boolean;
|
||||
isMutedThread: boolean;
|
||||
isWatching: boolean;
|
||||
};
|
||||
}
|
9
packages/megalodon/src/misskey/entities/stats.ts
Normal file
9
packages/megalodon/src/misskey/entities/stats.ts
Normal file
@ -0,0 +1,9 @@
|
||||
namespace MisskeyEntity {
|
||||
export type Stats = {
|
||||
notesCount: number;
|
||||
originalNotesCount: number;
|
||||
usersCount: number;
|
||||
originalUsersCount: number;
|
||||
instances: number;
|
||||
};
|
||||
}
|
13
packages/megalodon/src/misskey/entities/user.ts
Normal file
13
packages/megalodon/src/misskey/entities/user.ts
Normal file
@ -0,0 +1,13 @@
|
||||
/// <reference path="emoji.ts" />
|
||||
|
||||
namespace MisskeyEntity {
|
||||
export type User = {
|
||||
id: string;
|
||||
name: string;
|
||||
username: string;
|
||||
host: string | null;
|
||||
avatarUrl: string;
|
||||
avatarColor: string;
|
||||
emojis: Array<Emoji>;
|
||||
};
|
||||
}
|
34
packages/megalodon/src/misskey/entities/userDetail.ts
Normal file
34
packages/megalodon/src/misskey/entities/userDetail.ts
Normal file
@ -0,0 +1,34 @@
|
||||
/// <reference path="emoji.ts" />
|
||||
/// <reference path="field.ts" />
|
||||
/// <reference path="note.ts" />
|
||||
|
||||
namespace MisskeyEntity {
|
||||
export type UserDetail = {
|
||||
id: string;
|
||||
name: string;
|
||||
username: string;
|
||||
host: string | null;
|
||||
avatarUrl: string;
|
||||
avatarColor: string;
|
||||
isAdmin: boolean;
|
||||
isModerator: boolean;
|
||||
isBot: boolean;
|
||||
isCat: boolean;
|
||||
emojis: Array<Emoji>;
|
||||
createdAt: string;
|
||||
bannerUrl: string;
|
||||
bannerColor: string;
|
||||
isLocked: boolean;
|
||||
isSilenced: boolean;
|
||||
isSuspended: boolean;
|
||||
description: string;
|
||||
followersCount: number;
|
||||
followingCount: number;
|
||||
notesCount: number;
|
||||
avatarId: string;
|
||||
bannerId: string;
|
||||
pinnedNoteIds?: Array<string>;
|
||||
pinnedNotes?: Array<Note>;
|
||||
fields: Array<Field>;
|
||||
};
|
||||
}
|
36
packages/megalodon/src/misskey/entities/userDetailMe.ts
Normal file
36
packages/megalodon/src/misskey/entities/userDetailMe.ts
Normal file
@ -0,0 +1,36 @@
|
||||
/// <reference path="emoji.ts" />
|
||||
/// <reference path="field.ts" />
|
||||
/// <reference path="note.ts" />
|
||||
|
||||
namespace MisskeyEntity {
|
||||
export type UserDetailMe = {
|
||||
id: string;
|
||||
name: string;
|
||||
username: string;
|
||||
host: string | null;
|
||||
avatarUrl: string;
|
||||
avatarColor: string;
|
||||
isAdmin: boolean;
|
||||
isModerator: boolean;
|
||||
isBot: boolean;
|
||||
isCat: boolean;
|
||||
emojis: Array<Emoji>;
|
||||
createdAt: string;
|
||||
bannerUrl: string;
|
||||
bannerColor: string;
|
||||
isLocked: boolean;
|
||||
isSilenced: boolean;
|
||||
isSuspended: boolean;
|
||||
description: string;
|
||||
followersCount: number;
|
||||
followingCount: number;
|
||||
notesCount: number;
|
||||
avatarId: string;
|
||||
bannerId: string;
|
||||
pinnedNoteIds?: Array<string>;
|
||||
pinnedNotes?: Array<Note>;
|
||||
fields: Array<Field>;
|
||||
alwaysMarkNsfw: boolean;
|
||||
lang: string | null;
|
||||
};
|
||||
}
|
8
packages/megalodon/src/misskey/entities/userkey.ts
Normal file
8
packages/megalodon/src/misskey/entities/userkey.ts
Normal file
@ -0,0 +1,8 @@
|
||||
/// <reference path="user.ts" />
|
||||
|
||||
namespace MisskeyEntity {
|
||||
export type UserKey = {
|
||||
accessToken: string;
|
||||
user: User;
|
||||
};
|
||||
}
|
28
packages/megalodon/src/misskey/entity.ts
Normal file
28
packages/megalodon/src/misskey/entity.ts
Normal file
@ -0,0 +1,28 @@
|
||||
/// <reference path="entities/app.ts" />
|
||||
/// <reference path="entities/announcement.ts" />
|
||||
/// <reference path="entities/blocking.ts" />
|
||||
/// <reference path="entities/createdNote.ts" />
|
||||
/// <reference path="entities/emoji.ts" />
|
||||
/// <reference path="entities/favorite.ts" />
|
||||
/// <reference path="entities/field.ts" />
|
||||
/// <reference path="entities/file.ts" />
|
||||
/// <reference path="entities/follower.ts" />
|
||||
/// <reference path="entities/following.ts" />
|
||||
/// <reference path="entities/followRequest.ts" />
|
||||
/// <reference path="entities/hashtag.ts" />
|
||||
/// <reference path="entities/list.ts" />
|
||||
/// <reference path="entities/meta.ts" />
|
||||
/// <reference path="entities/mute.ts" />
|
||||
/// <reference path="entities/note.ts" />
|
||||
/// <reference path="entities/notification.ts" />
|
||||
/// <reference path="entities/poll.ts" />
|
||||
/// <reference path="entities/reaction.ts" />
|
||||
/// <reference path="entities/relation.ts" />
|
||||
/// <reference path="entities/user.ts" />
|
||||
/// <reference path="entities/userDetail.ts" />
|
||||
/// <reference path="entities/userDetailMe.ts" />
|
||||
/// <reference path="entities/userkey.ts" />
|
||||
/// <reference path="entities/session.ts" />
|
||||
/// <reference path="entities/stats.ts" />
|
||||
|
||||
export default MisskeyEntity;
|
18
packages/megalodon/src/misskey/notification.ts
Normal file
18
packages/megalodon/src/misskey/notification.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import MisskeyEntity from "./entity";
|
||||
|
||||
namespace MisskeyNotificationType {
|
||||
export const Follow: MisskeyEntity.NotificationType = "follow";
|
||||
export const Mention: MisskeyEntity.NotificationType = "mention";
|
||||
export const Reply: MisskeyEntity.NotificationType = "reply";
|
||||
export const Renote: MisskeyEntity.NotificationType = "renote";
|
||||
export const Quote: MisskeyEntity.NotificationType = "quote";
|
||||
export const Reaction: MisskeyEntity.NotificationType = "favourite";
|
||||
export const PollEnded: MisskeyEntity.NotificationType = "pollEnded";
|
||||
export const ReceiveFollowRequest: MisskeyEntity.NotificationType =
|
||||
"receiveFollowRequest";
|
||||
export const FollowRequestAccepted: MisskeyEntity.NotificationType =
|
||||
"followRequestAccepted";
|
||||
export const GroupInvited: MisskeyEntity.NotificationType = "groupInvited";
|
||||
}
|
||||
|
||||
export default MisskeyNotificationType;
|
458
packages/megalodon/src/misskey/web_socket.ts
Normal file
458
packages/megalodon/src/misskey/web_socket.ts
Normal file
@ -0,0 +1,458 @@
|
||||
import WS from "ws";
|
||||
import dayjs, { Dayjs } from "dayjs";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { EventEmitter } from "events";
|
||||
import { WebSocketInterface } from "../megalodon";
|
||||
import proxyAgent, { ProxyConfig } from "../proxy_config";
|
||||
import MisskeyAPI from "./api_client";
|
||||
|
||||
/**
|
||||
* WebSocket
|
||||
* Misskey is not support http streaming. It supports websocket instead of streaming.
|
||||
* So this class connect to Misskey server with WebSocket.
|
||||
*/
|
||||
export default class WebSocket
|
||||
extends EventEmitter
|
||||
implements WebSocketInterface
|
||||
{
|
||||
public url: string;
|
||||
public channel:
|
||||
| "user"
|
||||
| "localTimeline"
|
||||
| "hybridTimeline"
|
||||
| "globalTimeline"
|
||||
| "conversation"
|
||||
| "list";
|
||||
public parser: any;
|
||||
public headers: { [key: string]: string };
|
||||
public proxyConfig: ProxyConfig | false = false;
|
||||
public listId: string | null = null;
|
||||
private _converter: MisskeyAPI.Converter;
|
||||
private _accessToken: string;
|
||||
private _reconnectInterval: number;
|
||||
private _reconnectMaxAttempts: number;
|
||||
private _reconnectCurrentAttempts: number;
|
||||
private _connectionClosed: boolean;
|
||||
private _client: WS | null = null;
|
||||
private _channelID: string;
|
||||
private _pongReceivedTimestamp: Dayjs;
|
||||
private _heartbeatInterval = 60000;
|
||||
private _pongWaiting = false;
|
||||
|
||||
/**
|
||||
* @param url Full url of websocket: e.g. wss://firefish.io/streaming
|
||||
* @param channel Channel name is user, localTimeline, hybridTimeline, globalTimeline, conversation or list.
|
||||
* @param accessToken The access token.
|
||||
* @param listId This parameter is required when you specify list as channel.
|
||||
*/
|
||||
constructor(
|
||||
url: string,
|
||||
channel:
|
||||
| "user"
|
||||
| "localTimeline"
|
||||
| "hybridTimeline"
|
||||
| "globalTimeline"
|
||||
| "conversation"
|
||||
| "list",
|
||||
accessToken: string,
|
||||
listId: string | undefined,
|
||||
userAgent: string,
|
||||
proxyConfig: ProxyConfig | false = false,
|
||||
converter: MisskeyAPI.Converter,
|
||||
) {
|
||||
super();
|
||||
this.url = url;
|
||||
this.parser = new Parser();
|
||||
this.channel = channel;
|
||||
this.headers = {
|
||||
"User-Agent": userAgent,
|
||||
};
|
||||
if (listId === undefined) {
|
||||
this.listId = null;
|
||||
} else {
|
||||
this.listId = listId;
|
||||
}
|
||||
this.proxyConfig = proxyConfig;
|
||||
this._accessToken = accessToken;
|
||||
this._reconnectInterval = 10000;
|
||||
this._reconnectMaxAttempts = Infinity;
|
||||
this._reconnectCurrentAttempts = 0;
|
||||
this._connectionClosed = false;
|
||||
this._channelID = uuid();
|
||||
this._pongReceivedTimestamp = dayjs();
|
||||
this._converter = converter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start websocket connection.
|
||||
*/
|
||||
public start() {
|
||||
this._connectionClosed = false;
|
||||
this._resetRetryParams();
|
||||
this._startWebSocketConnection();
|
||||
}
|
||||
|
||||
private baseUrlToHost(baseUrl: string): string {
|
||||
return baseUrl.replace("https://", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset connection and start new websocket connection.
|
||||
*/
|
||||
private _startWebSocketConnection() {
|
||||
this._resetConnection();
|
||||
this._setupParser();
|
||||
this._client = this._connect();
|
||||
this._bindSocket(this._client);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop current connection.
|
||||
*/
|
||||
public stop() {
|
||||
this._connectionClosed = true;
|
||||
this._resetConnection();
|
||||
this._resetRetryParams();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up current connection, and listeners.
|
||||
*/
|
||||
private _resetConnection() {
|
||||
if (this._client) {
|
||||
this._client.close(1000);
|
||||
this._client.removeAllListeners();
|
||||
this._client = null;
|
||||
}
|
||||
|
||||
if (this.parser) {
|
||||
this.parser.removeAllListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the parameters used in reconnect.
|
||||
*/
|
||||
private _resetRetryParams() {
|
||||
this._reconnectCurrentAttempts = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the endpoint.
|
||||
*/
|
||||
private _connect(): WS {
|
||||
let options: WS.ClientOptions = {
|
||||
headers: this.headers,
|
||||
};
|
||||
if (this.proxyConfig) {
|
||||
options = Object.assign(options, {
|
||||
agent: proxyAgent(this.proxyConfig),
|
||||
});
|
||||
}
|
||||
const cli: WS = new WS(`${this.url}?i=${this._accessToken}`, options);
|
||||
return cli;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect specified channels in websocket.
|
||||
*/
|
||||
private _channel() {
|
||||
if (!this._client) {
|
||||
return;
|
||||
}
|
||||
switch (this.channel) {
|
||||
case "conversation":
|
||||
this._client.send(
|
||||
JSON.stringify({
|
||||
type: "connect",
|
||||
body: {
|
||||
channel: "main",
|
||||
id: this._channelID,
|
||||
},
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case "user":
|
||||
this._client.send(
|
||||
JSON.stringify({
|
||||
type: "connect",
|
||||
body: {
|
||||
channel: "main",
|
||||
id: this._channelID,
|
||||
},
|
||||
}),
|
||||
);
|
||||
this._client.send(
|
||||
JSON.stringify({
|
||||
type: "connect",
|
||||
body: {
|
||||
channel: "homeTimeline",
|
||||
id: this._channelID,
|
||||
},
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case "list":
|
||||
this._client.send(
|
||||
JSON.stringify({
|
||||
type: "connect",
|
||||
body: {
|
||||
channel: "userList",
|
||||
id: this._channelID,
|
||||
params: {
|
||||
listId: this.listId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
this._client.send(
|
||||
JSON.stringify({
|
||||
type: "connect",
|
||||
body: {
|
||||
channel: this.channel,
|
||||
id: this._channelID,
|
||||
},
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconnects to the same endpoint.
|
||||
*/
|
||||
|
||||
private _reconnect() {
|
||||
setTimeout(() => {
|
||||
// Skip reconnect when client is connecting.
|
||||
// https://github.com/websockets/ws/blob/7.2.1/lib/websocket.js#L365
|
||||
if (this._client && this._client.readyState === WS.CONNECTING) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._reconnectCurrentAttempts < this._reconnectMaxAttempts) {
|
||||
this._reconnectCurrentAttempts++;
|
||||
this._clearBinding();
|
||||
if (this._client) {
|
||||
// In reconnect, we want to close the connection immediately,
|
||||
// because recoonect is necessary when some problems occur.
|
||||
this._client.terminate();
|
||||
}
|
||||
// Call connect methods
|
||||
console.log("Reconnecting");
|
||||
this._client = this._connect();
|
||||
this._bindSocket(this._client);
|
||||
}
|
||||
}, this._reconnectInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear binding event for websocket client.
|
||||
*/
|
||||
private _clearBinding() {
|
||||
if (this._client) {
|
||||
this._client.removeAllListeners("close");
|
||||
this._client.removeAllListeners("pong");
|
||||
this._client.removeAllListeners("open");
|
||||
this._client.removeAllListeners("message");
|
||||
this._client.removeAllListeners("error");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind event for web socket client.
|
||||
* @param client A WebSocket instance.
|
||||
*/
|
||||
private _bindSocket(client: WS) {
|
||||
client.on("close", (code: number, _reason: Buffer) => {
|
||||
if (code === 1000) {
|
||||
this.emit("close", {});
|
||||
} else {
|
||||
console.log(`Closed connection with ${code}`);
|
||||
if (!this._connectionClosed) {
|
||||
this._reconnect();
|
||||
}
|
||||
}
|
||||
});
|
||||
client.on("pong", () => {
|
||||
this._pongWaiting = false;
|
||||
this.emit("pong", {});
|
||||
this._pongReceivedTimestamp = dayjs();
|
||||
// It is required to anonymous function since get this scope in checkAlive.
|
||||
setTimeout(
|
||||
() => this._checkAlive(this._pongReceivedTimestamp),
|
||||
this._heartbeatInterval,
|
||||
);
|
||||
});
|
||||
client.on("open", () => {
|
||||
this.emit("connect", {});
|
||||
this._channel();
|
||||
// Call first ping event.
|
||||
setTimeout(() => {
|
||||
client.ping("");
|
||||
}, 10000);
|
||||
});
|
||||
client.on("message", (data: WS.Data, isBinary: boolean) => {
|
||||
this.parser.parse(data, isBinary, this._channelID);
|
||||
});
|
||||
client.on("error", (err: Error) => {
|
||||
this.emit("error", err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up parser when receive message.
|
||||
*/
|
||||
private _setupParser() {
|
||||
this.parser.on("update", (note: MisskeyAPI.Entity.Note) => {
|
||||
this.emit(
|
||||
"update",
|
||||
this._converter.note(note, this.baseUrlToHost(this.url)),
|
||||
);
|
||||
});
|
||||
this.parser.on(
|
||||
"notification",
|
||||
(notification: MisskeyAPI.Entity.Notification) => {
|
||||
this.emit(
|
||||
"notification",
|
||||
this._converter.notification(
|
||||
notification,
|
||||
this.baseUrlToHost(this.url),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
this.parser.on("conversation", (note: MisskeyAPI.Entity.Note) => {
|
||||
this.emit(
|
||||
"conversation",
|
||||
this._converter.noteToConversation(note, this.baseUrlToHost(this.url)),
|
||||
);
|
||||
});
|
||||
this.parser.on("error", (err: Error) => {
|
||||
this.emit("parser-error", err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Call ping and wait to pong.
|
||||
*/
|
||||
private _checkAlive(timestamp: Dayjs) {
|
||||
const now: Dayjs = dayjs();
|
||||
// Block multiple calling, if multiple pong event occur.
|
||||
// It the duration is less than interval, through ping.
|
||||
if (
|
||||
now.diff(timestamp) > this._heartbeatInterval - 1000 &&
|
||||
!this._connectionClosed
|
||||
) {
|
||||
// Skip ping when client is connecting.
|
||||
// https://github.com/websockets/ws/blob/7.2.1/lib/websocket.js#L289
|
||||
if (this._client && this._client.readyState !== WS.CONNECTING) {
|
||||
this._pongWaiting = true;
|
||||
this._client.ping("");
|
||||
setTimeout(() => {
|
||||
if (this._pongWaiting) {
|
||||
this._pongWaiting = false;
|
||||
this._reconnect();
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parser
|
||||
* This class provides parser for websocket message.
|
||||
*/
|
||||
export class Parser extends EventEmitter {
|
||||
/**
|
||||
* @param message Message body of websocket.
|
||||
* @param channelID Parse only messages which has same channelID.
|
||||
*/
|
||||
public parse(data: WS.Data, isBinary: boolean, channelID: string) {
|
||||
const message = isBinary ? data : data.toString();
|
||||
if (typeof message !== "string") {
|
||||
this.emit("heartbeat", {});
|
||||
return;
|
||||
}
|
||||
|
||||
if (message === "") {
|
||||
this.emit("heartbeat", {});
|
||||
return;
|
||||
}
|
||||
|
||||
let obj: {
|
||||
type: string;
|
||||
body: {
|
||||
id: string;
|
||||
type: string;
|
||||
body: any;
|
||||
};
|
||||
};
|
||||
let body: {
|
||||
id: string;
|
||||
type: string;
|
||||
body: any;
|
||||
};
|
||||
|
||||
try {
|
||||
obj = JSON.parse(message);
|
||||
if (obj.type !== "channel") {
|
||||
return;
|
||||
}
|
||||
if (!obj.body) {
|
||||
return;
|
||||
}
|
||||
body = obj.body;
|
||||
if (body.id !== channelID) {
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
this.emit(
|
||||
"error",
|
||||
new Error(
|
||||
`Error parsing websocket reply: ${message}, error message: ${err}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (body.type) {
|
||||
case "note":
|
||||
this.emit("update", body.body as MisskeyAPI.Entity.Note);
|
||||
break;
|
||||
case "notification":
|
||||
this.emit("notification", body.body as MisskeyAPI.Entity.Notification);
|
||||
break;
|
||||
case "mention": {
|
||||
const note = body.body as MisskeyAPI.Entity.Note;
|
||||
if (note.visibility === "specified") {
|
||||
this.emit("conversation", note);
|
||||
}
|
||||
break;
|
||||
}
|
||||
// When renote and followed event, the same notification will be received.
|
||||
case "renote":
|
||||
case "followed":
|
||||
case "follow":
|
||||
case "unfollow":
|
||||
case "receiveFollowRequest":
|
||||
case "meUpdated":
|
||||
case "readAllNotifications":
|
||||
case "readAllUnreadSpecifiedNotes":
|
||||
case "readAllAntennas":
|
||||
case "readAllUnreadMentions":
|
||||
case "unreadNotification":
|
||||
// Ignore these events
|
||||
break;
|
||||
default:
|
||||
this.emit(
|
||||
"error",
|
||||
new Error(`Unknown event has received: ${JSON.stringify(body)}`),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
14
packages/megalodon/src/notification.ts
Normal file
14
packages/megalodon/src/notification.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import Entity from "./entity";
|
||||
|
||||
namespace NotificationType {
|
||||
export const Follow: Entity.NotificationType = "follow";
|
||||
export const Favourite: Entity.NotificationType = "favourite";
|
||||
export const Reblog: Entity.NotificationType = "reblog";
|
||||
export const Mention: Entity.NotificationType = "mention";
|
||||
export const Reaction: Entity.NotificationType = "reaction";
|
||||
export const FollowRequest: Entity.NotificationType = "follow_request";
|
||||
export const Status: Entity.NotificationType = "status";
|
||||
export const Poll: Entity.NotificationType = "poll";
|
||||
}
|
||||
|
||||
export default NotificationType;
|
123
packages/megalodon/src/oauth.ts
Normal file
123
packages/megalodon/src/oauth.ts
Normal file
@ -0,0 +1,123 @@
|
||||
/**
|
||||
* OAuth
|
||||
* Response data when oauth request.
|
||||
**/
|
||||
namespace OAuth {
|
||||
export type AppDataFromServer = {
|
||||
id: string;
|
||||
name: string;
|
||||
website: string | null;
|
||||
redirect_uri: string;
|
||||
client_id: string;
|
||||
client_secret: string;
|
||||
};
|
||||
|
||||
export type TokenDataFromServer = {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
scope: string;
|
||||
created_at: number;
|
||||
expires_in: number | null;
|
||||
refresh_token: string | null;
|
||||
};
|
||||
|
||||
export class AppData {
|
||||
public url: string | null;
|
||||
public session_token: string | null;
|
||||
constructor(
|
||||
public id: string,
|
||||
public name: string,
|
||||
public website: string | null,
|
||||
public redirect_uri: string,
|
||||
public client_id: string,
|
||||
public client_secret: string,
|
||||
) {
|
||||
this.url = null;
|
||||
this.session_token = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize raw application data from server
|
||||
* @param raw from server
|
||||
*/
|
||||
static from(raw: AppDataFromServer) {
|
||||
return new this(
|
||||
raw.id,
|
||||
raw.name,
|
||||
raw.website,
|
||||
raw.redirect_uri,
|
||||
raw.client_id,
|
||||
raw.client_secret,
|
||||
);
|
||||
}
|
||||
|
||||
get redirectUri() {
|
||||
return this.redirect_uri;
|
||||
}
|
||||
get clientId() {
|
||||
return this.client_id;
|
||||
}
|
||||
get clientSecret() {
|
||||
return this.client_secret;
|
||||
}
|
||||
}
|
||||
|
||||
export class TokenData {
|
||||
public _scope: string;
|
||||
constructor(
|
||||
public access_token: string,
|
||||
public token_type: string,
|
||||
scope: string,
|
||||
public created_at: number,
|
||||
public expires_in: number | null = null,
|
||||
public refresh_token: string | null = null,
|
||||
) {
|
||||
this._scope = scope;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize raw token data from server
|
||||
* @param raw from server
|
||||
*/
|
||||
static from(raw: TokenDataFromServer) {
|
||||
return new this(
|
||||
raw.access_token,
|
||||
raw.token_type,
|
||||
raw.scope,
|
||||
raw.created_at,
|
||||
raw.expires_in,
|
||||
raw.refresh_token,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* OAuth Aceess Token
|
||||
*/
|
||||
get accessToken() {
|
||||
return this.access_token;
|
||||
}
|
||||
get tokenType() {
|
||||
return this.token_type;
|
||||
}
|
||||
get scope() {
|
||||
return this._scope;
|
||||
}
|
||||
/**
|
||||
* Application ID
|
||||
*/
|
||||
get createdAt() {
|
||||
return this.created_at;
|
||||
}
|
||||
get expiresIn() {
|
||||
return this.expires_in;
|
||||
}
|
||||
/**
|
||||
* OAuth Refresh Token
|
||||
*/
|
||||
get refreshToken() {
|
||||
return this.refresh_token;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default OAuth;
|
94
packages/megalodon/src/parser.ts
Normal file
94
packages/megalodon/src/parser.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import { EventEmitter } from "events";
|
||||
import Entity from "./entity";
|
||||
|
||||
/**
|
||||
* Parser
|
||||
* Parse response data in streaming.
|
||||
**/
|
||||
export class Parser extends EventEmitter {
|
||||
private message: string;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.message = "";
|
||||
}
|
||||
|
||||
public parse(chunk: string) {
|
||||
// skip heartbeats
|
||||
if (chunk === ":thump\n") {
|
||||
this.emit("heartbeat", {});
|
||||
return;
|
||||
}
|
||||
|
||||
this.message += chunk;
|
||||
chunk = this.message;
|
||||
|
||||
const size: number = chunk.length;
|
||||
let start = 0;
|
||||
let offset = 0;
|
||||
let curr: string | undefined;
|
||||
let next: string | undefined;
|
||||
|
||||
while (offset < size) {
|
||||
curr = chunk[offset];
|
||||
next = chunk[offset + 1];
|
||||
|
||||
if (curr === "\n" && next === "\n") {
|
||||
const piece: string = chunk.slice(start, offset);
|
||||
|
||||
offset += 2;
|
||||
start = offset;
|
||||
|
||||
if (!piece.length) continue; // empty object
|
||||
|
||||
const root: Array<string> = piece.split("\n");
|
||||
|
||||
// should never happen, as long as mastodon doesn't change API messages
|
||||
if (root.length !== 2) continue;
|
||||
|
||||
// remove event and data markers
|
||||
const event: string = root[0].substr(7);
|
||||
const data: string = root[1].substr(6);
|
||||
|
||||
let jsonObj = {};
|
||||
try {
|
||||
jsonObj = JSON.parse(data);
|
||||
} catch (err) {
|
||||
// delete event does not have json object
|
||||
if (event !== "delete") {
|
||||
this.emit(
|
||||
"error",
|
||||
new Error(
|
||||
`Error parsing API reply: '${piece}', error message: '${err}'`,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
switch (event) {
|
||||
case "update":
|
||||
this.emit("update", jsonObj as Entity.Status);
|
||||
break;
|
||||
case "notification":
|
||||
this.emit("notification", jsonObj as Entity.Notification);
|
||||
break;
|
||||
case "conversation":
|
||||
this.emit("conversation", jsonObj as Entity.Conversation);
|
||||
break;
|
||||
case "delete":
|
||||
// When delete, data is an ID of the deleted status
|
||||
this.emit("delete", data);
|
||||
break;
|
||||
default:
|
||||
this.emit(
|
||||
"error",
|
||||
new Error(`Unknown event has received: ${event}`),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
offset++;
|
||||
}
|
||||
this.message = chunk.slice(start, size);
|
||||
}
|
||||
}
|
92
packages/megalodon/src/proxy_config.ts
Normal file
92
packages/megalodon/src/proxy_config.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import { HttpsProxyAgent, HttpsProxyAgentOptions } from "https-proxy-agent";
|
||||
import { SocksProxyAgent, SocksProxyAgentOptions } from "socks-proxy-agent";
|
||||
|
||||
export type ProxyConfig = {
|
||||
host: string;
|
||||
port: number;
|
||||
auth?: {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
protocol:
|
||||
| "http"
|
||||
| "https"
|
||||
| "socks4"
|
||||
| "socks4a"
|
||||
| "socks5"
|
||||
| "socks5h"
|
||||
| "socks";
|
||||
};
|
||||
|
||||
class ProxyProtocolError extends Error {}
|
||||
|
||||
const proxyAgent = (
|
||||
proxyConfig: ProxyConfig,
|
||||
): HttpsProxyAgent | SocksProxyAgent => {
|
||||
switch (proxyConfig.protocol) {
|
||||
case "http": {
|
||||
let options: HttpsProxyAgentOptions = {
|
||||
host: proxyConfig.host,
|
||||
port: proxyConfig.port,
|
||||
secureProxy: false,
|
||||
};
|
||||
if (proxyConfig.auth) {
|
||||
options = Object.assign(options, {
|
||||
auth: `${proxyConfig.auth.username}:${proxyConfig.auth.password}`,
|
||||
});
|
||||
}
|
||||
const httpsAgent = new HttpsProxyAgent(options);
|
||||
return httpsAgent;
|
||||
}
|
||||
case "https": {
|
||||
let options: HttpsProxyAgentOptions = {
|
||||
host: proxyConfig.host,
|
||||
port: proxyConfig.port,
|
||||
secureProxy: true,
|
||||
};
|
||||
if (proxyConfig.auth) {
|
||||
options = Object.assign(options, {
|
||||
auth: `${proxyConfig.auth.username}:${proxyConfig.auth.password}`,
|
||||
});
|
||||
}
|
||||
const httpsAgent = new HttpsProxyAgent(options);
|
||||
return httpsAgent;
|
||||
}
|
||||
case "socks4":
|
||||
case "socks4a": {
|
||||
let options: SocksProxyAgentOptions = {
|
||||
type: 4,
|
||||
hostname: proxyConfig.host,
|
||||
port: proxyConfig.port,
|
||||
};
|
||||
if (proxyConfig.auth) {
|
||||
options = Object.assign(options, {
|
||||
userId: proxyConfig.auth.username,
|
||||
password: proxyConfig.auth.password,
|
||||
});
|
||||
}
|
||||
const socksAgent = new SocksProxyAgent(options);
|
||||
return socksAgent;
|
||||
}
|
||||
case "socks5":
|
||||
case "socks5h":
|
||||
case "socks": {
|
||||
let options: SocksProxyAgentOptions = {
|
||||
type: 5,
|
||||
hostname: proxyConfig.host,
|
||||
port: proxyConfig.port,
|
||||
};
|
||||
if (proxyConfig.auth) {
|
||||
options = Object.assign(options, {
|
||||
userId: proxyConfig.auth.username,
|
||||
password: proxyConfig.auth.password,
|
||||
});
|
||||
}
|
||||
const socksAgent = new SocksProxyAgent(options);
|
||||
return socksAgent;
|
||||
}
|
||||
default:
|
||||
throw new ProxyProtocolError("protocol is not accepted");
|
||||
}
|
||||
};
|
||||
export default proxyAgent;
|
8
packages/megalodon/src/response.ts
Normal file
8
packages/megalodon/src/response.ts
Normal file
@ -0,0 +1,8 @@
|
||||
type Response<T = any> = {
|
||||
data: T;
|
||||
status: number;
|
||||
statusText: string;
|
||||
headers: any;
|
||||
};
|
||||
|
||||
export default Response;
|
27
packages/megalodon/test/integration/megalodon.spec.ts
Normal file
27
packages/megalodon/test/integration/megalodon.spec.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { detector } from '../../src/index'
|
||||
|
||||
describe('detector', () => {
|
||||
describe('mastodon', () => {
|
||||
const url = 'https://fedibird.com'
|
||||
it('should be mastodon', async () => {
|
||||
const mastodon = await detector(url)
|
||||
expect(mastodon).toEqual('mastodon')
|
||||
})
|
||||
})
|
||||
|
||||
describe('pleroma', () => {
|
||||
const url = 'https://pleroma.soykaf.com'
|
||||
it('should be pleroma', async () => {
|
||||
const pleroma = await detector(url)
|
||||
expect(pleroma).toEqual('pleroma')
|
||||
})
|
||||
})
|
||||
|
||||
describe('misskey', () => {
|
||||
const url = 'https://misskey.io'
|
||||
it('should be misskey', async () => {
|
||||
const misskey = await detector(url)
|
||||
expect(misskey).toEqual('misskey')
|
||||
})
|
||||
})
|
||||
})
|
204
packages/megalodon/test/integration/misskey.spec.ts
Normal file
204
packages/megalodon/test/integration/misskey.spec.ts
Normal file
@ -0,0 +1,204 @@
|
||||
import MisskeyEntity from '@/misskey/entity'
|
||||
import MisskeyNotificationType from '@/misskey/notification'
|
||||
import Misskey from '@/misskey'
|
||||
import MegalodonNotificationType from '@/notification'
|
||||
import axios, { AxiosResponse } from 'axios'
|
||||
|
||||
jest.mock('axios')
|
||||
|
||||
const user: MisskeyEntity.User = {
|
||||
id: '1',
|
||||
name: 'test_user',
|
||||
username: 'TestUser',
|
||||
host: 'misskey.io',
|
||||
avatarUrl: 'https://example.com/icon.png',
|
||||
avatarColor: '#000000',
|
||||
emojis: []
|
||||
}
|
||||
|
||||
const note: MisskeyEntity.Note = {
|
||||
id: '1',
|
||||
createdAt: '2021-02-01T01:49:29',
|
||||
userId: '1',
|
||||
user: user,
|
||||
text: 'hogehoge',
|
||||
cw: null,
|
||||
visibility: 'public',
|
||||
renoteCount: 0,
|
||||
repliesCount: 0,
|
||||
reactions: {},
|
||||
emojis: [],
|
||||
fileIds: [],
|
||||
files: [],
|
||||
replyId: null,
|
||||
renoteId: null
|
||||
}
|
||||
|
||||
const follow: MisskeyEntity.Notification = {
|
||||
id: '1',
|
||||
createdAt: '2021-02-01T01:49:29',
|
||||
userId: user.id,
|
||||
user: user,
|
||||
type: MisskeyNotificationType.Follow
|
||||
}
|
||||
|
||||
const mention: MisskeyEntity.Notification = {
|
||||
id: '1',
|
||||
createdAt: '2021-02-01T01:49:29',
|
||||
userId: user.id,
|
||||
user: user,
|
||||
type: MisskeyNotificationType.Mention,
|
||||
note: note
|
||||
}
|
||||
|
||||
const reply: MisskeyEntity.Notification = {
|
||||
id: '1',
|
||||
createdAt: '2021-02-01T01:49:29',
|
||||
userId: user.id,
|
||||
user: user,
|
||||
type: MisskeyNotificationType.Reply,
|
||||
note: note
|
||||
}
|
||||
|
||||
const renote: MisskeyEntity.Notification = {
|
||||
id: '1',
|
||||
createdAt: '2021-02-01T01:49:29',
|
||||
userId: user.id,
|
||||
user: user,
|
||||
type: MisskeyNotificationType.Renote,
|
||||
note: note
|
||||
}
|
||||
|
||||
const quote: MisskeyEntity.Notification = {
|
||||
id: '1',
|
||||
createdAt: '2021-02-01T01:49:29',
|
||||
userId: user.id,
|
||||
user: user,
|
||||
type: MisskeyNotificationType.Quote,
|
||||
note: note
|
||||
}
|
||||
|
||||
const reaction: MisskeyEntity.Notification = {
|
||||
id: '1',
|
||||
createdAt: '2021-02-01T01:49:29',
|
||||
userId: user.id,
|
||||
user: user,
|
||||
type: MisskeyNotificationType.Reaction,
|
||||
note: note,
|
||||
reaction: '♥'
|
||||
}
|
||||
|
||||
const pollVote: MisskeyEntity.Notification = {
|
||||
id: '1',
|
||||
createdAt: '2021-02-01T01:49:29',
|
||||
userId: user.id,
|
||||
user: user,
|
||||
type: MisskeyNotificationType.PollEnded,
|
||||
note: note
|
||||
}
|
||||
|
||||
const receiveFollowRequest: MisskeyEntity.Notification = {
|
||||
id: '1',
|
||||
createdAt: '2021-02-01T01:49:29',
|
||||
userId: user.id,
|
||||
user: user,
|
||||
type: MisskeyNotificationType.ReceiveFollowRequest
|
||||
}
|
||||
|
||||
const followRequestAccepted: MisskeyEntity.Notification = {
|
||||
id: '1',
|
||||
createdAt: '2021-02-01T01:49:29',
|
||||
userId: user.id,
|
||||
user: user,
|
||||
type: MisskeyNotificationType.FollowRequestAccepted
|
||||
}
|
||||
|
||||
const groupInvited: MisskeyEntity.Notification = {
|
||||
id: '1',
|
||||
createdAt: '2021-02-01T01:49:29',
|
||||
userId: user.id,
|
||||
user: user,
|
||||
type: MisskeyNotificationType.GroupInvited
|
||||
}
|
||||
|
||||
;(axios.CancelToken.source as any).mockImplementation(() => {
|
||||
return {
|
||||
token: {
|
||||
throwIfRequested: () => {},
|
||||
promise: {
|
||||
then: () => {},
|
||||
catch: () => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
describe('getNotifications', () => {
|
||||
const client = new Misskey('http://localhost', 'sample token')
|
||||
const cases: Array<{ event: MisskeyEntity.Notification; expected: Entity.NotificationType; title: string }> = [
|
||||
{
|
||||
event: follow,
|
||||
expected: MegalodonNotificationType.Follow,
|
||||
title: 'follow'
|
||||
},
|
||||
{
|
||||
event: mention,
|
||||
expected: MegalodonNotificationType.Mention,
|
||||
title: 'mention'
|
||||
},
|
||||
{
|
||||
event: reply,
|
||||
expected: MegalodonNotificationType.Mention,
|
||||
title: 'reply'
|
||||
},
|
||||
{
|
||||
event: renote,
|
||||
expected: MegalodonNotificationType.Reblog,
|
||||
title: 'renote'
|
||||
},
|
||||
{
|
||||
event: quote,
|
||||
expected: MegalodonNotificationType.Reblog,
|
||||
title: 'quote'
|
||||
},
|
||||
{
|
||||
event: reaction,
|
||||
expected: MegalodonNotificationType.Reaction,
|
||||
title: 'reaction'
|
||||
},
|
||||
{
|
||||
event: pollVote,
|
||||
expected: MegalodonNotificationType.Poll,
|
||||
title: 'pollVote'
|
||||
},
|
||||
{
|
||||
event: receiveFollowRequest,
|
||||
expected: MegalodonNotificationType.FollowRequest,
|
||||
title: 'receiveFollowRequest'
|
||||
},
|
||||
{
|
||||
event: followRequestAccepted,
|
||||
expected: MegalodonNotificationType.Follow,
|
||||
title: 'followRequestAccepted'
|
||||
},
|
||||
{
|
||||
event: groupInvited,
|
||||
expected: MisskeyNotificationType.GroupInvited,
|
||||
title: 'groupInvited'
|
||||
}
|
||||
]
|
||||
cases.forEach(c => {
|
||||
it(`should be ${c.title} event`, async () => {
|
||||
const mockResponse: AxiosResponse<Array<MisskeyEntity.Notification>> = {
|
||||
data: [c.event],
|
||||
status: 200,
|
||||
statusText: '200OK',
|
||||
headers: {},
|
||||
config: {}
|
||||
}
|
||||
;(axios.post as any).mockResolvedValue(mockResponse)
|
||||
const res = await client.getNotifications()
|
||||
expect(res.data[0].type).toEqual(c.expected)
|
||||
})
|
||||
})
|
||||
})
|
233
packages/megalodon/test/unit/misskey/api_client.spec.ts
Normal file
233
packages/megalodon/test/unit/misskey/api_client.spec.ts
Normal file
@ -0,0 +1,233 @@
|
||||
import MisskeyAPI from '@/misskey/api_client'
|
||||
import MegalodonEntity from '@/entity'
|
||||
import MisskeyEntity from '@/misskey/entity'
|
||||
import MegalodonNotificationType from '@/notification'
|
||||
import MisskeyNotificationType from '@/misskey/notification'
|
||||
|
||||
const user: MisskeyEntity.User = {
|
||||
id: '1',
|
||||
name: 'test_user',
|
||||
username: 'TestUser',
|
||||
host: 'misskey.io',
|
||||
avatarUrl: 'https://example.com/icon.png',
|
||||
avatarColor: '#000000',
|
||||
emojis: []
|
||||
}
|
||||
|
||||
const converter: MisskeyAPI.Converter = new MisskeyAPI.Converter("https://example.com")
|
||||
|
||||
describe('api_client', () => {
|
||||
describe('notification', () => {
|
||||
describe('encode', () => {
|
||||
it('megalodon notification type should be encoded to misskey notification type', () => {
|
||||
const cases: Array<{ src: MegalodonEntity.NotificationType; dist: MisskeyEntity.NotificationType }> = [
|
||||
{
|
||||
src: MegalodonNotificationType.Follow,
|
||||
dist: MisskeyNotificationType.Follow
|
||||
},
|
||||
{
|
||||
src: MegalodonNotificationType.Mention,
|
||||
dist: MisskeyNotificationType.Reply
|
||||
},
|
||||
{
|
||||
src: MegalodonNotificationType.Favourite,
|
||||
dist: MisskeyNotificationType.Reaction
|
||||
},
|
||||
{
|
||||
src: MegalodonNotificationType.Reaction,
|
||||
dist: MisskeyNotificationType.Reaction
|
||||
},
|
||||
{
|
||||
src: MegalodonNotificationType.Reblog,
|
||||
dist: MisskeyNotificationType.Renote
|
||||
},
|
||||
{
|
||||
src: MegalodonNotificationType.Poll,
|
||||
dist: MisskeyNotificationType.PollEnded
|
||||
},
|
||||
{
|
||||
src: MegalodonNotificationType.FollowRequest,
|
||||
dist: MisskeyNotificationType.ReceiveFollowRequest
|
||||
}
|
||||
]
|
||||
cases.forEach(c => {
|
||||
expect(converter.encodeNotificationType(c.src)).toEqual(c.dist)
|
||||
})
|
||||
})
|
||||
})
|
||||
describe('decode', () => {
|
||||
it('misskey notification type should be decoded to megalodon notification type', () => {
|
||||
const cases: Array<{ src: MisskeyEntity.NotificationType; dist: MegalodonEntity.NotificationType }> = [
|
||||
{
|
||||
src: MisskeyNotificationType.Follow,
|
||||
dist: MegalodonNotificationType.Follow
|
||||
},
|
||||
{
|
||||
src: MisskeyNotificationType.Mention,
|
||||
dist: MegalodonNotificationType.Mention
|
||||
},
|
||||
{
|
||||
src: MisskeyNotificationType.Reply,
|
||||
dist: MegalodonNotificationType.Mention
|
||||
},
|
||||
{
|
||||
src: MisskeyNotificationType.Renote,
|
||||
dist: MegalodonNotificationType.Reblog
|
||||
},
|
||||
{
|
||||
src: MisskeyNotificationType.Quote,
|
||||
dist: MegalodonNotificationType.Reblog
|
||||
},
|
||||
{
|
||||
src: MisskeyNotificationType.Reaction,
|
||||
dist: MegalodonNotificationType.Reaction
|
||||
},
|
||||
{
|
||||
src: MisskeyNotificationType.PollEnded,
|
||||
dist: MegalodonNotificationType.Poll
|
||||
},
|
||||
{
|
||||
src: MisskeyNotificationType.ReceiveFollowRequest,
|
||||
dist: MegalodonNotificationType.FollowRequest
|
||||
},
|
||||
{
|
||||
src: MisskeyNotificationType.FollowRequestAccepted,
|
||||
dist: MegalodonNotificationType.Follow
|
||||
}
|
||||
]
|
||||
cases.forEach(c => {
|
||||
expect(converter.decodeNotificationType(c.src)).toEqual(c.dist)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
describe('reactions', () => {
|
||||
it('should be mapped', () => {
|
||||
const misskeyReactions = [
|
||||
{
|
||||
id: '1',
|
||||
createdAt: '2020-04-21T13:04:13.968Z',
|
||||
user: {
|
||||
id: '81u70uwsja',
|
||||
name: 'h3poteto',
|
||||
username: 'h3poteto',
|
||||
host: null,
|
||||
avatarUrl: 'https://s3.arkjp.net/misskey/thumbnail-63807d97-20ca-40ba-9493-179aa48065c1.png',
|
||||
avatarColor: 'rgb(146,189,195)',
|
||||
emojis: []
|
||||
},
|
||||
type: '❤'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
createdAt: '2020-04-21T13:04:13.968Z',
|
||||
user: {
|
||||
id: '81u70uwsja',
|
||||
name: 'h3poteto',
|
||||
username: 'h3poteto',
|
||||
host: null,
|
||||
avatarUrl: 'https://s3.arkjp.net/misskey/thumbnail-63807d97-20ca-40ba-9493-179aa48065c1.png',
|
||||
avatarColor: 'rgb(146,189,195)',
|
||||
emojis: []
|
||||
},
|
||||
type: '❤'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
createdAt: '2020-04-21T13:04:13.968Z',
|
||||
user: {
|
||||
id: '81u70uwsja',
|
||||
name: 'h3poteto',
|
||||
username: 'h3poteto',
|
||||
host: null,
|
||||
avatarUrl: 'https://s3.arkjp.net/misskey/thumbnail-63807d97-20ca-40ba-9493-179aa48065c1.png',
|
||||
avatarColor: 'rgb(146,189,195)',
|
||||
emojis: []
|
||||
},
|
||||
type: '☺'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
createdAt: '2020-04-21T13:04:13.968Z',
|
||||
user: {
|
||||
id: '81u70uwsja',
|
||||
name: 'h3poteto',
|
||||
username: 'h3poteto',
|
||||
host: null,
|
||||
avatarUrl: 'https://s3.arkjp.net/misskey/thumbnail-63807d97-20ca-40ba-9493-179aa48065c1.png',
|
||||
avatarColor: 'rgb(146,189,195)',
|
||||
emojis: []
|
||||
},
|
||||
type: '❤'
|
||||
}
|
||||
]
|
||||
|
||||
const reactions = converter.reactions(misskeyReactions)
|
||||
expect(reactions).toEqual([
|
||||
{
|
||||
count: 3,
|
||||
me: false,
|
||||
name: '❤'
|
||||
},
|
||||
{
|
||||
count: 1,
|
||||
me: false,
|
||||
name: '☺'
|
||||
}
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('status', () => {
|
||||
describe('plain content', () => {
|
||||
it('should be exported plain content and html content', () => {
|
||||
const plainContent = 'hoge\nfuga\nfuga'
|
||||
const content = 'hoge<br>fuga<br>fuga'
|
||||
const note: MisskeyEntity.Note = {
|
||||
id: '1',
|
||||
createdAt: '2021-02-01T01:49:29',
|
||||
userId: '1',
|
||||
user: user,
|
||||
text: plainContent,
|
||||
cw: null,
|
||||
visibility: 'public',
|
||||
renoteCount: 0,
|
||||
repliesCount: 0,
|
||||
reactions: {},
|
||||
emojis: [],
|
||||
fileIds: [],
|
||||
files: [],
|
||||
replyId: null,
|
||||
renoteId: null
|
||||
}
|
||||
const megalodonStatus = converter.note(note, user.host || 'misskey.io')
|
||||
expect(megalodonStatus.plain_content).toEqual(plainContent)
|
||||
expect(megalodonStatus.content).toEqual(content)
|
||||
})
|
||||
it('html tags should be escaped', () => {
|
||||
const plainContent = '<p>hoge\nfuga\nfuga<p>'
|
||||
const content = '<p>hoge<br>fuga<br>fuga<p>'
|
||||
const note: MisskeyEntity.Note = {
|
||||
id: '1',
|
||||
createdAt: '2021-02-01T01:49:29',
|
||||
userId: '1',
|
||||
user: user,
|
||||
text: plainContent,
|
||||
cw: null,
|
||||
visibility: 'public',
|
||||
renoteCount: 0,
|
||||
repliesCount: 0,
|
||||
reactions: {},
|
||||
emojis: [],
|
||||
fileIds: [],
|
||||
files: [],
|
||||
replyId: null,
|
||||
renoteId: null
|
||||
}
|
||||
const megalodonStatus = converter.note(note, user.host || 'misskey.io')
|
||||
expect(megalodonStatus.plain_content).toEqual(plainContent)
|
||||
expect(megalodonStatus.content).toEqual(content)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
152
packages/megalodon/test/unit/parser.spec.ts
Normal file
152
packages/megalodon/test/unit/parser.spec.ts
Normal file
@ -0,0 +1,152 @@
|
||||
import { Parser } from '@/parser'
|
||||
import Entity from '@/entity'
|
||||
|
||||
const account: Entity.Account = {
|
||||
id: '1',
|
||||
username: 'h3poteto',
|
||||
acct: 'h3poteto@pleroma.io',
|
||||
display_name: 'h3poteto',
|
||||
locked: false,
|
||||
created_at: '2019-03-26T21:30:32',
|
||||
followers_count: 10,
|
||||
following_count: 10,
|
||||
statuses_count: 100,
|
||||
note: 'engineer',
|
||||
url: 'https://pleroma.io',
|
||||
avatar: '',
|
||||
avatar_static: '',
|
||||
header: '',
|
||||
header_static: '',
|
||||
emojis: [],
|
||||
moved: null,
|
||||
fields: [],
|
||||
bot: false
|
||||
}
|
||||
|
||||
const status: Entity.Status = {
|
||||
id: '1',
|
||||
uri: 'http://example.com',
|
||||
url: 'http://example.com',
|
||||
account: account,
|
||||
in_reply_to_id: null,
|
||||
in_reply_to_account_id: null,
|
||||
reblog: null,
|
||||
content: 'hoge',
|
||||
plain_content: 'hoge',
|
||||
created_at: '2019-03-26T21:40:32',
|
||||
emojis: [],
|
||||
replies_count: 0,
|
||||
reblogs_count: 0,
|
||||
favourites_count: 0,
|
||||
reblogged: null,
|
||||
favourited: null,
|
||||
muted: null,
|
||||
sensitive: false,
|
||||
spoiler_text: '',
|
||||
visibility: 'public',
|
||||
media_attachments: [],
|
||||
mentions: [],
|
||||
tags: [],
|
||||
card: null,
|
||||
poll: null,
|
||||
application: {
|
||||
name: 'Web'
|
||||
} as Entity.Application,
|
||||
language: null,
|
||||
pinned: null,
|
||||
reactions: [],
|
||||
bookmarked: false,
|
||||
quote: null
|
||||
}
|
||||
|
||||
const notification: Entity.Notification = {
|
||||
id: '1',
|
||||
account: account,
|
||||
status: status,
|
||||
type: 'favourite',
|
||||
created_at: '2019-04-01T17:01:32'
|
||||
}
|
||||
|
||||
const conversation: Entity.Conversation = {
|
||||
id: '1',
|
||||
accounts: [account],
|
||||
last_status: status,
|
||||
unread: true
|
||||
}
|
||||
|
||||
describe('Parser', () => {
|
||||
let parser: Parser
|
||||
|
||||
beforeEach(() => {
|
||||
parser = new Parser()
|
||||
})
|
||||
|
||||
describe('parse', () => {
|
||||
describe('message is heartbeat', () => {
|
||||
const message: string = ':thump\n'
|
||||
it('should be called', () => {
|
||||
const spy = jest.fn()
|
||||
parser.on('heartbeat', spy)
|
||||
parser.parse(message)
|
||||
expect(spy).toHaveBeenLastCalledWith({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('message is not json', () => {
|
||||
describe('event is delete', () => {
|
||||
const message = `event: delete\ndata: 12asdf34\n\n`
|
||||
it('should be called', () => {
|
||||
const spy = jest.fn()
|
||||
parser.once('delete', spy)
|
||||
parser.parse(message)
|
||||
expect(spy).toHaveBeenCalledWith('12asdf34')
|
||||
})
|
||||
})
|
||||
|
||||
describe('event is not delete', () => {
|
||||
const message = `event: event\ndata: 12asdf34\n\n`
|
||||
it('should be error', () => {
|
||||
const error = jest.fn()
|
||||
const deleted = jest.fn()
|
||||
parser.once('error', error)
|
||||
parser.once('delete', deleted)
|
||||
parser.parse(message)
|
||||
expect(error).toHaveBeenCalled()
|
||||
expect(deleted).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('message is json', () => {
|
||||
describe('event is update', () => {
|
||||
const message = `event: update\ndata: ${JSON.stringify(status)}\n\n`
|
||||
it('should be called', () => {
|
||||
const spy = jest.fn()
|
||||
parser.once('update', spy)
|
||||
parser.parse(message)
|
||||
expect(spy).toHaveBeenCalledWith(status)
|
||||
})
|
||||
})
|
||||
|
||||
describe('event is notification', () => {
|
||||
const message = `event: notification\ndata: ${JSON.stringify(notification)}\n\n`
|
||||
it('should be called', () => {
|
||||
const spy = jest.fn()
|
||||
parser.once('notification', spy)
|
||||
parser.parse(message)
|
||||
expect(spy).toHaveBeenCalledWith(notification)
|
||||
})
|
||||
})
|
||||
|
||||
describe('event is conversation', () => {
|
||||
const message = `event: conversation\ndata: ${JSON.stringify(conversation)}\n\n`
|
||||
it('should be called', () => {
|
||||
const spy = jest.fn()
|
||||
parser.once('conversation', spy)
|
||||
parser.parse(message)
|
||||
expect(spy).toHaveBeenCalledWith(conversation)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
64
packages/megalodon/tsconfig.json
Normal file
64
packages/megalodon/tsconfig.json
Normal file
@ -0,0 +1,64 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Basic Options */
|
||||
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
|
||||
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
|
||||
"lib": ["es2021", "dom"], /* Specify library files to be included in the compilation. */
|
||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||
// "checkJs": true, /* Report errors in .js files. */
|
||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||
"declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||
"outDir": "./lib", /* Redirect output structure to the directory. */
|
||||
"rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||
// "composite": true, /* Enable project compilation */
|
||||
"removeComments": true, /* Do not emit comments to output. */
|
||||
// "noEmit": true, /* Do not emit outputs. */
|
||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||
"downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||
|
||||
/* Strict Type-Checking Options */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||
"strictNullChecks": true, /* Enable strict null checks. */
|
||||
"strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||
"strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||
"noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||
"alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||
|
||||
/* Additional Checks */
|
||||
"noUnusedLocals": false, /* Report errors on unused locals. */
|
||||
"noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
"noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
|
||||
/* Module Resolution Options */
|
||||
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||
"baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||
"paths": {
|
||||
"@*": ["src*"],
|
||||
"~*": ["./*"]
|
||||
}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||
// "types": [], /* Type declaration files to be included in compilation. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||
|
||||
/* Source Map Options */
|
||||
// "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||
// "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||
|
||||
/* Experimental Options */
|
||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||
},
|
||||
"include": ["./src", "./test"],
|
||||
"exclude": ["node_modules", "example"]
|
||||
}
|
1370
pnpm-lock.yaml
1370
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user