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