An Elixir client for the LINE platform — Messaging API today, LIFF / LINE Login planned.
Unofficial. Not affiliated with or endorsed by LY Corporation.
Design
- Credentials are values, never global. Build an
ExLine.Clientand pass it per call. Multi-channel / multi-tenant apps are first-class — there is no global registry to fight. - HTTP is swappable. Requests go through the
ExLine.Client.Adapterbehaviour (default:ExLine.Client.Req), so you can mock the network in tests. - Webhooks verify and route.
ExLine.Webhook.Signature/ExLine.Webhook.Plugverify thex-line-signature;ExLine.EventRouterdispatches events to handlers.
Installation
def deps do
[
{:ex_line, "~> 0.1.0"}
]
end:plug is an optional dependency, only needed if you use ExLine.Webhook.Plug /
ExLine.Webhook.BodyReader.
Configuration
You need credentials from the LINE Developers Console:
| Credential | Where it's used | From |
|---|---|---|
| Channel access token | sending messages (ExLine.Client) | Messaging API channel |
| Channel secret | webhook signature (ExLine.Webhook) | Messaging API channel |
ExLine never holds global credential state — you pass what each call needs. There are three ways to supply them, pick per use case:
1. Per-call value (default; multi-channel / multi-tenant friendly). Build a client from wherever you store the token (DB row, etc.) and pass it in:
client = ExLine.Client.new(access_token: channel.access_token)
ExLine.Api.Messaging.push(client, user_id, message)2. From application config (single-channel convenience).
# config/runtime.exs
config :ex_line,
access_token: System.fetch_env!("LINE_CHANNEL_ACCESS_TOKEN"),
channel_id: System.get_env("LINE_CHANNEL_ID")client = ExLine.Client.from_env()3. Webhook secret via a resolver (kept separate from the client). The channel
secret belongs to a different trust boundary, so it is passed directly — as a
static value, or a fn conn -> secret end resolver that picks the right channel
at request time (see Receiving webhooks):
plug ExLine.Webhook.Plug, secret: System.fetch_env!("LINE_CHANNEL_SECRET")
# or, multi-channel:
plug ExLine.Webhook.Plug, secret: fn conn -> MyApp.secret_for(conn) endNever commit tokens or secrets — load them from the environment.
Sending messages
client = ExLine.Client.new(access_token: "CHANNEL_ACCESS_TOKEN")
# push
ExLine.Api.Messaging.push(client, "U123...", ExLine.Message.text("hello"))
# reply (using a webhook replyToken)
ExLine.Api.Messaging.reply(client, reply_token, [
ExLine.Message.text("hi"),
ExLine.Message.Template.buttons("Pick one", [
ExLine.Message.Action.message("A", "a"),
ExLine.Message.Action.postback("B", "action=b")
])
])Push supports idempotent retries via X-Line-Retry-Key:
ExLine.Api.Messaging.push(client, "U123...", msg, retry_key: "a-uuid")Errors come back as {:error, %ExLine.Error{kind: kind}} where kind is one of
:transient, :quota_exceeded, :permanent, or :network (see
ExLine.Error.retryable?/1).
Receiving webhooks
Verify the signature (works with or without Plug):
ExLine.Webhook.Signature.valid?(raw_body, signature, channel_secret)With Plug, preserve the raw body in your parser, then verify in the pipeline. The
:secret option takes a static binary or a fn conn -> secret end resolver so you
can pick the right channel per request:
plug Plug.Parsers,
parsers: [:json],
body_reader: {ExLine.Webhook.BodyReader, :read_body, []},
json_decoder: Jason
plug ExLine.Webhook.Plug, secret: &MyApp.line_secret/1Routing events
defmodule MyApp.LineRouter do
use ExLine.EventRouter
text "hello", MyApp.HelpHandler, :hello
postback "buy", MyApp.ShopHandler, :buy
follow MyApp.OnboardHandler, :welcome
default MyApp.FallbackHandler, :unknown
@impl true
def before_action(event, assigns), do: {event, Map.put(assigns, :client, MyApp.client())}
end
defmodule MyApp.HelpHandler do
use ExLine.EventHandler
@impl true
def handle_event(:hello, %{"replyToken" => token}, %{client: client}) do
ExLine.Api.Messaging.reply(client, token, text("Need help?"))
:ok
end
end
# in your webhook controller, for each event:
MyApp.LineRouter.call(event, %{})Testing
Mock the adapter to assert outbound requests without hitting the network:
# test_helper.exs
Mox.defmock(MyApp.LineAdapterMock, for: ExLine.Client.Adapter)
# in a test
client = ExLine.Client.new(access_token: "tok", adapter: MyApp.LineAdapterMock)
Mox.expect(MyApp.LineAdapterMock, :request, fn req ->
assert req.url == "https://api.line.me/v2/bot/message/push"
{:ok, %{status: 200, body: %{}}}
end)
ExLine.Api.Messaging.push(client, "U1", ExLine.Message.text("hi"))Status
Early. Implemented: client + adapter, message builders (text / sticker / buttons /
confirm + actions), Messaging.reply / push, webhook signature verification +
Plug, and the event routing DSL. Broader Messaging coverage (multicast / broadcast /
rich menu / content) and LIFF support are planned — see notes/.