Improve Plugin API (allow to move encryption into plugin)

This commit is contained in:
Marvin W 2017-03-11 01:25:45 +01:00
parent 1ccad732b9
commit 4c48bdc072
No known key found for this signature in database
GPG Key ID: 072E9235DB996F2A
20 changed files with 239 additions and 168 deletions

View File

@ -62,7 +62,11 @@ compile_gresources(
vala_precompile(LIBDINO_VALA_C
SOURCES
src/plugin.vala
src/application.vala
src/plugin/interfaces.vala
src/plugin/loader.vala
src/plugin/registry.vala
src/dbus/login1.vala
src/dbus/networkmanager.vala
@ -72,6 +76,7 @@ SOURCES
src/entity/conversation.vala
src/entity/jid.vala
src/entity/message.vala
src/entity/encryption.vala
src/service/avatar_manager.vala
src/service/avatar_storage.vala
@ -101,7 +106,6 @@ SOURCES
src/ui/add_conversation/list_row.vala
src/ui/add_conversation/select_jid_fragment.vala
src/ui/avatar_generator.vala
src/ui/application.vala
src/ui/chat_input.vala
src/ui/conversation_list_titlebar.vala
src/ui/conversation_selector/chat_row.vala

View File

@ -4,7 +4,7 @@
<object class="GtkPopoverMenu" id="menu_encryption">
<property name="can_focus">False</property>
<child>
<object class="GtkBox">
<object class="GtkBox" id="encryption_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
@ -24,22 +24,6 @@
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="button_pgp">
<property name="label" translatable="yes">OpenPGP</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
<property name="group">button_unencrypted</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="submenu">main</property>

View File

@ -1,11 +1,13 @@
using Gtk;
using Dino.Entities;
using Dino.Ui;
public class Dino.Ui.Application : Gtk.Application {
public class Dino.Application : Gtk.Application {
private Database db;
private StreamInteractor stream_interaction;
public Database db;
public StreamInteractor stream_interaction;
public Plugins.Registry plugin_registry = new Plugins.Registry();
private Notifications notifications;
private UnifiedWindow? window;

View File

@ -3,11 +3,6 @@ public class Conversation : Object {
public signal void object_updated(Conversation conversation);
public enum Encryption {
UNENCRYPTED,
PGP
}
public enum Type {
CHAT,
GROUPCHAT
@ -27,7 +22,7 @@ public class Conversation : Object {
this.account = account;
this.active = false;
this.last_active = new DateTime.from_unix_utc(0);
this.encryption = Encryption.UNENCRYPTED;
this.encryption = Encryption.NONE;
}
public Conversation.with_id(Jid jid, Account account, int id) {

View File

@ -0,0 +1,9 @@
namespace Dino.Entities {
public enum Encryption {
NONE,
PGP,
OMEMO
}
}

View File

@ -16,11 +16,6 @@ public class Dino.Entities.Message : Object {
WONTSEND
}
public enum Encryption {
NONE,
PGP
}
public enum Type {
ERROR,
CHAT,

View File

@ -1,64 +0,0 @@
namespace Dino {
public errordomain PluginError {
NOT_SUPPORTED,
UNEXPECTED_TYPE,
NO_REGISTRATION_FUNCTION,
FAILED
}
public interface PluginIface : Object {
public abstract void registered(Dino.Ui.Application app);
}
private class PluginInfo : Object {
public Module module;
public Type gtype;
public PluginInfo(Type type, owned Module module) {
this.module = (owned) module;
this.gtype = type;
}
}
public class PluginLoader : Object {
[CCode (has_target = false)]
private delegate Type RegisterPluginFunction (Module module);
private PluginIface[] plugins = new PluginIface[0];
private PluginInfo[] infos = new PluginInfo[0];
public PluginIface load(string name, Dino.Ui.Application app) throws PluginError {
if (Module.supported () == false) {
throw new PluginError.NOT_SUPPORTED ("Plugins are not supported");
}
Module module = Module.open ("plugins/" + name, ModuleFlags.BIND_LAZY);
if (module == null) {
throw new PluginError.FAILED (Module.error ());
}
void* function;
module.symbol ("register_plugin", out function);
if (function == null) {
throw new PluginError.NO_REGISTRATION_FUNCTION ("register_plugin () not found");
}
RegisterPluginFunction register_plugin = (RegisterPluginFunction) function;
Type type = register_plugin (module);
if (type.is_a (typeof (PluginIface)) == false) {
throw new PluginError.UNEXPECTED_TYPE ("Unexpected type");
}
PluginInfo info = new PluginInfo (type, (owned) module);
infos += info;
PluginIface plugin = (PluginIface) Object.new (type);
plugins += plugin;
plugin.registered (app);
return plugin;
}
}
}

View File

@ -0,0 +1,16 @@
namespace Dino.Plugins {
public interface RootInterface : Object {
public abstract void registered(Dino.Application app);
public abstract void shutdown();
}
public interface EncryptionListEntry : Object {
public abstract Entities.Encryption encryption { get; }
public abstract string name { get; }
public abstract bool can_encrypt(Entities.Conversation conversation);
}
}

View File

@ -0,0 +1,66 @@
namespace Dino.Plugins {
public errordomain Error {
NOT_SUPPORTED,
UNEXPECTED_TYPE,
NO_REGISTRATION_FUNCTION,
FAILED
}
private class Info : Object {
public Module module;
public Type gtype;
public Info(Type type, owned Module module) {
this.module = (owned) module;
this.gtype = type;
}
}
public class Loader : Object {
[CCode (has_target = false)]
private delegate Type RegisterPluginFunction (Module module);
private RootInterface[] plugins = new RootInterface[0];
private Info[] infos = new Info[0];
public RootInterface load(string name, Dino.Application app) throws Error {
if (Module.supported () == false) {
throw new Error.NOT_SUPPORTED ("Plugins are not supported");
}
Module module = Module.open ("plugins/" + name, ModuleFlags.BIND_LAZY);
if (module == null) {
throw new Error.FAILED (Module.error ());
}
void* function;
module.symbol ("register_plugin", out function);
if (function == null) {
throw new Error.NO_REGISTRATION_FUNCTION ("register_plugin () not found");
}
RegisterPluginFunction register_plugin = (RegisterPluginFunction) function;
Type type = register_plugin (module);
if (type.is_a (typeof (RootInterface)) == false) {
throw new Error.UNEXPECTED_TYPE ("Unexpected type");
}
Info info = new Plugins.Info (type, (owned) module);
infos += info;
RootInterface plugin = (RootInterface) Object.new (type);
plugins += plugin;
plugin.registered (app);
return plugin;
}
public void shutdown() {
foreach (RootInterface p in plugins) {
p.shutdown();
}
}
}
}

View File

@ -0,0 +1,20 @@
using Gee;
namespace Dino.Plugins {
public class Registry {
internal ArrayList<EncryptionListEntry> encryption_list_entries = new ArrayList<EncryptionListEntry>();
public bool register_encryption_list_entry(EncryptionListEntry entry) {
lock(encryption_list_entries) {
foreach(var e in encryption_list_entries) {
if (e.encryption == entry.encryption) return false;
}
encryption_list_entries.add(entry);
encryption_list_entries.sort((a,b) => b.name.collate(a.name));
return true;
}
}
}
}

View File

@ -70,7 +70,7 @@ public class ConversationManager : StreamInteractionModule, Object {
}
}
private void on_message_received(Entities.Message message, Conversation conversation) {
private void on_message_received(Entities.Message message, Xmpp.Message.Stanza message_stanza, Conversation conversation) {
ensure_start_conversation(conversation.counterpart, conversation.account);
}

View File

@ -337,7 +337,7 @@ public class Database : Qlite.Database {
new_message.body = row[message.body];
new_message.account = get_account_by_id(row[message.account_id]); // TODO dont have to generate acc new
new_message.marked = (Message.Marked) row[message.marked];
new_message.encryption = (Message.Encryption) row[message.encryption];
new_message.encryption = (Encryption) row[message.encryption];
new_message.real_jid = get_real_jid_for_message(new_message);
new_message.notify.connect(on_message_update);
@ -396,7 +396,7 @@ public class Database : Qlite.Database {
int64? last_active = row[conversation.last_active];
if (last_active != null) new_conversation.last_active = new DateTime.from_unix_utc(last_active);
new_conversation.type_ = (Conversation.Type) row[conversation.type_];
new_conversation.encryption = (Conversation.Encryption) row[conversation.encryption];
new_conversation.encryption = (Encryption) row[conversation.encryption];
int? read_up_to = row[conversation.read_up_to];
if (read_up_to != null) new_conversation.read_up_to = get_message_by_id(read_up_to);

View File

@ -8,8 +8,10 @@ namespace Dino {
public class MessageManager : StreamInteractionModule, Object {
public const string ID = "message_manager";
public signal void pre_message_received(Entities.Message message, Conversation conversation);
public signal void pre_message_received(Entities.Message message, Xmpp.Message.Stanza message_stanza, Conversation conversation);
public signal void message_received(Entities.Message message, Conversation conversation);
public signal void out_message_created(Entities.Message message, Conversation conversation);
public signal void pre_message_send(Entities.Message message, Xmpp.Message.Stanza message_stanza, Conversation conversation);
public signal void message_sent(Entities.Message message, Conversation conversation);
private StreamInteractor stream_interactor;
@ -78,7 +80,6 @@ public class MessageManager : StreamInteractionModule, Object {
stream_interactor.module_manager.get_module(account, Xmpp.Message.Module.IDENTITY).received_message.connect( (stream, message) => {
on_message_received(account, message);
});
stream_interactor.stream_negotiated.connect(send_unsent_messages);
}
private void send_unsent_messages(Account account) {
@ -110,11 +111,8 @@ public class MessageManager : StreamInteractionModule, Object {
Xep.DelayedDelivery.MessageFlag? deleyed_delivery_flag = Xep.DelayedDelivery.MessageFlag.get_flag(message);
new_message.time = deleyed_delivery_flag != null ? deleyed_delivery_flag.datetime : new DateTime.now_utc();
new_message.local_time = new DateTime.now_utc();
if (Xep.Pgp.MessageFlag.get_flag(message) != null) {
new_message.encryption = Entities.Message.Encryption.PGP;
}
Conversation conversation = ConversationManager.get_instance(stream_interactor).get_add_conversation(new_message.counterpart, account);
pre_message_received(new_message, conversation);
pre_message_received(new_message, message, conversation);
bool is_uuid = new_message.stanza_id != null && Regex.match_simple("""[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}""", new_message.stanza_id);
if ((is_uuid && !db.contains_message_by_stanza_id(new_message.stanza_id)) ||
@ -149,43 +147,37 @@ public class MessageManager : StreamInteractionModule, Object {
message.direction = Entities.Message.DIRECTION_SENT;
message.counterpart = conversation.counterpart;
message.ourpart = new Jid(conversation.account.bare_jid.to_string() + "/" + conversation.account.resourcepart);
message.marked = Entities.Message.Marked.UNSENT;
message.encryption = conversation.encryption;
if (conversation.encryption == Conversation.Encryption.PGP) {
message.encryption = Entities.Message.Encryption.PGP;
}
out_message_created(message, conversation);
return message;
}
private void send_xmpp_message(Entities.Message message, Conversation conversation, bool delayed = false) {
Core.XmppStream stream = stream_interactor.get_stream(conversation.account);
message.marked = Entities.Message.Marked.NONE;
if (stream != null) {
Xmpp.Message.Stanza new_message = new Xmpp.Message.Stanza(message.stanza_id);
new_message.to = message.counterpart.to_string();
new_message.body = message.body;
if (conversation.type_ == Conversation.Type.GROUPCHAT) {
new_message.type_ = Xmpp.Message.Stanza.TYPE_GROUPCHAT;
} else {
new_message.type_ = Xmpp.Message.Stanza.TYPE_CHAT;
}
if (message.encryption == Entities.Message.Encryption.PGP) {
string? key_id = PgpManager.get_instance(stream_interactor).get_key_id(conversation.account, message.counterpart);
if (key_id != null) {
bool encrypted = stream.get_module(Xep.Pgp.Module.IDENTITY).encrypt(new_message, key_id);
if (!encrypted) {
message.marked = Entities.Message.Marked.WONTSEND;
return;
}
lock (messages) {
Core.XmppStream stream = stream_interactor.get_stream(conversation.account);
message.marked = Entities.Message.Marked.NONE;
if (stream != null) {
Xmpp.Message.Stanza new_message = new Xmpp.Message.Stanza(message.stanza_id);
new_message.to = message.counterpart.to_string();
new_message.body = message.body;
if (conversation.type_ == Conversation.Type.GROUPCHAT) {
new_message.type_ = Xmpp.Message.Stanza.TYPE_GROUPCHAT;
} else {
new_message.type_ = Xmpp.Message.Stanza.TYPE_CHAT;
}
pre_message_send(message, new_message, conversation);
if (message.marked == Entities.Message.Marked.UNSENT || message.marked == Entities.Message.Marked.WONTSEND) return;
if (delayed) {
stream.get_module(Xmpp.Xep.DelayedDelivery.Module.IDENTITY).set_message_delay(new_message, message.time);
}
stream.get_module(Xmpp.Message.Module.IDENTITY).send_message(stream, new_message);
message.stanza_id = new_message.id;
message.stanza = new_message;
} else {
message.marked = Entities.Message.Marked.UNSENT;
}
if (delayed) {
stream.get_module(Xmpp.Xep.DelayedDelivery.Module.IDENTITY).set_message_delay(new_message, message.time);
}
stream.get_module(Xmpp.Message.Module.IDENTITY).send_message(stream, new_message);
message.stanza_id = new_message.id;
message.stanza = new_message;
} else {
message.marked = Entities.Message.Marked.UNSENT;
}
}
}

View File

@ -161,7 +161,7 @@ public class MucManager : StreamInteractionModule, Object {
if (stream != null) stream.get_module(Xep.Bookmarks.Module.IDENTITY).get_conferences(stream, new BookmarksRetrieveResponseListener(this, account));
}
private void on_pre_message_received(Entities.Message message, Conversation conversation) {
private void on_pre_message_received(Entities.Message message, Xmpp.Message.Stanza message_stanza, Conversation conversation) {
if (conversation.type_ != Conversation.Type.GROUPCHAT) return;
Core.XmppStream stream = stream_interactor.get_stream(conversation.account);
if (stream == null) return;

View File

@ -1,4 +1,5 @@
using Gee;
using Xmpp;
using Xmpp;
using Dino.Entities;
@ -16,6 +17,27 @@ namespace Dino {
public static void start(StreamInteractor stream_interactor, Database db) {
PgpManager m = new PgpManager(stream_interactor, db);
stream_interactor.add_module(m);
(GLib.Application.get_default() as Application).plugin_registry.register_encryption_list_entry(new EncryptionListEntry(m));
}
private class EncryptionListEntry : Plugins.EncryptionListEntry, Object {
private PgpManager pgp_manager;
public EncryptionListEntry(PgpManager pgp_manager) {
this.pgp_manager = pgp_manager;
}
public Entities.Encryption encryption { get {
return Encryption.PGP;
}}
public string name { get {
return "OpenPGP";
}}
public bool can_encrypt(Entities.Conversation conversation) {
return pgp_manager.pgp_key_ids.has_key(conversation.counterpart);
}
}
private PgpManager(StreamInteractor stream_interactor, Database db) {
@ -23,6 +45,27 @@ namespace Dino {
this.db = db;
stream_interactor.account_added.connect(on_account_added);
MessageManager.get_instance(stream_interactor).pre_message_received.connect(on_pre_message_received);
MessageManager.get_instance(stream_interactor).pre_message_send.connect(on_pre_message_send);
}
private void on_pre_message_received(Entities.Message message, Xmpp.Message.Stanza message_stanza, Conversation conversation) {
if (Xep.Pgp.MessageFlag.get_flag(message_stanza) != null && Xep.Pgp.MessageFlag.get_flag(message_stanza).decrypted) {
message.encryption = Encryption.PGP;
}
}
private void on_pre_message_send(Entities.Message message, Xmpp.Message.Stanza message_stanza, Conversation conversation) {
if (message.encryption == Encryption.PGP) {
string? key_id = get_key_id(conversation.account, message.counterpart);
bool encrypted = false;
if (key_id != null) {
encrypted = stream_interactor.get_stream(conversation.account).get_module(Xep.Pgp.Module.IDENTITY).encrypt(message_stanza, key_id);
}
if (!encrypted) {
message.marked = Entities.Message.Marked.WONTSEND;
}
}
}
public string? get_key_id(Account account, Jid jid) {

View File

@ -64,7 +64,7 @@ public class StreamInteractor {
}
public interface StreamInteractionModule : Object {
internal abstract string get_id();
public abstract string get_id();
}
}

View File

@ -33,7 +33,7 @@ public class MergedMessageItem : Grid {
string display_name = Util.get_message_display_name(stream_interactor, message, conversation.account);
name_label.set_markup(@"<span foreground=\"#$(Util.get_name_hex_color(display_name))\">$display_name</span>");
Util.image_set_from_scaled_pixbuf(image, (new AvatarGenerator(30, 30, image.scale_factor)).draw_message(stream_interactor, message));
if (message.encryption == Entities.Message.Encryption.PGP) {
if (message.encryption != Encryption.NONE) {
encryption_image.visible = true;
encryption_image.set_from_icon_name("changes-prevent-symbolic", IconSize.SMALL_TOOLBAR);
}

View File

@ -1,4 +1,5 @@
using Gtk;
using Gee;
using Dino.Entities;
@ -12,7 +13,7 @@ public class ConversationTitlebar : Gtk.HeaderBar {
[GtkChild] private MenuButton groupchat_button;
private RadioButton? button_unencrypted;
private RadioButton? button_pgp;
private Map<RadioButton, Plugins.EncryptionListEntry> encryption_radios = new HashMap<RadioButton, Plugins.EncryptionListEntry>();
private StreamInteractor stream_interactor;
private Conversation? conversation;
@ -36,22 +37,19 @@ public class ConversationTitlebar : Gtk.HeaderBar {
}
private void update_encryption_menu_state() {
string? pgp_id = PgpManager.get_instance(stream_interactor).get_key_id(conversation.account, conversation.counterpart);
button_pgp.set_sensitive(pgp_id != null);
switch (conversation.encryption) {
case Conversation.Encryption.UNENCRYPTED:
button_unencrypted.set_active(true);
break;
case Conversation.Encryption.PGP:
button_pgp.set_active(true);
break;
foreach (RadioButton e in encryption_radios.keys) {
e.set_sensitive(encryption_radios[e].can_encrypt(conversation));
if (conversation.encryption == encryption_radios[e].encryption) e.set_active(true);
}
if (conversation.encryption == Encryption.NONE) {
button_unencrypted.set_active(true);
}
}
private void update_encryption_menu_icon() {
encryption_button.visible = (conversation.type_ == Conversation.Type.CHAT);
if (conversation.type_ == Conversation.Type.CHAT) {
if (conversation.encryption == Conversation.Encryption.UNENCRYPTED) {
if (conversation.encryption == Encryption.NONE) {
encryption_button.set_image(new Image.from_icon_name("changes-allow-symbolic", IconSize.BUTTON));
} else {
encryption_button.set_image(new Image.from_icon_name("changes-prevent-symbolic", IconSize.BUTTON));
@ -92,25 +90,35 @@ public class ConversationTitlebar : Gtk.HeaderBar {
menu_button.set_menu_model(menu);
}
private void encryption_changed() {
foreach (RadioButton e in encryption_radios.keys) {
if (e.get_active()) {
conversation.encryption = encryption_radios[e].encryption;
update_encryption_menu_icon();
return;
}
}
conversation.encryption = Encryption.NONE;
update_encryption_menu_icon();
}
private void create_encryption_menu() {
Builder builder = new Builder.from_resource("/org/dino-im/menu_encryption.ui");
PopoverMenu menu = builder.get_object("menu_encryption") as PopoverMenu;
Box encryption_box = builder.get_object("encryption_box") as Box;
button_unencrypted = builder.get_object("button_unencrypted") as RadioButton;
button_pgp = builder.get_object("button_pgp") as RadioButton;
button_unencrypted.toggled.connect(encryption_changed);
Application app = GLib.Application.get_default() as Application;
foreach(var e in app.plugin_registry.encryption_list_entries) {
RadioButton btn = new RadioButton.with_label(button_unencrypted.get_group(), e.name);
encryption_radios[btn] = e;
btn.toggled.connect(encryption_changed);
btn.visible = true;
encryption_box.pack_end(btn, false);
}
encryption_button.set_use_popover(true);
encryption_button.set_popover(menu);
encryption_button.set_image(new Image.from_icon_name("changes-allow-symbolic", IconSize.BUTTON));
button_unencrypted.toggled.connect(() => {
if (conversation != null) {
if (button_unencrypted.get_active()) {
conversation.encryption = Conversation.Encryption.UNENCRYPTED;
} else if (button_pgp.get_active()) {
conversation.encryption = Conversation.Encryption.PGP;
}
update_encryption_menu_icon();
}
});
}
private void on_groupchat_subject_set(Account account, Jid jid, string subject) {

View File

@ -5,16 +5,17 @@ namespace Dino {
void main(string[] args) {
Gtk.init(ref args);
Dino.Ui.Application app = new Dino.Ui.Application();
PluginLoader loader = new PluginLoader();
Application app = new Application();
Plugins.Loader loader = new Plugins.Loader();
foreach(string plugin in new string[]{}) {
try {
loader.load(plugin, app);
} catch (Dino.PluginError e) {
} catch (Plugins.Error e) {
print(@"Error loading plugin $plugin: $(e.message)\n");
}
}
app.run(args);
loader.shutdown();
}
}

View File

@ -1,13 +1,13 @@
namespace Xmpp {
string? get_bare_jid(string jid) {
public string? get_bare_jid(string jid) {
return jid.split("/")[0];
}
bool is_bare_jid(string jid) {
public bool is_bare_jid(string jid) {
return !jid.contains("/");
}
string? get_resource_part(string jid) {
public string? get_resource_part(string jid) {
return jid.split("/")[1];
}