From 1333b34fa47e5bf0a007d2e29bd3684a37063368 Mon Sep 17 00:00:00 2001 From: Ole Magnus Urdahl Date: Fri, 29 May 2026 22:48:39 +0200 Subject: [PATCH 1/2] Add reaction_added support and conversations.history Introduce handling for Slack reaction_added events and add Conversations.history support. - Add ReactionAddedEvent model and ReactionItem, plus IHandleReactionAdded abstraction. - Register ReactionAdded middleware and handler builder integration (IAppBuilderExtensions, SlackBotHandlersBuilder, ISlackbotHandlersBuilder, HttpItemsManager routing/deserialization). - Add ConversationsHistory API to ISlackClient and SlackClient, with ConversationsHistoryResponse model; include cursor handling. - Extend ConversationsRepliesResponse.Message to include bot-related fields (SubType, Bot_Id, Bot_Profile) for proper bot-message deserialization. - Add unit tests and a StubHttpMessageHandler to verify bot-message deserialization and ConversationsHistory pagination. These changes enable processing of reaction_added events and correctly deserialize bot/app-authored messages from conversation endpoints. --- .../Abstractions/IHandleReactionAdded.cs | 13 ++++ .../src/Slackbot.Net.Endpoints/EventTypes.cs | 1 + .../Hosting/IAppBuilderExtensions.cs | 1 + .../Hosting/ISlackbotHandlersBuilder.cs | 1 + .../Hosting/SlackBotHandlersBuilder.cs | 6 ++ .../Middlewares/HttpItemsManager.cs | 2 + .../Middlewares/ReactionAddedEvents.cs | 48 ++++++++++++ .../Models/Events/ReactionAddedEvent.cs | 20 +++++ .../ISlackClient.cs | 8 ++ .../ConversationsHistoryResponse.cs | 11 +++ .../ConversationsRepliesResponse.cs | 12 +++ .../SlackClient.cs | 21 +++++ ...ersationsBotMessageDeserializationTests.cs | 78 +++++++++++++++++++ .../Helpers/StubHttpMessageHandler.cs | 18 +++++ 14 files changed, 240 insertions(+) create mode 100644 source/src/Slackbot.Net.Endpoints/Abstractions/IHandleReactionAdded.cs create mode 100644 source/src/Slackbot.Net.Endpoints/Middlewares/ReactionAddedEvents.cs create mode 100644 source/src/Slackbot.Net.Endpoints/Models/Events/ReactionAddedEvent.cs create mode 100644 source/src/Slackbot.Net.SlackClients.Http/Models/Responses/ConversationsHistoryResponse/ConversationsHistoryResponse.cs create mode 100644 source/test/Slackbot.Net.SlackClients.Http.Tests/ConversationsBotMessageDeserializationTests.cs create mode 100644 source/test/Slackbot.Net.SlackClients.Http.Tests/Helpers/StubHttpMessageHandler.cs diff --git a/source/src/Slackbot.Net.Endpoints/Abstractions/IHandleReactionAdded.cs b/source/src/Slackbot.Net.Endpoints/Abstractions/IHandleReactionAdded.cs new file mode 100644 index 0000000..e20bfd5 --- /dev/null +++ b/source/src/Slackbot.Net.Endpoints/Abstractions/IHandleReactionAdded.cs @@ -0,0 +1,13 @@ +using Slackbot.Net.Endpoints.Models.Events; + +namespace Slackbot.Net.Endpoints.Abstractions; + +public interface IHandleReactionAdded +{ + Task Handle(EventMetaData eventMetadata, ReactionAddedEvent slackEvent); + + bool ShouldHandle(ReactionAddedEvent slackEvent) + { + return true; + } +} diff --git a/source/src/Slackbot.Net.Endpoints/EventTypes.cs b/source/src/Slackbot.Net.Endpoints/EventTypes.cs index f76ab3b..2e594da 100644 --- a/source/src/Slackbot.Net.Endpoints/EventTypes.cs +++ b/source/src/Slackbot.Net.Endpoints/EventTypes.cs @@ -10,4 +10,5 @@ public static class EventTypes public const string TeamJoin = "team_join"; public const string EmojiChanged = "emoji_changed"; public const string Message = "message"; + public const string ReactionAdded = "reaction_added"; } diff --git a/source/src/Slackbot.Net.Endpoints/Hosting/IAppBuilderExtensions.cs b/source/src/Slackbot.Net.Endpoints/Hosting/IAppBuilderExtensions.cs index 2231f39..13618d1 100644 --- a/source/src/Slackbot.Net.Endpoints/Hosting/IAppBuilderExtensions.cs +++ b/source/src/Slackbot.Net.Endpoints/Hosting/IAppBuilderExtensions.cs @@ -25,6 +25,7 @@ public static IApplicationBuilder UseSlackbot( app.MapWhen(TeamJoinEvents.ShouldRun, b => b.UseMiddleware()); app.MapWhen(EmojiChangedEvents.ShouldRun, b => b.UseMiddleware()); app.MapWhen(MessageEvents.ShouldRun, b => b.UseMiddleware()); + app.MapWhen(ReactionAddedEvents.ShouldRun, b => b.UseMiddleware()); return app; } diff --git a/source/src/Slackbot.Net.Endpoints/Hosting/ISlackbotHandlersBuilder.cs b/source/src/Slackbot.Net.Endpoints/Hosting/ISlackbotHandlersBuilder.cs index 3fe9a84..658eb37 100644 --- a/source/src/Slackbot.Net.Endpoints/Hosting/ISlackbotHandlersBuilder.cs +++ b/source/src/Slackbot.Net.Endpoints/Hosting/ISlackbotHandlersBuilder.cs @@ -19,4 +19,5 @@ public ISlackbotHandlersBuilder AddInteractiveBlockActionsHandler() public ISlackbotHandlersBuilder AddTeamJoinHandler() where T : class, IHandleTeamJoin; public ISlackbotHandlersBuilder AddEmojiChangedHandler() where T : class, IHandleEmojiChanged; public ISlackbotHandlersBuilder AddMessageHandler() where T : class, IHandleMessage; + public ISlackbotHandlersBuilder AddReactionAddedHandler() where T : class, IHandleReactionAdded; } diff --git a/source/src/Slackbot.Net.Endpoints/Hosting/SlackBotHandlersBuilder.cs b/source/src/Slackbot.Net.Endpoints/Hosting/SlackBotHandlersBuilder.cs index 592fbfa..4d86790 100644 --- a/source/src/Slackbot.Net.Endpoints/Hosting/SlackBotHandlersBuilder.cs +++ b/source/src/Slackbot.Net.Endpoints/Hosting/SlackBotHandlersBuilder.cs @@ -71,4 +71,10 @@ public ISlackbotHandlersBuilder AddMessageHandler() where T : class, IHandleM services.AddSingleton(); return this; } + + public ISlackbotHandlersBuilder AddReactionAddedHandler() where T : class, IHandleReactionAdded + { + services.AddSingleton(); + return this; + } } diff --git a/source/src/Slackbot.Net.Endpoints/Middlewares/HttpItemsManager.cs b/source/src/Slackbot.Net.Endpoints/Middlewares/HttpItemsManager.cs index 6ed2a3a..7554b30 100644 --- a/source/src/Slackbot.Net.Endpoints/Middlewares/HttpItemsManager.cs +++ b/source/src/Slackbot.Net.Endpoints/Middlewares/HttpItemsManager.cs @@ -81,6 +81,8 @@ private static SlackEvent ToEventType(JsonElement eventJson, string raw) return JsonSerializer.Deserialize(json, WebOptions); case EventTypes.Message: return JsonSerializer.Deserialize(json, WebOptions); + case EventTypes.ReactionAdded: + return JsonSerializer.Deserialize(json, WebOptions); default: var unknownSlackEvent = JsonSerializer.Deserialize(json, WebOptions); unknownSlackEvent.RawJson = raw; diff --git a/source/src/Slackbot.Net.Endpoints/Middlewares/ReactionAddedEvents.cs b/source/src/Slackbot.Net.Endpoints/Middlewares/ReactionAddedEvents.cs new file mode 100644 index 0000000..4b1ba1d --- /dev/null +++ b/source/src/Slackbot.Net.Endpoints/Middlewares/ReactionAddedEvents.cs @@ -0,0 +1,48 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Slackbot.Net.Endpoints.Abstractions; +using Slackbot.Net.Endpoints.Models.Events; + +namespace Slackbot.Net.Endpoints.Middlewares; + +public class ReactionAddedEvents( + RequestDelegate next, + ILogger logger, + IEnumerable responseHandlers +) +{ + public async Task Invoke(HttpContext context) + { + var reactionAdded = (ReactionAddedEvent)context.Items[HttpItemKeys.SlackEventKey]; + var metadata = (EventMetaData)context.Items[HttpItemKeys.EventMetadataKey]; + var handlers = responseHandlers.Where(h => h.ShouldHandle(reactionAdded)); + + logger.BeginScope(new Dictionary + { + ["Slack_TeamId"] = metadata?.Team_Id, + ["Slack_User"] = reactionAdded?.User + }); + + foreach (var handler in handlers) + { + try + { + logger.LogInformation("Handling using {HandlerType}", handler.GetType()); + var response = await handler.Handle(metadata, reactionAdded); + logger.LogInformation("Handler response: {Response}", response.Response); + } + catch (Exception e) + { + logger.LogError(e, e.Message); + } + } + + context.Response.StatusCode = 200; + } + + public static bool ShouldRun(HttpContext ctx) + { + return ctx.Items.ContainsKey(HttpItemKeys.EventTypeKey) + && ctx.Items[HttpItemKeys.EventTypeKey].ToString() == EventTypes.ReactionAdded; + } +} diff --git a/source/src/Slackbot.Net.Endpoints/Models/Events/ReactionAddedEvent.cs b/source/src/Slackbot.Net.Endpoints/Models/Events/ReactionAddedEvent.cs new file mode 100644 index 0000000..afd64a0 --- /dev/null +++ b/source/src/Slackbot.Net.Endpoints/Models/Events/ReactionAddedEvent.cs @@ -0,0 +1,20 @@ +namespace Slackbot.Net.Endpoints.Models.Events; + +// https://docs.slack.dev/reference/events/reaction_added +public class ReactionAddedEvent : SlackEvent +{ + public string User { get; set; } + public string Reaction { get; set; } + public string Item_User { get; set; } + public string Event_Ts { get; set; } + public ReactionItem Item { get; set; } +} + +public class ReactionItem +{ + public string Type { get; set; } // message | file | file_comment + public string Channel { get; set; } // message + public string Ts { get; set; } // message + public string File { get; set; } // file, file_comment + public string File_Comment { get; set; } // file_comment +} diff --git a/source/src/Slackbot.Net.SlackClients.Http/ISlackClient.cs b/source/src/Slackbot.Net.SlackClients.Http/ISlackClient.cs index 2236925..0592905 100644 --- a/source/src/Slackbot.Net.SlackClients.Http/ISlackClient.cs +++ b/source/src/Slackbot.Net.SlackClients.Http/ISlackClient.cs @@ -6,6 +6,7 @@ using Slackbot.Net.SlackClients.Http.Models.Responses; using Slackbot.Net.SlackClients.Http.Models.Responses.ChatGetPermalink; using Slackbot.Net.SlackClients.Http.Models.Responses.ChatPostMessage; +using Slackbot.Net.SlackClients.Http.Models.Responses.ConversationsHistoryResponse; using Slackbot.Net.SlackClients.Http.Models.Responses.ConversationsList; using Slackbot.Net.SlackClients.Http.Models.Responses.ConversationsRepliesResponse; using Slackbot.Net.SlackClients.Http.Models.Responses.FileUpload; @@ -83,6 +84,13 @@ public interface ISlackClient /// https://api.slack.com/methods/conversations.list Task ConversationsReplies(string channel, string ts, int? limit = null, string cursor = null); + /// + /// Scopes required: channels:history/groups:history/im:history or mpim:history + /// Fetches a conversation's message history. Bot/app-authored messages carry Bot_Id and SubType. + /// + /// https://api.slack.com/methods/conversations.history + Task ConversationsHistory(string channel, int? limit = null, string cursor = null); + /// /// Scopes required: channels:manage | groups:write | im:write | mpim:write /// diff --git a/source/src/Slackbot.Net.SlackClients.Http/Models/Responses/ConversationsHistoryResponse/ConversationsHistoryResponse.cs b/source/src/Slackbot.Net.SlackClients.Http/Models/Responses/ConversationsHistoryResponse/ConversationsHistoryResponse.cs new file mode 100644 index 0000000..dc9ef83 --- /dev/null +++ b/source/src/Slackbot.Net.SlackClients.Http/Models/Responses/ConversationsHistoryResponse/ConversationsHistoryResponse.cs @@ -0,0 +1,11 @@ +using Slackbot.Net.SlackClients.Http.Models.Responses.ConversationsList; +using Slackbot.Net.SlackClients.Http.Models.Responses.ConversationsRepliesResponse; + +namespace Slackbot.Net.SlackClients.Http.Models.Responses.ConversationsHistoryResponse; + +public class ConversationsHistoryResponse : Response +{ + public Message[] Messages { get; set; } + public bool Has_More { get; set; } + public ResponseMetadata Response_Metadata { get; set; } +} diff --git a/source/src/Slackbot.Net.SlackClients.Http/Models/Responses/ConversationsRepliesResponse/ConversationsRepliesResponse.cs b/source/src/Slackbot.Net.SlackClients.Http/Models/Responses/ConversationsRepliesResponse/ConversationsRepliesResponse.cs index 2658c3d..5bf1dbb 100644 --- a/source/src/Slackbot.Net.SlackClients.Http/Models/Responses/ConversationsRepliesResponse/ConversationsRepliesResponse.cs +++ b/source/src/Slackbot.Net.SlackClients.Http/Models/Responses/ConversationsRepliesResponse/ConversationsRepliesResponse.cs @@ -13,4 +13,16 @@ public class Message public string Thread_Ts { get; set; } public string Parent_User_id { get; set; } public string Ts { get; set; } + + // Present on bot/app-authored messages. See https://docs.slack.dev/reference/events/message/bot_message + public string SubType { get; set; } + public string Bot_Id { get; set; } + public BotProfile Bot_Profile { get; set; } +} + +public class BotProfile +{ + public string Id { get; set; } + public string App_Id { get; set; } + public string Name { get; set; } } \ No newline at end of file diff --git a/source/src/Slackbot.Net.SlackClients.Http/SlackClient.cs b/source/src/Slackbot.Net.SlackClients.Http/SlackClient.cs index 64e7c35..ea871ba 100644 --- a/source/src/Slackbot.Net.SlackClients.Http/SlackClient.cs +++ b/source/src/Slackbot.Net.SlackClients.Http/SlackClient.cs @@ -8,6 +8,7 @@ using Slackbot.Net.SlackClients.Http.Models.Responses; using Slackbot.Net.SlackClients.Http.Models.Responses.ChatGetPermalink; using Slackbot.Net.SlackClients.Http.Models.Responses.ChatPostMessage; +using Slackbot.Net.SlackClients.Http.Models.Responses.ConversationsHistoryResponse; using Slackbot.Net.SlackClients.Http.Models.Responses.ConversationsList; using Slackbot.Net.SlackClients.Http.Models.Responses.ConversationsRepliesResponse; using Slackbot.Net.SlackClients.Http.Models.Responses.FileUpload; @@ -128,9 +129,29 @@ public async Task ConversationsReplies(string chan new KeyValuePair("limit", (limit ?? 1000).ToString()), new KeyValuePair("include_all_metadata", "true"), }; + if (cursor != null) + { + parameters.Add(new KeyValuePair("cursor", cursor)); + } return await _client.PostParametersAsForm(parameters, "conversations.replies", s => _logger.LogTrace(s)); } + /// + public async Task ConversationsHistory(string channel, int? limit = null, string cursor = null) + { + var parameters = new List> + { + new KeyValuePair("channel", channel), + new KeyValuePair("limit", (limit ?? 100).ToString()), + new KeyValuePair("include_all_metadata", "true"), + }; + if (cursor != null) + { + parameters.Add(new KeyValuePair("cursor", cursor)); + } + return await _client.PostParametersAsForm(parameters, "conversations.history", s => _logger.LogTrace(s)); + } + /// public async Task ConversationsOpen(string[] users) { diff --git a/source/test/Slackbot.Net.SlackClients.Http.Tests/ConversationsBotMessageDeserializationTests.cs b/source/test/Slackbot.Net.SlackClients.Http.Tests/ConversationsBotMessageDeserializationTests.cs new file mode 100644 index 0000000..2f2e184 --- /dev/null +++ b/source/test/Slackbot.Net.SlackClients.Http.Tests/ConversationsBotMessageDeserializationTests.cs @@ -0,0 +1,78 @@ +using Microsoft.Extensions.Logging; +using Slackbot.Net.SlackClients.Http; +using Slackbot.Net.Tests.Helpers; + +namespace Slackbot.Net.Tests; + +// Self-contained: deserializes realistic Slack payloads through the real SlackClient +// path (no network, no credentials), asserting the bot-author fields populate. +public class ConversationsBotMessageDeserializationTests(ITestOutputHelper helper) +{ + private ISlackClient ClientReturning(string json) => + new SlackClient( + new HttpClient(new StubHttpMessageHandler(json)) { BaseAddress = new Uri("https://slack.com/api/") }, + new XUnitLogger(helper)); + + // Based on https://docs.slack.dev/reference/events/message/bot_message + private const string BotMessageJson = """ + { + "type": "message", + "subtype": "bot_message", + "text": "Pushing is the answer", + "ts": "1358877455.000010", + "username": "github", + "bot_id": "BB12033", + "bot_profile": { "id": "BB12033", "app_id": "A123ABC456", "name": "github" } + } + """; + + private const string HumanMessageJson = """ + { "type": "message", "user": "U123ABC456", "text": "hello there", "ts": "1512085950.000216" } + """; + + [Fact] + public async Task ConversationsReplies_PopulatesBotFields_OnBotMessage() + { + var json = $$""" + { "ok": true, "messages": [ {{HumanMessageJson}}, {{BotMessageJson}} ], "has_more": false } + """; + var response = await ClientReturning(json).ConversationsReplies("C0EC3DG5N", "1358877455.000010"); + + Assert.True(response.Ok); + + var bot = Assert.Single(response.Messages, m => m.Bot_Id != null); + Assert.Equal("BB12033", bot.Bot_Id); + Assert.Equal("bot_message", bot.SubType); + Assert.NotNull(bot.Bot_Profile); + Assert.Equal("github", bot.Bot_Profile.Name); + Assert.Equal("A123ABC456", bot.Bot_Profile.App_Id); + + var human = Assert.Single(response.Messages, m => m.User == "U123ABC456"); + Assert.Null(human.Bot_Id); + Assert.Null(human.SubType); + Assert.Null(human.Bot_Profile); + } + + [Fact] + public async Task ConversationsHistory_PopulatesBotFields_AndPagination() + { + var json = $$""" + { + "ok": true, + "messages": [ {{BotMessageJson}}, {{HumanMessageJson}} ], + "has_more": true, + "response_metadata": { "next_cursor": "bmV4dF90czoxNTEyMDg1ODYxMDAwNTQz" } + } + """; + var response = await ClientReturning(json).ConversationsHistory("C0EC3DG5N"); + + Assert.True(response.Ok); + Assert.True(response.Has_More); + Assert.Equal("bmV4dF90czoxNTEyMDg1ODYxMDAwNTQz", response.Response_Metadata.Next_Cursor); + + var bot = Assert.Single(response.Messages, m => m.Bot_Id != null); + Assert.Equal("BB12033", bot.Bot_Id); + Assert.Equal("bot_message", bot.SubType); + Assert.Equal("github", bot.Bot_Profile.Name); + } +} diff --git a/source/test/Slackbot.Net.SlackClients.Http.Tests/Helpers/StubHttpMessageHandler.cs b/source/test/Slackbot.Net.SlackClients.Http.Tests/Helpers/StubHttpMessageHandler.cs new file mode 100644 index 0000000..98fb08d --- /dev/null +++ b/source/test/Slackbot.Net.SlackClients.Http.Tests/Helpers/StubHttpMessageHandler.cs @@ -0,0 +1,18 @@ +using System.Net; +using System.Text; + +namespace Slackbot.Net.Tests.Helpers; + +// Returns a canned JSON body for any request, so the real SlackClient deserialization +// path (including its JsonSerializerOptions/naming policy) runs without hitting Slack. +public class StubHttpMessageHandler(string json) : HttpMessageHandler +{ + protected override Task SendAsync(HttpRequestMessage request, + CancellationToken cancellationToken) + { + return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(json, Encoding.UTF8, "application/json") + }); + } +} From f28e7f76ddaeca2ae65d315af9fefd47b5420c8b Mon Sep 17 00:00:00 2001 From: Ole Magnus Urdahl Date: Fri, 29 May 2026 23:04:47 +0200 Subject: [PATCH 2/2] Replace SubType with App_Id; adjust tests Remove the unreliable SubType property from the ConversationsReplies Message model, add App_Id, and clarify that Bot_Id is the reliable bot detector. Update tests to stop asserting SubType and add a new test covering an app/bot message shape (bot_id + app_id, user present, no bot_profile). Also adjust existing tests to reflect the model change. --- .../ConversationsRepliesResponse.cs | 5 +-- ...ersationsBotMessageDeserializationTests.cs | 32 +++++++++++++++++-- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/source/src/Slackbot.Net.SlackClients.Http/Models/Responses/ConversationsRepliesResponse/ConversationsRepliesResponse.cs b/source/src/Slackbot.Net.SlackClients.Http/Models/Responses/ConversationsRepliesResponse/ConversationsRepliesResponse.cs index 5bf1dbb..81d0240 100644 --- a/source/src/Slackbot.Net.SlackClients.Http/Models/Responses/ConversationsRepliesResponse/ConversationsRepliesResponse.cs +++ b/source/src/Slackbot.Net.SlackClients.Http/Models/Responses/ConversationsRepliesResponse/ConversationsRepliesResponse.cs @@ -14,9 +14,10 @@ public class Message public string Parent_User_id { get; set; } public string Ts { get; set; } - // Present on bot/app-authored messages. See https://docs.slack.dev/reference/events/message/bot_message - public string SubType { get; set; } + // Populated on bot/app-authored messages. Reliable bot detection: Bot_Id != null. + // See https://docs.slack.dev/reference/events/message/bot_message public string Bot_Id { get; set; } + public string App_Id { get; set; } public BotProfile Bot_Profile { get; set; } } diff --git a/source/test/Slackbot.Net.SlackClients.Http.Tests/ConversationsBotMessageDeserializationTests.cs b/source/test/Slackbot.Net.SlackClients.Http.Tests/ConversationsBotMessageDeserializationTests.cs index 2f2e184..016ce2a 100644 --- a/source/test/Slackbot.Net.SlackClients.Http.Tests/ConversationsBotMessageDeserializationTests.cs +++ b/source/test/Slackbot.Net.SlackClients.Http.Tests/ConversationsBotMessageDeserializationTests.cs @@ -42,17 +42,44 @@ public async Task ConversationsReplies_PopulatesBotFields_OnBotMessage() var bot = Assert.Single(response.Messages, m => m.Bot_Id != null); Assert.Equal("BB12033", bot.Bot_Id); - Assert.Equal("bot_message", bot.SubType); Assert.NotNull(bot.Bot_Profile); Assert.Equal("github", bot.Bot_Profile.Name); Assert.Equal("A123ABC456", bot.Bot_Profile.App_Id); var human = Assert.Single(response.Messages, m => m.User == "U123ABC456"); Assert.Null(human.Bot_Id); - Assert.Null(human.SubType); Assert.Null(human.Bot_Profile); } + // Real conversations.replies shape for an app/bot user: bot_id + app_id set, + // user present, no bot_profile. Bot_Id is the reliable detector. + [Fact] + public async Task ConversationsReplies_AppBotMessage_PopulatesBotIdAndAppId() + { + var json = """ + { + "ok": true, + "messages": [ + { "user": "U8TRAM5DF", "bot_id": null, "app_id": null, "subtype": null, "ts": "1780072138.186269", "text": "<@U08SS4D12PQ> this is an app mention" }, + { "user": "U08SS4D12PQ", "bot_id": "B08SS4CUV0E", "app_id": "A08SX23BHS9", "subtype": null, "ts": "1780072145.328429", "text": "Hei! :wave:" } + ] + } + """; + var response = await ClientReturning(json).ConversationsReplies("C0EC3DG5N", "1780072138.186269"); + + Assert.True(response.Ok); + + var bot = Assert.Single(response.Messages, m => m.Bot_Id != null); + Assert.Equal("B08SS4CUV0E", bot.Bot_Id); + Assert.Equal("A08SX23BHS9", bot.App_Id); + Assert.Equal("U08SS4D12PQ", bot.User); // app-bot users still carry a user id + Assert.Null(bot.Bot_Profile); + + var human = Assert.Single(response.Messages, m => m.Bot_Id == null); + Assert.Equal("U8TRAM5DF", human.User); + Assert.Null(human.App_Id); + } + [Fact] public async Task ConversationsHistory_PopulatesBotFields_AndPagination() { @@ -72,7 +99,6 @@ public async Task ConversationsHistory_PopulatesBotFields_AndPagination() var bot = Assert.Single(response.Messages, m => m.Bot_Id != null); Assert.Equal("BB12033", bot.Bot_Id); - Assert.Equal("bot_message", bot.SubType); Assert.Equal("github", bot.Bot_Profile.Name); } }