~jconti/recent-notifications/trunk

61 by Jason Conti
Renamed Monitor.py to Notification.py
1
"""
2
Notification.py
3
by Jason Conti
4
February 19, 2010
5
6
Monitors DBUS for org.freedesktop.Notifications.Notify messages, parses them,
7
and notifies listeners when they arrive.
8
"""
9
10
import dbus
11
import glib
12
import gobject
13
import gtk
14
import logging
15
16
from dbus.mainloop.glib import DBusGMainLoop
81 by Jason Conti
Adding support for gettext translations and a pointless en_US translation as an example. Using distutils-extra to automatically build the translations and the bonobo server. Replaced the build_servers target with update_prefix. This performs basically the same as build_servers except that it will do the substitution on any .in file, and it just writes the output file without the .in, instead of adding it to the data file install list.
17
from locale import nl_langinfo, T_FMT
61 by Jason Conti
Renamed Monitor.py to Notification.py
18
97 by Jason Conti
Adding the Timestamp module. Using the timestamp module to display timestamps as time ago in words.
19
import Timestamp
20
61 by Jason Conti
Renamed Monitor.py to Notification.py
21
from Icon import load_icon, load_icon_from_file
22
72 by Jason Conti
Improved expection logging, and updated the logging format. Minor version bump for package release.
23
logger = logging.getLogger("Notification")
24
25
class ImageDataException(Exception):
26
  pass
27
68 by Jason Conti
Adding support for the image_data hint in the Notifications. Removing CellRendererMessage.py and MessageView.py since they are no longer used.
28
class ImageData(object):
29
  """Parses the image_data hint from a DBUS message."""
30
  def __init__(self, image_data):
31
    if len(image_data) < 7:
72 by Jason Conti
Improved expection logging, and updated the logging format. Minor version bump for package release.
32
      raise ImageDataException("Invalid image_data: " + repr(image_data))
68 by Jason Conti
Adding support for the image_data hint in the Notifications. Removing CellRendererMessage.py and MessageView.py since they are no longer used.
33
34
    self.width = int(image_data[0])
35
    self.height = int(image_data[1])
36
    self.rowstride = int(image_data[2])
37
    self.has_alpha = bool(image_data[3])
38
    self.bits_per_sample = int(image_data[4])
39
    self.channels = int(image_data[5])
40
    self.data = self.dbus_array_to_str(image_data[6])
41
42
  def dbus_array_to_str(self, array):
43
    return "".join(map(chr, array))
44
45
  def get_pixbuf(self, size):
46
    """Creates a pixbuf from the image data and scale it to the appropriate size."""
47
    pixbuf = gtk.gdk.pixbuf_new_from_data(self.data, gtk.gdk.COLORSPACE_RGB,
48
        self.has_alpha, self.bits_per_sample, self.width, self.height,
49
        self.rowstride)
50
51
    return pixbuf.scale_simple(size, size, gtk.gdk.INTERP_BILINEAR)
52
72 by Jason Conti
Improved expection logging, and updated the logging format. Minor version bump for package release.
53
class MessageException(Exception):
54
  pass
55
61 by Jason Conti
Renamed Monitor.py to Notification.py
56
class Message(gobject.GObject):
57
  """Parses a DBUS message in the Notify format specified at:
58
  http://www.galago-project.org/specs/notification/0.9/index.html"""
59
  # Message urgency
60
  LOW = 0
61
  NORMAL = 1
62
  CRITICAL = 2
114 by Jason Conti
Ubuntu has special notifications when the media buttons on a keyboard are used to update the volume. They are mostly blank except for a special x-canonical-private-synchronous hint that contains the string volume. They don't have any useful information, so this commit adds code to detect and discard them.
63
  X_CANONICAL_PRIVATE_SYNCHRONOUS = "x-canonical-private-synchronous"
61 by Jason Conti
Renamed Monitor.py to Notification.py
64
  def __init__(self, dbus_message = None, timestamp = None):
65
    gobject.GObject.__init__(self)
66
68 by Jason Conti
Adding support for the image_data hint in the Notifications. Removing CellRendererMessage.py and MessageView.py since they are no longer used.
67
    self.timestamp = timestamp
68
69
    args = dbus_message.get_args_list()
70
71
    if len(args) != 8:
