formatting

This commit is contained in:
ThatOneCalculator 2023-03-30 19:10:03 -07:00
parent 5be627b869
commit 19c4a59513
27 changed files with 463 additions and 366 deletions

View file

@ -1 +1 @@
declare module 'koa-remove-trailing-slashes'; declare module "koa-remove-trailing-slashes";

View file

@ -7,32 +7,26 @@ const logger = dbLogger.createSubLogger("sonic", "gray", false);
logger.info("Connecting to Sonic"); logger.info("Connecting to Sonic");
const handlers = (type: string): SonicChannel.Handlers => ( const handlers = (type: string): SonicChannel.Handlers => ({
{ connected: () => {
connected: () => { logger.succ(`Connected to Sonic ${type}`);
logger.succ(`Connected to Sonic ${type}`); },
}, disconnected: (error) => {
disconnected: (error) => { logger.warn(`Disconnected from Sonic ${type}, error: ${error}`);
logger.warn(`Disconnected from Sonic ${type}, error: ${error}`); },
}, error: (error) => {
error: (error) => { logger.warn(`Sonic ${type} error: ${error}`);
logger.warn(`Sonic ${type} error: ${error}`); },
}, retrying: () => {
retrying: () => { logger.info(`Sonic ${type} retrying`);
logger.info(`Sonic ${type} retrying`); },
}, timeout: () => {
timeout: () => { logger.warn(`Sonic ${type} timeout`);
logger.warn(`Sonic ${type} timeout`); },
}, });
}
)
const hasConfig = const hasConfig =
config.sonic config.sonic && (config.sonic.host || config.sonic.port || config.sonic.auth);
&& ( config.sonic.host
|| config.sonic.port
|| config.sonic.auth
)
const host = hasConfig ? config.sonic.host ?? "localhost" : ""; const host = hasConfig ? config.sonic.host ?? "localhost" : "";
const port = hasConfig ? config.sonic.port ?? 1491 : 0; const port = hasConfig ? config.sonic.port ?? 1491 : 0;
@ -42,10 +36,14 @@ const bucket = hasConfig ? config.sonic.bucket ?? "default" : "";
export default hasConfig export default hasConfig
? { ? {
search: new SonicChannel.Search({host, port, auth}).connect(handlers("search")), search: new SonicChannel.Search({ host, port, auth }).connect(
ingest: new SonicChannel.Ingest({host, port, auth}).connect(handlers("ingest")), handlers("search"),
),
ingest: new SonicChannel.Ingest({ host, port, auth }).connect(
handlers("ingest"),
),
collection, collection,
bucket, bucket,
} }
: null; : null;

View file

