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
28 inviteable
.InviteablePseudoclient
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'),
42 ('sciandnature', 'ScienceAndNature'),
43 ('simpsons', 'Simpsons'),
44 ('sg1qs', 'Stargate'))
46 # channel name => Trivia instance
49 def start_threads(self
):
54 def bind_function(self
, function
):
55 func
= types
.MethodType(function
, self
, trivia
)
56 setattr(trivia
, function
.__name
__, func
)
59 def bind_admin_commands(self
):
60 list = cmd_admin
.get_commands()
61 self
.commands_admin
= []
64 self
.commands_admin
.append((command
, {'permission': 'j', 'callback': self
.bind_function(list[command
][0]),
65 'usage': list[command
][1]}))
68 AcidPlugin
.__init
__(self
)
71 self
.log
= logging
.getLogger(__name__
)
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
)
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
)
85 self
.bind_admin_commands()
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
)
96 AcidPlugin
.start(self
)
97 inviteable
.InviteablePseudoclient
.start(self
)
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
)
107 for channel
in self
.channels
.list_valid():
108 self
.join(channel
.name
)
110 self
.elog
.debug('Joined channels.')
114 except Exception, err
:
115 self
.log
.exception('Error starting threads for trivia module (%s)' % err
)
118 self
.initialized
= True
119 self
.elog
.debug('Started threads.')
123 if hasattr(self
, 'auth'):
126 if hasattr(self
, 'channels'):
128 self
.channels
.force()
131 self
.channels
.db_close()
133 if hasattr(self
, 'options'):
138 self
.options
.db_close()
140 for cname
, trivia
in self
.trivias
.iteritems():
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
),))
149 def part(self
, channel
):
150 super(trivia
, self
).part(channel
)
152 self
.stop_trivia(channel
, True)
153 self
.dbp
.execute("DELETE FROM trivia_chans WHERE name=%s", (str(channel
),))
155 def msg(self
, target
, message
):
157 Acidictive
.privmsg(self
.nick
, target
, format_ascii_irc(message
))
159 def multimsg(self
, target
, count
, intro
, separator
, pieces
, outro
= ''):
162 while cur
< len(pieces
):
163 self
.msg(target
, intro
+ separator
.join(pieces
[cur
:cur
+ count
]) + outro
)
166 def notice(self
, target
, message
):
168 Acidictive
.notice(self
.nick
, target
, format_ascii_irc(message
))
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]
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
:
183 self
.trivias
[istring(cname
)].stop(forced
)
184 del self
.trivias
[istring(cname
)]
186 def onPrivmsg(self
, source
, target
, message
):
187 # Parse ADD/DEL requests
188 if not self
.initialized
:
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
):
196 myself
= User
.findUser(self
.nick
)
199 userinfo
= User
.findUser(source
)
200 sender
= userinfo
['nick']
202 msg
= message
.strip()
203 index
= msg
.find(' ')
209 command
= msg
[:index
]
210 arg
= msg
[index
+ 1:]
212 command
= command
.lower()
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
)
238 if arg
.isdigit() and arg
> 0:
241 self
.dbp
.execute("SELECT theme FROM trivia_chans WHERE name = %s", (channel
,))
242 theme
= self
.dbp
.fetchone()[0]
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?
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]))
260 self
.notice(sender
, "No one has played yet.")
262 self
.notice(sender
, '; '.join(out
))
263 elif command
== 'rank':
265 querynick
= arg
.lower()
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
)):
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")
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])
279 self
.notice(sender
, out
)
282 self
.notice(sender
, "Nickname not found.")
283 #elif command == 'next': # Defunct in orig trivia
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(' ')
295 if len(args
) < 2 or args
[0].lower() != 'set':
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
304 self
.trivias
[istring(channel
)].check_answer(sender
, msg
)
306 def onChanModes(self
, prefix
, channel
, modes
):
307 if not self
.initialized
:
310 if not modes
== '-z':
313 if channel
in self
.channels
:
314 self
.channels
.remove(channel
)
316 self
.dbp
.execute("DELETE FROM trivia_scores WHERE channel = %s", (self
.get_cid(channel
),))
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
)
322 def getCommands(self
):
323 return self
.commands_admin