6e0519a232
- Implemented a new BrandAssetCleanupWorker to handle the cleanup of brand-related assets after a brand is deleted. - Added SQL queries for cleaning up articles, keywords, questions, competitors, and monitoring data associated with a brand. - Introduced a new API endpoint to delete publish records. - Updated the router to include the new delete publish record endpoint. - Added tests for the BrandAssetCleanupWorker to ensure proper functionality. - Created migration scripts to support soft deletion of publish records and to add the brand asset cleanup scheduler job.
562 lines
18 KiB
Go
562 lines
18 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"github.com/geo-platform/tenant-api/internal/shared/auth"
|
|
"github.com/geo-platform/tenant-api/internal/shared/response"
|
|
)
|
|
|
|
type MediaService struct {
|
|
pool *pgxpool.Pool
|
|
}
|
|
|
|
func NewMediaService(pool *pgxpool.Pool) *MediaService {
|
|
return &MediaService{pool: pool}
|
|
}
|
|
|
|
type MediaPlatformResponse struct {
|
|
ID int64 `json:"id"`
|
|
PlatformID string `json:"platform_id"`
|
|
Name string `json:"name"`
|
|
Category string `json:"category"`
|
|
ShortName string `json:"short_name"`
|
|
AccentColor string `json:"accent_color"`
|
|
LoginURL *string `json:"login_url"`
|
|
LogoURL *string `json:"logo_url"`
|
|
Status string `json:"status"`
|
|
SortOrder int `json:"sort_order"`
|
|
}
|
|
|
|
type PublishRecordResponse struct {
|
|
ID int64 `json:"id"`
|
|
PublishBatchID int64 `json:"publish_batch_id"`
|
|
ArticleID int64 `json:"article_id"`
|
|
BrandID int64 `json:"brand_id"`
|
|
BrandName *string `json:"brand_name"`
|
|
BrandDeleted bool `json:"brand_deleted"`
|
|
ArticleTitle *string `json:"article_title,omitempty"`
|
|
PlatformAccountID *int64 `json:"platform_account_id"`
|
|
DesktopAccountID *string `json:"desktop_account_id"`
|
|
DesktopTaskID *string `json:"desktop_task_id,omitempty"`
|
|
TargetType string `json:"target_type"`
|
|
TargetConnectionID *int64 `json:"target_connection_id"`
|
|
PlatformID string `json:"platform_id"`
|
|
PlatformName string `json:"platform_name"`
|
|
PlatformNickname string `json:"platform_nickname"`
|
|
Status string `json:"status"`
|
|
ExternalArticleID *string `json:"external_article_id"`
|
|
ExternalArticleURL *string `json:"external_article_url"`
|
|
ExternalManageURL *string `json:"external_manage_url"`
|
|
PublishedAt *time.Time `json:"published_at"`
|
|
ErrorMessage *string `json:"error_message"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
type ListPublishRecordsRequest struct {
|
|
Page int
|
|
PageSize int
|
|
Title string
|
|
}
|
|
|
|
type PublishRecordListResponse struct {
|
|
Items []PublishRecordResponse `json:"items"`
|
|
Page int `json:"page"`
|
|
PageSize int `json:"page_size"`
|
|
Total int `json:"total"`
|
|
PendingCount int `json:"pending_count"`
|
|
HistoryTotal int `json:"history_total"`
|
|
}
|
|
|
|
type DeletePublishRecordResponse struct {
|
|
Deleted bool `json:"deleted"`
|
|
}
|
|
|
|
func (s *MediaService) ListPlatforms(ctx context.Context) ([]MediaPlatformResponse, error) {
|
|
rows, err := s.pool.Query(ctx, `
|
|
SELECT id, platform_id, name, category, short_name, accent_color, login_url, logo_url, status, sort_order
|
|
FROM media_platforms
|
|
WHERE status = 'active'
|
|
ORDER BY sort_order ASC, id ASC
|
|
`)
|
|
if err != nil {
|
|
return nil, response.ErrInternal(50030, "media_platform_query_failed", "failed to list media platforms")
|
|
}
|
|
defer rows.Close()
|
|
|
|
items := make([]MediaPlatformResponse, 0)
|
|
for rows.Next() {
|
|
var item MediaPlatformResponse
|
|
if err := rows.Scan(
|
|
&item.ID,
|
|
&item.PlatformID,
|
|
&item.Name,
|
|
&item.Category,
|
|
&item.ShortName,
|
|
&item.AccentColor,
|
|
&item.LoginURL,
|
|
&item.LogoURL,
|
|
&item.Status,
|
|
&item.SortOrder,
|
|
); err != nil {
|
|
return nil, response.ErrInternal(50030, "media_platform_scan_failed", err.Error())
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
func (s *MediaService) ListPublishRecords(ctx context.Context, req ListPublishRecordsRequest) (*PublishRecordListResponse, error) {
|
|
actor := auth.MustActor(ctx)
|
|
if actor.TenantID == 0 || actor.UserID == 0 {
|
|
return nil, response.ErrUnauthorized(40132, "workspace_scope_missing", "workspace context is required")
|
|
}
|
|
|
|
page := req.Page
|
|
if page <= 0 {
|
|
page = 1
|
|
}
|
|
pageSize := req.PageSize
|
|
if pageSize <= 0 || pageSize > 50 {
|
|
pageSize = 10
|
|
}
|
|
title := strings.TrimSpace(req.Title)
|
|
pendingStatuses := []string{"queued", "publishing"}
|
|
historyStatuses := []string{"success", "failed", "cancelled", "canceled"}
|
|
|
|
pendingItems, err := s.listPublishRecordsByStatuses(ctx, actor.TenantID, actor.UserID, pendingStatuses, title, 0, 0, "pr.created_at ASC, pr.id ASC")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
historyTotal, err := s.countPublishRecordsByStatuses(ctx, actor.TenantID, actor.UserID, historyStatuses, title)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
offset := (page - 1) * pageSize
|
|
items := make([]PublishRecordResponse, 0, pageSize)
|
|
total := len(pendingItems) + historyTotal
|
|
|
|
if offset < len(pendingItems) {
|
|
end := offset + pageSize
|
|
if end > len(pendingItems) {
|
|
end = len(pendingItems)
|
|
}
|
|
items = append(items, pendingItems[offset:end]...)
|
|
}
|
|
|
|
remaining := pageSize - len(items)
|
|
historyOffset := offset - len(pendingItems)
|
|
if historyOffset < 0 {
|
|
historyOffset = 0
|
|
}
|
|
if remaining > 0 && historyOffset < historyTotal {
|
|
historyItems, listErr := s.listPublishRecordsByStatuses(ctx, actor.TenantID, actor.UserID, historyStatuses, title, remaining, historyOffset, "pr.updated_at DESC, pr.id DESC")
|
|
if listErr != nil {
|
|
return nil, listErr
|
|
}
|
|
items = append(items, historyItems...)
|
|
}
|
|
|
|
return &PublishRecordListResponse{
|
|
Items: items,
|
|
Page: page,
|
|
PageSize: pageSize,
|
|
Total: total,
|
|
PendingCount: len(pendingItems),
|
|
HistoryTotal: historyTotal,
|
|
}, nil
|
|
}
|
|
|
|
func (s *MediaService) listPublishRecordsByStatuses(
|
|
ctx context.Context,
|
|
tenantID int64,
|
|
userID int64,
|
|
statuses []string,
|
|
title string,
|
|
limit int,
|
|
offset int,
|
|
orderBy string,
|
|
) ([]PublishRecordResponse, error) {
|
|
query := `
|
|
SELECT pr.id, pr.publish_batch_id, pr.article_id,
|
|
a.brand_id, b.name AS brand_name, (b.id IS NULL OR b.deleted_at IS NOT NULL OR b.status IN ('deleting', 'deleted')) AS brand_deleted,
|
|
COALESCE(av.title, NULLIF(a.wizard_state_json ->> 'title', '')) AS article_title,
|
|
pr.platform_account_id, pa.desktop_id::text, dt.desktop_id::text,
|
|
COALESCE(pr.target_type, 'platform_account'), pr.target_connection_id, pr.platform_id,
|
|
COALESCE(mp.name, esc.site_name, esc.name, pr.platform_id) AS platform_name,
|
|
CASE
|
|
WHEN COALESCE(pr.target_type, 'platform_account') = 'enterprise_site'
|
|
THEN COALESCE(esc.site_name, esc.name, '企业站点')
|
|
ELSE COALESCE(pa.nickname, '')
|
|
END AS platform_nickname,
|
|
pr.status, pr.external_article_id, pr.external_article_url,
|
|
pr.external_manage_url, pr.published_at, pr.error_message, pr.created_at, pr.updated_at
|
|
FROM publish_records pr
|
|
JOIN articles a ON a.id = pr.article_id AND a.tenant_id = pr.tenant_id
|
|
LEFT JOIN brands b ON b.id = a.brand_id AND b.tenant_id = a.tenant_id
|
|
LEFT JOIN article_versions av ON av.id = a.current_version_id AND av.article_id = a.id
|
|
LEFT JOIN media_platforms mp ON mp.platform_id = pr.platform_id
|
|
LEFT JOIN platform_accounts pa ON pa.id = pr.platform_account_id
|
|
LEFT JOIN enterprise_site_connections esc
|
|
ON esc.id = pr.target_connection_id
|
|
AND esc.tenant_id = pr.tenant_id
|
|
LEFT JOIN LATERAL (
|
|
SELECT desktop_id
|
|
FROM desktop_tasks
|
|
WHERE tenant_id = pr.tenant_id
|
|
AND kind = 'publish'
|
|
AND payload->>'publish_record_id' = pr.id::text
|
|
ORDER BY updated_at DESC, desktop_id DESC
|
|
LIMIT 1
|
|
) dt ON TRUE
|
|
WHERE pr.tenant_id = $1
|
|
AND EXISTS (
|
|
SELECT 1
|
|
FROM publish_batches pb
|
|
WHERE pb.id = pr.publish_batch_id
|
|
AND pb.tenant_id = pr.tenant_id
|
|
AND pb.initiator_user_id = $2
|
|
)
|
|
AND pr.deleted_at IS NULL
|
|
AND pr.status = ANY($3)
|
|
AND (
|
|
$4 = ''
|
|
OR COALESCE(av.title, NULLIF(a.wizard_state_json ->> 'title', ''), '') ILIKE '%' || $4 || '%'
|
|
)
|
|
`
|
|
args := []any{tenantID, userID, statuses, title}
|
|
|
|
if strings.TrimSpace(orderBy) != "" {
|
|
query += "\nORDER BY " + orderBy
|
|
}
|
|
if limit > 0 {
|
|
query += "\nLIMIT $5 OFFSET $6"
|
|
args = append(args, limit, offset)
|
|
}
|
|
|
|
rows, err := s.pool.Query(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, response.ErrInternal(50043, "publish_record_query_failed", "failed to list publish records")
|
|
}
|
|
defer rows.Close()
|
|
|
|
items := make([]PublishRecordResponse, 0)
|
|
for rows.Next() {
|
|
var item PublishRecordResponse
|
|
var desktopAccountID *string
|
|
var desktopTaskID *string
|
|
if err := rows.Scan(
|
|
&item.ID,
|
|
&item.PublishBatchID,
|
|
&item.ArticleID,
|
|
&item.BrandID,
|
|
&item.BrandName,
|
|
&item.BrandDeleted,
|
|
&item.ArticleTitle,
|
|
&item.PlatformAccountID,
|
|
&desktopAccountID,
|
|
&desktopTaskID,
|
|
&item.TargetType,
|
|
&item.TargetConnectionID,
|
|
&item.PlatformID,
|
|
&item.PlatformName,
|
|
&item.PlatformNickname,
|
|
&item.Status,
|
|
&item.ExternalArticleID,
|
|
&item.ExternalArticleURL,
|
|
&item.ExternalManageURL,
|
|
&item.PublishedAt,
|
|
&item.ErrorMessage,
|
|
&item.CreatedAt,
|
|
&item.UpdatedAt,
|
|
); err != nil {
|
|
return nil, response.ErrInternal(50043, "publish_record_scan_failed", err.Error())
|
|
}
|
|
if desktopAccountID != nil && strings.TrimSpace(*desktopAccountID) != "" {
|
|
item.DesktopAccountID = desktopAccountID
|
|
}
|
|
if desktopTaskID != nil && strings.TrimSpace(*desktopTaskID) != "" {
|
|
item.DesktopTaskID = desktopTaskID
|
|
}
|
|
normalizePublishRecordForResponse(&item)
|
|
items = append(items, item)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, response.ErrInternal(50043, "publish_record_query_failed", "failed to list publish records")
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
func (s *MediaService) countPublishRecordsByStatuses(
|
|
ctx context.Context,
|
|
tenantID int64,
|
|
userID int64,
|
|
statuses []string,
|
|
title string,
|
|
) (int, error) {
|
|
var count int
|
|
err := s.pool.QueryRow(ctx, `
|
|
SELECT COUNT(1)
|
|
FROM publish_records pr
|
|
JOIN articles a ON a.id = pr.article_id AND a.tenant_id = pr.tenant_id
|
|
LEFT JOIN article_versions av ON av.id = a.current_version_id AND av.article_id = a.id
|
|
WHERE pr.tenant_id = $1
|
|
AND EXISTS (
|
|
SELECT 1
|
|
FROM publish_batches pb
|
|
WHERE pb.id = pr.publish_batch_id
|
|
AND pb.tenant_id = pr.tenant_id
|
|
AND pb.initiator_user_id = $2
|
|
)
|
|
AND pr.deleted_at IS NULL
|
|
AND pr.status = ANY($3)
|
|
AND (
|
|
$4 = ''
|
|
OR COALESCE(av.title, NULLIF(a.wizard_state_json ->> 'title', ''), '') ILIKE '%' || $4 || '%'
|
|
)
|
|
`, tenantID, userID, statuses, title).Scan(&count)
|
|
if err != nil {
|
|
return 0, response.ErrInternal(50043, "publish_record_query_failed", "failed to list publish records")
|
|
}
|
|
return count, nil
|
|
}
|
|
|
|
func (s *MediaService) ListArticlePublishRecords(ctx context.Context, articleID int64) ([]PublishRecordResponse, error) {
|
|
actor := auth.MustActor(ctx)
|
|
brandID, err := requireCurrentBrandID(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return s.listArticlePublishRecords(ctx, actor.TenantID, brandID, articleID)
|
|
}
|
|
|
|
func (s *MediaService) listArticlePublishRecords(ctx context.Context, tenantID, brandID, articleID int64) ([]PublishRecordResponse, error) {
|
|
rows, err := s.pool.Query(ctx, `
|
|
SELECT pr.id, pr.publish_batch_id, pr.article_id,
|
|
a.brand_id, b.name AS brand_name, (b.id IS NULL OR b.deleted_at IS NOT NULL OR b.status IN ('deleting', 'deleted')) AS brand_deleted,
|
|
pr.platform_account_id, pa.desktop_id::text,
|
|
COALESCE(pr.target_type, 'platform_account'), pr.target_connection_id, pr.platform_id,
|
|
COALESCE(mp.name, esc.site_name, esc.name, pr.platform_id) AS platform_name,
|
|
CASE
|
|
WHEN COALESCE(pr.target_type, 'platform_account') = 'enterprise_site'
|
|
THEN COALESCE(esc.site_name, esc.name, '企业站点')
|
|
ELSE COALESCE(pa.nickname, '')
|
|
END AS platform_nickname,
|
|
pr.status, pr.external_article_id, pr.external_article_url,
|
|
pr.external_manage_url, pr.published_at, pr.error_message, pr.created_at, pr.updated_at
|
|
FROM publish_records pr
|
|
LEFT JOIN media_platforms mp ON mp.platform_id = pr.platform_id
|
|
LEFT JOIN platform_accounts pa ON pa.id = pr.platform_account_id
|
|
LEFT JOIN enterprise_site_connections esc
|
|
ON esc.id = pr.target_connection_id
|
|
AND esc.tenant_id = pr.tenant_id
|
|
JOIN articles a ON a.id = pr.article_id AND a.tenant_id = pr.tenant_id
|
|
LEFT JOIN brands b ON b.id = a.brand_id AND b.tenant_id = a.tenant_id
|
|
WHERE pr.tenant_id = $1 AND pr.article_id = $2 AND a.brand_id = $3 AND a.deleted_at IS NULL AND pr.deleted_at IS NULL
|
|
ORDER BY pr.created_at DESC, pr.id DESC
|
|
`, tenantID, articleID, brandID)
|
|
if err != nil {
|
|
return nil, response.ErrInternal(50043, "publish_record_query_failed", "failed to list publish records")
|
|
}
|
|
defer rows.Close()
|
|
|
|
items := make([]PublishRecordResponse, 0)
|
|
for rows.Next() {
|
|
var item PublishRecordResponse
|
|
var desktopAccountID *string
|
|
if err := rows.Scan(
|
|
&item.ID,
|
|
&item.PublishBatchID,
|
|
&item.ArticleID,
|
|
&item.BrandID,
|
|
&item.BrandName,
|
|
&item.BrandDeleted,
|
|
&item.PlatformAccountID,
|
|
&desktopAccountID,
|
|
&item.TargetType,
|
|
&item.TargetConnectionID,
|
|
&item.PlatformID,
|
|
&item.PlatformName,
|
|
&item.PlatformNickname,
|
|
&item.Status,
|
|
&item.ExternalArticleID,
|
|
&item.ExternalArticleURL,
|
|
&item.ExternalManageURL,
|
|
&item.PublishedAt,
|
|
&item.ErrorMessage,
|
|
&item.CreatedAt,
|
|
&item.UpdatedAt,
|
|
); err != nil {
|
|
return nil, response.ErrInternal(50043, "publish_record_scan_failed", err.Error())
|
|
}
|
|
if desktopAccountID != nil && strings.TrimSpace(*desktopAccountID) != "" {
|
|
item.DesktopAccountID = desktopAccountID
|
|
}
|
|
normalizePublishRecordForResponse(&item)
|
|
items = append(items, item)
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
func (s *MediaService) DeletePublishRecord(ctx context.Context, recordID int64) (*DeletePublishRecordResponse, error) {
|
|
if recordID <= 0 {
|
|
return nil, response.ErrBadRequest(40001, "invalid_publish_record_id", "publish record id must be a positive number")
|
|
}
|
|
actor := auth.MustActor(ctx)
|
|
if actor.TenantID == 0 || actor.UserID == 0 {
|
|
return nil, response.ErrUnauthorized(40132, "workspace_scope_missing", "workspace context is required")
|
|
}
|
|
|
|
tx, err := s.pool.Begin(ctx)
|
|
if err != nil {
|
|
return nil, response.ErrInternal(50044, "publish_record_delete_begin_failed", "failed to delete publish record")
|
|
}
|
|
defer tx.Rollback(ctx)
|
|
|
|
var publishBatchID int64
|
|
var articleID int64
|
|
var cancelErrorJSON []byte
|
|
cancelErrorJSON, err = marshalOptionalJSON(map[string]any{
|
|
"reason": "publish_record_deleted",
|
|
"source": "tenant_web",
|
|
})
|
|
if err != nil {
|
|
return nil, response.ErrInternal(50044, "publish_record_delete_payload_failed", "failed to delete publish record")
|
|
}
|
|
err = tx.QueryRow(ctx, `
|
|
SELECT pr.publish_batch_id, pr.article_id
|
|
FROM publish_records pr
|
|
JOIN articles a
|
|
ON a.id = pr.article_id
|
|
AND a.tenant_id = pr.tenant_id
|
|
JOIN publish_batches pb
|
|
ON pb.id = pr.publish_batch_id
|
|
AND pb.tenant_id = pr.tenant_id
|
|
WHERE pr.id = $1
|
|
AND pr.tenant_id = $2
|
|
AND pb.initiator_user_id = $3
|
|
AND pr.deleted_at IS NULL
|
|
FOR UPDATE OF pr
|
|
`, recordID, actor.TenantID, actor.UserID).Scan(&publishBatchID, &articleID)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, response.ErrNotFound(40435, "publish_record_not_found", "publish record not found")
|
|
}
|
|
return nil, response.ErrInternal(50044, "publish_record_delete_lookup_failed", "failed to delete publish record")
|
|
}
|
|
|
|
if tag, err := tx.Exec(ctx, `
|
|
UPDATE publish_records
|
|
SET deleted_at = NOW(),
|
|
updated_at = NOW()
|
|
WHERE id = $1
|
|
AND tenant_id = $2
|
|
AND deleted_at IS NULL
|
|
`, recordID, actor.TenantID); err != nil {
|
|
return nil, response.ErrInternal(50044, "publish_record_delete_failed", "failed to delete publish record")
|
|
} else if tag.RowsAffected() == 0 {
|
|
return nil, response.ErrNotFound(40435, "publish_record_not_found", "publish record not found")
|
|
}
|
|
|
|
if _, err := tx.Exec(ctx, `
|
|
UPDATE desktop_tasks
|
|
SET status = 'aborted',
|
|
error = $3,
|
|
active_attempt_id = NULL,
|
|
lease_token_hash = NULL,
|
|
lease_expires_at = NULL,
|
|
updated_at = NOW()
|
|
WHERE tenant_id = $1
|
|
AND kind = 'publish'
|
|
AND status = 'queued'
|
|
AND payload->>'publish_record_id' = $2
|
|
`, actor.TenantID, recordID, cancelErrorJSON); err != nil {
|
|
return nil, response.ErrInternal(50044, "publish_record_delete_cancel_task_failed", "failed to cancel queued publish task before deleting record")
|
|
}
|
|
|
|
batchStatus, err := recalculatePublishBatchStatus(ctx, tx, publishBatchID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := tx.Exec(ctx, `
|
|
UPDATE publish_batches
|
|
SET status = $1, updated_at = NOW()
|
|
WHERE id = $2 AND tenant_id = $3
|
|
`, batchStatus, publishBatchID, actor.TenantID); err != nil {
|
|
return nil, response.ErrInternal(50054, "publish_batch_update_failed", "failed to update publish batch")
|
|
}
|
|
|
|
articleStatus, err := recalculateArticlePublishStatus(ctx, tx, actor.TenantID, articleID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := tx.Exec(ctx, `
|
|
UPDATE articles
|
|
SET publish_status = $1, updated_at = NOW()
|
|
WHERE id = $2 AND tenant_id = $3 AND deleted_at IS NULL
|
|
`, articleStatus, articleID, actor.TenantID); err != nil {
|
|
return nil, response.ErrInternal(50055, "article_publish_update_failed", "failed to update article publish status")
|
|
}
|
|
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return nil, response.ErrInternal(50044, "publish_record_delete_commit_failed", "failed to delete publish record")
|
|
}
|
|
return &DeletePublishRecordResponse{Deleted: true}, nil
|
|
}
|
|
|
|
func publishBatchRequiresCover(accounts []platformAccountSeed) bool {
|
|
for _, account := range accounts {
|
|
if publishPlatformRequiresCover(account.PlatformID) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func publishPlatformRequiresCover(platformID string) bool {
|
|
switch strings.TrimSpace(platformID) {
|
|
case "weixin_gzh", "baijiahao", "dongchedi", "smzdm":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func pointerStringValue(value *string) string {
|
|
if value == nil {
|
|
return ""
|
|
}
|
|
return *value
|
|
}
|
|
|
|
func normalizePublishStatus(status string) string {
|
|
switch status {
|
|
case "success", "published", "publish_success":
|
|
return "success"
|
|
case "partial_success":
|
|
return "partial_success"
|
|
case "publishing", "pending", "queued", "running":
|
|
return "publishing"
|
|
default:
|
|
return "failed"
|
|
}
|
|
}
|
|
|
|
func newSessionToken() (string, error) {
|
|
buf := make([]byte, 16)
|
|
if _, err := rand.Read(buf); err != nil {
|
|
return "", err
|
|
}
|
|
return hex.EncodeToString(buf), nil
|
|
}
|