Skip to content

Commit

Permalink
first version of mattermost and matrix backends
Browse files Browse the repository at this point in the history
  • Loading branch information
Guilhem Saurel committed Sep 14, 2017
1 parent 49b0922 commit 9eec6ee
Show file tree
Hide file tree
Showing 5 changed files with 279 additions and 22 deletions.
43 changes: 30 additions & 13 deletions pipobot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from pipobot.lib.loader import BotModuleLoader
from pipobot.translation import setup_i18n
from pipobot.bot_jabber import BotJabber, XMPPException
from pipobot.bot_mattermost import BotMattermost, MattermostException
from pipobot.bot_test import TestBot

LOGGER = logging.getLogger('pipobot.manager')
Expand Down Expand Up @@ -175,7 +176,7 @@ def _daemonize(self, fd):
getattr(sys, desc).close()
setattr(sys, desc, null)

def _jabber_bot(self, rooms, modules):
def _bot(self, rooms, modules):
signal.signal(signal.SIGINT, self._signal_handler)
signal.signal(signal.SIGTERM, self._signal_handler)
signal.signal(signal.SIGQUIT, self._signal_handler)
Expand All @@ -189,19 +190,35 @@ def _jabber_bot(self, rooms, modules):
bots = []

for room in rooms:
try:
bot = BotJabber(room.login, room.passwd, room.resource,
room.chan, room.nick, modules[room],
self._db_session, self._config.force_ipv4,
room.address, room.port)
except XMPPException as exc:
LOGGER.error("Unable to join room '%s': %s", room.chan,
exc)
continue

LOGGER.info("joining room %s on %s", room.chan, room.protocol)
if room.protocol == 'xmpp':
try:
bot = BotJabber(room.login, room.passwd, room.resource,
room.chan, room.nick, modules[room],
self._db_session, self._config.force_ipv4,
room.address, room.port)
except XMPPException as exc:
LOGGER.error("Unable to join room '%s': %s", room.chan, exc)
continue
elif room.protocol == 'mattermost':
try:
bot = BotMattermost(login=room.login, passwd=room.passwd, modules=modules[room],
session=self._db_session, address=room.address, default_team=room.default_team,
default_channel=room.default_channel)
except MattermostException as exc:
LOGGER.error("Unable to join mattermost '%s': %s", room.address, exc)
continue
elif room.protocol == 'matrix':
try:
# Avoid importing matrix_client lib if not necessary
from pipobot.bot_matrix import BotMatrix # isort:skip
bot = BotMatrix(login=room.login, passwd=room.passwd, chan=room.chan, modules=modules[room],
session=self._db_session, address=room.address)
except Exception as exc:
LOGGER.error("Unable to join matrix '%s': %s", room.chan, exc)
continue
bots.append(bot)


while self.is_running:
signal.pause()

Expand Down Expand Up @@ -294,7 +311,7 @@ def run(self):
if e :
_abort("Unable to load all modules")

self._jabber_bot(rooms, m)
self._bot(rooms, m)

LOGGER.debug("Exiting…")
logging.shutdown()
Expand Down
110 changes: 110 additions & 0 deletions pipobot/bot_matrix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# !/usr/bin/python
"""This file contains the class 'BotMatrix' which is a bot for Matrix Chan"""

import logging
import threading
import time

from matrix_client.client import MatrixClient

from pipobot.bot import PipoBot

logger = logging.getLogger('pipobot.bot_matrix')


class BotMatrix(PipoBot):
"""The implementation of a bot for Matrix Chan"""

def __init__(self, login, passwd, chan, modules, session, name='pipobot', address="https://matrix.org"):
logger.info("Connecting to %s", address)
self.client = MatrixClient(address)
logger.debug("login in")
token = self.client.login_with_password(username=login, password=passwd)
if token:
logger.debug("logged in")
else:
logger.error("login failed")
self.room = self.client.join_room(chan)
self.room.add_listener(self.on_message)
logger.debug("connected to %s", self.room)
if name:
logger.debug("set name to %s", name)
self.client.get_user(self.client.user_id).set_display_name(name)
self.name = name

super(BotMatrix, self).__init__(name, login, chan, modules, session)

logger.debug("start listener thread")
self.client.start_listener_thread()
self.say(_("Hi there"))
logger.info("init done")

def on_message(self, room, event):
logger.debug("new event")
if event['type'] == 'm.room.message':
logger.debug("event is a message")
self.message_handler(event)
elif event['type'] == 'm.room.member':
logger.debug("event is a presence")
self.presence_handler(event)

