LINE Messaging APIをSDKを使わずにSpring Bootで実装

LINE Messaging APIを使用したwebhookベースのbotを、SDKを使わずにSpring BootとJavaで実装する方法が注目されています。このアプローチでは、メッセージ、フォロー、フォロー解除、アカウント連携などの主要なイベントを処理するカスタムコードを作成し、より柔軟で制御可能なボット開発が可能になります。

以下は、LINE Messaging APIのwebhookを受け取るSpring Bootアプリケーションのサンプルコードです。このコードは、メッセージイベント、フォローイベント、フォロー解除イベント、アカウント連携イベントを処理します。

```java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

@SpringBootApplication
@RestController
public class LineBotApplication {

    private static final String CHANNEL_SECRET = "YOUR_CHANNEL_SECRET";

    public static void main(String[] args) {
        SpringApplication.run(LineBotApplication.class, args);
    }

    @PostMapping("/callback")
    public String handleCallback(@RequestBody String requestBody, @RequestHeader("X-Line-Signature") String signature) {
        if (verifySignature(requestBody, signature)) {
            try {
                ObjectMapper objectMapper = new ObjectMapper();
                JsonNode jsonNode = objectMapper.readTree(requestBody);
                JsonNode events = jsonNode.get("events");
                for (JsonNode event : events) {
                    String eventType = event.get("type").asText();
                    switch (eventType) {
                        case "message":
                            handleMessageEvent(event);
                            break;
                        case "follow":
                            handleFollowEvent(event);
                            break;
                        case "unfollow":
                            handleUnfollowEvent(event);
                            break;
                        case "accountLink":
                            handleAccountLinkEvent(event);
                            break;
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return "OK";
    }

    private void handleMessageEvent(JsonNode event) {
        String messageType = event.get("message").get("type").asText();
        String userId = event.get("source").get("userId").asText();
        System.out.println("Received message from user: " + userId);
        // メッセージタイプに応じた処理を実装
    }

    private void handleFollowEvent(JsonNode event) {
        String userId = event.get("source").get("userId").asText();
        System.out.println("User followed: " + userId);
        // フォロー時の処理を実装
    }

    private void handleUnfollowEvent(JsonNode event) {
        String userId = event.get("source").get("userId").asText();
        System.out.println("User unfollowed: " + userId);
        // フォロー解除時の処理を実装
    }

    private void handleAccountLinkEvent(JsonNode event) {
        String userId = event.get("source").get("userId").asText();
        String result = event.get("link").get("result").asText();
        System.out.println("Account link event for user: " + userId + ", result: " + result);
        // アカウント連携時の処理を実装
    }

    private boolean verifySignature(String requestBody, String signature) {
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            SecretKeySpec secret = new SecretKeySpec(CHANNEL_SECRET.getBytes(), "HmacSHA256");
            mac.init(secret);
            byte[] hash = mac.doFinal(requestBody.getBytes());
            String calculatedSignature = Base64.getEncoder().encodeToString(hash);
            return calculatedSignature.equals(signature);
        } catch (NoSuchAlgorithmException | InvalidKeyException e) {
            e.printStackTrace();
            return false;
        }
    }
}
```

このサンプルコードでは、以下の処理を実装しています[1][3][6]:

1. `/callback`エンドポイントでwebhookを受け取ります。

2. 受信したwebhookの署名を検証します。

3. イベントタイプに応じて適切なハンドラメソッドを呼び出します。

4. 各イベントタイプ（メッセージ、フォロー、フォロー解除、アカウント連携）に対する基本的な処理を実装しています。

5. 実際のLINE Messaging APIの応答を実装するには、LINE PlatformへのHTTPリクエストを送信する必要があります。これは`handleMessageEvent`などのメソッド内で実装します[11][14]。

このコードを使用するには、`CHANNEL_SECRET`を自分のLINEチャネルのシークレットに置き換える必要があります。また、実際のアプリケーションでは、セキュリティを強化し、エラー処理を追加し、LINE Platformへの応答を実装する必要があります[15][16]。

Citations:
[1] https://github.com/hunkim/Line-chatbot-for-COMP3111/blob/master/line-bot-spring-boot/README.md
[2] https://stackoverflow.com/questions/78526601/line-webhook-not-getting-active-programatically/78526649
[3] https://github.com/line/line-bot-sdk-java/
[4] https://syogo0417.hatenablog.com/entry/2017/08/23/130004
[5] https://developers.line.biz/ja/docs/messaging-api/receiving-messages/
[6] https://developers.line.biz/en/docs/messaging-api/receiving-messages/
[7] https://innovationm.co/line-messaging-api-integration-with-spring-boot/
[8] https://www.divx.co.jp/media/techblog-231220-02
[9] https://github.com/line/line-bot-sdk-java/
[10] https://qiita.com/blue_islands/items/582db1f8354aadea4f70
[11] https://developers.line.biz/ja/docs/messaging-api/nodejs-sample/
[12] https://kikutaro777.hatenablog.com/entry/2017/01/16/230122
[13] https://stackoverflow.com/questions/79021823/line-messaging-api-webhook-stuck-in-standby-mode-despite-being-set-to-active
[14] https://zenn.dev/otkshol/articles/52d2649c6fa4dc
[15] https://github.com/line/line-bot-sdk-java/issues/515
[16] https://developers.worksmobile.com/jp/docs/bot-callback
[17] https://qiita.com/shindo_ryo/items/54fccbe09ca904b4b9e8
[18] https://github.com/gishi-yama/linebot-java-handson/blob/master/doc/Basics/05.md
[19] https://udemy.benesse.co.jp/development/app/line-bot-making.html
[20] https://blog.socialplus.jp/knowledge/line-webhook/