Пример #1
0
def get_standup_members(server_config: Dict) -> List[str]:
    d = Driver(server_config)

    d.login()

    channel_id = d.channels.get_channel_by_name_and_team_name(
        'team_name', 'channel')['id']

    channel_members = d.channels.get_channel_members(channel_id)

    d.logout()

    return channel_members
Пример #2
0
def post_mattermost(msg, hashtag="", data=None):
    """
    Post a message to a mattermost server.

    Optionally pass a dictionary to print as a table.

    All messages prefixed with #API

    :param msg: String message
    :param hashtag: e.g. #Oncore
    :param data: Dictionary
    :return:
    """
    try:
        driver = Driver(
            dict(url=MATTERMOST_URL,
                 login_id=MATTERMOST_USER,
                 password=MATTERMOST_PW,
                 scheme='https',
                 verify=False,
                 port=443))

        if data is not None:
            msg = f'| #API  {hashtag} | {msg} |\n' \
                  f'| :--- | :--- |\n'
            for k in data:
                v = data[k]
                if isinstance(v, str) and v is not "":
                    msg += f'| {k} | {v} |\n'
                elif isinstance(v, dict):
                    for inner_key in v:
                        inner_val = v[inner_key]
                        if isinstance(inner_val, str) and inner_val is not "":
                            msg += f'| {inner_key} | {v[inner_key]} |\n'
        else:
            msg = f'#API {hashtag} {msg}'

        driver.login()
        channel_id = driver.channels.get_channel_by_name_and_team_name(
            MATTERMOST_TEAM, MATTERMOST_CHANNEL)['id']
        driver.posts.create_post(options={
            'channel_id': channel_id,
            'message': msg
        })
        driver.logout()
    except Exception as e:
        logging.error("Error while posting to mattermost")
        logging.error(e)
Пример #3
0
class Connection:
    """
	This class allows the user to connect to a Mattermost server and start the bot.
	
	There should be only one instance per program."""

    instance = None

    def __init__(self, settings):
        assert Connection.instance is None
        self.driver = Driver(settings)
        #self.driver.client.activate_verbose_logging()
        self.bot = Bot(convert_to_mmpy_bot(settings))
        Connection.instance = self

        # workaround for python versions < 3.7
        if sys.version_info.major < 3 or sys.version_info.minor < 7:
            api.load_driver_attributes()

    def __getattr__(self, name):
        return getattr(self.driver, name)

    def login(self):
        logger.info("login...")
        self.driver.login()
        api.me = api.User.by_id(self.driver.client.userid)
        logger.info(f"logged in as {api.me}")

    def start(self):
        # start bot at the end because the method will not return unless an exception occurs
        logger.info("start bot...")
        self.bot.run()
        logger.info("bot started")

    def stop(self):
        self.driver.logout()
        # TODO stop bot

    def __enter__(self):
        self.start()
        return self

    def __exit__(self, type, value, tb):
        self.stop()
