def __init__( self, server_list, nickname, realname, reconnection_interval=missing, recon=ExponentialBackoff(), **connect_params): super(SingleServerIRCBot, self).__init__() self.__connect_params = connect_params self.channels = IRCDict() self.server_list = [ ServerSpec(*server) if isinstance(server, (tuple, list)) else server for server in server_list ] assert all( isinstance(server, ServerSpec) for server in self.server_list ) self.recon = recon # for compatibility if reconnection_interval is not missing: warnings.warn( "reconnection_interval is deprecated; " "pass a ReconnectStrategy object instead") self.recon = ExponentialBackoff(min_interval=reconnection_interval) self._nickname = nickname self._realname = realname for i in ["disconnect", "join", "kick", "mode", "namreply", "nick", "part", "quit"]: self.connection.add_global_handler( i, getattr(self, "_on_" + i), -20)
def __init__(self, channel, nickname, server, port=6667, **connect_params): super(IRCClient, self).__init__() if server is None: raise Exception("IRC Server not provided") if nickname is None or " " in nickname: raise Exception("Invalid nickname: must be one word") if channel is None or " " in channel or "#" not in channel: raise Exception("Invalid channel: must be a word starting with #") self.__connect_params = connect_params self.channels = IRCDict() self.channel = channel self.server = ServerSpec(server, port) self._nickname = nickname self.clientname = socket.gethostname().split(".")[0] if 0 <= self.min_reconnect_wait: raise Exception("Minimum reconnect wait must be positive number") if self.min_reconnect_wait <= self.max_reconnect_wait: raise Exception( "Maximum reconnect wait must be larger than minimum") self._check_scheduled = False # Global handlers to handle channel/nick associations # Mostly when a nick is already in use for i in [ "disconnect", "join", "kick", "mode", "namreply", "nick", "part", "quit", ]: self.connection.add_global_handler(i, getattr(self, "_on_" + i), -20)
def __init__(self): self.userdict = IRCDict() self.operdict = IRCDict() self.voiceddict = IRCDict() self.ownerdict = IRCDict() self.halfopdict = IRCDict() self.modes = {}
def __init__(self, server_list, nickname, realname, reconnection_interval=60, **connect_params): """Constructor for SingleServerIRCBot objects. Arguments: server_list -- A list of ServerSpec objects or tuples of parameters suitable for constructing ServerSpec objects. Defines the list of servers the bot will use (in order). nickname -- The bot's nickname. realname -- The bot's realname. reconnection_interval -- How long the bot should wait before trying to reconnect. dcc_connections -- A list of initiated/accepted DCC connections. **connect_params -- parameters to pass through to the connect method. """ super(SingleServerIRCBot, self).__init__() self.__connect_params = connect_params self.channels = IRCDict() self.server_list = [ ServerSpec(*server) if isinstance(server, (tuple, list)) else server for server in server_list ] assert all( isinstance(server, ServerSpec) for server in self.server_list) if not reconnection_interval or reconnection_interval < 0: reconnection_interval = 2**31 self.reconnection_interval = reconnection_interval self._nickname = nickname self._realname = realname for i in [ "disconnect", "join", "kick", "mode", "namreply", "nick", "part", "quit" ]: self.connection.add_global_handler(i, getattr(self, "_on_" + i), -20)
def __init__(self, logchannel=None, **kwargs): """Uses PanHandler automatically.""" IRCClient.__init__(self, PanHandler, **kwargs) print "Successfully created", self self.channels = IRCDict() self.db = MongoClient(document_class=SON).mzpn self.logchan = logchannel
def __init__(self, namreply=None): Channel.__init__(self) self.admindict = IRCDict() self.logdict = IRCDict() if namreply: for nick in namreply.split(' '): if nick[0] == '~': self.ownerdict[nick[1:]] = 1 elif nick[0] == '&': self.admindict[nick[1:]] = 1 elif nick[0] == '@': self.operdict[nick[1:]] = 1 elif nick[0] =='%': self.halfopdict[nick[1:]] = 1 elif nick[0] == '+': self.voicedict[nick[1:]] = 1 else: self.userdict[nick] = 1 self.logdict[nick] = tempfile.NamedTemporaryFile(bufsize=5120) continue self.userdict[nick[1:]] = 1 self.logdict[nick[1:]] = tempfile.NamedTemporaryFile(bufsize=5120)
def __init__(self, server_list, channel_list, nickname, reconnection_interval=5, **connect_params): """Constructor for IRCChatBot objects. Arguments: server_list -- A list of ServerSpec objects or tuples of parameters suitable for constructing ServerSpec objects. Defines the list of servers the bot will use (in order). channel_list -- A list of channel names to join nickname -- The bot's nickname. reconnection_interval -- How long the bot should wait before trying to reconnect. **connect_params -- parameters to pass through to the connect method. """ super(IRCChatBot, self).__init__() self.connection.set_rate_limit(0.5) self.nickname = nickname self.__connect_params = connect_params self.channels = IRCDict() self.channel_list = channel_list self.server_list = [ ServerSpec(*server) if isinstance(server, (tuple, list)) else server for server in server_list ] assert all( isinstance(server, ServerSpec) for server in self.server_list) if not reconnection_interval or reconnection_interval < 0: reconnection_interval = 2**31 self.reconnection_interval = reconnection_interval
def __init__(self, namreply=None): Channel.__init__(self) self.admindict = IRCDict() self.logdict = IRCDict() if namreply: for nick in namreply.split(' '): if nick[0] == '~': self.ownerdict[nick[1:]] = 1 elif nick[0] == '&': self.admindict[nick[1:]] = 1 elif nick[0] == '@': self.operdict[nick[1:]] = 1 elif nick[0] == '%': self.halfopdict[nick[1:]] = 1 elif nick[0] == '+': self.voicedict[nick[1:]] = 1 else: self.userdict[nick] = 1 self.logdict[nick] = tempfile.NamedTemporaryFile(bufsize=5120) continue self.userdict[nick[1:]] = 1 self.logdict[nick[1:]] = tempfile.NamedTemporaryFile(bufsize=5120)
def __init__(self, channel, nickname, server, port=6667, **connect_params): super(IRCClient, self).__init__() if server is None: raise Exception("IRC Server not provided") if nickname is None or ' ' in nickname: raise Exception("Invalid nickname: must be one word") if channel is None or ' ' in channel or '#' not in channel: raise Exception("Invalid channel: must be a word starting with #") self.__connect_params = connect_params self.channels = IRCDict() self.channel = channel self.server = ServerSpec(server, port) self._nickname = nickname self.clientname = socket.gethostname().split('.')[0] assert 0 <= self.min_reconnect_wait <= self.max_reconnect_wait self._check_scheduled = False # Global handlers to handle channel/nick associations # Mostly when a nick is already in use for i in ["disconnect", "join", "kick", "mode", "namreply", "nick", "part", "quit"]: self.connection.add_global_handler( i, getattr(self, "_on_" + i), -20 )
class PanChan(Channel): """Uses the strings from namreply to set the values for each user in a channel.""" def __init__(self, namreply=None): Channel.__init__(self) self.admindict = IRCDict() self.logdict = IRCDict() if namreply: for nick in namreply.split(' '): if nick[0] == '~': self.ownerdict[nick[1:]] = 1 elif nick[0] == '&': self.admindict[nick[1:]] = 1 elif nick[0] == '@': self.operdict[nick[1:]] = 1 elif nick[0] == '%': self.halfopdict[nick[1:]] = 1 elif nick[0] == '+': self.voicedict[nick[1:]] = 1 else: self.userdict[nick] = 1 self.logdict[nick] = tempfile.NamedTemporaryFile(bufsize=5120) continue self.userdict[nick[1:]] = 1 self.logdict[nick[1:]] = tempfile.NamedTemporaryFile(bufsize=5120) def add_user(self, nick): self.logdict[nick] = tempfile.NamedTemporaryFile(bufsize=5120) Channel.add_user(self, nick) def clear_mode(self, mode, value=None): if mode == 'a': del self.admindict[value] else: Channel.clear_mode(self, mode, value) def change_nick(self, before, after): Channel.change_nick(self, before, after) self.logdict[after] = self.logdict[before] del self.logdict[before] def get_userlog(self, nick): if self.logdict.has_key(nick): log = self.logdict[nick] log.seek(0) return log.readlines() return None def has_privs(self, nick): return self.is_oper(nick) or self.is_halfop(nick) or self.isadmin or self.is_owner(nick) def handle_modes(self, modes, args): m = mod.match(modes) if m: l = list(args) s = m.group('mode1') if s[0] == '+': for each in s[1:]: if each in 'qaohv' and l != []: self.set_mode(each, l.pop(0)) else: self.set_mode(each) else: for each in s[1:]: if each in 'qaohv' and l != []: self.clear_mode(each, l.pop(0)) else: self.clear_mode(each) if m.group('mode2'): self.handle_modes(m.group('mode2'), *l) def is_admin(self, nick): return nick in self.admindict def log_user(self, nick, msg): log = self.logdict[nick] log.seek(0) lines = log.readlines() if len(lines) < LINE_LIMIT: log.write(msg + '\n') elif len(lines) == LINE_LIMIT: lines.pop(0) lines.append(msg + '\n') log.seek(0) log.writelines(*lines) log.flush() def set_mode(self, mode, value=None): if mode == 'a': self.admindict[value] = 1 else: Channel.set_mode(self, mode, value)
def __init__(self): self._users = IRCDict() self.mode_users = collections.defaultdict(IRCDict) self.modes = {}
class Channel(object): """ A class for keeping information about an IRC channel. """ user_modes = 'ovqha' """ Modes which are applicable to individual users, and which should be tracked in the mode_users dictionary. """ def __init__(self): self._users = IRCDict() self.mode_users = collections.defaultdict(IRCDict) self.modes = {} def users(self): """Returns an unsorted list of the channel's users.""" return self._users.keys() def opers(self): """Returns an unsorted list of the channel's operators.""" return self.mode_users['o'].keys() def voiced(self): """Returns an unsorted list of the persons that have voice mode set in the channel.""" return self.mode_users['v'].keys() def owners(self): """Returns an unsorted list of the channel's owners.""" return self.mode_users['q'].keys() def halfops(self): """Returns an unsorted list of the channel's half-operators.""" return self.mode_users['h'].keys() def admins(self): """Returns an unsorted list of the channel's admins.""" return self.mode_users['a'].keys() def has_user(self, nick): """Check whether the channel has a user.""" return nick in self._users def is_oper(self, nick): """Check whether a user has operator status in the channel.""" return nick in self.mode_users['o'] def is_voiced(self, nick): """Check whether a user has voice mode set in the channel.""" return nick in self.mode_users['v'] def is_owner(self, nick): """Check whether a user has owner status in the channel.""" return nick in self.mode_users['q'] def is_halfop(self, nick): """Check whether a user has half-operator status in the channel.""" return nick in self.mode_users['h'] def is_admin(self, nick): """Check whether a user has admin status in the channel.""" return nick in self.mode_users['a'] def add_user(self, nick): self._users[nick] = 1 @property def user_dicts(self): yield self._users for d in self.mode_users.values(): yield d def remove_user(self, nick): for d in self.user_dicts: d.pop(nick, None) def change_nick(self, before, after): self._users[after] = self._users.pop(before) for mode_lookup in self.mode_users.values(): if before in mode_lookup: mode_lookup[after] = mode_lookup.pop(before) def set_userdetails(self, nick, details): if nick in self._users: self._users[nick] = details def set_mode(self, mode, value=None): """Set mode on the channel. Arguments: mode -- The mode (a single-character string). value -- Value """ if mode in self.user_modes: self.mode_users[mode][value] = 1 else: self.modes[mode] = value def clear_mode(self, mode, value=None): """Clear mode on the channel. Arguments: mode -- The mode (a single-character string). value -- Value """ try: if mode in self.user_modes: del self.mode_users[mode][value] else: del self.modes[mode] except KeyError: pass def has_mode(self, mode): return mode in self.modes def is_moderated(self): return self.has_mode("m") def is_secret(self): return self.has_mode("s") def is_protected(self): return self.has_mode("p") def has_topic_lock(self): return self.has_mode("t") def is_invite_only(self): return self.has_mode("i") def has_allow_external_messages(self): return self.has_mode("n") def has_limit(self): return self.has_mode("l") def limit(self): if self.has_limit(): return self.modes["l"] else: return None def has_key(self): return self.has_mode("k")
def _on_disconnect(self, c, e): self.channels = IRCDict() self.recon.run(self)
class IRCClient(irc.client.SimpleIRCClient): """ This class represents an IRC client that must have its poll() called periodically to process messages incoming from the IRC server. The client is limited in its ability to handle messages incoming from the IRC server and only has enough functionality to respond to PING events to maintain its connection. The primary role of the client is to act as a relay for the external caller calling poll() so that messages from that external caller are passed to the joined IRC channel. """ # Heavily influenced by irc.client.SingleServerIRCBot and the example at # https://github.com/jaraco/irc/blob/master/scripts/testbot.py min_reconnect_wait = 1 max_reconnect_wait = 10 def __init__(self, channel, nickname, server, port=6667, **connect_params): super(IRCClient, self).__init__() if server is None: raise Exception("IRC Server not provided") if nickname is None or " " in nickname: raise Exception("Invalid nickname: must be one word") if channel is None or " " in channel or "#" not in channel: raise Exception("Invalid channel: must be a word starting with #") self.__connect_params = connect_params self.channels = IRCDict() self.channel = channel self.server = ServerSpec(server, port) self._nickname = nickname self.clientname = socket.gethostname().split(".")[0] if 0 <= self.min_reconnect_wait: raise Exception("Minimum reconnect wait must be positive number") if self.min_reconnect_wait <= self.max_reconnect_wait: raise Exception( "Maximum reconnect wait must be larger than minimum") self._check_scheduled = False # Global handlers to handle channel/nick associations # Mostly when a nick is already in use for i in [ "disconnect", "join", "kick", "mode", "namreply", "nick", "part", "quit", ]: self.connection.add_global_handler(i, getattr(self, "_on_" + i), -20) def _on_disconnect(self, c, e): self.channels = IRCDict() self.reconnect() def reconnect(self): """ Called on a disconnect event to start a reconnection. The actual reconnection is deferred for some random amount of seconds. """ def check(): self._check_scheduled = False if not self.connection.is_connected(): self.reconnect() if self.connection.is_connected(): self.disconnect() self.connect() if self._check_scheduled: return reconnect_wait = max(self.min_reconnect_wait, int(self.max_reconnect_wait * random())) self.reactor.scheduler.execute_after(reconnect_wait, check) self._check_scheduled = True def _on_join(self, c, e): ch = e.target nick = e.source.nick if nick == c.get_nickname(): self.channels[ch] = Channel() self.channels[ch].add_user(nick) def _on_kick(self, c, e): nick = e.arguments[0] channel = e.target if nick == c.get_nickname(): del self.channels[channel] else: self.channels[channel].remove_user(nick) def _on_mode(self, c, e): t = e.target if not irc.client.is_channel(t): # mode on self; disregard return ch = self.channels[t] modes = irc.modes.parse_channel_modes(" ".join(e.arguments)) for sign, mode, argument in modes: f = {"+": ch.set_mode, "-": ch.clear_mode}[sign] f(mode, argument) def _on_namreply(self, c, e): """ e.arguments[0] == "@" for secret channels, "*" for private channels, "=" for others (public channels) e.arguments[1] == channel e.arguments[2] == nick list """ ch_type, channel, nick_list = e.arguments if channel == "*": # User is not in any visible channel # http://tools.ietf.org/html/rfc2812#section-3.2.5 return for nick in nick_list.split(): nick_modes = [] if nick[0] in self.connection.features.prefix: nick_modes.append(self.connection.features.prefix[nick[0]]) nick = nick[1:] for mode in nick_modes: self.channels[channel].set_mode(mode, nick) self.channels[channel].add_user(nick) def _on_nick(self, c, e): before = e.source.nick after = e.target for ch in self.channels.values(): if ch.has_user(before): ch.change_nick(before, after) def _on_part(self, c, e): nick = e.source.nick channel = e.target if nick == c.get_nickname(): del self.channels[channel] else: self.channels[channel].remove_user(nick) def _on_quit(self, c, e): nick = e.source.nick for ch in self.channels.values(): if ch.has_user(nick): ch.remove_user(nick) def on_nicknameinuser(self, c, e): """ When connecting it is discovered that the nick is already is use, provide an alternative. """ c.nick(c.get_nickname() + "_") def on_welcome(self, c, e): """ Automatically join the channel once welcomed by the server. """ c.join(self.channel) self.say("{} has started on host {}".format(__progname__, self.clientname)) def on_privmsg(self, c, e): """ Disable private messaging with users. """ nick = e.source.nick msg = "I am sorry, {}; I do not do private messages.".format(nick) c.notice(nick, msg) def on_dccmsg(self, c, e): """ Disable dcc messaging with users. """ nick = e.source.nick # text = e.arguments[0].decode('utf-8') msg = "I am sorry, {}; I do not do dcc messages.".format(nick) c.privmsg(nick, msg) def disconnect(self): self.connection.disconnect("Bye!") def get_version(self): return "irc.client ({version})".format( version=irc.client.VERSION_STRING) def on_ctcp(self, c, e): nick = e.source.nick if e.arguments[0] == "VERSION": c.ctcp_reply(nick, "VERSION " + self.get_version()) elif e.arguments[0] == "PING": if len(e.arguments) > 1: c.ctcp_reply(nick, "PING " + e.arguments[1]) def on_pubmsg(self, c, e): nick = e.source.nick command = e.arguments[0] if "hello" in command or "hi" in command: self.say("Hello, {}".format(nick)) elif "help" in command: self.say("{}, commands are: version".format(nick)) elif "version" in command: self.say("{}, I am running {} {} using {} on {}".format( nick, __progname__, __version__, self.get_version(), self.clientname)) def poll(self): """ Poll the IRC connection for events. The reactor processing uses select on the socket so give a 0.2s timeout so that control returns. Polling must occur or else the IRC server will likely disconnect the client due to ping timeout. """ self.reactor.process_once(0.2) def connect(self): server = self.server try: super(IRCClient, self).connect(server.host, server.port, self._nickname, server.password, username=self._nickname, ircname=self._nickname, **self.__connect_params) except irc.client.ServerConnectionError: self.reconnect() # Schedule a deferred reconnection retry pass def say(self, message): """ The main interface into this client (other than poll() and connect()) to send messages from the external caller to the IRC channel. If there happens to be no connection when something is said, then say nothing. A (re)connection is either happening or the server is down. Note that this is an issue only because a message is sent to the IRC server explicitly and not a handler reacting to something the server has sent (which requires that the connection is up to occur). """ try: self.connection.privmsg(self.channel, message) except irc.client.ServerNotConnectedError: # Connection down? Try a reconnect, and skip saying anything. The # time to say it has already passed. self.reconnect() pass
class SingleServerIRCBot(irc.client.SimpleIRCClient): """A single-server IRC bot class. The bot tries to reconnect if it is disconnected. The bot keeps track of the channels it has joined, the other clients that are present in the channels and which of those that have operator or voice modes. The "database" is kept in the self.channels attribute, which is an IRCDict of Channels. Arguments: server_list -- A list of ServerSpec objects or tuples of parameters suitable for constructing ServerSpec objects. Defines the list of servers the bot will use (in order). nickname -- The bot's nickname. realname -- The bot's realname. recon -- A ReconnectStrategy for reconnecting on disconnect or failed connection. dcc_connections -- A list of initiated/accepted DCC connections. **connect_params -- parameters to pass through to the connect method. """ def __init__( self, server_list, nickname, realname, reconnection_interval=missing, recon=ExponentialBackoff(), **connect_params): super(SingleServerIRCBot, self).__init__() self.__connect_params = connect_params self.channels = IRCDict() self.server_list = [ ServerSpec(*server) if isinstance(server, (tuple, list)) else server for server in server_list ] assert all( isinstance(server, ServerSpec) for server in self.server_list ) self.recon = recon # for compatibility if reconnection_interval is not missing: warnings.warn( "reconnection_interval is deprecated; " "pass a ReconnectStrategy object instead") self.recon = ExponentialBackoff(min_interval=reconnection_interval) self._nickname = nickname self._realname = realname for i in ["disconnect", "join", "kick", "mode", "namreply", "nick", "part", "quit"]: self.connection.add_global_handler( i, getattr(self, "_on_" + i), -20) def _connect(self): """ Establish a connection to the server at the front of the server_list. """ server = self.server_list[0] try: self.connect( server.host, server.port, self._nickname, server.password, ircname=self._realname, **self.__connect_params) self.connection_attempts = 1 except irc.client.ServerConnectionError: pass def _on_disconnect(self, c, e): self.channels = IRCDict() self.recon.run(self) def _on_join(self, c, e): ch = e.target nick = e.source.nick if nick == c.get_nickname(): self.channels[ch] = Channel() self.channels[ch].add_user(nick) def _on_kick(self, c, e): nick = e.arguments[0] channel = e.target if nick == c.get_nickname(): del self.channels[channel] else: self.channels[channel].remove_user(nick) def _on_mode(self, c, e): t = e.target if not irc.client.is_channel(t): # mode on self; disregard return ch = self.channels[t] modes = irc.modes.parse_channel_modes(" ".join(e.arguments)) for sign, mode, argument in modes: f = {"+": ch.set_mode, "-": ch.clear_mode}[sign] f(mode, argument) def _on_namreply(self, c, e): """ e.arguments[0] == "@" for secret channels, "*" for private channels, "=" for others (public channels) e.arguments[1] == channel e.arguments[2] == nick list """ ch_type, channel, nick_list = e.arguments if channel == '*': # User is not in any visible channel # http://tools.ietf.org/html/rfc2812#section-3.2.5 return for nick in nick_list.split(): nick_modes = [] if nick[0] in self.connection.features.prefix: nick_modes.append(self.connection.features.prefix[nick[0]]) nick = nick[1:] for mode in nick_modes: self.channels[channel].set_mode(mode, nick) self.channels[channel].add_user(nick) def _on_nick(self, c, e): before = e.source.nick after = e.target for ch in self.channels.values(): if ch.has_user(before): ch.change_nick(before, after) def _on_part(self, c, e): nick = e.source.nick channel = e.target if nick == c.get_nickname(): del self.channels[channel] else: self.channels[channel].remove_user(nick) def _on_quit(self, c, e): nick = e.source.nick for ch in self.channels.values(): if ch.has_user(nick): ch.remove_user(nick) def die(self, msg="Bye, cruel world!"): """Let the bot die. Arguments: msg -- Quit message. """ self.connection.disconnect(msg) sys.exit(0) def disconnect(self, msg="I'll be back!"): """Disconnect the bot. The bot will try to reconnect after a while. Arguments: msg -- Quit message. """ self.connection.disconnect(msg) def get_version(self): """Returns the bot version. Used when answering a CTCP VERSION request. """ return "Python irc.bot ({version})".format( version=irc.client.VERSION_STRING) def jump_server(self, msg="Changing servers"): """Connect to a new server, possibly disconnecting from the current. The bot will skip to next server in the server_list each time jump_server is called. """ if self.connection.is_connected(): self.connection.disconnect(msg) self.server_list.append(self.server_list.pop(0)) self._connect() def on_ctcp(self, c, e): """Default handler for ctcp events. Replies to VERSION and PING requests and relays DCC requests to the on_dccchat method. """ nick = e.source.nick if e.arguments[0] == "VERSION": c.ctcp_reply(nick, "VERSION " + self.get_version()) elif e.arguments[0] == "PING": if len(e.arguments) > 1: c.ctcp_reply(nick, "PING " + e.arguments[1]) elif ( e.arguments[0] == "DCC" and e.arguments[1].split(" ", 1)[0] == "CHAT"): self.on_dccchat(c, e) def on_dccchat(self, c, e): pass def start(self): """Start the bot.""" self._connect() super(SingleServerIRCBot, self).start()
def __init__(self, gui, server, port, nickname, password=None, username=None, ircname=None, ssl=False): QThread.__init__(self) self.gui = gui self.server = server self.port = port self.nickname = nickname self.password = password self.username = username self.ircname = ircname self.ssl = ssl self.irc = None self.channels = IRCDict() self.cusers = defaultdict(list) self.blink_pubmsg = signal("pubmsg") self.blink_privmsg = signal("privmsg") self.blink_chanjoin = signal("join") self.blink_chanpart = signal("part") self.blink_registered = signal("registered") self.blink_gotnames = signal("names") self.blink_gotmotd = signal("motd") self.blink_nameend = signal("endnames") self.blink_mynick = signal("mynick") self.blink_gotmode = signal("mode") self.blink_gotumode = signal("umode") self.blink_gotnick = signal("newnick") self.blink_gotquit = signal("quit") self.blink_gotact = signal("action") self.blink_joinerror = signal("joinerror") self.blink_chanerror = signal("chanerror") self.blink_miscerror = signal("miscerror") self.blink_notice = signal("notice") self.blink_nicklength = signal("nicklen") self.blink_network = signal("network") self.blink_disconnected = signal("disconnected") self.blink_currenttopic = signal("currenttopic") self.blink_topicsetter = signal("topicsetter") self.blink_topic = signal("topic") self.blink_pubmsg.connect(self.pubchat) self.blink_privmsg.connect(self.privchat) self.blink_chanjoin.connect(self.join_channel) self.blink_chanpart.connect(self.part_channel) self.blink_registered.connect(self.welcome) self.blink_gotnames.connect(self.channel_users) self.blink_gotmotd.connect(self.motd) self.blink_nameend.connect(self.endnames) self.blink_mynick.connect(self.my_nick) self.blink_gotmode.connect(self.got_mode) self.blink_gotumode.connect(self.got_umode) self.blink_gotnick.connect(self.got_nick) self.blink_gotquit.connect(self.got_quit) self.blink_gotact.connect(self.got_action) self.blink_joinerror.connect(self.join_error) self.blink_chanerror.connect(self.chan_error) self.blink_miscerror.connect(self.misc_error) self.blink_notice.connect(self.got_notice) self.blink_nicklength.connect(self.got_nicklength) self.blink_network.connect(self.got_network) self.blink_disconnected.connect(self.got_disco) self.blink_currenttopic.connect(self.got_ctopic) self.blink_topicsetter.connect(self.got_stopic) self.blink_topic.connect(self.got_topic)
class Channel(object): """ A class for keeping information about an IRC channel. """ def __init__(self): self.userdict = IRCDict() self.operdict = IRCDict() self.voiceddict = IRCDict() self.ownerdict = IRCDict() self.halfopdict = IRCDict() self.modes = {} def users(self): """Returns an unsorted list of the channel's users.""" return self.userdict.keys() def opers(self): """Returns an unsorted list of the channel's operators.""" return self.operdict.keys() def voiced(self): """Returns an unsorted list of the persons that have voice mode set in the channel.""" return self.voiceddict.keys() def owners(self): """Returns an unsorted list of the channel's owners.""" return self.ownerdict.keys() def halfops(self): """Returns an unsorted list of the channel's half-operators.""" return self.halfopdict.keys() def has_user(self, nick): """Check whether the channel has a user.""" return nick in self.userdict def is_oper(self, nick): """Check whether a user has operator status in the channel.""" return nick in self.operdict def is_voiced(self, nick): """Check whether a user has voice mode set in the channel.""" return nick in self.voiceddict def is_owner(self, nick): """Check whether a user has owner status in the channel.""" return nick in self.ownerdict def is_halfop(self, nick): """Check whether a user has half-operator status in the channel.""" return nick in self.halfopdict def add_user(self, nick): self.userdict[nick] = 1 def remove_user(self, nick): for d in self.userdict, self.operdict, self.voiceddict: if nick in d: del d[nick] def change_nick(self, before, after): self.userdict[after] = self.userdict.pop(before) if before in self.operdict: self.operdict[after] = self.operdict.pop(before) if before in self.voiceddict: self.voiceddict[after] = self.voiceddict.pop(before) def set_userdetails(self, nick, details): if nick in self.userdict: self.userdict[nick] = details def set_mode(self, mode, value=None): """Set mode on the channel. Arguments: mode -- The mode (a single-character string). value -- Value """ if mode == "o": self.operdict[value] = 1 elif mode == "v": self.voiceddict[value] = 1 elif mode == "q": self.ownerdict[value] = 1 elif mode == "h": self.halfopdict[value] = 1 else: self.modes[mode] = value def clear_mode(self, mode, value=None): """Clear mode on the channel. Arguments: mode -- The mode (a single-character string). value -- Value """ try: if mode == "o": del self.operdict[value] elif mode == "v": del self.voiceddict[value] elif mode == "q": del self.ownerdict[value] elif mode == "h": del self.halfopdict[value] else: del self.modes[mode] except KeyError: pass def has_mode(self, mode): return mode in self.modes def is_moderated(self): return self.has_mode("m") def is_secret(self): return self.has_mode("s") def is_protected(self): return self.has_mode("p") def has_topic_lock(self): return self.has_mode("t") def is_invite_only(self): return self.has_mode("i") def has_allow_external_messages(self): return self.has_mode("n") def has_limit(self): return self.has_mode("l") def limit(self): if self.has_limit(): return self.modes["l"] else: return None def has_key(self): return self.has_mode("k")
def on_disconnect(self, c, e): self.channels = IRCDict() self.reactor.scheduler.execute_after(self.reconnection_interval, self._connected_checker)
class IRCChatBot(SimpleIRCClient): def __init__(self, server_list, channel_list, nickname, reconnection_interval=5, **connect_params): """Constructor for IRCChatBot objects. Arguments: server_list -- A list of ServerSpec objects or tuples of parameters suitable for constructing ServerSpec objects. Defines the list of servers the bot will use (in order). channel_list -- A list of channel names to join nickname -- The bot's nickname. reconnection_interval -- How long the bot should wait before trying to reconnect. **connect_params -- parameters to pass through to the connect method. """ super(IRCChatBot, self).__init__() self.connection.set_rate_limit(0.5) self.nickname = nickname self.__connect_params = connect_params self.channels = IRCDict() self.channel_list = channel_list self.server_list = [ ServerSpec(*server) if isinstance(server, (tuple, list)) else server for server in server_list ] assert all( isinstance(server, ServerSpec) for server in self.server_list) if not reconnection_interval or reconnection_interval < 0: reconnection_interval = 2**31 self.reconnection_interval = reconnection_interval def _connected_checker(self): if not self.connection.is_connected(): self.reactor.scheduler.execute_after(self.reconnection_interval, self._connected_checker) self.jump_server() def _connect(self): server = self.server_list[0] try: self.connect(server.host, server.port, self.nickname, server.password, ircname=self.nickname, **self.__connect_params) except ServerConnectionError: pass def on_disconnect(self, c, e): self.channels = IRCDict() self.reactor.scheduler.execute_after(self.reconnection_interval, self._connected_checker) def on_join(self, c, e): ch = e.target nick = e.source.nick if nick == c.get_nickname(): self.channels[ch] = Channel() self.channels[ch].add_user(nick) lg.debug('IRCChatBot::on_join: %s =====>>> %s', nick, ch) def on_part(self, c, e): ch = e.target nick = e.source.nick lg.debug('IRCChatBot::on_part: %s <<<===== %s', nick, ch) if nick == c.get_nickname(): del self.channels[ch] else: self.channels[ch].remove_user(nick) def on_nick(self, c, e): before = e.source.nick after = e.target for ch in self.channels.values(): if ch.has_user(before): ch.change_nick(before, after) print('%s changed nickname to %s' % (before, after)) def on_nicknameinuse(self, c, e): c.nick(c.get_nickname() + "_") def on_welcome(self, c, e): for ch in self.channel_list: c.join(ch) def on_privmsg(self, c, e): nick = e.source.nick text = ' '.join(e.arguments) lg.debug('IRCChatBot::on_privmsg(): %s: %s', nick, text) # ignore my own message if nick == self.username: return None # commands must start with + if text[0] != '+': return None now = datetime.utcnow().replace(tzinfo=pytz.utc) msg = { 'created_utc': calendar.timegm(now.utctimetuple()), 'author': { 'name': nick }, 'channel': e.target } msg['id'] = str(msg['created_utc']) msg['body'] = text print(msg) action = ctb_action.eval_message(ctb_misc.DotDict(msg), self.ctb) if action: lg.info("IRCChatBot::on_pubmsg(): %s from %s", action.type, action.u_from.name) lg.debug("IRCChatBot::on_pubmsg(): comment body: <%s>", action.msg.body) action.do() def on_pubmsg(self, c, e): ch = e.target nick = e.source.nick text = ' '.join(e.arguments) lg.debug('IRCChatBot::on_pubmsg(): %s on %s: %s', nick, ch, text) # ignore my own message if nick == self.username: return None # commands must start with + if text[0] != '+': return None now = datetime.utcnow().replace(tzinfo=pytz.utc) msg = { 'created_utc': calendar.timegm(now.utctimetuple()), 'author': { 'name': nick } } msg['id'] = str(msg['created_utc']) msg['body'] = text print(msg) action = ctb_action.eval_message(ctb_misc.DotDict(msg), self.ctb) if action: lg.info("IRCChatBot::on_pubmsg(): %s from %s", action.type, action.u_from.name) lg.debug("IRCChatBot::on_pubmsg(): comment body: <%s>", action.msg.body) action.do() def disconnect(self, msg="disconnecting"): self.connection.disconnect(msg) def jump_server(self, msg="switching server"): """Connect to a new server, possibly disconnecting from the current. The bot will skip to next server in the server_list each time jump_server is called. """ if self.connection.is_connected(): self.connection.disconnect(msg) self.server_list.append(self.server_list.pop(0)) self._connect() def send_msg(self, recipient, msg): self.connection.privmsg(recipient, msg) def start(self): self._connect() super(IRCChatBot, self).start()
class DixieBot(irc.bot.SingleServerIRCBot): # Class-level variables which form attributes. These all refer to aspects # of the bot. joined_channels = IRCDict() canonical_name = "" nick = "" owner = "" # Connection information. server = "" port = 0 # The bot's owner's authentication password. password = "" # Is the bot's owner authenticated or not? authenticated = "" # Whether or not the connection is SSL/TLS encrypted or not. usessl = "" # Response engine's hostname and port. engine = "" # Bot's API key to interface with the response engine. api_key = "" # One instance of wordfilter.Wordfilter() to rule them all... wordfilter = None # Whether or not to use the conversation engine to respond? respond = None # Whether or not the bot's owner can speak through the bot by using # private messages. By default, the bot doesn't let you do that. ghost = None # Methods on the connection object to investigate: # connect() - Connect to a server? # connected() - # disconnect() - # get_nickname() - # get_server_name() - # info() - # ircname() - # is_connected() - See if the connection is still up? # part() - Leave channel? # privmsg() - Send privmsg? # quit() - Terminate IRC connection? # reconnect() - Reconnect to server? # send_raw() - # stats() - # time() - def __init__(self, channels, nickname, server, port, owner, usessl, password, engine_host, engine_port, api_key, respond): # Initialize the class' attributes. for i in channels: self.joined_channels[i] = 1 self.canonical_name = nick self.nick = nick self.owner = owner self.server = server self.port = port self.password = password self.authenticated = False self.usessl = usessl self.engine = 'http://' + engine_host + ':' + engine_port self.api_key = api_key self.wordfilter = Wordfilter() self.respond = respond self.ghost = False # Connection factory object handle. factory = "" # If SSL/TLS support is requested, pass the ssl.wrap_socket() method # as a keyword argument. if self.usessl: logger.debug("Constructing SSL/TLS server connector.") factory = irc.connection.Factory(wrapper=ssl.wrap_socket) else: logger.debug("Constructing plaintext server connector.") factory = irc.connection.Factory() # Initialize an instance of this class by running the parent class' # default initializer method. # # [(server, port)] can be a list of one or more (server, port) tuples # because it can connect to more than one at once. # The other two arguments are the bot's nickname and realname. logger.debug("Instantiating SingleServerIRCBot superclass.") irc.bot.SingleServerIRCBot.__init__(self, [(self.server, self.port)], self.nick, self.nick, connect_factory=factory) logger.debug("Channels configured for this bot:") logger.debug(" " + str(self.joined_channels)) # This method fires if the configured nickname is already in use. If that # happens, change the bot's nick slightly. # Note that the name of this method is specifically what the irc module # looks for. def on_nicknameinuse(self, connection, event): logger.info("Bot nickname " + self.nick + " is already taken. Falling back to bot nickname " + self.nick + "_.") connection.privmsg( self.owner, self.nick + " seems to be taken already. Falling back to nickname " + self.nick + "_.") connection.nick(connection.get_nickname() + "_") # This method fires when the server accepts the bot's connection. It walks # through the IRCDict of channels and tries to join each one. def on_welcome(self, connection, event): logger.debug("Entered DixieBot.on_welcome().") for channel in self.joined_channels: logger.debug("Trying to join channel " + channel + ".") connection.join(channel) logger.info("Joined channel " + channel + ".") connection.privmsg(self.owner, "Joined " + channel + ".") # Just to be silly, roll 1d10. On a 1, say hello to the channel. roll = random.randint(1, 10) if roll == 1: pause = random.randint(1, 10) time.sleep(pause) logger.debug("Bot has randomly decided to announce itself.") connection.privmsg( channel, "Hey, bro! I'm " + self.nick + ", the best cowboy who ever punched deck!") logger.debug("Exiting DixieBot.on_welcome().") # This method fires if the bot gets kicked from a channel. The smart # thing to do is sleep for a random period of time (between one and three # minutes) before trying to join again. def on_kick(self, connection, event): delay = random.randint(60, 180) logger.debug("Got kicked from " + event.target + ". Sleeping for " + str(delay) + " seconds.") connection.privmsg( self.owner, "Got kicked from " + event.target + ". Sleeping for " + str(delay) + " seconds.") time.sleep(delay) logger.debug("Rejoining channel " + event.target + ".") connection.privmsg(self.owner, "Rejoining channel " + event.target + ".") connection.join(event.target) logger.info("Successfully re-joined channel " + event.target + ".") connection.privmsg( self.owner, "Successfully re-joined channel " + event.target + ".") return # This method fires if the bot gets kickbanned. def on_bannedfromchan(self, connection, event): logger.warn("Uh-oh - I got kickbanned from " + event.target + ". I know when I'm not wanted.") self.privmsg( self.owner, "Uh-oh - I got kickbanned from " + event.target + ". I know when I'm not wanted.") self.joined_channels.remove(event.target) return # This method fires when the server disconnects the bot for some reason. # Ideally, the bot should try to connect again after a random number of # seconds. def on_disconnect(self, connection, event): delay = random.randint(60, 180) logger.warn("Connection dropped from server " + self.server + ". Sleeping for " + str(delay) + " seconds.") time.sleep(delay) logger.warn("Reconnecting to server " + self.server + " on port " + str(self.port) + ".") try: irc.bot.SingleServerIRCBot.connect(self, [(self.server, self.port)], self.nick, self.nick) logger.info("Successfully reconnected to server " + self.server + ".") except: logger.warn("Unable to reconnect to " + self.server + ". Something's really wrong.") # This method fires when the bot receives a private message. For the # moment, if it's the bot's owner always learn from the text because this # is an ideal way to get more interesting stuff into the bot's brain. # It'll make a good place to look for and respond to specific commands, # too. def on_privmsg(self, connection, line): # IRC nick that sent a line to the bot in private chat. sending_nick = line.source.split("!~")[0] # Line of text sent from the channel or private message. irc_text = line.arguments[0] # String that holds what may or may not be a channel name. possible_channel_name = None # String that may or may not hold a respond to a channel in ghost mode. irc_response = None # Handle to an HTTP request object. http_connection = "" # JSON document containing responses from the conversation engine. json_response = {} # See if the owner is authenticating to the bot. if "!auth " in irc_text: self._authenticate(connection, sending_nick, irc_text) return # Handle messages from the bot's owner (if authenticated). if sending_nick == self.owner: if not self.authenticated: connection.privmsg(sending_nick, "You're not authenticated.") return # If the owner asks for online help, provide it. if irc_text == "!help" or irc_text == "!commands": self._help(connection, sending_nick) return # See if the owner is asking the bot to self-terminate. if irc_text == "!quit": logger.info("The bot's owner has told it to shut down.") connection.privmsg(sending_nick, "I get the hint. Shuttin' down.") sys.exit(0) # See if the owner is asking for the bot's current configuration. if irc_text == "!config": self._current_config(connection, sending_nick) return # See if the owner is asking the bot to ping the conversation # engine's server. if irc_text == "!ping": self._ping(connection, sending_nick) return # See if the owner is asking the bot to change its nick. if "!nick" in irc_text: self._nick(connection, irc_text, sending_nick) return # See if the owner is asking the bot to join a channel. if "!join " in irc_text: self._join(connection, irc_text, sending_nick) return # See if the owner is flipping the self.respond flag. if "!respond" in irc_text: self._respond(connection, irc_text, sending_nick) return # See if the owner is asking for help on ghost mode. if "!ghosthelp" in irc_text: self._ghost_help(connection, sending_nick) return # See if the owner is flipping the self.ghost flag. if "!ghost" in irc_text: self._ghost_mode(connection, sending_nick) return # If the bot's in ghost mode, determine whether or not the bot's # owner has sent text destined for a channel the bot's sitting in. # If this is the case, send the channel the text sent by the # bot's owner. possible_channel_name = irc_text.split()[0] logger.debug("Value of possible_channel_name: " + possible_channel_name) if self.ghost: if "#" in possible_channel_name: # Test to see if the bot is in the channel in question. in_channel = False for channel in self.joined_channels: if channel == possible_channel_name: in_channel = True break if not in_channel: logger.debug("Not in channel " + possible_channel_name + ".") connection.privmsg( sending_nick, "I'm not in channel " + possible_channel_name + ".") return logger.debug("In channel " + possible_channel_name + ".") # Send the text to the channel. irc_response = " ".join(irc_text.split()[1:]) logger.debug("Value of irc_response: " + irc_response) connection.privmsg(possible_channel_name, irc_response) # Always learn from private messages from the bot's owner. Do not # respond to them if the bot's in ghost mode. Determine whether # or not a #channelname is at the head of the text and if so # elide it by setting the line of text from the IRC channel to # the IRC response which already has the #channelname removed. if "#" in possible_channel_name: irc_text = " ".join(irc_text.split()[1:]) logger.debug( "Got a possible channel name. Set value of irc_text to: " + str(irc_text)) # Train the bot on text sent by the bot's owner. json_response = json.loads(self._teach_brain(irc_text)) if json_response['id'] != 200: logger.warn( "DixieBot.on_privmsg(): Conversation engine returned error code " + str(json_response['id']) + ".") # Don't get responses when in ghost mode. if self.ghost: return # Get a response for text sent by the bot's owner. json_response = json.loads(self._get_response(irc_text)) if json_response['id'] != 200: logger.warn( "DixieBot.on_privmsg(): Conversation engine returned error code " + str(json_response['id']) + ".") return # Send the response text back to the bot's owner. connection.privmsg(sending_nick, json_response['response']) return else: logger.debug( "Somebody messaged me. The content of the message was: " + irc_text) # Helper method for authenticating the bot's owner. def _authenticate(self, connection, nick, text): logger.warn("IRC user " + nick + " is attempting to authenticate to the bot.") if self.password in text: connection.privmsg(nick, "Authentication confirmed. Welcome back.") self.owner = nick self.authenticated = True return else: connection.privmsg(nick, "Incorrect.") return # Helper method that implements online help. def _help(self, connection, nick): connection.privmsg(nick, "Here are the commands I support:") connection.privmsg( nick, "!help and !commands - You're reading them right now.") connection.privmsg(nick, "!quit - Shut me down.") connection.privmsg( nick, "!auth - Authenticate your current IRC nick as my admin.") connection.privmsg(nick, "!config - Send my current configuration.") connection.privmsg( nick, "!ping - Ping the conversation engine to make sure I can contact it." ) connection.privmsg(nick, "!nick <new nick> - Try to change my IRC nick.") connection.privmsg(nick, "!join <channel> - Join a channel.") connection.privmsg( nick, "!respond - Toggle respond/don't respond to users flag.") connection.privmsg(nick, "!ghosthelp - Get online help for ghost mode.") connection.privmsg( nick, "!ghost - Whether or not the bot's registered owner can remotely interact with a channel the bot's a member of using the bot as a client." ) return # Helper method that tells the bot's owner what the bot's current runtime # configuration is. def _current_config(self, connection, nick): connection.privmsg(nick, "Here's my current runtime configuration.") connection.privmsg(nick, "Channels I'm connected to: ") for channel in self.joined_channels: connection.privmsg(nick, " " + channel) connection.privmsg(nick, "Current nick: " + self.nick) connection.privmsg( nick, "Canonical name (for interacting with the conversation engine): " + self.canonical_name) connection.privmsg( nick, "Server and port: " + self.server + " " + str(self.port) + "/tcp") if self.usessl: connection.privmsg(nick, "My connection to the server is encrypted.") else: connection.privmsg(nick, "My connection to the server isn't encrypted.") if self.respond: connection.privmsg(nick, "I respond to people talking to me.") else: connection.privmsg(nick, "I don't respond to people talking to me.") if self.ghost: connection.privmsg(nick, "I am monitoring IRC channels in ghost mode.") else: connection.privmsg(nick, "I am not in ghost mode.") return # Helper method that pings the bot's conversation engine. I realize that # doing this is probably a little weird, but seeing as how I'm splitting # everything else out into helper methods to make adding functionality # later on easier I may as well. def _ping(self, connection, nick): connection.privmsg(nick, "Pinging the conversation engine...") http_connection = requests.get(self.engine + "/ping") if http_connection.text == "pong": connection.privmsg(nick, "I can hit the conversation engine.") else: connection.privmsg( nick, "I don't seem to be able to reach the conversation engine.") return # Helper method that will allow the bot to change its nick. def _nick(self, connection, text, nick): connection.privmsg(nick, "Trying to change my IRC nick...") self.nick = text.split()[1].strip() connection.nick(self.nick) logger.debug("New IRC nick: " + self.nick) connection.privmsg(nick, "Done.") return # Helper method that will allow the bot to join a channel. def _join(self, connection, text, nick): new_channel = text.split()[1].strip() connection.privmsg(nick, "Trying to join channel " + new_channel + ".") logger.debug("Trying to join channel " + new_channel + ".") connection.join(new_channel) self.joined_channels[new_channel] = 1 connection.privmsg(nick, "Joined " + new_channel + ".") return # Helper method that flips the bot's mode from "respond when spoken to" to # don't respond when spoken to. def _respond(self, connection, text, nick): if self.respond == True: self.respond = False logger.info("Turn off the bot's auto-response mode.") connection.privmsg(nick, "I won't respond to people talking to me.") return if self.respond == False: self.respond = True logger.info("Turn on the bot's auto-response mode.") connection.privmsg(nick, "Now responding to people talking to me.") return # Send the user online help for ghost mode. def _ghost_help(self, connection, nick): connection.privmsg( nick, "Ghost mode lets you interact with any channel I'm sitting in remotely so you don't have to join it." ) connection.privmsg( nick, "This is ideal if you want to maintain a certain degree of stealth." ) connection.privmsg( nick, "I can join the channel from one server and interact with everyone like a bot, and you can connect from another server without joining any channels, !auth to me, and communicate through me." ) connection.privmsg( nick, "If I get rumbled, I get bounced and your disposable server can be banned, and all you have to do is get a copy of my conversation engine to preserve me. You should be okay." ) connection.privmsg( nick, "Please note that if you have me join a number of busy channels you may not be able to keep up with all the traffic, so choose the channels I join wisely. Keep the number small for best results." ) connection.privmsg( nick, "Put the name of the channel you want me to send text to at the front of a private message, like this:" ) connection.privmsg(nick, "/msg botname") connection.privmsg(nick, "#somechannel Hello, world.") connection.privmsg( nick, "I will send activity in the channel back to you via the same privmsg as long as you're authenticated." ) return # Flips the ghost mode flag. def _ghost_mode(self, connection, nick): if self.ghost == False: self.ghost = True logger.info("Ghost mode now activated.") connection.privmsg(nick, "Ghost mode activated.") connection.privmsg( nick, "You can now interact with the following channels through me: " ) for channel in self.joined_channels: connection.privmsg(nick, " " + channel) return if self.ghost == True: self.ghost = False logger.info("Ghost mode now deactivated.") connection.privmsg(nick, "Ghost mode deactivated.") return # This method fires every time a public message is posted to an IRC # channel. Technically, 'line' should be 'event' but I'm just now getting # this module figured out... def on_pubmsg(self, connection, line): # JSON document from the conversation engine. json_response = {} # IRC nick that sent a line to the channel. sending_nick = line.source.split("!~")[0] logger.debug("Sending nick: " + sending_nick) # Line of text sent from the channel. irc_text = line.arguments[0] # If the line is from the bot's owner, learn from it and then decide # whether to respond or not. Just in case somebody grabs the nick of # the bot's owner, don't respond if they're not authenticated (because # that could go real bad, real fast...) if sending_nick == self.owner and self.authenticated: # If the bot's owner addressed it directly, always respond. Just # make sure to remove the bot's nick from the text to minimize # spurious entries in the bot's brain. asked_directly = irc_text.split(':')[0].strip() if asked_directly == self.nick: logger.debug( "The bot's owner addressed the construct directly. This is a special case." ) # Extract the dialogue from the text in the IRC channel. dialogue_text = irc_text.split(':')[1].strip() # Send a request to train the conversation engine on the text. logger.debug("Training engine on text: " + dialogue_text) json_response = json.loads(self._teach_brain(dialogue_text)) if json_response['id'] != int(200): logger.warn( "DixieBot.on_pubmsg(): Conversation engine returned error code " + str(json_response['id']) + ".") return # If the bot is in ghost mode, do not respond. if self.ghost: return # Get a response to the text from the channel. json_response = json.loads(self._get_response(irc_text)) if json_response['id'] != int(200): logger.warn( "DixieBot.on_pubmsg(): Conversation engine returned error code " + str(json_response['id']) + ".") return # Send the reply to the channel. connection.privmsg(line.target, json_response['response']) return # Otherwise, just learn from the bot's owner. json_response = json.loads(self._teach_brain(irc_text)) if json_response['id'] != int(200): logger.warn( "DixieBot.on_pubmsg(): Conversation engine returned error code " + str(json_response['id']) + ".") return # Check the respond/don't respond flag. If it's set to False, # don't say anything. if not self.respond: return # If the respond/don't respond flag it set to True, decide if the # bot is going to respond or not. To be polite to people, only # respond 5% of the time. 10% was too much. roll = random.randint(1, 100) if roll <= 5: json_response = json.loads(self._get_response(irc_text)) if json_response['id'] != int(200): logger.warn( "DixieBot.on_pubmsg(): Conversation engine returned error code " + str(json_response['id']) + ".") return # connection.privmsg() can be used to send text to either a # channel or a user. # Send the response. connection.privmsg(line.target, json_response['response']) return # If the line is not from the bot's owner, and the bot is in ghost # mode, relay the line to the bot's owner via privmsg. if self.ghost and self.authenticated: logger.debug("Relaying a line of text from " + line.target + " to the bot's owner.") connection.privmsg(self.owner, line.target + ":: " + irc_text) # If the line is not from the bot's owner, decide randomly if the bot # should learn from it, or learn from and respond to it. Respect the # respond/don't respond flag. roll = random.randint(1, 10) if roll == 1: logger.debug("Learning from the last line seen in the channel.") if self.wordfilter.blacklisted(irc_text): logger.warn("Wordfilter: Nope nope nope...") return json_response = json.loads(self._teach_brain(irc_text)) if json_response['id'] != int(200): logger.warn( "DixieBot.on_pubmsg(): Conversation engine returned error code " + str(json_response['id']) + ".") return if roll == 2: logger.debug( "Learning from the last line seen in the channel. I might respond to it." ) if self.wordfilter.blacklisted(irc_text): logger.warn("Wordfilter: Nope nope nope...") return json_response = json.loads(self._teach_brain(irc_text)) if json_response['id'] != int(200): logger.warn( "DixieBot.on_pubmsg(): Conversation engine returned error code " + str(json_response['id']) + ".") return # Check the respond/don't respond flag. If it's set to False, # don't say anything. if not self.respond: return # Get and send a response. json_response = json.loads(self._get_response(irc_text)) if json_response['id'] != int(200): logger.warn( "DixieBot.on_pubmsg(): Conversation engine returned error code " + str(json_response['id']) + ".") return connection.privmsg(line.target, json_response['response']) return # This method should fire when a client in the current channel emits a QUIT # event relayed by the server. It detects the bot's owner disconnecting # and deauthenticates them. def on_quit(self, connection, event): sending_nick = event.source.split("!~")[0] if event.type == "quit" and sending_nick == self.owner and self.authenticated: logger.info("The bot's owner has disconnected. Deauthenticating.") self.authenticated = False connection.privmsg(line.target, "Seeya, boss.") return # Sends text to train the conversation engine on. def _teach_brain(self, text): # Custom headers required by the conversation engine. headers = {"Content-Type": "application/json"} # HTTP request object handle. http_request = "" # JSON documents sent to and received from the conversation engine. json_request = {} json_request['botname'] = self.canonical_name json_request['apikey'] = self.api_key json_request['stimulus'] = text json_response = {} # Make an HTTP request to the conversation engine. http_request = requests.put(self.engine + "/learn", headers=headers, data=json.dumps(json_request)) json_response = json.loads(http_request.content) return json_response # Gets a response from the conversation engine. Return a response. def _get_response(self, text): # Custom headers required by the conversation engine. headers = {"Content-Type": "application/json"} # HTTP request object handle. http_request = "" # Response to send to the channel or user. response = "" # JSON documents sent to and received from the conversation engine. json_request = {} json_request['botname'] = self.canonical_name json_request['apikey'] = self.api_key json_request['stimulus'] = text json_response = {} # Contact the conversation engine to get a response. http_request = requests.get(self.engine + "/response", headers=headers, data=json.dumps(json_request)) json_response = json.loads(http_request.content) return json_response
class IRCClient(irc.client.SimpleIRCClient): """ This class represents an IRC client that must have its poll() called periodically to process messages incoming from the IRC server. The client is limited in its ability to handle messages incoming from the IRC server and only has enough functionality to respond to PING events to maintain its connection. The primary role of the client is to act as a relay for the external caller calling poll() so that messages from that external caller are passed to the joined IRC channel. """ # Heavily influenced by irc.client.SingleServerIRCBot and the example at # https://github.com/jaraco/irc/blob/master/scripts/testbot.py min_reconnect_wait = 1 max_reconnect_wait = 10 def __init__(self, channel, nickname, server, port=6667, **connect_params): super(IRCClient, self).__init__() if server is None: raise Exception("IRC Server not provided") if nickname is None or ' ' in nickname: raise Exception("Invalid nickname: must be one word") if channel is None or ' ' in channel or '#' not in channel: raise Exception("Invalid channel: must be a word starting with #") self.__connect_params = connect_params self.channels = IRCDict() self.channel = channel self.server = ServerSpec(server, port) self._nickname = nickname self.clientname = socket.gethostname().split('.')[0] assert 0 <= self.min_reconnect_wait <= self.max_reconnect_wait self._check_scheduled = False # Global handlers to handle channel/nick associations # Mostly when a nick is already in use for i in ["disconnect", "join", "kick", "mode", "namreply", "nick", "part", "quit"]: self.connection.add_global_handler( i, getattr(self, "_on_" + i), -20 ) def _on_disconnect(self, c, e): self.channels = IRCDict() self.reconnect() def reconnect(self): """ Called on a disconnect event to start a reconnection. The actual reconnection is deferred for some random amount of seconds. """ def check(): self._check_scheduled = False if not self.connection.is_connected(): self.reconnect() if self.connection.is_connected(): self.disconnect() self.connect() if self._check_scheduled: return reconnect_wait = max( self.min_reconnect_wait, int(self.max_reconnect_wait * random()) ) self.reactor.scheduler.execute_after(reconnect_wait, check) self._check_scheduled = True def _on_join(self, c, e): ch = e.target nick = e.source.nick if nick == c.get_nickname(): self.channels[ch] = Channel() self.channels[ch].add_user(nick) def _on_kick(self, c, e): nick = e.arguments[0] channel = e.target if nick == c.get_nickname(): del self.channels[channel] else: self.channels[channel].remove_user(nick) def _on_mode(self, c, e): t = e.target if not irc.client.is_channel(t): # mode on self; disregard return ch = self.channels[t] modes = irc.modes.parse_channel_modes(" ".join(e.arguments)) for sign, mode, argument in modes: f = {"+": ch.set_mode, "-": ch.clear_mode}[sign] f(mode, argument) def _on_namreply(self, c, e): """ e.arguments[0] == "@" for secret channels, "*" for private channels, "=" for others (public channels) e.arguments[1] == channel e.arguments[2] == nick list """ ch_type, channel, nick_list = e.arguments if channel == '*': # User is not in any visible channel # http://tools.ietf.org/html/rfc2812#section-3.2.5 return for nick in nick_list.split(): nick_modes = [] if nick[0] in self.connection.features.prefix: nick_modes.append(self.connection.features.prefix[nick[0]]) nick = nick[1:] for mode in nick_modes: self.channels[channel].set_mode(mode, nick) self.channels[channel].add_user(nick) def _on_nick(self, c, e): before = e.source.nick after = e.target for ch in self.channels.values(): if ch.has_user(before): ch.change_nick(before, after) def _on_part(self, c, e): nick = e.source.nick channel = e.target if nick == c.get_nickname(): del self.channels[channel] else: self.channels[channel].remove_user(nick) def _on_quit(self, c, e): nick = e.source.nick for ch in self.channels.values(): if ch.has_user(nick): ch.remove_user(nick) def on_nicknameinuser(self, c, e): """ When connecting it is discovered that the nick is already is use, provide an alternative. """ c.nick(c.get_nickname() + "_") def on_welcome(self, c, e): """ Automatically join the channel once welcomed by the server. """ c.join(self.channel) self.say('ttt has started on host {}'.format(self.clientname)) def on_privmsg(self, c, e): """ Disable private messaging with users. """ nick = e.source.nick msg = "I am sorry, {}; I do not do private messages.".format(nick) c.notice(nick, msg) def on_dccmsg(self, c, e): """ Disable dcc messaging with users. """ nick = e.source.nick # text = e.arguments[0].decode('utf-8') msg = "I am sorry, {}; I do not do dcc messages.".format(nick) c.privmsg(nick, msg) def disconnect(self): self.connection.disconnect("Bye!") def get_version(self): return "irc.client ({version})".format( version=irc.client.VERSION_STRING) def on_ctcp(self, c, e): nick = e.source.nick if e.arguments[0] == "VERSION": c.ctcp_reply(nick, "VERSION " + self.get_version()) elif e.arguments[0] == "PING": if len(e.arguments) > 1: c.ctcp_reply(nick, "PING " + e.arguments[1]) def on_pubmsg(self, c, e): nick = e.source.nick command = e.arguments[0] if 'hello' in command or 'hi' in command: self.say("Hello, {}".format(nick)) elif 'help' in command: self.say("Commands are: version".format(nick)) elif 'version' in command: self.say("I am running ttt {} using {} on {}".format( __version__, self.get_version(), self.clientname )) def poll(self): """ Poll the IRC connection for events. The reactor processing uses select on the socket so give a 0.2s timeout so that control returns. Polling must occur or else the IRC server will likely disconnect the client due to ping timeout. """ self.reactor.process_once(0.2) def connect(self): server = self.server try: super(IRCClient, self).connect( server.host, server.port, self._nickname, server.password, username=self._nickname, ircname=self._nickname, **self.__connect_params ) except irc.client.ServerConnectionError: self.reconnect() # Schedule a deferred reconnection retry pass def say(self, message): """ The main interface into this client (other than poll() and connect()) to send messages from the external caller to the IRC channel. If there happens to be no connection when something is said, then say nothing. A (re)connection is either happening or the server is down. Note that this is an issue only because a message is sent to the IRC server explicitly and not a handler reacting to something the server has sent (which requires that the connection is up to occur). """ try: self.connection.privmsg(self.channel, message) except irc.client.ServerNotConnectedError: # Connection down? Try a reconnect, and skip saying anything. The # time to say it has already passed. self.reconnect() pass
class PanChan(Channel): """Uses the strings from namreply to set the values for each user in a channel.""" def __init__(self, namreply=None): Channel.__init__(self) self.admindict = IRCDict() self.logdict = IRCDict() if namreply: for nick in namreply.split(' '): if nick[0] == '~': self.ownerdict[nick[1:]] = 1 elif nick[0] == '&': self.admindict[nick[1:]] = 1 elif nick[0] == '@': self.operdict[nick[1:]] = 1 elif nick[0] =='%': self.halfopdict[nick[1:]] = 1 elif nick[0] == '+': self.voicedict[nick[1:]] = 1 else: self.userdict[nick] = 1 self.logdict[nick] = tempfile.NamedTemporaryFile(bufsize=5120) continue self.userdict[nick[1:]] = 1 self.logdict[nick[1:]] = tempfile.NamedTemporaryFile(bufsize=5120) def add_user(self, nick): self.logdict[nick] = tempfile.NamedTemporaryFile(bufsize=5120) Channel.add_user(self, nick) def clear_mode(self, mode, value=None): if mode == 'a': del self.admindict[value] else: Channel.clear_mode(self, mode, value) def change_nick(self, before, after): Channel.change_nick(self, before, after) self.logdict[after] = self.logdict[before] del self.logdict[before] def get_userlog(self, nick): if self.logdict.has_key(nick): log = self.logdict[nick] log.seek(0) return log.readlines() return None def has_privs(self, nick): return self.is_oper(nick) or self.is_halfop(nick) or self.isadmin or self.is_owner(nick) def handle_modes(self, modes, args): m = mod.match(modes) if m: l = list(args) s = m.group('mode1') if s[0] == '+': for each in s[1:]: if each in 'qaohv' and l != []: self.set_mode(each, l.pop(0)) else: self.set_mode(each) else: for each in s[1:]: if each in 'qaohv' and l != []: self.clear_mode(each, l.pop(0)) else: self.clear_mode(each) if m.group('mode2'): self.handle_modes(m.group('mode2'), *l) def is_admin(self, nick): return nick in self.admindict def log_user(self, nick, msg): log = self.logdict[nick] log.seek(0) lines = log.readlines() if len(lines) < LINE_LIMIT: log.write(msg + '\n') elif len(lines) == LINE_LIMIT: lines.pop(0) lines.append(msg + '\n') log.seek(0) log.writelines(*lines) log.flush() def set_mode(self, mode, value=None): if mode == 'a': self.admindict[value] = 1 else: Channel.set_mode(self, mode, value)
def _on_disconnect(self, c, e): self.channels = IRCDict() self.reconnect()
def _on_disconnect(self, c, e): self.channels = IRCDict() self.connection.execute_delayed(self.reconnection_interval, self._connected_checker)