165 lines
4.9 KiB
Go
165 lines
4.9 KiB
Go
package line
|
|
|
|
// LineService provides high-level operations for the LINE channel.
|
|
// Reference: LINE Messaging API — https://developers.line.biz/en/docs/messaging-api/
|
|
//
|
|
// This service coordinates:
|
|
// - Sending messages via LINE Messaging API (push + reply)
|
|
// - Fetching user profiles from LINE
|
|
// - Validating channel access tokens
|
|
// - Managing channel configuration
|
|
//
|
|
// Design: Follows the same service pattern as TikTokService and WhatsAppService.
|
|
|
|
import (
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/go-resty/resty/v2"
|
|
|
|
applogger "github.com/gochat/gochat/pkg/logger"
|
|
)
|
|
|
|
// LineService provides LINE Messaging API operations.
|
|
type LineService struct {
|
|
repo *Repository
|
|
client *resty.Client
|
|
}
|
|
|
|
// NewLineService creates a LINE service instance.
|
|
func NewLineService(repo *Repository) *LineService {
|
|
client := resty.New()
|
|
client.SetTimeout(30 * time.Second)
|
|
return &LineService{
|
|
repo: repo,
|
|
client: client,
|
|
}
|
|
}
|
|
|
|
// === Message Sending ===
|
|
|
|
// ReplyMessage sends a reply using a webhook event's reply token.
|
|
// Reply tokens expire after 30 seconds — must respond quickly.
|
|
func (s *LineService) ReplyMessage(ctx context.Context, channelAccessToken, replyToken string, messages []OutboundMsg) error {
|
|
url := "https://api.line.me/v2/bot/message/reply"
|
|
|
|
resp, err := s.client.R().
|
|
SetContext(ctx).
|
|
SetHeader("Authorization", "Bearer "+channelAccessToken).
|
|
SetHeader("Content-Type", "application/json").
|
|
SetBody(ReplyMessageRequest{
|
|
ReplyToken: replyToken,
|
|
Messages: messages,
|
|
}).
|
|
Post(url)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("line ReplyMessage: API call failed: %w", err)
|
|
}
|
|
if resp.StatusCode() != 200 {
|
|
return fmt.Errorf("line ReplyMessage: API returned status %d: %s", resp.StatusCode(), resp.String())
|
|
}
|
|
|
|
applogger.L().Debugf("LINE ReplyMessage sent: replyToken=%s", replyToken)
|
|
return nil
|
|
}
|
|
|
|
// PushMessage sends a proactive message to a LINE user.
|
|
func (s *LineService) PushMessage(ctx context.Context, channelAccessToken, to string, messages []OutboundMsg) (*PushMessageResponse, error) {
|
|
url := "https://api.line.me/v2/bot/message/push"
|
|
|
|
var result PushMessageResponse
|
|
resp, err := s.client.R().
|
|
SetContext(ctx).
|
|
SetHeader("Authorization", "Bearer "+channelAccessToken).
|
|
SetHeader("Content-Type", "application/json").
|
|
SetBody(PushMessageRequest{
|
|
To: to,
|
|
Messages: messages,
|
|
}).
|
|
SetResult(&result).
|
|
Post(url)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("line PushMessage: API call failed: %w", err)
|
|
}
|
|
if resp.StatusCode() != 200 {
|
|
return nil, fmt.Errorf("line PushMessage: API returned status %d: %s", resp.StatusCode(), resp.String())
|
|
}
|
|
|
|
applogger.L().Debugf("LINE PushMessage sent: to=%s", to)
|
|
return &result, nil
|
|
}
|
|
|
|
// === Profile ===
|
|
|
|
// GetUserProfile fetches a LINE user's profile information.
|
|
func (s *LineService) GetUserProfile(ctx context.Context, channelAccessToken, userID string) (*UserProfile, error) {
|
|
url := fmt.Sprintf("https://api.line.me/v2/bot/profile/%s", userID)
|
|
|
|
var profile UserProfile
|
|
resp, err := s.client.R().
|
|
SetContext(ctx).
|
|
SetHeader("Authorization", "Bearer "+channelAccessToken).
|
|
SetResult(&profile).
|
|
Get(url)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("line GetUserProfile: API call failed: %w", err)
|
|
}
|
|
if resp.StatusCode() != 200 {
|
|
return nil, fmt.Errorf("line GetUserProfile: API returned status %d", resp.StatusCode())
|
|
}
|
|
|
|
return &profile, nil
|
|
}
|
|
|
|
// === Webhook Verification ===
|
|
|
|
// VerifySignature validates the X-Line-Signature HMAC-SHA256 header.
|
|
// LINE signs every webhook request with the channel secret.
|
|
func (s *LineService) VerifySignature(channelSecret, requestBody, signature string) bool {
|
|
mac := hmac.New(sha256.New, []byte(channelSecret))
|
|
mac.Write([]byte(requestBody))
|
|
expectedSig := base64.StdEncoding.EncodeToString(mac.Sum(nil))
|
|
return hmac.Equal([]byte(expectedSig), []byte(signature))
|
|
}
|
|
|
|
// === Token Management ===
|
|
|
|
// ValidateAccessToken verifies a LINE channel access token.
|
|
func (s *LineService) ValidateAccessToken(ctx context.Context, channelAccessToken string) error {
|
|
url := "https://api.line.me/v2/bot/info"
|
|
|
|
resp, err := s.client.R().
|
|
SetContext(ctx).
|
|
SetHeader("Authorization", "Bearer "+channelAccessToken).
|
|
Get(url)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("line ValidateAccessToken: API call failed: %w", err)
|
|
}
|
|
if resp.StatusCode() != 200 {
|
|
return fmt.Errorf("line ValidateAccessToken: invalid token (status %d)", resp.StatusCode())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// === Configuration ===
|
|
|
|
// UpdateChannel updates LINE channel configuration fields.
|
|
func (s *LineService) UpdateChannel(ctx context.Context, channelID uint, updates map[string]interface{}) error {
|
|
return s.repo.UpdateFields(ctx, channelID, updates)
|
|
}
|
|
|
|
// MarkReauthorizationRequired flags a channel needing re-authentication.
|
|
func (s *LineService) MarkReauthorizationRequired(ctx context.Context, channelID uint) error {
|
|
return s.UpdateChannel(ctx, channelID, map[string]interface{}{
|
|
"reauthorization_required": true,
|
|
})
|
|
}
|