1### 2# Copyright (c) 2003-2005, Daniel DiPaolo 3# Copyright (c) 2010, James McCoy 4# All rights reserved. 5# 6# Redistribution and use in source and binary forms, with or without 7# modification, are permitted provided that the following conditions are met: 8# 9# * Redistributions of source code must retain the above copyright notice, 10# this list of conditions, and the following disclaimer. 11# * Redistributions in binary form must reproduce the above copyright notice, 12# this list of conditions, and the following disclaimer in the 13# documentation and/or other materials provided with the distribution. 14# * Neither the name of the author of this software nor the name of 15# contributors to this software may be used to endorse or promote products 16# derived from this software without specific prior written consent. 17# 18# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 21# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 22# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 23# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 24# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 26# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 27# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 28# POSSIBILITY OF SUCH DAMAGE. 29### 30 31import os 32import re 33import time 34import operator 35 36import supybot.dbi as dbi 37import supybot.conf as conf 38import supybot.ircdb as ircdb 39import supybot.utils as utils 40from supybot.commands import * 41import supybot.plugins as plugins 42import supybot.ircutils as ircutils 43import supybot.callbacks as callbacks 44from supybot import commands 45from supybot.i18n import PluginInternationalization, internationalizeDocstring 46_ = PluginInternationalization('Todo') 47 48class TodoRecord(dbi.Record): 49 __fields__ = [ 50 ('priority', int), 51 'at', 52 'task', 53 'active', 54 ] 55 56dataDir = conf.supybot.directories.data 57 58class FlatTodoDb(object): 59 def __init__(self): 60 self.directory = dataDir.dirize('Todo') 61 if not os.path.exists(self.directory): 62 os.mkdir(self.directory) 63 self.dbs = {} 64 65 def _getDb(self, uid): 66 dbfile = os.path.join(self.directory, str(uid)) 67 if uid not in self.dbs: 68 self.dbs[uid] = dbi.DB(dbfile, Record=TodoRecord) 69 return self.dbs[uid] 70 71 def close(self): 72 for db in self.dbs.values(): 73 db.close() 74 75 def get(self, uid, tid): 76 db = self._getDb(uid) 77 return db.get(tid) 78 79 def getTodos(self, uid): 80 db = self._getDb(uid) 81 L = [R for R in db.select(lambda r: r.active)] 82 if not L: 83 raise dbi.NoRecordError 84 return L 85 86 def add(self, priority, now, uid, task): 87 db = self._getDb(uid) 88 return db.add(TodoRecord(priority=priority, at=now, 89 task=task, active=True)) 90 91 def remove(self, uid, tid): 92 db = self._getDb(uid) 93 t = db.get(tid) 94 t.active = False 95 db.set(tid, t) 96 97 def select(self, uid, criteria): 98 db = self._getDb(uid) 99 def match(todo): 100 for p in criteria: 101 if not p(todo.task): 102 return False 103 return True 104 todos = db.select(lambda t: match(t)) 105 if not todos: 106 raise dbi.NoRecordError 107 return todos 108 109 def setpriority(self, uid, tid, priority): 110 db = self._getDb(uid) 111 t = db.get(tid) 112 t.priority = priority 113 db.set(tid, t) 114 115 def change(self, uid, tid, replacer): 116 db = self._getDb(uid) 117 t = db.get(tid) 118 t.task = replacer(t.task) 119 db.set(tid, t) 120 121class Todo(callbacks.Plugin): 122 """This plugin allows you to create your own personal to-do list on 123 the bot.""" 124 def __init__(self, irc): 125 self.__parent = super(Todo, self) 126 self.__parent.__init__(irc) 127 self.db = FlatTodoDb() 128 129 def die(self): 130 self.__parent.die() 131 self.db.close() 132 133 def _shrink(self, s): 134 return utils.str.ellipsisify(s, 50) 135 136 @internationalizeDocstring 137 def todo(self, irc, msg, args, user, taskid): 138 """[<username>] [<task id>] 139 140 Retrieves a task for the given task id. If no task id is given, it 141 will return a list of task ids that that user has added to their todo 142 list. 143 """ 144 try: 145 u = ircdb.users.getUser(msg.prefix) 146 except KeyError: 147 u = None 148 if u != user and not self.registryValue('allowThirdpartyReader'): 149 irc.error(_('You are not allowed to see other users todo-list.')) 150 return 151 # List the active tasks for the given user 152 if not taskid: 153 try: 154 tasks = self.db.getTodos(user.id) 155 utils.sortBy(operator.attrgetter('priority'), tasks) 156 tasks = [format(_('#%i: %s'), t.id, self._shrink(t.task)) 157 for t in tasks] 158 Todo = 'Todo' 159 if len(tasks) != 1: 160 Todo = 'Todos' 161 irc.reply(format(_('%s for %s: %L'), 162 Todo, user.name, tasks)) 163 except dbi.NoRecordError: 164 if u != user: 165 irc.reply(_('That user has no tasks in their todo list.')) 166 else: 167 irc.reply(_('You have no tasks in your todo list.')) 168 return 169 # Reply with the user's task 170 else: 171 try: 172 t = self.db.get(user.id, taskid) 173 if t.active: 174 active = _('Active') 175 else: 176 active = _('Inactive') 177 if t.priority: 178 t.task += format(_(', priority: %i'), t.priority) 179 at = time.strftime(conf.supybot.reply.format.time(), 180 time.localtime(t.at)) 181 s = format(_('%s todo for %s: %s (Added at %s)'), 182 active, user.name, t.task, at) 183 irc.reply(s) 184 except dbi.NoRecordError: 185 irc.errorInvalid(_('task id'), taskid) 186 todo = wrap(todo, [first('otherUser', 'user'), additional(('id', 'task'))]) 187 188 @internationalizeDocstring 189 def add(self, irc, msg, args, user, optlist, text, now): 190 """[--priority=<num>] <text> 191 192 Adds <text> as a task in your own personal todo list. The optional 193 priority argument allows you to set a task as a high or low priority. 194 Any integer is valid. 195 """ 196 priority = 0 197 for (option, arg) in optlist: 198 if option == 'priority': 199 priority = arg 200 todoId = self.db.add(priority, now, user.id, text) 201 irc.replySuccess(format(_('(Todo #%i added)'), todoId)) 202 add = wrap(add, ['user', getopts({'priority': ('int', 'priority')}), 203 'text', 'now']) 204 205 @internationalizeDocstring 206 def remove(self, irc, msg, args, user, tasks): 207 """<task id> [<task id> ...] 208 209 Removes <task id> from your personal todo list. 210 """ 211 invalid = [] 212 for taskid in tasks: 213 try: 214 self.db.get(user.id, taskid) 215 except dbi.NoRecordError: 216 invalid.append(taskid) 217 if invalid and len(invalid) == 1: 218 irc.error(format(_('Task %i could not be removed either because ' 219 'that id doesn\'t exist or it has been removed ' 220 'already.'), invalid[0])) 221 elif invalid: 222 irc.error(format(_('No tasks were removed because the following ' 223 'tasks could not be removed: %L.'), invalid)) 224 else: 225 for taskid in tasks: 226 self.db.remove(user.id, taskid) 227 irc.replySuccess() 228 remove = wrap(remove, ['user', many(('id', 'task'))]) 229 230 @internationalizeDocstring 231 def search(self, irc, msg, args, user, optlist, globs): 232 """[--{regexp} <value>] [<glob> <glob> ...] 233 234 Searches your todos for tasks matching <glob>. If --regexp is given, 235 its associated value is taken as a regexp and matched against the 236 tasks. 237 """ 238 if not optlist and not globs: 239 raise callbacks.ArgumentError 240 criteria = [] 241 for (option, arg) in optlist: 242 if option == 'regexp': 243 criteria.append(lambda s: 244 regexp_wrapper(s, reobj=arg, timeout=0.1, 245 plugin_name=self.name(), 246 fcn_name='search')) 247 for glob in globs: 248 glob = utils.python.glob2re(glob) 249 criteria.append(re.compile(glob).search) 250 try: 251 tasks = self.db.select(user.id, criteria) 252 L = [format('#%i: %s', t.id, self._shrink(t.task)) for t in tasks] 253 irc.reply(format('%L', L)) 254 except dbi.NoRecordError: 255 irc.reply(_('No tasks matched that query.')) 256 search = wrap(search, 257 ['user', getopts({'regexp': 'regexpMatcher'}), any('glob')]) 258 259 @internationalizeDocstring 260 def setpriority(self, irc, msg, args, user, id, priority): 261 """<id> <priority> 262 263 Sets the priority of the todo with the given id to the specified value. 264 """ 265 try: 266 self.db.setpriority(user.id, id, priority) 267 irc.replySuccess() 268 except dbi.NoRecordError: 269 irc.errorInvalid(_('task id'), id) 270 setpriority = wrap(setpriority, 271 ['user', ('id', 'task'), ('int', 'priority')]) 272 273 @internationalizeDocstring 274 def change(self, irc, msg, args, user, id, replacer): 275 """<task id> <regexp> 276 277 Modify the task with the given id using the supplied regexp. 278 """ 279 try: 280 self.db.change(user.id, id, replacer) 281 irc.replySuccess() 282 except dbi.NoRecordError: 283 irc.errorInvalid(_('task id'), id) 284 change = wrap(change, ['user', ('id', 'task'), 'regexpReplacer']) 285 286 287Class = Todo 288 289 290# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: 291