From ddbb00ee56ca8ddc5db3ed9b6f4003bb6020ed6b Mon Sep 17 00:00:00 2001 From: fiaxh Date: Fri, 22 May 2026 17:09:04 +0200 Subject: Add support for MUC affiliations versioning (XEP-0463), add affiliation storage --- libdino/meson.build | 1 + libdino/src/application.vala | 2 +- libdino/src/service/contact_model.vala | 2 +- libdino/src/service/database.vala | 34 ++++++++++++- libdino/src/service/muc_affiliations.vala | 82 +++++++++++++++++++++++++++++++ libdino/src/service/muc_manager.vala | 68 +++++++++++++++++++------ 6 files changed, 171 insertions(+), 18 deletions(-) create mode 100644 libdino/src/service/muc_affiliations.vala (limited to 'libdino') diff --git a/libdino/meson.build b/libdino/meson.build index 5b537251..73b1c512 100644 --- a/libdino/meson.build +++ b/libdino/meson.build @@ -63,6 +63,7 @@ sources = files( 'src/service/message_processor.vala', 'src/service/message_storage.vala', 'src/service/module_manager.vala', + 'src/service/muc_affiliations.vala', 'src/service/muc_manager.vala', 'src/service/notification_events.vala', 'src/service/occupant_id_store.vala', diff --git a/libdino/src/application.vala b/libdino/src/application.vala index ef2d883e..19650495 100644 --- a/libdino/src/application.vala +++ b/libdino/src/application.vala @@ -42,7 +42,7 @@ public interface Application : GLib.Application { Calls.start(stream_interactor, db); ConversationManager.start(stream_interactor, db); OccupantIdStore.start(stream_interactor, db); - MucManager.start(stream_interactor); + MucManager.start(stream_interactor, db); AvatarManager.start(stream_interactor, db); RosterManager.start(stream_interactor, db); FileManager.start(stream_interactor, db); diff --git a/libdino/src/service/contact_model.vala b/libdino/src/service/contact_model.vala index 312df4f7..33f6126b 100644 --- a/libdino/src/service/contact_model.vala +++ b/libdino/src/service/contact_model.vala @@ -27,7 +27,7 @@ namespace Dino { stream_interactor.get_module(MucManager.IDENTITY).room_info_updated.connect((account, jid) => { check_update_models(account, jid, Conversation.Type.GROUPCHAT); }); - stream_interactor.get_module(MucManager.IDENTITY).private_room_occupant_updated.connect((account, room, occupant) => { + stream_interactor.get_module(MucManager.IDENTITY).private_room_occupants_updated.connect((account, room) => { check_update_models(account, room, Conversation.Type.GROUPCHAT); }); stream_interactor.get_module(MucManager.IDENTITY).subject_set.connect((account, jid, subject) => { diff --git a/libdino/src/service/database.vala b/libdino/src/service/database.vala index 54f5ab42..7a5b940a 100644 --- a/libdino/src/service/database.vala +++ b/libdino/src/service/database.vala @@ -7,7 +7,7 @@ using Dino.Entities; namespace Dino { public class Database : Qlite.Database { - private const int VERSION = 31; + private const int VERSION = 32; public class AccountTable : Table { public Column id = new Column.Integer("id") { primary_key = true, auto_increment = true }; @@ -385,6 +385,32 @@ public class Database : Qlite.Database { } } + public class MucAffiliationTable : Table { + public Column account_id = new Column.Integer("account_id"); + public Column muc_jid_id = new Column.Integer("muc_jid_id"); + public Column real_jid_id = new Column.Integer("real_jid_id"); + public Column affiliation = new Column.Integer("affiliation"); + + internal MucAffiliationTable(Database db) { + base(db, "muc_affiliation"); + init({account_id, muc_jid_id, real_jid_id, affiliation}); + index("muc_affiliation_idx", {muc_jid_id}); + unique({account_id, muc_jid_id, real_jid_id}, "REPLACE"); + } + } + + public class MucAffiliationVersionTable : Table { + public Column account_id = new Column.Integer("account_id"); + public Column muc_jid_id = new Column.Integer("muc_jid_id"); + public Column version = new Column.Text("version"); + + internal MucAffiliationVersionTable(Database db) { + base(db, "muc_affiliation_version"); + init({account_id, muc_jid_id, version}); + unique({account_id, muc_jid_id}, "REPLACE"); + } + } + public class ReactionTable : Table { public Column id = new Column.Integer("id") { primary_key = true, auto_increment = true }; public Column account_id = new Column.Integer("account_id") { not_null = true }; @@ -473,6 +499,8 @@ public class Database : Qlite.Database { public RosterTable roster { get; private set; } public MamCatchupTable mam_catchup { get; private set; } public ReactionTable reaction { get; private set; } + public MucAffiliationTable muc_affiliation { get; private set; } + public MucAffiliationVersionTable muc_affiliation_version { get; private set; } public SettingsTable settings { get; private set; } public AccountSettingsTable account_settings { get; private set; } public ConversationSettingsTable conversation_settings { get; private set; } @@ -507,10 +535,12 @@ public class Database : Qlite.Database { roster = new RosterTable(this); mam_catchup = new MamCatchupTable(this); reaction = new ReactionTable(this); + muc_affiliation = new MucAffiliationTable(this); + muc_affiliation_version = new MucAffiliationVersionTable(this); settings = new SettingsTable(this); account_settings = new AccountSettingsTable(this); conversation_settings = new ConversationSettingsTable(this); - init({ account, jid, entity, content_item, message, message_occupant_id, body_meta, message_correction, reply, real_jid, occupantid, file_transfer, file_hashes, file_thumbnails, sfs_sources, call, call_counterpart, conversation, avatar, entity_identity, entity_feature, roster, mam_catchup, reaction, settings, account_settings, conversation_settings }); + init({ account, jid, entity, content_item, message, message_occupant_id, body_meta, message_correction, reply, real_jid, occupantid, file_transfer, file_hashes, file_thumbnails, sfs_sources, call, call_counterpart, conversation, avatar, entity_identity, entity_feature, roster, mam_catchup, reaction, muc_affiliation, muc_affiliation_version, settings, account_settings, conversation_settings }); try { exec("PRAGMA journal_mode = WAL"); diff --git a/libdino/src/service/muc_affiliations.vala b/libdino/src/service/muc_affiliations.vala new file mode 100644 index 00000000..41107a93 --- /dev/null +++ b/libdino/src/service/muc_affiliations.vala @@ -0,0 +1,82 @@ +using Gee; + +using Dino.Entities; +using Xmpp; +using Xmpp.Xep; + +namespace Dino { + public class MucAffiliations { + + private Database db; + + public MucAffiliations(Database db) { + this.db = db; + } + + public string? get_mav_version(Account account, Jid muc_jid) { + Qlite.RowOption version_rowopt = db.muc_affiliation_version.select() + .with(db.muc_affiliation_version.account_id, "=", account.id) + .with(db.muc_affiliation_version.muc_jid_id, "=", db.get_jid_id(muc_jid)) + .single() + .row(); + + return version_rowopt.is_present() ? version_rowopt.inner[db.muc_affiliation_version.version] : null; + } + + public void clear_affiliations(Account account, Jid muc_jid) { + db.muc_affiliation.delete() + .with(db.muc_affiliation.account_id, "=", account.id) + .with(db.muc_affiliation.muc_jid_id, "=", db.get_jid_id(muc_jid)) + .perform(); + } + + public void clear_mav_version(Account account, Jid muc_jid) { + db.muc_affiliation_version.delete() + .with(db.muc_affiliation_version.account_id, "=", account.id) + .with(db.muc_affiliation_version.muc_jid_id, "=", db.get_jid_id(muc_jid)) + .perform(); + } + + public void update_affiliations(Account account, Jid muc_jid, HashMap affiliations, string? mav_since, string? mav_until) { + // Update version (if MUC affiliation versioning is used) + if (mav_until != null && mav_since == null) { + clear_affiliations(account, muc_jid); + } else if (mav_until != null) { + string? previous_version = get_mav_version(account, muc_jid); + if (mav_since != previous_version) { + warning("Got an unexpected affiliations version 'since'. Expected %s; got %s", previous_version, mav_since); + clear_affiliations(account, muc_jid); + clear_mav_version(account, muc_jid); + } + } + + if (mav_until != null) { + db.muc_affiliation_version.upsert() + .value(db.muc_affiliation_version.account_id, account.id, true) + .value(db.muc_affiliation_version.muc_jid_id, db.get_jid_id(muc_jid), true) + .value(db.muc_affiliation_version.version, mav_until) + .perform(); + } + + // Update affiliations + foreach (Jid real_jid in affiliations.keys) { + Muc.Affiliation affiliation = affiliations[real_jid]; + + if (affiliation == Muc.Affiliation.NONE) { + db.muc_affiliation.delete() + .with(db.muc_affiliation.account_id, "=", account.id) + .with(db.muc_affiliation.muc_jid_id, "=", db.get_jid_id(muc_jid)) + .with(db.muc_affiliation.real_jid_id, "=", db.get_jid_id(real_jid)) + .perform(); + } else { + db.muc_affiliation.insert() + .value(db.muc_affiliation.account_id, account.id) + .value(db.muc_affiliation.muc_jid_id, db.get_jid_id(muc_jid)) + .value(db.muc_affiliation.real_jid_id, db.get_jid_id(real_jid)) + .value(db.muc_affiliation.affiliation, (int)affiliation) + .perform(); + } + } + } + } +} \ No newline at end of file diff --git a/libdino/src/service/muc_manager.vala b/libdino/src/service/muc_manager.vala index 29c6c5ca..b47a52bf 100644 --- a/libdino/src/service/muc_manager.vala +++ b/libdino/src/service/muc_manager.vala @@ -12,7 +12,7 @@ public class MucManager : StreamInteractionModule, Object { public signal void left(Account account, Jid jid); public signal void subject_set(Account account, Jid jid, string? subject); public signal void room_info_updated(Account account, Jid muc_jid); - public signal void private_room_occupant_updated(Account account, Jid room, Jid occupant); + public signal void private_room_occupants_updated(Account account, Jid room); public signal void invite_received(Account account, Jid room_jid, Jid from_jid, string? password, string? reason); public signal void voice_request_received(Account account, Jid room_jid, Jid from_jid, string nick); public signal void received_occupant_role(Account account, Jid jid, Xep.Muc.Role? role); @@ -21,6 +21,9 @@ public class MucManager : StreamInteractionModule, Object { public signal void conference_removed(Account account, Jid jid); private StreamInteractor stream_interactor; + private Database db; + private MucAffiliations muc_affiliations; + private HashMap> mucs_joined = new HashMap>(Account.hash_func, Account.equals_func); private HashMap> mucs_joining = new HashMap>(Account.hash_func, Account.equals_func); private HashMap> mucs_sync_cancellables = new HashMap>(Account.hash_func, Account.equals_func); @@ -31,13 +34,16 @@ public class MucManager : StreamInteractionModule, Object { public HashMap default_muc_server = new HashMap(Account.hash_func, Account.equals_func); private HashMap> own_occupant_ids = new HashMap>(Account.hash_func, Account.equals_func); - public static void start(StreamInteractor stream_interactor) { - MucManager m = new MucManager(stream_interactor); + public static void start(StreamInteractor stream_interactor, Database db) { + MucManager m = new MucManager(stream_interactor, db); stream_interactor.add_module(m); } - private MucManager(StreamInteractor stream_interactor) { + private MucManager(StreamInteractor stream_interactor, Database db) { this.stream_interactor = stream_interactor; + this.db = db; + this.muc_affiliations = new MucAffiliations(db); + this.received_message_listener = new ReceivedMessageListener(stream_interactor); stream_interactor.account_added.connect(on_account_added); stream_interactor.stream_negotiated.connect(on_stream_negotiated); @@ -86,7 +92,9 @@ public class MucManager : StreamInteractionModule, Object { } mucs_joining[account].add(jid); - Muc.JoinResult? res = yield stream.get_module(Xep.Muc.Module.IDENTITY).enter(stream, jid.bare_jid, nick_, password, history_since, receive_history, null); + string? mav_version = muc_affiliations.get_mav_version(account, jid); + + Muc.JoinResult? res = yield stream.get_module(Xep.Muc.Module.IDENTITY).enter(stream, jid.bare_jid, nick_, password, history_since, receive_history, null, mav_version); mucs_joining[account].remove(jid); @@ -100,6 +108,14 @@ public class MucManager : StreamInteractionModule, Object { joined_conversation.nickname = nick; stream_interactor.get_module(ConversationManager.IDENTITY).start_conversation(joined_conversation); + bool can_do_affiliation_versioning = yield entity_info.has_feature(account, jid, Xmpp.Xep.MucAffiliationsVersioning.NS_URI); + if (!can_do_affiliation_versioning) { + stream.get_module(Xep.Muc.Module.IDENTITY).fetch_affiliations.begin(stream, jid.bare_jid, () => { + muc_affiliations.clear_affiliations(account, jid); + persist_muc_affiliations(account, jid.bare_jid); + }); + } + if (can_do_mam) { var history_sync = stream_interactor.get_module(MessageProcessor.IDENTITY).history_sync; if (conversation == null) { @@ -371,11 +387,16 @@ public class MucManager : StreamInteractionModule, Object { } public Gee.List? get_offline_members(Jid jid, Account account) { - Xep.Muc.Flag? flag = get_muc_flag(account); - if (flag != null) { - return flag.get_offline_members(jid); + Gee.List ret = new ArrayList(Jid.equals_func); + + var qry = db.muc_affiliation.select() + .with(db.muc_affiliation.account_id, "=", account.id) + .with(db.muc_affiliation.muc_jid_id, "=", db.get_jid_id(jid)); + + foreach (Qlite.Row affiliation_row in qry) { + ret.add(db.get_jid_by_id(affiliation_row[db.muc_affiliation.real_jid_id])); } - return null; + return ret; } public Gee.List? get_other_offline_members(Jid jid, Account account) { @@ -444,14 +465,15 @@ public class MucManager : StreamInteractionModule, Object { stream_interactor.module_manager.get_module(account, Xep.Muc.Module.IDENTITY).received_occupant_role.connect( (stream, from_jid, role) => { received_occupant_role(account, from_jid, role); }); + stream_interactor.module_manager.get_module(account, Xep.Muc.Module.IDENTITY).received_affiliations.connect( (stream, muc_jid, affiliations, mav_since, mav_until) => { + muc_affiliations.update_affiliations(account, muc_jid, affiliations, mav_since, mav_until); + if (is_private_room(account, muc_jid)) { + private_room_occupants_updated(account, muc_jid); + } + }); stream_interactor.module_manager.get_module(account, Xep.Muc.Module.IDENTITY).room_info_updated.connect( (stream, muc_jid) => { room_info_updated(account, muc_jid); }); - stream_interactor.module_manager.get_module(account, Xep.Muc.Module.IDENTITY).received_occupant_jid.connect( (stream, room, occupant) => { - if (is_private_room(account, room.bare_jid)) { - private_room_occupant_updated(account, room, occupant); - } - }); stream_interactor.module_manager.get_module(account, Xep.OccupantIds.Module.IDENTITY).received_own_occupant_id.connect( (stream, jid, occupant_id) => { if (!own_occupant_ids.has_key(account)) { own_occupant_ids[account] = new HashMap(Jid.hash_bare_func, Jid.equals_bare_func); @@ -555,6 +577,24 @@ public class MucManager : StreamInteractionModule, Object { }); } + private void persist_muc_affiliations(Account account, Jid muc_jid) { + Xep.Muc.Flag? flag = get_muc_flag(account); + if (flag == null) return; + + var affiliations = new HashMap(); + + Gee.List offline_members = flag.get_offline_members(muc_jid); + foreach (Jid offline_member in offline_members) { + Xep.Muc.Affiliation affiliation = flag.get_affiliation(muc_jid, offline_member); + affiliations[offline_member] = affiliation; + } + + muc_affiliations.update_affiliations(account, muc_jid, affiliations, null, null); + if (is_private_room(account, muc_jid)) { + private_room_occupants_updated(account, muc_jid); + } + } + private void join_all_active(Account account) { Gee.List conversations = stream_interactor.get_module(ConversationManager.IDENTITY).get_active_conversations(account); foreach (Conversation conversation in conversations) { -- cgit v1.3