def message_handler(self, event):
"""Method called when the bot receives a message"""
# We ignore messages in some cases :
# - the bot is muted
# - the message is empty
if self.mute or event["content"]["body"] == "":
return

thread = threading.Thread(target=self.answer, args=(event,))
thread.start()

def answer(self, mess):
logger.debug('handling message')
result = self.module_answer(mess)
if type(result) is list:
for to_send in result:
self.say(to_send)
else:
self.say(result)
logger.debug('handled message')

def kill(self):
"""Method used to kill the bot"""

# The bot says goodbye
self.say(_("I’ve been asked to leave you"))
# The bot leaves the room
self.client.logout()
self.stop_modules()
logger.info('killed')

def say(self, msg, priv=None, in_reply_to=None):
"""The method to call to make the bot sending messages"""
# If the bot has not been disabled
logger.debug('say %s', msg)
if not self.mute:
if type(msg) is str or type(msg) is unicode:
self.room.send_text(msg)
elif type(msg) is list:

for line in msg:
time.sleep(0.3)
self.room.send_text(line)
elif type(msg) is dict:
if "users" in msg:
pass
else:
if "xhtml" in mess:
mess_xhtml = mess["xhtml"]
mess_xhtml = "<p>%s</p>" % mess_xhtml
if type(mess_xhtml) is unicode:
mess_xhtml = mess_xhtml.encode("utf-8")
self.room.send_html(mess_xhtml, msg["text"])
else:
self.room.send_text(msg["text"])

def presence_handler(self, mess):
"""Method called when the bot receives a presence message.
Used to record users in the room, as well as their jid and roles"""
pass
113 changes: 113 additions & 0 deletions pipobot/bot_mattermost.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# !/usr/bin/python
"""This file contains the class 'BotMattermost' which is a bot for Mattermost API"""

import logging
import threading
import time
from json import loads, dumps
from websocket import create_connection
import requests

from pipobot.bot import PipoBot

logger = logging.getLogger('pipobot.bot_mattermost')


class MattermostException(Exception):
""" For errors due to Mattermost (conflict, connection/authentification failed, …) """
pass


class BotMattermost(PipoBot):
"""The implementation of a bot for a Mattermost instance"""

def __init__(self, login, passwd, modules, session, address, default_team, default_channel):
address += '/api/v4'
self.address = address
auth = requests.post('https://%s/users/login' % address, json={'login_id': login, 'password': passwd})

if auth.status_code != 200:
logger.error(_("Unable to connect !"))
raise MattermostException(_("Unable to connect !"))

self.headers = {'Authorization': 'Bearer %s' % auth.headers['Token']}
self.user_id = requests.get('https://%s/users/me' % address, headers=self.headers).json()['id']
team_url = 'https://%s/teams/name/%s' % (address, default_team)
self.default_team_id = requests.get(team_url, headers=self.headers).json()['id']
channel_url = 'https://%s/teams/%s/channels/name/%s' % (address, self.default_team_id, default_channel)
self.default_channel_id = requests.get(channel_url, headers=self.headers).json()['id']

challenge = dumps({"seq": 1, "action": "authentication_challenge", "data": {'token': auth.headers['Token']}})
logger.debug('creating WS')
self.ws = create_connection('wss://%s/websocket' % address)
logger.debug('Sending challenge')
self.ws.send(challenge)

if not self.ws.connected:
logger.error(_("Unable to authenticate websocket !"))
raise MattermostException(_("Unable to authenticate websocket !"))

super(BotMattermost, self).__init__('...', login, default_channel, modules, session)

self.thread = threading.Thread(name='mattermost_' + default_channel, target=self.process)
self.thread.start()
self.say(_("Hi there"))

def process(self):
self.run = True
while self.run:
msg = loads(self.ws.recv())
self.message_handler(msg)


def message_handler(self, msg):
"""Method called when the bot receives a message"""
if self.mute or 'event' not in msg or msg['event'] != 'posted':
return

thread = threading.Thread(target=self.answer, args=(msg,))
thread.start()

def answer(self, mess):
post = loads(mess['data']['post'])
message = {'body': post['message'], 'from': DummySender(mess['data']['sender_name']), 'type': 'chat'}
result = self.module_answer(message)
kwargs = {'root_id': post['parent_id'], 'channel_id': post['channel_id']}
if type(result) is list:
for to_send in result:
self.say(to_send, **kwargs)
elif type(result) is dict:
to_send = result['text'] if 'text' in result else result['xhtml']
if 'monospace' in result and result['monospace']:
to_send = '`%s`' % to_send
self.say(to_send, **kwargs)
else:
self.say(result, **kwargs)

