// SPDX-FileCopyrightText: 2026 TatuChat // // SPDX-License-Identifier: AGPL-3.0-or-later /// Names of the `` tags a mini app document must declare to be /// installable. They act as the mini app's manifest. const String miniAppMetaIdTag = 'mini-app-id'; const String miniAppMetaNameTag = 'mini-app-name'; const String miniAppMetaCategoryTag = 'mini-app-category'; const String miniAppMetaDescriptionTag = 'mini-app-description'; const String miniAppMetaLogoTag = 'mini-app-logo'; const String miniAppMetaInChatTag = 'mini-app-in-chat'; const List miniAppMetaTags = [ miniAppMetaIdTag, miniAppMetaNameTag, miniAppMetaCategoryTag, miniAppMetaDescriptionTag, miniAppMetaLogoTag, miniAppMetaInChatTag, ]; /// Manifest of a mini app, extracted from its `` tags. /// /// [id] is mandatory and non-empty. It is used as the account data key under /// which the app's store and HTML source are persisted /// (`im.tatuchat.miniapps.data.` / `im.tatuchat.miniapps.source.`). class MiniAppMeta { final String id; final String name; final String category; final String description; final String logo; /// Whether the mini app can run inside a chat room. `false` by default. final bool inChat; const MiniAppMeta({ required this.id, required this.name, this.category = '', this.description = '', this.logo = '', this.inChat = false, }); } /// Extracts the meta tags of an HTML document into a `name -> content` map /// (names normalized to lower case). Attribute order and quoting style are /// irrelevant. Map extractMetaTags(String html) { final result = {}; final metaRegex = RegExp(r']*>', caseSensitive: false); final attrRegex = RegExp( r"""(name|content)\s*=\s*["']([^"']*)["']""", caseSensitive: false, ); for (final match in metaRegex.allMatches(html)) { final attributes = {}; for (final attr in attrRegex.allMatches(match.group(0)!)) { attributes[attr.group(1)!.toLowerCase()] = attr.group(2)!; } final name = attributes['name']; final content = attributes['content']; if (name != null && content != null) { result[name.toLowerCase()] = content; } } return result; } /// Parses the mini app manifest out of the `` tags of [html]. /// /// All [miniAppMetaTags] must be present (their content may be empty), except /// `mini-app-id` and `mini-app-name`, which must also be non-empty. Throws a /// [FormatException] with a human readable reason otherwise. MiniAppMeta parseMiniAppMeta(String html) { final tags = extractMetaTags(html); final missing = [ for (final tag in miniAppMetaTags) if (!tags.containsKey(tag)) tag, ]; if (missing.isNotEmpty) { throw FormatException( 'This document is missing the required meta tag(s): ' '${missing.join(', ')}', ); } final id = tags[miniAppMetaIdTag]!.trim(); final name = tags[miniAppMetaNameTag]!.trim(); if (id.isEmpty) { throw FormatException( 'The meta tag "$miniAppMetaIdTag" must not be empty', ); } if (name.isEmpty) { throw FormatException( 'The meta tag "$miniAppMetaNameTag" must not be empty', ); } return MiniAppMeta( id: id, name: name, category: tags[miniAppMetaCategoryTag]!.trim(), description: tags[miniAppMetaDescriptionTag]!.trim(), logo: tags[miniAppMetaLogoTag]!.trim(), inChat: tags[miniAppMetaInChatTag]!.trim().toLowerCase() == 'true', ); }