Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions SlackLineBridge.sln
Original file line number Diff line number Diff line change
@@ -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
Expand Down
104 changes: 58 additions & 46 deletions SlackLineBridge/Controllers/WebhookController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -31,6 +28,7 @@ public partial class WebhookController(
IOptionsSnapshot<SlackChannels> slackChannels,
IOptionsSnapshot<LineChannels> lineChannels,
IOptionsSnapshot<SlackLineBridges> bridges,
LineMessageProcessingService lineMessageProcessingService,
ConcurrentQueue<(string signature, string body, string host)> lineRequestQueue,
IHttpClientFactory clientFactory,
SlackSigningSecret slackSigningSecret,
Expand All @@ -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<IActionResult> Slack2()
Expand Down Expand Up @@ -167,59 +166,72 @@ private async Task<IActionResult> PushToLine(string host, SlackChannel slackChan
}

{
var message = new
var messages = new List<dynamic>
{
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();
Expand Down
28 changes: 21 additions & 7 deletions SlackLineBridge/Program.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand All @@ -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<bool>("Sentry:UseSentry");
if (useSentry)
{
sentryDsn = buildConfig.GetValue<string>("Sentry:Dsn");
}
})
.ConfigureWebHostDefaults(webBuilder =>
{
if (useSentry)
{
webBuilder.UseSentry(o =>
{
o.Dsn = sentryDsn;
o.TracesSampleRate = 0;
o.SendDefaultPii = true;
});
}
webBuilder.UseStartup<Startup>();
});
}
}
}
23 changes: 20 additions & 3 deletions SlackLineBridge/Services/LineMessageProcessingService.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -28,6 +25,8 @@ public class LineMessageProcessingService : BackgroundService
private readonly ILogger<LineMessageProcessingService> _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<string, string> _lastReplyTokens = [];

public LineMessageProcessingService(
IOptionsMonitor<SlackChannels> slackChannels,
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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();
Expand Down
9 changes: 5 additions & 4 deletions SlackLineBridge/SlackLineBridge.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AWS.Logger.AspNetCore" Version="3.5.3" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="8.0.4" />
<PackageReference Include="AWS.Logger.AspNetCore" Version="4.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.8" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="9.0.0" />
<PackageReference Include="Sentry.AspNetCore" Version="5.14.0" />
</ItemGroup>

</Project>
5 changes: 3 additions & 2 deletions SlackLineBridge/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);
Expand All @@ -67,8 +66,10 @@ public void ConfigureServices(IServiceCollection services)
var jsonOptions = new JsonSerializerOptions();
jsonOptions.EnableDynamicTypes();
services.AddSingleton(jsonOptions);
services.AddHostedService<LineMessageProcessingService>();
services.AddSingleton<LineMessageProcessingService>();
services.AddHostedService(sp=>sp.GetRequiredService<LineMessageProcessingService>());
services.AddHostedService<BackgroundAccessingService>();
services.AddControllers();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
Expand Down