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 { Emoji } from '@/models/entities/emoji.js'; import { 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; // eslint-disable-next-line import/no-default-export 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 ) ), ]); let descSplit = splitN(meta.description, '\n', 2); let shortDesc = markup(descSplit.length > 0 ? descSplit[0]: ''); let 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 => e in emojis).map(e => ({ shortcode: e, static_url: `${config.url}/files/${emojis[e].publicUrl}`, url: `${config.url}/files/${emojis[e].publicUrl}`, 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)) ?? '';