2005-01-19 14:14:38 +01:00
###
2005-01-19 14:33:05 +01:00
# Copyright (c) 2002-2005 Jeremiah Fincher
2005-01-19 14:14:38 +01:00
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
2009-03-12 20:04:22 +01:00
import re
2005-01-19 14:14:38 +01:00
import copy
import time
2019-10-25 23:17:10 +02:00
import enum
2005-01-19 14:14:38 +01:00
import random
2011-09-13 11:07:52 +02:00
import base64
2019-10-25 23:07:31 +02:00
import textwrap
2020-01-23 14:25:10 +01:00
import warnings
2015-12-04 01:37:23 +01:00
import collections
2005-01-19 14:14:38 +01:00
2014-12-27 18:39:38 +01:00
try :
2019-12-26 12:14:05 +01:00
class crypto :
import cryptography
from cryptography . hazmat . primitives . serialization \
import load_pem_private_key
from cryptography . hazmat . backends import default_backend
from cryptography . hazmat . primitives . asymmetric . ec import ECDSA
from cryptography . hazmat . primitives . asymmetric . utils import Prehashed
from cryptography . hazmat . primitives . hashes import SHA256
2014-12-27 18:39:38 +01:00
except ImportError :
2019-12-26 12:14:05 +01:00
crypto = None
2016-09-10 20:16:28 +02:00
try :
import pyxmpp2_scram as scram
except ImportError :
scram = None
2014-12-27 18:39:38 +01:00
2012-09-18 04:12:11 +02:00
from . import conf , ircdb , ircmsgs , ircutils , log , utils , world
2019-12-07 23:33:04 +01:00
from . drivers import Server
2012-09-18 04:12:11 +02:00
from . utils . str import rsplit
2015-03-03 09:02:29 +01:00
from . utils . iter import chain
2020-05-10 08:42:25 +02:00
from . utils . structures import smallqueue , RingBuffer , ExpiringDict
2005-01-19 14:14:38 +01:00
2020-05-07 18:30:07 +02:00
MAX_LINE_SIZE = 512 # Including \r\n, but excluding server_tags
2019-10-25 22:31:02 +02:00
2005-01-19 14:14:38 +01:00
###
# The base class for a callback to be registered with an Irc object. Shows
# the required interface for callbacks -- name(),
2009-08-16 23:17:05 +02:00
# inFilter(irc, msg), outFilter(irc, msg), and __call__(irc, msg) [used so as
# to make functions used as callbacks conceivable, and so if refactoring ever
2005-01-19 14:14:38 +01:00
# changes the nature of the callbacks from classes to functions, syntactical
2009-08-16 23:17:05 +02:00
# changes elsewhere won't be required.]
2005-01-19 14:14:38 +01:00
###
class IrcCommandDispatcher ( object ) :
""" Base class for classes that must dispatch on a command. """
2020-01-23 14:25:10 +01:00
def dispatchCommand ( self , command , args = None ) :
2005-01-19 14:14:38 +01:00
""" Given a string ' command ' , dispatches to doCommand. """
2020-01-23 14:25:10 +01:00
if args is None :
warnings . warn (
" dispatchCommand now takes an ' args ' attribute, which is "
" a list of the command ' s arguments (ie. IrcMsg.args). " ,
DeprecationWarning )
args = [ ]
command = command . upper ( )
subcommand = None
method = None
# Dispatch on command + subcommand, if there is a subcommand, and
# a method with the matching name exists
if command in ( ' FAIL ' , ' WARN ' , ' NOTE ' ) and len ( args ) > = 1 :
subcommand = args [ 0 ]
elif command in ( ' CAP ' , ) and len ( args ) > = 2 :
# Note: this only covers the server-to-client format
subcommand = args [ 1 ]
command = command . capitalize ( )
if subcommand is not None :
subcommand = subcommand . capitalize ( )
method = getattr ( self , ' do ' + command + subcommand , None )
# If not dispatched on command + subcommand, then dispatch on command
if method is None :
method = getattr ( self , ' do ' + command , None )
return method
2005-01-19 14:14:38 +01:00
2015-08-11 16:50:23 +02:00
class IrcCallback ( IrcCommandDispatcher , log . Firewalled ) :
2005-01-19 14:14:38 +01:00
""" Base class for standard callbacks.
Callbacks derived from this class should have methods of the form
" doCommand " - - doPrivmsg , doNick , do433 , etc . These will be called
on matching messages .
"""
callAfter = ( )
callBefore = ( )
2020-08-23 16:27:22 +02:00
echoMessage = False
echo_message = False # deprecated alias of echoMessage
2005-01-19 14:14:38 +01:00
__firewalled__ = { ' die ' : None ,
' reset ' : None ,
' __call__ ' : None ,
' inFilter ' : lambda self , irc , msg : msg ,
' outFilter ' : lambda self , irc , msg : msg ,
2020-05-01 20:19:53 +02:00
' postTransition ' : None ,
2005-05-15 18:50:10 +02:00
' name ' : lambda self : self . __class__ . __name__ ,
' callPrecedence ' : lambda self , irc : ( [ ] , [ ] ) ,
}
2005-01-19 14:14:38 +01:00
2005-02-28 08:59:46 +01:00
def __init__ ( self , * args , * * kwargs ) :
2008-07-18 16:49:51 +02:00
#object doesn't take any args, so the buck stops here.
#super(IrcCallback, self).__init__(*args, **kwargs)
pass
2005-06-29 16:20:54 +02:00
2005-01-19 14:14:38 +01:00
def __repr__ ( self ) :
2005-08-15 07:37:05 +02:00
return ' < %s %s %s > ' % \
( self . __class__ . __name__ , self . name ( ) , object . __repr__ ( self ) )
2005-01-19 14:14:38 +01:00
def name ( self ) :
""" Returns the name of the callback. """
return self . __class__ . __name__
def callPrecedence ( self , irc ) :
""" Returns a pair of (callbacks to call before me,
callbacks to call after me ) """
after = [ ]
before = [ ]
for name in self . callBefore :
cb = irc . getCallback ( name )
if cb is not None :
after . append ( cb )
for name in self . callAfter :
cb = irc . getCallback ( name )
if cb is not None :
before . append ( cb )
assert self not in after , ' %s was in its own after. ' % self . name ( )
assert self not in before , ' %s was in its own before. ' % self . name ( )
return ( before , after )
2005-06-29 16:20:54 +02:00
2005-01-19 14:14:38 +01:00
def inFilter ( self , irc , msg ) :
""" Used for filtering/modifying messages as they ' re entering.
ircmsgs . IrcMsg objects are immutable , so this method is expected to
return another ircmsgs . IrcMsg object . Obviously the same IrcMsg
can be returned .
"""
return msg
def outFilter ( self , irc , msg ) :
""" Used for filtering/modifying messages as they ' re leaving.
As with inFilter , an IrcMsg is returned .
"""
return msg
2020-05-01 20:19:53 +02:00
def postTransition ( self , irc , msg , from_state , to_state ) :
""" Called when the state of the IRC connection changes.
` msg ` is the message that triggered the transition , if any . """
pass
2005-01-19 14:14:38 +01:00
def __call__ ( self , irc , msg ) :
""" Used for handling each message. """
2020-08-23 16:27:22 +02:00
if not self . echoMessage and not self . echo_message \
2020-05-07 21:00:16 +02:00
and msg . command in ( ' PRIVMSG ' , ' NOTICE ' , ' TAGMSG ' ) \
and ( ' label ' in msg . server_tags
2020-11-04 11:32:09 +01:00
or msg . tagged ( ' emulatedEcho ' ) ) :
2020-05-07 21:00:16 +02:00
# This is an echo of a message we sent; and the plugin didn't
# opt-in to receiving echos; ignoring it.
# `'label' in msg.server_tags` detects echos when labeled-response
2020-11-04 11:32:09 +01:00
# is enabled; and `msg.tag('emulatedEcho')` detects simulated
2020-05-07 21:00:16 +02:00
# echos. As we don't enable real echo-message unless
# labeled-response is enabled; this is an exhaustive check of echos
# in all cases.
# See "When a client sends a private message to its own nick" at
# <https://ircv3.net/specs/extensions/labeled-response>
return
2020-01-23 14:25:10 +01:00
method = self . dispatchCommand ( msg . command , msg . args )
2005-01-19 14:14:38 +01:00
if method is not None :
method ( irc , msg )
def reset ( self ) :
""" Resets the callback. Called when reconnecting to the server. """
pass
def die ( self ) :
""" Makes the callback die. Called when the parent Irc object dies. """
pass
###
# Basic queue for IRC messages. It doesn't presently (but should at some
# later point) reorder messages based on priority or penalty calculations.
###
2017-08-03 22:01:49 +02:00
_high = frozenset ( [ ' MODE ' , ' KICK ' , ' PONG ' , ' NICK ' , ' PASS ' , ' CAPAB ' , ' REMOVE ' ] )
2005-04-14 03:35:35 +02:00
_low = frozenset ( [ ' PRIVMSG ' , ' PING ' , ' WHO ' , ' NOTICE ' , ' JOIN ' ] )
2005-01-19 14:14:38 +01:00
class IrcMsgQueue ( object ) :
""" Class for a queue of IrcMsgs. Eventually, it should be smart.
Probably smarter than it is now , though it ' s gotten quite a bit smarter
than it originally was . A method to " score " methods , and a heapq to
maintain a priority queue of the messages would be the ideal way to do
intelligent queuing .
2009-08-16 23:17:05 +02:00
As it stands , however , we simply keep track of ' high priority ' messages ,
2005-01-19 14:14:38 +01:00
' low priority ' messages , and normal messages , and just make sure to return
the ' high priority ' ones before the normal ones before the ' low priority '
ones .
"""
2005-04-14 03:35:35 +02:00
__slots__ = ( ' msgs ' , ' highpriority ' , ' normal ' , ' lowpriority ' , ' lastJoin ' )
2005-01-19 14:14:38 +01:00
def __init__ ( self , iterable = ( ) ) :
self . reset ( )
for msg in iterable :
self . enqueue ( msg )
def reset ( self ) :
""" Clears the queue. """
2005-04-14 03:35:35 +02:00
self . lastJoin = 0
2005-01-19 14:14:38 +01:00
self . highpriority = smallqueue ( )
self . normal = smallqueue ( )
self . lowpriority = smallqueue ( )
def enqueue ( self , msg ) :
""" Enqueues a given message. """
2005-02-09 06:30:14 +01:00
if msg in self and \
2005-04-14 02:56:26 +02:00
conf . supybot . protocols . irc . queuing . duplicates ( ) :
2005-01-19 14:14:38 +01:00
s = str ( msg ) . strip ( )
2005-02-01 14:43:57 +01:00
log . info ( ' Not adding message % q to queue, already added. ' , s )
2005-01-19 14:14:38 +01:00
return False
else :
if msg . command in _high :
self . highpriority . enqueue ( msg )
elif msg . command in _low :
self . lowpriority . enqueue ( msg )
else :
self . normal . enqueue ( msg )
return True
def dequeue ( self ) :
""" Dequeues a given message. """
msg = None
if self . highpriority :
msg = self . highpriority . dequeue ( )
elif self . normal :
msg = self . normal . dequeue ( )
elif self . lowpriority :
msg = self . lowpriority . dequeue ( )
2005-04-14 03:35:35 +02:00
if msg . command == ' JOIN ' :
limit = conf . supybot . protocols . irc . queuing . rateLimit . join ( )
now = time . time ( )
if self . lastJoin + limit < = now :
self . lastJoin = now
else :
self . lowpriority . enqueue ( msg )
msg = None
2005-01-19 14:14:38 +01:00
return msg
2005-02-09 06:30:14 +01:00
def __contains__ ( self , msg ) :
return msg in self . normal or \
msg in self . lowpriority or \
msg in self . highpriority
2014-01-20 15:19:06 +01:00
def __bool__ ( self ) :
2005-01-19 14:14:38 +01:00
return bool ( self . highpriority or self . normal or self . lowpriority )
2014-01-20 15:19:06 +01:00
__nonzero__ = __bool__
2005-01-19 14:14:38 +01:00
def __len__ ( self ) :
2005-02-09 06:30:14 +01:00
return len ( self . highpriority ) + len ( self . lowpriority ) + len ( self . normal )
2005-01-19 14:14:38 +01:00
def __repr__ ( self ) :
name = self . __class__ . __name__
return ' %s ( %r ) ' % ( name , list ( chain ( self . highpriority ,
self . normal ,
self . lowpriority ) ) )
__str__ = __repr__
###
# Maintains the state of IRC connection -- the most recent messages, the
# status of various modes (especially ops/halfops/voices) in channels, etc.
###
2006-01-12 04:57:10 +01:00
class ChannelState ( utils . python . Object ) :
2020-09-27 15:24:27 +02:00
""" Represents the known state of an IRC channel.
. . attribute : : topic
The topic of a channel ( possibly the empty stringà
: type : str
. . attribute : : created
Timestamp of the channel creation , according to the server .
: type : int
. . attribute : : ops
Set of the nicks of all the operators of the channel .
: type : ircutils . IrcSet [ str ]
. . attribute : : halfops
Set of the nicks of all the half - operators of the channel .
: type : ircutils . IrcSet [ str ]
. . attribute : : voices
Set of the nicks of all the voiced users of the channel .
: type : ircutils . IrcSet [ str ]
. . attribute : : users
Set of the nicks of all the users in the channel .
: type : ircutils . IrcSet [ str ]
. . attribute : : bans
Set of the all the banmasks set in the channel .
: type : ircutils . IrcSet [ str ]
. . attribute : : modes
Dict of all the modes set in the channel , with they value , if any .
This excludes the following modes : ovhbeq
: type : Dict [ str , Optional [ str ] ]
"""
2005-01-19 14:14:38 +01:00
__slots__ = ( ' users ' , ' ops ' , ' halfops ' , ' bans ' ,
' voices ' , ' topic ' , ' modes ' , ' created ' )
def __init__ ( self ) :
self . topic = ' '
self . created = 0
self . ops = ircutils . IrcSet ( )
self . bans = ircutils . IrcSet ( )
self . users = ircutils . IrcSet ( )
self . voices = ircutils . IrcSet ( )
self . halfops = ircutils . IrcSet ( )
2012-06-06 12:00:48 +02:00
self . modes = { }
2005-01-19 14:14:38 +01:00
def isOp ( self , nick ) :
2020-09-27 15:24:27 +02:00
""" Returns whether the given nick is an op. """
2005-01-19 14:14:38 +01:00
return nick in self . ops
2020-09-27 15:24:27 +02:00
2013-05-17 17:47:43 +02:00
def isOpPlus ( self , nick ) :
2020-09-27 15:24:27 +02:00
""" Returns whether the given nick is an op. """
2013-05-17 17:47:43 +02:00
return nick in self . ops
2020-09-27 15:24:27 +02:00
2005-01-19 14:14:38 +01:00
def isVoice ( self , nick ) :
2020-09-27 15:24:27 +02:00
""" Returns whether the given nick is voiced. """
2005-01-19 14:14:38 +01:00
return nick in self . voices
2020-09-27 15:24:27 +02:00
2013-05-17 17:47:43 +02:00
def isVoicePlus ( self , nick ) :
2020-09-27 15:24:27 +02:00
""" Returns whether the given nick is voiced, an halfop, or an op. """
2013-05-17 17:47:43 +02:00
return nick in self . voices or nick in self . halfops or nick in self . ops
2020-09-27 15:24:27 +02:00
2005-01-19 14:14:38 +01:00
def isHalfop ( self , nick ) :
2020-09-27 15:24:27 +02:00
""" Returns whether the given nick is an halfop. """
2005-01-19 14:14:38 +01:00
return nick in self . halfops
2020-09-27 15:24:27 +02:00
2013-05-17 17:47:43 +02:00
def isHalfopPlus ( self , nick ) :
2020-09-27 15:24:27 +02:00
""" Returns whether the given nick is an halfop, or an op. """
2013-05-17 17:47:43 +02:00
return nick in self . halfops or nick in self . ops
2005-06-29 16:20:54 +02:00
2005-01-19 14:14:38 +01:00
def addUser ( self , user ) :
" Adds a given user to the ChannelState. Power prefixes are handled. "
2009-10-24 01:20:02 +02:00
nick = user . lstrip ( ' @ % +&~! ' )
2005-01-19 14:14:38 +01:00
if not nick :
return
2005-11-30 17:02:09 +01:00
# & is used to denote protected users in UnrealIRCd
# ~ is used to denote channel owner in UnrealIRCd
2009-10-24 01:20:02 +02:00
# ! is used to denote protected users in UltimateIRCd
while user and user [ 0 ] in ' @ % +&~! ' :
2005-01-19 14:14:38 +01:00
( marker , user ) = ( user [ 0 ] , user [ 1 : ] )
assert user , ' Looks like my caller is passing chars, not nicks. '
2009-10-24 01:20:02 +02:00
if marker in ' @&~! ' :
2005-01-19 14:14:38 +01:00
self . ops . add ( nick )
elif marker == ' % ' :
self . halfops . add ( nick )
elif marker == ' + ' :
self . voices . add ( nick )
self . users . add ( nick )
def replaceUser ( self , oldNick , newNick ) :
""" Changes the user oldNick to newNick; used for NICK changes. """
# Note that this doesn't have to have the sigil (@%+) that users
# have to have for addUser; it just changes the name of the user
2013-03-08 20:34:31 +01:00
# without changing any of their categories.
2005-01-19 14:14:38 +01:00
for s in ( self . users , self . ops , self . halfops , self . voices ) :
if oldNick in s :
s . remove ( oldNick )
s . add ( newNick )
def removeUser ( self , user ) :
""" Removes a given user from the channel. """
self . users . discard ( user )
self . ops . discard ( user )
self . halfops . discard ( user )
self . voices . discard ( user )
def setMode ( self , mode , value = None ) :
assert mode not in ' ovhbeq '
self . modes [ mode ] = value
def unsetMode ( self , mode ) :
assert mode not in ' ovhbeq '
if mode in self . modes :
del self . modes [ mode ]
def doMode ( self , msg ) :
def getSet ( c ) :
if c == ' o ' :
2005-07-10 20:27:40 +02:00
Set = self . ops
2005-01-19 14:14:38 +01:00
elif c == ' v ' :
2005-07-10 20:27:40 +02:00
Set = self . voices
2005-01-19 14:14:38 +01:00
elif c == ' h ' :
2005-07-10 20:27:40 +02:00
Set = self . halfops
2005-01-19 14:14:38 +01:00
elif c == ' b ' :
2005-07-10 20:27:40 +02:00
Set = self . bans
2005-01-19 14:14:38 +01:00
else : # We don't care yet, so we'll just return an empty set.
2005-07-10 20:27:40 +02:00
Set = set ( )
return Set
2005-01-19 14:14:38 +01:00
for ( mode , value ) in ircutils . separateModes ( msg . args [ 1 : ] ) :
( action , modeChar ) = mode
if modeChar in ' ovhbeq ' : # We don't handle e or q yet.
2005-07-10 20:27:40 +02:00
Set = getSet ( modeChar )
2005-01-19 14:14:38 +01:00
if action == ' - ' :
2005-07-10 20:27:40 +02:00
Set . discard ( value )
2005-01-19 14:14:38 +01:00
elif action == ' + ' :
2005-07-10 20:27:40 +02:00
Set . add ( value )
2005-01-19 14:14:38 +01:00
else :
if action == ' + ' :
self . setMode ( modeChar , value )
else :
assert action == ' - '
self . unsetMode ( modeChar )
def __getstate__ ( self ) :
return [ getattr ( self , name ) for name in self . __slots__ ]
def __setstate__ ( self , t ) :
for ( name , value ) in zip ( self . __slots__ , t ) :
setattr ( self , name , value )
def __eq__ ( self , other ) :
ret = True
for name in self . __slots__ :
ret = ret and getattr ( self , name ) == getattr ( other , name )
return ret
2020-09-27 15:24:27 +02:00
2021-03-03 23:32:00 +01:00
Batch = collections . namedtuple ( ' Batch ' , ' name type arguments messages parent_batch ' )
2020-09-27 15:24:27 +02:00
""" Represents a batch of messages, see
2021-03-03 23:32:00 +01:00
< https : / / ircv3 . net / specs / extensions / batch - 3.2 >
Only access attributes by their name and do not create Batch objects
in plugins ; so we can extend the structure without breaking plugins . """
2020-09-27 15:24:27 +02:00
2005-01-19 14:14:38 +01:00
2019-10-25 23:17:10 +02:00
class IrcStateFsm ( object ) :
''' Finite State Machine keeping track of what part of the connection
initialization we are in . '''
__slots__ = ( ' state ' , )
@enum.unique
class States ( enum . Enum ) :
2020-09-27 15:24:27 +02:00
""" Enumeration of all the states of an IRC connection. """
2019-10-25 23:17:10 +02:00
UNINITIALIZED = 10
''' Nothing received yet (except server notices) '''
INIT_CAP_NEGOTIATION = 20
''' Sent CAP LS, did not send CAP END yet '''
INIT_SASL = 30
''' In an AUTHENTICATE session '''
INIT_WAITING_MOTD = 50
''' Waiting for start of MOTD '''
INIT_MOTD = 60
''' Waiting for end of MOTD '''
CONNECTED = 70
''' Normal state of the connections '''
CONNECTED_SASL = 80
''' Doing SASL authentication in the middle of a connection. '''
2019-12-08 21:25:59 +01:00
SHUTTING_DOWN = 100
2019-10-25 23:17:10 +02:00
def __init__ ( self ) :
self . reset ( )
def reset ( self ) :
2019-12-08 21:25:59 +01:00
if getattr ( self , ' state ' , None ) is not None :
log . debug ( ' resetting from %s to %s ' ,
self . state , self . States . UNINITIALIZED )
2019-10-25 23:17:10 +02:00
self . state = self . States . UNINITIALIZED
2020-05-01 20:19:53 +02:00
def _transition ( self , irc , msg , to_state , expected_from = None ) :
""" Transitions to state `to_state`.
If ` expected_from ` is not ` None ` , first checks the current state is
in the set .
After the transition , calls the
` postTransition ( irc , msg , from_state , to_state ) ` method of all objects
in ` irc . callbacks ` .
` msg ` may be None if the transition isn ' t triggered by a message, but
` irc ` may not . """
from_state = self . state
if expected_from is None or from_state in expected_from :
2019-10-25 23:17:10 +02:00
log . debug ( ' transition from %s to %s ' , self . state , to_state )
self . state = to_state
2020-05-01 20:19:53 +02:00
for callback in reversed ( irc . callbacks ) :
msg = callback . postTransition ( irc , msg , from_state , to_state )
2019-10-25 23:17:10 +02:00
else :
raise ValueError ( ' unexpected transition to %s while in state %s ' %
( to_state , self . state ) )
def expect_state ( self , expected_states ) :
if self . state not in expected_states :
raise ValueError ( ( ' Connection in state %s , but expected to be '
' in state %s ' ) % ( self . state , expected_states ) )
2020-05-01 20:19:53 +02:00
def on_init_messages_sent ( self , irc ) :
2019-10-25 23:17:10 +02:00
''' As soon as USER/NICK/CAP LS are sent '''
2020-05-01 20:19:53 +02:00
self . _transition ( irc , None , self . States . INIT_CAP_NEGOTIATION , [
2019-10-25 23:17:10 +02:00
self . States . UNINITIALIZED ,
] )
2020-05-01 20:19:53 +02:00
def on_sasl_cap ( self , irc , msg ) :
2019-10-25 23:17:10 +02:00
''' Whenever we see the ' sasl ' capability in a CAP LS response '''
if self . state == self . States . INIT_CAP_NEGOTIATION :
2020-05-01 20:19:53 +02:00
self . _transition ( irc , msg , self . States . INIT_SASL )
2019-10-25 23:17:10 +02:00
elif self . state == self . States . CONNECTED :
2020-05-01 20:19:53 +02:00
self . _transition ( irc , msg , self . States . CONNECTED_SASL )
2019-10-25 23:17:10 +02:00
else :
raise ValueError ( ' Got sasl cap while in state %s ' % self . state )
2020-05-01 20:19:53 +02:00
def on_sasl_auth_finished ( self , irc , msg ) :
2019-10-25 23:17:10 +02:00
''' When sasl auth either succeeded or failed. '''
if self . state == self . States . INIT_SASL :
2020-05-01 20:19:53 +02:00
self . _transition ( irc , msg , self . States . INIT_CAP_NEGOTIATION )
2019-10-25 23:17:10 +02:00
elif self . state == self . States . CONNECTED_SASL :
2020-05-01 20:19:53 +02:00
self . _transition ( irc , msg , self . States . CONNECTED )
2019-10-25 23:17:10 +02:00
else :
raise ValueError ( ' Finished SASL auth while in state %s ' % self . state )
2020-05-01 20:19:53 +02:00
def on_cap_end ( self , irc , msg ) :
2019-10-25 23:17:10 +02:00
''' When we send CAP END '''
2020-05-01 20:19:53 +02:00
self . _transition ( irc , msg , self . States . INIT_WAITING_MOTD , [
2019-10-25 23:17:10 +02:00
self . States . INIT_CAP_NEGOTIATION ,
] )
2020-05-01 20:19:53 +02:00
def on_start_motd ( self , irc , msg ) :
2019-10-25 23:17:10 +02:00
''' On 375 (RPL_MOTDSTART) '''
2020-05-01 20:19:53 +02:00
self . _transition ( irc , msg , self . States . INIT_MOTD , [
2019-10-25 23:17:10 +02:00
self . States . INIT_CAP_NEGOTIATION ,
self . States . INIT_WAITING_MOTD ,
2020-09-19 17:08:52 +02:00
self . States . CONNECTED ,
self . States . CONNECTED_SASL ,
2019-10-25 23:17:10 +02:00
] )
2020-05-01 20:19:53 +02:00
def on_end_motd ( self , irc , msg ) :
2019-10-25 23:17:10 +02:00
''' On 376 (RPL_ENDOFMOTD) or 422 (ERR_NOMOTD) '''
2020-05-01 20:19:53 +02:00
self . _transition ( irc , msg , self . States . CONNECTED , [
2019-10-25 23:17:10 +02:00
self . States . INIT_CAP_NEGOTIATION ,
self . States . INIT_WAITING_MOTD ,
2020-09-19 17:08:52 +02:00
self . States . INIT_MOTD ,
self . States . CONNECTED ,
self . States . CONNECTED_SASL ,
2019-10-25 23:17:10 +02:00
] )
2020-05-01 20:19:53 +02:00
def on_shutdown ( self , irc , msg ) :
self . _transition ( irc , msg , self . States . SHUTTING_DOWN )
2019-12-08 21:25:59 +01:00
2015-08-11 16:50:23 +02:00
class IrcState ( IrcCommandDispatcher , log . Firewalled ) :
2005-01-19 14:14:38 +01:00
""" Maintains state of the Irc connection. Should also become smarter.
2020-09-27 15:24:27 +02:00
. . attribute : : fsm
A finite - state machine representing the current state of the IRC
connection : various steps while connecting , then remains in the
CONNECTED state ( or CONNECTED_SASL when doing SASL in the middle of a
connection ) .
: type : IrcStateFsm
. . attribute : : capabilities_req
Set of all capabilities requested from the server .
See < https : / / ircv3 . net / specs / core / capability - negotiation >
: type : Set [ str ]
. . attribute : : capabilities_acq
Set of all capabilities requested from and acknowledged by the
server . See < https : / / ircv3 . net / specs / core / capability - negotiation >
: type : Set [ str ]
. . attribute : : capabilities_nak
Set of all capabilities requested from and refused by the server .
This should always be empty unless the bot , a plugin , or the server is
misbehaving . See < https : / / ircv3 . net / specs / core / capability - negotiation >
: type : Set [ str ]
. . attribute : : capabilities_ls
Stores all the capabilities advertised by the server , as well as their
value , if any .
: type : Dict [ str , Optional [ str ] ]
. . attribute : : ircd
Identification string of the software running the server we are
connected to . See
< https : / / defs . ircdocs . horse / defs / numerics . html #rpl-myinfo-004>
: type : str
. . attribute : : supported
Stores the value of ISUPPORT sent when connecting .
See < https : / / defs . ircdocs . horse / defs / isupport . html > for the list of
keys .
: type : utils . InsensitivePreservingDict [ str , Any ]
. . attribute : : history
History of messages received from the network . Automatically discards
messages so it doesn ' t exceed
` ` supybot . protocols . irc . maxHistoryLength ` ` .
: type : RingBuffer [ ircmsgs . IrcMsg ]
. . attribute : : channels
Store channel states .
: type : ircutils . IrcDict [ str , ChannelState ]
. . attribute : : nickToHostmask
Stores the last hostmask of a seen nick .
: type : ircutils . IrcDict [ str , str ]
2005-01-19 14:14:38 +01:00
"""
__firewalled__ = { ' addMsg ' : None }
2020-09-27 15:24:27 +02:00
2005-01-19 14:14:38 +01:00
def __init__ ( self , history = None , supported = None ,
2015-05-15 13:01:26 +02:00
nicksToHostmasks = None , channels = None ,
2019-10-25 23:17:10 +02:00
capabilities_req = None ,
2015-05-24 12:25:42 +02:00
capabilities_ack = None , capabilities_nak = None ,
capabilities_ls = None ) :
2019-10-25 23:17:10 +02:00
self . fsm = IrcStateFsm ( )
2005-01-19 14:14:38 +01:00
if history is None :
history = RingBuffer ( conf . supybot . protocols . irc . maxHistoryLength ( ) )
if supported is None :
supported = utils . InsensitivePreservingDict ( )
if nicksToHostmasks is None :
nicksToHostmasks = ircutils . IrcDict ( )
if channels is None :
channels = ircutils . IrcDict ( )
2019-10-25 23:17:10 +02:00
self . capabilities_req = capabilities_req or set ( )
2015-05-15 13:01:26 +02:00
self . capabilities_ack = capabilities_ack or set ( )
self . capabilities_nak = capabilities_nak or set ( )
2015-05-24 12:25:42 +02:00
self . capabilities_ls = capabilities_ls or { }
2013-03-31 21:22:59 +02:00
self . ircd = None
2005-01-19 14:14:38 +01:00
self . supported = supported
self . history = history
self . channels = channels
self . nicksToHostmasks = nicksToHostmasks
2020-05-06 20:39:21 +02:00
# Batches should always finish and be way shorter than 3600s, but
# let's just make sure to avoid leaking memory.
2020-05-10 08:42:25 +02:00
self . batches = ExpiringDict ( timeout = 3600 )
2005-01-19 14:14:38 +01:00
def reset ( self ) :
""" Resets the state to normal, unconnected state. """
2019-10-25 23:17:10 +02:00
self . fsm . reset ( )
2005-01-19 14:14:38 +01:00
self . history . reset ( )
2019-10-25 22:40:51 +02:00
self . history . resize ( conf . supybot . protocols . irc . maxHistoryLength ( ) )
self . ircd = None
2005-01-19 14:14:38 +01:00
self . channels . clear ( )
self . supported . clear ( )
self . nicksToHostmasks . clear ( )
2020-05-06 20:39:21 +02:00
self . batches . clear ( )
2019-10-25 23:17:10 +02:00
self . capabilities_req = set ( )
2019-10-25 22:40:51 +02:00
self . capabilities_ack = set ( )
self . capabilities_nak = set ( )
2019-11-09 18:27:07 +01:00
self . capabilities_ls = { }
2005-01-19 14:14:38 +01:00
def __reduce__ ( self ) :
return ( self . __class__ , ( self . history , self . supported ,
self . nicksToHostmasks , self . channels ) )
2005-06-29 16:20:54 +02:00
2005-01-19 14:14:38 +01:00
def __eq__ ( self , other ) :
return self . history == other . history and \
self . channels == other . channels and \
self . supported == other . supported and \
2015-12-04 01:37:23 +01:00
self . nicksToHostmasks == other . nicksToHostmasks and \
self . batches == other . batches
2005-01-19 14:14:38 +01:00
def __ne__ ( self , other ) :
return not self == other
def copy ( self ) :
ret = self . __class__ ( )
ret . history = copy . deepcopy ( self . history )
ret . nicksToHostmasks = copy . deepcopy ( self . nicksToHostmasks )
ret . channels = copy . deepcopy ( self . channels )
2015-12-04 01:37:23 +01:00
ret . batches = copy . deepcopy ( self . batches )
2005-01-19 14:14:38 +01:00
return ret
def addMsg ( self , irc , msg ) :
""" Updates the state based on the irc object and the message. """
self . history . append ( msg )
if ircutils . isUserHostmask ( msg . prefix ) and not msg . command == ' NICK ' :
self . nicksToHostmasks [ msg . nick ] = msg . prefix
2015-12-04 01:37:23 +01:00
if ' batch ' in msg . server_tags :
2021-03-03 23:32:00 +01:00
batch_name = msg . server_tags [ ' batch ' ]
assert batch_name in self . batches , \
' Server references undeclared batch %r ' % batch_name
for batch in self . getParentBatches ( msg ) :
batch . messages . append ( msg )
2020-01-23 14:25:10 +01:00
method = self . dispatchCommand ( msg . command , msg . args )
2005-01-19 14:14:38 +01:00
if method is not None :
method ( irc , msg )
def getTopic ( self , channel ) :
""" Returns the topic for a given channel. """
return self . channels [ channel ] . topic
def nickToHostmask ( self , nick ) :
""" Returns the hostmask for a given nick. """
return self . nicksToHostmasks [ nick ]
2021-03-03 23:32:00 +01:00
def getParentBatches ( self , msg ) :
""" Given an IrcMsg, returns a list of all batches that contain it,
innermost first .
Raises ValueError if ` ` msg ` ` is not in a batch ;
or if it is in a batch that has already ended .
This restriction may be relaxed in the future .
This means that you should not call ` ` getParentBatches ` `
on a message that was already processed .
For example , assume Limnoria received the following : :
: irc . host BATCH + outer example . com / foo
@batch = outer : irc . host BATCH + inner example . com / bar
@batch = inner : nick ! user @host PRIVMSG #channel :Hi
@batch = outer : irc . host BATCH - inner
: irc . host BATCH - outer
If you call getParentBatches on any of the middle three messages ,
you get ` ` [ Batch ( name = ' inner ' , . . . ) , Batch ( name = ' outer ' , . . . ) ] ` ` .
And if you call getParentBatches on either the first or the last
message , you get ` ` [ Batch ( name = ' outer ' , . . . ) ] ` `
And you may only call ` getParentBatches ` ` on the PRIVMSG
if only the first three messages were processed .
"""
batch = msg . tagged ( ' batch ' )
if not batch :
# msg is not a BATCH command
batch_name = msg . server_tags . get ( ' batch ' )
if batch_name :
batch = self . batches . get ( batch_name )
if not batch :
raise ValueError (
' Called getParentBatches for a message in a batch that '
' already ended. '
)
else :
raise ValueError (
' Called getParentBatches for a message not in a batch. ' )
batches = [ ]
while batch :
batches . append ( batch )
batch = batch . parent_batch
return batches
2021-03-18 19:53:35 +01:00
def getClientTagDenied ( self , tag ) :
""" Returns whether the given tag is denied by the server, according
to its CLIENTTAGDENY policy .
This is only informative , and servers may still allow or deny tags
at their discretion .
For details , see the RPL_ISUPPORT section in
< https : / / ircv3 . net / specs / extensions / message - tags >
"""
tag = tag . lstrip ( " + " )
denied_tags = self . supported . get ( ' CLIENTTAGDENY ' )
if not denied_tags :
return False
denied_tags = denied_tags . split ( ' , ' )
if ' * ' in denied_tags :
# All tags are denied by default, check the whitelist
return ( ' - ' + tag ) not in denied_tags
else :
return tag in denied_tags
2011-10-29 23:22:27 +02:00
def do004 ( self , irc , msg ) :
""" Handles parsing the 004 reply
Supported user and channel modes are cached """
2012-08-26 18:59:41 +02:00
# msg.args = [nick, server, ircd-version, umodes, modes,
2011-10-29 23:22:27 +02:00
# modes that require arguments? (non-standard)]
2020-07-05 19:45:08 +02:00
self . ircd = msg . args [ 2 ] if len ( msg . args ) > 2 else msg . args [ 1 ]
# The conditionals are for Twitch, which doesn't send umodes or
# chanmodes.
if len ( msg . args ) > 3 :
self . supported [ ' umodes ' ] = frozenset ( msg . args [ 3 ] )
if len ( msg . args ) > 4 :
self . supported [ ' chanmodes ' ] = frozenset ( msg . args [ 4 ] )
2011-10-29 23:22:27 +02:00
2005-01-19 14:14:38 +01:00
_005converters = utils . InsensitivePreservingDict ( {
2020-05-07 18:31:39 +02:00
' modes ' : lambda s : int ( s ) if s else None , # it's optional
2005-01-19 14:14:38 +01:00
' keylen ' : int ,
' nicklen ' : int ,
' userlen ' : int ,
' hostlen ' : int ,
' kicklen ' : int ,
' awaylen ' : int ,
' silence ' : int ,
' topiclen ' : int ,
' channellen ' : int ,
' maxtargets ' : int ,
' maxnicklen ' : int ,
' maxchannels ' : int ,
' watch ' : int , # DynastyNet, EnterTheGame
} )
def _prefixParser ( s ) :
if ' ) ' in s :
( left , right ) = s . split ( ' ) ' )
assert left [ 0 ] == ' ( ' , ' Odd PREFIX in 005: %s ' % s
left = left [ 1 : ]
assert len ( left ) == len ( right ) , ' Odd PREFIX in 005: %s ' % s
2014-01-21 10:40:18 +01:00
return dict ( list ( zip ( left , right ) ) )
2005-01-19 14:14:38 +01:00
else :
2014-01-21 10:40:18 +01:00
return dict ( list ( zip ( ' ovh ' , s ) ) )
2005-01-19 14:14:38 +01:00
_005converters [ ' prefix ' ] = _prefixParser
del _prefixParser
2005-06-29 16:20:54 +02:00
def _maxlistParser ( s ) :
modes = ' '
limits = [ ]
pairs = s . split ( ' , ' )
for pair in pairs :
( mode , limit ) = pair . split ( ' : ' , 1 )
modes + = mode
limits + = ( int ( limit ) , ) * len ( mode )
2014-01-21 10:40:18 +01:00
return dict ( list ( zip ( modes , limits ) ) )
2005-06-29 16:20:54 +02:00
_005converters [ ' maxlist ' ] = _maxlistParser
del _maxlistParser
def _maxbansParser ( s ) :
# IRCd using a MAXLIST style string (IRCNet)
if ' : ' in s :
modes = ' '
limits = [ ]
pairs = s . split ( ' , ' )
for pair in pairs :
( mode , limit ) = pair . split ( ' : ' , 1 )
modes + = mode
limits + = ( int ( limit ) , ) * len ( mode )
2014-01-21 10:40:18 +01:00
d = dict ( list ( zip ( modes , limits ) ) )
2005-06-29 16:20:54 +02:00
assert ' b ' in d
return d [ ' b ' ]
else :
return int ( s )
_005converters [ ' maxbans ' ] = _maxbansParser
del _maxbansParser
2005-01-19 14:14:38 +01:00
def do005 ( self , irc , msg ) :
for arg in msg . args [ 1 : - 1 ] : # 0 is nick, -1 is "are supported"
if ' = ' in arg :
( name , value ) = arg . split ( ' = ' , 1 )
converter = self . _005converters . get ( name , lambda x : x )
try :
self . supported [ name ] = converter ( value )
2015-03-03 09:02:29 +01:00
except Exception :
2005-01-19 14:14:38 +01:00
log . exception ( ' Uncaught exception in 005 converter: ' )
log . error ( ' Name: %s , Converter: %s ' , name , converter )
else :
self . supported [ arg ] = None
2005-06-29 16:20:54 +02:00
2014-08-03 13:58:49 +02:00
def do352 ( self , irc , msg ) :
2005-01-19 14:14:38 +01:00
# WHO reply.
2014-08-02 12:30:24 +02:00
2014-08-03 13:58:49 +02:00
( nick , user , host ) = ( msg . args [ 5 ] , msg . args [ 2 ] , msg . args [ 3 ] )
hostmask = ' %s ! %s @ %s ' % ( nick , user , host )
self . nicksToHostmasks [ nick ] = hostmask
def do354 ( self , irc , msg ) :
# WHOX reply.
2018-01-16 16:21:41 +01:00
if len ( msg . args ) != 9 or msg . args [ 1 ] != ' 1 ' :
2014-08-04 21:32:13 +02:00
return
2018-01-16 16:21:41 +01:00
# irc.nick 1 user ip host nick status account gecos
( n , t , user , ip , host , nick , status , account , gecos ) = msg . args
2005-01-19 14:14:38 +01:00
hostmask = ' %s ! %s @ %s ' % ( nick , user , host )
self . nicksToHostmasks [ nick ] = hostmask
def do353 ( self , irc , msg ) :
# NAMES reply.
2015-08-22 10:33:59 +02:00
( __ , type , channel , items ) = msg . args
2005-01-19 14:14:38 +01:00
if channel not in self . channels :
self . channels [ channel ] = ChannelState ( )
c = self . channels [ channel ]
2015-08-22 10:33:59 +02:00
for item in items . split ( ) :
if ircutils . isUserHostmask ( item ) :
2015-08-22 14:25:20 +02:00
name = ircutils . nickFromHostmask ( item )
2015-08-22 10:33:59 +02:00
self . nicksToHostmasks [ name ] = name
else :
name = item
2005-01-19 14:14:38 +01:00
c . addUser ( name )
if type == ' @ ' :
c . modes [ ' s ' ] = None
2015-08-22 20:48:03 +02:00
def doChghost ( self , irc , msg ) :
( user , host ) = msg . args
nick = msg . nick
hostmask = ' %s ! %s @ %s ' % ( nick , user , host )
self . nicksToHostmasks [ nick ] = hostmask
2005-01-19 14:14:38 +01:00
def doJoin ( self , irc , msg ) :
for channel in msg . args [ 0 ] . split ( ' , ' ) :
if channel in self . channels :
self . channels [ channel ] . addUser ( msg . nick )
elif msg . nick : # It must be us.
chan = ChannelState ( )
chan . addUser ( msg . nick )
self . channels [ channel ] = chan
# I don't know why this assert was here.
#assert msg.nick == irc.nick, msg
2012-12-23 17:19:25 +01:00
def do367 ( self , irc , msg ) :
# Example:
# :server 367 user #chan some!random@user evil!channel@op 1356276459
2013-07-13 12:01:53 +02:00
try :
state = self . channels [ msg . args [ 1 ] ]
except KeyError :
# We have been kicked of the channel before the server replied to
# the MODE +b command.
pass
else :
state . bans . add ( msg . args [ 2 ] )
2005-01-19 14:14:38 +01:00
def doMode ( self , irc , msg ) :
channel = msg . args [ 0 ]
2019-08-04 18:11:28 +02:00
if irc . isChannel ( channel ) : # There can be user modes, as well.
2005-01-19 14:14:38 +01:00
try :
chan = self . channels [ channel ]
except KeyError :
chan = ChannelState ( )
self . channels [ channel ] = chan
chan . doMode ( msg )
def do324 ( self , irc , msg ) :
channel = msg . args [ 1 ]
2013-10-10 16:10:06 +02:00
try :
chan = self . channels [ channel ]
except KeyError :
chan = ChannelState ( )
self . channels [ channel ] = chan
2005-01-19 14:14:38 +01:00
for ( mode , value ) in ircutils . separateModes ( msg . args [ 2 : ] ) :
modeChar = mode [ 1 ]
if mode [ 0 ] == ' + ' and mode [ 1 ] not in ' ovh ' :
chan . setMode ( modeChar , value )
elif mode [ 0 ] == ' - ' and mode [ 1 ] not in ' ovh ' :
chan . unsetMode ( modeChar )
def do329 ( self , irc , msg ) :
# This is the last part of an empty mode.
channel = msg . args [ 1 ]
2013-10-10 16:10:06 +02:00
try :
chan = self . channels [ channel ]
except KeyError :
chan = ChannelState ( )
self . channels [ channel ] = chan
2005-01-19 14:14:38 +01:00
chan . created = int ( msg . args [ 2 ] )
def doPart ( self , irc , msg ) :
for channel in msg . args [ 0 ] . split ( ' , ' ) :
try :
chan = self . channels [ channel ]
except KeyError :
continue
if ircutils . strEqual ( msg . nick , irc . nick ) :
del self . channels [ channel ]
else :
chan . removeUser ( msg . nick )
def doKick ( self , irc , msg ) :
( channel , users ) = msg . args [ : 2 ]
chan = self . channels [ channel ]
for user in users . split ( ' , ' ) :
2005-01-28 14:58:59 +01:00
if ircutils . strEqual ( user , irc . nick ) :
del self . channels [ channel ]
return
else :
chan . removeUser ( user )
2005-01-19 14:14:38 +01:00
def doQuit ( self , irc , msg ) :
2015-12-04 01:14:42 +01:00
channel_names = ircutils . IrcSet ( )
for ( name , channel ) in self . channels . items ( ) :
if msg . nick in channel . users :
channel_names . add ( name )
channel . removeUser ( msg . nick )
# Remember which channels the user was on
msg . tag ( ' channels ' , channel_names )
2005-01-19 14:14:38 +01:00
if msg . nick in self . nicksToHostmasks :
# If we're quitting, it may not be.
del self . nicksToHostmasks [ msg . nick ]
def doTopic ( self , irc , msg ) :
if len ( msg . args ) == 1 :
return # Empty TOPIC for information. Does not affect state.
try :
chan = self . channels [ msg . args [ 0 ] ]
chan . topic = msg . args [ 1 ]
except KeyError :
pass # We don't have to be in a channel to send a TOPIC.
def do332 ( self , irc , msg ) :
chan = self . channels [ msg . args [ 1 ] ]
chan . topic = msg . args [ 2 ]
def doNick ( self , irc , msg ) :
newNick = msg . args [ 0 ]
oldNick = msg . nick
try :
if msg . user and msg . host :
# Nick messages being handed out from the bot itself won't
# have the necessary prefix to make a hostmask.
newHostmask = ircutils . joinHostmask ( newNick , msg . user , msg . host )
self . nicksToHostmasks [ newNick ] = newHostmask
del self . nicksToHostmasks [ oldNick ]
except KeyError :
pass
2015-12-04 13:27:11 +01:00
channel_names = ircutils . IrcSet ( )
for ( name , channel ) in self . channels . items ( ) :
if msg . nick in channel . users :
channel_names . add ( name )
2005-01-19 14:14:38 +01:00
channel . replaceUser ( oldNick , newNick )
2015-12-04 13:27:11 +01:00
msg . tag ( ' channels ' , channel_names )
2005-01-19 14:14:38 +01:00
2015-12-04 01:37:23 +01:00
def doBatch ( self , irc , msg ) :
batch_name = msg . args [ 0 ] [ 1 : ]
if msg . args [ 0 ] . startswith ( ' + ' ) :
batch_type = msg . args [ 1 ]
batch_arguments = tuple ( msg . args [ 2 : ] )
2021-03-03 23:32:00 +01:00
# Both are possibly None:
parent_batch_name = msg . server_tags . get ( " batch " )
parent_batch = self . batches . get ( parent_batch_name )
batch = Batch (
name = batch_name ,
type = batch_type ,
arguments = batch_arguments ,
messages = [ msg ] ,
parent_batch = parent_batch
)
msg . tag ( ' batch ' , batch )
self . batches [ batch_name ] = batch
2015-12-04 01:37:23 +01:00
elif msg . args [ 0 ] . startswith ( ' - ' ) :
batch = self . batches . pop ( batch_name )
2020-05-06 18:29:17 +02:00
batch . messages . append ( msg )
2015-12-04 01:37:23 +01:00
msg . tag ( ' batch ' , batch )
else :
assert False , msg . args [ 0 ]
2005-01-19 14:14:38 +01:00
2016-01-09 11:10:41 +01:00
def doAway ( self , irc , msg ) :
channel_names = ircutils . IrcSet ( )
for ( name , channel ) in self . channels . items ( ) :
if msg . nick in channel . users :
channel_names . add ( name )
msg . tag ( ' channels ' , channel_names )
2005-01-19 14:14:38 +01:00
###
# The basic class for handling a connection to an IRC server. Accepts
# callbacks of the IrcCallback interface. Public attributes include 'driver',
# 'queue', and 'state', in addition to the standard nick/user/ident attributes.
###
_callbacks = [ ]
2015-08-11 16:50:23 +02:00
class Irc ( IrcCommandDispatcher , log . Firewalled ) :
2005-01-19 14:14:38 +01:00
""" The base class for an IRC connection.
Handles PING commands already .
2020-09-27 15:24:27 +02:00
. . attribute : : zombie
Whether or not this object represents a living IRC connection .
: type : bool
. . attribute : : network
The name of the network this object is connected to .
: type : str
. . attribute : : startedAt
When this connection was ( re ) started .
: type : float
. . attribute : : callbacks
List of all callbacks ( ie . plugins ) currently loaded
: type : List [ IrcCallback ]
. . attribute : : queue
Queue of messages waiting to be sent . Plugins should use the
` ` queueMsg ` ` method instead of accessing this directly .
: type : IrcMsgQueue
. . attribute : : fastqueue
Same as ` ` queue ` ` , but for messages with high priority . Plugins should
use the ` ` sendMsg ` ` method instead of accessing this directly ( or
` queueMsg ` if the message isn ' t high priority).
: type : smallqueue
. . attribute : : driver
Driver of the IRC connection ( normally , a
: py : class : ` supybot . drivers . Socket . SocketDriver ` object ) .
Plugins normally do not need to access this .
. . attribute : : startedSync
When joining a channel , a ` ` ' #channel ' : time . time ( ) ` ` entry is added
to this dict , which is then removed when the join is completed .
Plugins should not change this value , it is automatically handled when
they send a JOIN .
: type : ircutils . IrcDict [ str , float ]
. . attribute : : monitoring
A dict with nicks as keys and the number of plugins monitoring this
nick as value .
Plugins should not access this directly , and should use the ` ` monitor ` `
and ` ` unmonitor ` ` methods instead .
: type : ircutils . IrcDict [ str , int ]
. . attribute : : state
An : py : class : ` supybot . irclib . IrcState ` object , which stores all the
known information about the connection with the IRC network .
: type : supybot . irclib . IrcState
2005-01-19 14:14:38 +01:00
"""
__firewalled__ = { ' die ' : None ,
' feedMsg ' : None ,
' takeMsg ' : None , }
2014-01-26 21:49:28 +01:00
_nickSetters = set ( [ ' 001 ' , ' 002 ' , ' 003 ' , ' 004 ' , ' 250 ' , ' 251 ' , ' 252 ' ,
2005-02-03 20:17:26 +01:00
' 254 ' , ' 255 ' , ' 265 ' , ' 266 ' , ' 372 ' , ' 375 ' , ' 376 ' ,
2014-01-26 21:49:28 +01:00
' 333 ' , ' 353 ' , ' 332 ' , ' 366 ' , ' 005 ' ] )
2005-01-19 14:14:38 +01:00
# We specifically want these callbacks to be common between all Ircs,
# that's why we don't do the normal None default with a check.
def __init__ ( self , network , callbacks = _callbacks ) :
self . zombie = False
world . ircs . append ( self )
self . network = network
2012-06-30 02:32:49 +02:00
self . startedAt = time . time ( )
2005-01-19 14:14:38 +01:00
self . callbacks = callbacks
self . state = IrcState ( )
self . queue = IrcMsgQueue ( )
self . fastqueue = smallqueue ( )
2021-03-13 12:12:03 +01:00
# Messages of batches that are currently in one self.queue (not
# self.fastqueue).
# This works by adding only the first message of a batch in a queue,
# and when self.takeMsg pops that message from the queue, it will
# also pop the whole batch from self._queued_batches and atomically
# add it to self.fastqueue
self . _queued_batches = { }
2005-01-19 14:14:38 +01:00
self . driver = None # The driver should set this later.
self . _setNonResettingVariables ( )
self . _queueConnectMessages ( )
self . startedSync = ircutils . IrcDict ( )
2015-05-15 19:24:24 +02:00
self . monitoring = ircutils . IrcDict ( )
2005-01-19 14:14:38 +01:00
def isChannel ( self , s ) :
""" Helper function to check whether a given string is a channel on
the network this Irc object is connected to . """
kw = { }
if ' chantypes ' in self . state . supported :
kw [ ' chantypes ' ] = self . state . supported [ ' chantypes ' ]
if ' channellen ' in self . state . supported :
kw [ ' channellen ' ] = self . state . supported [ ' channellen ' ]
return ircutils . isChannel ( s , * * kw )
def isNick ( self , s ) :
2020-09-27 15:24:27 +02:00
""" Returns whether the given argument is a valid nick on this
network .
"""
2005-01-19 14:14:38 +01:00
kw = { }
if ' nicklen ' in self . state . supported :
kw [ ' nicklen ' ] = self . state . supported [ ' nicklen ' ]
return ircutils . isNick ( s , * * kw )
# This *isn't* threadsafe!
def addCallback ( self , callback ) :
2013-02-01 20:38:53 +01:00
""" Adds a callback to the callbacks list.
: param callback : A callback object
: type callback : supybot . irclib . IrcCallback
"""
2005-08-15 07:36:23 +02:00
assert not self . getCallback ( callback . name ( ) )
2005-01-19 14:14:38 +01:00
self . callbacks . append ( callback )
# This is the new list we're building, which will be tsorted.
cbs = [ ]
# The vertices are self.callbacks itself. Now we make the edges.
2005-02-03 20:17:26 +01:00
edges = set ( )
2005-01-19 14:14:38 +01:00
for cb in self . callbacks :
( before , after ) = cb . callPrecedence ( self )
assert cb not in after , ' cb was in its own after. '
assert cb not in before , ' cb was in its own before. '
for otherCb in before :
edges . add ( ( otherCb , cb ) )
for otherCb in after :
edges . add ( ( cb , otherCb ) )
def getFirsts ( ) :
2005-02-03 20:17:26 +01:00
firsts = set ( self . callbacks ) - set ( cbs )
2005-01-19 14:14:38 +01:00
for ( before , after ) in edges :
firsts . discard ( after )
return firsts
firsts = getFirsts ( )
while firsts :
# Then we add these to our list of cbs, and remove all edges that
# originate with these cbs.
for cb in firsts :
cbs . append ( cb )
edgesToRemove = [ ]
for edge in edges :
if edge [ 0 ] is cb :
edgesToRemove . append ( edge )
for edge in edgesToRemove :
edges . remove ( edge )
firsts = getFirsts ( )
assert len ( cbs ) == len ( self . callbacks ) , \
' cbs: %s , self.callbacks: %s ' % ( cbs , self . callbacks )
self . callbacks [ : ] = cbs
def getCallback ( self , name ) :
""" Gets a given callback by name. """
name = name . lower ( )
for callback in self . callbacks :
if callback . name ( ) . lower ( ) == name :
return callback
else :
return None
def removeCallback ( self , name ) :
""" Removes a callback from the callback list. """
name = name . lower ( )
def nameMatches ( cb ) :
return cb . name ( ) . lower ( ) == name
2005-01-27 07:59:08 +01:00
( bad , good ) = utils . iter . partition ( nameMatches , self . callbacks )
2005-01-19 14:14:38 +01:00
self . callbacks [ : ] = good
return bad
def queueMsg ( self , msg ) :
""" Queues a message to be sent to the server. """
2021-03-13 12:12:03 +01:00
if msg . command . upper ( ) == ' BATCH ' :
log . error ( ' Tried to send a BATCH message using queueMsg '
' instead of queueBatch: %r ' , msg )
2005-01-19 14:14:38 +01:00
if not self . zombie :
return self . queue . enqueue ( msg )
else :
log . warning ( ' Refusing to queue %r ; %s is a zombie. ' , msg , self )
return False
def sendMsg ( self , msg ) :
""" Queues a message to be sent to the server *immediately* """
2021-03-13 12:12:03 +01:00
if msg . command . upper ( ) == ' BATCH ' :
log . error ( ' Tried to send a BATCH message using sendMsg '
' instead of queueBatch: %r ' , msg )
2005-01-19 14:14:38 +01:00
if not self . zombie :
self . fastqueue . enqueue ( msg )
else :
log . warning ( ' Refusing to send %r ; %s is a zombie. ' , msg , self )
2021-03-13 12:12:03 +01:00
def queueBatch ( self , msgs ) :
""" Queues a batch of messages to be sent to the server.
See < https : / / ircv3 . net / specs / extensions / batch - 3.2 >
queueMsg / sendMsg must not be used repeatedly to send a batch , because
they do not guarantee the batch is send atomically , which is
required because " Clients MUST NOT send messages other than PRIVMSG
while a multiline batch is open . "
- - < https : / / ircv3 . net / specs / extensions / multiline >
"""
if not conf . supybot . protocols . irc . experimentalExtensions ( ) :
2021-03-13 12:22:39 +01:00
raise ValueError (
' queueBatch is disabled because it depends on draft '
' IRC specifications. If you know what you are doing, '
' set supybot.protocols.irc.experimentalExtensions. ' )
2021-03-13 13:03:26 +01:00
if len ( msgs ) < 2 :
2021-03-13 12:22:39 +01:00
raise ValueError (
' queueBatch called with less than two messages. ' )
2021-03-13 12:12:03 +01:00
if msgs [ 0 ] . command . upper ( ) != ' BATCH ' or msgs [ 0 ] . args [ 0 ] [ 0 ] != ' + ' :
2021-03-13 12:22:39 +01:00
raise ValueError (
' queueBatch called with non- " BATCH + " as first message. ' )
2021-03-13 12:12:03 +01:00
if msgs [ - 1 ] . command . upper ( ) != ' BATCH ' or msgs [ - 1 ] . args [ 0 ] [ 0 ] != ' - ' :
2021-03-13 12:22:39 +01:00
raise ValueError (
' queueBatch called with non- " BATCH - " as last message. ' )
2021-03-13 12:12:03 +01:00
batch_name = msgs [ 0 ] . args [ 0 ] [ 1 : ]
2021-03-13 12:22:39 +01:00
2021-03-13 13:03:26 +01:00
if msgs [ - 1 ] . args [ 0 ] [ 1 : ] != batch_name :
2021-03-13 12:22:39 +01:00
raise ValueError (
' queueBatch called with mismatched BATCH name args. ' )
2021-03-13 13:03:26 +01:00
if any ( msg . server_tags [ ' batch ' ] != batch_name for msg in msgs [ 1 : - 1 ] ) :
2021-03-13 12:22:39 +01:00
raise ValueError (
' queueBatch called with mismatched batch names. ' )
2021-03-13 12:12:03 +01:00
return
if batch_name in self . _queued_batches :
2021-03-13 12:22:39 +01:00
raise ValueError (
' queueBatch called with a batch name already in flight ' )
2021-03-13 13:03:26 +01:00
2021-03-13 12:12:03 +01:00
self . _queued_batches [ batch_name ] = msgs
# Enqueue only the start of the batch. When takeMsg sees it, it will
# enqueue the full batch in self.fastqueue.
# We don't enqueue the full batch in self.fastqueue here, because
# there is no reason for this batch to jump in front of all other
# queued messages.
# TODO: the batch will be ordered with the priority of a BATCH
# message (ie. normal), but if the batch is made only of low-priority
# messages like PRIVMSG, it should have that priority.
2021-03-13 12:15:21 +01:00
# (or maybe order on the batch type instead of commands inside
# the batch?)
2021-03-13 12:12:03 +01:00
self . queue . enqueue ( msgs [ 0 ] )
2020-05-07 18:30:07 +02:00
def _truncateMsg ( self , msg ) :
msg_str = str ( msg )
if msg_str [ 0 ] == ' @ ' :
( msg_tags_str , msg_rest_str ) = msg_str . split ( ' ' , 1 )
msg_tags_str + = ' '
else :
msg_tags_str = ' '
msg_rest_str = msg_str
2021-03-11 19:02:55 +01:00
msg_rest_bytes = msg_rest_str . encode ( )
if len ( msg_rest_bytes ) > MAX_LINE_SIZE :
2020-05-07 18:30:07 +02:00
# Yes, this violates the contract, but at this point it doesn't
# matter. That's why we gotta go munging in private attributes
#
# I'm changing this to a log.debug to fix a possible loop in
# the LogToIrc plugin. Since users can't do anything about
# this issue, there's no fundamental reason to make it a
# warning.
log . debug ( ' Truncating %r , message is too long. ' , msg )
2021-03-11 19:02:55 +01:00
# Truncate to 512 bytes (minus 2 for '\r\n')
msg_rest_bytes = msg_rest_bytes [ : MAX_LINE_SIZE - 2 ]
# The above truncation may have truncated in the middle of a
# multi-byte character.
# I was about to write a UTF-8 decoder here just to trim them
# properly, but fortunately there is a neat trick to trim it
# while decoding: just ignore invalid bytes!
# https://stackoverflow.com/a/1820949/539465
msg_rest_str = msg_rest_bytes . decode ( errors = " ignore " )
msg . _str = msg_tags_str + msg_rest_str + ' \r \n '
2020-05-07 18:30:07 +02:00
msg . _len = len ( str ( msg ) )
# TODO: truncate tags
2005-01-19 14:14:38 +01:00
def takeMsg ( self ) :
""" Called by the IrcDriver; takes a message to be sent. """
if not self . callbacks :
log . critical ( ' No callbacks in %s . ' , self )
now = time . time ( )
msg = None
if self . fastqueue :
msg = self . fastqueue . dequeue ( )
elif self . queue :
2021-04-24 19:48:05 +02:00
if now - self . lastTake < conf . supybot . protocols . irc . throttleTime ( ) :
2005-01-19 14:14:38 +01:00
log . debug ( ' Irc.takeMsg throttling. ' )
else :
self . lastTake = now
msg = self . queue . dequeue ( )
elif self . afterConnect and \
conf . supybot . protocols . irc . ping ( ) and \
now > self . lastping + conf . supybot . protocols . irc . ping . interval ( ) :
if self . outstandingPing :
s = ' Ping sent at %s not replied to. ' % \
log . timestamp ( self . lastping )
log . warning ( s )
self . feedMsg ( ircmsgs . error ( s ) )
self . driver . reconnect ( )
elif not self . zombie :
self . lastping = now
now = str ( int ( now ) )
self . outstandingPing = True
self . queueMsg ( ircmsgs . ping ( now ) )
2021-03-13 12:12:03 +01:00
2005-01-19 14:14:38 +01:00
if msg :
2021-04-02 20:53:38 +02:00
# Copy the msg before altering it back. Without a copy, it
# can cause all sorts of issues if the msg is reused (eg. Relay
# sends the same message object to the same network, so when
# sending the msg for the second time, it would already be
# tagged with emulatedEcho and fail the assertion; or it can be
# added a label because we have labeled-response on a network)
msg = ircmsgs . IrcMsg ( msg = msg )
2021-03-13 12:12:03 +01:00
if msg . command . upper ( ) == ' BATCH ' :
if not conf . supybot . protocols . irc . experimentalExtensions ( ) :
2021-03-13 18:11:52 +01:00
log . error ( ' Dropping outgoing batch. '
' supybot.protocols.irc.experimentalExtensions '
2021-03-13 12:12:03 +01:00
' is disabled, so plugins should not send '
' batches. This is a bug, please report it. ' )
return None
if msg . args [ 0 ] . startswith ( ' + ' ) :
# Start of a batch; created by self.queueBatch. We need to
2021-03-13 12:16:19 +01:00
# *prepend* the rest of the batch to the fastqueue
2021-03-13 12:12:03 +01:00
# so that no other message is sent while the batch is
# open.
# "Clients MUST NOT send messages other than PRIVMSG while
# a multiline batch is open."
# -- <https://ircv3.net/specs/extensions/multiline>
#
# (Yes, *prepend* to the queue. Fortunately, it should be
# empty, because BATCH cannot be queued in the fastqueue
# and we just got a BATCH, which means it's from the
# regular queue, which means the fastqueue is empty.
# But let's not take any risk, eg. if race condition
# with a plugin appending directly to the fastqueue.)
2021-03-13 13:03:26 +01:00
batch_name = msg . args [ 0 ] [ 1 : ]
batch_messages = self . _queued_batches . pop ( batch_name )
2021-03-13 12:12:03 +01:00
if batch_messages [ 0 ] != msg :
log . error ( ' Enqueue " BATCH + " message does not match '
' the one of the batch in flight. ' )
self . fastqueue [ : 0 ] = batch_messages [ 1 : ]
2020-05-07 20:56:59 +02:00
if not world . testing and ' label ' not in msg . server_tags \
and ' labeled-response ' in self . state . capabilities_ack :
# Not adding labels while testing, because it would break
# all plugin tests using IrcMsg equality (unless they
# explicitly add the label, but it becomes a burden).
msg . server_tags [ ' label ' ] = ircutils . makeLabel ( )
msg . _len = msg . _str = None
2005-01-19 14:14:38 +01:00
for callback in reversed ( self . callbacks ) :
2019-08-24 17:50:05 +02:00
self . _setMsgChannel ( msg )
2005-01-19 14:14:38 +01:00
msg = callback . outFilter ( self , msg )
if msg is None :
2005-05-18 06:18:35 +02:00
log . debug ( ' %s .outFilter returned None. ' , callback . name ( ) )
2005-01-19 14:14:38 +01:00
return self . takeMsg ( )
world . debugFlush ( )
2020-05-07 18:30:07 +02:00
self . _truncateMsg ( msg )
2020-05-07 21:00:16 +02:00
if msg . command . upper ( ) in ( ' PRIVMSG ' , ' NOTICE ' , ' TAGMSG ' ) \
and ' echo-message ' not in self . state . capabilities_ack :
# echo-message is not implemented by server; let's emulate it
# here, just before sending it to the driver.
assert not msg . tagged ( ' receivedAt ' )
irclib: Fix test failure
FAIL: testMoreIsCaseInsensitive (Misc.test.MiscTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File /home/travis/virtualenv/python3.8-dev/lib/python3.8/site-packages/supybot/test.py, line 214, in runTest
originalRunTest()
File ./plugins/Misc/test.py, line 260, in testMoreIsCaseInsensitive
self.assertNotError('more %s' % nick.upper())
File /home/travis/virtualenv/python3.8-dev/lib/python3.8/site-packages/supybot/test.py, line 355, in assertNotError
m = self._feedMsg(query, **kwargs)
File /home/travis/virtualenv/python3.8-dev/lib/python3.8/site-packages/supybot/test.py, line 526, in _feedMsg
response = self.irc.takeMsg()
File /home/travis/virtualenv/python3.8-dev/lib/python3.8/site-packages/supybot/log.py, line 368, in m
return f(self, *args, **kwargs)
File /home/travis/virtualenv/python3.8-dev/lib/python3.8/site-packages/supybot/irclib.py, line 1308, in takeMsg
assert not msg.tagged('emulatedEcho')
AssertionError
I don't understand why it's happening or why it's only that specific test,
but there we go.
2020-11-10 09:47:36 +01:00
if not world . testing :
assert not msg . tagged ( ' emulatedEcho ' )
2021-04-01 19:08:54 +02:00
2021-04-02 20:53:38 +02:00
msg . tag ( ' emulatedEcho ' , True )
self . feedMsg ( msg , tag = False )
2020-05-07 21:00:16 +02:00
else :
# I don't think we should do this. Why should it matter? If it's
# something important, then the server will send it back to us,
# and if it's just a privmsg/notice/etc., we don't care.
# On second thought, we need this for testing.
if world . testing :
self . state . addMsg ( self , msg )
2011-12-28 11:37:10 +01:00
log . debug ( ' Outgoing message ( %s ): %s ' , self . network , str ( msg ) . rstrip ( ' \r \n ' ) )
2005-01-19 14:14:38 +01:00
return msg
elif self . zombie :
# We kill the driver here so it doesn't continue to try to
# take messages from us.
self . driver . die ( )
self . _reallyDie ( )
else :
return None
2019-08-24 14:14:33 +02:00
def _tagMsg ( self , msg ) :
""" Sets attribute on an incoming IRC message. Will usually only be
called by feedMsg , but may be useful in tests as well . """
2005-01-19 14:14:38 +01:00
msg . tag ( ' receivedBy ' , self )
msg . tag ( ' receivedOn ' , self . network )
2011-12-06 05:52:38 +01:00
msg . tag ( ' receivedAt ' , time . time ( ) )
2019-08-17 22:23:51 +02:00
2019-08-24 17:50:05 +02:00
self . _setMsgChannel ( msg )
def _setMsgChannel ( self , msg ) :
2019-09-20 21:23:49 +02:00
channel = None
2019-08-17 22:23:51 +02:00
if msg . args :
2019-09-20 21:23:49 +02:00
channel = msg . args [ 0 ]
2019-08-18 10:09:11 +02:00
if msg . command in ( ' NOTICE ' , ' PRIVMSG ' ) and \
not conf . supybot . protocols . irc . strictRfc ( ) :
2019-09-20 21:23:49 +02:00
channel = self . stripChannelPrefix ( channel )
if not self . isChannel ( channel ) :
channel = None
msg . channel = channel
2019-08-24 14:14:33 +02:00
2019-08-24 17:50:05 +02:00
def stripChannelPrefix ( self , channel ) :
2019-08-24 14:14:33 +02:00
statusmsg_chars = self . state . supported . get ( ' statusmsg ' , ' ' )
return channel . lstrip ( statusmsg_chars )
_numericErrorCommandRe = re . compile ( r ' ^[45][0-9][0-9]$ ' )
2020-05-07 21:00:16 +02:00
def feedMsg ( self , msg , tag = True ) :
""" Called by the IrcDriver; feeds a message received.
` tag = False ` is used when simulating echo messages , to skip adding
received * tags . """
if tag :
self . _tagMsg ( msg )
2021-04-24 21:21:57 +02:00
channel = msg . channel # used by dynamicScope (ew)
2019-08-17 22:23:51 +02:00
2005-08-04 23:19:41 +02:00
preInFilter = str ( msg ) . rstrip ( ' \r \n ' )
2009-12-28 19:17:27 +01:00
log . debug ( ' Incoming message ( %s ): %s ' , self . network , preInFilter )
2005-01-19 14:14:38 +01:00
# Yeah, so this is odd. Some networks (oftc) seem to give us certain
# messages with our nick instead of our prefix. We'll fix that here.
if msg . prefix == self . nick :
log . debug ( ' Got one of those odd nick-instead-of-prefix msgs. ' )
msg = ircmsgs . IrcMsg ( prefix = self . prefix , msg = msg )
# This catches cases where we know our own nick (from sending it to the
# server) but we don't yet know our prefix.
if msg . nick == self . nick and self . prefix != msg . prefix :
self . prefix = msg . prefix
# This keeps our nick and server attributes updated.
if msg . command in self . _nickSetters :
if msg . args [ 0 ] != self . nick :
self . nick = msg . args [ 0 ]
log . debug ( ' Updating nick attribute to %s . ' , self . nick )
if msg . prefix != self . server :
self . server = msg . prefix
2005-02-23 17:40:21 +01:00
log . debug ( ' Updating server attribute to %s . ' , self . server )
2005-01-19 14:14:38 +01:00
# Dispatch to specific handlers for commands.
2020-01-23 14:25:10 +01:00
method = self . dispatchCommand ( msg . command , msg . args )
2005-01-19 14:14:38 +01:00
if method is not None :
method ( msg )
2009-03-12 20:04:22 +01:00
elif self . _numericErrorCommandRe . search ( msg . command ) :
log . error ( ' Unhandled error message from server: %r ' % msg )
2005-01-19 14:14:38 +01:00
# Now update the IrcState object.
try :
self . state . addMsg ( self , msg )
except :
log . exception ( ' Exception in update of IrcState object: ' )
# Now call the callbacks.
world . debugFlush ( )
for callback in self . callbacks :
try :
m = callback . inFilter ( self , msg )
if not m :
2005-05-18 06:18:35 +02:00
log . debug ( ' %s .inFilter returned None ' , callback . name ( ) )
2005-01-19 14:14:38 +01:00
return
msg = m
except :
log . exception ( ' Uncaught exception in inFilter: ' )
world . debugFlush ( )
2005-08-04 23:19:41 +02:00
postInFilter = str ( msg ) . rstrip ( ' \r \n ' )
if postInFilter != preInFilter :
log . debug ( ' Incoming message (post-inFilter): %s ' , postInFilter )
2005-01-19 14:14:38 +01:00
for callback in self . callbacks :
try :
if callback is not None :
callback ( self , msg )
except :
log . exception ( ' Uncaught exception in callback: ' )
world . debugFlush ( )
def die ( self ) :
""" Makes the Irc object *promise* to die -- but it won ' t die (of its
own volition ) until all its queues are clear . Isn ' t that cool? " " "
self . zombie = True
if not self . afterConnect :
self . _reallyDie ( )
# This is useless because it's in world.ircs, so it won't be deleted until
# the program exits. Just figured you might want to know.
#def __del__(self):
# self._reallyDie()
def reset ( self ) :
""" Resets the Irc object. Called when the driver reconnects. """
self . _setNonResettingVariables ( )
self . state . reset ( )
self . queue . reset ( )
self . fastqueue . reset ( )
self . startedSync . clear ( )
for callback in self . callbacks :
2020-05-17 12:46:01 +02:00
try :
callback . reset ( )
except Exception :
log . exception ( ' Uncaught exception in %r .reset() ' , callback )
2005-01-19 14:14:38 +01:00
self . _queueConnectMessages ( )
def _setNonResettingVariables ( self ) :
# Configuration stuff.
2015-12-12 16:40:48 +01:00
network_config = conf . supybot . networks . get ( self . network )
2014-05-08 10:33:01 +02:00
def get_value ( name ) :
2015-12-12 16:40:48 +01:00
return getattr ( network_config , name ) ( ) or \
2014-05-08 10:33:01 +02:00
getattr ( conf . supybot , name ) ( )
self . nick = get_value ( ' nick ' )
2016-05-23 03:18:14 +02:00
# Expand variables like $version in realname.
self . user = ircutils . standardSubstitute ( self , None , get_value ( ' user ' ) )
2014-05-08 10:33:01 +02:00
self . ident = get_value ( ' ident ' )
2005-01-19 14:14:38 +01:00
self . alternateNicks = conf . supybot . nick . alternates ( ) [ : ]
2019-01-27 09:10:06 +01:00
self . triedNicks = ircutils . IrcSet ( )
2015-12-12 16:40:48 +01:00
self . password = network_config . password ( )
2005-01-19 14:14:38 +01:00
self . prefix = ' %s ! %s @ %s ' % ( self . nick , self . ident , ' unset.domain ' )
# The rest.
self . lastTake = 0
self . server = ' unset '
self . afterConnect = False
2013-06-17 08:09:16 +02:00
self . startedAt = time . time ( )
2005-01-19 14:14:38 +01:00
self . lastping = time . time ( )
self . outstandingPing = False
2015-12-12 00:52:44 +01:00
self . capNegociationEnded = False
2015-12-12 16:40:48 +01:00
self . requireStarttls = not network_config . ssl ( ) and \
network_config . requireStarttls ( )
2018-01-24 09:27:39 +01:00
if self . requireStarttls :
log . error ( ( ' STARTTLS is no longer supported. Set '
' supybot.networks. %s .requireStarttls to False '
' to disable it, and use supybot.networks. %s .ssl '
' instead. ' ) % ( self . network , self . network ) )
self . driver . die ( )
self . _reallyDie ( )
return
2015-12-11 23:30:57 +01:00
self . resetSasl ( )
2005-01-19 14:14:38 +01:00
2015-12-11 23:30:57 +01:00
def resetSasl ( self ) :
2015-12-11 10:56:05 +01:00
network_config = conf . supybot . networks . get ( self . network )
2015-12-12 00:52:44 +01:00
self . sasl_authenticated = False
2015-12-11 23:30:57 +01:00
self . sasl_username = network_config . sasl . username ( )
self . sasl_password = network_config . sasl . password ( )
self . sasl_ecdsa_key = network_config . sasl . ecdsa_key ( )
2016-09-10 20:16:28 +02:00
self . sasl_scram_state = { ' step ' : ' uninitialized ' }
2015-12-11 23:30:57 +01:00
self . authenticate_decoder = None
2015-12-11 10:56:05 +01:00
self . sasl_next_mechanisms = [ ]
self . sasl_current_mechanism = None
for mechanism in network_config . sasl . mechanisms ( ) :
if mechanism == ' ecdsa-nist256p-challenge ' and \
2019-12-26 12:14:05 +01:00
crypto and self . sasl_username and \
self . sasl_ecdsa_key :
2015-12-11 10:56:05 +01:00
self . sasl_next_mechanisms . append ( mechanism )
elif mechanism == ' external ' and (
network_config . certfile ( ) or
conf . supybot . protocols . irc . certfile ( ) ) :
self . sasl_next_mechanisms . append ( mechanism )
2016-09-10 20:16:28 +02:00
elif mechanism . startswith ( ' scram- ' ) and scram and \
self . sasl_username and self . sasl_password :
self . sasl_next_mechanisms . append ( mechanism )
2015-12-11 10:56:05 +01:00
elif mechanism == ' plain ' and \
self . sasl_username and self . sasl_password :
self . sasl_next_mechanisms . append ( mechanism )
2015-12-12 16:40:48 +01:00
if self . sasl_next_mechanisms :
self . REQUEST_CAPABILITIES . add ( ' sasl ' )
2013-02-03 18:50:20 +01:00
2020-05-07 21:00:16 +02:00
# Note: echo-message is only requested if labeled-response is available.
2015-05-24 12:29:11 +02:00
REQUEST_CAPABILITIES = set ( [ ' account-notify ' , ' extended-join ' ,
2015-08-22 10:33:59 +02:00
' multi-prefix ' , ' metadata-notify ' , ' account-tag ' ,
2015-08-22 20:48:03 +02:00
' userhost-in-names ' , ' invite-notify ' , ' server-time ' ,
2019-12-05 21:10:31 +01:00
' chghost ' , ' batch ' , ' away-notify ' , ' message-tags ' ,
2020-05-07 21:00:16 +02:00
' msgid ' , ' setname ' , ' labeled-response ' , ' echo-message ' ] )
2020-09-27 15:24:27 +02:00
""" IRCv3 capabilities requested when they are available.
echo - message is special - cased to be requested only with labeled - response .
To check if a capability was negotiated , use ` irc . state . capabilities_ack ` .
"""
2015-05-24 12:25:42 +02:00
2021-03-04 21:30:48 +01:00
REQUEST_EXPERIMENTAL_CAPABILITIES = set ( [ ' draft/multiline ' ] )
""" Like REQUEST_CAPABILITIES, but these capabilities are only requested
if supybot . protocols . irc . experimentalExtensions is enabled . """
2005-01-19 14:14:38 +01:00
def _queueConnectMessages ( self ) :
if self . zombie :
self . driver . die ( )
self . _reallyDie ( )
2014-08-02 12:30:24 +02:00
return
2015-12-12 16:40:48 +01:00
self . sendMsg ( ircmsgs . IrcMsg ( command = ' CAP ' , args = ( ' LS ' , ' 302 ' ) ) )
self . sendAuthenticationMessages ( )
2020-05-01 20:19:53 +02:00
self . state . fsm . on_init_messages_sent ( self )
2019-10-25 23:17:10 +02:00
2015-12-12 16:40:48 +01:00
def sendAuthenticationMessages ( self ) :
2015-06-12 14:30:50 +02:00
# Notes:
# * using sendMsg instead of queueMsg because these messages cannot
# be throttled.
2015-05-17 12:31:14 +02:00
if self . password :
log . info ( ' %s : Queuing PASS command, not logging the password. ' ,
self . network )
self . sendMsg ( ircmsgs . password ( self . password ) )
log . debug ( ' %s : Sending NICK command, nick is %s . ' ,
self . network , self . nick )
self . sendMsg ( ircmsgs . nick ( self . nick ) )
log . debug ( ' %s : Sending USER command, ident is %s , user is %s . ' ,
self . network , self . ident , self . user )
self . sendMsg ( ircmsgs . user ( self . ident , self . user ) )
2020-05-01 20:19:53 +02:00
def capUpkeep ( self , msg ) :
"""
Called after getting a CAP ACK / NAK to check it ' s consistent with what
was requested , and to end the cap negotiation when we received all the
ACK / NAKs we were waiting for .
` msg ` is the message that triggered this call . """
2019-10-25 23:17:10 +02:00
self . state . fsm . expect_state ( [
# Normal CAP ACK / CAP NAK during cap negotiation
IrcStateFsm . States . INIT_CAP_NEGOTIATION ,
# CAP ACK / CAP NAK after a CAP NEW (probably)
IrcStateFsm . States . CONNECTED ,
] )
capabilities_responded = ( self . state . capabilities_ack |
self . state . capabilities_nak )
if not capabilities_responded < = self . state . capabilities_req :
log . error ( ' Server responded with unrequested ACK/NAK '
' capabilities: req= %r , ack= %r , nak= %r ' ,
self . state . capabilities_req ,
self . state . capabilities_ack ,
self . state . capabilities_nak )
2019-12-08 21:25:59 +01:00
self . driver . reconnect ( wait = True )
2019-10-25 23:17:10 +02:00
elif capabilities_responded == self . state . capabilities_req :
log . debug ( ' Got all capabilities ACKed/NAKed ' )
# We got all the capabilities we asked for
if ' sasl ' in self . state . capabilities_ack :
if self . state . fsm . state in [
IrcStateFsm . States . INIT_CAP_NEGOTIATION ,
IrcStateFsm . States . CONNECTED ] :
2020-05-01 20:19:53 +02:00
self . _maybeStartSasl ( msg )
2019-10-25 23:17:10 +02:00
else :
pass # Already in the middle of a SASL auth
2020-09-19 16:55:51 +02:00
elif self . state . fsm . state != IrcStateFsm . States . CONNECTED :
# If we are still in the initial cap negotiation (ie. if this
# is not in response to a 'CAP NEW'), send a CAP END so the
# server sends us the MOTD
2020-05-01 20:19:53 +02:00
self . endCapabilityNegociation ( msg )
2019-10-25 23:17:10 +02:00
else :
log . debug ( ' Waiting for ACK/NAK of capabilities: %r ' ,
self . state . capabilities_req - capabilities_responded )
pass # Do nothing, we'll get more
2020-05-01 20:19:53 +02:00
def endCapabilityNegociation ( self , msg ) :
self . state . fsm . on_cap_end ( self , msg )
2019-10-25 23:17:10 +02:00
self . sendMsg ( ircmsgs . IrcMsg ( command = ' CAP ' , args = ( ' END ' , ) ) )
2015-12-12 00:52:44 +01:00
2015-12-11 10:56:05 +01:00
def sendSaslString ( self , string ) :
for chunk in ircutils . authenticate_generator ( string ) :
self . sendMsg ( ircmsgs . IrcMsg ( command = ' AUTHENTICATE ' ,
args = ( chunk , ) ) )
2014-08-26 15:14:30 +02:00
2020-05-01 20:19:53 +02:00
def tryNextSaslMechanism ( self , msg ) :
2019-10-25 23:17:10 +02:00
self . state . fsm . expect_state ( [
IrcStateFsm . States . INIT_SASL ,
IrcStateFsm . States . CONNECTED_SASL ,
] )
2015-12-11 10:56:05 +01:00
if self . sasl_next_mechanisms :
self . sasl_current_mechanism = self . sasl_next_mechanisms . pop ( 0 )
self . sendMsg ( ircmsgs . IrcMsg ( command = ' AUTHENTICATE ' ,
args = ( self . sasl_current_mechanism . upper ( ) , ) ) )
2018-09-10 22:48:49 +02:00
elif conf . supybot . networks . get ( self . network ) . sasl . required ( ) :
log . error ( ' None of the configured SASL mechanisms succeeded, '
' aborting connection. ' )
2015-12-11 10:56:05 +01:00
else :
self . sasl_current_mechanism = None
2020-05-01 20:19:53 +02:00
self . state . fsm . on_sasl_auth_finished ( self , msg )
2019-10-25 23:17:10 +02:00
if self . state . fsm . state == IrcStateFsm . States . INIT_CAP_NEGOTIATION :
2020-05-01 20:19:53 +02:00
self . endCapabilityNegociation ( msg )
2014-08-02 12:30:24 +02:00
2020-05-01 20:19:53 +02:00
def _maybeStartSasl ( self , msg ) :
2019-10-25 23:17:10 +02:00
if not self . sasl_authenticated and \
' sasl ' in self . state . capabilities_ack :
2020-05-01 20:19:53 +02:00
self . state . fsm . on_sasl_cap ( self , msg )
2019-10-25 23:17:10 +02:00
assert ' sasl ' in self . state . capabilities_ls , (
' Got " CAP ACK sasl " without receiving " CAP LS sasl " or '
' " CAP NEW sasl " first. ' )
s = self . state . capabilities_ls [ ' sasl ' ]
if s is not None :
available = set ( map ( str . lower , s . split ( ' , ' ) ) )
self . sasl_next_mechanisms = [
x for x in self . sasl_next_mechanisms
if x . lower ( ) in available ]
2020-05-01 20:19:53 +02:00
self . tryNextSaslMechanism ( msg )
2015-12-11 23:30:57 +01:00
2014-05-24 15:57:27 +02:00
def doAuthenticate ( self , msg ) :
2019-10-25 23:17:10 +02:00
self . state . fsm . expect_state ( [
IrcStateFsm . States . INIT_SASL ,
IrcStateFsm . States . CONNECTED_SASL ,
] )
2015-12-10 20:08:53 +01:00
if not self . authenticate_decoder :
self . authenticate_decoder = ircutils . AuthenticateDecoder ( )
self . authenticate_decoder . feed ( msg )
if not self . authenticate_decoder . ready :
return # Waiting for other messages
string = self . authenticate_decoder . get ( )
self . authenticate_decoder = None
2015-12-11 10:56:05 +01:00
mechanism = self . sasl_current_mechanism
if mechanism == ' ecdsa-nist256p-challenge ' :
2020-01-23 14:24:41 +01:00
self . _doAuthenticateEcdsa ( string )
2016-09-10 20:16:28 +02:00
elif mechanism == ' external ' :
self . sendSaslString ( b ' ' )
elif mechanism . startswith ( ' scram- ' ) :
step = self . sasl_scram_state [ ' step ' ]
2014-12-27 18:39:38 +01:00
try :
2016-09-10 20:16:28 +02:00
if step == ' uninitialized ' :
2017-01-11 00:10:46 +01:00
log . debug ( ' %s : starting SCRAM. ' ,
self . network )
2020-01-23 14:24:41 +01:00
self . _doAuthenticateScramFirst ( mechanism )
2016-09-10 20:16:28 +02:00
elif step == ' first-sent ' :
2017-01-11 00:10:46 +01:00
log . debug ( ' %s : received SCRAM challenge. ' ,
self . network )
2020-01-23 14:24:41 +01:00
self . _doAuthenticateScramChallenge ( string )
2016-09-10 20:16:28 +02:00
elif step == ' final-sent ' :
2017-01-11 00:10:46 +01:00
log . debug ( ' %s : finishing SCRAM. ' ,
self . network )
2020-01-23 14:24:41 +01:00
self . _doAuthenticateScramFinish ( string )
2016-09-10 20:16:28 +02:00
else :
assert False
except scram . ScramException :
2015-12-10 20:08:53 +01:00
self . sendMsg ( ircmsgs . IrcMsg ( command = ' AUTHENTICATE ' ,
2015-12-11 10:56:05 +01:00
args = ( ' * ' , ) ) )
self . tryNextSaslMechanism ( )
elif mechanism == ' plain ' :
authstring = b ' \0 ' . join ( [
self . sasl_username . encode ( ' utf-8 ' ) ,
self . sasl_username . encode ( ' utf-8 ' ) ,
self . sasl_password . encode ( ' utf-8 ' ) ,
] )
self . sendSaslString ( authstring )
2020-01-23 14:24:41 +01:00
def _doAuthenticateEcdsa ( self , string ) :
2016-09-10 20:16:28 +02:00
if string == b ' ' :
self . sendSaslString ( self . sasl_username . encode ( ' utf-8 ' ) )
return
2019-12-26 12:14:05 +01:00
2016-09-10 20:16:28 +02:00
try :
2019-12-26 12:14:05 +01:00
with open ( self . sasl_ecdsa_key , ' rb ' ) as fd :
private_key = crypto . load_pem_private_key (
fd . read ( ) , password = None , backend = crypto . default_backend ( ) )
authstring = private_key . sign (
string , crypto . ECDSA ( crypto . Prehashed ( crypto . SHA256 ( ) ) ) )
2016-09-10 20:16:28 +02:00
self . sendSaslString ( authstring )
2019-12-26 12:14:05 +01:00
except ( OSError , ValueError ) :
2016-09-10 20:16:28 +02:00
self . sendMsg ( ircmsgs . IrcMsg ( command = ' AUTHENTICATE ' ,
args = ( ' * ' , ) ) )
self . tryNextSaslMechanism ( )
2020-01-23 14:24:41 +01:00
def _doAuthenticateScramFirst ( self , mechanism ) :
2016-09-10 20:16:28 +02:00
""" Handle sending the client-first message of SCRAM auth. """
hash_name = mechanism [ len ( ' scram- ' ) : ]
if hash_name . endswith ( ' -plus ' ) :
hash_name = hash_name [ : - len ( ' -plus ' ) ]
2017-01-11 00:10:46 +01:00
hash_name = hash_name . upper ( )
if hash_name not in scram . HASH_FACTORIES :
log . debug ( ' %s : SCRAM hash %r not supported, aborting. ' ,
self . network , hash_name )
self . tryNextSaslMechanism ( )
return
2016-09-10 20:16:28 +02:00
authenticator = scram . SCRAMClientAuthenticator ( hash_name ,
channel_binding = False )
self . sasl_scram_state [ ' authenticator ' ] = authenticator
client_first = authenticator . start ( {
' username ' : self . sasl_username ,
' password ' : self . sasl_password ,
} )
self . sendSaslString ( client_first )
self . sasl_scram_state [ ' step ' ] = ' first-sent '
2020-01-23 14:24:41 +01:00
def _doAuthenticateScramChallenge ( self , challenge ) :
2016-09-10 20:16:28 +02:00
client_final = self . sasl_scram_state [ ' authenticator ' ] \
. challenge ( challenge )
2017-01-11 00:10:46 +01:00
self . sendSaslString ( client_final )
2016-09-10 20:16:28 +02:00
self . sasl_scram_state [ ' step ' ] = ' final-sent '
2020-01-23 14:24:41 +01:00
def _doAuthenticateScramFinish ( self , data ) :
2017-01-11 00:10:46 +01:00
try :
res = self . sasl_scram_state [ ' authenticator ' ] \
. finish ( data )
except scram . BadSuccessException as e :
log . warning ( ' %s : SASL authentication failed with SCRAM error: %e ' ,
self . network , e )
self . tryNextSaslMechanism ( )
else :
2018-01-14 22:53:40 +01:00
self . sendSaslString ( b ' ' )
2017-01-11 00:10:46 +01:00
self . sasl_scram_state [ ' step ' ] = ' authenticated '
2016-09-10 20:16:28 +02:00
2015-12-11 10:56:05 +01:00
def do903 ( self , msg ) :
log . info ( ' %s : SASL authentication successful ' , self . network )
2015-12-12 00:52:44 +01:00
self . sasl_authenticated = True
2020-05-01 20:19:53 +02:00
self . state . fsm . on_sasl_auth_finished ( self , msg )
2019-10-25 23:17:10 +02:00
if self . state . fsm . state == IrcStateFsm . States . INIT_CAP_NEGOTIATION :
2020-05-01 20:19:53 +02:00
self . endCapabilityNegociation ( msg )
2015-12-11 10:56:05 +01:00
def do904 ( self , msg ) :
2019-11-02 20:52:58 +01:00
log . warning ( ' %s : SASL authentication failed (mechanism: %s ) ' ,
2019-11-09 18:38:40 +01:00
self . network , self . sasl_current_mechanism )
2020-05-01 20:19:53 +02:00
self . tryNextSaslMechanism ( msg )
2015-12-11 10:56:05 +01:00
def do905 ( self , msg ) :
log . warning ( ' %s : SASL authentication failed because the username or '
' password is too long. ' , self . network )
2020-05-01 20:19:53 +02:00
self . tryNextSaslMechanism ( msg )
2015-12-11 10:56:05 +01:00
def do906 ( self , msg ) :
log . warning ( ' %s : SASL authentication aborted ' , self . network )
2021-05-25 18:59:02 +02:00
self . tryNextSaslMechanism ( msg ) # TODO: should not try this in state INIT_WAITING_MOTD (set when sending CAP END because of exhausted list of SASL mechs)
2015-12-11 10:56:05 +01:00
def do907 ( self , msg ) :
log . warning ( ' %s : Attempted SASL authentication when we were already '
' authenticated. ' , self . network )
2020-05-01 20:19:53 +02:00
self . tryNextSaslMechanism ( msg )
2015-12-11 10:56:05 +01:00
def do908 ( self , msg ) :
log . info ( ' %s : Supported SASL mechanisms: %s ' ,
self . network , msg . args [ 1 ] )
2015-12-11 23:30:57 +01:00
self . filterSaslMechanisms ( set ( msg . args [ 1 ] . split ( ' , ' ) ) )
2014-05-24 17:25:32 +02:00
2015-05-24 12:25:42 +02:00
def doCapAck ( self , msg ) :
if len ( msg . args ) != 3 :
log . warning ( ' Bad CAP ACK from server: %r ' , msg )
return
2015-08-22 10:17:32 +02:00
caps = msg . args [ 2 ] . split ( )
assert caps , ' Empty list of capabilities '
2016-02-21 11:01:31 +01:00
log . debug ( ' %s : Server acknowledged capabilities: % L ' ,
2015-06-12 14:30:50 +02:00
self . network , caps )
self . state . capabilities_ack . update ( caps )
2020-05-01 20:19:53 +02:00
self . capUpkeep ( msg )
2019-10-25 23:17:10 +02:00
2015-05-24 12:25:42 +02:00
def doCapNak ( self , msg ) :
2015-05-15 13:01:26 +02:00
if len ( msg . args ) != 3 :
2015-05-24 12:25:42 +02:00
log . warning ( ' Bad CAP NAK from server: %r ' , msg )
2015-05-15 13:01:26 +02:00
return
2015-08-22 10:17:32 +02:00
caps = msg . args [ 2 ] . split ( )
assert caps , ' Empty list of capabilities '
2015-06-12 14:30:50 +02:00
self . state . capabilities_nak . update ( caps )
2015-08-22 10:17:32 +02:00
log . warning ( ' %s : Server refused capabilities: % L ' ,
2015-06-12 14:30:50 +02:00
self . network , caps )
2020-05-01 20:19:53 +02:00
self . capUpkeep ( msg )
2019-10-25 23:17:10 +02:00
2020-05-01 20:19:53 +02:00
def _onCapSts ( self , policy , msg ) :
2019-12-08 21:25:59 +01:00
secure_connection = self . driver . currentServer . force_tls_verification \
or ( self . driver . ssl and self . driver . anyCertValidationEnabled ( ) )
2019-12-08 15:54:48 +01:00
parsed_policy = ircutils . parseStsPolicy (
log , policy , parseDuration = secure_connection )
2019-12-07 23:33:04 +01:00
if parsed_policy is None :
2020-06-20 20:15:28 +02:00
# There was an error (and it was logged). Ignore it and proceed
# with the connection.
# Currently this shouldn't happen, but let's future-proof it, eg.
# in case https://github.com/ircv3/ircv3-specifications/pull/390
# gets adopted.
2019-12-07 23:33:04 +01:00
return
2019-12-08 15:54:48 +01:00
if secure_connection :
2019-12-07 23:33:04 +01:00
# TLS is enabled and certificate is verified; write the STS policy
# in stone.
# For future-proofing (because we don't want to write an invalid
# value), we write the raw policy received from the server instead
# of the parsed one.
2019-12-08 21:25:59 +01:00
log . debug ( ' Storing STS policy: %s ' , policy )
2019-12-07 23:33:04 +01:00
ircdb . networks . getNetwork ( self . network ) . addStsPolicy (
2019-12-08 21:25:59 +01:00
self . driver . currentServer . hostname , policy )
2019-12-08 15:54:48 +01:00
else :
2019-12-08 21:25:59 +01:00
hostname = self . driver . currentServer . hostname
2021-01-11 23:22:21 +01:00
attempt = self . driver . currentServer . attempt
2019-12-08 21:25:59 +01:00
log . info ( ' Got STS policy over insecure connection; '
' reconnecting to secure port. %r ' ,
self . driver . currentServer )
2019-12-08 15:54:48 +01:00
# Reconnect to the server, but with TLS *and* certificate
# validation this time.
2020-05-01 20:19:53 +02:00
self . state . fsm . on_shutdown ( self , msg )
2021-01-11 23:22:21 +01:00
2019-12-08 15:54:48 +01:00
self . driver . reconnect (
2021-01-11 23:22:21 +01:00
server = Server ( hostname , parsed_policy [ ' port ' ] , attempt , True ) ,
2019-12-08 21:25:59 +01:00
wait = True )
2019-12-07 23:33:04 +01:00
2020-05-01 20:19:53 +02:00
def _addCapabilities ( self , capstring , msg ) :
2015-05-24 12:25:42 +02:00
for item in capstring . split ( ) :
while item . startswith ( ( ' = ' , ' ~ ' ) ) :
item = item [ 1 : ]
if ' = ' in item :
( cap , value ) = item . split ( ' = ' , 1 )
2019-12-07 23:33:04 +01:00
if cap == ' sts ' :
2020-05-01 20:19:53 +02:00
self . _onCapSts ( value , msg )
2015-05-24 12:25:42 +02:00
self . state . capabilities_ls [ cap ] = value
else :
2019-12-07 23:33:04 +01:00
if item == ' sts ' :
log . error ( ' Got " sts " capability without value. Aborting '
' connection. ' )
2019-12-08 21:25:59 +01:00
self . driver . reconnect ( wait = True )
2015-05-24 12:25:42 +02:00
self . state . capabilities_ls [ item ] = None
2019-10-25 23:17:10 +02:00
2019-12-08 21:25:59 +01:00
2015-05-24 12:25:42 +02:00
def doCapLs ( self , msg ) :
if len ( msg . args ) == 4 :
# Multi-line LS
if msg . args [ 2 ] != ' * ' :
log . warning ( ' Bad CAP LS from server: %r ' , msg )
return
2020-05-01 20:19:53 +02:00
self . _addCapabilities ( msg . args [ 3 ] , msg )
2015-05-24 12:25:42 +02:00
elif len ( msg . args ) == 3 : # End of LS
2020-05-01 20:19:53 +02:00
self . _addCapabilities ( msg . args [ 2 ] , msg )
2019-12-08 21:25:59 +01:00
if self . state . fsm . state == IrcStateFsm . States . SHUTTING_DOWN :
return
2019-10-25 23:17:10 +02:00
self . state . fsm . expect_state ( [
# Normal case:
IrcStateFsm . States . INIT_CAP_NEGOTIATION ,
# Should only happen if a plugin sends a CAP LS (which they
# shouldn't do):
IrcStateFsm . States . CONNECTED ,
IrcStateFsm . States . CONNECTED_SASL ,
] )
2019-10-25 23:07:31 +02:00
# Normally at this point, self.state.capabilities_ack should be
# empty; but let's just make sure we're not requesting the same
# caps twice for no reason.
2021-03-04 21:30:48 +01:00
want_capabilities = self . REQUEST_CAPABILITIES
if conf . supybot . protocols . irc . experimentalExtensions ( ) :
want_capabilities | = self . REQUEST_EXPERIMENTAL_CAPABILITIES
2019-10-25 23:07:31 +02:00
new_caps = (
set ( self . state . capabilities_ls ) &
2021-03-04 21:30:48 +01:00
want_capabilities -
2019-10-25 23:07:31 +02:00
self . state . capabilities_ack )
2015-06-12 14:30:50 +02:00
# NOTE: Capabilities are requested in alphabetic order, because
# sets are unordered, and their "order" is nondeterministic.
# This is needed for the tests.
2019-10-25 23:07:31 +02:00
if new_caps :
self . _requestCaps ( new_caps )
2015-09-05 13:14:05 +02:00
else :
2020-05-01 20:19:53 +02:00
self . endCapabilityNegociation ( msg )
2015-05-24 12:25:42 +02:00
else :
log . warning ( ' Bad CAP LS from server: %r ' , msg )
return
2019-10-25 23:17:10 +02:00
2015-08-22 20:25:39 +02:00
def doCapDel ( self , msg ) :
if len ( msg . args ) != 3 :
log . warning ( ' Bad CAP DEL from server: %r ' , msg )
return
caps = msg . args [ 2 ] . split ( )
assert caps , ' Empty list of capabilities '
for cap in caps :
# The spec says "If capability negotiation 3.2 was used, extensions
# listed MAY contain values." for CAP NEW and CAP DEL
cap = cap . split ( ' = ' ) [ 0 ]
try :
del self . state . capabilities_ls [ cap ]
except KeyError :
pass
try :
2015-12-12 00:52:44 +01:00
self . state . capabilities_ack . remove ( cap )
2015-08-22 20:25:39 +02:00
except KeyError :
pass
2019-10-25 23:17:10 +02:00
2015-12-12 00:52:44 +01:00
def doCapNew ( self , msg ) :
2019-10-25 23:17:10 +02:00
# Note that in theory, this method may be called at any time, even
# before CAP END (or even before the initial CAP LS).
2015-12-12 00:52:44 +01:00
if len ( msg . args ) != 3 :
log . warning ( ' Bad CAP NEW from server: %r ' , msg )
return
caps = msg . args [ 2 ] . split ( )
assert caps , ' Empty list of capabilities '
2020-05-01 20:19:53 +02:00
self . _addCapabilities ( msg . args [ 2 ] , msg )
2019-12-08 21:25:59 +01:00
if self . state . fsm . state == IrcStateFsm . States . SHUTTING_DOWN :
return
2015-12-12 00:52:44 +01:00
common_supported_unrequested_capabilities = (
set ( self . state . capabilities_ls ) &
self . REQUEST_CAPABILITIES -
self . state . capabilities_ack )
if common_supported_unrequested_capabilities :
2019-10-25 23:07:31 +02:00
self . _requestCaps ( common_supported_unrequested_capabilities )
def _requestCaps ( self , caps ) :
2020-05-07 21:00:16 +02:00
caps = list ( sorted ( caps ) )
cap_lines = [ ]
if ' echo-message ' in caps \
and ' labeled-response ' not in self . state . capabilities_ack :
# Make sure echo-message is never requested unless we either have
# labeled-response already, or we request it *on the same line*
# so they are both accepted or both rejected). The reason for this
# is that this is required to properly deal with PRIVMSGs sent to
# oneself.
# See "When a client sends a private message to its own nick" at
# <https://ircv3.net/specs/extensions/labeled-response>
caps . remove ( ' echo-message ' )
if ' labeled-response ' in caps :
caps . remove ( ' labeled-response ' )
# This makes sure they are always on the same line (which
# happens to be the first):
caps = [ ' echo-message ' , ' labeled-response ' ] + caps
2020-05-18 20:50:14 +02:00
self . state . capabilities_req | = set ( caps )
2020-05-07 21:00:16 +02:00
caps = ' ' . join ( caps )
2019-10-25 23:07:31 +02:00
# textwrap works here because in ASCII, all chars are 1 bytes:
2020-05-07 18:34:18 +02:00
cap_lines = textwrap . wrap (
caps , MAX_LINE_SIZE - len ( ' CAP REQ : ' ) ,
break_long_words = False , break_on_hyphens = False )
2019-10-25 23:07:31 +02:00
for cap_line in cap_lines :
2015-12-12 00:52:44 +01:00
self . sendMsg ( ircmsgs . IrcMsg ( command = ' CAP ' ,
2019-10-25 23:07:31 +02:00
args = ( ' REQ ' , cap_line ) ) )
2015-01-18 20:35:14 +01:00
2015-05-15 19:24:24 +02:00
def monitor ( self , targets ) :
""" Increment a counter of how many callbacks monitor each target;
and send a MONITOR + to the server if the target is not yet
monitored . """
if isinstance ( targets , str ) :
targets = [ targets ]
not_yet_monitored = set ( )
for target in targets :
if target in self . monitoring :
self . monitoring [ target ] + = 1
else :
not_yet_monitored . add ( target )
self . monitoring [ target ] = 1
if not_yet_monitored :
self . queueMsg ( ircmsgs . monitor ( ' + ' , not_yet_monitored ) )
return not_yet_monitored
def unmonitor ( self , targets ) :
""" Decrements a counter of how many callbacks monitor each target;
and send a MONITOR - to the server if the counter drops to 0. """
if isinstance ( targets , str ) :
targets = [ targets ]
should_be_unmonitored = set ( )
for target in targets :
self . monitoring [ target ] - = 1
if self . monitoring [ target ] == 0 :
del self . monitoring [ target ]
should_be_unmonitored . add ( target )
if should_be_unmonitored :
self . queueMsg ( ircmsgs . monitor ( ' - ' , should_be_unmonitored ) )
return should_be_unmonitored
2005-01-19 14:14:38 +01:00
def _getNextNick ( self ) :
if self . alternateNicks :
nick = self . alternateNicks . pop ( 0 )
if ' %s ' in nick :
2011-10-27 12:31:37 +02:00
network_nick = conf . supybot . networks . get ( self . network ) . nick ( )
if network_nick == ' ' :
nick % = conf . supybot . nick ( )
else :
nick % = network_nick
2019-01-27 09:10:06 +01:00
if nick not in self . triedNicks :
self . triedNicks . add ( nick )
return nick
nick = conf . supybot . nick ( )
network_nick = conf . supybot . networks . get ( self . network ) . nick ( )
if network_nick != ' ' :
nick = network_nick
ret = nick
L = list ( nick )
while len ( L ) < = 3 :
L . append ( ' ` ' )
while ret in self . triedNicks :
L [ random . randrange ( len ( L ) ) ] = utils . iter . choice ( ' 0123456789 ' )
ret = ' ' . join ( L )
self . triedNicks . add ( ret )
return ret
2005-01-19 14:14:38 +01:00
def do002 ( self , msg ) :
""" Logs the ircd version. """
( beginning , version ) = rsplit ( msg . args [ - 1 ] , maxsplit = 1 )
log . info ( ' Server %s has version %s ' , self . server , version )
def doPing ( self , msg ) :
""" Handles PING messages. """
self . sendMsg ( ircmsgs . pong ( msg . args [ 0 ] ) )
def doPong ( self , msg ) :
""" Handles PONG messages. """
self . outstandingPing = False
2020-05-01 20:19:00 +02:00
def do375 ( self , msg ) :
self . state . fsm . on_start_motd ( self , msg )
log . info ( ' Got start of MOTD from %s ' , self . server )
2005-01-19 14:14:38 +01:00
def do376 ( self , msg ) :
2020-05-01 20:19:53 +02:00
self . state . fsm . on_end_motd ( self , msg )
2005-01-19 14:14:38 +01:00
log . info ( ' Got end of MOTD from %s ' , self . server )
self . afterConnect = True
# Let's reset nicks in case we had to use a weird one.
self . alternateNicks = conf . supybot . nick . alternates ( ) [ : ]
2021-02-27 18:07:23 +01:00
self . _setUmodes ( )
do377 = do422 = do376
def _setUmodes ( self ) :
# Get the configured umodes
2012-08-06 15:59:00 +02:00
umodes = conf . supybot . networks . get ( self . network ) . umodes ( )
if umodes == ' ' :
umodes = conf . supybot . protocols . irc . umodes ( )
2021-02-27 18:07:23 +01:00
# Add the bot mode if the server advertizes one;
# and if the configured umode doesn't already have it
# explicitly set or unset
bot_mode = self . state . supported . get ( " BOT " )
if bot_mode and len ( bot_mode ) == 1 :
if bot_mode not in umodes :
umodes + = " + " + bot_mode
# Filter out umodes not supported by the server
2011-11-11 23:50:55 +01:00
supported = self . state . supported . get ( ' umodes ' )
2014-08-04 16:26:08 +02:00
if supported :
acceptedchars = supported . union ( ' +- ' )
umodes = ' ' . join ( [ m for m in umodes if m in acceptedchars ] )
2021-02-27 18:07:23 +01:00
# Send the umodes
2005-01-19 14:14:38 +01:00
if umodes :
log . info ( ' Sending user modes to %s : %s ' , self . network , umodes )
self . sendMsg ( ircmsgs . mode ( self . nick , umodes ) )
2005-05-18 07:23:38 +02:00
def do43x ( self , msg , problem ) :
2005-01-19 14:14:38 +01:00
if not self . afterConnect :
2021-03-04 21:29:23 +01:00
self . triedNicks . add ( self . nick )
2005-01-19 14:14:38 +01:00
newNick = self . _getNextNick ( )
2021-03-04 21:30:48 +01:00
assert newNick != self . nick , \
( self . nick , self . alternateNicks , self . triedNicks )
2015-03-26 06:33:04 +01:00
log . info ( ' Got %s : %s %s . Trying %s . ' ,
msg . command , self . nick , problem , newNick )
2005-01-19 14:14:38 +01:00
self . sendMsg ( ircmsgs . nick ( newNick ) )
2015-03-26 06:33:04 +01:00
def do437 ( self , msg ) :
self . do43x ( msg , ' is temporarily unavailable ' )
2005-05-18 07:23:38 +02:00
def do433 ( self , msg ) :
self . do43x ( msg , ' is in use ' )
def do432 ( self , msg ) :
self . do43x ( msg , ' is not a valid nickname ' )
2005-01-19 14:14:38 +01:00
def doJoin ( self , msg ) :
if msg . nick == self . nick :
channel = msg . args [ 0 ]
2018-01-16 16:21:41 +01:00
self . queueMsg ( ircmsgs . who ( channel , args = ( ' % tuhnairf,1 ' , ) ) ) # Ends with 315.
2005-01-19 14:14:38 +01:00
self . queueMsg ( ircmsgs . mode ( channel ) ) # Ends with 329.
2012-12-26 15:37:52 +01:00
for channel in msg . args [ 0 ] . split ( ' , ' ) :
self . queueMsg ( ircmsgs . mode ( channel , ' +b ' ) )
2005-01-19 14:14:38 +01:00
self . startedSync [ channel ] = time . time ( )
def do315 ( self , msg ) :
channel = msg . args [ 1 ]
if channel in self . startedSync :
now = time . time ( )
started = self . startedSync . pop ( channel )
elapsed = now - started
log . info ( ' Join to %s on %s synced in %.2f seconds. ' ,
channel , self . network , elapsed )
def doError ( self , msg ) :
""" Handles ERROR messages. """
2009-03-12 20:04:22 +01:00
log . warning ( ' Error message from %s : %s ' , self . network , msg . args [ 0 ] )
2005-01-19 14:14:38 +01:00
if not self . zombie :
2015-09-28 04:18:15 +02:00
if msg . args [ 0 ] . lower ( ) . startswith ( ' closing link ' ) :
2005-01-19 14:14:38 +01:00
self . driver . reconnect ( )
elif ' too fast ' in msg . args [ 0 ] : # Connecting too fast.
self . driver . reconnect ( wait = True )
def doNick ( self , msg ) :
""" Handles NICK messages. """
if msg . nick == self . nick :
newNick = msg . args [ 0 ]
self . nick = newNick
( nick , user , domain ) = ircutils . splitHostmask ( msg . prefix )
self . prefix = ircutils . joinHostmask ( self . nick , user , domain )
elif conf . supybot . followIdentificationThroughNickChanges ( ) :
# We use elif here because this means it's someone else's nick
# change, not our own.
try :
id = ircdb . users . getUserId ( msg . prefix )
u = ircdb . users . getUser ( id )
except KeyError :
return
if u . auth :
( _ , user , host ) = ircutils . splitHostmask ( msg . prefix )
newhostmask = ircutils . joinHostmask ( msg . args [ 0 ] , user , host )
for ( i , ( when , authmask ) ) in enumerate ( u . auth [ : ] ) :
if ircutils . strEqual ( msg . prefix , authmask ) :
log . info ( ' Following identification for %s : %s -> %s ' ,
u . name , authmask , newhostmask )
u . auth [ i ] = ( u . auth [ i ] [ 0 ] , newhostmask )
ircdb . users . setUser ( u )
def _reallyDie ( self ) :
""" Makes the Irc object die. Dead. """
2005-05-18 06:18:35 +02:00
log . info ( ' Irc object for %s dying. ' , self . network )
2005-01-19 14:14:38 +01:00
# XXX This hasattr should be removed, I'm just putting it here because
# we're so close to a release. After 0.80.0 we should remove this
# and fix whatever AttributeErrors arise in the drivers themselves.
if self . driver is not None and hasattr ( self . driver , ' die ' ) :
self . driver . die ( )
if self in world . ircs :
world . ircs . remove ( self )
# Only kill the callbacks if we're the last Irc.
if not world . ircs :
for cb in self . callbacks :
cb . die ( )
# If we shared our list of callbacks, this ensures that
# cb.die() is only called once for each callback. It's
# not really necessary since we already check to make sure
# we're the only Irc object, but a little robustitude never
# hurt anybody.
log . debug ( ' Last Irc, clearing callbacks. ' )
self . callbacks [ : ] = [ ]
else :
2005-02-15 14:57:57 +01:00
log . warning ( ' Irc object killed twice: %s ' , utils . stackTrace ( ) )
2005-01-19 14:14:38 +01:00
def __hash__ ( self ) :
return id ( self )
def __eq__ ( self , other ) :
2005-03-28 15:00:37 +02:00
# We check isinstance here, so that if some proxy object (like those
# defined in callbacks.py) has overridden __eq__, it takes precedence.
if isinstance ( other , self . __class__ ) :
return id ( self ) == id ( other )
else :
2012-08-04 13:25:47 +02:00
return other . __eq__ ( self )
2005-01-19 14:14:38 +01:00
def __ne__ ( self , other ) :
return not ( self == other )
def __str__ ( self ) :
return ' Irc object for %s ' % self . network
def __repr__ ( self ) :
return ' <irclib.Irc object for %s > ' % self . network
2006-02-11 16:52:51 +01:00
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: