]>
Commit | Line | Data |
---|---|---|
685e346e A |
1 | #!/usr/bin/python pseudoserver.py |
2 | # psm_quotes.py | |
3 | # based on psm_limitserv.py written by celestin - martin <martin@rizon.net> | |
4 | ||
5 | import sys | |
6 | import types | |
7 | import random | |
8 | from istring import istring | |
9 | from pseudoclient import sys_log, sys_options, sys_channels, inviteable | |
10 | from utils import * | |
11 | ||
12 | from pyva import * | |
13 | import logging | |
14 | from core import * | |
15 | from plugin import * | |
16 | ||
17 | import cmd_admin, sys_auth | |
18 | ||
19 | class quotes( | |
20 | AcidPlugin, | |
21 | inviteable.InviteablePseudoclient | |
22 | ): | |
23 | initialized = False | |
24 | ||
25 | def start_threads(self): | |
26 | self.options.start() | |
27 | self.channels.start() | |
28 | self.auth.start() | |
29 | ||
30 | def bind_function(self, function): | |
31 | func = types.MethodType(function, self, quotes) | |
32 | setattr(quotes, function.__name__, func) | |
33 | return func | |
34 | ||
35 | def bind_admin_commands(self): | |
36 | list = cmd_admin.get_commands() | |
37 | self.commands_admin = [] | |
38 | ||
39 | for command in list: | |
40 | self.commands_admin.append((command, {'permission': 'j', 'callback': self.bind_function(list[command][0]), | |
41 | 'usage': list[command][1]})) | |
42 | ||
43 | def __init__(self): | |
44 | AcidPlugin.__init__(self) | |
45 | ||
46 | self.name = "quotes" | |
47 | self.log = logging.getLogger(__name__) | |
48 | ||
49 | try: | |
50 | self.nick = istring(self.config.get('quotes', 'nick')) | |
51 | except Exception, err: | |
52 | self.log.exception("Error reading 'quotes:nick' configuration option: %s" % err) | |
53 | raise | |
54 | ||
55 | try: | |
56 | self.chan = istring(self.config.get('quotes', 'channel')) | |
57 | except Exception, err: | |
58 | self.log.exception("Error reading 'quotes:channel' configuration option: %s" % err) | |
59 | raise | |
60 | ||
61 | self.bind_admin_commands() | |
62 | ||
63 | def start(self): | |
64 | try: | |
65 | self.dbp.execute("CREATE TABLE IF NOT EXISTS quotes_chans (id INT(10) NOT NULL PRIMARY KEY AUTO_INCREMENT, name VARCHAR(200) NOT NULL, UNIQUE KEY(name)) ENGINE=MyISAM;") | |
66 | self.dbp.execute("CREATE TABLE IF NOT EXISTS quotes_quotes (id INT(10) NOT NULL, quote VARCHAR(512) NOT NULL, nick VARCHAR(30) NOT NULL, time TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, channel INT NOT NULL, KEY `chan_index` (`channel`)) ENGINE=MyISAM;") | |
67 | except Exception, err: | |
68 | self.log.exception("Error creating table for quotes module (%s)" % err) | |
69 | raise | |
70 | ||
71 | try: | |
72 | AcidPlugin.start(self) | |
73 | inviteable.InviteablePseudoclient.start(self) | |
74 | ||
75 | self.options = sys_options.OptionManager(self) | |
76 | self.elog = sys_log.LogManager(self) | |
77 | self.auth = sys_auth.QuotesAuthManager(self) | |
78 | self.channels = sys_channels.ChannelManager(self) | |
79 | except Exception, err: | |
80 | self.log.exception('Error initializing subsystems for quotes module (%s)' % err) | |
81 | raise | |
82 | ||
83 | for channel in self.channels.list_valid(): | |
84 | self.join(channel.name) | |
85 | ||
86 | self.elog.debug('Joined channels.') | |
87 | ||
88 | try: | |
89 | self.start_threads() | |
90 | except Exception, err: | |
91 | self.log.exception('Error starting threads for quotes module (%s)' % err) | |
92 | raise | |
93 | ||
94 | self.initialized = True | |
95 | self.elog.debug('Started threads.') | |
96 | return True | |
97 | ||
98 | def onSync(self): | |
99 | for channel in self.channels.list_valid(): | |
100 | self.join(channel.name) | |
101 | ||
102 | self.elog.debug('Joined channels.') | |
103 | ||
104 | def stop(self): | |
105 | if hasattr(self, 'auth'): | |
106 | self.auth.stop() | |
107 | ||
108 | if hasattr(self, 'channels'): | |
109 | if self.initialized: | |
110 | self.channels.force() | |
111 | ||
112 | self.channels.stop() | |
113 | self.channels.db_close() | |
114 | ||
115 | if hasattr(self, 'options'): | |
116 | if self.initialized: | |
117 | self.options.force() | |
118 | ||
119 | self.options.stop() | |
120 | self.options.db_close() | |
121 | ||
122 | def join(self, channel): | |
123 | me = self.inter.findUser(self.nick) | |
124 | me.joinChan(channel) | |
125 | self.dbp.execute("INSERT IGNORE INTO quotes_chans(name) VALUES(%s)", (str(channel),)) | |
126 | ||
127 | def part(self, channel): | |
128 | me = self.inter.findUser(self.nick) | |
129 | me.partChan(channel) | |
130 | ||
131 | self.dbp.execute("DELETE FROM quotes_quotes WHERE channel=%s", (self.get_cid(channel),)) | |
132 | self.dbp.execute("DELETE FROM quotes_chans WHERE name=%s", (str(channel),)) | |
133 | ||
134 | def msg(self, target, message): | |
135 | if message != '': | |
136 | self.inter.privmsg(self.nick, target, format_ascii_irc(message)) | |
137 | ||
138 | def multimsg(self, target, count, intro, separator, pieces, outro = ''): | |
139 | cur = 0 | |
140 | ||
141 | while cur < len(pieces): | |
142 | self.msg(target, intro + separator.join(pieces[cur:cur + count]) + outro) | |
143 | cur += count | |
144 | ||
145 | def notice(self, target, message): | |
146 | if message != '': | |
147 | self.inter.notice(self.nick, target, format_ascii_irc(message)) | |
148 | ||
149 | def get_cid(self, cname): | |
150 | """Fetches the channel id for a given channel name.""" | |
151 | self.dbp.execute("SELECT id FROM quotes_chans WHERE name = %s", (str(cname),)) | |
152 | cid = self.dbp.fetchone()[0] | |
153 | return cid | |
154 | ||
155 | def read_quote(self, qid, cid, cname): | |
156 | """Fetches the quote id qid from the channel with id cid and displays | |
157 | the result in the channel cname. cname is to be passed on so we can | |
158 | avoid querying the db about the name, which we have in all scenarios | |
159 | where read_quote is called anyway.""" | |
160 | qid = str(qid) | |
161 | num = self.dbp.execute("SELECT nick, quote, time FROM quotes_quotes WHERE id = %s AND channel = %s", | |
162 | (qid, cid)) | |
163 | ||
164 | if num == 0: | |
165 | self.msg(cname, "[Quote] " + qid + " does not exist!") | |
166 | return | |
167 | ||
168 | nick, quote, time = self.dbp.fetchone() | |
169 | tdelta = int((datetime.now() - time).total_seconds()) | |
170 | tdeltastr = "" | |
171 | years = weeks = days = hours = minutes = 0 | |
172 | ||
173 | while tdelta > 31540000: | |
174 | years += 1 | |
175 | tdelta -= 31540000 | |
176 | while tdelta > 604800: | |
177 | weeks += 1 | |
178 | tdelta -= 604800 | |
179 | while tdelta > 86400: | |
180 | days += 1 | |
181 | tdelta -= 86400 | |
182 | while tdelta > 3600: | |
183 | hours += 1 | |
184 | tdelta -= 3600 | |
185 | while tdelta > 60: | |
186 | minutes += 1 | |
187 | tdelta -= 60 | |
188 | ||
189 | if years > 0: | |
190 | tdeltastr += str(years) + " year" | |
191 | if years != 1: | |
192 | tdeltastr += "s" | |
193 | tdeltastr += " " | |
194 | if weeks > 0: | |
195 | tdeltastr += str(weeks) + " week" | |
196 | if weeks != 1: | |
197 | tdeltastr += "s" | |
198 | tdeltastr += " " | |
199 | if days > 0: | |
200 | tdeltastr += str(days) + " day" | |
201 | if days != 1: | |
202 | tdeltastr += "s" | |
203 | tdeltastr += " " | |
204 | if hours > 0: | |
205 | tdeltastr += str(hours) + " hour" | |
206 | if hours != 1: | |
207 | tdeltastr += "s" | |
208 | tdeltastr += " " | |
209 | if minutes > 0: | |
210 | tdeltastr += str(minutes) + " minute" | |
211 | if minutes != 1: | |
212 | tdeltastr += "s" | |
213 | tdeltastr += " " | |
214 | if tdelta > 0: | |
215 | tdeltastr += str(tdelta) + " second" | |
216 | if tdelta != 1: | |
217 | tdeltastr += "s" | |
218 | ||
219 | self.msg(cname, "[Quote] #%s added by %s %s ago." % (qid, nick, tdeltastr)) | |
220 | self.inter.privmsg(self.nick, cname, '[Quote] %s' % quote) | |
221 | return | |
222 | ||
223 | def onPrivmsg(self, source, target, message): | |
224 | # Parse ADD/DEL requests | |
225 | if not self.initialized: | |
226 | return | |
227 | ||
228 | # HACKY | |
229 | # if inviteable didn't catch the command, it means we can handle it here instead | |
230 | if not inviteable.InviteablePseudoclient.onPrivmsg(self, source, target, message): | |
231 | return | |
232 | ||
233 | myself = self.inter.findUser(self.nick) | |
234 | channel = target | |
235 | ||
236 | userinfo = self.inter.findUser(source) | |
237 | sender = userinfo['nick'] | |
238 | ||
239 | msg = message.strip() | |
240 | index = msg.find(' ') | |
241 | ||
242 | if index == -1: | |
243 | command = msg | |
244 | arg = '' | |
245 | else: | |
246 | command = msg[:index] | |
247 | arg = msg[index + 1:] | |
248 | ||
249 | command = command.lower() | |
250 | ||
251 | if self.channels.is_valid(channel) and command.startswith("."): # a channel message | |
252 | command = command[1:] | |
253 | if command == 'help' and arg == '': | |
254 | self.notice(sender, "Quotes: .help quotes - for quote commands.") | |
255 | elif command == 'help' and arg.startswith('quotes'): | |
256 | self.notice(sender, "Quotes: .quote add <quote> - adds given quote to database, must have voice or higher on channel.") | |
257 | self.notice(sender, "Quotes: .quote del <number> - removes quote number from database, must be channel founder.") | |
258 | self.notice(sender, "Quotes: .quote search <word> - searches database for given word.") | |
259 | self.notice(sender, "Quotes: .quote read <number> - messages quote matching given number.") | |
260 | self.notice(sender, "Quotes: .quote random - messages a random quote.") | |
261 | self.notice(sender, "Quotes: .quote total - messages number of quotes in database.") | |
262 | elif command == 'quote': | |
263 | args = arg.split(' ') | |
264 | cid = self.get_cid(channel) | |
265 | ||
266 | if args[0] == 'add': | |
267 | c = self.inter.findChannel(channel) | |
268 | # Voice is the lowest rank, i.e., we can just check for emptiness | |
269 | if not c or c.getModes(sender) == "": | |
270 | self.notice(sender, "You must have voice or higher on the channel to add quotes.") | |
271 | return | |
272 | quote = istring(' '.join(args[1:])) | |
273 | ||
274 | # Extremely inefficient. If possible, somehow merge into | |
275 | # just one MySQL query | |
276 | # MAX(id) may return NULL if (and only if) there are no results, i.e., 0 | |
277 | # quotes. Work around that with IFNULL(expr,elsevalue). | |
278 | self.dbp.execute("SELECT IFNULL(MAX(id)+1,1) FROM quotes_quotes WHERE channel = %s", (cid,)) | |
279 | qid = self.dbp.fetchone()[0] | |
280 | ||
281 | self.dbp.execute("INSERT INTO quotes_quotes(id, quote, nick, channel) VALUES(%s, %s, %s, %s)", | |
282 | (qid, str(quote), sender, cid)) | |
283 | ||
284 | self.msg(channel, "[Quote] Added quote #%d by %s" % (qid, sender)) | |
285 | self.inter.privmsg(self.nick, channel, '[Quote] %s' % quote) | |
286 | ||
287 | if args[0] == 'del': | |
288 | if len(args) > 1 and not args[1].isdigit(): | |
289 | self.notice(sender, args[1] + " is not a valid quote number.") | |
290 | return | |
291 | ||
292 | self.notice(sender, "Checking if you are the channel founder.") | |
293 | self.auth.request(sender, channel, 'delete_quote' + args[1]) | |
294 | ||
295 | if args[0] == 'search': | |
296 | if len(args) < 2: | |
297 | return | |
298 | ||
299 | num = self.dbp.execute("SELECT id FROM quotes_quotes WHERE channel = %s AND quote LIKE CONCAT('%%',%s,'%%')", | |
300 | (cid, args[1])) | |
301 | res = self.dbp.fetchall() | |
302 | if num == 0: | |
303 | self.msg(channel, "[Quote] No quotes found.") | |
304 | return | |
305 | if num > 1: | |
306 | ids = [] | |
307 | for row in res: | |
308 | ids.append(str(row[0])) | |
309 | self.msg(channel, "[Quote] %d matches found: #%s" % (num, ','.join(ids))) | |
310 | return | |
311 | ||
312 | self.read_quote(res[0][0], cid, channel) | |
313 | ||
314 | if args[0] == 'read': | |
315 | if len(args) < 2: | |
316 | return | |
317 | ||
318 | self.read_quote(args[1], cid, channel) | |
319 | ||
320 | if args[0] == 'random': | |
321 | self.dbp.execute("SELECT id FROM quotes_quotes WHERE channel = %s ORDER BY RAND() LIMIT 1", (cid,)) | |
322 | res = self.dbp.fetchall() | |
323 | ||
324 | if not res: | |
325 | self.msg(channel, "[Quote] No quotes found!") | |
326 | return | |
327 | ||
328 | self.read_quote(res[0][0], cid, channel) | |
329 | ||
330 | if args[0] == 'total': | |
331 | self.dbp.execute("SELECT COUNT(id) FROM quotes_quotes WHERE channel = %s", (cid,)) | |
332 | qtotal = self.dbp.fetchone()[0] | |
333 | ||
334 | if qtotal != 1: | |
335 | self.msg(channel, "[Quote] %d quotes in total" % qtotal) | |
336 | else: | |
337 | self.msg(channel, "[Quote] %d quote in total" % qtotal) | |
338 | ||
339 | if args[0] == 'last': | |
340 | self.dbp.execute("SELECT id FROM quotes_quotes where channel = %s ORDER BY id DESC LIMIT 1", (cid,)) | |
341 | res = self.dbp.fetchall() | |
342 | ||
343 | if not res: | |
344 | self.msg(channel, "[Quote] No quotes found!") | |
345 | return | |
346 | ||
347 | self.read_quote(res[0][0], cid, channel) | |
348 | ||
349 | return | |
350 | ||
351 | def delete_quote(self, channel, user, qid): | |
352 | cid = self.get_cid(channel) | |
353 | ||
354 | self.dbp.execute("DELETE FROM quotes_quotes WHERE id = %s AND channel = %s", (qid, cid)) | |
355 | self.notice(user, "Deleted quote #%d" % qid) | |
356 | return | |
357 | ||
358 | def onChanModes(self, prefix, channel, modes): | |
359 | if not self.initialized: | |
360 | return | |
361 | ||
362 | if not modes == '-z': | |
363 | return | |
364 | ||
365 | if channel in self.channels: | |
366 | self.channels.remove(channel) | |
367 | try: | |
368 | self.dbp.execute("DELETE FROM quotes_quotes WHERE channel = %s", (self.get_cid(channel),)) | |
369 | except: | |
370 | pass | |
371 | self.dbp.execute("DELETE FROM quotes_chans WHERE name = %s", (channel,)) | |
372 | self.elog.request('Channel @b%s@b was dropped. Deleting it.' % channel) | |
373 | ||
374 | def getCommands(self): | |
375 | return self.commands_admin |