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)) ?? "";