5c5acdbd3f7ca7681b20bf56f423ebf59ff22dc8
[be.git] / libbe / command / subscribe.py
1 # Copyright (C) 2009 W. Trevor King <wking@drexel.edu>
2 #
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public License along
14 # with this program; if not, write to the Free Software Foundation, Inc.,
15 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
16
17 import copy
18 import os
19
20 import libbe
21 import libbe.bug
22 import libbe.command
23 import libbe.diff
24 import libbe.command.util
25 import libbe.util.tree
26
27
28 TAG="SUBSCRIBE:"
29
30
31 class Subscribe (libbe.command.Command):
32     """(Un)subscribe to change notification
33
34     >>> import sys
35     >>> import libbe.bugdir
36     >>> bd = libbe.bugdir.SimpleBugDir(memory=False)
37     >>> cmd = Subscribe()
38     >>> cmd._storage = bd.storage
39     >>> cmd._setup_io = lambda i_enc,o_enc : None
40     >>> cmd.stdout = sys.stdout
41
42     >>> a = bd.bug_from_uuid('a')
43     >>> print a.extra_strings
44     []
45     >>> ret = cmd.run({'subscriber':'John Doe <j@doe.com>'], ['/a']) # doctest: +NORMALIZE_WHITESPACE
46     Subscriptions for abc/a:
47     John Doe <j@doe.com>    all    *
48     >>> bd.flush_reload()
49     >>> a = bd.bug_from_uuid('a')
50     >>> print a.extra_strings
51     ['SUBSCRIBE:John Doe <j@doe.com>\\tall\\t*']
52     >>> ret = cmd.run({'subscriber':'Jane Doe <J@doe.com>', 'servers':'a.com,b.net'}, ['/a']) # doctest: +NORMALIZE_WHITESPACE
53     Subscriptions for abc/a:
54     Jane Doe <J@doe.com>    all    a.com,b.net
55     John Doe <j@doe.com>    all    *
56     >>> ret = cmd.run({'subscriber':'Jane Doe <J@doe.com>', 'servers':'a.edu'}, ['/a']) # doctest: +NORMALIZE_WHITESPACE
57     Subscriptions for a:
58     Jane Doe <J@doe.com>    all    a.com,a.edu,b.net
59     John Doe <j@doe.com>    all    *
60     >>> ret = cmd.run({'-u', 'subscriber':'Jane Doe <J@doe.com>', 'servers':'a.com'}, ['/a']) # doctest: +NORMALIZE_WHITESPACE
61     Subscriptions for a:
62     Jane Doe <J@doe.com>    all    a.edu,b.net
63     John Doe <j@doe.com>    all    *
64     >>> ret = cmd.run({'subscriber':'Jane Doe <J@doe.com>', 'servers':'*'}, ['/a']) # doctest: +NORMALIZE_WHITESPACE
65     Subscriptions for a:
66     Jane Doe <J@doe.com>    all    *
67     John Doe <j@doe.com>    all    *
68     >>> ret = cmd.run({'unsubscribe':True, 'subscriber':'Jane Doe <J@doe.com>'}, ['/a']) # doctest: +NORMALIZE_WHITESPACE
69     Subscriptions for a:
70     John Doe <j@doe.com>    all    *
71     >>> ret = cmd.run({'unsubscribe':True, 'subscriber':'John Doe <j@doe.com>'}, ['/a'])
72     >>> ret = cmd.run({'subscriber':'Jane Doe <J@doe.com>', '-t':'new'}, 'DIR']) # doctest: +NORMALIZE_WHITESPACE
73     Subscriptions for bug directory:
74     Jane Doe <J@doe.com>    new    *
75     >>> ret = cmd.run({'subscriber':'Jane Doe <J@doe.com>'}, ['DIR']) # doctest: +NORMALIZE_WHITESPACE
76     Subscriptions for bug directory:
77     Jane Doe <J@doe.com>    all    *
78     >>> bd.cleanup()
79     """
80     name = 'subscribe'
81
82     def __init__(self, *args, **kwargs):
83         libbe.command.Command.__init__(self, *args, **kwargs)
84         self.options.extend([
85                 libbe.command.Option(name='unsubscribe', short_name='u',
86                     help='Unsubscribe instead of subscribing'),
87                 libbe.command.Option(name='list-all', short_name='a',
88                     help='List all subscribers (no ID argument, read only action)'),
89                 libbe.command.Option(name='list', short_name='l',
90                     help='List subscribers (read only action).'),
91                 libbe.command.Option(name='subscriber', short_name='s',
92                     help='Email address of the subscriber (defaults to bugdir.user_id).',
93                     arg=libbe.command.Argument(
94                         name='subscriber', metavar='EMAIL')),
95                 libbe.command.Option(name='servers', short_name='S',
96                     help='Servers from which you want notification.',
97                     arg=libbe.command.Argument(
98                         name='servers', metavar='STRING')),
99                 libbe.command.Option(name='types', short_name='t',
100                     help='Types of changes you wish to be notified about.',
101                     arg=libbe.command.Argument(
102                         name='types', metavar='STRING')),
103                 ])
104         self.args.extend([
105                 libbe.command.Argument(
106                     name='id', metavar='ID', default=None,
107                     optional=True, repeatable=True,
108                     completion_callback=libbe.command.util.complete_bug_comment_id),
109                 ])
110
111     def _run(self, **params):
112         bugdir = self._get_bugdir()
113         if params['list-all'] == True or params['list'] == True:
114             writeable = bugdir.storage.writeable
115             bugdir.storage.writeable = False
116             if params['list-all'] == True:
117                 assert len(params['id']) == 0, params['id']
118         subscriber = params['subscriber']
119         if subscriber == None:
120             subscriber = self._get_user_id()
121         if params['unsubscribe'] == True:
122             if params['servers'] == None:
123                 params['servers'] = 'INVALID'
124             if params['types'] == None:
125                 params['types'] = 'INVALID'
126         else:
127             if params['servers'] == None:
128                 params['servers'] = '*'
129             if params['types'] == None:
130                 params['types'] = 'all'
131         servers = params['servers'].split(',')
132         types = params['types'].split(',')
133     
134         if params['id'] == None:
135             params['id'] = libbe.diff.BUGDIR_ID
136         for id in params['id']:
137             if id == libbe.diff.BUGDIR_ID: # directory-wide subscriptions
138                 type_root = libbe.diff.BUGDIR_TYPE_ALL
139                 entity = bugdir
140                 entity_name = 'bug directory'
141             else: # bug-specific subscriptions
142                 type_root = libbe.diff.BUG_TYPE_ALL
143                 bug,dummy_comment = libbe.command.util.bug_comment_from_user_id(
144                     bugdir, params['id'])
145                 entity = bug
146                 entity_name = bug.id.user()
147             if params['list-all'] == True:
148                 entity_name = 'anything in the bug directory'
149             types = [libbe.diff.type_from_name(name, type_root, default=libbe.diff.INVALID_TYPE,
150                                          default_ok=params['unsubscribe'])
151                      for name in types]
152             estrs = entity.extra_strings
153             if params['list'] == True or params['list-all'] == True:
154                 pass
155             else: # alter subscriptions
156                 if params['unsubscribe'] == True:
157                     estrs = unsubscribe(estrs, subscriber, types, servers, type_root)
158                 else: # add the tag
159                     estrs = subscribe(estrs, subscriber, types, servers, type_root)
160                 entity.extra_strings = estrs # reassign to notice change
161         
162             if params['list-all'] == True:
163                 bugdir.load_all_bugs()
164                 subscriptions = get_bugdir_subscribers(bugdir, servers[0])
165             else:
166                 subscriptions = []
167                 for estr in entity.extra_strings:
168                     if estr.startswith(TAG):
169                         subscriptions.append(estr[len(TAG):])
170     
171             if len(subscriptions) > 0:
172                 print >> self.stdout, 'Subscriptions for %s:' % entity_name
173                 print >> self.stdout, '\n'.join(subscriptions)
174         if params['list-all'] == True or params['list'] == True:
175             bugdir.storage.writeable = writeable
176         return 0
177
178     def _long_help(self):
179         return """
180 ID can be either a bug id, or blank/"DIR", in which case it refers to the
181 whole bug directory.
182
183 SERVERS specifies the servers from which you would like to receive
184 notification.  Multiple severs may be specified in a comma-separated
185 list, or you can use "*" to match all servers (the default).  If you
186 have not selected a server, it should politely refrain from notifying
187 you of changes, although there is no way to guarantee this behavior.
188
189 Available TYPES:
190   For bugs:
191 %s
192   For %s:
193 %s
194
195 For unsubscription, any listed SERVERS and TYPES are removed from your
196 subscription.  Either the catch-all server "*" or type "%s" will
197 remove SUBSCRIBER entirely from the specified ID.
198
199 This command is intended for use primarily by public interfaces, since
200 if you're just hacking away on your private repository, you'll known
201 what's changed ;).  This command just (un)sets the appropriate
202 subscriptions, and leaves it up to each interface to perform the
203 notification.
204 """ % (libbe.diff.BUG_TYPE_ALL.string_tree(6), libbe.diff.BUGDIR_ID,
205        libbe.diff.BUGDIR_TYPE_ALL.string_tree(6),
206        libbe.diff.BUGDIR_TYPE_ALL)
207
208
209 # internal helper functions
210
211 def _generate_string(subscriber, types, servers):
212     types = sorted([str(t) for t in types])
213     servers = sorted(servers)
214     return "%s%s\t%s\t%s" % (TAG,subscriber,",".join(types),",".join(servers))
215
216 def _parse_string(string, type_root):
217     assert string.startswith(TAG), string
218     string = string[len(TAG):]
219     subscriber,types,servers = string.split("\t")
220     types = [libbe.diff.type_from_name(name, type_root) for name in types.split(",")]
221     return (subscriber,types,servers.split(","))
222
223 def _get_subscriber(extra_strings, subscriber, type_root):
224     for i,string in enumerate(extra_strings):
225         if string.startswith(TAG):
226             s,ts,srvs = _parse_string(string, type_root)
227             if s == subscriber:
228                 return i,s,ts,srvs # match!
229     return None # no match
230
231 # functions exposed to other modules
232
233 def subscribe(extra_strings, subscriber, types, servers, type_root):
234     args = _get_subscriber(extra_strings, subscriber, type_root)
235     if args == None: # no match
236         extra_strings.append(_generate_string(subscriber, types, servers))
237         return extra_strings
238     # Alter matched string
239     i,s,ts,srvs = args
240     for t in types:
241         if t not in ts:
242             ts.append(t)
243     # remove descendant types
244     all_ts = copy.copy(ts)
245     for t in all_ts:
246         for tt in all_ts:
247             if tt in ts and t.has_descendant(tt):
248                 ts.remove(tt)
249     if "*" in servers+srvs:
250         srvs = ["*"]
251     else:
252         srvs = list(set(servers+srvs))
253     extra_strings[i] = _generate_string(subscriber, ts, srvs)
254     return extra_strings
255
256 def unsubscribe(extra_strings, subscriber, types, servers, type_root):
257     args = _get_subscriber(extra_strings, subscriber, type_root)
258     if args == None: # no match
259         return extra_strings # pass
260     # Remove matched string
261     i,s,ts,srvs = args
262     all_ts = copy.copy(ts)
263     for t in types:
264         for tt in all_ts:
265             if tt in ts and t.has_descendant(tt):
266                 ts.remove(tt)
267     if "*" in servers+srvs:
268         srvs = []
269     else:
270         for srv in servers:
271             if srv in srvs:
272                 srvs.remove(srv)
273     if len(ts) == 0 or len(srvs) == 0:
274         extra_strings.pop(i)
275     else:
276         extra_strings[i] = _generate_string(subscriber, ts, srvs)
277     return extra_strings
278
279 def get_subscribers(extra_strings, type, server, type_root,
280                     match_ancestor_types=False,
281                     match_descendant_types=False):
282     """
283     Set match_ancestor_types=True if you want to find eveyone who
284     cares about your particular type.
285
286     Set match_descendant_types=True if you want to find subscribers
287     who may only care about some subset of your type.  This is useful
288     for generating lists of all the subscribers in a given set of
289     extra_strings.
290
291     >>> def sgs(*args, **kwargs):
292     ...     return sorted(get_subscribers(*args, **kwargs))
293     >>> es = []
294     >>> es = subscribe(es, "John Doe <j@doe.com>", [libbe.diff.BUGDIR_TYPE_ALL],
295     ...                ["a.com"], libbe.diff.BUGDIR_TYPE_ALL)
296     >>> es = subscribe(es, "Jane Doe <J@doe.com>", [libbe.diff.BUGDIR_TYPE_NEW],
297     ...                ["*"], libbe.diff.BUGDIR_TYPE_ALL)
298     >>> sgs(es, libbe.diff.BUGDIR_TYPE_ALL, "a.com", libbe.diff.BUGDIR_TYPE_ALL)
299     ['John Doe <j@doe.com>']
300     >>> sgs(es, libbe.diff.BUGDIR_TYPE_ALL, "a.com", libbe.diff.BUGDIR_TYPE_ALL,
301     ...     match_descendant_types=True)
302     ['Jane Doe <J@doe.com>', 'John Doe <j@doe.com>']
303     >>> sgs(es, libbe.diff.BUGDIR_TYPE_ALL, "b.net", libbe.diff.BUGDIR_TYPE_ALL,
304     ...     match_descendant_types=True)
305     ['Jane Doe <J@doe.com>']
306     >>> sgs(es, libbe.diff.BUGDIR_TYPE_NEW, "a.com", libbe.diff.BUGDIR_TYPE_ALL)
307     ['Jane Doe <J@doe.com>']
308     >>> sgs(es, libbe.diff.BUGDIR_TYPE_NEW, "a.com", libbe.diff.BUGDIR_TYPE_ALL,
309     ... match_ancestor_types=True)
310     ['Jane Doe <J@doe.com>', 'John Doe <j@doe.com>']
311     """
312     for string in extra_strings:
313         if not string.startswith(TAG):
314             continue
315         subscriber,types,servers = _parse_string(string, type_root)
316         type_match = False
317         if type in types:
318             type_match = True
319         if type_match == False and match_ancestor_types == True:
320             for t in types:
321                 if t.has_descendant(type):
322                     type_match = True
323                     break
324         if type_match == False and match_descendant_types == True:
325             for t in types:
326                 if type.has_descendant(t):
327                     type_match = True
328                     break
329         server_match = False
330         if server in servers or servers == ["*"] or server == "*":
331             server_match = True
332         if type_match == True and server_match == True:
333             yield subscriber
334
335 def get_bugdir_subscribers(bugdir, server):
336     """
337     I have a bugdir.  Who cares about it, and what do they care about?
338     Returns a dict of dicts:
339       subscribers[user][id] = types
340     where id is either a bug.uuid (in the case of a bug subscription)
341     or "%(bugdir_id)s" (in the case of a bugdir subscription).
342
343     Only checks bugs that are currently in memory, so you might want
344     to call bugdir.load_all_bugs() first.
345
346     >>> bd = bugdir.SimpleBugDir(sync_with_disk=False)
347     >>> a = bd.bug_from_shortname("a")
348     >>> bd.extra_strings = subscribe(bd.extra_strings, "John Doe <j@doe.com>",
349     ...                [libbe.diff.BUGDIR_TYPE_ALL], ["a.com"], libbe.diff.BUGDIR_TYPE_ALL)
350     >>> bd.extra_strings = subscribe(bd.extra_strings, "Jane Doe <J@doe.com>",
351     ...                [libbe.diff.BUGDIR_TYPE_NEW], ["*"], libbe.diff.BUGDIR_TYPE_ALL)
352     >>> a.extra_strings = subscribe(a.extra_strings, "John Doe <j@doe.com>",
353     ...                [libbe.diff.BUG_TYPE_ALL], ["a.com"], libbe.diff.BUG_TYPE_ALL)
354     >>> subscribers = get_bugdir_subscribers(bd, "a.com")
355     >>> subscribers["Jane Doe <J@doe.com>"]["%(bugdir_id)s"]
356     [<SubscriptionType: new>]
357     >>> subscribers["John Doe <j@doe.com>"]["%(bugdir_id)s"]
358     [<SubscriptionType: all>]
359     >>> subscribers["John Doe <j@doe.com>"]["a"]
360     [<SubscriptionType: all>]
361     >>> get_bugdir_subscribers(bd, "b.net")
362     {'Jane Doe <J@doe.com>': {'%(bugdir_id)s': [<SubscriptionType: new>]}}
363     >>> bd.cleanup()
364     """ % {'bugdir_id':libbe.diff.BUGDIR_ID}
365     subscribers = {}
366     for sub in get_subscribers(bugdir.extra_strings, libbe.diff.BUGDIR_TYPE_ALL,
367                                server, libbe.diff.BUGDIR_TYPE_ALL,
368                                match_descendant_types=True):
369         i,s,ts,srvs = _get_subscriber(bugdir.extra_strings, sub,
370                                       libbe.diff.BUGDIR_TYPE_ALL)
371         subscribers[sub] = {"DIR":ts}
372     for bug in bugdir:
373         for sub in get_subscribers(bug.extra_strings, libbe.diff.BUG_TYPE_ALL,
374                                    server, libbe.diff.BUG_TYPE_ALL,
375                                    match_descendant_types=True):
376             i,s,ts,srvs = _get_subscriber(bug.extra_strings, sub,
377                                           libbe.diff.BUG_TYPE_ALL)
378             if sub in subscribers:
379                 subscribers[sub][bug.uuid] = ts
380             else:
381                 subscribers[sub] = {bug.uuid:ts}
382     return subscribers