~gwibber-committers/gwibber/2.30

490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
1
#!/usr/bin/env python
674.2.1 by Behrooz
Added proper support for RTL messages
2
# -*- coding: utf-8 -*-
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
3
674.1.42 by Ken VanDine
* revert the change dropping threading from MapAsync, without it there is some blocking.
4
import multiprocessing, threading, traceback, json, time
490.1.23 by Ryan Paul
Respect the user's notification settings
5
import gobject, dbus, dbus.service, mx.DateTime
599 by Ryan Paul
Added support for Qaiku
6
import twitter, identica, statusnet, flickr, facebook
674.6.4 by Travis B. Hartwell
Remove unused import.
7
import qaiku, friendfeed, digg
490.1.23 by Ryan Paul
Respect the user's notification settings
8
527 by Ken VanDine
Ported urlshortening
9
import urlshorter
490.1.23 by Ryan Paul
Respect the user's notification settings
10
import util, util.couch
674.1.37 by Ken VanDine
MapAsync doesn't need to inherit from threading.Thread
11
import util.keyring
490.7.7 by Ken VanDine
fixed some imports
12
from util import log
490.7.6 by Ken VanDine
* Added a new console logger with the -o arg to gwibber-daemon
13
from util import resources
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
14
from util.couch import Monitor as CouchMonitor
490.1.23 by Ryan Paul
Respect the user's notification settings
15
from util.couch import RecordMonitor
693 by Ken VanDine
Raise account dialog when password is missing from the keyring
16
from util import exceptions
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
17
from desktopcouch.records.server import CouchDatabase
18
from desktopcouch.records.record import Record as CouchRecord
674.2.1 by Behrooz
Added proper support for RTL messages
19
import re
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
20
from util.const import *
21
490.2.23 by Ken VanDine
added notifications and messaging indicator support
22
try:
23
  import indicate
24
except:
25
  indicate = None
26
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
27
gobject.threads_init()
28
490.7.1 by Ken VanDine
* Log to a file
29
log.logger.name = "Gwibber Dispatcher"
30
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
31
PROTOCOLS = {
32
  "twitter": twitter,
33
  "identica": identica,
34
  "flickr": flickr,
490.2.27 by Ken VanDine
start of accounts admin for facebook
35
  "facebook": facebook,
490.1.11 by Ryan Paul
Updated the FriendFeed module to make it compatible with the new backend
36
  "friendfeed": friendfeed,
596 by Ryan Paul
Added support for StatusNet
37
  "statusnet": statusnet,
597 by Ryan Paul
Added support for Digg
38
  "digg": digg,
599 by Ryan Paul
Added support for Qaiku
39
  "qaiku": qaiku,
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
40
}
41
490.1.3 by Ryan Paul
Fixed the startup scripts in bin
42
FEATURES = json.loads(GWIBBER_OPERATIONS)
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
43
SERVICES = dict([(k, v.PROTOCOL_INFO) for k, v in PROTOCOLS.items()])
490.1.23 by Ryan Paul
Respect the user's notification settings
44
SETTINGS = RecordMonitor(COUCH_DB_SETTINGS, COUCH_RECORD_SETTINGS, COUCH_TYPE_CONFIG, DEFAULT_SETTINGS)
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
45
46
def perform_operation((acctid, opname, args, transient)):
47
  try:
48
    stream = FEATURES[opname]["stream"] or opname
49
    accounts = CouchDatabase(COUCH_DB_ACCOUNTS, create=True)
50
    messages = CouchDatabase(COUCH_DB_MESSAGES, create=True)
51
    account = dict(accounts.get_record(acctid).items())
