5fedfd2599
Fixes #10537
3422 lines
78 KiB
TypeScript
3422 lines
78 KiB
TypeScript
import FormData from "form-data";
|
|
import AsyncLock from "async-lock";
|
|
|
|
import MisskeyAPI from "./misskey/api_client";
|
|
import { DEFAULT_UA } from "./default";
|
|
import { ProxyConfig } from "./proxy_config";
|
|
import OAuth from "./oauth";
|
|
import Response from "./response";
|
|
import Entity from "./entity";
|
|
import {
|
|
MegalodonInterface,
|
|
WebSocketInterface,
|
|
NoImplementedError,
|
|
ArgumentError,
|
|
UnexpectedError,
|
|
} from "./megalodon";
|
|
import MegalodonEntity from "@/entity";
|
|
import fs from "node:fs";
|
|
import MisskeyNotificationType from "./misskey/notification";
|
|
|
|
type AccountCache = {
|
|
locks: AsyncLock;
|
|
accounts: Entity.Account[];
|
|
};
|
|
|
|
export default class Misskey implements MegalodonInterface {
|
|
public client: MisskeyAPI.Interface;
|
|
public converter: MisskeyAPI.Converter;
|
|
public baseUrl: string;
|
|
public proxyConfig: ProxyConfig | false;
|
|
|
|
/**
|
|
* @param baseUrl hostname or base URL
|
|
* @param accessToken access token from OAuth2 authorization
|
|
* @param userAgent UserAgent is specified in header on request.
|
|
* @param proxyConfig Proxy setting, or set false if don't use proxy.
|
|
*/
|
|
constructor(
|
|
baseUrl: string,
|
|
accessToken: string | null = null,
|
|
userAgent: string | null = DEFAULT_UA,
|
|
proxyConfig: ProxyConfig | false = false,
|
|
) {
|
|
let token = "";
|
|
if (accessToken) {
|
|
token = accessToken;
|
|
}
|
|
let agent: string = DEFAULT_UA;
|
|
if (userAgent) {
|
|
agent = userAgent;
|
|
}
|
|
this.converter = new MisskeyAPI.Converter(baseUrl);
|
|
this.client = new MisskeyAPI.Client(
|
|
baseUrl,
|
|
token,
|
|
agent,
|
|
proxyConfig,
|
|
this.converter,
|
|
);
|
|
this.baseUrl = baseUrl;
|
|
this.proxyConfig = proxyConfig;
|
|
}
|
|
|
|
private baseUrlToHost(baseUrl: string): string {
|
|
return baseUrl.replace("https://", "");
|
|
}
|
|
|
|
public cancel(): void {
|
|
return this.client.cancel();
|
|
}
|
|
|
|
public async registerApp(
|
|
client_name: string,
|
|
options: Partial<{
|
|
scopes: Array<string>;
|
|
redirect_uris: string;
|
|
website: string;
|
|
}> = {
|
|
scopes: MisskeyAPI.DEFAULT_SCOPE,
|
|
redirect_uris: this.baseUrl,
|
|
},
|
|
): Promise<OAuth.AppData> {
|
|
return this.createApp(client_name, options).then(async (appData) => {
|
|
return this.generateAuthUrlAndToken(appData.client_secret).then(
|
|
(session) => {
|
|
appData.url = session.url;
|
|
appData.session_token = session.token;
|
|
return appData;
|
|
},
|
|
);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* POST /api/app/create
|
|
*
|
|
* Create an application.
|
|
* @param client_name Your application's name.
|
|
* @param options Form data.
|
|
*/
|
|
public async createApp(
|
|
client_name: string,
|
|
options: Partial<{
|
|
scopes: Array<string>;
|
|
redirect_uris: string;
|
|
website: string;
|
|
}> = {
|
|
scopes: MisskeyAPI.DEFAULT_SCOPE,
|
|
redirect_uris: this.baseUrl,
|
|
},
|
|
): Promise<OAuth.AppData> {
|
|
const redirect_uris = options.redirect_uris || this.baseUrl;
|
|
const scopes = options.scopes || MisskeyAPI.DEFAULT_SCOPE;
|
|
|
|
const params: {
|
|
name: string;
|
|
description: string;
|
|
permission: Array<string>;
|
|
callbackUrl: string;
|
|
} = {
|
|
name: client_name,
|
|
description: "",
|
|
permission: scopes,
|
|
callbackUrl: redirect_uris,
|
|
};
|
|
|
|
/**
|
|
* The response is:
|
|
{
|
|
"id": "xxxxxxxxxx",
|
|
"name": "string",
|
|
"callbackUrl": "string",
|
|
"permission": [
|
|
"string"
|
|
],
|
|
"secret": "string"
|
|
}
|
|
*/
|
|
return this.client
|
|
.post<MisskeyAPI.Entity.App>("/api/app/create", params)
|
|
.then((res: Response<MisskeyAPI.Entity.App>) => {
|
|
const appData: OAuth.AppDataFromServer = {
|
|
id: res.data.id,
|
|
name: res.data.name,
|
|
website: null,
|
|
redirect_uri: res.data.callbackUrl,
|
|
client_id: "",
|
|
client_secret: res.data.secret,
|
|
};
|
|
return OAuth.AppData.from(appData);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* POST /api/auth/session/generate
|
|
*/
|
|
public async generateAuthUrlAndToken(
|
|
clientSecret: string,
|
|
): Promise<MisskeyAPI.Entity.Session> {
|
|
return this.client
|
|
.post<MisskeyAPI.Entity.Session>("/api/auth/session/generate", {
|
|
appSecret: clientSecret,
|
|
})
|
|
.then((res: Response<MisskeyAPI.Entity.Session>) => res.data);
|
|
}
|
|
|
|
// ======================================
|
|
// apps
|
|
// ======================================
|
|
public async verifyAppCredentials(): Promise<Response<Entity.Application>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
// ======================================
|
|
// apps/oauth
|
|
// ======================================
|
|
/**
|
|
* POST /api/auth/session/userkey
|
|
*
|
|
* @param _client_id This parameter is not used in this method.
|
|
* @param client_secret Application secret key which will be provided in createApp.
|
|
* @param session_token Session token string which will be provided in generateAuthUrlAndToken.
|
|
* @param _redirect_uri This parameter is not used in this method.
|
|
*/
|
|
public async fetchAccessToken(
|
|
_client_id: string | null,
|
|
client_secret: string,
|
|
session_token: string,
|
|
_redirect_uri?: string,
|
|
): Promise<OAuth.TokenData> {
|
|
return this.client
|
|
.post<MisskeyAPI.Entity.UserKey>("/api/auth/session/userkey", {
|
|
appSecret: client_secret,
|
|
token: session_token,
|
|
})
|
|
.then((res) => {
|
|
const token = new OAuth.TokenData(
|
|
res.data.accessToken,
|
|
"misskey",
|
|
"",
|
|
0,
|
|
null,
|
|
null,
|
|
);
|
|
return token;
|
|
});
|
|
}
|
|
|
|
public async refreshToken(
|
|
_client_id: string,
|
|
_client_secret: string,
|
|
_refresh_token: string,
|
|
): Promise<OAuth.TokenData> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
public async revokeToken(
|
|
_client_id: string,
|
|
_client_secret: string,
|
|
_token: string,
|
|
): Promise<Response<{}>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
// ======================================
|
|
// accounts
|
|
// ======================================
|
|
public async registerAccount(
|
|
_username: string,
|
|
_email: string,
|
|
_password: string,
|
|
_agreement: boolean,
|
|
_locale: string,
|
|
_reason?: string | null,
|
|
): Promise<Response<Entity.Token>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* POST /api/i
|
|
*/
|
|
public async verifyAccountCredentials(): Promise<Response<Entity.Account>> {
|
|
return this.client
|
|
.post<MisskeyAPI.Entity.UserDetail>("/api/i")
|
|
.then((res) => {
|
|
return Object.assign(res, {
|
|
data: this.converter.userDetail(
|
|
res.data,
|
|
this.baseUrlToHost(this.baseUrl),
|
|
),
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* POST /api/i/update
|
|
*/
|
|
public async updateCredentials(options?: {
|
|
discoverable?: boolean;
|
|
bot?: boolean;
|
|
display_name?: string;
|
|
note?: string;
|
|
avatar?: string;
|
|
header?: string;
|
|
locked?: boolean;
|
|
source?: {
|
|
privacy?: string;
|
|
sensitive?: boolean;
|
|
language?: string;
|
|
} | null;
|
|
fields_attributes?: Array<{ name: string; value: string }>;
|
|
}): Promise<Response<Entity.Account>> {
|
|
let params = {};
|
|
if (options) {
|
|
if (options.bot !== undefined) {
|
|
params = Object.assign(params, {
|
|
isBot: options.bot,
|
|
});
|
|
}
|
|
if (options.display_name) {
|
|
params = Object.assign(params, {
|
|
name: options.display_name,
|
|
});
|
|
}
|
|
if (options.note) {
|
|
params = Object.assign(params, {
|
|
description: options.note,
|
|
});
|
|
}
|
|
if (options.locked !== undefined) {
|
|
params = Object.assign(params, {
|
|
isLocked: options.locked,
|
|
});
|
|
}
|
|
if (options.source) {
|
|
if (options.source.language) {
|
|
params = Object.assign(params, {
|
|
lang: options.source.language,
|
|
});
|
|
}
|
|
if (options.source.sensitive) {
|
|
params = Object.assign(params, {
|
|
alwaysMarkNsfw: options.source.sensitive,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return this.client
|
|
.post<MisskeyAPI.Entity.UserDetail>("/api/i", params)
|
|
.then((res) => {
|
|
return Object.assign(res, {
|
|
data: this.converter.userDetail(
|
|
res.data,
|
|
this.baseUrlToHost(this.baseUrl),
|
|
),
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* POST /api/users/show
|
|
*/
|
|
public async getAccount(id: string): Promise<Response<Entity.Account>> {
|
|
return this.client
|
|
.post<MisskeyAPI.Entity.UserDetail>("/api/users/show", {
|
|
userId: id,
|
|
})
|
|
.then((res) => {
|
|
return Object.assign(res, {
|
|
data: this.converter.userDetail(
|
|
res.data,
|
|
this.baseUrlToHost(this.baseUrl),
|
|
),
|
|
});
|
|
});
|
|
}
|
|
|
|
public async getAccountByName(
|
|
user: string,
|
|
host: string | null,
|
|
): Promise<Response<Entity.Account>> {
|
|
return this.client
|
|
.post<MisskeyAPI.Entity.UserDetail>("/api/users/show", {
|
|
username: user,
|
|
host: host ?? null,
|
|
})
|
|
.then((res) => {
|
|
return Object.assign(res, {
|
|
data: this.converter.userDetail(
|
|
res.data,
|
|
this.baseUrlToHost(this.baseUrl),
|
|
),
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* POST /api/users/notes
|
|
*/
|
|
public async getAccountStatuses(
|
|
id: string,
|
|
options?: {
|
|
limit?: number;
|
|
max_id?: string;
|
|
since_id?: string;
|
|
pinned?: boolean;
|
|
exclude_replies: boolean;
|
|
exclude_reblogs: boolean;
|
|
only_media?: boolean;
|
|
},
|
|
): Promise<Response<Array<Entity.Status>>> {
|
|
const accountCache = this.getFreshAccountCache();
|
|
|
|
if (options?.pinned) {
|
|
return this.client
|
|
.post<MisskeyAPI.Entity.UserDetail>("/api/users/show", {
|
|
userId: id,
|
|
})
|
|
.then(async (res) => {
|
|
if (res.data.pinnedNotes) {
|
|
return {
|
|
...res,
|
|
data: await Promise.all(
|
|
res.data.pinnedNotes.map((n) =>
|
|
this.noteWithDetails(
|
|
n,
|
|
this.baseUrlToHost(this.baseUrl),
|
|
accountCache,
|
|
),
|
|
),
|
|
),
|
|
};
|
|
}
|
|
return { ...res, data: [] };
|
|
});
|
|
}
|
|
|
|
let params = {
|
|
userId: id,
|
|
};
|
|
if (options) {
|
|
if (options.limit) {
|
|
params = Object.assign(params, {
|
|
limit: options.limit,
|
|
});
|
|
} else {
|
|
params = Object.assign(params, {
|
|
limit: 20,
|
|
});
|
|
}
|
|
if (options.max_id) {
|
|
params = Object.assign(params, {
|
|
untilId: options.max_id,
|
|
});
|
|
}
|
|
if (options.since_id) {
|
|
params = Object.assign(params, {
|
|
sinceId: options.since_id,
|
|
});
|
|
}
|
|
if (options.exclude_replies) {
|
|
params = Object.assign(params, {
|
|
includeReplies: false,
|
|
});
|
|
}
|
|
if (options.exclude_reblogs) {
|
|
params = Object.assign(params, {
|
|
includeMyRenotes: false,
|
|
});
|
|
}
|
|
if (options.only_media) {
|
|
params = Object.assign(params, {
|
|
withFiles: options.only_media,
|
|
});
|
|
}
|
|
} else {
|
|
params = Object.assign(params, {
|
|
limit: 20,
|
|
});
|
|
}
|
|
return this.client
|
|
.post<Array<MisskeyAPI.Entity.Note>>("/api/users/notes", params)
|
|
.then(async (res) => {
|
|
const statuses: Array<Entity.Status> = await Promise.all(
|
|
res.data.map((note) =>
|
|
this.noteWithDetails(
|
|
note,
|
|
this.baseUrlToHost(this.baseUrl),
|
|
accountCache,
|
|
),
|
|
),
|
|
);
|
|
return Object.assign(res, {
|
|
data: statuses,
|
|
});
|
|
});
|
|
}
|
|
|
|
public async getAccountFavourites(
|
|
id: string,
|
|
options?: {
|
|
limit?: number;
|
|
max_id?: string;
|
|
since_id?: string;
|
|
},
|
|
): Promise<Response<Array<Entity.Status>>> {
|
|
const accountCache = this.getFreshAccountCache();
|
|
|
|
let params = {
|
|
userId: id,
|
|
};
|
|
if (options) {
|
|
if (options.limit) {
|
|
params = Object.assign(params, {
|
|
limit: options.limit <= 100 ? options.limit : 100,
|
|
});
|
|
}
|
|
if (options.max_id) {
|
|
params = Object.assign(params, {
|
|
untilId: options.max_id,
|
|
});
|
|
}
|
|
if (options.since_id) {
|
|
params = Object.assign(params, {
|
|
sinceId: options.since_id,
|
|
});
|
|
}
|
|
}
|
|
return this.client
|
|
.post<Array<MisskeyAPI.Entity.Favorite>>("/api/users/reactions", params)
|
|
.then(async (res) => {
|
|
return Object.assign(res, {
|
|
data: await Promise.all(
|
|
res.data.map((fav) =>
|
|
this.noteWithDetails(
|
|
fav.note,
|
|
this.baseUrlToHost(this.baseUrl),
|
|
accountCache,
|
|
),
|
|
),
|
|
),
|
|
});
|
|
});
|
|
}
|
|
|
|
public async subscribeAccount(
|
|
_id: string,
|
|
): Promise<Response<Entity.Relationship>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
public async unsubscribeAccount(
|
|
_id: string,
|
|
): Promise<Response<Entity.Relationship>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* POST /api/users/followers
|
|
*/
|
|
public async getAccountFollowers(
|
|
id: string,
|
|
options?: {
|
|
limit?: number;
|
|
max_id?: string;
|
|
since_id?: string;
|
|
},
|
|
): Promise<Response<Array<Entity.Account>>> {
|
|
let params = {
|
|
userId: id,
|
|
};
|
|
if (options) {
|
|
if (options.limit) {
|
|
params = Object.assign(params, {
|
|
limit: options.limit <= 100 ? options.limit : 100,
|
|
});
|
|
} else {
|
|
params = Object.assign(params, {
|
|
limit: 40,
|
|
});
|
|
}
|
|
} else {
|
|
params = Object.assign(params, {
|
|
limit: 40,
|
|
});
|
|
}
|
|
return this.client
|
|
.post<Array<MisskeyAPI.Entity.Follower>>("/api/users/followers", params)
|
|
.then(async (res) => {
|
|
return Object.assign(res, {
|
|
data: await Promise.all(
|
|
res.data.map(async (f) =>
|
|
this.getAccount(f.followerId).then((p) => p.data),
|
|
),
|
|
),
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* POST /api/users/following
|
|
*/
|
|
public async getAccountFollowing(
|
|
id: string,
|
|
options?: {
|
|
limit?: number;
|
|
max_id?: string;
|
|
since_id?: string;
|
|
},
|
|
): Promise<Response<Array<Entity.Account>>> {
|
|
let params = {
|
|
userId: id,
|
|
};
|
|
if (options) {
|
|
if (options.limit) {
|
|
params = Object.assign(params, {
|
|
limit: options.limit <= 100 ? options.limit : 100,
|
|
});
|
|
}
|
|
}
|
|
return this.client
|
|
.post<Array<MisskeyAPI.Entity.Following>>("/api/users/following", params)
|
|
.then(async (res) => {
|
|
return Object.assign(res, {
|
|
data: await Promise.all(
|
|
res.data.map(async (f) =>
|
|
this.getAccount(f.followeeId).then((p) => p.data),
|
|
),
|
|
),
|
|
});
|
|
});
|
|
}
|
|
|
|
public async getAccountLists(
|
|
_id: string,
|
|
): Promise<Response<Array<Entity.List>>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
public async getIdentityProof(
|
|
_id: string,
|
|
): Promise<Response<Array<Entity.IdentityProof>>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* POST /api/following/create
|
|
*/
|
|
public async followAccount(
|
|
id: string,
|
|
_options?: { reblog?: boolean },
|
|
): Promise<Response<Entity.Relationship>> {
|
|
await this.client.post<{}>("/api/following/create", {
|
|
userId: id,
|
|
});
|
|
return this.client
|
|
.post<MisskeyAPI.Entity.Relation>("/api/users/relation", {
|
|
userId: id,
|
|
})
|
|
.then((res) => {
|
|
return Object.assign(res, {
|
|
data: this.converter.relation(res.data),
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* POST /api/following/delete
|
|
*/
|
|
public async unfollowAccount(
|
|
id: string,
|
|
): Promise<Response<Entity.Relationship>> {
|
|
await this.client.post<{}>("/api/following/delete", {
|
|
userId: id,
|
|
});
|
|
return this.client
|
|
.post<MisskeyAPI.Entity.Relation>("/api/users/relation", {
|
|
userId: id,
|
|
})
|
|
.then((res) => {
|
|
return Object.assign(res, {
|
|
data: this.converter.relation(res.data),
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* POST /api/blocking/create
|
|
*/
|
|
public async blockAccount(
|
|
id: string,
|
|
): Promise<Response<Entity.Relationship>> {
|
|
await this.client.post<{}>("/api/blocking/create", {
|
|
userId: id,
|
|
});
|
|
return this.client
|
|
.post<MisskeyAPI.Entity.Relation>("/api/users/relation", {
|
|
userId: id,
|
|
})
|
|
.then((res) => {
|
|
return Object.assign(res, {
|
|
data: this.converter.relation(res.data),
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* POST /api/blocking/delete
|
|
*/
|
|
public async unblockAccount(
|
|
id: string,
|
|
): Promise<Response<Entity.Relationship>> {
|
|
await this.client.post<{}>("/api/blocking/delete", {
|
|
userId: id,
|
|
});
|
|
return this.client
|
|
.post<MisskeyAPI.Entity.Relation>("/api/users/relation", {
|
|
userId: id,
|
|
})
|
|
.then((res) => {
|
|
return Object.assign(res, {
|
|
data: this.converter.relation(res.data),
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* POST /api/mute/create
|
|
*/
|
|
public async muteAccount(
|
|
id: string,
|
|
_notifications: boolean,
|
|
): Promise<Response<Entity.Relationship>> {
|
|
await this.client.post<{}>("/api/mute/create", {
|
|
userId: id,
|
|
});
|
|
return this.client
|
|
.post<MisskeyAPI.Entity.Relation>("/api/users/relation", {
|
|
userId: id,
|
|
})
|
|
.then((res) => {
|
|
return Object.assign(res, {
|
|
data: this.converter.relation(res.data),
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* POST /api/mute/delete
|
|
*/
|
|
public async unmuteAccount(
|
|
id: string,
|
|
): Promise<Response<Entity.Relationship>> {
|
|
await this.client.post<{}>("/api/mute/delete", {
|
|
userId: id,
|
|
});
|
|
return this.client
|
|
.post<MisskeyAPI.Entity.Relation>("/api/users/relation", {
|
|
userId: id,
|
|
})
|
|
.then((res) => {
|
|
return Object.assign(res, {
|
|
data: this.converter.relation(res.data),
|
|
});
|
|
});
|
|
}
|
|
|
|
public async pinAccount(_id: string): Promise<Response<Entity.Relationship>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
public async unpinAccount(
|
|
_id: string,
|
|
): Promise<Response<Entity.Relationship>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* POST /api/users/relation
|
|
*
|
|
* @param id The accountID, for example `'1sdfag'`
|
|
*/
|
|
public async getRelationship(
|
|
id: string,
|
|
): Promise<Response<Entity.Relationship>> {
|
|
return this.client
|
|
.post<MisskeyAPI.Entity.Relation>("/api/users/relation", {
|
|
userId: id,
|
|
})
|
|
.then((res) => {
|
|
return Object.assign(res, {
|
|
data: this.converter.relation(res.data),
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* POST /api/users/relation
|
|
*
|
|
* @param id Array of account ID, for example `['1sdfag', 'ds12aa']`.
|
|
*/
|
|
public async getRelationships(
|
|
ids: Array<string>,
|
|
): Promise<Response<Array<Entity.Relationship>>> {
|
|
return Promise.all(ids.map((id) => this.getRelationship(id))).then(
|
|
(results) => ({
|
|
...results[0],
|
|
data: results.map((r) => r.data),
|
|
}),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* POST /api/users/search
|
|
*/
|
|
public async searchAccount(
|
|
q: string,
|
|
options?: {
|
|
following?: boolean;
|
|
resolve?: boolean;
|
|
limit?: number;
|
|
max_id?: string;
|
|
since_id?: string;
|
|
},
|
|
): Promise<Response<Array<Entity.Account>>> {
|
|
let params = {
|
|
query: q,
|
|
detail: true,
|
|
};
|
|
if (options) {
|
|
if (options.resolve !== undefined) {
|
|
params = Object.assign(params, {
|
|
localOnly: options.resolve,
|
|
});
|
|
}
|
|
if (options.limit) {
|
|
params = Object.assign(params, {
|
|
limit: options.limit,
|
|
});
|
|
} else {
|
|
params = Object.assign(params, {
|
|
limit: 40,
|
|
});
|
|
}
|
|
} else {
|
|
params = Object.assign(params, {
|
|
limit: 40,
|
|
});
|
|
}
|
|
return this.client
|
|
.post<Array<MisskeyAPI.Entity.UserDetail>>("/api/users/search", params)
|
|
.then((res) => {
|
|
return Object.assign(res, {
|
|
data: res.data.map((u) =>
|
|
this.converter.userDetail(u, this.baseUrlToHost(this.baseUrl)),
|
|
),
|
|
});
|
|
});
|
|
}
|
|
|
|
// ======================================
|
|
// accounts/bookmarks
|
|
// ======================================
|
|
/**
|
|
* POST /api/i/favorites
|
|
*/
|
|
public async getBookmarks(options?: {
|
|
limit?: number;
|
|
max_id?: string;
|
|
since_id?: string;
|
|
min_id?: string;
|
|
}): Promise<Response<Array<Entity.Status>>> {
|
|
const accountCache = this.getFreshAccountCache();
|
|
|
|
let params = {};
|
|
if (options) {
|
|
if (options.limit) {
|
|
params = Object.assign(params, {
|
|
limit: options.limit <= 100 ? options.limit : 100,
|
|
});
|
|
} else {
|
|
params = Object.assign(params, {
|
|
limit: 40,
|
|
});
|
|
}
|
|
if (options.max_id) {
|
|
params = Object.assign(params, {
|
|
untilId: options.max_id,
|
|
});
|
|
}
|
|
if (options.min_id) {
|
|
params = Object.assign(params, {
|
|
sinceId: options.min_id,
|
|
});
|
|
}
|
|
} else {
|
|
params = Object.assign(params, {
|
|
limit: 40,
|
|
});
|
|
}
|
|
return this.client
|
|
.post<Array<MisskeyAPI.Entity.Favorite>>("/api/i/favorites", params)
|
|
.then(async (res) => {
|
|
return Object.assign(res, {
|
|
data: await Promise.all(
|
|
res.data.map((s) =>
|
|
this.noteWithDetails(
|
|
s.note,
|
|
this.baseUrlToHost(this.baseUrl),
|
|
accountCache,
|
|
),
|
|
),
|
|
),
|
|
});
|
|
});
|
|
}
|
|
|
|
// ======================================
|
|
// accounts/favourites
|
|
// ======================================
|
|
public async getFavourites(options?: {
|
|
limit?: number;
|
|
max_id?: string;
|
|
min_id?: string;
|
|
}): Promise<Response<Array<Entity.Status>>> {
|
|
const userId = await this.client
|
|
.post<MisskeyAPI.Entity.UserDetail>("/api/i")
|
|
.then((res) => res.data.id);
|
|
return this.getAccountFavourites(userId, options);
|
|
}
|
|
|
|
// ======================================
|
|
// accounts/mutes
|
|
// ======================================
|
|
/**
|
|
* POST /api/mute/list
|
|
*/
|
|
public async getMutes(options?: {
|
|
limit?: number;
|
|
max_id?: string;
|
|
min_id?: string;
|
|
}): Promise<Response<Array<Entity.Account>>> {
|
|
let params = {};
|
|
if (options) {
|
|
if (options.limit) {
|
|
params = Object.assign(params, {
|
|
limit: options.limit,
|
|
});
|
|
} else {
|
|
params = Object.assign(params, {
|
|
limit: 40,
|
|
});
|
|
}
|
|
if (options.max_id) {
|
|
params = Object.assign(params, {
|
|
untilId: options.max_id,
|
|
});
|
|
}
|
|
if (options.min_id) {
|
|
params = Object.assign(params, {
|
|
sinceId: options.min_id,
|
|
});
|
|
}
|
|
} else {
|
|
params = Object.assign(params, {
|
|
limit: 40,
|
|
});
|
|
}
|
|
return this.client
|
|
.post<Array<MisskeyAPI.Entity.Mute>>("/api/mute/list", params)
|
|
.then((res) => {
|
|
return Object.assign(res, {
|
|
data: res.data.map((mute) =>
|
|
this.converter.userDetail(
|
|
mute.mutee,
|
|
this.baseUrlToHost(this.baseUrl),
|
|
),
|
|
),
|
|
});
|
|
});
|
|
}
|
|
|
|
// ======================================
|
|
// accounts/blocks
|
|
// ======================================
|
|
/**
|
|
* POST /api/blocking/list
|
|
*/
|
|
public async getBlocks(options?: {
|
|
limit?: number;
|
|
max_id?: string;
|
|
min_id?: string;
|
|
}): Promise<Response<Array<Entity.Account>>> {
|
|
let params = {};
|
|
if (options) {
|
|
if (options.limit) {
|
|
params = Object.assign(params, {
|
|
limit: options.limit,
|
|
});
|
|
} else {
|
|
params = Object.assign(params, {
|
|
limit: 40,
|
|
});
|
|
}
|
|
if (options.max_id) {
|
|
params = Object.assign(params, {
|
|
untilId: options.max_id,
|
|
});
|
|
}
|
|
if (options.min_id) {
|
|
params = Object.assign(params, {
|
|
sinceId: options.min_id,
|
|
});
|
|
}
|
|
} else {
|
|
params = Object.assign(params, {
|
|
limit: 40,
|
|
});
|
|
}
|
|
return this.client
|
|
.post<Array<MisskeyAPI.Entity.Blocking>>("/api/blocking/list", params)
|
|
.then((res) => {
|
|
return Object.assign(res, {
|
|
data: res.data.map((blocking) =>
|
|
this.converter.userDetail(
|
|
blocking.blockee,
|
|
this.baseUrlToHost(this.baseUrl),
|
|
),
|
|
),
|
|
});
|
|
});
|
|
}
|
|
|
|
// ======================================
|
|
// accounts/domain_blocks
|
|
// ======================================
|
|
public async getDomainBlocks(_options?: {
|
|
limit?: number;
|
|
max_id?: string;
|
|
min_id?: string;
|
|
}): Promise<Response<Array<string>>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
public async blockDomain(_domain: string): Promise<Response<{}>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
public async unblockDomain(_domain: string): Promise<Response<{}>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
// ======================================
|
|
// accounts/filters
|
|
// ======================================
|
|
public async getFilters(): Promise<Response<Array<Entity.Filter>>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
public async getFilter(_id: string): Promise<Response<Entity.Filter>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
public async createFilter(
|
|
_phrase: string,
|
|
_context: Array<string>,
|
|
_options?: {
|
|
irreversible?: boolean;
|
|
whole_word?: boolean;
|
|
expires_in?: string;
|
|
},
|
|
): Promise<Response<Entity.Filter>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
public async updateFilter(
|
|
_id: string,
|
|
_phrase: string,
|
|
_context: Array<string>,
|
|
_options?: {
|
|
irreversible?: boolean;
|
|
whole_word?: boolean;
|
|
expires_in?: string;
|
|
},
|
|
): Promise<Response<Entity.Filter>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
public async deleteFilter(_id: string): Promise<Response<Entity.Filter>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
// ======================================
|
|
// accounts/reports
|
|
// ======================================
|
|
/**
|
|
* POST /api/users/report-abuse
|
|
*/
|
|
public async report(
|
|
account_id: string,
|
|
comment: string,
|
|
_options?: {
|
|
status_ids?: Array<string>;
|
|
forward?: boolean;
|
|
},
|
|
): Promise<Response<Entity.Report>> {
|
|
return this.client
|
|
.post<{}>("/api/users/report-abuse", {
|
|
userId: account_id,
|
|
comment: comment,
|
|
})
|
|
.then((res) => {
|
|
return Object.assign(res, {
|
|
data: {
|
|
id: "",
|
|
action_taken: "",
|
|
comment: comment,
|
|
account_id: account_id,
|
|
status_ids: [],
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
// ======================================
|
|
// accounts/follow_requests
|
|
// ======================================
|
|
/**
|
|
* POST /api/following/requests/list
|
|
*/
|
|
public async getFollowRequests(
|
|
_limit?: number,
|
|
): Promise<Response<Array<Entity.Account>>> {
|
|
return this.client
|
|
.post<Array<MisskeyAPI.Entity.FollowRequest>>(
|
|
"/api/following/requests/list",
|
|
)
|
|
.then((res) => {
|
|
return Object.assign(res, {
|
|
data: res.data.map((r) => this.converter.user(r.follower)),
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* POST /api/following/requests/accept
|
|
*/
|
|
public async acceptFollowRequest(
|
|
id: string,
|
|
): Promise<Response<Entity.Relationship>> {
|
|
await this.client.post<{}>("/api/following/requests/accept", {
|
|
userId: id,
|
|
});
|
|
return this.client
|
|
.post<MisskeyAPI.Entity.Relation>("/api/users/relation", {
|
|
userId: id,
|
|
})
|
|
.then((res) => {
|
|
return Object.assign(res, {
|
|
data: this.converter.relation(res.data),
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* POST /api/following/requests/reject
|
|
*/
|
|
public async rejectFollowRequest(
|
|
id: string,
|
|
): Promise<Response<Entity.Relationship>> {
|
|
await this.client.post<{}>("/api/following/requests/reject", {
|
|
userId: id,
|
|
});
|
|
return this.client
|
|
.post<MisskeyAPI.Entity.Relation>("/api/users/relation", {
|
|
userId: id,
|
|
})
|
|
.then((res) => {
|
|
return Object.assign(res, {
|
|
data: this.converter.relation(res.data),
|
|
});
|
|
});
|
|
}
|
|
|
|
// ======================================
|
|
// accounts/endorsements
|
|
// ======================================
|
|
public async getEndorsements(_options?: {
|
|
limit?: number;
|
|
max_id?: string;
|
|
since_id?: string;
|
|
}): Promise<Response<Array<Entity.Account>>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
// ======================================
|
|
// accounts/featured_tags
|
|
// ======================================
|
|
public async getFeaturedTags(): Promise<Response<Array<Entity.FeaturedTag>>> {
|
|
return this.getAccountFeaturedTags();
|
|
}
|
|
|
|
public async getAccountFeaturedTags(): Promise<
|
|
Response<Array<Entity.FeaturedTag>>
|
|
> {
|
|
const tags: Entity.FeaturedTag[] = [];
|
|
const res: Response = {
|
|
headers: undefined,
|
|
statusText: "",
|
|
status: 200,
|
|
data: tags,
|
|
};
|
|
return new Promise((resolve) => resolve(res));
|
|
}
|
|
|
|
public async createFeaturedTag(
|
|
_name: string,
|
|
): Promise<Response<Entity.FeaturedTag>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
public async deleteFeaturedTag(_id: string): Promise<Response<{}>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
public async getSuggestedTags(): Promise<Response<Array<Entity.Tag>>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
// ======================================
|
|
// accounts/preferences
|
|
// ======================================
|
|
public async getPreferences(): Promise<Response<Entity.Preferences>> {
|
|
return this.client
|
|
.post<MisskeyAPI.Entity.UserDetailMe>("/api/i")
|
|
.then(async (res) => {
|
|
return Object.assign(res, {
|
|
data: this.converter.userPreferences(
|
|
res.data,
|
|
await this.getDefaultPostPrivacy(),
|
|
),
|
|
});
|
|
});
|
|
}
|
|
|
|
// ======================================
|
|
// accounts/suggestions
|
|
// ======================================
|
|
/**
|
|
* POST /api/users/recommendation
|
|
*/
|
|
public async getSuggestions(
|
|
limit?: number,
|
|
): Promise<Response<Array<Entity.Account>>> {
|
|
let params = {};
|
|
if (limit) {
|
|
params = Object.assign(params, {
|
|
limit: limit,
|
|
});
|
|
}
|
|
return this.client
|
|
.post<Array<MisskeyAPI.Entity.UserDetail>>(
|
|
"/api/users/recommendation",
|
|
params,
|
|
)
|
|
.then((res) => ({
|
|
...res,
|
|
data: res.data.map((u) =>
|
|
this.converter.userDetail(u, this.baseUrlToHost(this.baseUrl)),
|
|
),
|
|
}));
|
|
}
|
|
|
|
// ======================================
|
|
// accounts/tags
|
|
// ======================================
|
|
public async getFollowedTags(): Promise<Response<Array<Entity.Tag>>> {
|
|
const tags: Entity.Tag[] = [];
|
|
const res: Response = {
|
|
headers: undefined,
|
|
statusText: "",
|
|
status: 200,
|
|
data: tags,
|
|
};
|
|
return new Promise((resolve) => resolve(res));
|
|
}
|
|
|
|
public async getTag(_id: string): Promise<Response<Entity.Tag>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
public async followTag(_id: string): Promise<Response<Entity.Tag>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
public async unfollowTag(_id: string): Promise<Response<Entity.Tag>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
// ======================================
|
|
// statuses
|
|
// ======================================
|
|
public async postStatus(
|
|
status: string,
|
|
options?: {
|
|
media_ids?: Array<string>;
|
|
poll?: {
|
|
options: Array<string>;
|
|
expires_in: number;
|
|
multiple?: boolean;
|
|
hide_totals?: boolean;
|
|
};
|
|
in_reply_to_id?: string;
|
|
sensitive?: boolean;
|
|
spoiler_text?: string;
|
|
visibility?: "public" | "unlisted" | "private" | "direct";
|
|
scheduled_at?: string;
|
|
language?: string;
|
|
quote_id?: string;
|
|
},
|
|
): Promise<Response<Entity.Status>> {
|
|
let params = {
|
|
text: status,
|
|
};
|
|
if (options) {
|
|
if (options.media_ids) {
|
|
params = Object.assign(params, {
|
|
fileIds: options.media_ids,
|
|
});
|
|
}
|
|
if (options.poll) {
|
|
let pollParam = {
|
|
choices: options.poll.options,
|
|
expiresAt: null,
|
|
expiredAfter: options.poll.expires_in * 1000,
|
|
};
|
|
if (options.poll.multiple !== undefined) {
|
|
pollParam = Object.assign(pollParam, {
|
|
multiple: options.poll.multiple,
|
|
});
|
|
}
|
|
params = Object.assign(params, {
|
|
poll: pollParam,
|
|
});
|
|
}
|
|
if (options.in_reply_to_id) {
|
|
params = Object.assign(params, {
|
|
replyId: options.in_reply_to_id,
|
|
});
|
|
}
|
|
if (options.sensitive) {
|
|
params = Object.assign(params, {
|
|
cw: "",
|
|
});
|
|
}
|
|
if (options.spoiler_text) {
|
|
params = Object.assign(params, {
|
|
cw: options.spoiler_text,
|
|
});
|
|
}
|
|
if (options.visibility) {
|
|
params = Object.assign(params, {
|
|
visibility: this.converter.encodeVisibility(options.visibility),
|
|
});
|
|
}
|
|
if (options.quote_id) {
|
|
params = Object.assign(params, {
|
|
renoteId: options.quote_id,
|
|
});
|
|
}
|
|
}
|
|
return this.client
|
|
.post<MisskeyAPI.Entity.CreatedNote>("/api/notes/create", params)
|
|
.then(async (res) => ({
|
|
...res,
|
|
data: await this.noteWithDetails(
|
|
res.data.createdNote,
|
|
this.baseUrlToHost(this.baseUrl),
|
|
this.getFreshAccountCache(),
|
|
),
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* POST /api/notes/show
|
|
*/
|
|
public async getStatus(id: string): Promise<Response<Entity.Status>> {
|
|
return this.client
|
|
.post<MisskeyAPI.Entity.Note>("/api/notes/show", {
|
|
noteId: id,
|
|
})
|
|
.then(async (res) => ({
|
|
...res,
|
|
data: await this.noteWithDetails(
|
|
res.data,
|
|
this.baseUrlToHost(this.baseUrl),
|
|
this.getFreshAccountCache(),
|
|
),
|
|
}));
|
|
}
|
|
|
|
private getFreshAccountCache(): AccountCache {
|
|
return {
|
|
locks: new AsyncLock(),
|
|
accounts: [],
|
|
};
|
|
}
|
|
|
|
public async notificationWithDetails(
|
|
n: MisskeyAPI.Entity.Notification,
|
|
host: string,
|
|
cache: AccountCache,
|
|
): Promise<MegalodonEntity.Notification> {
|
|
const notification = this.converter.notification(n, host);
|
|
if (n.note)
|
|
notification.status = await this.noteWithDetails(n.note, host, cache);
|
|
if (notification.account)
|
|
notification.account = (
|
|
await this.getAccount(notification.account.id)
|
|
).data;
|
|
return notification;
|
|
}
|
|
|
|
public async noteWithDetails(
|
|
n: MisskeyAPI.Entity.Note,
|
|
host: string,
|
|
cache: AccountCache,
|
|
): Promise<MegalodonEntity.Status> {
|
|
const status = await this.addUserDetailsToStatus(
|
|
this.converter.note(n, host),
|
|
cache,
|
|
);
|
|
status.bookmarked = await this.isStatusBookmarked(n.id);
|
|
return this.addMentionsToStatus(status, cache);
|
|
}
|
|
|
|
public async isStatusBookmarked(id: string): Promise<boolean> {
|
|
return this.client
|
|
.post<MisskeyAPI.Entity.State>("/api/notes/state", {
|
|
noteId: id,
|
|
})
|
|
.then((p) => p.data.isFavorited ?? false);
|
|
}
|
|
|
|
public async addUserDetailsToStatus(
|
|
status: Entity.Status,
|
|
cache: AccountCache,
|
|
): Promise<Entity.Status> {
|
|
if (
|
|
status.account.followers_count === 0 &&
|
|
status.account.followers_count === 0 &&
|
|
status.account.statuses_count === 0
|
|
)
|
|
status.account =
|
|
(await this.getAccountCached(
|
|
status.account.id,
|
|
status.account.acct,
|
|
cache,
|
|
)) ?? status.account;
|
|
|
|
if (status.reblog != null)
|
|
status.reblog = await this.addUserDetailsToStatus(status.reblog, cache);
|
|
|
|
if (status.quote != null)
|
|
status.quote = await this.addUserDetailsToStatus(status.quote, cache);
|
|
|
|
return status;
|
|
}
|
|
|
|
public async addMentionsToStatus(
|
|
status: Entity.Status,
|
|
cache: AccountCache,
|
|
): Promise<Entity.Status> {
|
|
if (status.mentions.length > 0) return status;
|
|
|
|
if (status.reblog != null)
|
|
status.reblog = await this.addMentionsToStatus(status.reblog, cache);
|
|
|
|
if (status.quote != null)
|
|
status.quote = await this.addMentionsToStatus(status.quote, cache);
|
|
|
|
const idx = status.account.acct.indexOf('@');
|
|
const origin = idx < 0 ? null : status.account.acct.substring(idx + 1);
|
|
|
|
status.mentions = (
|
|
await this.getMentions(status.plain_content!, origin, cache)
|
|
).filter((p) => p != null);
|
|
for (const m of status.mentions.filter(
|
|
(value, index, array) => array.indexOf(value) === index,
|
|
)) {
|
|
const regexFull = new RegExp(`(?<=^|\\s|>)@${m.acct}(?=[^a-zA-Z0-9]|$)`, 'gi');
|
|
const regexLocalUser = new RegExp(`(?<=^|\\s|>)@${m.acct}@${this.baseUrlToHost(this.baseUrl)}(?=[^a-zA-Z0-9]|$)`, 'gi');
|
|
const regexRemoteUser = new RegExp(`(?<=^|\\s|>)@${m.username}(?=[^a-zA-Z0-9@]|$)`, 'gi');
|
|
|
|
if (m.acct == m.username) {
|
|
status.content = status.content.replace(
|
|
regexLocalUser,
|
|
`@${m.acct}`,
|
|
);
|
|
}
|
|
|
|
else if (!status.content.match(regexFull)) {
|
|
status.content = status.content.replace(
|
|
regexRemoteUser,
|
|
`@${m.acct}`,
|
|
);
|
|
}
|
|
|
|
status.content = status.content.replace(
|
|
regexFull,
|
|
`<a href="${m.url}" class="u-url mention" rel="nofollow noopener noreferrer" target="_blank">@${m.acct}</a>`,
|
|
);
|
|
}
|
|
return status;
|
|
}
|
|
|
|
public async getMentions(
|
|
text: string,
|
|
origin: string | null,
|
|
cache: AccountCache,
|
|
): Promise<Entity.Mention[]> {
|
|
const mentions: Entity.Mention[] = [];
|
|
|
|
if (text == undefined) return mentions;
|
|
|
|
const mentionMatch = text.matchAll(
|
|
/(?<=^|\s)@(?<user>[a-zA-Z0-9_]+)(?:@(?<host>[a-zA-Z0-9-.]+\.[a-zA-Z0-9-]+)|)(?=[^a-zA-Z0-9]|$)/g,
|
|
);
|
|
|
|
for (const m of mentionMatch) {
|
|
try {
|
|
if (m.groups == null) continue;
|
|
|
|
const account = await this.getAccountByNameCached(
|
|
m.groups.user,
|
|
m.groups.host ?? origin,
|
|
cache,
|
|
);
|
|
|
|
if (account == null) continue;
|
|
|
|
mentions.push({
|
|
id: account.id,
|
|
url: account.url,
|
|
username: account.username,
|
|
acct: account.acct,
|
|
});
|
|
} catch {}
|
|
}
|
|
|
|
return mentions;
|
|
}
|
|
|
|
public async getAccountByNameCached(
|
|
user: string,
|
|
host: string | null,
|
|
cache: AccountCache,
|
|
): Promise<Entity.Account | undefined | null> {
|
|
const acctToFind = host == null ? user : `${user}@${host}`;
|
|
|
|
return await cache.locks.acquire(acctToFind, async () => {
|
|
const cacheHit = cache.accounts.find((p) => p.acct === acctToFind);
|
|
const account =
|
|
cacheHit ?? (await this.getAccountByName(user, host ?? null)).data;
|
|
|
|
if (!account) {
|
|
return null;
|
|
}
|
|
|
|
if (cacheHit == null) {
|
|
cache.accounts.push(account);
|
|
}
|
|
|
|
return account;
|
|
});
|
|
}
|
|
|
|
public async getAccountCached(
|
|
id: string,
|
|
acct: string,
|
|
cache: AccountCache,
|
|
): Promise<Entity.Account | undefined | null> {
|
|
return await cache.locks.acquire(acct, async () => {
|
|
const cacheHit = cache.accounts.find((p) => p.id === id);
|
|
const account = cacheHit ?? (await this.getAccount(id)).data;
|
|
|
|
if (!account) {
|
|
return null;
|
|
}
|
|
|
|
if (cacheHit == null) {
|
|
cache.accounts.push(account);
|
|
}
|
|
|
|
return account;
|
|
});
|
|
}
|
|
|
|
public async editStatus(
|
|
_id: string,
|
|
_options: {
|
|
status?: string;
|
|
spoiler_text?: string;
|
|
sensitive?: boolean;
|
|
media_ids?: Array<string>;
|
|
poll?: {
|
|
options?: Array<string>;
|
|
expires_in?: number;
|
|
multiple?: boolean;
|
|
hide_totals?: boolean;
|
|
};
|
|
},
|
|
): Promise<Response<Entity.Status>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* POST /api/notes/delete
|
|
*/
|
|
public async deleteStatus(id: string): Promise<Response<{}>> {
|
|
return this.client.post<{}>("/api/notes/delete", {
|
|
noteId: id,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* POST /api/notes/children
|
|
*/
|
|
public async getStatusContext(
|
|
id: string,
|
|
options?: { limit?: number; max_id?: string; since_id?: string },
|
|
): Promise<Response<Entity.Context>> {
|
|
let params = {
|
|
noteId: id,
|
|
};
|
|
if (options) {
|
|
if (options.limit) {
|
|
params = Object.assign(params, {
|
|
limit: options.limit,
|
|
depth: 12,
|
|
});
|
|
} else {
|
|
params = Object.assign(params, {
|
|
limit: 30,
|
|
depth: 12,
|
|
});
|
|
}
|
|
if (options.max_id) {
|
|
params = Object.assign(params, {
|
|
untilId: options.max_id,
|
|
});
|
|
}
|
|
if (options.since_id) {
|
|
params = Object.assign(params, {
|
|
sinceId: options.since_id,
|
|
});
|
|
}
|
|
} else {
|
|
params = Object.assign(params, {
|
|
limit: 30,
|
|
depth: 12,
|
|
});
|
|
}
|
|
return this.client
|
|
.post<Array<MisskeyAPI.Entity.Note>>("/api/notes/children", params)
|
|
.then(async (res) => {
|
|
const accountCache = this.getFreshAccountCache();
|
|
const conversation = await this.client.post<
|
|
Array<MisskeyAPI.Entity.Note>
|
|
>("/api/notes/conversation", params);
|
|
const parents = await Promise.all(
|
|
conversation.data.map((n) =>
|
|
this.noteWithDetails(
|
|
n,
|
|
this.baseUrlToHost(this.baseUrl),
|
|
accountCache,
|
|
),
|
|
),
|
|
);
|
|
|
|
const context: Entity.Context = {
|
|
ancestors: parents.reverse(),
|
|
descendants: this.dfs(
|
|
await Promise.all(
|
|
res.data.map((n) =>
|
|
this.noteWithDetails(
|
|
n,
|
|
this.baseUrlToHost(this.baseUrl),
|
|
accountCache,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
};
|
|
return {
|
|
...res,
|
|
data: context,
|
|
};
|
|
});
|
|
}
|
|
|
|
private dfs(graph: Entity.Status[]) {
|
|
// we don't need to run dfs if we have zero or one elements
|
|
if (graph.length <= 1) {
|
|
return graph;
|
|
}
|
|
|
|
// sort the graph first, so we can grab the correct starting point
|
|
graph = graph.sort((a, b) => {
|
|
if (a.id < b.id) return -1;
|
|
if (a.id > b.id) return 1;
|
|
return 0;
|
|
});
|
|
|
|
const initialPostId = graph[0].in_reply_to_id;
|
|
|
|
// populate stack with all top level replies
|
|
const stack = graph
|
|
.filter((reply) => reply.in_reply_to_id === initialPostId)
|
|
.reverse();
|
|
const visited = new Set();
|
|
const result = [];
|
|
|
|
while (stack.length) {
|
|
const currentPost = stack.pop();
|
|
|
|
if (currentPost === undefined) return result;
|
|
|
|
if (!visited.has(currentPost)) {
|
|
visited.add(currentPost);
|
|
result.push(currentPost);
|
|
|
|
for (const reply of graph
|
|
.filter((reply) => reply.in_reply_to_id === currentPost.id)
|
|
.reverse()) {
|
|
stack.push(reply);
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
public async getStatusHistory(): Promise<Response<Array<Entity.StatusEdit>>> {
|
|
// FIXME: stub, implement once we have note edit history in the database
|
|
const history: Entity.StatusEdit[] = [];
|
|
const res: Response = {
|
|
headers: undefined,
|
|
statusText: "",
|
|
status: 200,
|
|
data: history,
|
|
};
|
|
return new Promise((resolve) => resolve(res));
|
|
}
|
|
|
|
/**
|
|
* POST /api/notes/renotes
|
|
*/
|
|
public async getStatusRebloggedBy(
|
|
id: string,
|
|
): Promise<Response<Array<Entity.Account>>> {
|
|
return this.client
|
|
.post<Array<MisskeyAPI.Entity.Note>>("/api/notes/renotes", {
|
|
noteId: id,
|
|
})
|
|
.then(async (res) => ({
|
|
...res,
|
|
data: (
|
|
await Promise.all(res.data.map((n) => this.getAccount(n.user.id)))
|
|
).map((p) => p.data),
|
|
}));
|
|
}
|
|
|
|
public async getStatusFavouritedBy(
|
|
id: string,
|
|
): Promise<Response<Array<Entity.Account>>> {
|
|
return this.client
|
|
.post<Array<MisskeyAPI.Entity.Reaction>>("/api/notes/reactions", {
|
|
noteId: id,
|
|
})
|
|
.then(async (res) => ({
|
|
...res,
|
|
data: (
|
|
await Promise.all(res.data.map((n) => this.getAccount(n.user.id)))
|
|
).map((p) => p.data),
|
|
}));
|
|
}
|
|
|
|
public async favouriteStatus(id: string): Promise<Response<Entity.Status>> {
|
|
return this.createEmojiReaction(id, await this.getDefaultFavoriteEmoji());
|
|
}
|
|
|
|
private async getDefaultFavoriteEmoji(): Promise<string> {
|
|
// NOTE: get-unsecure is calckey's extension.
|
|
// Misskey doesn't have this endpoint and regular `/i/registry/get` won't work
|
|
// unless you have a 'nativeToken', which is reserved for the frontend webapp.
|
|
|
|
return await this.client
|
|
.post<Array<string>>("/api/i/registry/get-unsecure", {
|
|
key: "reactions",
|
|
scope: ["client", "base"],
|
|
})
|
|
.then((res) => res.data[0] ?? "⭐");
|
|
}
|
|
|
|
private async getDefaultPostPrivacy(): Promise<
|
|
"public" | "unlisted" | "private" | "direct"
|
|
> {
|
|
// NOTE: get-unsecure is calckey's extension.
|
|
// Misskey doesn't have this endpoint and regular `/i/registry/get` won't work
|
|
// unless you have a 'nativeToken', which is reserved for the frontend webapp.
|
|
|
|
return this.client
|
|
.post<string>("/api/i/registry/get-unsecure", {
|
|
key: "defaultNoteVisibility",
|
|
scope: ["client", "base"],
|
|
})
|
|
.then((res) => {
|
|
if (
|
|
!res.data ||
|
|
(res.data != "public" &&
|
|
res.data != "home" &&
|
|
res.data != "followers" &&
|
|
res.data != "specified")
|
|
)
|
|
return "public";
|
|
return this.converter.visibility(res.data);
|
|
})
|
|
.catch((_) => "public");
|
|
}
|
|
|
|
public async unfavouriteStatus(id: string): Promise<Response<Entity.Status>> {
|
|
// NOTE: Misskey allows only one reaction per status, so we don't need to care what that emoji was.
|
|
return this.deleteEmojiReaction(id, "");
|
|
}
|
|
|
|
/**
|
|
* POST /api/notes/create
|
|
*/
|
|
public async reblogStatus(id: string): Promise<Response<Entity.Status>> {
|
|
return this.client
|
|
.post<MisskeyAPI.Entity.CreatedNote>("/api/notes/create", {
|
|
renoteId: id,
|
|
})
|
|
.then(async (res) => ({
|
|
...res,
|
|
data: await this.noteWithDetails(
|
|
res.data.createdNote,
|
|
this.baseUrlToHost(this.baseUrl),
|
|
this.getFreshAccountCache(),
|
|
),
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* POST /api/notes/unrenote
|
|
*/
|
|
public async unreblogStatus(id: string): Promise<Response<Entity.Status>> {
|
|
await this.client.post<{}>("/api/notes/unrenote", {
|
|
noteId: id,
|
|
});
|
|
return this.client
|
|
.post<MisskeyAPI.Entity.Note>("/api/notes/show", {
|
|
noteId: id,
|
|
})
|
|
.then(async (res) => ({
|
|
...res,
|
|
data: await this.noteWithDetails(
|
|
res.data,
|
|
this.baseUrlToHost(this.baseUrl),
|
|
this.getFreshAccountCache(),
|
|
),
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* POST /api/notes/favorites/create
|
|
*/
|
|
public async bookmarkStatus(id: string): Promise<Response<Entity.Status>> {
|
|
await this.client.post<{}>("/api/notes/favorites/create", {
|
|
noteId: id,
|
|
});
|
|
return this.client
|
|
.post<MisskeyAPI.Entity.Note>("/api/notes/show", {
|
|
noteId: id,
|
|
})
|
|
.then(async (res) => ({
|
|
...res,
|
|
data: await this.noteWithDetails(
|
|
res.data,
|
|
this.baseUrlToHost(this.baseUrl),
|
|
this.getFreshAccountCache(),
|
|
),
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* POST /api/notes/favorites/delete
|
|
*/
|
|
public async unbookmarkStatus(id: string): Promise<Response<Entity.Status>> {
|
|
await this.client.post<{}>("/api/notes/favorites/delete", {
|
|
noteId: id,
|
|
});
|
|
return this.client
|
|
.post<MisskeyAPI.Entity.Note>("/api/notes/show", {
|
|
noteId: id,
|
|
})
|
|
.then(async (res) => ({
|
|
...res,
|
|
data: await this.noteWithDetails(
|
|
res.data,
|
|
this.baseUrlToHost(this.baseUrl),
|
|
this.getFreshAccountCache(),
|
|
),
|
|
}));
|
|
}
|
|
|
|
public async muteStatus(_id: string): Promise<Response<Entity.Status>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
public async unmuteStatus(_id: string): Promise<Response<Entity.Status>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* POST /api/i/pin
|
|
*/
|
|
public async pinStatus(id: string): Promise<Response<Entity.Status>> {
|
|
await this.client.post<{}>("/api/i/pin", {
|
|
noteId: id,
|
|
});
|
|
return this.client
|
|
.post<MisskeyAPI.Entity.Note>("/api/notes/show", {
|
|
noteId: id,
|
|
})
|
|
.then(async (res) => ({
|
|
...res,
|
|
data: await this.noteWithDetails(
|
|
res.data,
|
|
this.baseUrlToHost(this.baseUrl),
|
|
this.getFreshAccountCache(),
|
|
),
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* POST /api/i/unpin
|
|
*/
|
|
public async unpinStatus(id: string): Promise<Response<Entity.Status>> {
|
|
await this.client.post<{}>("/api/i/unpin", {
|
|
noteId: id,
|
|
});
|
|
return this.client
|
|
.post<MisskeyAPI.Entity.Note>("/api/notes/show", {
|
|
noteId: id,
|
|
})
|
|
.then(async (res) => ({
|
|
...res,
|
|
data: await this.noteWithDetails(
|
|
res.data,
|
|
this.baseUrlToHost(this.baseUrl),
|
|
this.getFreshAccountCache(),
|
|
),
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Convert a Unicode emoji or custom emoji name to a Misskey reaction.
|
|
* @see Misskey's reaction-lib.ts
|
|
*/
|
|
private reactionName(name: string): string {
|
|
// See: https://github.com/tc39/proposal-regexp-unicode-property-escapes#matching-emoji
|
|
const isUnicodeEmoji = /\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation}|\p{Emoji}\uFE0F/gu.test(name);
|
|
if (isUnicodeEmoji) {
|
|
return name;
|
|
}
|
|
return `:${name}:`;
|
|
}
|
|
|
|
/**
|
|
* POST /api/notes/reactions/create
|
|
*/
|
|
public async reactStatus(id: string, name: string): Promise<Response<Entity.Status>> {
|
|
await this.client.post<{}>("/api/notes/reactions/create", {
|
|
noteId: id,
|
|
reaction: this.reactionName(name),
|
|
});
|
|
return this.client
|
|
.post<MisskeyAPI.Entity.Note>("/api/notes/show", {
|
|
noteId: id,
|
|
})
|
|
.then(async (res) => ({
|
|
...res,
|
|
data: await this.noteWithDetails(
|
|
res.data,
|
|
this.baseUrlToHost(this.baseUrl),
|
|
this.getFreshAccountCache(),
|
|
),
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* POST /api/notes/reactions/delete
|
|
*/
|
|
public async unreactStatus(id: string, name: string): Promise<Response<Entity.Status>> {
|
|
await this.client.post<{}>("/api/notes/reactions/delete", {
|
|
noteId: id,
|
|
reaction: this.reactionName(name),
|
|
});
|
|
return this.client
|
|
.post<MisskeyAPI.Entity.Note>("/api/notes/show", {
|
|
noteId: id,
|
|
})
|
|
.then(async (res) => ({
|
|
...res,
|
|
data: await this.noteWithDetails(
|
|
res.data,
|
|
this.baseUrlToHost(this.baseUrl),
|
|
this.getFreshAccountCache(),
|
|
),
|
|
}));
|
|
}
|
|
|
|
// ======================================
|
|
// statuses/media
|
|
// ======================================
|
|
/**
|
|
* POST /api/drive/files/create
|
|
*/
|
|
public async uploadMedia(
|
|
file: any,
|
|
options?: { description?: string; focus?: string },
|
|
): Promise<Response<Entity.Attachment>> {
|
|
const formData = new FormData();
|
|
formData.append("file", fs.createReadStream(file.path), {
|
|
contentType: file.mimetype,
|
|
});
|
|
|
|
if (file.originalname != null && file.originalname !== "file")
|
|
formData.append("name", file.originalname);
|
|
|
|
if (options?.description != null)
|
|
formData.append("comment", options.description);
|
|
|
|
let headers: { [key: string]: string } = {};
|
|
if (typeof formData.getHeaders === "function") {
|
|
headers = formData.getHeaders();
|
|
}
|
|
return this.client
|
|
.post<MisskeyAPI.Entity.File>(
|
|
"/api/drive/files/create",
|
|
formData,
|
|
headers,
|
|
)
|
|
.then((res) => ({ ...res, data: this.converter.file(res.data) }));
|
|
}
|
|
|
|
public async getMedia(id: string): Promise<Response<Entity.Attachment>> {
|
|
const res = await this.client.post<MisskeyAPI.Entity.File>(
|
|
"/api/drive/files/show",
|
|
{ fileId: id },
|
|
);
|
|
return { ...res, data: this.converter.file(res.data) };
|
|
}
|
|
|
|
/**
|
|
* POST /api/drive/files/update
|
|
*/
|
|
public async updateMedia(
|
|
id: string,
|
|
options?: {
|
|
file?: any;
|
|
description?: string;
|
|
focus?: string;
|
|
is_sensitive?: boolean;
|
|
},
|
|
): Promise<Response<Entity.Attachment>> {
|
|
let params = {
|
|
fileId: id,
|
|
};
|
|
if (options) {
|
|
if (options.is_sensitive !== undefined) {
|
|
params = Object.assign(params, {
|
|
isSensitive: options.is_sensitive,
|
|
});
|
|
}
|
|
|
|
if (options.description !== undefined) {
|
|
params = Object.assign(params, {
|
|
comment: options.description,
|
|
});
|
|
}
|
|
}
|
|
return this.client
|
|
.post<MisskeyAPI.Entity.File>("/api/drive/files/update", params)
|
|
.then((res) => ({ ...res, data: this.converter.file(res.data) }));
|
|
}
|
|
|
|
// ======================================
|
|
// statuses/polls
|
|
// ======================================
|
|
public async getPoll(id: string): Promise<Response<Entity.Poll>> {
|
|
const res = await this.getStatus(id);
|
|
if (res.data.poll == null) throw new Error("poll not found");
|
|
return { ...res, data: res.data.poll };
|
|
}
|
|
|
|
/**
|
|
* POST /api/notes/polls/vote
|
|
*/
|
|
public async votePoll(
|
|
id: string,
|
|
choices: Array<number>,
|
|
): Promise<Response<Entity.Poll>> {
|
|
if (!id) {
|
|
return new Promise((_, reject) => {
|
|
const err = new ArgumentError("id is required");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
for (const c of choices) {
|
|
const params = {
|
|
noteId: id,
|
|
choice: +c,
|
|
};
|
|
await this.client.post<{}>("/api/notes/polls/vote", params);
|
|
}
|
|
|
|
const res = await this.client
|
|
.post<MisskeyAPI.Entity.Note>("/api/notes/show", {
|
|
noteId: id,
|
|
})
|
|
.then(async (res) => {
|
|
const note = await this.noteWithDetails(
|
|
res.data,
|
|
this.baseUrlToHost(this.baseUrl),
|
|
this.getFreshAccountCache(),
|
|
);
|
|
return { ...res, data: note.poll };
|
|
});
|
|
if (!res.data) {
|
|
return new Promise((_, reject) => {
|
|
const err = new UnexpectedError("poll does not exist");
|
|
reject(err);
|
|
});
|
|
}
|
|
return { ...res, data: res.data };
|
|
}
|
|
|
|
// ======================================
|
|
// statuses/scheduled_statuses
|
|
// ======================================
|
|
public async getScheduledStatuses(_options?: {
|
|
limit?: number;
|
|
max_id?: string;
|
|
since_id?: string;
|
|
min_id?: string;
|
|
}): Promise<Response<Array<Entity.ScheduledStatus>>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
public async getScheduledStatus(
|
|
_id: string,
|
|
): Promise<Response<Entity.ScheduledStatus>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
public async scheduleStatus(
|
|
_id: string,
|
|
_scheduled_at?: string | null,
|
|
): Promise<Response<Entity.ScheduledStatus>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
public async cancelScheduledStatus(_id: string): Promise<Response<{}>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
// ======================================
|
|
// timelines
|
|
// ======================================
|
|
/**
|
|
* POST /api/notes/global-timeline
|
|
*/
|
|
public async getPublicTimeline(options?: {
|
|
only_media?: boolean;
|
|
limit?: number;
|
|
max_id?: string;
|
|
since_id?: string;
|
|
min_id?: string;
|
|
}): Promise<Response<Array<Entity.Status>>> {
|
|
const accountCache = this.getFreshAccountCache();
|
|
|
|
let params = {};
|
|
if (options) {
|
|
if (options.only_media !== undefined) {
|
|
params = Object.assign(params, {
|
|
withFiles: options.only_media,
|
|
});
|
|
}
|
|
if (options.limit) {
|
|
params = Object.assign(params, {
|
|
limit: options.limit,
|
|
});
|
|
} else {
|
|
params = Object.assign(params, {
|
|
limit: 20,
|
|
});
|
|
}
|
|
if (options.max_id) {
|
|
params = Object.assign(params, {
|
|
untilId: options.max_id,
|
|
});
|
|
}
|
|
if (options.since_id) {
|
|
params = Object.assign(params, {
|
|
sinceId: options.since_id,
|
|
});
|
|
}
|
|
if (options.min_id) {
|
|
params = Object.assign(params, {
|
|
sinceId: options.min_id,
|
|
});
|
|
}
|
|
} else {
|
|
params = Object.assign(params, {
|
|
limit: 20,
|
|
});
|
|
}
|
|
return this.client
|
|
.post<Array<MisskeyAPI.Entity.Note>>("/api/notes/global-timeline", params)
|
|
.then(async (res) => ({
|
|
...res,
|
|
data: (
|
|
await Promise.all(
|
|
res.data.map((n) =>
|
|
this.noteWithDetails(
|
|
n,
|
|
this.baseUrlToHost(this.baseUrl),
|
|
accountCache,
|
|
),
|
|
),
|
|
)
|
|
).sort(this.sortByIdDesc),
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* POST /api/notes/local-timeline
|
|
*/
|
|
public async getLocalTimeline(options?: {
|
|
only_media?: boolean;
|
|
limit?: number;
|
|
max_id?: string;
|
|
since_id?: string;
|
|
min_id?: string;
|
|
}): Promise<Response<Array<Entity.Status>>> {
|
|
const accountCache = this.getFreshAccountCache();
|
|
|
|
let params = {};
|
|
if (options) {
|
|
if (options.only_media !== undefined) {
|
|
params = Object.assign(params, {
|
|
withFiles: options.only_media,
|
|
});
|
|
}
|
|
if (options.limit) {
|
|
params = Object.assign(params, {
|
|
limit: options.limit,
|
|
});
|
|
} else {
|
|
params = Object.assign(params, {
|
|
limit: 20,
|
|
});
|
|
}
|
|
if (options.max_id) {
|
|
params = Object.assign(params, {
|
|
untilId: options.max_id,
|
|
});
|
|
}
|
|
if (options.since_id) {
|
|
params = Object.assign(params, {
|
|
sinceId: options.since_id,
|
|
});
|
|
}
|
|
if (options.min_id) {
|
|
params = Object.assign(params, {
|
|
sinceId: options.min_id,
|
|
});
|
|
}
|
|
} else {
|
|
params = Object.assign(params, {
|
|
limit: 20,
|
|
});
|
|
}
|
|
return this.client
|
|
.post<Array<MisskeyAPI.Entity.Note>>("/api/notes/local-timeline", params)
|
|
.then(async (res) => ({
|
|
...res,
|
|
data: (
|
|
await Promise.all(
|
|
res.data.map((n) =>
|
|
this.noteWithDetails(
|
|
n,
|
|
this.baseUrlToHost(this.baseUrl),
|
|
accountCache,
|
|
),
|
|
),
|
|
)
|
|
).sort(this.sortByIdDesc),
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* POST /api/notes/search-by-tag
|
|
*/
|
|
public async getTagTimeline(
|
|
hashtag: string,
|
|
options?: {
|
|
local?: boolean;
|
|
only_media?: boolean;
|
|
limit?: number;
|
|
max_id?: string;
|
|
since_id?: string;
|
|
min_id?: string;
|
|
},
|
|
): Promise<Response<Array<Entity.Status>>> {
|
|
const accountCache = this.getFreshAccountCache();
|
|
|
|
let params = {
|
|
tag: hashtag,
|
|
};
|
|
if (options) {
|
|
if (options.only_media !== undefined) {
|
|
params = Object.assign(params, {
|
|
withFiles: options.only_media,
|
|
});
|
|
}
|
|
if (options.limit) {
|
|
params = Object.assign(params, {
|
|
limit: options.limit,
|
|
});
|
|
} else {
|
|
params = Object.assign(params, {
|
|
limit: 20,
|
|
});
|
|
}
|
|
if (options.max_id) {
|
|
params = Object.assign(params, {
|
|
untilId: options.max_id,
|
|
});
|
|
}
|
|
if (options.since_id) {
|
|
params = Object.assign(params, {
|
|
sinceId: options.since_id,
|
|
});
|
|
}
|
|
if (options.min_id) {
|
|
params = Object.assign(params, {
|
|
sinceId: options.min_id,
|
|
});
|
|
}
|
|
} else {
|
|
params = Object.assign(params, {
|
|
limit: 20,
|
|
});
|
|
}
|
|
return this.client
|
|
.post<Array<MisskeyAPI.Entity.Note>>("/api/notes/search-by-tag", params)
|
|
.then(async (res) => ({
|
|
...res,
|
|
data: (
|
|
await Promise.all(
|
|
res.data.map((n) =>
|
|
this.noteWithDetails(
|
|
n,
|
|
this.baseUrlToHost(this.baseUrl),
|
|
accountCache,
|
|
),
|
|
),
|
|
)
|
|
).sort(this.sortByIdDesc),
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* POST /api/notes/timeline
|
|
*/
|
|
public async getHomeTimeline(options?: {
|
|
local?: boolean;
|
|
limit?: number;
|
|
max_id?: string;
|
|
since_id?: string;
|
|
min_id?: string;
|
|
}): Promise<Response<Array<Entity.Status>>> {
|
|
const accountCache = this.getFreshAccountCache();
|
|
|
|
let params = {
|
|
withFiles: false,
|
|
};
|
|
if (options) {
|
|
if (options.limit) {
|
|
params = Object.assign(params, {
|
|
limit: options.limit,
|
|
});
|
|
} else {
|
|
params = Object.assign(params, {
|
|
limit: 20,
|
|
});
|
|
}
|
|
if (options.max_id) {
|
|
params = Object.assign(params, {
|
|
untilId: options.max_id,
|
|
});
|
|
}
|
|
if (options.since_id) {
|
|
params = Object.assign(params, {
|
|
sinceId: options.since_id,
|
|
});
|
|
}
|
|
if (options.min_id) {
|
|
params = Object.assign(params, {
|
|
sinceId: options.min_id,
|
|
});
|
|
}
|
|
} else {
|
|
params = Object.assign(params, {
|
|
limit: 20,
|
|
});
|
|
}
|
|
return this.client
|
|
.post<Array<MisskeyAPI.Entity.Note>>("/api/notes/timeline", params)
|
|
.then(async (res) => ({
|
|
...res,
|
|
data: (
|
|
await Promise.all(
|
|
res.data.map((n) =>
|
|
this.noteWithDetails(
|
|
n,
|
|
this.baseUrlToHost(this.baseUrl),
|
|
accountCache,
|
|
),
|
|
),
|
|
)
|
|
).sort(this.sortByIdDesc),
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* POST /api/notes/user-list-timeline
|
|
*/
|
|
public async getListTimeline(
|
|
list_id: string,
|
|
options?: {
|
|
limit?: number;
|
|
max_id?: string;
|
|
since_id?: string;
|
|
min_id?: string;
|
|
},
|
|
): Promise<Response<Array<Entity.Status>>> {
|
|
const accountCache = this.getFreshAccountCache();
|
|
|
|
let params = {
|
|
listId: list_id,
|
|
withFiles: false,
|
|
};
|
|
if (options) {
|
|
if (options.limit) {
|
|
params = Object.assign(params, {
|
|
limit: options.limit,
|
|
});
|
|
} else {
|
|
params = Object.assign(params, {
|
|
limit: 20,
|
|
});
|
|
}
|
|
if (options.max_id) {
|
|
params = Object.assign(params, {
|
|
untilId: options.max_id,
|
|
});
|
|
}
|
|
if (options.since_id) {
|
|
params = Object.assign(params, {
|
|
sinceId: options.since_id,
|
|
});
|
|
}
|
|
if (options.min_id) {
|
|
params = Object.assign(params, {
|
|
sinceId: options.min_id,
|
|
});
|
|
}
|
|
} else {
|
|
params = Object.assign(params, {
|
|
limit: 20,
|
|
});
|
|
}
|
|
return this.client
|
|
.post<Array<MisskeyAPI.Entity.Note>>(
|
|
"/api/notes/user-list-timeline",
|
|
params,
|
|
)
|
|
.then(async (res) => ({
|
|
...res,
|
|
data: (
|
|
await Promise.all(
|
|
res.data.map((n) =>
|
|
this.noteWithDetails(
|
|
n,
|
|
this.baseUrlToHost(this.baseUrl),
|
|
accountCache,
|
|
),
|
|
),
|
|
)
|
|
).sort(this.sortByIdDesc),
|
|
}));
|
|
}
|
|
|
|
// ======================================
|
|
// timelines/conversations
|
|
// ======================================
|
|
/**
|
|
* POST /api/notes/mentions
|
|
*/
|
|
public async getConversationTimeline(options?: {
|
|
limit?: number;
|
|
max_id?: string;
|
|
since_id?: string;
|
|
min_id?: string;
|
|
}): Promise<Response<Array<Entity.Conversation>>> {
|
|
let params = {
|
|
visibility: "specified",
|
|
};
|
|
if (options) {
|
|
if (options.limit) {
|
|
params = Object.assign(params, {
|
|
limit: options.limit,
|
|
});
|
|
} else {
|
|
params = Object.assign(params, {
|
|
limit: 20,
|
|
});
|
|
}
|
|
if (options.max_id) {
|
|
params = Object.assign(params, {
|
|
untilId: options.max_id,
|
|
});
|
|
}
|
|
if (options.since_id) {
|
|
params = Object.assign(params, {
|
|
sinceId: options.since_id,
|
|
});
|
|
}
|
|
if (options.min_id) {
|
|
params = Object.assign(params, {
|
|
sinceId: options.min_id,
|
|
});
|
|
}
|
|
} else {
|
|
params = Object.assign(params, {
|
|
limit: 20,
|
|
});
|
|
}
|
|
return this.client
|
|
.post<Array<MisskeyAPI.Entity.Note>>("/api/notes/mentions", params)
|
|
.then((res) => ({
|
|
...res,
|
|
data: res.data.map((n) =>
|
|
this.converter.noteToConversation(
|
|
n,
|
|
this.baseUrlToHost(this.baseUrl),
|
|
),
|
|
),
|
|
}));
|
|
// FIXME: ^ this should also parse mentions
|
|
}
|
|
|
|
public async deleteConversation(_id: string): Promise<Response<{}>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
public async readConversation(
|
|
_id: string,
|
|
): Promise<Response<Entity.Conversation>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
private sortByIdDesc(a: Entity.Status, b: Entity.Status): number {
|
|
if (a.id < b.id) return 1;
|
|
if (a.id > b.id) return -1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
// ======================================
|
|
// timelines/lists
|
|
// ======================================
|
|
/**
|
|
* POST /api/users/lists/list
|
|
*/
|
|
public async getLists(): Promise<Response<Array<Entity.List>>> {
|
|
return this.client
|
|
.post<Array<MisskeyAPI.Entity.List>>("/api/users/lists/list")
|
|
.then((res) => ({
|
|
...res,
|
|
data: res.data.map((l) => this.converter.list(l)),
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* POST /api/users/lists/show
|
|
*/
|
|
public async getList(id: string): Promise<Response<Entity.List>> {
|
|
return this.client
|
|
.post<MisskeyAPI.Entity.List>("/api/users/lists/show", {
|
|
listId: id,
|
|
})
|
|
.then((res) => ({ ...res, data: this.converter.list(res.data) }));
|
|
}
|
|
|
|
/**
|
|
* POST /api/users/lists/create
|
|
*/
|
|
public async createList(title: string): Promise<Response<Entity.List>> {
|
|
return this.client
|
|
.post<MisskeyAPI.Entity.List>("/api/users/lists/create", {
|
|
name: title,
|
|
})
|
|
.then((res) => ({ ...res, data: this.converter.list(res.data) }));
|
|
}
|
|
|
|
/**
|
|
* POST /api/users/lists/update
|
|
*/
|
|
public async updateList(
|
|
id: string,
|
|
title: string,
|
|
): Promise<Response<Entity.List>> {
|
|
return this.client
|
|
.post<MisskeyAPI.Entity.List>("/api/users/lists/update", {
|
|
listId: id,
|
|
name: title,
|
|
})
|
|
.then((res) => ({ ...res, data: this.converter.list(res.data) }));
|
|
}
|
|
|
|
/**
|
|
* POST /api/users/lists/delete
|
|
*/
|
|
public async deleteList(id: string): Promise<Response<{}>> {
|
|
return this.client.post<{}>("/api/users/lists/delete", {
|
|
listId: id,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* POST /api/users/lists/show
|
|
*/
|
|
public async getAccountsInList(
|
|
id: string,
|
|
_options?: {
|
|
limit?: number;
|
|
max_id?: string;
|
|
since_id?: string;
|
|
},
|
|
): Promise<Response<Array<Entity.Account>>> {
|
|
const res = await this.client.post<MisskeyAPI.Entity.List>(
|
|
"/api/users/lists/show",
|
|
{
|
|
listId: id,
|
|
},
|
|
);
|
|
const promise = res.data.userIds.map((userId) => this.getAccount(userId));
|
|
const accounts = await Promise.all(promise);
|
|
return { ...res, data: accounts.map((r) => r.data) };
|
|
}
|
|
|
|
/**
|
|
* POST /api/users/lists/push
|
|
*/
|
|
public async addAccountsToList(
|
|
id: string,
|
|
account_ids: Array<string>,
|
|
): Promise<Response<{}>> {
|
|
return this.client.post<{}>("/api/users/lists/push", {
|
|
listId: id,
|
|
userId: account_ids[0],
|
|
});
|
|
}
|
|
|
|
/**
|
|
* POST /api/users/lists/pull
|
|
*/
|
|
public async deleteAccountsFromList(
|
|
id: string,
|
|
account_ids: Array<string>,
|
|
): Promise<Response<{}>> {
|
|
return this.client.post<{}>("/api/users/lists/pull", {
|
|
listId: id,
|
|
userId: account_ids[0],
|
|
});
|
|
}
|
|
|
|
// ======================================
|
|
// timelines/markers
|
|
// ======================================
|
|
public async getMarkers(
|
|
_timeline: Array<string>,
|
|
): Promise<Response<Entity.Marker | {}>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
public async saveMarkers(_options?: {
|
|
home?: { last_read_id: string };
|
|
notifications?: { last_read_id: string };
|
|
}): Promise<Response<Entity.Marker>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
// ======================================
|
|
// notifications
|
|
// ======================================
|
|
/**
|
|
* POST /api/i/notifications
|
|
*/
|
|
public async getNotifications(options?: {
|
|
limit?: number;
|
|
max_id?: string;
|
|
since_id?: string;
|
|
min_id?: string;
|
|
exclude_type?: Array<Entity.NotificationType>;
|
|
account_id?: string;
|
|
}): Promise<Response<Array<Entity.Notification>>> {
|
|
let params = {};
|
|
if (options) {
|
|
if (options.limit) {
|
|
params = Object.assign(params, {
|
|
limit: options.limit <= 100 ? options.limit : 100,
|
|
});
|
|
} else {
|
|
params = Object.assign(params, {
|
|
limit: 20,
|
|
});
|
|
}
|
|
if (options.max_id) {
|
|
params = Object.assign(params, {
|
|
untilId: options.max_id,
|
|
});
|
|
}
|
|
if (options.since_id) {
|
|
params = Object.assign(params, {
|
|
sinceId: options.since_id,
|
|
});
|
|
}
|
|
if (options.min_id) {
|
|
params = Object.assign(params, {
|
|
sinceId: options.min_id,
|
|
});
|
|
}
|
|
if (options.exclude_type) {
|
|
params = Object.assign(params, {
|
|
excludeType: options.exclude_type.map((e) =>
|
|
this.converter.encodeNotificationType(e),
|
|
),
|
|
});
|
|
}
|
|
} else {
|
|
params = Object.assign(params, {
|
|
limit: 20,
|
|
});
|
|
}
|
|
const cache = this.getFreshAccountCache();
|
|
return this.client
|
|
.post<Array<MisskeyAPI.Entity.Notification>>(
|
|
"/api/i/notifications",
|
|
params,
|
|
)
|
|
.then(async (res) => ({
|
|
...res,
|
|
data: await Promise.all(
|
|
res.data
|
|
.filter(
|
|
(p) => p.type != MisskeyNotificationType.FollowRequestAccepted,
|
|
) // these aren't supported on mastodon
|
|
.map((n) =>
|
|
this.notificationWithDetails(
|
|
n,
|
|
this.baseUrlToHost(this.baseUrl),
|
|
cache,
|
|
),
|
|
),
|
|
),
|
|
}));
|
|
}
|
|
|
|
public async getNotification(
|
|
_id: string,
|
|
): Promise<Response<Entity.Notification>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* POST /api/notifications/mark-all-as-read
|
|
*/
|
|
public async dismissNotifications(): Promise<Response<{}>> {
|
|
return this.client.post<{}>("/api/notifications/mark-all-as-read");
|
|
}
|
|
|
|
public async dismissNotification(_id: string): Promise<Response<{}>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
public async readNotifications(_options: {
|
|
id?: string;
|
|
max_id?: string;
|
|
}): Promise<Response<Entity.Notification | Array<Entity.Notification>>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("mastodon does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
// ======================================
|
|
// notifications/push
|
|
// ======================================
|
|
public async subscribePushNotification(
|
|
_subscription: { endpoint: string; keys: { p256dh: string; auth: string } },
|
|
_data?: {
|
|
alerts: {
|
|
follow?: boolean;
|
|
favourite?: boolean;
|
|
reblog?: boolean;
|
|
mention?: boolean;
|
|
poll?: boolean;
|
|
};
|
|
} | null,
|
|
): Promise<Response<Entity.PushSubscription>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
public async getPushSubscription(): Promise<
|
|
Response<Entity.PushSubscription>
|
|
> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
public async updatePushSubscription(
|
|
_data?: {
|
|
alerts: {
|
|
follow?: boolean;
|
|
favourite?: boolean;
|
|
reblog?: boolean;
|
|
mention?: boolean;
|
|
poll?: boolean;
|
|
};
|
|
} | null,
|
|
): Promise<Response<Entity.PushSubscription>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* DELETE /api/v1/push/subscription
|
|
*/
|
|
public async deletePushSubscription(): Promise<Response<{}>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
// ======================================
|
|
// search
|
|
// ======================================
|
|
public async search(
|
|
q: string,
|
|
type: "accounts" | "hashtags" | "statuses",
|
|
options?: {
|
|
limit?: number;
|
|
max_id?: string;
|
|
min_id?: string;
|
|
resolve?: boolean;
|
|
offset?: number;
|
|
following?: boolean;
|
|
account_id?: string;
|
|
exclude_unreviewed?: boolean;
|
|
},
|
|
): Promise<Response<Entity.Results>> {
|
|
const accountCache = this.getFreshAccountCache();
|
|
|
|
switch (type) {
|
|
case "accounts": {
|
|
if (q.startsWith("http://") || q.startsWith("https://")) {
|
|
return this.client
|
|
.post("/api/ap/show", { uri: q })
|
|
.then(async (res) => {
|
|
if (res.status != 200 || res.data.type != "User") {
|
|
res.status = 200;
|
|
res.statusText = "OK";
|
|
res.data = {
|
|
accounts: [],
|
|
statuses: [],
|
|
hashtags: [],
|
|
};
|
|
|
|
return res;
|
|
}
|
|
|
|
const account = await this.converter.userDetail(
|
|
res.data.object as MisskeyAPI.Entity.UserDetail,
|
|
this.baseUrlToHost(this.baseUrl),
|
|
);
|
|
|
|
return {
|
|
...res,
|
|
data: {
|
|
accounts:
|
|
options?.max_id && options?.max_id >= account.id
|
|
? []
|
|
: [account],
|
|
statuses: [],
|
|
hashtags: [],
|
|
},
|
|
};
|
|
});
|
|
}
|
|
let params = {
|
|
query: q,
|
|
};
|
|
if (options) {
|
|
if (options.limit) {
|
|
params = Object.assign(params, {
|
|
limit: options.limit,
|
|
});
|
|
} else {
|
|
params = Object.assign(params, {
|
|
limit: 20,
|
|
});
|
|
}
|
|
if (options.offset) {
|
|
params = Object.assign(params, {
|
|
offset: options.offset,
|
|
});
|
|
}
|
|
if (options.resolve) {
|
|
params = Object.assign(params, {
|
|
localOnly: options.resolve,
|
|
});
|
|
}
|
|
} else {
|
|
params = Object.assign(params, {
|
|
limit: 20,
|
|
});
|
|
}
|
|
|
|
try {
|
|
const match = q.match(/^@(?<user>[a-zA-Z0-9_]+)(?:@(?<host>[a-zA-Z0-9-.]+\.[a-zA-Z0-9-]+)|)$/);
|
|
if (match) {
|
|
const lookupQuery = {
|
|
username: match.groups?.user,
|
|
host: match.groups?.host,
|
|
};
|
|
|
|
const result = await this.client
|
|
.post<MisskeyAPI.Entity.UserDetail>(
|
|
"/api/users/show",
|
|
lookupQuery,
|
|
)
|
|
.then((res) => ({
|
|
...res,
|
|
data: {
|
|
accounts: [
|
|
this.converter.userDetail(
|
|
res.data,
|
|
this.baseUrlToHost(this.baseUrl),
|
|
),
|
|
],
|
|
statuses: [],
|
|
hashtags: [],
|
|
},
|
|
}));
|
|
|
|
if (result.status !== 200) {
|
|
result.status = 200;
|
|
result.statusText = "OK";
|
|
result.data = {
|
|
accounts: [],
|
|
statuses: [],
|
|
hashtags: [],
|
|
};
|
|
}
|
|
|
|
return result;
|
|
}
|
|
} catch {}
|
|
|
|
return this.client
|
|
.post<Array<MisskeyAPI.Entity.UserDetail>>(
|
|
"/api/users/search",
|
|
params,
|
|
)
|
|
.then((res) => ({
|
|
...res,
|
|
data: {
|
|
accounts: res.data.map((u) =>
|
|
this.converter.userDetail(u, this.baseUrlToHost(this.baseUrl)),
|
|
),
|
|
statuses: [],
|
|
hashtags: [],
|
|
},
|
|
}));
|
|
}
|
|
case "statuses": {
|
|
if (q.startsWith("http://") || q.startsWith("https://")) {
|
|
return this.client
|
|
.post("/api/ap/show", { uri: q })
|
|
.then(async (res) => {
|
|
if (res.status != 200 || res.data.type != "Note") {
|
|
res.status = 200;
|
|
res.statusText = "OK";
|
|
res.data = {
|
|
accounts: [],
|
|
statuses: [],
|
|
hashtags: [],
|
|
};
|
|
|
|
return res;
|
|
}
|
|
|
|
const post = await this.noteWithDetails(
|
|
res.data.object as MisskeyAPI.Entity.Note,
|
|
this.baseUrlToHost(this.baseUrl),
|
|
accountCache,
|
|
);
|
|
|
|
return {
|
|
...res,
|
|
data: {
|
|
accounts: [],
|
|
statuses:
|
|
options?.max_id && options.max_id >= post.id ? [] : [post],
|
|
hashtags: [],
|
|
},
|
|
};
|
|
});
|
|
}
|
|
let params = {
|
|
query: q,
|
|
};
|
|
if (options) {
|
|
if (options.limit) {
|
|
params = Object.assign(params, {
|
|
limit: options.limit,
|
|
});
|
|
}
|
|
if (options.offset) {
|
|
params = Object.assign(params, {
|
|
offset: options.offset,
|
|
});
|
|
}
|
|
if (options.max_id) {
|
|
params = Object.assign(params, {
|
|
untilId: options.max_id,
|
|
});
|
|
}
|
|
if (options.min_id) {
|
|
params = Object.assign(params, {
|
|
sinceId: options.min_id,
|
|
});
|
|
}
|
|
if (options.account_id) {
|
|
params = Object.assign(params, {
|
|
userId: options.account_id,
|
|
});
|
|
}
|
|
}
|
|
return this.client
|
|
.post<Array<MisskeyAPI.Entity.Note>>("/api/notes/search", params)
|
|
.then(async (res) => ({
|
|
...res,
|
|
data: {
|
|
accounts: [],
|
|
statuses: await Promise.all(
|
|
res.data.map((n) =>
|
|
this.noteWithDetails(
|
|
n,
|
|
this.baseUrlToHost(this.baseUrl),
|
|
accountCache,
|
|
),
|
|
),
|
|
),
|
|
hashtags: [],
|
|
},
|
|
}));
|
|
}
|
|
case "hashtags": {
|
|
let params = {
|
|
query: q,
|
|
};
|
|
if (options) {
|
|
if (options.limit) {
|
|
params = Object.assign(params, {
|
|
limit: options.limit,
|
|
});
|
|
}
|
|
if (options.offset) {
|
|
params = Object.assign(params, {
|
|
offset: options.offset,
|
|
});
|
|
}
|
|
}
|
|
return this.client
|
|
.post<Array<string>>("/api/hashtags/search", params)
|
|
.then((res) => ({
|
|
...res,
|
|
data: {
|
|
accounts: [],
|
|
statuses: [],
|
|
hashtags: res.data.map((h) => ({
|
|
name: h,
|
|
url: h,
|
|
history: null,
|
|
following: false,
|
|
})),
|
|
},
|
|
}));
|
|
}
|
|
}
|
|
}
|
|
|
|
// ======================================
|
|
// instance
|
|
// ======================================
|
|
/**
|
|
* POST /api/meta
|
|
* POST /api/stats
|
|
*/
|
|
public async getInstance(): Promise<Response<Entity.Instance>> {
|
|
const meta = await this.client
|
|
.post<MisskeyAPI.Entity.Meta>("/api/meta")
|
|
.then((res) => res.data);
|
|
return this.client
|
|
.post<MisskeyAPI.Entity.Stats>("/api/stats")
|
|
.then((res) => ({ ...res, data: this.converter.meta(meta, res.data) }));
|
|
}
|
|
|
|
public async getInstancePeers(): Promise<Response<Array<string>>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
public async getInstanceActivity(): Promise<
|
|
Response<Array<Entity.Activity>>
|
|
> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
// ======================================
|
|
// instance/trends
|
|
// ======================================
|
|
/**
|
|
* POST /api/hashtags/trend
|
|
*/
|
|
public async getInstanceTrends(
|
|
_limit?: number | null,
|
|
): Promise<Response<Array<Entity.Tag>>> {
|
|
return this.client
|
|
.post<Array<MisskeyAPI.Entity.Hashtag>>("/api/hashtags/trend")
|
|
.then((res) => ({
|
|
...res,
|
|
data: res.data.map((h) => this.converter.hashtag(h)),
|
|
}));
|
|
}
|
|
|
|
// ======================================
|
|
// instance/directory
|
|
// ======================================
|
|
public async getInstanceDirectory(_options?: {
|
|
limit?: number;
|
|
offset?: number;
|
|
order?: "active" | "new";
|
|
local?: boolean;
|
|
}): Promise<Response<Array<Entity.Account>>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
// ======================================
|
|
// instance/custom_emojis
|
|
// ======================================
|
|
/**
|
|
* POST /api/meta
|
|
*/
|
|
public async getInstanceCustomEmojis(): Promise<
|
|
Response<Array<Entity.Emoji>>
|
|
> {
|
|
return this.client
|
|
.post<MisskeyAPI.Entity.Meta>("/api/meta")
|
|
.then((res) => ({
|
|
...res,
|
|
data: res.data.emojis.map((e) => this.converter.emoji(e)),
|
|
}));
|
|
}
|
|
|
|
// ======================================
|
|
// instance/announcements
|
|
// ======================================
|
|
public async getInstanceAnnouncements(
|
|
with_dismissed?: boolean | null,
|
|
): Promise<Response<Array<Entity.Announcement>>> {
|
|
let params = {};
|
|
if (with_dismissed) {
|
|
params = Object.assign(params, {
|
|
withUnreads: with_dismissed,
|
|
});
|
|
}
|
|
return this.client
|
|
.post<Array<MisskeyAPI.Entity.Announcement>>("/api/announcements", params)
|
|
.then((res) => ({
|
|
...res,
|
|
data: res.data.map((t) => this.converter.announcement(t)),
|
|
}));
|
|
}
|
|
|
|
public async dismissInstanceAnnouncement(id: string): Promise<Response<{}>> {
|
|
return this.client.post<{}>("/api/i/read-announcement", {
|
|
announcementId: id,
|
|
});
|
|
}
|
|
|
|
// ======================================
|
|
// Emoji reactions
|
|
// ======================================
|
|
/**
|
|
* POST /api/notes/reactions/create
|
|
*
|
|
* @param {string} id Target note ID.
|
|
* @param {string} emoji Reaction emoji string. This string is raw unicode emoji.
|
|
*/
|
|
public async createEmojiReaction(
|
|
id: string,
|
|
emoji: string,
|
|
): Promise<Response<Entity.Status>> {
|
|
await this.client.post<{}>("/api/notes/reactions/create", {
|
|
noteId: id,
|
|
reaction: emoji,
|
|
});
|
|
return this.client
|
|
.post<MisskeyAPI.Entity.Note>("/api/notes/show", {
|
|
noteId: id,
|
|
})
|
|
.then(async (res) => ({
|
|
...res,
|
|
data: await this.noteWithDetails(
|
|
res.data,
|
|
this.baseUrlToHost(this.baseUrl),
|
|
this.getFreshAccountCache(),
|
|
),
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* POST /api/notes/reactions/delete
|
|
*/
|
|
public async deleteEmojiReaction(
|
|
id: string,
|
|
_emoji: string,
|
|
): Promise<Response<Entity.Status>> {
|
|
await this.client.post<{}>("/api/notes/reactions/delete", {
|
|
noteId: id,
|
|
});
|
|
return this.client
|
|
.post<MisskeyAPI.Entity.Note>("/api/notes/show", {
|
|
noteId: id,
|
|
})
|
|
.then(async (res) => ({
|
|
...res,
|
|
data: await this.noteWithDetails(
|
|
res.data,
|
|
this.baseUrlToHost(this.baseUrl),
|
|
this.getFreshAccountCache(),
|
|
),
|
|
}));
|
|
}
|
|
|
|
public async getEmojiReactions(
|
|
id: string,
|
|
): Promise<Response<Array<Entity.Reaction>>> {
|
|
return this.client
|
|
.post<Array<MisskeyAPI.Entity.Reaction>>("/api/notes/reactions", {
|
|
noteId: id,
|
|
})
|
|
.then((res) => ({
|
|
...res,
|
|
data: this.converter.reactions(res.data),
|
|
}));
|
|
}
|
|
|
|
public async getEmojiReaction(
|
|
_id: string,
|
|
_emoji: string,
|
|
): Promise<Response<Entity.Reaction>> {
|
|
return new Promise((_, reject) => {
|
|
const err = new NoImplementedError("misskey does not support");
|
|
reject(err);
|
|
});
|
|
}
|
|
|
|
public userSocket(): WebSocketInterface {
|
|
return this.client.socket("user");
|
|
}
|
|
|
|
public publicSocket(): WebSocketInterface {
|
|
return this.client.socket("globalTimeline");
|
|
}
|
|
|
|
public localSocket(): WebSocketInterface {
|
|
return this.client.socket("localTimeline");
|
|
}
|
|
|
|
public tagSocket(_tag: string): WebSocketInterface {
|
|
throw new NoImplementedError("TODO: implement");
|
|
}
|
|
|
|
public listSocket(list_id: string): WebSocketInterface {
|
|
return this.client.socket("list", list_id);
|
|
}
|
|
|
|
public directSocket(): WebSocketInterface {
|
|
return this.client.socket("conversation");
|
|
}
|
|
}
|