diff options
| -rw-r--r-- | .gitignore | 3 | ||||
| -rw-r--r-- | README.md | 69 | ||||
| -rw-r--r-- | main.py | 86 | ||||
| -rw-r--r-- | msgraph.py | 292 | ||||
| -rw-r--r-- | requirements.txt | 3 | ||||
| -rw-r--r-- | slack_parser.py | 169 | ||||
| -rw-r--r-- | test.py | 96 |
7 files changed, 718 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..42db6d2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +resources/ +params*.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..6a70d6a --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# SlackToTeams + +## What it does +Migrate Slack messages into Teams. The information that gets retained for each message: +- the (user) sender of the message. +- the timestamp of the message. +- the content of the message. + - if the content includes `code blocks`, those get special formatting. +- the (user) other tagged users in the message. + +### What it can't do +- Replies, on either Slack or Teams side. +- Profile pictures (avatars). +- emojis, reactions to messages. +- uploaded images. +- every message MUST be sent as the **same** "user", i.e. on Teams it will show up as the same 0365 user sending each message. + - we work around this issue by manually adding the true sender (slack sender) into the body of the message as a "From: " field. + +This is a clunky workaround, but at the moment the send ChatMessage API endpoint is not supported by "Applications" (its in Beta afterall). Only individual users can make this request. +For this reason, if we want to make a writeMessage request, we are limited to "on-behalf-of" user **flow**, where authentication is made for 1 user who has special permissions to be able to make this request. The Application is then able to make all requests "on-behalf-of" this user. + +I have even tried to make ChatMessage requests with a different user "path" - using `/users/<userID>/chat/<chatID>` path, but that is also un-authorized. MS says that this endpoint is to be used strictly by the api-caller. +In order words, to send a message as another user, you have to authenticate the app on-behalf-of that user. This is obviously impractical if you have hundreds of users, so its best to wait for MS to get this endpoint out of Beta, and allow the normal "App" flow. + +## The Setup +- Code is built up from Azure Sample - https://github.com/Azure-Samples/ms-identity-python-daemon/tree/master/1-Call-MsGraph-WithSecret. +- I've created objects representing the overall structure of the app. We have two main entities: + 1. SlackParser: handles file io, message parsing logic involved with the Slack export. + 2. MSGraphApi: handles auth with MSGrpah, and provides a few MSGraph Api endpoints to query or write data. +- `test.py` has rudimentary tests for parsing logic functions, I suggest these are run anytime code is updated. +- A user with special permssions setup on the O365 side. Specifically it needs: + - User.Read, User.Read.All, User.ReadWrite, User.ReadWrite.All + - Group.Read.All, Group.ReadWrite.All + +## Dependencies +- You must have your own `params.json` file, and a `resources` dir containing: + 1. `users.json` file. + 2. the slack channel dir from a slack export `resources/devops/` that contains all the messages by day as json files. +- `params.json` file (explained below). + +## Running +1. `pip3 install -r requirements.txt` +2. `python3 main.py params.json` to run the app + - `python3 test.py` to run the tests, though they get run at the start of main.py itself. + +## params.json explaination +The configuration file would look like this (sans those // comments): +``` +{ + "authority": "https://login.microsoftonline.com/Enter_the_Tenant_Name_Here", + "client_id": "your_client_id", + "scope": ["https://graph.microsoft.com/.default"], + // For more information about scopes for an app, refer: + // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow#second-case-access-token-request-with-a-certificate" + + "secret": "The secret generated by AAD during your confidential app registration", + // For information about generating client secret, refer: + // https://github.com/AzureAD/microsoft-authentication-library-for-python/wiki/Client-Credentials#registering-client-secrets-using-the-application-registration-portal + + "username": "https://graph.microsoft.com/v1.0/users", + // this is required for obo flow + "password": "https://graph.microsoft.com/v1.0/users", + // this is required for obo flow + "teamname": "Engineering", + // the exact name of the team containing your channel", + "channelname": "Acuity Changelog" + // the exact name of the channgel you want to write to. +} +``` @@ -0,0 +1,86 @@ +import sys # read config from args +import json +import time +import logging +import unittest +import test + + +from slack_parser import SlackParser +from msgraph import MSGraphApi + +# Optional logging +logging.basicConfig(level=logging.INFO) +LIMIT_THROUGHPUT = False + +if __name__ == '__main__': + + # run the tests first + suite = unittest.TestLoader().loadTestsFromModule(test) + result = unittest.TextTestRunner(verbosity=0).run(suite) + if result.failures != []: + logging.error("1 or more tests failed. Exiting with code 1.") + sys.exit(1) + + config = json.load(open(sys.argv[1])) + + #msapi = MSGraphApi(config).getAuth() # "app flow" + msapi = MSGraphApi(config, flow="obo").getAuth() + + if msapi is not None: + # connection acquired, begin doing stuff + + # Example Calls + teamId = msapi.getTeamByName(config["teamname"]) + channelId = msapi.getChatByNameAndTeamId(teamId, config["channelname"]) + + #graph_data = msapi.getUsers().json() + #graph_data = msapi.getChatByTeamAndId(teamId, channelId).json() + #graph_data = msapi.writeChatMessageByTeamAndId( + # teamId=teamId, + # channelId=channelId, + # payload={ "body" : { "content" : "hello world" } } + #).json() + + #print("Graph API call result: ") + #print(json.dumps(graph_data, indent=2)) + #sys.exit(0) + + sp = SlackParser("resources/channels/devops", "resources/users.json", ["text", "ts", "name"]).buildUsersLibrary() + if sp.usersLib is {}: + logging.error("Failed to read users. Shutting down with code 1.") + sys.exit(1) + + stats_mssg_counter = 1 + logging.info("LIMIT_THROUGHPUT: " + str(LIMIT_THROUGHPUT)) + + # load 1 json file at a time in memory + for slackDay in sp.getSortedMessageFiles(): + # parse and write 1 json message at a time + for message in sp.getMessages(slackDay): + reduced_message = sp.getKeyValues(message) + if reduced_message is not None: + teams_mssg = msapi.buildTeamsMessage(reduced_message) + graph_data = msapi.writeChatMessageByTeamAndId( + teamId=teamId, + channelId=channelId, + payload=teams_mssg + ) + if graph_data.status_code != 201: + logging.error("failed to write to Teams") + logging.error(teams_mssg) + logging.error(graph_data.json()) + else: + logging.info("success") + stats_mssg_counter += 1 + + # control the throughput + if LIMIT_THROUGHPUT: + if stats_mssg_counter % 10 == 0: + time.sleep(10) + else: + time.sleep(1) + + else: + logging.error("Failed to Authenticate with Ms Graph. Shutting down with code 1.") + sys.exit(1) diff --git a/msgraph.py b/msgraph.py new file mode 100644 index 0000000..0ed77bc --- /dev/null +++ b/msgraph.py @@ -0,0 +1,292 @@ +import logging +import json +import requests +import msal + +class MSGraphApi(object): + """Docstring for MSGraphApi. """ + + def __init__(self, config=None, flow="app"): + + # TODO: make these private vars + self.config = {} + self.app = None + self.access_token = None + + if config is not None: + self.config = config + else: + return + + self.flow = flow + if self.flow == "obo": + app = msal.PublicClientApplication( + self.config["client_id"], + authority=self.config["authority"] + ) + else: + # no flow was given, assume app flow + app = msal.ConfidentialClientApplication( + self.config["client_id"], + authority=self.config["authority"], + client_credential=self.config["secret"], + ) + self.app = app + + def getAuth(self): + """Docstring for getAuth. + Get the MS Authorization to use the Graph API + + :returns: on success, the instance of this object - self + on failure, None + + """ + # The pattern to acquire a token looks like this. + result = None + # Firstly, looks up a token from cache + if self.flow == "obo": + result = self.app.acquire_token_silent( + self.config["scope"], + account=self.app.get_accounts(self.config["username"]) + ) + else: + # Since we are looking for token for the current app, NOT for an end user, + # notice we give account parameter as None. + result = self.app.acquire_token_silent(self.config["scope"], account=None) + + if not result: + logging.info("No suitable token exists in cache. Let's get a new one from AAD.") + + if self.flow == "obo": + result = self.app.acquire_token_by_username_password( + self.config["username"], + self.config["password"], + scopes=self.config["scope"] + ) + else: + result = self.app.acquire_token_for_client(scopes=self.config["scope"]) + + # auth succeeded + if "access_token" in result: + self.setAccessToken(result["access_token"]) + return self + else: + logging.error(result.get("error")) + logging.error(result.get("error_description")) + logging.error(result.get("correlation_id")) # may need this when reporting a bug + return None + + def setAccessToken(self, tok): + """Docstring for setAccessToken. + A setter method to set access_token for the object instance. + + :tok: the ms access token + + """ + self.access_token = tok + + def getUsers(self): + """Docstring for getUsers. + Get all the users in the ms org. + + :returns: respose object containing ms response + + """ + endpoint = "https://graph.microsoft.com/v1.0/users" + headers = { "Authorization": "Bearer " + self.access_token } + + graph_data = requests.get( + endpoint, + headers=headers + ) + return graph_data + + def getMyTeams(self): + """Docstring for getUsers. + Get all the teams for this user. + + :returns: respose object containing ms response + + """ + endpoint = "https://graph.microsoft.com/beta/me/joinedTeams" + headers = { "Authorization": "Bearer " + self.access_token } + + graph_data = requests.get( + endpoint, + headers=headers + ) + return graph_data + + + def getChatsByTeam(self, teamId): + """Docstring for getChatsByTeam. + Get all chats in given team id. + + :teamId: the team ID for the team to get the chats for + :returns: respose object containing ms response + + """ + endpoint = "https://graph.microsoft.com/beta/teams/%s/channels/" % (teamId) + headers = { "Authorization": "Bearer " + self.access_token } + + graph_data = requests.get( + endpoint, + headers=headers + ) + return graph_data + + def getChatByTeamAndId(self, teamId, channelId): + """Docstring for getChatByTeamAndId. + Get a specific channel given team and channel ids. + + :teamId: the team ID for the team to get the chats for + :channelId: the channel ID for the channel to get + :returns: respose object containing ms response + + """ + endpoint = "https://graph.microsoft.com/beta/teams/%s/channels/%s" % (teamId, channelId) + headers = { "Authorization": "Bearer " + self.access_token } + + graph_data = requests.get( + endpoint, + headers=headers + ) + return graph_data + + def writeChatMessageByTeamAndId(self, teamId, channelId, payload): + """Docstring for writeChatMessageByTeamAndId. + + :teamId: the team ID for the team to post the chat for + :channelId: the chat/channel ID for the channel to post to + :payload: the exact POST dict payload to be used + :returns: respose object containing ms response + + """ + # other pathways + #userId = "8a18e679-3603-4e10-a5e4-c75ad362fde0" + #endpoint = "https://graph.microsoft.com/beta/users/%s/chats/%s/messages" % (userId, channelId) + #endpoint = "https://graph.microsoft.com/beta/chats/%s/messages" % (channelId) + + # ms recommended pathway + endpoint = "https://graph.microsoft.com/beta/teams/%s/channels/%s/messages/" % (teamId, channelId) + headers = { + "Authorization": "Bearer " + self.access_token, + "Content-Type": "application/json" + } + + graph_data = requests.post( + endpoint, + headers=headers, + data=json.dumps(payload) + ) + return graph_data + + def getTeamByName(self, teamName): + """TODO: Docstring for getTeamByName. + + :teamName: TODO + :returns: TODO + + """ + # get all my teams + teamsLib = self.getMyTeams().json() + + if "value" in teamsLib: + teams = {} + for team in teamsLib["value"]: + teams[team["id"]] = team["displayName"] + + # search lib for my team + teamid = self.searchValuesinDict(teams, teamName) + return teamid + return "" + + def getChatByNameAndTeamId(self, teamId, channelName): + """TODO: Docstring for getChatByName. + + :channelName: TODO + :returns: TODO + + """ + # get all channels in the Teams + channelsLib = self.getChatsByTeam(teamId).json() + + if "value" in channelsLib: + channels = {} + for channel in channelsLib["value"]: + channels[channel["id"]] = channel["displayName"] + + # search lib for my channel + chatId = self.searchValuesinDict(channels, channelName) + return chatId + return "" + + @staticmethod + def buildTeamsMessage(customMessage): + """Docstring for buildTeamsMessage. + Build a Teams compatible message as per spec: + https://docs.microsoft.com/en-us/graph/api/resources/chatmessage?view=graph-rest-beta + + :customMessage: an dict containing the "name" and "text" fields + :returns: a valid Teams message dict payload + + """ + # schema + teams_mssg = { + "messageType": "message", + "body": { + "content": "", + "contentType": "html" + } + } + + # convert ``` snippets into block quotes ie. #<blockquote> some code </blockquote> + rich_html_content = MSGraphApi.insertCodeBlocksInMessage(customMessage["text"]) + + html_content = "<HTML><BODY>" + \ + "<p>From: <strong>%s</strong> <small>%s</small></p>" % (customMessage["name"], customMessage["ts"]) + \ + "<p>%s</p>" % (rich_html_content) + \ + "</BODY></HTML>" + + split_content = html_content.split("\n") + if len(split_content) > 1: + for i, line in enumerate(split_content): + split_content[i] = "<p>{}</p>".format(line) + + split_content_str = "".join(split_content) + + # TODO: sanitize string + + #teams_mssg["body"]["content"] = html_content + teams_mssg["body"]["content"] = split_content_str + return teams_mssg + + @staticmethod + def insertCodeBlocksInMessage(text): + """Docstring for insertCodeBlocksInMessage. + Replace ``` code ``` to be html compliant, i.e. wrapped in blockquotes + + :text: the input string possibly containing ``` + :returns: a string with html codeblocks instead of ``` + + """ + toks = text.split("```") + for i, tok in enumerate(toks): + # IMPORTANT! this assumes that every ``` comes in pairs + if (i % 2) != 0: + toks[i] = "<blockquote>{}</blockquote>".format(tok) + + quoted_str = "".join(toks) + return quoted_str + + @staticmethod + def searchValuesinDict(lib, value): + """TODO: Docstring for searchValuesinDict. + + :returns: the key of the dict where the value matches + + """ + for key in lib.keys(): + if lib[key] == value: + return key + return "" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3ceef42 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +requests>=2,<3 +msal>=0,<2 +jsonpath-ng>=1.5 diff --git a/slack_parser.py b/slack_parser.py new file mode 100644 index 0000000..8756992 --- /dev/null +++ b/slack_parser.py @@ -0,0 +1,169 @@ +import os +import json +import re +import time +from jsonpath_ng import jsonpath, parse + + +# jsonpath match will match in order of appearence. +# This mapping defines which position in the RESULT of match to look for which key. +# so. eg. at 0th position (of the match) will be the key "text", etc. +key_orderings = { + "text": 0, + "ts": 1, + "name": 2 +} + +class SlackParser(object): + + """Docstring for SlackParser. """ + + def __init__(self, messagesDir=None, usersFile=None, keys=None): + + self.messagesDir = "" + self.usersFile = "" + self.keys = list() + if messagesDir is not None: + self.messagesDir += messagesDir + if usersFile is not None: + self.usersFile += usersFile + if keys is not None: + self.keys += keys + + self.usersLib = None + + def getSortedMessageFiles(self): + """Docstring for getSortedMessageFiles. + Get the files that contain channel data sorted. + + :returns: a sorted list of json files names for the channel + + """ + files = sorted(os.listdir( self.messagesDir )) + return files + + def getUsers(self): + """Docstring for getUsers + Get the parsed json that contains the raw user info. + + :returns: a dict with all users infos + + """ + path = self.usersFile + # TODO: check for path validity + content = json.load(open(path)) + return content + + + def buildUsersLibrary(self): + """Docstring for buildUsersLibrary + Build a Users dict that keeps only usersid to name mappings. + + :returns: a dict user info + + """ + userLib = {} + for userjson in self.getUsers(): + if "name" in userjson and "id" in userjson: + userLib[userjson["id"]] = userjson["name"] + self.setUsersLib(userLib) + return self + + def setUsersLib(self, usersLib): + """Docstring for buildUsersLibrary + A setter method that sets the object lib to the given lib. + + """ + self.usersLib = usersLib + + def getMessages(self, slackFile): + """Docstring for getMessages. + Given a file, get the messages in that file. + + :slackFile: the json file name containing messages + :returns: a list of all messages in the file + + """ + path = self.messagesDir + "/" + slackFile + # TODO: check for path validity + content = json.load(open(path)) + return content + + def getKeyValues(self, slackMessage): + """Docstring for getKeyValues. + Get the value for each key (defined during init) from the mssg. + + :slackMessage: the parsed json slack message + :returns: on success, a dict with only required keys and values + on failure, None + + """ + data = None + # we also check for "subtype" to filter out mssgs like: + # "s joined the channel", "x left the channel" etc. + if "type" in slackMessage and "subtype" not in slackMessage: + if slackMessage["type"] == "message" and slackMessage["text"] != "": + data = {} + jsonpath_expression = parse("$.." + str(self.keys)) + # search slackMessage for each key, and add all matches (incl. dups.) in-order + match = jsonpath_expression.find(slackMessage) + #print(match[0]) + for key in self.keys: + # TODO: validation for matches possible "index out of range" here. + data[key] = match[key_orderings[key]].value + # get the user id in the text + userIds = self.getUserIdsFromMessage(data["text"]) + if userIds != []: + # find corresponding usernames from library + userNamesinMessageDict = self.getUserNamesFromIds(self.usersLib, userIds) + # inject usernames in the text replace userid + data["text"] = self.replaceMessageUserIdsWithNames(userNamesinMessageDict, data["text"]) + # convert ts to a human readble time + data["ts"] = time.strftime( "%H:%M %b %d, %Y", time.localtime(float(data["ts"])) ) + + return data + + @staticmethod + def getUserIdsFromMessage(text): + """Docstring for getUserIdsFromMessage. + A helper method that searches a text for Slack user ids. It maps: + "<@SLACKID1>adsfasdf <@SLACKID2>" -> ["SLACKID1", "SLACKID2"] + + :text: raw text from Slack message json + :returns: a list of userids strings in the text in order of match + + """ + regex = r"<@(.+?)>" + matches = re.findall(regex, text) + return matches + + @staticmethod + def getUserNamesFromIds(usersDict, userIds): + """Docstring for getUserNamesFromIds. + A helper method to search a dict for a keys, and get the values. + + :usersDict: a dict library of userid and name mappings + :ids: a string list of userids + :returns: dict of each userid and its username + + """ + userMap = {} + for userId in userIds: + if userId in usersDict: + userMap[userId] = usersDict[userId] + return userMap + + @staticmethod + def replaceMessageUserIdsWithNames(messageUsersDict, text): + """Docstring for replaceMessageUserIdsWithNames. + A helper method to replace all Slackuser id tags with their @username. So it maps: + { "USERID1" : "name1" , "USERID2": "name2" },"<@USERID1> blah blah <@USERID2>." -> "@name1 blah blah @name2." + + :messageUsersDict: the dict containing userid to username mappings for this text + :text: the original string contianing Slack userids in it + :returns: a string with each Slack userid replaced with mapping value from dict. + + """ + for userId in messageUsersDict.keys(): + text = re.sub( "<@{}>".format(userId), "<i style='color: #B18904;'>@{}</i>".format(messageUsersDict[userId]), text ) + return text @@ -0,0 +1,96 @@ +import unittest + +from msgraph import MSGraphApi +from slack_parser import SlackParser + +class Test(unittest.TestCase): + """ + The basic class that inherits unittest.TestCase + """ + msapi = MSGraphApi() + sp = SlackParser() + + # test case function to check the MSGraphApi.insertCodeBlocksInMessage function + def test_0_insert_block_quotes(self): + input_string = "sdfsdf" + expected = "sdfsdf" + actual = self.msapi.insertCodeBlocksInMessage(input_string) + self.assertEqual( expected, actual ) + + def test_1_insert_block_quotes(self): + input_string = "``` CODE1 ```" + expected = "<blockquote> CODE1 </blockquote>" + actual = self.msapi.insertCodeBlocksInMessage(input_string) + self.assertEqual( expected, actual ) + + def test_2_insert_block_quotes(self): + input_string = " there and ``` CODE 1 ``` asdfasdf ``` CODE 2 ``` and then again ``` CODE 3 ``` end." + expected = " there and <blockquote> CODE 1 </blockquote> asdfasdf <blockquote> CODE 2 </blockquote> and then again <blockquote> CODE 3 </blockquote> end." + actual = self.msapi.insertCodeBlocksInMessage(input_string) + self.assertEqual( expected, actual ) + + def test_3_insert_long_block_quotes(self): + input_string = """<@U5F956SJH> a long running query on the master from job01.....job-consumer-2 logs showing\n```>1|ERROR|2020-03-17 15:50:00,018-0400|org.springframework.jms.listener.DefaultMessageListenerContainer#0-3|GenerateEmailNotificationTemplate:106|Unexpected error in processing. Abending.\norg.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException:\n### Error querying database. Cause: java.lang.IllegalArgumentException: Mapped Statements collection does not contain value for com.acuityads.notification.dao.NotificationMapper.findNotifications\n### Cause: java.lang.IllegalArgumentException: Mapped Statements collection does not contain value for com.acuityads.notification.dao.NotificationMapper.findNotifications\n at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:77)\n at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:446)\n at com.sun.proxy.$Proxy49.selectList(Unknown Source)\n at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:230)\n at com.acuityads.persistence.dao.SelectQuery.selectList(SelectQuery.java:74)```\n + """ + expected = """<@U5F956SJH> a long running query on the master from job01.....job-consumer-2 logs showing\n<blockquote>>1|ERROR|2020-03-17 15:50:00,018-0400|org.springframework.jms.listener.DefaultMessageListenerContainer#0-3|GenerateEmailNotificationTemplate:106|Unexpected error in processing. Abending.\norg.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException:\n### Error querying database. Cause: java.lang.IllegalArgumentException: Mapped Statements collection does not contain value for com.acuityads.notification.dao.NotificationMapper.findNotifications\n### Cause: java.lang.IllegalArgumentException: Mapped Statements collection does not contain value for com.acuityads.notification.dao.NotificationMapper.findNotifications\n at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:77)\n at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:446)\n at com.sun.proxy.$Proxy49.selectList(Unknown Source)\n at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:230)\n at com.acuityads.persistence.dao.SelectQuery.selectList(SelectQuery.java:74)</blockquote>\n + """ + + actual = self.msapi.insertCodeBlocksInMessage(input_string) + self.assertEqual( expected, actual ) + + def test_4_insert_block_quotes(self): + input_string = """<@U5F956SJH> a long running query on the master from job01.....job-consumer-2 logs showing<p>```>1|ERROR|2020-03-17 15:50:00,018-0400|org.springframework.jms.listener.DefaultMessageListenerContainer#0-3|GenerateEmailNotificationTemplate:106|Unexpected error in processing. Abending.</p>org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException:<p>### Error querying database. Cause: java.lang.IllegalArgumentException: Mapped Statements collection does not contain value for com.acuityads.notification.dao.NotificationMapper.findNotifications</p>### Cause: java.lang.IllegalArgumentException: Mapped Statements collection does not contain value for com.acuityads.notification.dao.NotificationMapper.findNotifications<p> at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:77)</p> at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:446)<p> at com.sun.proxy.$Proxy49.selectList(Unknown Source)</p> at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:230)<p> at com.acuityads.persistence.dao.SelectQuery.selectList(SelectQuery.java:74)```</p> + """ + expected = """<@U5F956SJH> a long running query on the master from job01.....job-consumer-2 logs showing<p><blockquote>>1|ERROR|2020-03-17 15:50:00,018-0400|org.springframework.jms.listener.DefaultMessageListenerContainer#0-3|GenerateEmailNotificationTemplate:106|Unexpected error in processing. Abending.</p>org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException:<p>### Error querying database. Cause: java.lang.IllegalArgumentException: Mapped Statements collection does not contain value for com.acuityads.notification.dao.NotificationMapper.findNotifications</p>### Cause: java.lang.IllegalArgumentException: Mapped Statements collection does not contain value for com.acuityads.notification.dao.NotificationMapper.findNotifications<p> at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:77)</p> at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:446)<p> at com.sun.proxy.$Proxy49.selectList(Unknown Source)</p> at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:230)<p> at com.acuityads.persistence.dao.SelectQuery.selectList(SelectQuery.java:74)</blockquote></p> + """ + actual = self.msapi.insertCodeBlocksInMessage(input_string) + self.assertEqual( expected, actual ) + + def test_5_parse_user_ids(self): + input_string = "a long history of thisg" + expected = [] + actual = self.sp.getUserIdsFromMessage(input_string) + self.assertEqual( expected, actual ) + + def test_6_parse_user_ids(self): + input_string = "<@U32498ksjl> a long history of thisg" + expected = ["U32498ksjl"] + actual = self.sp.getUserIdsFromMessage(input_string) + self.assertEqual( expected, actual ) + + def test_7_parse_user_ids(self): + input_string = " what <p> <@U32498ksjl> a long history </p> of thisg <@kumar12>" + expected = ["U32498ksjl", "kumar12"] + actual = self.sp.getUserIdsFromMessage(input_string) + self.assertEqual( expected, actual ) + + def test_8_get_user_name_from_user_ids(self): + usersDict = { "U32498ksjl" : "kd1" , "UBLAH" : "kd2" } + test_input = ["U32498ksjl"] + expected = { "U32498ksjl" : "kd1" } + actual = self.sp.getUserNamesFromIds(usersDict, test_input) + self.assertEqual( expected, actual ) + + def test_9_get_user_name_from_user_ids(self): + usersDict = { "U32498ksjl" : "kd1" , "UBLAH" : "kd2" } + test_input = ["U32498ksjl", "UBLAH"] + expected = { "U32498ksjl" : "kd1", "UBLAH" : "kd2" } + actual = self.sp.getUserNamesFromIds(usersDict, test_input) + self.assertEqual( expected, actual ) + + def test_10_replace_userids_with_names(self): + usersDict = { "U32498ksjl" : "kd1" } + input_string = "<@U32498ksjl> a long history of thisg" + expected = "<i style='color: #B18904;'>@kd1</i> a long history of thisg" + actual = self.sp.replaceMessageUserIdsWithNames(usersDict, input_string) + self.assertEqual( expected, actual ) + + def test_11_replace_userids_with_names(self): + usersDict = { "U32498ksjl" : "kd1" , "UBLAH" : "kd2" } + input_string = "<@U32498ksjl> a long history of thisg from <@UBLAH> and this <@UBLAH> again." + expected = "<i style='color: #B18904;'>@kd1</i> a long history of thisg from <i style='color: #B18904;'>@kd2</i> and this <i style='color: #B18904;'>@kd2</i> again." + actual = self.sp.replaceMessageUserIdsWithNames(usersDict, input_string) + self.assertEqual( expected, actual ) + +if __name__ == '__main__': + unittest.main() |
