line_webhook

GitHub pub package GitHub Stars

A Dart package for handling LINE Messaging API Webhooks.

日本語

Features

  • Parse Webhook request bodies from JSON
  • Verify X-Line-Signature headers (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

Libraries

line_webhook