Skip to content

Commit

Permalink
Merge pull request #26 from YDKK/develop
Browse files Browse the repository at this point in the history
v4.1.0
  • Loading branch information
YDKK authored Sep 8, 2024
2 parents 6e29148 + a488bdd commit d643a85
Show file tree
Hide file tree
Showing 7 changed files with 81 additions and 100 deletions.
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(data.@event.subtype == "bot_message")
if (data.@event.subtype == "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 = data.@event.text;
string userId = data.@event.user;
var user = await GetSlackUserNameAndIcon(userId);
var (userName, icon) = await GetSlackUserNameAndIcon(userId);

JsonDynamicArray files = data.@event.files;
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

0 comments on commit d643a85

Please sign in to comment.