通過 LINE Notify 發送訊息

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)
  1. 不需要個人授權
  2. 適用訊息發送給群組
  • 建立通知服務
  1. 需要個人授權,OAuth2 流程
  2. 適用訊息發送給個人或群組

以下就來演練要怎麼取得 Access Token,

  1. 首先得先用你的開發帳號登入 LINE Notify
  2. 訊息的發送可以用你習慣的 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 端點

目的:

  1. 接收 Authorize Code
  2. 用 Authorize Code 換 Access Token

步驟:

  1. 新增一個 ASP.NET Core Web API,裡面有一個 Post 端點,這個就是在 LINE Notity Service 設定的 CallbackUrl
  2. 當用戶同意並連結之後,就會靠瀏覽器 Redirect 回到你的 localhost 服務
  3. 接收 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 註銷

  1. 在用戶管理介面斷開關聯
  2. 調用 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

Image result for microsoft+mvp+logo