class MattermostBackend(ErrBot):
	def __init__(self, config):
		super().__init__(config)
		identity = config.BOT_IDENTITY
		self._login = identity.get('login', None)
		self._password = identity.get('password', None)
		self._personal_access_token = identity.get('token', None)
		self._mfa_token = identity.get('mfa_token', None)
		self.team = identity.get('team')
		self._scheme = identity.get('scheme', 'https')
		self._port = identity.get('port', 8065)
		self.cards_hook = identity.get('cards_hook', None)
		self.url = identity.get('server').rstrip('/')
		self.insecure = identity.get('insecure', False)
		self.timeout = identity.get('timeout', DEFAULT_TIMEOUT)
		self.teamid = ''
		self.token = ''
		self.bot_identifier = None
		self.driver = None
		self.md = md()

	@property
	def userid(self):
		return "{}".format(self.bot_identifier.userid)

	@property
	def mode(self):
		return 'mattermost'

	def username_to_userid(self, name):
		"""Converts a name prefixed with @ to the userid"""
		name = name.lstrip('@')
		user = self.driver.users.get_user_by_username(username=name)
		if user is None:
			raise UserDoesNotExistError("Cannot find user {}".format(name))
		return user['id']

	@asyncio.coroutine
	def mattermost_event_handler(self, payload):
		if not payload:
			return

		payload = json.loads(payload)
		if 'event' not in payload:
			log.debug("Message contains no event: {}".format(payload))
			return

		event_handlers = {
			'posted': self._message_event_handler,
			'status_change': self._status_change_event_handler,
			'hello': self._hello_event_handler,
			'user_added': self._room_joined_event_handler,
			'user_removed': self._room_left_event_handler,
		}

		event = payload['event']
		event_handler = event_handlers.get(event)

		if event_handler is None:
			log.debug("No event handler available for {}, ignoring.".format(event))
			return
		# noinspection PyBroadException
		try:
			event_handler(payload)
		except Exception:
			log.exception("{} event handler raised an exception".format(event))

	def _room_joined_event_handler(self, message):
		log.debug('User added to channel')
		if message['data']['user_id'] == self.userid:
			self.callback_room_joined(self)

	def _room_left_event_handler(self, message):
		log.debug('User removed from channel')
		if message['broadcast']['user_id'] == self.userid:
			self.callback_room_left(self)

	def _message_event_handler(self, message):
		log.debug(message)
		data = message['data']

		# In some cases (direct messages) team_id is an empty string
		if data['team_id'] != '' and self.teamid != data['team_id']:
			log.info("Message came from another team ({}), ignoring...".format(data['team_id']))
			return

		broadcast = message['broadcast']

		if 'channel_id' in data:
			channelid = data['channel_id']
		elif 'channel_id' in broadcast:
			channelid = broadcast['channel_id']
		else:
			log.error("Couldn't find a channelid for event {}".format(message))
			return

		channel_type = data['channel_type']

		if channel_type != 'D':
			channel = data['channel_name']
		else:
			channel = channelid

		text = ''
		post_id = ''
		file_ids = None
		userid = None

		if 'post' in data:
			post = json.loads(data['post'])
			text = post['message']
			userid = post['user_id']
			if 'file_ids' in post:
				file_ids = post['file_ids']
			post_id = post['id']
			if 'type' in post and post['type'] == 'system_add_remove':
				log.info("Ignoring message from System")
				return

		if 'user_id' in data:
			userid = data['user_id']

		if not userid:
			log.error('No userid in event {}'.format(message))
			return

		mentions = []
		if 'mentions' in data:
			# TODO: Only user, not channel mentions are in here at the moment
			mentions = self.mentions_build_identifier(json.loads(data['mentions']))

		# Thread root post id
		root_id = post.get('root_id')
		if root_id is '':
			root_id = post_id

		msg = Message(
			text,
			extras={
				'id': post_id,
				'root_id': root_id,
				'mattermost_event': message,
				'url': '{scheme:s}://{domain:s}:{port:s}/{teamname:s}/pl/{postid:s}'.format(
					scheme=self.driver.options['scheme'],
					domain=self.driver.options['url'],
					port=str(self.driver.options['port']),
					teamname=self.team,
					postid=post_id
				)
			}
		)
		if file_ids:
			msg.extras['attachments'] = file_ids

		# TODO: Slack handles bots here, but I am not sure if bot users is a concept in mattermost
		if channel_type == 'D':
			msg.frm = MattermostPerson(self.driver, userid=userid, channelid=channelid, teamid=self.teamid)
			msg.to = MattermostPerson(
				self.driver, userid=self.bot_identifier.userid, channelid=channelid, teamid=self.teamid)
		elif channel_type == 'O' or channel_type == 'P':
			msg.frm = MattermostRoomOccupant(self.driver, userid=userid, channelid=channelid, teamid=self.teamid, bot=self)
			msg.to = MattermostRoom(channel, teamid=self.teamid, bot=self)
		else:
			log.warning('Unknown channel type \'{}\'! Unable to handle {}.'.format(
				channel_type,
				channel
			))
			return

		self.callback_message(msg)

		if mentions:
			self.callback_mention(msg, mentions)

	def _status_change_event_handler(self, message):
		"""Event handler for the 'presence_change' event"""
		idd = MattermostPerson(self.driver, message['data']['user_id'])
		status = message['data']['status']
		if status == 'online':
			status = ONLINE
		elif status == 'away':
			status = AWAY
		else:
			log.error(
				"It appears the Mattermost API changed, I received an unknown status type %s" % status
			)
			status = ONLINE
		self.callback_presence(Presence(identifier=idd, status=status))

	def _hello_event_handler(self, message):
		"""Event handler for the 'hello' event"""
		self.connect_callback()
		self.callback_presence(Presence(identifier=self.bot_identifier, status=ONLINE))

	@lru_cache(1024)
	def get_direct_channel(self, userid, other_user_id):
		"""
		Get the direct channel to another user.
		If it does not exist, it will be created.
		"""
		try:
			return self.driver.channels.create_direct_message_channel(options=[userid, other_user_id])
		except (InvalidOrMissingParameters, NotEnoughPermissions):
			raise RoomDoesNotExistError("Could not find Direct Channel for users with ID {} and {}".format(
				userid, other_user_id
			))

	def build_identifier(self, txtrep):
		"""
		Convert a textual representation into a :class:`~MattermostPerson` or :class:`~MattermostRoom`

		Supports strings with the following formats::

			@username
			~channelname
			channelid
		"""
		txtrep = txtrep.strip()
		if txtrep.startswith('~'):
			# Channel
			channelid = self.channelname_to_channelid(txtrep[1:])
			if channelid is not None:
				return MattermostRoom(channelid=channelid, teamid=self.teamid, bot=self)
		else:
			# Assuming either a channelid or a username
			if txtrep.startswith('@'):
				# Username
				userid = self.username_to_userid(txtrep[1:])
			else:
				# Channelid
				userid = txtrep

			if userid is not None:
				return MattermostPerson(
					self.driver,
					userid=userid,
					channelid=self.get_direct_channel(self.userid, userid)['id'],
					teamid=self.teamid
				)
		raise Exception(
			'Invalid or unsupported Mattermost identifier: %s' % txtrep
		)

	def mentions_build_identifier(self, mentions):
		identifier = []
		for mention in mentions:
			if mention != self.bot_identifier.userid:
				identifier.append(
					self.build_identifier(mention)
				)
		return identifier

	def serve_once(self):
		self.driver = Driver({
			'scheme': self._scheme,
			'url': self.url,
			'port': self._port,
			'verify': not self.insecure,
			'timeout': self.timeout,
			'login_id': self._login,
			'password': self._password,
			'token': self._personal_access_token,
			'mfa_token': self._mfa_token
		})
		self.driver.login()

		self.teamid = self.driver.teams.get_team_by_name(name=self.team)['id']
		userid = self.driver.users.get_user(user_id='me')['id']

		self.token = self.driver.client.token

		self.bot_identifier = MattermostPerson(self.driver, userid=userid, teamid=self.teamid)

		# noinspection PyBroadException
		try:
			loop = self.driver.init_websocket(event_handler=self.mattermost_event_handler)
			self.reset_reconnection_count()
			loop.run_forever()
		except KeyboardInterrupt:
			log.info("Interrupt received, shutting down..")
			return True
		except Exception:
			log.exception("Error reading from RTM stream:")
		finally:
			log.debug("Triggering disconnect callback")
			self.disconnect_callback()

	def _prepare_message(self, message):
		to_name = "<unknown>"
		if message.is_group:
			to_channel_id = message.to.id
			if message.to.name:
				to_name = message.to.name
			else:
				self.channelid_to_channelname(channelid=to_channel_id)
		else:
			to_name = message.to.username

			if isinstance(message.to, RoomOccupant):  # private to a room occupant -> this is a divert to private !
				log.debug("This is a divert to private message, sending it directly to the user.")
				channel = self.get_direct_channel(self.userid, self.username_to_userid(to_name))
				to_channel_id = channel['id']
			else:
				to_channel_id = message.to.channelid
		return to_name, to_channel_id

	def send_message(self, message):
		super().send_message(message)
		try:
			to_name, to_channel_id = self._prepare_message(message)

			message_type = "direct" if message.is_direct else "channel"
			log.debug('Sending %s message to %s (%s)' % (message_type, to_name, to_channel_id))

			body = self.md.convert(message.body)
			log.debug('Message size: %d' % len(body))

			limit = min(self.bot_config.MESSAGE_SIZE_LIMIT, MATTERMOST_MESSAGE_LIMIT)
			parts = self.prepare_message_body(body, limit)

			root_id = None
			if message.parent is not None:
				root_id = message.parent.extras.get('root_id')

			for part in parts:
				self.driver.posts.create_post(options={
					'channel_id': to_channel_id,
					'message': part,
					'root_id': root_id,
				})
		except (InvalidOrMissingParameters, NotEnoughPermissions):
			log.exception(
				"An exception occurred while trying to send the following message "
				"to %s: %s" % (to_name, message.body)
			)

	def send_card(self, card: Card):
		if isinstance(card.to, RoomOccupant):
			card.to = card.to.room

		to_humanreadable, to_channel_id = self._prepare_message(card)

		attachment = {}
		if card.summary:
			attachment['pretext'] = card.summary
		if card.title:
			attachment['title'] = card.title
		if card.link:
			attachment['title_link'] = card.link
		if card.image:
			attachment['image_url'] = card.image
		if card.thumbnail:
			attachment['thumb_url'] = card.thumbnail
		attachment['text'] = card.body

		if card.color:
			attachment['color'] = COLORS[card.color] if card.color in COLORS else card.color

		if card.fields:
			attachment['fields'] = [{'title': key, 'value': value, 'short': True} for key, value in card.fields]

		data = {
			'attachments': [attachment]
		}

		if card.to:
			if isinstance(card.to, MattermostRoom):
				data['channel'] = card.to.name

		try:
			log.debug('Sending data:\n%s', data)
			# We need to send a webhook - mattermost has no api endpoint for attachments/cards
			# For this reason, we need to build our own url, since we need /hooks and not /api/v4
			# Todo: Reminder to check if this is still the case
			self.driver.webhooks.call_webhook(self.cards_hook, options=data)
		except (
					InvalidOrMissingParameters,
					NotEnoughPermissions,
					ContentTooLarge,
					FeatureDisabled,
					NoAccessTokenProvided
				):
			log.exception(
				"An exception occurred while trying to send a card to %s.[%s]" % (to_humanreadable, card)
			)

	def prepare_message_body(self, body, size_limit):
		"""
		Returns the parts of a message chunked and ready for sending.
		This is a staticmethod for easier testing.
		Args:
			body (str)
			size_limit (int): chunk the body into sizes capped at this maximum
		Returns:
			[str]
		"""
		fixed_format = body.startswith('```')  # hack to fix the formatting
		parts = list(split_string_after(body, size_limit))

		if len(parts) == 1:
			# If we've got an open fixed block, close it out
			if parts[0].count('```') % 2 != 0:
				parts[0] += '\n```\n'
		else:
			for i, part in enumerate(parts):
				starts_with_code = part.startswith('```')

				# If we're continuing a fixed block from the last part
				if fixed_format and not starts_with_code:
					parts[i] = '```\n' + part

				# If we've got an open fixed block, close it out
				if parts[i].count('```') % 2 != 0:
					parts[i] += '\n```\n'

		return parts

	def change_presence(self, status: str=ONLINE, message: str=''):
		pass  # Mattermost does not have a request/websocket event to change the presence

	def is_from_self(self, message: Message):
		return self.bot_identifier.userid == message.frm.userid

	def shutdown(self):
		self.driver.logout()
		super().shutdown()

	def query_room(self, room):
		""" Room can either be a name or a channelid """
		return MattermostRoom(room, teamid=self.teamid, bot=self)

	def prefix_groupchat_reply(self, message: Message, identifier):
		super().prefix_groupchat_reply(message, identifier)
		message.body = '@{0}: {1}'.format(identifier.nick, message.body)

	def build_reply(self, message, text=None, private=False, threaded=False):
		response = self.build_message(text)
		response.frm = self.bot_identifier
		if private:
			response.to = message.frm
		else:
			response.to = message.frm.room if isinstance(message.frm, RoomOccupant) else message.frm

		if threaded:
			response.extras['root_id'] = message.extras.get('root_id')
			self.driver.posts.get_post(message.extras.get('root_id'))
			response.parent = message

		return response

	def get_public_channels(self):
		channels = []
		page = 0
		channel_page_limit = 200
		while True:
			channel_list = self.driver.channels.get_public_channels(
				team_id=self.teamid,
				params={'page': page, 'per_page': channel_page_limit}
			)
			if len(channel_list) == 0:
				break
			else:
				channels.extend(channel_list)
			page += 1
		return channels

	def channels(self, joined_only=False):
		channels = []
		channels.extend(self.driver.channels.get_channels_for_user(user_id=self.userid, team_id=self.teamid))
		if not joined_only:
			public_channels = self.get_public_channels()
			for channel in public_channels:
				if channel not in channels:
					channels.append(channel)
		return channels

	def rooms(self):
		"""Return public and private channels, but no direct channels"""
		rooms = self.channels(joined_only=True)
		channels = [channel for channel in rooms if channel['type'] != 'D']
		return [MattermostRoom(channelid=channel['id'], teamid=channel['team_id'], bot=self) for channel in channels]

	def channelid_to_channelname(self, channelid):
		"""Convert the channelid in the current team to the channel name"""
		channel = self.driver.channels.get_channel(channel_id=channelid)
		if 'name' not in channel:
			raise RoomDoesNotExistError("No channel with ID {} exists in team with ID {}".format(
				id, self.teamid
			))
		return channel['name']

	def channelname_to_channelid(self, name):
		"""Convert the channelname in the current team to the channel id"""
		channel = self.driver.channels.get_channel_by_name(team_id=self.teamid, channel_name=name)
		if 'id' not in channel:
			raise RoomDoesNotExistError("No channel with name {} exists in team with ID {}".format(
				name, self.teamid
			))
		return channel['id']

	def __hash__(self):
		return 0  # This is a singleton anyway
            if emoji == 'flashlight':
                ledon()
            if emoji == 'sunny':
                ledoff()
            if emoji == 'wink':
                blink()

        if message['event'] == 'posted' or message['event'] == 'post_edited':
            reaction = json.loads(message['data']['post'])
            post = reaction['message']
            if 'stop' in post:
                stop()
            if 'up' in post:
                print('forward')
                forward()
            if 'down' in post:
                backwards()
            if 'left' in post:
                left()
            if 'right' in post:
                right()
            if 'on' in post:
                ledon()
            if 'off' in post:
                ledoff()
            if 'blink' in post:
                blink()


