1 #!/usr/bin/python pseudoserver.py
4 # based on psm_quotes.py, which is based on psm_limitserv.py written by
5 # celestin - martin <martin@rizon.net>
11 from istring
import istring
12 from pseudoclient
import sys_log
, sys_options
, sys_channels
, inviteable
20 import cmd_admin
, sys_auth
, trivia_engine
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 import pyva_net_rizon_acid_core_Channel
as Channel
29 inviteable
.InviteablePseudoclient
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'),
43 ('sciandnature', 'ScienceAndNature'),
44 ('simpsons', 'Simpsons'),
45 ('sg1qs', 'Stargate'))
47 # channel name => Trivia instance
50 def start_threads(self
):
55 def bind_function(self
, function
):
56 func
= types
.MethodType(function
, self
, trivia
)
57 setattr(trivia
, function
.__name
__, func
)
60 def bind_admin_commands(self
):
61 list = cmd_admin
.get_commands()
62 self
.commands_admin
= []
65 self
.commands_admin
.append((command
, {'permission': 'j', 'callback': self
.bind_function(list[command
][0]),
66 'usage': list[command
][1]}))
69 AcidPlugin
.__init
__(self
)
72 self
.log
= logging
.getLogger(__name__
)
75 self
.nick
= istring(self
.config
.get('trivia').get('nick'))
76 except Exception, err
:
77 self
.log
.exception("Error reading 'trivia:nick' configuration option: %s" % err
)
81 self
.chan
= istring(self
.config
.get('trivia').get('channel'))
82 except Exception, err
:
83 self
.log
.exception("Error reading 'trivia:channel' configuration option: %s" % err
)
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
)
92 self
.bind_admin_commands()
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")
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;")
98 except Exception, err
:
99 self
.log
.exception("Error creating table for trivia module (%s)" % err
)
103 AcidPlugin
.start(self
)
104 inviteable
.InviteablePseudoclient
.start(self
)
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
)
114 for channel
in self
.channels
.list_valid():
115 self
.join(channel
.name
)
117 self
.elog
.debug('Joined channels.')
121 except Exception, err
:
122 self
.log
.exception('Error starting threads for trivia module (%s)' % err
)
125 self
.initialized
= True
126 self
.elog
.debug('Started threads.')
130 if hasattr(self
, 'auth'):
133 if hasattr(self
, 'channels'):
135 self
.channels
.force()
138 self
.channels
.db_close()
140 if hasattr(self
, 'options'):
145 self
.options
.db_close()
147 for cname
, trivia
in self
.trivias
.iteritems():
152 def join(self
, channel
):
153 super(trivia
, self
).join(channel
)
154 self
.dbp
.execute("INSERT IGNORE INTO trivia_chans(name) VALUES(%s)", (str(channel
),))
156 def part(self
, channel
):
157 super(trivia
, self
).part(channel
)
159 self
.stop_trivia(channel
, True)
160 self
.dbp
.execute("DELETE FROM trivia_chans WHERE name=%s", (str(channel
),))
162 def msg(self
, target
, message
):
164 Acidictive
.privmsg(self
.nick
, target
, format_ascii_irc(message
))
166 def multimsg(self
, target
, count
, intro
, separator
, pieces
, outro
= ''):
169 while cur
< len(pieces
):
170 self
.msg(target
, intro
+ separator
.join(pieces
[cur
:cur
+ count
]) + outro
)
173 def notice(self
, target
, message
):
175 Acidictive
.notice(self
.nick
, target
, format_ascii_irc(message
))
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]
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
:
190 self
.trivias
[istring(cname
)].skip()
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
:
197 self
.trivias
[istring(cname
)].stop(forced
)
198 del self
.trivias
[istring(cname
)]
200 def onPrivmsg(self
, source
, target
, message
):
201 # Parse ADD/DEL requests
202 if not self
.initialized
:
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
):
210 myself
= User
.findUser(self
.nick
)
213 userinfo
= User
.findUser(source
)
214 sender
= userinfo
['nick']
216 msg
= message
.strip()
217 index
= msg
.find(' ')
223 command
= msg
[:index
]
224 arg
= msg
[index
+ 1:]
226 command
= command
.lower()
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'):
238 self
.notice(sender
, "Trivia: .trivia [number] - to start playing. (max {0} rounds)".format(self
.maxrounds
))
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.")
242 self
.notice(sender
, "Trivia: .next - skips question. (Must be half operator or higher on the channel)")
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
)
250 rounds
= self
.maxrounds
251 if arg
.isdigit() and int(arg
) > 0:
252 rounds
= min(int(arg
), self
.maxrounds
)
254 self
.dbp
.execute("SELECT theme FROM trivia_chans WHERE name = %s", (channel
,))
255 theme
= self
.dbp
.fetchone()[0]
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?
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]))
273 self
.notice(sender
, "No one has played yet.")
275 self
.notice(sender
, '; '.join(out
))
276 elif command
== 'rank':
278 querynick
= arg
.lower()
280 querynick
= sender
.lower()
281 self
.dbp
.execute("SELECT nick, points FROM trivia_scores WHERE channel = %s ORDER BY points DESC;", (self
.get_cid(channel
),))
282 rows
= self
.dbp
.fetchall()
283 for i
in range(len(rows
)):
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")
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])
292 self
.notice(sender
, out
)
295 self
.notice(sender
, "Nickname not found.")
296 #elif command == 'next': # Defunct in orig trivia
298 elif command
== 'themes':
299 for theme
in self
.themes
:
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])
302 self
.notice(sender
, "Theme: %s, %d questions" %
303 (theme
[1], self
.dbp
.fetchone()[0]))
304 elif command
== 'theme':
305 args
= arg
.split(' ')
307 if len(args
) < 2 or args
[0].lower() != 'set':
310 self
.notice(sender
, "Checking if you are the channel founder.")
311 self
.auth
.request(sender
, channel
, 'set_theme_' + args
[1])
312 elif command
== 'skip' or command
== 'next':
313 channelObj
= Channel
.findChannel(channel
);
318 membership
= channelObj
.findUser(userinfo
)
323 if not membership
.isOp():
324 self
.notice(sender
, "You're not an (half)operator on the channel")
327 self
.skip_trivia(channel
)
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
332 self
.trivias
[istring(channel
)].check_answer(sender
, msg
)
334 def onChanModes(self
, prefix
, channel
, modes
):
335 if not self
.initialized
:
338 if not modes
== '-z':
341 if channel
in self
.channels
:
342 self
.channels
.remove(channel
)
344 self
.dbp
.execute("DELETE FROM trivia_scores WHERE channel = %s", (self
.get_cid(channel
),))
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
)
350 def getCommands(self
):
351 return self
.commands_admin