Пример #4
0
class ConnectorMattermost(Connector):
    """A connector for Mattermost."""
    def __init__(self, config, opsdroid=None):
        """Create the connector."""
        super().__init__(config, opsdroid=opsdroid)
        _LOGGER.debug(_("Starting Mattermost connector"))
        self.name = config.get("name", "mattermost")
        self.token = config["token"]
        self.url = config["url"]
        self.team_name = config["team-name"]
        self.scheme = config.get("scheme", "https")
        self.port = config.get("port", 8065)
        self.verify = config.get("ssl-verify", True)
        self.timeout = config.get("connect-timeout", 30)
        self.request_timeout = None
        self.mfa_token = None
        self.debug = False
        self.listening = True
        self.bot_id = None

        self.mm_driver = Driver({
            "url": self.url,
            "token": self.token,
            "scheme": self.scheme,
            "port": self.port,
            "verify": self.verify,
            "timeout": self.timeout,
            "request_timeout": self.request_timeout,
            "mfa_token": self.mfa_token,
            "debug": self.debug,
        })

    async def connect(self):
        """Connect to the chat service."""
        _LOGGER.info(_("Connecting to Mattermost"))

        login_response = self.mm_driver.login()

        _LOGGER.info(login_response)

        if "id" in login_response:
            self.bot_id = login_response["id"]
        if "username" in login_response:
            self.bot_name = login_response["username"]
            _LOGGER.info(_("Connected as %s"), self.bot_name)

        self.mm_driver.websocket = Websocket(self.mm_driver.options,
                                             self.mm_driver.client.token)

        _LOGGER.info(_("Connected successfully"))

    async def disconnect(self):
        """Disconnect from Mattermost."""
        self.listening = False
        self.mm_driver.logout()

    async def listen(self):
        """Listen for and parse new messages."""
        await self.mm_driver.websocket.connect(self.process_message)

    async def process_message(self, raw_message):
        """Process a raw message and pass it to the parser."""
        _LOGGER.info(raw_message)

        message = json.loads(raw_message)

        if "event" in message and message["event"] == "posted":
            data = message["data"]
            post = json.loads(data["post"])
            # if connected to Mattermost, don't parse our own messages
            # (https://github.com/opsdroid/opsdroid/issues/1775)
            if self.bot_id is None or self.bot_id != post["user_id"]:
                await self.opsdroid.parse(
                    Message(
                        text=post["message"],
                        user=data["sender_name"],
                        target=data["channel_name"],
                        connector=self,
                        raw_event=message,
                    ))

    @register_event(Message)
    async def send_message(self, message):
        """Respond with a message."""
        _LOGGER.debug(_("Responding with: '%s' in room  %s"), message.text,
                      message.target)
        channel_id = self.mm_driver.channels.get_channel_by_name_and_team_name(
            self.team_name, message.target)["id"]
        self.mm_driver.posts.create_post(options={
            "channel_id": channel_id,
            "message": message.text
        })
Пример #5
0
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
Пример #6
0
myUserID = myuser['id']
myTeam = foo.teams.get_user_teams(myUserID)
myTeamID = myTeam[0]['id']
myChannelsList = foo.channels.get_channels_for_user(myUserID, myTeamID)
for myChannel in myChannelsList:  #parcours de mes chans
    myChannelID = myChannel['id']
    page = 0
    myChannelMessageLength = 1
    # tant qu'il reste des messages dans le set de msg de la page
    while (myChannelMessageLength != 0):
        # provient de l'API V4, et permet d'accéder
        # à des options supplémentaires
        options = {'page': page, 'per_page': PER_PAGE}

        myChannelsMessageList = foo.posts.get_posts_for_channel(
            myChannelID, options)
        myChannelMessage = myChannelsMessageList['order']
        myChannelMessageLength = len(myChannelMessage)
        print("taille :" + str(myChannelMessageLength) + " - page :" +
              str(page))
        # si on est dans un set de msg avec une seule page
        if (myChannelMessageLength < PER_PAGE):
            delete_posts(foo, myChannelMessage)
            break
# s'il y a plusieurs pages, on boucle
        else:
            delete_posts(foo, myChannelMessage)
            page = page + 1