mm.init_websocket(my_event_handler)
        return True

    if event == "channel_viewed":
        mark_channel_viewed(cur, conn, channel_id)
    elif event == "posted":
        if isinstance(message["data"]["post"], str):
            message["data"]["post"] = json.loads(message["data"]["post"])
        sender_id = message["data"]["post"]["user_id"]
        if sender_id == user_id:
            mark_channel_viewed(cur, conn, channel_id)
        else:
            mentions = message.get("data", {}).get("mentions", []) or []
            omitted_users = message.get("broadcast", {}).get("omit_users",
                                                             []) or []
            if user_id in mentions and user_id not in omitted_users:
                increment_channel_mention_count(cur, conn, channel_id)
    conn.close()


with open(stdout_loc, "w+") as stdout:

    with open(stderr_loc, "w+") as stderr:

        with daemon.DaemonContext(stdout=stdout, stderr=stderr):

            d = Driver(options=options)

            d.login()

            d.init_websocket(my_event_handler, websocket_cls=CustomWebsocket)
class MattermostBackend(ErrBot):
    def __init__(self, config):
        super().__init__(config)
        identity = config.BOT_IDENTITY
        self._login = identity.get("login", None)
        self._password = identity.get("password", None)
        self._personal_access_token = identity.get("token", None)
        self._mfa_token = identity.get("mfa_token", None)
        self.team = identity.get("team")
        self._scheme = identity.get("scheme", "https")
        self._port = identity.get("port", 8065)
        self.cards_hook = identity.get("cards_hook", None)
        self.url = identity.get("server").rstrip("/")
        self.insecure = identity.get("insecure", False)
        self.timeout = identity.get("timeout", DEFAULT_TIMEOUT)
        self.teamid = ""
        self.token = ""
        self.bot_identifier = None
        self.driver = None
        self.md = md()
        self.event_handlers = {
            "posted": [self._message_event_handler],
            "status_change": [self._status_change_event_handler],
            "hello": [self._hello_event_handler],
            "user_added": [self._room_joined_event_handler],
            "user_removed": [self._room_left_event_handler],
        }

    def set_message_size_limit(self, limit=16377, hard_limit=16383):
        """
        Mattermost message limit is 16383 chars, need to leave some space for
        backticks when messages are split
        """
        super().set_message_size_limit(limit, hard_limit)

    @property
    def userid(self):
        return "{}".format(self.bot_identifier.userid)

    @property
    def mode(self):
        return "mattermost"

    def username_to_userid(self, name):
        """Converts a name prefixed with @ to the userid"""
        name = name.lstrip("@")
        user = self.driver.users.get_user_by_username(username=name)
        if user is None:
            raise UserDoesNotExistError("Cannot find user {}".format(name))
        return user["id"]

    def register_handler(self, event, handler):
        if event not in self.event_handlers:
            self.event_handlers[event] = []
        self.event_handlers[event].append(handler)

    @asyncio.coroutine
    def mattermost_event_handler(self, payload):
        if not payload:
            return

        payload = json.loads(payload)
        if "event" not in payload:
            log.debug("Message contains no event: {}".format(payload))
            return

        event = payload["event"]
        event_handlers = self.event_handlers.get(event)

        if event_handlers is None:
            log.debug(
                "No event handlers available for {}, ignoring.".format(event))
            return
        # noinspection PyBroadException
        for event_handler in event_handlers:
            try:
                event_handler(payload)
            except Exception:
                log.exception(
                    "{} event handler raised an exception".format(event))

    def _room_joined_event_handler(self, message):
        log.debug("User added to channel")
        if message["data"]["user_id"] == self.userid:
            self.callback_room_joined(self)

    def _room_left_event_handler(self, message):
        log.debug("User removed from channel")
        if message["broadcast"]["user_id"] == self.userid:
            self.callback_room_left(self)

    def _message_event_handler(self, message):
        log.debug(message)
        data = message["data"]

        # In some cases (direct messages) team_id is an empty string
        if data["team_id"] != "" and self.teamid != data["team_id"]:
            log.info("Message came from another team ({}), ignoring...".format(
                data["team_id"]))
            return

        broadcast = message["broadcast"]

        if "channel_id" in data:
            channelid = data["channel_id"]
        elif "channel_id" in broadcast:
            channelid = broadcast["channel_id"]
        else:
            log.error("Couldn't find a channelid for event {}".format(message))
            return

        channel_type = data["channel_type"]

        if channel_type != "D":
            channel = data["channel_name"]
        else:
            channel = channelid

        text = ""
        post_id = ""
        file_ids = None
        userid = None

        if "post" in data:
            post = json.loads(data["post"])
            text = post["message"]
            userid = post["user_id"]
            if "file_ids" in post:
                file_ids = post["file_ids"]
            post_id = post["id"]
            if "type" in post and post["type"] == "system_add_remove":
                log.info("Ignoring message from System")
                return

        if "user_id" in data:
            userid = data["user_id"]

        if not userid:
            log.error("No userid in event {}".format(message))
            return

        mentions = []
        if "mentions" in data:
            # TODO: Only user, not channel mentions are in here at the moment
            mentions = self.mentions_build_identifier(
                json.loads(data["mentions"]))

        # Thread root post id
        root_id = post.get("root_id", "")
        if root_id == "":
            root_id = post_id

        msg = Message(
            text,
            extras={
                "id":
                post_id,
                "root_id":
                root_id,
                "mattermost_event":
                message,
                "url":
                "{scheme:s}://{domain:s}:{port:s}/{teamname:s}/pl/{postid:s}".
                format(
                    scheme=self.driver.options["scheme"],
                    domain=self.driver.options["url"],
                    port=str(self.driver.options["port"]),
                    teamname=self.team,
                    postid=post_id,
                ),
            },
        )
        if file_ids:
            msg.extras["attachments"] = file_ids

        # TODO: Slack handles bots here, but I am not sure if bot users is a concept in mattermost
        if channel_type == "D":
            msg.frm = MattermostPerson(self.driver,
                                       userid=userid,
                                       channelid=channelid,
                                       teamid=self.teamid)
            msg.to = MattermostPerson(
                self.driver,
                userid=self.bot_identifier.userid,
                channelid=channelid,
                teamid=self.teamid,
            )
        elif channel_type == "O" or channel_type == "P":
            msg.frm = MattermostRoomOccupant(
                self.driver,
                userid=userid,
                channelid=channelid,
                teamid=self.teamid,
                bot=self,
            )
            msg.to = MattermostRoom(channel, teamid=self.teamid, bot=self)
        else:
            log.warning(
                "Unknown channel type '{}'! Unable to handle {}.".format(
                    channel_type, channel))
            return

        self.callback_message(msg)

        if mentions:
            self.callback_mention(msg, mentions)

    def _status_change_event_handler(self, message):
        """Event handler for the 'presence_change' event"""
        idd = MattermostPerson(self.driver, message["data"]["user_id"])
        status = message["data"]["status"]
        if status == "online":
            status = ONLINE
        elif status == "away":
            status = AWAY
        else:
            log.error(
                "It appears the Mattermost API changed, I received an unknown status type %s"
                % status)
            status = ONLINE
        self.callback_presence(Presence(identifier=idd, status=status))

    def _hello_event_handler(self, message):
        """Event handler for the 'hello' event"""
        self.connect_callback()
        self.callback_presence(
            Presence(identifier=self.bot_identifier, status=ONLINE))

    @lru_cache(1024)
    def get_direct_channel(self, userid, other_user_id):
        """
        Get the direct channel to another user.
        If it does not exist, it will be created.
        """
        try:
            return self.driver.channels.create_direct_message_channel(
                options=[userid, other_user_id])
        except (InvalidOrMissingParameters, NotEnoughPermissions):
            raise RoomDoesNotExistError(
                "Could not find Direct Channel for users with ID {} and {}".
                format(userid, other_user_id))

    def build_identifier(self, txtrep):
        """
        Convert a textual representation into a :class:`~MattermostPerson` or :class:`~MattermostRoom`

        Supports strings with the following formats::

                @username
                ~channelname
                channelid
        """
        txtrep = txtrep.strip()
        if txtrep.startswith("~"):
            # Channel
            channelid = self.channelname_to_channelid(txtrep[1:])
            if channelid is not None:
                return MattermostRoom(channelid=channelid,
                                      teamid=self.teamid,
                                      bot=self)
        else:
            # Assuming either a channelid or a username
            if txtrep.startswith("@"):
                # Username
                userid = self.username_to_userid(txtrep[1:])
            else:
                # Channelid
                userid = txtrep

            if userid is not None:
                return MattermostPerson(
                    self.driver,
                    userid=userid,
                    channelid=self.get_direct_channel(self.userid,
                                                      userid)["id"],
                    teamid=self.teamid,
                )
        raise Exception("Invalid or unsupported Mattermost identifier: %s" %
                        txtrep)

    def mentions_build_identifier(self, mentions):
        identifier = []
        for mention in mentions:
            if mention != self.bot_identifier.userid:
                identifier.append(self.build_identifier(mention))
        return identifier

    def serve_once(self):
        self.driver = Driver({
            "scheme": self._scheme,
            "url": self.url,
            "port": self._port,
            "verify": not self.insecure,
            "timeout": self.timeout,
            "login_id": self._login,
            "password": self._password,
            "token": self._personal_access_token,
            "mfa_token": self._mfa_token,
        })
        self.driver.login()

        self.teamid = self.driver.teams.get_team_by_name(name=self.team)["id"]
        userid = self.driver.users.get_user(user_id="me")["id"]

        self.token = self.driver.client.token

        self.bot_identifier = MattermostPerson(self.driver,
                                               userid=userid,
                                               teamid=self.teamid)

        # noinspection PyBroadException
        try:
            loop = self.driver.init_websocket(
                event_handler=self.mattermost_event_handler)
            self.reset_reconnection_count()
            loop.run_forever()
        except KeyboardInterrupt:
            log.info("Interrupt received, shutting down..")
            return True
        except Exception:
            log.exception("Error reading from RTM stream:")
        finally:
            log.debug("Triggering disconnect callback")
            self.disconnect_callback()

    def _prepare_message(self, message):
        to_name = "<unknown>"
        if message.is_group:
            to_channel_id = message.to.id
            if message.to.name:
                to_name = message.to.name
            else:
                self.channelid_to_channelname(channelid=to_channel_id)
        else:
            to_name = message.to.username

            if isinstance(
                    message.to, RoomOccupant
            ):  # private to a room occupant -> this is a divert to private !
                log.debug(
                    "This is a divert to private message, sending it directly to the user."
                )
                channel = self.get_direct_channel(
                    self.userid, self.username_to_userid(to_name))
                to_channel_id = channel["id"]
            else:
                to_channel_id = message.to.channelid
        return to_name, to_channel_id

    def send_message(self, message):
        super().send_message(message)
        try:
            to_name, to_channel_id = self._prepare_message(message)

            message_type = "direct" if message.is_direct else "channel"
            log.debug("Sending %s message to %s (%s)" %
                      (message_type, to_name, to_channel_id))

            body = self.md.convert(message.body)
            log.debug("Message size: %d" % len(body))

            parts = self.prepare_message_body(body, self.message_size_limit)

            root_id = None
            if message.parent is not None:
                root_id = message.parent.extras.get("root_id")

            for part in parts:
                self.driver.posts.create_post(
                    options={
                        "channel_id": to_channel_id,
                        "message": part,
                        "root_id": root_id,
                    })
        except (InvalidOrMissingParameters, NotEnoughPermissions):
            log.exception(
                "An exception occurred while trying to send the following message "
                "to %s: %s" % (to_name, message.body))

    def send_card(self, card: Card):
        if isinstance(card.to, RoomOccupant):
            card.to = card.to.room

        to_humanreadable, to_channel_id = self._prepare_message(card)

        attachment = {}
        if card.summary:
            attachment["pretext"] = card.summary
        if card.title:
            attachment["title"] = card.title
        if card.link:
            attachment["title_link"] = card.link
        if card.image:
            attachment["image_url"] = card.image
        if card.thumbnail:
            attachment["thumb_url"] = card.thumbnail
        attachment["text"] = card.body

        if card.color:
            attachment["color"] = (COLORS[card.color]
                                   if card.color in COLORS else card.color)

        if card.fields:
            attachment["fields"] = [{
                "title": key,
                "value": value,
                "short": True
            } for key, value in card.fields]

        data = {"attachments": [attachment]}

        if card.to:
            if isinstance(card.to, MattermostRoom):
                data["channel"] = card.to.name

        try:
            log.debug("Sending data:\n%s", data)
            # We need to send a webhook - mattermost has no api endpoint for attachments/cards
            # For this reason, we need to build our own url, since we need /hooks and not /api/v4
            # Todo: Reminder to check if this is still the case
            self.driver.webhooks.call_webhook(self.cards_hook, options=data)
        except (
                InvalidOrMissingParameters,
                NotEnoughPermissions,
                ContentTooLarge,
                FeatureDisabled,
                NoAccessTokenProvided,
        ):
            log.exception(
                "An exception occurred while trying to send a card to %s.[%s]"
                % (to_humanreadable, card))

    def prepare_message_body(self, body, size_limit):
        """
        Returns the parts of a message chunked and ready for sending.
        This is a staticmethod for easier testing.
        Args:
                body (str)
                size_limit (int): chunk the body into sizes capped at this maximum
        Returns:
                [str]
        """
        fixed_format = body.startswith("```")  # hack to fix the formatting
        parts = list(split_string_after(body, size_limit))

        if len(parts) == 1:
            # If we've got an open fixed block, close it out
            if parts[0].count("```") % 2 != 0:
                parts[0] += "\n```\n"
        else:
            for i, part in enumerate(parts):
                starts_with_code = part.startswith("```")

                # If we're continuing a fixed block from the last part
                if fixed_format and not starts_with_code:
                    parts[i] = "```\n" + part

                # If we've got an open fixed block, close it out
                if parts[i].count("```") % 2 != 0:
                    parts[i] += "\n```\n"

        return parts

    def change_presence(self, status: str = ONLINE, message: str = ""):
        pass  # Mattermost does not have a request/websocket event to change the presence

    def is_from_self(self, message: Message):
        return self.bot_identifier.userid == message.frm.userid

    def shutdown(self):
        self.driver.logout()
        super().shutdown()

    def query_room(self, room):
        """Room can either be a name or a channelid"""
        return MattermostRoom(room, teamid=self.teamid, bot=self)

    def prefix_groupchat_reply(self, message: Message, identifier):
        super().prefix_groupchat_reply(message, identifier)
        message.body = "@{0}: {1}".format(identifier.nick, message.body)

    def build_reply(self, message, text=None, private=False, threaded=False):
        response = self.build_message(text)
        response.frm = self.bot_identifier
        if private:
            response.to = message.frm
        else:
            response.to = (message.frm.room if isinstance(
                message.frm, RoomOccupant) else message.frm)

        if threaded:
            response.extras["root_id"] = message.extras.get("root_id")
            self.driver.posts.get_post(message.extras.get("root_id"))
            response.parent = message

        return response

    def get_public_channels(self):
        channels = []
        page = 0
        channel_page_limit = 200
        while True:
            channel_list = self.driver.channels.get_public_channels(
                team_id=self.teamid,
                params={
                    "page": page,
                    "per_page": channel_page_limit
                },
            )
            if len(channel_list) == 0:
                break
            else:
                channels.extend(channel_list)
            page += 1
        return channels

    def channels(self, joined_only=False):
        channels = []
        channels.extend(
            self.driver.channels.get_channels_for_user(user_id=self.userid,
                                                       team_id=self.teamid))
        if not joined_only:
            public_channels = self.get_public_channels()
            for channel in public_channels:
                if channel not in channels:
                    channels.append(channel)
        return channels

    def rooms(self):
        """Return public and private channels, but no direct channels"""
        rooms = self.channels(joined_only=True)
        channels = [channel for channel in rooms if channel["type"] != "D"]
        return [
            MattermostRoom(channelid=channel["id"],
                           teamid=channel["team_id"],
                           bot=self) for channel in channels
        ]

    def channelid_to_channelname(self, channelid):
        """Convert the channelid in the current team to the channel name"""
        channel = self.driver.channels.get_channel(channel_id=channelid)
        if "name" not in channel:
            raise RoomDoesNotExistError(
                "No channel with ID {} exists in team with ID {}".format(
                    id, self.teamid))
        return channel["name"]

    def channelname_to_channelid(self, name):
        """Convert the channelname in the current team to the channel id"""
        channel = self.driver.channels.get_channel_by_name(team_id=self.teamid,
                                                           channel_name=name)
        if "id" not in channel:
            raise RoomDoesNotExistError(
                "No channel with name {} exists in team with ID {}".format(
                    name, self.teamid))
        return channel["id"]

    def __hash__(self):
        return 0  # This is a singleton anyway
