Files
Qlyra/lib/backend/modules/messages.dart
T

379 lines
9.6 KiB
Dart

import 'dart:convert';
import 'package:flutter/foundation.dart';
import '../api.dart';
import '../../core/protocol/opcode_map.dart';
import '../../core/storage/app_database.dart';
import '../../models/attachment.dart';
class ContactCache {
static final Map<int, String> _cache = {};
static void put(int id, String name) {
_cache[id] = name;
}
static String? get(int id) => _cache[id];
}
class CachedMessage {
final String id;
final int accountId;
final int chatId;
final int senderId;
final String? text;
final int time;
final String? status;
final Map<String, dynamic>? payload;
final List<MessageAttachment>? attachments;
const CachedMessage({
required this.id,
required this.accountId,
required this.chatId,
required this.senderId,
this.text,
required this.time,
this.status,
this.payload,
this.attachments,
});
factory CachedMessage.fromDbRow(Map<String, dynamic> row) {
Map<String, dynamic>? payload;
final payloadRaw = row['payload'];
if (payloadRaw is String && payloadRaw.isNotEmpty) {
try {
payload = jsonDecode(payloadRaw) as Map<String, dynamic>;
} catch (_) {}
}
List<MessageAttachment>? attachments;
if (payload != null) {
final linkType = payload['link']?['type'] as String?;
if (linkType == 'FORWARD') {
attachments = [ForwardedMessageAttachment.fromMap(payload)];
} else {
final attaches = payload['attaches'] as List?;
if (attaches != null) {
attachments = attaches
.map(
(a) => MessageAttachment.fromMap(
Map<String, dynamic>.from(a as Map),
),
)
.toList();
}
}
}
return CachedMessage(
id: row['id'] as String,
accountId: row['account_id'] as int,
chatId: row['chat_id'] as int,
senderId: row['sender_id'] as int,
text: row['text'] as String?,
time: row['time'] as int,
status: row['status'] as String?,
payload: payload,
attachments: attachments,
);
}
Map<String, dynamic> toDbRow() => {
'id': id,
'account_id': accountId,
'chat_id': chatId,
'sender_id': senderId,
'text': text,
'time': time,
'status': status,
'payload': payload != null ? jsonEncode(payload) : null,
};
}
class MessagesModule {
final Api _api;
MessagesModule(this._api);
/// Загружает историю сообщений для указанного чата.
///
/// [fromTime] — опционально, время от которого грузить (миллисекунды).
/// Если не указано, грузит самые свежие.
/// [count] — количество сообщений.
Future<List<CachedMessage>> fetchHistory(
int accountId,
int chatId, {
int? fromTime,
int count = 50,
}) async {
final payload = {
'chatId': chatId,
'from':
fromTime ??
(DateTime.now().millisecondsSinceEpoch +
86400000), // +1 день для запаса
'forward': 0,
'backward': count,
'getMessages': true,
};
final response = await _api.sendRequest(Opcode.chatHistory, payload);
if (!response.isOk) return [];
final data = response.payload;
if (data is! Map) return [];
final messagesData = data['messages'];
if (messagesData is! List) return [];
final List<CachedMessage> results = [];
final List<Map<String, dynamic>> rows = [];
for (var i = 0; i < messagesData.length; i++) {
final m = messagesData[i];
if (m is! Map) continue;
final msg = _parseMessage(m.cast<dynamic, dynamic>(), accountId, chatId);
if (msg != null) {
results.add(msg);
rows.add(msg.toDbRow());
}
if (i > 0 && i % 20 == 0) {
await Future.delayed(Duration.zero);
}
}
if (rows.isNotEmpty) {
AppDatabase.saveMessages(rows).ignore();
}
return results;
}
/// Загружает сообщения из локальной базы данных.
Future<List<CachedMessage>> getLocalHistory(
int accountId,
int chatId, {
int limit = 50,
int offset = 0,
}) async {
final rows = await AppDatabase.loadMessages(
accountId,
chatId,
limit: limit,
offset: offset,
);
return rows.map(CachedMessage.fromDbRow).toList();
}
CachedMessage? _parseMessage(
Map<dynamic, dynamic> m,
int accountId,
int chatId,
) {
final id = m['id']?.toString();
if (id == null) return null;
final linkRaw = m['link'];
String? linkType;
if (linkRaw is Map) {
linkType = linkRaw['type'] as String?;
}
List<MessageAttachment>? attachments;
if (linkType == 'FORWARD') {
final fwdMap = Map<String, dynamic>.from(m.cast());
attachments = [ForwardedMessageAttachment.fromMap(fwdMap)];
} else {
final attaches = m['attaches'] as List?;
if (attaches != null) {
attachments = attaches
.whereType<Map>()
.map((a) => MessageAttachment.fromMap(Map<String, dynamic>.from(a)))
.toList();
}
}
return CachedMessage(
id: id,
accountId: accountId,
chatId: chatId,
senderId: (m['sender'] as int?) ?? 0,
text: m['text'] as String?,
time: (m['time'] as int?) ?? 0,
status: m['status'] as String?,
payload: Map<String, dynamic>.from(m.cast()),
attachments: attachments,
);
}
Future<void> sendMessage(
int accountId,
int chatId,
String text, {
bool notify = true,
}) async {
final payload = {
'chatId': chatId,
'message': {
'text': text,
'cid': DateTime.now().millisecondsSinceEpoch * -1,
'elements': [],
'attaches': [],
},
'notify': notify,
};
await _api.sendRequest(Opcode.msgSend, payload);
}
Future<Uint8List?> downloadPhoto(String baseUrl, String photoToken) async {
try {
final response = await _api.sendRequest(Opcode.fileDownload, {
'url': baseUrl,
'token': photoToken,
});
if (!response.isOk) return null;
final data = response.payload;
if (data is! Map) return null;
final content = data['content'];
if (content is String) {
return Uri.parse(content).host.isNotEmpty ? null : null;
}
return null;
} catch (e) {
return null;
}
}
Future<String?> getPhotoUrl(String baseUrl, String photoToken) async {
try {
final response = await _api.sendRequest(Opcode.fileDownload, {
'url': baseUrl,
'token': photoToken,
});
if (!response.isOk) return null;
final data = response.payload;
if (data is! Map) return null;
return data['content'] as String?;
} catch (e) {
return null;
}
}
Future<Uint8List?> downloadVideo(String baseUrl, String videoToken) async {
try {
final response = await _api.sendRequest(Opcode.fileDownload, {
'url': baseUrl,
'token': videoToken,
});
if (!response.isOk) return null;
final data = response.payload;
if (data is! Map) return null;
final content = data['content'];
if (content is String) {
return Uri.parse(content).host.isNotEmpty ? null : null;
}
return null;
} catch (e) {
return null;
}
}
Future<String?> getVideoUrl(String baseUrl, String videoToken) async {
try {
final response = await _api.sendRequest(Opcode.fileDownload, {
'url': baseUrl,
'token': videoToken,
});
if (!response.isOk) return null;
final data = response.payload;
if (data is! Map) return null;
return data['content'] as String?;
} catch (e) {
return null;
}
}
Future<Uint8List?> downloadFile(String baseUrl, String fileToken) async {
try {
final response = await _api.sendRequest(Opcode.fileDownload, {
'url': baseUrl,
'token': fileToken,
});
if (!response.isOk) return null;
final data = response.payload;
if (data is! Map) return null;
final content = data['content'];
if (content is String) {
return Uri.parse(content).host.isNotEmpty ? null : null;
}
return null;
} catch (e) {
return null;
}
}
Future<String?> getFileUrl(String baseUrl, String fileToken) async {
try {
final response = await _api.sendRequest(Opcode.fileDownload, {
'url': baseUrl,
'token': fileToken,
});
if (!response.isOk) return null;
final data = response.payload;
if (data is! Map) return null;
return data['content'] as String?;
} catch (e) {
return null;
}
}
Future<String?> searchContactById(int contactId) async {
final cached = ContactCache.get(contactId);
if (cached != null) return cached;
try {
final response = await _api.sendRequest(Opcode.contactInfo, {
'id': contactId,
});
if (!response.isOk) return null;
final data = response.payload;
if (data is! Map) return null;
final names = data['names'] as List?;
if (names != null && names.isNotEmpty) {
final name = names.first;
if (name is Map) {
final firstName = name['firstName'] as String? ?? '';
final lastName = name['lastName'] as String?;
final fullName = lastName != null
? '$firstName $lastName'
: firstName;
ContactCache.put(contactId, fullName);
return fullName;
}
}
} catch (e) {
debugPrint('searchContactById error: $e');
}
return null;
}
}