250 lines
7.3 KiB
Go
250 lines
7.3 KiB
Go
package kutt
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// Client 是 Kutt API 客户端
|
||
// 用于创建和管理短链接
|
||
type Client struct {
|
||
apiURL string // Kutt API 基础 URL
|
||
apiKey string // API 认证密钥
|
||
httpClient *http.Client // HTTP 客户端
|
||
}
|
||
|
||
// NewClient 创建一个新的 Kutt 客户端
|
||
//
|
||
// 参数:
|
||
// - apiURL: Kutt API 基础 URL (例如: https://kutt.it/api/v2)
|
||
// - apiKey: Kutt API 密钥
|
||
//
|
||
// 返回:
|
||
// - *Client: Kutt 客户端实例
|
||
func NewClient(apiURL, apiKey string) *Client {
|
||
return &Client{
|
||
apiURL: apiURL,
|
||
apiKey: apiKey,
|
||
httpClient: &http.Client{
|
||
Timeout: 10 * time.Second,
|
||
},
|
||
}
|
||
}
|
||
|
||
// CreateLinkRequest 创建短链接的请求参数
|
||
type CreateLinkRequest struct {
|
||
Target string `json:"target"` // 目标 URL (必填)
|
||
Description string `json:"description,omitempty"` // 链接描述 (可选)
|
||
ExpireIn string `json:"expire_in,omitempty"` // 过期时间,例如 "2 days" (可选)
|
||
Password string `json:"password,omitempty"` // 访问密码 (可选)
|
||
CustomURL string `json:"customurl,omitempty"` // 自定义短链后缀 (可选)
|
||
Reuse bool `json:"reuse,omitempty"` // 如果目标 URL 已存在则复用 (可选)
|
||
Domain string `json:"domain,omitempty"` // 自定义域名 (可选)
|
||
}
|
||
|
||
// Link 短链接响应结构
|
||
type Link struct {
|
||
ID string `json:"id"` // 链接 UUID
|
||
Address string `json:"address"` // 短链地址后缀
|
||
Banned bool `json:"banned"` // 是否被封禁
|
||
CreatedAt time.Time `json:"created_at"` // 创建时间
|
||
Link string `json:"link"` // 完整短链接 URL
|
||
Password bool `json:"password"` // 是否有密码保护
|
||
Target string `json:"target"` // 目标 URL
|
||
Description string `json:"description"` // 链接描述
|
||
UpdatedAt time.Time `json:"updated_at"` // 更新时间
|
||
VisitCount int `json:"visit_count"` // 访问次数
|
||
}
|
||
|
||
// ErrorResponse Kutt API 错误响应
|
||
type ErrorResponse struct {
|
||
Error string `json:"error"`
|
||
Message string `json:"message"`
|
||
}
|
||
|
||
// CreateShortLink 创建短链接
|
||
//
|
||
// 参数:
|
||
// - ctx: 上下文
|
||
// - req: 创建请求参数
|
||
//
|
||
// 返回:
|
||
// - *Link: 创建的短链接信息
|
||
// - error: 错误信息
|
||
func (c *Client) CreateShortLink(ctx context.Context, req *CreateLinkRequest) (*Link, error) {
|
||
// 序列化请求体
|
||
body, err := json.Marshal(req)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("marshal request failed: %w", err)
|
||
}
|
||
|
||
// 创建 HTTP 请求
|
||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.apiURL+"/links", bytes.NewReader(body))
|
||
if err != nil {
|
||
return nil, fmt.Errorf("create request failed: %w", err)
|
||
}
|
||
|
||
// 设置请求头
|
||
httpReq.Header.Set("Content-Type", "application/json")
|
||
httpReq.Header.Set("X-API-KEY", c.apiKey)
|
||
|
||
// 发送请求
|
||
resp, err := c.httpClient.Do(httpReq)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("send request failed: %w", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
// 读取响应体
|
||
respBody, err := io.ReadAll(resp.Body)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("read response failed: %w", err)
|
||
}
|
||
|
||
// 检查响应状态
|
||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
||
var errResp ErrorResponse
|
||
if err := json.Unmarshal(respBody, &errResp); err == nil && errResp.Error != "" {
|
||
return nil, fmt.Errorf("kutt api error: %s - %s", errResp.Error, errResp.Message)
|
||
}
|
||
return nil, fmt.Errorf("kutt api error: status %d, body: %s", resp.StatusCode, string(respBody))
|
||
}
|
||
|
||
// 解析响应
|
||
var link Link
|
||
if err := json.Unmarshal(respBody, &link); err != nil {
|
||
return nil, fmt.Errorf("unmarshal response failed: %w", err)
|
||
}
|
||
|
||
return &link, nil
|
||
}
|
||
|
||
// CreateInviteShortLink 为邀请码创建短链接
|
||
// 这是一个便捷方法,用于生成邀请链接
|
||
//
|
||
// 参数:
|
||
// - ctx: 上下文
|
||
// - baseURL: 注册页面基础 URL (例如: https://gethifast.net)
|
||
// - inviteCode: 邀请码
|
||
// - domain: 短链接域名 (例如: getsapp.net),可为空
|
||
//
|
||
// 返回:
|
||
// - string: 短链接 URL
|
||
// - error: 错误信息
|
||
func (c *Client) CreateInviteShortLink(ctx context.Context, baseURL, inviteCode, domain string) (string, error) {
|
||
// 构建目标 URL - 落地页
|
||
// 格式:https://gethifast.net/?ic=邀请码
|
||
targetURL := fmt.Sprintf("%s?ic=%s", baseURL, inviteCode)
|
||
|
||
req := &CreateLinkRequest{
|
||
Target: targetURL,
|
||
Description: fmt.Sprintf("Invite link for code: %s", inviteCode),
|
||
Reuse: true, // 如果已存在相同目标 URL 则复用
|
||
Domain: domain,
|
||
}
|
||
|
||
link, err := c.CreateShortLink(ctx, req)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
// 强制使用 HTTPS
|
||
if strings.HasPrefix(link.Link, "http://") {
|
||
link.Link = strings.Replace(link.Link, "http://", "https://", 1)
|
||
}
|
||
|
||
return link.Link, nil
|
||
}
|
||
|
||
// PeriodStats 时间段统计数据
|
||
type PeriodStats struct {
|
||
Total int `json:"total"` // 总访问量
|
||
Views []int `json:"views"` // 时间序列数据(按天/小时)
|
||
Stats StatsDetail `json:"stats"` // 详细统计
|
||
}
|
||
|
||
// StatsDetail 详细统计信息
|
||
type StatsDetail struct {
|
||
Browser []StatItem `json:"browser"` // 浏览器分布
|
||
OS []StatItem `json:"os"` // 操作系统分布
|
||
Country []StatItem `json:"country"` // 国家分布
|
||
Referrer []StatItem `json:"referrer"` // 来源分布
|
||
}
|
||
|
||
// StatItem 统计项
|
||
type StatItem struct {
|
||
Name string `json:"name"`
|
||
Value int `json:"value"`
|
||
}
|
||
|
||
// LinkStatsResponse 链接详细统计响应
|
||
type LinkStatsResponse struct {
|
||
ID string `json:"id"`
|
||
Address string `json:"address"`
|
||
Link string `json:"link"`
|
||
Target string `json:"target"`
|
||
VisitCount int `json:"visit_count"`
|
||
LastDay PeriodStats `json:"lastDay"`
|
||
LastWeek PeriodStats `json:"lastWeek"`
|
||
LastMonth PeriodStats `json:"lastMonth"`
|
||
LastYear PeriodStats `json:"lastYear"`
|
||
}
|
||
|
||
// GetLinkStats 获取链接的详细统计数据
|
||
//
|
||
// 参数:
|
||
// - ctx: 上下文
|
||
// - linkID: 链接的 UUID
|
||
//
|
||
// 返回:
|
||
// - *LinkStatsResponse: 详细统计数据
|
||
// - error: 错误信息
|
||
func (c *Client) GetLinkStats(ctx context.Context, linkID string) (*LinkStatsResponse, error) {
|
||
// 创建 HTTP 请求
|
||
url := fmt.Sprintf("%s/links/%s/stats", c.apiURL, linkID)
|
||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("create request failed: %w", err)
|
||
}
|
||
|
||
// 设置请求头
|
||
httpReq.Header.Set("X-API-KEY", c.apiKey)
|
||
httpReq.Header.Set("Content-Type", "application/json")
|
||
|
||
// 发送请求
|
||
resp, err := c.httpClient.Do(httpReq)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("send request failed: %w", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
// 读取响应体
|
||
respBody, err := io.ReadAll(resp.Body)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("read response failed: %w", err)
|
||
}
|
||
|
||
// 检查响应状态
|
||
if resp.StatusCode != http.StatusOK {
|
||
var errResp ErrorResponse
|
||
if err := json.Unmarshal(respBody, &errResp); err == nil && errResp.Error != "" {
|
||
return nil, fmt.Errorf("kutt api error: %s - %s", errResp.Error, errResp.Message)
|
||
}
|
||
return nil, fmt.Errorf("kutt api error: status %d, body: %s", resp.StatusCode, string(respBody))
|
||
}
|
||
|
||
// 解析响应
|
||
var stats LinkStatsResponse
|
||
if err := json.Unmarshal(respBody, &stats); err != nil {
|
||
return nil, fmt.Errorf("unmarshal response failed: %w", err)
|
||
}
|
||
|
||
return &stats, nil
|
||
}
|