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