mirror of
				https://github.com/jlu5/PyLink.git
				synced 2025-11-04 00:47:21 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			261 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			261 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
# relay_clientbot.py: Clientbot extensions for Relay
 | 
						|
import string
 | 
						|
import time
 | 
						|
 | 
						|
from pylinkirc import utils, conf, world
 | 
						|
from pylinkirc.log import log
 | 
						|
 | 
						|
# Clientbot default styles:
 | 
						|
# These use template strings as documented @ https://docs.python.org/3/library/string.html#template-strings
 | 
						|
default_styles = {'MESSAGE': '\x02[$netname]\x02 <$mode_prefix$colored_sender> $text',
 | 
						|
                  'KICK': '\x02[$netname]\x02 - $colored_sender$sender_identhost has kicked $target_nick from $channel ($text)',
 | 
						|
                  'PART': '\x02[$netname]\x02 - $colored_sender$sender_identhost has left $channel ($text)',
 | 
						|
                  'JOIN': '\x02[$netname]\x02 - $colored_sender$sender_identhost has joined $channel',
 | 
						|
                  'NICK': '\x02[$netname]\x02 - $colored_sender$sender_identhost is now known as $newnick',
 | 
						|
                  'QUIT': '\x02[$netname]\x02 - $colored_sender$sender_identhost has quit ($text)',
 | 
						|
                  'ACTION': '\x02[$netname]\x02 * $mode_prefix$colored_sender $text',
 | 
						|
                  'NOTICE': '\x02[$netname]\x02 - Notice from $mode_prefix$colored_sender: $text',
 | 
						|
                  'SQUIT': '\x02[$netname]\x02 - Netsplit lost users: $colored_nicks',
 | 
						|
                  'SJOIN': '\x02[$netname]\x02 - Netjoin gained users: $colored_nicks',
 | 
						|
                  'MODE': '\x02[$netname]\x02 - $colored_sender$sender_identhost sets mode $modes on $channel',
 | 
						|
                  'PM': 'PM from $sender on $netname: $text',
 | 
						|
                  'PNOTICE': '<$sender> $text',
 | 
						|
                  }
 | 
						|
 | 
						|
def color_text(s):
 | 
						|
    """
 | 
						|
    Returns a colorized version of the given text based on a simple hash algorithm.
 | 
						|
    """
 | 
						|
    colors = ('03', '04', '05', '06', '07', '08', '09', '10', '11', '12', '13', '15')
 | 
						|
    hash_output = hash(s.encode())
 | 
						|
    num = hash_output % len(colors)
 | 
						|
    return "\x03%s%s\x03" % (colors[num], s)
 | 
						|
 | 
						|