foo.logout()
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
Пример #8
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")
Пример #9
0
class MycroftChat(MycroftSkill):
    def __init__(self):
        MycroftSkill.__init__(self)

    def initialize(self):
        self.register_entity_file('channel.entity')
        self.register_entity_file('service.entity')

        self.state = "idle"
        self.mm = None
        # login data
        self.username = self.settings.get("username", "")
        self.token = self.settings.get("token", "")
        self.login_id = self.settings.get("login_id", "")
        self.password = self.settings.get("password", "")
        # monitoring
        self.ttl = self.settings.get("ttl", 10) * 60
        self.notify_on_updates = self.settings.get("notify_on_updates", False)
        LOG.debug("username: {}".format(self.username))

        mm_driver_config = {
            'url': self.settings.get("url", "chat.mycroft.ai"),
            'scheme': self.settings.get("scheme", "https"),
            'port': self.settings.get("port", 443),
            'verify': self.settings.get("verify", True)
        }

        if self.settings.get("url", "chat.mycroft.ai") == "chat.mycroft.ai":
            self.service_name = "Mycroft Chat"
        else:
            self.service_name = "Mattermost"

        if self.token:
            mm_driver_config['token'] = self.token
        elif self.login_id and self.password:
            mm_driver_config['login_id'] = self.login_id
            mm_driver_config['password'] = self.password

        if self.username:
            self.mm = Driver(mm_driver_config)
            try:
                self.mm.login()
                self.userid = \
                    self.mm.users.get_user_by_username(self.username)['id']
                # TODO check if user is member of several teams
                self.teamid = self.mm.teams.get_team_members_for_user(
                    self.userid)[0]['team_id']
                LOG.debug("userid: {} teamid: {}".format(
                    self.userid, self.teamid))
            except (mme.ResourceNotFound, mme.HTTPError,
                    mme.NoAccessTokenProvided, mme.NotEnoughPermissions,
                    mme.InvalidOrMissingParameters) as e:
                LOG.debug("Exception: {}".format(e))
                self.mm = None
                self.speak_dialog("mattermost.error", {'exception': e})
            if self.mm:
                # info on all subscribed public channels as returned by MM
                self.channel_subscriptions = None
                self.channel_subs_ts = 0
                # basic info of channels
                # channel_id, display_name, (unread) msg_count, mentions
                self.channel_info = None
                self.channel_info_ts = 0
                self.usercache = {}
                self.prev_unread = 0
                self.prev_mentions = 0
                if self.settings.get('monitoring', False):
                    self.monitoring = True
                    self.schedule_repeating_event(
                        self._mattermost_monitoring_handler, None, self.ttl,
                        'Mattermost')
                else:
                    self.monitoring = False

        # Check and then monitor for credential changes
        self.settings_change_callback = self.on_websettings_changed

    def on_websettings_changed(self):
        LOG.debug("websettings changed!")
        if self.mm:
            self.mm.logout()
        if self.monitoring:
            self.cancel_scheduled_event('Mattermost')
        self.initialize()

    @intent_file_handler('read.unread.channel.intent')
    def read_channel_messages(self, message):
        if not self.mm:
            self.speak_dialog("skill.not.initialized")
            return
        elif self.state != "idle":
            return
        else:
            self.state = "speaking"
        channel_name = message.data.get('channel')
        LOG.debug("data {}".format(message.data))
        if not channel_name:
            self.speak_dialog('channel.unknown', data={'channel': ''})
            self.state = "idle"
            return
        # do some fuzzy matching on channel
        best_chan = {}
        best_score = 66  # minimum score required
        for chan in self._get_channel_info():
            score = fuzz.ratio(channel_name.lower(),
                               chan['display_name'].lower())
            # LOG.debug("{}->{}".format(unr['display_name'], score))
            if score > best_score:
                best_chan = chan
                best_score = score
        LOG.debug("{} -> {}".format(best_chan, best_score))
        if not best_chan:
            self.speak_dialog('channel.unknown',
                              data={'channel': channel_name})
        elif best_chan['msg_count'] == 0:
            self.speak_dialog('no.unread.channel.messages',
                              data={'channel': channel_name})
        else:
            self._read_unread_channel(best_chan)
        self.state = "idle"

    @intent_file_handler('start.monitoring.intent')
    def start_monitoring_mattermost(self, message):
        if not self.mm:
            self.speak_dialog("skill.not.initialized")
            return
        LOG.debug("start monitoring with ttl {} secs".format(self.ttl))
        self.schedule_repeating_event(self._mattermost_monitoring_handler,
                                      None, self.ttl, 'Mattermost')
        self.monitoring = True
        self.settings['monitoring'] = True
        self.settings.store(force=True)
        self.speak_dialog('monitoring.active', {'service': self.service_name})

    @intent_file_handler('end.monitoring.intent')
    def end_monitoring_mattermost(self, message):
        LOG.debug("end monitoring")
        self.cancel_scheduled_event('Mattermost')
        self.monitoring = False
        self.settings['monitoring'] = False
        self.settings.store(force=True)
        self.speak_dialog('monitoring.inactive',
                          data={'service': self.service_name})

    @intent_file_handler('read.unread.messages.intent')
    def read_unread_messages(self, message):
        if not self.mm:
            self.speak_dialog("skill.not.initialized")
            return
        elif self.state != "idle":
            return
        else:
            self.state = "speaking"
        for chan in self._get_channel_info():
            if self.state == "stopped":
                break
            self._read_unread_channel(chan)
        self.state = "idle"

    @intent_file_handler('list.unread.channels.intent')
    def list_unread_channels(self, message):
        if not self.mm:
            self.speak_dialog("skill.not.initialized")
            return
        elif self.state != "idle":
            return
        else:
            self.state = "speaking"

        count = 0
        for ch in self._get_channel_info():
            responses = []
            if (ch['msg_count'] and ch['mention_count']):
                responses.append(
                    self.dialog_renderer.render(
                        "channel.unread.and.mentioned",
                        {
                            'msg_count':
                            ch['msg_count'],  # TODO use nice_number
                            'display_name': ch['display_name'],
                            'mention_count': ch['mention_count']
                        }))
            elif ch['msg_count']:
                responses.append(
                    self.dialog_renderer.render(
                        "channel.unread",
                        {
                            'msg_count':
                            ch['msg_count'],  # TODO use nice_number
                            'display_name': ch['display_name']
                        }))
            elif ch['mention_count']:
                responses.append(
                    self.dialog_renderer.render(
                        "channel.mentioned", {
                            'mention_count': ch['mention_count'],
                            'display_name': ch['display_name']
                        }))

            if responses:
                count += 1
                for res in responses:
                    if self.state == "stopped":
                        break
                    self.speak(res, wait=True)

        if count == 0:
            # no unread/mentions
            self.speak_dialog('no.unread.messages',
                              data={'service': self.service_name})
        self.state = "idle"

    @intent_file_handler('unread.messages.intent')
    def check_unread_messages_and_mentions(self, message):
        if not self.mm:
            self.speak_dialog("skill.not.initialized")
            return
        elif self.state != "idle":
            return
        else:
            self.state = "speaking"
        unreadmsg = self._get_unread_msg_count()
        mentions = self._get_mention_count()
        response = self.__render_unread_dialog(unreadmsg, mentions,
                                               self.service_name)
        self.enclosure.deactivate_mouth_events()
        self.enclosure.mouth_text('unread: {} mentions: {}'.format(
            unreadmsg, mentions))
        self.speak(response, wait=True)
        self.enclosure.activate_mouth_events()
        self.enclosure.mouth_reset()
        self.state = "idle"

    def stop(self):
        # this requires mycroft-stop skill installed
        if self.state == "speaking":
            LOG.debug("stopping")
            self.state = "stopped"
            return True
        return False

    def _read_unread_channel(self, chan):
        if self.state == "stopped":
            return
        msg_count = chan['msg_count']
        if msg_count:
            channel_message = self.dialog_renderer.render(
                "messages.for.channel", {'display_name': chan['display_name']})
            LOG.debug(channel_message)
            self.speak(channel_message)
            pfc = self.mm.posts.get_posts_for_channel(chan['channel_id'])
            order = pfc['order']
            # in case returned posts are less than number of unread
            # avoid 'index out of bounds'
            msg_count = msg_count if msg_count < len(order) else len(order)
            prev_date = ""
            for i in range(0, msg_count):
                if self.state == "stopped":
                    break
                # order starts with newest to oldest,
                # start to read the oldest of the unread
                post = pfc['posts'][order[msg_count - i - 1]]
                create_at = ""
                # nice_date does only support en-us yet - bummer!
                # MM timestamps are in millisecs, python in secs
                msg_date = nice_date(datetime.fromtimestamp(post['create_at'] /
                                                            1000),
                                     self.lang,
                                     now=datetime.now())
                if prev_date != msg_date:
                    create_at = msg_date + " "
                    prev_date = msg_date
                msg_time = nice_time(
                    datetime.fromtimestamp(post['create_at'] / 1000),
                    self.lang)
                create_at += msg_time
                msg = self.dialog_renderer.render(
                    "message", {
                        'user_name': self._get_user_name(post['user_id']),
                        'create_at': create_at,
                        'message': post['message']
                    })
                LOG.debug(msg)
                self.speak(msg, wait=True)
                time.sleep(.3)
            # mark channel as read
            self.mm.channels.view_channel(self.userid,
                                          {'channel_id': chan['channel_id']})
            # TODO clarify when to reset prev_unread/prev_mentions
            self.prev_unread = 0
            self.prev_mentions = 0

    def _get_unread_msg_count(self):
        unreadmsg = 0
        for chan in self._get_channel_info():
            if (chan['msg_count']):
                unreadmsg += chan['msg_count']
        return unreadmsg

    def _get_mention_count(self):
        mentions = 0
        for chan in self._get_channel_info():
            if (chan['mention_count']):
                mentions += chan['mention_count']
        return mentions

    def __render_unread_dialog(self, unreadmsg, mentions, service=None):
        if not service:
            service = self.service_name
        LOG.debug("unread {} mentions {}".format(unreadmsg, mentions))
        response = ""
        if unreadmsg:
            response += self.dialog_renderer.render('unread.messages', {
                'unreadmsg': unreadmsg,
                'service': service
            })
            response += " "
        if mentions:
            response += self.dialog_renderer.render('mentioned', {
                'mentions': mentions,
                'service': service
            })
        if not response:
            response = self.dialog_renderer.render('no.unread.messages',
                                                   {'service': service})
        return response

    def _mattermost_monitoring_handler(self):
        LOG.debug("mm monitoring handler")
        # do not update when last run was less than 30secs before
        if (time.time() - self.channel_subs_ts) > 30:
            self._get_channel_subscriptions()
        if (time.time() - self.channel_info_ts) > 30:
            self._get_channel_info()

        LOG.debug("check for notifications")
        unreadmsg = self._get_unread_msg_count()
        mentions = self._get_mention_count()
        # TODO clarify when to reset prev_unread/prev_mentions
        if unreadmsg != self.prev_unread:
            self.prev_unread = unreadmsg
        else:
            unreadmsg = 0
        if mentions != self.prev_mentions:
            self.prev_mentions = mentions
        else:
            mentions = 0

        LOG.debug("unread: {} mentions: {}".format(unreadmsg, mentions))
        if unreadmsg or mentions:
            # display unread and mentions on Mark-1/2 display
            display_text = self.dialog_renderer.render('display.message.count',
                                                       {
                                                           'unread': unreadmsg,
                                                           'mentions': mentions
                                                       })
            if self.config_core.get("enclosure").get("platform", "") == \
               'mycroft_mark_1':
                self.enclosure.deactivate_mouth_events()
                self.enclosure.mouth_text(display_text)
                # clear display after 30 seconds
                self.schedule_event(self._mattermost_display_handler, 30, None,
                                    'mmdisplay')
            elif self.config_core.get("enclosure").get("platform", "") == \
                'mycroft_mark_2':
                self.gui.show_text(display_text, self.service_name)

            if self.notify_on_updates:
                self.speak(self.__render_unread_dialog(unreadmsg, mentions))

    def _mattermost_display_handler(self):
        # clear display and reset display handler
        self.enclosure.activate_mouth_events()
        self.enclosure.mouth_reset()
        self.cancel_scheduled_event('mmdisplay')

    def _get_channel_subscriptions(self):
        # update channel subscriptions only every second ttl interval
        if (time.time() - self.channel_subs_ts) > (self.ttl * 2):
            LOG.debug("get channel subscriptions...")
            self.channel_subscriptions = \
                self.mm.channels.get_channels_for_user(self.userid,
                                                       self.teamid)
            self.channel_subs_ts = time.time()
            LOG.debug("...done")
            # LOG.debug(self.channel_subscriptions)
        return self.channel_subscriptions

    def _get_channel_info(self):
        if (time.time() - self.channel_info_ts) > self.ttl:
            LOG.debug("get channel info...")
            info = []
            for chan in self._get_channel_subscriptions():
                if chan['team_id'] != self.teamid:
                    continue
                unr = self.mm.channels.get_unread_messages(
                    self.userid, chan['id'])
                info.append({
                    'display_name': chan['display_name'],
                    'msg_count': unr['msg_count'],
                    'mention_count': unr['mention_count'],
                    'channel_id': chan['id']
                })
            self.channel_info = info
            self.channel_info_ts = time.time()
            LOG.debug("...done")
            # LOG.debug(self.channel_info)
        return self.channel_info

    def _get_user_name(self, userid):
        if not (userid in self.usercache):
            user = self.mm.users.get_user(userid)
            self.usercache[userid] = user['username']
            # LOG.debug("usercache {}->{}".format(userid, user['username']))
        return self.usercache[userid]
