package provider import ( "context" "encoding/json" "fmt" "net/http" "sort" "strconv" "strings" "time" "orchestra/internal/domain" "orchestra/internal/human" "orchestra/internal/store" ) // GiteaComments reads issue comments as human input. It is a separate type // from Gitea so the ingestion path and the authority path cannot be confused // for each other: this one never creates or closes a task. type GiteaComments struct{ Gitea } type giteaComment struct { ID int64 `json:"id"` Body string `json:"body"` User struct { Login string `json:"login"` } `json:"user"` CreatedAt time.Time `json:"created_at"` } // FetchAfter returns the comments on the task's issue with an id above the // cursor, oldest first. The cursor is the highest comment id already // reconciled; comment ids are monotonic per repo, which makes them a usable // resume point even when a comment is edited later. func (g GiteaComments) FetchAfter(ctx context.Context, task domain.Task, cursor store.SourceCursor) ([]human.Input, store.SourceCursor, error) { next := store.SourceCursor{TaskID: task.ID, Provider: g.SourceName(), Cursor: cursor.Cursor} if task.ExternalID == "" || task.Source != g.SourceName() { return nil, next, nil } var after int64 if cursor.Cursor != "" { v, err := strconv.ParseInt(cursor.Cursor, 10, 64) if err != nil { return nil, next, fmt.Errorf("gitea comments: bad cursor %q: %w", cursor.Cursor, err) } after = v } u := strings.TrimRight(g.BaseURL, "/") + "/api/v1/repos/" + g.Owner + "/" + g.Repo + "/issues/" + task.ExternalID + "/comments" req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil) if err != nil { return nil, next, err } if g.Token != "" { req.Header.Set("Authorization", "token "+g.Token) } resp, err := g.client().Do(req) if err != nil { return nil, next, err } defer resp.Body.Close() if resp.StatusCode/100 != 2 { return nil, next, fmt.Errorf("gitea comments: %s", resp.Status) } var comments []giteaComment if err := json.NewDecoder(resp.Body).Decode(&comments); err != nil { return nil, next, err } sort.Slice(comments, func(a, b int) bool { return comments[a].ID < comments[b].ID }) var out []human.Input highest := after for _, c := range comments { if c.ID <= after { continue } out = append(out, human.Input{ Provider: g.SourceName(), ExternalID: strconv.FormatInt(c.ID, 10), Author: c.User.Login, At: c.CreatedAt, Body: c.Body, }) if c.ID > highest { highest = c.ID } } if highest > after { next.Cursor = strconv.FormatInt(highest, 10) } return out, next, nil }