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