line_webhook
A Dart package for handling LINE Messaging API Webhooks.
Features
- Parse Webhook request bodies from JSON
- Verify
X-Line-Signatureheaders (HMAC-SHA256) - Full coverage of all Webhook event types via sealed classes with exhaustive pattern matching
Installation
Add to your pubspec.yaml:
dependencies:
line_webhook: ^0.1.0
Usage
Signature verification
Always verify the signature to reject requests not originating from the LINE platform.
import 'package:line_webhook/line_webhook.dart';
bool handleRequest(String body, String signatureHeader) {
final isValid = verifyWebhookSignature(
channelSecret: 'YOUR_CHANNEL_SECRET',
body: body, // raw request body string
signature: signatureHeader, // value of X-Line-Signature header
);
if (!isValid) {
// treat as unauthorized, e.g. return 403
return false;
}
// ...
return true;
}
Parsing the request body
import 'dart:convert';
import 'package:line_webhook/line_webhook.dart';
void handleWebhook(String rawBody) {
final json = jsonDecode(rawBody) as Map<String, dynamic>;
final body = WebhookRequestBody.fromJson(json);
print('destination: ${body.destination}');
for (final event in body.events) {
handleEvent(event);
}
}
Handling events
WebhookEvent is a sealed class, so switch expressions are exhaustively checked at compile time.
void handleEvent(WebhookEvent event) {
switch (event) {
case MessageEvent():
handleMessage(event);
case FollowEvent():
print('Followed by: ${(event.source as UserSource).userId}');
case UnfollowEvent():
print('Blocked');
case JoinEvent():
print('Added to group/room');
case LeaveEvent():
print('Removed from group/room');
case PostbackEvent():
print('Postback: ${event.postback.data}');
case MemberJoinedEvent():
print('Member joined');
case MemberLeftEvent():
print('Member left');
case UnsendEvent():
print('Message unsent: ${event.unsend.messageId}');
case VideoPlayCompleteEvent():
print('Video viewed: ${event.videoPlayComplete.trackingId}');
case BeaconEvent():
print('Beacon: ${event.beacon.hwid}');
}
}
Handling messages
Message is also a sealed class.
void handleMessage(MessageEvent event) {
switch (event.message) {
case TextMessage(:final text):
print('Text: $text');
case ImageMessage():
print('Image (ID: ${event.message.id})');
case VideoMessage(:final duration):
print('Video (${duration}ms)');
case AudioMessage(:final duration):
print('Audio (${duration}ms)');
case FileMessage(:final fileName, :final fileSize):
print('File: $fileName ($fileSize bytes)');
case LocationMessage(:final latitude, :final longitude):
print('Location: $latitude, $longitude');
case StickerMessage(:final packageId, :final stickerId):
print('Sticker: $packageId / $stickerId');
}
}
Checking the event source
void checkSource(WebhookEvent event) {
switch (event.source) {
case UserSource(:final userId):
print('1-on-1 chat: $userId');
case GroupSource(:final groupId, :final userId):
print('Group: $groupId (user: $userId)');
case RoomSource(:final roomId, :final userId):
print('Room: $roomId (user: $userId)');
}
}
Detecting redelivered Webhooks
if (event.deliveryContext.isRedelivery) {
// Use webhookEventId for idempotency checks to avoid duplicate processing
print('Redelivered event: ${event.webhookEventId}');
}
Complete example with shelf
import 'dart:convert';
import 'package:line_webhook/line_webhook.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as io;
const channelSecret = 'YOUR_CHANNEL_SECRET';
Handler get app => const Pipeline().addHandler(_handler);
Future<Response> _handler(Request request) async {
if (request.method != 'POST') {
return Response(405);
}
final body = await request.readAsString();
final signature = request.headers['x-line-signature'] ?? '';
if (!verifyWebhookSignature(
channelSecret: channelSecret,
body: body,
signature: signature,
)) {
return Response.forbidden('Invalid signature');
}
final webhookBody = WebhookRequestBody.fromJson(
jsonDecode(body) as Map<String, dynamic>,
);
for (final event in webhookBody.events) {
handleEvent(event);
}
return Response.ok('OK');
}
void main() async {
await io.serve(app, 'localhost', 8080);
print('Server running at http://localhost:8080');
}
Supported events
| Event | Class |
|---|---|
| Message | MessageEvent |
| Unsend | UnsendEvent |
| Follow | FollowEvent |
| Unfollow (block) | UnfollowEvent |
| Join | JoinEvent |
| Leave | LeaveEvent |
| Member joined | MemberJoinedEvent |
| Member left | MemberLeftEvent |
| Postback | PostbackEvent |
| Video play complete | VideoPlayCompleteEvent |
| Beacon | BeaconEvent |
Supported message types
TextMessage / ImageMessage / VideoMessage / AudioMessage / FileMessage / LocationMessage / StickerMessage