Beispiel #1
0
        add_result = db_helper.check_add_game(white, black, result, sender,
                                              comment)
        if add_result.startswith("fehler"):
            myroom.send_text(add_result)
        else:
            myroom.send_text(msg_result + "\n" + add_result)


myroom.add_listener(on_message)
client.start_listener_thread()
myroom.send_text("Naelob returns!")

try:
    get_input = raw_input
except NameError:
    get_input = input

#====== MAIN LOOP: ==========
while True:
    msg = get_input()
    #TEST
    #time.sleep(0.3)
    if msg == "/quit":
        break

client.stop_listener_thread()
myroom.send_text("Naelob verabschiedet sich und geht offline.")

client.logout()
Beispiel #2
0
class SparseManager(object):
    def __init__(self):
        self.active_room = None
        self.active_listener_id = None
        self.active_start = None

    def login(self, url, username, password):
        self.client = MatrixClient(url)

        # New user
        # token = client.register_with_password(username="******", password="******")

        # Existing user
        create_path(FILENAME)
        token = self.client.login_with_password(username=username,
                                                password=password)
        print("Logged in with token: {token}".format(token=token))

        data = {'token': token, 'user_id': self.client.user_id, 'url': url}

        with open(FILENAME, 'w') as f:
            json.dump(data, f, ensure_ascii=False)

        return data

    def login_with_token(self):
        with open(FILENAME, 'r') as f:
            data = json.load(f)

        self.client = MatrixClient(data["url"],
                                   user_id=data["user_id"],
                                   token=data["token"])
        return data

    def get_rooms(self):
        self.rooms = self.client.get_rooms()
        ids = [{
            "name": x.display_name,
            "topic": x.topic,
            "room_id": x.room_id,
            "has_unread_messages": x.has_unread_messages
        } for x in self.rooms.values()]
        return ids

    def enter_room(self, room_id):
        import threading
        print("Threads running: %s self: %s" %
              (len(threading.enumerate()), id(self)))
        if self.active_room and self.active_room.room_id == room_id:
            return
        if self.active_room:
            self.deactivate_room()

        # self.active_room = self.client.join_room(room_id)

        self.active_room = self.rooms[room_id]
        self.active_listener_id = self.active_room.add_listener(
            self.on_message)
        self.active_start = self.active_room.get_room_messages(limit=20)
        print("Message end: %s" % self.active_start)
        self.client.start_listener_thread()

    def get_next_messages(self):
        # TODO prepend at beginning
        self.active_start = self.active_room.get_room_messages(
            limit=20, start=self.active_start)
        return self.active_start

    def deactivate_room(self):
        self.active_room.remove_listener(self.active_listener_id)
        self.client.stop_listener_thread(blocking=False)
        self.active_room = None
        self.active_listener_id = None
        self.active_start = None

    def on_message(self, room, event):
        if event['type'] in ("m.room.message", "m.room.encrypted"):
            to_send = {}
            if "msgtype" in event["content"] and event["content"][
                    "msgtype"] == "m.image":
                to_send["image_url"] = self.client.api.get_download_url(
                    event["content"]["url"])
                to_send["msgtype"] = "image"
            if event["sender"] == self.client.user_id:
                user_id = self.client.user_id
            else:
                user_id = event["user_id"]
            if "body" in event["content"]:
                to_send["body"] = event["content"]["body"]
            elif "ciphertext" in event["content"]:
                to_send["body"] = "... encrypted ..."
            else:
                to_send["body"] = "... no message ..."
            if "redacted_because" in event:
                to_send["body"] = "... redacted ..."
            user = room._members.get(user_id)
            avatar_url = None
            displayname = None
            if user and user.avatar_url:
                avatar_url = user.avatar_url
            if user and user.displayname:
                displayname = user.displayname
            # XXX to expensive take up to 2 sec member and message
            # elif user and not user.avatar_url:
            #     avatar_url = user.get_avatar_url()
            to_send["origin_server_ts"] = event["origin_server_ts"]
            to_send["time"] = datetime.datetime.fromtimestamp(
                event["origin_server_ts"] / 1000, datetime.timezone.utc)
            to_send["avatar_url"] = avatar_url
            to_send["displayname"] = displayname if displayname else event[
                "sender"]
            pyotherside.send('r.room.message', {"event": to_send})
        else:
            pass
            # print(event["type"])
            # print(event)

    def send_text(self, text):
        self.active_room.send_text(text)
