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
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)
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")
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
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)
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)
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))