diff --git a/SlackLineBridge.sln b/SlackLineBridge.sln index c66a84d..4376a4e 100644 --- a/SlackLineBridge.sln +++ b/SlackLineBridge.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29326.143 +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35222.181 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SlackLineBridge", "SlackLineBridge\SlackLineBridge.csproj", "{20565EFE-65C3-4EB1-8E09-7C9A9324BB66}" EndProject diff --git a/SlackLineBridge/Controllers/WebhookController.cs b/SlackLineBridge/Controllers/WebhookController.cs index b421058..cdfcae3 100644 --- a/SlackLineBridge/Controllers/WebhookController.cs +++ b/SlackLineBridge/Controllers/WebhookController.cs @@ -2,23 +2,20 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Dynamic; using System.IO; using System.Linq; using System.Net.Http; using System.Text; using System.Text.Json; -using System.Text.Json.Serialization.Samples; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Web; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; -using SlackLineBridge.Models; using SlackLineBridge.Models.Configurations; +using SlackLineBridge.Services; using SlackLineBridge.Utils; using static System.Text.Json.Serialization.Samples.JsonSerializerExtensions; @@ -31,6 +28,7 @@ public partial class WebhookController( IOptionsSnapshot slackChannels, IOptionsSnapshot lineChannels, IOptionsSnapshot bridges, + LineMessageProcessingService lineMessageProcessingService, ConcurrentQueue<(string signature, string body, string host)> lineRequestQueue, IHttpClientFactory clientFactory, SlackSigningSecret slackSigningSecret, @@ -40,6 +38,7 @@ public partial class WebhookController( private readonly LineChannels _lineChannels = lineChannels.Value; private readonly SlackLineBridges _bridges = bridges.Value; private readonly string _slackSigningSecret = slackSigningSecret.Secret; + private readonly LineMessageProcessingService _lineMessageProcessingService = lineMessageProcessingService; [HttpPost("/slack2")] public async Task Slack2() @@ -167,59 +166,72 @@ private async Task PushToLine(string host, SlackChannel slackChan } { - var message = new + var messages = new List { - type = "text", - altText = text, - text, - sender = new + new { - name = userName, - iconUrl = $"https://{host}/proxy/slack/{Crypt.GetHMACHex(userIconUrl, _slackSigningSecret)}/{HttpUtility.UrlEncode(userIconUrl)}" - }, + type = "text", + altText = text, + text, + sender = new + { + name = userName, + iconUrl = $"https://{host}/proxy/slack/{Crypt.GetHMACHex(userIconUrl, _slackSigningSecret)}/{HttpUtility.UrlEncode(userIconUrl)}" + }, + } }; - var json = new + if (files != null) { - to = lineChannel.Id, - messages = new dynamic[] + var fileMessages = files.Where(x => x.mimeType.StartsWith("image")).Select(file => { - message - }.ToArray() - }; - var jsonStr = JsonSerializer.Serialize(json); - logger.LogInformation("Push message to LINE: {jsonStr}", jsonStr); - var result = await client.PostAsync($"message/push", new StringContent(jsonStr, Encoding.UTF8, "application/json")); - logger.LogInformation("LINE API result [{result.StatusCode}]: {result.Content}", result.StatusCode, await result.Content.ReadAsStringAsync()); - } + var urlPrivate = file.urlPrivate; + var urlThumb360 = file.thumb360; + return new + { + type = "image", + originalContentUrl = $"https://{host}/proxy/slack/{Crypt.GetHMACHex(urlPrivate, _slackSigningSecret)}/{HttpUtility.UrlEncode(urlPrivate)}", + previewImageUrl = $"https://{host}/proxy/slack/{Crypt.GetHMACHex(urlThumb360, _slackSigningSecret)}/{HttpUtility.UrlEncode(urlThumb360)}", + sender = new + { + name = userName, + iconUrl = $"https://{host}/proxy/slack/{Crypt.GetHMACHex(userIconUrl, _slackSigningSecret)}/{HttpUtility.UrlEncode(userIconUrl)}" + }, + }; + }); + messages.AddRange(fileMessages); + } - if (files != null) - { - var messages = files.Where(x => x.mimeType.StartsWith("image")).Select(file => + var replyToken = _lineMessageProcessingService.GetReplyToken(lineChannel.Id); + if (replyToken != null) { - var urlPrivate = file.urlPrivate; - var urlThumb360 = file.thumb360; - return new + var json = new { - type = "image", - originalContentUrl = $"https://{host}/proxy/slack/{Crypt.GetHMACHex(urlPrivate, _slackSigningSecret)}/{HttpUtility.UrlEncode(urlPrivate)}", - previewImageUrl = $"https://{host}/proxy/slack/{Crypt.GetHMACHex(urlThumb360, _slackSigningSecret)}/{HttpUtility.UrlEncode(urlThumb360)}", - sender = new - { - name = userName, - iconUrl = $"https://{host}/proxy/slack/{Crypt.GetHMACHex(userIconUrl, _slackSigningSecret)}/{HttpUtility.UrlEncode(userIconUrl)}" - }, + replyToken, + messages = messages.ToArray() }; - }); - var json = new + var jsonStr = JsonSerializer.Serialize(json); + logger.LogInformation("Push message to LINE (using replyToken): {jsonStr}", jsonStr); + var result = await client.PostAsync($"message/reply", new StringContent(jsonStr, Encoding.UTF8, "application/json")); + logger.LogInformation("LINE API result [{result.StatusCode}]: {result.Content}", result.StatusCode, await result.Content.ReadAsStringAsync()); + if (result.IsSuccessStatusCode) + { + continue; + } + } + + // 通常のプッシュメッセージにフォールバック { - to = lineChannel.Id, - messages = messages.ToArray() - }; - var jsonStr = JsonSerializer.Serialize(json); - logger.LogInformation("Push images to LINE: {jsonStr}", jsonStr); - var result = await client.PostAsync($"message/push", new StringContent(jsonStr, Encoding.UTF8, "application/json")); - logger.LogInformation("LINE API result [{result.StatusCode}]: {result.Content}", result.StatusCode, await result.Content.ReadAsStringAsync()); + var json = new + { + to = lineChannel.Id, + messages = messages.ToArray() + }; + var jsonStr = JsonSerializer.Serialize(json); + logger.LogInformation("Push message to LINE: {jsonStr}", jsonStr); + var result = await client.PostAsync($"message/push", new StringContent(jsonStr, Encoding.UTF8, "application/json")); + logger.LogInformation("LINE API result [{result.StatusCode}]: {result.Content}", result.StatusCode, await result.Content.ReadAsStringAsync()); + } } } return Ok(); diff --git a/SlackLineBridge/Program.cs b/SlackLineBridge/Program.cs index 392adbd..001f668 100644 --- a/SlackLineBridge/Program.cs +++ b/SlackLineBridge/Program.cs @@ -1,12 +1,7 @@ -using System; -using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; namespace SlackLineBridge { @@ -17,17 +12,36 @@ public static void Main(string[] args) CreateHostBuilder(args).Build().Run(); } - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) + public static IHostBuilder CreateHostBuilder(string[] args) + { + bool useSentry = false; + string sentryDsn = ""; + return Host.CreateDefaultBuilder(args) .ConfigureAppConfiguration((hostingContext, config) => { config.SetBasePath(Directory.GetCurrentDirectory()); config.AddJsonFile("config.json", false, true); config.AddJsonFile("appsettings.AWS.json", true, true); + var buildConfig = config.Build(); + useSentry = buildConfig.GetValue("Sentry:UseSentry"); + if (useSentry) + { + sentryDsn = buildConfig.GetValue("Sentry:Dsn"); + } }) .ConfigureWebHostDefaults(webBuilder => { + if (useSentry) + { + webBuilder.UseSentry(o => + { + o.Dsn = sentryDsn; + o.TracesSampleRate = 0; + o.SendDefaultPii = true; + }); + } webBuilder.UseStartup(); }); + } } } diff --git a/SlackLineBridge/Services/LineMessageProcessingService.cs b/SlackLineBridge/Services/LineMessageProcessingService.cs index fbcee80..cf72a9f 100644 --- a/SlackLineBridge/Services/LineMessageProcessingService.cs +++ b/SlackLineBridge/Services/LineMessageProcessingService.cs @@ -1,7 +1,6 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using SlackLineBridge.Models; using SlackLineBridge.Models.Configurations; using SlackLineBridge.Utils; using System; @@ -10,8 +9,6 @@ using System.Dynamic; using System.Linq; using System.Net.Http; -using System.Security.Cryptography; -using System.Security.Policy; using System.Text; using System.Text.Json; using System.Threading; @@ -28,6 +25,8 @@ public class LineMessageProcessingService : BackgroundService private readonly ILogger _logger; private readonly ConcurrentQueue<(string signature, string body, string host)> _queue; private readonly string _lineChannelSecret; + // key: LINE channel id, value: last reply token + private readonly ConcurrentDictionary _lastReplyTokens = []; public LineMessageProcessingService( IOptionsMonitor slackChannels, @@ -86,6 +85,14 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) { continue; } + if (e.TryGetProperty("replyToken", out var replyTokenElement)) + { + var replyToken = replyTokenElement.GetString(); + if (!string.IsNullOrEmpty(replyToken)) + { + _lastReplyTokens.AddOrUpdate(lineChannel.Id, replyToken, (key, value) => replyToken); + } + } var (userName, pictureUrl) = await GetLineProfileAsync(e); var message = e.GetProperty("message"); @@ -154,6 +161,16 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) _logger.LogDebug($"LineMessageProcessing background task is stopped."); } + public string GetReplyToken(string lineChannelID) + { + if (_lastReplyTokens.TryRemove(lineChannelID, out var token)) + { + return token; + } + + return null; + } + private async Task SendToSlack(string webhookUrl, string channelId, string pictureUrl, string userName, string text, string imageUrl) { var client = _clientFactory.CreateClient(); diff --git a/SlackLineBridge/SlackLineBridge.csproj b/SlackLineBridge/SlackLineBridge.csproj index 8c74c2b..fc067e5 100644 --- a/SlackLineBridge/SlackLineBridge.csproj +++ b/SlackLineBridge/SlackLineBridge.csproj @@ -6,10 +6,11 @@ - - - - + + + + + diff --git a/SlackLineBridge/Startup.cs b/SlackLineBridge/Startup.cs index ff4fe56..60025ce 100644 --- a/SlackLineBridge/Startup.cs +++ b/SlackLineBridge/Startup.cs @@ -48,7 +48,6 @@ public void ConfigureServices(IServiceCollection services) x.AddAWSProvider(awsLoggingConfig); } }); - services.AddControllers(); services.AddHttpClient("Line", c => { c.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Configuration["lineAccessToken"]); @@ -67,8 +66,10 @@ public void ConfigureServices(IServiceCollection services) var jsonOptions = new JsonSerializerOptions(); jsonOptions.EnableDynamicTypes(); services.AddSingleton(jsonOptions); - services.AddHostedService(); + services.AddSingleton(); + services.AddHostedService(sp=>sp.GetRequiredService()); services.AddHostedService(); + services.AddControllers(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.