1717# this program. If not, see <http://www.gnu.org/licenses/>.
1818"""Base IRC bot and mixins for commonly desired functionality."""
1919
20+ import base64
2021import functools
2122import logging
23+ import ssl
2224
2325import irc .bot
2426import irc .buffer
2527import irc .client
28+ import irc .connection
2629
2730logger = logging .getLogger (__name__ )
2831
@@ -112,11 +115,27 @@ def _handle_bannedfromchan(self, conn, event):
112115 60 , functools .partial (conn .join , event .arguments [0 ]))
113116
114117
115- class FreenodePasswdAuth (object ):
118+ class JoinChannels (object ):
119+ """Join channels one at a time to avoid flooding."""
120+ def join_channels (self , channels ):
121+ """Join a list of channels, one at a time."""
122+ try :
123+ car , cdr = channels [0 ], channels [1 :]
124+ except (IndexError , TypeError ):
125+ logger .exception ('Failed to find channel to join.' )
126+ else :
127+ logger .info ('Joining %s' , car )
128+ self .connection .join (car )
129+ if cdr :
130+ self .reactor .scheduler .execute_after (
131+ 1 , functools .partial (self .join_channels , cdr ))
132+
133+
134+ class FreenodePasswdAuth (JoinChannels ):
116135 """Authenticate with NickServ before joining channels."""
117136 def __init__ (
118137 self , server_list , nickname , realname ,
119- ident_password , channels , ** kwargs ):
138+ ident_password , channels = [] , ** kwargs ):
120139 """
121140 :param server_list: List of servers the bot will use.
122141 :param nickname: The bot's nickname
@@ -178,8 +197,7 @@ def _handle_privnotice(self, conn, event):
178197 self ._identify_to_nickserv ()
179198 elif 'You are now identified' in msg :
180199 logger .debug ('Authentication succeeded' )
181- self .reactor .scheduler .execute_after (
182- 1 , self ._join_next_channel )
200+ self .join_channels (self ._channels )
183201 elif 'Invalid password' in msg :
184202 logger .error ('Password invalid. Check your config!' )
185203 self .die ()
@@ -190,21 +208,6 @@ def _identify_to_nickserv(self):
190208 self .connection .privmsg ('NickServ' , 'identify %s %s' % (
191209 self ._primary_nick , self ._ident_password ))
192210
193- def _join_next_channel (self , channels = None ):
194- """Join the next channel in our join list."""
195- if channels is None :
196- channels = self ._channels
197- try :
198- car , cdr = channels [0 ], channels [1 :]
199- except (IndexError , TypeError ):
200- logger .exception ('Failed to find channel to join.' )
201- else :
202- logger .info ('Joining %s' , car )
203- self .connection .join (car )
204- if cdr :
205- self .reactor .scheduler .execute_after (
206- 1 , functools .partial (self ._join_next_channel , cdr ))
207-
208211 def _nickserv_regain (self ):
209212 if not self .has_primary_nick ():
210213 # REGAIN disconnects an old user session, or somebody
@@ -218,3 +221,79 @@ def _nickserv_regain(self):
218221 def has_primary_nick (self ):
219222 """Do we currently have our primary nick?"""
220223 return self .connection .get_nickname () == self ._primary_nick
224+
225+
226+ class SASL (JoinChannels ):
227+ def __init__ (
228+ self , server_list , nickname , realname ,
229+ ident_password , channels = [], username = None , ** kwargs ):
230+ """
231+ :param server_list: List of servers the bot will use.
232+ :param nickname: The bot's nickname
233+ :param realname: The bot's realname
234+ :param ident_password: The bot's password
235+ :param channels: List of channels to join after authenticating
236+ """
237+ self ._primary_nick = nickname
238+ self ._username = username or nickname
239+ self ._ident_password = ident_password
240+ self ._channels = channels
241+
242+ super (SASL , self ).__init__ (
243+ server_list = server_list ,
244+ nickname = nickname ,
245+ realname = realname ,
246+ username = self ._username ,
247+ ** kwargs )
248+ self .reactor ._on_connect = self ._handle_connect
249+
250+ for event in ['cap' , 'authenticate' , '903' , '908' , 'welcome' ]:
251+ logger .debug ('Registering for %s' , event )
252+ self .connection .add_global_handler (
253+ event , getattr (self , '_handle_%s' % event ))
254+
255+ def _handle_connect (self , sock ):
256+ """Send CAP REQ :sasl on connect."""
257+ self .connection .cap ('REQ' , 'sasl' )
258+
259+ def _handle_cap (self , conn , event ):
260+ """Handle CAP responses."""
261+ if event .arguments and event .arguments [0 ] == 'ACK' :
262+ conn .send_raw ('AUTHENTICATE PLAIN' )
263+ else :
264+ logger .warning ('Unexpcted CAP response: %s' , event )
265+ conn .disconnect ()
266+
267+ def _handle_authenticate (self , conn , event ):
268+ """Handle AUTHENTICATE responses."""
269+ if event .target == '+' :
270+ creds = '{username}\0 {username}\0 {password}' .format (
271+ username = self ._username ,
272+ password = self ._ident_password )
273+ conn .send_raw ('AUTHENTICATE {}' .format (
274+ base64 .b64encode (creds .encode ('utf8' )).decode ('utf8' )))
275+ else :
276+ logger .warning ('Unexpcted AUTHENTICATE response: %s' , event )
277+ conn .disconnect ()
278+
279+ def _handle_903 (self , conn , event ):
280+ """Handle 903 RPL_SASLSUCCESS responses."""
281+ self .connection .cap ('END' )
282+
283+ def _handle_908 (self , conn , event ):
284+ """Handle 908 RPL_SASLMECHS responses."""
285+ logger .warning ('SASL PLAIN not supported: %s' , event )
286+ self .die ()
287+
288+ def _handle_welcome (self , conn , event ):
289+ """Handle WELCOME message."""
290+ logger .info ('Connected to server %s' , conn .get_server_name ())
291+ self .join_channels (self ._channels )
292+
293+
294+ class SSL (object ):
295+ """Use SSL connections."""
296+ def __init__ (self , * args , ** kwargs ):
297+ kwargs ['connect_factory' ] = irc .connection .Factory (
298+ wrapper = ssl .wrap_socket )
299+ super (SSL , self ).__init__ (* args , ** kwargs )
0 commit comments