342 lines
9.4 KiB
Dart
342 lines
9.4 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
import '../../backend/api.dart';
|
|
import '../../models/bot_info.dart';
|
|
import '../../models/chat_info.dart';
|
|
import '../../models/contact_info.dart';
|
|
import '../protocol/opcode_map.dart';
|
|
|
|
Api? _api;
|
|
|
|
void attachInfoCacheApi(Api api) {
|
|
_api = api;
|
|
}
|
|
|
|
class _Entry<T> {
|
|
T? value;
|
|
DateTime? fetchedAt;
|
|
DateTime? failedAt;
|
|
Future<T?>? inFlight;
|
|
}
|
|
|
|
class InfoCache<T> {
|
|
final Duration ttl;
|
|
final Duration failureBackoff;
|
|
final Future<T?> Function(int id) fetcher;
|
|
final Map<int, _Entry<T>> _entries = {};
|
|
|
|
InfoCache({
|
|
required this.ttl,
|
|
required this.fetcher,
|
|
this.failureBackoff = const Duration(seconds: 10),
|
|
});
|
|
|
|
bool _isFresh(_Entry<T> e) {
|
|
if (e.fetchedAt == null) return false;
|
|
return DateTime.now().difference(e.fetchedAt!) < ttl;
|
|
}
|
|
|
|
bool _isInFailureBackoff(_Entry<T> e) {
|
|
if (e.failedAt == null) return false;
|
|
return DateTime.now().difference(e.failedAt!) < failureBackoff;
|
|
}
|
|
|
|
Future<T?> get(int id, {bool forceRefresh = false}) {
|
|
final entry = _entries.putIfAbsent(id, () => _Entry<T>());
|
|
|
|
if (!forceRefresh && _isFresh(entry)) {
|
|
return Future.value(entry.value);
|
|
}
|
|
if (!forceRefresh && _isInFailureBackoff(entry)) {
|
|
return Future.value(null);
|
|
}
|
|
if (entry.inFlight != null) return entry.inFlight!;
|
|
|
|
final future = _runFetch(entry, id);
|
|
entry.inFlight = future;
|
|
return future;
|
|
}
|
|
|
|
Future<T?> _runFetch(_Entry<T> entry, int id) async {
|
|
try {
|
|
final result = await fetcher(id);
|
|
entry.value = result;
|
|
entry.fetchedAt = DateTime.now();
|
|
entry.failedAt = null;
|
|
return result;
|
|
} catch (_) {
|
|
entry.failedAt = DateTime.now();
|
|
return null;
|
|
} finally {
|
|
entry.inFlight = null;
|
|
}
|
|
}
|
|
|
|
T? peek(int id) {
|
|
final entry = _entries[id];
|
|
if (entry == null || !_isFresh(entry)) return null;
|
|
return entry.value;
|
|
}
|
|
|
|
void invalidate(int id) => _entries.remove(id);
|
|
void clear() => _entries.clear();
|
|
|
|
void putValue(int id, T value, {DateTime? at}) {
|
|
final entry = _entries.putIfAbsent(id, () => _Entry<T>());
|
|
entry.value = value;
|
|
entry.fetchedAt = at ?? DateTime.now();
|
|
entry.failedAt = null;
|
|
}
|
|
|
|
void markFailed(int id, {DateTime? at}) {
|
|
final entry = _entries.putIfAbsent(id, () => _Entry<T>());
|
|
entry.failedAt = at ?? DateTime.now();
|
|
}
|
|
}
|
|
|
|
class ContactInfoFetch {
|
|
static final _cache = InfoCache<ContactInfo>(
|
|
ttl: const Duration(minutes: 5),
|
|
fetcher: _fetch,
|
|
);
|
|
|
|
static Future<ContactInfo?> get(int id, {bool forceRefresh = false}) =>
|
|
_cache.get(id, forceRefresh: forceRefresh);
|
|
|
|
static ContactInfo? peek(int id) => _cache.peek(id);
|
|
|
|
static void invalidate(int id) => _cache.invalidate(id);
|
|
static void clear() => _cache.clear();
|
|
|
|
static void putContact(int id, Map<dynamic, dynamic> contact) {
|
|
_cache.putValue(id, ContactInfo.fromMap(Map<String, dynamic>.from(contact)));
|
|
}
|
|
|
|
static Future<Map<int, ContactInfo>> getMany(
|
|
List<int> ids, {
|
|
bool forceRefresh = false,
|
|
}) async {
|
|
final result = <int, ContactInfo>{};
|
|
final missing = <int>[];
|
|
for (final id in ids) {
|
|
if (!forceRefresh) {
|
|
final cached = _cache.peek(id);
|
|
if (cached != null) {
|
|
result[id] = cached;
|
|
continue;
|
|
}
|
|
}
|
|
missing.add(id);
|
|
}
|
|
if (missing.isEmpty) return result;
|
|
|
|
final api = _api;
|
|
if (api == null || api.state != SessionState.online) return result;
|
|
try {
|
|
final resp = await api.sendRequest(Opcode.contactInfo, {
|
|
'contactIds': missing,
|
|
});
|
|
final data = resp.payload;
|
|
final contacts = data is Map ? data['contacts'] : null;
|
|
if (contacts is List) {
|
|
final now = DateTime.now();
|
|
for (final c in contacts.whereType<Map>()) {
|
|
final id = c['id'];
|
|
if (id is! int) continue;
|
|
final info = ContactInfo.fromMap(Map<String, dynamic>.from(c));
|
|
_cache.putValue(id, info, at: now);
|
|
result[id] = info;
|
|
}
|
|
}
|
|
} catch (_) {}
|
|
return result;
|
|
}
|
|
|
|
static Future<ContactInfo?> _fetch(int id) async {
|
|
final api = _api;
|
|
if (api == null || api.state != SessionState.online) return null;
|
|
final resp = await api.sendRequest(Opcode.contactInfo, {
|
|
'contactIds': [id],
|
|
});
|
|
final data = resp.payload;
|
|
if (data is! Map) return null;
|
|
final contacts = data['contacts'];
|
|
if (contacts is! List || contacts.isEmpty) return null;
|
|
final first = contacts.first;
|
|
if (first is! Map) return null;
|
|
return ContactInfo.fromMap(Map<String, dynamic>.from(first));
|
|
}
|
|
}
|
|
|
|
class PresenceFetch {
|
|
static final _cache = InfoCache<Map<String, dynamic>>(
|
|
ttl: const Duration(seconds: 60),
|
|
fetcher: _fetch,
|
|
);
|
|
|
|
static Future<Map<String, dynamic>?> get(
|
|
int id, {
|
|
bool forceRefresh = false,
|
|
}) => _cache.get(id, forceRefresh: forceRefresh);
|
|
|
|
static Map<String, dynamic>? peek(int id) => _cache.peek(id);
|
|
|
|
static final Map<int, Map<String, dynamic>> _live = {};
|
|
static final ValueNotifier<int> revision = ValueNotifier<int>(0);
|
|
|
|
static Map<String, dynamic>? live(int id) => _live[id] ?? _cache.peek(id);
|
|
|
|
static bool isOnline(int id) => (live(id)?['status'] as int?) == 1;
|
|
|
|
static void apply(int id, Map<String, dynamic> presence) {
|
|
if (id <= 0) return;
|
|
_live[id] = presence;
|
|
_cache.putValue(id, presence);
|
|
revision.value++;
|
|
}
|
|
|
|
static void invalidate(int id) => _cache.invalidate(id);
|
|
|
|
static void clear() {
|
|
_cache.clear();
|
|
_live.clear();
|
|
revision.value++;
|
|
}
|
|
|
|
static void primeAll(Map<dynamic, dynamic> presence) {
|
|
final now = DateTime.now();
|
|
presence.forEach((key, value) {
|
|
if (value is! Map) return;
|
|
final id = key is int ? key : int.tryParse(key.toString());
|
|
if (id == null) return;
|
|
final map = Map<String, dynamic>.from(value);
|
|
_cache.putValue(id, map, at: now);
|
|
_live[id] = map;
|
|
});
|
|
revision.value++;
|
|
}
|
|
|
|
static Future<Map<String, dynamic>?> _fetch(int id) async {
|
|
final results = await _fetchBatch([id]);
|
|
return results[id];
|
|
}
|
|
|
|
static Future<Map<int, Map<String, dynamic>>> getMany(
|
|
List<int> ids, {
|
|
bool forceRefresh = false,
|
|
}) async {
|
|
final result = <int, Map<String, dynamic>>{};
|
|
final missing = <int>[];
|
|
for (final id in ids) {
|
|
if (!forceRefresh) {
|
|
final cached = _cache.peek(id);
|
|
if (cached != null) {
|
|
result[id] = cached;
|
|
continue;
|
|
}
|
|
}
|
|
missing.add(id);
|
|
}
|
|
if (missing.isNotEmpty) {
|
|
final fetched = await _fetchBatch(missing);
|
|
final now = DateTime.now();
|
|
for (final id in missing) {
|
|
final value = fetched[id];
|
|
if (value != null) {
|
|
_cache.putValue(id, value, at: now);
|
|
result[id] = value;
|
|
} else {
|
|
_cache.markFailed(id, at: now);
|
|
}
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static Future<Map<int, Map<String, dynamic>>> _fetchBatch(
|
|
List<int> ids,
|
|
) async {
|
|
final api = _api;
|
|
if (api == null || api.state != SessionState.online || ids.isEmpty) {
|
|
return const {};
|
|
}
|
|
final resp = await api.sendRequest(Opcode.contactPresence, {
|
|
'contactIds': ids,
|
|
});
|
|
final data = resp.payload;
|
|
if (data is! Map) return const {};
|
|
final presence = data['presence'];
|
|
if (presence is! Map) return const {};
|
|
final out = <int, Map<String, dynamic>>{};
|
|
for (final id in ids) {
|
|
final entry = presence[id.toString()] ?? presence[id];
|
|
if (entry is Map) {
|
|
out[id] = Map<String, dynamic>.from(entry);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
}
|
|
|
|
class BotInfoFetch {
|
|
static final _cache = InfoCache<BotInfo>(
|
|
ttl: const Duration(minutes: 30),
|
|
fetcher: _fetch,
|
|
);
|
|
|
|
static Future<BotInfo?> get(int botId, {bool forceRefresh = false}) =>
|
|
_cache.get(botId, forceRefresh: forceRefresh);
|
|
|
|
static BotInfo? peek(int botId) => _cache.peek(botId);
|
|
|
|
static List<BotCommand> commandsOf(int botId) =>
|
|
_cache.peek(botId)?.commands ?? const [];
|
|
|
|
static void invalidate(int botId) => _cache.invalidate(botId);
|
|
static void clear() => _cache.clear();
|
|
|
|
static Future<BotInfo?> _fetch(int botId) async {
|
|
final api = _api;
|
|
if (api == null || api.state != SessionState.online) return null;
|
|
final resp = await api.sendRequest(Opcode.botInfo, {'botId': botId});
|
|
final data = resp.payload;
|
|
if (data is! Map) return null;
|
|
final info = BotInfo.fromPayload(botId, Map<String, dynamic>.from(data));
|
|
final contact = info.contact;
|
|
if (contact != null) ContactInfoFetch.putContact(botId, contact.raw);
|
|
return info;
|
|
}
|
|
}
|
|
|
|
class ChatInfoFetch {
|
|
static final _cache = InfoCache<ChatInfo>(
|
|
ttl: const Duration(minutes: 5),
|
|
fetcher: _fetch,
|
|
);
|
|
|
|
static Future<ChatInfo?> get(int id, {bool forceRefresh = false}) =>
|
|
_cache.get(id, forceRefresh: forceRefresh);
|
|
|
|
static ChatInfo? peek(int id) => _cache.peek(id);
|
|
|
|
static void invalidate(int id) => _cache.invalidate(id);
|
|
static void clear() => _cache.clear();
|
|
|
|
static Future<ChatInfo?> _fetch(int id) async {
|
|
final api = _api;
|
|
if (api == null || api.state != SessionState.online) return null;
|
|
final resp = await api.sendRequest(Opcode.chatInfo, {
|
|
'chatIds': [id],
|
|
});
|
|
final data = resp.payload;
|
|
if (data is! Map) return null;
|
|
final chats = data['chats'];
|
|
if (chats is! List || chats.isEmpty) return null;
|
|
final first = chats.first;
|
|
if (first is! Map) return null;
|
|
return ChatInfo.fromMap(Map<String, dynamic>.from(first));
|
|
}
|
|
}
|