Пример #10
0
class Bot:
    bot = None

    host = None
    port = None
    https = None
    username = None
    passwd = None

    driver = None

    bot_info = None

    def __init__(self, host, username, passwd, port=443, https=True):
        Bot.bot = self
        self.host = host
        self.port = port
        self.https = https
        self.username = username
        self.passwd = passwd

        ClientLogger.log(LoggingMode.INFO, "Creating mettermost driver... ")
        self.create_driver()
        ClientLogger.log(LoggingMode.INFO, "Logging in... ")
        self.login()
        ClientLogger.log(LoggingMode.INFO, 'Successfully logged in!')
        from connection.WebSocket import WebSocket
        ClientLogger.log(LoggingMode.INFO, "Setting up web socket...")
        websocket = WebSocket()
        ClientLogger.log(LoggingMode.INFO, "Listening for user commands...")
        asyncio.get_event_loop().run_until_complete(websocket.listen())

    @classmethod
    def get_bot(cls):
        return Bot.bot

    def create_driver(self):
        self.driver = Driver({
            'url': self.host,
            'login_id': self.username,
            'password': self.passwd,
            'scheme': 'https' if self.https else 'http',
            'port': self.port
        })

    def login(self):
        self.driver.login()

        self.bot_info = self.driver.users.get_user_by_username(
            Config.get_user())

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

    def kill(self):
        self.logout()
        sys.exit()

    def get_bot_id(self):
        return self.get_id_from_json_data(self.bot_info)

    def get_bot_channel_id(self):
        return self.get_id_from_json_data(
            self.driver.channels.get_channel_by_name_and_team_name(
                Config.get_team(), Config.get_channel()))

    def create_message(self, text):
        self.driver.posts.create_post({
            'user_id': self.get_bot_id(),
            'channel_id': self.get_bot_channel_id(),
            'message': text
        })

    def get_all_unread_messages(self):
        return self.driver.channels.get_unread_messages(
            self.get_bot_id(), self.get_bot_channel_id())

    def get_auth_token(self):
        return self.driver.users.get_au

    @staticmethod
    def get_team_name():
        return Config.get_team()

    @staticmethod
    def get_id_from_json_data(data):
        json.loads(json.dumps(data))
        return data['id']
