ChannelSurf icon indicating copy to clipboard operation
ChannelSurf copied to clipboard

Posting Slack Messages

Open argvMatey opened this issue 7 years ago • 6 comments
trafficstars

I thought it would be nice to import the Slack messages into MS teams as messages, so I developed a bit of (ugly!) code to do so. I stuck this method in messages.cs, and jammed the call to it into CreateSlackMessageHtmlArchiveFile. The best part about these posts as opposed to those from a webhook is that they're searchable. It will churn along happily, posting messages to the appropriate team channel, until it hits the Activity Limit for the API returning

REASON: { "error": { "code": "ActivityLimitReached", "message": "Failed to execute Skype backend request PostMessageRequest.", "innerError": { "request-id": "blahblahblah", "date": "2018-04-30T18:39:45" } } }

As you can see below, I back off new posts exponentially when the activity limit is reached, but, after a while the access token expires and the app needs to be restarted. I tried batching the requests, but I hit the limit sooner that way for some reason. I am currently playing around with increasing the expiration of the access token in Azure Ad, increasing the default expiration from 1 to 23 hours. I think the way to handle this is using the refresh token to get another access token, but this is beyond me so far. I'll keep looking into it. Hope this helps someone, and sorry if I'm just cluttering things up with this issue; I've never posted to github before and am not sure the proper forum for this sort of thing.

Post individual message:

CreateSlackMessageHtmlArchiveFile:

for (int i = 0; i < numOfMessagesToTake; i++) { var messageAsHtml = MessageToHtml(messageList[messageIndexPosition + i], channelsMapping); fileBody.AppendLine(messageAsHtml); rInt = r.Next(100, 1000); System.Threading.Thread.Sleep(rInt); string check = postMessage(aadAccessToken, selectedTeamId, channelsMapping.id, messageAsHtml); }

public static string postMessage(string aadAccessToken, string teamID, string channelID, string bodyContent) {

        int sleeper = 1;
        Helpers.httpClient.DefaultRequestHeaders.Clear();
        Helpers.httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", aadAccessToken);
        Helpers.httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

        // this might break on some platforms
        dynamic messageBody = new JObject();
        dynamic newMessage = new JObject();
        dynamic rootMessage = new JObject();

        messageBody.contentType = 2;
        messageBody.content = bodyContent;
        rootMessage.body = messageBody;
        newMessage.rootMessage = rootMessage;

        var createMsGroupPostData = JsonConvert.SerializeObject(newMessage);
        var httpResponseMessage =
            Helpers.httpClient.PostAsync(O365.MsGraphBetaEndpoint + "groups/" + teamID + "/Channels/" + channelID + "/chatThreads",
                new StringContent(createMsGroupPostData, Encoding.UTF8, "application/json")).Result;
        Console.WriteLine(httpResponseMessage.ReasonPhrase);

        //need to back off the requests if tooManyRequests 
        while (!httpResponseMessage.IsSuccessStatusCode)
        {
                Console.WriteLine(httpResponseMessage.ReasonPhrase);
                Console.WriteLine("ERROR: Message Not Posted");
            Console.WriteLine("REASON: " + httpResponseMessage.Content.ReadAsStringAsync().Result);

            JObject j = JObject.Parse(httpResponseMessage.Content.ReadAsStringAsync().Result);
            Console.WriteLine(j["error"]["code"].ToString());
            if(j["error"]["code"].ToString() == "ActivityLimitReached")
            {
                
            }
            if (j["error"]["code"].ToString() == "AuthenticationTokenExpired")
            {
                // need to re-authenticate when token expires - made authenticationcontext public static 
                //tried to set auth token expiry to 23 hours
                //string newToken = Program.authenticationContext.AcquireTokenSilentAsync(O365.MsGraphBetaEndpoint, Program.Configuration["AzureAd:ClientId"]).Result.AccessToken;
                //return newToken;
            }

                Console.WriteLine("Retrying Message Post in " + Convert.ToString(sleeper) + " seconds");  
                System.Threading.Thread.Sleep(sleeper * 1000);
                httpResponseMessage =
                Helpers.httpClient.PostAsync(O365.MsGraphBetaEndpoint + "groups/" + teamID + "/Channels/" + channelID + "/chatThreads",
                    new StringContent(createMsGroupPostData, Encoding.UTF8, "application/json")).Result;
                sleeper = sleeper * 2;
        }

        return "1";
    }

Post batch of messages:

int messageBatchSize = 20;

  Random r = new Random();
                    int rInt = r.Next(100, 1000);

                    dynamic wrapper = new JObject();
                    JArray requests = new JArray();

