Kaarsemaker.net


chanserv.py
19.59 KB

513 lines

Download

booking_beta.js
launchpadduser.py

Preview

 1 # Simple chanserv helper script for Xchat
 2 # (c) 2006-2010 Dennis Kaarsemaker
 3 #
 4 # Latest version can be found on http://www.kaarsemaker.net/software/
 5 # 
 6 # This script is free software; you can redistribute it and/or
 7 # modify it under the terms of the GNU General Public License
 8 # version 3, as published by the Free Software Foundation.
 9 # 
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 # 
15 # You should have received a copy of the GNU General Public License along
  1 # Simple chanserv helper script for Xchat
  2 # (c) 2006-2010 Dennis Kaarsemaker
  3 #
  4 # Latest version can be found on http://www.kaarsemaker.net/software/
  5 # 
  6 # This script is free software; you can redistribute it and/or
  7 # modify it under the terms of the GNU General Public License
  8 # version 3, as published by the Free Software Foundation.
  9 # 
 10 # This program is distributed in the hope that it will be useful,
 11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 13 # GNU General Public License for more details.
 14 # 
 15 # You should have received a copy of the GNU General Public License along
 16 # with this program; if not, write to the Free Software Foundation, Inc.,
 17 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 18 
 19 # Usage instructions:
 20 # - Place in ~/.xchat2 for it to be autoloaded
 21 # - Use /py load chanserv.py if you already started X-chat
 22 # - Connect to freenode, if not connected (other networks will not work)
 23 #
 24 # It adds one command to xchat: /cs
 25 # /cs understands the following arguments
 26 #
 27 # To give/take ops/voice:
 28 #
 29 # o  or op      - Let chanserv op you/others (/cs op, /cs op nick)
 30 # v  or voice   - Let chanserv give you/others voice
 31 # d  or deop    - Let chanserv deop you/others (/cs deop, /cs deop nick)
 32 # dv or devoice - Let chanserv decoice you/others (/cs devoice, /cs devoice nick)
 33 #
 34 # To op yourself, perform an action, and deop:
 35 #
 36 # k  or kick    - Kick a user, possibly with comment (/cs kick nick [comment])
 37 # b  or ban     - Ban a user (/cs ban [-nihar] nick)
 38 # kb or kickban - Kick and ban a user (/cs ban [-nihar] nick)
 39 # f  or forward - Ban a user with a forward (/cs forward [-nihar] nick chan)
 40 # kf or kickforward - Kickban a user with a forward (/cs forward [-nihar] nick chan)
 41 # m  or mute    - Mute a user (/cs mute [-nihar] nick)
 42 # l  or lart    - A combination of kick and ban on all fields
 43 # u  or unban   - Remove all bans for a user (/cs u nick)
 44 # t  or topic   - Set channel topic (/cs t New topic here)
 45 # m  or mode    - Change channel mode (/cs mode modes here)
 46 # i  or invite  - Invite yourself or someone else (/cs invite [nick])
 47 # bans          - Show bans that apply to someone without removing them (/cs bans nick)
 48 #
 49 # * Bans, forwards and mute take an extra optional argument that specifies 
 50 #   what should be banned: nickname, ident, host, account and/or realname.
 51 #   /cs ban -nah nick -- Ban nick, account and host
 52 #   /cs forward -nihra nick #somewhere -- Forward all
 53 #
 54 # * Unban will remove all bans matching the nick or mask you give as argument
 55 #   (*  and ? wildcards work)
 56 # * It won't actually kick, but use the /remove command
 57 #
 58 # The following additional features are implemented
 59 # - Autorejoin for /remove
 60 # - Auto-unmute when muted
 61 # - Auto-unban via chanserv
 62 # - Auto-invite via chanserv
 63 # - Auto-getkey via chanserv
 64 
 65 __module_name__        = "chanserv"
 66 __module_version__     = "2.0.4"
 67 __module_description__ = "Chanserv helper"
 68 
 69 import collections
 70 import xchat
 71 import time
 72 import re
 73 
 74 # Event queue
 75 pending = []
 76 # /whois cache
 77 users = {}
 78 # /mode bq 'cache'
 79 bans = collections.defaultdict(list)
 80 quiets = collections.defaultdict(list)
 81 bq_switch = {}
 82 
 83 abbreviations = {'kick': 'k', 'ban': 'b', 'kickban': 'kb', 'forward': 'f', 
 84                  'kickforward': 'kf', 'mute': 'm', 'topic': 't', 'unban': 'u',
 85                  'mode': 'm', 'invite': 'i', 'op': 'o', 'deop': 'd', 'lart': 'l',
 86                  'voice': 'v', 'devoice': 'dv', 'bans': 'bans'}
 87 expansions = dict([x[::-1] for x in abbreviations.items()])
 88 simple_commands = ['op', 'deop', 'voice', 'devoice']
 89 kick_commands = ['kick', 'kickforward', 'kickban', 'lart']
 90 forward_commands = ['kickforward', 'forward']
 91 ban_commands = ['ban', 'forward', 'mute', 'lart', 'kickban', 'kickforward']
 92 simple_commands += [abbreviations[x] for x in simple_commands]
 93 kick_commands += [abbreviations[x] for x in kick_commands]
 94 ban_commands += [abbreviations[x] for x in ban_commands]
 95 forward_commands += [abbreviations[x] for x in forward_commands]
 96 all_commands = abbreviations.keys() + abbreviations.values()
 97 ban_sentinel = '!'
 98 
 99 def cs(word, word_eol, userdata):