72 by Jason Conti
Improved expection logging, and updated the logging format. Minor version bump for package release.
72
      raise MessageException("Invalid message args_list: " + repr(args))
68 by Jason Conti
Adding support for the image_data hint in the Notifications. Removing CellRendererMessage.py and MessageView.py since they are no longer used.
73
74
    self.app_name = str(args[0])
75
    self.replaces_id = args[1]
72 by Jason Conti
Improved expection logging, and updated the logging format. Minor version bump for package release.
76
    self.app_icon = str(args[2])
68 by Jason Conti
Adding support for the image_data hint in the Notifications. Removing CellRendererMessage.py and MessageView.py since they are no longer used.
77
    self.summary = str(args[3])
78
    self.body = str(args[4])
79
    self.actions = args[5]
80
    self.hints = dict(args[6])
81
    self.expire_timeout = args[7]
61 by Jason Conti
Renamed Monitor.py to Notification.py
82
83
    if "urgency" in self.hints:
84
      urgency = self.hints["urgency"]
85
      if urgency == 0:
86
        self.urgency = Message.LOW
87
      elif urgency == 2:
88
        self.urgency = Message.CRITICAL
89
      else:
90
        self.urgency = Message.NORMAL
91
    else:
92
      self.urgency = Message.NORMAL
93
68 by Jason Conti
Adding support for the image_data hint in the Notifications. Removing CellRendererMessage.py and MessageView.py since they are no longer used.
94
    if "image_data" in self.hints:
95
      self.image_data = ImageData(self.hints["image_data"])
96
    else:
97
      self.image_data = None
98
214 by Jason Conti
* Reorder the icon priority in Notification.get_icon to match the Desktop
99
    if "icon_data" in self.hints:
100
      self.icon_data = ImageData(self.hints["icon_data"])
101
    else:
102
      self.icon_data = None
103
104
    if "image-path" in self.hints:
105
      self.image_path = self.hints["image-path"]
106
    else:
107
      self.image_path = None
123 by Jason Conti
It seems there is another hint in the spec for icon_data, which seems redundant, since it is exactly identical to image_data (which is better documented in the hints section). Adding it because Dropbox notifications use it.
108
68 by Jason Conti
Adding support for the image_data hint in the Notifications. Removing CellRendererMessage.py and MessageView.py since they are no longer used.
109
    self.log_message()
61 by Jason Conti
Renamed Monitor.py to Notification.py
110
111
  def get_icon(self, size = 48):
214 by Jason Conti
* Reorder the icon priority in Notification.get_icon to match the Desktop
112
    """Loads the icon into a pixbuf."""
217 by Jason Conti
Fix typo in Notification.py.
113
    if self.image_path != None:
214 by Jason Conti
* Reorder the icon priority in Notification.get_icon to match the Desktop
114
      icon_name = self.image_path
115
    else:
116
      icon_name = self.app_icon
61 by Jason Conti
Renamed Monitor.py to Notification.py
117
    
214 by Jason Conti
* Reorder the icon priority in Notification.get_icon to match the Desktop
118
    # Try to load the image data from the message
119
    if self.image_data != None:
120
      return self.image_data.get_pixbuf(size)
121
61 by Jason Conti
Renamed Monitor.py to Notification.py
122
    # Try to load the pixbuf from a file
214 by Jason Conti
* Reorder the icon priority in Notification.get_icon to match the Desktop
123
    elif icon_name.startswith("file://") or icon_name.startswith("/"):
61 by Jason Conti
Renamed Monitor.py to Notification.py
124
      icon = load_icon_from_file(icon_name, size)
125
      if icon != None:
126
        return icon
127
128
    # Try to load the pixbuf from the current icon theme
129
    elif icon_name != "":
130
      icon = load_icon(icon_name, size)
131
      if icon != None:
132
        return icon
133
214 by Jason Conti
* Reorder the icon priority in Notification.get_icon to match the Desktop
134
    # Try to load the icon data from the message
135
    elif self.icon_data != None:
136
      return self.icon_data.get_pixbuf(size)
68 by Jason Conti
Adding support for the image_data hint in the Notifications. Removing CellRendererMessage.py and MessageView.py since they are no longer used.
137
61 by Jason Conti
Renamed Monitor.py to Notification.py
138
    return self.get_default_icon(size)
139
140
  def get_default_icon(self, size = 48):
141
    """Attempts to load the default message icon, returns None on failure."""
