initial
This commit is contained in:
commit
97fd2fb78d
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
*.xml
|
||||||
|
gqrss
|
15
LICENSE
Normal file
15
LICENSE
Normal 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
14
README.md
Normal 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
8
go.mod
Normal 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
6
go.sum
Normal 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
196
main.go
Normal 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)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user