Example #5
0
class SubscriptionBot:
    """ A mattermost bot implementing a publish/subscribe mechanism. """

    SUBSCRIBED_MESSAGE = "Hi there - thx for joining!"
    UNSUBSCRIBED_MESSAGE = "Bye then, couch potato!"
    NOT_SUBSCRIBED_MESSAGE = "Are you also trying to cancel your gym membership before even registering?"
    UNKNOWN_COMMAND_TEXT = "I don't get it, want to join? Try 'subscribe' instead. 'help' may also be your friend."
    HELP_TEXT = """
|Command|Description|
|:------|:----------|
|subscribe|Join the growing list of subscribers now!|
|unsubscribe|Go back to your boring office-chair-only life.|
|help|I'm quite sure, you know what this one does.|        
"""

    def __init__(self, username, password, scheme='https', debug=False):
        self.subscriptions = set()

        self.username = username
        self.debug = debug

        self.driver = Driver({
            'url': "192.168.122.254",
            'login_id': username,
            'password': password,
            'scheme': scheme,
            'debug': debug,
        })
        self.driver.login()

        # get userid for username since it is not automatically set to driver.client.userid ... for reasons
        res = self.driver.users.get_user_by_username('bot')
        self.userid = res['id']

    def start_listening(self):
        worker = threading.Thread(
            target=SubscriptionBot._start_listening_in_thread, args=(self, ))
        worker.daemon = True
        worker.start()

        print("Initialized bot.")

    def _start_listening_in_thread(self):
        # Setting event loop for thread
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)

        self.driver.init_websocket(self.websocket_handler)

    @asyncio.coroutine
    def websocket_handler(self, event_json):
        event = json.loads(event_json)

        if self.debug:
            print("websocket_handler:" + json.dumps(event, indent=4))

        if 'event' in event and event['event'] == 'posted':
            # mentions is automatically set in direct messages
            mentions = json.loads(event['data']['mentions']
                                  ) if 'mentions' in event['data'] else []

            post = json.loads(event['data']['post'])
            post_id = post['id']
            message = post['message']
            channel_id = post['channel_id']
            sender_id = post['user_id']

            if self.userid in mentions:
                self.handle_bot_message(channel_id, post_id, sender_id,
                                        message)

    def handle_bot_message(self, channel_id, post_id, sender_id, message):
        if re.match(r'(@' + self.username + ')?\s*help\s*', message):
            self._show_help(channel_id, post_id)
        elif re.match(r'(@' + self.username + ')?\s*subscribe\s*', message):
            self._handle_subscription(sender_id, channel_id, post_id)
        elif re.match(r'(@' + self.username + ')?\s*unsubscribe\s*', message):
            self._handle_unsubscription(channel_id, post_id, sender_id)
        else:
            self._handle_unknown_command(channel_id, post_id)

    def _show_help(self, channel_id, post_id):
        self.driver.posts.create_post({
            'channel_id': channel_id,
            'message': self.HELP_TEXT,
            'root_id': post_id,
        })

    def _handle_subscription(self, sender_id, channel_id, post_id):
        self.subscriptions.add(sender_id)
        if self.debug:
            print(sender_id + " subscribed.")

        self.driver.posts.create_post({
            'channel_id': channel_id,
            'message': self.SUBSCRIBED_MESSAGE,
            'root_id': post_id,
        })

    def _handle_unsubscription(self, channel_id, post_id, sender_id):
        if sender_id in self.subscriptions:
            self.subscriptions.discard(sender_id)
            if self.debug:
                print(sender_id + " unsubscribed.")

            self.driver.posts.create_post({
                'channel_id': channel_id,
                'message': self.UNSUBSCRIBED_MESSAGE,
                'root_id': post_id,
            })
        else:
            self.driver.posts.create_post({
                'channel_id': channel_id,
                'message': self.UNSUBSCRIBED_MESSAGE,
                'root_id': post_id,
            })

    def _handle_unknown_command(self, channel_id, post_id):
        self.driver.posts.create_post({
            'channel_id': channel_id,
            'message': self.UNKNOWN_COMMAND_TEXT,
            'root_id': post_id,
        })

    def send_messages_to_subscribers(self, message):
        for subscriber in self.subscriptions:
            self._send_direct_message(subscriber, message)

    def _send_direct_message(self, user_id, message, root_id=None):
        res = self.driver.channels.create_direct_message_channel(
            [self.userid, user_id])
        channel_id = res['id']

        post_options = {
            'channel_id': channel_id,
            'message': message,
        }
        if root_id:
            post_options['root_id'] = root_id

        self.driver.posts.create_post(post_options)