Beispiel #3
0
class MatrixProtocol(Protocol):

  # List of occupants in each room
  # Used to avoid having to re-request the list of members each time
  room_occupants = {}
  # Keep track of when we joined rooms this session
  # Used to filter out historical messages after accepting room invites
  join_timestamps = {}

  # called on bot init; the following are already created by __init__:
  #   self.bot = SibylBot instance
  #   self.log = the logger you should use
  def setup(self):
    self.rooms = {}
    self.bot.add_var("credentials",persist=True)

    # Incoming message queue - messageHandler puts messages in here and
    # process() looks here periodically to send them to sibyl
    self.msg_queue = Queue()

    # Create a client in setup() because we might use self.client before
    # connect() is called
    homeserver = self.opt('matrix.server')
    self.client = MatrixClient(homeserver)

  # @raise (ConnectFailure) if can't connect to server
  # @raise (AuthFailure) if failed to authenticate to server
  def connect(self):
    homeserver = self.opt('matrix.server')
    user = self.opt('matrix.username')
    pw = self.opt('matrix.password')

    self.log.debug("Connecting to %s" % homeserver)

    try:
      self.log.debug("Logging in as %s" % user)

      # Log in with the existing access token if we already have a token
      if(self.bot.credentials and self.bot.credentials[0] == user):
        self.client = MatrixClient(homeserver, user_id=user, token=self.bot.credentials[1])
      # Otherwise, log in with the configured username and password
      else:
        token = self.client.login_with_password(user,pw)
        self.bot.credentials = (user, token)

      self.rooms = self.client.get_rooms()
      self.log.debug("Already in rooms: %s" % self.rooms)

      # Connect to Sibyl's message callback
      self.client.add_listener(self.messageHandler)
      self.client.add_invite_listener(self.inviteHandler)

      self.log.debug("Starting Matrix listener thread")
      self.client.start_listener_thread(exception_handler=self._matrix_exception_handler)

    except MatrixRequestError as e:
      if(e.code in [401, 403]):
        self.log.debug("Credentials incorrect! Maybe your access token is outdated?")
        raise self.AuthFailure
      else:
        if(self.opt('matrix.debug')):
          tb = traceback.format_exc()
          self.log.debug(tb)
        self.log.debug("Failed to connect to homeserver!")
        raise self.ConnectFailure
    except MatrixHttpLibError as e:
      self.log.error("Failed to connect to homeserver!")
      self.log.debug("Received error:" + str(e))
      raise self.ConnectFailure

  def _matrix_exception_handler(self, e):
    self.msg_queue.put(e)

  # receive/process messages and call bot._cb_message()
  # must ignore msgs from myself and from users not in any of our rooms
  # @call bot._cb_message(Message) upon receiving a valid status or message
  # @raise (PingTimeout) if implemented
  # @raise (ConnectFailure) if disconnected
  # @raise (ServerShutdown) if server shutdown
  def process(self):
    while(not self.msg_queue.empty()):
      next = self.msg_queue.get()
      if(isinstance(next, Message)):
        self.log.debug("Placing message into queue: " + next.get_text())
        self.bot._cb_message(next)
      elif(isinstance(next, MatrixHttpLibError)):
        self.log.debug("Received error from Matrix SDK, stopping listener thread: " + str(next))
        self.client.stop_listener_thread()
        raise self.ConnectFailure("Connection error returned by requests library: " + str(next))


  def messageHandler(self, msg):
    if(self.opt('matrix.debug')):
      self.log.debug(str(msg))

    try:
      # Create a new Message to send to Sibyl
      u = self.new_user(msg['sender'], Message.GROUP)
      r = self.new_room(msg['room_id'])

      if(r in self.join_timestamps
         and datetime.datetime.fromtimestamp(msg['origin_server_ts']/1000, pytz.utc) < self.join_timestamps[r]):
        self.log.info('Message received in {} from before room join, ignoring'.format(msg['room_id']))
        return None

      if('msgtype' in msg['content']):
        msgtype = msg['content']['msgtype']

        if(msgtype == 'm.text'):
          m = Message(u, msg['content']['body'], room=r, typ=Message.GROUP)
          self.log.debug('Handling m.text: ' + msg['content']['body'])
          self.msg_queue.put(m)

        elif(msgtype == 'm.emote'):
          m = Message(u, msg['content']['body'], room=r, typ=Message.GROUP,
          emote=True)
          self.log.debug('Handling m.emote: ' + msg['content']['body'])
          self.msg_queue.put(m)

        elif(msgtype == 'm.image' or msgtype == 'm.audio' or msgtype == 'm.file' or msgtype == 'm.video'):
          media_url = urlparse(msg['content']['url'])
          http_url = self.client.api.base_url + "/_matrix/media/r0/download/{0}{1}".format(media_url.netloc, media_url.path)
          if(msgtype == 'm.image'):
            body = "{0} uploaded {1}: {2}".format(msg['sender'], msg['content'].get('body', 'an image'), http_url)
          elif(msgtype == 'm.audio'):
            body = "{0} uploaded {1}: {2}".format(msg['sender'], msg['content'].get('body', 'an audio file'), http_url)
          elif(msgtype == 'm.video'):
            body = "{0} uploaded {1}: {2}".format(msg['sender'], msg['content'].get('body', 'a video file'), http_url)
          elif(msgtype == 'm.file'):
            body = "{0} uploaded {1}: {2}".format(msg['sender'], msg['content'].get('body', 'a file'), http_url)
          m = Message(u, body, room=r, typ=Message.GROUP)
          self.log.debug("Handling " + msgtype + ": " + body)
          self.msg_queue.put(m)

        elif(msgtype == 'm.location'):
          body = "{0} sent a location: {1}".format(msg['sender'], msg['content']['geo_uri'])
          m = Message(u, body, room=r, typ=Message.GROUP)
          self.log.debug('Handling m.location: ' + body)
          self.msg_queue.put(m)


        else:
          self.log.debug('Not handling message, unknown msgtype')

      elif('membership' in msg):
        if(msg['membership'] == 'join'):
          self.room_occupants[r].add(self.new_user(msg['state_key'], Message.GROUP))
        elif(msg['membership'] == 'leave'):
          self.room_occupants[r].remove(self.new_user(msg['state_key'], Message.GROUP))

    except KeyError as e:
      self.log.debug("Incoming message did not have all required fields: " + e.message)


  def inviteHandler(self, room_id, state):
    join_on_invite = self.opt('matrix.join_on_invite')

    invite_events = [x for x in state['events'] if x['type'] == 'm.room.member'
                     and x['state_key'] == str(self.get_user())
                     and x['content']['membership'] == 'invite']
    if(len(invite_events) != 1):
      raise KeyError("Something's up, found more than one invite state event for " + room_id)

    inviter = invite_events[0]['sender']
    inviter_domain = inviter.split(':')[1]
    my_domain = str(self.get_user()).split(':')[1]

    if(join_on_invite == 'accept' or (join_on_invite == 'domain' and inviter_domain == my_domain)):
      self.log.debug('Joining {} on invite from {}'.format(room_id, inviter))
      self.join_room(MatrixRoom(self, room_id))

    elif(join_on_invite == 'domain' and inviter_domain != my_domain):
      self.log.debug("Received invite for {} but inviter {} is on a different homeserver").format(room_id, inviter)

    else:
      self.log.debug("Received invite for {} from {} but join_on_invite is disabled".format(room_id, inviter))


  # called when the bot is exiting for whatever reason
  # NOTE: sibylbot will already call part_room() on every room in get_rooms()
  def shutdown(self):
    pass

  # send a message to a user
  # @param mess (Message) message to be sent
  # @raise (ConnectFailure) if failed to send message
  # Check: get_emote()
  def send(self,mess):
    (text,to) = (mess.get_text(),mess.get_to())
    try:
      if(mess.get_emote()):
        to.room.send_emote(text)
      else:
        to.room.send_text(text)
    except MatrixError as e:
      raise self.ConnectFailure

  # send a message with text to every user in a room
  # optionally note that the broadcast was requested by a specific User
  # @param mess (Message) the message to broadcast
  # @return (str,unicode) the text that was actually sent
  # Check: get_user(), get_users()
  def broadcast(self,mess):
    """send a message to every user in a room"""

    (text,room,frm) = (mess.get_text(),mess.get_to(),mess.get_user())
    users = self.get_occupants(room)+(mess.get_users() or [])

    # Matrix has no built-in broadcast, so we'll just highlight everyone
    s = 'all: %s --- ' % text
    if frm:
      self.log.debug('Broadcast message from: ' + str(frm))
      s += frm.get_name()+' --- '

    me = self.get_user()
    names = [u.get_name() for u in users if (u!=me and (not frm or u!=frm))]
    s += ', '.join(set(names))

    self.send(Message(self.get_user(),s,to=room))
    return s

  # join the specified room using the specified nick and password
  # @param room (Room) the room to join
  # @call bot._cb_join_room_success(room) on successful join
  # @call bot._cb_join_room_failure(room,error) on failed join
  def join_room(self,room):
    try:
      res = self.client.join_room(room.room.room_id)
      self.bot._cb_join_room_success(room)
      self.join_timestamps[room] = datetime.datetime.now(pytz.utc)
    except MatrixError as e:
      self.bot._cb_join_room_failure(room, e.message)

  # part the specified room
  # @param room (Room) the room to leave
  def part_room(self,room):
    raise NotImplementedError

  # helper function for get_rooms() for protocol-specific flags
  # only needs to handle: FLAG_PARTED, FLAG_PENDING, FLAG_IN, FLAG_ALL
  # @param flag (int) one of Room.FLAG_* enums
  # @return (list of Room) rooms matching the flag
  def _get_rooms(self,flag):
    mxrooms = self.client.get_rooms()
    return [self.new_room(mxroom) for mxroom in mxrooms]


  # @param room (Room) the room to query
  # @return (list of User) the Users in the specified room
  def get_occupants(self,room):
    if(room in self.room_occupants):
      return list(self.room_occupants[room])
    else:
      try:
        memberdict = room.room.get_joined_members()
        users = [ self.new_user(x) for x in memberdict ]
        self.room_occupants[room] = set(users)
        return users
      except MatrixError as e:
        raise self.ConnectFailure

  # @param room (Room) the room to query
  # @return (str) the nick name we are using in the specified room
  def get_nick(self,room):
    return self.get_user().get_name() # TODO: per-room nicknames

  # @param room (Room) the room to query
  # @param nick (str) the nick to examine
  # @return (User) the "real" User behind the specified nick/room
  def get_real(self,room,nick):
    raise NotImplementedError

  # @return (User) our username
  def get_user(self):
    return MatrixUser(self,self.opt('matrix.username'),Message.GROUP)

  # @param user (str) a user id to parse
  # @param typ (int) either Message.GROUP or Message.PRIVATE
  # @param real (User) [self] the "real" user behind this user
  # @return (User) a new instance of this protocol's User subclass
  def new_user(self,user,typ=None,real=None):
    return MatrixUser(self,user,typ,real)

  # @param name (object) the identifier for this Room
  # @param nick (str) [None] the nick name to use in this Room
  # @param pword (str) [None] the password for joining this Room
  # @return (Room) a new instance of this protocol's Room subclass
  def new_room(self,room_id_or_alias,nick=None,pword=None):
    return MatrixRoom(self,room_id_or_alias,nick,pword)