Пример #11
0
def main():
    module = AnsibleModule(
        argument_spec={
            "username": {
                "required": True,
                "type": "str"
            },
            "password": {
                "required": True,
                "type": "str",
                "no_log": True
            },
            "token_description": {
                "required": False,
                "type": "str",
                "default": ""
            },
            "url": {
                "required": False,
                "type": "str",
                "default": "localhost"
            },
            "scheme": {
                "required": False,
                "type": "str",
                "default": "http"
            },
            "port": {
                "required": False,
                "type": "int",
                "default": 8065
            },
            "path": {
                "required": True,
                "type": "str"
            },
        })

    token = None
    try:
        token = open(module.params["path"]).read().strip()
    except FileNotFoundError:
        pass

    if token is not None:
        test_driver = Driver({
            "token": token,
            "url": module.params['url'],
            "port": module.params['port'],
            "scheme": module.params['scheme'],
        })

        try:
            test_driver.login()
            userid = test_driver.client.userid
            test_driver.logout()
            module.exit_json(changed=False,
                             meta={
                                 "token": token,
                                 "userid": userid
                             })
        except HTTPError:
            pass

    driver = Driver({
        "login_id": module.params['username'],
        "password": module.params['password'],
        "url": module.params['url'],
        "port": module.params['port'],
        "scheme": module.params['scheme'],
    })
    driver.login()
    userid = driver.client.userid
    token = driver.users.create_user_access_token(
        driver.client.userid,
        {"description": module.params["token_description"]})
    driver.logout()
    open(module.params["path"], "w").write(token["token"])
    module.exit_json(changed=True,
                     meta={
                         "token": token["token"],
                         "userid": userid
                     })
