LINE 服務有一個 LINE Notify 帳號,這是一個 LINEBot 機器人,專門用來發送訊息的帳號,只要你曾經有訂閱任何一個 LINE Notify 通知服務,它就能發訊息給你;或是把 LINE Notify 帳號加入群組也可以。LINE Notify 可以讓開發人員免費的發送訊息 + 表情或是圖片給用戶,在需要即時的通知用戶的場景下,是一個相當實惠的解決方案;尤其在台灣 LINE 等於是一個基礎建設,人手都有帳號,用 LINE 整合訊息通知應該是比較方便的。
開發環境
- Rider 2021.1.3
- .NET 5
- Windows 10
取得發送訊息的 Access Token
LINE 官方提供幾種方式可以讓開發人員用程式發送訊息
- Generate access token (For developers)
- 不需要個人授權
- 適用訊息發送給群組
- 建立通知服務
- 需要個人授權,OAuth2 流程
- 適用訊息發送給個人或群組
以下就來演練要怎麼取得 Access Token,
- 首先得先用你的開發帳號登入 LINE Notify。
- 訊息的發送可以用你習慣的 REST API Client,這裡我使用 cURL 來演練,開始之前先到巧克力安裝 cURL
choco install curl -y
Generate access token (For developers)
這個方法不需要用戶授權同意,由管理介面取得 Token 後,必須要自己記錄下來
申請 Token
有了這個 Access Token 之後就可以發送訊息了
這個 Access Token 必須要自己管理,一旦不見了就重新申請了
curl -H "Authorization: Bearer f6yOmYbusz0PUdIhtQTmtqadv4kPfO0xNA9OgFHsSlD" -d "message=Hello World~" https://notify-api.line.me/api/notify
效果如下圖:
建立通知服務
這個方法需要用戶授權同意,步驟也較多,取得 Access Token 之後必須要自己記錄下來
1.申請服務
訪問 LINE Notify,建立 Service
新增一個 Service,填入註冊資訊,其中 Callback URL 可以填上開發中 (localhost) 的位置
2.請求用戶同意
在瀏覽器輸入以下連結,把 client_id 、redirect_uri 換成你自己的
https://notify-bot.line.me/oauth/authorize?response_type=code&scope=notify&response_mode=form_post&client_id=Ppu33o7F0c2BTcryJ3PVDQ&redirect_uri=https://localhost:5001/AuthorizeCode&state=1234567
redirect_uri:就是在LINE Notify 畫面上申請的 Callback URL
我將產生 URL 封裝在 LneNotifyProvider.CreateAuthorizeCodeUrl 方法
實作方式請參考 https://notify-bot.line.me/doc/en/
3.建立接收 Line Notify Authorize Code 端點
目的:
- 接收 Authorize Code
- 用 Authorize Code 換 Access Token
步驟:
- 新增一個 ASP.NET Core Web API,裡面有一個 Post 端點,這個就是在 LINE Notity Service 設定的 CallbackUrl
- 當用戶同意並連結之後,就會靠瀏覽器 Redirect 回到你的 localhost 服務
- 接收 Authorize Code,再用 Authorize Code 換 Access Token,需要剛剛在 Line Notify 申請的 Client ID、Client Secret,這兩個狀態被我放在 application.json 檔案裡
[ApiController]
[Route("[controller]")]
public class AuthorizeCodeController : ControllerBase
{
private readonly IConfiguration _config;
private readonly ILineNotifyProvider _lineNotifyProvider;
private readonly ILogger<AuthorizeCodeController> _logger;
public AuthorizeCodeController(ILogger<AuthorizeCodeController> logger,
IConfiguration config,
ILineNotifyProvider lineNotifyProvider)
{
this._logger = logger;
this._config = config;
this._lineNotifyProvider = lineNotifyProvider;
}
[HttpPost]
public async Task<IActionResult> Post([FromForm] IFormCollection data, CancellationToken cancelToken)
{
if (data.TryGetValue("code", out var code) == false)
{
this.ModelState.AddModelError("code 欄位", "必填");
return this.BadRequest(this.ModelState);
}
if (data.TryGetValue("state", out var state) == false)
{
this.ModelState.AddModelError("state 欄位", "必填");
return this.BadRequest(this.ModelState);
}
var config = this._config;
var lineNotifyProvider = this._lineNotifyProvider;
var lineConfig = config.GetSection("LineNotify");
var request = new TokenRequest
{
Code = code,
ClientId = lineConfig.GetValue<string>("clientId"),
ClientSecret = lineConfig.GetValue<string>("clientSecret"),
CallbackUrl = lineConfig.GetValue<string>("redirectUri"),
};
var accessToken = await lineNotifyProvider.GetAccessTokenAsync(request, cancelToken);
//TODO:應該記錄在你的 DB 或是其它地方,不應該回傳 Access Token
return this.Ok(accessToken);
}
}
取得 Authorize Code 之後,就要拿著這個 code,去跟 LINE Notify Service 換 Token,換 Token 的方法我就將它封裝在 LneNotifyProvider.GetAccessTokenAsync 方法
實作方式請參考 https://notify-bot.line.me/doc/en/
實作結果如下:
有了 Access Token 之後就可以送訊息了
curl -H "Authorization: Bearer sQA3JzHwtK4JQ4cv5Atws0uV2bAfswPvWhhcvSlusJc" -d "message=Hello World~" https://notify-api.line.me/api/notify
以上就是 LINE Notify 提供兩種的取得 Access Token 的方式,就看你的設計。
從 官方文件 得知有了 Access Token 除了發送訊息還可以調查 Access Token 的狀態;免費的畢竟是有一些限制的,當無法發送訊息的時候可以查一下是不是超過限制了
發送訊息
發送訊息+表情
表情符號的 ID,請參考:https://developers.line.biz/en/docs/messaging-api/sticker-list/#sticker-definitions
public async Task<GenericResponse> NotifyAsync(NotifyWithStickerRequest request,
CancellationToken cancelToken)
{
Validation.Validate(request);
var url = "api/notify";
var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)
{
Headers = {Authorization = new AuthenticationHeaderValue("Bearer", request.AccessToken)},
Content = new FormUrlEncodedContent(new Dictionary<string, string>
{
{"message", request.Message},
{"stickerPackageId", request.StickerPackageId},
{"stickerId", request.StickerId},
}),
};
using var client = this.CreateApiClient();
var response = await client.SendAsync(httpRequest, cancelToken);
if (response.StatusCode != HttpStatusCode.OK)
{
if (this.IsThrowInternalError)
{
var error = await response.Content.ReadAsStringAsync(cancelToken);
throw new LineNotifyProviderException(error);
}
}
return await response.Content.ReadAsAsync<GenericResponse>(cancelToken);
}
NotifyWithStickerRequest 請參考:sample.dotblog/NotifyWithStickerRequest.cs at master · yaochangyu/sample.dotblog (github.com)
我已經很習慣用測試程式碼來 Survey API,所以接下來的案例是沒有意義的
測試程式碼如下:
[TestMethod]
public void 發送訊息和表情()
{
var provider = new LineNotifyProvider();
var response = provider.NotifyAsync(new NotifyWithStickerRequest
{
AccessToken = "3lZwryen62tiQ4BKfh3uH3NFoFtALF4SrfgLWMIKrXh",
Message = "HI~請給我黃金",
StickerPackageId = 1.ToString(),
StickerId = 113.ToString()
}, CancellationToken.None)
.Result;
Assert.AreEqual(200, response.Status);
}
演練結果如下圖:
發送訊息+圖片
public async Task<GenericResponse> NotifyAsync(NotifyWithImageRequest request,
CancellationToken cancelToken)
{
Validation.Validate(request);
var url = $"api/notify?message={request.Message}";
using var formDataContent = new MultipartFormDataContent();
var imageName = Path.GetFileName(request.FilePath);
var mimeType = MimeTypeMapping.GetMimeType(imageName);
var imageContent = new ByteArrayContent(request.FileBytes);
imageContent.Headers.ContentType = new MediaTypeHeaderValue(mimeType);
formDataContent.Add(imageContent, "imageFile", imageName);
var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)
{
Headers = {Authorization = new AuthenticationHeaderValue("Bearer", request.AccessToken)},
Content = formDataContent
};
using var client = this.CreateApiClient();
var response = await client.SendAsync(httpRequest, cancelToken);
if (response.StatusCode != HttpStatusCode.OK)
{
if (this.IsThrowInternalError)
{
var error = await response.Content.ReadAsStringAsync(cancelToken);
throw new LineNotifyProviderException(error);
}
}
return await response.Content.ReadAsAsync<GenericResponse>(cancelToken);
}
NotifyWithImageRequest 請參考:sample.dotblog/NotifyWithImageRequest.cs at master · yaochangyu/sample.dotblog (github.com)
演練結果如下:
實作方式請參考 https://notify-bot.line.me/doc/en/
取得 Access Token 狀態
X-RateLimit-Remaining:Access Token 在這一個小時內還能用幾次,預設一小時 1000 則訊息
X-RateLimit-Reset:Reset 用量限制的時間
public async Task<TokenInfoResponse> GetAccessTokenInfoAsync(string accessToken,
CancellationToken cancelToken)
{
if (string.IsNullOrWhiteSpace(accessToken))
{
throw new ArgumentNullException(nameof(accessToken));
}
var url = "api/status";
var httpRequest = new HttpRequestMessage(HttpMethod.Get, url)
{
Headers = {Authorization = new AuthenticationHeaderValue("Bearer", accessToken)},
Content = new FormUrlEncodedContent(new Dictionary<string, string>()),
};
using var client = this.CreateApiClient();
var response = await client.SendAsync(httpRequest, cancelToken);
if (response.StatusCode != HttpStatusCode.OK)
{
if (this.IsThrowInternalError)
{
var error = await response.Content.ReadAsStringAsync(cancelToken);
throw new LineNotifyProviderException(error);
}
}
var tokenInfo = await response.Content.ReadAsAsync<TokenInfoResponse>(cancelToken);
tokenInfo.Limit = GetValue<int>(response, "X-RateLimit-Limit");
tokenInfo.ImageLimit = GetValue<int>(response, "X-RateLimit-ImageLimit");
tokenInfo.Remaining = GetValue<int>(response, "X-RateLimit-Remaining");
tokenInfo.ImageRemaining = GetValue<int>(response, "X-RateLimit-ImageRemaining");
tokenInfo.Reset = GetValue<int>(response, "X-RateLimit-Reset");
tokenInfo.ResetLocalTime = ToLocalTime(tokenInfo.Reset);
return tokenInfo;
}
轉換時間
private static DateTime ToLocalTime(long source)
{
var timeOffset = DateTimeOffset.FromUnixTimeSeconds(source);
return timeOffset.DateTime.ToUniversalTime();
}
測試程式碼如下:
[TestMethod]
public void 取得AccessToken狀態()
{
var provider = new LineNotifyProvider();
var response = provider
.GetAccessTokenInfoAsync("3lZwryen62tiQ4BKfh3uH3NFoFtALF4SrfgLWMIKrXh",
CancellationToken.None).Result;
Assert.AreEqual(200, response.Status);
}
演練結果如下圖:
實作方式請參考 https://notify-bot.line.me/doc/en/
每一個 Access Token 在 LINE Notity 使用的限制
註銷 Access Token
有兩種方式可以把 Access Token 註銷
- 在用戶管理介面斷開關聯
- 調用 API
在用戶管理介面斷開關聯
訪問 LINE Notify
調用 API
public async Task<GenericResponse> RevokeAsync(string accessToken, CancellationToken cancelToken)
{
if (string.IsNullOrWhiteSpace(accessToken))
{
throw new ArgumentNullException(nameof(accessToken));
}
var url = "api/revoke";
var httpRequest = new HttpRequestMessage(HttpMethod.Post, url)
{
Headers = {Authorization = new AuthenticationHeaderValue("Bearer", accessToken)},
Content = new FormUrlEncodedContent(new Dictionary<string, string>()),
};
using var client = this.CreateApiClient();
var response = await client.SendAsync(httpRequest, cancelToken);
if (response.StatusCode != HttpStatusCode.OK)
{
if (this.IsThrowInternalError)
{
var error = await response.Content.ReadAsStringAsync();
throw new LineNotifyProviderException(error);
}
}
return await response.Content.ReadAsAsync<GenericResponse>(cancelToken);
}
測試程式碼如下:
[TestMethod]
public void 註銷AccessToken()
{
var provider = new LineNotifyProvider();
var response = provider.RevokeAsync("3lZwryen62tiQ4BKfh3uH3NFoFtALF4SrfgLWMIKrXh",
CancellationToken.None)
.Result;
Assert.AreEqual(200, response.Status);
}
實作方式請參考 https://notify-bot.line.me/doc/en/
範例位置
sample.dotblog/Line/Lab.LineNotify at master · yaochangyu/sample.dotblog (github.com)
參考資料
上手 LINE Notify 不求人:一行代碼都不用寫的推播通知方法 | The Will Will Web (miniasp.com)
.NET Walker: 使用C#開發LineBot(6) - 不用申請Bot也能發訊息的Line Notify (studyhost.blogspot.com)
poychang/TestLineNotifyAPI: 測試 Line Notify API (github.com)
實作 Line Notify 通知服務 (1) (poychang.net)
若有謬誤,煩請告知,新手發帖請多包涵
Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET