2026-06-26 08:37:04 +00:00
|
|
|
package persona
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"strings"
|
2026-07-06 06:13:14 +00:00
|
|
|
"time"
|
2026-06-26 08:37:04 +00:00
|
|
|
|
|
|
|
|
app "haixun-backend/internal/library/errors"
|
|
|
|
|
"haixun-backend/internal/library/errors/code"
|
|
|
|
|
libthreads "haixun-backend/internal/library/threadsapi"
|
|
|
|
|
"haixun-backend/internal/library/threadspost"
|
|
|
|
|
copydraftdomain "haixun-backend/internal/model/copy_draft/domain/usecase"
|
2026-07-06 06:13:14 +00:00
|
|
|
publishqueuedomain "haixun-backend/internal/model/publish_queue/domain/usecase"
|
2026-06-26 08:37:04 +00:00
|
|
|
"haixun-backend/internal/svc"
|
|
|
|
|
"haixun-backend/internal/types"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type PublishPersonaCopyDraftLogic struct {
|
|
|
|
|
ctx context.Context
|
|
|
|
|
svcCtx *svc.ServiceContext
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewPublishPersonaCopyDraftLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PublishPersonaCopyDraftLogic {
|
|
|
|
|
return &PublishPersonaCopyDraftLogic{ctx: ctx, svcCtx: svcCtx}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (l *PublishPersonaCopyDraftLogic) PublishPersonaCopyDraft(
|
|
|
|
|
req *types.PublishCopyDraftHandlerReq,
|
|
|
|
|
) (*types.PublishCopyDraftData, error) {
|
|
|
|
|
tenantID, uid, err := actorFrom(l.ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
personaID := strings.TrimSpace(req.ID)
|
|
|
|
|
draftID := strings.TrimSpace(req.DraftID)
|
|
|
|
|
if !req.Confirm {
|
|
|
|
|
return nil, app.For(code.Persona).InputMissingRequired("請確認 confirm=true 後再發布貼文")
|
|
|
|
|
}
|
|
|
|
|
if _, err := l.svcCtx.Persona.Get(l.ctx, tenantID, uid, personaID); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
draftItem, err := l.svcCtx.CopyDraft.Get(l.ctx, tenantID, uid, personaID, draftID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
draft := draftItem
|
|
|
|
|
if draft.Status == "published" {
|
|
|
|
|
return nil, app.For(code.Persona).ResInvalidState("此草稿已發布")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
text := strings.TrimSpace(req.Text)
|
|
|
|
|
if text == "" {
|
|
|
|
|
text = strings.TrimSpace(draft.Text)
|
|
|
|
|
}
|
|
|
|
|
if text == "" {
|
|
|
|
|
return nil, app.For(code.Persona).InputMissingRequired("text is required")
|
|
|
|
|
}
|
|
|
|
|
if err := threadspost.ValidatePublish(text); err != nil {
|
|
|
|
|
return nil, app.For(code.Persona).InputInvalidFormat(err.Error())
|
|
|
|
|
}
|
2026-07-06 06:13:14 +00:00
|
|
|
topicTag := firstNonEmpty(req.TopicTag, draft.TopicTag)
|
|
|
|
|
patch := copydraftdomain.CopyDraftPatch{}
|
|
|
|
|
needsUpdate := false
|
2026-06-26 08:37:04 +00:00
|
|
|
if strings.TrimSpace(req.Text) != "" && req.Text != draft.Text {
|
2026-07-06 06:13:14 +00:00
|
|
|
patch.Text = &req.Text
|
|
|
|
|
needsUpdate = true
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(req.TopicTag) != "" && req.TopicTag != draft.TopicTag {
|
|
|
|
|
patch.TopicTag = &req.TopicTag
|
|
|
|
|
needsUpdate = true
|
|
|
|
|
}
|
|
|
|
|
if needsUpdate {
|
|
|
|
|
updated, err := l.svcCtx.CopyDraft.Update(l.ctx, copydraftdomain.UpdateRequest{
|
2026-06-26 08:37:04 +00:00
|
|
|
TenantID: tenantID,
|
|
|
|
|
OwnerUID: uid,
|
|
|
|
|
PersonaID: personaID,
|
|
|
|
|
DraftID: draftID,
|
2026-07-06 06:13:14 +00:00
|
|
|
Patch: patch,
|
2026-06-26 08:37:04 +00:00
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-07-06 06:13:14 +00:00
|
|
|
draft = updated
|
|
|
|
|
text = strings.TrimSpace(updated.Text)
|
|
|
|
|
topicTag = firstNonEmpty(updated.TopicTag, req.TopicTag)
|
2026-06-26 08:37:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cred, err := l.svcCtx.ThreadsAccount.ResolveMemberThreadsPublishCredential(l.ctx, tenantID, uid)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
result, err := libthreads.PublishText(l.ctx, libthreads.PublishTextInput{
|
|
|
|
|
ThreadsUserID: cred.ThreadsUserID,
|
|
|
|
|
AccessToken: cred.AccessToken,
|
|
|
|
|
Text: text,
|
2026-07-06 06:13:14 +00:00
|
|
|
TopicTag: topicTag,
|
2026-06-26 08:37:04 +00:00
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, app.For(code.ThreadsAccount).SvcThirdParty("Threads API 發布貼文失敗:" + err.Error())
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-06 06:13:14 +00:00
|
|
|
now := time.Now().UnixNano()
|
|
|
|
|
if cred.AccountID != "" {
|
|
|
|
|
_, _ = l.svcCtx.PublishQueue.RecordPublished(l.ctx, publishqueuedomain.RecordPublishedRequest{
|
|
|
|
|
TenantID: tenantID,
|
|
|
|
|
OwnerUID: uid,
|
|
|
|
|
AccountID: cred.AccountID,
|
|
|
|
|
Text: text,
|
|
|
|
|
TopicTag: topicTag,
|
|
|
|
|
MediaID: result.MediaID,
|
|
|
|
|
Permalink: result.Permalink,
|
|
|
|
|
PublishedAt: now,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-26 08:37:04 +00:00
|
|
|
updated, err := l.svcCtx.CopyDraft.MarkPublished(l.ctx, copydraftdomain.MarkPublishedRequest{
|
|
|
|
|
TenantID: tenantID,
|
|
|
|
|
OwnerUID: uid,
|
|
|
|
|
PersonaID: personaID,
|
|
|
|
|
DraftID: draftID,
|
|
|
|
|
MediaID: result.MediaID,
|
|
|
|
|
Permalink: result.Permalink,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &types.PublishCopyDraftData{
|
|
|
|
|
DraftID: draftID,
|
|
|
|
|
MediaID: result.MediaID,
|
|
|
|
|
Permalink: result.Permalink,
|
|
|
|
|
Status: updated.Status,
|
|
|
|
|
Message: "仿寫貼文已透過 Threads API 發布",
|
|
|
|
|
}, nil
|
|
|
|
|
}
|