296 lines
11 KiB
Python
296 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
# Nemubot is a modulable IRC bot, built around XML configuration files.
|
|
# Copyright (C) 2012 Mercier Pierre-Olivier
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program 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 Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import errno
|
|
import os
|
|
import ssl
|
|
import socket
|
|
import threading
|
|
import traceback
|
|
|
|
from channel import Channel
|
|
from server.DCC import DCC
|
|
from hooks import Hook
|
|
import message
|
|
import server
|
|
import xmlparser
|
|
|
|
class IRCServer(server.Server):
|
|
"""Class to interact with an IRC server"""
|
|
|
|
def __init__(self, node, nick, owner, realname, ssl=False):
|
|
"""Initialize an IRC server
|
|
|
|
Arguments:
|
|
node -- server node from XML configuration
|
|
nick -- nick used by the bot on this server
|
|
owner -- nick used by the bot owner on this server
|
|
realname -- string used as realname on this server
|
|
ssl -- require SSL?
|
|
"""
|
|
self.node = node
|
|
|
|
server.Server.__init__(self)
|
|
|
|
self.nick = nick
|
|
self.owner = owner
|
|
self.realname = realname
|
|
self.ssl = ssl
|
|
|
|
# Listen private messages?
|
|
self.listen_nick = True
|
|
|
|
self.dcc_clients = dict()
|
|
|
|
self.channels = dict()
|
|
for chn in self.node.getNodes("channel"):
|
|
chan = Channel(chn["name"], chn["password"])
|
|
self.channels[chan.name] = chan
|
|
|
|
|
|
@property
|
|
def host(self):
|
|
"""Return the server hostname"""
|
|
if self.node is not None and self.node.hasAttribute("server"):
|
|
return self.node["server"]
|
|
else:
|
|
return "localhost"
|
|
|
|
@property
|
|
def port(self):
|
|
"""Return the connection port used on this server"""
|
|
if self.node is not None and self.node.hasAttribute("port"):
|
|
return self.node.getInt("port")
|
|
else:
|
|
return "6667"
|
|
|
|
@property
|
|
def password(self):
|
|
"""Return the password used to connect to this server"""
|
|
if self.node is not None and self.node.hasAttribute("password"):
|
|
return self.node["password"]
|
|
else:
|
|
return None
|
|
|
|
@property
|
|
def allow_all(self):
|
|
"""If True, treat message from all channels, not only listed one"""
|
|
return (self.node is not None and self.node.hasAttribute("allowall")
|
|
and self.node["allowall"] == "true")
|
|
|
|
@property
|
|
def autoconnect(self):
|
|
"""Autoconnect the server when added"""
|
|
if self.node is not None and self.node.hasAttribute("autoconnect"):
|
|
value = self.node["autoconnect"].lower()
|
|
return value != "no" and value != "off" and value != "false"
|
|
else:
|
|
return False
|
|
|
|
@property
|
|
def id(self):
|
|
"""Gives the server identifiant"""
|
|
return self.host + ":" + str(self.port)
|
|
|
|
def register_hooks(self):
|
|
self.add_hook(Hook(self.evt_channel, "JOIN"))
|
|
self.add_hook(Hook(self.evt_channel, "PART"))
|
|
self.add_hook(Hook(self.evt_server, "NICK"))
|
|
self.add_hook(Hook(self.evt_server, "QUIT"))
|
|
self.add_hook(Hook(self.evt_channel, "332"))
|
|
self.add_hook(Hook(self.evt_channel, "353"))
|
|
|
|
def evt_server(self, msg, srv):
|
|
for chan in self.channels:
|
|
self.channels[chan].treat(msg.cmd, msg)
|
|
|
|
def evt_channel(self, msg, srv):
|
|
if msg.channel is not None:
|
|
if msg.channel in self.channels:
|
|
self.channels[msg.channel].treat(msg.cmd, msg)
|
|
|
|
def accepted_channel(self, chan, sender=None):
|
|
"""Return True if the channel (or the user) is authorized"""
|
|
if self.allow_all:
|
|
return True
|
|
elif self.listen_nick:
|
|
return (chan in self.channels and (sender is None or sender in
|
|
self.channels[chan].people)
|
|
) or chan == self.nick
|
|
else:
|
|
return chan in self.channels and (sender is None or sender
|
|
in self.channels[chan].people)
|
|
|
|
def join(self, chan, password=None, force=False):
|
|
"""Join a channel"""
|
|
if force or (chan is not None and
|
|
self.connected and chan not in self.channels):
|
|
self.channels[chan] = Channel(chan, password)
|
|
if password is not None:
|
|
self.s.send(("JOIN %s %s\r\n" % (chan, password)).encode())
|
|
else:
|
|
self.s.send(("JOIN %s\r\n" % chan).encode())
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def leave(self, chan):
|
|
"""Leave a channel"""
|
|
if chan is not None and self.connected and chan in self.channels:
|
|
if isinstance(chan, list):
|
|
for c in chan:
|
|
self.leave(c)
|
|
else:
|
|
self.s.send(("PART %s\r\n" % self.channels[chan].name).encode())
|
|
del self.channels[chan]
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
# Main loop
|
|
def run(self):
|
|
if not self.connected:
|
|
self.s = socket.socket() #Create the socket
|
|
if self.ssl:
|
|
ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
|
|
self.s = ctx.wrap_socket(self.s)
|
|
try:
|
|
self.s.connect((self.host, self.port)) #Connect to server
|
|
except socket.error as e:
|
|
self.s = None
|
|
self.logger.critical("Unable to connect to %s:%d: %s"
|
|
% (self.host, self.port, os.strerror(e.errno)))
|
|
return
|
|
self.stopping.clear()
|
|
|
|
if self.password != None:
|
|
self.s.send(b"PASS " + self.password.encode () + b"\r\n")
|
|
self.s.send(("NICK %s\r\n" % self.nick).encode ())
|
|
self.s.send(("USER %s %s bla :%s\r\n" % (self.nick, self.host,
|
|
self.realname)).encode())
|
|
raw = self.s.recv(1024)
|
|
if not raw:
|
|
self.logger.critical("Unable to connect to %s:%d" % (self.host, self.port))
|
|
return
|
|
self.connected = True
|
|
self.logger.info("Connection to %s:%d completed" % (self.host, self.port))
|
|
|
|
if len(self.channels) > 0:
|
|
for chn in self.channels.keys():
|
|
self.join(self.channels[chn].name,
|
|
self.channels[chn].password, force=True)
|
|
|
|
|
|
readbuffer = b'' #Here we store all the messages from server
|
|
try:
|
|
while not self.stop:
|
|
readbuffer = readbuffer + raw
|
|
temp = readbuffer.split(b'\n')
|
|
readbuffer = temp.pop()
|
|
|
|
for line in temp:
|
|
self.treat_msg(line)
|
|
raw = self.s.recv(1024) #recieve server messages
|
|
except socket.error:
|
|
pass
|
|
|
|
if self.connected:
|
|
self.s.close()
|
|
self.connected = False
|
|
if self.closing_event is not None:
|
|
self.closing_event()
|
|
self.logger.info("Server `%s' successfully stopped." % self.id)
|
|
self.stopping.set()
|
|
# Rearm Thread
|
|
threading.Thread.__init__(self)
|
|
|
|
|
|
# Overwritted methods
|
|
|
|
def disconnect(self):
|
|
"""Close the socket with the server and all DCC client connections"""
|
|
#Close all DCC connection
|
|
clts = [c for c in self.dcc_clients]
|
|
for clt in clts:
|
|
self.dcc_clients[clt].disconnect()
|
|
return server.Server.disconnect(self)
|
|
|
|
|
|
|
|
# Abstract methods
|
|
|
|
def send_pong(self, cnt):
|
|
"""Send a PONG command to the server with argument cnt"""
|
|
self.s.send(("PONG %s\r\n" % cnt).encode())
|
|
|
|
def msg_treated(self, origin):
|
|
"""Do nothing; here for implement abstract class"""
|
|
pass
|
|
|
|
def send_dcc(self, msg, to):
|
|
"""Send a message through DCC connection"""
|
|
if msg is not None and to is not None:
|
|
realname = to.split("!")[1]
|
|
if realname not in self.dcc_clients.keys():
|
|
d = DCC(self, to)
|
|
self.dcc_clients[realname] = d
|
|
self.dcc_clients[realname].send_dcc(msg)
|
|
|
|
def send_msg_final(self, channel, line, cmd="PRIVMSG", endl="\r\n"):
|
|
"""Send a message without checks or format"""
|
|
#TODO: add something for post message treatment here
|
|
if channel == self.nick:
|
|
self.logger.warn("Nemubot talks to himself: %s" % msg)
|
|
self.logger.debug(traceback.print_stack())
|
|
if line is not None and channel is not None:
|
|
if self.s is None:
|
|
self.logger.warn("Attempt to send message on a non connected server: %s: %s" % (self.id, line))
|
|
self.logger.debug(traceback.format_stack())
|
|
elif len(line) < 442:
|
|
self.s.send(("%s %s :%s%s" % (cmd, channel, line, endl)).encode ())
|
|
else:
|
|
self.logger.warn("Message truncated due to size (%d ; max : 442) : %s" % (len(line), line))
|
|
self.logger.debug(traceback.format_stack())
|
|
self.s.send (("%s %s :%s%s" % (cmd, channel, line[0:442]+"<…>", endl)).encode ())
|
|
|
|
def send_msg_usr(self, user, msg):
|
|
"""Send a message to a user instead of a channel"""
|
|
if user is not None and user[0] != "#":
|
|
realname = user.split("!")[1]
|
|
if realname in self.dcc_clients or user in self.dcc_clients:
|
|
self.send_dcc(msg, user)
|
|
else:
|
|
for line in msg.split("\n"):
|
|
if line != "":
|
|
self.send_msg_final(user.split('!')[0], msg)
|
|
|
|
def send_msg(self, channel, msg, cmd="PRIVMSG", endl="\r\n"):
|
|
"""Send a message to a channel"""
|
|
if self.accepted_channel(channel):
|
|
server.Server.send_msg(self, channel, msg, cmd, endl)
|
|
|
|
def send_msg_verified(self, sender, channel, msg, cmd = "PRIVMSG", endl = "\r\n"):
|
|
"""Send a message to a channel, only if the source user is on this channel too"""
|
|
if self.accepted_channel(channel, sender):
|
|
self.send_msg_final(channel, msg, cmd, endl)
|
|
|
|
def send_global(self, msg, cmd="PRIVMSG", endl="\r\n"):
|
|
"""Send a message to all channels on this server"""
|
|
for channel in self.channels.keys():
|
|
self.send_msg(channel, msg, cmd, endl)
|