52
711 by Ken VanDine
Fix handling of raising the keyring error (LP: #539781)
53
    if not 'failed_id' in globals():
54
      global failed_id
55
      failed_id = []
56
674.1.16 by Ryan Paul
Use the keyring to store private account fields
57
    for key, val in account.items():
58
      if isinstance(val, str) and val.startswith(":KEYRING:"):
674.1.37 by Ken VanDine
MapAsync doesn't need to inherit from threading.Thread
59
        value = util.keyring.get_account_password(account["_id"])
674.6.1 by Travis B. Hartwell
First pass at fixing LP: #554005
60
61
        if value is None:
711 by Ken VanDine
Fix handling of raising the keyring error (LP: #539781)
62
          if account["_id"] not in failed_id:
63
            log.logger.debug("Adding %s to failed_id global", account["_id"])
64
            failed_id.append(account["_id"])
65
            log.logger.debug("Raising error to resolve failure for %s", account["_id"])
66
            raise exceptions.GwibberProtocolError("keyring")
693 by Ken VanDine
Raise account dialog when password is missing from the keyring
67
          return ("Failure", 0)
674.1.16 by Ryan Paul
Use the keyring to store private account fields
68
69
        account[key] = value
70
490.1.4 by Ryan Paul
Make dispatcher use logging module for debug instead of print statements
71
    logtext = "<%s:%s>" % (account["protocol"], opname)
490.7.1 by Ken VanDine
* Log to a file
72
    log.logger.debug("%s Performing operation", logtext)
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
73
502 by Ryan Paul
Added support for liking and deleting messages
74
    args = dict((str(k), v) for k, v in args.items())
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
75
    message_data = PROTOCOLS[account["protocol"]].Client(account)(opname, **args)
76
    new_messages = []
674.2.1 by Behrooz
Added proper support for RTL messages
77
    text_cleaner = re.compile(u"[: \n\t\r♻♺]+|@[^ ]+|![^ ]+|#[^ ]+") # signs, @nickname, !group, #tag
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
78
674.1.42 by Ken VanDine
* revert the change dropping threading from MapAsync, without it there is some blocking.
79
    if message_data is not None:
80
      for m in message_data:
699 by Ken VanDine
use has_key instead of hasattr to check for message contents
81
        if m.has_key("id"):
674.1.42 by Ken VanDine
* revert the change dropping threading from MapAsync, without it there is some blocking.
82
          key = (m["id"], m["account"], opname, transient)
83
          key = "-".join(x for x in key if x)
744 by Ken VanDine
Drop the responses operation from facebook, it abuses the
84
          m["operation"] = opname
85
          m["stream"] = stream
86
          m["transient"] = transient
87
          m["rtl"] = util.isRTL(re.sub(text_cleaner, "", m["text"].decode('utf-8')))
674.1.42 by Ken VanDine
* revert the change dropping threading from MapAsync, without it there is some blocking.
88
          if not messages.record_exists(key):
89
            log.logger.debug("%s Adding record", logtext)
90
            new_messages.append(m)
91
            messages.put_record(CouchRecord(m, COUCH_TYPE_MESSAGE, key))
744 by Ken VanDine
Drop the responses operation from facebook, it abuses the
92
          else:
93
            if m.has_key("comments"):
94
              try:
95
                if m["comments"] > 0 and m["comments"] != dict(messages.get_record(key).items())["comments"]:
96
                  log.logger.debug("%s Updating record", logtext)
97
                  messages.update_fields(key, m)
98
              except:
99
                pass
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
100
490.7.1 by Ken VanDine
* Log to a file
101
    log.logger.debug("%s Finished operation", logtext)
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
102
    return ("Success", new_messages)
103
  except Exception as e:
490.1.4 by Ryan Paul
Make dispatcher use logging module for debug instead of print statements
104
    if not "logtext" in locals(): logtext = "<UNKNOWN>"
490.7.4 by Ken VanDine
set somethings to log using the error level
105
    log.logger.error("%s Operation failed", logtext)
490.7.1 by Ken VanDine
* Log to a file
106
    log.logger.debug("Traceback:\n%s", traceback.format_exc())
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
107
    return ("Failure", traceback.format_exc())
108
109
class OperationCollector:
110
  def __init__(self):
111
    self.accounts = CouchDatabase(COUCH_DB_ACCOUNTS, create=True)
112
    self.settings = CouchDatabase(COUCH_DB_SETTINGS, create=True)
113
    self.messages = CouchDatabase(COUCH_DB_MESSAGES, create=True)
114
115
  def handle_max_id(self, acct, opname, id=None):
116
    if not id: id = acct["_id"]
117
    if "sinceid" in SERVICES[acct["protocol"]]["features"]:
118
      view = self.messages.execute_view("maxid", "messages")
119
      result = view[[id, opname]][[id, opname]].rows
120
      if len(result) > 0: return {"since": result[0].value}
121
    return {}
122
123
  def validate_operation(self, acct, opname, enabled="receive_enabled"):
124
    protocol = SERVICES[acct["protocol"]]
125
    return acct["protocol"] in PROTOCOLS and \
126
           opname in protocol["features"] and \
127
           opname in FEATURES and acct[enabled]
128
490.1.25 by Ryan Paul
Perform operation immediately when transient stream is created
129
  def stream_to_operation(self, stream):
130
    account = self.accounts.get_record(stream["account"])
131
    args = stream["parameters"]
132
    opname = stream["operation"]
133
    if self.validate_operation(account, opname):
134
      args.update(self.handle_max_id(account, opname, stream["_id"]))
135
      return (stream["account"], stream["operation"], args, stream["_id"])
136
506.1.4 by Ryan Paul
Added support for search in single stream mode
137
  def search_to_operations(self, search):
138
    for account in self.accounts.get_records(COUCH_TYPE_ACCOUNT, True):
139
      account = account.value
140
      args = {"query": search["query"]}
141
      if self.validate_operation(account, "search"):
142
        args.update(self.handle_max_id(account, "search", search["_id"]))
143
        yield (account["_id"], "search", args, search["_id"])
144
540 by Ryan Paul
Force account to refresh when it is created
145
  def account_to_operations(self, acct):
146
    if isinstance(acct, basestring):
147
      acct = dict(self.accounts.get_record(acct).items())
148
    for opname in SERVICES[acct["protocol"]]["default_streams"]:
149
      if self.validate_operation(acct, opname):
150
        args = self.handle_max_id(acct, opname)
151
        yield (acct["_id"], opname, args, False)
152
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
153
  def get_send_operations(self, message):
490.1.6 by Ryan Paul
Make sure that the CouchDB databases all get properly initialized
154
    for account in self.accounts.get_records(COUCH_TYPE_ACCOUNT, True):
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
155
      account = account.value
156
      if self.validate_operation(account, "send", "send_enabled"):
490.1.25 by Ryan Paul
Perform operation immediately when transient stream is created
157
        yield (account["_id"], "send", {"message": message}, False)
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
158
490.1.25 by Ryan Paul
Perform operation immediately when transient stream is created
159
  def get_operation_by_id(self, id):
160
    if self.settings.record_exists(id):
161
      item = dict(self.settings.get_record(id).items())
162
      if item["record_type"] == COUCH_TYPE_STREAM:
506.1.4 by Ryan Paul
Added support for search in single stream mode
163
        return [self.stream_to_operation(item)]
164
      if item["record_type"] == COUCH_TYPE_SEARCH:
165
        return list(self.search_to_operations(item))
674.6.2 by Travis B. Hartwell
Undo whitespace changes made previously.
166
        
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
167
  def get_operations(self):
490.1.6 by Ryan Paul
Make sure that the CouchDB databases all get properly initialized
168
    for acct in self.accounts.get_records(COUCH_TYPE_ACCOUNT, True):
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
169
      acct = acct.value
540 by Ryan Paul
Force account to refresh when it is created
170
      for o in self.account_to_operations(acct):
171
        yield o
674.6.2 by Travis B. Hartwell
Undo whitespace changes made previously.
172
      
490.1.6 by Ryan Paul
Make sure that the CouchDB databases all get properly initialized
173
    for stream in self.settings.get_records(COUCH_TYPE_STREAM, True):
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
174
      stream = stream.value
175
      if self.accounts.record_exists(stream["account"]):
490.1.27 by Ryan Paul
Refactored message sending in the Gwibber backend
176
        o = self.stream_to_operation(stream)
177
        if o: yield o
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
178
490.1.6 by Ryan Paul
Make sure that the CouchDB databases all get properly initialized
179
    for search in self.settings.get_records(COUCH_TYPE_SEARCH, True):
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
180
      search = search.value
506.1.4 by Ryan Paul
Added support for search in single stream mode
181
      for o in self.search_to_operations(search):
182
        yield o
490.1.22 by Ryan Paul
Improved default handling for RecordMonitor
183
490.1.20 by Ryan Paul
Make the navigation update when transient streams are added or removed
184
class StreamMonitor(dbus.service.Object):
185
  __dbus_object_path__ = "/com/gwibber/Streams"
674.6.2 by Travis B. Hartwell
Undo whitespace changes made previously.
186
  
490.1.20 by Ryan Paul
Make the navigation update when transient streams are added or removed
187
  def __init__(self):
188
    self.bus = dbus.SessionBus()
517 by Ken VanDine
* Make the dbus interface names match the object path
189
    bus_name = dbus.service.BusName("com.Gwibber.Streams", bus=self.bus)
490.1.20 by Ryan Paul
Make the navigation update when transient streams are added or removed
190
    dbus.service.Object.__init__(self, bus_name, self.__dbus_object_path__)
191
192
    setting_monitor = CouchMonitor(COUCH_DB_SETTINGS)
193
    setting_monitor.connect("record-updated", self.on_setting_changed)
194
    setting_monitor.connect("record-deleted", self.on_setting_deleted)
195
196
  def on_setting_changed(self, monitor, id):
197
    if id == "settings": self.SettingChanged()
198
    else:
490.7.6 by Ken VanDine
* Added a new console logger with the -o arg to gwibber-daemon
199
      log.logger.debug("Stream changed: %s", id)
490.1.20 by Ryan Paul
Make the navigation update when transient streams are added or removed
200
      self.StreamChanged(id)
201
202
  def on_setting_deleted(self, monitor, id):
490.7.6 by Ken VanDine
* Added a new console logger with the -o arg to gwibber-daemon
203
    log.logger.debug("Stream closed: %s", id)
490.1.20 by Ryan Paul
Make the navigation update when transient streams are added or removed
204
    self.StreamClosed(id)
205
517 by Ken VanDine
* Make the dbus interface names match the object path
206
  @dbus.service.signal("com.Gwibber.Streams", signature="s")
490.1.20 by Ryan Paul
Make the navigation update when transient streams are added or removed
207
  def StreamChanged(self, id): pass
208
517 by Ken VanDine
* Make the dbus interface names match the object path
209
  @dbus.service.signal("com.Gwibber.Streams", signature="s")
490.1.20 by Ryan Paul
Make the navigation update when transient streams are added or removed
210
  def StreamClosed(self, id): pass
211
517 by Ken VanDine
* Make the dbus interface names match the object path
212
  @dbus.service.signal("com.Gwibber.Streams")
490.1.20 by Ryan Paul
Make the navigation update when transient streams are added or removed
213
  def SettingChanged(self): pass
214
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
215
class AccountMonitor(dbus.service.Object):
216
  __dbus_object_path__ = "/com/gwibber/Accounts"
217
218
  def __init__(self):
219
    self.bus = dbus.SessionBus()
517 by Ken VanDine
* Make the dbus interface names match the object path
220
    bus_name = dbus.service.BusName("com.Gwibber.Accounts", bus=self.bus)
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
221
    dbus.service.Object.__init__(self, bus_name, self.__dbus_object_path__)
222
490.1.20 by Ryan Paul
Make the navigation update when transient streams are added or removed
223
    account_monitor = CouchMonitor(COUCH_DB_ACCOUNTS)
224
    account_monitor.connect("record-updated", self.on_account_changed)
225
    account_monitor.connect("record-deleted", self.on_account_deleted)
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
226
227
  def on_account_changed(self, monitor, id):
490.7.1 by Ken VanDine
* Log to a file
228
    log.logger.debug("Account changed: %s", id)
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
229
    self.AccountChanged(id)
230
231
  def on_account_deleted(self, monitor, id):
490.7.1 by Ken VanDine
* Log to a file
232
    log.logger.debug("Account deleted: %s", id)
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
233
    self.AccountDeleted(id)
234
517 by Ken VanDine
* Make the dbus interface names match the object path
235
  @dbus.service.signal("com.Gwibber.Accounts", signature="s")
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
236
  def AccountChanged(self, id): pass
237
517 by Ken VanDine
* Make the dbus interface names match the object path
238
  @dbus.service.signal("com.Gwibber.Accounts", signature="s")
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
239
  def AccountDeleted(self, id): pass
240
490.2.23 by Ken VanDine
added notifications and messaging indicator support
241
class MessagesMonitor(dbus.service.Object):
242
  __dbus_object_path__ = "/com/gwibber/Messages"
243
244
  def __init__(self):
245
    self.bus = dbus.SessionBus()
517 by Ken VanDine
* Make the dbus interface names match the object path
246
    bus_name = dbus.service.BusName("com.Gwibber.Messages", bus=self.bus)
490.2.23 by Ken VanDine
added notifications and messaging indicator support
247
    dbus.service.Object.__init__(self, bus_name, self.__dbus_object_path__)
248
647 by Ken VanDine
Moved the init_design_doc method call to MessageMonitor, this fixes a bug loading the design document before it is created
249
    self.messages = CouchDatabase(COUCH_DB_MESSAGES, create=True)
250
    util.couch.init_design_doc(self.messages, "messages", COUCH_VIEW_MESSAGES)
674.1.16 by Ryan Paul
Use the keyring to store private account fields
251
    #util.couch.exclude_databases(["gwibber_messages"])
647 by Ken VanDine
Moved the init_design_doc method call to MessageMonitor, this fixes a bug loading the design document before it is created
252
490.2.23 by Ken VanDine
added notifications and messaging indicator support
253
    self.monitor = CouchMonitor(COUCH_DB_MESSAGES)
254
    self.monitor.connect("record-updated", self.on_message_updated)
255
256
    self.indicator_items = {}
257
    self.notified_items = []
258
515 by Ken VanDine
- Don't use the indicator if we can't find a desktop file
259
    if indicate and util.resources.get_desktop_file():
490.2.23 by Ken VanDine
added notifications and messaging indicator support
260
      self.indicate = indicate.indicate_server_ref_default()
261
      self.indicate.set_type("message.gwibber")
515 by Ken VanDine
- Don't use the indicator if we can't find a desktop file
262
      self.indicate.set_desktop_file(util.resources.get_desktop_file())
490.2.23 by Ken VanDine
added notifications and messaging indicator support
263
      self.indicate.connect("server-display", self.on_indicator_activate)
264
      self.indicate.show()
265
266
  def on_message_updated(self, monitor, id):
490.5.3 by Ken VanDine
* make config values in PROTOCOL_INFO required for account data
267
    try:
607 by Ryan Paul
Properly escape HTML so that it isn't treated as markup in the stream
268
      #log.logger.debug("Message updated: %s", id)
490.5.3 by Ken VanDine
* make config values in PROTOCOL_INFO required for account data
269
      message = self.messages.get_record(id)
270
      self.new_message(message)
271
    except:
490.7.4 by Ken VanDine
set somethings to log using the error level
272
      log.logger.error("Message updated: %s, failed", id)
674.6.2 by Travis B. Hartwell
Undo whitespace changes made previously.
273
    
490.2.23 by Ken VanDine
added notifications and messaging indicator support
274
517 by Ken VanDine
* Make the dbus interface names match the object path
275
  @dbus.service.signal("com.Gwibber.Messages", signature="s")
490.2.23 by Ken VanDine
added notifications and messaging indicator support
276
  def MessageUpdated(self, id): pass
277
576 by Ken VanDine
* removed an import for gtk
278
  def on_indicator_activate(self, indicator, timestamp):
490.2.23 by Ken VanDine
added notifications and messaging indicator support
279
    dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
280
    client_bus = dbus.SessionBus()
490.7.1 by Ken VanDine
* Log to a file
281
    log.logger.debug("Raising gwibber client")
490.2.23 by Ken VanDine
added notifications and messaging indicator support
282
    try:
283
      client_obj = client_bus.get_object("com.GwibberClient",
284
        "/com/GwibberClient", follow_name_owner_changes = True,
285
        introspect = False)
286
      gw = dbus.Interface(client_obj, "com.GwibberClient")
287
      gw.focus_client(reply_handler=self.handle_focus_reply,
288
                      error_handler=self.handle_focus_error)
289
    except dbus.DBusException:
632 by Ken VanDine
fixed failed calls to print_exc
290
      traceback.print_exc()
490.2.23 by Ken VanDine
added notifications and messaging indicator support
291
292
576 by Ken VanDine
* removed an import for gtk
293
  def on_indicator_reply_activate(self, indicator, timestamp):
490.7.1 by Ken VanDine
* Log to a file
294
    log.logger.debug("Raising gwibber client, focusing replies stream")
490.2.23 by Ken VanDine
added notifications and messaging indicator support
295
    client_bus = dbus.SessionBus()
296
    try:
297
      client_obj = client_bus.get_object("com.GwibberClient", "/com/GwibberClient")
298
      gw = dbus.Interface(client_obj, "com.GwibberClient")
299
      gw.show_replies(reply_handler=self.handle_focus_reply,
300
                      error_handler=self.handle_focus_error)
301
      indicator.hide()
302
    except dbus.DBusException:
632 by Ken VanDine
fixed failed calls to print_exc
303
      traceback.print_exc()
490.2.23 by Ken VanDine
added notifications and messaging indicator support
304
519 by Ken VanDine
Really raise the client when called
305
  def handle_focus_reply(self, *args):
615 by Ken VanDine
* fixed a bug that prevented mentions/replies to get added to the messaging indicator
306
    log.logger.debug("Gwibber Client raised")
490.2.23 by Ken VanDine
added notifications and messaging indicator support
307
519 by Ken VanDine
Really raise the client when called
308
  def handle_focus_error(self, *args):
615 by Ken VanDine
* fixed a bug that prevented mentions/replies to get added to the messaging indicator
309
    log.logger.error("Failed to raise client %s", args)
490.2.23 by Ken VanDine
added notifications and messaging indicator support
310
311
  def new_message(self, message):
312
    min_time = mx.DateTime.DateTimeFromTicks() - mx.DateTime.TimeDelta(minutes=10.0)
615 by Ken VanDine
* fixed a bug that prevented mentions/replies to get added to the messaging indicator
313
    log.logger.debug("Checking message %s timestamp (%s) to see if it is newer than %s", message["id"], mx.DateTime.DateTimeFromTicks(message["time"]).localtime(), min_time)
490.2.25 by Ken VanDine
compare timestamps using localtime and added a little more logging
314
    if mx.DateTime.DateTimeFromTicks(message["time"]).localtime()  > mx.DateTime.DateTimeFromTicks(min_time):
674.6.2 by Travis B. Hartwell
Undo whitespace changes made previously.
315
      log.logger.debug("Message %s newer than %s, notifying", message["id"], min_time)      
632.2.1 by Ken VanDine
Fixed notifications for mentions only
316
      if indicate and message["to_me"]:
490.1.23 by Ryan Paul
Respect the user's notification settings
317
        if message["id"] not in self.indicator_items:
615 by Ken VanDine
* fixed a bug that prevented mentions/replies to get added to the messaging indicator
318
          log.logger.debug("Message %s is a reply, adding messaging indicator", message["id"])
319
          self.handle_indicator_item(message)
490.2.23 by Ken VanDine
added notifications and messaging indicator support
320
      if message["id"] not in self.notified_items:
321
        self.notified_items.append(message["id"])
322
        self.show_notification_bubble(message)
323
490.1.23 by Ryan Paul
Respect the user's notification settings
324
  def handle_indicator_item(self, message):
325
    indicator = indicate.Indicator() if hasattr(indicate, "Indicator") else indicate.IndicatorMessage()
326
    indicator.connect("user-display", self.on_indicator_reply_activate)
615 by Ken VanDine
* fixed a bug that prevented mentions/replies to get added to the messaging indicator
327
    indicator.set_property("subtype", "im.gwibber")
490.1.23 by Ryan Paul
Respect the user's notification settings
328
    indicator.set_property("sender", message["sender"].get("name", ""))
329
    indicator.set_property("body", message["text"])
330
    indicator.set_property_time("time",
331
        mx.DateTime.DateTimeFromTicks(message["time"]).localtime().ticks())
332
    self.indicator_items[message["id"]] = indicator
333
    indicator.show()
615 by Ken VanDine
* fixed a bug that prevented mentions/replies to get added to the messaging indicator
334
    log.logger.debug("Message from %s added to indicator", message["sender"].get("name", ""))
490.1.23 by Ryan Paul
Respect the user's notification settings
335
490.2.23 by Ken VanDine
added notifications and messaging indicator support
336
  def show_notification_bubble(self, message):
490.1.23 by Ryan Paul
Respect the user's notification settings
337
    if util.can_notify and SETTINGS["show_notifications"]:
632.2.1 by Ken VanDine
Fixed notifications for mentions only
338
      if SETTINGS["notify_mentions_only"] and not message["to_me"]: return
695 by Ken VanDine
Respect full name preference in notification bubbles
339
      
340
      if SETTINGS["show_fullname"]:
341
        sender_name = message["sender"].get("name", 0) or data["sender"].get("nick", "")
342
      else:
343
        sender_name = message["sender"].get("nick", 0) or data["sender"].get("name", "")
344
490.2.23 by Ken VanDine
added notifications and messaging indicator support
345
      #until image caching is working again, we will post the gwibber icon
346
      #image = hasattr(message, "image_path") and message["image_path"] or ''
490.5.5 by Ken VanDine
* Moved the gtk widgets from gwibber.microblog.gtk to gwibber.lib.gtk
347
      image = util.resources.get_ui_asset("gwibber.svg")
490.2.23 by Ken VanDine
added notifications and messaging indicator support
348
      expire_timeout = 5000
695 by Ken VanDine
Respect full name preference in notification bubbles
349
      n = util.notify(sender_name, message["text"], image, expire_timeout)
490.2.23 by Ken VanDine
added notifications and messaging indicator support
350
674.1.42 by Ken VanDine
* revert the change dropping threading from MapAsync, without it there is some blocking.
351
class MapAsync(threading.Thread):
727 by Ken VanDine
bump the timeout for each thread to allow enough time for pycurl to finish (or timeout)
352
  def __init__(self, func, iterable, cbsuccess, cbfailure, timeout=240):
674.1.42 by Ken VanDine
* revert the change dropping threading from MapAsync, without it there is some blocking.
353
    threading.Thread.__init__(self)
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
354
    self.iterable = iterable
355
    self.callback = cbsuccess
356
    self.failure = cbfailure
357
    self.timeout = timeout
674.1.42 by Ken VanDine
* revert the change dropping threading from MapAsync, without it there is some blocking.
358
    self.daemon = True
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
359
    self.func = func
674.1.42 by Ken VanDine
* revert the change dropping threading from MapAsync, without it there is some blocking.
360
    self.start()
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
361
362
  def run(self):
363
    try:
364
      pool = multiprocessing.Pool()
727 by Ken VanDine
bump the timeout for each thread to allow enough time for pycurl to finish (or timeout)
365
      pool.map_async(self.func, self.iterable, callback=self.callback).get(timeout=self.timeout)
490.1.4 by Ryan Paul
Make dispatcher use logging module for debug instead of print statements
366
    except Exception as e:
367
      self.failure(e, traceback.format_exc())
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
368
696.1.1 by Ken VanDine
don't make Dispatcher inherit from thread.Threading, it isn't actually used
369
class Dispatcher(dbus.service.Object):
674.1.7 by Ken VanDine
* Moved the dbus docs into docstrings in dispatcher.py
370
  """
371
  The Gwibber Dispatcher handles all the backend operations.
372
  """
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
373
  __dbus_object_path__ = "/com/gwibber/Service"
374
375
  def __init__(self, loop, autorefresh=True):
376
    self.bus = dbus.SessionBus()
517 by Ken VanDine
* Make the dbus interface names match the object path
377
    bus_name = dbus.service.BusName("com.Gwibber.Service", bus=self.bus)
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
378
    dbus.service.Object.__init__(self, bus_name, self.__dbus_object_path__)
674.6.2 by Travis B. Hartwell
Undo whitespace changes made previously.
379
    
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
380
    self.collector = OperationCollector()
381
382
    self.refresh_count = 0
383
    self.mainloop = loop
384
674.6.6 by Travis B. Hartwell
Move loading account passwords to the Dispatcher class and expose it
385
    self.RefreshCreds()
386
    
740 by Ken VanDine
Ensure there is always only one refresh timer (LP: #595265)
387
    self.refresh_timer_id = None
388
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
389
    if autorefresh:
390
      self.refresh()
391
490.2.4 by Ken VanDine
merged
392
    self.accounts = CouchDatabase(COUCH_DB_ACCOUNTS, create=True)
393
517 by Ken VanDine
* Make the dbus interface names match the object path
394
  @dbus.service.signal("com.Gwibber.Service")
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
395
  def LoadingComplete(self): pass
396
517 by Ken VanDine
* Make the dbus interface names match the object path
397
  @dbus.service.signal("com.Gwibber.Service")
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
398
  def LoadingStarted(self): pass
399
517 by Ken VanDine
* Make the dbus interface names match the object path
400
  @dbus.service.method("com.Gwibber.Service")
674.1.12 by Ryan Paul
Added support for automatically compacting the database every 20 refresh cycles
401
  def CompactDB(self):
402
    log.logger.debug("Compacting the database")
403
    self.collector.messages.db.compact()
404
405
  @dbus.service.method("com.Gwibber.Service")
599.1.1 by Ken VanDine
Added some doc strings
406
  def Refresh(self):
407
    """
408
    Calls the Gwibber Service to trigger a refresh operation
674.1.7 by Ken VanDine
* Moved the dbus docs into docstrings in dispatcher.py
409
    example:
410
            import dbus
411
            obj = dbus.SessionBus().get_object("com.Gwibber.Service", "/com/gwibber/Service")
412
            service = dbus.Interface(obj, "com.Gwibber.Service")
413
            service.Refresh()
414
599.1.1 by Ken VanDine
Added some doc strings
415
    """
416
    self.refresh()
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
417
517 by Ken VanDine
* Make the dbus interface names match the object path
418
  @dbus.service.method("com.Gwibber.Service", in_signature="s")
490.1.26 by Ryan Paul
Added support for replies
419
  def PerformOp(self, opdata):
490.1.27 by Ryan Paul
Refactored message sending in the Gwibber backend
420
    try: o = json.loads(opdata)
421
    except: return
674.6.2 by Travis B. Hartwell
Undo whitespace changes made previously.
422
    
490.7.10 by Ken VanDine
Merged the NM branch
423
    log.logger.debug("** Starting Single Operation **")
490.1.26 by Ryan Paul
Added support for replies
424
    self.LoadingStarted()
674.6.2 by Travis B. Hartwell
Undo whitespace changes made previously.
425
    
490.1.27 by Ryan Paul
Refactored message sending in the Gwibber backend
426
    params = ["account", "operation", "args", "transient"]
427
    operation = None
674.6.2 by Travis B. Hartwell
Undo whitespace changes made previously.
428
    
490.1.27 by Ryan Paul
Refactored message sending in the Gwibber backend
429
    if "id" in o:
430
      operation = self.collector.get_operation_by_id(o["id"])
431
    elif all(i in o for i in params):
506.1.4 by Ryan Paul
Added support for search in single stream mode
432
      operation = [tuple(o[i] for i in params)]
540 by Ryan Paul
Force account to refresh when it is created
433
    elif "account" in o and self.accounts.record_exists(o["account"]):
434
      operation = self.collector.account_to_operations(o["account"])
490.1.27 by Ryan Paul
Refactored message sending in the Gwibber backend
435
436
    if operation:
506.1.4 by Ryan Paul
Added support for search in single stream mode
437
      MapAsync(perform_operation, operation, self.loading_complete, self.loading_failed)
490.1.27 by Ryan Paul
Refactored message sending in the Gwibber backend
438
517 by Ken VanDine
* Make the dbus interface names match the object path
439
  @dbus.service.method("com.Gwibber.Service", in_signature="s")
490.1.27 by Ryan Paul
Refactored message sending in the Gwibber backend
440
  def SendMessage(self, message):
599.1.1 by Ken VanDine
Added some doc strings
441
    """
674.6.2 by Travis B. Hartwell
Undo whitespace changes made previously.
442
    Posts a message/status update to all accounts with send_enabled = True.  It 
599.1.1 by Ken VanDine
Added some doc strings
443
    takes one argument, which is a message formated as a string.
674.1.7 by Ken VanDine
* Moved the dbus docs into docstrings in dispatcher.py
444
    example:
445
            import dbus
446
            obj = dbus.SessionBus().get_object("com.Gwibber.Service", "/com/gwibber/Service")
447
            service = dbus.Interface(obj, "com.Gwibber.Service")
448
            service.SendMessage("Your message")
599.1.1 by Ken VanDine
Added some doc strings
449
    """
490.1.27 by Ryan Paul
Refactored message sending in the Gwibber backend
450
    self.send(self.collector.get_send_operations(message))
451
517 by Ken VanDine
* Make the dbus interface names match the object path
452
  @dbus.service.method("com.Gwibber.Service", in_signature="s")
490.1.27 by Ryan Paul
Refactored message sending in the Gwibber backend
453
  def Send(self, opdata):
497 by Ryan Paul
Fixed retweeting and added support for more formats
454
    try:
455
      o = json.loads(opdata)
456
      if "target" in o:
457
        args = {"message": o["message"], "target": o["target"]}
458
        operations = [(o["target"]["account"], "send_thread", args, None)]
459
      elif "accounts" in o:
460
        operations = [(a, "send", {"message": o["message"]}, None) for a in o["accounts"]]
461
      self.send(operations)
462
    except: pass
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
463
517 by Ken VanDine
* Make the dbus interface names match the object path
464
  @dbus.service.method("com.Gwibber.Service", out_signature="s")
599.1.1 by Ken VanDine
Added some doc strings
465
  def GetServices(self):
466
    """
467
    Returns a list of services available as json string
674.1.7 by Ken VanDine
* Moved the dbus docs into docstrings in dispatcher.py
468
    example:
469
            import dbus, json
470
            obj = dbus.SessionBus().get_object("com.Gwibber.Service", "/com/gwibber/Service")
471
            service = dbus.Interface(obj, "com.Gwibber.Service")
472
            services = json.loads(service.GetServices())
473
599.1.1 by Ken VanDine
Added some doc strings
474
    """
475
    return json.dumps(SERVICES)
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
476
517 by Ken VanDine
* Make the dbus interface names match the object path
477
  @dbus.service.method("com.Gwibber.Service", out_signature="s")
674.1.7 by Ken VanDine
* Moved the dbus docs into docstrings in dispatcher.py
478
  def GetFeatures(self):
479
    """
480
    Returns a list of features as json string
481
    example:
482
            import dbus, json
483
            obj = dbus.SessionBus().get_object("com.Gwibber.Service", "/com/gwibber/Service")
484
            service = dbus.Interface(obj, "com.Gwibber.Service")
485
            features = json.loads(service.GetFeatures())
486
    """
487
    return json.dumps(FEATURES)
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
488
517 by Ken VanDine
* Make the dbus interface names match the object path
489
  @dbus.service.method("com.Gwibber.Service", out_signature="s")
674.6.2 by Travis B. Hartwell
Undo whitespace changes made previously.
490
  def GetAccounts(self): 
599.1.1 by Ken VanDine
Added some doc strings
491
    """
492
    Returns a list of accounts as json string
674.1.7 by Ken VanDine
* Moved the dbus docs into docstrings in dispatcher.py
493
    example:
494
            import dbus, json
495
            obj = dbus.SessionBus().get_object("com.Gwibber.Service", "/com/gwibber/Service")
496
            service = dbus.Interface(obj, "com.Gwibber.Service")
497
            accounts = json.loads(service.GetAccounts())
599.1.1 by Ken VanDine
Added some doc strings
498
    """
490.2.4 by Ken VanDine
merged
499
    all_accounts = []
500
    for account in self.accounts.get_records(COUCH_TYPE_ACCOUNT, True):
501
      all_accounts.append(account.value)
502
    return json.dumps(all_accounts)
503
517 by Ken VanDine
* Make the dbus interface names match the object path
504
  @dbus.service.method("com.Gwibber.Service")
674.6.2 by Travis B. Hartwell
Undo whitespace changes made previously.
505
  def Quit(self): 
674.1.7 by Ken VanDine
* Moved the dbus docs into docstrings in dispatcher.py
506
    """
674.1.22 by Ken VanDine
fixed a typo
507
    Shutdown the service
674.1.7 by Ken VanDine
* Moved the dbus docs into docstrings in dispatcher.py
508
    example:
509
            import dbus
510
            obj = dbus.SessionBus().get_object("com.Gwibber.Service", "/com/gwibber/Service")
511
            service = dbus.Interface(obj, "com.Gwibber.Service")
512
            service.Quit()
513
    """
622 by Ken VanDine
log quit and close events in the client and service
514
    log.logger.info("Gwibber Service is being shutdown")
515
    self.mainloop.quit()
674.6.6 by Travis B. Hartwell
Move loading account passwords to the Dispatcher class and expose it
516
517
  @dbus.service.method("com.Gwibber.Service")
518
  def RefreshCreds(self):
519
    """
520
    Reload accounts and credentials from Gnome Keyring
521
    example:
522
            import dbus
523
            obj = dbus.SessionBus().get_object("com.Gwibber.Service", "/com/gwibber/Service")
524
            service = dbus.Interface(obj, "com.Gwibber.Service")
525
            service.RefreshCreds()
526
    """
527
    log.logger.info("Gwibber Service is reloading account credentials")
528
    util.keyring.get_account_passwords()
529
    
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
530
  def loading_complete(self, output):
531
    self.refresh_count += 1
532
    self.LoadingComplete()
490.7.6 by Ken VanDine
* Added a new console logger with the -o arg to gwibber-daemon
533
    log.logger.info("Loading complete: %s - %s", self.refresh_count, [o[0] for o in output])
674.1.12 by Ryan Paul
Added support for automatically compacting the database every 20 refresh cycles
534
    if self.refresh_count % 20 == 0 and self.refresh_count > 1:
535
      self.CompactDB()
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
536
490.1.4 by Ryan Paul
Make dispatcher use logging module for debug instead of print statements
537
  def loading_failed(self, exception, tb):
490.7.4 by Ken VanDine
set somethings to log using the error level
538
    log.logger.error("Loading failed: %s - %s", exception, tb)
490.1.1 by Ryan Paul
Major refactoring for Gwibber 2.30
539
490.1.27 by Ryan Paul
Refactored message sending in the Gwibber backend
540
  def send(self, operations):
541
    operations = util.compact(operations)
542
    if operations:
543
      self.LoadingStarted()
601 by Ken VanDine
fixed merge breakage
544
      log.logger.debug("*** Sending Message ***")
490.1.27 by Ryan Paul
Refactored message sending in the Gwibber backend
545
      MapAsync(perform_operation, operations, self.loading_complete, self.loading_failed)
546
740 by Ken VanDine
Ensure there is always only one refresh timer (LP: #595265)
547
  def refresh(self): 
548
    if self.refresh_timer_id:
549
      gobject.source_remove(self.refresh_timer_id)
550
    log.logger.info("Refresh interval is set to %s", SETTINGS["interval"])
490.1.27 by Ryan Paul
Refactored message sending in the Gwibber backend
551
    operations = list(self.collector.get_operations())
552
    if operations:
740 by Ken VanDine
Ensure there is always only one refresh timer (LP: #595265)
553
      log.logger.info("** Starting Refresh - %s **", time.asctime())
490.1.27 by Ryan Paul
Refactored message sending in the Gwibber backend
554
      self.LoadingStarted()
555
      MapAsync(perform_operation, operations, self.loading_complete, self.loading_failed)
740 by Ken VanDine
Ensure there is always only one refresh timer (LP: #595265)
556
    self.refresh_timer_id = gobject.timeout_add_seconds(int(60 * SETTINGS["interval"]), self.refresh)
694 by Ken VanDine
Reset refresh interval to pick up changes in settings
557
    return False
490.1.27 by Ryan Paul
Refactored message sending in the Gwibber backend
558
507 by Ken VanDine
set online/offline mode depending on NM_STATE
559
class ConnectionMonitor(dbus.service.Object):
560
  __dbus_object_path__ = "/com/gwibber/Connection"
561
562
  def __init__(self):
563
    self.bus = dbus.SessionBus()
517 by Ken VanDine
* Make the dbus interface names match the object path
564
    bus_name = dbus.service.BusName("com.Gwibber.Connection", bus=self.bus)
507 by Ken VanDine
set online/offline mode depending on NM_STATE
565
    dbus.service.Object.__init__(self, bus_name, self.__dbus_object_path__)
566
567
    self.sysbus = dbus.SystemBus()
509 by Ken VanDine
Attempt to not actually require NetworkManager, if it can't get the state from NM assume online
568
    try:
569
      self.nm = self.sysbus.get_object(NM_DBUS_SERVICE, NM_DBUS_OBJECT_PATH)
570
      self.nm.connect_to_signal("StateChanged", self.on_connection_changed)
571
    except:
572
      pass
507 by Ken VanDine
set online/offline mode depending on NM_STATE
573
574
  def on_connection_changed(self, state):
575
    if state == NM_STATE_CONNECTED:
490.7.10 by Ken VanDine
Merged the NM branch
576
      log.logger.info("Network state changed to Online")
507 by Ken VanDine
set online/offline mode depending on NM_STATE
577
      self.ConnectionOnline(state)
578
    if state == NM_STATE_DISCONNECTED:
490.7.10 by Ken VanDine
Merged the NM branch
579
      log.logger.info("Network state changed to Offline")
507 by Ken VanDine
set online/offline mode depending on NM_STATE
580
      self.ConnectionOffline(state)
581
632.1.1 by Ken VanDine
fixed typo that would cause the ConnectionMonitor to fail
582
  @dbus.service.signal("com.Gwibber.Connection", signature="u")
507 by Ken VanDine
set online/offline mode depending on NM_STATE
583
  def ConnectionOnline(self, state): pass
584
517 by Ken VanDine
* Make the dbus interface names match the object path
585
  @dbus.service.signal("com.Gwibber.Connection", signature="u")
507 by Ken VanDine
set online/offline mode depending on NM_STATE
586
  def ConnectionOffline(self, state): pass
587
517 by Ken VanDine
* Make the dbus interface names match the object path
588
  @dbus.service.method("com.Gwibber.Connection")
674.6.2 by Travis B. Hartwell
Undo whitespace changes made previously.
589
  def isConnected(self): 
509 by Ken VanDine
Attempt to not actually require NetworkManager, if it can't get the state from NM assume online
590
    try:
511 by Ken VanDine
Fixed a method call
591
      if self.nm.state() == NM_STATE_CONNECTED:
509 by Ken VanDine
Attempt to not actually require NetworkManager, if it can't get the state from NM assume online
592
        return True
593
      return False
594
    except:
507 by Ken VanDine
set online/offline mode depending on NM_STATE
595
      return True
596
527 by Ken VanDine
Ported urlshortening
597
class URLShorten(dbus.service.Object):
598
  __dbus_object_path__ = "/com/gwibber/URLShorten"
599
600
  def __init__(self):
601
    self.bus = dbus.SessionBus()
602
    bus_name = dbus.service.BusName("com.Gwibber.URLShorten", bus=self.bus)
603
    dbus.service.Object.__init__(self, bus_name, self.__dbus_object_path__)
604
674.1.7 by Ken VanDine
* Moved the dbus docs into docstrings in dispatcher.py
605
  @dbus.service.method("com.Gwibber.URLShorten", in_signature="s", out_signature="s")
606
  def Shorten(self, url):
607
    """
608
    Takes a url as a string and returns a shortened url as a string.
609
    example:
610
            import dbus
611
            url = "http://www.example.com/this/is/a/long/url"
612
            obj = dbus.SessionBus().get_object("com.Gwibber.URLShorten", "/com/gwibber/URLShorten")
613
            shortener = dbus.Interface(obj, "com.Gwibber.URLShorten")
614
            short_url = shortener.Shorten(url)
615
    """
738 by Ken VanDine
Remove the tr.im urlshortener, the service is closed (LP: #583316)
616
    if SETTINGS["urlshorter"] in urlshorter.PROTOCOLS.keys():
617
      service = SETTINGS["urlshorter"]
618
    else:
619
      service = "is.gd"
674.1.7 by Ken VanDine
* Moved the dbus docs into docstrings in dispatcher.py
620
    log.logger.info("Shortening URL %s with %s", url, service)
557.2.1 by Ken VanDine
Make dbus method camel case
621
    if self.IsShort(url): return url
527 by Ken VanDine
Ported urlshortening
622
    try:
623
      s = urlshorter.PROTOCOLS[service].URLShorter()
624
      return s.short(url)
625
    except: return url
626
557.2.1 by Ken VanDine
Make dbus method camel case
627
  def IsShort(self, url):
527 by Ken VanDine
Ported urlshortening
628
    for us in urlshorter.PROTOCOLS.values():
629
      if url.startswith(us.PROTOCOL_INFO["fqdn"]):
630
        return True
631
    return False