#!/usr/bin/env python #Copyright 2004,2005,2006 Sebastian Hagen # This file is part of eucharis. # eucharis is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation # eucharis is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with eucharis; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA import socket import select import re import string import logging import socket_management import input_handlers class irc_base_connection(socket_management.sock_stream_connection_linebased): def __init__(self, address, input_handler, close_handler, parent_loggername=''): self.loggername = parent_loggername + '.irc_base' self.logger = logging.getLogger(self.loggername) socket_management.sock_stream_connection_linebased.__init__(self, line_delimiters=['\n', '\r']) self.input_handler = self.input_process_irc self.input_handler_parent = input_handler self.close_handler = self.connection_close_handle self.close_handler_parent = close_handler self.server = { 'address':address } self.regular_expressions = { 'line_full':re.compile("\n$") } self.connection_init(address) self.limits_linelength = { 'PRIVMSG':95, 'NOTICE':95 } def connection_close_handle(self, fd): self.input_process_irc() self.close_handler_parent(self) def input_process_irc(self): """Process input lines in the buffer, and pass results on to the input_handler defined for this instance.""" for line in self.buffer_lines: if ((line == '') or (line.isspace())): continue self.logger.log(10, '> %r' % (line,)) line_data = { 'source':None, 'command':None, 'arguments':[], } line_split = line.split(None,1) if (len(line_split) > 0): if (line_split[0][0] in (':', '@')): line_data['source'] = line_split.pop(0) if (len(line_split) > 0): line_split = line_split[0].split(None,1) if (len(line_split) > 0): line_data['command'] = line_split.pop(0).upper() if (len(line_split) > 0): argumentstring = line_split[0] while (len(argumentstring) > 0): arguments_split = argumentstring.split(None,1) if (len(arguments_split) < 1): break elif (arguments_split[0][0] == ':'): line_data['arguments'].append(argumentstring[1:]) argumentstring = '' break elif (len(arguments_split) < 2): line_data['arguments'].append(arguments_split[0]) argumentstring = '' break else: line_data['arguments'].append(arguments_split[0]) argumentstring = arguments_split[1] if (((line_data['command'] == 'PRIVMSG') or (line_data['command'] == 'NOTICE')) and (len(line_data['arguments']) > 0)): #PRIVMSG/NOTICE dequoting according to http://www.irchelp.org/irchelp/rfc/ctcpspec.html #Hopefully servers won't try to send \020 directly for anything important. If clients choose #to do it, it's their problem if they don't adhere to this pseduo-standard. Nevertheless, #it might be a good idea to avoid using it in triggers. input_mapping = { '0':'\000', 'n':'\n', 'r':'\015', '\020':'\020' } text = line_data['arguments'][-1] offset = text.find('\020') while (offset >= 0): if (len(text) > offset + 1): escaped_character = text[offset + 1] if escaped_character in input_mapping: text = text[:offset] + input_mapping[escaped_character] + text[offset + 2:] else: text = text[:offset] + text[offset + 1:] else: text = text[:-1] break offset = text.find('\020', offset + 1) line_data['arguments'][-1] = text self.input_handler_parent(self, line_data) self.buffer_lines = [] def send_command(self, source=None, command=None, arguments=[]): """Send a commandline over the irc connection.""" output_string = '' if (source != None): output_string = ':' + source + ' ' if (command != None): output_string = output_string + command + ' ' if (((command == 'PRIVMSG') or (command == 'NOTICE')) and (len(arguments) >= 2)): output_mapping = { '\020':'\020\020', '\000':'\0200', '\n':'\020n', '\015':'\020r', } for character in output_mapping: arguments[-1] = string.replace(arguments[-1], character, output_mapping[character]) #if ((command in self.limits_linelength) and (((source == None) and (self.limits_linelength[command] < len(text) + 3)) or ((self.limits_linelength[command] < len(source) + len(text) + 3) and (self.limits_linelength[command] > len(source) + 3)))): # if ((source == None) and (self.limits_linelength[command] < len(text) + 3)): # limit_linelength_effective = self.limits_linelength[command] - 3 # # elif ((self.limits_linelength[command] < len(source) + len(text) + 3) and (self.limits_linelength[command] > len(source) + 3)): # limit_linelength_effective = self.limits_linelength[command] - 3 - len(source) # # else: # raise StandardError('Unable to decide how to split this commandline up. Dump: source: %s command: %s arguments: %s text: %s' % (str(source), str(command), str(arguments), str(text))) # if (limit_linelength_effective <= 0): # raise StandardError('Calculated incorrect effective linelength limit %f For commandline %s.' % (limit_linelength_effective, str(source), str(command), str(arguments), str(text))) # text_remaining = text # output_string_prefix = output_string # output_string = '' # while (len(text_remaining) > 0): # output_string = output_string + output_string_prefix + ':' + text_remaining[:limit_linelength_effective] + '\n' # text_remaining = text_remaining[limit_linelength_effective:] #else: if (len(arguments) > 0): arguments_strings = [] for argument in arguments[:]: arguments_strings.append(str(argument)) if (' ' in arguments_strings[-1]): arguments_strings[-1] = ':' + arguments_strings[-1] output_string = output_string + ' '.join(arguments_strings) output_string = output_string.rstrip('\n').rstrip('\r') + '\n' self.send_data(output_string) def clean_up(self): self.send_command(command='ERROR', arguments=['Shutting down connection.']) socket_management.sock_stream_connection_linebased.clean_up(self) class irc_logger_filter(logging.Filter): def filter(self, record): source_loggername_split = record.name.split('.') if ('irc_base' in source_loggername_split): return False else: return True class irc_logger_handler(logging.Handler): def __init__(self, irc_output, targets, level=20): logging.Handler.__init__(self, level=level) self.irc_output = irc_output self.targets = targets self.default_formatter = logging.Formatter('%(message)s','') self.setFormatter(self.default_formatter) self.addFilter(irc_logger_filter()) def emit(self, record): output_string = self.format(record) for target in self.targets: for line in output_string.split('\n'): socket_management.timers_add(delay=-1000, callback_handler=self.irc_output, kwargs={'command':'PRIVMSG', 'arguments':[target, line]}) class handlers_irc(input_handlers.handlers_base): def __init__(self, parent, output, event_types, parent_loggername=''): input_handlers.handlers_base.__init__(self, parent=parent, output=output, event_types=event_types, loggername=parent_loggername + '.handlers') self.send_command = output self.settings_network = {} self.settings_network['chantypes'] = [] def output(self, source=None, command=None, arguments=[]): """Send output, if possible. Otherwise, silently discard it.""" if (self.send_command != None): self.send_command(source=source, command=command, arguments=arguments) def event_connection(self, statechange, data): self.handle_event(event_type='statechange_connection', event=statechange, data=data) def event_nicks(self, statechange, data): self.handle_event(event_type='statechange_nicks', event=statechange, data=data) def event_channels(self, statechange, data): self.handle_event(event_type='statechange_channels', event=statechange, data=data) def process_input(self, source, command, arguments): """Process input, and trigger any matching handlers.""" input_data = { 'source':source, 'command':command, 'arguments':arguments, } self.handle_event(event_type='command', event=command, data=input_data) if ((command == 'PRIVMSG') and (len(arguments) >= 2) and (len(arguments[-1]) > 0)): #This implements ctcp-level quoting and splitting of messages according to the protocol specification at #http://www.irchelp.org/irchelp/rfc/ctcpspec.html delimiter_index_odd = arguments[-1].find('\001') ctcp_messages = [] #Separate ctcp-messages from the normal message text. while (delimiter_index_odd >= 0): delimiter_index_even = arguments[-1].find('\001', delimiter_index_odd + 1) if (delimiter_index_even >= 0): ctcp_messages.append(arguments[-1][delimiter_index_odd+1:delimiter_index_even]) if (delimiter_index_even == 0): arguments[-1] = arguments[-1][delimiter_index_even + 1:] else: arguments[-1] = arguments[-1][:delimiter_index_odd] + arguments[-1][delimiter_index_even + 1:] delimiter_index_odd = arguments[-1].find('\001') else: break #Perform ctcp-level dequoting. ctcp_input_mapping = { '\141':'\001', '\134':'\134' } for index in range(len(ctcp_messages)): offset = ctcp_messages[index].find('\134') while (offset >= 0): if (len(ctcp_messages[index]) > offset + 1): escaped_character = ctcp_messages[index][offset + 1] if escaped_character in ctcp_input_mapping: ctcp_messages[index] = ctcp_messages[index][:offset] + ctcp_input_mapping[escaped_character] + ctcp_messages[index][offset + 2:] else: ctcp_messages[index] = ctcp_messages[index][:offset] + ctcp_messages[index][offset + 1:] else: ctcp_messages[index] = ctcp_messages[index][:-1] break offset = ctcp_messages[index].find('\134', offset+1) for ctcp_message in ctcp_messages: if not ((ctcp_message == '') or (ctcp_message.isspace())): ctcp_data = { 'source':source, } ctcp_message_split = ctcp_message.lstrip().split(' ',1) ctcp_data['command'] = ctcp_message_split[0] if (len(ctcp_message_split) > 1): ctcp_data['text'] = ctcp_message_split[1].lstrip() else: ctcp_data['text'] = '' self.handle_event(event_type='CTCP', event=ctcp_data['command'], data=ctcp_data) text = arguments[-1].strip() valid_command = False if ((len(text) > 0) and (len(arguments) > 0) and (len(arguments[0][0]) > 0)): text_split = text.split() text_command = text_split[0] target = arguments[0] if (target[0] in self.settings_network['chantypes']): target_is_channel = True else: target_is_channel = False text_arguments = [] if ((self.parent.nick_our(text_command)) and (len(text_split) > 1)): valid_command = True text_command = text_split[1] if (len(text_split) > 2): text_arguments = text_split[2:] elif (text_command[0] in self.command_prefixes): valid_command = True if (len(text_split) > 1): text_arguments = text_split[1:] elif not (target_is_channel): valid_command = True if (len(text_split) > 1): text_arguments = text_split[1:] if (text_command[0] in self.command_prefixes): text_command = text_command[1:] if (valid_command): trigger_data = { 'source':source, 'command':text_command, 'arguments':text_arguments } if (target_is_channel): trigger_data['reply_target'] = target else: trigger_data['reply_target'] = source if (source in self.parent.network_nicks): permissions = self.parent.network_nicks[source]['permissions'] else: permissions = [] self.handle_event(event_type='trigger', event=text_command, data=trigger_data, permissions=permissions)