@ -6,7 +6,12 @@ export type Post = {
}; };
export function parse(acct: any): Post { export function parse(acct: any): Post {
return { text: acct.text, cw: acct.cw, localOnly: acct.localOnly, createdAt: new Date(acct.createdAt) }; return {
text: acct.text,
cw: acct.cw,
localOnly: acct.localOnly,
createdAt: new Date(acct.createdAt),
};
} }
export function toJson(acct: Post): string { export function toJson(acct: Post): string {

View file

@ -440,14 +440,10 @@ export function createCleanRemoteFilesJob() {
} }
export function createIndexAllNotesJob(data = {}) { export function createIndexAllNotesJob(data = {}) {
return backgroundQueue.add( return backgroundQueue.add("indexAllNotes", data, {
"indexAllNotes", removeOnComplete: true,
data, removeOnFail: true,
{ });
removeOnComplete: true,
removeOnFail: true,
},
);
} }
export function webhookDeliver( export function webhookDeliver(

View file

@ -3,26 +3,30 @@ import type Bull from "bull";
import { queueLogger } from "../../logger.js"; import { queueLogger } from "../../logger.js";
import { Notes } from "@/models/index.js"; import { Notes } from "@/models/index.js";
import { MoreThan } from "typeorm"; import { MoreThan } from "typeorm";
import { index } from "@/services/note/create.js" import { index } from "@/services/note/create.js";
import { Note } from "@/models/entities/note.js"; import { Note } from "@/models/entities/note.js";
const logger = queueLogger.createSubLogger("index-all-notes"); const logger = queueLogger.createSubLogger("index-all-notes");
export default async function indexAllNotes( export default async function indexAllNotes(
job: Bull.Job<Record<string, unknown>>, job: Bull.Job<Record<string, unknown>>,
done: ()=>void, done: () => void,
): Promise<void> { ): Promise<void> {
logger.info("Indexing all notes..."); logger.info("Indexing all notes...");
let cursor: string|null = job.data.cursor as string ?? null; let cursor: string | null = (job.data.cursor as string) ?? null;
let indexedCount: number = job.data.indexedCount as number ?? 0; let indexedCount: number = (job.data.indexedCount as number) ?? 0;
let total: number = job.data.total as number ?? 0; let total: number = (job.data.total as number) ?? 0;
let running = true; let running = true;
const take = 50000; const take = 50000;
const batch = 100; const batch = 100;
while (running) { while (running) {
logger.info(`Querying for ${take} notes ${indexedCount}/${total ? total : '?'} at ${cursor}`); logger.info(
`Querying for ${take} notes ${indexedCount}/${
total ? total : "?"
} at ${cursor}`,
);
let notes: Note[] = []; let notes: Note[] = [];
try { try {
@ -49,22 +53,21 @@ export default async function indexAllNotes(
try { try {
const count = await Notes.count(); const count = await Notes.count();
total = count; total = count;
job.update({ indexedCount, cursor, total }) job.update({ indexedCount, cursor, total });
} catch (e) { } catch (e) {}
}
for (let i = 0; i < notes.length; i += batch) { for (let i = 0; i < notes.length; i += batch) {
const chunk = notes.slice(i, i + batch); const chunk = notes.slice(i, i + batch);
await Promise.all(chunk.map(note => index(note))); await Promise.all(chunk.map((note) => index(note)));
indexedCount += chunk.length; indexedCount += chunk.length;
const pct = (indexedCount / total)*100; const pct = (indexedCount / total) * 100;
job.update({ indexedCount, cursor, total }) job.update({ indexedCount, cursor, total });
job.progress(+(pct.toFixed(1))); job.progress(+pct.toFixed(1));
logger.info(`Indexed notes ${indexedCount}/${total ? total : '?'}`); logger.info(`Indexed notes ${indexedCount}/${total ? total : "?"}`);
} }
cursor = notes[notes.length - 1].id; cursor = notes[notes.length - 1].id;
job.update({ indexedCount, cursor, total }) job.update({ indexedCount, cursor, total });
if (notes.length < take) { if (notes.length < take) {
running = false; running = false;

View file

@ -3,10 +3,7 @@ import indexAllNotes from "./index-all-notes.js";
const jobs = { const jobs = {
indexAllNotes, indexAllNotes,
} as Record< } as Record<string, Bull.ProcessCallbackFunction<Record<string, unknown>>>;
string,
Bull.ProcessCallbackFunction<Record<string, unknown>>
>;
export default function (q: Bull.Queue) { export default function (q: Bull.Queue) {
for (const [k, v] of Object.entries(jobs)) { for (const [k, v] of Object.entries(jobs)) {

View file

@ -88,7 +88,7 @@ export async function importPosts(
continue; continue;
} }
if (job.data.signatureCheck) { if (job.data.signatureCheck) {
if(!post.signature) { if (!post.signature) {
continue; continue;
} }
} }

View file

@ -20,7 +20,7 @@ export default async (job: Bull.Job<WebhookDeliverJobData>) => {
"X-Calckey-Host": config.host, "X-Calckey-Host": config.host,
"X-Calckey-Hook-Id": job.data.webhookId, "X-Calckey-Hook-Id": job.data.webhookId,
"X-Calckey-Hook-Secret": job.data.secret, "X-Calckey-Hook-Secret": job.data.secret,
'Content-Type': 'application/json' "Content-Type": "application/json",
}, },
body: JSON.stringify({ body: JSON.stringify({
hookId: job.data.webhookId, hookId: job.data.webhookId,

View file

@ -205,7 +205,9 @@ export async function createPerson(
if (typeof person.followers === "string") { if (typeof person.followers === "string") {
try { try {
let data = await fetch(person.followers, { headers: { "Accept": "application/json" } }); let data = await fetch(person.followers, {
headers: { Accept: "application/json" },
});
let json_data = JSON.parse(await data.text()); let json_data = JSON.parse(await data.text());
followersCount = json_data.totalItems; followersCount = json_data.totalItems;
@ -218,7 +220,9 @@ export async function createPerson(
if (typeof person.following === "string") { if (typeof person.following === "string") {
try { try {
let data = await fetch(person.following, { headers: { "Accept": "application/json" } }); let data = await fetch(person.following, {
headers: { Accept: "application/json" },
});
let json_data = JSON.parse(await data.text()); let json_data = JSON.parse(await data.text());
followingCount = json_data.totalItems; followingCount = json_data.totalItems;
@ -227,7 +231,6 @@ export async function createPerson(
} }
} }
// Create user // Create user
let user: IRemoteUser; let user: IRemoteUser;
try { try {
@ -255,14 +258,20 @@ export async function createPerson(
followersUri: person.followers followersUri: person.followers
? getApId(person.followers) ? getApId(person.followers)
: undefined, : undefined,
followersCount: followersCount !== undefined followersCount:
? followersCount followersCount !== undefined
: person.followers && typeof person.followers !== "string" && isCollectionOrOrderedCollection(person.followers) ? followersCount
: person.followers &&
typeof person.followers !== "string" &&
isCollectionOrOrderedCollection(person.followers)
? person.followers.totalItems ? person.followers.totalItems
: undefined, : undefined,
followingCount: followingCount !== undefined followingCount:
? followingCount followingCount !== undefined
: person.following && typeof person.following !== "string" && isCollectionOrOrderedCollection(person.following) ? followingCount
: person.following &&
typeof person.following !== "string" &&
isCollectionOrOrderedCollection(person.following)
? person.following.totalItems ? person.following.totalItems
: undefined, : undefined,
featured: person.featured ? getApId(person.featured) : undefined, featured: person.featured ? getApId(person.featured) : undefined,
@ -440,7 +449,9 @@ export async function updatePerson(
if (typeof person.followers === "string") { if (typeof person.followers === "string") {
try { try {
let data = await fetch(person.followers, { headers: { "Accept": "application/json" } } ); let data = await fetch(person.followers, {
headers: { Accept: "application/json" },
});
let json_data = JSON.parse(await data.text()); let json_data = JSON.parse(await data.text());
followersCount = json_data.totalItems; followersCount = json_data.totalItems;
@ -449,12 +460,13 @@ export async function updatePerson(
} }
} }
let followingCount: number | undefined; let followingCount: number | undefined;
if (typeof person.following === "string") { if (typeof person.following === "string") {
try { try {
let data = await fetch(person.following, { headers: { "Accept": "application/json" } } ); let data = await fetch(person.following, {
headers: { Accept: "application/json" },
});
let json_data = JSON.parse(await data.text()); let json_data = JSON.parse(await data.text());
followingCount = json_data.totalItems; followingCount = json_data.totalItems;
@ -470,14 +482,20 @@ export async function updatePerson(
person.sharedInbox || person.sharedInbox ||
(person.endpoints ? person.endpoints.sharedInbox : undefined), (person.endpoints ? person.endpoints.sharedInbox : undefined),
followersUri: person.followers ? getApId(person.followers) : undefined, followersUri: person.followers ? getApId(person.followers) : undefined,
followersCount: followersCount !== undefined followersCount:
? followersCount followersCount !== undefined
: person.followers && typeof person.followers !== "string" && isCollectionOrOrderedCollection(person.followers) ? followersCount
: person.followers &&
typeof person.followers !== "string" &&
isCollectionOrOrderedCollection(person.followers)
? person.followers.totalItems ? person.followers.totalItems
: undefined, : undefined,
followingCount: followingCount !== undefined followingCount:
? followingCount followingCount !== undefined
: person.following && typeof person.following !== "string" && isCollectionOrOrderedCollection(person.following) ? followingCount
: person.following &&
typeof person.following !== "string" &&
isCollectionOrOrderedCollection(person.following)
? person.following.totalItems ? person.following.totalItems
: undefined, : undefined,
featured: person.featured, featured: person.featured,

View file

@ -54,7 +54,11 @@ export const paramDef = {
folderId: { type: "string", format: "misskey:id", nullable: true }, folderId: { type: "string", format: "misskey:id", nullable: true },
name: { type: "string" }, name: { type: "string" },
isSensitive: { type: "boolean" }, isSensitive: { type: "boolean" },
comment: { type: "string", nullable: true, maxLength: DB_MAX_IMAGE_COMMENT_LENGTH }, comment: {
type: "string",
nullable: true,
maxLength: DB_MAX_IMAGE_COMMENT_LENGTH,
},
}, },
required: ["fileId"], required: ["fileId"],
} as const; } as const;

View file

@ -11,7 +11,8 @@ export const meta = {
res: { res: {
type: "object", type: "object",
optional: false, nullable: false, optional: false,
nullable: false,
ref: "Emoji", ref: "Emoji",
}, },
} as const; } as const;

View file

@ -151,7 +151,7 @@ export default define(meta, paramDef, async (ps, user) => {
} }
// テキストが無いかつ添付ファイルも無かったらエラー // テキストが無いかつ添付ファイルも無かったらエラー
if ((ps.text == null || ps.text.trim() === '') && file == null) { if ((ps.text == null || ps.text.trim() === "") && file == null) {
throw new ApiError(meta.errors.contentRequired); throw new ApiError(meta.errors.contentRequired);
} }

View file

@ -139,7 +139,7 @@ export default define(meta, paramDef, async (ps, me) => {
}) })
.map((key) => key.id); .map((key) => key.id);
ids.push(...res); ids.push(...res);
} }
// Sort all the results by note id DESC (newest first) // Sort all the results by note id DESC (newest first)
@ -160,7 +160,7 @@ export default define(meta, paramDef, async (ps, me) => {
}); });
// The notes are checked for visibility and muted/blocked users when packed // The notes are checked for visibility and muted/blocked users when packed
found.push(...await Notes.packMany(notes, me)); found.push(...(await Notes.packMany(notes, me)));
start += chunkSize; start += chunkSize;
} }

View file

@ -7,7 +7,10 @@ import Router from "@koa/router";
import multer from "@koa/multer"; import multer from "@koa/multer";
import bodyParser from "koa-bodyparser"; import bodyParser from "koa-bodyparser";
import cors from "@koa/cors"; import cors from "@koa/cors";
import { apiMastodonCompatible, getClient } from "./mastodon/ApiMastodonCompatibleService.js"; import {
apiMastodonCompatible,
getClient,
} from "./mastodon/ApiMastodonCompatibleService.js";
import { Instances, AccessTokens, Users } from "@/models/index.js"; import { Instances, AccessTokens, Users } from "@/models/index.js";
import config from "@/config/index.js"; import config from "@/config/index.js";
import fs from "fs"; import fs from "fs";
@ -21,10 +24,10 @@ import discord from "./service/discord.js";
import github from "./service/github.js"; import github from "./service/github.js";
import twitter from "./service/twitter.js"; import twitter from "./service/twitter.js";
import { koaBody } from "koa-body"; import { koaBody } from "koa-body";
import { convertId, IdConvertType as IdType } from "native-utils" import { convertId, IdConvertType as IdType } from "native-utils";
// re-export native rust id conversion (function and enum) // re-export native rust id conversion (function and enum)
export { IdType, convertId }; export { IdType, convertId };
// Init app // Init app
const app = new Koa(); const app = new Koa();
@ -74,7 +77,6 @@ mastoRouter.use(
}), }),
); );
mastoFileRouter.post("/v1/media", upload.single("file"), async (ctx) => { mastoFileRouter.post("/v1/media", upload.single("file"), async (ctx) => {
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;

View file

@ -77,7 +77,10 @@ export function apiAccountMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.search((ctx.request.query as any).acct, 'accounts'); const data = await client.search(
(ctx.request.query as any).acct,
"accounts",
);
let resp = data.data.accounts[0]; let resp = data.data.accounts[0];
resp.id = convertId(resp.id, IdType.MastodonId); resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp; ctx.body = resp;
@ -88,26 +91,23 @@ export function apiAccountMastodon(router: Router): void {
ctx.body = e.response.data; ctx.body = e.response.data;
} }
}); });
router.get<{ Params: { id: string } }>( router.get<{ Params: { id: string } }>("/v1/accounts/:id", async (ctx) => {
"/v1/accounts/:id", const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
async (ctx) => { const accessTokens = ctx.headers.authorization;
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; const client = getClient(BASE_URL, accessTokens);
const accessTokens = ctx.headers.authorization; try {
const client = getClient(BASE_URL, accessTokens); const calcId = convertId(ctx.params.id, IdType.CalckeyId);
try { const data = await client.getAccount(calcId);
const calcId = convertId(ctx.params.id, IdType.CalckeyId); let resp = data.data;
const data = await client.getAccount(calcId); resp.id = convertId(resp.id, IdType.MastodonId);
let resp = data.data; ctx.body = resp;
resp.id = convertId(resp.id, IdType.MastodonId); } catch (e: any) {
ctx.body = resp; console.error(e);
} catch (e: any) { console.error(e.response.data);
console.error(e); ctx.status = 401;
console.error(e.response.data); ctx.body = e.response.data;
ctx.status = 401; }
ctx.body = e.response.data; });
}
},
);
router.get<{ Params: { id: string } }>( router.get<{ Params: { id: string } }>(
"/v1/accounts/:id/statuses", "/v1/accounts/:id/statuses",
async (ctx) => { async (ctx) => {
@ -122,11 +122,19 @@ export function apiAccountMastodon(router: Router): void {
let resp = data.data; let resp = data.data;
for (let statIdx = 0; statIdx < resp.length; statIdx++) { for (let statIdx = 0; statIdx < resp.length; statIdx++) {
resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId); resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId);
resp[statIdx].in_reply_to_account_id = resp[statIdx].in_reply_to_account_id ? convertId(resp[statIdx].in_reply_to_account_id, IdType.MastodonId) : null; resp[statIdx].in_reply_to_account_id = resp[statIdx]
resp[statIdx].in_reply_to_id = resp[statIdx].in_reply_to_id ? convertId(resp[statIdx].in_reply_to_id, IdType.MastodonId) : null; .in_reply_to_account_id
let mentions = resp[statIdx].mentions ? convertId(resp[statIdx].in_reply_to_account_id, IdType.MastodonId)
: null;
resp[statIdx].in_reply_to_id = resp[statIdx].in_reply_to_id
? convertId(resp[statIdx].in_reply_to_id, IdType.MastodonId)
: null;
let mentions = resp[statIdx].mentions;
for (let mtnIdx = 0; mtnIdx < mentions.length; mtnIdx++) { for (let mtnIdx = 0; mtnIdx < mentions.length; mtnIdx++) {
resp[statIdx].mentions[mtnIdx].id = convertId(mentions[mtnIdx].id, IdType.MastodonId); resp[statIdx].mentions[mtnIdx].id = convertId(
mentions[mtnIdx].id,
IdType.MastodonId,
);
} }
} }
ctx.body = resp; ctx.body = resp;
@ -210,7 +218,9 @@ export function apiAccountMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.followAccount(convertId(ctx.params.id, IdType.CalckeyId)); const data = await client.followAccount(
convertId(ctx.params.id, IdType.CalckeyId),
);
let acct = data.data; let acct = data.data;
acct.following = true; acct.following = true;
acct.id = convertId(acct.id, IdType.MastodonId); acct.id = convertId(acct.id, IdType.MastodonId);
@ -230,7 +240,9 @@ export function apiAccountMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.unfollowAccount(convertId(ctx.params.id, IdType.CalckeyId)); const data = await client.unfollowAccount(
convertId(ctx.params.id, IdType.CalckeyId),
);
let acct = data.data; let acct = data.data;
acct.id = convertId(acct.id, IdType.MastodonId); acct.id = convertId(acct.id, IdType.MastodonId);
acct.following = false; acct.following = false;
@ -250,7 +262,9 @@ export function apiAccountMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.blockAccount(convertId(ctx.params.id, IdType.CalckeyId)); const data = await client.blockAccount(
convertId(ctx.params.id, IdType.CalckeyId),
);
let resp = data.data; let resp = data.data;
resp.id = convertId(resp.id, IdType.MastodonId); resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp; ctx.body = resp;
@ -269,7 +283,9 @@ export function apiAccountMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.unblockAccount(convertId(ctx.params.id, IdType.MastodonId)); const data = await client.unblockAccount(
convertId(ctx.params.id, IdType.MastodonId),
);
let resp = data.data; let resp = data.data;
resp.id = convertId(resp.id, IdType.MastodonId); resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp; ctx.body = resp;
@ -310,7 +326,9 @@ export function apiAccountMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.unmuteAccount(convertId(ctx.params.id, IdType.CalckeyId)); const data = await client.unmuteAccount(
convertId(ctx.params.id, IdType.CalckeyId),
);
let resp = data.data; let resp = data.data;
resp.id = convertId(resp.id, IdType.MastodonId); resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp; ctx.body = resp;
@ -365,15 +383,25 @@ export function apiAccountMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = (await client.getBookmarks(limitToInt(ctx.query as any))) as any; const data = (await client.getBookmarks(
limitToInt(ctx.query as any),
)) as any;
let resp = data.data; let resp = data.data;
for (let statIdx = 0; statIdx < resp.length; statIdx++) { for (let statIdx = 0; statIdx < resp.length; statIdx++) {
resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId); resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId);
resp[statIdx].in_reply_to_account_id = resp[statIdx].in_reply_to_account_id ? convertId(resp[statIdx].in_reply_to_account_id, IdType.MastodonId) : null; resp[statIdx].in_reply_to_account_id = resp[statIdx]
resp[statIdx].in_reply_to_id = resp[statIdx].in_reply_to_id ? convertId(resp[statIdx].in_reply_to_id, IdType.MastodonId) : null; .in_reply_to_account_id
let mentions = resp[statIdx].mentions ? convertId(resp[statIdx].in_reply_to_account_id, IdType.MastodonId)
: null;
resp[statIdx].in_reply_to_id = resp[statIdx].in_reply_to_id
? convertId(resp[statIdx].in_reply_to_id, IdType.MastodonId)
: null;
let mentions = resp[statIdx].mentions;
for (let mtnIdx = 0; mtnIdx < mentions.length; mtnIdx++) { for (let mtnIdx = 0; mtnIdx < mentions.length; mtnIdx++) {
resp[statIdx].mentions[mtnIdx].id = convertId(mentions[mtnIdx].id, IdType.MastodonId); resp[statIdx].mentions[mtnIdx].id = convertId(
mentions[mtnIdx].id,
IdType.MastodonId,
);
} }
} }
ctx.body = resp; ctx.body = resp;
@ -393,11 +421,19 @@ export function apiAccountMastodon(router: Router): void {
let resp = data.data; let resp = data.data;
for (let statIdx = 0; statIdx < resp.length; statIdx++) { for (let statIdx = 0; statIdx < resp.length; statIdx++) {
resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId); resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId);
resp[statIdx].in_reply_to_account_id = resp[statIdx].in_reply_to_account_id ? convertId(resp[statIdx].in_reply_to_account_id, IdType.MastodonId) : null; resp[statIdx].in_reply_to_account_id = resp[statIdx]
resp[statIdx].in_reply_to_id = resp[statIdx].in_reply_to_id ? convertId(resp[statIdx].in_reply_to_id, IdType.MastodonId) : null; .in_reply_to_account_id
let mentions = resp[statIdx].mentions ? convertId(resp[statIdx].in_reply_to_account_id, IdType.MastodonId)
: null;
resp[statIdx].in_reply_to_id = resp[statIdx].in_reply_to_id
? convertId(resp[statIdx].in_reply_to_id, IdType.MastodonId)
: null;
let mentions = resp[statIdx].mentions;
for (let mtnIdx = 0; mtnIdx < mentions.length; mtnIdx++) { for (let mtnIdx = 0; mtnIdx < mentions.length; mtnIdx++) {
resp[statIdx].mentions[mtnIdx].id = convertId(mentions[mtnIdx].id, IdType.MastodonId); resp[statIdx].mentions[mtnIdx].id = convertId(
mentions[mtnIdx].id,
IdType.MastodonId,
);
} }
} }
ctx.body = resp; ctx.body = resp;
@ -471,7 +507,9 @@ export function apiAccountMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.acceptFollowRequest(convertId(ctx.params.id, IdType.CalckeyId)); const data = await client.acceptFollowRequest(
convertId(ctx.params.id, IdType.CalckeyId),
);
let resp = data.data; let resp = data.data;
resp.id = convertId(resp.id, IdType.MastodonId); resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp; ctx.body = resp;
@ -490,7 +528,9 @@ export function apiAccountMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.rejectFollowRequest(convertId(ctx.params.id, IdType.CalckeyId)); const data = await client.rejectFollowRequest(
convertId(ctx.params.id, IdType.CalckeyId),
);
let resp = data.data; let resp = data.data;
resp.id = convertId(resp.id, IdType.MastodonId); resp.id = convertId(resp.id, IdType.MastodonId);
ctx.body = resp; ctx.body = resp;

View file

@ -44,7 +44,7 @@ const writeScope = [
export function apiAuthMastodon(router: Router): void { export function apiAuthMastodon(router: Router): void {
router.post("/v1/apps", async (ctx) => { router.post("/v1/apps", async (ctx) => {
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
const client = getClient(BASE_URL, ''); const client = getClient(BASE_URL, "");
const body: any = ctx.request.body || ctx.request.query; const body: any = ctx.request.body || ctx.request.query;
try { try {
let scope = body.scopes; let scope = body.scopes;
@ -68,9 +68,9 @@ export function apiAuthMastodon(router: Router): void {
website: body.website, website: body.website,
redirect_uri: red, redirect_uri: red,
client_id: Buffer.from(appData.url || "").toString("base64"), client_id: Buffer.from(appData.url || "").toString("base64"),
client_secret: appData.clientSecret client_secret: appData.clientSecret,
}; };
console.log(returns) console.log(returns);
ctx.body = returns; ctx.body = returns;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);

View file

@ -11,17 +11,20 @@ export async function getInstance(response: Entity.Instance) {
return { return {
uri: response.uri, uri: response.uri,
title: response.title || "Calckey", title: response.title || "Calckey",
short_description: response.description.substring(0, 50) || "See real server website", short_description:
description: response.description || "This is a vanilla Calckey Instance. It doesnt seem to have a description. BTW you are using the Mastodon api to access this server :)", response.description.substring(0, 50) || "See real server website",
description:
response.description ||
"This is a vanilla Calckey Instance. It doesnt seem to have a description. BTW you are using the Mastodon api to access this server :)",
email: response.email || "", email: response.email || "",
version: "3.0.0 compatible (3.5+ Calckey)", //I hope this version string is correct, we will need to test it. version: "3.0.0 compatible (3.5+ Calckey)", //I hope this version string is correct, we will need to test it.
urls: response.urls, urls: response.urls,
stats: { stats: {
user_count: (await totalUsers), user_count: await totalUsers,
status_count: (await totalStatuses), status_count: await totalStatuses,
domain_count: response.stats.domain_count domain_count: response.stats.domain_count,
}, },
thumbnail: response.thumbnail || 'https://http.cat/404', thumbnail: response.thumbnail || "https://http.cat/404",
languages: meta.langs, languages: meta.langs,
registrations: !meta.disableRegistration || response.registrations, registrations: !meta.disableRegistration || response.registrations,
approval_required: !response.registrations, approval_required: !response.registrations,

View file

@ -44,7 +44,7 @@ export function apiSearchMastodon(router: Router): void {
} }
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = (401); ctx.status = 401;
ctx.body = e.response.data; ctx.body = e.response.data;
} }
}); });
@ -52,11 +52,15 @@ export function apiSearchMastodon(router: Router): void {
const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`; const BASE_URL = `${ctx.request.protocol}://${ctx.request.hostname}`;
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
try { try {
const data = await getHighlight(BASE_URL, ctx.request.hostname, accessTokens); const data = await getHighlight(
BASE_URL,
ctx.request.hostname,
accessTokens,
);
ctx.body = data; ctx.body = data;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = (401); ctx.status = 401;
ctx.body = e.response.data; ctx.body = e.response.data;
} }
}); });
@ -75,7 +79,7 @@ export function apiSearchMastodon(router: Router): void {
ctx.body = data; ctx.body = data;
} catch (e: any) { } catch (e: any) {
console.error(e); console.error(e);
ctx.status = (401); ctx.status = 401;
ctx.body = e.response.data; ctx.body = e.response.data;
} }
}); });

View file

@ -2,13 +2,13 @@ import Router from "@koa/router";
import { getClient } from "../ApiMastodonCompatibleService.js"; import { getClient } from "../ApiMastodonCompatibleService.js";
import { emojiRegexAtStartToEnd } from "@/misc/emoji-regex.js"; import { emojiRegexAtStartToEnd } from "@/misc/emoji-regex.js";
import axios from "axios"; import axios from "axios";
import querystring from 'node:querystring' import querystring from "node:querystring";
import qs from 'qs' import qs from "qs";
import { limitToInt } from "./timeline.js"; import { limitToInt } from "./timeline.js";
function normalizeQuery(data: any) { function normalizeQuery(data: any) {
const str = querystring.stringify(data); const str = querystring.stringify(data);
return qs.parse(str); return qs.parse(str);
} }
export function apiStatusMastodon(router: Router): void { export function apiStatusMastodon(router: Router): void {
@ -18,11 +18,14 @@ export function apiStatusMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
let body: any = ctx.request.body; let body: any = ctx.request.body;
if ((!body.poll && body['poll[options][]']) || (!body.media_ids && body['media_ids[]'])) { if (
body = normalizeQuery(body) (!body.poll && body["poll[options][]"]) ||
(!body.media_ids && body["media_ids[]"])
) {
body = normalizeQuery(body);
} }
const text = body.status; const text = body.status;
const removed = text.replace(/@\S+/g, "").replace(/\s|/g, '') const removed = text.replace(/@\S+/g, "").replace(/\s|/g, "");
const isDefaultEmoji = emojiRegexAtStartToEnd.test(removed); const isDefaultEmoji = emojiRegexAtStartToEnd.test(removed);
const isCustomEmoji = /^:[a-zA-Z0-9@_]+:$/.test(removed); const isCustomEmoji = /^:[a-zA-Z0-9@_]+:$/.test(removed);
if ((body.in_reply_to_id && isDefaultEmoji) || isCustomEmoji) { if ((body.in_reply_to_id && isDefaultEmoji) || isCustomEmoji) {
@ -47,8 +50,9 @@ export function apiStatusMastodon(router: Router): void {
} }
if (!body.media_ids) body.media_ids = undefined; if (!body.media_ids) body.media_ids = undefined;
if (body.media_ids && !body.media_ids.length) body.media_ids = undefined; if (body.media_ids && !body.media_ids.length) body.media_ids = undefined;
const { sensitive } = body const { sensitive } = body;
body.sensitive = typeof sensitive === 'string' ? sensitive === 'true' : sensitive body.sensitive =
typeof sensitive === "string" ? sensitive === "true" : sensitive;
const data = await client.postStatus(text, body); const data = await client.postStatus(text, body);
ctx.body = data.data; ctx.body = data.data;
} catch (e: any) { } catch (e: any) {
@ -57,38 +61,32 @@ export function apiStatusMastodon(router: Router): void {
ctx.body = e.response.data; ctx.body = e.response.data;
} }
}); });
router.get<{ Params: { id: string } }>( router.get<{ Params: { id: string } }>("/v1/statuses/:id", async (ctx) => {
"/v1/statuses/:id", const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
async (ctx) => { const accessTokens = ctx.headers.authorization;
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; const client = getClient(BASE_URL, accessTokens);
const accessTokens = ctx.headers.authorization; try {
const client = getClient(BASE_URL, accessTokens); const data = await client.getStatus(ctx.params.id);
try { ctx.body = data.data;
const data = await client.getStatus(ctx.params.id); } catch (e: any) {
ctx.body = data.data; console.error(e);
} catch (e: any) { ctx.status = 401;
console.error(e); ctx.body = e.response.data;
ctx.status = 401; }
ctx.body = e.response.data; });
} router.delete<{ Params: { id: string } }>("/v1/statuses/:id", async (ctx) => {
}, const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
); const accessTokens = ctx.headers.authorization;
router.delete<{ Params: { id: string } }>( const client = getClient(BASE_URL, accessTokens);
"/v1/statuses/:id", try {
async (ctx) => { const data = await client.deleteStatus(ctx.params.id);
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; ctx.body = data.data;
const accessTokens = ctx.headers.authorization; } catch (e: any) {
const client = getClient(BASE_URL, accessTokens); console.error(e.response.data, request.params.id);
try { ctx.status = 401;
const data = await client.deleteStatus(ctx.params.id); ctx.body = e.response.data;
ctx.body = data.data; }
} catch (e: any) { });
console.error(e.response.data, request.params.id);
ctx.status = 401;
ctx.body = e.response.data;
}
},
);
interface IReaction { interface IReaction {
id: string; id: string;
createdAt: string; createdAt: string;
@ -103,12 +101,15 @@ export function apiStatusMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const id = ctx.params.id; const id = ctx.params.id;
const data = await client.getStatusContext(id, limitToInt(ctx.query as any)); const data = await client.getStatusContext(
id,
limitToInt(ctx.query as any),
);
const status = await client.getStatus(id); const status = await client.getStatus(id);
let reqInstance = axios.create({ let reqInstance = axios.create({
headers: { headers: {
Authorization : ctx.headers.authorization Authorization: ctx.headers.authorization,
} },
}); });
const reactionsAxios = await reqInstance.get( const reactionsAxios = await reqInstance.get(
`${BASE_URL}/api/notes/reactions?noteId=${id}`, `${BASE_URL}/api/notes/reactions?noteId=${id}`,
@ -296,57 +297,48 @@ export function apiStatusMastodon(router: Router): void {
} }
}, },
); );
router.get<{ Params: { id: string } }>( router.get<{ Params: { id: string } }>("/v1/media/:id", async (ctx) => {
"/v1/media/:id", const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
async (ctx) => { const accessTokens = ctx.headers.authorization;
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; const client = getClient(BASE_URL, accessTokens);
const accessTokens = ctx.headers.authorization; try {
const client = getClient(BASE_URL, accessTokens); const data = await client.getMedia(ctx.params.id);
try { ctx.body = data.data;
const data = await client.getMedia(ctx.params.id); } catch (e: any) {
ctx.body = data.data; console.error(e);
} catch (e: any) { ctx.status = 401;
console.error(e); ctx.body = e.response.data;
ctx.status = 401; }
ctx.body = e.response.data; });
} router.put<{ Params: { id: string } }>("/v1/media/:id", async (ctx) => {
}, const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
); const accessTokens = ctx.headers.authorization;
router.put<{ Params: { id: string } }>( const client = getClient(BASE_URL, accessTokens);
"/v1/media/:id", try {
async (ctx) => { const data = await client.updateMedia(
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; ctx.params.id,
const accessTokens = ctx.headers.authorization; ctx.request.body as any,
const client = getClient(BASE_URL, accessTokens); );
try { ctx.body = data.data;
const data = await client.updateMedia( } catch (e: any) {
ctx.params.id, console.error(e);
ctx.request.body as any, ctx.status = 401;
); ctx.body = e.response.data;
ctx.body = data.data; }
} catch (e: any) { });
console.error(e); router.get<{ Params: { id: string } }>("/v1/polls/:id", async (ctx) => {
ctx.status = 401; const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
ctx.body = e.response.data; const accessTokens = ctx.headers.authorization;
} const client = getClient(BASE_URL, accessTokens);
}, try {
); const data = await client.getPoll(ctx.params.id);
router.get<{ Params: { id: string } }>( ctx.body = data.data;
"/v1/polls/:id", } catch (e: any) {
async (ctx) => { console.error(e);
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; ctx.status = 401;
const accessTokens = ctx.headers.authorization; ctx.body = e.response.data;
const client = getClient(BASE_URL, accessTokens); }
try { });
const data = await client.getPoll(ctx.params.id);
ctx.body = data.data;
} catch (e: any) {
console.error(e);
ctx.status = 401;
ctx.body = e.response.data;
}
},
);
router.post<{ Params: { id: string } }>( router.post<{ Params: { id: string } }>(
"/v1/polls/:id/votes", "/v1/polls/:id/votes",
async (ctx) => { async (ctx) => {

View file

@ -16,7 +16,8 @@ export function limitToInt(q: ParsedUrlQuery) {
export function argsToBools(q: ParsedUrlQuery) { export function argsToBools(q: ParsedUrlQuery) {
// Values taken from https://docs.joinmastodon.org/client/intro/#boolean // Values taken from https://docs.joinmastodon.org/client/intro/#boolean
const toBoolean = (value: string) => !['0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].includes(value); const toBoolean = (value: string) =>
!["0", "f", "F", "false", "FALSE", "off", "OFF"].includes(value);
let object: any = q; let object: any = q;
if (q.only_media) if (q.only_media)
@ -35,26 +36,26 @@ export function toTextWithReaction(status: Entity.Status[], host: string) {
if (!t.emoji_reactions) return t; if (!t.emoji_reactions) return t;
if (t.reblog) t.reblog = toTextWithReaction([t.reblog], host)[0]; if (t.reblog) t.reblog = toTextWithReaction([t.reblog], host)[0];
const reactions = t.emoji_reactions.map((r) => { const reactions = t.emoji_reactions.map((r) => {
const emojiNotation = r.url ? `:${r.name.replace('@.', '')}:` : r.name const emojiNotation = r.url ? `:${r.name.replace("@.", "")}:` : r.name;
return `${emojiNotation} (${r.count}${r.me ? `* ` : ''})` return `${emojiNotation} (${r.count}${r.me ? `* ` : ""})`;
}); });
const reaction = t.emoji_reactions as Entity.Reaction[]; const reaction = t.emoji_reactions as Entity.Reaction[];
const emoji = t.emojis || [] const emoji = t.emojis || [];
for (const r of reaction) { for (const r of reaction) {
if (!r.url) continue if (!r.url) continue;
emoji.push({ emoji.push({
'shortcode': r.name, shortcode: r.name,
'url': r.url, url: r.url,
'static_url': r.url, static_url: r.url,
'visible_in_picker': true, visible_in_picker: true,
category: "" category: "",
},) });
} }
const isMe = reaction.findIndex((r) => r.me) > -1; const isMe = reaction.findIndex((r) => r.me) > -1;
const total = reaction.reduce((sum, reaction) => sum + reaction.count, 0); const total = reaction.reduce((sum, reaction) => sum + reaction.count, 0);
t.favourited = isMe; t.favourited = isMe;
t.favourites_count = total; t.favourites_count = total;
t.emojis = emoji t.emojis = emoji;
t.content = `<p>${autoLinker(t.content, host)}</p><p>${reactions.join( t.content = `<p>${autoLinker(t.content, host)}</p><p>${reactions.join(
", ", ", ",
)}</p>`; )}</p>`;
@ -126,23 +127,20 @@ export function apiTimelineMastodon(router: Router): void {
} }
}, },
); );
router.get( router.get("/v1/timelines/home", async (ctx, reply) => {
"/v1/timelines/home", const BASE_URL = `${ctx.protocol}://${ctx.hostname}`;
async (ctx, reply) => { const accessTokens = ctx.headers.authorization;
const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; const client = getClient(BASE_URL, accessTokens);
const accessTokens = ctx.headers.authorization; try {
const client = getClient(BASE_URL, accessTokens); const data = await client.getHomeTimeline(limitToInt(ctx.query));
try { ctx.body = toTextWithReaction(data.data, ctx.hostname);
const data = await client.getHomeTimeline(limitToInt(ctx.query)); } catch (e: any) {
ctx.body = toTextWithReaction(data.data, ctx.hostname); console.error(e);
} catch (e: any) { console.error(e.response.data);
console.error(e); ctx.status = 401;
console.error(e.response.data); ctx.body = e.response.data;
ctx.status = 401; }
ctx.body = e.response.data; });
}
},
);
router.get<{ Params: { listId: string } }>( router.get<{ Params: { listId: string } }>(
"/v1/timelines/list/:listId", "/v1/timelines/list/:listId",
async (ctx, reply) => { async (ctx, reply) => {

View file

@ -12,7 +12,11 @@ import {
} from "@/models/index.js"; } from "@/models/index.js";
import type { ILocalUser } from "@/models/entities/user.js"; import type { ILocalUser } from "@/models/entities/user.js";
import { genId } from "@/misc/gen-id.js"; import { genId } from "@/misc/gen-id.js";
import { comparePassword, hashPassword, isOldAlgorithm } from '@/misc/password.js'; import {
comparePassword,
hashPassword,
isOldAlgorithm,
} from "@/misc/password.js";
import { verifyLogin, hash } from "../2fa.js"; import { verifyLogin, hash } from "../2fa.js";
import { randomBytes } from "node:crypto"; import { randomBytes } from "node:crypto";
import { IsNull } from "typeorm"; import { IsNull } from "typeorm";

View file

@ -414,7 +414,7 @@ export default class Connection {
const client = getClient(this.host, this.accessToken); const client = getClient(this.host, this.accessToken);
client.getStatus(payload.id).then((data) => { client.getStatus(payload.id).then((data) => {
const newPost = toTextWithReaction([data.data], this.host); const newPost = toTextWithReaction([data.data], this.host);
const targetPost = newPost[0] const targetPost = newPost[0];
for (const stream of this.currentSubscribe) { for (const stream of this.currentSubscribe) {
this.wsConnection.send( this.wsConnection.send(
JSON.stringify({ JSON.stringify({

View file

@ -31,7 +31,7 @@ import webServer from "./web/index.js";
import { initializeStreamingServer } from "./api/streaming.js"; import { initializeStreamingServer } from "./api/streaming.js";
import { koaBody } from "koa-body"; import { koaBody } from "koa-body";
import removeTrailingSlash from "koa-remove-trailing-slashes"; import removeTrailingSlash from "koa-remove-trailing-slashes";
import {v4 as uuid} from "uuid"; import { v4 as uuid } from "uuid";
export const serverLogger = new Logger("server", "gray", false); export const serverLogger = new Logger("server", "gray", false);
@ -162,19 +162,19 @@ mastoRouter.get("/oauth/authorize", async (ctx) => {
const { client_id, state, redirect_uri } = ctx.request.query; const { client_id, state, redirect_uri } = ctx.request.query;
console.log(ctx.request.req); console.log(ctx.request.req);
let param = "mastodon=true"; let param = "mastodon=true";
if (state) if (state) param += `&state=${state}`;
param += `&state=${state}`; if (redirect_uri) param += `&redirect_uri=${redirect_uri}`;
if (redirect_uri) const client = client_id ? client_id : "";
param += `&redirect_uri=${redirect_uri}`; ctx.redirect(
const client = client_id? client_id : ""; `${Buffer.from(client.toString(), "base64").toString()}?${param}`,
ctx.redirect(`${Buffer.from(client.toString(), 'base64').toString()}?${param}`); );
}); });
mastoRouter.post("/oauth/token", async (ctx) => { mastoRouter.post("/oauth/token", async (ctx) => {
const body: any = ctx.request.body || ctx.request.query; const body: any = ctx.request.body || ctx.request.query;
console.log('token-request', body); console.log("token-request", body);
console.log('token-query', ctx.request.query); console.log("token-query", ctx.request.query);
if (body.grant_type === 'client_credentials') { if (body.grant_type === "client_credentials") {
const ret = { const ret = {
access_token: uuid(), access_token: uuid(),
token_type: "Bearer", token_type: "Bearer",
@ -197,8 +197,8 @@ mastoRouter.post("/oauth/token", async (ctx) => {
// return; // return;
//} //}
//token = `${m[1]}-${m[2]}-${m[3]}-${m[4]}-${m[5]}` //token = `${m[1]}-${m[2]}-${m[3]}-${m[4]}-${m[5]}`
console.log(body.code, token) console.log(body.code, token);
token = body.code token = body.code;
} }
if (client_id instanceof Array) { if (client_id instanceof Array) {
client_id = client_id.toString(); client_id = client_id.toString();
@ -214,10 +214,10 @@ mastoRouter.post("/oauth/token", async (ctx) => {
const ret = { const ret = {
access_token: atData.accessToken, access_token: atData.accessToken,
token_type: "Bearer", token_type: "Bearer",
scope: body.scope || 'read write follow push', scope: body.scope || "read write follow push",
created_at: Math.floor(new Date().getTime() / 1000), created_at: Math.floor(new Date().getTime() / 1000),
}; };
console.log('token-response', ret) console.log("token-response", ret);
ctx.body = ret; ctx.body = ret;
} catch (err: any) { } catch (err: any) {
console.error(err); console.error(err);

View file

@ -136,7 +136,9 @@ export const routes = [
{ {
path: "/custom-katex-macro", path: "/custom-katex-macro",
name: "custom-katex-macro", name: "custom-katex-macro",
component: page(() => import("./pages/settings/custom-katex-macro.vue")), component: page(
() => import("./pages/settings/custom-katex-macro.vue"),
),
}, },
{ {
path: "/account-info", path: "/account-info",
@ -243,7 +245,9 @@ export const routes = [
{ {
path: "/custom-katex-macro", path: "/custom-katex-macro",
name: "general", name: "general",
component: page(() => import("./pages/settings/custom-katex-macro.vue")), component: page(
() => import("./pages/settings/custom-katex-macro.vue"),
),
}, },
{ {
path: "/accounts", path: "/accounts",

View file

@ -262,7 +262,9 @@ export function getUserMenu(user, router: Router = mainRouter) {
menu = menu.concat([ menu = menu.concat([
null, null,
{ {
icon: user.isMuted ? "ph-eye ph-bold ph-lg" : "ph-eye-slash ph-bold ph-lg", icon: user.isMuted
? "ph-eye ph-bold ph-lg"
: "ph-eye-slash ph-bold ph-lg",
text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute, text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute,
hidden: user.isBlocking === true, hidden: user.isBlocking === true,
action: toggleMute, action: toggleMute,

View file

@ -1,19 +1,17 @@
type KaTeXMacro = { type KaTeXMacro = {
args: number; args: number;
rule: (string | number)[]; rule: (string | number)[];
}; };
function parseSingleKaTeXMacro(src: string): [string, KaTeXMacro] { function parseSingleKaTeXMacro(src: string): [string, KaTeXMacro] {
const invalid: [string, KaTeXMacro] = ["", { args: 0, rule: [] }]; const invalid: [string, KaTeXMacro] = ["", { args: 0, rule: [] }];
const skipSpaces = (pos: number): number => { const skipSpaces = (pos: number): number => {
while (src[pos] === " ") while (src[pos] === " ") ++pos;
++pos;
return pos; return pos;
}; };
if (!src.startsWith("\\newcommand") || src.slice(-1) !== "}") if (!src.startsWith("\\newcommand") || src.slice(-1) !== "}") return invalid;
return invalid;
// current index we are checking (= "\\newcommand".length) // current index we are checking (= "\\newcommand".length)
let currentPos: number = 11; let currentPos: number = 11;
@ -21,28 +19,21 @@ function parseSingleKaTeXMacro(src: string): [string, KaTeXMacro] {
// parse {\name}, (\name), or [\name] // parse {\name}, (\name), or [\name]
let bracket: string; let bracket: string;
if (src[currentPos] === "{") if (src[currentPos] === "{") bracket = "{}";
bracket = "{}"; else if (src[currentPos] === "(") bracket = "()";
else if (src[currentPos] === "(") else if (src[currentPos] === "[") bracket = "[]";
bracket = "()"; else return invalid;
else if (src[currentPos] === "[")
bracket = "[]";
else
return invalid;
++currentPos; ++currentPos;
currentPos = skipSpaces(currentPos); currentPos = skipSpaces(currentPos);
if (src[currentPos] !== "\\") if (src[currentPos] !== "\\") return invalid;
return invalid;
const closeNameBracketPos: number = src.indexOf(bracket[1], currentPos); const closeNameBracketPos: number = src.indexOf(bracket[1], currentPos);
if (closeNameBracketPos === -1) if (closeNameBracketPos === -1) return invalid;
return invalid;
const name: string = src.slice(currentPos + 1, closeNameBracketPos).trim(); const name: string = src.slice(currentPos + 1, closeNameBracketPos).trim();
if (!/^[a-zA-Z]+$/.test(name)) if (!/^[a-zA-Z]+$/.test(name)) return invalid;
return invalid;
currentPos = skipSpaces(closeNameBracketPos + 1); currentPos = skipSpaces(closeNameBracketPos + 1);
@ -54,8 +45,7 @@ function parseSingleKaTeXMacro(src: string): [string, KaTeXMacro] {
macro.args = Number(src.slice(currentPos + 1, closeArgsBracketPos).trim()); macro.args = Number(src.slice(currentPos + 1, closeArgsBracketPos).trim());
currentPos = closeArgsBracketPos + 1; currentPos = closeArgsBracketPos + 1;
if (Number.isNaN(macro.args) || macro.args < 0) if (Number.isNaN(macro.args) || macro.args < 0) return invalid;
return invalid;
} else if (src[currentPos] === "{") { } else if (src[currentPos] === "{") {
macro.args = 0; macro.args = 0;
} else { } else {
@ -65,8 +55,7 @@ function parseSingleKaTeXMacro(src: string): [string, KaTeXMacro] {
currentPos = skipSpaces(currentPos); currentPos = skipSpaces(currentPos);
// parse {rule} // parse {rule}
if (src[currentPos] !== "{") if (src[currentPos] !== "{") return invalid;
return invalid;
++currentPos; ++currentPos;
currentPos = skipSpaces(currentPos); currentPos = skipSpaces(currentPos);
@ -94,8 +83,11 @@ function parseSingleKaTeXMacro(src: string): [string, KaTeXMacro] {
break; break;
} }
const argIndexEndPos = src.slice(numbersignPos + 1).search(/[^\d]/) + numbersignPos; const argIndexEndPos =
const argIndex: number = Number(src.slice(numbersignPos + 1, argIndexEndPos + 1)); src.slice(numbersignPos + 1).search(/[^\d]/) + numbersignPos;
const argIndex: number = Number(
src.slice(numbersignPos + 1, argIndexEndPos + 1),
);
if (Number.isNaN(argIndex) || argIndex < 1 || macro.args < argIndex) if (Number.isNaN(argIndex) || argIndex < 1 || macro.args < argIndex)
return invalid; return invalid;
@ -107,10 +99,8 @@ function parseSingleKaTeXMacro(src: string): [string, KaTeXMacro] {
currentPos = argIndexEndPos + 1; currentPos = argIndexEndPos + 1;
} }
if (macro.args === 0) if (macro.args === 0) return [name, macro];
return [name, macro]; else return [name + bracket[0], macro];
else
return [name + bracket[0], macro];
} }
export function parseKaTeXMacros(src: string): string { export function parseKaTeXMacros(src: string): string {
@ -118,8 +108,7 @@ export function parseKaTeXMacros(src: string): string {
for (const s of src.split("\n")) { for (const s of src.split("\n")) {
const [name, macro]: [string, KaTeXMacro] = parseSingleKaTeXMacro(s.trim()); const [name, macro]: [string, KaTeXMacro] = parseSingleKaTeXMacro(s.trim());
if (name !== "") if (name !== "") result[name] = macro;
result[name] = macro;
} }
return JSON.stringify(result); return JSON.stringify(result);
@ -127,11 +116,22 @@ export function parseKaTeXMacros(src: string): string {
// returns [expanded text, whether something is expanded, how many times we can expand more] // returns [expanded text, whether something is expanded, how many times we can expand more]
// the boolean value is used for multi-pass expansions (macros can expand to other macros) // the boolean value is used for multi-pass expansions (macros can expand to other macros)
function expandKaTeXMacroOnce(src: string, macros: { [name: string]: KaTeXMacro }, maxNumberOfExpansions: number) function expandKaTeXMacroOnce(
: [string, boolean, number] { src: string,
macros: { [name: string]: KaTeXMacro },
maxNumberOfExpansions: number,
): [string, boolean, number] {
const bracketKinds = 3; const bracketKinds = 3;
const openBracketId: { [bracket: string]: number } = {"(": 0, "{": 1, "[": 2}; const openBracketId: { [bracket: string]: number } = {
const closeBracketId: { [bracket: string]: number } = {")": 0, "}": 1, "]": 2}; "(": 0,
"{": 1,
"[": 2,
};
const closeBracketId: { [bracket: string]: number } = {
")": 0,
"}": 1,
"]": 2,
};
const openBracketFromId = ["(", "{", "["]; const openBracketFromId = ["(", "{", "["];
const closeBracketFromId = [")", "}", "]"]; const closeBracketFromId = [")", "}", "]"];
@ -142,21 +142,29 @@ function expandKaTeXMacroOnce(src: string, macros: { [name: string]: KaTeXMacro
let result: BracketMapping = {}; let result: BracketMapping = {};
const n = src.length; const n = src.length;
let depths = new Array<number>(bracketKinds).fill(0); // current bracket depth for "()", "{}", and "[]" let depths = new Array<number>(bracketKinds).fill(0); // current bracket depth for "()", "{}", and "[]"
let buffer = Array.from(Array<number[]>(bracketKinds), () => Array<number>(n)); let buffer = Array.from(Array<number[]>(bracketKinds), () =>
Array<number>(n),
);
let isEscaped = false; let isEscaped = false;
for (let i = 0; i < n; ++i) { for (let i = 0; i < n; ++i) {
if (!isEscaped && src[i] === "\\" && i + 1 < n && ["{", "}", "\\"].includes(src[i+1])) { if (
!isEscaped &&
src[i] === "\\" &&
i + 1 < n &&
["{", "}", "\\"].includes(src[i + 1])
) {
isEscaped = true; isEscaped = true;
continue; continue;
} }
if (isEscaped if (
|| (src[i] !== "\\" isEscaped ||
&& !openBracketFromId.includes(src[i]) (src[i] !== "\\" &&
&& !closeBracketFromId.includes(src[i]))) !openBracketFromId.includes(src[i]) &&
{ !closeBracketFromId.includes(src[i]))
) {
isEscaped = false; isEscaped = false;
continue; continue;
} }
@ -178,27 +186,29 @@ function expandKaTeXMacroOnce(src: string, macros: { [name: string]: KaTeXMacro
return result; return result;
})(); })();
function expandSingleKaTeXMacro(expandedArgs: string[], macroName: string): string { function expandSingleKaTeXMacro(
expandedArgs: string[],
macroName: string,
): string {
let result = ""; let result = "";
for (const block of macros[macroName].rule) { for (const block of macros[macroName].rule) {
if (typeof block === "string") if (typeof block === "string") result += block;
result += block; else result += expandedArgs[block - 1];
else
result += expandedArgs[block - 1];
} }
return result; return result;
} }
// only expand src.slice(beginPos, endPos) // only expand src.slice(beginPos, endPos)
function expandKaTeXMacroImpl(beginPos: number, endPos: number): [string, boolean] { function expandKaTeXMacroImpl(
if (endPos <= beginPos) beginPos: number,
return ["", false]; endPos: number,
): [string, boolean] {
if (endPos <= beginPos) return ["", false];
const raw: string = src.slice(beginPos, endPos); const raw: string = src.slice(beginPos, endPos);
const fallback: string = raw; // returned for invalid inputs or too many expansions const fallback: string = raw; // returned for invalid inputs or too many expansions
if (maxNumberOfExpansions <= 0) if (maxNumberOfExpansions <= 0) return [fallback, false];
return [fallback, false];
--maxNumberOfExpansions; --maxNumberOfExpansions;
// search for a custom macro // search for a custom macro
@ -218,14 +228,13 @@ function expandKaTeXMacroOnce(src: string, macros: { [name: string]: KaTeXMacro
checkedPos = src.indexOf("\\", checkedPos + 1); checkedPos = src.indexOf("\\", checkedPos + 1);
// there is no macro to expand // there is no macro to expand
if (checkedPos === -1) if (checkedPos === -1) return [raw, false];
return [raw, false];
// is it a custom macro? // is it a custom macro?
let nonAlphaPos = src.slice(checkedPos + 1).search(/[^A-Za-z]/) + checkedPos + 1; let nonAlphaPos =
src.slice(checkedPos + 1).search(/[^A-Za-z]/) + checkedPos + 1;
if (nonAlphaPos === checkedPos) if (nonAlphaPos === checkedPos) nonAlphaPos = endPos;
nonAlphaPos = endPos;
let macroNameCandidate = src.slice(checkedPos + 1, nonAlphaPos); let macroNameCandidate = src.slice(checkedPos + 1, nonAlphaPos);
if (macros.hasOwnProperty(macroNameCandidate)) { if (macros.hasOwnProperty(macroNameCandidate)) {
@ -239,19 +248,17 @@ function expandKaTeXMacroOnce(src: string, macros: { [name: string]: KaTeXMacro
let nextOpenBracketPos = endPos; let nextOpenBracketPos = endPos;
for (let i = 0; i < bracketKinds; ++i) { for (let i = 0; i < bracketKinds; ++i) {
const pos = src.indexOf(openBracketFromId[i], checkedPos + 1); const pos = src.indexOf(openBracketFromId[i], checkedPos + 1);
if (pos !== -1 && pos < nextOpenBracketPos) if (pos !== -1 && pos < nextOpenBracketPos) nextOpenBracketPos = pos;
nextOpenBracketPos = pos;
} }
if (nextOpenBracketPos === endPos) if (nextOpenBracketPos === endPos) continue; // there is no open bracket
continue; // there is no open bracket
macroNameCandidate += src[nextOpenBracketPos]; macroNameCandidate += src[nextOpenBracketPos];
if (macros.hasOwnProperty(macroNameCandidate)) { if (macros.hasOwnProperty(macroNameCandidate)) {
macroBackslashPos = checkedPos; macroBackslashPos = checkedPos;
macroArgBeginPos = nextOpenBracketPos; macroArgBeginPos = nextOpenBracketPos;
macroArgEndPos = nextOpenBracketPos; // to search the first arg from here macroArgEndPos = nextOpenBracketPos; // to search the first arg from here
macroName = macroNameCandidate; macroName = macroNameCandidate;
break; break;
} }
@ -265,31 +272,46 @@ function expandKaTeXMacroOnce(src: string, macros: { [name: string]: KaTeXMacro
for (let i = 0; i < numArgs; ++i) { for (let i = 0; i < numArgs; ++i) {
// find the first open bracket after what we've searched // find the first open bracket after what we've searched
const nextOpenBracketPos = src.indexOf(openBracket, macroArgEndPos); const nextOpenBracketPos = src.indexOf(openBracket, macroArgEndPos);
if (nextOpenBracketPos === -1) if (nextOpenBracketPos === -1) return [fallback, false]; // not enough arguments are provided
return [fallback, false]; // not enough arguments are provided if (!bracketMapping[nextOpenBracketPos]) return [fallback, false]; // found open bracket doesn't correspond to any close bracket
if (!bracketMapping[nextOpenBracketPos])
return [fallback, false]; // found open bracket doesn't correspond to any close bracket
macroArgEndPos = bracketMapping[nextOpenBracketPos]; macroArgEndPos = bracketMapping[nextOpenBracketPos];
expandedArgs[i] = expandKaTeXMacroImpl(nextOpenBracketPos + 1, macroArgEndPos)[0]; expandedArgs[i] = expandKaTeXMacroImpl(
nextOpenBracketPos + 1,
macroArgEndPos,
)[0];
} }
return [src.slice(beginPos, macroBackslashPos) return [
+ expandSingleKaTeXMacro(expandedArgs, macroName) src.slice(beginPos, macroBackslashPos) +
+ expandKaTeXMacroImpl(macroArgEndPos + 1, endPos)[0], true]; expandSingleKaTeXMacro(expandedArgs, macroName) +
expandKaTeXMacroImpl(macroArgEndPos + 1, endPos)[0],
true,
];
} }
const [expandedText, expandedFlag]: [string, boolean] = expandKaTeXMacroImpl(0, src.length); const [expandedText, expandedFlag]: [string, boolean] = expandKaTeXMacroImpl(
0,
src.length,
);
return [expandedText, expandedFlag, maxNumberOfExpansions]; return [expandedText, expandedFlag, maxNumberOfExpansions];
} }
export function expandKaTeXMacro(src: string, macrosAsJSONString: string, maxNumberOfExpansions: number): string { export function expandKaTeXMacro(
src: string,
macrosAsJSONString: string,
maxNumberOfExpansions: number,
): string {
const macros = JSON.parse(macrosAsJSONString); const macros = JSON.parse(macrosAsJSONString);
let expandMore = true; let expandMore = true;
while (expandMore && (0 < maxNumberOfExpansions)) while (expandMore && 0 < maxNumberOfExpansions)
[src, expandMore, maxNumberOfExpansions] = expandKaTeXMacroOnce(src, macros, maxNumberOfExpansions); [src, expandMore, maxNumberOfExpansions] = expandKaTeXMacroOnce(
src,
macros,
maxNumberOfExpansions,
);
return src; return src;
} }

View file

@ -4,15 +4,19 @@ import { expandKaTeXMacro } from "@/scripts/katex-macro";
export function preprocess(text: string): string { export function preprocess(text: string): string {
if (defaultStore.state.enableCustomKaTeXMacro) { if (defaultStore.state.enableCustomKaTeXMacro) {
const parsedKaTeXMacro = localStorage.getItem("customKaTeXMacroParsed") ?? "{}"; const parsedKaTeXMacro =
const maxNumberOfExpansions = 200; // to prevent infinite expansion loops localStorage.getItem("customKaTeXMacroParsed") ?? "{}";
const maxNumberOfExpansions = 200; // to prevent infinite expansion loops
let nodes = mfm.parse(text); let nodes = mfm.parse(text);
for (let node of nodes) { for (let node of nodes) {
if (node["type"] === "mathInline" || node["type"] === "mathBlock") { if (node["type"] === "mathInline" || node["type"] === "mathBlock") {
node["props"]["formula"] node["props"]["formula"] = expandKaTeXMacro(
= expandKaTeXMacro(node["props"]["formula"], parsedKaTeXMacro, maxNumberOfExpansions); node["props"]["formula"],
parsedKaTeXMacro,
maxNumberOfExpansions,
);
} }
} }