Botframework-sdk: Can't send custom Slack Message with version 3 of Bot Connector

Created on 15 Aug 2016  路  18Comments  路  Source: microsoft/botframework-sdk

I have a bot that replies with a message consisting only of attachments. When it works on Slack, it uses Slack attachment formatting quite heavily, thus I have to use ChannelData property.

In version 1 of BotConnector, the code was like this

    var reply = message.CreateReplyMessage();
    reply.Attachments = new List<Attachment>();
    var attachments = new List<object>(); //Slack-formatted attachments
    //filling attachments...
    reply.ChannelData = new {attachments};

and it worked. Now, in version 3 code has changed to

    var reply = activity.CreateReply();
    reply.Attachments = new List<Attachment>();
    var attachments = new List<object>(); //Slack-formatted attachments
    //filling attachments...
    reply.ChannelData = new {attachments};

    var connector = new ConnectorClient(new Uri(activity.ServiceUrl));
    await connector.Conversations.ReplyToActivityAsync(reply);

which, basically, boils down to using different method to create reply, and another one to send the reply back.

Now, the problem is, that I don't get the reply back to Slack. Diagnostics in AppInsight show me that somewhere in Connector something like this happens:

Exception type: System.ArgumentNullException
Failed method: SlackChannel.SlackMapper+d__5.MoveNext
Exception Message: value cannot be null. Parameter name: source
ChannelData: {}
message: Invalid ChannelData

Please note that ChannelData in this diagnostics seems to be empty. So what I gather from all this is that something has changed in the way BotConnector processes ChannelData. How can I find out what exactly am I doing wrong?

Most helpful comment

...and now I've found why that happens. Here the culprit:

SerializationSettings = new JsonSerializerSettings
{ 
    //...
    ContractResolver = new ReadOnlyJsonContractResolver(),
};

ReadOnlyJsonContractResolver skips all read-only properties, and the properties in anonymous classes are read-only. If I switch to "regular" class for my Slack message, it works just fine.

Now, the problem is that it's quite cumbersome to have regular class for every channel... so is this intentional or not?

All 18 comments

Please add your botId and i'll investigate

aqubot

aqubot looks to be a v1 bot. What is the botId of the v3 bot that is failing? Also, It might be helpful to include the CorrelationId from the errors that you are seeing in you App Insights so I can look them up in our logs. Thanks

I switch it back and forth between v1 and v3. Operation IDs are:

VkhLmE2xzWE=
MAxwJ7BEFPY=
GjSGnhQyrOY=

There's no CorrelationId on the entries.

I can confirm that this is a bug in the framework and will be fixed the next time we deploy. As a work around until we get the fix deployed you can try setting ChannelData to null instead of an empty object {}.

The problem is, I'm not setting ChannelData to empty object, I'm setting it to a custom Slack message (which I want to be sent to Slack).

The three OperationId's you included above all contain empty ChannelData. Here's first one:
{
"type": "message",
"timestamp": "2016-08-15T08:40:44.1689733Z",
"serviceUrl": "https://slack.botframework.com/",
"channelId": "slack",
"from": {
"id": "B1BN6LF5W:T03CE4PJV",
"name": "aqu"
},
"conversation": {
"id": "B1BN6LF5W:T03CE4PJV:C1BN25KB7"
},
"recipient": {
"id": "U1AGRREMA:T03CE4PJV",
"name": "srogovtsev"
},
"text": "",
"attachments": [],
"entities": [],
"channelData": {},
"replyToId": "a7cd06540bd0482ab047060cdba6b9af"
}

And I suppose this is the problem, because as you can see from code I'm sending the ChannelData with attachments, so it gets lost somewhere...

In V3, ChannelData should contain a properly formatted Slack message see Custom Slack Messages. Try changing your code to something like:

var reply = activity.CreateReply();
reply.Attachments = new List<Attachment>();
var attachments = new List<object>(); //Slack-formatted attachments
//filling attachments...
reply.ChannelData = new JObject();
reply.ChannelData.Attachments = new JArray(attachments);

var connector = new ConnectorClient(new Uri(activity.ServiceUrl));
await connector.Conversations.ReplyToActivityAsync(reply);

What do you mean by "properly formatted"? Does it have to be a complete Slack message with addressing and so on? Because example message on your link isn't complete, it has only text and attachments, no addressing.

Try changing your code to something like:

It won't compile: JObject has no property Attachments. But let's try changing it to dynamic:

