90 lines
3.2 KiB
C#
90 lines
3.2 KiB
C#
using System.Text;
|
|
using System.Text.Json;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.Extensions.Options;
|
|
using ROLAC.API.DTOs.Notifications;
|
|
using ROLAC.API.Services.Notifications;
|
|
|
|
namespace ROLAC.API.Controllers;
|
|
|
|
/// <summary>
|
|
/// Anonymous Line webhook. Verifies the X-Line-Signature over the raw body, then dispatches
|
|
/// follow/message/join/leave events. Always returns 200 for valid payloads so Line does not retry;
|
|
/// returns 400 only on signature failure.
|
|
/// </summary>
|
|
[ApiController]
|
|
[Route("api/line")]
|
|
[AllowAnonymous]
|
|
public sealed class LineWebhookController : ControllerBase
|
|
{
|
|
private static readonly JsonSerializerOptions JsonOpts = new() { PropertyNameCaseInsensitive = true };
|
|
|
|
private readonly ILineNotificationService _line;
|
|
private readonly IMessageChannel _channel;
|
|
private readonly LineOptions _options;
|
|
|
|
public LineWebhookController(
|
|
ILineNotificationService line, IMessageChannel channel, IOptions<LineOptions> options)
|
|
{
|
|
_line = line;
|
|
_channel = channel;
|
|
_options = options.Value;
|
|
}
|
|
|
|
[HttpPost("webhook")]
|
|
[RequestSizeLimit(262_144)]
|
|
public async Task<IActionResult> Webhook(CancellationToken ct)
|
|
{
|
|
using var reader = new StreamReader(Request.Body, Encoding.UTF8);
|
|
var rawBody = await reader.ReadToEndAsync(ct);
|
|
var signature = Request.Headers["X-Line-Signature"].FirstOrDefault();
|
|
|
|
if (!LineSignature.IsValid(_options.ChannelSecret, Encoding.UTF8.GetBytes(rawBody), signature))
|
|
return BadRequest();
|
|
|
|
var payload = JsonSerializer.Deserialize<LineWebhookPayload>(rawBody, JsonOpts);
|
|
if (payload?.Events is not null)
|
|
foreach (var evt in payload.Events)
|
|
await DispatchAsync(evt, ct);
|
|
|
|
return Ok();
|
|
}
|
|
|
|
private async Task DispatchAsync(LineWebhookEvent evt, CancellationToken ct)
|
|
{
|
|
switch (evt.Type)
|
|
{
|
|
case "follow":
|
|
if (evt.ReplyToken is not null)
|
|
await _channel.ReplyAsync(evt.ReplyToken, "歡迎!請輸入您的綁定碼以連結教會帳號。", ct);
|
|
break;
|
|
|
|
case "message":
|
|
if (evt.Message?.Type == "text"
|
|
&& evt.Source?.UserId is { } userId
|
|
&& evt.Message.Text is { } text)
|
|
{
|
|
var result = await _line.TryBindMemberAsync(userId, text, ct);
|
|
if (evt.ReplyToken is not null)
|
|
await _channel.ReplyAsync(evt.ReplyToken, result.Message, ct);
|
|
}
|
|
break;
|
|
|
|
case "join":
|
|
if (evt.Source?.GroupId is { } joinGroupId)
|
|
{
|
|
await _line.RegisterGroupAsync(joinGroupId, ct);
|
|
if (evt.ReplyToken is not null)
|
|
await _channel.ReplyAsync(evt.ReplyToken, "已加入群組,請至後台命名此群組。", ct);
|
|
}
|
|
break;
|
|
|
|
case "leave":
|
|
if (evt.Source?.GroupId is { } leaveGroupId)
|
|
await _line.DeactivateGroupAsync(leaveGroupId, ct);
|
|
break;
|
|
}
|
|
}
|
|
}
|