Beispiel #4
0
class MatrixBot:
    def __init__(self, username, password, server, allowed_rooms=None):
        self.username = username
        self._username_re = re.compile('@{}'.format(re.escape(username)), re.I)
        self.server = server
        self.handlers = []
        self.rooms = {}
        self.client = MatrixClient(server)
        try:
            self.client.login_with_password(username, password)
        except MatrixRequestError as error:
            if error.code == 403:
                logger.error('Username and/or password mismatch')
            raise
        self._join_rooms(allowed_rooms)
        if not self.rooms:
            logger.info('No rooms given, listening for invitations')
            self.client.add_invite_listener(self.handle_invite)
            for room_id, room in self.client.rooms.items():
                room.add_listener(self.handle_message)
                self._add_room(room_id, room)

    def _add_room(self, room_id, room):
        logger.debug('Adding room (ID: %s): %s', room_id, room)
        self.rooms[room_id] = room

    def _join_rooms(self, room_ids):
        for room_id in room_ids:
            room = self.client.join_room(room_id)
            room.add_listener(self.handle_message)
            self._add_room(room_id, room)

    def send(self, msg, room_ids=None):
        rooms = room_ids or self.rooms.keys()
        for room_id in rooms:
            room = self.rooms.get(room_id)
            if room:
                room.send_text(msg)

    def handle_message(self, room, event):
        if self._username_re.match(event['sender']):
            # Don't handle own messages
            return
        for handler in self.handlers:
            assert isinstance(handler, BaseHandler)
            if handler.should_run(room, event):
                try:
                    handler(room, event)
                except Exception:
                    logger.exception('Error while calling handler function')

    def handle_invite(self, room_id, state):
        logger.info('Invitation received to: %s', room_id)
        logger.info('Trying to join now...')
        room = self.client.join_room(room_id)
        room.add_listener(self.handle_message)
        self.rooms[room_id] = room

    def start(self):
        """Starts listening for messages in a new thread. If the calling
        script has nothing more to do, it can simply do:
        >>> bot = MatrixBot(username, password, server, allowed_rooms)
        >>> bot_thread = bot.start()
        >>> bot_thread.join()
        This will run forever.
        """
        self.client.start_listener_thread()
        return self.client.sync_thread

    def stop(self):
        self.client.stop_listener_thread()