def cb_relay_core(irc, source, command, args):
 | 
						|
    """
 | 
						|
    This function takes Clientbot events and formats them as text to the target channel / user.
 | 
						|
    """
 | 
						|
    real_command = command.split('_')[-1]
 | 
						|
 | 
						|
    relay = world.plugins.get('relay')
 | 
						|
 | 
						|
    private = False
 | 
						|
 | 
						|
    if irc.pseudoclient and relay:
 | 
						|
        try:
 | 
						|
            sourcename = irc.get_friendly_name(source)
 | 
						|
        except KeyError:  # User has left due to /quit
 | 
						|
            sourcename = args['userdata'].nick
 | 
						|
 | 
						|
        relay_conf = conf.conf.get('relay') or {}
 | 
						|
 | 
						|
        # Be less floody on startup: don't relay non-PRIVMSGs for the first X seconds after connect.
 | 
						|
        startup_delay = relay_conf.get('clientbot_startup_delay', 20)
 | 
						|
 | 
						|
        # Special case for CTCPs.
 | 
						|
        if real_command == 'MESSAGE':
 | 
						|
            # CTCP action, format accordingly
 | 
						|
            if (not args.get('is_notice')) and args['text'].startswith('\x01ACTION ') and args['text'].endswith('\x01'):
 | 
						|
                args['text'] = args['text'][8:-1]
 | 
						|
 | 
						|
                real_command = 'ACTION'
 | 
						|
 | 
						|
            elif not irc.is_channel(args['target'].lstrip(''.join(irc.prefixmodes.values()))):
 | 
						|
                # Target is a user; handle this accordingly.
 | 
						|
                if relay_conf.get('allow_clientbot_pms'):
 | 
						|
                    real_command = 'PNOTICE' if args.get('is_notice') else 'PM'
 | 
						|
                    private = True
 | 
						|
 | 
						|
            # Other CTCPs are ignored
 | 
						|
            elif args['text'].startswith('\x01'):
 | 
						|
                return
 | 
						|
 | 
						|
            elif args.get('is_notice'):  # Different syntax for notices
 | 
						|
                real_command = 'NOTICE'
 | 
						|
        elif (time.time() - irc.start_ts) < startup_delay:
 | 
						|
            log.debug('(%s) relay_cb_core: Not relaying %s because of startup delay of %s.', irc.name,
 | 
						|
                      real_command, startup_delay)
 | 
						|
            return
 | 
						|
 | 
						|
        # Try to fetch the format for the given command from the relay:clientbot_styles:$command
 | 
						|
        # key, falling back to one defined in default_styles above, and then nothing if not found
 | 
						|
        # there.
 | 
						|
        text_template = (relay_conf.get('clientbot_styles') or {}).get(real_command,
 | 
						|
                                                                       default_styles.get(real_command, ''))
 | 
						|
        text_template = string.Template(text_template)
 | 
						|
 | 
						|
        if text_template:
 | 
						|
            if irc.get_service_bot(source):
 | 
						|
                # HACK: service bots are global and lack the relay state we look for.
 | 
						|
                # just pretend the message comes from the current network.
 | 
						|
                log.debug('(%s) relay_cb_core: Overriding network origin to local (source=%s)', irc.name, source)
 | 
						|
                sourcenet = irc.name
 | 
						|
                realsource = source
 | 
						|
            else:
 | 
						|
                # Get the original client that the relay client source was meant for.
 | 
						|
                log.debug('(%s) relay_cb_core: Trying to find original sender (user) for %s', irc.name, source)
 | 
						|
                try:
 | 
						|
                    origuser = relay.get_orig_user(irc, source) or args['userdata'].remote
 | 
						|
                except (AttributeError, KeyError):
 | 
						|
                    log.debug('(%s) relay_cb_core: Trying to find original sender (server) for %s. serverdata=%s', irc.name, source, args.get('serverdata'))
 | 
						|
                    try:
 | 
						|
                        localsid = args.get('serverdata') or irc.servers[source]
 | 
						|
                        origuser = (localsid.remote, world.networkobjects[localsid.remote].uplink)
 | 
						|
                    except (AttributeError, KeyError):
 | 
						|
                        return
 | 
						|
 | 
						|
                log.debug('(%s) relay_cb_core: Original sender found as %s', irc.name, origuser)
 | 
						|
                sourcenet, realsource = origuser
 | 
						|
 | 
						|
            try:  # Try to get the full network name
 | 
						|
                netname = conf.conf['servers'][sourcenet]['netname']
 | 
						|
            except KeyError:
 | 
						|
                netname = sourcenet
 | 
						|
 | 
						|
            # Figure out where the message is destined to.
 | 
						|
            stripped_target = target = args.get('channel') or args.get('target')
 | 
						|
            if target is not None:
 | 
						|
                # HACK: cheap fix to prevent @#channel messages from interpreted as non-channel specific
 | 
						|
                stripped_target = target.lstrip(''.join(irc.prefixmodes.values()))
 | 
						|
 | 
						|
            if target is None or not (irc.is_channel(stripped_target) or private):
 | 
						|
                # Non-channel specific message (e.g. QUIT or NICK). If this isn't a PM, figure out
 | 
						|
                # all channels that the sender shares over the relay, and relay them to those
 | 
						|
                # channels.
 | 
						|
                userdata = args.get('userdata') or irc.users.get(source)
 | 
						|
                if not userdata:
 | 
						|
                    # No user data given. This was probably some other global event such as SQUIT.
 | 
						|
                    userdata = irc.pseudoclient
 | 
						|
 | 
						|
                targets = [channel for channel in userdata.channels if relay.get_relay(irc, channel)]
 | 
						|
            else:
 | 
						|
                # Pluralize the channel so that we can iterate over it.
 | 
						|
                targets = [target]
 | 
						|
                args['channel'] = stripped_target
 | 
						|
            log.debug('(%s) relay_cb_core: Relaying event %s to channels: %s', irc.name, real_command, targets)
 | 
						|
 | 
						|
            identhost = ''
 | 
						|
            if source in irc.users:
 | 
						|
                try:
 | 
						|
                    identhost = irc.get_hostmask(source).split('!')[-1]
 | 
						|
                except KeyError:  # User got removed due to quit
 | 
						|
                    identhost = '%s@%s' % (args['olduser'].ident, args['olduser'].host)
 | 
						|
                # This is specifically spaced so that ident@host is only shown for users that have
 | 
						|
                # one, and not servers.
 | 
						|
                identhost = ' (%s)' % identhost
 | 
						|
 | 
						|
            # $target_nick: Convert the target for kicks, etc. from a UID to a nick
 | 
						|
            if args.get("target") in irc.users:
 | 
						|
                args["target_nick"] = irc.get_friendly_name(args['target'])
 | 
						|
 | 
						|
            # Join up modes from their list form
 | 
						|
            if args.get('modes'):
 | 
						|
                args['modes'] = irc.join_modes(args['modes'])
 | 
						|
 | 
						|
            mode_prefix = ''
 | 
						|
            if 'channel' in args:
 | 
						|
                # Display the real (remote) channel name instead of the local one, if applicable.
 | 
						|
                args['local_channel'] = args['channel']
 | 
						|
                log.debug('(%s) relay_clientbot: coersing $channel from %s to %s', irc.name, args['local_channel'], args['channel'])
 | 
						|
 | 
						|
                sourceirc = world.networkobjects.get(sourcenet)
 | 
						|
                log.debug('(%s) relay_clientbot: Checking prefix modes for %s on %s (relaying to %s)',
 | 
						|
                          irc.name, realsource, sourcenet, args['channel'])
 | 
						|
                if sourceirc:
 | 
						|
                    args['channel'] = remotechan = relay.get_remote_channel(irc, sourceirc, args['channel'])
 | 
						|
                    if source in irc.users and remotechan in sourceirc.channels and \
 | 
						|
                            realsource in sourceirc.channels[remotechan].users:
 | 
						|
                        # Fetch the prefixmode prefixes (e.g. ~@%) for the sender, if available.
 | 
						|
                        prefixmodes = sourceirc.channels[remotechan].get_prefix_modes(realsource)
 | 
						|
                        log.debug('(%s) relay_clientbot: got prefix modes %s for %s on %s@%s',
 | 
						|
                                  irc.name, prefixmodes, realsource, remotechan, sourcenet)
 | 
						|
                        if prefixmodes:
 | 
						|
                            # Only pick the highest prefix.
 | 
						|
                            mode_prefix = sourceirc.prefixmodes.get(
 | 
						|
                                sourceirc.cmodes.get(prefixmodes[0]))
 | 
						|
 | 
						|
            args.update({
 | 
						|
                'netname': netname, 'sender': sourcename, 'sender_identhost': identhost,
 | 
						|
                'colored_sender': color_text(sourcename), 'colored_netname': color_text(netname),
 | 
						|
                'mode_prefix': mode_prefix
 | 
						|
            })
 | 
						|
 | 
						|
            for target in targets:
 | 
						|
                cargs = args.copy()  # Copy args list to manipulate them in a channel specific way
 | 
						|
 | 
						|
                # $nicks / $colored_nicks: used when the event affects multiple users, such as SJOIN or SQUIT.
 | 
						|
                # For SJOIN, this is simply a list of nicks. For SQUIT, this is sent as a dict
 | 
						|
                # mapping channels to lists of nicks, as netsplits aren't channel specific but
 | 
						|
                # still have to be relayed as such.
 | 
						|
                nicklist = args.get('nicks')
 | 
						|
                if nicklist:
 | 
						|
                    # Get channel-specific nick list if relevent.
 | 
						|
                    if isinstance(nicklist, dict):
 | 
						|
                        nicklist = nicklist.get(target, [])
 | 
						|
 | 
						|
                    # Ignore if no nicks are affected on the channel.
 | 
						|
                    if not nicklist:
 | 
						|
                        continue
 | 
						|
 | 
						|
                    colored_nicks = [color_text(nick) for nick in nicklist]
 | 
						|
 | 
						|
                    # Join both the nicks and colored_nicks fields into a comma separated string.
 | 
						|
                    cargs['nicks'] = ', '.join(nicklist)
 | 
						|
                    cargs['colored_nicks'] = ', '.join(colored_nicks)
 | 
						|
 | 
						|
                text = text_template.safe_substitute(cargs)
 | 
						|
                # PMs are always sent as notice - this prevents unknown command loops with bots.
 | 
						|
                irc.msg(target, text, loopback=False, notice=private)
 | 
						|
 | 
						|
