Skip to content

Commit e2279ed

Browse files
authored
Merge pull request #6 from YDKK/develop
Update
2 parents 3ed64a2 + e5e13b2 commit e2279ed

4 files changed

Lines changed: 236 additions & 126 deletions

File tree

SlackLineBridge/Controllers/WebhookController.cs

Lines changed: 11 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Concurrent;
23
using System.Collections.Generic;
34
using System.Dynamic;
45
using System.IO;
@@ -25,19 +26,22 @@ public class WebhookController : ControllerBase
2526
private readonly LineChannels _lineChannels;
2627
private readonly SlackLineBridges _bridges;
2728
private readonly IHttpClientFactory _clientFactory;
29+
private readonly ConcurrentQueue<(string signature, string body)> _lineRequestQueue;
2830

2931
public WebhookController(
3032
ILogger<WebhookController> logger,
3133
IOptionsSnapshot<SlackChannels> slackChannels,
3234
IOptionsSnapshot<LineChannels> lineChannels,
3335
IOptionsSnapshot<SlackLineBridges> bridges,
36+
ConcurrentQueue<(string signature, string body)> lineRequestQueue,
3437
IHttpClientFactory clientFactory)
3538
{
3639
_logger = logger;
3740
_slackChannels = slackChannels.Value;
3841
_lineChannels = lineChannels.Value;
3942
_bridges = bridges.Value;
4043
_clientFactory = clientFactory;
44+
_lineRequestQueue = lineRequestQueue;
4145
}
4246

4347
[HttpPost("/slack")]
@@ -86,141 +90,23 @@ public async Task<OkResult> Slack([FromForm]SlackData data)
8690
}
8791

8892
[HttpPost("/line")]
89-
public async Task<OkResult> Line()
93+
public async Task<StatusCodeResult> Line()
9094
{
91-
// TODO: check signature
92-
var data = await JsonSerializer.DeserializeAsync<JsonElement>(Request.Body);
93-
94-
foreach (var e in data.GetProperty("events").EnumerateArray())
95+
if (!Request.Headers.ContainsKey("X-Line-Signature"))
9596
{
96-
switch (e.GetProperty("type").GetString())
97-
{
98-
case "message":
99-
{
100-
LineChannel lineChannel = GetLineChannel(e);
101-
if (lineChannel == null)
102-
{
103-
_logger.LogInformation($"message from unknown line channel: {GetLineEventSourceId(e)}");
104-
continue;
105-
}
106-
107-
var bridges = GetBridges(lineChannel);
108-
if (!bridges.Any())
109-
{
110-
continue;
111-
}
112-
string userName = null;
113-
if (e.GetProperty("source").TryGetProperty("userId", out var userId))
114-
{
115-
var client = _clientFactory.CreateClient("Line");
116-
117-
try
118-
{
119-
var result = await client.GetAsync($"profile/{userId}");
120-
if (result.IsSuccessStatusCode)
121-
{
122-
var profile = await JsonSerializer.DeserializeAsync<JsonElement>(await result.Content.ReadAsStreamAsync());
123-
userName = profile.GetProperty("displayName").GetString();
124-
}
125-
}
126-
catch (Exception ex)
127-
{
128-
_logger.LogError(ex, "get profile data failed");
129-
}
130-
131-
if (userName == null)
132-
{
133-
userName = $"Unknown ({userId})";
134-
}
135-
}
136-
else
137-
{
138-
userName = "Unknown";
139-
}
140-
141-
var message = e.GetProperty("message");
142-
var type = message.GetProperty("type").GetString();
143-
var text = type switch
144-
{
145-
"text" => message.GetProperty("text").GetString(),
146-
_ => $"<{type}>",
147-
};
97+
_logger.LogInformation("X-Line-Signature header missing.");
14898

149-
foreach (var bridge in bridges)
150-
{
151-
var slackChannel = _slackChannels.Channels.FirstOrDefault(x => x.Name == bridge.Slack);
152-
if (slackChannel == null)
153-
{
154-
_logger.LogError($"bridge configured but cannot find target slackChannel: {bridge.Slack}");
155-
continue;
156-
}
157-
158-
await SendToSlack(slackChannel.WebhookUrl, slackChannel.ChannelId, userName, text);
159-
}
160-
}
161-
break;
162-
default:
163-
{
164-
var sourceId = GetLineEventSourceId(e);
165-
if (sourceId == null)
166-
{
167-
var type = e.GetProperty("type").GetString();
168-
_logger.LogInformation($"{type} event from sourceId: {e.GetProperty("source").GetProperty("type").GetString()}");
169-
continue;
170-
}
171-
}
172-
break;
173-
}
99+
return BadRequest();
174100
}
175101

176-
_logger.LogInformation("Receive request from line: " + data.GetRawText());
177-
178-
return Ok();
179-
}
180-
181-
private async Task SendToSlack(string webhookUrl, string channelId, string userName, string text)
182-
{
183-
var client = _clientFactory.CreateClient();
184-
185-
var message = new
102+
using (var reader = new StreamReader(Request.Body))
186103
{
187-
channel = channelId,
188-
username = userName,
189-
icon_emoji = ":line:",
190-
text
191-
};
104+
_lineRequestQueue.Enqueue((Request.Headers["X-Line-Signature"], await reader.ReadToEndAsync()));
192105

193-
await client.PostAsync(webhookUrl, new StringContent(JsonSerializer.Serialize(message), Encoding.UTF8, "application/json"));
194-
}
195-
196-
private string GetLineEventSourceId(JsonElement e)
197-
{
198-
var source = e.GetProperty("source");
199-
var type = source.GetProperty("type").GetString();
200-
switch (type)
201-
{
202-
case "user":
203-
return source.GetProperty("userId").GetString();
204-
case "group":
205-
return source.GetProperty("groupId").GetString();
206-
case "room":
207-
return source.GetProperty("roomId").GetString();
208-
default:
209-
_logger.LogError($"unknown source type: {type}");
210-
return null;
106+
return Ok();
211107
}
212108
}
213109