100     """Main command dispatcher"""
101     if len(word) == 1:
102         return xchat.EAT_ALL
103     command = word[1].lower()
104 
105     if command not in all_commands:
106         return xchat.EAT_NONE
107 
108     args = dict(enumerate(word_eol[2:]))
109     me = xchat.get_info('nick')
110 
111     action = Action(channel = xchat.get_info('channel'),
112                     me = me,
113                     context = xchat.get_context())
114 
115     # The simple ones: op/voice
116     if command in simple_commands:
117         action.target = args.get(0, me)
118         action.deop = (action.target != me)
119         action.needs_op = False
120         command = expansions.get(command,command)
121         action.actions.append('chanserv %s %%(channel)s %%(target_nick)s' % command)
122         return action.schedule()
123 
124     # Usage check
125     if len(word) < 3:
126         if command in all_commands:
127             xchat.emit_print("Server Error", "Not enough arguments for %s" % command)
128             return xchat.EAT_ALL
129         return xchat.EAT_NONE
130         
131     if command in ('t','topic'):
132         action.actions.append('chanserv TOPIC %%(channel)s %s' % args[0])
133         action.needs_op = False
134         return action.schedule()
135 
136     if command in ('m','mode') and args[0][0] in '+=-':
137         action.actions.append('MODE %%(channel)s %s' % args[0])
138         return action.schedule()
139 
140     if command in ('i','invite'):
141         target = args[0]
142         if target.startswith('#'):
143             action.needs_op = False
144             action.actions.append('chanserv INVITE %s' % target)
145         else:
146             if target.lower() in [x.nick.lower() for x in action.context.get_list('users')]:
147                 xchat.emit_print("Server Error", "%s is already in %s" % (target, action.channel))
148                 return xchat.EAT_ALL
149             action.actions.append('INVITE %s %%(channel)s' % target)
150         return action.schedule()
151 
152     # Kick/ban/forward/mute handling
153     if len(word) < 4 and command in forward_commands:
154         xchat.emit_print("Server Error", "Not enough arguments for %s" % command)
155         return xchat.EAT_ALL
156 
157     # Command dispatch
158     # Check for -nihra argument
159     if command in ban_commands:
160         if args[0].startswith('-'):
161             action.bans = args[0][1:].split(None, 1)[0]
162             args = dict(enumerate(word_eol[3:]))
163     if command in ('lart','l'):
164         action.bans = 'nihra'
165 
166     # Set target
167     action.target = args[0].split(None,1)[0]
168 
169     if not valid_nickname(action.target) and not valid_mask(action.target):
170         xchat.emit_print("Server Error", "Invalid target: %s" % action.target)
171         return xchat.EAT_ALL
172 
173     if action.bans and not valid_nickname(action.target):
174         xchat.emit_print("Server Error", "Ban types and lart can only be used with nicks, not with complete masks")
175         return xchat.EAT_ALL
176 
177     if valid_mask(action.target):
178         action.bans = 'f'
179     
180     if not action.bans:
181         action.bans = 'h'
182 
183     # Find forward channel
184     if command in forward_commands:
185         action.forward_to = '$' + args[1].split(None,1)[0] # Kludge
186         if not valid_channel(action.forward_to[1:]):
187             xchat.emit_print("Server Error", "Invalid channel: %s" % action.forward_to[1:])
188             return xchat.EAT_ALL
189 
190     # Check if target is there and schedule kick
191     if command in kick_commands:
192         if action.target.lower() not in [x.nick.lower() for x in action.context.get_list('users')]: 
193             xchat.emit_print("Server Error", "%s is not in %s" % (action.target, action.channel)) 
194             return xchat.EAT_ALL
195         action.reason = args.get(1, 'Goodbye')
196         action.actions.append('remove %(channel)s %(target_nick)s :%(reason)s')
197 
198     if command in ('m','mute'):
199         action.banmode = 'q'
200 
201     if command in ban_commands:
202         action.do_ban = True
203         if 'n' in action.bans: action.actions.append('mode %(channel)s +%(banmode)s %(target_nick)s!*@*%(forward_to)s')
204         if 'i' in action.bans: action.actions.append('mode %(channel)s +%(banmode)s *!%(target_ident)s@*%(forward_to)s')
205         if 'h' in action.bans: action.actions.append('mode %(channel)s +%(banmode)s *!*@%(target_host)s%(forward_to)s')
206         if 'r' in action.bans: action.actions.append('mode %(channel)s +%(banmode)s $r:%(target_name_bannable)s%(forward_to)s')
207         if 'a' in action.bans: action.actions.append('mode %(channel)s +%(banmode)s $a:%(target_account)s%(forward_to)s')
208         if 'f' in action.bans: action.actions.append('mode %(channel)s +%(banmode)s %(target)s%(forward_to)s')
209 
210     if command in ('u','unban'):
211         action.do_unban = True
212 
213     if command == 'bans':
214         action.do_bans = True
215         action.needs_op = False
216 
217     return action.schedule()
218 xchat.hook_command('cs',cs,"For help with /cs, please read the comments in the script")
219 
220 class Action:
221     """A list of actions to do, and information needed for them"""
222     def __init__(self, channel, me, context):
223         self.channel = channel
224         self.me = me
225         self.context = context
226         self.stamp = time.time()
227 
228         # Defaults
229         self.deop = True
230         self.needs_op = True
231         self.do_ban = self.do_unban = self.do_bans = False
232         self.banmode = 'b'
233         self.reason = ''
234         self.bans = ''
235         self.actions = []
236         self.resolved = True
237         self.target = ''
238         self.forward_to = ''
239 
240     def schedule(self):
241         """Request information and add ourselves to the queue"""
242         pending.append(self)
243         # Am I opped?
244         self.am_op = False
245         for user in self.context.get_list('users'):
246             if user.nick == self.me and user.prefix == '@':
247                 self.am_op = True
248                 self.deop = False
249 
250         if self.needs_op and not self.am_op:
251             self.context.command("chanserv op %s" % self.channel)
252 
253         # Find needed information
254         if ('a' in self.bans or 'r' in self.bans) and valid_mask(self.target) and not self.target.startswith('$'):
255             xchat.emit_print('Server Error', "Invalid argument %s for account/realname ban" % self.target)
256             return xchat.EAT_ALL
257         if self.do_ban or self.do_unban or self.do_bans:
258             self.resolve_nick()
259         else:
260             self.target_nick = self.target
261 
262         if self.do_unban or self.do_bans:
263             self.fetch_bans()
264 
265         run_pending()
266         return xchat.EAT_ALL
267 
268     def resolve_nick(self, request=True):
269         """Try to find nickname, ident and host"""
270         self.target_nick = None
271         self.target_ident = None
272         self.target_host = None
273         self.target_name = None
274         self.target_account = None
275         self.resolved = False
276 
277         if valid_mask(self.target):
278             if self.target.startswith('$a:'):
279                 self.target_account = self.target[3:]
280             elif self.target.startswith('$r:'):
281                 self.target_name = self.target[3:]
282             else:
283                 self.target_nick, self.target_mask, self.target_host = re.split('[!@]', self.target)
284             self.resolved = True
285             return
286 
287         self.target_nick = self.target.lower()
288         if self.target_nick in users:
289             if users[self.target_nick].time < time.time() - 10:
290                 del users[self.target_nick]
291                 if request:
292                     self.context.command('whois %s' % self.target_nick)
293             else:
294                 self.target_ident = users[self.target_nick].ident
295                 self.target_host = users[self.target_nick].host
296                 self.target_name = users[self.target_nick].name
297                 self.target_name_bannable = re.sub('[^a-zA-Z0-9]', '?', self.target_name)
298                 self.target_account = users[self.target_nick].account
299                 self.resolved = True
300                 if 'gateway/' in self.target_host and self.bans == 'h' and self.do_ban:
301                     # For gateway/* users, default to ident ban 
302                     self.actions.append('mode %(channel)s +%(banmode)s *!%(target_ident)s@gateway/*%(forward_to)s')
303                     self.actions.remove('mode %(channel)s +%(banmode)s *!*@%(target_host)s%(forward_to)s')
304         else:
305             if request:
306                 self.context.command('whois %s' % self.target_nick)
307 
308     def fetch_bans(self):
309         """Read bans for a channel"""
310         bq_switch[self.channel] = 'b'
311         bans[self.channel] = []
312         quiets[self.channel] = []
313         self.context.command("mode %s +bq" % self.channel)
314 
315     def run(self):
316         """Perform our actions"""
317         kwargs = dict(self.__dict__.items())
318 
319         if self.do_bans:
320             xchat.emit_print('Server Text', "Bans matching %s!%s@%s (r:%s, a:%s)" % 
321                     (self.target_nick, self.target_ident, self.target_host, self.target_name, self.target_account))
322 
323         if self.do_unban or self.do_bans:
324 
325             for b in bans[self.channel]:
326                 if self.match(b):
327                     if self.do_bans:
328                         xchat.emit_print('Server Text', b)
329                     else:
330                         self.actions.append('mode %s -b %s' % (self.channel, b))
331 
332             for b in quiets[self.channel]:
333                 if self.match(b):
334                     if self.do_bans:
335                         xchat.emit_print('Server Text', b + ' (quiet)')
336                     else:
337                         self.actions.append('mode %s -q %s' % (self.channel, b))
338 
339         # Perform all registered actions
340         for action in self.actions:
341             self.context.command(action % kwargs)
342 
343         self.done()
344 
345     def done(self):
346         """Finaliazation and cleanup"""
347         # Done!
348         pending.remove(self)
349 
350         # Deop?
351         if not self.am_op or not self.needs_op:
352             return
353 
354         for p in pending:
355             if p.channel == self.channel and p.needs_op or not p.deop:
356                 self.deop = False
357                 break
358 
359         if self.deop:
360             self.context.command("chanserv deop %s" % self.channel)
361 
362     def match(self, ban):
363         """Does a ban match this action"""
364         if ban.startswith('$r:') and self.target_name:
365             return ban2re(ban[3:]).match(self.target_name)
366         elif ban.startswith('$a:') and self.target_account:
367             return ban2re(ban[3:]).match(self.target_account)
368         else:
369             if '#' in ban:
370                 ban = ban[:ban.find('$#')]
371             return ban2re(ban).match('%s!%s@%s' % (self.target_nick, self.target_ident, self.target_host))
372 
373 def run_pending(just_opped = None):
374     """Check all actions and run them if all information is there"""
375     now = time.time()
376 
377     for p in pending:
378         if p.channel == just_opped:
379             p.am_op = True
380 
381         if p.target_nick in users and not p.resolved:
382             p.resolve_nick(request = False)
383 
384         # Timeout?
385         if p.stamp < now - 10:
386             p.done()
387             continue
388 
389         can_run = (bans[p.channel] and quiets[p.channel]) or not (p.do_unban or p.do_bans)
390         if can_run and p.resolved and (p.am_op or not p.needs_op):
391             p.run()
392 
393 # Helper functions
394 def ban2re(data):
395     return re.compile('^' + re.escape(data).replace(r'\*','.*').replace(r'\?','.') + '$')
396 
397 _valid_nickname = re.compile(r'^[-a-zA-Z0-9\[\]{}`|_]{0,30}$')
398 valid_nickname = lambda data: _valid_nickname.match(data)
399 _valid_channel = re.compile(r'^[#~].*') # OK, this is cheating
400 valid_channel = lambda data: _valid_channel.match(data)
401 _valid_mask = re.compile(r'^([-a-zA-Z0-9\[\]{}`|_*?]{0,30}!.*?@.*?|\$[ar]:.*)$')
402 valid_mask = lambda data: _valid_mask.match(data)
403  
404 # Data processing
405 def do_mode(word, word_eol, userdata):
406     """Run pending actions when chanserv opped us"""
407     ctx = xchat.get_context()
408     if 'chanserv!' in word[0].lower() and '+o' in word[3] and ctx.get_info('nick') in word:
409         run_pending(just_opped = ctx.get_info('channel'))
410 xchat.hook_server('MODE', do_mode)
411 
412 class User(object):
413     def __init__(self, nick, ident, host, name):
414         self.nick = nick; self.ident = ident; self.host = host; self.name = name
415         self.account = None
416         self.time = time.time()
417 def do_whois(word, word_eol, userdata):
418     """Store whois replies in global cache"""
419     nick = word[3].lower()
420     if word[1] == '330':
421         users[nick].account = word[4]
422     else:
423         users[nick] = User(nick, word[4], word[5], word_eol[7][1:])
424 xchat.hook_server('311', do_whois)
425 xchat.hook_server('330', do_whois)
426 xchat.hook_server('314', do_whois) # This actually is a /whowas reply
427 
428 def do_missing(word, word_eol, userdata):
429     """Fall back to whowas if whois fails"""
430     for p in pending:
431         if p.target == word[3]:
432             p.context.command('whowas %s' % word[3])
433             break
434 xchat.hook_server('401', do_missing)
435 
436 def do_endwas(word, word_eol, userdata):
437     """Display error if nickname cannot be resolved"""
438     for p in pending:
439         if p.target == word[3]:
440             xchat.emit_print("Server Error", "%s could not be found" % p.target)
441             pending.remove(p)
442 xchat.hook_server('406', do_endwas)
443 
444 def endofwhois(word, word_eol, userdata):
445     """Process the queue after nickname resolution"""
446     run_pending()
447 xchat.hook_server('318', endofwhois)
448 xchat.hook_server('369', endofwhois)
449 
450 xchat.hook_server('482', lambda word, word_eol, userdata: xchat.emit_print('Server Error', '%s in %s' % (word_eol[4][1:], word[3])))
451 
452 def do_ban(word, word_eol, userdata):
453     """Process banlists"""
454     channel, ban = word[3:5]
455     if channel in bq_switch:
456         if bq_switch[channel] == 'b':
457             bans[channel].append(ban)
458         else:
459             quiets[channel].append(ban)
460         return xchat.EAT_ALL
461     return xchat.EAT_NONE
462 xchat.hook_server('367', do_ban)
463 
464 def do_endban(word, word_eol, userdata):
465     """Process end-of-ban markers"""
466     channel = word[3]
467     if channel in bq_switch:
468         if 'Ban' in word:
469             bans[channel].append(ban_sentinel)
470             bq_switch[channel] = 'q'
471         elif 'Quiet' in word:
472             quiets[channel].append(ban_sentinel)
473             del bq_switch[channel]
474             run_pending()
475         return xchat.EAT_ALL
476     return xchat.EAT_NONE
477 xchat.hook_server('368', do_endban)
478 
479 # Turn on autorejoin
480 xchat.command('SET -quiet irc_auto_rejoin ON')
481 
482 def rejoin(word, word_eol, userdata):
483     """Rejoin when /remove'd"""
484     if word[0][1:word[0].find('!')] == xchat.get_info('nick') and len(word) > 3 and word[3][1:].lower() == 'requested':
485         xchat.command('join %s' % word[2])
486 xchat.hook_server('PART', rejoin)
487 
488 # Unban when muted
489 xchat.hook_server('404', lambda word, word_eol, userdata: xchat.command('quote cs unban %s' % word[3]))
490 
491 # Convince chanserv to let me in when key/unban/invite is needed
492 xchat.hook_server('471', lambda word, word_eol, userdata: xchat.command('quote cs invite %s' % word[3])) # 471 = limit reached
493 xchat.hook_server('473', lambda word, word_eol, userdata: xchat.command('quote cs invite %s' % word[3]))
494 xchat.hook_server('474', lambda word, word_eol, userdata: xchat.command('quote cs unban %s' % word[3]))
495 xchat.hook_server('475', lambda word, word_eol, userdata: xchat.command('quote cs getkey %s' % word[3]))
496 
497 def on_invite(word, word_eol, userdata):
498     """Autojoin when chanserv invites us"""
499     if word[0] == ':ChanServ!ChanServ@services.':
500         xchat.command('join %s' % word[-1][1:])
501 xchat.hook_server('INVITE', on_invite)
502 
503 def on_notice(word, word_eol, userdata):
504     """Autojoin when chanserv unbans us or sent us a key"""
505     if word[0] != ':ChanServ!ChanServ@services.':
506         return
507     if 'Unbanned' in word_eol[0]:
508         xchat.command('JOIN %s' % word[6].strip()[1:-1])
509     if 'key is' in word_eol[0]:
510         xchat.command('JOIN %s %s' % (word[4][1:-1], word[-1]))
511 xchat.hook_server('NOTICE', on_notice)
512 
513 xchat.emit_print('Server Text',"Loaded %s %s by Seveas <dennis@kaarsemaker.net>" % (__module_description__, __module_version__))

Show all