142
    if self.urgency == Message.LOW:
143
      return load_icon("notification-low", size)
144
    elif self.urgency == Message.CRITICAL:
145
      return load_icon("notification-critical", size)
146
    else:
147
      return load_icon("notification-normal", size)
148
114 by Jason Conti
Ubuntu has special notifications when the media buttons on a keyboard are used to update the volume. They are mostly blank except for a special x-canonical-private-synchronous hint that contains the string volume. They don't have any useful information, so this commit adds code to detect and discard them.
149
  def is_volume_notification(self):
150
    """Returns true if this is a volume message. The volume notifications
151
    in Ubuntu are a special case, and mostly a blank message. Clutters up
152
    the display and provides no useful information, so it is better to
153
    discard them."""
154
    if Message.X_CANONICAL_PRIVATE_SYNCHRONOUS in self.hints:
155
      return str(self.hints[Message.X_CANONICAL_PRIVATE_SYNCHRONOUS]) == "volume"
156
61 by Jason Conti
Renamed Monitor.py to Notification.py
157
  def log_message(self):
158
    """Write debug info about a message."""
72 by Jason Conti
Improved expection logging, and updated the logging format. Minor version bump for package release.
159
    result = [
160
        "-" * 50,
97 by Jason Conti
Adding the Timestamp module. Using the timestamp module to display timestamps as time ago in words.
161
        "Message created at: " + Timestamp.locale_datetime(self.timestamp),
72 by Jason Conti
Improved expection logging, and updated the logging format. Minor version bump for package release.
162
        "app_name: " + repr(self.app_name),
163
        "replaces_id: " + repr(self.replaces_id),
164
        "app_icon: " + repr(self.app_icon),
165
        "summary: " + repr(self.summary),
166
        "body: " + repr(self.body),
167
        "actions: " + repr(self.actions),
168
        "expire_timeout: " + repr(self.expire_timeout),
85 by Jason Conti
Updated Message.log_message. The image_data hints were cluttering up the log, so now they are listed separately, using only the image metadata, not the actual image data.
169
        "hints:"
72 by Jason Conti
Improved expection logging, and updated the logging format. Minor version bump for package release.
170
        ]
85 by Jason Conti
Updated Message.log_message. The image_data hints were cluttering up the log, so now they are listed separately, using only the image metadata, not the actual image data.
171
172
    # Log all the hints except image_data
173
    for key in self.hints:
174
      if key != "image_data":
175
        result.append("  " + str(key) + ": " + repr(self.hints[key]))
176
177
    # Log info about the image_data
178
    if self.image_data != None:
179
      result.append("image_data:")
180
      result.append("  width: " + repr(self.image_data.width))
181
      result.append("  height: " + repr(self.image_data.height))
182
      result.append("  rowstride: " + repr(self.image_data.rowstride))
183
      result.append("  has_alpha: " + repr(self.image_data.has_alpha))
184
      result.append("  bits_per_sample: " + repr(self.image_data.bits_per_sample))
185
      result.append("  channels: " + repr(self.image_data.channels))
186
187
    result.append("-" * 50)
188
72 by Jason Conti
Improved expection logging, and updated the logging format. Minor version bump for package release.
189
    logger.debug("\n" + "\n".join(result))
61 by Jason Conti
Renamed Monitor.py to Notification.py
190
191
class Notification(gobject.GObject):
192
  """Monitors DBUS for org.freedesktop.Notifications.Notify messages, parses them,
193
  and notifies listeners when they arrive."""
194
  __gsignals__ = {
195
      "message-received": (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, [Message])
196
  }
197
  def __init__(self):
198
    gobject.GObject.__init__(self)
199
167 by Jason Conti
Fixed a bug in Notification.py if no blacklist is set.
200
    self._blacklist = None
61 by Jason Conti
Renamed Monitor.py to Notification.py
201
    self._match_string = "type='method_call',interface='org.freedesktop.Notifications',member='Notify'"
202
203
    DBusGMainLoop(set_as_default=True)
204
    self._bus = dbus.SessionBus()
205
    self._bus.add_match_string(self._match_string)
206
    self._bus.add_message_filter(self._message_filter)
207
208
  def close(self):
