Files
tatuchat/lib/utils/miniapp_meta.dart
Welton Moura 6c689d1f34
Some checks failed
Main Deploy Workflow / deploy_web (push) Has been cancelled
Main Deploy Workflow / deploy_playstore_internal (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
feat: tela de apps com logos/menu de exclusao, correcao de tema nas mini apps e sidebar de navegacao
2026-09-19 00:08:35 -03:00

109 lines
3.4 KiB
Dart

// SPDX-FileCopyrightText: 2026 TatuChat
//
// SPDX-License-Identifier: AGPL-3.0-or-later
/// Names of the `<meta>` 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<String> miniAppMetaTags = [
miniAppMetaIdTag,
miniAppMetaNameTag,
miniAppMetaCategoryTag,
miniAppMetaDescriptionTag,
miniAppMetaLogoTag,
miniAppMetaInChatTag,
];
/// Manifest of a mini app, extracted from its `<meta>` 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.<id>` / `im.tatuchat.miniapps.source.<id>`).
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<String, String> extractMetaTags(String html) {
final result = <String, String>{};
final metaRegex = RegExp(r'<meta\b[^>]*>', caseSensitive: false);
final attrRegex = RegExp(
r"""(name|content)\s*=\s*["']([^"']*)["']""",
caseSensitive: false,
);
for (final match in metaRegex.allMatches(html)) {
final attributes = <String, String>{};
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 `<meta>` 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',
);
}