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