//for (int i = 0; i < messageBatchSize; i++) //{ // var messageAsHtml = MessageToHtml(messageList[messageIndexPosition + i], channelsMapping); // //hitting activity limit. cannot batch requests added random 100 - 1000 ms delay here, and exponential backoff in postMessage on failure // requests.Add(buildMessageBatch(i + 1, selectedTeamId, channelsMapping.id, messageAsHtml));

                    //    //
                    //}

                    ////trying to post a batch of messages - what is the upper limit?
                    //wrapper.requests = requests;
                    //rInt = r.Next(100, 500);
                    //System.Threading.Thread.Sleep(rInt);
                    //postMessageBatch(aadAccessToken, wrapper);

  public static string postMessageBatch(string aadAccessToken, JObject wrapper)
    {
        int i = 2;
        int sleeper = 1;
        Helpers.httpClient.DefaultRequestHeaders.Clear();
        Helpers.httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", aadAccessToken);
        Helpers.httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

        var createMsGroupPostData = JsonConvert.SerializeObject(wrapper);
        //Console.WriteLine(createMsGroupPostData);

        var httpResponseMessage =
            Helpers.httpClient.PostAsync(O365.MsGraphBetaEndpoint + "$batch",
                new StringContent(createMsGroupPostData, Encoding.UTF8, "application/json")).Result;

        String content = httpResponseMessage.Content.ReadAsStringAsync().Result;
        JObject responseData = JObject.Parse(content);
        JArray a = JArray.Parse(content);
        string responses = JsonConvert.SerializeObject(responseData);


        //var responses = responseData["responses"];
        //var response = responses.Children();
        //Console.WriteLine(response[0]);



        System.Threading.Thread.Sleep(5000);




            // batching requests does not throw an error code in this manner. Begin the Whackamole
            //need to back off the requests if tooManyRequests 
            while (!httpResponseMessage.IsSuccessStatusCode)
            {
                Console.WriteLine("ERROR: Message Not Posted");
                Console.WriteLine("REASON: " + httpResponseMessage.Content.ReadAsStringAsync().Result);
                Console.WriteLine("Retrying Message Post");
                System.Threading.Thread.Sleep(sleeper * 1000);

                httpResponseMessage =
                Helpers.httpClient.PostAsync(O365.MsGraphBetaEndpoint + "$batch",
                    new StringContent(createMsGroupPostData, Encoding.UTF8, "application/json")).Result;
                sleeper = sleeper * 2;
            }

           

        return "1";
    }

    public static JObject buildMessageBatch(int i, string teamID, string channelID, string bodyContent)
    {
     

        // this might break on some platforms
        dynamic messageBody = new JObject();
        dynamic newMessage = new JObject();
        dynamic rootMessage = new JObject();
        
        dynamic request = new JObject();
        dynamic dependsOn = new JArray();
        dynamic headers = new JObject();
        dynamic requestBody = new JObject();
        
        messageBody.contentType = 2;
        messageBody.content = bodyContent;
        rootMessage.body = messageBody;
        requestBody.rootMessage = rootMessage;

        headers["Content-Type"] = "application/json";

        request.headers = headers;
        request.body = requestBody;
        request.id = Convert.ToString(i);
        if (i != 1)
        {
            dependsOn.Add(Convert.ToString(i - 1));
            request.dependsOn = dependsOn;
        }
        request.method = "POST";
        request.url = "/groups/" + teamID + "/Channels/" + channelID + "/chatThreads";


        ///requests.Add(request);
        //wrapper.requests = requests;

        //var createMsGroupPostData = JsonConvert.SerializeObject(wrapper);
        //Console.WriteLine(createMsGroupPostData);

        return request;
    }

argvMatey avatar Apr 30 '18 18:04 argvMatey

Some endpoints in the MS Graph API will insert a retry-after value in the response header when returning a 429 code. Teams isn't indicated as being one of them, but docs have been known to be wrong.

https://developer.microsoft.com/en-us/graph/docs/concepts/throttling

Batching may not necessarily make things better: it depends how the Teams API that sits underneath the Graph is counting API calls.

tamhinsf avatar May 02 '18 21:05 tamhinsf

Yeah, I saw that page as well, and I can confirm that there's no retry-after value in the response header. Since batching the requests doesn't result in more messages being posted, I'm assuming there's a hard limit in the Teams API on the number of posts per hour. Throttling seems to reset after an hour.

Also, this bit of code will return a new token after the original expires. Right now i am just returning the new token from postMessage, so it will only allow you to upload a single channel because the new token never makes it back to the GetAndUploadMessages loop at the top of Messages.cs, but because we are making a new team for each channel, it works well enough for our purposes. Sorry for linearizing your beautifully object oriented program!

if (j["error"]["code"].ToString() == "InvalidAuthenticationToken") { // need to re-authenticate when token expires - made authenticationcontext public static //this piece is successfully re-authenticating //new access token is returned from method string newToken = Program.authenticationContext.AcquireTokenSilentAsync(Program.aadResourceAppId, Program.Configuration["AzureAd:ClientId"]).Result.AccessToken;

                Helpers.httpClient.DefaultRequestHeaders.Clear();
                Helpers.httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", newToken);
                Helpers.httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

argvMatey avatar May 03 '18 16:05 argvMatey

@argvMatey Thanks for sharing your improvements. Do you still have the code? Could you share it as a Pull-Request? I guess it would increase the chance somebody building on the progress you have made.

haraldreingruber avatar Dec 18 '18 09:12 haraldreingruber

is that code posted anywhere i am running into issues when i try and run it

iradzik avatar Apr 28 '20 15:04 iradzik

@tamhinsf is there any new development into this direction going on, would be great, to fix this issue? and make the result... way more "pretty"

dschief001 avatar May 29 '20 14:05 dschief001

We can import chats from slack to teams for more information you may want to take a look at this Slack to Teams migration guide. https://www.cloudfuze.com/slack-to-teams-migration-guide-for-it-admins/

JustinWalker72 avatar Aug 18 '21 06:08 JustinWalker72