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