1/* 2 * Copyright (C) 2010 Michal Hruby <michal.mhr@gmail.com> 3 * Copyright (C) 2010 Alberto Aldegheri <albyrock87+dev@gmail.com> 4 * 5 * This program is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation; either version 2 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program; if not, write to the Free Software 17 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Authored by Alberto Aldegheri <albyrock87+dev@gmail.com> 20 * 21 */ 22 23namespace Synapse 24{ 25 [DBus (name = "im.pidgin.purple.PurpleInterface")] 26 interface PurpleInterface : Object { 27 public const string UNIQUE_NAME = "im.pidgin.purple.PurpleService"; 28 public const string OBJECT_PATH = "/im/pidgin/purple/PurpleObject"; 29 30 public abstract string purple_account_get_protocol_name (int account) throws Error; 31 public abstract int purple_buddy_get_account (int buddy) throws Error; 32 public abstract string purple_buddy_get_name (int buddy) throws Error; 33 public abstract string purple_buddy_get_alias (int buddy) throws Error; 34 public abstract string purple_buddy_icon_get_full_path (int icon) throws Error; 35 public abstract int purple_buddy_get_icon (int buddy) throws Error; 36 public abstract int purple_buddy_is_online (int buddy) throws Error; 37 38 public abstract int[] purple_accounts_get_all_active () throws Error; 39 public abstract int[] purple_find_buddies (int account, string pattern = "") throws Error; 40 41 public abstract int purple_conversation_new (int type, int account, string name) throws Error; 42 public abstract void purple_conversation_present (int conv) throws Error; 43 public abstract int purple_conv_im (int conv) throws Error; 44 public abstract void purple_conv_im_send (int im, string mess) throws Error; 45 46 public abstract signal void account_added (int acc); 47 public abstract signal void account_removed (int acc); 48 public abstract signal void buddy_added (int buddy); 49 public abstract signal void buddy_removed (int buddy); 50 public abstract signal void buddy_signed_on (int buddy); 51 public abstract signal void buddy_signed_off (int buddy); 52 public abstract signal void buddy_icon_changed (int buddy); 53 54 public abstract void serv_send_file (int conn, string who, string file) throws Error; 55 public abstract int purple_account_get_connection (int account) throws Error; 56 } 57 58 public class PidginPlugin : Object, Activatable, ItemProvider, ActionProvider 59 { 60 public bool enabled { get; set; default = true; } 61 62 public void activate () 63 { 64 65 } 66 67 public void deactivate () 68 { 69 70 } 71 72 static void register_plugin () 73 { 74 PluginRegistry.get_default ().register_plugin ( 75 typeof (PidginPlugin), 76 "Pidgin", 77 _("Get access to your Pidgin contacts"), 78 "pidgin", 79 register_plugin, 80 Environment.find_program_in_path ("pidgin") != null, 81 _("Pidgin is not installed.") 82 ); 83 } 84 85 static construct 86 { 87 register_plugin (); 88 } 89 90 private class SendToContact : Action 91 { 92 public SendToContact () 93 { 94 Object (title: _("Send in chat to.."), 95 description: _("Send selected file within Pidgin"), 96 icon_name: "document-send", has_thumbnail: false, 97 default_relevancy: MatchScore.AVERAGE); 98 } 99 100 public override void do_execute (Match match, Match? target = null) 101 { 102 unowned Contact? c = target as Contact; 103 return_if_fail (c != null); 104 unowned UriMatch? u = match as UriMatch; 105 return_if_fail (u != null); 106 107 c.send_file (u.uri); 108 } 109 110 public override bool valid_for_match (Match match) 111 { 112 return (match is UriMatch && (((UriMatch) match).file_type & QueryFlags.FILES) != 0); 113 } 114 115 public override bool needs_target () 116 { 117 return true; 118 } 119 120 public override QueryFlags target_flags () 121 { 122 return QueryFlags.CONTACTS; 123 } 124 } 125 126 private class Contact : ContactMatch 127 { 128 public PidginPlugin plugin { get; construct set; } 129 130 public int account_id { get; construct set; } 131 public int contact_id { get; construct set; } 132 public string name { get; construct set; } 133 public bool online { get; set; } 134 135 public Contact (PidginPlugin plugin, int account_id, int contact_id, string name, bool online, 136 string alias, string? icon_path, string description) 137 { 138 Object (title: alias, 139 description: description, 140 online: online, 141 name: name, 142 icon_name: icon_path ?? "stock_person", 143 has_thumbnail: false, 144 plugin: plugin, 145 account_id: account_id, 146 contact_id: contact_id); 147 } 148 149 public override void send_message (string message, bool present) 150 { 151 plugin.send_message (this, message, present); 152 } 153 154 public override void open_chat () 155 { 156 plugin.open_chat (this); 157 } 158 159 public void send_file (string path) 160 { 161 plugin.send_file (this, path); 162 } 163 } 164 165 private void send_file (Contact contact, string uri) 166 { 167 File f; 168 f = File.new_for_uri (uri); 169 if (!f.query_exists ()) 170 { 171 warning (_("File \"%s\"does not exist."), uri); 172 return; 173 } 174 string path = f.get_path (); 175 try { 176 int conn = p.purple_account_get_connection (contact.account_id); 177 if (conn <= 0) 178 { 179 warning ("Cannot send file to %s", contact.title); 180 return; 181 } 182 p.serv_send_file (conn, contact.name, path); 183 } catch (Error err) 184 { 185 warning ("Cannot send file to %s", contact.title); 186 } 187 } 188 189 private void send_message (Contact contact, string? message, bool present) 190 { 191 try { 192 var conv = p.purple_conversation_new (1, contact.account_id, contact.name); 193 if (message != null) 194 { 195 var im = p.purple_conv_im (conv); 196 p.purple_conv_im_send (im, message); 197 } 198 if (present) p.purple_conversation_present (conv); 199 } catch (Error err) 200 { 201 warning ("Cannot open chat for %s", contact.title); 202 } 203 } 204 205 private void open_chat (Contact contact) 206 { 207 send_message (contact, null, true); 208 } 209 210 private Gee.Map<int, Contact> contacts; 211 private PurpleInterface p; 212 213 private void connect_to_bus () 214 { 215 p = null; 216 217 try { 218 p = Bus.get_proxy_sync (BusType.SESSION, 219 PurpleInterface.UNIQUE_NAME, 220 PurpleInterface.OBJECT_PATH); 221 } 222 catch { 223 } 224 225 if (p != null) 226 { 227 init_contacts.begin ( 228 (obj, res) => { 229 connect_to_signals (); 230 }); 231 } 232 } 233 234 private void connect_to_signals () 235 { 236 p.account_added.connect ((acc) => { 237 init_contacts.begin (); 238 }); 239 240 p.account_removed.connect ((acc) => { 241 init_contacts.begin (); 242 }); 243 244 p.buddy_added.connect ((buddy) => { 245 contact_changed (buddy, -1, 1); 246 }); 247 p.buddy_removed.connect ((buddy) => { 248 contact_changed (buddy, -1, 0); 249 }); 250 p.buddy_signed_on.connect ((buddy) => { 251 contact_changed (buddy, 1); 252 }); 253 p.buddy_signed_off.connect ((buddy) => { 254 contact_changed (buddy, 0); 255 }); 256 p.buddy_icon_changed.connect ((buddy) => { 257 contact_changed (buddy, -1, 0); 258 contact_changed (buddy, -1, 1); 259 }); 260 } 261 262 private void contact_changed (int buddy, int online = -1, int addremove = -1) 263 { 264 if (online >= 0) 265 { 266 var contact = contacts[buddy]; 267 if (contact == null) return; 268 contact.online = online > 0; 269 } 270 else if (addremove >= 0) 271 { 272 if (addremove == 1) 273 get_contact.begin (buddy); 274 else 275 contacts.unset (buddy); 276 } 277 } 278 279 private Gee.List<Action> actions; 280 281 construct 282 { 283 actions = new Gee.ArrayList<Action> (); 284 actions.add (new SendToContact ()); 285 286 contacts = new Gee.HashMap<int, Contact> (); 287 var service = DBusService.get_default (); 288 289 if (service.name_has_owner (PurpleInterface.UNIQUE_NAME)) 290 { 291 connect_to_bus (); 292 } 293 294 service.owner_changed.connect ((name, is_owned) => { 295 if (name == PurpleInterface.UNIQUE_NAME) 296 { 297 if (is_owned) 298 connect_to_bus (); 299 else 300 { 301 p = null; 302 contacts.clear (); 303 } 304 } 305 }); 306 } 307 308 public ResultSet? find_for_match (ref Query query, Match match) 309 { 310 if (p == null) return null; 311 bool query_empty = query.query_string == ""; 312 var results = new ResultSet (); 313 314 if (query_empty) 315 { 316 foreach (var action in actions) 317 { 318 if (!action.valid_for_match (match)) continue; 319 results.add (action, action.get_relevancy_for_match (match)); 320 } 321 } 322 else 323 { 324 var matchers = Query.get_matchers_for_query (query.query_string, 0, 325 RegexCompileFlags.OPTIMIZE | RegexCompileFlags.CASELESS); 326 foreach (var action in actions) 327 { 328 if (!action.valid_for_match (match)) continue; 329 foreach (var matcher in matchers) 330 { 331 if (matcher.key.match (action.title)) 332 { 333 results.add (action, matcher.value); 334 break; 335 } 336 } 337 } 338 } 339 340 return results; 341 } 342 343 private async void get_contact (int buddy, int account = -1, string? protocol = null) throws Error 344 { 345 if (p == null) return; 346 string prot = protocol; 347 if (account < 0) 348 account = p.purple_buddy_get_account (buddy); 349 if (protocol == null) 350 prot = p.purple_account_get_protocol_name (account); 351 352 string alias = p.purple_buddy_get_alias (buddy); 353 string name = p.purple_buddy_get_name (buddy); 354 355 bool online = p.purple_buddy_is_online (buddy) > 0; 356 357 if (alias == null || alias == "") alias = name; 358 359 int iconid = p.purple_buddy_get_icon (buddy); 360 string icon = null; 361 if (iconid > 0) 362 icon = p.purple_buddy_icon_get_full_path (iconid); 363 364 contacts[buddy] = new Contact (this, account, buddy, name, online, alias, icon, "%s (%s)".printf (name, prot)); 365 } 366 367 private async void init_contacts () 368 { 369 contacts.clear (); 370 if (p == null) return; 371 try { 372 var accounts = p.purple_accounts_get_all_active (); 373 foreach (var account in accounts) 374 { 375 if (p == null) return; 376 var protocol = p.purple_account_get_protocol_name (account); 377 var buddies = p.purple_find_buddies (account); 378 379 foreach (var buddy in buddies) 380 { 381 if (p == null) return; 382 yield get_contact (buddy, account, protocol); 383 } 384 } 385 386 } catch (Error err) { 387 warning ("Cannot load Pidgin contacts"); 388 } 389 } 390 391 public bool handles_query (Query query) 392 { 393 return (QueryFlags.CONTACTS in query.query_type); 394 } 395 396 public async ResultSet? search (Query q) throws SearchError 397 { 398 // we only search for actions 399 if (!(QueryFlags.CONTACTS in q.query_type)) return null; 400 401 var result = new ResultSet (); 402 403 var matchers = Query.get_matchers_for_query (q.query_string, 0, 404 RegexCompileFlags.OPTIMIZE | RegexCompileFlags.CASELESS); 405 406 var matches = contacts.entries; 407 408 foreach (var contact in matches) 409 { 410 if (!contact.value.online) continue; 411 foreach (var matcher in matchers) 412 { 413 if (matcher.key.match (contact.value.title)) 414 { 415 result.add (contact.value, matcher.value - MatchScore.INCREMENT_SMALL); 416 break; 417 } 418 } 419 } 420 421 q.check_cancellable (); 422 423 return result; 424 } 425 } 426} 427