Пример #12
0
class EmojiContext:
    """
    Custom Click Context class
    to store global settings and manage authentication
    """
    def __init__(self):
        self.output = "table"
        self.mattermost = None

    @contextmanager
    def authenticate(self, url, token, login_id, password, mfa_token,
                     insecure):
        """Authenticate against the Mattermost server"""

        if token and (login_id or password or mfa_token):
            click.echo(
                "Warning: Token specified along"
                " with Login-ID/Password/MFA-token."
                "Only Token will be used.",
                err=True,
            )

        settings = {
            "scheme": url.scheme,
            "url": url.hostname,
            "basepath": getattr(url, "path", ""),
            "verify": not insecure,
            "login_id": login_id,
            "password": password,
            "token": token,
            "mfa_token": mfa_token,
        }

        if url.port:
            settings["port"] = url.port
        elif url.scheme == "https":
            settings["port"] = 443
        else:
            settings["port"] = 80

        self.mattermost = Mattermost(settings)
        try:
            try:
                yield self.mattermost.login()
            finally:
                # Logout is unnecessary if token was used
                if token is None:
                    self.mattermost.logout()
        except (requests.exceptions.ConnectionError, MethodNotAllowed):
            raise click.ClickException(
                "Unable to reach Mattermost API at {}".format(
                    self.mattermost.client.url))
        except requests.exceptions.HTTPError as e:
            raise click.ClickException(e.args if e.args != () else repr(e))

    def print_dict(self, data):
        """Print dataset generated by a command to the standard output"""
        dataset = Dataset()
        dataset.dict = data
        if dataset.height:
            if self.output == "table":
                click.echo(tabulate(dataset.dict, headers="keys"))
            else:
                # we will probably implement JSON output only in the long run
                # and get rid of the `tablib` dependency
                click.echo(dataset.export(self.output))

        click.echo(
            "\n({} emoji{})".format(dataset.height,
                                    "" if dataset.height == 1 else "s"),
            err=True,
        )
