]> jfr.im git - irc/rizon/acid.git/blame - pyva/pyva/src/main/python/trivia/trivia.py
Allow channel half operators or higher to skip a trivia question
[irc/rizon/acid.git] / pyva / pyva / src / main / python / trivia / trivia.py
CommitLineData
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
7import sys
8import types
9import datetime
10import random
11from istring import istring
12from pseudoclient import sys_log, sys_options, sys_channels, inviteable
13from utils import *
14
15from pyva import *
16import logging
17from core import *
18from plugin import *
19
20import cmd_admin, sys_auth, trivia_engine
21
2d09c59a 22import pyva_net_rizon_acid_core_Acidictive as Acidictive
f6353b7a 23import pyva_net_rizon_acid_core_AcidCore as AcidCore
2d09c59a 24import pyva_net_rizon_acid_core_User as User
520ccd8f 25import pyva_net_rizon_acid_core_Channel as Channel
f6353b7a 26
685e346e
A
27class 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