137 by Jason Conti
Adding an option to blacklist notifications based on app_name. Applications to be blacklisted should be listed in the file ~/.config/recent-notifications/blacklist, one app_name per line. The app_names are case-sensitive and are listed in the notifications as "from app_name" such as "from Gwibber" or "from Pidgin". They can also be found in ~/.cache/recent-notifications.log on the app_name lines. Thanks to Dinero Francis for reporting the issue.
209
    """Closes the connection to the session bus."""
61 by Jason Conti
Renamed Monitor.py to Notification.py
210
    self._bus.close()
211
137 by Jason Conti
Adding an option to blacklist notifications based on app_name. Applications to be blacklisted should be listed in the file ~/.config/recent-notifications/blacklist, one app_name per line. The app_names are case-sensitive and are listed in the notifications as "from app_name" such as "from Gwibber" or "from Pidgin". They can also be found in ~/.cache/recent-notifications.log on the app_name lines. Thanks to Dinero Francis for reporting the issue.
212
  def set_blacklist(self, blacklist):
213
    """Defines the set of app_names of messages to be discarded."""
139 by Jason Conti
Fixed several bugs in Config and it is now used to read the blacklist.
214
    self._blacklist = blacklist
137 by Jason Conti
Adding an option to blacklist notifications based on app_name. Applications to be blacklisted should be listed in the file ~/.config/recent-notifications/blacklist, one app_name per line. The app_names are case-sensitive and are listed in the notifications as "from app_name" such as "from Gwibber" or "from Pidgin". They can also be found in ~/.cache/recent-notifications.log on the app_name lines. Thanks to Dinero Francis for reporting the issue.
215
61 by Jason Conti
Renamed Monitor.py to Notification.py
216
  def _message_filter(self, connection, dbus_message):
137 by Jason Conti
Adding an option to blacklist notifications based on app_name. Applications to be blacklisted should be listed in the file ~/.config/recent-notifications/blacklist, one app_name per line. The app_names are case-sensitive and are listed in the notifications as "from app_name" such as "from Gwibber" or "from Pidgin". They can also be found in ~/.cache/recent-notifications.log on the app_name lines. Thanks to Dinero Francis for reporting the issue.
217
    """Triggers when messages are received from the session bus."""
61 by Jason Conti
Renamed Monitor.py to Notification.py
218
    if dbus_message.get_member() == "Notify" and dbus_message.get_interface() == "org.freedesktop.Notifications":
72 by Jason Conti
Improved expection logging, and updated the logging format. Minor version bump for package release.
219
      try:
97 by Jason Conti
Adding the Timestamp module. Using the timestamp module to display timestamps as time ago in words.
220
        message = Message(dbus_message, Timestamp.now())
72 by Jason Conti
Improved expection logging, and updated the logging format. Minor version bump for package release.
221
      except:
222
        logger.exception("Failed to parse dbus message: " + repr(dbus_message.get_args_list()))
223
      else:
137 by Jason Conti
Adding an option to blacklist notifications based on app_name. Applications to be blacklisted should be listed in the file ~/.config/recent-notifications/blacklist, one app_name per line. The app_names are case-sensitive and are listed in the notifications as "from app_name" such as "from Gwibber" or "from Pidgin". They can also be found in ~/.cache/recent-notifications.log on the app_name lines. Thanks to Dinero Francis for reporting the issue.
224
        # Discard unwanted messages
167 by Jason Conti
Fixed a bug in Notification.py if no blacklist is set.
225
        if message.is_volume_notification():
226
          return
227
        if self._blacklist and self._blacklist.get_bool(message.app_name, False):
137 by Jason Conti
Adding an option to blacklist notifications based on app_name. Applications to be blacklisted should be listed in the file ~/.config/recent-notifications/blacklist, one app_name per line. The app_names are case-sensitive and are listed in the notifications as "from app_name" such as "from Gwibber" or "from Pidgin". They can also be found in ~/.cache/recent-notifications.log on the app_name lines. Thanks to Dinero Francis for reporting the issue.
228
          return
72 by Jason Conti
Improved expection logging, and updated the logging format. Minor version bump for package release.
229
        glib.idle_add(self.emit, "message-received", message)
61 by Jason Conti
Renamed Monitor.py to Notification.py
230
231
if gtk.pygtk_version < (2, 8, 0):
232
  gobject.type_register(Message)
233
  gobject.type_register(Notification)
234
235
if __name__ == '__main__':
236
  main()