Beispiel #5
0
class MatrixProtocol(Protocol):

  # List of occupants in each room
  # Used to avoid having to re-request the list of members each time
  room_occupants = {}
  # Keep track of when we joined rooms this session
  # Used to filter out historical messages after accepting room invites
  join_timestamps = {}

  # called on bot init; the following are already created by __init__:
  #   self.bot = SibylBot instance
  #   self.log = the logger you should use
  def setup(self):
    self.rooms = {}
    self.bot.add_var("credentials",persist=True)

    # Incoming message queue - messageHandler puts messages in here and
    # process() looks here periodically to send them to sibyl
    self.msg_queue = Queue()

    # Create a client in setup() because we might use self.client before
    # connect() is called
    homeserver = self.opt('matrix.server')
    self.client = MatrixClient(homeserver)

  # @raise (ConnectFailure) if can't connect to server
  # @raise (AuthFailure) if failed to authenticate to server
  def connect(self):
    homeserver = self.opt('matrix.server')
    user = self.opt('matrix.username')
    pw = self.opt('matrix.password')

    self.log.debug("Connecting to %s" % homeserver)

    try:
      self.log.debug("Logging in as %s" % user)

      # Log in with the existing access token if we already have a token
      if(self.bot.credentials and self.bot.credentials[0] == user):
        self.client = MatrixClient(homeserver, user_id=user, token=self.bot.credentials[1])
      # Otherwise, log in with the configured username and password
      else:
        token = self.client.login_with_password(user,pw)
        self.bot.credentials = (user, token)

      self.rooms = self.client.get_rooms()
      self.log.debug("Already in rooms: %s" % self.rooms)

      # Connect to Sibyl's message callback
      self.client.add_listener(self.messageHandler)
      self.client.add_invite_listener(self.inviteHandler)

      self.log.debug("Starting Matrix listener thread")
      self.client.start_listener_thread(exception_handler=self._matrix_exception_handler)

    except MatrixRequestError as e:
      if(e.code in [401, 403]):
        self.log.debug("Credentials incorrect! Maybe your access token is outdated?")
        raise self.AuthFailure
      else:
        if(self.opt('matrix.debug')):
          tb = traceback.format_exc()
          self.log.debug(tb)
        self.log.debug("Failed to connect to homeserver!")
        raise self.ConnectFailure
    except MatrixHttpLibError as e:
      self.log.error("Failed to connect to homeserver!")
      self.log.debug("Received error:" + str(e))
      raise self.ConnectFailure

  def _matrix_exception_handler(self, e):
    self.msg_queue.put(e)

  # receive/process messages and call bot._cb_message()
  # must ignore msgs from myself and from users not in any of our rooms
  # @call bot._cb_message(Message) upon receiving a valid status or message
  # @raise (PingTimeout) if implemented
  # @raise (ConnectFailure) if disconnected
  # @raise (ServerShutdown) if server shutdown
  def process(self):
    while(not self.msg_queue.empty()):
      next = self.msg_queue.get()
      if(isinstance(next, Message)):
        self.log.debug("Placing message into queue: " + next.get_text())
        self.bot._cb_message(next)
      elif(isinstance(next, MatrixHttpLibError)):
        self.log.debug("Received error from Matrix SDK, stopping listener thread: " + str(next))
        self.client.stop_listener_thread()
        raise self.ConnectFailure("Connection error returned by requests library: " + str(next))


  def messageHandler(self, msg):
    if(self.opt('matrix.debug')):
      self.log.debug(str(msg))

    try:
      # Create a new Message to send to Sibyl
      u = self.new_user(msg['sender'], Message.GROUP)
      r = self.new_room(msg['room_id'])

      if(r in self.join_timestamps
         and datetime.datetime.fromtimestamp(msg['origin_server_ts']/1000, pytz.utc) < self.join_timestamps[r]):
        self.log.info('Message received in {} from before room join, ignoring'.format(msg['room_id']))
        return None

      if('msgtype' in msg['content']):
        msgtype = msg['content']['msgtype']

        if(msgtype == 'm.text'):
          m = Message(u, msg['content']['body'], room=r, typ=Message.GROUP)
          self.log.debug('Handling m.text: ' + msg['content']['body'])
          self.msg_queue.put(m)

        elif(msgtype == 'm.emote'):
          m = Message(u, msg['content']['body'], room=r, typ=Message.GROUP,
          emote=True)
          self.log.debug('Handling m.emote: ' + msg['content']['body'])
          self.msg_queue.put(m)

        elif(msgtype == 'm.image' or msgtype == 'm.audio' or msgtype == 'm.file' or msgtype == 'm.video'):
          media_url = urlparse(msg['content']['url'])
          http_url = self.client.api.base_url + "/_matrix/media/r0/download/{0}{1}".format(media_url.netloc, media_url.path)
          if(msgtype == 'm.image'):
            body = "{0} uploaded {1}: {2}".format(msg['sender'], msg['content'].get('body', 'an image'), http_url)
          elif(msgtype == 'm.audio'):
            body = "{0} uploaded {1}: {2}".format(msg['sender'], msg['content'].get('body', 'an audio file'), http_url)
          elif(msgtype == 'm.video'):
            body = "{0} uploaded {1}: {2}".format(msg['sender'], msg['content'].get('body', 'a video file'), http_url)
          elif(msgtype == 'm.file'):
            body = "{0} uploaded {1}: {2}".format(msg['sender'], msg['content'].get('body', 'a file'), http_url)
          m = Message(u, body, room=r, typ=Message.GROUP)
          self.log.debug("Handling " + msgtype + ": " + body)
          self.msg_queue.put(m)

        elif(msgtype == 'm.location'):
          body = "{0} sent a location: {1}".format(msg['sender'], msg['content']['geo_uri'])
          m = Message(u, body, room=r, typ=Message.GROUP)
          self.log.debug('Handling m.location: ' + body)
          self.msg_queue.put(m)


        else:
          self.log.debug('Not handling message, unknown msgtype')

      elif('membership' in msg):
        if(msg['membership'] == 'join'):
          self.room_occupants[r].add(self.new_user(msg['state_key'], Message.GROUP))
        elif(msg['membership'] == 'leave'):
          self.room_occupants[r].remove(self.new_user(msg['state_key'], Message.GROUP))

    except KeyError as e:
      self.log.debug("Incoming message did not have all required fields: " + e.message)


  def inviteHandler(self, room_id, state):
    join_on_invite = self.opt('matrix.join_on_invite')

    invite_events = [x for x in state['events'] if x['type'] == 'm.room.member'
                     and x['state_key'] == str(self.get_user())
                     and x['content']['membership'] == 'invite']
    if(len(invite_events) != 1):
      raise KeyError("Something's up, found more than one invite state event for " + room_id)

    inviter = invite_events[0]['sender']
    inviter_domain = inviter.split(':')[1]
    my_domain = str(self.get_user()).split(':')[1]

    if(join_on_invite == 'accept' or (join_on_invite == 'domain' and inviter_domain == my_domain)):
      self.log.debug('Joining {} on invite from {}'.format(room_id, inviter))
      self.join_room(MatrixRoom(self, room_id))

    elif(join_on_invite == 'domain' and inviter_domain != my_domain):
      self.log.debug("Received invite for {} but inviter {} is on a different homeserver").format(room_id, inviter)

    else:
      self.log.debug("Received invite for {} from {} but join_on_invite is disabled".format(room_id, inviter))


  # called when the bot is exiting for whatever reason
  # NOTE: sibylbot will already call part_room() on every room in get_rooms()
  def shutdown(self):
    pass

  # send a message to a user
  # @param mess (Message) message to be sent
  # @raise (ConnectFailure) if failed to send message
  # Check: get_emote()
  def send(self,mess):
    (text,to) = (mess.get_text(),mess.get_to())
    if(mess.get_emote()):
      to.room.send_emote(text)
    else:
      to.room.send_text(text)

  # send a message with text to every user in a room
  # optionally note that the broadcast was requested by a specific User
  # @param mess (Message) the message to broadcast
  # @return (str,unicode) the text that was actually sent
  # Check: get_user(), get_users()
  def broadcast(self,mess):
    """send a message to every user in a room"""

    (text,room,frm) = (mess.get_text(),mess.get_to(),mess.get_user())
    users = self.get_occupants(room)+(mess.get_users() or [])

    # Matrix has no built-in broadcast, so we'll just highlight everyone
    s = 'all: %s --- ' % text
    if frm:
      self.log.debug('Broadcast message from: ' + str(frm))
      s += frm.get_name()+' --- '

    me = self.get_user()
    names = [u.get_name() for u in users if (u!=me and (not frm or u!=frm))]
    s += ', '.join(set(names))

    self.send(Message(self.get_user(),s,to=room))
    return s

  # join the specified room using the specified nick and password
  # @param room (Room) the room to join
  # @call bot._cb_join_room_success(room) on successful join
  # @call bot._cb_join_room_failure(room,error) on failed join
  def join_room(self,room):
    try:
      res = self.client.join_room(room.room.room_id)
      self.bot._cb_join_room_success(room)
      self.join_timestamps[room] = datetime.datetime.now(pytz.utc)
    except MatrixRequestError as e:
      self.bot._cb_join_room_failure(room, e.message)

  # part the specified room
  # @param room (Room) the room to leave
  def part_room(self,room):
    raise NotImplementedError

  # helper function for get_rooms() for protocol-specific flags
  # only needs to handle: FLAG_PARTED, FLAG_PENDING, FLAG_IN, FLAG_ALL
  # @param flag (int) one of Room.FLAG_* enums
  # @return (list of Room) rooms matching the flag
  def _get_rooms(self,flag):
    mxrooms = self.client.get_rooms()
    return [self.new_room(mxroom) for mxroom in mxrooms]


  # @param room (Room) the room to query
  # @return (list of User) the Users in the specified room
  def get_occupants(self,room):
    if(room in self.room_occupants):
      return list(self.room_occupants[room])
    else:
      memberdict = room.room.get_joined_members()
      users = [ self.new_user(x) for x in memberdict ]
      self.room_occupants[room] = set(users)
      return users

  # @param room (Room) the room to query
  # @return (str) the nick name we are using in the specified room
  def get_nick(self,room):
    return self.get_user().get_name() # TODO: per-room nicknames

  # @param room (Room) the room to query
  # @param nick (str) the nick to examine
  # @return (User) the "real" User behind the specified nick/room
  def get_real(self,room,nick):
    raise NotImplementedError

  # @return (User) our username
  def get_user(self):
    return MatrixUser(self,self.opt('matrix.username'),Message.GROUP)

  # @param user (str) a user id to parse
  # @param typ (int) either Message.GROUP or Message.PRIVATE
  # @param real (User) [self] the "real" user behind this user
  # @return (User) a new instance of this protocol's User subclass
  def new_user(self,user,typ=None,real=None):
    return MatrixUser(self,user,typ,real)

  # @param name (object) the identifier for this Room
  # @param nick (str) [None] the nick name to use in this Room
  # @param pword (str) [None] the password for joining this Room
  # @return (Room) a new instance of this protocol's Room subclass
  def new_room(self,room_id_or_alias,nick=None,pword=None):
    return MatrixRoom(self,room_id_or_alias,nick,pword)