dynamic channelData = new JObject();
channelData.Attachments = new JArray(attachments);
reply.ChannelData = channelData;

It fails with

System.ArgumentException: Could not determine JSON object type for type <>f__AnonymousType7`13[System.String,System.String,System.String,System.String,System.String,System.String,System.String,<>f__AnonymousType2`3[System.String,System.String,System.Boolean][],System.String,System.String,System.Int64,System.String,System.Object[]].
   at Newtonsoft.Json.Linq.JValue.GetValueType(Nullable`1 current, Object value)
   at Newtonsoft.Json.Linq.JContainer.CreateFromContent(Object content)
   at Newtonsoft.Json.Linq.JContainer.AddInternal(Int32 index, Object content, Boolean skipParentCheck)
   at Newtonsoft.Json.Linq.JContainer.AddInternal(Int32 index, Object content, Boolean skipParentCheck)
   at Aqu.Web.MessagesController.<Post>d__4.MoveNext() in C:\repos\srogovtsev\aqu\Sources\Aqu.Web\Controllers\MessagesController.cs:line 251

But if I go back to my simple reply.ChannelData = new {attachments};, I get (at least on my side, just before sending) a valid JSON:

    "ChannelData": {
      "attachments": [
        {
          "author_name": "XXX",
          "author_icon": "https://secure.gravatar.com/avatar/XXX",
          "color": "#14892c",
          "fallback": "XXX",
          "title": "XXX",
          "title_link": "https://xxx",
          "text": "XXX",
          "fields": [
            {
              "title": "Assigned to",
              "value": "XXX",
              "short": false
            },
            {
              "title": "Fix version/s",
              "value": "XXX",
              "short": true
            }
          ],
          "footer": "Bug",
          "footer_icon": "https://XXX",
          "ts": 1471265352.0,
          "callback_id": null,
          "actions": null
        }
      ]
    }

But I still get the same error in AppInsights (OperationId ELF8tQYqo5o=). So it looks like ChannelData gets blanked somewhere between my code and SlackChannel.SlackMapper.

I've managed to pin down the problem. The JSON you see above is logged before the reply is sent via connector.Conversations.ReplyToActivityAsync(reply). But if I add a log inside the connector (kudos to developers for having the injection point!), I see "channelData": {}. So the problem is somewhere inside ReplyToActivityAsync.

Minimal repro is actually quite easy:

reply.ChannelData = new {text = "here should be attachments"};
connector.Conversations.ReplyToActivityAsync(reply);
  1. JsonConvert.SerializeObject(reply) shows

    "channelData": {"text": "here should be attachments" }
    
  2. IServiceClientTracingInterceptor logs EnterMethod for ReplyToActivity with

    ChannelData: {text: "here should be attachments"}
    
  3. IServiceClientTracingInterceptor logs SendRequest with

    "channelData": {}
    
  4. delegating handler logs

    "channelData": {}
    

It boils down (I think, at least) to this line in Microsoft.Bot.Connector.Conversations.ReplyToActivityWithHttpMessagesAsync:

  _httpRequest.Content = (HttpContent) new StringContent(SafeJsonConvert.SerializeObject((object) activity, this.Client.SerializationSettings), Encoding.UTF8);

which for some reason serializes channelData as empty object.

...and now I've found why that happens. Here the culprit:

SerializationSettings = new JsonSerializerSettings
{ 
    //...
    ContractResolver = new ReadOnlyJsonContractResolver(),
};

ReadOnlyJsonContractResolver skips all read-only properties, and the properties in anonymous classes are read-only. If I switch to "regular" class for my Slack message, it works just fine.

Now, the problem is that it's quite cumbersome to have regular class for every channel... so is this intentional or not?

I was able to get it to work by creating the ChannelData object as a JObject as follows:

var attachments = new List { JObject.Parse("{'title': 'title', 'text': 'some text...' }") };
dynamic channelData = new JObject();
channelData.Attachments = new JArray(attachments);

ReadOnlyJsonContractSerialzer is the default from AutoRest generated client code and does not work with anonymous types.

Okay, here's the simplest solution:

reply.ChannelData = JObject.FromObject(new {attachments});

I like it!

Though the below worked
var attachments = new List { JObject.Parse("{'title': 'title', 'text': 'some text...' }") }; dynamic channelData = new JObject(); channelData.Attachments = new JArray(attachments);

I couldn't see any rich content. Was limited to ChannelData text property only, Trying to find if attachements are being stripped

Was this page helpful?
0 / 5 - 0 ratings