Пример #13
0
class Mattermost:
    """Class that is responsible for connecting to mattermost bot."""
    def __init__(self):
        connection_parameters = {
            'url': settings.MATTERMOST_SERVER,
            'port': settings.MATTERMOST_PORT,
            'username': settings.MATTERMOST_LOGIN_ID,
            'token': settings.MATTERMOST_ACCESS_TOKEN
        }
        self.mattermost_connection = Driver(connection_parameters)

    def __enter__(self):
        try:
            self.mattermost_connection.login()
        except HTTPError as e:
            # Log exception to Sentry if call fails, but do not break the server.
            if not settings.DEBUG:
                # noinspection PyUnresolvedReferences
                from sentry_sdk import capture_exception
                capture_exception(e)
        return self

    def __exit__(self, *args):
        self.mattermost_connection.logout()

    def get_usernames_from_emails(self, emails: list[str]) -> list[str]:
        """
        Function that helps to get mattermost usernames from emails.

        :param emails: Emails of the users.
        :return: Mattermost usernames of the users.
        """
        usernames = []
        for email in emails:
            try:
                username = self.mattermost_connection.users.get_user_by_email(
                    email).get('username')
                usernames.append(username)
            except HTTPError as e:
                # Log exception to Sentry if call fails, but do not break the server.
                if not settings.DEBUG:
                    # noinspection PyUnresolvedReferences
                    from sentry_sdk import capture_exception
                    capture_exception(e)

        return usernames

    def post_message_to_channel(self, channel_name: str, message: str) -> None:
        """
        Post the message to the channel using the channel name.

        :param channel_name: Name of the channel to post message.
        :param message: Message to be posted.
        """
        try:
            channels = self.mattermost_connection.channels
            channel_id = channels.get_channel_by_name_and_team_name(
                settings.MATTERMOST_TEAM_NAME, channel_name).get('id')
            self.mattermost_connection.posts.create_post({
                'channel_id': channel_id,
                'message': message
            })
        except HTTPError as e:
            # Log exception to Sentry if call fails, but do not break the server.
            if not settings.DEBUG:
                # noinspection PyUnresolvedReferences
                from sentry_sdk import capture_exception
                capture_exception(e)