Example #6
0
class MattermostBackend():
    def __init__(self):
        self.url = 'mattermost.example.com'
        self._login = '******'
        self._scheme = 'https'
        self._port = 443
        self.insecure = False
        self.timeout = DEFAULT_TIMEOUT
        self.teamid = ''
        self.token = ''
        self.driver = None

        # Get password from Gnome keyring, matching the stored Chromium password
        self._password = Secret.password_lookup_sync(
            SECRET_SCHEMA, {
                'username_value': self._login,
                'action_url': 'https://mattermost.example.com/login'
            }, None)

    @asyncio.coroutine
    def mattermost_event_handler(self, payload):
        if not payload:
            return

        payload = json.loads(payload)
        if 'event' not in payload:
            log.debug('Message contains no event: {}'.format(payload))
            return

        event_handlers = {
            'posted': self._message_event_handler,
        }

        event = payload['event']
        event_handler = event_handlers.get(event)

        if event_handler is None:
            log.debug(
                'No event handler available for {}, ignoring.'.format(event))
            return

        try:
            event_handler(payload)
        except Exception:
            log.exception(
                '{} event handler raised an exception. Exiting.'.format(event))
            sys.exit(1)

    def _message_event_handler(self, message):
        log.debug(message)
        data = message['data']

        broadcast = message['broadcast']

        if 'channel_id' in data:
            channelid = data['channel_id']
        elif 'channel_id' in broadcast:
            channelid = broadcast['channel_id']
        else:
            log.error("Couldn't find a channelid for event {}".format(message))
            return

        channel_type = data['channel_type']

        if channel_type != 'D':
            channel = data['channel_name']
            if 'team_id' in data:
                teamid = data['team_id']
                if teamid:
                    team = self.driver.api['teams'].get_team(team_id=teamid)
                    teamname = team['display_name']
        else:
            channel = channelid
            teamname = None

        text = ''
        userid = None

        if 'post' in data:
            post = json.loads(data['post'])
            text = post['message']
            userid = post['user_id']
            if 'type' in post and post['type'] == 'system_add_remove':
                log.info('Ignoring message from System')
                return

        if 'user_id' in data:
            userid = data['user_id']

        if not userid:
            log.error('No userid in event {}'.format(message))
            return

        mentions = []
        if 'mentions' in data:
            mentions = json.loads(data['mentions'])

        if mentions:
            username = self.driver.api['users'].get_user(
                user_id=userid)['username']

            print('mentioned: ', teamname, '"', mentions, '"', username, text)
            if self.userid in mentions:
                if teamname:
                    self.notify(
                        '{} in {}/{}'.format(username, teamname, channel),
                        text)
                else:
                    self.notify('{} in DM'.format(username), text)
            log.info('"posted" event from {}: {}'.format(
                self.driver.api['users'].get_user(user_id=userid)['username'],
                text))

    def notify(self, summary, desc=''):
        self._notification = Notification(summary, desc)

    def serve_once(self):
        self.driver = Driver({
            'scheme': self._scheme,
            'url': self.url,
            'port': self._port,
            'verify': not self.insecure,
            'timeout': self.timeout,
            'login_id': self._login,
            'password': self._password
        })
        self.driver.login()

        self.userid = self.driver.api['users'].get_user(user_id='me')['id']

        self.token = self.driver.client.token

        try:
            loop = self.driver.init_websocket(
                event_handler=self.mattermost_event_handler)
            loop.run_forever()
            # loop.stop()
        except KeyboardInterrupt:
            log.info("Interrupt received, shutting down..")
            Notify.uninit()
            self.driver.logout()
            return True
        except Exception:
            log.exception("Error reading from RTM stream:")
        finally:
            log.debug("Triggering disconnect callback")
Example #7
0
class MMostBot:
    def __init__(self,
                 mail,
                 pswd,
                 url,
                 welcome="hello",
                 tags=None,
                 debug=False):
        self.debug = debug
        self.config = {
            "url": url,
            "login_id": mail,
            "password": pswd,
            "scheme": "https",
            "port": 443,
            "verify": True,
            "debug": debug,
            "welcome": welcome
        }
        self.driver = Driver(self.config)
        self.tags = tags or []

    @property
    def user_id(self):
        return self.driver.users.get_user(user_id='me')["id"]

    def listen(self):
        self.driver.login()
        self.driver.init_websocket(self.event_handler)

    #def event_handler(self, event):
    async def event_handler(self, event):
        event = json.loads(event)
        event_type = event.get("event", "")
        if event_type == "hello":
            self.on_connect(event)
        elif event_type == "status_change":
            self.on_status_change(event)
        elif event_type == "typing":
            self.on_typing(event)
        elif event_type == "posted":
            self.on_message(event)
        elif event_type == "channel_viewed":
            self.on_viewed(event)
        elif event_type == "preferences_changed":
            self.on_preferences_changed(event)
        elif event_type == "post_deleted":
            self.on_post_deleted(event)
        elif event_type == "user_added":
            self.on_user_added(event)
        elif event_type == "user_removed":
            self.on_user_removed(event)
        else:
            LOG.debug(event)

    def on_connect(self, event):
        LOG.info("Connected")

    def on_status_change(self, event):
        user_id = event["data"]["user_id"]
        status = event["data"]["status"]

        user_data = self.driver.users.get_user(user_id=user_id)
        username = user_data["username"]
        email = user_data["email"]

        LOG.info(username + ":" + status)

    def on_typing(self, event):
        user_id = event["data"]["user_id"]
        channel_id = event["broadcast"]["channel_id"]

        channel_data = self.driver.channels.get_channel(channel_id)
        channel_name = channel_data["name"]

        user_data = self.driver.users.get_user(user_id=user_id)
        username = user_data["username"]

        if channel_name == self.user_id + "__" + user_id:
            LOG.info(username + " is typing a direct message")
        else:
            LOG.info(username + " is typing a message in channel: " +
                     channel_name)

    def on_message(self, event):
        post = event["data"]["post"]
        post = json.loads(post)
        sender = event["data"]["sender_name"]
        msg = post["message"]
        channel_id = post["channel_id"]
        user_id = post["user_id"]

        channel_data = self.driver.channels.get_channel(channel_id)
        channel_name = channel_data["name"]

        if channel_name == user_id + "__" + self.user_id:
            # direct_message
            if user_id != self.user_id:
                self.on_direct_message(event)
        else:
            if user_id != self.user_id:
                mention = False
                for tag in self.tags:
                    if tag in msg:
                        mention = True
                        break
                if mention:
                    self.on_mention(event)
                else:
                    LOG.info("New message at channel: " + channel_name)
                    LOG.info(sender + " said: " + msg)
                    msg = [sender, msg, channel_name]
                    #mycroft.send_msg(msg)
                    return msg

    def on_mention(self, event):
        post = event["data"]["post"]
        post = json.loads(post)
        sender = event["data"]["sender_name"]
        msg = post["message"]
        channel_id = post["channel_id"]
        user_id = post["user_id"]
        channel_data = self.driver.channels.get_channel(channel_id)
        channel_name = channel_data["name"]

        for tag in self.tags:
            msg = msg.replace(tag, "")

        LOG.info("New mention at channel: " + channel_name)
        LOG.info(sender + " said: " + msg)

        self.handle_mention(msg, sender, channel_id)

    def on_user_added(self, event):
        user_id = event["data"]["user_id"]
        channel_id = event["broadcast"]["channel_id"]

        if user_id != self.user_id:
            user_data = self.driver.users.get_user(user_id=user_id)
            username = user_data["username"]
            self.send_message(channel_id, "@" + user_id + " " + welcome)
        else:
            self.send_message(channel_id, "Blip Blop, I am a Bot!")

    def on_user_removed(self, event):
        user_id = event["broadcast"]["user_id"]
        #channel_id = event["data"]["channel_id"]
        #remover_id = event["data"]["remover_id"]

    def on_direct_message(self, event):
        post = event["data"]["post"]
        post = json.loads(post)
        sender = event["data"]["sender_name"]
        msg = post["message"]
        channel_id = post["channel_id"]

        LOG.info("Direct Message from: " + sender)
        LOG.info("Message: " + msg)
        # echo
        self.handle_direct_message(msg, sender, channel_id)

    def on_viewed(self, event):
        channel_id = event["data"]["channel_id"]
        user_id = event["broadcast"]["user_id"]

    def on_preferences_changed(self, event):
        preferences = json.loads(event["data"]["preferences"])
        for pref in preferences:
            user_id = pref["user_id"]
            category = pref["category"]
            value = pref["value"]
            LOG.debug(category + ":" + value)

    def on_post_deleted(self, event):
        msg = event["data"]["message"]

    def send_message(self, channel_id, message, file_paths=None):
        file_paths = file_paths or []
        file_ids = []
        # TODO not working
        #for f in file_paths:
        #    file_id = self.driver.files.upload_file(
        #        channel_id=channel_id,
        #        files = {'files': (f, open(f))}
        #    )['file_infos'][0]['id']
        #    file_ids.append(file_id)

        post = {'channel_id': channel_id, 'message': message}
        if len(file_ids):
            post["file_ids"] = file_ids

        self.driver.posts.create_post(options=post)

    # Relevant handlers
    def handle_direct_message(self, message, sender, channel_id):
        pass

    def handle_mention(self, message, sender, channel_id):
        pass
