Files
tgex-backend/telegram_bot/adapter/backend/backend_client.go
2026-01-01 19:33:37 +03:00

769 lines
15 KiB
Go

package backend
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"time"
)
var ErrNotFound = errors.New("not found")
type Config struct {
BaseURL string `envconfig:"BACKEND__BASE_URL" default:"http_v1://localhost:8000"`
LoginURL string `envconfig:"LOGIN_URL" required:"true"`
}
type Client struct {
http *http.Client
baseURL string
loginURL string
}
func New(cfg Config) *Client {
return &Client{
http: &http.Client{
Timeout: 5 * time.Second,
},
baseURL: cfg.BaseURL,
loginURL: cfg.LoginURL,
}
}
func withBearer(token string) func(*http.Request) {
return func(r *http.Request) {
r.Header.Set("Authorization", "Bearer "+token)
}
}
func (c *Client) do(ctx context.Context, method string, path string, in any, out any, opts ...func(*http.Request)) error {
var body io.Reader
if in != nil {
b, err := json.Marshal(in)
if err != nil {
return fmt.Errorf("json.Marshal: %w", err)
}
body = bytes.NewReader(b)
}
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+"/"+path, body)
if err != nil {
return fmt.Errorf("http_v1.NewRequest: %w", err)
}
if in != nil {
req.Header.Set("Content-Type", "application/json")
}
for _, opt := range opts {
opt(req)
}
resp, err := c.http.Do(req)
if err != nil {
return fmt.Errorf("client.Do: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return ErrNotFound
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
b, _ := io.ReadAll(io.LimitReader(resp.Body, 8<<10))
return fmt.Errorf("request failed: %s: %s", resp.Status, b)
}
if out != nil {
if err := json.NewDecoder(resp.Body).Decode(out); err != nil {
return fmt.Errorf("json.Decode: %w", err)
}
}
return nil
}
func (c *Client) GetLoginURL(token string) string {
return c.loginURL + token
}
func (c *Client) CreateLoginToken(
ctx context.Context,
telegramID int64,
username *string,
) (string, error) {
req := struct {
TelegramID int64 `json:"telegram_id"`
Username *string `json:"username"`
}{
TelegramID: telegramID,
Username: username,
}
var token string
err := c.do(
ctx,
http.MethodPost,
"api/v1/internal/auth/login-token",
req,
&token,
)
return token, err
}
func (c *Client) GetJWTByTelegramID(
ctx context.Context,
telegramID int64,
) (string, error) {
q := url.Values{}
q.Set("telegram_id", fmt.Sprint(telegramID))
path := "api/v1/internal/auth/jwt?" + q.Encode()
var resp struct {
AccessToken string `json:"access_token"`
}
err := c.do(ctx, http.MethodGet, path, nil, &resp)
return resp.AccessToken, err
}
type Workspace struct {
ID string `json:"id"`
Name string `json:"name"`
}
type Project struct {
ID string `json:"id"`
TelegramID int64 `json:"telegram_id"`
Title string `json:"title"`
Username *string `json:"username"`
Status string `json:"status"`
InviteLinkType string `json:"invite_link_type"` // "public" или "approval"
}
type Page struct {
Items []Project `json:"items"`
Total int `json:"total"`
Page int `json:"page"`
Size int `json:"size"`
Pages int `json:"pages"`
}
func (c *Client) GetWorkspaces(
ctx context.Context,
jwt string,
) ([]Workspace, error) {
var resp struct {
Items []Workspace `json:"items"`
}
err := c.do(
ctx,
http.MethodGet,
"api/v1/workspaces",
nil,
&resp,
withBearer(jwt),
)
return resp.Items, err
}
func (c *Client) GetProjects(
ctx context.Context,
jwt string,
workspaceID string,
page, size int,
) (*Page, error) {
q := url.Values{}
q.Set("page", fmt.Sprint(page))
q.Set("size", fmt.Sprint(size))
path := fmt.Sprintf("api/v1/workspaces/%s/projects?%s", workspaceID, q.Encode())
var resp Page
err := c.do(
ctx,
http.MethodGet,
path,
nil,
&resp,
withBearer(jwt),
)
return &resp, err
}
func (c *Client) GetProject(
ctx context.Context,
workspaceID string,
projectID string,
) (*Project, error) {
path := fmt.Sprintf("api/v1/internal/projects/%s/%s", workspaceID, projectID)
var project Project
err := c.do(
ctx,
http.MethodGet,
path,
nil,
&project,
)
return &project, err
}
func (c *Client) UpdateProjectInviteLinkType(
ctx context.Context,
jwt string,
workspaceID string,
projectID string,
inviteLinkType string, // "public" или "approval"
) (*Project, error) {
path := fmt.Sprintf("api/v1/workspaces/%s/projects/%s/invite-link-type", workspaceID, projectID)
payload := map[string]string{
"invite_link_type": inviteLinkType,
}
var project Project
err := c.do(
ctx,
http.MethodPatch,
path,
payload,
&project,
withBearer(jwt),
)
return &project, err
}
func (c *Client) ForwardTelegramUpdate(
ctx context.Context,
update any,
) error {
return c.do(
ctx,
http.MethodPost,
"api/v1/internal/telegram/updates",
update,
nil,
)
}
func (c *Client) AttachChannelToWorkspace(
ctx context.Context,
channelID string,
workspaceID string,
userTelegramID int64,
) (*Project, error) {
input := map[string]any{
"channel_id": channelID,
"workspace_id": workspaceID,
"user_telegram_id": userTelegramID,
}
var project Project
err := c.do(
ctx,
http.MethodPost,
"api/v1/internal/projects",
input,
&project,
)
return &project, err
}
func (c *Client) AcceptWorkspaceInvite(
ctx context.Context,
jwt string,
inviteID string,
) error {
path := fmt.Sprintf("api/v1/invites/%s/accept", inviteID)
return c.do(
ctx,
http.MethodPost,
path,
nil,
nil,
withBearer(jwt),
)
}
// ============================================================================
// Creatives
// ============================================================================
type Creative struct {
ID string `json:"id"`
ProjectID string `json:"project_id"`
Name string `json:"name"`
Text string `json:"text"`
IsArchived bool `json:"is_archived"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
type CreativesPage struct {
Items []Creative `json:"items"`
Total int `json:"total"`
Page int `json:"page"`
Size int `json:"size"`
Pages int `json:"pages"`
}
func (c *Client) GetCreatives(
ctx context.Context,
jwt string,
workspaceID string,
projectID *string,
includeArchived bool,
page, size int,
) (*CreativesPage, error) {
q := url.Values{}
if projectID != nil {
q.Set("project_id", *projectID)
}
q.Set("include_archived", fmt.Sprint(includeArchived))
q.Set("page", fmt.Sprint(page))
q.Set("size", fmt.Sprint(size))
path := fmt.Sprintf("api/v1/workspaces/%s/creatives?%s", workspaceID, q.Encode())
var resp CreativesPage
err := c.do(
ctx,
http.MethodGet,
path,
nil,
&resp,
withBearer(jwt),
)
return &resp, err
}
func (c *Client) GetCreative(
ctx context.Context,
jwt string,
workspaceID string,
creativeID string,
) (*Creative, error) {
path := fmt.Sprintf("api/v1/workspaces/%s/creatives/%s", workspaceID, creativeID)
var creative Creative
err := c.do(
ctx,
http.MethodGet,
path,
nil,
&creative,
withBearer(jwt),
)
return &creative, err
}
type CreateCreativeInput struct {
Name string `json:"name"`
Text string `json:"text"`
}
func (c *Client) CreateCreative(
ctx context.Context,
jwt string,
workspaceID string,
projectID string,
input CreateCreativeInput,
) (*Creative, error) {
q := url.Values{}
q.Set("project_id", projectID)
path := fmt.Sprintf("api/v1/workspaces/%s/creatives?%s", workspaceID, q.Encode())
var creative Creative
err := c.do(
ctx,
http.MethodPost,
path,
input,
&creative,
withBearer(jwt),
)
return &creative, err
}
type UpdateCreativeInput struct {
Name *string `json:"name,omitempty"`
Text *string `json:"text,omitempty"`
IsArchived *bool `json:"is_archived,omitempty"`
}
func (c *Client) UpdateCreative(
ctx context.Context,
jwt string,
workspaceID string,
creativeID string,
input UpdateCreativeInput,
) (*Creative, error) {
path := fmt.Sprintf("api/v1/workspaces/%s/creatives/%s", workspaceID, creativeID)
var creative Creative
err := c.do(
ctx,
http.MethodPatch,
path,
input,
&creative,
withBearer(jwt),
)
return &creative, err
}
func (c *Client) DeleteCreative(
ctx context.Context,
jwt string,
workspaceID string,
creativeID string,
) error {
path := fmt.Sprintf("api/v1/workspaces/%s/creatives/%s", workspaceID, creativeID)
return c.do(
ctx,
http.MethodDelete,
path,
nil,
nil,
withBearer(jwt),
)
}
// ============================================================================
// Placements (Размещения/Закупы)
// ============================================================================
type Placement struct {
ID string `json:"id"`
ProjectID string `json:"project_id"`
PlacementChannelID string `json:"placement_channel_id"`
CreativeID *string `json:"creative_id"`
Name string `json:"name"`
Price *int `json:"price"`
Status string `json:"status"`
InviteLink *string `json:"invite_link"`
IsArchived bool `json:"is_archived"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
type PlacementsPage struct {
Items []Placement `json:"items"`
Total int `json:"total"`
Page int `json:"page"`
Size int `json:"size"`
Pages int `json:"pages"`
}
func (c *Client) GetPlacements(
ctx context.Context,
jwt string,
workspaceID string,
projectID *string,
placementChannelID *string,
creativeID *string,
includeArchived bool,
page, size int,
) (*PlacementsPage, error) {
q := url.Values{}
if projectID != nil {
q.Set("project_id", *projectID)
}
if placementChannelID != nil {
q.Set("placement_channel_id", *placementChannelID)
}
if creativeID != nil {
q.Set("creative_id", *creativeID)
}
q.Set("include_archived", fmt.Sprint(includeArchived))
q.Set("page", fmt.Sprint(page))
q.Set("size", fmt.Sprint(size))
path := fmt.Sprintf("api/v1/workspaces/%s/placements?%s", workspaceID, q.Encode())
var resp PlacementsPage
err := c.do(
ctx,
http.MethodGet,
path,
nil,
&resp,
withBearer(jwt),
)
return &resp, err
}
func (c *Client) GetPlacement(
ctx context.Context,
jwt string,
workspaceID string,
placementID string,
) (*Placement, error) {
path := fmt.Sprintf("api/v1/workspaces/%s/placements/%s", workspaceID, placementID)
var placement Placement
err := c.do(
ctx,
http.MethodGet,
path,
nil,
&placement,
withBearer(jwt),
)
return &placement, err
}
type CreatePlacementInput struct {
ProjectID string `json:"project_id"`
PlacementChannelID string `json:"placement_channel_id"`
CreativeID *string `json:"creative_id,omitempty"`
Name string `json:"name"`
Price *int `json:"price,omitempty"`
}
func (c *Client) CreatePlacement(
ctx context.Context,
jwt string,
workspaceID string,
input CreatePlacementInput,
) (*Placement, error) {
path := fmt.Sprintf("api/v1/workspaces/%s/placements", workspaceID)
var placement Placement
err := c.do(
ctx,
http.MethodPost,
path,
input,
&placement,
withBearer(jwt),
)
return &placement, err
}
type UpdatePlacementInput struct {
Name *string `json:"name,omitempty"`
Price *int `json:"price,omitempty"`
Status *string `json:"status,omitempty"`
IsArchived *bool `json:"is_archived,omitempty"`
}
func (c *Client) UpdatePlacement(
ctx context.Context,
jwt string,
workspaceID string,
placementID string,
input UpdatePlacementInput,
) (*Placement, error) {
path := fmt.Sprintf("api/v1/workspaces/%s/placements/%s", workspaceID, placementID)
var placement Placement
err := c.do(
ctx,
http.MethodPatch,
path,
input,
&placement,
withBearer(jwt),
)
return &placement, err
}
func (c *Client) DeletePlacement(
ctx context.Context,
jwt string,
workspaceID string,
placementID string,
) error {
path := fmt.Sprintf("api/v1/workspaces/%s/placements/%s", workspaceID, placementID)
return c.do(
ctx,
http.MethodDelete,
path,
nil,
nil,
withBearer(jwt),
)
}
// ============================================================================
// Purchase Plan
// ============================================================================
type Channel struct {
ID string `json:"id"`
TelegramID *int64 `json:"telegram_id"`
Title *string `json:"title"`
Username *string `json:"username"`
}
type PurchasePlanChannel struct {
ID string `json:"id"`
Channel Channel `json:"channel"`
Status string `json:"status"`
PlannedCost *float64 `json:"planned_cost"`
Comment *string `json:"comment"`
}
func (c *Client) GetPurchasePlanChannels(
ctx context.Context,
jwt string,
workspaceID string,
projectID string,
) ([]PurchasePlanChannel, error) {
path := fmt.Sprintf("api/v1/workspaces/%s/projects/%s/purchase-plan/channels", workspaceID, projectID)
var resp struct {
Items []PurchasePlanChannel `json:"items"`
}
err := c.do(
ctx,
http.MethodGet,
path,
nil,
&resp,
withBearer(jwt),
)
return resp.Items, err
}
type AttachChannelToPurchasePlanInput struct {
Username string `json:"username"`
Status *string `json:"status,omitempty"`
PlannedCost *float64 `json:"planned_cost,omitempty"`
Comment *string `json:"comment,omitempty"`
}
func (c *Client) AttachChannelToPurchasePlan(
ctx context.Context,
jwt string,
workspaceID string,
projectID string,
input AttachChannelToPurchasePlanInput,
) (*PurchasePlanChannel, error) {
path := fmt.Sprintf("api/v1/workspaces/%s/projects/%s/purchase-plan/channels", workspaceID, projectID)
var planChannel PurchasePlanChannel
err := c.do(
ctx,
http.MethodPost,
path,
input,
&planChannel,
withBearer(jwt),
)
return &planChannel, err
}
type UpdatePurchasePlanChannelInput struct {
Status *string `json:"status,omitempty"`
PlannedCost *float64 `json:"planned_cost,omitempty"`
Comment *string `json:"comment,omitempty"`
}
func (c *Client) UpdatePurchasePlanChannel(
ctx context.Context,
jwt string,
workspaceID string,
projectID string,
channelID string,
input UpdatePurchasePlanChannelInput,
) (*PurchasePlanChannel, error) {
path := fmt.Sprintf("api/v1/workspaces/%s/projects/%s/purchase-plan/channels/%s", workspaceID, projectID, channelID)
var planChannel PurchasePlanChannel
err := c.do(
ctx,
http.MethodPatch,
path,
input,
&planChannel,
withBearer(jwt),
)
return &planChannel, err
}
func (c *Client) RemoveChannelFromPurchasePlan(
ctx context.Context,
jwt string,
workspaceID string,
projectID string,
channelID string,
) error {
path := fmt.Sprintf("api/v1/workspaces/%s/projects/%s/purchase-plan/channels/%s", workspaceID, projectID, channelID)
return c.do(
ctx,
http.MethodDelete,
path,
nil,
nil,
withBearer(jwt),
)
}
// ============================================================================
// Workspace Members
// ============================================================================
type WorkspaceMember struct {
ID string `json:"id"`
WorkspaceID string `json:"workspace_id"`
UserID string `json:"user_id"`
Username string `json:"username"`
}
func (c *Client) GetWorkspaceMembers(
ctx context.Context,
jwt string,
workspaceID string,
) ([]WorkspaceMember, error) {
path := fmt.Sprintf("api/v1/workspaces/%s/members", workspaceID)
var resp struct {
Items []WorkspaceMember `json:"items"`
}
err := c.do(
ctx,
http.MethodGet,
path,
nil,
&resp,
withBearer(jwt),
)
return resp.Items, err
}