Improve /api/v1/instance accuracy
This commit is contained in:
parent
56bf7a0ffb
commit
466917feb6
4 changed files with 30 additions and 300 deletions
|
@ -1,11 +1,9 @@
|
|||
import type { IEndpoint } from "./endpoints";
|
||||
|
||||
import * as cp___instance_info from "./endpoints/compatibility/instance-info.js";
|
||||
import * as cp___custom_emojis from "./endpoints/compatibility/custom-emojis.js";
|
||||
import * as ep___instance_peers from "./endpoints/compatibility/peers.js";
|
||||
|
||||
const cps = [
|
||||
["v1/instance", cp___instance_info],
|
||||
["v1/custom_emojis", cp___custom_emojis],
|
||||
["v1/instance/peers", ep___instance_peers],
|
||||
];
|
||||
|
|
|
@ -1,232 +0,0 @@
|
|||
import * as mfm from "mfm-js";
|
||||
import { toHtml } from "@/mfm/to-html.js";
|
||||
import config from "@/config/index.js";
|
||||
import { fetchMeta } from "@/misc/fetch-meta.js";
|
||||
import {
|
||||
Users,
|
||||
Notes,
|
||||
Instances,
|
||||
UserProfiles,
|
||||
Emojis,
|
||||
DriveFiles,
|
||||
} from "@/models/index.js";
|
||||
import type { Emoji } from "@/models/entities/emoji.js";
|
||||
import type { User } from "@/models/entities/user.js";
|
||||
import { IsNull, In } from "typeorm";
|
||||
import { MAX_NOTE_TEXT_LENGTH, FILE_TYPE_BROWSERSAFE } from "@/const.js";
|
||||
import define from "../../define.js";
|
||||
|
||||
export const meta = {
|
||||
requireCredential: false,
|
||||
requireCredentialPrivateMode: true,
|
||||
allowGet: true,
|
||||
|
||||
tags: ["meta"],
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: "object",
|
||||
properties: {},
|
||||
required: [],
|
||||
} as const;
|
||||
|
||||
export default define(meta, paramDef, async () => {
|
||||
const now = Date.now();
|
||||
const [meta, total, localPosts, instanceCount, firstAdmin, emojis] =
|
||||
await Promise.all([
|
||||
fetchMeta(true),
|
||||
Users.count({ where: { host: IsNull() } }),
|
||||
Notes.count({ where: { userHost: IsNull(), replyId: IsNull() } }),
|
||||
Instances.count(),
|
||||
Users.findOne({
|
||||
where: {
|
||||
host: IsNull(),
|
||||
isAdmin: true,
|
||||
isDeleted: false,
|
||||
isBot: false,
|
||||
},
|
||||
order: { id: "ASC" },
|
||||
}),
|
||||
Emojis.find({
|
||||
where: { host: IsNull(), type: In(FILE_TYPE_BROWSERSAFE) },
|
||||
select: ["id", "name", "originalUrl", "publicUrl"],
|
||||
}).then((l) =>
|
||||
l.reduce((a, e) => {
|
||||
a[e.name] = e;
|
||||
return a;
|
||||
}, {} as Record<string, Emoji>),
|
||||
),
|
||||
]);
|
||||
|
||||
const descSplit = splitN(meta.description, "\n", 2);
|
||||
const shortDesc = markup(descSplit.length > 0 ? descSplit[0] : "");
|
||||
const longDesc = markup(meta.description ?? "");
|
||||
|
||||
return {
|
||||
uri: config.hostname,
|
||||
title: meta.name,
|
||||
short_description: shortDesc,
|
||||
description: longDesc,
|
||||
email: meta.maintainerEmail,
|
||||
version: config.version,
|
||||
urls: {
|
||||
streaming_api: `wss://${config.host}`,
|
||||
},
|
||||
stats: {
|
||||
user_count: total,
|
||||
status_count: localPosts,
|
||||
domain_count: instanceCount,
|
||||
},
|
||||
thumbnail: meta.logoImageUrl,
|
||||
languages: meta.langs,
|
||||
registrations: !meta.disableRegistration,
|
||||
approval_required: false,
|
||||
invites_enabled: false,
|
||||
configuration: {
|
||||
accounts: {
|
||||
max_featured_tags: 16,
|
||||
},
|
||||
statuses: {
|
||||
max_characters: MAX_NOTE_TEXT_LENGTH,
|
||||
max_media_attachments: 16,
|
||||
characters_reserved_per_url: 0,
|
||||
},
|
||||
media_attachments: {
|
||||
supported_mime_types: FILE_TYPE_BROWSERSAFE,
|
||||
image_size_limit: 10485760,
|
||||
image_matrix_limit: 16777216,
|
||||
video_size_limit: 41943040,
|
||||
video_frame_rate_limit: 60,
|
||||
video_matrix_limit: 2304000,
|
||||
},
|
||||
polls: {
|
||||
max_options: 10,
|
||||
max_characters_per_option: 50,
|
||||
min_expiration: 15,
|
||||
max_expiration: -1,
|
||||
},
|
||||
},
|
||||
contact_account: await getContact(firstAdmin, emojis),
|
||||
rules: [],
|
||||
};
|
||||
});
|
||||
|
||||
const splitN = (s: string | null, split: string, n: number): string[] => {
|
||||
const ret: string[] = [];
|
||||
if (s == null) return ret;
|
||||
if (s === "") {
|
||||
ret.push(s);
|
||||
return ret;
|
||||
}
|
||||
|
||||
let start = 0;
|
||||
let pos = s.indexOf(split);
|
||||
if (pos === -1) {
|
||||
ret.push(s);
|
||||
return ret;
|
||||
}
|
||||
|
||||
for (let i = 0; i < n - 1; i++) {
|
||||
ret.push(s.substring(start, pos));
|
||||
start = pos + split.length;
|
||||
pos = s.indexOf(split, start);
|
||||
if (pos === -1) break;
|
||||
}
|
||||
ret.push(s.substring(start));
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
type ContactType = {
|
||||
id: string;
|
||||
username: string;
|
||||
acct: string;
|
||||
display_name: string;
|
||||
note?: string;
|
||||
noindex?: boolean;
|
||||
fields?: {
|
||||
name: string;
|
||||
value: string;
|
||||
verified_at: string | null;
|
||||
}[];
|
||||
locked: boolean;
|
||||
bot: boolean;
|
||||
created_at: string;
|
||||
url: string;
|
||||
followers_count: number;
|
||||
following_count: number;
|
||||
statuses_count: number;
|
||||
last_status_at?: string;
|
||||
emojis: any;
|
||||
} | null;
|
||||
|
||||
const getContact = async (
|
||||
user: User | null,
|
||||
emojis: Record<string, Emoji>,
|
||||
): Promise<ContactType> => {
|
||||
if (!user) return null;
|
||||
|
||||
let contact: ContactType = {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
acct: user.username,
|
||||
display_name: user.name ?? user.username,
|
||||
locked: user.isLocked,
|
||||
bot: user.isBot,
|
||||
created_at: user.createdAt.toISOString(),
|
||||
url: `${config.url}/@${user.username}`,
|
||||
followers_count: user.followersCount,
|
||||
following_count: user.followingCount,
|
||||
statuses_count: user.notesCount,
|
||||
last_status_at: user.lastActiveDate?.toISOString(),
|
||||
emojis: emojis
|
||||
? user.emojis
|
||||
.filter((e, i, a) => e in emojis && a.indexOf(e) === i)
|
||||
.map((e) => ({
|
||||
shortcode: e,
|
||||
static_url: emojis[e].publicUrl,
|
||||
url: emojis[e].originalUrl,
|
||||
visible_in_picker: true,
|
||||
}))
|
||||
: [],
|
||||
};
|
||||
|
||||
const [profile] = await Promise.all([
|
||||
UserProfiles.findOne({ where: { userId: user.id } }),
|
||||
loadDriveFiles(contact, "avatar", user.avatarId),
|
||||
loadDriveFiles(contact, "header", user.bannerId),
|
||||
]);
|
||||
|
||||
if (!profile) {
|
||||
return contact;
|
||||
}
|
||||
|
||||
contact = {
|
||||
...contact,
|
||||
note: markup(profile.description ?? ""),
|
||||
noindex: profile.noCrawle,
|
||||
fields: profile.fields.map((f) => ({
|
||||
name: f.name,
|
||||
value: f.value,
|
||||
verified_at: null,
|
||||
})),
|
||||
};
|
||||
|
||||
return contact;
|
||||
};
|
||||
|
||||
const loadDriveFiles = async (
|
||||
contact: any,
|
||||
key: string,
|
||||
fileId: string | null,
|
||||
) => {
|
||||
if (fileId) {
|
||||
const file = await DriveFiles.findOneBy({ id: fileId });
|
||||
if (file) {
|
||||
contact[key] = file.webpublicUrl ?? file.url;
|
||||
contact[`${key}_static`] = contact[key];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const markup = (text: string): string => toHtml(mfm.parse(text)) ?? "";
|
|
@ -8,8 +8,10 @@ import { apiTimelineMastodon } from "./endpoints/timeline.js";
|
|||
import { apiNotificationsMastodon } from "./endpoints/notifications.js";
|
||||
import { apiSearchMastodon } from "./endpoints/search.js";
|
||||
import { getInstance } from "./endpoints/meta.js";
|
||||
import { convertAnnouncement, convertFilter } from "./converters.js";
|
||||
import {convertAccount, convertAnnouncement, convertFilter} from "./converters.js";
|
||||
import { convertId, IdType } from "../index.js";
|
||||
import { Users } from "@/models/index.js";
|
||||
import { IsNull } from "typeorm";
|
||||
|
||||
export function getClient(
|
||||
BASE_URL: string,
|
||||
|
@ -52,7 +54,17 @@ export function apiMastodonCompatible(router: Router): void {
|
|||
// displayed without being logged in
|
||||
try {
|
||||
const data = await client.getInstance();
|
||||
ctx.body = await getInstance(data.data);
|
||||
const admin = await Users.findOne({
|
||||
where: {
|
||||
host: IsNull(),
|
||||
isAdmin: true,
|
||||
isDeleted: false,
|
||||
isSuspended: false,
|
||||
},
|
||||
order: { id: "ASC" },
|
||||
});
|
||||
const contact = admin == null ? null : convertAccount((await client.getAccount(admin.id)).data);
|
||||
ctx.body = await getInstance(data.data, contact);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
ctx.status = 401;
|
||||
|
|
|
@ -2,13 +2,17 @@ import { Entity } from "megalodon";
|
|||
import config from "@/config/index.js";
|
||||
import { fetchMeta } from "@/misc/fetch-meta.js";
|
||||
import { Users, Notes } from "@/models/index.js";
|
||||
import { IsNull, MoreThan } from "typeorm";
|
||||
import { IsNull } from "typeorm";
|
||||
import { MAX_NOTE_TEXT_LENGTH, FILE_TYPE_BROWSERSAFE } from "@/const.js";
|
||||
|
||||
export async function getInstance(response: Entity.Instance, contact: Entity.Account) {
|
||||
const [meta, totalUsers, totalStatuses] =
|
||||
await Promise.all([
|
||||
fetchMeta(true),
|
||||
Users.count({ where: { host: IsNull() } }),
|
||||
Notes.count({ where: { userHost: IsNull() } }),
|
||||
]);
|
||||
|
||||
// TODO: add firefish features
|
||||
export async function getInstance(response: Entity.Instance) {
|
||||
const meta = await fetchMeta(true);
|
||||
const totalUsers = Users.count({ where: { host: IsNull() } });
|
||||
const totalStatuses = Notes.count({ where: { userHost: IsNull() } });
|
||||
return {
|
||||
uri: response.uri,
|
||||
title: response.title || "Firefish",
|
||||
|
@ -35,41 +39,12 @@ export async function getInstance(response: Entity.Instance) {
|
|||
max_featured_tags: 20,
|
||||
},
|
||||
statuses: {
|
||||
max_characters: 3000,
|
||||
max_media_attachments: 4,
|
||||
max_characters: MAX_NOTE_TEXT_LENGTH,
|
||||
max_media_attachments: 16,
|
||||
characters_reserved_per_url: response.uri.length,
|
||||
},
|
||||
media_attachments: {
|
||||
supported_mime_types: [
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/gif",
|
||||
"image/heic",
|
||||
"image/heif",
|
||||
"image/webp",
|
||||
"image/avif",
|
||||
"video/webm",
|
||||
"video/mp4",
|
||||
"video/quicktime",
|
||||
"video/ogg",
|
||||
"audio/wave",
|
||||
"audio/wav",
|
||||
"audio/x-wav",
|
||||
"audio/x-pn-wave",
|
||||
"audio/vnd.wave",
|
||||
"audio/ogg",
|
||||
"audio/vorbis",
|
||||
"audio/mpeg",
|
||||
"audio/mp3",
|
||||
"audio/webm",
|
||||
"audio/flac",
|
||||
"audio/aac",
|
||||
"audio/m4a",
|
||||
"audio/x-m4a",
|
||||
"audio/mp4",
|
||||
"audio/3gpp",
|
||||
"video/x-ms-asf",
|
||||
],
|
||||
supported_mime_types: FILE_TYPE_BROWSERSAFE,
|
||||
image_size_limit: 10485760,
|
||||
image_matrix_limit: 16777216,
|
||||
video_size_limit: 41943040,
|
||||
|
@ -77,36 +52,13 @@ export async function getInstance(response: Entity.Instance) {
|
|||
video_matrix_limit: 2304000,
|
||||
},
|
||||
polls: {
|
||||
max_options: 8,
|
||||
max_options: 10,
|
||||
max_characters_per_option: 50,
|
||||
min_expiration: 300,
|
||||
min_expiration: 50,
|
||||
max_expiration: 2629746,
|
||||
},
|
||||
},
|
||||
contact_account: {
|
||||
id: "1",
|
||||
username: "admin",
|
||||
acct: "admin",
|
||||
display_name: "admin",
|
||||
locked: true,
|
||||
bot: true,
|
||||
discoverable: false,
|
||||
group: false,
|
||||
created_at: new Date().toISOString(),
|
||||
note: "<p>Please refer to the original instance for the actual admin contact.</p>",
|
||||
url: `${response.uri}/`,
|
||||
avatar: `${response.uri}/static-assets/badges/info.png`,
|
||||
avatar_static: `${response.uri}/static-assets/badges/info.png`,
|
||||
header: "/static-assets/transparent.png",
|
||||
header_static: "/static-assets/transparent.png",
|
||||
followers_count: -1,
|
||||
following_count: 0,
|
||||
statuses_count: 0,
|
||||
last_status_at: new Date().toISOString(),
|
||||
noindex: true,
|
||||
emojis: [],
|
||||
fields: [],
|
||||
},
|
||||
contact_account: contact,
|
||||
rules: [],
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue