]> jfr.im git - irc/rizon/acid.git/blame - pyva/pyva/src/main/python/trivia/trivia.py
Cleanup e-Sim a little bit
[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
f6353b7a 25
685e346e
A
26class 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