464 lines
14 KiB
Dart
464 lines
14 KiB
Dart
import 'dart:async';
|
|
import 'dart:collection';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:get_it/get_it.dart';
|
|
import 'package:logging/logging.dart';
|
|
import 'package:moxlib/moxlib.dart';
|
|
import 'package:moxplatform/moxplatform.dart';
|
|
import 'package:moxxmpp/moxxmpp.dart';
|
|
import 'package:moxxyv2/service/contacts.dart';
|
|
import 'package:moxxyv2/service/conversation.dart';
|
|
import 'package:moxxyv2/service/database/database.dart';
|
|
import 'package:moxxyv2/service/not_specified.dart';
|
|
import 'package:moxxyv2/service/service.dart';
|
|
import 'package:moxxyv2/shared/events.dart';
|
|
import 'package:moxxyv2/shared/models/conversation.dart';
|
|
import 'package:moxxyv2/shared/models/roster.dart';
|
|
|
|
/// Closure which returns true if the jid of a [RosterItem] is equal to [jid].
|
|
bool Function(RosterItem) _jidEqualsWrapper(String jid) {
|
|
return (i) => i.jid == jid;
|
|
}
|
|
|
|
typedef AddRosterItemFunction = Future<RosterItem> Function(
|
|
String avatarUrl,
|
|
String avatarHash,
|
|
String jid,
|
|
String title,
|
|
String subscription,
|
|
String ask,
|
|
bool pseudoRosterItem,
|
|
String? contactId,
|
|
String? contactAvatarPath,
|
|
String? contactDisplayName,
|
|
{
|
|
List<String> groups,
|
|
}
|
|
);
|
|
typedef UpdateRosterItemFunction = Future<RosterItem> Function(
|
|
int id, {
|
|
String? avatarUrl,
|
|
String? avatarHash,
|
|
String? title,
|
|
String? subscription,
|
|
String? ask,
|
|
Object pseudoRosterItem,
|
|
List<String>? groups,
|
|
}
|
|
);
|
|
typedef RemoveRosterItemFunction = Future<void> Function(String jid);
|
|
typedef GetConversationFunction = Future<Conversation?> Function(String jid);
|
|
typedef SendEventFunction = void Function(BackgroundEvent event, { String? id });
|
|
|
|
/// Compare the local roster with the roster we received either by request or by push.
|
|
/// Returns a diff between the roster before and after the request or the push.
|
|
/// NOTE: This abuses the [RosterDiffEvent] type a bit.
|
|
Future<RosterDiffEvent> processRosterDiff(
|
|
List<RosterItem> currentRoster,
|
|
List<XmppRosterItem> remoteRoster,
|
|
bool isRosterPush,
|
|
AddRosterItemFunction addRosterItemFromData,
|
|
UpdateRosterItemFunction updateRosterItem,
|
|
RemoveRosterItemFunction removeRosterItemByJid,
|
|
GetConversationFunction getConversationByJid,
|
|
SendEventFunction _sendEvent,
|
|
) async {
|
|
final css = GetIt.I.get<ContactsService>();
|
|
final removed = List<String>.empty(growable: true);
|
|
final modified = List<RosterItem>.empty(growable: true);
|
|
final added = List<RosterItem>.empty(growable: true);
|
|
|
|
for (final item in remoteRoster) {
|
|
if (isRosterPush) {
|
|
final litem = firstWhereOrNull(currentRoster, _jidEqualsWrapper(item.jid));
|
|
if (litem != null) {
|
|
if (item.subscription == 'remove') {
|
|
// We have the item locally but it has been removed
|
|
|
|
if (litem.contactId != null) {
|
|
// We have the contact associated with a contact
|
|
final newItem = await updateRosterItem(
|
|
litem.id,
|
|
ask: 'none',
|
|
subscription: 'none',
|
|
pseudoRosterItem: true,
|
|
);
|
|
modified.add(newItem);
|
|
} else {
|
|
await removeRosterItemByJid(item.jid);
|
|
removed.add(item.jid);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// Item has been modified
|
|
final newItem = await updateRosterItem(
|
|
litem.id,
|
|
subscription: item.subscription,
|
|
title: item.name,
|
|
ask: item.ask,
|
|
pseudoRosterItem: false,
|
|
groups: item.groups,
|
|
);
|
|
|
|
modified.add(newItem);
|
|
|
|
// Check if we have a conversation that we need to modify
|
|
final conv = await getConversationByJid(item.jid);
|
|
if (conv != null) {
|
|
_sendEvent(
|
|
ConversationUpdatedEvent(
|
|
conversation: conv.copyWith(subscription: item.subscription),
|
|
),
|
|
);
|
|
}
|
|
} else {
|
|
// Item does not exist locally
|
|
if (item.subscription == 'remove') {
|
|
// Item has been removed but we don't have it locally
|
|
removed.add(item.jid);
|
|
} else {
|
|
// Item has been added and we don't have it locally
|
|
final contactId = await css.getContactIdForJid(item.jid);
|
|
final newItem = await addRosterItemFromData(
|
|
'',
|
|
'',
|
|
item.jid,
|
|
item.name ?? item.jid.split('@')[0],
|
|
item.subscription,
|
|
item.ask ?? '',
|
|
false,
|
|
contactId,
|
|
await css.getProfilePicturePathForJid(item.jid),
|
|
await css.getContactDisplayName(contactId),
|
|
groups: item.groups,
|
|
);
|
|
|
|
added.add(newItem);
|
|
}
|
|
}
|
|
} else {
|
|
final litem = firstWhereOrNull(currentRoster, _jidEqualsWrapper(item.jid));
|
|
if (litem != null) {
|
|
// Item is modified
|
|
if (litem.title != item.name || litem.subscription != item.subscription || !listEquals(litem.groups, item.groups)) {
|
|
final modifiedItem = await updateRosterItem(
|
|
litem.id,
|
|
title: item.name,
|
|
subscription: item.subscription,
|
|
pseudoRosterItem: false,
|
|
groups: item.groups,
|
|
);
|
|
modified.add(modifiedItem);
|
|
|
|
// Check if we have a conversation that we need to modify
|
|
final conv = await getConversationByJid(litem.jid);
|
|
if (conv != null) {
|
|
_sendEvent(
|
|
ConversationUpdatedEvent(
|
|
conversation: conv.copyWith(subscription: item.subscription),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
// Item is new
|
|
final contactId = await css.getContactIdForJid(item.jid);
|
|
added.add(await addRosterItemFromData(
|
|
'',
|
|
'',
|
|
item.jid,
|
|
item.jid.split('@')[0],
|
|
item.subscription,
|
|
item.ask ?? '',
|
|
false,
|
|
contactId,
|
|
await css.getProfilePicturePathForJid(item.jid),
|
|
await css.getContactDisplayName(contactId),
|
|
groups: item.groups,
|
|
),);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!isRosterPush) {
|
|
for (final item in currentRoster) {
|
|
final ritem = firstWhereOrNull(remoteRoster, (XmppRosterItem i) => i.jid == item.jid);
|
|
if (ritem == null) {
|
|
await removeRosterItemByJid(item.jid);
|
|
removed.add(item.jid);
|
|
}
|
|
// We don't handle the modification case here as that is covered by the huge
|
|
// loop above
|
|
}
|
|
}
|
|
|
|
return RosterDiffEvent(
|
|
added: added,
|
|
modified: modified,
|
|
removed: removed,
|
|
);
|
|
}
|
|
|
|
class RosterService {
|
|
|
|
RosterService()
|
|
: _rosterCache = HashMap(),
|
|
_rosterLoaded = false,
|
|
_log = Logger('RosterService');
|
|
final HashMap<String, RosterItem> _rosterCache;
|
|
bool _rosterLoaded;
|
|
final Logger _log;
|
|
|
|
Future<bool> isInRoster(String jid) async {
|
|
if (!_rosterLoaded) {
|
|
await loadRosterFromDatabase();
|
|
}
|
|
|
|
return _rosterCache.containsKey(jid);
|
|
}
|
|
|
|
/// Wrapper around [DatabaseService]'s addRosterItemFromData that updates the cache.
|
|
Future<RosterItem> addRosterItemFromData(
|
|
String avatarUrl,
|
|
String avatarHash,
|
|
String jid,
|
|
String title,
|
|
String subscription,
|
|
String ask,
|
|
bool pseudoRosterItem,
|
|
String? contactId,
|
|
String? contactAvatarPath,
|
|
String? contactDisplayName,
|
|
{
|
|
List<String> groups = const [],
|
|
}
|
|
) async {
|
|
final item = await GetIt.I.get<DatabaseService>().addRosterItemFromData(
|
|
avatarUrl,
|
|
avatarHash,
|
|
jid,
|
|
title,
|
|
subscription,
|
|
ask,
|
|
pseudoRosterItem,
|
|
contactId,
|
|
contactAvatarPath,
|
|
contactDisplayName,
|
|
groups: groups,
|
|
);
|
|
|
|
// Update the cache
|
|
_rosterCache[item.jid] = item;
|
|
|
|
return item;
|
|
}
|
|
|
|
/// Wrapper around [DatabaseService]'s updateRosterItem that updates the cache.
|
|
Future<RosterItem> updateRosterItem(
|
|
int id, {
|
|
String? avatarUrl,
|
|
String? avatarHash,
|
|
String? title,
|
|
String? subscription,
|
|
String? ask,
|
|
Object pseudoRosterItem = notSpecified,
|
|
List<String>? groups,
|
|
Object? contactId = notSpecified,
|
|
Object? contactAvatarPath = notSpecified,
|
|
Object? contactDisplayName = notSpecified,
|
|
}
|
|
) async {
|
|
final newItem = await GetIt.I.get<DatabaseService>().updateRosterItem(
|
|
id,
|
|
avatarUrl: avatarUrl,
|
|
avatarHash: avatarHash,
|
|
title: title,
|
|
subscription: subscription,
|
|
ask: ask,
|
|
pseudoRosterItem: pseudoRosterItem,
|
|
groups: groups,
|
|
contactId: contactId,
|
|
contactAvatarPath: contactAvatarPath,
|
|
contactDisplayName: contactDisplayName,
|
|
);
|
|
|
|
// Update cache
|
|
_rosterCache[newItem.jid] = newItem;
|
|
|
|
return newItem;
|
|
}
|
|
|
|
/// Wrapper around [DatabaseService]'s removeRosterItem.
|
|
Future<void> removeRosterItem(int id) async {
|
|
await GetIt.I.get<DatabaseService>().removeRosterItem(id);
|
|
|
|
/// Update cache
|
|
_rosterCache.removeWhere((_, value) => value.id == id);
|
|
}
|
|
|
|
/// Removes a roster item from the database based on its JID.
|
|
Future<void> removeRosterItemByJid(String jid) async {
|
|
if (!_rosterLoaded) {
|
|
await loadRosterFromDatabase();
|
|
}
|
|
|
|
for (final item in _rosterCache.values) {
|
|
if (item.jid == jid) {
|
|
await removeRosterItem(item.id);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns the entire roster
|
|
Future<List<RosterItem>> getRoster() async {
|
|
if (!_rosterLoaded) {
|
|
await loadRosterFromDatabase();
|
|
}
|
|
|
|
return _rosterCache.values.toList();
|
|
}
|
|
|
|
/// Returns the roster item with jid [jid] if it exists. Null otherwise.
|
|
Future<RosterItem?> getRosterItemByJid(String jid) async {
|
|
if (await isInRoster(jid)) {
|
|
return _rosterCache[jid];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// Load the roster from the database. This function is guarded against loading the
|
|
/// roster multiple times and thus creating too many "RosterDiff" actions.
|
|
Future<List<RosterItem>> loadRosterFromDatabase() async {
|
|
final items = await GetIt.I.get<DatabaseService>().loadRosterItems();
|
|
|
|
_rosterLoaded = true;
|
|
for (final item in items) {
|
|
_rosterCache[item.jid] = item;
|
|
}
|
|
|
|
return items;
|
|
}
|
|
|
|
/// Attempts to add an item to the roster by first performing the roster set
|
|
/// and, if it was successful, create the database entry. Returns the
|
|
/// [RosterItem] model object.
|
|
Future<RosterItem> addToRosterWrapper(String avatarUrl, String avatarHash, String jid, String title) async {
|
|
final css = GetIt.I.get<ContactsService>();
|
|
final contactId = await css.getContactIdForJid(jid);
|
|
final item = await addRosterItemFromData(
|
|
avatarUrl,
|
|
avatarHash,
|
|
jid,
|
|
title,
|
|
'none',
|
|
'',
|
|
false,
|
|
contactId,
|
|
await css.getProfilePicturePathForJid(jid),
|
|
await css.getContactDisplayName(contactId),
|
|
);
|
|
final result = await GetIt.I.get<XmppConnection>().getRosterManager().addToRoster(jid, title);
|
|
if (!result) {
|
|
// TODO(Unknown): Signal error?
|
|
}
|
|
|
|
GetIt.I.get<XmppConnection>().getPresenceManager().sendSubscriptionRequest(jid);
|
|
|
|
sendEvent(RosterDiffEvent(added: [ item ]));
|
|
return item;
|
|
}
|
|
|
|
/// Removes the [RosterItem] with jid [jid] from the server-side roster and, if
|
|
/// successful, from the database. If [unsubscribe] is true, then [jid] won't receive
|
|
/// our presence anymore.
|
|
Future<bool> removeFromRosterWrapper(String jid, { bool unsubscribe = true }) async {
|
|
final roster = GetIt.I.get<XmppConnection>().getRosterManager();
|
|
final presence = GetIt.I.get<XmppConnection>().getPresenceManager();
|
|
final result = await roster.removeFromRoster(jid);
|
|
if (result == RosterRemovalResult.okay || result == RosterRemovalResult.itemNotFound) {
|
|
if (unsubscribe) {
|
|
presence.sendUnsubscriptionRequest(jid);
|
|
}
|
|
|
|
_log.finest('Removing from roster maybe worked. Removing from database');
|
|
await removeRosterItemByJid(jid);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
Future<void> requestRoster() async {
|
|
final roster = GetIt.I.get<XmppConnection>().getManagerById<RosterManager>(rosterManager)!;
|
|
Result<RosterRequestResult?, RosterError> result;
|
|
if (roster.rosterVersioningAvailable()) {
|
|
_log.fine('Stream supports roster versioning');
|
|
result = await roster.requestRosterPushes();
|
|
_log.fine('Requesting roster pushes done');
|
|
} else {
|
|
_log.fine('Stream does not support roster versioning');
|
|
result = await roster.requestRoster();
|
|
}
|
|
|
|
if (result.isType<RosterError>()) {
|
|
_log.warning('Failed to request roster');
|
|
return;
|
|
}
|
|
|
|
final value = result.get<RosterRequestResult?>();
|
|
if (value != null) {
|
|
final currentRoster = await getRoster();
|
|
sendEvent(
|
|
await processRosterDiff(
|
|
currentRoster,
|
|
value.items,
|
|
false,
|
|
addRosterItemFromData,
|
|
updateRosterItem,
|
|
removeRosterItemByJid,
|
|
GetIt.I.get<ConversationService>().getConversationByJid,
|
|
sendEvent,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Handles a roster push.
|
|
Future<void> handleRosterPushEvent(RosterPushEvent event) async {
|
|
final item = event.item;
|
|
final currentRoster = await getRoster();
|
|
sendEvent(
|
|
await processRosterDiff(
|
|
currentRoster,
|
|
[ item ],
|
|
true,
|
|
addRosterItemFromData,
|
|
updateRosterItem,
|
|
removeRosterItemByJid,
|
|
GetIt.I.get<ConversationService>().getConversationByJid,
|
|
sendEvent,
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> acceptSubscriptionRequest(String jid) async {
|
|
GetIt.I.get<XmppConnection>().getPresenceManager().sendSubscriptionRequestApproval(jid);
|
|
}
|
|
|
|
Future<void> rejectSubscriptionRequest(String jid) async {
|
|
GetIt.I.get<XmppConnection>().getPresenceManager().sendSubscriptionRequestRejection(jid);
|
|
}
|
|
|
|
void sendSubscriptionRequest(String jid) {
|
|
GetIt.I.get<XmppConnection>().getPresenceManager().sendSubscriptionRequest(jid);
|
|
}
|
|
|
|
void sendUnsubscriptionRequest(String jid) {
|
|
GetIt.I.get<XmppConnection>().getPresenceManager().sendUnsubscriptionRequest(jid);
|
|
}
|
|
}
|