From 466917feb601994fe86c8105cb8181a61539a57e Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Sat, 22 Jul 2023 20:15:58 +0200 Subject: [PATCH] Improve /api/v1/instance accuracy --- .../backend/src/server/api/compatibility.ts | 2 - .../endpoints/compatibility/instance-info.ts | 232 ------------------ .../mastodon/ApiMastodonCompatibleService.ts | 16 +- .../src/server/api/mastodon/endpoints/meta.ts | 80 ++---- 4 files changed, 30 insertions(+), 300 deletions(-) delete mode 100644 packages/backend/src/server/api/endpoints/compatibility/instance-info.ts diff --git a/packages/backend/src/server/api/compatibility.ts b/packages/backend/src/server/api/compatibility.ts index 42be40e104..624e5ff430 100644 --- a/packages/backend/src/server/api/compatibility.ts +++ b/packages/backend/src/server/api/compatibility.ts @@ -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], ]; diff --git a/packages/backend/src/server/api/endpoints/compatibility/instance-info.ts b/packages/backend/src/server/api/endpoints/compatibility/instance-info.ts deleted file mode 100644 index 4e692568c5..0000000000 --- a/packages/backend/src/server/api/endpoints/compatibility/instance-info.ts +++ /dev/null @@ -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), - ), - ]); - - 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, -): Promise => { - 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)) ?? ""; diff --git a/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts b/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts index f3fbd4badb..bd868c64b8 100644 --- a/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts +++ b/packages/backend/src/server/api/mastodon/ApiMastodonCompatibleService.ts @@ -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; diff --git a/packages/backend/src/server/api/mastodon/endpoints/meta.ts b/packages/backend/src/server/api/mastodon/endpoints/meta.ts index 9a81aad844..f77ed0d2a7 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/meta.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/meta.ts @@ -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: "

Please refer to the original instance for the actual admin contact.

", - 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: [], }; }