Beispiel #6
0
class BridgeBot:
    xmpp = None                # type: ClientXMPP
    matrix = None              # type: MatrixClient
    topic_room_id_map = None   # type: Dict[str, str]
    special_rooms = None       # type: Dict[str, MatrixRoom]
    special_room_names = None  # type: Dict[str, str]
    groupchat_flag = None      # type: str
    groupchat_jids = None      # type: List[str]

    users_to_invite = None      # type: List[str]
    matrix_room_topics = None   # type: Dict[str, str]
    matrix_server = None        # type: Dict[str, str]
    matrix_login = None         # type: Dict[str, str]
    xmpp_server = None          # type: Tuple[str, int]
    xmpp_login = None           # type: Dict[str, str]
    xmpp_roster_options = None  # type: Dict[str, bool]
    xmpp_groupchat_nick = None  # type: str

    default_actions = None          # type: Dict[str, bool]
    jid_actions = None              # type: Dict[str, Dict[str, bool]]
    groupchat_mute_own_nick = True                  # type: bool
    groupchat_send_messages_to_all_chat = True      # type: bool

    inbound_xmpp = None         # type: Queue

    exception = None            # type: Exception or None


    @property
    def bot_id(self) -> str:
        return self.matrix_login['username']

    def __init__(self, config_file: str=CONFIG_FILE):
        self.groupchat_jids = []
        self.topic_room_id_map = {}
        self.special_rooms = {
                'control': None,
                'all_chat': None,
                }
        self.special_room_names = {
                'control': 'XMPP Control Room',
                'all_chat': 'XMPP All Chat',
                }
        self.xmpp_roster_options = {}
        self.inbound_xmpp = Queue()

        self.load_config(config_file)

        self.matrix = MatrixClient(**self.matrix_server)
        self.xmpp = ClientXMPP(self.inbound_xmpp,
                               **self.xmpp_login,
                               **self.xmpp_roster_options)

        self.matrix.login_with_password(**self.matrix_login)

        # Recover existing matrix rooms
        for room in list(self.matrix.get_rooms().values()):
            room.update_room_topic()
            topic = room.topic

            if topic in self.special_rooms.keys():
                logger.debug('Recovering special room: ' + topic)
                self.special_rooms[topic] = room

            elif topic is None:
                room.leave()

            elif topic.startswith(self.groupchat_flag):
                room_jid = topic[len(self.groupchat_flag):]
                self.groupchat_jids.append(room_jid)

            elif not self.jid_actions.get(topic, self.default_actions)['send_messages_to_jid_rooms']:
                logger.info('Room ' + topic + ' is not needed due to send_messages_to_jid_rooms setting, leaving!')
                room.leave()

        # Prepare matrix special rooms and their listeners
        for topic, room in self.special_rooms.items():
            if room is None:
                room = self.matrix.create_room()
            self.setup_special_room(room, topic)

        self.special_rooms['control'].add_listener(self.matrix_control_message, 'm.room.message')
        self.special_rooms['all_chat'].add_listener(self.matrix_all_chat_message, 'm.room.message')

        # Invite users to special rooms
        for room in self.special_rooms.values():
            for user_id in self.users_to_invite:
                room.invite_user(user_id)

        # Connect to XMPP and start processing XMPP events
        self.xmpp.connect(self.xmpp_server)
        self.xmpp.process(block=False)

        # Rejoin group chats
        logger.debug('Rejoining group chats')
        for room_jid in self.groupchat_jids:
            self.xmpp.plugin['xep_0045'].joinMUC(room_jid, self.xmpp_groupchat_nick)

        # Listen for Matrix events
        def exception_handler(e: Exception):
            self.exception = e

        self.matrix.start_listener_thread(exception_handler=exception_handler)

        logger.debug('Done with bot init')

    def shutdown(self):
        self.matrix.stop_listener_thread()
        self.xmpp.disconnect()

    def handle_inbound_xmpp(self):
        while self.exception is None:
            event = self.inbound_xmpp.get()

            if isinstance(event, sleekxmpp.Presence):
                handler = {
                        'available': self.xmpp_presence_available,
                        'unavailable': self.xmpp_presence_unavailable,
                }.get(event.get_type(), self.xmpp_unrecognized_event)

            elif isinstance(event, sleekxmpp.Message):
                handler = {
                        'normal': self.xmpp_message,
                        'chat': self.xmpp_message,
                        'groupchat': self.xmpp_groupchat_message,
                }.get(event.get_type(), self.xmpp_unrecognized_event)

            elif isinstance(event, sleekxmpp.Iq) and event.get_query() == 'jabber:iq:roster':
                handler = self.xmpp_roster_update

            else:
                handler = self.xmpp_unrecognized_event

            handler(event)
        raise self.exception

    def load_config(self, path: str):
        with open(path, 'r') as conf_file:
            config = yaml.safe_load(conf_file)

        self.users_to_invite = config['matrix']['users_to_invite']
        self.matrix_room_topics = config['matrix']['room_topics']
        self.groupchat_flag = config['matrix']['groupchat_flag']

        self.matrix_server = config['matrix']['server']
        self.matrix_login = config['matrix']['login']
        self.xmpp_server = (config['xmpp']['server']['host'],
                            config['xmpp']['server']['port'])
        self.xmpp_login = config['xmpp']['login']
        self.xmpp_groupchat_nick = config['xmpp']['groupchat_nick']

        self.default_actions = {k: config[k] for k in ('send_messages_to_all_chat',
                                                       'send_messages_to_jid_rooms',
                                                       'send_presences_to_control')}
        self.jid_actions = {}
        for group in config['jid_groups']:
            group_data = group.copy()
            group_jids = group_data.pop('jids')
            for jid in group['jids']:
                self.jid_actions.setdefault(jid, self.default_actions.copy())
                self.jid_actions[jid].update(group_data)

        self.groupchat_mute_own_nick = config['groupchat_mute_own_nick']
        self.groupchat_send_messages_to_all_chat = config['groupchat_send_messages_to_all_chat']

        self.xmpp_roster_options = config['xmpp']['roster_options']

    def get_room_for_topic(self, jid: str) -> MatrixRoom:
        """
        Return the room corresponding to the given XMPP JID
        :param jid: bare XMPP JID, should not include the resource
        :return: Matrix room object for chatting with that JID
        """
        room_id = self.topic_room_id_map[jid]
        return self.matrix.get_rooms()[room_id]

    def get_unmapped_rooms(self) -> List[MatrixRoom]:
        """
        Returns a list of all Matrix rooms which are not a special room (e.g., the control room) and
        do not have a corresponding entry in the topic -> room map.
        :return: List of unmapped, non-special Matrix room objects.
        """
        special_room_ids = [r.room_id for r in self.special_rooms.values()]
        valid_room_ids = [v for v in self.topic_room_id_map.values()] + special_room_ids
        unmapped_rooms = [room for room_id, room in self.matrix.get_rooms().items()
                          if room_id not in valid_room_ids]
        return unmapped_rooms

    def get_empty_rooms(self) -> List[MatrixRoom]:
        """
        Returns a list of all Matrix rooms which are occupied by only one user
        (the bot itself).
        :return: List of Matrix rooms occupied by only the bot.
        """
        empty_rooms = [room for room in self.matrix.get_rooms().values()
                       if len(room.get_joined_members()) < 2]
        return empty_rooms

    def setup_special_room(self, room, topic: str):
        """
        Sets up a Matrix room with the requested topic and adds it to the self.special_rooms map.

        If a special room with that topic already exists, it is replaced in the special_rooms
         map by the new room.
        :param room: Room to set up
        :param topic: Topic for the room
        """
        room.set_room_topic(topic)
        room.set_room_name(self.special_room_names[topic])
        self.special_rooms[topic] = room

        logger.debug('Set up special room with topic {} and id'.format(
            str(room.topic), room.room_id))

    def create_mapped_room(self, topic: str, name: str=None) -> MatrixRoom or None:
        """
        Create a new room and add it to self.topic_room_id_map.

        :param topic: Topic for the new room
        :param name: (Optional) Name for the new room
        :return: Room which was created
        """
        if topic in self.groupchat_jids:
            logger.debug('Topic {} is a groupchat without its flag, ignoring'.format(topic))
            return None
        elif topic in self.topic_room_id_map.keys():
            room_id = self.topic_room_id_map[topic]
            room = self.matrix.get_rooms()[room_id]
            logger.debug('Room with topic {} already exists!'.format(topic))
        else:
            room = self.matrix.create_room()
            room.set_room_topic(topic)
            self.topic_room_id_map[topic] = room.room_id
            logger.info('Created mapped room with topic {} and id {}'.format(topic, str(room.room_id)))
            room.add_listener(self.matrix_message, 'm.room.message')

        if room.name != name:
            if name != "":
                room.set_room_name(name)
                room.set_user_profile(displayname=name)
            else:
                room.set_room_name(topic.split('@')[0])

        return room

    def leave_mapped_room(self, topic: str) -> bool:
        """
        Leave an existing, mapped room and remove it from self.topic_room_id_map.

        :param topic: Topic for room to leave
        :retrun: True if the room was left, False if the room was not found.
        """
        if topic in self.groupchat_jids:
            logger.debug('Topic {} is a groupchat without its flag, ignoring'.format(topic))
            return False

        if topic not in self.topic_room_id_map.keys():
            err_msg = 'Room with topic {} isn\'t mapped or doesn\'t exist'.format(topic)
            logger.warning(err_msg)
            return False

        if topic.startswith(self.groupchat_flag):
            # Leave the groupchat
            room_jid = topic[len(self.groupchat_flag):]
            if room_jid in self.groupchat_jids:
                self.groupchat_jids.remove(room_jid)
            logger.info('XMPP MUC leave: {}'.format(room_jid))
            self.xmpp.plugin['xep_0045'].leaveMUC(room_jid, self.xmpp_groupchat_nick)

        room = self.get_room_for_topic(topic)
        del self.topic_room_id_map[topic]
        room.leave()
        logger.info('Left mapped room with topic {}'.format(topic))
        return True

    def map_rooms_by_topic(self):
        """
        Add unmapped rooms to self.topic_room_id_map, and listen to messages from those rooms.

        Rooms whose topics are empty or do not contain an '@' symbol are assumed to be special
         rooms, and will not be mapped.
        """
        unmapped_rooms = self.get_unmapped_rooms()

        for room in unmapped_rooms:
            room.update_room_topic()

            logger.debug('Unmapped room {} ({}) [{}]'.format(room.room_id, room.name, room.topic))

            if room.topic is None or '@' not in room.topic:
                logger.debug('Leaving it as-is (special room, topic does not contain @)')
            else:
                self.topic_room_id_map[room.topic] = room.room_id
                room.add_listener(self.matrix_message, 'm.room.message')

    def matrix_control_message(self, room: MatrixRoom, event: Dict):
        """
        Handle a message sent to the control room.

        Does nothing unless a valid command is received:
          refresh  Probes the presence of all XMPP contacts, and updates the roster.
          purge    Leaves any ((un-mapped and non-special) or empty) Matrix rooms.
          joinmuc [email protected]   Joins a muc
          leavemuc [email protected]  Leaves a muc

        :param room: Matrix room object representing the control room
        :param event: The Matrix event that was received. Assumed to be an m.room.message .
        """
        # Always ignore our own messages
        if event['sender'] == self.bot_id:
            return

        logger.debug('matrix_control_message: {}  {}'.format(room.room_id, str(event)))

        if event['content']['msgtype'] == 'm.text':
            message_body = event['content']['body']
            logger.info('Matrix received control message: ' + message_body)

            message_parts = message_body.split()
            if len(message_parts) < 1:
                logger.warning('Received empty control message, ignoring')
                return

            if message_parts[0] == 'refresh':
                for jid in self.topic_room_id_map.keys():
                    self.xmpp.send_presence(pto=jid, ptype='probe')
                self.xmpp.send_presence()
                self.xmpp.get_roster()

            elif message_parts[0] == 'purge':
                self.special_rooms['control'].send_text('Purging unused rooms')

                # Leave from unwanted rooms
                for room in self.get_unmapped_rooms() + self.get_empty_rooms():
                    logger.info('Leaving room {r.room_id} ({r.name}) [{r.topic}]'.format(r=room))

                    if room.topic in self.topic_room_id_map.keys():
                        self.leave_mapped_room(room.topic)
                    else:
                        room.leave()

            elif message_parts[0] == 'joinmuc':
                if len(message_parts) < 2:
                    logger.warning('joinmuc command didn\'t specify a room, ignoring')
                    return

                room_jid = message_parts[1]
                logger.info('XMPP MUC join: {}'.format(room_jid))
                self.create_groupchat_room(room_jid)
                self.xmpp.plugin['xep_0045'].joinMUC(room_jid, self.xmpp_groupchat_nick)

            elif message_parts[0] == 'leavemuc':
                if len(message_parts) < 2:
                    logger.warning('leavemuc command didn\'t specify a room, ignoring')
                    return

                room_jid = message_parts[1]
                room_topic = self.groupchat_flag + room_jid

                success = self.leave_mapped_room(room_topic)
                if not success:
                    msg = 'Groupchat {} isn\'t mapped or doesn\'t exist'.format(room_jid)
                else:
                    msg = 'Left groupchat {}'.format(room_jid)
                self.special_rooms['control'].send_notice(msg)

    def matrix_all_chat_message(self, room: MatrixRoom, event: Dict):
        """
        Handle a message sent to Matrix all-chat room.

        Allows manual sending of xmpp messages: "/m target_jid your message here".
        Sends a notice with the expected format if it isn't there by default.

        :param room: Matrix room object representing the all-chat room
        :param event: The Matrix event that was received. Assumed to be an m.room.message .
        """
        # Always ignore our own messages
        if event['sender'] == self.bot_id:
            return

        logger.debug('matrix_all_chat_message: {}  {}'.format(room.room_id, str(event)))

        if event['content']['msgtype'] == 'm.text':
            message_body = event['content']['body']
            message_parts = message_body.split()
            if message_parts[0] == '/m':
                jid = message_parts[1]
                payload = message_body[message_body.find(jid) + len(jid) + 1:]
                logger.info('sending manual message to '+ jid + ' : ' + payload)
                self.xmpp.send_message(mto=jid, mbody=payload, mtype='chat')
            else:
                room.send_notice('Expected message format: "/m DEST_JID your message here"')

    def matrix_message(self, room: MatrixRoom, event: Dict):
        """
        Handle a message sent to a mapped Matrix room.

        Sends the message to the xmpp handle specified by the room's topic.

        :param room: Matrix room object representing the room in which the message was received.
        :param event: The Matrix event that was received. Assumed to be an m.room.message .
        """
        if event['sender'] == self.bot_id:
            return

        if room.topic in self.special_rooms.keys():
            logger.error('matrix_message called on special channel')

        logger.debug('matrix_message: {}  {}'.format(room.room_id, event))

        if event['content']['msgtype'] == 'm.text':
            message_body = event['content']['body']

            if room.topic.startswith(self.groupchat_flag):
                jid = room.topic[len(self.groupchat_flag):]
                message_type = 'groupchat'
            else:
                jid = room.topic
                message_type = 'chat'

            logger.info('Matrix received message to {} : {}'.format(jid, message_body))
            self.xmpp.send_message(mto=jid, mbody=message_body, mtype=message_type)

            # Possible that we're in a room that wasn't mapped
            if jid not in self.xmpp.jid_nick_map:
                logger.error('Received message in matrix room with topic {},'.format(jid) +
                              'which wasn\'t in the jid_nick_map')
            name = self.xmpp.jid_nick_map.get(jid, jid)

            send_message = self.jid_actions.get(jid, self.default_actions)['send_messages_to_all_chat']
            if send_message:
                self.special_rooms['all_chat'].send_notice('To {} : {}'.format(name, message_body))

    def xmpp_message(self, message: Dict):
        """
        Handle a message received by the XMPP client.

        Sends the message to the relevant mapped Matrix room, as well as the Matrix all-chat room.

        :param message: The message that was received.
        :return:
        """
        logger.info('XMPP received {} : {}'.format(message['from'].full, message['body']))

        if message['type'] in ('normal', 'chat'):
            from_jid = message['from'].bare
            from_name = self.xmpp.jid_nick_map.get(from_jid, from_jid)

            send_message2all = self.jid_actions.get(from_jid, self.default_actions)['send_messages_to_all_chat']
            if send_message2all:
                self.special_rooms['all_chat'].send_text('From  ({})\n{}: {}'.format(from_jid, from_name, message['body']))

            send_message2room  = self.jid_actions.get(from_jid, self.default_actions)['send_messages_to_jid_rooms']
            if send_message2room:
                if from_jid not in self.xmpp.jid_nick_map.keys():
                    logger.error('xmpp_message: JID {} NOT IN ROSTER!?'.format(from_jid))
                    self.xmpp.get_roster(block=True)

                room = self.get_room_for_topic(from_jid)
                room.send_text(message['body'])

    def xmpp_groupchat_message(self, message: Dict):
        """
        Handle a groupchat message received by the XMPP client.

        Sends the message to the relevant mapped Matrix room, as well as the Matrix all-chat room.

        :param message: The message that was received.
        :return:
        """
        logger.info('XMPP MUC received {} : {}'.format(message['from'].full, message['body']))

        if message['type'] == 'groupchat':
            from_jid = message['from'].bare
            from_name = message['mucnick']

            if self.groupchat_mute_own_nick and from_name == self.xmpp_groupchat_nick:
                return

            room = self.get_room_for_topic(self.groupchat_flag + from_jid)
            room.send_text(from_name + ': ' + message['body'])

            if self.groupchat_send_messages_to_all_chat:
                self.special_rooms['all_chat'].send_text(
                    'Room {}, from {}: {}'.format(from_jid, from_name, message['body']))

    def create_groupchat_room(self, room_jid: str):
        room = self.create_mapped_room(topic=self.groupchat_flag + room_jid)
        if room_jid not in self.groupchat_jids:
            self.groupchat_jids.append(room_jid)
        for user_id in self.users_to_invite:
            room.invite_user(user_id)

    def xmpp_presence_available(self, presence: Dict):
        """
        Handle a presence of type "available".

        Sends a notice to the control channel.

        :param presence: The presence that was received.
        """
        logger.debug('XMPP received {} : (available)'.format(presence['from'].full))

        jid = presence['from'].bare
        if jid not in self.xmpp.jid_nick_map.keys():
            logger.error('xmpp_presence_available: JID {} NOT IN ROSTER!?'.format(jid))
            self.xmpp.get_roster(block=True)

        send_presence = self.jid_actions.get(jid, self.default_actions)['send_presences_to_control']
        if send_presence:
            name = self.xmpp.jid_nick_map.get(jid, jid)
            self.special_rooms['control'].send_notice('{} available ({})'.format(name, jid))

    def xmpp_presence_unavailable(self, presence):
        """
        Handle a presence of type "unavailable".

        Sends a notice to the control channel.

        :param presence: The presence that was received.
        """
        logger.debug('XMPP received {} : (unavailable)'.format(presence['from'].full))

        jid = presence['from'].bare
        if jid not in self.xmpp.jid_nick_map.keys():
            logger.error('xmpp_presence_unavailable: JID {} NOT IN ROSTER!?'.format(jid))
            self.xmpp.get_roster(block=True)

        send_presence = self.jid_actions.get(jid, self.default_actions)['send_presences_to_control']
        if send_presence:
            name = self.xmpp.jid_nick_map.get(jid, jid)
            self.special_rooms['control'].send_notice('{} unavailable ({})'.format(name, jid))

    def xmpp_roster_update(self, _event):
        """
        Handle an XMPP roster update.

        Maps all existing Matrix rooms, creates a new mapped room for each JID in the roster
        which doesn't have one yet, and invites the users specified in the config in to all the rooms.

        :param _event: The received roster update event (unused).
        """
        logger.debug('######### ROSTER UPDATE ###########')

        rjids = [jid for jid in self.xmpp.roster]
        if len(rjids) > 1:
            raise Exception('Not sure what to do with more than one roster...')

        roster0 = self.xmpp.roster[rjids[0]]
        self.xmpp.roster_dict = {jid: roster0[jid] for jid in roster0}
        roster = self.xmpp.roster_dict

        self.map_rooms_by_topic()

        # Create new rooms where none exist
        for jid, info in roster.items():
            if '@' not in jid:
                logger.warning('Skipping fake jid in roster: ' + jid)
                continue

            name = info['name']
            self.xmpp.jid_nick_map[jid] = name

            # Check if we need to create a room
            if self.jid_actions.get(jid, self.default_actions)['send_messages_to_jid_rooms']:
                self.create_mapped_room(topic=jid, name=name)

        logger.debug('Sending invitations..')
        # Invite to all rooms
        for room in self.matrix.get_rooms().values():
            users_in_room = room.get_joined_members()
            for user_id in self.users_to_invite:
                if user_id not in users_in_room:
                    room.invite_user(user_id)

        logger.debug('######## Done with roster update #######')

    def xmpp_unrecognized_event(self, event):
        logger.error('Unrecognized event: {} || {}'.format(type(event), event))