Example #8
0
class BlinkServer(object):
    def __init__(self):
        self.config = Config()
        self.not_read_channels = []
        self.blink = Blink()
        self.mattermost_driver = Driver({
            'url':
            self.config.get_string('MATTERMOST', 'url'),
            'login_id':
            self.config.get_string('MATTERMOST', 'login_id'),
            'password':
            self.config.get_string('MATTERMOST', 'password'),
            'verify':
            self.config.get_bool('MATTERMOST', 'verify'),
            'scheme':
            self.config.get_string('MATTERMOST', 'scheme'),
            'port':
            self.config.get_int('MATTERMOST', 'port'),
            'debug':
            self.config.get_bool('MATTERMOST', 'debug')
        })
        self.ignored_channels = self.config.get_string(
            'MATTERMOST', 'ignored_channels').split(',')
        self.server = None
        self.start()

    def start_wbe_server(self, thread_name):
        self.server = Server(self.blink)

    @asyncio.coroutine
    def socket_handler(self, message):
        print(message)
        self.handle_message(json.loads(message))

    def handle_message(self, json_message):
        if 'event' not in json_message:
            return

        if json_message['event'] == "posted":
            try:
                self.ignored_channels.index(
                    json_message['data']['channel_display_name'])
            except ValueError:
                self.parse_post(json.loads(json_message['data']['post']))
        elif json_message['event'] == "channel_viewed":
            self.mark_chanel_as_read(json_message['data']['channel_id'])

    def parse_post(self, post_data):
        try:
            self.not_read_channels.index(post_data['channel_id'])
        except ValueError:
            self.not_read_channels.append(post_data['channel_id'])
        self.blink.start_unread_blink()

    def mark_chanel_as_read(self, channel_id):
        try:
            index = self.not_read_channels.index(channel_id)
            del self.not_read_channels[index]
            if len(self.not_read_channels) == 0:
                self.blink.stop_unread_blinking()
        except ValueError:
            pass

    def start(self):
        _thread.start_new_thread(self.start_wbe_server, ('webserver', ))
        # todo: polaczenie z gitlabem - nowy MR
        self.mattermost_driver.login()
        while True:
            try:
                self.mattermost_driver.init_websocket(self.socket_handler)
            except Exception:
                print('Reconnecting to websocket')
                time.sleep(60)
