]>
Commit | Line | Data |
---|---|---|
685e346e A |
1 | #!/usr/bin/python pseudoserver.py |
2 | # psm_trvia.py | |
3 | # | |
4 | # based on psm_quotes.py, which is based on psm_limitserv.py written by | |
5 | # celestin - martin <martin@rizon.net> | |
6 | ||
7 | import sys | |
8 | import types | |
9 | import datetime | |
10 | import random | |
11 | from istring import istring | |
12 | from pseudoclient import sys_log, sys_options, sys_channels, inviteable | |
13 | from utils import * | |
14 | ||
15 | from pyva import * | |
16 | import logging | |
17 | from core import * | |
18 | from plugin import * | |
19 | ||
20 | import cmd_admin, sys_auth, trivia_engine | |
21 | ||
2d09c59a | 22 | import pyva_net_rizon_acid_core_Acidictive as Acidictive |
f6353b7a | 23 | import pyva_net_rizon_acid_core_AcidCore as AcidCore |
2d09c59a | 24 | import pyva_net_rizon_acid_core_User as User |
520ccd8f | 25 | import pyva_net_rizon_acid_core_Channel as Channel |
f6353b7a | 26 | |
685e346e A |
27 | class trivia( |
28 | AcidPlugin, | |
29 | inviteable.InviteablePseudoclient | |
30 | ): | |
31 | initialized = False | |
32 | ||
33 | # (table name, display name); display name is currently forced to be | |
34 | # compatible with original Trivia | |
35 | themes = (('anime', 'Anime'), | |
36 | ('default', 'default'), | |
37 | ('geography', 'Geography'), | |
38 | ('history', 'History'), | |
39 | ('lotr-books', 'LOTR-Books'), | |
40 | ('lotr-movies', 'LOTR-Movies'), | |
41 | ('movies', 'Movies'), | |
42 | ('naruto', 'Naruto'), | |
43 | ('sciandnature', 'ScienceAndNature'), | |
44 | ('simpsons', 'Simpsons'), | |
45 | ('sg1qs', 'Stargate')) | |
46 | ||
47 | # channel name => Trivia instance | |
48 | trivias = {} | |
49 | ||
50 | def start_threads(self): | |
51 | self.options.start() | |
52 | self.channels.start() | |
53 | self.auth.start() | |
54 | ||
55 | def bind_function(self, function): | |
56 | func = types.MethodType(function, self, trivia) | |
57 | setattr(trivia, function.__name__, func) | |
58 | return func | |
59 | ||
60 | def bind_admin_commands(self): | |
61 | list = cmd_admin.get_commands() | |
62 | self.commands_admin = [] | |
63 | ||
64 | for command in list: | |
65 | self.commands_admin.append((command, {'permission': 'j', 'callback': self.bind_function(list[command][0]), | |
66 | 'usage': list[command][1]})) | |
67 | ||
68 | def __init__(self): | |
69 | AcidPlugin.__init__(self) | |
70 | ||
71 | self.name = "trivia" | |
72 | self.log = logging.getLogger(__name__) | |
73 | ||
74 | try: | |
41ae6aae | 75 | self.nick = istring(self.config.get('trivia').get('nick')) |
685e346e A |
76 | except Exception, err: |
77 | self.log.exception("Error reading 'trivia:nick' configuration option: %s" % err) | |
78 | raise | |
79 | ||
80 | try: | |
41ae6aae | 81 | self.chan = istring(self.config.get('trivia').get('channel')) |
685e346e A |
82 | except Exception, err: |
83 | self.log.exception("Error reading 'trivia:channel' configuration option: %s" % err) | |
84 | raise | |
046ad1ba D |
85 | |
86 | try: | |
87 | self.maxrounds = int(self.config.get('trivia').get('maxrounds')) | |
88 | except Exception, err: | |
89 | self.log.exception("Error reading 'trivia:maxrounds' configuration option: %s" % err) | |
90 | raise | |
685e346e A |
91 | |
92 | self.bind_admin_commands() | |
93 | ||
94 | def start(self): | |
95 | try: | |
96 | self.dbp.execute("CREATE TABLE IF NOT EXISTS trivia_chans (id INT(10) NOT NULL PRIMARY KEY AUTO_INCREMENT, name VARCHAR(200) NOT NULL, theme VARCHAR(64), UNIQUE KEY(name)) ENGINE=MyISAM") | |
98ea0c99 | 97 | self.dbp.execute("CREATE TABLE IF NOT EXISTS trivia_scores (nick VARCHAR(30) DEFAULT NULL, points INT(10) DEFAULT '0', fastest_time FLOAT DEFAULT NULL, highest_streak INT(10) DEFAULT NULL, channel INT(10) DEFAULT NULL, KEY `idx` (`channel`,`points`) USING BTREE) ENGINE=MyISAM DEFAULT CHARSET=latin1;") |
685e346e A |
98 | except Exception, err: |
99 | self.log.exception("Error creating table for trivia module (%s)" % err) | |
100 | raise | |
101 | ||
102 | try: | |
103 | AcidPlugin.start(self) | |
104 | inviteable.InviteablePseudoclient.start(self) | |
105 | ||
106 | self.options = sys_options.OptionManager(self) | |
107 | self.elog = sys_log.LogManager(self) | |
108 | self.auth = sys_auth.TriviaAuthManager(self) | |
109 | self.channels = sys_channels.ChannelManager(self) | |
110 | except Exception, err: | |
111 | self.log.exception('Error initializing subsystems for trivia module (%s)' % err) | |
112 | raise | |
685e346e | 113 | |
e7697283 A |
114 | for channel in self.channels.list_valid(): |
115 | self.join(channel.name) | |
f6353b7a | 116 | |
e7697283 | 117 | self.elog.debug('Joined channels.') |
685e346e A |
118 | |
119 | try: | |
120 | self.start_threads() | |
121 | except Exception, err: | |
122 | self.log.exception('Error starting threads for trivia module (%s)' % err) | |
123 | raise | |
124 | ||
125 | self.initialized = True | |
126 | self.elog.debug('Started threads.') | |
127 | return True | |
685e346e A |
128 | |
129 | def stop(self): | |
130 | if hasattr(self, 'auth'): | |
131 | self.auth.stop() | |
132 | ||
133 | if hasattr(self, 'channels'): | |
134 | if self.initialized: | |
135 | self.channels.force() | |
136 | ||
137 | self.channels.stop() | |
138 | self.channels.db_close() | |
139 | ||
140 | if hasattr(self, 'options'): | |
141 | if self.initialized: | |
142 | self.options.force() | |
143 | ||
144 | self.options.stop() | |
145 | self.options.db_close() | |
146 | ||
147 | for cname, trivia in self.trivias.iteritems(): | |
148 | trivia.stop(True) | |
149 | ||
150 | self.trivias.clear() | |
151 | ||
152 | def join(self, channel): | |
8ec6b926 | 153 | super(trivia, self).join(channel) |
685e346e A |
154 | self.dbp.execute("INSERT IGNORE INTO trivia_chans(name) VALUES(%s)", (str(channel),)) |
155 | ||
156 | def part(self, channel): | |
8ec6b926 | 157 | super(trivia, self).part(channel) |
685e346e A |
158 | |
159 | self.stop_trivia(channel, True) | |
160 | self.dbp.execute("DELETE FROM trivia_chans WHERE name=%s", (str(channel),)) | |
161 | ||
162 | def msg(self, target, message): | |
163 | if message != '': | |
2d09c59a | 164 | Acidictive.privmsg(self.nick, target, format_ascii_irc(message)) |
685e346e A |
165 | |
166 | def multimsg(self, target, count, intro, separator, pieces, outro = ''): | |
167 | cur = 0 | |
168 | ||
169 | while cur < len(pieces): | |
170 | self.msg(target, intro + separator.join(pieces[cur:cur + count]) + outro) | |
171 | cur += count | |
172 | ||
173 | def notice(self, target, message): | |
174 | if message != '': | |
2d09c59a | 175 | Acidictive.notice(self.nick, target, format_ascii_irc(message)) |
685e346e A |
176 | |
177 | ## Begin event hooks | |
178 | ||
179 | def get_cid(self, cname): | |
180 | """Fetches the channel id for a given channel name.""" | |
181 | self.dbp.execute("SELECT id FROM trivia_chans WHERE name = %s", (cname,)) | |
182 | cid = self.dbp.fetchone()[0] | |
183 | return cid | |
184 | ||
520ccd8f O |
185 | def skip_trivia(self, cname): |
186 | '''Skips a trivia question and moves on to the next''' | |
187 | if istring(cname) not in self.trivias: | |
188 | return | |
189 | ||
190 | self.trivias[istring(cname)].skip() | |
191 | ||
685e346e A |
192 | def stop_trivia(self, cname, forced): |
193 | '''Stops a trivia instance and removes it from our dict.''' | |
194 | if istring(cname) not in self.trivias: | |
195 | return | |
196 | ||
197 | self.trivias[istring(cname)].stop(forced) | |
198 | del self.trivias[istring(cname)] | |
199 | ||
200 | def onPrivmsg(self, source, target, message): | |
201 | # Parse ADD/DEL requests | |
202 | if not self.initialized: | |
203 | return | |
204 | ||
205 | # HACKY | |
206 | # if inviteable didn't catch the command, it means we can handle it here instead | |
207 | if not inviteable.InviteablePseudoclient.onPrivmsg(self, source, target, message): | |
208 | return | |
209 | ||
2d09c59a | 210 | myself = User.findUser(self.nick) |
685e346e A |
211 | channel = target |
212 | ||
2d09c59a | 213 | userinfo = User.findUser(source) |
685e346e A |
214 | sender = userinfo['nick'] |
215 | ||
216 | msg = message.strip() | |
217 | index = msg.find(' ') | |
218 | ||
219 | if index == -1: | |
220 | command = msg | |
221 | arg = '' | |
222 | else: | |
223 | command = msg[:index] | |
224 | arg = msg[index + 1:] | |
225 | ||
226 | command = command.lower() | |
227 | ||
228 | if self.channels.is_valid(channel): # a channel message | |
229 | if command.startswith("."): # a command | |
230 | # Log each and every command. This is very, very abusive and | |
231 | # thus should NOT be used in production if it can be helped. | |
232 | # But who cares about ethics? Well, I apparently don't. | |
233 | self.elog.debug("%s:%s > %s" % (sender, channel, msg[1:])) | |
234 | command = command[1:] | |
235 | if command == 'help' and arg == '': | |
236 | self.notice(sender, "Trivia: .help trivia - for trivia commands.") | |
237 | elif command == 'help' and arg.startswith('trivia'): | |
046ad1ba | 238 | self.notice(sender, "Trivia: .trivia [number] - to start playing. (max {0} rounds)".format(self.maxrounds)) |
685e346e A |
239 | self.notice(sender, "Trivia: .strivia - to stop current round.") |
240 | self.notice(sender, "Trivia: .topten/.tt - lists top ten players.") | |
241 | self.notice(sender, "Trivia: .rank [nick] - shows yours or given nicks current rank.") | |
520ccd8f | 242 | self.notice(sender, "Trivia: .next - skips question. (Must be half operator or higher on the channel)") |
685e346e A |
243 | self.notice(sender, "Trivia: .themes - lists available question themes.") |
244 | self.notice(sender, "Trivia: .theme set <name> - changes current question theme (must be channel founder).") | |
245 | elif command == 'trivia': | |
246 | if istring(channel) in self.trivias: | |
247 | self.elog.debug("Trivia, but we're in %s" % channel) | |
248 | return | |
249 | ||
046ad1ba D |
250 | rounds = self.maxrounds |
251 | if arg.isdigit() and int(arg) > 0: | |
252 | rounds = min(int(arg), self.maxrounds) | |
685e346e A |
253 | |
254 | self.dbp.execute("SELECT theme FROM trivia_chans WHERE name = %s", (channel,)) | |
255 | theme = self.dbp.fetchone()[0] | |
256 | if not theme: | |
257 | theme = "default" | |
258 | ||
259 | self.trivias[istring(channel)] = trivia_engine.Trivia(channel, self, theme, int(rounds)) | |
260 | elif command == 'strivia': | |
261 | # stop_trivia does sanity checking | |
262 | self.stop_trivia(channel, True) | |
263 | elif command == 'topten' or command == 'tt': | |
264 | self.dbp.execute("SELECT nick, points FROM trivia_scores WHERE channel = %s ORDER BY points DESC LIMIT 10", (self.get_cid(channel),)) | |
265 | # XXX: This is silent if no entries have been done; but | |
266 | # original trivia does so, too. Silly behavior? | |
267 | out = [] | |
268 | for i, row in enumerate(self.dbp.fetchall()): | |
269 | # i+1 = 1 should equal out[0] | |
270 | out.append("%d. %s %d" % (i+1, row[0], row[1])) | |
b274b5d9 M |
271 | |
272 | if len(out) == 0: | |
273 | self.notice(sender, "No one has played yet.") | |
274 | else: | |
275 | self.notice(sender, '; '.join(out)) | |
685e346e | 276 | elif command == 'rank': |
98ea0c99 A |
277 | if arg != '': |
278 | querynick = arg.lower() | |
279 | else: | |
280 | querynick = sender.lower() | |
e518e517 | 281 | self.dbp.execute("SELECT nick, points FROM trivia_scores WHERE channel = %s ORDER BY points DESC;", (self.get_cid(channel),)) |
685e346e | 282 | rows = self.dbp.fetchall() |
e518e517 M |
283 | for i in range(len(rows)): |
284 | userdata = rows[i] | |
285 | if userdata[0].lower() == querynick.lower(): | |
286 | out = "%s is currently ranked #%d with %d %s" % (userdata[0], i + 1, userdata[1], "point" if userdata[1] == 1 else "points") | |
287 | if i > 0: | |
288 | otherdata = rows[i - 1] | |
289 | diff = otherdata[1] - userdata[1] | |
290 | out += ", %d %s behind %s" % (diff, "point" if diff == 1 else "points", otherdata[0]) | |
291 | out += "." | |
292 | self.notice(sender, out) | |
293 | break | |
98ea0c99 A |
294 | else: |
295 | self.notice(sender, "Nickname not found.") | |
685e346e A |
296 | #elif command == 'next': # Defunct in orig trivia |
297 | #pass | |
298 | elif command == 'themes': | |
299 | for theme in self.themes: | |
c59c529c A |
300 | query = "SELECT COUNT(tq.id) FROM `trivia_questions` AS `tq` JOIN `trivia_themes` AS `tt` ON tq.theme_id=tt.theme_id AND tt.theme_name='%s'" |
301 | self.dbp.execute(query % theme[1]) | |
685e346e A |
302 | self.notice(sender, "Theme: %s, %d questions" % |
303 | (theme[1], self.dbp.fetchone()[0])) | |
304 | elif command == 'theme': | |
305 | args = arg.split(' ') | |
306 | settheme = "" | |
307 | if len(args) < 2 or args[0].lower() != 'set': | |
308 | return | |
309 | ||
310 | self.notice(sender, "Checking if you are the channel founder.") | |
311 | self.auth.request(sender, channel, 'set_theme_' + args[1]) | |
520ccd8f O |
312 | elif command == 'skip' or command == 'next': |
313 | channelObj = Channel.findChannel(channel); | |
314 | ||
315 | if not channelObj: | |
316 | return | |
317 | ||
318 | membership = channelObj.findUser(userinfo) | |
319 | ||
320 | if not membership: | |
321 | return | |
322 | ||
323 | if not membership.isOp(): | |
324 | self.notice(sender, "You're not an (half)operator on the channel") | |
325 | return | |
326 | ||
327 | self.skip_trivia(channel) | |
685e346e A |
328 | else: # not a command, but might be an answer! |
329 | if istring(channel) not in self.trivias: | |
330 | return # no trivia running, disregard that | |
331 | ||
332 | self.trivias[istring(channel)].check_answer(sender, msg) | |
333 | ||
334 | def onChanModes(self, prefix, channel, modes): | |
335 | if not self.initialized: | |
336 | return | |
337 | ||
338 | if not modes == '-z': | |
339 | return | |
340 | ||
341 | if channel in self.channels: | |
342 | self.channels.remove(channel) | |
343 | try: | |
344 | self.dbp.execute("DELETE FROM trivia_scores WHERE channel = %s", (self.get_cid(channel),)) | |
345 | except: | |
346 | pass | |
347 | self.dbp.execute("DELETE FROM trivia_chans WHERE name = %s", (channel,)) | |
348 | self.elog.request('Channel @b%s@b was dropped. Deleting it.' % channel) | |
349 | ||
350 | def getCommands(self): | |
351 | return self.commands_admin |