From 2516129da1467fd1aabd47dc9761c6afd7889ea1 Mon Sep 17 00:00:00 2001 From: John Johansson <1345217+we4sz@users.noreply.github.com> Date: Tue, 13 Oct 2026 07:51:34 +0200 Subject: [PATCH 1/3] Adding PreviousMessage to MessageDeleted --- SlackNet.Tests/MessageDeletedTests.cs | 50 ++++++++++++++++++++++ SlackNet/Events/Messages/MessageDeleted.cs | 4 ++ 2 files changed, 54 insertions(+) create mode 100644 SlackNet.Tests/MessageDeletedTests.cs diff --git a/SlackNet.Tests/MessageDeletedTests.cs b/SlackNet.Tests/MessageDeletedTests.cs new file mode 100644 index 00000000..72382f27 --- /dev/null +++ b/SlackNet.Tests/MessageDeletedTests.cs @@ -0,0 +1,50 @@ +using EasyAssertions; +using Newtonsoft.Json; +using NUnit.Framework; +using SlackNet.Events; + +namespace SlackNet.Tests; + +public class MessageDeletedTests +{ + private JsonSerializerSettings _sut; + + [SetUp] + public void SetUp() + { + _sut = Default.JsonSettings(Default.SlackTypeResolver(Default.AssembliesContainingSlackTypes)).SerializerSettings; + } + + [Test] + public void MessageDeleted_DeserializedWithPreviousMessage() + { + var result = Deserialize(@"{ + ""type"": ""message"", + ""subtype"": ""message_deleted"", + ""hidden"": true, + ""channel"": ""C123ABC456"", + ""ts"": ""1358878755.000001"", + ""deleted_ts"": ""1358878749.000002"", + ""event_ts"": ""1358878755.000001"", + ""previous_message"": { ""type"": ""message"", ""user"": ""U123ABC456"", ""text"": ""Hello"", ""ts"": ""1358878749.000002"", ""thread_ts"": ""1358878700.000001"" } + }"); + + var deleted = result.ShouldBeA().And; + deleted.DeletedTs.ShouldBe("1358878749.000002"); + deleted.PreviousMessage.User.ShouldBe("U123ABC456"); + deleted.PreviousMessage.Text.ShouldBe("Hello"); + deleted.PreviousMessage.Ts.ShouldBe("1358878749.000002"); + deleted.PreviousMessage.ThreadTs.ShouldBe("1358878700.000001"); + deleted.ExtraProperties.ContainsKey("previous_message").ShouldBe(false); + } + + [Test] + public void MessageDeleted_WithoutPreviousMessage_HasNone() + { + var result = Deserialize(@"{ ""type"": ""message"", ""subtype"": ""message_deleted"", ""hidden"": true, ""channel"": ""C123ABC456"", ""ts"": ""1358878755.000001"", ""deleted_ts"": ""1358878749.000002"" }"); + + result.ShouldBeA().And.PreviousMessage.ShouldBeNull(); + } + + private T Deserialize(string json) => JsonConvert.DeserializeObject(json, _sut); +} diff --git a/SlackNet/Events/Messages/MessageDeleted.cs b/SlackNet/Events/Messages/MessageDeleted.cs index 8c78dc8a..23dc2fd3 100644 --- a/SlackNet/Events/Messages/MessageDeleted.cs +++ b/SlackNet/Events/Messages/MessageDeleted.cs @@ -15,4 +15,8 @@ public class MessageDeleted : MessageEvent public string DeletedTs { get; set; } [JsonIgnore] public DateTime DeleteTimestamp => DeletedTs.ToDateTime().GetValueOrDefault(); + /// + /// The message that was deleted. + /// + public MessageEvent PreviousMessage { get; set; } } \ No newline at end of file From dc4bdd27fa6dc524bb6e0686b274478b1f8f4e0a Mon Sep 17 00:00:00 2001 From: John Johansson <1345217+we4sz@users.noreply.github.com> Date: Tue, 13 Oct 2026 07:51:34 +0200 Subject: [PATCH 2/3] Adding UserTeam and SourceTeam to MessageEventBase --- SlackNet.Tests/MessageTeamTests.cs | 67 ++++++++++++++++++++ SlackNet/Events/Messages/MessageEventBase.cs | 8 +++ 2 files changed, 75 insertions(+) create mode 100644 SlackNet.Tests/MessageTeamTests.cs diff --git a/SlackNet.Tests/MessageTeamTests.cs b/SlackNet.Tests/MessageTeamTests.cs new file mode 100644 index 00000000..42eb1170 --- /dev/null +++ b/SlackNet.Tests/MessageTeamTests.cs @@ -0,0 +1,67 @@ +using EasyAssertions; +using Newtonsoft.Json; +using NUnit.Framework; +using SlackNet.Events; + +namespace SlackNet.Tests; + +public class MessageTeamTests +{ + private JsonSerializerSettings _sut; + + [SetUp] + public void SetUp() + { + _sut = Default.JsonSettings(Default.SlackTypeResolver(Default.AssembliesContainingSlackTypes)).SerializerSettings; + } + + [Test] + public void MessageEvent_FromSharedChannel_HasUserTeamAndSourceTeam() + { + var result = Deserialize(@"{ ""type"": ""message"", ""user"": ""U123ABC456"", ""text"": ""Hello"", ""ts"": ""1355517523.000005"", ""team"": ""T111"", ""user_team"": ""T222"", ""source_team"": ""T333"" }"); + + var message = result.ShouldBeA().And; + message.Team.ShouldBe("T111"); + message.UserTeam.ShouldBe("T222"); + message.SourceTeam.ShouldBe("T333"); + message.ExtraProperties.ShouldBeEmpty(); + } + + [Test] + public void MessageEvent_FromUnsharedChannel_HasNoUserTeamOrSourceTeam() + { + var result = Deserialize(@"{ ""type"": ""message"", ""user"": ""U123ABC456"", ""text"": ""Hello"", ""ts"": ""1355517523.000005"", ""team"": ""T111"" }"); + + var message = result.ShouldBeA().And; + message.UserTeam.ShouldBeNull(); + message.SourceTeam.ShouldBeNull(); + } + + [Test] + public void MessageChanged_MessagesHaveUserTeamAndSourceTeam() + { + var result = Deserialize(@"{ + ""type"": ""message"", + ""subtype"": ""message_changed"", + ""channel"": ""C123ABC456"", + ""message"": { ""type"": ""message"", ""user"": ""U123ABC456"", ""text"": ""Hello again"", ""ts"": ""1355517523.000005"", ""user_team"": ""T222"", ""source_team"": ""T333"" }, + ""previous_message"": { ""type"": ""message"", ""user"": ""U123ABC456"", ""text"": ""Hello"", ""ts"": ""1355517523.000005"", ""user_team"": ""T222"", ""source_team"": ""T333"" } + }"); + + var changed = result.ShouldBeA().And; + changed.Message.UserTeam.ShouldBe("T222"); + changed.PreviousMessage.SourceTeam.ShouldBe("T333"); + } + + [Test] + public void AppMention_FromSharedChannel_HasUserTeamAndSourceTeam() + { + var result = Deserialize(@"{ ""type"": ""app_mention"", ""user"": ""U123ABC456"", ""text"": ""<@U0LAN0Z89> hi"", ""ts"": ""1355517523.000005"", ""user_team"": ""T222"", ""source_team"": ""T333"" }"); + + var mention = result.ShouldBeA().And; + mention.UserTeam.ShouldBe("T222"); + mention.SourceTeam.ShouldBe("T333"); + } + + private T Deserialize(string json) => JsonConvert.DeserializeObject(json, _sut); +} diff --git a/SlackNet/Events/Messages/MessageEventBase.cs b/SlackNet/Events/Messages/MessageEventBase.cs index 900c93c6..2ec8e9d0 100644 --- a/SlackNet/Events/Messages/MessageEventBase.cs +++ b/SlackNet/Events/Messages/MessageEventBase.cs @@ -12,6 +12,14 @@ public abstract class MessageEventBase : Event public string Text { get; set; } public string Ts { get; set; } public string Team { get; set; } + /// + /// The workspace of the user who sent the message. Only sent when the message comes from a channel that is shared between workspaces. + /// + public string UserTeam { get; set; } + /// + /// The workspace the message was sent from. Only sent when the message comes from a channel that is shared between workspaces. + /// + public string SourceTeam { get; set; } public bool DisplayAsBot { get; set; } [JsonIgnore] public DateTime Timestamp => Ts.ToDateTime().GetValueOrDefault(); From 837293932e563f5bb385345a0eee7d5b854608da Mon Sep 17 00:00:00 2001 From: John Johansson <1345217+we4sz@users.noreply.github.com> Date: Tue, 13 Oct 2026 07:51:34 +0200 Subject: [PATCH 3/3] Adding MetadataJson and MetadataObject to MessageUpdate --- SlackNet.Tests/ChatUpdateMetadataTests.cs | 90 ++++++++++++++++++++++ SlackNet/WebApi/ChatApi.cs | 3 +- SlackNet/WebApi/Responses/MessageUpdate.cs | 10 +++ 3 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 SlackNet.Tests/ChatUpdateMetadataTests.cs diff --git a/SlackNet.Tests/ChatUpdateMetadataTests.cs b/SlackNet.Tests/ChatUpdateMetadataTests.cs new file mode 100644 index 00000000..ff291c0a --- /dev/null +++ b/SlackNet.Tests/ChatUpdateMetadataTests.cs @@ -0,0 +1,90 @@ +#nullable enable +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using EasyAssertions; +using Newtonsoft.Json.Linq; +using NUnit.Framework; +using SlackNet.WebApi; + +namespace SlackNet.Tests; + +public class ChatUpdateMetadataTests +{ + private RecordingHttp _http = null!; + private ISlackApiClient _sut = null!; + + [SetUp] + public void SetUp() + { + var jsonSettings = Default.JsonSettings(Default.SlackTypeResolver(Default.AssembliesContainingSlackTypes)); + _http = new RecordingHttp(); + _sut = new SlackApiClient(_http, Default.UrlBuilder(jsonSettings), jsonSettings, "token"); + } + + [Test] + public async Task Update_WithoutMetadata_SendsNoMetadata() + { + await _sut.Chat.Update(new MessageUpdate { ChannelId = "C123", Ts = "1503435956.000247", Text = "edited" }); + + _http.Method.ShouldBe("chat.update"); + _http.Body.ContainsKey("metadata").ShouldBe(false); + } + + [Test] + public async Task Update_WithMetadataJson_SendsMetadata() + { + await _sut.Chat.Update(new MessageUpdate + { + ChannelId = "C123", + Ts = "1503435956.000247", + Text = "edited", + MetadataJson = new MessageMetadata { EventType = "task_created", EventPayload = JObject.Parse(@"{ ""id"": ""TK-42"" }") } + }); + + _http.Body["channel"]!.Value().ShouldBe("C123"); + _http.Body["text"]!.Value().ShouldBe("edited"); + JToken.DeepEquals(JObject.Parse(@"{ ""event_type"": ""task_created"", ""event_payload"": { ""id"": ""TK-42"" } }"), _http.Body["metadata"]).ShouldBe(true); + } + + [Test] + public async Task Update_WithMetadataObject_SendsItAsMetadata() + { + await _sut.Chat.Update(new MessageUpdate { ChannelId = "C123", Ts = "1503435956.000247", MetadataObject = new TaskCreated { TaskId = "TK-42" } }); + + JToken.DeepEquals(JObject.Parse(@"{ ""event_type"": ""task_created"", ""event_payload"": { ""task_id"": ""TK-42"" } }"), _http.Body["metadata"]).ShouldBe(true); + } + + [Test] + public async Task Update_WithMetadataJsonAndMetadataObject_SendsMetadataJson() + { + await _sut.Chat.Update(new MessageUpdate + { + ChannelId = "C123", + Ts = "1503435956.000247", + MetadataJson = new MessageMetadata { EventType = "from_json", EventPayload = new JObject() }, + MetadataObject = new TaskCreated { TaskId = "TK-42" } + }); + + _http.Body["metadata"]!["event_type"]!.Value().ShouldBe("from_json"); + } + + [SlackType("task_created")] + private class TaskCreated + { + public string TaskId { get; set; } = ""; + } + + private class RecordingHttp : IHttp + { + public string Method { get; private set; } = ""; + public JObject Body { get; private set; } = new(); + + public async Task Execute(HttpRequestMessage requestMessage, CancellationToken cancellationToken = default) + { + Method = requestMessage.RequestUri!.Segments[^1]; + Body = JObject.Parse(await requestMessage.Content!.ReadAsStringAsync().ConfigureAwait(false)); + return (T)(object)new WebApiResponse { Ok = true, Data = new JObject { ["ok"] = true } }; + } + } +} diff --git a/SlackNet/WebApi/ChatApi.cs b/SlackNet/WebApi/ChatApi.cs index b6d8cbb1..b79896b1 100644 --- a/SlackNet/WebApi/ChatApi.cs +++ b/SlackNet/WebApi/ChatApi.cs @@ -329,7 +329,8 @@ public Task Update(MessageUpdate messageUpdate, Cancellat { "parse", messageUpdate.Parse }, { "link_names", messageUpdate.LinkNames }, { "as_user", messageUpdate.AsUser }, - { "file_ids", messageUpdate.FileIds } + { "file_ids", messageUpdate.FileIds }, + { "metadata", messageUpdate.MetadataJson ?? MessageMetadata.FromObject(messageUpdate.MetadataObject, jsonSettings) } }, cancellationToken); diff --git a/SlackNet/WebApi/Responses/MessageUpdate.cs b/SlackNet/WebApi/Responses/MessageUpdate.cs index 65c3854d..bee50f2b 100644 --- a/SlackNet/WebApi/Responses/MessageUpdate.cs +++ b/SlackNet/WebApi/Responses/MessageUpdate.cs @@ -42,4 +42,14 @@ public class MessageUpdate /// Array of new file ids that will be sent with this message. /// public IList FileIds { get; set; } + /// + /// A custom event attached to the message. Leave as null to leave the existing metadata alone. + /// + /// Will take precedence over . + public MessageMetadata MetadataJson { get; set; } + /// + /// A custom event attached to the message. Leave as null to leave the existing metadata alone. + /// + /// The specified object will be automatically converted to a using the standard Slack JSON conventions. + public object MetadataObject { get; set; } } \ No newline at end of file