class ChannelBot:
    """ A mattermost bot acting in a specified channel. """
    def __init__(self,
                 url,
                 token,
                 channel_name,
                 team_name,
                 help_text,
                 message_handler,
                 port=8065,
                 scheme='https',
                 debug=False):
        self.help_text = help_text
        self.message_handler = message_handler
        self.debug = debug

        self.driver = Driver({
            'url': url,
            'port': port,
            'token': token,
            'scheme': scheme,
            'debug': debug,
        })
        user_result = self.driver.login()
        self.username = user_result["username"]
        self.userid = user_result["id"]

        # get channel id for name
        res = self.driver.channels.get_channel_by_name_and_team_name(
            team_name, channel_name)
        self.channel_id = res['id']

    def start_listening(self):
        worker = threading.Thread(target=ChannelBot._start_listening_in_thread,
                                  args=(self, ))
        worker.daemon = True
        worker.start()

        print("Initialized bot.")

    def _start_listening_in_thread(self):
        # Setting event loop for thread
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)

        self.driver.init_websocket(self.websocket_handler)

    @asyncio.coroutine
    def websocket_handler(self, event_json):
        event = json.loads(event_json)

        if self.debug:
            print("websocket_handler:" + json.dumps(event, indent=4))

        if 'event' in event and event['event'] == 'posted':
            # mentions is automatically set in direct messages
            mentions = json.loads(event['data']['mentions']
                                  ) if 'mentions' in event['data'] else []

            post = json.loads(event['data']['post'])
            post_id = post['id']
            message = post['message']
            channel_id = post['channel_id']
            sender_id = post['user_id']

            if self.userid in mentions:  # and channel_id == self.channel_id:
                self._handle_bot_message(channel_id, post_id, sender_id,
                                         message)

    def _handle_bot_message(self, channel_id, post_id, sender_id, message):
        if re.match(r'(@' + self.username + ')?\s*help\s*', message):
            self._show_help(channel_id, post_id)
        else:
            res = self.driver.users.get_user(sender_id)
            sender_name = res['username']

            self.message_handler.handle_message(sender_id, sender_name,
                                                message, post_id, channel_id,
                                                self)

    def _show_help(self, channel_id, post_id):
        self.driver.posts.create_post({
            'channel_id': channel_id,
            'message': self.help_text,
            'root_id': post_id,
        })

    def send_message_to_channel(self, message):
        post_options = {
            'channel_id': self.channel_id,
            'message': message,
        }

        self.driver.posts.create_post(post_options)

    def answer_message_in_channel(self, channel_id, post_id, message):
        post_options = {
            'channel_id': channel_id,
            'root_id': post_id,
            'message': message,
        }

        self.driver.posts.create_post(post_options)
Example #10
0
class Raccooner:
    def __init__(self):
        with open('settings.yaml', "r") as file:
            self.parsed_data = yaml.safe_load(file)

        self.matt = Driver({
            'url':
            self.parsed_data["Raccooner"]["mattermost"]["general"]["url"],
            'token':
            self.parsed_data["Raccooner"]["mattermost"]["general"]
            ["access_token"],
            'port':
            self.parsed_data["Raccooner"]["mattermost"]["general"]["port"],
            'debug':
            False
        })
        self.matt.login()  # Automatic login
        self.raccoon_stats = {}  # Create an empty dictionary

    @asyncio.coroutine
    def event_handler(self, message):
        msg = json.loads(message)  # Parse the reported event

        try:
            event_type = msg["event"]

            # Try to parse the message as a post
            if event_type == "reaction_added" or event_type == "reaction_removed":
                reaction = json.loads(
                    msg["data"]["reaction"])  # Parse reaction data
                if reaction["emoji_name"] == "raccoon":
                    if event_type == "reaction_removed":
                        count = self.raccoon_stats[
                            reaction["post_id"]]["raccoon_count"]
                        self.raccoon_stats[
                            reaction["post_id"]]["raccoon_count"] = count - 1
                    else:
                        if not self.raccoon_stats[
                                reaction["post_id"]]["reaction_time"]:
                            self.raccoon_stats[reaction["post_id"]][
                                "reaction_time"] = reaction["create_at"]
                        count = self.raccoon_stats[
                            reaction["post_id"]]["raccoon_count"]
                        self.raccoon_stats[
                            reaction["post_id"]]["raccoon_count"] = count + 1

            else:
                post = json.loads(msg["data"]["post"])
                excluded_channel = bool(
                    msg["data"]["channel_name"] not in
                    self.parsed_data["Raccooner"]["mattermost"]["general"]
                    ["excluded_channels"])

                if event_type == "posted" and msg["data"]["channel_type"] == "O" and not post["hashtags"] and not \
                        post["root_id"] and excluded_channel:
                    rac_url = "https://" + MM_URL + "/" + self.matt.teams.get_team(msg["data"]["team_id"])["name"] + \
                              "/pl/" + post["id"]
                    requests.post(
                        self.parsed_data["Raccooner"]["mattermost"]
                        ["bot_specific"]["hook_url"],
                        json={
                            "channel":
                            self.parsed_data["Raccooner"]["mattermost"]
                            ["bot_specific"]["channel"],
                            "username":
                            self.parsed_data["Raccooner"]["mattermost"]
                            ["bot_specific"]["username"],
                            "icon_url":
                            self.parsed_data["Raccooner"]["mattermost"]
                            ["bot_specific"]["icon"],
                            "text":
                            "**#RaccoonsInAction**\n# Raccoon Squad Time for Action!!\n%s"
                            % rac_url
                        })
                    self.raccoon_stats.update({
                        post["id"]: {
                            "post_time": post["create_at"],
                            "reaction_time": 0,
                            "permalink": rac_url,
                            "deleted": False,
                            "raccoon_count": 0
                        }
                    })
                elif event_type == "post_deleted":
                    self.raccoon_stats[post["id"]]["deleted"] = True
        except KeyError:
            pass

    def report_statistics(self):
        core_string = ""
        stat_string = ""
        total_raccoons = 0
        counter = 1

        utc = time.gmtime()
        delta_utc_decimal = int(
            (self.parsed_data["Raccooner"]["mattermost"]["bot_specific"]
             ["utc_update_time"] - utc.tm_hour + utc.tm_min / 60.0 +
             utc.tm_sec / 3600.0) * 3600)
        if delta_utc_decimal < 0:
            delta_utc_decimal = 86400 - delta_utc_decimal  # Remove the elapsed seconds
        try:
            for post in self.raccoon_stats.keys():
                if counter == 1:
                    date = datetime.utcfromtimestamp(
                        self.raccoon_stats[post]["post_time"] /
                        1000).strftime('%d/%m/%Y')
                    stat_string = "**#RaccooningStats**\nRaccoon Statistics for {}\n\n".format(
                        date)
                if not self.raccoon_stats[post]["deleted"]:
                    if bool(self.raccoon_stats[post]["reaction_time"] != 0):
                        delta_t = (
                            self.raccoon_stats[post]["reaction_time"] -
                            self.raccoon_stats[post]["post_time"]) / 1000.0
                        if int(delta_t) < 60:
                            delta_t = "{} seconds".format(round(delta_t))
                        else:
                            delta_t = "{} minutes".format(
                                round(delta_t / 60.0, 2))
                    else:
                        delta_t = "_Missed_"
                    core_string = core_string + "[**Post {}**]({})\n".format(
                        counter, self.raccoon_stats[post]["permalink"])
                    core_string = core_string + "* Reaction time: {}\n" \
                                                "* Raccoons born: {}\n".format(delta_t,
                                                                               self.raccoon_stats[post]["raccoon_count"]
                                                                               )
                    total_raccoons = total_raccoons + self.raccoon_stats[post][
                        "raccoon_count"]
                counter = counter + 1

            # Send the statistics
            if core_string:
                stat_string = stat_string + "Total raccoons born: {}\n" \
                                            "Total raccoon families: {}\n\n".format(total_raccoons, counter) + \
                              core_string
                requests.post(self.parsed_data["Raccooner"]["mattermost"]
                              ["bot_specific"]["hook_url"],
                              json={
                                  "channel":
                                  self.parsed_data["Raccooner"]["mattermost"]
                                  ["bot_specific"]["channel"],
                                  "username":
                                  self.parsed_data["Raccooner"]["mattermost"]
                                  ["bot_specific"]["username"],
                                  "icon_url":
                                  self.parsed_data["Raccooner"]["mattermost"]
                                  ["bot_specific"]["icon"],
                                  "text":
                                  stat_string
                              })
        except KeyError:
            pass
        self.raccoon_stats = {}  # Reset the statistics dictionary
        threading.Timer(delta_utc_decimal, self.report_statistics).start()

    def run(self):
        self.report_statistics()  # Start the statistics thread
        self.matt.init_websocket(self.event_handler)
