Merge remote-tracking branch 'upstream/master' into master-windows-changes
|
@ -42,6 +42,7 @@ SOURCES
|
||||||
src/service/entity_info.vala
|
src/service/entity_info.vala
|
||||||
src/service/file_manager.vala
|
src/service/file_manager.vala
|
||||||
src/service/file_transfer_storage.vala
|
src/service/file_transfer_storage.vala
|
||||||
|
src/service/history_sync.vala
|
||||||
src/service/jingle_file_transfers.vala
|
src/service/jingle_file_transfers.vala
|
||||||
src/service/message_correction.vala
|
src/service/message_correction.vala
|
||||||
src/service/message_processor.vala
|
src/service/message_processor.vala
|
||||||
|
@ -50,6 +51,7 @@ SOURCES
|
||||||
src/service/muc_manager.vala
|
src/service/muc_manager.vala
|
||||||
src/service/notification_events.vala
|
src/service/notification_events.vala
|
||||||
src/service/presence_manager.vala
|
src/service/presence_manager.vala
|
||||||
|
src/service/reactions.vala
|
||||||
src/service/registration.vala
|
src/service/registration.vala
|
||||||
src/service/roster_manager.vala
|
src/service/roster_manager.vala
|
||||||
src/service/search_processor.vala
|
src/service/search_processor.vala
|
||||||
|
|
|
@ -55,6 +55,7 @@ public interface Application : GLib.Application {
|
||||||
EntityInfo.start(stream_interactor, db);
|
EntityInfo.start(stream_interactor, db);
|
||||||
MessageCorrection.start(stream_interactor, db);
|
MessageCorrection.start(stream_interactor, db);
|
||||||
FileTransferStorage.start(stream_interactor, db);
|
FileTransferStorage.start(stream_interactor, db);
|
||||||
|
Reactions.start(stream_interactor, db);
|
||||||
|
|
||||||
create_actions();
|
create_actions();
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ public class Conversation : Object {
|
||||||
public Jid counterpart { get; private set; }
|
public Jid counterpart { get; private set; }
|
||||||
public string? nickname { get; set; }
|
public string? nickname { get; set; }
|
||||||
public bool active { get; set; default = false; }
|
public bool active { get; set; default = false; }
|
||||||
|
public DateTime active_last_changed { get; private set; }
|
||||||
private DateTime? _last_active;
|
private DateTime? _last_active;
|
||||||
public DateTime? last_active {
|
public DateTime? last_active {
|
||||||
get { return _last_active; }
|
get { return _last_active; }
|
||||||
|
@ -63,6 +64,7 @@ public class Conversation : Object {
|
||||||
if (type_ == Conversation.Type.GROUPCHAT_PM) counterpart = counterpart.with_resource(resource);
|
if (type_ == Conversation.Type.GROUPCHAT_PM) counterpart = counterpart.with_resource(resource);
|
||||||
nickname = type_ == Conversation.Type.GROUPCHAT ? resource : null;
|
nickname = type_ == Conversation.Type.GROUPCHAT ? resource : null;
|
||||||
active = row[db.conversation.active];
|
active = row[db.conversation.active];
|
||||||
|
active_last_changed = new DateTime.from_unix_utc(row[db.conversation.active_last_changed]);
|
||||||
int64? last_active = row[db.conversation.last_active];
|
int64? last_active = row[db.conversation.last_active];
|
||||||
if (last_active != null) this.last_active = new DateTime.from_unix_utc(last_active);
|
if (last_active != null) this.last_active = new DateTime.from_unix_utc(last_active);
|
||||||
encryption = (Encryption) row[db.conversation.encryption];
|
encryption = (Encryption) row[db.conversation.encryption];
|
||||||
|
@ -78,12 +80,15 @@ public class Conversation : Object {
|
||||||
|
|
||||||
public void persist(Database db) {
|
public void persist(Database db) {
|
||||||
this.db = db;
|
this.db = db;
|
||||||
|
this.active_last_changed = new DateTime.now_utc();
|
||||||
|
|
||||||
var insert = db.conversation.insert()
|
var insert = db.conversation.insert()
|
||||||
.value(db.conversation.account_id, account.id)
|
.value(db.conversation.account_id, account.id)
|
||||||
.value(db.conversation.jid_id, db.get_jid_id(counterpart))
|
.value(db.conversation.jid_id, db.get_jid_id(counterpart))
|
||||||
.value(db.conversation.type_, type_)
|
.value(db.conversation.type_, type_)
|
||||||
.value(db.conversation.encryption, encryption)
|
.value(db.conversation.encryption, encryption)
|
||||||
.value(db.conversation.active, active)
|
.value(db.conversation.active, active)
|
||||||
|
.value(db.conversation.active_last_changed, (long) active_last_changed.to_unix())
|
||||||
.value(db.conversation.notification, notify_setting)
|
.value(db.conversation.notification, notify_setting)
|
||||||
.value(db.conversation.send_typing, send_typing)
|
.value(db.conversation.send_typing, send_typing)
|
||||||
.value(db.conversation.send_marker, send_marker);
|
.value(db.conversation.send_marker, send_marker);
|
||||||
|
@ -176,7 +181,9 @@ public class Conversation : Object {
|
||||||
case "nickname":
|
case "nickname":
|
||||||
update.set(db.conversation.resource, nickname); break;
|
update.set(db.conversation.resource, nickname); break;
|
||||||
case "active":
|
case "active":
|
||||||
update.set(db.conversation.active, active); break;
|
update.set(db.conversation.active, active);
|
||||||
|
update.set(db.conversation.active_last_changed, (long) new DateTime.now_utc().to_unix());
|
||||||
|
break;
|
||||||
case "last-active":
|
case "last-active":
|
||||||
if (last_active != null) {
|
if (last_active != null) {
|
||||||
update.set(db.conversation.last_active, (long) last_active.to_unix());
|
update.set(db.conversation.last_active, (long) last_active.to_unix());
|
||||||
|
|
|
@ -154,7 +154,8 @@ public interface ConversationItemWidgetInterface: Object {
|
||||||
public delegate void MessageActionEvoked(Object button, Plugins.MetaConversationItem evoked_on, Object widget);
|
public delegate void MessageActionEvoked(Object button, Plugins.MetaConversationItem evoked_on, Object widget);
|
||||||
public class MessageAction : Object {
|
public class MessageAction : Object {
|
||||||
public string icon_name;
|
public string icon_name;
|
||||||
public MessageActionEvoked callback;
|
public Object? popover;
|
||||||
|
public MessageActionEvoked? callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract class MetaConversationNotification : Object {
|
public abstract class MetaConversationNotification : Object {
|
||||||
|
|
|
@ -188,7 +188,7 @@ public class ChatInteraction : StreamInteractionModule, Object {
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
|
public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
|
||||||
if (Xep.MessageArchiveManagement.MessageFlag.get_flag(stanza) != null) return false;
|
if (Xmpp.MessageArchiveManagement.MessageFlag.get_flag(stanza) != null) return false;
|
||||||
|
|
||||||
ChatInteraction outer = stream_interactor.get_module(ChatInteraction.IDENTITY);
|
ChatInteraction outer = stream_interactor.get_module(ChatInteraction.IDENTITY);
|
||||||
outer.send_delivery_receipt(message, stanza, conversation);
|
outer.send_delivery_receipt(message, stanza, conversation);
|
||||||
|
|
|
@ -176,7 +176,7 @@ public class ConversationManager : StreamInteractionModule, Object {
|
||||||
conversation.last_active = message.time;
|
conversation.last_active = message.time;
|
||||||
|
|
||||||
if (stanza != null) {
|
if (stanza != null) {
|
||||||
bool is_mam_message = Xep.MessageArchiveManagement.MessageFlag.get_flag(stanza) != null;
|
bool is_mam_message = Xmpp.MessageArchiveManagement.MessageFlag.get_flag(stanza) != null;
|
||||||
bool is_recent = message.time.compare(new DateTime.now_utc().add_days(-3)) > 0;
|
bool is_recent = message.time.compare(new DateTime.now_utc().add_days(-3)) > 0;
|
||||||
if (is_mam_message && !is_recent) return false;
|
if (is_mam_message && !is_recent) return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ using Dino.Entities;
|
||||||
namespace Dino {
|
namespace Dino {
|
||||||
|
|
||||||
public class Database : Qlite.Database {
|
public class Database : Qlite.Database {
|
||||||
private const int VERSION = 22;
|
private const int VERSION = 23;
|
||||||
|
|
||||||
public class AccountTable : Table {
|
public class AccountTable : Table {
|
||||||
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
|
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
|
||||||
|
@ -119,6 +119,20 @@ public class Database : Qlite.Database {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class OccupantIdTable : Table {
|
||||||
|
public Column<int> id = new Column.Integer("id") { primary_key = true };
|
||||||
|
public Column<int> account_id = new Column.Integer("account_id") { not_null = true };
|
||||||
|
public Column<string> last_nick = new Column.Text("last_nick");
|
||||||
|
public Column<int> jid_id = new Column.Integer("jid_id");
|
||||||
|
public Column<string> occupant_id = new Column.Text("occupant_id");
|
||||||
|
|
||||||
|
internal OccupantIdTable(Database db) {
|
||||||
|
base(db, "occupant_id");
|
||||||
|
init({id, account_id, last_nick, jid_id, occupant_id});
|
||||||
|
unique({account_id, jid_id, occupant_id}, "REPLACE");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public class UndecryptedTable : Table {
|
public class UndecryptedTable : Table {
|
||||||
public Column<int> message_id = new Column.Integer("message_id");
|
public Column<int> message_id = new Column.Integer("message_id");
|
||||||
public Column<int> type_ = new Column.Integer("type");
|
public Column<int> type_ = new Column.Integer("type");
|
||||||
|
@ -193,6 +207,7 @@ public class Database : Qlite.Database {
|
||||||
public Column<int> jid_id = new Column.Integer("jid_id") { not_null = true };
|
public Column<int> jid_id = new Column.Integer("jid_id") { not_null = true };
|
||||||
public Column<string> resource = new Column.Text("resource") { min_version=1 };
|
public Column<string> resource = new Column.Text("resource") { min_version=1 };
|
||||||
public Column<bool> active = new Column.BoolInt("active");
|
public Column<bool> active = new Column.BoolInt("active");
|
||||||
|
public Column<long> active_last_changed = new Column.Integer("active_last_changed") { not_null=true, default="0", min_version=23 };
|
||||||
public Column<long> last_active = new Column.Long("last_active");
|
public Column<long> last_active = new Column.Long("last_active");
|
||||||
public Column<int> type_ = new Column.Integer("type");
|
public Column<int> type_ = new Column.Integer("type");
|
||||||
public Column<int> encryption = new Column.Integer("encryption");
|
public Column<int> encryption = new Column.Integer("encryption");
|
||||||
|
@ -204,7 +219,7 @@ public class Database : Qlite.Database {
|
||||||
|
|
||||||
internal ConversationTable(Database db) {
|
internal ConversationTable(Database db) {
|
||||||
base(db, "conversation");
|
base(db, "conversation");
|
||||||
init({id, account_id, jid_id, resource, active, last_active, type_, encryption, read_up_to, read_up_to_item, notification, send_typing, send_marker});
|
init({id, account_id, jid_id, resource, active, active_last_changed, last_active, type_, encryption, read_up_to, read_up_to_item, notification, send_typing, send_marker});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -263,15 +278,33 @@ public class Database : Qlite.Database {
|
||||||
public class MamCatchupTable : Table {
|
public class MamCatchupTable : Table {
|
||||||
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
|
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
|
||||||
public Column<int> account_id = new Column.Integer("account_id") { not_null = true };
|
public Column<int> account_id = new Column.Integer("account_id") { not_null = true };
|
||||||
public Column<bool> from_end = new Column.BoolInt("from_end");
|
public Column<string> server_jid = new Column.Text("server_jid") { not_null = true };
|
||||||
public Column<string> from_id = new Column.Text("from_id");
|
public Column<string> from_id = new Column.Text("from_id") { not_null = true };
|
||||||
public Column<long> from_time = new Column.Long("from_time") { not_null = true };
|
public Column<long> from_time = new Column.Long("from_time") { not_null = true };
|
||||||
public Column<string> to_id = new Column.Text("to_id");
|
public Column<bool> from_end = new Column.BoolInt("from_end") { not_null = true };
|
||||||
|
public Column<string> to_id = new Column.Text("to_id") { not_null = true };
|
||||||
public Column<long> to_time = new Column.Long("to_time") { not_null = true };
|
public Column<long> to_time = new Column.Long("to_time") { not_null = true };
|
||||||
|
|
||||||
internal MamCatchupTable(Database db) {
|
internal MamCatchupTable(Database db) {
|
||||||
base(db, "mam_catchup");
|
base(db, "mam_catchup");
|
||||||
init({id, account_id, from_end, from_id, from_time, to_id, to_time});
|
init({id, account_id, server_jid, from_end, from_id, from_time, to_id, to_time});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ReactionTable : Table {
|
||||||
|
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
|
||||||
|
public Column<int> account_id = new Column.Integer("account_id") { not_null = true };
|
||||||
|
public Column<int> occupant_id = new Column.Integer("occupant_id");
|
||||||
|
public Column<int> content_item_id = new Column.Integer("content_item_id") { not_null = true };
|
||||||
|
public Column<long> time = new Column.Long("time") { not_null = true };
|
||||||
|
public Column<int> jid_id = new Column.Integer("jid_id");
|
||||||
|
public Column<string> emojis = new Column.Text("emojis");
|
||||||
|
|
||||||
|
internal ReactionTable(Database db) {
|
||||||
|
base(db, "reaction");
|
||||||
|
init({id, account_id, occupant_id, content_item_id, time, jid_id, emojis});
|
||||||
|
unique({account_id, content_item_id, jid_id}, "REPLACE");
|
||||||
|
unique({account_id, content_item_id, occupant_id}, "REPLACE");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -306,6 +339,7 @@ public class Database : Qlite.Database {
|
||||||
public MessageTable message { get; private set; }
|
public MessageTable message { get; private set; }
|
||||||
public MessageCorrectionTable message_correction { get; private set; }
|
public MessageCorrectionTable message_correction { get; private set; }
|
||||||
public RealJidTable real_jid { get; private set; }
|
public RealJidTable real_jid { get; private set; }
|
||||||
|
public OccupantIdTable occupantid { get; private set; }
|
||||||
public FileTransferTable file_transfer { get; private set; }
|
public FileTransferTable file_transfer { get; private set; }
|
||||||
public CallTable call { get; private set; }
|
public CallTable call { get; private set; }
|
||||||
public CallCounterpartTable call_counterpart { get; private set; }
|
public CallCounterpartTable call_counterpart { get; private set; }
|
||||||
|
@ -315,6 +349,7 @@ public class Database : Qlite.Database {
|
||||||
public EntityFeatureTable entity_feature { get; private set; }
|
public EntityFeatureTable entity_feature { get; private set; }
|
||||||
public RosterTable roster { get; private set; }
|
public RosterTable roster { get; private set; }
|
||||||
public MamCatchupTable mam_catchup { get; private set; }
|
public MamCatchupTable mam_catchup { get; private set; }
|
||||||
|
public ReactionTable reaction { get; private set; }
|
||||||
public SettingsTable settings { get; private set; }
|
public SettingsTable settings { get; private set; }
|
||||||
public ConversationSettingsTable conversation_settings { get; private set; }
|
public ConversationSettingsTable conversation_settings { get; private set; }
|
||||||
|
|
||||||
|
@ -330,6 +365,7 @@ public class Database : Qlite.Database {
|
||||||
content_item = new ContentItemTable(this);
|
content_item = new ContentItemTable(this);
|
||||||
message = new MessageTable(this);
|
message = new MessageTable(this);
|
||||||
message_correction = new MessageCorrectionTable(this);
|
message_correction = new MessageCorrectionTable(this);
|
||||||
|
occupantid = new OccupantIdTable(this);
|
||||||
real_jid = new RealJidTable(this);
|
real_jid = new RealJidTable(this);
|
||||||
file_transfer = new FileTransferTable(this);
|
file_transfer = new FileTransferTable(this);
|
||||||
call = new CallTable(this);
|
call = new CallTable(this);
|
||||||
|
@ -340,9 +376,10 @@ public class Database : Qlite.Database {
|
||||||
entity_feature = new EntityFeatureTable(this);
|
entity_feature = new EntityFeatureTable(this);
|
||||||
roster = new RosterTable(this);
|
roster = new RosterTable(this);
|
||||||
mam_catchup = new MamCatchupTable(this);
|
mam_catchup = new MamCatchupTable(this);
|
||||||
|
reaction = new ReactionTable(this);
|
||||||
settings = new SettingsTable(this);
|
settings = new SettingsTable(this);
|
||||||
conversation_settings = new ConversationSettingsTable(this);
|
conversation_settings = new ConversationSettingsTable(this);
|
||||||
init({ account, jid, entity, content_item, message, message_correction, real_jid, file_transfer, call, call_counterpart, conversation, avatar, entity_identity, entity_feature, roster, mam_catchup, settings, conversation_settings });
|
init({ account, jid, entity, content_item, message, message_correction, real_jid, occupantid, file_transfer, call, call_counterpart, conversation, avatar, entity_identity, entity_feature, roster, mam_catchup, reaction, settings, conversation_settings });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
exec("PRAGMA journal_mode = WAL");
|
exec("PRAGMA journal_mode = WAL");
|
||||||
|
@ -474,6 +511,25 @@ public class Database : Qlite.Database {
|
||||||
// FROM call2");
|
// FROM call2");
|
||||||
// exec("DROP TABLE call2");
|
// exec("DROP TABLE call2");
|
||||||
}
|
}
|
||||||
|
if (oldVersion < 23) {
|
||||||
|
try {
|
||||||
|
exec("ALTER TABLE mam_catchup RENAME TO mam_catchup2");
|
||||||
|
mam_catchup.create_table_at_version(VERSION);
|
||||||
|
exec("""INSERT INTO mam_catchup (id, account_id, server_jid, from_id, from_time, from_end, to_id, to_time)
|
||||||
|
SELECT mam_catchup2.id, account_id, bare_jid, ifnull(from_id, ""), from_time, ifnull(from_end, 0), ifnull(to_id, ""), to_time
|
||||||
|
FROM mam_catchup2 JOIN account ON mam_catchup2.account_id=account.id""");
|
||||||
|
exec("DROP TABLE mam_catchup2");
|
||||||
|
} catch (Error e) {
|
||||||
|
error("Failed to upgrade to database version 23 (mam_catchup): %s", e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
long active_last_updated = (long) new DateTime.now_utc().to_unix();
|
||||||
|
exec(@"UPDATE conversation SET active_last_changed=$active_last_updated WHERE active_last_changed=0");
|
||||||
|
} catch (Error e) {
|
||||||
|
error("Failed to upgrade to database version 23 (conversation): %s", e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ArrayList<Account> get_accounts() {
|
public ArrayList<Account> get_accounts() {
|
||||||
|
|
557
libdino/src/service/history_sync.vala
Normal file
|
@ -0,0 +1,557 @@
|
||||||
|
using Gee;
|
||||||
|
|
||||||
|
using Xmpp;
|
||||||
|
using Xmpp.Xep;
|
||||||
|
using Dino.Entities;
|
||||||
|
using Qlite;
|
||||||
|
|
||||||
|
public class Dino.HistorySync {
|
||||||
|
|
||||||
|
private StreamInteractor stream_interactor;
|
||||||
|
private Database db;
|
||||||
|
|
||||||
|
public HashMap<Account, HashMap<Jid, int>> current_catchup_id = new HashMap<Account, HashMap<Jid, int>>(Account.hash_func, Account.equals_func);
|
||||||
|
public HashMap<Account, HashMap<string, DateTime>> mam_times = new HashMap<Account, HashMap<string, DateTime>>();
|
||||||
|
public HashMap<string, int> hitted_range = new HashMap<string, int>();
|
||||||
|
|
||||||
|
// Server ID of the latest message of the previous segment
|
||||||
|
public HashMap<Account, string> catchup_until_id = new HashMap<Account, string>(Account.hash_func, Account.equals_func);
|
||||||
|
// Time of the latest message of the previous segment
|
||||||
|
public HashMap<Account, DateTime> catchup_until_time = new HashMap<Account, DateTime>(Account.hash_func, Account.equals_func);
|
||||||
|
|
||||||
|
private HashMap<string, Gee.List<Xmpp.MessageStanza>> stanzas = new HashMap<string, Gee.List<Xmpp.MessageStanza>>();
|
||||||
|
|
||||||
|
public class HistorySync(Database db, StreamInteractor stream_interactor) {
|
||||||
|
this.stream_interactor = stream_interactor;
|
||||||
|
this.db = db;
|
||||||
|
|
||||||
|
stream_interactor.account_added.connect(on_account_added);
|
||||||
|
|
||||||
|
stream_interactor.connection_manager.stream_opened.connect((account, stream) => {
|
||||||
|
debug("MAM: [%s] Reset catchup_id", account.bare_jid.to_string());
|
||||||
|
current_catchup_id.unset(account);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool process(Account account, Xmpp.MessageStanza message_stanza) {
|
||||||
|
var mam_flag = Xmpp.MessageArchiveManagement.MessageFlag.get_flag(message_stanza);
|
||||||
|
|
||||||
|
if (mam_flag != null) {
|
||||||
|
process_mam_message(account, message_stanza, mam_flag);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
update_latest_db_range(account, message_stanza);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void update_latest_db_range(Account account, Xmpp.MessageStanza message_stanza) {
|
||||||
|
Jid mam_server = stream_interactor.get_module(MucManager.IDENTITY).might_be_groupchat(message_stanza.from, account) ? message_stanza.from.bare_jid : account.bare_jid;
|
||||||
|
|
||||||
|
if (!current_catchup_id.has_key(account) || !current_catchup_id[account].has_key(mam_server)) return;
|
||||||
|
|
||||||
|
string? stanza_id = UniqueStableStanzaIDs.get_stanza_id(message_stanza, mam_server);
|
||||||
|
if (stanza_id == null) return;
|
||||||
|
|
||||||
|
db.mam_catchup.update()
|
||||||
|
.with(db.mam_catchup.id, "=", current_catchup_id[account][mam_server])
|
||||||
|
.set(db.mam_catchup.to_time, (long)new DateTime.now_utc().to_unix())
|
||||||
|
.set(db.mam_catchup.to_id, stanza_id)
|
||||||
|
.perform();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void process_mam_message(Account account, Xmpp.MessageStanza message_stanza, Xmpp.MessageArchiveManagement.MessageFlag mam_flag) {
|
||||||
|
Jid mam_server = mam_flag.sender_jid;
|
||||||
|
Jid message_author = message_stanza.from;
|
||||||
|
|
||||||
|
// MUC servers may only send MAM messages from that MUC
|
||||||
|
bool is_muc_mam = stream_interactor.get_module(MucManager.IDENTITY).might_be_groupchat(mam_server, account) &&
|
||||||
|
message_author.equals_bare(mam_server);
|
||||||
|
|
||||||
|
bool from_our_server = mam_server.equals_bare(account.bare_jid);
|
||||||
|
|
||||||
|
if (!is_muc_mam && !from_our_server) {
|
||||||
|
warning("Received alleged MAM message from %s, ignoring", mam_server.to_string());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stanzas.has_key(mam_flag.query_id)) stanzas[mam_flag.query_id] = new ArrayList<Xmpp.MessageStanza>();
|
||||||
|
stanzas[mam_flag.query_id].add(message_stanza);
|
||||||
|
|
||||||
|
print(@"[$(message_stanza.from)] qid $(mam_flag.query_id) time $(mam_flag.server_time) $(mam_flag.mam_id) $(message_stanza.body ?? "[none]")\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void on_unprocessed_message(Account account, XmppStream stream, MessageStanza message) {
|
||||||
|
// Check that it's a legit MAM server
|
||||||
|
bool is_muc_mam = stream_interactor.get_module(MucManager.IDENTITY).might_be_groupchat(message.from, account);
|
||||||
|
bool from_our_server = message.from.equals_bare(account.bare_jid);
|
||||||
|
if (!is_muc_mam && !from_our_server) return;
|
||||||
|
|
||||||
|
// Get the server time of the message and store it in `mam_times`
|
||||||
|
Xmpp.MessageArchiveManagement.Flag? mam_flag = stream != null ? stream.get_flag(Xmpp.MessageArchiveManagement.Flag.IDENTITY) : null;
|
||||||
|
if (mam_flag == null) return;
|
||||||
|
string? id = message.stanza.get_deep_attribute(mam_flag.ns_ver + ":result", "id");
|
||||||
|
if (id == null) return;
|
||||||
|
StanzaNode? delay_node = message.stanza.get_deep_subnode(mam_flag.ns_ver + ":result", StanzaForwarding.NS_URI + ":forwarded", DelayedDelivery.NS_URI + ":delay");
|
||||||
|
if (delay_node == null) {
|
||||||
|
warning("MAM result did not contain delayed time %s", message.stanza.to_string());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DateTime? time = DelayedDelivery.get_time_for_node(delay_node);
|
||||||
|
if (time == null) return;
|
||||||
|
mam_times[account][id] = time;
|
||||||
|
|
||||||
|
// Check if this is the target message
|
||||||
|
string? query_id = message.stanza.get_deep_attribute(mam_flag.ns_ver + ":result", mam_flag.ns_ver + ":queryid");
|
||||||
|
if (query_id != null && id == catchup_until_id[account]) {
|
||||||
|
debug("MAM: [%s] Hitted range (id) %s", account.bare_jid.to_string(), id);
|
||||||
|
hitted_range[query_id] = -2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void on_server_id_duplicate(Account account, Xmpp.MessageStanza message_stanza, Entities.Message message) {
|
||||||
|
Xmpp.MessageArchiveManagement.MessageFlag? mam_flag = Xmpp.MessageArchiveManagement.MessageFlag.get_flag(message_stanza);
|
||||||
|
if (mam_flag == null) return;
|
||||||
|
|
||||||
|
// debug(@"MAM: [%s] Hitted range duplicate server id. id %s qid %s", account.bare_jid.to_string(), message.server_id, mam_flag.query_id);
|
||||||
|
if (catchup_until_time.has_key(account) && mam_flag.server_time.compare(catchup_until_time[account]) < 0) {
|
||||||
|
hitted_range[mam_flag.query_id] = -1;
|
||||||
|
// debug(@"MAM: [%s] In range (time) %s < %s", account.bare_jid.to_string(), mam_flag.server_time.to_string(), catchup_until_time[account].to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void fetch_everything(Account account, Jid mam_server, DateTime until_earliest_time = new DateTime.from_unix_utc(0)) {
|
||||||
|
print(@"Fetch everything for $(mam_server) %s\n".printf(until_earliest_time != null ? @"(until $until_earliest_time)" : ""));
|
||||||
|
RowOption latest_row_opt = db.mam_catchup.select()
|
||||||
|
.with(db.mam_catchup.account_id, "=", account.id)
|
||||||
|
.with(db.mam_catchup.server_jid, "=", mam_server.to_string())
|
||||||
|
.with(db.mam_catchup.to_time, ">=", (long) until_earliest_time.to_unix())
|
||||||
|
.order_by(db.mam_catchup.to_time, "DESC")
|
||||||
|
.single().row();
|
||||||
|
Row? latest_row = latest_row_opt.is_present() ? latest_row_opt.inner : null;
|
||||||
|
|
||||||
|
Row? new_row = yield fetch_latest_page(account, mam_server, latest_row, until_earliest_time);
|
||||||
|
|
||||||
|
if (new_row != null) {
|
||||||
|
current_catchup_id[account][mam_server] = new_row[db.mam_catchup.id];
|
||||||
|
} else if (latest_row != null) {
|
||||||
|
current_catchup_id[account][mam_server] = latest_row[db.mam_catchup.id];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the previous and current row
|
||||||
|
print(@"$(new_row == null) $(latest_row == null)\n");
|
||||||
|
Row? previous_row = null;
|
||||||
|
Row? current_row = null;
|
||||||
|
if (new_row != null) {
|
||||||
|
print(@"Fetch everything $(mam_server) a\n");
|
||||||
|
current_row = new_row;
|
||||||
|
previous_row = latest_row;
|
||||||
|
} else if (latest_row != null) {
|
||||||
|
print(@"Fetch everything $(mam_server) b\n");
|
||||||
|
current_row = latest_row;
|
||||||
|
RowOption previous_row_opt = db.mam_catchup.select()
|
||||||
|
.with(db.mam_catchup.account_id, "=", account.id)
|
||||||
|
.with(db.mam_catchup.server_jid, "=", mam_server.to_string())
|
||||||
|
.with(db.mam_catchup.to_time, "<", current_row[db.mam_catchup.from_time])
|
||||||
|
.with(db.mam_catchup.to_time, ">=", (long) until_earliest_time.to_unix())
|
||||||
|
.order_by(db.mam_catchup.to_time, "DESC")
|
||||||
|
.single().row();
|
||||||
|
previous_row = previous_row_opt.is_present() ? previous_row_opt.inner : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
print(@"Fetch everything $(mam_server) c $(current_row == null) $(previous_row == null)\n");
|
||||||
|
// Fetch messages between two db ranges and merge them
|
||||||
|
while (current_row != null && previous_row != null) {
|
||||||
|
if (current_row[db.mam_catchup.from_end]) return;
|
||||||
|
|
||||||
|
print("FETCH BETWEEN RANGES\n");
|
||||||
|
current_row = yield fetch_between_ranges(account, mam_server, previous_row, current_row);
|
||||||
|
if (current_row == null) return;
|
||||||
|
|
||||||
|
RowOption previous_row_opt = db.mam_catchup.select()
|
||||||
|
.with(db.mam_catchup.account_id, "=", account.id)
|
||||||
|
.with(db.mam_catchup.server_jid, "=", mam_server.to_string())
|
||||||
|
.with(db.mam_catchup.to_time, "<", current_row[db.mam_catchup.from_time])
|
||||||
|
.with(db.mam_catchup.to_time, ">=", (long) until_earliest_time.to_unix())
|
||||||
|
.order_by(db.mam_catchup.to_time, "DESC")
|
||||||
|
.single().row();
|
||||||
|
previous_row = previous_row_opt.is_present() ? previous_row_opt.inner : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We're at the earliest range. Try to expand it even further back.
|
||||||
|
if (current_row == null || current_row[db.mam_catchup.from_end]) return;
|
||||||
|
// We don't want to fetch before the earliest range over and over again in MUCs if it's after until_earliest_time.
|
||||||
|
// For now, don't query if we are within a week of until_earliest_time
|
||||||
|
if (until_earliest_time != null &&
|
||||||
|
current_row[db.mam_catchup.from_time] > until_earliest_time.add(-TimeSpan.DAY * 7).to_unix()) return;
|
||||||
|
print("FETCH BEFORE RANGE\n");
|
||||||
|
yield fetch_before_range(account, mam_server, current_row, until_earliest_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetches the latest page (up to previous db row). Extends the previous db row if it was reached, creates a new row otherwise.
|
||||||
|
public async Row? fetch_latest_page(Account account, Jid mam_server, Row? latest_row, DateTime? until_earliest_time) {
|
||||||
|
debug("MAM: [%s | %s] Fetching latest page", mam_server.to_string(), mam_server.to_string());
|
||||||
|
|
||||||
|
int latest_row_id = -1;
|
||||||
|
DateTime latest_message_time = until_earliest_time;
|
||||||
|
string? latest_message_id = null;
|
||||||
|
|
||||||
|
if (latest_row != null) {
|
||||||
|
latest_row_id = latest_row[db.mam_catchup.id];
|
||||||
|
latest_message_time = (new DateTime.from_unix_utc(latest_row[db.mam_catchup.to_time])).add_minutes(-5);
|
||||||
|
print(@"latest msg time $latest_message_time\n");
|
||||||
|
latest_message_id = latest_row[db.mam_catchup.to_id];
|
||||||
|
|
||||||
|
// Make sure we only fetch to until_earliest_time if latest_message_time is further back
|
||||||
|
if (until_earliest_time != null && latest_message_time.compare(until_earliest_time) < 0) {
|
||||||
|
latest_message_time = until_earliest_time.add_minutes(-5);
|
||||||
|
latest_message_id = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var query_params = new Xmpp.MessageArchiveManagement.V2.MamQueryParams.query_latest(mam_server, latest_message_time, latest_message_id);
|
||||||
|
|
||||||
|
PageRequestResult page_result = yield get_mam_page(account, query_params, null);
|
||||||
|
|
||||||
|
if (page_result.page_result == PageResult.Error || page_result.page_result == PageResult.Duplicate) {
|
||||||
|
debug("MAM [%s | %s] Failed fetching latest page %s", mam_server.to_string(), mam_server.to_string(), page_result.page_result.to_string());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
print(@"MAM result: $(page_result.page_result))\n");
|
||||||
|
|
||||||
|
// Catchup finished within first page. Update latest db entry.
|
||||||
|
if (page_result.page_result in new PageResult[] { PageResult.TargetReached, PageResult.NoMoreMessages } && latest_row_id != -1) {
|
||||||
|
if (page_result.stanzas == null || page_result.stanzas.is_empty) return null;
|
||||||
|
|
||||||
|
string first_mam_id = page_result.query_result.first;
|
||||||
|
long first_mam_time = (long) mam_times[account][first_mam_id].to_unix();
|
||||||
|
|
||||||
|
print(@"Updating $mam_server to $first_mam_time, $first_mam_id\n");
|
||||||
|
var query = db.mam_catchup.update()
|
||||||
|
.with(db.mam_catchup.id, "=", latest_row_id)
|
||||||
|
.set(db.mam_catchup.to_time, first_mam_time)
|
||||||
|
.set(db.mam_catchup.to_id, first_mam_id);
|
||||||
|
|
||||||
|
if (page_result.page_result == PageResult.NoMoreMessages) {
|
||||||
|
// If the server doesn't have more messages, store that this range is at its end.
|
||||||
|
query.set(db.mam_catchup.from_end, true);
|
||||||
|
}
|
||||||
|
query.perform();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (page_result.query_result.first == null || page_result.query_result.last == null) {
|
||||||
|
print(@"from/to id null\n");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Either we need to fetch more pages or this is the first db entry ever
|
||||||
|
debug("MAM: [%s | %s] Creating new db range for latest page", mam_server.to_string(), mam_server.to_string());
|
||||||
|
|
||||||
|
string from_id = page_result.query_result.first;
|
||||||
|
string to_id = page_result.query_result.last;
|
||||||
|
|
||||||
|
if (!mam_times[account].has_key(from_id) || !mam_times[account].has_key(to_id)) {
|
||||||
|
print(@"Missing from/to id $from_id $to_id\n");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
long from_time = (long) mam_times[account][from_id].to_unix();
|
||||||
|
long to_time = (long) mam_times[account][to_id].to_unix();
|
||||||
|
|
||||||
|
int new_row_id = (int) db.mam_catchup.insert()
|
||||||
|
.value(db.mam_catchup.account_id, account.id)
|
||||||
|
.value(db.mam_catchup.server_jid, mam_server.to_string())
|
||||||
|
.value(db.mam_catchup.from_id, from_id)
|
||||||
|
.value(db.mam_catchup.from_time, from_time)
|
||||||
|
.value(db.mam_catchup.from_end, false)
|
||||||
|
.value(db.mam_catchup.to_id, to_id)
|
||||||
|
.value(db.mam_catchup.to_time, to_time)
|
||||||
|
.perform();
|
||||||
|
return db.mam_catchup.select().with(db.mam_catchup.id, "=", new_row_id).single().row().inner;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fetches messages between the end of `earlier_range` and start of `later_range`
|
||||||
|
** Merges the `earlier_range` db row into the `later_range` db row.
|
||||||
|
** @return The resulting range comprising `earlier_range`, `later_rage`, and everything in between. null if fetching/merge failed.
|
||||||
|
**/
|
||||||
|
private async Row? fetch_between_ranges(Account account, Jid mam_server, Row earlier_range, Row later_range) {
|
||||||
|
int later_range_id = (int) later_range[db.mam_catchup.id];
|
||||||
|
DateTime earliest_time = new DateTime.from_unix_utc(earlier_range[db.mam_catchup.to_time]);
|
||||||
|
DateTime latest_time = new DateTime.from_unix_utc(later_range[db.mam_catchup.from_time]);
|
||||||
|
debug("MAM [%s | %s] Fetching between %s (%s) and %s (%s)", mam_server.to_string(), mam_server.to_string(), earliest_time.to_string(), earlier_range[db.mam_catchup.to_id], latest_time.to_string(), later_range[db.mam_catchup.from_id]);
|
||||||
|
var query_params = new Xmpp.MessageArchiveManagement.V2.MamQueryParams.query_between(mam_server,
|
||||||
|
earliest_time, earlier_range[db.mam_catchup.to_id],
|
||||||
|
latest_time, later_range[db.mam_catchup.from_id]);
|
||||||
|
|
||||||
|
print("fetch between ranges\n");
|
||||||
|
PageRequestResult page_result = yield fetch_query(account, query_params, later_range_id);
|
||||||
|
print(@"page result null? $(page_result == null)\n");
|
||||||
|
|
||||||
|
if (page_result.page_result == PageResult.TargetReached) {
|
||||||
|
debug("MAM [%s | %s] Merging range %i into %i", mam_server.to_string(), mam_server.to_string(), earlier_range[db.mam_catchup.id], later_range_id);
|
||||||
|
// Merge earlier range into later one.
|
||||||
|
db.mam_catchup.update()
|
||||||
|
.with(db.mam_catchup.id, "=", later_range_id)
|
||||||
|
.set(db.mam_catchup.from_time, earlier_range[db.mam_catchup.from_time])
|
||||||
|
.set(db.mam_catchup.from_id, earlier_range[db.mam_catchup.from_id])
|
||||||
|
.set(db.mam_catchup.from_end, earlier_range[db.mam_catchup.from_end])
|
||||||
|
.perform();
|
||||||
|
|
||||||
|
db.mam_catchup.delete().with(db.mam_catchup.id, "=", earlier_range[db.mam_catchup.id]).perform();
|
||||||
|
|
||||||
|
// Return the updated version of the later range
|
||||||
|
return db.mam_catchup.select().with(db.mam_catchup.id, "=", later_range_id).single().row().inner;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void fetch_before_range(Account account, Jid mam_server, Row range, DateTime? until_earliest_time) {
|
||||||
|
DateTime latest_time = new DateTime.from_unix_utc(range[db.mam_catchup.from_time]);
|
||||||
|
string latest_id = range[db.mam_catchup.from_id];
|
||||||
|
|
||||||
|
Xmpp.MessageArchiveManagement.V2.MamQueryParams query_params;
|
||||||
|
if (until_earliest_time == null) {
|
||||||
|
query_params = new Xmpp.MessageArchiveManagement.V2.MamQueryParams.query_before(mam_server, latest_time, latest_id);
|
||||||
|
} else {
|
||||||
|
query_params = new Xmpp.MessageArchiveManagement.V2.MamQueryParams.query_between(
|
||||||
|
mam_server,
|
||||||
|
until_earliest_time, null,
|
||||||
|
latest_time, latest_id
|
||||||
|
);
|
||||||
|
}
|
||||||
|
PageRequestResult page_result = yield fetch_query(account, query_params, range[db.mam_catchup.id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iteratively fetches all pages returned for a query (until a PageResult other than MorePagesAvailable is returned)
|
||||||
|
* @return The last PageRequestResult result
|
||||||
|
**/
|
||||||
|
private async PageRequestResult fetch_query(Account account, Xmpp.MessageArchiveManagement.V2.MamQueryParams query_params, int db_id) {
|
||||||
|
print("fetch query\n");
|
||||||
|
PageRequestResult? page_result = null;
|
||||||
|
do {
|
||||||
|
page_result = yield get_mam_page(account, query_params, page_result);
|
||||||
|
print(@"page result $(page_result.page_result) $(page_result.stanzas == null)\n");
|
||||||
|
|
||||||
|
if (page_result.page_result == PageResult.Error || page_result.stanzas == null) return page_result;
|
||||||
|
|
||||||
|
string last_mam_id = page_result.query_result.last;
|
||||||
|
long last_mam_time = (long)mam_times[account][last_mam_id].to_unix();
|
||||||
|
|
||||||
|
print(@"Updating $(query_params.mam_server) to $last_mam_time, $last_mam_id\n");
|
||||||
|
var query = db.mam_catchup.update()
|
||||||
|
.with(db.mam_catchup.id, "=", db_id)
|
||||||
|
.set(db.mam_catchup.from_time, last_mam_time)
|
||||||
|
.set(db.mam_catchup.from_id, last_mam_id);
|
||||||
|
|
||||||
|
if (page_result.page_result == PageResult.NoMoreMessages) {
|
||||||
|
// If the server doesn't have more messages, store that this range is at its end.
|
||||||
|
print("no more message\n");
|
||||||
|
query.set(db.mam_catchup.from_end, true);
|
||||||
|
}
|
||||||
|
query.perform();
|
||||||
|
} while (page_result.page_result == PageResult.MorePagesAvailable);
|
||||||
|
|
||||||
|
print(@"page result 2 $(page_result.page_result)\n");
|
||||||
|
return page_result;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PageResult {
|
||||||
|
MorePagesAvailable,
|
||||||
|
TargetReached,
|
||||||
|
NoMoreMessages,
|
||||||
|
Duplicate, // TODO additional boolean
|
||||||
|
Error
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* prev_page_result: null if this is the first page request
|
||||||
|
**/
|
||||||
|
private async PageRequestResult get_mam_page(Account account, Xmpp.MessageArchiveManagement.V2.MamQueryParams query_params, PageRequestResult? prev_page_result) {
|
||||||
|
XmppStream stream = stream_interactor.get_stream(account);
|
||||||
|
Xmpp.MessageArchiveManagement.QueryResult query_result = null;
|
||||||
|
if (prev_page_result == null) {
|
||||||
|
query_result = yield Xmpp.MessageArchiveManagement.V2.query_archive(stream, query_params);
|
||||||
|
} else {
|
||||||
|
query_result = yield Xmpp.MessageArchiveManagement.V2.page_through_results(stream, query_params, prev_page_result.query_result);
|
||||||
|
}
|
||||||
|
return yield process_query_result(account, query_result, query_params.query_id, query_params.start_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async PageRequestResult process_query_result(Account account, Xmpp.MessageArchiveManagement.QueryResult query_result, string query_id, string? after_id) {
|
||||||
|
PageResult page_result = PageResult.MorePagesAvailable;
|
||||||
|
|
||||||
|
if (query_result.malformed || query_result.error) {
|
||||||
|
print(@"$(query_result.malformed) $(query_result.error)\n");
|
||||||
|
page_result = PageResult.Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We wait until all the messages from the page are processed (and we got the `mam_times` from them)
|
||||||
|
Idle.add(process_query_result.callback, Priority.LOW);
|
||||||
|
yield;
|
||||||
|
|
||||||
|
// We might have successfully reached the target or the server doesn't have all messages stored anymore
|
||||||
|
// If it's the former, we'll overwrite the value with PageResult.MorePagesAvailable below.
|
||||||
|
if (query_result.complete) {
|
||||||
|
page_result = PageResult.NoMoreMessages;
|
||||||
|
}
|
||||||
|
|
||||||
|
string selection = null;
|
||||||
|
string[] selection_args = {};
|
||||||
|
|
||||||
|
// Check the server id of all returned messages. Check if we've hit our target (from_id) or got a duplicate.
|
||||||
|
if (stanzas.has_key(query_id) && !stanzas[query_id].is_empty) {
|
||||||
|
print(@"$(stanzas.has_key(query_id)) $(!stanzas[query_id].is_empty) looking for $(after_id ?? "")\n");
|
||||||
|
foreach (Xmpp.MessageStanza message in stanzas[query_id]) {
|
||||||
|
Xmpp.MessageArchiveManagement.MessageFlag? mam_message_flag = Xmpp.MessageArchiveManagement.MessageFlag.get_flag(message);
|
||||||
|
if (mam_message_flag != null && mam_message_flag.mam_id != null) {
|
||||||
|
if (after_id != null && mam_message_flag.mam_id == after_id) {
|
||||||
|
// Successfully fetched the whole range
|
||||||
|
page_result = PageResult.TargetReached;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selection != null) selection += " OR ";
|
||||||
|
selection = @"$(db.message.server_id) = ?";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hitted_range.has_key(query_id)) {
|
||||||
|
// Message got filtered out by xmpp-vala, but succesfull range fetch nevertheless
|
||||||
|
page_result = PageResult.TargetReached;
|
||||||
|
}
|
||||||
|
|
||||||
|
int64 duplicates_found = db.message.select().where(selection, selection_args).count();
|
||||||
|
if (duplicates_found > 0) {
|
||||||
|
// We got a duplicate although we thought we have to catch up.
|
||||||
|
// There was a server bug where prosody would send all messages if it didn't know the after ID that was given
|
||||||
|
page_result = PageResult.Duplicate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var res = new PageRequestResult() { stanzas=stanzas[query_id], page_result=page_result, query_result=query_result };
|
||||||
|
send_messages_back_into_pipeline(account, query_id);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void send_messages_back_into_pipeline(Account account, string query_id) {
|
||||||
|
print(@"send_messages_back_into_pipeline $query_id\n");
|
||||||
|
if (!stanzas.has_key(query_id)) return;
|
||||||
|
|
||||||
|
foreach (Xmpp.MessageStanza message in stanzas[query_id]) {
|
||||||
|
stream_interactor.get_module(MessageProcessor.IDENTITY).run_pipeline_announce.begin(account, message);
|
||||||
|
}
|
||||||
|
stanzas.unset(query_id);
|
||||||
|
print(@"send_messages_back_into_pipeline $query_id done\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void on_account_added(Account account) {
|
||||||
|
cleanup_db_ranges(db, account);
|
||||||
|
|
||||||
|
mam_times[account] = new HashMap<string, DateTime>();
|
||||||
|
|
||||||
|
XmppStream? stream_bak = null;
|
||||||
|
stream_interactor.module_manager.get_module(account, Xmpp.MessageArchiveManagement.Module.IDENTITY).feature_available.connect( (stream) => {
|
||||||
|
if (stream == stream_bak) return;
|
||||||
|
|
||||||
|
current_catchup_id[account] = new HashMap<Jid, int>(Jid.hash_func, Jid.equals_func);
|
||||||
|
stream_bak = stream;
|
||||||
|
debug("MAM: [%s] MAM available", account.bare_jid.to_string());
|
||||||
|
fetch_everything.begin(account, account.bare_jid);
|
||||||
|
});
|
||||||
|
|
||||||
|
stream_interactor.module_manager.get_module(account, Xmpp.MessageModule.IDENTITY).received_message_unprocessed.connect((stream, message) => {
|
||||||
|
on_unprocessed_message(account, stream, message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void cleanup_db_ranges(Database db, Account account) {
|
||||||
|
var ranges = new HashMap<Jid, ArrayList<MamRange>>(Jid.hash_func, Jid.equals_func);
|
||||||
|
foreach (Row row in db.mam_catchup.select().with(db.mam_catchup.account_id, "=", account.id)) {
|
||||||
|
var mam_range = new MamRange();
|
||||||
|
mam_range.id = row[db.mam_catchup.id];
|
||||||
|
mam_range.server_jid = new Jid(row[db.mam_catchup.server_jid]);
|
||||||
|
mam_range.from_time = row[db.mam_catchup.from_time];
|
||||||
|
mam_range.from_id = row[db.mam_catchup.from_id];
|
||||||
|
mam_range.from_end = row[db.mam_catchup.from_end];
|
||||||
|
mam_range.to_time = row[db.mam_catchup.to_time];
|
||||||
|
mam_range.to_id = row[db.mam_catchup.to_id];
|
||||||
|
|
||||||
|
if (!ranges.has_key(mam_range.server_jid)) ranges[mam_range.server_jid] = new ArrayList<MamRange>();
|
||||||
|
ranges[mam_range.server_jid].add(mam_range);
|
||||||
|
}
|
||||||
|
|
||||||
|
var to_delete = new ArrayList<MamRange>();
|
||||||
|
|
||||||
|
foreach (Jid server_jid in ranges.keys) {
|
||||||
|
foreach (var range1 in ranges[server_jid]) {
|
||||||
|
if (to_delete.contains(range1)) continue;
|
||||||
|
|
||||||
|
foreach (MamRange range2 in ranges[server_jid]) {
|
||||||
|
print(@"$(account.bare_jid) | $(server_jid) | $(range1.from_time) - $(range1.to_time) vs $(range2.from_time) - $(range2.to_time)\n");
|
||||||
|
if (range1 == range2 || to_delete.contains(range2)) continue;
|
||||||
|
|
||||||
|
// Check if range2 is a subset of range1
|
||||||
|
// range1: #####################
|
||||||
|
// range2: ######
|
||||||
|
if (range1.from_time <= range2.from_time && range1.to_time >= range2.to_time) {
|
||||||
|
critical(@"MAM: Removing db range which is a subset of another one");
|
||||||
|
to_delete.add(range2);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if range2 is an extension of range1 (towards earlier)
|
||||||
|
// range1: #####################
|
||||||
|
// range2: ###############
|
||||||
|
if (range1.from_time <= range2.from_time <= range1.to_time && range1.to_time < range2.to_time) {
|
||||||
|
critical(@"MAM: Removing db range that overlapped another one (towards earlier)");
|
||||||
|
db.mam_catchup.update()
|
||||||
|
.with(db.mam_catchup.id, "=", range1.id)
|
||||||
|
.set(db.mam_catchup.from_id, range2.to_id)
|
||||||
|
.set(db.mam_catchup.from_time, range2.to_time)
|
||||||
|
.set(db.mam_catchup.from_end, range2.from_end)
|
||||||
|
.perform();
|
||||||
|
to_delete.add(range2);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if range2 is an extension of range1 (towards more current)
|
||||||
|
// range1: #####################
|
||||||
|
// range2: ###############
|
||||||
|
if (range1.from_time <= range2.from_time <= range1.to_time && range1.to_time < range2.to_time) {
|
||||||
|
critical(@"MAM: Removing db range that overlapped another one (towards more current)");
|
||||||
|
db.mam_catchup.update()
|
||||||
|
.with(db.mam_catchup.id, "=", range1.id)
|
||||||
|
.set(db.mam_catchup.to_id, range2.to_id)
|
||||||
|
.set(db.mam_catchup.to_time, range2.to_time)
|
||||||
|
.perform();
|
||||||
|
to_delete.add(range2);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (MamRange row in to_delete) {
|
||||||
|
db.mam_catchup.delete().with(db.mam_catchup.id, "=", row.id).perform();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MamRange {
|
||||||
|
public int id;
|
||||||
|
public Jid server_jid;
|
||||||
|
public long from_time;
|
||||||
|
public string from_id;
|
||||||
|
public bool from_end;
|
||||||
|
public long to_time;
|
||||||
|
public string to_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PageRequestResult {
|
||||||
|
public Gee.List<MessageStanza> stanzas { get; set; }
|
||||||
|
public PageResult page_result { get; set; }
|
||||||
|
public Xmpp.MessageArchiveManagement.QueryResult query_result { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,15 +18,11 @@ public class MessageProcessor : StreamInteractionModule, Object {
|
||||||
public signal void message_sent_or_received(Entities.Message message, Conversation conversation);
|
public signal void message_sent_or_received(Entities.Message message, Conversation conversation);
|
||||||
public signal void history_synced(Account account);
|
public signal void history_synced(Account account);
|
||||||
|
|
||||||
|
public HistorySync history_sync;
|
||||||
public MessageListenerHolder received_pipeline = new MessageListenerHolder();
|
public MessageListenerHolder received_pipeline = new MessageListenerHolder();
|
||||||
|
|
||||||
private StreamInteractor stream_interactor;
|
private StreamInteractor stream_interactor;
|
||||||
private Database db;
|
private Database db;
|
||||||
private HashMap<Account, int> current_catchup_id = new HashMap<Account, int>(Account.hash_func, Account.equals_func);
|
|
||||||
private HashMap<Account, HashMap<string, DateTime>> mam_times = new HashMap<Account, HashMap<string, DateTime>>();
|
|
||||||
public HashMap<string, int> hitted_range = new HashMap<string, int>();
|
|
||||||
public HashMap<Account, string> catchup_until_id = new HashMap<Account, string>(Account.hash_func, Account.equals_func);
|
|
||||||
public HashMap<Account, DateTime> catchup_until_time = new HashMap<Account, DateTime>(Account.hash_func, Account.equals_func);
|
|
||||||
|
|
||||||
public static void start(StreamInteractor stream_interactor, Database db) {
|
public static void start(StreamInteractor stream_interactor, Database db) {
|
||||||
MessageProcessor m = new MessageProcessor(stream_interactor, db);
|
MessageProcessor m = new MessageProcessor(stream_interactor, db);
|
||||||
|
@ -36,6 +32,7 @@ public class MessageProcessor : StreamInteractionModule, Object {
|
||||||
private MessageProcessor(StreamInteractor stream_interactor, Database db) {
|
private MessageProcessor(StreamInteractor stream_interactor, Database db) {
|
||||||
this.stream_interactor = stream_interactor;
|
this.stream_interactor = stream_interactor;
|
||||||
this.db = db;
|
this.db = db;
|
||||||
|
this.history_sync = new HistorySync(db, stream_interactor);
|
||||||
|
|
||||||
received_pipeline.connect(new DeduplicateMessageListener(this, db));
|
received_pipeline.connect(new DeduplicateMessageListener(this, db));
|
||||||
received_pipeline.connect(new FilterMessageListener());
|
received_pipeline.connect(new FilterMessageListener());
|
||||||
|
@ -47,11 +44,6 @@ public class MessageProcessor : StreamInteractionModule, Object {
|
||||||
|
|
||||||
stream_interactor.stream_negotiated.connect(send_unsent_chat_messages);
|
stream_interactor.stream_negotiated.connect(send_unsent_chat_messages);
|
||||||
stream_interactor.stream_resumed.connect(send_unsent_chat_messages);
|
stream_interactor.stream_resumed.connect(send_unsent_chat_messages);
|
||||||
|
|
||||||
stream_interactor.connection_manager.stream_opened.connect((account, stream) => {
|
|
||||||
debug("MAM: [%s] Reset catchup_id", account.bare_jid.to_string());
|
|
||||||
current_catchup_id.unset(account);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Entities.Message send_text(string text, Conversation conversation) {
|
public Entities.Message send_text(string text, Conversation conversation) {
|
||||||
|
@ -106,43 +98,10 @@ public class MessageProcessor : StreamInteractionModule, Object {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void on_account_added(Account account) {
|
private void on_account_added(Account account) {
|
||||||
mam_times[account] = new HashMap<string, DateTime>();
|
|
||||||
|
|
||||||
stream_interactor.module_manager.get_module(account, Xmpp.MessageModule.IDENTITY).received_message.connect( (stream, message) => {
|
stream_interactor.module_manager.get_module(account, Xmpp.MessageModule.IDENTITY).received_message.connect( (stream, message) => {
|
||||||
on_message_received.begin(account, message);
|
on_message_received.begin(account, message);
|
||||||
});
|
});
|
||||||
XmppStream? stream_bak = null;
|
|
||||||
stream_interactor.module_manager.get_module(account, Xmpp.Xep.MessageArchiveManagement.Module.IDENTITY).feature_available.connect( (stream) => {
|
|
||||||
if (stream == stream_bak) return;
|
|
||||||
|
|
||||||
current_catchup_id.unset(account);
|
|
||||||
stream_bak = stream;
|
|
||||||
debug("MAM: [%s] MAM available", account.bare_jid.to_string());
|
|
||||||
do_mam_catchup.begin(account);
|
|
||||||
});
|
|
||||||
|
|
||||||
stream_interactor.module_manager.get_module(account, Xmpp.MessageModule.IDENTITY).received_message_unprocessed.connect((stream, message) => {
|
|
||||||
if (!message.from.equals(account.bare_jid)) return;
|
|
||||||
|
|
||||||
Xep.MessageArchiveManagement.Flag? mam_flag = stream != null ? stream.get_flag(Xep.MessageArchiveManagement.Flag.IDENTITY) : null;
|
|
||||||
if (mam_flag == null) return;
|
|
||||||
string? id = message.stanza.get_deep_attribute(mam_flag.ns_ver + ":result", "id");
|
|
||||||
if (id == null) return;
|
|
||||||
StanzaNode? delay_node = message.stanza.get_deep_subnode(mam_flag.ns_ver + ":result", "urn:xmpp:forward:0:forwarded", "urn:xmpp:delay:delay");
|
|
||||||
if (delay_node == null) {
|
|
||||||
warning("MAM result did not contain delayed time %s", message.stanza.to_string());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
DateTime? time = DelayedDelivery.get_time_for_node(delay_node);
|
|
||||||
if (time == null) return;
|
|
||||||
mam_times[account][id] = time;
|
|
||||||
|
|
||||||
string? query_id = message.stanza.get_deep_attribute(mam_flag.ns_ver + ":result", mam_flag.ns_ver + ":queryid");
|
|
||||||
if (query_id != null && id == catchup_until_id[account]) {
|
|
||||||
debug("MAM: [%s] Hitted range (id) %s", account.bare_jid.to_string(), id);
|
|
||||||
hitted_range[query_id] = -2;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
stream_interactor.module_manager.get_module(account, Xmpp.MessageModule.IDENTITY).received_error.connect((stream, message_stanza, error_stanza) => {
|
stream_interactor.module_manager.get_module(account, Xmpp.MessageModule.IDENTITY).received_error.connect((stream, message_stanza, error_stanza) => {
|
||||||
Message? message = null;
|
Message? message = null;
|
||||||
|
|
||||||
|
@ -164,203 +123,20 @@ public class MessageProcessor : StreamInteractionModule, Object {
|
||||||
convert_sending_to_unsent_msgs(account);
|
convert_sending_to_unsent_msgs(account);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void do_mam_catchup(Account account) {
|
|
||||||
debug("MAM: [%s] Start catchup", account.bare_jid.to_string());
|
|
||||||
string? earliest_id = null;
|
|
||||||
DateTime? earliest_time = null;
|
|
||||||
bool continue_sync = true;
|
|
||||||
|
|
||||||
while (continue_sync) {
|
|
||||||
continue_sync = false;
|
|
||||||
|
|
||||||
// Get previous row
|
|
||||||
var previous_qry = db.mam_catchup.select().with(db.mam_catchup.account_id, "=", account.id).order_by(db.mam_catchup.to_time, "DESC");
|
|
||||||
if (current_catchup_id.has_key(account)) {
|
|
||||||
previous_qry.with(db.mam_catchup.id, "!=", current_catchup_id[account]);
|
|
||||||
}
|
|
||||||
RowOption previous_row = previous_qry.single().row();
|
|
||||||
if (previous_row.is_present()) {
|
|
||||||
catchup_until_id[account] = previous_row[db.mam_catchup.to_id];
|
|
||||||
catchup_until_time[account] = (new DateTime.from_unix_utc(previous_row[db.mam_catchup.to_time])).add_minutes(-5);
|
|
||||||
debug("MAM: [%s] Previous entry exists", account.bare_jid.to_string());
|
|
||||||
} else {
|
|
||||||
catchup_until_id.unset(account);
|
|
||||||
catchup_until_time.unset(account);
|
|
||||||
}
|
|
||||||
|
|
||||||
string query_id = Xmpp.random_uuid();
|
|
||||||
yield get_mam_range(account, query_id, null, null, earliest_time, earliest_id);
|
|
||||||
|
|
||||||
if (!hitted_range.has_key(query_id)) {
|
|
||||||
debug("MAM: [%s] Set catchup end reached", account.bare_jid.to_string());
|
|
||||||
db.mam_catchup.update()
|
|
||||||
.set(db.mam_catchup.from_end, true)
|
|
||||||
.with(db.mam_catchup.id, "=", current_catchup_id[account])
|
|
||||||
.perform();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hitted_range.has_key(query_id)) {
|
|
||||||
if (merge_ranges(account, null)) {
|
|
||||||
RowOption current_row = db.mam_catchup.row_with(db.mam_catchup.id, current_catchup_id[account]);
|
|
||||||
bool range_from_complete = current_row[db.mam_catchup.from_end];
|
|
||||||
if (!range_from_complete) {
|
|
||||||
continue_sync = true;
|
|
||||||
earliest_id = current_row[db.mam_catchup.from_id];
|
|
||||||
earliest_time = (new DateTime.from_unix_utc(current_row[db.mam_catchup.from_time])).add_seconds(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Merges the row with `current_catchup_id` with the previous range (optional: with `earlier_id`)
|
|
||||||
* Changes `current_catchup_id` to the previous range
|
|
||||||
*/
|
|
||||||
private bool merge_ranges(Account account, int? earlier_id) {
|
|
||||||
RowOption current_row = db.mam_catchup.row_with(db.mam_catchup.id, current_catchup_id[account]);
|
|
||||||
RowOption previous_row = null;
|
|
||||||
|
|
||||||
if (earlier_id != null) {
|
|
||||||
previous_row = db.mam_catchup.row_with(db.mam_catchup.id, earlier_id);
|
|
||||||
} else {
|
|
||||||
previous_row = db.mam_catchup.select()
|
|
||||||
.with(db.mam_catchup.account_id, "=", account.id)
|
|
||||||
.with(db.mam_catchup.id, "!=", current_catchup_id[account])
|
|
||||||
.order_by(db.mam_catchup.to_time, "DESC").single().row();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!previous_row.is_present()) {
|
|
||||||
debug("MAM: [%s] Merging: No previous row", account.bare_jid.to_string());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var qry = db.mam_catchup.update().with(db.mam_catchup.id, "=", previous_row[db.mam_catchup.id]);
|
|
||||||
debug("MAM: [%s] Merging %ld-%ld with %ld- %ld", account.bare_jid.to_string(), previous_row[db.mam_catchup.from_time], previous_row[db.mam_catchup.to_time], current_row[db.mam_catchup.from_time], current_row[db.mam_catchup.to_time]);
|
|
||||||
if (current_row[db.mam_catchup.from_time] < previous_row[db.mam_catchup.from_time]) {
|
|
||||||
qry.set(db.mam_catchup.from_id, current_row[db.mam_catchup.from_id])
|
|
||||||
.set(db.mam_catchup.from_time, current_row[db.mam_catchup.from_time]);
|
|
||||||
}
|
|
||||||
if (current_row[db.mam_catchup.to_time] > previous_row[db.mam_catchup.to_time]) {
|
|
||||||
qry.set(db.mam_catchup.to_id, current_row[db.mam_catchup.to_id])
|
|
||||||
.set(db.mam_catchup.to_time, current_row[db.mam_catchup.to_time]);
|
|
||||||
}
|
|
||||||
qry.perform();
|
|
||||||
|
|
||||||
current_catchup_id[account] = previous_row[db.mam_catchup.id];
|
|
||||||
|
|
||||||
db.mam_catchup.delete().with(db.mam_catchup.id, "=", current_row[db.mam_catchup.id]).perform();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async bool get_mam_range(Account account, string? query_id, DateTime? from_time, string? from_id, DateTime? to_time, string? to_id) {
|
|
||||||
debug("MAM: [%s] Get range %s - %s", account.bare_jid.to_string(), from_time != null ? from_time.to_string() : "", to_time != null ? to_time.to_string() : "");
|
|
||||||
XmppStream stream = stream_interactor.get_stream(account);
|
|
||||||
|
|
||||||
Iq.Stanza? iq = yield stream.get_module(Xep.MessageArchiveManagement.Module.IDENTITY).query_archive(stream, null, query_id, from_time, from_id, to_time, to_id);
|
|
||||||
|
|
||||||
if (iq == null) {
|
|
||||||
debug(@"MAM: [%s] IQ null", account.bare_jid.to_string());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (iq.stanza.get_deep_string_content("urn:xmpp:mam:2:fin", "http://jabber.org/protocol/rsm" + ":set", "first") == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (iq != null) {
|
|
||||||
string? earliest_id = iq.stanza.get_deep_string_content("urn:xmpp:mam:2:fin", "http://jabber.org/protocol/rsm" + ":set", "first");
|
|
||||||
if (earliest_id == null) return true;
|
|
||||||
string? latest_id = iq.stanza.get_deep_string_content("urn:xmpp:mam:2:fin", "http://jabber.org/protocol/rsm" + ":set", "last");
|
|
||||||
|
|
||||||
// We wait until all the messages from the page are processed (and we got the `mam_times` from them)
|
|
||||||
Idle.add(get_mam_range.callback, Priority.LOW);
|
|
||||||
yield;
|
|
||||||
|
|
||||||
int wait_ms = 1000;
|
|
||||||
|
|
||||||
|
|
||||||
if (mam_times[account].has_key(earliest_id) && (current_catchup_id.has_key(account) || mam_times[account].has_key(latest_id))) {
|
|
||||||
|
|
||||||
debug("MAM: [%s] Update from_id %s", account.bare_jid.to_string(), earliest_id);
|
|
||||||
if (!current_catchup_id.has_key(account)) {
|
|
||||||
debug("MAM: [%s] We get our first MAM page", account.bare_jid.to_string());
|
|
||||||
current_catchup_id[account] = (int) db.mam_catchup.insert()
|
|
||||||
.value(db.mam_catchup.account_id, account.id)
|
|
||||||
.value(db.mam_catchup.from_id, earliest_id)
|
|
||||||
.value(db.mam_catchup.from_time, (long)mam_times[account][earliest_id].to_unix())
|
|
||||||
.value(db.mam_catchup.to_id, latest_id)
|
|
||||||
.value(db.mam_catchup.to_time, (long)mam_times[account][latest_id].to_unix())
|
|
||||||
.perform();
|
|
||||||
} else {
|
|
||||||
// Update existing id
|
|
||||||
db.mam_catchup.update()
|
|
||||||
.set(db.mam_catchup.from_id, earliest_id)
|
|
||||||
.set(db.mam_catchup.from_time, (long)mam_times[account][earliest_id].to_unix())
|
|
||||||
.with(db.mam_catchup.id, "=", current_catchup_id[account])
|
|
||||||
.perform();
|
|
||||||
}
|
|
||||||
|
|
||||||
TimeSpan catchup_time_ago = (new DateTime.now_utc()).difference(mam_times[account][earliest_id]);
|
|
||||||
|
|
||||||
if (catchup_time_ago > 14 * TimeSpan.DAY) {
|
|
||||||
wait_ms = 2000;
|
|
||||||
} else if (catchup_time_ago > 5 * TimeSpan.DAY) {
|
|
||||||
wait_ms = 1000;
|
|
||||||
} else if (catchup_time_ago > 2 * TimeSpan.DAY) {
|
|
||||||
wait_ms = 200;
|
|
||||||
} else if (catchup_time_ago > TimeSpan.DAY) {
|
|
||||||
wait_ms = 50;
|
|
||||||
} else {
|
|
||||||
wait_ms = 10;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
warning("Didn't have time for MAM id; earliest_id:%s latest_id:%s", mam_times[account].has_key(earliest_id).to_string(), mam_times[account].has_key(latest_id).to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
mam_times[account] = new HashMap<string, DateTime>();
|
|
||||||
|
|
||||||
Timeout.add(wait_ms, () => {
|
|
||||||
if (hitted_range.has_key(query_id)) {
|
|
||||||
debug(@"MAM: [%s] Hitted contains key %s", account.bare_jid.to_string(), query_id);
|
|
||||||
iq = null;
|
|
||||||
Idle.add(get_mam_range.callback);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
stream.get_module(Xep.MessageArchiveManagement.Module.IDENTITY).page_through_results.begin(stream, null, query_id, from_time, to_time, iq, (_, res) => {
|
|
||||||
iq = stream.get_module(Xep.MessageArchiveManagement.Module.IDENTITY).page_through_results.end(res);
|
|
||||||
Idle.add(get_mam_range.callback);
|
|
||||||
});
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
yield;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void on_message_received(Account account, Xmpp.MessageStanza message_stanza) {
|
private async void on_message_received(Account account, Xmpp.MessageStanza message_stanza) {
|
||||||
|
|
||||||
|
// If it's a message from MAM, it's going to be processed by HistorySync which calls run_pipeline_announce later.
|
||||||
|
if (history_sync.process(account, message_stanza)) return;
|
||||||
|
|
||||||
|
run_pipeline_announce(account, message_stanza);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void run_pipeline_announce(Account account, Xmpp.MessageStanza message_stanza) {
|
||||||
Entities.Message message = yield parse_message_stanza(account, message_stanza);
|
Entities.Message message = yield parse_message_stanza(account, message_stanza);
|
||||||
|
|
||||||
Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation_for_message(message);
|
Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation_for_message(message);
|
||||||
if (conversation == null) return;
|
if (conversation == null) return;
|
||||||
|
|
||||||
// MAM state database update
|
|
||||||
Xep.MessageArchiveManagement.MessageFlag? mam_flag = Xep.MessageArchiveManagement.MessageFlag.get_flag(message_stanza);
|
|
||||||
if (mam_flag == null) {
|
|
||||||
if (current_catchup_id.has_key(account)) {
|
|
||||||
string? stanza_id = UniqueStableStanzaIDs.get_stanza_id(message_stanza, account.bare_jid);
|
|
||||||
if (stanza_id != null) {
|
|
||||||
db.mam_catchup.update()
|
|
||||||
.with(db.mam_catchup.id, "=", current_catchup_id[account])
|
|
||||||
.set(db.mam_catchup.to_time, (long)message.local_time.to_unix())
|
|
||||||
.set(db.mam_catchup.to_id, stanza_id)
|
|
||||||
.perform();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool abort = yield received_pipeline.run(message, message_stanza, conversation);
|
bool abort = yield received_pipeline.run(message, message_stanza, conversation);
|
||||||
if (abort) return;
|
if (abort) return;
|
||||||
|
|
||||||
|
@ -373,7 +149,7 @@ public class MessageProcessor : StreamInteractionModule, Object {
|
||||||
message_sent_or_received(message, conversation);
|
message_sent_or_received(message, conversation);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Entities.Message parse_message_stanza(Account account, Xmpp.MessageStanza message) {
|
public async Entities.Message parse_message_stanza(Account account, Xmpp.MessageStanza message) {
|
||||||
string? body = message.body;
|
string? body = message.body;
|
||||||
if (body != null) body = body.strip();
|
if (body != null) body = body.strip();
|
||||||
Entities.Message new_message = new Entities.Message(body);
|
Entities.Message new_message = new Entities.Message(body);
|
||||||
|
@ -393,20 +169,20 @@ public class MessageProcessor : StreamInteractionModule, Object {
|
||||||
new_message.ourpart = new_message.direction == Entities.Message.DIRECTION_SENT ? message.from : message.to;
|
new_message.ourpart = new_message.direction == Entities.Message.DIRECTION_SENT ? message.from : message.to;
|
||||||
|
|
||||||
XmppStream? stream = stream_interactor.get_stream(account);
|
XmppStream? stream = stream_interactor.get_stream(account);
|
||||||
Xep.MessageArchiveManagement.MessageFlag? mam_message_flag = Xep.MessageArchiveManagement.MessageFlag.get_flag(message);
|
Xmpp.MessageArchiveManagement.MessageFlag? mam_message_flag = Xmpp.MessageArchiveManagement.MessageFlag.get_flag(message);
|
||||||
Xep.MessageArchiveManagement.Flag? mam_flag = stream != null ? stream.get_flag(Xep.MessageArchiveManagement.Flag.IDENTITY) : null;
|
Xmpp.MessageArchiveManagement.Flag? mam_flag = stream != null ? stream.get_flag(Xmpp.MessageArchiveManagement.Flag.IDENTITY) : null;
|
||||||
EntityInfo entity_info = stream_interactor.get_module(EntityInfo.IDENTITY);
|
EntityInfo entity_info = stream_interactor.get_module(EntityInfo.IDENTITY);
|
||||||
if (mam_message_flag != null && mam_flag != null && mam_flag.ns_ver == Xep.MessageArchiveManagement.NS_URI_2 && mam_message_flag.mam_id != null) {
|
if (mam_message_flag != null && mam_flag != null && mam_flag.ns_ver == Xmpp.MessageArchiveManagement.NS_URI_2 && mam_message_flag.mam_id != null) {
|
||||||
new_message.server_id = mam_message_flag.mam_id;
|
new_message.server_id = mam_message_flag.mam_id;
|
||||||
} else if (message.type_ == Xmpp.MessageStanza.TYPE_GROUPCHAT) {
|
} else if (message.type_ == Xmpp.MessageStanza.TYPE_GROUPCHAT) {
|
||||||
bool server_supports_sid = (yield entity_info.has_feature(account, new_message.counterpart.bare_jid, Xep.UniqueStableStanzaIDs.NS_URI)) ||
|
bool server_supports_sid = (yield entity_info.has_feature(account, new_message.counterpart.bare_jid, Xep.UniqueStableStanzaIDs.NS_URI)) ||
|
||||||
(yield entity_info.has_feature(account, new_message.counterpart.bare_jid, Xep.MessageArchiveManagement.NS_URI_2));
|
(yield entity_info.has_feature(account, new_message.counterpart.bare_jid, Xmpp.MessageArchiveManagement.NS_URI_2));
|
||||||
if (server_supports_sid) {
|
if (server_supports_sid) {
|
||||||
new_message.server_id = Xep.UniqueStableStanzaIDs.get_stanza_id(message, new_message.counterpart.bare_jid);
|
new_message.server_id = Xep.UniqueStableStanzaIDs.get_stanza_id(message, new_message.counterpart.bare_jid);
|
||||||
}
|
}
|
||||||
} else if (message.type_ == Xmpp.MessageStanza.TYPE_CHAT) {
|
} else if (message.type_ == Xmpp.MessageStanza.TYPE_CHAT) {
|
||||||
bool server_supports_sid = (yield entity_info.has_feature(account, account.bare_jid, Xep.UniqueStableStanzaIDs.NS_URI)) ||
|
bool server_supports_sid = (yield entity_info.has_feature(account, account.bare_jid, Xep.UniqueStableStanzaIDs.NS_URI)) ||
|
||||||
(yield entity_info.has_feature(account, account.bare_jid, Xep.MessageArchiveManagement.NS_URI_2));
|
(yield entity_info.has_feature(account, account.bare_jid, Xmpp.MessageArchiveManagement.NS_URI_2));
|
||||||
if (server_supports_sid) {
|
if (server_supports_sid) {
|
||||||
new_message.server_id = Xep.UniqueStableStanzaIDs.get_stanza_id(message, account.bare_jid);
|
new_message.server_id = Xep.UniqueStableStanzaIDs.get_stanza_id(message, account.bare_jid);
|
||||||
}
|
}
|
||||||
|
@ -474,7 +250,6 @@ public class MessageProcessor : StreamInteractionModule, Object {
|
||||||
public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
|
public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
|
||||||
Account account = conversation.account;
|
Account account = conversation.account;
|
||||||
|
|
||||||
Xep.MessageArchiveManagement.MessageFlag? mam_flag = Xep.MessageArchiveManagement.MessageFlag.get_flag(stanza);
|
|
||||||
|
|
||||||
// Deduplicate by server_id
|
// Deduplicate by server_id
|
||||||
if (message.server_id != null) {
|
if (message.server_id != null) {
|
||||||
|
@ -482,16 +257,12 @@ public class MessageProcessor : StreamInteractionModule, Object {
|
||||||
.with(db.message.server_id, "=", message.server_id)
|
.with(db.message.server_id, "=", message.server_id)
|
||||||
.with(db.message.counterpart_id, "=", db.get_jid_id(message.counterpart))
|
.with(db.message.counterpart_id, "=", db.get_jid_id(message.counterpart))
|
||||||
.with(db.message.account_id, "=", account.id);
|
.with(db.message.account_id, "=", account.id);
|
||||||
bool duplicate = builder.count() > 0;
|
|
||||||
|
|
||||||
if (duplicate && mam_flag != null) {
|
// If the message is a duplicate
|
||||||
debug(@"MAM: [%s] Hitted range duplicate server id. id %s qid %s", account.bare_jid.to_string(), message.server_id, mam_flag.query_id);
|
if (builder.count() > 0) {
|
||||||
if (outer.catchup_until_time.has_key(account) && mam_flag.server_time.compare(outer.catchup_until_time[account]) < 0) {
|
outer.history_sync.on_server_id_duplicate(account, stanza, message);
|
||||||
outer.hitted_range[mam_flag.query_id] = -1;
|
return true;
|
||||||
debug(@"MAM: [%s] In range (time) %s < %s", account.bare_jid.to_string(), mam_flag.server_time.to_string(), outer.catchup_until_time[account].to_string());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (duplicate) return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deduplicate messages by uuid
|
// Deduplicate messages by uuid
|
||||||
|
@ -514,14 +285,7 @@ public class MessageProcessor : StreamInteractionModule, Object {
|
||||||
builder.with_null(db.message.our_resource);
|
builder.with_null(db.message.our_resource);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RowOption row_opt = builder.single().row();
|
bool duplicate = builder.single().row().is_present();
|
||||||
bool duplicate = row_opt.is_present();
|
|
||||||
|
|
||||||
if (duplicate && mam_flag != null && row_opt[db.message.server_id] == null &&
|
|
||||||
outer.catchup_until_time.has_key(account) && mam_flag.server_time.compare(outer.catchup_until_time[account]) > 0) {
|
|
||||||
outer.hitted_range[mam_flag.query_id] = -1;
|
|
||||||
debug(@"MAM: [%s] Hitted range duplicate message id. id %s qid %s", account.bare_jid.to_string(), message.stanza_id, mam_flag.query_id);
|
|
||||||
}
|
|
||||||
return duplicate;
|
return duplicate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -608,9 +372,9 @@ public class MessageProcessor : StreamInteractionModule, Object {
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
|
public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
|
||||||
bool is_mam_message = Xep.MessageArchiveManagement.MessageFlag.get_flag(stanza) != null;
|
bool is_mam_message = Xmpp.MessageArchiveManagement.MessageFlag.get_flag(stanza) != null;
|
||||||
XmppStream? stream = stream_interactor.get_stream(conversation.account);
|
XmppStream? stream = stream_interactor.get_stream(conversation.account);
|
||||||
Xep.MessageArchiveManagement.Flag? mam_flag = stream != null ? stream.get_flag(Xep.MessageArchiveManagement.Flag.IDENTITY) : null;
|
Xmpp.MessageArchiveManagement.Flag? mam_flag = stream != null ? stream.get_flag(Xmpp.MessageArchiveManagement.Flag.IDENTITY) : null;
|
||||||
if (is_mam_message || (mam_flag != null && mam_flag.cought_up == true)) {
|
if (is_mam_message || (mam_flag != null && mam_flag.cought_up == true)) {
|
||||||
conversation.account.mam_earliest_synced = message.local_time;
|
conversation.account.mam_earliest_synced = message.local_time;
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,7 +57,7 @@ public class ModuleManager {
|
||||||
module_map[account].add(new Xep.Bookmarks2.Module());
|
module_map[account].add(new Xep.Bookmarks2.Module());
|
||||||
module_map[account].add(new Presence.Module());
|
module_map[account].add(new Presence.Module());
|
||||||
module_map[account].add(new Xmpp.MessageModule());
|
module_map[account].add(new Xmpp.MessageModule());
|
||||||
module_map[account].add(new Xep.MessageArchiveManagement.Module());
|
module_map[account].add(new Xmpp.MessageArchiveManagement.Module());
|
||||||
module_map[account].add(new Xep.MessageCarbons.Module());
|
module_map[account].add(new Xep.MessageCarbons.Module());
|
||||||
module_map[account].add(new Xep.Muc.Module());
|
module_map[account].add(new Xep.Muc.Module());
|
||||||
module_map[account].add(new Xep.Pubsub.Module());
|
module_map[account].add(new Xep.Pubsub.Module());
|
||||||
|
@ -70,6 +70,7 @@ public class ModuleManager {
|
||||||
module_map[account].add(new StreamError.Module());
|
module_map[account].add(new StreamError.Module());
|
||||||
module_map[account].add(new Xep.InBandRegistration.Module());
|
module_map[account].add(new Xep.InBandRegistration.Module());
|
||||||
module_map[account].add(new Xep.HttpFileUpload.Module());
|
module_map[account].add(new Xep.HttpFileUpload.Module());
|
||||||
|
module_map[account].add(new Xep.Reactions.Module());
|
||||||
module_map[account].add(new Xep.Socks5Bytestreams.Module());
|
module_map[account].add(new Xep.Socks5Bytestreams.Module());
|
||||||
module_map[account].add(new Xep.InBandBytestreams.Module());
|
module_map[account].add(new Xep.InBandBytestreams.Module());
|
||||||
module_map[account].add(new Xep.Jingle.Module());
|
module_map[account].add(new Xep.Jingle.Module());
|
||||||
|
@ -80,6 +81,7 @@ public class ModuleManager {
|
||||||
module_map[account].add(new Xep.LastMessageCorrection.Module());
|
module_map[account].add(new Xep.LastMessageCorrection.Module());
|
||||||
module_map[account].add(new Xep.DirectMucInvitations.Module());
|
module_map[account].add(new Xep.DirectMucInvitations.Module());
|
||||||
module_map[account].add(new Xep.JingleMessageInitiation.Module());
|
module_map[account].add(new Xep.JingleMessageInitiation.Module());
|
||||||
|
module_map[account].add(new Xep.OccupantIds.Module());
|
||||||
module_map[account].add(new Xep.JingleRawUdp.Module());
|
module_map[account].add(new Xep.JingleRawUdp.Module());
|
||||||
module_map[account].add(new Xep.Muji.Module());
|
module_map[account].add(new Xep.Muji.Module());
|
||||||
module_map[account].add(new Xep.CallInvites.Module());
|
module_map[account].add(new Xep.CallInvites.Module());
|
||||||
|
|
|
@ -28,6 +28,7 @@ public class MucManager : StreamInteractionModule, Object {
|
||||||
private HashMap<Account, BookmarksProvider> bookmarks_provider = new HashMap<Account, BookmarksProvider>(Account.hash_func, Account.equals_func);
|
private HashMap<Account, BookmarksProvider> bookmarks_provider = new HashMap<Account, BookmarksProvider>(Account.hash_func, Account.equals_func);
|
||||||
private HashMap<Account, Gee.List<Jid>> invites = new HashMap<Account, Gee.List<Jid>>(Account.hash_func, Account.equals_func);
|
private HashMap<Account, Gee.List<Jid>> invites = new HashMap<Account, Gee.List<Jid>>(Account.hash_func, Account.equals_func);
|
||||||
public HashMap<Account, Jid> default_muc_server = new HashMap<Account, Jid>(Account.hash_func, Account.equals_func);
|
public HashMap<Account, Jid> default_muc_server = new HashMap<Account, Jid>(Account.hash_func, Account.equals_func);
|
||||||
|
private HashMap<Account, HashMap<Jid, string>> own_occupant_ids = new HashMap<Account, HashMap<Jid, string>>(Account.hash_func, Account.equals_func);
|
||||||
|
|
||||||
public static void start(StreamInteractor stream_interactor) {
|
public static void start(StreamInteractor stream_interactor) {
|
||||||
MucManager m = new MucManager(stream_interactor);
|
MucManager m = new MucManager(stream_interactor);
|
||||||
|
@ -68,6 +69,15 @@ public class MucManager : StreamInteractionModule, Object {
|
||||||
if (last_message != null) history_since = last_message.time;
|
if (last_message != null) history_since = last_message.time;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool receive_history = true;
|
||||||
|
EntityInfo entity_info = stream_interactor.get_module(EntityInfo.IDENTITY);
|
||||||
|
bool can_do_mam = yield entity_info.has_feature(account, jid, Xmpp.MessageArchiveManagement.NS_URI_2);
|
||||||
|
print(@"$(jid) $can_do_mam\n");
|
||||||
|
if (can_do_mam) {
|
||||||
|
receive_history = false;
|
||||||
|
history_since = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!mucs_joining.has_key(account)) {
|
if (!mucs_joining.has_key(account)) {
|
||||||
mucs_joining[account] = new HashSet<Jid>(Jid.hash_bare_func, Jid.equals_bare_func);
|
mucs_joining[account] = new HashSet<Jid>(Jid.hash_bare_func, Jid.equals_bare_func);
|
||||||
}
|
}
|
||||||
|
@ -78,7 +88,7 @@ public class MucManager : StreamInteractionModule, Object {
|
||||||
}
|
}
|
||||||
mucs_todo[account].add(jid.with_resource(nick_));
|
mucs_todo[account].add(jid.with_resource(nick_));
|
||||||
|
|
||||||
Muc.JoinResult? res = yield stream.get_module(Xep.Muc.Module.IDENTITY).enter(stream, jid.bare_jid, nick_, password, history_since, null);
|
Muc.JoinResult? res = yield stream.get_module(Xep.Muc.Module.IDENTITY).enter(stream, jid.bare_jid, nick_, password, history_since, receive_history, null);
|
||||||
|
|
||||||
mucs_joining[account].remove(jid);
|
mucs_joining[account].remove(jid);
|
||||||
|
|
||||||
|
@ -91,6 +101,18 @@ public class MucManager : StreamInteractionModule, Object {
|
||||||
Conversation joined_conversation = stream_interactor.get_module(ConversationManager.IDENTITY).create_conversation(jid, account, Conversation.Type.GROUPCHAT);
|
Conversation joined_conversation = stream_interactor.get_module(ConversationManager.IDENTITY).create_conversation(jid, account, Conversation.Type.GROUPCHAT);
|
||||||
joined_conversation.nickname = nick;
|
joined_conversation.nickname = nick;
|
||||||
stream_interactor.get_module(ConversationManager.IDENTITY).start_conversation(joined_conversation);
|
stream_interactor.get_module(ConversationManager.IDENTITY).start_conversation(joined_conversation);
|
||||||
|
|
||||||
|
if (can_do_mam) {
|
||||||
|
if (conversation == null) {
|
||||||
|
// We never joined the conversation before, just fetch the latest MAM page
|
||||||
|
yield stream_interactor.get_module(MessageProcessor.IDENTITY).history_sync
|
||||||
|
.fetch_latest_page(account, jid.bare_jid, null, new DateTime.from_unix_utc(0));
|
||||||
|
} else {
|
||||||
|
// Fetch everything up to the last time the user actively joined
|
||||||
|
stream_interactor.get_module(MessageProcessor.IDENTITY).history_sync
|
||||||
|
.fetch_everything.begin(account, jid.bare_jid, conversation.active_last_changed);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (res.muc_error != null) {
|
} else if (res.muc_error != null) {
|
||||||
// Join failed
|
// Join failed
|
||||||
enter_errors[jid] = res.muc_error;
|
enter_errors[jid] = res.muc_error;
|
||||||
|
@ -365,6 +387,13 @@ public class MucManager : StreamInteractionModule, Object {
|
||||||
return get_own_jid(jid, account) != null;
|
return get_own_jid(jid, account) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string? get_own_occupant_id(Account account, Jid muc_jid) {
|
||||||
|
if (account in own_occupant_ids && muc_jid in own_occupant_ids[account]) {
|
||||||
|
return own_occupant_ids[account][muc_jid];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private void on_account_added(Account account) {
|
private void on_account_added(Account account) {
|
||||||
stream_interactor.module_manager.get_module(account, Xep.Muc.Module.IDENTITY).self_removed_from_room.connect( (stream, jid, code) => {
|
stream_interactor.module_manager.get_module(account, Xep.Muc.Module.IDENTITY).self_removed_from_room.connect( (stream, jid, code) => {
|
||||||
left(account, jid);
|
left(account, jid);
|
||||||
|
@ -392,6 +421,12 @@ public class MucManager : StreamInteractionModule, Object {
|
||||||
private_room_occupant_updated(account, room, occupant);
|
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 (!(account in own_occupant_ids)) {
|
||||||
|
own_occupant_ids[account] = new HashMap<Jid, string>(Jid.hash_bare_func, Jid.equals_bare_func);
|
||||||
|
}
|
||||||
|
own_occupant_ids[account][jid] = occupant_id;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void search_default_muc_server(Account account) {
|
private async void search_default_muc_server(Account account) {
|
||||||
|
@ -634,6 +669,10 @@ public class MucManager : StreamInteractionModule, Object {
|
||||||
if (m != null) {
|
if (m != null) {
|
||||||
// For own messages from this device (msg is a duplicate)
|
// For own messages from this device (msg is a duplicate)
|
||||||
m.marked = Message.Marked.RECEIVED;
|
m.marked = Message.Marked.RECEIVED;
|
||||||
|
string? server_id = Xep.UniqueStableStanzaIDs.get_stanza_id(stanza, m.counterpart.bare_jid);
|
||||||
|
if (server_id != null) {
|
||||||
|
m.server_id = server_id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// For own messages from other devices (msg is not a duplicate msg)
|
// For own messages from other devices (msg is not a duplicate msg)
|
||||||
message.marked = Message.Marked.RECEIVED;
|
message.marked = Message.Marked.RECEIVED;
|
||||||
|
|
488
libdino/src/service/reactions.vala
Normal file
|
@ -0,0 +1,488 @@
|
||||||
|
using Gee;
|
||||||
|
using Qlite;
|
||||||
|
|
||||||
|
using Xmpp;
|
||||||
|
using Xmpp.Xep;
|
||||||
|
using Dino.Entities;
|
||||||
|
|
||||||
|
public class Dino.Reactions : StreamInteractionModule, Object {
|
||||||
|
public static ModuleIdentity<Reactions> IDENTITY = new ModuleIdentity<Reactions>("reactions");
|
||||||
|
public string id { get { return IDENTITY.id; } }
|
||||||
|
|
||||||
|
public signal void reaction_added(Account account, int content_item_id, Jid jid, string reaction);
|
||||||
|
// [Signal(detailed=true)]
|
||||||
|
public signal void reaction_removed(Account account, int content_item_id, Jid jid, string reaction);
|
||||||
|
|
||||||
|
private StreamInteractor stream_interactor;
|
||||||
|
private Database db;
|
||||||
|
private HashMap<string, Gee.List<ReactionInfo>> reaction_infos = new HashMap<string, Gee.List<ReactionInfo>>();
|
||||||
|
|
||||||
|
public static void start(StreamInteractor stream_interactor, Database database) {
|
||||||
|
Reactions m = new Reactions(stream_interactor, database);
|
||||||
|
stream_interactor.add_module(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Reactions(StreamInteractor stream_interactor, Database database) {
|
||||||
|
this.stream_interactor = stream_interactor;
|
||||||
|
this.db = database;
|
||||||
|
stream_interactor.account_added.connect(on_account_added);
|
||||||
|
|
||||||
|
stream_interactor.get_module(MessageProcessor.IDENTITY).message_sent_or_received.connect(on_new_message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void add_reaction(Conversation conversation, ContentItem content_item, string reaction) {
|
||||||
|
Gee.List<string> reactions = get_own_reactions(conversation, content_item);
|
||||||
|
if (!reactions.contains(reaction)) {
|
||||||
|
reactions.add(reaction);
|
||||||
|
}
|
||||||
|
send_reactions(conversation, content_item, reactions);
|
||||||
|
reaction_added(conversation.account, content_item.id, conversation.account.bare_jid, reaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void remove_reaction(Conversation conversation, ContentItem content_item, string reaction) {
|
||||||
|
Gee.List<string> reactions = get_own_reactions(conversation, content_item);
|
||||||
|
reactions.remove(reaction);
|
||||||
|
send_reactions(conversation, content_item, reactions);
|
||||||
|
reaction_removed(conversation.account, content_item.id, conversation.account.bare_jid, reaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Gee.List<ReactionUsers> get_item_reactions(Conversation conversation, ContentItem content_item) {
|
||||||
|
if (conversation.type_ == Conversation.Type.CHAT) {
|
||||||
|
return get_chat_message_reactions(conversation.account, content_item);
|
||||||
|
} else {
|
||||||
|
return get_muc_message_reactions(conversation.account, content_item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async bool conversation_supports_reactions(Conversation conversation) {
|
||||||
|
if (conversation.type_ == Conversation.Type.CHAT) {
|
||||||
|
Gee.List<Jid>? resources = stream_interactor.get_module(PresenceManager.IDENTITY).get_full_jids(conversation.counterpart, conversation.account);
|
||||||
|
if (resources == null) return false;
|
||||||
|
|
||||||
|
foreach (Jid full_jid in resources) {
|
||||||
|
bool? has_feature = yield stream_interactor.get_module(EntityInfo.IDENTITY).has_feature(conversation.account, full_jid, Xep.Reactions.NS_URI);
|
||||||
|
if (has_feature == true) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// The MUC server needs to 1) support stable stanza ids 2) either support occupant ids or be a private room (where we know real jids)
|
||||||
|
var entity_info = stream_interactor.get_module(EntityInfo.IDENTITY);
|
||||||
|
bool server_supports_sid = (yield entity_info.has_feature(conversation.account, conversation.counterpart.bare_jid, Xep.UniqueStableStanzaIDs.NS_URI)) ||
|
||||||
|
(yield entity_info.has_feature(conversation.account, conversation.counterpart.bare_jid, Xmpp.MessageArchiveManagement.NS_URI_2));
|
||||||
|
if (!server_supports_sid) return false;
|
||||||
|
|
||||||
|
bool? supports_occupant_ids = yield entity_info.has_feature(conversation.account, conversation.counterpart, Xep.OccupantIds.NS_URI);
|
||||||
|
if (supports_occupant_ids) return true;
|
||||||
|
|
||||||
|
return stream_interactor.get_module(MucManager.IDENTITY).is_private_room(conversation.account, conversation.counterpart);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void send_reactions(Conversation conversation, ContentItem content_item, Gee.List<string> reactions) {
|
||||||
|
Message? message = null;
|
||||||
|
|
||||||
|
FileItem? file_item = content_item as FileItem;
|
||||||
|
if (file_item != null) {
|
||||||
|
int message_id = int.parse(file_item.file_transfer.info);
|
||||||
|
message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(message_id, conversation);
|
||||||
|
}
|
||||||
|
MessageItem? message_item = content_item as MessageItem;
|
||||||
|
if (message_item != null) {
|
||||||
|
message = message_item.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
XmppStream stream = stream_interactor.get_stream(conversation.account);
|
||||||
|
if (conversation.type_ == Conversation.Type.GROUPCHAT || conversation.type_ == Conversation.Type.GROUPCHAT_PM) {
|
||||||
|
if (conversation.type_ == Conversation.Type.GROUPCHAT) {
|
||||||
|
stream.get_module(Xmpp.Xep.Reactions.Module.IDENTITY).send_reaction(stream, conversation.counterpart, "groupchat", message.server_id ?? message.stanza_id, reactions);
|
||||||
|
} else if (conversation.type_ == Conversation.Type.GROUPCHAT_PM) {
|
||||||
|
stream.get_module(Xmpp.Xep.Reactions.Module.IDENTITY).send_reaction(stream, conversation.counterpart, "chat", message.server_id ?? message.stanza_id, reactions);
|
||||||
|
}
|
||||||
|
// We save the reaction when it gets reflected back to us
|
||||||
|
} else if (conversation.type_ == Conversation.Type.CHAT) {
|
||||||
|
stream.get_module(Xmpp.Xep.Reactions.Module.IDENTITY).send_reaction(stream, conversation.counterpart, "chat", message.stanza_id, reactions);
|
||||||
|
var datetime_now = new DateTime.now();
|
||||||
|
long now_long = (long) (datetime_now.to_unix() * 1000 + datetime_now.get_microsecond());
|
||||||
|
save_chat_reactions(conversation.account, conversation.account.bare_jid, content_item.id, now_long, reactions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Gee.List<string> get_own_reactions(Conversation conversation, ContentItem content_item) {
|
||||||
|
if (conversation.type_ == Conversation.Type.CHAT) {
|
||||||
|
return get_chat_user_reactions(conversation.account, content_item.id, conversation.account.bare_jid)
|
||||||
|
.emojis;
|
||||||
|
} else if (conversation.type_ == Conversation.Type.GROUPCHAT) {
|
||||||
|
string own_occupant_id = stream_interactor.get_module(MucManager.IDENTITY).get_own_occupant_id(conversation.account, content_item.jid);
|
||||||
|
return get_muc_user_reactions(conversation.account, content_item.id, own_occupant_id, conversation.account.bare_jid)
|
||||||
|
.emojis;
|
||||||
|
}
|
||||||
|
return new ArrayList<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ReactionsTime {
|
||||||
|
public Gee.List<string>? emojis = null;
|
||||||
|
public long time = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReactionsTime get_chat_user_reactions(Account account, int content_item_id, Jid jid) {
|
||||||
|
int jid_id = db.get_jid_id(jid);
|
||||||
|
|
||||||
|
QueryBuilder query = db.reaction.select()
|
||||||
|
.with(db.reaction.account_id, "=", account.id)
|
||||||
|
.with(db.reaction.content_item_id, "=", content_item_id)
|
||||||
|
.with(db.reaction.jid_id, "=", jid_id);
|
||||||
|
|
||||||
|
RowOption row = query.single().row();
|
||||||
|
ReactionsTime ret = new ReactionsTime();
|
||||||
|
if (row.is_present()) {
|
||||||
|
ret.emojis = string_to_emoji_list(row[db.reaction.emojis]);
|
||||||
|
ret.time = row[db.reaction.time];
|
||||||
|
} else {
|
||||||
|
ret.emojis = new ArrayList<string>();
|
||||||
|
ret.time = -1;
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReactionsTime get_muc_user_reactions(Account account, int content_item_id, string? occupantid, Jid? real_jid) {
|
||||||
|
QueryBuilder query = db.reaction.select()
|
||||||
|
.with(db.reaction.account_id, "=", account.id)
|
||||||
|
.with(db.reaction.content_item_id, "=", content_item_id)
|
||||||
|
.join_with(db.occupantid, db.occupantid.id, db.reaction.occupant_id)
|
||||||
|
.with(db.occupantid.occupant_id, "=", occupantid);
|
||||||
|
|
||||||
|
RowOption row = query.single().row();
|
||||||
|
ReactionsTime ret = new ReactionsTime();
|
||||||
|
if (row.is_present()) {
|
||||||
|
ret.emojis = string_to_emoji_list(row[db.reaction.emojis]);
|
||||||
|
ret.time = row[db.reaction.time];
|
||||||
|
} else {
|
||||||
|
ret.emojis = new ArrayList<string>();
|
||||||
|
ret.time = -1;
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Gee.List<string> string_to_emoji_list(string emoji_str) {
|
||||||
|
ArrayList<string> ret = new ArrayList<string>();
|
||||||
|
foreach (string emoji in emoji_str.split(",")) {
|
||||||
|
if (emoji.length != 0)
|
||||||
|
ret.add(emoji);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Gee.List<ReactionUsers> get_chat_message_reactions(Account account, ContentItem content_item) {
|
||||||
|
QueryBuilder select = db.reaction.select()
|
||||||
|
.with(db.reaction.account_id, "=", account.id)
|
||||||
|
.with(db.reaction.content_item_id, "=", content_item.id)
|
||||||
|
.order_by(db.reaction.time, "DESC");
|
||||||
|
|
||||||
|
var ret = new ArrayList<ReactionUsers>();
|
||||||
|
var index = new HashMap<string, ReactionUsers>();
|
||||||
|
foreach (Row row in select) {
|
||||||
|
string emoji_str = row[db.reaction.emojis];
|
||||||
|
Jid jid = db.get_jid_by_id(row[db.reaction.jid_id]);
|
||||||
|
|
||||||
|
foreach (string emoji in emoji_str.split(",")) {
|
||||||
|
if (!index.has_key(emoji)) {
|
||||||
|
index[emoji] = new ReactionUsers() { reaction=emoji, jids=new ArrayList<Jid>(Jid.equals_func) };
|
||||||
|
ret.add(index[emoji]);
|
||||||
|
}
|
||||||
|
index[emoji].jids.add(jid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Gee.List<ReactionUsers> get_muc_message_reactions(Account account, ContentItem content_item) {
|
||||||
|
QueryBuilder select = db.reaction.select()
|
||||||
|
.with(db.reaction.account_id, "=", account.id)
|
||||||
|
.with(db.reaction.content_item_id, "=", content_item.id)
|
||||||
|
.join_with(db.occupantid, db.occupantid.id, db.reaction.occupant_id)
|
||||||
|
.order_by(db.reaction.time, "DESC");
|
||||||
|
|
||||||
|
string? own_occupant_id = stream_interactor.get_module(MucManager.IDENTITY).get_own_occupant_id(account, content_item.jid);
|
||||||
|
|
||||||
|
var ret = new ArrayList<ReactionUsers>();
|
||||||
|
var index = new HashMap<string, ReactionUsers>();
|
||||||
|
foreach (Row row in select) {
|
||||||
|
string emoji_str = row[db.reaction.emojis];
|
||||||
|
|
||||||
|
Jid jid = null;
|
||||||
|
if (row[db.occupantid.occupant_id] == own_occupant_id) {
|
||||||
|
jid = account.bare_jid;
|
||||||
|
} else {
|
||||||
|
string nick = row[db.occupantid.last_nick];
|
||||||
|
jid = content_item.jid.with_resource(nick);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (string emoji in emoji_str.split(",")) {
|
||||||
|
if (!index.has_key(emoji)) {
|
||||||
|
index[emoji] = new ReactionUsers() { reaction=emoji, jids=new ArrayList<Jid>(Jid.equals_func) };
|
||||||
|
ret.add(index[emoji]);
|
||||||
|
}
|
||||||
|
index[emoji].jids.add(jid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void on_account_added(Account account) {
|
||||||
|
// TODO get time from delays
|
||||||
|
stream_interactor.module_manager.get_module(account, Xmpp.Xep.Reactions.Module.IDENTITY).received_reactions.connect((stream, from_jid, message_id, reactions, stanza) => {
|
||||||
|
on_reaction_received.begin(account, from_jid, message_id, reactions, stanza);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void on_reaction_received(Account account, Jid from_jid, string message_id, Gee.List<string> reactions, MessageStanza stanza) {
|
||||||
|
if (stanza.type_ == MessageStanza.TYPE_GROUPCHAT) {
|
||||||
|
// Apply the same restrictions for incoming reactions as we do on sending them
|
||||||
|
Conversation muc_conversation = stream_interactor.get_module(ConversationManager.IDENTITY).approx_conversation_for_stanza(from_jid, account.bare_jid, account, MessageStanza.TYPE_GROUPCHAT);
|
||||||
|
bool muc_supports_reactions = yield conversation_supports_reactions(muc_conversation);
|
||||||
|
if (!muc_supports_reactions) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Message reaction_message = yield stream_interactor.get_module(MessageProcessor.IDENTITY).parse_message_stanza(account, stanza);
|
||||||
|
Conversation conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation_for_message(reaction_message);
|
||||||
|
|
||||||
|
Message? message = get_message_for_reaction(conversation, message_id);
|
||||||
|
var reaction_info = new ReactionInfo() { account=account, from_jid=from_jid, reactions=reactions, stanza=stanza, received_time=new DateTime.now() };
|
||||||
|
|
||||||
|
if (message != null) {
|
||||||
|
process_reaction_for_message(message.id, reaction_info);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store reaction infos for later processing after we got the message
|
||||||
|
print(@"Got reaction for $message_id but dont have message yet $(db.get_jid_id(stanza.from.bare_jid))\n");
|
||||||
|
if (!reaction_infos.has_key(message_id)) {
|
||||||
|
reaction_infos[message_id] = new ArrayList<ReactionInfo>();
|
||||||
|
}
|
||||||
|
reaction_infos[message_id].add(reaction_info);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void on_new_message(Message message, Conversation conversation) {
|
||||||
|
Gee.List<ReactionInfo>? reaction_info_list = null;
|
||||||
|
if (conversation.type_ == Conversation.Type.CHAT) {
|
||||||
|
reaction_info_list = reaction_infos[message.stanza_id];
|
||||||
|
} else {
|
||||||
|
reaction_info_list = reaction_infos[message.server_id];
|
||||||
|
}
|
||||||
|
if (reaction_info_list == null) return;
|
||||||
|
|
||||||
|
// Check if the (or potentially which) reaction fits the message
|
||||||
|
ReactionInfo? reaction_info = null;
|
||||||
|
foreach (ReactionInfo info in reaction_info_list) {
|
||||||
|
if (!info.account.equals(conversation.account)) return;
|
||||||
|
switch (info.stanza.type_) {
|
||||||
|
case MessageStanza.TYPE_CHAT:
|
||||||
|
Jid counterpart = message.from.equals_bare(conversation.account.bare_jid) ? info.stanza.from: info.stanza.to;
|
||||||
|
if (message.type_ != Message.Type.CHAT || !counterpart.equals_bare(conversation.counterpart)) continue;
|
||||||
|
break;
|
||||||
|
case MessageStanza.TYPE_GROUPCHAT:
|
||||||
|
if (message.type_ != Message.Type.GROUPCHAT || !message.from.equals_bare(conversation.counterpart)) continue;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
reaction_info = info;
|
||||||
|
}
|
||||||
|
if (reaction_info == null) return;
|
||||||
|
reaction_info_list.remove(reaction_info);
|
||||||
|
if (reaction_info_list.is_empty) reaction_infos.unset(message.stanza_id);
|
||||||
|
|
||||||
|
print(@"Got message for reaction\n");
|
||||||
|
process_reaction_for_message(message.id, reaction_info);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Message? get_message_for_reaction(Conversation conversation, string message_id) {
|
||||||
|
// Query message from a specific account and counterpart. This also makes sure it's a valid reaction for the message.
|
||||||
|
if (conversation.type_ == Conversation.Type.CHAT) {
|
||||||
|
return stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_stanza_id(message_id, conversation);
|
||||||
|
} else {
|
||||||
|
return stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_server_id(message_id, conversation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void process_reaction_for_message(int message_db_id, ReactionInfo reaction_info) {
|
||||||
|
Account account = reaction_info.account;
|
||||||
|
MessageStanza stanza = reaction_info.stanza;
|
||||||
|
Jid from_jid = reaction_info.from_jid;
|
||||||
|
Gee.List<string> reactions = reaction_info.reactions;
|
||||||
|
|
||||||
|
RowOption file_transfer_row = db.file_transfer.select()
|
||||||
|
.with(db.file_transfer.account_id, "=", account.id)
|
||||||
|
.with(db.file_transfer.info, "=", message_db_id.to_string())
|
||||||
|
.single().row(); // TODO better
|
||||||
|
|
||||||
|
var content_item_row = db.content_item.select();
|
||||||
|
|
||||||
|
if (file_transfer_row.is_present()) {
|
||||||
|
content_item_row.with(db.content_item.foreign_id, "=", file_transfer_row[db.file_transfer.id])
|
||||||
|
.with(db.content_item.content_type, "=", 2);
|
||||||
|
} else {
|
||||||
|
content_item_row.with(db.content_item.foreign_id, "=", message_db_id)
|
||||||
|
.with(db.content_item.content_type, "=", 1);
|
||||||
|
}
|
||||||
|
var content_item_row_opt = content_item_row.single().row();
|
||||||
|
if (!content_item_row_opt.is_present()) return;
|
||||||
|
int content_item_id = content_item_row_opt[db.content_item.id];
|
||||||
|
|
||||||
|
// Get reaction time
|
||||||
|
DateTime? reaction_time = null;
|
||||||
|
DelayedDelivery.MessageFlag? delayed_message_flag = DelayedDelivery.MessageFlag.get_flag(stanza);
|
||||||
|
if (delayed_message_flag != null) {
|
||||||
|
reaction_time = delayed_message_flag.datetime;
|
||||||
|
}
|
||||||
|
if (reaction_time == null) {
|
||||||
|
MessageArchiveManagement.MessageFlag? mam_message_flag = MessageArchiveManagement.MessageFlag.get_flag(stanza);
|
||||||
|
if (mam_message_flag != null) reaction_time = mam_message_flag.server_time;
|
||||||
|
}
|
||||||
|
var time_now = new DateTime.now_local();
|
||||||
|
if (reaction_time == null) reaction_time = time_now;
|
||||||
|
if (reaction_time.compare(time_now) > 0) {
|
||||||
|
reaction_time = reaction_info.received_time;
|
||||||
|
}
|
||||||
|
long reaction_time_long = (long) (reaction_time.to_unix() * 1000 + reaction_time.get_microsecond() / 1000);
|
||||||
|
|
||||||
|
// Get current reactions
|
||||||
|
string? occupant_id = OccupantIds.get_occupant_id(stanza.stanza);
|
||||||
|
Jid? real_jid = stream_interactor.get_module(MucManager.IDENTITY).get_real_jid(from_jid, account);
|
||||||
|
if (stanza.type_ == MessageStanza.TYPE_GROUPCHAT && occupant_id == null && real_jid == null) {
|
||||||
|
warning("Attempting to add reaction to message w/o knowing occupant id or real jid");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReactionsTime reactions_time = null;
|
||||||
|
if (stanza.type_ == MessageStanza.TYPE_GROUPCHAT) {
|
||||||
|
reactions_time = get_muc_user_reactions(account, content_item_id, occupant_id, real_jid);
|
||||||
|
} else if (stanza.type_ == MessageStanza.TYPE_CHAT) {
|
||||||
|
reactions_time = get_chat_user_reactions(account, content_item_id, from_jid);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reaction_time_long <= reactions_time.time) {
|
||||||
|
// We already have a more recent reaction
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save reactions
|
||||||
|
if (stanza.type_ == MessageStanza.TYPE_CHAT) {
|
||||||
|
save_chat_reactions(account, from_jid, content_item_id, reaction_time_long, reactions);
|
||||||
|
} else if (stanza.type_ == MessageStanza.TYPE_GROUPCHAT) {
|
||||||
|
save_muc_reactions(account, content_item_id, from_jid, occupant_id, real_jid, reaction_time_long, reactions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify about reaction changes
|
||||||
|
Gee.List<string>? current_reactions = reactions_time.emojis;
|
||||||
|
|
||||||
|
Jid signal_jid = from_jid;
|
||||||
|
if (stanza.type_ == MessageStanza.TYPE_GROUPCHAT &&
|
||||||
|
signal_jid.equals(stream_interactor.get_module(MucManager.IDENTITY).get_own_jid(from_jid, account))) {
|
||||||
|
signal_jid = account.bare_jid;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (string current_reaction in current_reactions) {
|
||||||
|
if (!reactions.contains(current_reaction)) {
|
||||||
|
reaction_removed(account, content_item_id, signal_jid, current_reaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach (string new_reaction in reactions) {
|
||||||
|
if (!current_reactions.contains(new_reaction)) {
|
||||||
|
reaction_added(account, content_item_id, signal_jid, new_reaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print("reactions were: ");
|
||||||
|
foreach (string reac in current_reactions) {
|
||||||
|
print(reac + " ");
|
||||||
|
}
|
||||||
|
print("\n");
|
||||||
|
print("reactions new : ");
|
||||||
|
foreach (string reac in reactions) {
|
||||||
|
print(reac + " ");
|
||||||
|
}
|
||||||
|
print("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void save_chat_reactions(Account account, Jid jid, int content_item_id, long reaction_time, Gee.List<string> reactions) {
|
||||||
|
var emoji_builder = new StringBuilder();
|
||||||
|
for (int i = 0; i < reactions.size; i++) {
|
||||||
|
if (i != 0) emoji_builder.append(",");
|
||||||
|
emoji_builder.append(reactions[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
db.reaction.upsert()
|
||||||
|
.value(db.reaction.account_id, account.id, true)
|
||||||
|
.value(db.reaction.content_item_id, content_item_id, true)
|
||||||
|
.value(db.reaction.jid_id, db.get_jid_id(jid), true)
|
||||||
|
.value(db.reaction.emojis, emoji_builder.str, false)
|
||||||
|
.value(db.reaction.time, reaction_time, false)
|
||||||
|
.perform();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void save_muc_reactions(Account account, int content_item_id, Jid jid, string? occupant_id, Jid? real_jid, long reaction_time, Gee.List<string> reactions) {
|
||||||
|
assert(occupant_id != null || real_jid != null);
|
||||||
|
|
||||||
|
int jid_id = db.get_jid_id(jid);
|
||||||
|
|
||||||
|
var emoji_builder = new StringBuilder();
|
||||||
|
for (int i = 0; i < reactions.size; i++) {
|
||||||
|
if (i != 0) emoji_builder.append(",");
|
||||||
|
emoji_builder.append(reactions[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
var builder = db.reaction.upsert()
|
||||||
|
.value(db.reaction.account_id, account.id, true)
|
||||||
|
.value(db.reaction.content_item_id, content_item_id, true)
|
||||||
|
.value(db.reaction.emojis, emoji_builder.str, false)
|
||||||
|
.value(db.reaction.time, reaction_time, false);
|
||||||
|
|
||||||
|
if (real_jid != null) {
|
||||||
|
builder.value(db.reaction.jid_id, db.get_jid_id(real_jid), occupant_id == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (occupant_id != null) {
|
||||||
|
RowOption row = db.occupantid.select()
|
||||||
|
.with(db.occupantid.account_id, "=", account.id)
|
||||||
|
.with(db.occupantid.jid_id, "=", jid_id)
|
||||||
|
.with(db.occupantid.occupant_id, "=", occupant_id)
|
||||||
|
.single().row();
|
||||||
|
|
||||||
|
int occupant_db_id = -1;
|
||||||
|
if (row.is_present()) {
|
||||||
|
occupant_db_id = row[db.occupantid.id];
|
||||||
|
} else {
|
||||||
|
occupant_db_id = (int)db.occupantid.upsert()
|
||||||
|
.value(db.occupantid.account_id, account.id, true)
|
||||||
|
.value(db.occupantid.jid_id, jid_id, true)
|
||||||
|
.value(db.occupantid.occupant_id, occupant_id, true)
|
||||||
|
.value(db.occupantid.last_nick, jid.resourcepart, false)
|
||||||
|
.perform();
|
||||||
|
}
|
||||||
|
builder.value(db.reaction.occupant_id, occupant_db_id, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.perform();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Dino.ReactionUsers {
|
||||||
|
public string reaction { get; set; }
|
||||||
|
public Gee.List<Jid> jids { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Dino.ReactionInfo {
|
||||||
|
public Account account { get; set; }
|
||||||
|
public Jid from_jid { get; set; }
|
||||||
|
public Gee.List<string> reactions { get; set; }
|
||||||
|
public MessageStanza stanza { get; set; }
|
||||||
|
public DateTime received_time { get; set; }
|
||||||
|
}
|
|
@ -14,7 +14,12 @@ find_packages(MAIN_PACKAGES REQUIRED
|
||||||
)
|
)
|
||||||
|
|
||||||
set(RESOURCE_LIST
|
set(RESOURCE_LIST
|
||||||
|
dino-conversation-list-placeholder-arrow.svg
|
||||||
|
|
||||||
icons/scalable/actions/dino-account-plus-symbolic.svg
|
icons/scalable/actions/dino-account-plus-symbolic.svg
|
||||||
|
icons/scalable/actions/dino-emoticon-add-symbolic.svg
|
||||||
|
icons/scalable/actions/dino-emoticon-symbolic.svg
|
||||||
|
icons/scalable/actions/dino-qr-code-symbolic.svg
|
||||||
|
|
||||||
icons/scalable/apps/im.dino.Dino.svg
|
icons/scalable/apps/im.dino.Dino.svg
|
||||||
icons/scalable/apps/im.dino.Dino-symbolic.svg
|
icons/scalable/apps/im.dino.Dino-symbolic.svg
|
||||||
|
@ -27,8 +32,6 @@ set(RESOURCE_LIST
|
||||||
icons/scalable/devices/dino-phone-ring-symbolic.svg
|
icons/scalable/devices/dino-phone-ring-symbolic.svg
|
||||||
icons/scalable/devices/dino-phone-symbolic.svg
|
icons/scalable/devices/dino-phone-symbolic.svg
|
||||||
|
|
||||||
icons/scalable/emotes/dino-emoticon-symbolic.svg
|
|
||||||
|
|
||||||
icons/scalable/mimetypes/dino-file-document-symbolic.svg
|
icons/scalable/mimetypes/dino-file-document-symbolic.svg
|
||||||
icons/scalable/mimetypes/dino-file-download-symbolic.svg
|
icons/scalable/mimetypes/dino-file-download-symbolic.svg
|
||||||
icons/scalable/mimetypes/dino-file-image-symbolic.svg
|
icons/scalable/mimetypes/dino-file-image-symbolic.svg
|
||||||
|
@ -40,6 +43,7 @@ set(RESOURCE_LIST
|
||||||
icons/scalable/status/dino-double-tick-symbolic.svg
|
icons/scalable/status/dino-double-tick-symbolic.svg
|
||||||
icons/scalable/status/dino-microphone-off-symbolic.svg
|
icons/scalable/status/dino-microphone-off-symbolic.svg
|
||||||
icons/scalable/status/dino-microphone-symbolic.svg
|
icons/scalable/status/dino-microphone-symbolic.svg
|
||||||
|
icons/scalable/status/dino-party-popper-symbolic.svg
|
||||||
icons/scalable/status/dino-security-high-symbolic.svg
|
icons/scalable/status/dino-security-high-symbolic.svg
|
||||||
icons/scalable/status/dino-status-away.svg
|
icons/scalable/status/dino-status-away.svg
|
||||||
icons/scalable/status/dino-status-chat.svg
|
icons/scalable/status/dino-status-chat.svg
|
||||||
|
@ -49,10 +53,6 @@ set(RESOURCE_LIST
|
||||||
icons/scalable/status/dino-video-off-symbolic.svg
|
icons/scalable/status/dino-video-off-symbolic.svg
|
||||||
icons/scalable/status/dino-video-symbolic.svg
|
icons/scalable/status/dino-video-symbolic.svg
|
||||||
|
|
||||||
icons/scalable/ui/dino-conversation-list-placeholder-arrow.svg
|
|
||||||
icons/scalable/ui/dino-qr-code-symbolic.svg
|
|
||||||
icons/scalable/ui/dino-party-popper-symbolic.svg
|
|
||||||
|
|
||||||
add_conversation/add_contact_dialog.ui
|
add_conversation/add_contact_dialog.ui
|
||||||
add_conversation/add_groupchat_dialog.ui
|
add_conversation/add_groupchat_dialog.ui
|
||||||
add_conversation/conference_details_fragment.ui
|
add_conversation/conference_details_fragment.ui
|
||||||
|
@ -157,6 +157,7 @@ SOURCES
|
||||||
src/ui/conversation_content_view/file_image_widget.vala
|
src/ui/conversation_content_view/file_image_widget.vala
|
||||||
src/ui/conversation_content_view/file_widget.vala
|
src/ui/conversation_content_view/file_widget.vala
|
||||||
src/ui/conversation_content_view/message_widget.vala
|
src/ui/conversation_content_view/message_widget.vala
|
||||||
|
src/ui/conversation_content_view/reactions_widget.vala
|
||||||
src/ui/conversation_content_view/subscription_notification.vala
|
src/ui/conversation_content_view/subscription_notification.vala
|
||||||
|
|
||||||
src/ui/chat_input/chat_input_controller.vala
|
src/ui/chat_input/chat_input_controller.vala
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkGrid">
|
<object class="GtkGrid">
|
||||||
|
<property name="valign">center</property>
|
||||||
<property name="orientation">vertical</property>
|
<property name="orientation">vertical</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkLabel" id="name_label">
|
<object class="GtkLabel" id="name_label">
|
||||||
|
|
|
@ -36,6 +36,9 @@
|
||||||
<property name="margin-end">10</property>
|
<property name="margin-end">10</property>
|
||||||
<property name="halign">end</property>
|
<property name="halign">end</property>
|
||||||
<property name="valign">start</property>
|
<property name="valign">start</property>
|
||||||
|
<style>
|
||||||
|
<class name="linked"/>
|
||||||
|
</style>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkButton" id="button1">
|
<object class="GtkButton" id="button1">
|
||||||
<property name="visible">0</property>
|
<property name="visible">0</property>
|
||||||
|
|
Before Width: | Height: | Size: 673 B After Width: | Height: | Size: 673 B |
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg version="1.1" viewBox="0 0 23.568 23.711" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="m10 19.211c2.33 0 4.3-1.46 5.11-3.5h-10.22c0.8 2.04 2.78 3.5 5.11 3.5m-3.5-6.5c0.82843 0 1.5-0.67157 1.5-1.5s-0.67157-1.5-1.5-1.5-1.5 0.67157-1.5 1.5 0.67157 1.5 1.5 1.5m7 0c0.82843 0 1.5-0.67157 1.5-1.5s-0.67157-1.5-1.5-1.5-1.5 0.67157-1.5 1.5 0.67157 1.5 1.5 1.5m-3.5 9c-4.4183 0-8-3.5817-8-8s3.5817-8 8-8c1.4367-0.016553 1.4581-1.9613 0-2-5.53 0-10 4.5-10 10 0 5.5228 4.4772 10 10 10s10-4.4772 10-10c0-1.0544-2-1.0324-2 0 0 4.4183-3.5817 8-8 8"/>
|
||||||
|
<path d="m18.908 0c-0.33046 0-0.61972 0.12233-0.80078 0.36133-0.18106 0.239-0.25586 0.56747-0.25586 0.97266v2.6523h-2.2715c-0.38896 0-0.70598 0.064394-0.94727 0.21875-0.26371 0.15646-0.38672 0.4669-0.38672 0.83789 0 0.36179 0.12302 0.66715 0.38086 0.83398l0.001953 0.0019531h0.001953c0.24918 0.15228 0.56865 0.21875 0.94922 0.21875h2.2715v2.6543c0 0.40519 0.074767 0.73218 0.25586 0.9707 0.18109 0.23852 0.46892 0.35944 0.79883 0.36133 0.32038 0.001837 0.60667-0.12152 0.78906-0.35937 0.18239-0.23785 0.26374-0.56552 0.26758-0.9707v-0.0019531-2.6543h2.2559c0.41061 0 0.74047-0.073348 0.98242-0.25391s0.36719-0.46969 0.36719-0.80078c0-0.33109-0.12524-0.62218-0.36719-0.80273s-0.57181-0.25391-0.98242-0.25391h-2.2559v-2.6523c0-0.38896-0.064393-0.70598-0.21875-0.94727-0.15646-0.26371-0.46495-0.38672-0.83594-0.38672z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 593 B After Width: | Height: | Size: 593 B |
Before Width: | Height: | Size: 568 B After Width: | Height: | Size: 568 B |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
@ -82,6 +82,8 @@ window.dino-main .dino-sidebar > frame {
|
||||||
border-bottom: 1px solid @borders;
|
border-bottom: 1px solid @borders;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Message */
|
||||||
|
|
||||||
.message-box {
|
.message-box {
|
||||||
transition: background .05s ease;
|
transition: background .05s ease;
|
||||||
}
|
}
|
||||||
|
@ -107,6 +109,21 @@ window.dino-main .dino-conversation .message-box.error:hover {
|
||||||
background: alpha(@error_color, 0.12);
|
background: alpha(@error_color, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Message Menu */
|
||||||
|
|
||||||
|
.message-menu-box {
|
||||||
|
background-color: @theme_base_color;
|
||||||
|
border: 1px solid alpha(@theme_fg_color, 0.15);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-menu-button {
|
||||||
|
padding: 6px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fie Widget */
|
||||||
|
|
||||||
window.dino-main .file-box-outer,
|
window.dino-main .file-box-outer,
|
||||||
window.dino-main .call-box-outer {
|
window.dino-main .call-box-outer {
|
||||||
background: @theme_base_color;
|
background: @theme_base_color;
|
||||||
|
@ -140,6 +157,8 @@ window.dino-main .file-image-widget .file-box-outer button:hover {
|
||||||
background: rgba(100, 100, 100, 0.5);
|
background: rgba(100, 100, 100, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Call widget */
|
||||||
|
|
||||||
window.dino-main .call-box-outer.incoming {
|
window.dino-main .call-box-outer.incoming {
|
||||||
border-color: alpha(@theme_selected_bg_color, 0.3);
|
border-color: alpha(@theme_selected_bg_color, 0.3);
|
||||||
}
|
}
|
||||||
|
@ -153,6 +172,44 @@ window.dino-main .multiparty-participants {
|
||||||
background: alpha(@theme_fg_color, 0.04);
|
background: alpha(@theme_fg_color, 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Reactions */
|
||||||
|
|
||||||
|
window.dino-main menubutton.reaction-box image {
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.dino-main button.reaction-box,
|
||||||
|
window.dino-main menubutton.reaction-box > button {
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 3px 5px ;
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: alpha(@theme_fg_color, 0.07);
|
||||||
|
background-image: none;
|
||||||
|
box-shadow: none;
|
||||||
|
min-height: 0;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.dino-main button.reaction-box.own-reaction,
|
||||||
|
window.dino-main menubutton.reaction-box.own-reaction > button {
|
||||||
|
color: mix(@theme_selected_bg_color, @theme_fg_color, 0.4);
|
||||||
|
border-color: @theme_selected_bg_color;
|
||||||
|
background-color: alpha(@theme_selected_bg_color, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.dino-main button.reaction-box:hover,
|
||||||
|
window.dino-main menubutton.reaction-box:hover > button {
|
||||||
|
background-color: alpha(@theme_fg_color, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.dino-main button.reaction-box.own-reaction:hover,
|
||||||
|
window.dino-main menubutton.reaction-box.own-reaction > button {
|
||||||
|
background-color: alpha(@theme_selected_bg_color, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
|
||||||
window.dino-main .dino-sidebar > frame.collapsed {
|
window.dino-main .dino-sidebar > frame.collapsed {
|
||||||
border-bottom: 1px solid @borders;
|
border-bottom: 1px solid @borders;
|
||||||
}
|
}
|
||||||
|
@ -165,6 +222,8 @@ window.dino-main .dino-sidebar frame.auto-complete list > row {
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* File overlay */
|
||||||
|
|
||||||
window.dino-main .dino-white-overlay {
|
window.dino-main .dino-white-overlay {
|
||||||
background: @theme_base_color;
|
background: @theme_base_color;
|
||||||
}
|
}
|
||||||
|
@ -175,6 +234,8 @@ window.dino-main .dino-file-overlay {
|
||||||
box-shadow: 0 2px 3px alpha(black, 0.1);
|
box-shadow: 0 2px 3px alpha(black, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Chat Input*/
|
||||||
|
|
||||||
window.dino-main .dino-chatinput frame box {
|
window.dino-main .dino-chatinput frame box {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
@ -386,6 +447,6 @@ box.dino-input-error .chat-input-status.input-status-highlight-once {
|
||||||
box-shadow: 0 0 2px 0 rgba(0,0,0,0.5);
|
box-shadow: 0 0 2px 0 rgba(0,0,0,0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.qrcode-container {
|
.qrcode-container > contents {
|
||||||
background: white; /* Color of the quiet zone. MUST have the same "reflectance" as light modules of the QR code. */
|
background: white; /* Color of the quiet zone. MUST have the same "reflectance" as light modules of the QR code. */
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,17 +12,7 @@ protected class ConferenceDetailsFragment : Box {
|
||||||
|
|
||||||
public signal void joined();
|
public signal void joined();
|
||||||
|
|
||||||
public bool done {
|
public bool done { get; private set; }
|
||||||
get {
|
|
||||||
try {
|
|
||||||
Jid parsed_jid = new Jid(jid);
|
|
||||||
return parsed_jid.localpart != null && parsed_jid.resourcepart == null && nick != null;
|
|
||||||
} catch (InvalidJidError e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private set {}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Account account {
|
public Account account {
|
||||||
owned get { return account_combobox.selected; }
|
owned get { return account_combobox.selected; }
|
||||||
|
@ -41,6 +31,7 @@ protected class ConferenceDetailsFragment : Box {
|
||||||
jid_label.label = value;
|
jid_label.label = value;
|
||||||
jid_entry.text = value;
|
jid_entry.text = value;
|
||||||
jid_stack.set_visible_child_name("label");
|
jid_stack.set_visible_child_name("label");
|
||||||
|
check_if_done();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public string? nick {
|
public string? nick {
|
||||||
|
@ -49,6 +40,7 @@ protected class ConferenceDetailsFragment : Box {
|
||||||
nick_label.label = value ?? "";
|
nick_label.label = value ?? "";
|
||||||
nick_entry.text = value ?? "";
|
nick_entry.text = value ?? "";
|
||||||
nick_stack.set_visible_child_name("label");
|
nick_stack.set_visible_child_name("label");
|
||||||
|
check_if_done();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public string? password {
|
public string? password {
|
||||||
|
@ -118,8 +110,15 @@ protected class ConferenceDetailsFragment : Box {
|
||||||
// nick_entry.key_release_event.connect(on_nick_key_release_event);
|
// nick_entry.key_release_event.connect(on_nick_key_release_event);
|
||||||
// password_entry.key_release_event.connect(on_password_key_release_event);
|
// password_entry.key_release_event.connect(on_password_key_release_event);
|
||||||
|
|
||||||
// jid_entry.key_release_event.connect(() => { done = true; return false; }); // just for notifying
|
var jid_entry_controller = new EventControllerKey();
|
||||||
// nick_entry.key_release_event.connect(() => { done = true; return false; });
|
jid_entry_controller.key_released.connect(() => { check_if_done(); });
|
||||||
|
jid_entry.add_controller(jid_entry_controller);
|
||||||
|
|
||||||
|
var nick_entry_controller = new EventControllerKey();
|
||||||
|
nick_entry_controller.key_released.connect(() => { check_if_done(); });
|
||||||
|
nick_entry.add_controller(nick_entry_controller);
|
||||||
|
|
||||||
|
check_if_done();
|
||||||
|
|
||||||
notification_button.clicked.connect(() => { notification_revealer.set_reveal_child(false); });
|
notification_button.clicked.connect(() => { notification_revealer.set_reveal_child(false); });
|
||||||
|
|
||||||
|
@ -195,6 +194,15 @@ protected class ConferenceDetailsFragment : Box {
|
||||||
notification_revealer.set_reveal_child(true);
|
notification_revealer.set_reveal_child(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void check_if_done() {
|
||||||
|
try {
|
||||||
|
Jid parsed_jid = new Jid(jid);
|
||||||
|
done = parsed_jid.localpart != null && parsed_jid.resourcepart == null && nick != null;
|
||||||
|
} catch (InvalidJidError e) {
|
||||||
|
done = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// private bool on_jid_key_release_event(EventKey event) {
|
// private bool on_jid_key_release_event(EventKey event) {
|
||||||
// jid_label.label = jid_entry.text;
|
// jid_label.label = jid_entry.text;
|
||||||
// if (event.keyval == Key.Return) jid_stack.set_visible_child_name("label");
|
// if (event.keyval == Key.Return) jid_stack.set_visible_child_name("label");
|
||||||
|
|
|
@ -23,6 +23,7 @@ protected class ConferenceList {
|
||||||
this.stream_interactor = stream_interactor;
|
this.stream_interactor = stream_interactor;
|
||||||
|
|
||||||
bookmarks_updated_handler_id = stream_interactor.get_module(MucManager.IDENTITY).bookmarks_updated.connect((account, conferences) => {
|
bookmarks_updated_handler_id = stream_interactor.get_module(MucManager.IDENTITY).bookmarks_updated.connect((account, conferences) => {
|
||||||
|
print(@"$(this == null) $(lists == null)\n");
|
||||||
lists[account] = conferences;
|
lists[account] = conferences;
|
||||||
refresh_conferences();
|
refresh_conferences();
|
||||||
});
|
});
|
||||||
|
@ -69,7 +70,7 @@ protected class ConferenceList {
|
||||||
account_widgets_cpy.set_all(widgets[account]);
|
account_widgets_cpy.set_all(widgets[account]);
|
||||||
|
|
||||||
foreach (Jid jid in account_widgets_cpy.keys) {
|
foreach (Jid jid in account_widgets_cpy.keys) {
|
||||||
remove_conference(account, jid);
|
list_box.remove(widgets[account][jid]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@ public class Dino.Ui.CallBottomBar : Gtk.Box {
|
||||||
audio_button.margin_end = audio_button.margin_bottom = 5; // space for the small settings button
|
audio_button.margin_end = audio_button.margin_bottom = 5; // space for the small settings button
|
||||||
audio_button_overlay.set_child(audio_button);
|
audio_button_overlay.set_child(audio_button);
|
||||||
audio_button_overlay.add_overlay(audio_settings_button);
|
audio_button_overlay.add_overlay(audio_settings_button);
|
||||||
menu_button_set_icon_with_size(audio_settings_button, "go-up-symbolic", 10);
|
Util.menu_button_set_icon_with_size(audio_settings_button, "go-up-symbolic", 10);
|
||||||
audio_settings_button.add_css_class("call-mediadevice-settings-button");
|
audio_settings_button.add_css_class("call-mediadevice-settings-button");
|
||||||
main_buttons.append(audio_button_overlay);
|
main_buttons.append(audio_button_overlay);
|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@ public class Dino.Ui.CallBottomBar : Gtk.Box {
|
||||||
video_button.margin_end = video_button.margin_bottom = 5;
|
video_button.margin_end = video_button.margin_bottom = 5;
|
||||||
video_button_overlay.set_child(video_button);
|
video_button_overlay.set_child(video_button);
|
||||||
video_button_overlay.add_overlay(video_settings_button);
|
video_button_overlay.add_overlay(video_settings_button);
|
||||||
menu_button_set_icon_with_size(video_settings_button, "go-up-symbolic", 10);
|
Util.menu_button_set_icon_with_size(video_settings_button, "go-up-symbolic", 10);
|
||||||
video_settings_button.add_css_class("call-mediadevice-settings-button");
|
video_settings_button.add_css_class("call-mediadevice-settings-button");
|
||||||
main_buttons.append(video_button_overlay);
|
main_buttons.append(video_button_overlay);
|
||||||
|
|
||||||
|
@ -76,21 +76,6 @@ public class Dino.Ui.CallBottomBar : Gtk.Box {
|
||||||
this.add_css_class("call-bottom-bar");
|
this.add_css_class("call-bottom-bar");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void menu_button_set_icon_with_size(MenuButton menu_button, string icon_name, int pixel_size) {
|
|
||||||
#if GTK_4_6
|
|
||||||
menu_button.set_child(new Image.from_icon_name(icon_name) { pixel_size=pixel_size });
|
|
||||||
#else
|
|
||||||
menu_button.set_icon_name(icon_name);
|
|
||||||
var button = menu_button.get_first_child() as Button;
|
|
||||||
if (button == null) return;
|
|
||||||
var box = button.child as Box;
|
|
||||||
if (box == null) return;
|
|
||||||
var image = box.get_first_child() as Image;
|
|
||||||
if (image == null) return;
|
|
||||||
image.pixel_size = pixel_size;
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
public AudioSettingsPopover? show_audio_device_choices(bool show) {
|
public AudioSettingsPopover? show_audio_device_choices(bool show) {
|
||||||
audio_settings_button.visible = show;
|
audio_settings_button.visible = show;
|
||||||
if (audio_settings_popover != null) audio_settings_popover.visible = false;
|
if (audio_settings_popover != null) audio_settings_popover.visible = false;
|
||||||
|
|
|
@ -32,6 +32,7 @@ public class ConversationItemSkeleton : Plugins.ConversationItemWidgetInterface,
|
||||||
public Entities.Message.Marked item_mark { get; set; }
|
public Entities.Message.Marked item_mark { get; set; }
|
||||||
public ContentMetaItem content_meta_item = null;
|
public ContentMetaItem content_meta_item = null;
|
||||||
public Widget? widget = null;
|
public Widget? widget = null;
|
||||||
|
private ReactionsController? reactions_controller = null;
|
||||||
|
|
||||||
private uint time_update_timeout = 0;
|
private uint time_update_timeout = 0;
|
||||||
private ulong updated_roster_handler_id = 0;
|
private ulong updated_roster_handler_id = 0;
|
||||||
|
@ -64,6 +65,15 @@ public class ConversationItemSkeleton : Plugins.ConversationItemWidgetInterface,
|
||||||
this.notify["show-skeleton"].connect(update_margin);
|
this.notify["show-skeleton"].connect(update_margin);
|
||||||
this.notify["show-skeleton"].connect(set_header);
|
this.notify["show-skeleton"].connect(set_header);
|
||||||
|
|
||||||
|
ContentMetaItem? content_meta_item = item as ContentMetaItem;
|
||||||
|
if (content_meta_item != null) {
|
||||||
|
reactions_controller = new ReactionsController(conversation, content_meta_item.content_item, stream_interactor);
|
||||||
|
reactions_controller.box_activated.connect((widget) => {
|
||||||
|
main_grid.attach(widget, 1, 2, 4, 1);
|
||||||
|
});
|
||||||
|
reactions_controller.init();
|
||||||
|
}
|
||||||
|
|
||||||
update_margin();
|
update_margin();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,19 +15,20 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
|
||||||
[GtkChild] public unowned ScrolledWindow scrolled;
|
[GtkChild] public unowned ScrolledWindow scrolled;
|
||||||
[GtkChild] private unowned Revealer notification_revealer;
|
[GtkChild] private unowned Revealer notification_revealer;
|
||||||
[GtkChild] private unowned Box message_menu_box;
|
[GtkChild] private unowned Box message_menu_box;
|
||||||
[GtkChild] private unowned Button button1;
|
|
||||||
[GtkChild] private unowned Image button1_icon;
|
|
||||||
[GtkChild] private unowned Box notifications;
|
[GtkChild] private unowned Box notifications;
|
||||||
[GtkChild] private unowned Box main;
|
[GtkChild] private unowned Box main;
|
||||||
[GtkChild] private unowned Box main_wrap_box;
|
[GtkChild] private unowned Box main_wrap_box;
|
||||||
[GtkChild] private unowned Stack stack;
|
[GtkChild] private unowned Stack stack;
|
||||||
|
|
||||||
|
private ArrayList<Widget> action_buttons = new ArrayList<Widget>();
|
||||||
|
private Gee.List<Dino.Plugins.MessageAction>? message_actions = null;
|
||||||
|
|
||||||
private StreamInteractor stream_interactor;
|
private StreamInteractor stream_interactor;
|
||||||
private Gee.TreeSet<Plugins.MetaConversationItem> content_items = new Gee.TreeSet<Plugins.MetaConversationItem>(compare_meta_items);
|
private Gee.TreeSet<Plugins.MetaConversationItem> content_items = new Gee.TreeSet<Plugins.MetaConversationItem>(compare_meta_items);
|
||||||
private Gee.TreeSet<Plugins.MetaConversationItem> meta_items = new TreeSet<Plugins.MetaConversationItem>(compare_meta_items);
|
private Gee.TreeSet<Plugins.MetaConversationItem> meta_items = new TreeSet<Plugins.MetaConversationItem>(compare_meta_items);
|
||||||
private Gee.HashMap<Plugins.MetaConversationItem, ConversationItemSkeleton> item_item_skeletons = new Gee.HashMap<Plugins.MetaConversationItem, ConversationItemSkeleton>();
|
private Gee.HashMap<Plugins.MetaConversationItem, ConversationItemSkeleton> item_item_skeletons = new Gee.HashMap<Plugins.MetaConversationItem, ConversationItemSkeleton>();
|
||||||
private Gee.HashMap<Plugins.MetaConversationItem, Widget> widgets = new Gee.HashMap<Plugins.MetaConversationItem, Widget>();
|
private Gee.HashMap<Plugins.MetaConversationItem, Widget> widgets = new Gee.HashMap<Plugins.MetaConversationItem, Widget>();
|
||||||
private Gee.List<ConversationItemSkeleton> item_skeletons = new Gee.ArrayList<ConversationItemSkeleton>();
|
private Gee.List<Widget> widget_order = new Gee.ArrayList<Widget>();
|
||||||
private ContentProvider content_populator;
|
private ContentProvider content_populator;
|
||||||
private SubscriptionNotitication subscription_notification;
|
private SubscriptionNotitication subscription_notification;
|
||||||
|
|
||||||
|
@ -81,11 +82,6 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
|
||||||
main.add_controller(main_motion_events);
|
main.add_controller(main_motion_events);
|
||||||
main_motion_events.motion.connect(update_highlight);
|
main_motion_events.motion.connect(update_highlight);
|
||||||
|
|
||||||
button1.clicked.connect(() => {
|
|
||||||
current_meta_item.get_item_actions(Plugins.WidgetType.GTK4)[0].callback(button1, current_meta_item, currently_highlighted);
|
|
||||||
update_message_menu();
|
|
||||||
});
|
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,7 +103,20 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool is_highlight_fixed() {
|
||||||
|
foreach (Widget widget in action_buttons) {
|
||||||
|
MenuButton? menu_button = widget as MenuButton;
|
||||||
|
if (menu_button != null && menu_button.popover.visible) return true;
|
||||||
|
|
||||||
|
ToggleButton? toggle_button = widget as ToggleButton;
|
||||||
|
if (toggle_button != null && toggle_button.active) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private void on_leave_notify_event() {
|
private void on_leave_notify_event() {
|
||||||
|
if (is_highlight_fixed()) return;
|
||||||
|
|
||||||
if (currently_highlighted != null) {
|
if (currently_highlighted != null) {
|
||||||
currently_highlighted.remove_css_class("highlight");
|
currently_highlighted.remove_css_class("highlight");
|
||||||
currently_highlighted = null;
|
currently_highlighted = null;
|
||||||
|
@ -116,6 +125,8 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
|
||||||
}
|
}
|
||||||
|
|
||||||
private void update_highlight(double x, double y) {
|
private void update_highlight(double x, double y) {
|
||||||
|
if (is_highlight_fixed()) return;
|
||||||
|
|
||||||
if (currently_highlighted != null && (last_y - y).abs() <= 2) {
|
if (currently_highlighted != null && (last_y - y).abs() <= 2) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -174,11 +185,42 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var actions = current_meta_item.get_item_actions(Plugins.WidgetType.GTK4);
|
foreach (Widget widget in action_buttons) {
|
||||||
message_menu_box.visible = actions != null && actions.size > 0;
|
message_menu_box.remove(widget);
|
||||||
if (actions != null && actions.size == 1) {
|
}
|
||||||
button1.visible = true;
|
action_buttons.clear();
|
||||||
button1_icon.set_from_icon_name(actions[0].icon_name);
|
|
||||||
|
message_actions = current_meta_item.get_item_actions(Plugins.WidgetType.GTK4);
|
||||||
|
|
||||||
|
if (message_actions != null) {
|
||||||
|
message_menu_box.visible = true;
|
||||||
|
|
||||||
|
// Configure as many buttons as we need with the actions for the current meta item
|
||||||
|
for (int i = 0; i < message_actions.size; i++) {
|
||||||
|
if (message_actions[i].popover != null) {
|
||||||
|
MenuButton button = new MenuButton();
|
||||||
|
button.icon_name = message_actions[i].icon_name;
|
||||||
|
button.set_popover(message_actions[i].popover as Popover);
|
||||||
|
action_buttons.add(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message_actions[i].callback != null) {
|
||||||
|
var message_action = message_actions[i];
|
||||||
|
Button button = new Button();
|
||||||
|
button.icon_name = message_action.icon_name;
|
||||||
|
button.clicked.connect(() => {
|
||||||
|
print(@"$(current_meta_item.jid) skdfj \n");
|
||||||
|
message_action.callback(button, current_meta_item, currently_highlighted);
|
||||||
|
});
|
||||||
|
action_buttons.add(button);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Widget widget in action_buttons) {
|
||||||
|
message_menu_box.append(widget);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message_menu_box.visible = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -309,7 +351,7 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
|
||||||
if (skeleton != null) {
|
if (skeleton != null) {
|
||||||
main.remove(skeleton.get_widget());
|
main.remove(skeleton.get_widget());
|
||||||
widgets.unset(item);
|
widgets.unset(item);
|
||||||
item_skeletons.remove(skeleton);
|
widget_order.remove(skeleton.get_widget());
|
||||||
item_item_skeletons.unset(item);
|
item_item_skeletons.unset(item);
|
||||||
|
|
||||||
content_items.remove(item);
|
content_items.remove(item);
|
||||||
|
@ -353,8 +395,8 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
|
||||||
// Fill datastructure
|
// Fill datastructure
|
||||||
ConversationItemSkeleton item_skeleton = new ConversationItemSkeleton(stream_interactor, conversation, item, !animate);
|
ConversationItemSkeleton item_skeleton = new ConversationItemSkeleton(stream_interactor, conversation, item, !animate);
|
||||||
item_item_skeletons[item] = item_skeleton;
|
item_item_skeletons[item] = item_skeleton;
|
||||||
int index = lower_item != null ? item_skeletons.index_of(item_item_skeletons[lower_item]) + 1 : 0;
|
int index = lower_item != null ? widget_order.index_of(item_item_skeletons[lower_item].get_widget()) + 1 : 0;
|
||||||
item_skeletons.insert(index, item_skeleton);
|
widget_order.insert(index, item_skeleton.get_widget());
|
||||||
|
|
||||||
// Insert widget
|
// Insert widget
|
||||||
widgets[item] = item_skeleton.get_widget();
|
widgets[item] = item_skeleton.get_widget();
|
||||||
|
@ -382,7 +424,7 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
|
||||||
// If an item from the past was added, add everything between that item and the (post-)first present item
|
// If an item from the past was added, add everything between that item and the (post-)first present item
|
||||||
if (index == 0) {
|
if (index == 0) {
|
||||||
Dino.Application app = Dino.Application.get_default();
|
Dino.Application app = Dino.Application.get_default();
|
||||||
if (item_skeletons.size == 1) {
|
if (widget_order.size == 1) {
|
||||||
foreach (Plugins.ConversationAdditionPopulator populator in app.plugin_registry.conversation_addition_populators) {
|
foreach (Plugins.ConversationAdditionPopulator populator in app.plugin_registry.conversation_addition_populators) {
|
||||||
populator.populate_timespan(conversation, item.time, new DateTime.now_utc());
|
populator.populate_timespan(conversation, item.time, new DateTime.now_utc());
|
||||||
}
|
}
|
||||||
|
@ -404,6 +446,15 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
|
||||||
(upper_item.mark == Message.Marked.WONTSEND) == (lower_item.mark == Message.Marked.WONTSEND);
|
(upper_item.mark == Message.Marked.WONTSEND) == (lower_item.mark == Message.Marked.WONTSEND);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void on_action_button_clicked(ToggleButton button) {
|
||||||
|
int button_idx = action_buttons.index_of(button);
|
||||||
|
print(button_idx.to_string() + "\n");
|
||||||
|
Plugins.MessageAction message_action = message_actions[button_idx];
|
||||||
|
if (message_action.callback != null) {
|
||||||
|
message_action.callback(button, current_meta_item, currently_highlighted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void on_upper_notify() {
|
private void on_upper_notify() {
|
||||||
if (was_upper == null || scrolled.vadjustment.value > was_upper - was_page_size - 1) { // scrolled down or content smaller than page size
|
if (was_upper == null || scrolled.vadjustment.value > was_upper - was_page_size - 1) { // scrolled down or content smaller than page size
|
||||||
if (at_current_content) {
|
if (at_current_content) {
|
||||||
|
@ -471,7 +522,7 @@ public class ConversationView : Widget, Plugins.ConversationItemCollection, Plug
|
||||||
was_page_size = null;
|
was_page_size = null;
|
||||||
content_items.clear();
|
content_items.clear();
|
||||||
meta_items.clear();
|
meta_items.clear();
|
||||||
item_skeletons.clear();
|
widget_order.clear();
|
||||||
item_item_skeletons.clear();
|
item_item_skeletons.clear();
|
||||||
foreach (Widget widget in widgets.values) {
|
foreach (Widget widget in widgets.values) {
|
||||||
main.remove(widget);
|
main.remove(widget);
|
||||||
|
|
|
@ -22,6 +22,7 @@ public class MessageMetaItem : ContentMetaItem {
|
||||||
|
|
||||||
MessageItemEditMode? edit_mode = null;
|
MessageItemEditMode? edit_mode = null;
|
||||||
ChatTextViewController? controller = null;
|
ChatTextViewController? controller = null;
|
||||||
|
private bool supports_reaction = false;
|
||||||
AdditionalInfo additional_info = AdditionalInfo.NONE;
|
AdditionalInfo additional_info = AdditionalInfo.NONE;
|
||||||
|
|
||||||
ulong realize_id = -1;
|
ulong realize_id = -1;
|
||||||
|
@ -35,6 +36,8 @@ public class MessageMetaItem : ContentMetaItem {
|
||||||
message_item = content_item as MessageItem;
|
message_item = content_item as MessageItem;
|
||||||
this.stream_interactor = stream_interactor;
|
this.stream_interactor = stream_interactor;
|
||||||
|
|
||||||
|
init.begin();
|
||||||
|
|
||||||
label.activate_link.connect(on_label_activate_link);
|
label.activate_link.connect(on_label_activate_link);
|
||||||
|
|
||||||
Message message = ((MessageItem) content_item).message;
|
Message message = ((MessageItem) content_item).message;
|
||||||
|
@ -68,6 +71,10 @@ public class MessageMetaItem : ContentMetaItem {
|
||||||
update_label();
|
update_label();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async void init() {
|
||||||
|
supports_reaction = yield stream_interactor.get_module(Reactions.IDENTITY).conversation_supports_reactions(message_item.conversation);
|
||||||
|
}
|
||||||
|
|
||||||
private string generate_markup_text(ContentItem item) {
|
private string generate_markup_text(ContentItem item) {
|
||||||
MessageItem message_item = item as MessageItem;
|
MessageItem message_item = item as MessageItem;
|
||||||
Conversation conversation = message_item.conversation;
|
Conversation conversation = message_item.conversation;
|
||||||
|
@ -187,11 +194,13 @@ public class MessageMetaItem : ContentMetaItem {
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Gee.List<Plugins.MessageAction>? get_item_actions(Plugins.WidgetType type) {
|
public override Gee.List<Plugins.MessageAction>? get_item_actions(Plugins.WidgetType type) {
|
||||||
if (content_item as FileItem != null) return null;
|
if (content_item as FileItem != null || this.in_edit_mode) return null;
|
||||||
|
if (in_edit_mode) return null;
|
||||||
|
|
||||||
bool allowed = stream_interactor.get_module(MessageCorrection.IDENTITY).is_own_correction_allowed(message_item.conversation, message_item.message);
|
|
||||||
Gee.List<Plugins.MessageAction> actions = new ArrayList<Plugins.MessageAction>();
|
Gee.List<Plugins.MessageAction> actions = new ArrayList<Plugins.MessageAction>();
|
||||||
if (allowed && !in_edit_mode) {
|
|
||||||
|
bool correction_allowed = stream_interactor.get_module(MessageCorrection.IDENTITY).is_own_correction_allowed(message_item.conversation, message_item.message);
|
||||||
|
if (correction_allowed) {
|
||||||
Plugins.MessageAction action1 = new Plugins.MessageAction();
|
Plugins.MessageAction action1 = new Plugins.MessageAction();
|
||||||
action1.icon_name = "document-edit-symbolic";
|
action1.icon_name = "document-edit-symbolic";
|
||||||
action1.callback = (button, content_meta_item_activated, widget) => {
|
action1.callback = (button, content_meta_item_activated, widget) => {
|
||||||
|
@ -199,6 +208,17 @@ public class MessageMetaItem : ContentMetaItem {
|
||||||
};
|
};
|
||||||
actions.add(action1);
|
actions.add(action1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (supports_reaction) {
|
||||||
|
Plugins.MessageAction action2 = new Plugins.MessageAction();
|
||||||
|
action2.icon_name = "dino-emoticon-add-symbolic";
|
||||||
|
EmojiChooser chooser = new EmojiChooser();
|
||||||
|
chooser.emoji_picked.connect((emoji) => {
|
||||||
|
stream_interactor.get_module(Reactions.IDENTITY).add_reaction(message_item.conversation, message_item, emoji);
|
||||||
|
});
|
||||||
|
action2.popover = chooser;
|
||||||
|
actions.add(action2);
|
||||||
|
}
|
||||||
return actions;
|
return actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
191
main/src/ui/conversation_content_view/reactions_widget.vala
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
using Gee;
|
||||||
|
using Gtk;
|
||||||
|
|
||||||
|
using Dino.Entities;
|
||||||
|
using Xmpp;
|
||||||
|
|
||||||
|
namespace Dino.Ui.ConversationSummary {
|
||||||
|
|
||||||
|
public class ReactionsController : Object {
|
||||||
|
public signal void box_activated(Widget widget);
|
||||||
|
|
||||||
|
private Conversation conversation;
|
||||||
|
private Account account;
|
||||||
|
private ContentItem content_item;
|
||||||
|
private StreamInteractor stream_interactor;
|
||||||
|
|
||||||
|
private HashMap<string, Gee.List<Jid>> reactions = new HashMap<string, Gee.List<Jid>>();
|
||||||
|
|
||||||
|
private ReactionsWidget? widget = null;
|
||||||
|
|
||||||
|
public ReactionsController(Conversation conversation, ContentItem content_item, StreamInteractor stream_interactor) {
|
||||||
|
this.conversation = conversation;
|
||||||
|
this.account = conversation.account;
|
||||||
|
this.content_item = content_item;
|
||||||
|
this.stream_interactor = stream_interactor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void init() {
|
||||||
|
Gee.List<ReactionUsers> reactions = stream_interactor.get_module(Reactions.IDENTITY).get_item_reactions(conversation, content_item);
|
||||||
|
if (reactions.size > 0) {
|
||||||
|
initialize_widget();
|
||||||
|
}
|
||||||
|
foreach (ReactionUsers reaction_users in reactions) {
|
||||||
|
foreach (Jid jid in reaction_users.jids) {
|
||||||
|
reaction_added(reaction_users.reaction, jid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stream_interactor.get_module(Reactions.IDENTITY).reaction_added.connect((account, content_item_id, jid, reaction) => {
|
||||||
|
if (this.content_item.id == content_item_id) {
|
||||||
|
reaction_added(reaction, jid);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
stream_interactor.get_module(Reactions.IDENTITY).reaction_removed.connect((account, content_item_id, jid, reaction) => {
|
||||||
|
if (this.content_item.id == content_item_id) {
|
||||||
|
reaction_removed(reaction, jid);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initialize_widget() {
|
||||||
|
widget = new ReactionsWidget();
|
||||||
|
widget.emoji_picked.connect((emoji) => {
|
||||||
|
stream_interactor.get_module(Reactions.IDENTITY).add_reaction(conversation, content_item, emoji);
|
||||||
|
});
|
||||||
|
widget.emoji_clicked.connect((emoji) => {
|
||||||
|
if (account.bare_jid in reactions[emoji]) {
|
||||||
|
stream_interactor.get_module(Reactions.IDENTITY).remove_reaction(conversation, content_item, emoji);
|
||||||
|
} else {
|
||||||
|
stream_interactor.get_module(Reactions.IDENTITY).add_reaction(conversation, content_item, emoji);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
box_activated(widget);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reaction_added(string reaction, Jid jid) {
|
||||||
|
if (widget == null) {
|
||||||
|
initialize_widget();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!reactions.has_key(reaction)) {
|
||||||
|
reactions[reaction] = new ArrayList<Jid>(Jid.equals_func);
|
||||||
|
}
|
||||||
|
if (jid.equals_bare(account.bare_jid) && reactions[reaction].contains(jid)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reactions[reaction].add(jid);
|
||||||
|
|
||||||
|
if (reactions[reaction].size == 0) return;
|
||||||
|
|
||||||
|
widget.update_reaction(reaction, reactions[reaction].size, reactions[reaction].contains(account.bare_jid), update_tooltip(reaction));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reaction_removed(string reaction, Jid jid) {
|
||||||
|
if (!reactions.has_key(reaction)) return;
|
||||||
|
reactions[reaction].remove(jid);
|
||||||
|
|
||||||
|
if (reactions[reaction].size > 0) {
|
||||||
|
widget.update_reaction(reaction, reactions[reaction].size, reactions[reaction].contains(account.bare_jid), update_tooltip(reaction));
|
||||||
|
} else {
|
||||||
|
widget.remove_reaction(reaction);
|
||||||
|
reactions.unset(reaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reactions.size == 0) {
|
||||||
|
widget.unparent();
|
||||||
|
widget = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Gee.List<string> update_tooltip(string reaction) {
|
||||||
|
var name_list = new ArrayList<string>();
|
||||||
|
if (reactions[reaction].size > 0) {
|
||||||
|
if (account.bare_jid in reactions[reaction]) {
|
||||||
|
name_list.add(_("You"));
|
||||||
|
}
|
||||||
|
foreach (Jid jid in reactions[reaction]) {
|
||||||
|
if (jid.equals(account.bare_jid)) continue;
|
||||||
|
|
||||||
|
name_list.add(Util.get_participant_display_name(stream_interactor, conversation, jid));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return name_list;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ReactionsWidget : Grid {
|
||||||
|
|
||||||
|
public signal void emoji_picked(string emoji);
|
||||||
|
public signal void emoji_clicked(string emoji);
|
||||||
|
|
||||||
|
private HashMap<string, Label> reaction_counts = new HashMap<string, Label>();
|
||||||
|
private HashMap<string, Button> reaction_buttons = new HashMap<string, Button>();
|
||||||
|
private MenuButton add_button;
|
||||||
|
|
||||||
|
public ReactionsWidget() {
|
||||||
|
this.row_spacing = this.column_spacing = 5;
|
||||||
|
this.margin_top = 2;
|
||||||
|
|
||||||
|
add_button = new MenuButton() { tooltip_text= _("Add reaction") };
|
||||||
|
add_button.get_style_context().add_class("reaction-box");
|
||||||
|
Util.menu_button_set_icon_with_size(add_button, "dino-emoticon-add-symbolic", 14);
|
||||||
|
|
||||||
|
EmojiChooser chooser = new EmojiChooser();
|
||||||
|
chooser.emoji_picked.connect((emoji) => {
|
||||||
|
emoji_picked(emoji);
|
||||||
|
});
|
||||||
|
add_button.set_popover(chooser);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void update_reaction(string reaction, int count, bool own, Gee.List<string> names) {
|
||||||
|
if (!reaction_buttons.has_key(reaction)) {
|
||||||
|
Label reaction_label = new Label("<span size='small'>" + reaction + "</span>") { use_markup=true };
|
||||||
|
Label count_label = new Label("") { use_markup=true };
|
||||||
|
Button button = new Button();
|
||||||
|
button.get_style_context().add_class("reaction-box");
|
||||||
|
Box reaction_box = new Box(Orientation.HORIZONTAL, 4);
|
||||||
|
reaction_box.append(reaction_label);
|
||||||
|
reaction_box.append(count_label);
|
||||||
|
button.set_child(reaction_box);
|
||||||
|
|
||||||
|
reaction_counts[reaction] = count_label;
|
||||||
|
reaction_buttons[reaction] = button;
|
||||||
|
|
||||||
|
this.attach(button, (reaction_buttons.size - 1) % 10, (reaction_buttons.size - 1) / 10, 1, 1);
|
||||||
|
if (add_button.get_parent() != null) this.remove(add_button);
|
||||||
|
this.attach(add_button, reaction_buttons.size % 10, reaction_buttons.size / 10, 1, 1);
|
||||||
|
|
||||||
|
|
||||||
|
button.clicked.connect(() => {
|
||||||
|
emoji_clicked(reaction);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
reaction_counts[reaction].label = "<span font_family='monospace' size='small'>" + count.to_string() + "</span>";
|
||||||
|
if (own) {
|
||||||
|
reaction_buttons[reaction].get_style_context().add_class("own-reaction");
|
||||||
|
} else {
|
||||||
|
reaction_buttons[reaction].get_style_context().remove_class("own-reaction");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build tooltip
|
||||||
|
StringBuilder tooltip_builder = new StringBuilder ();
|
||||||
|
for (int i = 0; i < names.size - 1; i++) {
|
||||||
|
tooltip_builder.append(names[i]);
|
||||||
|
if (i < names.size - 2) tooltip_builder.append(", ");
|
||||||
|
}
|
||||||
|
if (names.size > 1) {
|
||||||
|
tooltip_builder.append(" and ");
|
||||||
|
}
|
||||||
|
tooltip_builder.append(names[names.size - 1]);
|
||||||
|
tooltip_builder.append(" reacted with " + reaction);
|
||||||
|
reaction_buttons[reaction].set_tooltip_text(tooltip_builder.str);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void remove_reaction(string reaction) {
|
||||||
|
reaction_buttons[reaction].unparent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -55,12 +55,6 @@ public class ConversationView : Widget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void dispose() {
|
|
||||||
// To prevent a warning when closing Dino
|
|
||||||
// "Can't set a target list on a widget until you've called gtk_drag_dest_set() to make the widget into a drag destination"
|
|
||||||
// Gtk.drag_dest_unset(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void on_upper_notify() {
|
private void on_upper_notify() {
|
||||||
print("on_upper_notify\n");
|
print("on_upper_notify\n");
|
||||||
if (at_current_content) {
|
if (at_current_content) {
|
||||||
|
|
|
@ -82,7 +82,7 @@ public class ConversationViewController : Object {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
stream_interactor.get_module(RosterManager.IDENTITY).updated_roster_item.connect((account, jid, roster_item) => {
|
stream_interactor.get_module(RosterManager.IDENTITY).updated_roster_item.connect((account, jid, roster_item) => {
|
||||||
if (conversation.account.equals(account) && conversation.counterpart.equals(jid)) {
|
if (conversation != null && conversation.account.equals(account) && conversation.counterpart.equals(jid)) {
|
||||||
update_conversation_display_name();
|
update_conversation_display_name();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -154,20 +154,20 @@ public class ConversationViewController : Object {
|
||||||
conversation_topic = null;
|
conversation_topic = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void update_file_upload_status() {
|
private async void update_file_upload_status() {
|
||||||
stream_interactor.get_module(FileManager.IDENTITY).is_upload_available.begin(conversation, (_, res) => {
|
if (conversation == null) return;
|
||||||
bool upload_available = stream_interactor.get_module(FileManager.IDENTITY).is_upload_available.end(res);
|
|
||||||
chat_input_controller.set_file_upload_active(upload_available);
|
bool upload_available = yield stream_interactor.get_module(FileManager.IDENTITY).is_upload_available(conversation);
|
||||||
if (conversation.account.bare_jid.to_string().has_prefix("f")) {
|
chat_input_controller.set_file_upload_active(upload_available);
|
||||||
if (drop_event_controller.widget == null) {
|
if (upload_available && overlay_dialog == null) {
|
||||||
view.add_controller(drop_event_controller);
|
if (drop_event_controller.widget == null) {
|
||||||
}
|
view.add_controller(drop_event_controller);
|
||||||
} else {
|
|
||||||
if (drop_event_controller.widget != null) {
|
|
||||||
view.remove_controller(drop_event_controller);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
} else {
|
||||||
|
if (drop_event_controller.widget != null) {
|
||||||
|
view.remove_controller(drop_event_controller);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void update_conversation_display_name() {
|
private void update_conversation_display_name() {
|
||||||
|
|
|
@ -77,7 +77,7 @@ public class MainWindow : Gtk.Window {
|
||||||
search_frame.set_child(global_search.get_widget());
|
search_frame.set_child(global_search.get_widget());
|
||||||
|
|
||||||
Image conversation_list_placeholder_image = (Image) builder.get_object("conversation_list_placeholder_image");
|
Image conversation_list_placeholder_image = (Image) builder.get_object("conversation_list_placeholder_image");
|
||||||
conversation_list_placeholder_image.set_from_pixbuf(new Pixbuf.from_resource("/im/dino/Dino/icons/scalable/ui/dino-conversation-list-placeholder-arrow.svg"));
|
conversation_list_placeholder_image.set_from_pixbuf(new Pixbuf.from_resource("/im/dino/Dino/dino-conversation-list-placeholder-arrow.svg"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setup_headerbar() {
|
private void setup_headerbar() {
|
||||||
|
|
|
@ -134,7 +134,7 @@ public class AddAccountDialog : Gtk.Dialog {
|
||||||
create_account_box.visible = false;
|
create_account_box.visible = false;
|
||||||
register_box.visible = false;
|
register_box.visible = false;
|
||||||
success_box.visible = false;
|
success_box.visible = false;
|
||||||
// set_default(sign_in_jid_continue_button);
|
set_default_widget(sign_in_jid_continue_button);
|
||||||
|
|
||||||
sign_in_jid_error_label.label = "";
|
sign_in_jid_error_label.label = "";
|
||||||
jid_entry.sensitive = true;
|
jid_entry.sensitive = true;
|
||||||
|
@ -174,7 +174,7 @@ public class AddAccountDialog : Gtk.Dialog {
|
||||||
create_account_box.visible = false;
|
create_account_box.visible = false;
|
||||||
register_box.visible = false;
|
register_box.visible = false;
|
||||||
success_box.visible = false;
|
success_box.visible = false;
|
||||||
// set_default(sign_in_password_continue_button);
|
set_default_widget(sign_in_password_continue_button);
|
||||||
|
|
||||||
sign_in_password_error_label.label = "";
|
sign_in_password_error_label.label = "";
|
||||||
sign_in_password_title.label = _("Sign in to %s").printf(login_jid.to_string());
|
sign_in_password_title.label = _("Sign in to %s").printf(login_jid.to_string());
|
||||||
|
@ -184,7 +184,7 @@ public class AddAccountDialog : Gtk.Dialog {
|
||||||
private void show_select_server() {
|
private void show_select_server() {
|
||||||
server_entry.text = "";
|
server_entry.text = "";
|
||||||
server_entry.grab_focus();
|
server_entry.grab_focus();
|
||||||
// set_default(select_server_continue);
|
set_default_widget(select_server_continue);
|
||||||
|
|
||||||
server_list_box.row_activated.disconnect(on_server_list_row_activated);
|
server_list_box.row_activated.disconnect(on_server_list_row_activated);
|
||||||
server_list_box.unselect_all();
|
server_list_box.unselect_all();
|
||||||
|
@ -209,7 +209,7 @@ public class AddAccountDialog : Gtk.Dialog {
|
||||||
create_account_box.visible = false;
|
create_account_box.visible = false;
|
||||||
success_box.visible = false;
|
success_box.visible = false;
|
||||||
|
|
||||||
// set_default(register_form_continue);
|
set_default_widget(register_form_continue);
|
||||||
animate_window_resize(register_box);
|
animate_window_resize(register_box);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -223,7 +223,7 @@ public class AddAccountDialog : Gtk.Dialog {
|
||||||
register_box.visible = false;
|
register_box.visible = false;
|
||||||
success_description.label = _("You can now use the account %s.").printf("<b>" + Markup.escape_text(account.bare_jid.to_string()) + "</b>");
|
success_description.label = _("You can now use the account %s.").printf("<b>" + Markup.escape_text(account.bare_jid.to_string()) + "</b>");
|
||||||
|
|
||||||
// set_default(success_continue_button);
|
set_default_widget(success_continue_button);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void on_jid_entry_changed() {
|
private void on_jid_entry_changed() {
|
||||||
|
@ -334,7 +334,7 @@ public class AddAccountDialog : Gtk.Dialog {
|
||||||
form_box.remove(widget);
|
form_box.remove(widget);
|
||||||
widget = form_box.get_first_child();
|
widget = form_box.get_first_child();
|
||||||
}
|
}
|
||||||
// form_box.foreach((widget) => { form_box.remove(widget); });
|
|
||||||
register_title.label = _("Register on %s").printf(server.to_string());
|
register_title.label = _("Register on %s").printf(server.to_string());
|
||||||
|
|
||||||
if (form.oob != null) {
|
if (form.oob != null) {
|
||||||
|
|
|
@ -452,4 +452,19 @@ public bool use_tooltips() {
|
||||||
return Gtk.MINOR_VERSION != 6 || (Gtk.MICRO_VERSION < 4 || Gtk.MICRO_VERSION > 6);
|
return Gtk.MINOR_VERSION != 6 || (Gtk.MICRO_VERSION < 4 || Gtk.MICRO_VERSION > 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void menu_button_set_icon_with_size(MenuButton menu_button, string icon_name, int pixel_size) {
|
||||||
|
#if GTK_4_6
|
||||||
|
menu_button.set_child(new Image.from_icon_name(icon_name) { pixel_size=pixel_size });
|
||||||
|
#else
|
||||||
|
menu_button.set_icon_name(icon_name);
|
||||||
|
var button = menu_button.get_first_child() as Button;
|
||||||
|
if (button == null) return;
|
||||||
|
var box = button.child as Box;
|
||||||
|
if (box == null) return;
|
||||||
|
var image = box.get_first_child() as Image;
|
||||||
|
if (image == null) return;
|
||||||
|
image.pixel_size = pixel_size;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -104,9 +104,9 @@ public class ContactDetailsDialog : Gtk.Dialog {
|
||||||
|
|
||||||
const int QUIET_ZONE_MODULES = 4; // MUST be at least 4
|
const int QUIET_ZONE_MODULES = 4; // MUST be at least 4
|
||||||
const int MODULE_SIZE_PX = 4; // arbitrary
|
const int MODULE_SIZE_PX = 4; // arbitrary
|
||||||
var qr_pixbuf = new QRcode(iri, 2)
|
var qr_paintable = new QRcode(iri, 2)
|
||||||
.to_pixbuf(MODULE_SIZE_PX * qrcode_image.scale_factor);
|
.to_paintable(MODULE_SIZE_PX * qrcode_image.scale_factor);
|
||||||
qrcode_image.set_from_pixbuf(qr_pixbuf);
|
qrcode_image.paintable = qr_paintable;
|
||||||
qrcode_image.margin_top = qrcode_image.margin_end =
|
qrcode_image.margin_top = qrcode_image.margin_end =
|
||||||
qrcode_image.margin_bottom = qrcode_image.margin_start = QUIET_ZONE_MODULES*MODULE_SIZE_PX;
|
qrcode_image.margin_bottom = qrcode_image.margin_start = QUIET_ZONE_MODULES*MODULE_SIZE_PX;
|
||||||
qrcode_popover.add_css_class("qrcode-container");
|
qrcode_popover.add_css_class("qrcode-container");
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
using Gdk;
|
|
||||||
|
|
||||||
[CCode (cheader_filename = "qrencode.h")]
|
[CCode (cheader_filename = "qrencode.h")]
|
||||||
namespace Qrencode {
|
namespace Qrencode {
|
||||||
|
|
||||||
|
@ -36,13 +34,14 @@ namespace Qrencode {
|
||||||
[CCode (cname = "QRcode_encodeString")]
|
[CCode (cname = "QRcode_encodeString")]
|
||||||
public QRcode (string str, int version = 0, ECLevel level = ECLevel.L, EncodeMode hint = EncodeMode.EIGHT_BIT, bool casesensitive = true);
|
public QRcode (string str, int version = 0, ECLevel level = ECLevel.L, EncodeMode hint = EncodeMode.EIGHT_BIT, bool casesensitive = true);
|
||||||
|
|
||||||
public Pixbuf to_pixbuf(int module_size) {
|
public Gdk.Paintable to_paintable(int module_size) {
|
||||||
GLib.assert(module_size > 0);
|
GLib.assert(module_size > 0);
|
||||||
var dst_width = width*module_size;
|
var dst_width = width*module_size;
|
||||||
var dst_data = new uint8[dst_width*dst_width*3];
|
var dst_data = new uint8[dst_width*dst_width*3];
|
||||||
expand_and_upsample(data,width,width, dst_data,dst_width,dst_width);
|
expand_and_upsample(data,width,width, dst_data,dst_width,dst_width);
|
||||||
return new Pixbuf.from_data(dst_data,
|
return new Gdk.MemoryTexture(dst_width, dst_width, Gdk.MemoryFormat.R8G8B8,
|
||||||
Colorspace.RGB, false, 8, dst_width, dst_width, dst_width*3);
|
new GLib.Bytes.take((owned) dst_data), dst_width*3);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Does 2D nearest-neighbor upsampling of an array of single-byte
|
/** Does 2D nearest-neighbor upsampling of an array of single-byte
|
||||||
|
|
|
@ -75,6 +75,7 @@ SOURCES
|
||||||
|
|
||||||
"src/module/xep/0047_in_band_bytestreams.vala"
|
"src/module/xep/0047_in_band_bytestreams.vala"
|
||||||
"src/module/xep/0049_private_xml_storage.vala"
|
"src/module/xep/0049_private_xml_storage.vala"
|
||||||
|
"src/module/xep/0059_result_set_management.vala"
|
||||||
"src/module/xep/0054_vcard/module.vala"
|
"src/module/xep/0054_vcard/module.vala"
|
||||||
"src/module/xep/0060_pubsub.vala"
|
"src/module/xep/0060_pubsub.vala"
|
||||||
"src/module/xep/0065_socks5_bytestreams.vala"
|
"src/module/xep/0065_socks5_bytestreams.vala"
|
||||||
|
@ -126,9 +127,11 @@ SOURCES
|
||||||
"src/module/xep/0261_jingle_in_band_bytestreams.vala"
|
"src/module/xep/0261_jingle_in_band_bytestreams.vala"
|
||||||
"src/module/xep/0272_muji.vala"
|
"src/module/xep/0272_muji.vala"
|
||||||
"src/module/xep/0280_message_carbons.vala"
|
"src/module/xep/0280_message_carbons.vala"
|
||||||
|
"src/module/xep/0297_stanza_forwarding.vala"
|
||||||
"src/module/xep/0298_coin.vala"
|
"src/module/xep/0298_coin.vala"
|
||||||
"src/module/xep/0308_last_message_correction.vala"
|
"src/module/xep/0308_last_message_correction.vala"
|
||||||
"src/module/xep/0313_message_archive_management.vala"
|
"src/module/xep/0313_message_archive_management.vala"
|
||||||
|
"src/module/xep/0313_2_message_archive_management.vala"
|
||||||
"src/module/xep/0333_chat_markers.vala"
|
"src/module/xep/0333_chat_markers.vala"
|
||||||
"src/module/xep/0334_message_processing_hints.vala"
|
"src/module/xep/0334_message_processing_hints.vala"
|
||||||
"src/module/xep/0353_jingle_message_initiation.vala"
|
"src/module/xep/0353_jingle_message_initiation.vala"
|
||||||
|
@ -138,6 +141,8 @@ SOURCES
|
||||||
"src/module/xep/0380_explicit_encryption.vala"
|
"src/module/xep/0380_explicit_encryption.vala"
|
||||||
"src/module/xep/0391_jingle_encrypted_transports.vala"
|
"src/module/xep/0391_jingle_encrypted_transports.vala"
|
||||||
"src/module/xep/0410_muc_self_ping.vala"
|
"src/module/xep/0410_muc_self_ping.vala"
|
||||||
|
"src/module/xep/0421_occupant_ids.vala"
|
||||||
|
"src/module/xep/0444_reactions.vala"
|
||||||
"src/module/xep/pixbuf_storage.vala"
|
"src/module/xep/pixbuf_storage.vala"
|
||||||
|
|
||||||
"src/util.vala"
|
"src/util.vala"
|
||||||
|
|
|
@ -81,7 +81,7 @@ public class Module : XmppStreamModule {
|
||||||
received_pipeline_listener = new ReceivedPipelineListener(this);
|
received_pipeline_listener = new ReceivedPipelineListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async JoinResult? enter(XmppStream stream, Jid bare_jid, string nick, string? password, DateTime? history_since, StanzaNode? additional_node) {
|
public async JoinResult? enter(XmppStream stream, Jid bare_jid, string nick, string? password, DateTime? history_since, bool receive_history, StanzaNode? additional_node) {
|
||||||
try {
|
try {
|
||||||
Presence.Stanza presence = new Presence.Stanza();
|
Presence.Stanza presence = new Presence.Stanza();
|
||||||
presence.to = bare_jid.with_resource(nick);
|
presence.to = bare_jid.with_resource(nick);
|
||||||
|
@ -90,10 +90,15 @@ public class Module : XmppStreamModule {
|
||||||
if (password != null) {
|
if (password != null) {
|
||||||
x_node.put_node(new StanzaNode.build("password", NS_URI).put_node(new StanzaNode.text(password)));
|
x_node.put_node(new StanzaNode.build("password", NS_URI).put_node(new StanzaNode.text(password)));
|
||||||
}
|
}
|
||||||
if (history_since != null) {
|
if (history_since != null || !receive_history) {
|
||||||
StanzaNode history_node = new StanzaNode.build("history", NS_URI);
|
StanzaNode history_node = new StanzaNode.build("history", NS_URI);
|
||||||
history_node.set_attribute("since", DateTimeProfiles.to_datetime(history_since));
|
|
||||||
x_node.put_node(history_node);
|
x_node.put_node(history_node);
|
||||||
|
|
||||||
|
if (history_since != null) {
|
||||||
|
history_node.set_attribute("since", DateTimeProfiles.to_datetime(history_since));
|
||||||
|
} else if (!receive_history) {
|
||||||
|
history_node.set_attribute("maxchars", "0");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
presence.stanza.put_node(x_node);
|
presence.stanza.put_node(x_node);
|
||||||
|
|
||||||
|
@ -561,7 +566,7 @@ public class ReceivedPipelineListener : StanzaListener<MessageStanza> {
|
||||||
StanzaNode? reason_node = invite_node.get_subnode("reason", NS_URI_USER);
|
StanzaNode? reason_node = invite_node.get_subnode("reason", NS_URI_USER);
|
||||||
string? reason = null;
|
string? reason = null;
|
||||||
if (reason_node != null) reason = reason_node.get_string_content();
|
if (reason_node != null) reason = reason_node.get_string_content();
|
||||||
bool is_mam_message = Xep.MessageArchiveManagement.MessageFlag.get_flag(message) != null; // TODO
|
bool is_mam_message = Xmpp.MessageArchiveManagement.MessageFlag.get_flag(message) != null; // TODO
|
||||||
if (!is_mam_message) outer.invite_received(stream, message.from, from_jid, password, reason);
|
if (!is_mam_message) outer.invite_received(stream, message.from, from_jid, password, reason);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
30
xmpp-vala/src/module/xep/0059_result_set_management.vala
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
namespace Xmpp.ResultSetManagement {
|
||||||
|
public const string NS_URI = "http://jabber.org/protocol/rsm";
|
||||||
|
|
||||||
|
public class ResultSetParameters {
|
||||||
|
string? before { get; set; }
|
||||||
|
string? after { get; set; }
|
||||||
|
int? max { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public StanzaNode create_set_rsm_node_before(string? before_id) {
|
||||||
|
var max_node = (new StanzaNode.build("max", Xmpp.ResultSetManagement.NS_URI)).put_node(new StanzaNode.text("20"));
|
||||||
|
var node = (new StanzaNode.build("set", Xmpp.ResultSetManagement.NS_URI)).add_self_xmlns()
|
||||||
|
.put_node(max_node);
|
||||||
|
var before_node = new StanzaNode.build("before", Xmpp.ResultSetManagement.NS_URI);
|
||||||
|
if (before_id != null) before_node.put_node(new StanzaNode.text(before_id));
|
||||||
|
node.put_node(before_node);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StanzaNode create_set_rsm_node_after(string after_id) {
|
||||||
|
var max_node = (new StanzaNode.build("max", Xmpp.ResultSetManagement.NS_URI)).put_node(new StanzaNode.text("20"));
|
||||||
|
var node = (new StanzaNode.build("set", Xmpp.ResultSetManagement.NS_URI)).add_self_xmlns()
|
||||||
|
.put_node(max_node);
|
||||||
|
|
||||||
|
var after_node = new StanzaNode.build("after", Xmpp.ResultSetManagement.NS_URI)
|
||||||
|
.put_node(new StanzaNode.text(after_id));
|
||||||
|
node.put_node(after_node);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
namespace Xmpp.Xep.DelayedDelivery {
|
namespace Xmpp.Xep.DelayedDelivery {
|
||||||
|
|
||||||
private const string NS_URI = "urn:xmpp:delay";
|
public const string NS_URI = "urn:xmpp:delay";
|
||||||
|
|
||||||
public static DateTime? get_time_for_node(StanzaNode node) {
|
public static DateTime? get_time_for_node(StanzaNode node) {
|
||||||
string? time = node.get_attribute("stamp");
|
string? time = node.get_attribute("stamp");
|
||||||
|
|
|
@ -15,7 +15,7 @@ namespace Xmpp.Xep.Muji {
|
||||||
|
|
||||||
group_call.our_nick = "%08x".printf(Random.next_int());
|
group_call.our_nick = "%08x".printf(Random.next_int());
|
||||||
debug(@"[%s] MUJI joining as %s", stream.get_flag(Bind.Flag.IDENTITY).my_jid.to_string(), group_call.our_nick);
|
debug(@"[%s] MUJI joining as %s", stream.get_flag(Bind.Flag.IDENTITY).my_jid.to_string(), group_call.our_nick);
|
||||||
Xep.Muc.JoinResult? result = yield stream.get_module(Muc.Module.IDENTITY).enter(stream, muc_jid, group_call.our_nick, null, null, initial_muji_node);
|
Xep.Muc.JoinResult? result = yield stream.get_module(Muc.Module.IDENTITY).enter(stream, muc_jid, group_call.our_nick, null, null, false, initial_muji_node);
|
||||||
if (result == null || result.nick == null) return null;
|
if (result == null || result.nick == null) return null;
|
||||||
debug(@"[%s] MUJI joining as %s done", stream.get_flag(Bind.Flag.IDENTITY).my_jid.to_string(), group_call.our_nick);
|
debug(@"[%s] MUJI joining as %s done", stream.get_flag(Bind.Flag.IDENTITY).my_jid.to_string(), group_call.our_nick);
|
||||||
|
|
||||||
|
|
3
xmpp-vala/src/module/xep/0297_stanza_forwarding.vala
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
namespace Xmpp.StanzaForwarding {
|
||||||
|
public const string NS_URI = "urn:xmpp:forward:0";
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
using Gee;
|
||||||
|
using Xmpp.Xep;
|
||||||
|
|
||||||
|
namespace Xmpp.MessageArchiveManagement.V2 {
|
||||||
|
|
||||||
|
public class MamQueryParams {
|
||||||
|
public bool use_ns2_extended = false;
|
||||||
|
|
||||||
|
public string query_id = Xmpp.random_uuid();
|
||||||
|
public Jid mam_server { get; set; }
|
||||||
|
public Jid? with { get; set; }
|
||||||
|
// "The 'start' field is used to filter out messages before a certain date/time."
|
||||||
|
public DateTime? start { get; set; }
|
||||||
|
// "the 'end' field is used to exclude from the results messages after a certain point in time"
|
||||||
|
public DateTime? end { get; set; }
|
||||||
|
public string? start_id { get; set; }
|
||||||
|
public string? end_id { get; set; }
|
||||||
|
|
||||||
|
public MamQueryParams.query_latest(Jid mam_server, DateTime? latest_known_time, string? latest_known_id) {
|
||||||
|
this.mam_server = mam_server;
|
||||||
|
this.start = latest_known_time;
|
||||||
|
this.start_id = latest_known_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MamQueryParams.query_between(Jid mam_server,
|
||||||
|
DateTime? earliest_time, string? earliest_id,
|
||||||
|
DateTime? latest_time, string? latest_id) {
|
||||||
|
this.mam_server = mam_server;
|
||||||
|
this.start = earliest_time;
|
||||||
|
this.start_id = earliest_id;
|
||||||
|
this.end = latest_time;
|
||||||
|
this.end_id = latest_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MamQueryParams.query_before(Jid mam_server, DateTime? earliest_time, string? earliest_id) {
|
||||||
|
this.mam_server = mam_server;
|
||||||
|
this.end = earliest_time;
|
||||||
|
this.end_id = earliest_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private StanzaNode create_base_query(XmppStream stream, MamQueryParams mam_params) {
|
||||||
|
var fields = new ArrayList<DataForms.DataForm.Field>();
|
||||||
|
|
||||||
|
if (mam_params.with != null) {
|
||||||
|
DataForms.DataForm.Field field = new DataForms.DataForm.Field() { var="with" };
|
||||||
|
field.set_value_string(mam_params.with.to_string());
|
||||||
|
fields.add(field);
|
||||||
|
}
|
||||||
|
if (mam_params.start != null) {
|
||||||
|
DataForms.DataForm.Field field = new DataForms.DataForm.Field() { var="start" };
|
||||||
|
field.set_value_string(DateTimeProfiles.to_datetime(mam_params.start));
|
||||||
|
fields.add(field);
|
||||||
|
}
|
||||||
|
if (mam_params.end != null) {
|
||||||
|
DataForms.DataForm.Field field = new DataForms.DataForm.Field() { var="end" };
|
||||||
|
field.set_value_string(DateTimeProfiles.to_datetime(mam_params.end));
|
||||||
|
fields.add(field);
|
||||||
|
}
|
||||||
|
|
||||||
|
return MessageArchiveManagement.create_base_query(stream, MessageArchiveManagement.NS_URI_2, mam_params.query_id, fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async QueryResult query_archive(XmppStream stream, MamQueryParams mam_params) {
|
||||||
|
var query_node = create_base_query(stream, mam_params);
|
||||||
|
if (!mam_params.use_ns2_extended) {
|
||||||
|
query_node.put_node(ResultSetManagement.create_set_rsm_node_before(mam_params.end_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
return yield MessageArchiveManagement.query_archive(stream, MessageArchiveManagement.NS_URI_2, mam_params.mam_server, query_node);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async QueryResult page_through_results(XmppStream stream, MamQueryParams mam_params, QueryResult prev_result) {
|
||||||
|
var query_node = create_base_query(stream, mam_params);
|
||||||
|
query_node.put_node(ResultSetManagement.create_set_rsm_node_before(prev_result.first));
|
||||||
|
|
||||||
|
return yield MessageArchiveManagement.query_archive(stream, MessageArchiveManagement.NS_URI_2, mam_params.mam_server, query_node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,11 +1,18 @@
|
||||||
namespace Xmpp.Xep.MessageArchiveManagement {
|
using Gee;
|
||||||
|
using Xmpp.Xep;
|
||||||
|
|
||||||
|
namespace Xmpp.MessageArchiveManagement {
|
||||||
|
|
||||||
public const string NS_URI = "urn:xmpp:mam:2";
|
public const string NS_URI = "urn:xmpp:mam:2";
|
||||||
public const string NS_URI_2 = "urn:xmpp:mam:2";
|
public const string NS_URI_2 = "urn:xmpp:mam:2";
|
||||||
public const string NS_URI_1 = "urn:xmpp:mam:1";
|
public const string NS_URI_1 = "urn:xmpp:mam:1";
|
||||||
|
|
||||||
private static string NS_VER(XmppStream stream) {
|
public class QueryResult {
|
||||||
return stream.get_flag(Flag.IDENTITY).ns_ver;
|
public bool error { get; set; default=false; }
|
||||||
|
public bool malformed { get; set; default=false; }
|
||||||
|
public bool complete { get; set; default=false; }
|
||||||
|
public string first { get; set; }
|
||||||
|
public string last { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Module : XmppStreamModule {
|
public class Module : XmppStreamModule {
|
||||||
|
@ -15,54 +22,6 @@ public class Module : XmppStreamModule {
|
||||||
|
|
||||||
private ReceivedPipelineListener received_pipeline_listener = new ReceivedPipelineListener();
|
private ReceivedPipelineListener received_pipeline_listener = new ReceivedPipelineListener();
|
||||||
|
|
||||||
private StanzaNode crate_base_query(XmppStream stream, string? jid, string? queryid, DateTime? start, DateTime? end) {
|
|
||||||
DataForms.DataForm data_form = new DataForms.DataForm();
|
|
||||||
DataForms.DataForm.HiddenField form_type_field = new DataForms.DataForm.HiddenField() { var="FORM_TYPE" };
|
|
||||||
form_type_field.set_value_string(NS_VER(stream));
|
|
||||||
data_form.add_field(form_type_field);
|
|
||||||
if (jid != null) {
|
|
||||||
DataForms.DataForm.Field field = new DataForms.DataForm.Field() { var="with" };
|
|
||||||
field.set_value_string(jid);
|
|
||||||
data_form.add_field(field);
|
|
||||||
}
|
|
||||||
if (start != null) {
|
|
||||||
DataForms.DataForm.Field field = new DataForms.DataForm.Field() { var="start" };
|
|
||||||
field.set_value_string(DateTimeProfiles.to_datetime(start));
|
|
||||||
data_form.add_field(field);
|
|
||||||
}
|
|
||||||
if (end != null) {
|
|
||||||
DataForms.DataForm.Field field = new DataForms.DataForm.Field() { var="end" };
|
|
||||||
field.set_value_string(DateTimeProfiles.to_datetime(end));
|
|
||||||
data_form.add_field(field);
|
|
||||||
}
|
|
||||||
StanzaNode query_node = new StanzaNode.build("query", NS_VER(stream)).add_self_xmlns().put_node(data_form.get_submit_node());
|
|
||||||
if (queryid != null) {
|
|
||||||
query_node.put_attribute("queryid", queryid);
|
|
||||||
}
|
|
||||||
return query_node;
|
|
||||||
}
|
|
||||||
|
|
||||||
private StanzaNode create_set_rsm_node(string? before_id) {
|
|
||||||
var before_node = new StanzaNode.build("before", "http://jabber.org/protocol/rsm");
|
|
||||||
if (before_id != null) {
|
|
||||||
before_node.put_node(new StanzaNode.text(before_id));
|
|
||||||
}
|
|
||||||
var max_node = (new StanzaNode.build("max", "http://jabber.org/protocol/rsm")).put_node(new StanzaNode.text("20"));
|
|
||||||
return (new StanzaNode.build("set", "http://jabber.org/protocol/rsm")).add_self_xmlns()
|
|
||||||
.put_node(before_node)
|
|
||||||
.put_node(max_node);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Iq.Stanza? query_archive(XmppStream stream, string? jid, string? query_id, DateTime? start_time, string? start_id, DateTime? end_time, string? end_id) {
|
|
||||||
if (stream.get_flag(Flag.IDENTITY) == null) return null;
|
|
||||||
|
|
||||||
var query_node = crate_base_query(stream, jid, query_id, start_time, end_time);
|
|
||||||
query_node.put_node(create_set_rsm_node(end_id));
|
|
||||||
Iq.Stanza iq = new Iq.Stanza.set(query_node);
|
|
||||||
|
|
||||||
return yield stream.get_module(Iq.Module.IDENTITY).send_iq_async(stream, iq);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void attach(XmppStream stream) {
|
public override void attach(XmppStream stream) {
|
||||||
stream.get_module(MessageModule.IDENTITY).received_pipeline.connect(received_pipeline_listener);
|
stream.get_module(MessageModule.IDENTITY).received_pipeline.connect(received_pipeline_listener);
|
||||||
stream.stream_negotiated.connect(query_availability);
|
stream.stream_negotiated.connect(query_availability);
|
||||||
|
@ -75,25 +34,6 @@ public class Module : XmppStreamModule {
|
||||||
public override string get_ns() { return NS_URI; }
|
public override string get_ns() { return NS_URI; }
|
||||||
public override string get_id() { return IDENTITY.id; }
|
public override string get_id() { return IDENTITY.id; }
|
||||||
|
|
||||||
public async Iq.Stanza? page_through_results(XmppStream stream, string? jid, string? query_id, DateTime? start_time, DateTime? end_time, Iq.Stanza iq) {
|
|
||||||
|
|
||||||
string? complete = iq.stanza.get_deep_attribute("urn:xmpp:mam:2:fin", "complete");
|
|
||||||
if (complete == "true") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
string? first = iq.stanza.get_deep_string_content(NS_VER(stream) + ":fin", "http://jabber.org/protocol/rsm" + ":set", "first");
|
|
||||||
if (first == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var query_node = crate_base_query(stream, jid, query_id, start_time, end_time);
|
|
||||||
query_node.put_node(create_set_rsm_node(first));
|
|
||||||
|
|
||||||
Iq.Stanza paging_iq = new Iq.Stanza.set(query_node);
|
|
||||||
|
|
||||||
return yield stream.get_module(Iq.Module.IDENTITY).send_iq_async(stream, paging_iq);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void query_availability(XmppStream stream) {
|
private async void query_availability(XmppStream stream) {
|
||||||
Jid own_jid = stream.get_flag(Bind.Flag.IDENTITY).my_jid.bare_jid;
|
Jid own_jid = stream.get_flag(Bind.Flag.IDENTITY).my_jid.bare_jid;
|
||||||
|
|
||||||
|
@ -113,6 +53,52 @@ public class Module : XmppStreamModule {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal StanzaNode create_base_query(XmppStream stream, string ns, string? queryid, Gee.List<DataForms.DataForm.Field> fields) {
|
||||||
|
DataForms.DataForm data_form = new DataForms.DataForm();
|
||||||
|
|
||||||
|
DataForms.DataForm.HiddenField form_type_field = new DataForms.DataForm.HiddenField() { var="FORM_TYPE" };
|
||||||
|
form_type_field.set_value_string(NS_VER(stream));
|
||||||
|
data_form.add_field(form_type_field);
|
||||||
|
|
||||||
|
foreach (var field in fields) {
|
||||||
|
data_form.add_field(field);
|
||||||
|
}
|
||||||
|
|
||||||
|
StanzaNode query_node = new StanzaNode.build("query", NS_VER(stream)).add_self_xmlns().put_node(data_form.get_submit_node());
|
||||||
|
if (queryid != null) {
|
||||||
|
query_node.put_attribute("queryid", queryid);
|
||||||
|
}
|
||||||
|
return query_node;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async QueryResult query_archive(XmppStream stream, string ns, Jid? mam_server, StanzaNode query_node) {
|
||||||
|
var res = new QueryResult();
|
||||||
|
|
||||||
|
if (stream.get_flag(Flag.IDENTITY) == null) { res.error = true; return res; }
|
||||||
|
|
||||||
|
// Build and send query
|
||||||
|
Iq.Stanza iq = new Iq.Stanza.set(query_node) { to=mam_server };
|
||||||
|
|
||||||
|
print(@"OUT:\n$(iq.stanza.to_string())\n");
|
||||||
|
Iq.Stanza result_iq = yield stream.get_module(Iq.Module.IDENTITY).send_iq_async(stream, iq);
|
||||||
|
|
||||||
|
print(result_iq.stanza.to_string() + "\n");
|
||||||
|
|
||||||
|
// Parse the response IQ into a QueryResult.
|
||||||
|
StanzaNode? fin_node = result_iq.stanza.get_subnode("fin", ns);
|
||||||
|
if (fin_node == null) { print(@"$ns a1\n"); res.malformed = true; return res; }
|
||||||
|
|
||||||
|
StanzaNode? rsm_node = fin_node.get_subnode("set", Xmpp.ResultSetManagement.NS_URI);
|
||||||
|
if (rsm_node == null) { print("a2\n"); res.malformed = true; return res; }
|
||||||
|
|
||||||
|
res.first = rsm_node.get_deep_string_content("first");
|
||||||
|
res.last = rsm_node.get_deep_string_content("last");
|
||||||
|
if ((res.first == null) != (res.last == null)) { print("a3\n"); res.malformed = true; }
|
||||||
|
res.complete = fin_node.get_attribute_bool("complete", false, ns);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
public class ReceivedPipelineListener : StanzaListener<MessageStanza> {
|
public class ReceivedPipelineListener : StanzaListener<MessageStanza> {
|
||||||
|
|
||||||
private string[] after_actions_const = {};
|
private string[] after_actions_const = {};
|
||||||
|
@ -123,19 +109,13 @@ public class ReceivedPipelineListener : StanzaListener<MessageStanza> {
|
||||||
public override async bool run(XmppStream stream, MessageStanza message) {
|
public override async bool run(XmppStream stream, MessageStanza message) {
|
||||||
if (stream.get_flag(Flag.IDENTITY) == null) return false;
|
if (stream.get_flag(Flag.IDENTITY) == null) return false;
|
||||||
|
|
||||||
StanzaNode? message_node = message.stanza.get_deep_subnode(NS_VER(stream) + ":result", "urn:xmpp:forward:0:forwarded", Xmpp.NS_URI + ":message");
|
StanzaNode? message_node = message.stanza.get_deep_subnode(NS_VER(stream) + ":result", StanzaForwarding.NS_URI + ":forwarded", Xmpp.NS_URI + ":message");
|
||||||
if (message_node != null) {
|
if (message_node != null) {
|
||||||
// MAM messages must come from our server // TODO or a MUC server
|
StanzaNode? forward_node = message.stanza.get_deep_subnode(NS_VER(stream) + ":result", StanzaForwarding.NS_URI + ":forwarded", DelayedDelivery.NS_URI + ":delay");
|
||||||
if (!message.from.equals(stream.get_flag(Bind.Flag.IDENTITY).my_jid.bare_jid)) {
|
|
||||||
warning("Received alleged MAM message from %s, ignoring", message.from.to_string());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
StanzaNode? forward_node = message.stanza.get_deep_subnode(NS_VER(stream) + ":result", "urn:xmpp:forward:0:forwarded", DelayedDelivery.NS_URI + ":delay");
|
|
||||||
DateTime? datetime = DelayedDelivery.get_time_for_node(forward_node);
|
DateTime? datetime = DelayedDelivery.get_time_for_node(forward_node);
|
||||||
string? mam_id = message.stanza.get_deep_attribute(NS_VER(stream) + ":result", NS_VER(stream) + ":id");
|
string? mam_id = message.stanza.get_deep_attribute(NS_VER(stream) + ":result", NS_VER(stream) + ":id");
|
||||||
string? query_id = message.stanza.get_deep_attribute(NS_VER(stream) + ":result", NS_VER(stream) + ":queryid");
|
string? query_id = message.stanza.get_deep_attribute(NS_VER(stream) + ":result", NS_VER(stream) + ":queryid");
|
||||||
message.add_flag(new MessageFlag(datetime, mam_id, query_id));
|
message.add_flag(new MessageFlag(message.from, datetime, mam_id, query_id));
|
||||||
|
|
||||||
message.stanza = message_node;
|
message.stanza = message_node;
|
||||||
message.rerun_parsing = true;
|
message.rerun_parsing = true;
|
||||||
|
@ -160,11 +140,13 @@ public class Flag : XmppStreamFlag {
|
||||||
public class MessageFlag : Xmpp.MessageFlag {
|
public class MessageFlag : Xmpp.MessageFlag {
|
||||||
public const string ID = "message_archive_management";
|
public const string ID = "message_archive_management";
|
||||||
|
|
||||||
|
public Jid sender_jid { get; private set; }
|
||||||
public DateTime? server_time { get; private set; }
|
public DateTime? server_time { get; private set; }
|
||||||
public string? mam_id { get; private set; }
|
public string? mam_id { get; private set; }
|
||||||
public string? query_id { get; private set; }
|
public string? query_id { get; private set; }
|
||||||
|
|
||||||
public MessageFlag(DateTime? server_time, string? mam_id, string? query_id) {
|
public MessageFlag(Jid sender_jid, DateTime? server_time, string? mam_id, string? query_id) {
|
||||||
|
this.sender_jid = sender_jid;
|
||||||
this.server_time = server_time;
|
this.server_time = server_time;
|
||||||
this.mam_id = mam_id;
|
this.mam_id = mam_id;
|
||||||
this.query_id = query_id;
|
this.query_id = query_id;
|
||||||
|
@ -176,4 +158,8 @@ public class MessageFlag : Xmpp.MessageFlag {
|
||||||
public override string get_id() { return ID; }
|
public override string get_id() { return ID; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string NS_VER(XmppStream stream) {
|
||||||
|
return stream.get_flag(Flag.IDENTITY).ns_ver;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -68,7 +68,7 @@ namespace Xmpp.Xep.CallInvites {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void on_received_message(XmppStream stream, MessageStanza message) {
|
private void on_received_message(XmppStream stream, MessageStanza message) {
|
||||||
Xep.MessageArchiveManagement.MessageFlag? mam_flag = Xep.MessageArchiveManagement.MessageFlag.get_flag(message);
|
Xmpp.MessageArchiveManagement.MessageFlag? mam_flag = Xmpp.MessageArchiveManagement.MessageFlag.get_flag(message);
|
||||||
if (mam_flag != null) return;
|
if (mam_flag != null) return;
|
||||||
|
|
||||||
StanzaNode? relevant_node = null;
|
StanzaNode? relevant_node = null;
|
||||||
|
|
|
@ -53,7 +53,7 @@ namespace Xmpp.Xep.JingleMessageInitiation {
|
||||||
private void on_received_message(XmppStream stream, MessageStanza message) {
|
private void on_received_message(XmppStream stream, MessageStanza message) {
|
||||||
if (message.type_ == MessageStanza.TYPE_GROUPCHAT) return;
|
if (message.type_ == MessageStanza.TYPE_GROUPCHAT) return;
|
||||||
|
|
||||||
Xep.MessageArchiveManagement.MessageFlag? mam_flag = Xep.MessageArchiveManagement.MessageFlag.get_flag(message);
|
Xmpp.MessageArchiveManagement.MessageFlag? mam_flag = Xmpp.MessageArchiveManagement.MessageFlag.get_flag(message);
|
||||||
if (mam_flag != null) return;
|
if (mam_flag != null) return;
|
||||||
|
|
||||||
StanzaNode? mi_node = null;
|
StanzaNode? mi_node = null;
|
||||||
|
|
45
xmpp-vala/src/module/xep/0421_occupant_ids.vala
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
namespace Xmpp.Xep.OccupantIds {
|
||||||
|
|
||||||
|
public const string NS_URI = "urn:xmpp:occupant-id:0";
|
||||||
|
|
||||||
|
public static string? get_occupant_id(StanzaNode stanza) {
|
||||||
|
StanzaNode? node = stanza.get_subnode("occupant-id", NS_URI);
|
||||||
|
if (node == null) return null;
|
||||||
|
|
||||||
|
return node.get_attribute("id");
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Module : XmppStreamModule {
|
||||||
|
public static ModuleIdentity<Module> IDENTITY = new ModuleIdentity<Module>(NS_URI, "0421_occupant_ids");
|
||||||
|
|
||||||
|
public signal void received_occupant_id(XmppStream stream, Jid jid, string occupant_id);
|
||||||
|
public signal void received_own_occupant_id(XmppStream stream, Jid jid, string occupant_id);
|
||||||
|
|
||||||
|
public override void attach(XmppStream stream) {
|
||||||
|
stream.get_module(Presence.Module.IDENTITY).received_available.connect(parse_occupant_id_from_presence);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void detach(XmppStream stream) {
|
||||||
|
stream.get_module(Presence.Module.IDENTITY).received_available.disconnect(parse_occupant_id_from_presence);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string get_ns() { return NS_URI; }
|
||||||
|
public override string get_id() { return IDENTITY.id; }
|
||||||
|
|
||||||
|
public void parse_occupant_id_from_presence(XmppStream stream, Presence.Stanza presence) {
|
||||||
|
string? occupant_id = get_occupant_id(presence.stanza);
|
||||||
|
if (occupant_id == null) return;
|
||||||
|
|
||||||
|
received_occupant_id(stream, presence.from, occupant_id);
|
||||||
|
|
||||||
|
StanzaNode? x_node = presence.stanza.get_subnode("x", "http://jabber.org/protocol/muc#user");
|
||||||
|
if (x_node == null) return;
|
||||||
|
foreach (StanzaNode status_node in x_node.get_subnodes("status", "http://jabber.org/protocol/muc#user")) {
|
||||||
|
if (int.parse(status_node.get_attribute("code")) == 110) {
|
||||||
|
received_own_occupant_id(stream, presence.from, occupant_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
74
xmpp-vala/src/module/xep/0444_reactions.vala
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
using Gee;
|
||||||
|
|
||||||
|
namespace Xmpp.Xep.Reactions {
|
||||||
|
|
||||||
|
public const string NS_URI = "urn:xmpp:reactions:0";
|
||||||
|
|
||||||
|
public class Module : XmppStreamModule {
|
||||||
|
public static ModuleIdentity<Module> IDENTITY = new ModuleIdentity<Module>(NS_URI, "reactions");
|
||||||
|
|
||||||
|
public signal void received_reactions(XmppStream stream, Jid from_jid, string message_id, Gee.List<string> reactions, MessageStanza stanza);
|
||||||
|
|
||||||
|
private ReceivedPipelineListener received_pipeline_listener = new ReceivedPipelineListener();
|
||||||
|
|
||||||
|
public void send_reaction(XmppStream stream, Jid jid, string stanza_type, string message_id, Gee.List<string> reactions) {
|
||||||
|
StanzaNode reactions_node = new StanzaNode.build("reactions", NS_URI).add_self_xmlns();
|
||||||
|
reactions_node.put_attribute("id", message_id);
|
||||||
|
foreach (string reaction in reactions) {
|
||||||
|
StanzaNode reaction_node = new StanzaNode.build("reaction", NS_URI);
|
||||||
|
reaction_node.put_node(new StanzaNode.text(reaction));
|
||||||
|
reactions_node.put_node(reaction_node);
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageStanza message = new MessageStanza() { to=jid, type_=stanza_type };
|
||||||
|
message.stanza.put_node(reactions_node);
|
||||||
|
|
||||||
|
MessageProcessingHints.set_message_hint(message, MessageProcessingHints.HINT_STORE);
|
||||||
|
|
||||||
|
stream.get_module(MessageModule.IDENTITY).send_message.begin(stream, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void attach(XmppStream stream) {
|
||||||
|
stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, NS_URI);
|
||||||
|
stream.get_module(MessageModule.IDENTITY).received_pipeline.connect(received_pipeline_listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void detach(XmppStream stream) {
|
||||||
|
stream.get_module(ServiceDiscovery.Module.IDENTITY).remove_feature(stream, NS_URI);
|
||||||
|
stream.get_module(MessageModule.IDENTITY).received_pipeline.disconnect(received_pipeline_listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string get_ns() { return NS_URI; }
|
||||||
|
public override string get_id() { return IDENTITY.id; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ReceivedPipelineListener : StanzaListener<MessageStanza> {
|
||||||
|
|
||||||
|
private const string[] after_actions_const = {"EXTRACT_MESSAGE_2"};
|
||||||
|
|
||||||
|
public override string action_group { get { return ""; } }
|
||||||
|
public override string[] after_actions { get { return after_actions_const; } }
|
||||||
|
|
||||||
|
public override async bool run(XmppStream stream, MessageStanza message) {
|
||||||
|
StanzaNode? reactions_node = message.stanza.get_subnode("reactions", NS_URI);
|
||||||
|
if (reactions_node == null) return false;
|
||||||
|
|
||||||
|
string? id_attribute = reactions_node.get_attribute("id");
|
||||||
|
if (id_attribute == null) return false;
|
||||||
|
|
||||||
|
Gee.List<string> reactions = new ArrayList<string>();
|
||||||
|
foreach (StanzaNode reaction_node in reactions_node.get_subnodes("reaction", NS_URI)) {
|
||||||
|
string? reaction = reaction_node.get_string_content();
|
||||||
|
if (reaction == null) return false;
|
||||||
|
|
||||||
|
if (!reactions.contains(reaction)) {
|
||||||
|
reactions.add(reaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stream.get_module(Module.IDENTITY).received_reactions(stream, message.from, id_attribute, reactions, message);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|