utils.add_hook(cb_relay_core, 'CLIENTBOT_MESSAGE')
 | 
						|
utils.add_hook(cb_relay_core, 'CLIENTBOT_KICK')
 | 
						|
utils.add_hook(cb_relay_core, 'CLIENTBOT_PART')
 | 
						|
utils.add_hook(cb_relay_core, 'CLIENTBOT_JOIN')
 | 
						|
utils.add_hook(cb_relay_core, 'CLIENTBOT_QUIT')
 | 
						|
utils.add_hook(cb_relay_core, 'CLIENTBOT_NICK')
 | 
						|
utils.add_hook(cb_relay_core, 'CLIENTBOT_SJOIN')
 | 
						|
utils.add_hook(cb_relay_core, 'CLIENTBOT_SQUIT')
 | 
						|
utils.add_hook(cb_relay_core, 'RELAY_RAW_MODE')
 | 
						|
 | 
						|
@utils.add_cmd
 | 
						|
def rpm(irc, source, args):
 | 
						|
    """<target> <text>
 | 
						|
 | 
						|
    Sends PMs to users over the relay, if Clientbot PMs are enabled.
 | 
						|
    """
 | 
						|
 | 
						|
    try:
 | 
						|
        target = args[0]
 | 
						|
        text = ' '.join(args[1:])
 | 
						|
    except IndexError:
 | 
						|
        irc.error('Not enough arguments. Needs 2: target nick and text.')
 | 
						|
        return
 | 
						|
 | 
						|
    relay = world.plugins.get('relay')
 | 
						|
    if irc.has_cap('can-spawn-clients'):
 | 
						|
        irc.error('This command is only supported on Clientbot networks. Try /msg %s <text>' % target)
 | 
						|
        return
 | 
						|
    elif relay is None:
 | 
						|
        irc.error('PyLink Relay is not loaded.')
 | 
						|
        return
 | 
						|
    elif not text:
 | 
						|
        irc.error('No text given.')
 | 
						|
        return
 | 
						|
    elif not conf.conf.get('relay').get('allow_clientbot_pms'):
 | 
						|
        irc.error('Private messages with users connected via Clientbot have been '
 | 
						|
                  'administratively disabled.')
 | 
						|
        return
 | 
						|
 | 
						|
    uid = irc.nick_to_uid(target)
 | 
						|
    if not uid:
 | 
						|
        irc.error('Unknown user %s.' % target)
 | 
						|
        return
 | 
						|
    elif not relay.is_relay_client(irc, uid):
 | 
						|
        irc.error('%s is not a relay user.' % target)
 | 
						|
        return
 | 
						|
    else:
 | 
						|
        assert not irc.is_internal_client(source), "rpm is not allowed from PyLink bots"
 | 
						|
        # Send the message through relay by faking a hook for its handler.
 | 
						|
        relay.handle_messages(irc, source, 'RELAY_CLIENTBOT_PRIVMSG', {'target': uid, 'text': text})
 | 
						|
        irc.reply('Message sent.')
 |