diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index d71245e2..bfda23d7 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -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 ./... diff --git a/README.md b/README.md index 8f3b24d7..c6a011e3 100644 --- a/README.md +++ b/README.md @@ -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 @@ -93,10 +93,10 @@ Then set the webhook URL to `https://.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) { diff --git a/go.mod b/go.mod index 0237fae1..054b654f 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 30303ed2..91c3337c 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/main.go b/main.go index 999411eb..e7a9b8c8 100644 --- a/main.go +++ b/main.go @@ -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) } }