Add support for JET-OMEMO
This commit is contained in:
parent
e899668213
commit
392cb472ab
|
@ -36,6 +36,8 @@ SOURCES
|
||||||
|
|
||||||
src/file_transfer/file_decryptor.vala
|
src/file_transfer/file_decryptor.vala
|
||||||
src/file_transfer/file_encryptor.vala
|
src/file_transfer/file_encryptor.vala
|
||||||
|
src/jingle/jingle_helper.vala
|
||||||
|
src/jingle/jet_omemo.vala
|
||||||
|
|
||||||
src/logic/database.vala
|
src/logic/database.vala
|
||||||
src/logic/encrypt_state.vala
|
src/logic/encrypt_state.vala
|
||||||
|
@ -59,6 +61,7 @@ SOURCES
|
||||||
src/ui/manage_key_dialog.vala
|
src/ui/manage_key_dialog.vala
|
||||||
src/ui/util.vala
|
src/ui/util.vala
|
||||||
CUSTOM_VAPIS
|
CUSTOM_VAPIS
|
||||||
|
${CMAKE_BINARY_DIR}/exports/crypto-vala.vapi
|
||||||
${CMAKE_BINARY_DIR}/exports/signal-protocol.vapi
|
${CMAKE_BINARY_DIR}/exports/signal-protocol.vapi
|
||||||
${CMAKE_BINARY_DIR}/exports/xmpp-vala.vapi
|
${CMAKE_BINARY_DIR}/exports/xmpp-vala.vapi
|
||||||
${CMAKE_BINARY_DIR}/exports/qlite.vapi
|
${CMAKE_BINARY_DIR}/exports/qlite.vapi
|
||||||
|
@ -74,7 +77,7 @@ OPTIONS
|
||||||
add_definitions(${VALA_CFLAGS} -DGETTEXT_PACKAGE=\"${GETTEXT_PACKAGE}\" -DLOCALE_INSTALL_DIR=\"${LOCALE_INSTALL_DIR}\" -DG_LOG_DOMAIN="OMEMO")
|
add_definitions(${VALA_CFLAGS} -DGETTEXT_PACKAGE=\"${GETTEXT_PACKAGE}\" -DLOCALE_INSTALL_DIR=\"${LOCALE_INSTALL_DIR}\" -DG_LOG_DOMAIN="OMEMO")
|
||||||
add_library(omemo SHARED ${OMEMO_VALA_C} ${OMEMO_GRESOURCES_TARGET})
|
add_library(omemo SHARED ${OMEMO_VALA_C} ${OMEMO_GRESOURCES_TARGET})
|
||||||
add_dependencies(omemo ${GETTEXT_PACKAGE}-translations)
|
add_dependencies(omemo ${GETTEXT_PACKAGE}-translations)
|
||||||
target_link_libraries(omemo libdino signal-protocol-vala ${OMEMO_PACKAGES})
|
target_link_libraries(omemo libdino signal-protocol-vala crypto-vala ${OMEMO_PACKAGES})
|
||||||
set_target_properties(omemo PROPERTIES PREFIX "")
|
set_target_properties(omemo PROPERTIES PREFIX "")
|
||||||
set_target_properties(omemo PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/plugins/)
|
set_target_properties(omemo PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/plugins/)
|
||||||
|
|
||||||
|
|
145
plugins/omemo/src/jingle/jet_omemo.vala
Normal file
145
plugins/omemo/src/jingle/jet_omemo.vala
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
using Crypto;
|
||||||
|
using Dino;
|
||||||
|
using Dino.Entities;
|
||||||
|
using Gee;
|
||||||
|
using Signal;
|
||||||
|
using Xmpp;
|
||||||
|
using Xmpp.Xep;
|
||||||
|
|
||||||
|
namespace Dino.Plugins.JetOmemo {
|
||||||
|
private const string NS_URI = "urn:xmpp:jingle:jet-omemo:0";
|
||||||
|
private const string AES_128_GCM_URI = "urn:xmpp:ciphers:aes-128-gcm-nopadding";
|
||||||
|
public class Module : XmppStreamModule, Jet.EnvelopEncoding {
|
||||||
|
public static Xmpp.ModuleIdentity<Module> IDENTITY = new Xmpp.ModuleIdentity<Module>(NS_URI, "0396_jet_omemo");
|
||||||
|
private Omemo.Plugin plugin;
|
||||||
|
|
||||||
|
public Module(Omemo.Plugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void attach(XmppStream stream) {
|
||||||
|
if (stream.get_module(Jet.Module.IDENTITY) != null) {
|
||||||
|
stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, NS_URI);
|
||||||
|
stream.get_module(Jet.Module.IDENTITY).register_envelop_encoding(this);
|
||||||
|
stream.get_module(Jet.Module.IDENTITY).register_cipher(new AesGcmCipher(16, AES_128_GCM_URI));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void detach(XmppStream stream) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool is_available(XmppStream stream, Jid full_jid) {
|
||||||
|
bool? has_feature = stream.get_flag(ServiceDiscovery.Flag.IDENTITY).has_entity_feature(full_jid, NS_URI);
|
||||||
|
if (has_feature == null || !(!)has_feature) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return stream.get_module(Xep.Jet.Module.IDENTITY).is_available(stream, full_jid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string get_type_uri() {
|
||||||
|
return Omemo.NS_URI;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Jet.TransportSecret decode_envolop(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, StanzaNode security) throws Jingle.IqError {
|
||||||
|
Store store = stream.get_module(Omemo.StreamModule.IDENTITY).store;
|
||||||
|
StanzaNode? encrypted = security.get_subnode("encrypted", Omemo.NS_URI);
|
||||||
|
if (encrypted == null) throw new Jingle.IqError.BAD_REQUEST("Invalid JET-OMEMO envelop: missing encrypted element");
|
||||||
|
StanzaNode? header = encrypted.get_subnode("header", Omemo.NS_URI);
|
||||||
|
if (header == null) throw new Jingle.IqError.BAD_REQUEST("Invalid JET-OMEMO envelop: missing header element");
|
||||||
|
string? iv_node = header.get_deep_string_content("iv");
|
||||||
|
if (header == null) throw new Jingle.IqError.BAD_REQUEST("Invalid JET-OMEMO envelop: missing iv element");
|
||||||
|
uint8[] iv = Base64.decode((!)iv_node);
|
||||||
|
foreach (StanzaNode key_node in header.get_subnodes("key")) {
|
||||||
|
if (key_node.get_attribute_int("rid") == store.local_registration_id) {
|
||||||
|
string? key_node_content = key_node.get_string_content();
|
||||||
|
|
||||||
|
uint8[] key;
|
||||||
|
Address address = new Address(peer_full_jid.bare_jid.to_string(), header.get_attribute_int("sid"));
|
||||||
|
if (key_node.get_attribute_bool("prekey")) {
|
||||||
|
PreKeySignalMessage msg = Omemo.Plugin.get_context().deserialize_pre_key_signal_message(Base64.decode((!)key_node_content));
|
||||||
|
SessionCipher cipher = store.create_session_cipher(address);
|
||||||
|
key = cipher.decrypt_pre_key_signal_message(msg);
|
||||||
|
} else {
|
||||||
|
SignalMessage msg = Omemo.Plugin.get_context().deserialize_signal_message(Base64.decode((!)key_node_content));
|
||||||
|
SessionCipher cipher = store.create_session_cipher(address);
|
||||||
|
key = cipher.decrypt_signal_message(msg);
|
||||||
|
}
|
||||||
|
address.device_id = 0; // TODO: Hack to have address obj live longer
|
||||||
|
|
||||||
|
uint8[] authtag = null;
|
||||||
|
if (key.length >= 32) {
|
||||||
|
int authtaglength = key.length - 16;
|
||||||
|
authtag = new uint8[authtaglength];
|
||||||
|
uint8[] new_key = new uint8[16];
|
||||||
|
Memory.copy(authtag, (uint8*)key + 16, 16);
|
||||||
|
Memory.copy(new_key, key, 16);
|
||||||
|
key = new_key;
|
||||||
|
}
|
||||||
|
// TODO: authtag?
|
||||||
|
return new Jet.TransportSecret(key, iv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Jingle.IqError.NOT_ACCEPTABLE("Not encrypted for targeted device");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void encode_envelop(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, Jet.SecurityParameters security_params, StanzaNode security) {
|
||||||
|
ArrayList<Account> accounts = plugin.app.stream_interactor.get_accounts();
|
||||||
|
Store store = stream.get_module(Omemo.StreamModule.IDENTITY).store;
|
||||||
|
Account? account = null;
|
||||||
|
foreach (Account compare in accounts) {
|
||||||
|
if (compare.bare_jid.equals_bare(local_full_jid)) {
|
||||||
|
account = compare;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (account == null) {
|
||||||
|
// TODO
|
||||||
|
critical("Sending from offline account %s", local_full_jid.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
StanzaNode header_node;
|
||||||
|
StanzaNode encrypted_node = new StanzaNode.build("encrypted", Omemo.NS_URI).add_self_xmlns()
|
||||||
|
.put_node(header_node = new StanzaNode.build("header", Omemo.NS_URI)
|
||||||
|
.put_attribute("sid", store.local_registration_id.to_string())
|
||||||
|
.put_node(new StanzaNode.build("iv", Omemo.NS_URI)
|
||||||
|
.put_node(new StanzaNode.text(Base64.encode(security_params.secret.initialization_vector)))));
|
||||||
|
|
||||||
|
plugin.trust_manager.encrypt_key(header_node, security_params.secret.transport_key, local_full_jid.bare_jid, new ArrayList<Jid>.wrap(new Jid[] {peer_full_jid.bare_jid}), stream, account);
|
||||||
|
security.put_node(encrypted_node);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string get_ns() { return NS_URI; }
|
||||||
|
public override string get_id() { return IDENTITY.id; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AesGcmCipher : Jet.Cipher, Object {
|
||||||
|
private int key_size;
|
||||||
|
private string uri;
|
||||||
|
public AesGcmCipher(int key_size, string uri) {
|
||||||
|
this.key_size = key_size;
|
||||||
|
this.uri = uri;
|
||||||
|
}
|
||||||
|
public string get_cipher_uri() {
|
||||||
|
return uri;
|
||||||
|
}
|
||||||
|
public Jet.TransportSecret generate_random_secret() {
|
||||||
|
uint8[] iv = new uint8[16];
|
||||||
|
Omemo.Plugin.get_context().randomize(iv);
|
||||||
|
uint8[] key = new uint8[key_size];
|
||||||
|
Omemo.Plugin.get_context().randomize(key);
|
||||||
|
return new Jet.TransportSecret(key, iv);
|
||||||
|
}
|
||||||
|
public InputStream wrap_input_stream(InputStream input, Jet.TransportSecret secret) requires (secret.transport_key.length == key_size) {
|
||||||
|
SymmetricCipher cipher = new SymmetricCipher("AES-GCM");
|
||||||
|
cipher.set_key(secret.transport_key);
|
||||||
|
cipher.set_iv(secret.initialization_vector);
|
||||||
|
return new ConverterInputStream(input, new SymmetricCipherDecrypter((owned) cipher, 16));
|
||||||
|
}
|
||||||
|
public OutputStream wrap_output_stream(OutputStream output, Jet.TransportSecret secret) requires (secret.transport_key.length == key_size) {
|
||||||
|
Crypto.SymmetricCipher cipher = new SymmetricCipher("AES-GCM");
|
||||||
|
cipher.set_key(secret.transport_key);
|
||||||
|
cipher.set_iv(secret.initialization_vector);
|
||||||
|
return new ConverterOutputStream(output, new SymmetricCipherEncrypter((owned) cipher, 16));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
53
plugins/omemo/src/jingle/jingle_helper.vala
Normal file
53
plugins/omemo/src/jingle/jingle_helper.vala
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
using Dino.Entities;
|
||||||
|
using Xmpp;
|
||||||
|
|
||||||
|
namespace Dino.Plugins.JetOmemo {
|
||||||
|
public class EncryptionHelper : JingleFileEncryptionHelper, Object {
|
||||||
|
private StreamInteractor stream_interactor;
|
||||||
|
|
||||||
|
public EncryptionHelper(StreamInteractor stream_interactor) {
|
||||||
|
this.stream_interactor = stream_interactor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool can_transfer(Conversation conversation) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool can_encrypt(Conversation conversation, FileTransfer file_transfer, Jid? full_jid) {
|
||||||
|
XmppStream? stream = stream_interactor.get_stream(conversation.account);
|
||||||
|
if (stream == null) return false;
|
||||||
|
|
||||||
|
Gee.List<Jid>? resources = stream.get_flag(Presence.Flag.IDENTITY).get_resources(conversation.counterpart);
|
||||||
|
if (resources == null) return false;
|
||||||
|
|
||||||
|
if (full_jid == null) {
|
||||||
|
foreach (Jid test_jid in resources) {
|
||||||
|
if (stream.get_module(Module.IDENTITY).is_available(stream, test_jid)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (stream.get_module(Module.IDENTITY).is_available(stream, full_jid)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? get_precondition_name(Conversation conversation, FileTransfer file_transfer) {
|
||||||
|
return Xep.Jet.NS_URI;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object? get_precondition_options(Conversation conversation, FileTransfer file_transfer) {
|
||||||
|
return new Xep.Jet.Options(Omemo.NS_URI, AES_128_GCM_URI);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FileMeta complete_meta(FileTransfer file_transfer, FileReceiveData receive_data, FileMeta file_meta, Xmpp.Xep.JingleFileTransfer.FileTransfer jingle_transfer) {
|
||||||
|
Xep.Jet.SecurityParameters? security = jingle_transfer.security as Xep.Jet.SecurityParameters;
|
||||||
|
if (security != null && security.encoding.get_type_uri() == Omemo.NS_URI) {
|
||||||
|
file_transfer.encryption = Encryption.OMEMO;
|
||||||
|
}
|
||||||
|
return file_meta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -72,14 +72,10 @@ public class TrustManager {
|
||||||
return key_node;
|
return key_node;
|
||||||
}
|
}
|
||||||
|
|
||||||
public EncryptState encrypt(MessageStanza message, Jid self_jid, Gee.List<Jid> recipients, XmppStream stream, Account account) {
|
internal EncryptState encrypt_key(StanzaNode header_node, uint8[] keytag, Jid self_jid, Gee.List<Jid> recipients, XmppStream stream, Account account) throws Error {
|
||||||
EncryptState status = new EncryptState();
|
EncryptState status = new EncryptState();
|
||||||
if (!Plugin.ensure_context()) return status;
|
|
||||||
if (message.to == null) return status;
|
|
||||||
|
|
||||||
StreamModule module = stream.get_module(StreamModule.IDENTITY);
|
StreamModule module = stream.get_module(StreamModule.IDENTITY);
|
||||||
|
|
||||||
try {
|
|
||||||
//Check we have the bundles and device lists needed to send the message
|
//Check we have the bundles and device lists needed to send the message
|
||||||
if (!is_known_address(account, self_jid)) return status;
|
if (!is_known_address(account, self_jid)) return status;
|
||||||
status.own_list = true;
|
status.own_list = true;
|
||||||
|
@ -95,30 +91,9 @@ public class TrustManager {
|
||||||
}
|
}
|
||||||
if (status.own_devices == 0 || status.other_devices == 0) return status;
|
if (status.own_devices == 0 || status.other_devices == 0) return status;
|
||||||
|
|
||||||
//Create a key and use it to encrypt the message
|
|
||||||
uint8[] key = new uint8[16];
|
|
||||||
Plugin.get_context().randomize(key);
|
|
||||||
uint8[] iv = new uint8[16];
|
|
||||||
Plugin.get_context().randomize(iv);
|
|
||||||
|
|
||||||
uint8[] aes_encrypt_result = aes_encrypt(Cipher.AES_GCM_NOPADDING, key, iv, message.body.data);
|
|
||||||
uint8[] ciphertext = aes_encrypt_result[0:aes_encrypt_result.length-16];
|
|
||||||
uint8[] tag = aes_encrypt_result[aes_encrypt_result.length-16:aes_encrypt_result.length];
|
|
||||||
uint8[] keytag = new uint8[key.length + tag.length];
|
|
||||||
Memory.copy(keytag, key, key.length);
|
|
||||||
Memory.copy((uint8*)keytag + key.length, tag, tag.length);
|
|
||||||
|
|
||||||
StanzaNode header_node;
|
|
||||||
StanzaNode encrypted_node = new StanzaNode.build("encrypted", NS_URI).add_self_xmlns()
|
|
||||||
.put_node(header_node = new StanzaNode.build("header", NS_URI)
|
|
||||||
.put_attribute("sid", module.store.local_registration_id.to_string())
|
|
||||||
.put_node(new StanzaNode.build("iv", NS_URI)
|
|
||||||
.put_node(new StanzaNode.text(Base64.encode(iv)))))
|
|
||||||
.put_node(new StanzaNode.build("payload", NS_URI)
|
|
||||||
.put_node(new StanzaNode.text(Base64.encode(ciphertext))));
|
|
||||||
|
|
||||||
//Encrypt the key for each recipient's device individually
|
//Encrypt the key for each recipient's device individually
|
||||||
Address address = new Address(message.to.bare_jid.to_string(), 0);
|
Address address = new Address("", 0);
|
||||||
foreach (Jid recipient in recipients) {
|
foreach (Jid recipient in recipients) {
|
||||||
foreach(int32 device_id in get_trusted_devices(account, recipient)) {
|
foreach(int32 device_id in get_trusted_devices(account, recipient)) {
|
||||||
if (module.is_ignored_device(recipient, device_id)) {
|
if (module.is_ignored_device(recipient, device_id)) {
|
||||||
|
@ -158,6 +133,41 @@ public class TrustManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EncryptState encrypt(MessageStanza message, Jid self_jid, Gee.List<Jid> recipients, XmppStream stream, Account account) {
|
||||||
|
EncryptState status = new EncryptState();
|
||||||
|
if (!Plugin.ensure_context()) return status;
|
||||||
|
if (message.to == null) return status;
|
||||||
|
|
||||||
|
StreamModule module = stream.get_module(StreamModule.IDENTITY);
|
||||||
|
|
||||||
|
try {
|
||||||
|
//Create a key and use it to encrypt the message
|
||||||
|
uint8[] key = new uint8[16];
|
||||||
|
Plugin.get_context().randomize(key);
|
||||||
|
uint8[] iv = new uint8[16];
|
||||||
|
Plugin.get_context().randomize(iv);
|
||||||
|
|
||||||
|
uint8[] aes_encrypt_result = aes_encrypt(Cipher.AES_GCM_NOPADDING, key, iv, message.body.data);
|
||||||
|
uint8[] ciphertext = aes_encrypt_result[0:aes_encrypt_result.length-16];
|
||||||
|
uint8[] tag = aes_encrypt_result[aes_encrypt_result.length-16:aes_encrypt_result.length];
|
||||||
|
uint8[] keytag = new uint8[key.length + tag.length];
|
||||||
|
Memory.copy(keytag, key, key.length);
|
||||||
|
Memory.copy((uint8*)keytag + key.length, tag, tag.length);
|
||||||
|
|
||||||
|
StanzaNode header_node;
|
||||||
|
StanzaNode encrypted_node = new StanzaNode.build("encrypted", NS_URI).add_self_xmlns()
|
||||||
|
.put_node(header_node = new StanzaNode.build("header", NS_URI)
|
||||||
|
.put_attribute("sid", module.store.local_registration_id.to_string())
|
||||||
|
.put_node(new StanzaNode.build("iv", NS_URI)
|
||||||
|
.put_node(new StanzaNode.text(Base64.encode(iv)))))
|
||||||
|
.put_node(new StanzaNode.build("payload", NS_URI)
|
||||||
|
.put_node(new StanzaNode.text(Base64.encode(ciphertext))));
|
||||||
|
|
||||||
|
status = encrypt_key(header_node, keytag, self_jid, recipients, stream, account);
|
||||||
|
|
||||||
message.stanza.put_node(encrypted_node);
|
message.stanza.put_node(encrypted_node);
|
||||||
Xep.ExplicitEncryption.add_encryption_tag_to_message(message, NS_URI, "OMEMO");
|
Xep.ExplicitEncryption.add_encryption_tag_to_message(message, NS_URI, "OMEMO");
|
||||||
message.body = "[This message is OMEMO encrypted]";
|
message.body = "[This message is OMEMO encrypted]";
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
using Dino.Entities;
|
||||||
|
|
||||||
extern const string GETTEXT_PACKAGE;
|
extern const string GETTEXT_PACKAGE;
|
||||||
extern const string LOCALE_INSTALL_DIR;
|
extern const string LOCALE_INSTALL_DIR;
|
||||||
|
|
||||||
|
@ -47,11 +49,13 @@ public class Plugin : RootInterface, Object {
|
||||||
this.app.plugin_registry.register_notification_populator(device_notification_populator);
|
this.app.plugin_registry.register_notification_populator(device_notification_populator);
|
||||||
this.app.stream_interactor.module_manager.initialize_account_modules.connect((account, list) => {
|
this.app.stream_interactor.module_manager.initialize_account_modules.connect((account, list) => {
|
||||||
list.add(new StreamModule());
|
list.add(new StreamModule());
|
||||||
|
list.add(new JetOmemo.Module(this));
|
||||||
this.own_notifications = new OwnNotifications(this, this.app.stream_interactor, account);
|
this.own_notifications = new OwnNotifications(this, this.app.stream_interactor, account);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.stream_interactor.get_module(FileManager.IDENTITY).add_file_decryptor(new OmemoFileDecryptor());
|
app.stream_interactor.get_module(FileManager.IDENTITY).add_file_decryptor(new OmemoFileDecryptor());
|
||||||
app.stream_interactor.get_module(FileManager.IDENTITY).add_file_encryptor(new OmemoFileEncryptor());
|
app.stream_interactor.get_module(FileManager.IDENTITY).add_file_encryptor(new OmemoFileEncryptor());
|
||||||
|
JingleFileHelperRegistry.instance.add_encryption_helper(Encryption.OMEMO, new JetOmemo.EncryptionHelper(app.stream_interactor));
|
||||||
|
|
||||||
Manager.start(this.app.stream_interactor, db, trust_manager);
|
Manager.start(this.app.stream_interactor, db, trust_manager);
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ public class StreamModule : XmppStreamModule {
|
||||||
this.store = Plugin.get_context().create_store();
|
this.store = Plugin.get_context().create_store();
|
||||||
store_created(store);
|
store_created(store);
|
||||||
stream.get_module(Pubsub.Module.IDENTITY).add_filtered_notification(stream, NODE_DEVICELIST, (stream, jid, id, node) => parse_device_list(stream, jid, id, node));
|
stream.get_module(Pubsub.Module.IDENTITY).add_filtered_notification(stream, NODE_DEVICELIST, (stream, jid, id, node) => parse_device_list(stream, jid, id, node));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void detach(XmppStream stream) {}
|
public override void detach(XmppStream stream) {}
|
||||||
|
|
Loading…
Reference in a new issue