Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v7
with:
go-version: '1.25'
go-version-file: go.mod

- name: Build
run: go build -v ./...
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ A minimal Golang LINE Bot template built on the official [line-bot-sdk-go v8](ht
- Replies with the sticker ID / resource type when it receives a sticker
- Logs every incoming event so you can see the payload shape while developing

That is the whole app — one file, [`main.go`](main.go), roughly 100 lines. It is meant to be read and then rewritten into your own bot.
That is the whole app — one file, [`main.go`](main.go), roughly 150 lines. It is meant to be read and then rewritten into your own bot.

## Requirements

| | |
|---|---|
| Go | 1.25 or later |
| SDK | `github.com/line/line-bot-sdk-go/v8` v8.22.0 |
| Go | 1.26 or later |
| SDK | `github.com/line/line-bot-sdk-go/v8` v8.23.0 |

## Environment variables

Expand Down Expand Up @@ -93,10 +93,10 @@ Then set the webhook URL to `https://<subdomain>.ngrok-free.app/callback`.

## Making it your own

All the interesting code lives in the event loop in `main.go`:
All the interesting code lives in `handleEvent` in `main.go`; `callbackHandler` verifies the signature and feeds it each event, and `replyText` is a helper for plain text replies:

```go
for _, event := range cb.Events {
func handleEvent(bot *messaging_api.MessagingApiAPI, event webhook.EventInterface) {
switch e := event.(type) {
case webhook.MessageEvent:
switch message := e.Message.(type) {
Expand Down
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/kkdai/LineBotTemplate

// +heroku goVersion go1.25
go 1.25
// +heroku goVersion go1.26
go 1.26

require github.com/line/line-bot-sdk-go/v8 v8.22.0
require github.com/line/line-bot-sdk-go/v8 v8.23.0
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
github.com/line/line-bot-sdk-go/v8 v8.22.0 h1:jkxX5Ds8zlPSxUYil63P/Xg52SJ+kN/Xa7A4t/ItlDw=
github.com/line/line-bot-sdk-go/v8 v8.22.0/go.mod h1:QMXJwPka2ysSeVQKWXkBp8DzBFs+CFAXFNo75KJtWho=
github.com/line/line-bot-sdk-go/v8 v8.23.0 h1:Yw+6RjCmVvZdKX+0H/ZeWIoTqYFFk8WojGB5qYuAafo=
github.com/line/line-bot-sdk-go/v8 v8.23.0/go.mod h1:WCVmO0tLqNcNZMu3Vldsg6CgXikjVOkzgZzWDENJ+Zo=
125 changes: 56 additions & 69 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,94 +24,81 @@ import (
)

func main() {
channelSecret := os.Getenv("ChannelSecret")
bot, err := messaging_api.NewMessagingApiAPI(
os.Getenv("ChannelAccessToken"),
)
bot, err := messaging_api.NewMessagingApiAPI(os.Getenv("ChannelAccessToken"))
if err != nil {
log.Fatal(err)
}

// Setup HTTP Server for receiving requests from LINE platform
http.HandleFunc("/callback", func(w http.ResponseWriter, req *http.Request) {
log.Println("/callback called...")
http.HandleFunc("/callback", callbackHandler(os.Getenv("ChannelSecret"), bot))

// This is just sample code.
// For actual use, you must support HTTPS by using `ListenAndServeTLS`, a reverse proxy or something else.
port := os.Getenv("PORT")
if port == "" {
port = "5000"
}
fmt.Println("http://localhost:" + port + "/")
if err := http.ListenAndServe(":"+port, nil); err != nil {
log.Fatal(err)
}
}

// callbackHandler returns the webhook handler: it verifies the LINE signature,
// then dispatches every event in the request.
func callbackHandler(channelSecret string, bot *messaging_api.MessagingApiAPI) http.HandlerFunc {
return func(w http.ResponseWriter, req *http.Request) {
cb, err := webhook.ParseRequest(channelSecret, req)
if err != nil {
log.Printf("Cannot parse request: %+v\n", err)
if errors.Is(err, webhook.ErrInvalidSignature) {
w.WriteHeader(400)
w.WriteHeader(http.StatusBadRequest)
} else {
w.WriteHeader(500)
w.WriteHeader(http.StatusInternalServerError)
}
return
}

log.Println("Handling events...")
for _, event := range cb.Events {
log.Printf("/callback called%+v...\n", event)

switch e := event.(type) {
case webhook.MessageEvent:
switch message := e.Message.(type) {
case webhook.TextMessageContent:
if _, err = bot.ReplyMessage(
&messaging_api.ReplyMessageRequest{
ReplyToken: e.ReplyToken,
Messages: []messaging_api.MessageInterface{
messaging_api.TextMessage{
Text: message.Text,
},
},
},
); err != nil {
log.Print(err)
} else {
log.Println("Sent text reply.")
}
case webhook.StickerMessageContent:
replyMessage := fmt.Sprintf(
"貼圖訊息: sticker id is %s, stickerResourceType is %s", message.StickerId, message.StickerResourceType)
if _, err = bot.ReplyMessage(
&messaging_api.ReplyMessageRequest{
ReplyToken: e.ReplyToken,
Messages: []messaging_api.MessageInterface{
messaging_api.TextMessage{
Text: replyMessage,
},
},
}); err != nil {
log.Print(err)
} else {
log.Println("Sent sticker reply.")
}
default:
log.Printf("Unsupported message content: %T\n", e.Message)
}
case webhook.FollowEvent:
log.Printf("Followed by %s\n", sourceID(e.Source))
case webhook.MemberJoinedEvent:
log.Printf("Members joined %s: %s\n", sourceID(e.Source), userIDs(orZero(e.Joined).Members))
case webhook.MemberLeftEvent:
log.Printf("Members left %s: %s\n", sourceID(e.Source), userIDs(orZero(e.Left).Members))
case webhook.BeaconEvent:
beacon := orZero(e.Beacon)
log.Printf("Beacon %q from %s: hwid=%s\n", beacon.Type, sourceID(e.Source), beacon.Hwid)
default:
log.Printf("Unsupported event: %T\n", event)
}
log.Printf("Handling event: %+v\n", event)
handleEvent(bot, event)
}
})
}
}

// This is just sample code.
// For actual use, you must support HTTPS by using `ListenAndServeTLS`, a reverse proxy or something else.
port := os.Getenv("PORT")
if port == "" {
port = "5000"
func handleEvent(bot *messaging_api.MessagingApiAPI, event webhook.EventInterface) {
switch e := event.(type) {
case webhook.MessageEvent:
switch message := e.Message.(type) {
case webhook.TextMessageContent:
replyText(bot, e.ReplyToken, message.Text)
case webhook.StickerMessageContent:
replyText(bot, e.ReplyToken, fmt.Sprintf(
"貼圖訊息: sticker id is %s, stickerResourceType is %s", message.StickerId, message.StickerResourceType))
default:
log.Printf("Unsupported message content: %T\n", e.Message)
}
case webhook.FollowEvent:
log.Printf("Followed by %s\n", sourceID(e.Source))
case webhook.MemberJoinedEvent:
log.Printf("Members joined %s: %s\n", sourceID(e.Source), userIDs(orZero(e.Joined).Members))
case webhook.MemberLeftEvent:
log.Printf("Members left %s: %s\n", sourceID(e.Source), userIDs(orZero(e.Left).Members))
case webhook.BeaconEvent:
beacon := orZero(e.Beacon)
log.Printf("Beacon %q from %s: hwid=%s\n", beacon.Type, sourceID(e.Source), beacon.Hwid)
default:
log.Printf("Unsupported event: %T\n", event)
}
fmt.Println("http://localhost:" + port + "/")
if err := http.ListenAndServe(":"+port, nil); err != nil {
log.Fatal(err)
}

// replyText answers a reply token with a single text message.
func replyText(bot *messaging_api.MessagingApiAPI, replyToken, text string) {
if _, err := bot.ReplyMessage(&messaging_api.ReplyMessageRequest{
ReplyToken: replyToken,
Messages: []messaging_api.MessageInterface{messaging_api.TextMessage{Text: text}},
}); err != nil {
log.Print(err)
}
}

Expand Down
Loading