blob: c0e92e26050750818fb570dfea2bb2ec5d90eaa1 [file] [log] [blame]
// Copyright 2024 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package discussion
import (
"context"
"iter"
"log/slog"
"net/http"
gql "github.com/shurcooL/githubv4"
"golang.org/x/oauth2"
"golang.org/x/oscar/internal/github"
"golang.org/x/oscar/internal/secret"
)
type gqlClient struct {
slog *slog.Logger
gql.Client
}
func newGQLClient(hc *http.Client) *gqlClient {
return &gqlClient{
Client: *gql.NewClient(hc),
slog: slog.Default(),
}
}
func authClient(ctx context.Context, sdb secret.DB) *http.Client {
return oauth2.NewClient(ctx, oauth2.StaticTokenSource(&oauth2.Token{
AccessToken: github.Token(sdb),
}))
}
// discussions returns an iterator over all discussions in the project,
// ordered by update time (latest first).
// It returns an error if any of the GitHub queries fails.
func (gc *gqlClient) discussions(ctx context.Context, owner, repo string) iter.Seq2[*Discussion, error] {
return func(yield func(*Discussion, error) bool) {
q, vars := newListQuery(owner, repo)
for d, err := range nodes(ctx, gc, q, vars) {
if err != nil {
yield(nil, err)
return
}
if !yield(d.convert(), nil) {
return
}
}
}
}
// comments returns an iterator over all discussion comments and replies in the project.
// The order is not guaranteed because ordering of comments and replies can't be configured
// via the GitHub GraphQL API.
// The function returns an error if any of the GitHub queries fails.
func (gc *gqlClient) comments(ctx context.Context, owner, repo string) iter.Seq2[*Comment, error] {
yieldCommentAndReplies := func(c *comment, yield func(*Comment, error) bool) bool {
if !yield(c.convert(), nil) {
return false
}
for r, err := range gc.replies(ctx, c) {
if err != nil {
return yield(nil, err)
}
if !yield(r.convert(), nil) {
return false
}
}
return true
}
return func(yield func(*Comment, error) bool) {
q, vars := newListDiscWithCommentsQuery(owner, repo)
for d, err := range nodes(ctx, gc, q, vars) {
if err != nil {
yield(nil, err)
return
}
for _, c := range d.Comments.Nodes {
if !yieldCommentAndReplies(c, yield) {
return
}
}
if d.Comments.PageInfo.HasNextPage {
q, vars := newListCommentsQuery(owner, repo, d.Number, d.Comments.PageInfo.EndCursor)
for c, err := range nodes(ctx, gc, q, vars) {
if err != nil {
yield(nil, err)
return
}
if !yieldCommentAndReplies(c, yield) {
return
}
}
}
}
}
}
// replies returns an iterator over the replies to the given comment.
// The order is not guaranteed. It returns an error if any of the GitHub queries fails.
func (gc *gqlClient) replies(ctx context.Context, c *comment) iter.Seq2[*reply, error] {
return func(yield func(*reply, error) bool) {
replies := c.Replies
for _, r := range replies.Nodes {
if !yield(r, nil) {
return
}
}
if replies.PageInfo.HasNextPage {
q, vars := newCommentQuery(c.ID, replies.PageInfo.EndCursor)
for r, err := range nodes(ctx, gc, q, vars) {
if err != nil {
yield(nil, err)
return
}
if !yield(r, nil) {
return
}
}
}
}
}
// nodes returns an iterator over the nodes of the query.
// q and vars are the initial inputs to [gql.Client.Query].
// It returns an error if any of the GitHub queries fails.
// TODO(tatianabradley): Add a check to see if we have hit a GitHub
// rate limit and slow down if so.
func nodes[N node, Q query[N]](ctx context.Context, gc *gqlClient, q Q, vars varsMap) iter.Seq2[N, error] {
return func(yield func(N, error) bool) {
var zero N
for {
if err := gc.Query(ctx, q, vars); err != nil {
yield(zero, err)
return
}
page := q.Page()
for _, n := range page.Nodes {
if !yield(n, nil) {
return
}
}
if !page.PageInfo.HasNextPage {
return
}
vars[q.CursorName()] = gql.NewString(page.PageInfo.EndCursor)
}
}
}