Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v4.1.0 #26

Merged
merged 6 commits into from
Sep 8, 2024
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
6 changes: 3 additions & 3 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v4
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
uses: actions/setup-dotnet@v4
with:
dotnet-version: 5.0.101
dotnet-version: 8
- name: Build with dotnet
run: dotnet build --configuration Release
10 changes: 5 additions & 5 deletions .github/workflows/push_develop_docker_image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v1
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v2
uses: docker/build-push-action@v6
with:
context: .
file: SlackLineBridge/Dockerfile
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/push_release_docker_image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v1
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
Expand All @@ -25,7 +25,7 @@ jobs:
echo "::set-output name=tag::${tag}"
id: get-tag
- name: Build and push
uses: docker/build-push-action@v2
uses: docker/build-push-action@v6
with:
context: .
file: SlackLineBridge/Dockerfile
Expand Down
31 changes: 11 additions & 20 deletions SlackLineBridge/Controllers/ProxyController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,24 +19,15 @@ namespace SlackLineBridge.Controllers
{
[Route("[controller]")]
[ApiController]
public class ProxyController : ControllerBase
public class ProxyController(
ILogger<ProxyController> logger,
IHttpClientFactory clientFactory,
LineChannelSecret lineChannelSecret,
SlackSigningSecret slackSigningSecret
) : ControllerBase
{
private readonly ILogger<ProxyController> _logger;
IHttpClientFactory _clientFactory;
string _lineChannelSecret;
string _slackSigningSecret;
public ProxyController(
ILogger<ProxyController> logger,
IHttpClientFactory clientFactory,
LineChannelSecret lineChannelSecret,
SlackSigningSecret slackSigningSecret
)
{
_logger = logger;
_clientFactory = clientFactory;
_lineChannelSecret = lineChannelSecret.Secret;
_slackSigningSecret = slackSigningSecret.Secret;
}
private readonly string _lineChannelSecret = lineChannelSecret.Secret;
private readonly string _slackSigningSecret = slackSigningSecret.Secret;

[HttpGet("line/{token}/{id}")]
public async Task<IActionResult> Line(string id, string token)
Expand All @@ -46,7 +37,7 @@ public async Task<IActionResult> Line(string id, string token)
return new StatusCodeResult((int)HttpStatusCode.Forbidden);
}

var client = _clientFactory.CreateClient("Line");
var client = clientFactory.CreateClient("Line");
var url = $"https://api-data.line.me/v2/bot/message/{id}/content";

return await ProxyContent(client, url);
Expand All @@ -55,15 +46,15 @@ public async Task<IActionResult> Line(string id, string token)
[HttpGet("slack/{token}/{encodedUrl}")]
public async Task<IActionResult> Slack(string encodedUrl, string token)
{
_logger.LogInformation($"Proxy request to Slack: {encodedUrl}, {token}");
logger.LogInformation("Proxy request to Slack: {encodedUrl}, {token}", encodedUrl, token);

var url = HttpUtility.UrlDecode(encodedUrl);
if (token != Crypt.GetHMACHex(url, _slackSigningSecret))
{
return new StatusCodeResult((int)HttpStatusCode.Forbidden);
}

var client = _clientFactory.CreateClient("Slack");
var client = clientFactory.CreateClient("Slack");

return await ProxyContent(client, url);
}
Expand Down
110 changes: 50 additions & 60 deletions SlackLineBridge/Controllers/WebhookController.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Dynamic;
using System.IO;
using System.Linq;
Expand All @@ -15,6 +16,7 @@
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.Utils;
Expand All @@ -24,50 +26,33 @@ namespace SlackLineBridge.Controllers
{
[ApiController]
[Route("[controller]")]
public class WebhookController : ControllerBase
public partial class WebhookController(
ILogger<WebhookController> logger,
IOptionsSnapshot<SlackChannels> slackChannels,
IOptionsSnapshot<LineChannels> lineChannels,
IOptionsSnapshot<SlackLineBridges> bridges,
ConcurrentQueue<(string signature, string body, string host)> lineRequestQueue,
IHttpClientFactory clientFactory,
SlackSigningSecret slackSigningSecret,
JsonSerializerOptions jsonOptions) : ControllerBase
{
private readonly ILogger<WebhookController> _logger;
private readonly SlackChannels _slackChannels;
private readonly LineChannels _lineChannels;
private readonly SlackLineBridges _bridges;
private readonly IHttpClientFactory _clientFactory;
private readonly ConcurrentQueue<(string signature, string body, string host)> _lineRequestQueue;
private static readonly Regex _urlRegex = new Regex(@"(\<(?<url>http[^\|\>]+)\|?.*?\>)");
private readonly JsonSerializerOptions _jsonOptions;
private readonly string _slackSigningSecret;

public WebhookController(
ILogger<WebhookController> logger,
IOptionsSnapshot<SlackChannels> slackChannels,
IOptionsSnapshot<LineChannels> lineChannels,
IOptionsSnapshot<SlackLineBridges> bridges,
ConcurrentQueue<(string signature, string body, string host)> lineRequestQueue,
IHttpClientFactory clientFactory,
SlackSigningSecret slackSigningSecret,
JsonSerializerOptions jsonOptions)
{
_logger = logger;
_slackChannels = slackChannels.Value;
_lineChannels = lineChannels.Value;
_bridges = bridges.Value;
_clientFactory = clientFactory;
_lineRequestQueue = lineRequestQueue;
_slackSigningSecret = slackSigningSecret.Secret;
_jsonOptions = jsonOptions;
}
private readonly SlackChannels _slackChannels = slackChannels.Value;
private readonly LineChannels _lineChannels = lineChannels.Value;
private readonly SlackLineBridges _bridges = bridges.Value;
private readonly string _slackSigningSecret = slackSigningSecret.Secret;

[HttpPost("/slack2")]
public async Task<IActionResult> Slack2()
{
//再送を無視する
if (Request.Headers.ContainsKey("X-Slack-Retry-Reason") && Request.Headers["X-Slack-Retry-Reason"].First() == "http_timeout")
if (Request.Headers.TryGetValue("X-Slack-Retry-Reason", out StringValues reason) && reason.First() == "http_timeout")
{
return Ok();
}

using var reader = new StreamReader(Request.Body);
var json = await reader.ReadToEndAsync();
_logger.LogInformation("Processing request from Slack: " + json);
logger.LogInformation("Processing request from Slack: {json}", json);

var timestampStr = Request.Headers["X-Slack-Request-Timestamp"].First();
if (long.TryParse(timestampStr, out var timestamp))
Expand All @@ -77,11 +62,11 @@ public async Task<IActionResult> Slack2()
var signature = $"v0={Crypt.GetHMACHex(sigBaseStr, _slackSigningSecret)}";
var slackSignature = Request.Headers["X-Slack-Signature"].First();

_logger.LogDebug($"Slack signature check (expected:{slackSignature}, calculated:{signature})");
logger.LogDebug("Slack signature check (expected:{slackSignature}, calculated:{signature})", slackSignature, signature);
if (signature == slackSignature)
{
// the request came from Slack!
dynamic data = JsonSerializer.Deserialize<dynamic>(json, _jsonOptions);
dynamic data = JsonSerializer.Deserialize<dynamic>(json, jsonOptions);
string type = data.type;
switch (type)
{
Expand All @@ -93,7 +78,7 @@ public async Task<IActionResult> Slack2()
switch (eventType)
{
case "message":
if([email protected] == "bot_message")
if ([email protected] == "bot_message")
{
return Ok();
}
Expand All @@ -103,18 +88,18 @@ public async Task<IActionResult> Slack2()
var slackChannel = slackChannels.FirstOrDefault(x => x.TeamId == teamId && x.ChannelId == channelId);
if (slackChannel == null)
{
_logger.LogInformation($"message from unknown slack channel: {teamId}/{channelId}");
logger.LogInformation("message from unknown slack channel: {teamId}/{channelId}", teamId, channelId);
return Ok();
}

string text = [email protected];
string userId = [email protected];
var user = await GetSlackUserNameAndIcon(userId);
var (userName, icon) = await GetSlackUserNameAndIcon(userId);

JsonDynamicArray files = [email protected];
SlackFile[] slackFiles = null;

if (files?.Any() == true)
if ((files?.Count ?? 0) > 0)
{
slackFiles = files.Cast<dynamic>().Select(x => new SlackFile
{
Expand All @@ -124,14 +109,14 @@ public async Task<IActionResult> Slack2()
}).ToArray();
}

return await PushToLine(Request.Host.ToString(), slackChannel, user.icon, user.userName, text, slackFiles);
return await PushToLine(Request.Host.ToString(), slackChannel, icon, userName, text, slackFiles);
}
break;
}
}
else
{
_logger.LogInformation("Slack signature missmatch.");
logger.LogInformation("Slack signature missmatch.");
}
}

Expand All @@ -140,22 +125,26 @@ public async Task<IActionResult> Slack2()

private async Task<(string userName, string icon)> GetSlackUserNameAndIcon(string userId)
{
var client = _clientFactory.CreateClient("Slack");
var client = clientFactory.CreateClient("Slack");
var result = await client.GetAsync($"https://slack.com/api/users.profile.get?user={userId}");
var json = await result.Content.ReadAsStringAsync();
dynamic data = JsonSerializer.Deserialize<dynamic>(json, _jsonOptions);
dynamic data = JsonSerializer.Deserialize<dynamic>(json, jsonOptions);
(string name, string icon) = (data.profile.display_name, data.profile.image_512);

return (name, icon);
}

[SuppressMessage("Style", "IDE1006:命名スタイル", Justification = "<保留中>")]
private record SlackFile
{
public string urlPrivate { get; set; }
public string thumb360 { get; set; }
public string mimeType { get; set; }
}

[GeneratedRegex(@"(\<(?<url>http[^\|\>]+)\|?.*?\>)")]
private static partial Regex UrlRegex();

private async Task<IActionResult> PushToLine(string host, SlackChannel slackChannel, string userIconUrl, string userName, string text, SlackFile[] files = null)
{
var bridges = GetBridges(slackChannel);
Expand All @@ -165,15 +154,15 @@ private async Task<IActionResult> PushToLine(string host, SlackChannel slackChan
}

//URLタグを抽出
var urls = _urlRegex.Matches(text);
text = UrlRegex().Replace(text, "${url}");

var client = _clientFactory.CreateClient("Line");
var client = clientFactory.CreateClient("Line");
foreach (var bridge in bridges)
{
var lineChannel = _lineChannels.Channels.FirstOrDefault(x => x.Name == bridge.Line);
if (lineChannel == null)
{
_logger.LogError($"bridge configured but cannot find target LineChannel: {bridge.Line}");
logger.LogError("bridge configured but cannot find target LineChannel: {bridge.Line}", bridge.Line);
return Ok();
}

Expand All @@ -182,27 +171,26 @@ private async Task<IActionResult> PushToLine(string host, SlackChannel slackChan
{
type = "text",
altText = text,
text = text,
sender = new {
text,
sender = new
{
name = userName,
iconUrl = $"https://{host}/proxy/slack/{Crypt.GetHMACHex(userIconUrl, _slackSigningSecret)}/{HttpUtility.UrlEncode(userIconUrl)}"
},
};
var urlMessages = urls.Select(x => x.Groups["url"].Value).Select(x => new
{
type = "text",
text = x
});

var json = new
{
to = lineChannel.Id,
messages = new dynamic[]
{
message
}.Concat(urlMessages).ToArray()
}.ToArray()
};
await client.PostAsync($"message/push", new StringContent(JsonSerializer.Serialize(json), Encoding.UTF8, "application/json"));
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());
}

if (files != null)
Expand All @@ -229,8 +217,9 @@ private async Task<IActionResult> PushToLine(string host, SlackChannel slackChan
messages = messages.ToArray()
};
var jsonStr = JsonSerializer.Serialize(json);
_logger.LogInformation("Push images to LINE: " + jsonStr);
await client.PostAsync($"message/push", new StringContent(jsonStr, Encoding.UTF8, "application/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());
}
}
return Ok();
Expand All @@ -239,9 +228,9 @@ private async Task<IActionResult> PushToLine(string host, SlackChannel slackChan
[HttpPost("/line")]
public async Task<IActionResult> LineAsync()
{
if (!Request.Headers.ContainsKey("X-Line-Signature"))
if (!Request.Headers.TryGetValue("X-Line-Signature", out StringValues signature))
{
_logger.LogInformation("X-Line-Signature header missing.");
logger.LogInformation("X-Line-Signature header missing.");

return BadRequest();
}
Expand All @@ -257,9 +246,10 @@ public async Task<IActionResult> LineAsync()
}
finally
{
Response.OnCompleted(async () =>
Response.OnCompleted(() =>
{
_lineRequestQueue.Enqueue((Request.Headers["X-Line-Signature"], json, host));
lineRequestQueue.Enqueue((signature, json, host));
return Task.CompletedTask;
});
}

Expand Down
Loading