irc-skill/__init__.py
2018-09-08 17:28:00 +01:00

404 lines
13 KiB
Python

from threading import Thread
from time import sleep
from time import time
import select
import socket
import socks
import ssl
import re
from adapt.intent import IntentBuilder
from mycroft.audio import wait_while_speaking
from mycroft import MycroftSkill, intent_handler
from mycroft.skills.core import MycroftSkill
from mycroft.util.log import getLogger
from mycroft.util import normalize
#########################################
# RFC #
# https://tools.ietf.org/html/rfc1459 #
# We don't fullfil all aspects of the #
# RFC to keep it simple. If you think #
# a command is needed, feel free to #
# open a PR with your changes. #
#########################################
LOGGER = getLogger(__name__)
class IRCSkill(MycroftSkill):
def __init__(self):
super(IRCSkill, self).__init__()
# TODO make them configureable
# TODO make them into lists
# options
self.settings['proxy'] = ""
self.settings['proxy-port'] = 9050
self.settings['proxy-user'] = ""
self.settings['proxy-passwd'] = ""
self.settings['server'] = "irc.freenode.net"
self.settings['server-password'] = ""
self.settings['port'] = 6697
self.settings['channel'] = "mycroft"
self.settings['channel-password'] = ""
self.settings['user'] = "dummy|m"
self.settings['password'] = ""
self.settings['ssl'] = True
self.settings['debug'] = False
self.settings['msg-join'] = True
self.settings['msg-part'] = True
self.settings['msg-disc'] = True
# IPC for comunicating between threads
self.irc_lock = False
self.irc_cmd = ""
self.irc_str = ""
def initialize(self):
if self.settings['proxy'] != "":
if self.settings['debug']:
self.speak("Using proxy: " + self.settings['proxy'])
self.speak("Port: " + str(self.settings['proxy-port']))
socks.set_default_proxy(socks.SOCKS5, self.settings['proxy'], self.settings['proxy-port'], True, 'user','passwd')
socket.socket = socks.socksocket
self._irc_start_thread()
@intent_handler(IntentBuilder('ConnectIntent').require('connect'))
def handle_connect_intent(self, message):
# TODO ability to connect to different server
if self.con_thread.isAlive() == False:
self._irc_start_thread()
if self.irc_lock == False:
self.speak("Connecting")
self.irc_lock == True
self.irc_cmd = "connect"
self.irc_str = ""
self.irc_lock = False
@intent_handler(IntentBuilder('JoinIntent').require('join'))
def handle_join_intent(self, message):
# TODO ability to join to different channels
if self.con_thread.isAlive() == False:
self._irc_start_thread()
if self.irc_lock == False:
self.speak("Joining")
self.irc_lock == True
self.irc_cmd = "join"
self.irc_str = ""
self.irc_lock = False
@intent_handler(IntentBuilder('PartIntent').require('part'))
def handle_part_intent(self, message):
# TODO ability to join to different channels
if self.con_thread.isAlive() == False:
self._irc_start_thread()
if self.irc_lock == False:
self.speak("Parting")
self.irc_lock == True
self.irc_cmd = "part"
self.irc_str = ""
self.irc_lock = False
@intent_handler(IntentBuilder('DisconnectIntent').require('disconnect'))
def handle_disconnect_intent(self, message):
# TODO ability to disconnect from different server
if self.con_thread.isAlive() == False:
self._irc_start_thread()
if self.irc_lock == False:
self.speak("Disconnecting")
self.irc_lock == True
self.irc_cmd = "disconnect"
self.irc_str = ""
self.irc_lock = False
@intent_handler(IntentBuilder('SendIntent').require('send'))
def handle_send_intent(self, message):
# TODO ability to send to different users and channels
if self.con_thread.isAlive() == False:
self._irc_start_thread()
if self.irc_lock == False:
response = self.get_response("get_msg")
if response != None:
self.irc_lock == True
self.irc_cmd = "send"
self.irc_str = response
self.irc_lock = False
else:
self.speak("I didn't understand a message")
@intent_handler(IntentBuilder('SetUserIntent').require('set-user'))
def handle_set_user_intent(self, message):
self._irc_set_user()
@intent_handler(IntentBuilder('DebugEnableIntent').require('debug-enable'))
def handle_debug_enable_intent(self, message):
self.settings['debug'] = True
self.speak("Debugging enabled")
@intent_handler(IntentBuilder('DebugDisableIntent').require('debug-disable'))
def handle_debug_disable_intent(self, message):
self.settings['debug'] = False
self.speak("Debugging disabled")
def _main_loop(self):
# Connecttion status: 0 = not connected, 1 = requested, 2 = connected
connection_state = 0
# Join status: 0 = not joined, 1 = requested, 2 = joined
join_statues = 0
while True:
sleep(2)
if connection_state != 0:
text = ""
try:
ready = select.select([irc], [], [], 2)
if ready[0]:
text = irc.recv(2040).decode()
except Exception:
continue
for line in text.splitlines():
if line != "":
if self.settings['debug']:
self.speak(str(line))
pass
# Prevent Timeout
match = re.search("^PING (.*)$", line, re.M)
if match != None:
irc.send(('PONG ' + match.group(1) + '\r\n').encode())
# detect timed out connections
if int(time()) - self.last_ping > 240:
self.speak("The connection has timed out. I try to reconnect you")
self._irc_disconnect(irc, True)
self._irc_connect(self.settings['server'], self.settings['port'], self.settings['ssl'], self.settings['server-password'], self.settings['user'], self.settings['password'])
self.speak("You're reconnected")
self.last_ping = int(time())
# reciving normal messages
match = re.search("^:(.*)!.*@.* JOIN", line, re.M)
if match != None:
if match.group(1) == self.settings['user']:
join_status = 2
self.speak("Joined")
else:
if self.settings['msg-join']:
self.speak(match.group(1) + " has joined the channel")
match = re.search("^:(.*)!.*@.* PART", line, re.M)
if match != None:
if self.settings['msg-part']:
self.speak(match.group(1) + " has left the channel")
match = re.search("^:(.*)!.*@.* QUIT", line, re.M)
if match != None:
if match.group(1) == self.settings['user']:
self.speak("You have been disconnected. I try to reconnect you")
was_joined = join_status
self._irc_disconnect(irc, True)
self._irc_connect(self.settings['server'], self.settings['port'], self.settings['ssl'], self.settings['server-password'], self.settings['user'], self.settings['password'])
self.speak("You are reconnected")
if was_joined == 2:
self._irc_join(irc, self.settings['channel'], self.settings['channel-password'])
elif self.settings['msg-disc']:
self.speak(match.group(1) + " has disconnected")
match = re.search("^:(.*)!.*@.* PRIVMSG #(.*) :(.*)", line, re.M)
if match != None:
self.speak(match.group(1) + " has written in " + match.group(2) + ": " + match.group(3))
match = re.search("^:(.*)!.*@.* NOTICE #.* :(.*)", line, re.M)
if match != None:
self.speak(match.group(1) + " has written a notice to " + match.group(2) + ". The notice is: " + match.group(3))
match = re.search("^:(.*)!.*@.* PRIVMSG " + re.escape(self.settings['user']) + " :(.*)$", line, re.M)
if match != None:
self.speak(match.group(1) + " has written you a private message: " + match.group(2))
match = re.search(":(.*)!.*@.* NOTICE " + re.escape(self.settings['user']) + " :(.*)", line)
if match != None:
self.speak(match.group(1) + " has written a private notice to you. The notice is: " + match.group(2))
# reciving status codes
match = re.search("^:(\S*\.*.\S*) (\d{3}) (.*)", line)
if match != None:
code = int(match.group(2))
if self.settings['debug']:
self.speak("Return code: " + str(code))
# This list of handles replies is incomplete
# We use the MOTD or the missing MOTD message for verify that a connect was successfull
if code == 372:
if connection_state != 2:
connection_state = 2
self.speak("Connected")
elif code == 401:
self.speak("The nickname wasn't found")
elif code == 422:
if connection_state != 2:
connection_state = 2
self.speak("Connected")
elif code == 433:
self.speak("Your nickname is already in use")
elif code == 464:
self.speak("It looks, like you password is wrong. Please check it")
elif code == 465:
self.speak("You're banned on this server")
# handling special messages
match = re.search("^ERROR :Closing link", line)
if match != None:
self.speak("The server has closed the connection")
connection_state = 0
irc.close()
cmd = ""
string = ""
if self.irc_cmd != "":
if self.irc_lock == False:
self.irc_lock = True
cmd = self.irc_cmd
string = self.irc_str
self.irc_cmd = ""
self.irc_str = ""
self.irc_lock = False
if cmd != "":
# check cmd and take action
if cmd == "connect":
# TODO add ability to connect to more than one server
if connection_state != 2:
connection_state, irc = self._irc_connect(self.settings['server'], self.settings['port'], self.settings['ssl'], self.settings['server-password'], self.settings['user'], self.settings['password'])
else:
self.speak("Already connected")
elif cmd == "join":
if connection_state == 2:
join_status = self._irc_join(irc, self.settings['channel'], self.settings['channel-password'])
else:
self.speak("Please connect to a server first")
elif cmd == "part":
if connection_state == 2:
if join_status == 2:
join_status = self._irc_part(irc, self.settings['channel'])
else:
self.speak("I'm in no channel I could part from")
else:
self.speak("Not connected to a server")
elif cmd == "disconnect":
if connection_state != 0:
connection_state = self._irc_disconnect(irc)
else:
self.speak("I'm to no server connected")
if connection_state == 0:
join_status = 0
elif cmd == "send":
if connection_state == 2:
if join_status == 2:
self._irc_send(irc, "#" + self.settings['channel'], string)
else:
self.speak("Please join a channel first")
else:
self.speak("Please connect to a server and join a channel first")
def _irc_connect(self, server, port, ssl_req, server_password, user, password):
# check if the default username is used and if so, ask the user to change it
if user == "dummy|m":
self.speak("You're using the default user name. Please change it now.")
changed = False
while changed == False:
changed = self._irc_set_user()
user = self.settings['user']
irc = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #defines the socket
# Connect
try:
if self.settings['debug']:
self.speak("Server: " + server)
self.speak("Port: " + str(port))
irc.settimeout(15)
irc.connect((server, port))
except Exception as e:
self.speak("Unable to connect to server.")
if self.settings['debug']:
self.speak("Error: " + str(e))
return False, irc
if ssl_req:
if self.settings['debug']:
self.speak("Use SSL")
irc = ssl.wrap_socket(irc)
irc.setblocking(0)
if server_password != "":
irc.send(("PASS %s\n" % (password)).encode())
irc.send(("USER " + user + " " + user + " " + user + " :IRC via VOICE -> Mycroft\n").encode())
irc.send(("NICK " + user + "\n").encode())
if password != "":
irc.send(("PRIVMSG nickserv :identify %s %s\r\n" % (user, password)).encode())
self.last_ping = int(time())
return 1, irc
def _irc_join(self, irc, channel, channel_password):
string = "JOIN #"+ channel
if channel_password != "":
string = string + " " + channel_password
irc.send((string +"\n").encode())
return 1
def _irc_part(self, irc, channel):
irc.send(("PART #" + channel).encode())
return 0 # this is the value that's written in `joined`
def _irc_disconnect(self, irc, quiet=False):
irc.send(("QUIT :Disconnected my mycroft\n").encode())
irc.close()
if quiet == False:
self.speak("Disconnected")
return 0 # this is the value that's written in `connected`
def _irc_send(self, irc, to, msg):
irc.send(("PRIVMSG " + to + " :" + msg + "\n").encode())
self.speak("Message sent")
def _irc_set_user(self):
self.speak("Which user name do you want to use?")
wait_while_speaking()
response = self.get_response("dummy")
if response != None:
response = response.replace(" ", "")
self.settings['user'] = response
self.speak("User name changed")
return True
self.speak("I didn't understand a user name")
return False
def _irc_start_thread(self):
if self.settings['debug']:
self.speak("Restart thread")
self.con_thread = Thread(target=self._main_loop)
self.con_thread.setDaemon(False)
self.con_thread.start()
def stop(self):
pass
def create_skill():
return IRCSkill()