package main import ( "bytes" "encoding/json" "flag" "fmt" "io/ioutil" "log" "net/http" "os" "time" "github.com/gorilla/feeds" "suah.dev/protect" ) var search string var prefix string // GQLQuery is what github wants in the POST request. type GQLQuery struct { Query string `json:"query"` } // GHResp represents a response from GitHub's GraphQL API. type GHResp struct { Data Data `json:"data"` } // PageInfo represents the page information. type PageInfo struct { HasNextPage bool `json:"hasNextPage"` EndCursor string `json:"endCursor"` } // Repository information has repo specific bits. type Repository struct { Name string `json:"name"` URL string `json:"url"` StargazerCount int `json:"stargazerCount"` } // Node is an entry from our "search". type Node struct { Repository Repository `json:"repository"` CreatedAt time.Time `json:"createdAt"` Title string `json:"title"` URL string `json:"url"` Author Author `json:"author"` BodyHTML string `json:"bodyHTML"` } // Edges are ... too edgy to tell.. type Edges struct { Node Node `json:"node,omitempty"` } // Search bundles our edges together type Search struct { IssueCount int `json:"issueCount"` PageInfo PageInfo `json:"pageInfo"` Edges []Edges `json:"edges"` } // Author is an individual author type Author struct { Login string `json:"login"` URL string `json:"url"` } // Data is the data returned from a search type Data struct { Search Search `json:"search"` } const endPoint = "https://api.github.com/graphql" const ghQuery = ` { search(first: 100, type: ISSUE, query: "state:open %s") { issueCount pageInfo { hasNextPage endCursor } edges { node { ... on Issue { repository { name url stargazerCount } createdAt title url bodyHTML author { login url } } } } } } ` func getData(q GQLQuery) (re *GHResp, err error) { var req *http.Request client := &http.Client{} buf := new(bytes.Buffer) if err := json.NewEncoder(buf).Encode(q); err != nil { return nil, err } req, err = http.NewRequest("POST", endPoint, buf) if err != nil { return nil, err } req.Header.Set("Authorization", fmt.Sprintf("bearer %s", os.Getenv("GH_AUTH_TOKEN"))) res, err := client.Do(req) if err != nil { return nil, err } defer res.Body.Close() data, err := ioutil.ReadAll(res.Body) if err != nil { return nil, err } if err = json.Unmarshal(data, &re); err != nil { return nil, err } return re, nil } func makeRSS(q *GHResp) { feed := &feeds.Feed{ Title: fmt.Sprintf("%s GitHub Issues", search), Link: &feeds.Link{Href: "https://github.com/qbit/gqrss"}, Description: fmt.Sprintf("Open GitHub issues relating to %s", search), Author: &feeds.Author{Name: "Aaron Bieber", Email: "aaron@bolddaemon.com"}, Copyright: "This work is copyright © Aaron Bieber", } for _, e := range q.Data.Search.Edges { if e.Node.Title == "" { continue } f := &feeds.Item{ Title: fmt.Sprintf("%s: %s", e.Node.Repository.Name, e.Node.Title), Link: &feeds.Link{Href: e.Node.URL}, Created: e.Node.CreatedAt, Description: e.Node.BodyHTML, Author: &feeds.Author{ Name: e.Node.Author.Login, }, } feed.Items = append(feed.Items, f) } atomFile, err := os.Create(fmt.Sprintf("%satom.xml", prefix)) if err != nil { log.Fatal(err) } rssFile, err := os.Create(fmt.Sprintf("%srss.xml", prefix)) if err != nil { log.Fatal(err) } err = feed.WriteAtom(atomFile) if err != nil { log.Fatal(err) } err = feed.WriteRss(rssFile) if err != nil { log.Fatal(err) } } func dieOnErr(err error) { if err != nil { log.Fatalln(err) } } func main() { flag.StringVar(&search, "search", "OpenBSD", "thing to search GitHub for") flag.StringVar(&prefix, "prefix", "", "prefix to prepend to file names") flag.Parse() dieOnErr(protect.Unveil("./", "rwc")) dieOnErr(protect.Unveil("/etc/ssl/cert.pem", "r")) dieOnErr(protect.Pledge("stdio unveil rpath wpath cpath flock dns inet")) dieOnErr(protect.UnveilBlock()) var q GQLQuery q.Query = fmt.Sprintf(ghQuery, search) resp, err := getData(q) if err != nil { fmt.Printf("%+v\n", err) } makeRSS(resp) }