/
coretasks.py
504 lines (430 loc) · 17.1 KB
/
coretasks.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
# coding=utf8
"""
coretasks.py - Willie Routine Core tasks
Copyright 2008-2011, Sean B. Palmer (inamidst.com) and Michael Yanovich
(yanovich.net)
Copyright © 2012, Elad Alfassa <elad@fedoraproject.org>
Copyright 2012, Edward Powell (embolalia.net)
Licensed under the Eiffel Forum License 2.
Willie: http://willie.dftba.net/
This is written as a module to make it easier to extend to support more
responses to standard IRC codes without having to shove them all into the
dispatch function in bot.py and making it easier to maintain.
"""
from __future__ import unicode_literals
import re
import time
import willie
from willie.tools import Identifier, iteritems
import base64
from willie.logger import get_logger
LOGGER = get_logger(__name__)
def auth_after_register(bot):
"""Do NickServ/AuthServ auth"""
if bot.config.core.auth_method == 'nickserv' or bot.config.core.nickserv_password:
nickserv_name = bot.config.core.auth_target or bot.config.core.nickserv_name or 'NickServ'
bot.msg(
nickserv_name,
'IDENTIFY %s' % bot.config.core.nickserv_password
)
elif bot.config.core.auth_method == 'authserv' or bot.config.core.authserv_password:
account = bot.config.core.auth_username or bot.config.core.authserv_account
password = bot.config.core.auth_password or bot.config.core.authserv_password
bot.write((
'AUTHSERV auth',
account + ' ' + password
))
@willie.module.event('001', '251')
@willie.module.rule('.*')
@willie.module.thread(False)
@willie.module.unblockable
def startup(bot, trigger):
"""Do tasks related to connecting to the network.
001 RPL_WELCOME is from RFC2812 and is the first message that is sent after
the connection has been registered on the network.
251 RPL_LUSERCLIENT is a mandatory message that is sent after client
connects to the server in rfc1459. RFC2812 does not require it and all
networks might not send it. We support both.
"""
if bot.connection_registered:
return
bot.connection_registered = True
auth_after_register(bot)
#Set bot modes per config, +B if no config option is defined
if bot.config.has_option('core', 'modes'):
modes = bot.config.core.modes
else:
modes = 'B'
bot.write(('MODE ', '%s +%s' % (bot.nick, modes)))
bot.memory['retry_join'] = dict()
if bot.config.has_option('core', 'throttle_join'):
throttle_rate = int(bot.config.core.throttle_join)
channels_joined = 0
for channel in bot.config.core.get_list('channels'):
channels_joined += 1
if not channels_joined % throttle_rate:
time.sleep(1)
bot.join(channel)
else:
for channel in bot.config.core.get_list('channels'):
bot.join(channel)
@willie.module.event('477')
@willie.module.rule('.*')
@willie.module.priority('high')
def retry_join(bot, trigger):
"""Give NickServer enough time to identify on a +R channel.
Give NickServ enough time to identify, and retry rejoining an
identified-only (+R) channel. Maximum of ten rejoin attempts.
"""
channel = trigger.args[1]
if channel in bot.memory['retry_join'].keys():
bot.memory['retry_join'][channel] += 1
if bot.memory['retry_join'][channel] > 10:
LOGGER.warning('Failed to join %s after 10 attempts.', channel)
return
else:
bot.memory['retry_join'][channel] = 0
bot.join(channel)
return
time.sleep(6)
bot.join(channel)
#Functions to maintain a list of chanops in all of willie's channels.
@willie.module.rule('(.*)')
@willie.module.event('353')
@willie.module.priority('high')
@willie.module.thread(False)
@willie.module.unblockable
def handle_names(bot, trigger):
"""Handle NAMES response, happens when joining to channels."""
names = trigger.split()
#TODO specific to one channel type. See issue 281.
channels = re.search('(#\S*)', trigger.raw)
if not channels:
return
channel = Identifier(channels.group(1))
if channel not in bot.privileges:
bot.privileges[channel] = dict()
bot.init_ops_list(channel)
# This could probably be made flexible in the future, but I don't think
# it'd be worth it.
mapping = {'+': willie.module.VOICE,
'%': willie.module.HALFOP,
'@': willie.module.OP,
'&': willie.module.ADMIN,
'~': willie.module.OWNER}
for name in names:
priv = 0
for prefix, value in iteritems(mapping):
if prefix in name:
priv = priv | value
nick = Identifier(name.lstrip(''.join(mapping.keys())))
bot.privileges[channel][nick] = priv
# Old op list maintenance is down here, and should be removed at some
# point
if '@' in name or '~' in name or '&' in name:
bot.add_op(channel, name.lstrip('@&%+~'))
bot.add_halfop(channel, name.lstrip('@&%+~'))
bot.add_voice(channel, name.lstrip('@&%+~'))
elif '%' in name:
bot.add_halfop(channel, name.lstrip('@&%+~'))
bot.add_voice(channel, name.lstrip('@&%+~'))
elif '+' in name:
bot.add_voice(channel, name.lstrip('@&%+~'))
@willie.module.rule('(.*)')
@willie.module.event('MODE')
@willie.module.priority('high')
@willie.module.thread(False)
@willie.module.unblockable
def track_modes(bot, trigger):
"""Track usermode changes and keep our lists of ops up to date."""
# Mode message format: <channel> *( ( "-" / "+" ) *<modes> *<modeparams> )
channel = Identifier(trigger.args[0])
line = trigger.args[1:]
# If the first character of where the mode is being set isn't a #
# then it's a user mode, not a channel mode, so we'll ignore it.
if channel.is_nick():
return
mapping = {'v': willie.module.VOICE,
'h': willie.module.HALFOP,
'o': willie.module.OP,
'a': willie.module.ADMIN,
'q': willie.module.OWNER}
modes = []
for arg in line:
if len(arg) == 0:
continue
if arg[0] in '+-':
# There was a comment claiming IRC allows e.g. MODE +aB-c foo, but
# I don't see it in any RFCs. Leaving in the extra parsing for now.
sign = ''
modes = []
for char in arg:
if char == '+' or char == '-':
sign = char
else:
modes.append(sign + char)
else:
arg = Identifier(arg)
for mode in modes:
priv = bot.privileges[channel].get(arg, 0)
value = mapping.get(mode[1])
if value is not None:
if mode[0] == '+':
priv = priv | value
else:
priv = priv & ~value
bot.privileges[channel][arg] = priv
@willie.module.rule('.*')
@willie.module.event('NICK')
@willie.module.priority('high')
@willie.module.thread(False)
@willie.module.unblockable
def track_nicks(bot, trigger):
"""Track nickname changes and maintain our chanops list accordingly."""
old = trigger.nick
new = Identifier(trigger)
# Give debug mssage, and PM the owner, if the bot's own nick changes.
if old == bot.nick:
privmsg = ("Hi, I'm your bot, %s."
"Something has made my nick change. "
"This can cause some problems for me, "
"and make me do weird things. "
"You'll probably want to restart me, "
"and figure out what made that happen "
"so you can stop it happening again. "
"(Usually, it means you tried to give me a nick "
"that's protected by NickServ.)") % bot.nick
debug_msg = ("Nick changed by server. "
"This can cause unexpected behavior. Please restart the bot.")
LOGGER.critical(debug_msg)
bot.msg(bot.config.core.owner, privmsg)
return
for channel in bot.privileges:
channel = Identifier(channel)
if old in bot.privileges[channel]:
value = bot.privileges[channel].pop(old)
bot.privileges[channel][new] = value
# Old privilege maintenance
for channel in bot.halfplus:
if old in bot.halfplus[channel]:
bot.del_halfop(channel, old)
bot.add_halfop(channel, new)
for channel in bot.ops:
if old in bot.ops[channel]:
bot.del_op(channel, old)
bot.add_op(channel, new)
for channel in bot.voices:
if old in bot.voices[channel]:
bot.del_voice(channel, old)
bot.add_voice(channel, new)
@willie.module.rule('(.*)')
@willie.module.event('PART')
@willie.module.priority('high')
@willie.module.thread(False)
@willie.module.unblockable
def track_part(bot, trigger):
if trigger.nick == bot.nick:
bot.channels.remove(trigger.sender)
del bot.privileges[trigger.sender]
else:
try:
del bot.privileges[trigger.sender][trigger.nick]
except KeyError:
pass
@willie.module.rule('.*')
@willie.module.event('KICK')
@willie.module.priority('high')
@willie.module.thread(False)
@willie.module.unblockable
def track_kick(bot, trigger):
nick = Identifier(trigger.args[1])
if nick == bot.nick:
bot.channels.remove(trigger.sender)
del bot.privileges[trigger.sender]
else:
# Temporary fix to stop KeyErrors from being sent to channel
# The privileges dict may not have all nicks stored at all times
# causing KeyErrors
try:
del bot.privileges[trigger.sender][nick]
except KeyError:
pass
@willie.module.rule('.*')
@willie.module.event('JOIN')
@willie.module.priority('high')
@willie.module.thread(False)
@willie.module.unblockable
def track_join(bot, trigger):
if trigger.nick == bot.nick and trigger.sender not in bot.channels:
bot.channels.append(trigger.sender)
bot.privileges[trigger.sender] = dict()
bot.privileges[trigger.sender][trigger.nick] = 0
@willie.module.rule('.*')
@willie.module.event('QUIT')
@willie.module.priority('high')
@willie.module.thread(False)
@willie.module.unblockable
def track_quit(bot, trigger):
for chanprivs in bot.privileges.values():
if trigger.nick in chanprivs:
del chanprivs[trigger.nick]
@willie.module.rule('.*')
@willie.module.event('CAP')
@willie.module.thread(False)
@willie.module.priority('high')
@willie.module.unblockable
def recieve_cap_list(bot, trigger):
# Server is listing capabilites
if trigger.args[1] == 'LS':
recieve_cap_ls_reply(bot, trigger)
# Server denied CAP REQ
elif trigger.args[1] == 'NAK':
entry = bot._cap_reqs.get(trigger, None)
# If it was requested with bot.cap_req
if entry:
for req in entry:
# And that request was mandatory/prohibit, and a callback was
# provided
if req[0] and req[2]:
# Call it.
req[2](bot, req[0] + trigger)
# Server is acknowledinge SASL for us.
elif (trigger.args[0] == bot.nick and trigger.args[1] == 'ACK' and
'sasl' in trigger.args[2]):
recieve_cap_ack_sasl(bot)
def recieve_cap_ls_reply(bot, trigger):
if bot.server_capabilities:
# We've already seen the results, so someone sent CAP LS from a module.
# We're too late to do SASL, and we don't want to send CAP END before
# the module has done what it needs to, so just return
return
bot.server_capabilities = set(trigger.split(' '))
# If some other module requests it, we don't need to add another request.
# If some other module prohibits it, we shouldn't request it.
if 'multi-prefix' not in bot._cap_reqs:
# Whether or not the server supports multi-prefix doesn't change how we
# parse it, so we don't need to worry if it fails.
bot._cap_reqs['multi-prefix'] = (['', 'coretasks', None],)
for cap, reqs in iteritems(bot._cap_reqs):
# At this point, we know mandatory and prohibited don't co-exist, but
# we need to call back for optionals if they're also prohibited
prefix = ''
for entry in reqs:
if prefix == '-' and entry[0] != '-':
entry[2](bot, entry[0] + cap)
continue
if entry[0]:
prefix = entry[0]
# It's not required, or it's supported, so we can request it
if prefix != '=' or cap in bot.server_capabilities:
# REQs fail as a whole, so we send them one capability at a time
bot.write(('CAP', 'REQ', entry[0] + cap))
elif req[2]:
# Server is going to fail on it, so we call the failure function
req[2](bot, entry[0] + cap)
# If we want to do SASL, we have to wait before we can send CAP END. So if
# we are, wait on 903 (SASL successful) to send it.
if bot.config.core.auth_method == 'sasl' or bot.config.core.sasl_password:
bot.write(('CAP', 'REQ', 'sasl'))
else:
bot.write(('CAP', 'END'))
def recieve_cap_ack_sasl(bot):
# Presumably we're only here if we said we actually *want* sasl, but still
# check anyway.
password = bot.config.core.auth_password or bot.config.core.sasl_password
if not password:
return
mech = bot.config.core.sasl_mechanism or 'PLAIN'
bot.write(('AUTHENTICATE', mech))
@willie.module.event('AUTHENTICATE')
@willie.module.rule('.*')
def auth_proceed(bot, trigger):
if trigger.args[0] != '+':
# How did we get here? I am not good with computer.
return
# Is this right?
sasl_username = bot.config.core.auth_username or bot.config.core.sasl_username or bot.nick
sasl_token = '\0'.join((sasl_username, sasl_username,
bot.config.core.sasl_password))
# Spec says we do a base 64 encode on the SASL stuff
bot.write(('AUTHENTICATE', base64.b64encode(sasl_token.encode('utf-8'))))
@willie.module.event('903')
@willie.module.rule('.*')
def sasl_success(bot, trigger):
bot.write(('CAP', 'END'))
#Live blocklist editing
@willie.module.commands('blocks')
@willie.module.priority('low')
@willie.module.thread(False)
@willie.module.unblockable
def blocks(bot, trigger):
"""Manage Willie's blocking features.
https://github.com/embolalia/willie/wiki/Making-Willie-ignore-people
"""
if not trigger.admin:
return
STRINGS = {
"success_del": "Successfully deleted block: %s",
"success_add": "Successfully added block: %s",
"no_nick": "No matching nick block found for: %s",
"no_host": "No matching hostmask block found for: %s",
"invalid": "Invalid format for %s a block. Try: .blocks add (nick|hostmask) willie",
"invalid_display": "Invalid input for displaying blocks.",
"nonelisted": "No %s listed in the blocklist.",
'huh': "I could not figure out what you wanted to do.",
}
masks = bot.config.core.get_list('host_blocks')
nicks = [Identifier(nick) for nick in bot.config.core.get_list('nick_blocks')]
text = trigger.group().split()
if len(text) == 3 and text[1] == "list":
if text[2] == "hostmask":
if len(masks) > 0 and masks.count("") == 0:
for each in masks:
if len(each) > 0:
bot.say("blocked hostmask: " + each)
else:
bot.reply(STRINGS['nonelisted'] % ('hostmasks'))
elif text[2] == "nick":
if len(nicks) > 0 and nicks.count("") == 0:
for each in nicks:
if len(each) > 0:
bot.say("blocked nick: " + each)
else:
bot.reply(STRINGS['nonelisted'] % ('nicks'))
else:
bot.reply(STRINGS['invalid_display'])
elif len(text) == 4 and text[1] == "add":
if text[2] == "nick":
nicks.append(text[3])
bot.config.core.nick_blocks = nicks
bot.config.save()
elif text[2] == "hostmask":
masks.append(text[3].lower())
bot.config.core.host_blocks = masks
else:
bot.reply(STRINGS['invalid'] % ("adding"))
return
bot.reply(STRINGS['success_add'] % (text[3]))
elif len(text) == 4 and text[1] == "del":
if text[2] == "nick":
if Identifier(text[3]) not in nicks:
bot.reply(STRINGS['no_nick'] % (text[3]))
return
nicks.remove(Identifier(text[3]))
bot.config.core.nick_blocks = nicks
bot.config.save()
bot.reply(STRINGS['success_del'] % (text[3]))
elif text[2] == "hostmask":
mask = text[3].lower()
if mask not in masks:
bot.reply(STRINGS['no_host'] % (text[3]))
return
masks.remove(mask)
bot.config.core.host_blocks = masks
bot.config.save()
bot.reply(STRINGS['success_del'] % (text[3]))
else:
bot.reply(STRINGS['invalid'] % ("deleting"))
return
else:
bot.reply(STRINGS['huh'])