Example #11
0
class RtmBot(object):
    def __init__(self, config):
        '''
            Params:
                - config (dict):
                    - URL: your mattermost address
                    - PORT: your mattermost port
                    - SCHEME: protocol for access your mattermost
                        http or https
                    - USER_NAME: your mattermost user name
                    - USER_PASS: your mattermost user password
                    - BASE_PATH (optional: defaults to execution directory) RtmBot will
                        look in this directory for plugins.
                    - LOGFILE (optional: defaults to rtmbot.log) The filename for logs, will
                        be stored inside the BASE_PATH directory
                    - DEBUG (optional: defaults to False) with debug enabled, RtmBot will
                        break on errors
        '''
        # set the config object
        self.config = config

        self.url = config.get('URL', None)
        if not self.url:
            raise ValueError("Please add a URL to your config file.")
        self.token = config.get('TOKEN', None)
        if not self.token:
            raise ValueError("Please add a TOKEN to your config file.")
        self.scheme = config.get('SCHEME', None)
        if not self.scheme:
            self.scheme = "https"
        self.port = config.get('PORT', None)
        if not self.port:
            self.port = 443

        # get list of directories to search for loading plugins
        self.active_plugins = config.get('ACTIVE_PLUGINS', [])

        # set base directory for logs and plugin search
        working_directory = os.path.abspath(os.path.dirname(sys.argv[0]))

        self.directory = self.config.get('BASE_PATH', working_directory)
        if not self.directory.startswith('/'):
            path = os.path.join(os.getcwd(), self.directory)
            self.directory = os.path.abspath(path)

        self.debug = self.config.get('DEBUG', False)
        # establish logging
        log_file = config.get('LOGFILE', 'rtmbot.log')
        if self.debug:
            log_level = logging.DEBUG
        else:
            log_level = logging.INFO
        logging.basicConfig(filename=log_file,
                            level=log_level,
                            format='%(asctime)s %(message)s')
        logging.info('Initialized in: {}'.format(self.directory))

        # initialize stateful fields
        self.bot_plugins = []
        self.client = Driver({
            "url": self.url,
            "token": self.token,
            "scheme": self.scheme,
            "port": self.port
        })

    def _dbg(self, debug_string):
        if self.debug:
            logging.debug(debug_string)

    def connect(self):
        self.client.login()
        if 'DAEMON' in self.config:
            if self.config.get('DAEMON'):
                import daemon
                with daemon.DaemonContext():
                    self.client.init_websocket(self._start)
        self.client.init_websocket(self._start)

    def start(self):
        self.client.login()
        self.load_plugins()
        if 'DAEMON' in self.config:
            if self.config.get('DAEMON'):
                import daemon
                with daemon.DaemonContext():
                    self.client.init_websocket(self._start)
        self.client.init_websocket(self._start)

    @asyncio.coroutine
    def _start(self, m):
        message = parse_message(m)
        for plugin in self.bot_plugins:
            try:
                self._dbg("Registering jobs for {}".format(plugin.name))
                plugin.register_jobs()
            except NotImplementedError:  # this plugin doesn't register jobs
                self._dbg("No jobs registered for {}".format(plugin.name))
            except Exception as error:
                self._dbg("Error registering jobs for {} - {}".format(
                    plugin.name, error))
        self.input(message)
        self.crons()
        self.output()
        time.sleep(1)

    def input(self, data):
        if "event" in data and isinstance(data, dict):
            function_name = "process_" + data['event']
            self._dbg("got {}".format(function_name))
            for plugin in self.bot_plugins:
                plugin.do(function_name, data)

    def output(self):
        for plugin in self.bot_plugins:
            limiter = False
            for output in plugin.do_output():
                channel = output[0]
                message = output[1]
                if channel is not None and message is not None:
                    if limiter:
                        time.sleep(.1)
                        limiter = False
                    # channel.send_message(message)
                    plugin.client.api['posts'].create_post(options={
                        'channel_id': channel,
                        'message': message
                    })
                    limiter = True

    def crons(self):
        for plugin in self.bot_plugins:
            plugin.do_jobs()

    def load_plugins(self):
        ''' Given a set of plugin_path strings (directory names on the python path),
        load any classes with Plugin in the name from any files within those dirs.
        '''
        self._dbg("Loading plugins")
        if not self.active_plugins:
            self._dbg("No plugins specified in conf file")
            return  # nothing to load

        for plugin_path in self.active_plugins:
            self._dbg("Importing {}".format(plugin_path))

            if self.debug is True:
                # this makes the plugin fail with stack trace in debug mode
                cls = import_string(plugin_path)
            else:
                # otherwise we log the exception and carry on
                try:
                    cls = import_string(plugin_path)
                except ImportError as error:
                    logging.exception("Problem importing {} - {}".format(
                        plugin_path, error))

            plugin_config = self.config.get(cls.__name__, {})
            plugin = cls(client=self.client,
                         plugin_config=plugin_config)  # instatiate!
            self.bot_plugins.append(plugin)
            self._dbg("Plugin registered: {}".format(plugin))