Skip to content

Commit

Permalink
Add backend to send teams messages via webhook
Browse files Browse the repository at this point in the history
  • Loading branch information
GeorgDangl committed Apr 30, 2024
1 parent 407584c commit 779e24c
Show file tree
Hide file tree
Showing 8 changed files with 259 additions and 0 deletions.
27 changes: 27 additions & 0 deletions src/IPA.Bcfier.App/Controllers/TeamsMessagesController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using IPA.Bcfier.App.Models.Controllers.TeamsMessages;
using IPA.Bcfier.App.Services;
using Microsoft.AspNetCore.Mvc;
using System.Net;

namespace IPA.Bcfier.App.Controllers
{
[ApiController]
[Route("api/teams-messages")]
public class TeamsMessagesController : ControllerBase
{
private readonly TeamsMessagesService _teamsMessagesService;

public TeamsMessagesController(TeamsMessagesService teamsMessagesService)
{
_teamsMessagesService = teamsMessagesService;
}

[HttpPost("projects/{projectId}/topics/{topicId}/comments")]
[ProducesResponseType((int)HttpStatusCode.NoContent)]
public async Task<IActionResult> AnnounceNewCommentInProjectTopicAsync(Guid projectId, Guid topicId, [FromBody] TeamsMessagePost model)
{
await _teamsMessagesService.AnnounceNewCommentInProjectTopicAsync(projectId, model);
return NoContent();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations;

namespace IPA.Bcfier.App.Models.Controllers.TeamsMessages
{
public class TeamsMessagePost
{
public string? TopicTitle { get; set; }

public string? Comment { get; set; }

public string? ViewpointBase64 { get; set; }

[Required]
public string Username { get; set; } = string.Empty;
}
}
13 changes: 13 additions & 0 deletions src/IPA.Bcfier.App/Models/Services/TeamsMessages/TeamsFact.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Newtonsoft.Json;

namespace IPA.Bcfier.App.Models.Services.TeamsMessages
{
public class TeamsFact
{
[JsonProperty("name")]
public string Name { get; set; } = string.Empty;

[JsonProperty("value")]
public string Value { get; set; } = string.Empty;
}
}
10 changes: 10 additions & 0 deletions src/IPA.Bcfier.App/Models/Services/TeamsMessages/TeamsImage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Newtonsoft.Json;

namespace IPA.Bcfier.App.Models.Services.TeamsMessages
{
public class TeamsImage
{
[JsonProperty("image")]
public string ImageBase64DataUrl { get; set; } = string.Empty;
}
}
23 changes: 23 additions & 0 deletions src/IPA.Bcfier.App/Models/Services/TeamsMessages/TeamsMessage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Newtonsoft.Json;

namespace IPA.Bcfier.App.Models.Services.TeamsMessages
{
public class TeamsMessage
{
[JsonProperty("@type")]
public string Type { get; } = "MessageCard";

[JsonProperty("@context")]
public string Context { get; } = "http://schema.org/extensions";

[JsonProperty("themeColor")]
public string ThemeColor { get; } = "0076D7";
//public string ThemeColor { get; } = "eed300";

[JsonProperty("summary")]
public string Summary { get; set; } = string.Empty;

[JsonProperty("sections")]
public List<TeamsSection> Sections { get; set; } = new List<TeamsSection>();
}
}
19 changes: 19 additions & 0 deletions src/IPA.Bcfier.App/Models/Services/TeamsMessages/TeamsSection.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Newtonsoft.Json;

namespace IPA.Bcfier.App.Models.Services.TeamsMessages
{
public class TeamsSection
{
[JsonProperty("activityTitle")]
public string? ActivityTitle { get; set; }

[JsonProperty("activitySubtitle")]
public string? ActivitySubtitle { get; set; }

[JsonProperty("facts")]
public List<TeamsFact>? Facts { get; set; }

[JsonProperty("images")]
public List<TeamsImage>? Images { get; set; }
}
}
150 changes: 150 additions & 0 deletions src/IPA.Bcfier.App/Services/TeamsMessagesService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
using IPA.Bcfier.App.Data;
using IPA.Bcfier.App.Models.Controllers.TeamsMessages;
using IPA.Bcfier.App.Models.Services.TeamsMessages;
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Processing;
using System.Text;

namespace IPA.Bcfier.App.Services
{
public class TeamsMessagesService
{
private readonly BcfierDbContext _context;

public TeamsMessagesService(BcfierDbContext context)
{
_context = context;
}

public async Task AnnounceNewCommentInProjectTopicAsync(Guid projectId,
TeamsMessagePost message)
{
var dbProject = await _context
.Projects
.Where(p => p.Id == projectId)
.Select(p => new
{
p.TeamsWebhook,
p.Name
})
.FirstOrDefaultAsync();
if (dbProject == default || string.IsNullOrWhiteSpace(dbProject.TeamsWebhook))
{
// Not doing anything for projects that either don't exist
// or don't have a webhook configured
return;
}

var teamsMessage = new TeamsMessage();
var title = !string.IsNullOrWhiteSpace(message.TopicTitle)
? "New topic: " + message.TopicTitle
: (!string.IsNullOrWhiteSpace(message.Comment)
? "New comment: " + message.Comment
: "New viewpoint");

var mainSection = new TeamsSection
{
ActivityTitle = title,
ActivitySubtitle = "Project: " + dbProject.Name,
Facts = new List<TeamsFact>
{
new TeamsFact
{
Name = "Author",
Value = message.Username
}
}
};
teamsMessage.Sections = new List<TeamsSection> { mainSection };

if (!string.IsNullOrWhiteSpace(message.ViewpointBase64))
{
teamsMessage.Sections.Add(new TeamsSection
{
Images = new List<TeamsImage>
{
new TeamsImage
{
ImageBase64DataUrl = "data:image/png;base64," + message.ViewpointBase64
}
}
});
}

await SendTeamsMessageAsync(teamsMessage, dbProject.TeamsWebhook);
}

private async Task SendTeamsMessageAsync(TeamsMessage message,
string teamsWebhookUrl)
{
// Apparently, the max message size for Teams webhooks is 28.000 characters
// so we're ensuring we resize images to fit in there
var messageJson = GetTeamsMessageWithMaxSizeInBytes(message, 28_000);
if (string.IsNullOrWhiteSpace(messageJson))
{
// Looks like the message was too large for the webhook
return;
}

using var httpClient = new HttpClient();
var body = new StringContent(messageJson, Encoding.Default, "application/json");
var request = new HttpRequestMessage(HttpMethod.Post, teamsWebhookUrl);
request.Content = body;

await httpClient.SendAsync(request);
}

private static string? GetTeamsMessageWithMaxSizeInBytes(TeamsMessage message,
int maxMessageLength)
{
var jsonOptions = new JsonSerializerSettings();
jsonOptions.Formatting = Formatting.None;
jsonOptions.NullValueHandling = NullValueHandling.Ignore;
var messageJson = JsonConvert.SerializeObject(message, jsonOptions);

if (messageJson.Length <= maxMessageLength)
{
return messageJson;
}

var maxTries = 5;
var currentTry = 0;
while (currentTry < maxTries)
{
maxTries++;
// Let's compress the images
foreach (var section in message.Sections.Where(s => s.Images != null))
{
foreach (var imageSection in section.Images.Where(image => !string.IsNullOrWhiteSpace(image.ImageBase64DataUrl)))
{
var imageBase64 = imageSection.ImageBase64DataUrl.Substring(imageSection.ImageBase64DataUrl.IndexOf(",") + 1);
var imageBytes = Convert.FromBase64String(imageBase64);

using var image = SixLabors.ImageSharp.Image.Load(new MemoryStream(imageBytes));
var width = image.Width / 2;
var height = image.Height / 2;
image.Mutate(x => x.Resize(width, height));
using var outMemStream = new MemoryStream();
image.Save(outMemStream, new PngEncoder());
imageSection.ImageBase64DataUrl = "data:image/png;base64," + Convert.ToBase64String(outMemStream.ToArray());
}
}

messageJson = JsonConvert.SerializeObject(message, jsonOptions);

if (messageJson.Length <= maxMessageLength)
{
return messageJson;
}
}

// If we're still to big, there's nothing we can do - we'll
// not keep decreasing, since we already halved it 5 times, so it
// probably was way too big anyway for anything practical here

return null;
}
}
}
1 change: 1 addition & 0 deletions src/IPA.Bcfier/IPA.Bcfier.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

<ItemGroup>
<PackageReference Include="Dangl.BCF" Version="2.0.0-beta0022" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.4" />
</ItemGroup>

</Project>

0 comments on commit 779e24c

Please sign in to comment.