This commit is contained in:
Aaron Bieber 2020-12-14 16:17:29 -07:00
commit 97fd2fb78d
6 changed files with 241 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*.xml
gqrss

15
LICENSE Normal file
View File

@ -0,0 +1,15 @@
/*
* Copyright (c) 2020 Aaron Bieber <aaron@bolddaemon.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/

14
README.md Normal file
View File

@ -0,0 +1,14 @@
# [gqrss](https://github.com/qbit/gqrss)
A tool to query and display GitHub issues relating to OpenBSD.
## Libraries used
- [gorilla/feeds](https://github.com/gorilla/feeds) for generating RSS/Atom
feeds.
- [suah.dev/protect](https://suah.dev/protect) for OpenBSD's
[pledge](https://man.openbsd.org/pledge)/[unveil](https://man.openbsd.org/unveil).
`gqrss` will produce a `rss.xml` and `atom.xml` file in the directory it was
ran. It expects a GitHub authentication token to be present in the
`GH_AUTH_TOKEN` environment variable.

8
go.mod Normal file
View File

@ -0,0 +1,8 @@
module suah.dev/gqrss
go 1.15
require (
github.com/gorilla/feeds v1.1.1
suah.dev/protect v1.0.0
)

6
go.sum Normal file
View File

@ -0,0 +1,6 @@
github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY=
github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3 h1:5B6i6EAiSYyejWfvc5Rc9BbI3rzIsrrXfAQBWnYfn+w=
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
suah.dev/protect v1.0.0 h1:X8pzDvDIZIiugmkmr6DES6JFO1XUdJWi34Ffmk6CMZY=
suah.dev/protect v1.0.0/go.mod h1:ZSgyBM30JUwhVPWJzVHh0jlu5W6Qz1VR6tIhAzqJZ9Y=

196
main.go Normal file
View File

@ -0,0 +1,196 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"time"
"github.com/gorilla/feeds"
"suah.dev/protect"
)
// 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 openbsd") {
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: "OpenBSD GitHub Issues",
Link: &feeds.Link{Href: "https://github.com/qbit/gqrss"},
Description: "Open GitHub issues relating to OpenBSD",
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("atom.xml")
if err != nil {
log.Fatal(err)
}
rssFile, err := os.Create("rss.xml")
if err != nil {
log.Fatal(err)
}
feed.WriteAtom(atomFile)
feed.WriteRss(rssFile)
}
func main() {
protect.Unveil("./", "rwc")
protect.Unveil("/etc/ssl/cert.pem", "r")
protect.Pledge("stdio unveil rpath wpath cpath flock dns inet")
protect.UnveilBlock()
var q GQLQuery
q.Query = ghQuery
resp, err := getData(q)
if err != nil {
fmt.Printf("%+v\n", err)
}
makeRSS(resp)
}