214-
private LineChannel GetLineChannel(JsonElement e)
215-
{
216-
var sourceId = GetLineEventSourceId(e);
217-
return _lineChannels.Channels.FirstOrDefault(x => x.Id == sourceId);
218-
}
219-
220-
private IEnumerable<Models.Configurations.SlackLineBridge> GetBridges(LineChannel channel)
221-
{
222-
return _bridges.Bridges.Where(x => x.Line == channel.Name);
223-
}
224110
private IEnumerable<Models.Configurations.SlackLineBridge> GetBridges(SlackChannel channel)
225111
{
226112
return _bridges.Bridges.Where(x => x.Slack == channel.Name);
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
using Microsoft.Extensions.Hosting;
2+
using Microsoft.Extensions.Logging;
3+
using Microsoft.Extensions.Options;
4+
using SlackLineBridge.Models;
5+
using SlackLineBridge.Models.Configurations;
6+
using System;
7+
using System.Collections.Concurrent;
8+
using System.Collections.Generic;
9+
using System.Linq;
10+
using System.Net.Http;
11+
using System.Security.Cryptography;
12+
using System.Text;
13+
using System.Text.Json;
14+
using System.Threading;
15+
using System.Threading.Tasks;
16+
17+
namespace SlackLineBridge.Services
18+
{
19+
public class LineMessageProcessingService : BackgroundService
20+
{
21+
private readonly IOptionsMonitor<SlackChannels> _slackChannels;
22+
private readonly IOptionsMonitor<LineChannels> _lineChannels;
23+
private readonly IOptionsMonitor<SlackLineBridges> _bridges;
24+
private readonly IHttpClientFactory _clientFactory;
25+
private readonly ILogger<LineMessageProcessingService> _logger;
26+
private readonly ConcurrentQueue<(string signature, string body)> _queue;
27+
private readonly string _lineChannelSecret;
28+
29+
public LineMessageProcessingService(
30+
IOptionsMonitor<SlackChannels> slackChannels,
31+
IOptionsMonitor<LineChannels> lineChannels,
32+
IOptionsMonitor<SlackLineBridges> bridges,
33+
IHttpClientFactory clientFactory,
34+
ConcurrentQueue<(string signature, string body)> lineRequestQueue,
35+
string lineChannelSecret,
36+
ILogger<LineMessageProcessingService> logger)
37+
{
38+
_slackChannels = slackChannels;
39+
_lineChannels = lineChannels;
40+
_bridges = bridges;
41+
_clientFactory = clientFactory;
42+
_logger = logger;
43+
_queue = lineRequestQueue;
44+
_lineChannelSecret = lineChannelSecret;
45+
}
46+
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
47+
{
48+
_logger.LogDebug($"LineMessageProcessingService is starting.");
49+
50+
stoppingToken.Register(() => _logger.LogDebug($" LineMessageProcessing background task is stopping."));
51+
52+
while (!stoppingToken.IsCancellationRequested)
53+
{
54+
if (_queue.TryDequeue(out var request))
55+
{
56+
_logger.LogInformation("Processing request from line: " + request.body);
57+
58+
var signature = GetHMAC(request.body, _lineChannelSecret);
59+
_logger.LogDebug($"LINE signature check (expected:{request.signature}, calculated:{signature})");
60+
if (request.signature != signature)
61+
{
62+
_logger.LogInformation("LINE signature missmatch.");
63+
continue;
64+
}
65+
66+
var data = JsonSerializer.Deserialize<JsonElement>(request.body);
67+
68+
foreach (var e in data.GetProperty("events").EnumerateArray())
69+
{
70+
switch (e.GetProperty("type").GetString())
71+
{
72+
case "message":
73+
{
74+
LineChannel lineChannel = GetLineChannel(e);
75+
if (lineChannel == null)
76+
{
77+
_logger.LogInformation($"message from unknown line channel: {GetLineEventSourceId(e)}");
78+
continue;
79+
}
80+
81+
var bridges = GetBridges(lineChannel);
82+
if (!bridges.Any())
83+
{
84+
continue;
85+
}
86+
string userName = null;
87+
if (e.GetProperty("source").TryGetProperty("userId", out var userId))
88+
{
89+
var client = _clientFactory.CreateClient("Line");
90+
91+
try
92+
{
93+
var result = await client.GetAsync($"profile/{userId}");
94+
if (result.IsSuccessStatusCode)
95+
{
96+
var profile = await JsonSerializer.DeserializeAsync<JsonElement>(await result.Content.ReadAsStreamAsync());
97+
userName = profile.GetProperty("displayName").GetString();
98+
}
99+
}
100+
catch (Exception ex)
101+
{
102+
_logger.LogError(ex, "get profile data failed");
103+
}
104+
105+
if (userName == null)
106+
{
107+
userName = $"Unknown ({userId})";
108+
}
109+
}
110+
else
111+
{
112+
userName = "Unknown";
113+
}
114+
115+
var message = e.GetProperty("message");
116+
var type = message.GetProperty("type").GetString();
117+
var text = type switch
118+
{
119+
"text" => message.GetProperty("text").GetString(),
120+
_ => $"<{type}>",
121+
};
122+
123+
foreach (var bridge in bridges)
124+
{
125+
var slackChannel = _slackChannels.CurrentValue.Channels.FirstOrDefault(x => x.Name == bridge.Slack);
126+
if (slackChannel == null)
127+
{
128+
_logger.LogError($"bridge configured but cannot find target slackChannel: {bridge.Slack}");
129+
continue;
130+
}
131+
132+
await SendToSlack(slackChannel.WebhookUrl, slackChannel.ChannelId, userName, text);
133+
}
134+
}
135+
break;
136+
default:
137+
{
138+
var sourceId = GetLineEventSourceId(e);
139+
if (sourceId == null)
140+
{
141+
var type = e.GetProperty("type").GetString();
142+
_logger.LogInformation($"{type} event from sourceId: {e.GetProperty("source").GetProperty("type").GetString()}");
143+
continue;
144+
}
145+
}
146+
break;
147+
}
148+
}
149+
}
150+
151+
await Task.Delay(1000, stoppingToken);
152+
}
153+
154+
_logger.LogDebug($"LineMessageProcessing background task is stopping.");
155+
}
156+
157+
private async Task SendToSlack(string webhookUrl, string channelId, string userName, string text)
158+
{
159+
var client = _clientFactory.CreateClient();
160+
161+
var message = new
162+
{
163+
channel = channelId,
164+
username = userName,
165+
icon_emoji = ":line:",
166+
text
167+
};
168+
169+
await client.PostAsync(webhookUrl, new StringContent(JsonSerializer.Serialize(message), Encoding.UTF8, "application/json"));
170+
}
171+
172+
private LineChannel GetLineChannel(JsonElement e)
173+
{
174+
var sourceId = GetLineEventSourceId(e);
175+
return _lineChannels.CurrentValue.Channels.FirstOrDefault(x => x.Id == sourceId);
176+
}
177+
178+
private IEnumerable<Models.Configurations.SlackLineBridge> GetBridges(LineChannel channel)
179+
{
180+
return _bridges.CurrentValue.Bridges.Where(x => x.Line == channel.Name);
181+
}
182+
183+
private string GetLineEventSourceId(JsonElement e)
184+
{
185+
var source = e.GetProperty("source");
186+
var type = source.GetProperty("type").GetString();
187+
switch (type)
188+
{
189+
case "user":
190+
return source.GetProperty("userId").GetString();
191+
case "group":
192+
return source.GetProperty("groupId").GetString();
193+
case "room":
194+
return source.GetProperty("roomId").GetString();
195+
default:
196+
_logger.LogError($"unknown source type: {type}");
197+
return null;
198+
}
199+
}
200+
201+
private static string GetHMAC(string text, string key)
202+
{
203+
var encoding = new UTF8Encoding();
204+
205+
var textBytes = encoding.GetBytes(text);
206+
var keyBytes = encoding.GetBytes(key);
207+
208+
byte[] hashBytes;
209+
210+
using (var hash = new HMACSHA256(keyBytes))
211+
{
212+
hashBytes = hash.ComputeHash(textBytes);
213+
}
214+
215+
return Convert.ToBase64String(hashBytes);
216+
}
217+
}
218+
}

0 commit comments

Comments
 (0)