def kill(self):
"""Method used to kill the bot"""

self.say(_("I’ve been asked to leave you"))
self.run = False
self.ws.close()
self.stop_modules()

def say(self, msg, channel_id=None, root_id=""):
"""The method to call to make the bot sending messages"""
# If the bot has not been disabled
if not self.mute:
if channel_id is None:
channel_id = self.default_channel_id
create_at = int(time.time() * 1000)
r = requests.post('https://%s/posts' % self.address, headers=self.headers, json={
'channel_id': channel_id,
'message': msg,
'root_id': root_id
}).json()
if 'status_code' in r:
logger.error(_('error in sent message %s:\nresult is: %s' % (msg, r)))


class DummySender(object):
def __init__(self, sender):
self.resource = sender
18 changes: 14 additions & 4 deletions pipobot/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,13 @@ def _load_modules(conf_modules) :
conf_file)

for conf_room in conf_rooms:
kwargs = {}
for param in ['chan', 'login', 'passwd', 'resource', 'nick']:
protocol = conf_room.get('protocol', 'xmpp')
kwargs = {'protocol': protocol}
required_params = {'xmpp': ['resource', 'nick'],
'mattermost': ['address', 'default_channel', 'default_team'],
'matrix': ['address', 'chan'],
}
for param in ['login', 'passwd'] + required_params[protocol]:
value = conf_room.get(param, "")
if not value or not isinstance(value, str):
if "chan" in kwargs:
Expand Down Expand Up @@ -236,9 +241,11 @@ def _load_modules(conf_modules) :


class Room(object):
__slots__ = ('chan', 'login', 'passwd', 'resource', 'nick', 'modules', 'address', 'port')
__slots__ = ('chan', 'login', 'passwd', 'resource', 'nick', 'modules', 'address', 'port', 'protocol',
'default_team', 'default_channel')

def __init__(self, chan, login, passwd, resource, nick, modules, address=None, port=None):
def __init__(self, login, passwd, modules, resource=None, nick=None, chan=None, address=None, port=None,
default_team='', default_channel='', protocol='xmpp', ):
self.chan = chan
self.login = login
self.passwd = passwd
Expand All @@ -247,6 +254,9 @@ def __init__(self, chan, login, passwd, resource, nick, modules, address=None, p
self.modules = modules
self.address = address
self.port = port
self.protocol = protocol
self.default_team = default_team
self.default_channel = default_channel


def get_configuration():
Expand Down
17 changes: 12 additions & 5 deletions pipobot/lib/modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,23 @@ def __init__(self, bot, desc):

self.prefixs.extend(base_prefixs)

def parse_mess(self, mess):
""" find message body, sender and type of a message """
if 'origin_server_ts' in mess: # Matrix
logger.debug('parsing matrix message')
return mess['content']['body'], mess['sender'], mess['type']
logger.debug('parsing not matrix message')
return mess['body'].lstrip(), mess['from'].resource, mess['type']

def do_answer(self, mess):
""" With an xmpp message `mess`, checking if this module is concerned
by it, and if so get the result of the module and make the bot
say it """

msg_body = mess["body"].lstrip()
sender = mess["from"].resource
msg_body, sender, msg_type = self.parse_mess(mess)

#The bot does not answer to itself (important to avoid some loops !)
if sender == self.bot.name:
if self.bot.name in sender:
return

#Check if the message is related to this module
Expand All @@ -76,15 +83,15 @@ def do_answer(self, mess):
if isinstance(self, SyncModule):
# Separates command/args and get answer from module
command, args = SyncModule.parse(msg_body, self.prefixs)
send = self._answer(sender, args, pm=(mess["type"] == "chat"))
send = self._answer(sender, args, pm=(msg_type == "chat"))
elif isinstance(self, ListenModule):
# In a Listen module the name of the command is not specified
# so nothing to parse
send = self.answer(sender, msg_body)
elif isinstance(self, MultiSyncModule):
# Separates command/args and get answer from module
command, args = SyncModule.parse(msg_body, self.prefixs)
send = self._answer(sender, command, args, pm=(mess["type"] == "chat"))
send = self._answer(sender, command, args, pm=(msg_type == "chat"))
else:
# A not specified module type !
return
Expand Down

0 comments on commit 9eec6ee

Please sign in to comment.