1 | # -*- coding: utf-8 -*- |
---|
2 | # |
---|
3 | # Copyright (c) 2008, Stephen Hansen |
---|
4 | # Copyright (c) 2009, Robert Corsaro |
---|
5 | # Copyright (c) 2010-2012, Steffen Hoffmann |
---|
6 | # |
---|
7 | # This software is licensed as described in the file COPYING, which |
---|
8 | # you should have received as part of this distribution. |
---|
9 | # |
---|
10 | |
---|
11 | import time |
---|
12 | |
---|
13 | from announcer import db_default |
---|
14 | from pkg_resources import resource_filename |
---|
15 | from trac.config import ExtensionOption |
---|
16 | from trac.core import Component, ExtensionPoint, Interface, TracError, \ |
---|
17 | implements |
---|
18 | from trac.db import DatabaseManager |
---|
19 | from trac.env import IEnvironmentSetupParticipant |
---|
20 | |
---|
21 | |
---|
22 | class IAnnouncementProducer(Interface): |
---|
23 | """Producer converts Trac events from different subsystems, into |
---|
24 | AnnouncerEvents. |
---|
25 | """ |
---|
26 | |
---|
27 | def realms(): |
---|
28 | """Returns an iterable that lists all the realms that this producer |
---|
29 | is capable of producing events for. |
---|
30 | """ |
---|
31 | |
---|
32 | |
---|
33 | class IAnnouncementSubscriber(Interface): |
---|
34 | """IAnnouncementSubscriber provides an interface where a Plug-In can |
---|
35 | register realms and categories of subscriptions it is able to provide. |
---|
36 | |
---|
37 | An IAnnouncementSubscriber component can use any means to determine |
---|
38 | if a user is interested in hearing about a given event. More then one |
---|
39 | component can handle the same realms and categories. |
---|
40 | |
---|
41 | The subscriber must also indicate not just that a user is interested |
---|
42 | in receiving a particular notice. Again, how it makes that decision is |
---|
43 | entirely up to a particular implementation.""" |
---|
44 | |
---|
45 | def matches(event): |
---|
46 | """Returns a list of subscriptions that match the given event. |
---|
47 | Responses should be yielded as 7 part tuples as follows: |
---|
48 | (distributor, sid, authenticated, address, format, priority, adverb) |
---|
49 | The default installation includes email and xmpp distributors. The |
---|
50 | default installation includes formats for text/plain and text/html. |
---|
51 | If an unknown format is return, it will be replaced by a default known |
---|
52 | format. Priority is used to resolve conflicting subscriptions for the |
---|
53 | same user/distribution pair. adverb is either always or never. |
---|
54 | """ |
---|
55 | |
---|
56 | def description(): |
---|
57 | """A description of the subscription that shows up in the users |
---|
58 | preferences. |
---|
59 | """ |
---|
60 | |
---|
61 | def requires_authentication(): |
---|
62 | """Returns True or False. If the user is required to be authenticated |
---|
63 | to create the subscription, then return True. This applies to things |
---|
64 | like ticket owner subscriber, since the ticket owner can never be the |
---|
65 | sid of an unauthenticated user and we have no way to lookup users by |
---|
66 | email address (as of yet). |
---|
67 | """ |
---|
68 | |
---|
69 | |
---|
70 | class IAnnouncementDefaultSubscriber(Interface): |
---|
71 | """Default subscriptions that the module will automatically generate. |
---|
72 | This should only be used in reasonable situations, where users can be |
---|
73 | determined by the event itself. For instance, ticket author has a |
---|
74 | default subscription that is controlled via trac.ini. This is because |
---|
75 | we can lookup the ticket author during the event and create a |
---|
76 | subscription for them. Default subscriptions should be low priority |
---|
77 | so that the user can easily override them. |
---|
78 | """ |
---|
79 | |
---|
80 | def default_subscriptions(): |
---|
81 | """Yields 5 part tuple containing (class, distributor, priority, |
---|
82 | adverb). This is used to display default subscriptions in the |
---|
83 | user UI and can also be used by matches to figure out what |
---|
84 | default subscriptions it should yield. |
---|
85 | """ |
---|
86 | |
---|
87 | |
---|
88 | class IAnnouncementSubscriptionFilter(Interface): |
---|
89 | """IAnnouncementSubscriptionFilter provides an interface where a component |
---|
90 | can filter subscribers from the final distribution list. |
---|
91 | """ |
---|
92 | |
---|
93 | def filter_subscriptions(event, subscriptions): |
---|
94 | """Returns a filtered iterator of subscriptions. This method is called |
---|
95 | after all get_subscriptions_for_event calls are made to allow |
---|
96 | components to remove addresses from the distribution list. This can |
---|
97 | be used for things like "never notify updater" functionality. |
---|
98 | """ |
---|
99 | |
---|
100 | |
---|
101 | class IAnnouncementFormatter(Interface): |
---|
102 | """Formatters are responsible for converting an event into a message |
---|
103 | appropriate for a given transport. |
---|
104 | |
---|
105 | For transports like 'aim' or 'irc', this may be a short summary of a |
---|
106 | change. For 'email', it may be a plaintext or html overview of all |
---|
107 | the changes and perhaps the existing state. |
---|
108 | |
---|
109 | It's up to a formatter to determine what ends up ultimately being sent |
---|
110 | to the end-user. It's capable of pulling data out of the target object |
---|
111 | that wasn't changed, picking and choosing details for whatever reason. |
---|
112 | |
---|
113 | Since a formatter must be intimately familiar with the realm that |
---|
114 | originated the event, formatters are tied to specific transport + realm |
---|
115 | combinations. This means there may be a proliferation of formatters as |
---|
116 | options expand. |
---|
117 | """ |
---|
118 | |
---|
119 | def format_styles(transport, realm): |
---|
120 | """Returns an iterable of styles that this formatter supports for |
---|
121 | a specified transport and realm. |
---|
122 | |
---|
123 | Many formatters may simply return a single style and never have more; |
---|
124 | that's fine. But if its useful to encapsulate code for several similar |
---|
125 | styles a formatter can handle more then one. For example, 'text/plain' |
---|
126 | and 'text/html' may be useful variants the same formatter handles. |
---|
127 | |
---|
128 | Formatters retain the ability to descriminate by transport, but don't |
---|
129 | need to. |
---|
130 | """ |
---|
131 | |
---|
132 | def alternative_style_for(transport, realm, style): |
---|
133 | """Returns an alternative style for the given style if one is |
---|
134 | available. |
---|
135 | """ |
---|
136 | |
---|
137 | def format(transport, realm, style, event): |
---|
138 | """Converts the event into the specified style. If the transport or |
---|
139 | realm passed into this method are not ones this formatter can handle, |
---|
140 | it should return silently and without error. |
---|
141 | |
---|
142 | The exact return type of this method is intentionally undefined. It |
---|
143 | will be whatever the distributor that it is designed to work with |
---|
144 | expects. |
---|
145 | """ |
---|
146 | |
---|
147 | |
---|
148 | class IAnnouncementDistributor(Interface): |
---|
149 | """The Distributor is responsible for actually delivering an event to the |
---|
150 | desired subscriptions. |
---|
151 | |
---|
152 | A distributor should attempt to avoid blocking; using subprocesses is |
---|
153 | preferred to threads. |
---|
154 | |
---|
155 | Each distributor handles a single transport, and only one distributor |
---|
156 | in the system should handle that. For example, there should not be |
---|
157 | two distributors for the 'email' transport. |
---|
158 | """ |
---|
159 | |
---|
160 | def transports(): |
---|
161 | """Returns an iter of the transport supported.""" |
---|
162 | |
---|
163 | def distribute(transport, recipients, event): |
---|
164 | """This method is meant to actually distribute the event to the |
---|
165 | specified recipients, over the specified transport. |
---|
166 | |
---|
167 | If it is passed a transport it does not support, it should return |
---|
168 | silently and without error. |
---|
169 | |
---|
170 | The recipients is a list of (name, address) pairs with either (but not |
---|
171 | both) being allowed to be None. If name is provided but address isn't, |
---|
172 | then the distributor should defer to IAnnouncementAddressResolver |
---|
173 | implementations to determine what the address should be. |
---|
174 | |
---|
175 | If the name is None but the address is not, then the distributor |
---|
176 | should rely on the address being correct and use it-- if possible. |
---|
177 | |
---|
178 | The distributor may initiate as many transactions as are necessecary |
---|
179 | to deliver a message, but should use as few as possible; for example |
---|
180 | in the EmailDistributor, if all of the recipients are receiving a |
---|
181 | plain text form of the message, a single message with many BCC's |
---|
182 | should be used. |
---|
183 | |
---|
184 | The distributor is responsible for determining which of the |
---|
185 | IAnnouncementFormatters should get the privilege of actually turning |
---|
186 | an event into content. In cases where multiple formatters are capable |
---|
187 | of converting an event into a message for a given transport, a |
---|
188 | user preference would be a dandy idea. |
---|
189 | """ |
---|
190 | |
---|
191 | |
---|
192 | class IAnnouncementPreferenceProvider(Interface): |
---|
193 | """Represents a single 'box' in the Announcements preference panel. |
---|
194 | |
---|
195 | Any component can always implement IPreferencePanelProvider to get |
---|
196 | preferences from users, of course. However, considering there may be |
---|
197 | several components related to the Announcement system, and many may |
---|
198 | have different preferences for a user to set, that would clutter up |
---|
199 | the preference interfac quite a bit. |
---|
200 | |
---|
201 | The IAnnouncementPreferenceProvider allows several boxes to be |
---|
202 | chained in the same panel to group the preferenecs related to the |
---|
203 | Announcement System. |
---|
204 | |
---|
205 | Implementing announcement preference boxes should be essentially |
---|
206 | identical to implementing entire panels. |
---|
207 | """ |
---|
208 | |
---|
209 | def get_announcement_preference_boxes(req): |
---|
210 | """Accepts a request object, and returns an iterable of |
---|
211 | (name, label) pairs; one for each box that the implementation |
---|
212 | can generate. |
---|
213 | |
---|
214 | If a single item is returned, be sure to 'yield' it instead of |
---|
215 | returning it.""" |
---|
216 | |
---|
217 | def render_announcement_preference_box(req, box): |
---|
218 | """Accepts a request object, and the name (as from the previous |
---|
219 | method) of the box that should be rendered. |
---|
220 | |
---|
221 | Returns a tuple of (template, data) with the template being a |
---|
222 | filename in a directory provided by an ITemplateProvider which |
---|
223 | shall be rendered into a single <div> element, when combined |
---|
224 | with the data member. |
---|
225 | """ |
---|
226 | |
---|
227 | |
---|
228 | class AnnouncementEvent(object): |
---|
229 | """AnnouncementEvent |
---|
230 | |
---|
231 | This packages together in a single place all data related to a particular |
---|
232 | event; notably the realm, category, and the target that represents the |
---|
233 | initiator of the event. |
---|
234 | |
---|
235 | In some (rare) cases, the target may be None; in cases where the message |
---|
236 | is all that matters and there's no possible data you could conceivably |
---|
237 | get beyond just the message. |
---|
238 | """ |
---|
239 | |
---|
240 | def __init__(self, realm, category, target, author=""): |
---|
241 | self.realm = realm |
---|
242 | self.category = category |
---|
243 | self.target = target |
---|
244 | self.author = author |
---|
245 | |
---|
246 | def get_basic_terms(self): |
---|
247 | return self.realm, self.category |
---|
248 | |
---|
249 | def get_session_terms(self, session_id): |
---|
250 | return tuple() |
---|
251 | |
---|
252 | |
---|
253 | class IAnnouncementSubscriptionResolver(Interface): |
---|
254 | """Supports new and old style of subscription resolution until new code |
---|
255 | is complete.""" |
---|
256 | |
---|
257 | def subscriptions(event): |
---|
258 | """Return all subscriptions as (dist, sid, auth, address, format) |
---|
259 | priority 1 is highest. adverb is 'always' or 'never'. |
---|
260 | """ |
---|
261 | |
---|
262 | |
---|
263 | class SubscriptionResolver(Component): |
---|
264 | """Collect, and resolve subscriptions.""" |
---|
265 | |
---|
266 | implements(IAnnouncementSubscriptionResolver) |
---|
267 | |
---|
268 | subscribers = ExtensionPoint(IAnnouncementSubscriber) |
---|
269 | |
---|
270 | def subscriptions(self, event): |
---|
271 | """Yields all subscriptions for a given event.""" |
---|
272 | |
---|
273 | subscriptions = [] |
---|
274 | for sp in self.subscribers: |
---|
275 | subscriptions.extend([x for x in sp.matches(event) if x]) |
---|
276 | |
---|
277 | """ |
---|
278 | This logic is meant to generate a list of subscriptions for each |
---|
279 | distribution method. The important thing is, that we pick the rule |
---|
280 | with the highest priority for each (sid, distribution) pair. |
---|
281 | If it is "never", then the user is dropped from the list, |
---|
282 | if it is "always", then the user is kept. |
---|
283 | Only users highest priority rule is used and all others are skipped. |
---|
284 | """ |
---|
285 | # sort by dist, sid, authenticated, priority |
---|
286 | subscriptions.sort(key=lambda i: (i[1], i[2], i[3], i[6])) |
---|
287 | |
---|
288 | resolved_subs = [] |
---|
289 | |
---|
290 | # collect highest priority for each (sid, dist) pair |
---|
291 | state = {'last': None} |
---|
292 | for s in subscriptions: |
---|
293 | if (s[1], s[2], s[3]) == state['last']: |
---|
294 | continue |
---|
295 | if s[-1] == 'always': |
---|
296 | self.log.debug("Adding (%s [%s]) for 'always' on rule (%s) " |
---|
297 | "for (%s)", s[2], s[3], s[0], s[1]) |
---|
298 | resolved_subs.append(s[1:6]) |
---|
299 | else: |
---|
300 | self.log.debug("Ignoring (%s [%s]) for 'never' on rule (%s) " |
---|
301 | "for (%s)", s[2], s[3], s[0], s[1]) |
---|
302 | |
---|
303 | # if s[1] is None, then the subscription is for a raw email |
---|
304 | # address that has been set in some field and we shouldn't skip |
---|
305 | # the next raw email subscription. In other words, all raw email |
---|
306 | # subscriptions should be added. |
---|
307 | if s[2]: |
---|
308 | state['last'] = (s[1], s[2], s[3]) |
---|
309 | |
---|
310 | return resolved_subs |
---|
311 | |
---|
312 | |
---|
313 | # Import i18n methods. Fallback to keep Babel optional. |
---|
314 | try: |
---|
315 | from trac.util.translation import domain_functions |
---|
316 | except ImportError: |
---|
317 | from genshi.builder import tag as tag_ |
---|
318 | from trac.util.translation import gettext |
---|
319 | |
---|
320 | _ = gettext |
---|
321 | |
---|
322 | def N_(text): |
---|
323 | return text |
---|
324 | |
---|
325 | def add_domain(a, b, c=None): |
---|
326 | pass |
---|
327 | else: |
---|
328 | add_domain, _, N_, tag_ = \ |
---|
329 | domain_functions('announcer', ('add_domain', '_', 'N_', 'tag_')) |
---|
330 | |
---|
331 | |
---|
332 | class AnnouncementSystem(Component): |
---|
333 | """AnnouncementSystem represents the entry-point into the announcement |
---|
334 | system, and is also the central controller that handles passing notices |
---|
335 | around. |
---|
336 | |
---|
337 | An announcement begins when something-- an announcement provider-- |
---|
338 | constructs an AnnouncementEvent (or subclass) and calls the send method |
---|
339 | on the AnnouncementSystem. |
---|
340 | |
---|
341 | Every event is classified by two required fields-- realm and category. |
---|
342 | In general, the realm corresponds to the realm of a Resource within Trac; |
---|
343 | ticket, wiki, milestone, and such. This is not a requirement, however. |
---|
344 | Realms can be anything distinctive-- if you specify novel realms to solve |
---|
345 | a particular problem, you'll simply also have to specify subscribers and |
---|
346 | formatters who are able to deal with data in those realms. |
---|
347 | |
---|
348 | The other classifier is a category that is defined by the providers and |
---|
349 | has no particular meaning; for the providers that implement the |
---|
350 | I*ChangeListener interfaces, the categories will often correspond to the |
---|
351 | kinds of events they receive. For tickets, they would be 'created', |
---|
352 | 'changed' and 'deleted'. |
---|
353 | |
---|
354 | There is no requirement for an event to have more then realm and category |
---|
355 | to classify an event, but if more is provided in a subclass that the |
---|
356 | subscribers can use to pick through events, all power to you. |
---|
357 | """ |
---|
358 | |
---|
359 | implements(IEnvironmentSetupParticipant) |
---|
360 | |
---|
361 | subscribers = ExtensionPoint(IAnnouncementSubscriber) |
---|
362 | subscription_filters = ExtensionPoint(IAnnouncementSubscriptionFilter) |
---|
363 | subscription_resolvers = ExtensionPoint(IAnnouncementSubscriptionResolver) |
---|
364 | distributors = ExtensionPoint(IAnnouncementDistributor) |
---|
365 | |
---|
366 | resolver = ExtensionOption('announcer', 'subscription_resolvers', |
---|
367 | IAnnouncementSubscriptionResolver, |
---|
368 | 'SubscriptionResolver', |
---|
369 | """Comma-separated list of subscription resolver components in the |
---|
370 | order they will be called. |
---|
371 | """) |
---|
372 | |
---|
373 | def __init__(self): |
---|
374 | # Bind the 'announcer' catalog to the specified locale directory. |
---|
375 | locale_dir = resource_filename(__name__, 'locale') |
---|
376 | add_domain(self.env.path, locale_dir) |
---|
377 | |
---|
378 | # IEnvironmentSetupParticipant methods |
---|
379 | |
---|
380 | def environment_created(self): |
---|
381 | self._upgrade_db() |
---|
382 | |
---|
383 | def environment_needs_upgrade(self): |
---|
384 | schema_ver = self.get_schema_version() |
---|
385 | if schema_ver == db_default.schema_version: |
---|
386 | return False |
---|
387 | if schema_ver > db_default.schema_version: |
---|
388 | raise TracError(_("""A newer plugin version has been installed |
---|
389 | before, but downgrading is unsupported.""")) |
---|
390 | self.log.info("TracAnnouncer db schema version is %d, should be %d", |
---|
391 | schema_ver, db_default.schema_version) |
---|
392 | return True |
---|
393 | |
---|
394 | def upgrade_environment(self): |
---|
395 | self._upgrade_db() |
---|
396 | |
---|
397 | # Internal methods |
---|
398 | |
---|
399 | def get_schema_version(self): |
---|
400 | """Return the current schema version for this plugin.""" |
---|
401 | with self.env.db_transaction as db: |
---|
402 | cursor = db.cursor() |
---|
403 | cursor.execute(""" |
---|
404 | SELECT value |
---|
405 | FROM system |
---|
406 | WHERE name='announcer_version' |
---|
407 | """) |
---|
408 | row = cursor.fetchone() |
---|
409 | if not (row and int(row[0]) > 5): |
---|
410 | # Care for pre-announcer-1.0 installations. |
---|
411 | dburi = self.config.get('trac', 'database') |
---|
412 | tables = self._get_tables(dburi, cursor) |
---|
413 | if 'subscription' in tables: |
---|
414 | # Version > 2 |
---|
415 | cursor.execute("SELECT * FROM subscription_attribute") |
---|
416 | columns = [col[0] for col in cursor.cursor.description] |
---|
417 | if 'authenticated' in columns: |
---|
418 | self.log.debug("TracAnnouncer needs to register " |
---|
419 | "schema version") |
---|
420 | return 5 |
---|
421 | if 'realm' in columns: |
---|
422 | self.log.debug("TracAnnouncer needs to change a table") |
---|
423 | return 4 |
---|
424 | self.log.debug("TracAnnouncer needs to change tables") |
---|
425 | return 3 |
---|
426 | if 'subscriptions' in tables: |
---|
427 | cursor.execute("SELECT * FROM subscriptions") |
---|
428 | columns = [col[0] for col in cursor.cursor.description] |
---|
429 | if 'format' not in columns: |
---|
430 | self.log.debug("TracAnnouncer needs to add new tables") |
---|
431 | return 2 |
---|
432 | self.log.debug("TracAnnouncer needs to add/change tables") |
---|
433 | return 1 |
---|
434 | # This is a new installation. |
---|
435 | return 0 |
---|
436 | # The expected outcome for any up-to-date installation. |
---|
437 | return row and int(row[0]) or 0 |
---|
438 | |
---|
439 | @staticmethod |
---|
440 | def _get_tables(dburi, cursor): |
---|
441 | """Code from TracMigratePlugin by Jun Omae (see tracmigrate.admin).""" |
---|
442 | if dburi.startswith('sqlite:'): |
---|
443 | sql = """ |
---|
444 | SELECT name |
---|
445 | FROM sqlite_master |
---|
446 | WHERE type='table' |
---|
447 | AND NOT name='sqlite_sequence' |
---|
448 | """ |
---|
449 | elif dburi.startswith('postgres:'): |
---|
450 | sql = """ |
---|
451 | SELECT tablename |
---|
452 | FROM pg_tables |
---|
453 | WHERE schemaname = ANY (current_schemas(false)) |
---|
454 | """ |
---|
455 | elif dburi.startswith('mysql:'): |
---|
456 | sql = "SHOW TABLES" |
---|
457 | else: |
---|
458 | raise TracError('Unsupported database type "%s"' |
---|
459 | % dburi.split(':')[0]) |
---|
460 | cursor.execute(sql) |
---|
461 | return sorted(row[0] for row in cursor) |
---|
462 | |
---|
463 | def _upgrade_db(self): |
---|
464 | """Each schema version should have its own upgrade module, named |
---|
465 | upgrades/dbN.py, where 'N' is the version number (int). |
---|
466 | """ |
---|
467 | db_mgr = DatabaseManager(self.env) |
---|
468 | schema_ver = self.get_schema_version() |
---|
469 | |
---|
470 | with self.env.db_transaction as db: |
---|
471 | cursor = db.cursor() |
---|
472 | # Is this a new installation? |
---|
473 | if not schema_ver: |
---|
474 | # Perform a single-step install: Create plugin schema and |
---|
475 | # insert default data into the database. |
---|
476 | connector = db_mgr.get_connector()[0] |
---|
477 | for table in db_default.schema: |
---|
478 | for stmt in connector.to_sql(table): |
---|
479 | cursor.execute(stmt) |
---|
480 | for table, cols, vals in db_default.get_data(db): |
---|
481 | cursor.executemany(""" |
---|
482 | INSERT INTO %s (%s) VALUES (%s) |
---|
483 | """ % (table, ','.join(cols), |
---|
484 | ','.join('%s' for c in cols)), vals) |
---|
485 | else: |
---|
486 | # Perform incremental upgrades. |
---|
487 | for i in range(schema_ver + 1, db_default.schema_version + 1): |
---|
488 | name = 'db%i' % i |
---|
489 | try: |
---|
490 | upgrades = __import__('announcer.upgrades', globals(), |
---|
491 | locals(), [name]) |
---|
492 | script = getattr(upgrades, name) |
---|
493 | except AttributeError: |
---|
494 | raise TracError(_(""" |
---|
495 | No upgrade module for version %(num)i |
---|
496 | (%(version)s.py) |
---|
497 | """, num=i, version=name)) |
---|
498 | script.do_upgrade(self.env, i, cursor) |
---|
499 | cursor.execute(""" |
---|
500 | UPDATE system |
---|
501 | SET value=%s |
---|
502 | WHERE name='announcer_version' |
---|
503 | """, (db_default.schema_version,)) |
---|
504 | self.log.info("Upgraded TracAnnouncer db schema from version " |
---|
505 | "%d to %d", schema_ver, db_default.schema_version) |
---|
506 | |
---|
507 | # AnnouncementSystem core methods |
---|
508 | |
---|
509 | def send(self, evt): |
---|
510 | start = time.time() |
---|
511 | self._real_send(evt) |
---|
512 | stop = time.time() |
---|
513 | self.log.debug("AnnouncementSystem sent event in %s seconds.", |
---|
514 | round(stop - start, 2)) |
---|
515 | |
---|
516 | def _real_send(self, evt): |
---|
517 | """Accepts a single AnnouncementEvent instance (or subclass), and |
---|
518 | returns nothing. |
---|
519 | |
---|
520 | There is no way (intentionally) to determine what the |
---|
521 | AnnouncementSystem did with a particular event besides looking through |
---|
522 | the debug logs. |
---|
523 | """ |
---|
524 | try: |
---|
525 | subscriptions = self.resolver.subscriptions(evt) |
---|
526 | for sf in self.subscription_filters: |
---|
527 | subscriptions = \ |
---|
528 | set(sf.filter_subscriptions(evt, subscriptions)) |
---|
529 | |
---|
530 | self.log.debug("AnnouncementSystem has found the following " |
---|
531 | "subscriptions: %s", |
---|
532 | ', '.join('[%s(%s) via %s]' |
---|
533 | % (s[1] or s[3], |
---|
534 | s[2] if 'authenticated' |
---|
535 | else 'not authenticated', |
---|
536 | s[0]) |
---|
537 | for s in subscriptions)) |
---|
538 | packages = {} |
---|
539 | for transport, sid, authenticated, address, subs_format \ |
---|
540 | in subscriptions: |
---|
541 | if transport not in packages: |
---|
542 | packages[transport] = set() |
---|
543 | packages[transport].add((sid, authenticated, address)) |
---|
544 | for distributor in self.distributors: |
---|
545 | for transport in distributor.transports(): |
---|
546 | if transport in packages: |
---|
547 | distributor.distribute(transport, packages[transport], |
---|
548 | evt) |
---|
549 | except Exception: |
---|
550 | self.log.error("AnnouncementSystem failed.", exc_info=True) |
---|