Implement components + uploads flow and gateway-aligned payloads
This commit is contained in:
1 parent
458be6f991
commit
a93d619b4c
7 files changed
+440
-11
No files matched your search
@@ -1,3 +1,5 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:chat_server/models.dart';
|
||||
import 'package:chat_server/objectbox.g.dart';
|
||||
import 'package:relic/relic.dart';
|
||||
@@ -135,12 +137,13 @@ Future<Response> sendMessageHandler(Request req) async {
|
||||
}
|
||||
|
||||
final body = await req.asJson();
|
||||
final content = body['content'] as String?;
|
||||
final channelId = body['channelId'] as int?;
|
||||
final components = _parseComponents(body);
|
||||
final contentOverride = (body['content'] as String?)?.trim();
|
||||
|
||||
if (content == null || content.trim().isEmpty) {
|
||||
if (components == null) {
|
||||
return Response.badRequest(
|
||||
body: Body.fromString('Content required'),
|
||||
body: Body.fromString('content or components/componentsJson required'),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -177,11 +180,13 @@ Future<Response> sendMessageHandler(Request req) async {
|
||||
authorUsername: user.username,
|
||||
authorAvatarUrl: user.avatarUrl,
|
||||
authorHomeServer: user.homeServer,
|
||||
content: content.trim(),
|
||||
content: _buildSearchContent(components, contentOverride),
|
||||
componentsJson: jsonEncode(components),
|
||||
);
|
||||
|
||||
final id = messageBox.put(message);
|
||||
message.id = id;
|
||||
_attachReferencedFiles(components);
|
||||
|
||||
broadcastMessageCreated(
|
||||
channelId: channelId,
|
||||
@@ -192,6 +197,7 @@ Future<Response> sendMessageHandler(Request req) async {
|
||||
body: {
|
||||
'id': id,
|
||||
'content': message.content,
|
||||
'componentsJson': message.componentsJson,
|
||||
'authorId': user.userId,
|
||||
'authorUsername': user.username,
|
||||
'authorAvatarUrl': user.avatarUrl,
|
||||
@@ -244,17 +250,20 @@ Future<Response> editMessageHandler(Request req) async {
|
||||
}
|
||||
|
||||
final body = await req.asJson();
|
||||
final content = body['content'] as String?;
|
||||
final components = _parseComponents(body);
|
||||
final contentOverride = (body['content'] as String?)?.trim();
|
||||
|
||||
if (content == null || content.trim().isEmpty) {
|
||||
if (components == null) {
|
||||
return Response.badRequest(
|
||||
body: Body.fromString('Content required'),
|
||||
body: Body.fromString('content or components/componentsJson required'),
|
||||
);
|
||||
}
|
||||
|
||||
message.content = content.trim();
|
||||
message.content = _buildSearchContent(components, contentOverride);
|
||||
message.componentsJson = jsonEncode(components);
|
||||
message.editedAt = DateTime.now();
|
||||
messageBox.put(message);
|
||||
_attachReferencedFiles(components);
|
||||
|
||||
broadcastMessageUpdated(
|
||||
channelId: message.channelId,
|
||||
@@ -265,6 +274,7 @@ Future<Response> editMessageHandler(Request req) async {
|
||||
body: {
|
||||
'id': message.id,
|
||||
'content': message.content,
|
||||
'componentsJson': message.componentsJson,
|
||||
'editedAt': message.editedAt?.toIso8601String(),
|
||||
}.toBody,
|
||||
);
|
||||
@@ -332,6 +342,7 @@ Map<String, dynamic> _messageToMap(ChatMessage message) {
|
||||
return {
|
||||
'id': message.id,
|
||||
'content': message.content,
|
||||
'componentsJson': message.componentsJson,
|
||||
'authorId': message.authorUserId,
|
||||
'authorUsername': message.authorUsername,
|
||||
'authorAvatarUrl': message.authorAvatarUrl,
|
||||
@@ -341,3 +352,136 @@ Map<String, dynamic> _messageToMap(ChatMessage message) {
|
||||
'editedAt': message.editedAt?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>>? _parseComponents(Map<String, dynamic> body) {
|
||||
final rawComponents = body['components'];
|
||||
if (rawComponents is List) {
|
||||
final components = <Map<String, dynamic>>[];
|
||||
for (final component in rawComponents) {
|
||||
if (component is Map<String, dynamic>) {
|
||||
components.add(component);
|
||||
} else if (component is Map) {
|
||||
components.add(component.map((k, v) => MapEntry(k.toString(), v)));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return components;
|
||||
}
|
||||
|
||||
final rawComponentsJson = body['componentsJson'];
|
||||
if (rawComponentsJson is String && rawComponentsJson.trim().isNotEmpty) {
|
||||
dynamic decoded;
|
||||
try {
|
||||
decoded = jsonDecode(rawComponentsJson);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
if (decoded is! List) {
|
||||
return null;
|
||||
}
|
||||
final components = <Map<String, dynamic>>[];
|
||||
for (final component in decoded) {
|
||||
if (component is Map<String, dynamic>) {
|
||||
components.add(component);
|
||||
} else if (component is Map) {
|
||||
components.add(component.map((k, v) => MapEntry(k.toString(), v)));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return components;
|
||||
}
|
||||
|
||||
final content = (body['content'] as String?)?.trim();
|
||||
if (content == null || content.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Map<String, dynamic>>[
|
||||
{
|
||||
'type': 'text',
|
||||
'modifiers': <String>[],
|
||||
'fallback': content,
|
||||
'data': {
|
||||
'text': content,
|
||||
},
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
String _buildSearchContent(
|
||||
List<Map<String, dynamic>> components,
|
||||
String? contentOverride,
|
||||
) {
|
||||
if (contentOverride != null && contentOverride.isNotEmpty) {
|
||||
return contentOverride;
|
||||
}
|
||||
|
||||
final parts = <String>[];
|
||||
for (final component in components) {
|
||||
final fallback = component['fallback'];
|
||||
if (fallback is String && fallback.trim().isNotEmpty) {
|
||||
parts.add(fallback.trim());
|
||||
}
|
||||
|
||||
final data = component['data'];
|
||||
if (data is! Map) {
|
||||
continue;
|
||||
}
|
||||
|
||||
void addDataString(String key) {
|
||||
final value = data[key];
|
||||
if (value is String && value.trim().isNotEmpty) {
|
||||
parts.add(value.trim());
|
||||
}
|
||||
}
|
||||
|
||||
addDataString('text');
|
||||
addDataString('title');
|
||||
addDataString('description');
|
||||
addDataString('caption');
|
||||
}
|
||||
|
||||
final content = parts.join(' ').trim();
|
||||
return content.isEmpty ? '[component]' : content;
|
||||
}
|
||||
|
||||
void _attachReferencedFiles(List<Map<String, dynamic>> components) {
|
||||
final referencedFileIds = <int>{};
|
||||
for (final component in components) {
|
||||
final data = component['data'];
|
||||
if (data is! Map) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final items = data['items'];
|
||||
if (items is! List) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (final item in items) {
|
||||
if (item is! Map) {
|
||||
continue;
|
||||
}
|
||||
final fileId = item['fileId'];
|
||||
final parsedId = switch (fileId) {
|
||||
int id when id > 0 => id,
|
||||
String value => int.tryParse(value),
|
||||
_ => null,
|
||||
};
|
||||
if (parsedId != null && parsedId > 0) {
|
||||
referencedFileIds.add(parsedId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (final fileId in referencedFileIds) {
|
||||
final file = fileBox.get(fileId);
|
||||
if (file == null || file.isAttached) {
|
||||
continue;
|
||||
}
|
||||
file.isAttached = true;
|
||||
fileBox.put(file);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user