11# coding=utf-8
2- """Tasks that allow the bot to run, but aren't user-facing functionality
2+ """Core Sopel plugin that handles IRC protocol functions.
3+
4+ This plugin allows the bot to run without user-facing functionality:
5+
6+ * it handles client capability negotiation
7+ * it handles client auth (both nick auth and server auth)
8+ * it handles connection registration (RPL_WELCOME, RPL_LUSERCLIENT), dealing
9+ with error cases such as nick already in use
10+ * it tracks known channels & users (join, quit, nick change and other events)
11+ * it manages blocked (ignored) users
312
413This is written as a plugin to make it easier to extend to support more
514responses to standard IRC codes without having to shove them all into the
6- dispatch function in bot.py and making it easier to maintain.
15+ dispatch function in :class:`sopel. bot.Sopel` and making it easier to maintain.
716"""
817# Copyright 2008-2011, Sean B. Palmer (inamidst.com) and Michael Yanovich
918# (yanovich.net)
4150
4251
4352def setup (bot ):
53+ """Set up the coretasks plugin.
54+
55+ The setup phase is used to activate the throttle feature to prevent a flood
56+ of JOIN commands when there are too many channels to join.
57+ """
4458 bot .memory ['join_events_queue' ] = collections .deque ()
4559
4660 # Manage JOIN flood protection
@@ -59,6 +73,7 @@ def processing_job(bot):
5973
6074
6175def shutdown (bot ):
76+ """Clean up coretasks-related values in the bot's memory."""
6277 try :
6378 bot .memory ['join_events_queue' ].clear ()
6479 except KeyError :
@@ -84,7 +99,25 @@ def _join_event_processing(bot):
8499
85100
86101def auth_after_register (bot ):
87- """Do NickServ/AuthServ auth"""
102+ """Do NickServ/AuthServ auth.
103+
104+ :param bot: a connected Sopel instance
105+ :type bot: :class:`sopel.bot.Sopel`
106+
107+ This function can be used, **after** the bot is connected, to handle one of
108+ these auth methods:
109+
110+ * ``nickserv``: send a private message to the NickServ service
111+ * ``authserv``: send an ``AUTHSERV`` command
112+ * ``Q``: send an ``AUTH`` command
113+ * ``userserv``: send a private message to the UserServ service
114+
115+ .. important::
116+
117+ If ``core.auth_method`` is set, then ``core.nick_auth_method`` will be
118+ ignored. If none is set, then this function does nothing.
119+
120+ """
88121 if bot .config .core .auth_method :
89122 auth_method = bot .config .core .auth_method
90123 auth_username = bot .config .core .auth_username
@@ -120,15 +153,28 @@ def auth_after_register(bot):
120153
121154
122155def _execute_perform (bot ):
123- """Execute commands specified to perform on IRC server connect."""
156+ """Execute commands specified to perform on IRC server connect.
157+
158+ This function executes the list of commands that can be found in the
159+ ``core.commands_on_connect`` setting. It automatically replaces any
160+ ``$nickname`` placeholder in the command with the bot's configured nick.
161+ """
124162 if not bot .connection_registered :
125163 # How did you even get this command, bot?
126164 raise Exception ('Bot must be connected to server to perform commands.' )
127165
128- LOGGER .debug ('{} commands to execute:' .format (len (bot .config .core .commands_on_connect )))
129- for i , command in enumerate (bot .config .core .commands_on_connect ):
166+ commands = bot .config .core .commands_on_connect
167+ count = len (commands )
168+
169+ if not count :
170+ LOGGER .info ('No custom command to execute.' )
171+ return
172+
173+ LOGGER .info ('Executing %d custom commands.' , count )
174+ for i , command in enumerate (commands , 1 ):
130175 command = command .replace ('$nickname' , bot .config .core .nick )
131- LOGGER .debug (command )
176+ LOGGER .debug (
177+ 'Executing custom command [%d/%d]: %s' , i , count , command )
132178 bot .write ((command ,))
133179
134180
@@ -137,6 +183,16 @@ def _execute_perform(bot):
137183@plugin .priority ('high' )
138184@plugin .unblockable
139185def on_nickname_in_use (bot , trigger ):
186+ """Change the bot's nick when the current one is already in use.
187+
188+ This can be triggered when the bot disconnects then reconnects before the
189+ server can notice a client timeout. Other reasons include mischief,
190+ trolling, and obviously, PEBKAC.
191+
192+ This will change the current nick by adding a trailing ``_``. If the bot
193+ sees that a user with its configured nick disconnects (see ``QUIT`` event
194+ handling), the bot will try to regain it.
195+ """
140196 LOGGER .error (
141197 'Nickname already in use! '
142198 '(Nick: %s; Sender: %s; Args: %r)' ,
@@ -151,7 +207,11 @@ def on_nickname_in_use(bot, trigger):
151207@module .require_admin ("This command requires admin privileges." )
152208@module .commands ('execute' )
153209def execute_perform (bot , trigger ):
154- """Execute commands specified to perform on IRC server connect."""
210+ """Execute commands specified to perform on IRC server connect.
211+
212+ This allows a bot owner or admin to force the execution of commands
213+ that are automatically performed when the bot connects.
214+ """
155215 _execute_perform (bot )
156216
157217
@@ -162,28 +222,41 @@ def execute_perform(bot, trigger):
162222def startup (bot , trigger ):
163223 """Do tasks related to connecting to the network.
164224
165- 001 RPL_WELCOME is from RFC2812 and is the first message that is sent after
166- the connection has been registered on the network.
225+ ``001 RPL_WELCOME`` is from RFC2812 and is the first message that is sent
226+ after the connection has been registered on the network.
227+
228+ ``251 RPL_LUSERCLIENT`` is a mandatory message that is sent after the
229+ client connects to the server in RFC1459. RFC2812 does not require it and
230+ all networks might not send it. We support both.
167231
168- 251 RPL_LUSERCLIENT is a mandatory message that is sent after client
169- connects to the server in rfc1459. RFC2812 does not require it and all
170- networks might not send it. We support both.
232+ If ``sopel.irc.AbstractBot.connection_registered`` is set, this function
233+ does nothing and returns immediately. Otherwise, the flag is set and the
234+ function proceeds normally to:
171235
236+ 1. trigger auth method
237+ 2. set bot's ``MODE`` (from ``core.modes``)
238+ 3. join channels (or queue them to join later)
239+ 4. check for security when the ``account-tag`` capability is enabled
240+ 5. execute custom commands
172241 """
173242 if bot .connection_registered :
174243 return
175244
245+ # set flag
176246 bot .connection_registered = True
177247
248+ # handle auth method
178249 auth_after_register (bot )
179250
251+ # set bot's MODE
180252 modes = bot .config .core .modes
181253 if modes :
182254 if not modes .startswith (('+' , '-' )):
183255 # Assume "+" by default.
184256 modes = '+' + modes
185257 bot .write (('MODE' , bot .nick , modes ))
186258
259+ # join channels
187260 bot .memory ['retry_join' ] = dict ()
188261
189262 channels = bot .config .core .channels
@@ -216,6 +289,7 @@ def startup(bot, trigger):
216289 for channel in bot .config .core .channels :
217290 bot .join (channel )
218291
292+ # warn for insecure auth method if necessary
219293 if (not bot .config .core .owner_account and
220294 'account-tag' in bot .enabled_capabilities and
221295 '@' not in bot .config .core .owner ):
@@ -227,6 +301,7 @@ def startup(bot, trigger):
227301 ).format (bot .config .core .help_prefix )
228302 bot .say (msg , bot .config .core .owner )
229303
304+ # execute custom commands
230305 _execute_perform (bot )
231306
232307
@@ -276,6 +351,14 @@ def parse_reply_myinfo(bot, trigger):
276351@module .require_owner ()
277352@module .commands ('useserviceauth' )
278353def enable_service_auth (bot , trigger ):
354+ """Set owner's account from an authenticated owner.
355+
356+ This command can be used to automatically configure ``core.owner_account``
357+ when the owner is known and has a registered account, but the bot doesn't
358+ have ``core.owner_account`` configured.
359+
360+ This doesn't work if the ``account-tag`` capability is not available.
361+ """
279362 if bot .config .core .owner_account :
280363 return
281364 if 'account-tag' not in bot .enabled_capabilities :
@@ -321,7 +404,10 @@ def retry_join(bot, trigger):
321404@module .thread (False )
322405@module .unblockable
323406def handle_names (bot , trigger ):
324- """Handle NAMES response, happens when joining to channels."""
407+ """Handle NAMES responses.
408+
409+ This function keeps track of users' privileges when Sopel joins channels.
410+ """
325411 names = trigger .split ()
326412
327413 # TODO specific to one channel type. See issue 281.
@@ -499,6 +585,7 @@ def track_nicks(bot, trigger):
499585@module .thread (False )
500586@module .unblockable
501587def track_part (bot , trigger ):
588+ """Track users leaving channels."""
502589 nick = trigger .nick
503590 channel = trigger .sender
504591 _remove_from_channel (bot , nick , channel )
@@ -509,6 +596,7 @@ def track_part(bot, trigger):
509596@module .thread (False )
510597@module .unblockable
511598def track_kick (bot , trigger ):
599+ """Track users kicked from channels."""
512600 nick = Identifier (trigger .args [1 ])
513601 channel = trigger .sender
514602 _remove_from_channel (bot , nick , channel )
@@ -587,6 +675,11 @@ def _periodic_send_who(bot):
587675@module .thread (False )
588676@module .unblockable
589677def track_join (bot , trigger ):
678+ """Track users joining channels.
679+
680+ When a user joins a channel, the bot will send (or queue) a ``WHO`` command
681+ to know more about said user (privileges, modes, etc.).
682+ """
590683 channel = trigger .sender
591684
592685 # is it a new channel?
@@ -624,6 +717,7 @@ def track_join(bot, trigger):
624717@module .thread (False )
625718@module .unblockable
626719def track_quit (bot , trigger ):
720+ """Track when users quit channels."""
627721 for chanprivs in bot .privileges .values ():
628722 chanprivs .pop (trigger .nick , None )
629723 for channel in bot .channels .values ():
@@ -641,6 +735,7 @@ def track_quit(bot, trigger):
641735@module .priority ('high' )
642736@module .unblockable
643737def receive_cap_list (bot , trigger ):
738+ """Handle client capability negotiation."""
644739 cap = trigger .strip ('-=~' )
645740 # Server is listing capabilities
646741 if trigger .args [1 ] == 'LS' :
@@ -833,6 +928,18 @@ def send_authenticate(bot, token):
833928@module .event ('AUTHENTICATE' )
834929@module .unblockable
835930def auth_proceed (bot , trigger ):
931+ """Handle client-initiated SASL auth.
932+
933+ If the chosen mechanism is client-first, the server sends an empty
934+ response (``AUTHENTICATE +``). In that case, Sopel will handle SASL auth
935+ that uses a token.
936+
937+ .. important::
938+
939+ If ``core.auth_method`` is set, then ``core.server_auth_method`` will
940+ be ignored. If none is set, then this function does nothing.
941+
942+ """
836943 if trigger .args [0 ] != '+' :
837944 # How did we get here? I am not good with computer.
838945 return
@@ -857,6 +964,13 @@ def _make_sasl_plain_token(account, password):
857964@module .event (events .RPL_SASLSUCCESS )
858965@module .unblockable
859966def sasl_success (bot , trigger ):
967+ """End CAP request on successful SASL auth.
968+
969+ If SASL is configured, then the bot won't send ``CAP END`` once it gets
970+ all the capability responses; it will wait for SASL auth result.
971+
972+ In this case, the SASL auth is a success, so we can close the negotiation.
973+ """
860974 bot .write (('CAP' , 'END' ))
861975
862976
@@ -1009,6 +1123,7 @@ def blocks(bot, trigger):
10091123
10101124@module .event ('ACCOUNT' )
10111125def account_notify (bot , trigger ):
1126+ """Track users' accounts."""
10121127 if trigger .nick not in bot .users :
10131128 bot .users [trigger .nick ] = target .User (
10141129 trigger .nick , trigger .user , trigger .host )
@@ -1022,6 +1137,7 @@ def account_notify(bot, trigger):
10221137@module .priority ('high' )
10231138@module .unblockable
10241139def recv_whox (bot , trigger ):
1140+ """Track ``WHO`` responses when ``WHOX`` is enabled."""
10251141 if len (trigger .args ) < 2 or trigger .args [1 ] not in who_reqs :
10261142 # Ignored, some plugin probably called WHO
10271143 return
@@ -1076,6 +1192,7 @@ def _record_who(bot, channel, user, host, nick, account=None, away=None, modes=N
10761192@module .priority ('high' )
10771193@module .unblockable
10781194def recv_who (bot , trigger ):
1195+ """Track ``WHO`` responses when ``WHOX`` is not enabled."""
10791196 channel , user , host , _ , nick , status = trigger .args [1 :7 ]
10801197 away = 'G' in status
10811198 modes = '' .join ([c for c in status if c in '~&@%+!' ])
@@ -1086,6 +1203,7 @@ def recv_who(bot, trigger):
10861203@module .priority ('high' )
10871204@module .unblockable
10881205def end_who (bot , trigger ):
1206+ """Handle the end of a response to a ``WHO`` command (if needed)."""
10891207 if 'WHOX' in bot .isupport :
10901208 who_reqs .pop (trigger .args [1 ], None )
10911209
@@ -1095,6 +1213,7 @@ def end_who(bot, trigger):
10951213@module .thread (False )
10961214@module .unblockable
10971215def track_notify (bot , trigger ):
1216+ """Track users going away or coming back."""
10981217 if trigger .nick not in bot .users :
10991218 bot .users [trigger .nick ] = target .User (
11001219 trigger .nick , trigger .user , trigger .host )
@@ -1108,6 +1227,7 @@ def track_notify(bot, trigger):
11081227@module .thread (False )
11091228@module .unblockable
11101229def track_topic (bot , trigger ):
1230+ """Track channels' topics."""
11111231 if trigger .event != 'TOPIC' :
11121232 channel = trigger .args [1 ]
11131233 else :
0 commit comments