forked from teocomi/BCFier
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add backend to send teams messages via webhook
- Loading branch information
1 parent
407584c
commit 779e24c
Showing
8 changed files
with
259 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} | ||
} |
16 changes: 16 additions & 0 deletions
16
src/IPA.Bcfier.App/Models/Controllers/TeamsMessages/NewCommentPost.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
13
src/IPA.Bcfier.App/Models/Services/TeamsMessages/TeamsFact.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
10
src/IPA.Bcfier.App/Models/Services/TeamsMessages/TeamsImage.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
23
src/IPA.Bcfier.App/Models/Services/TeamsMessages/TeamsMessage.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
19
src/IPA.Bcfier.App/Models/Services/TeamsMessages/TeamsSection.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters