quantum-inspiration / commit
Initial commit of random wiki pages being pushed to a Pushover client.
.gitignore | 2 ++ .gitlab-ci.yml | 20 +++++++++++++++ .vscode/launch.json | 29 ++++++++++++++++++++++ .vscode/settings.json | 7 ++++++ Makefile | 10 ++++++++ README.md | 11 ++++++++ go.mod | 3 +++ go.sum | 0 main.go | 16 ++++++++++++ pushover.go | 65 ++++++++++++++++++++++++++++++++++++++++++++++++ qrng.go | 67 +++++++++++++++++++++++++++++++++++++++++++++++++ wiki.go | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++ 12 files changed, 299 insertions(+) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..185145a --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +cache/ +__debug_bin diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..c99721d --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,20 @@ +image: golang:latest + +stages: + - build-and-test + - run + +before_script: + - go version + - make --version + +build-job: + stage: build-and-test + script: + - make build + - make test + +run-job: + stage: run + script: + - make run diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100755 index 0000000..73cc6e2 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,29 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Package", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${fileDirname}", + "internalConsoleOptions": "openOnSessionStart" + }, + { + "name": "Launch file", + "type": "go", + "request": "launch", + "mode": "debug", + "program": "${file}" + }, + { + "type": "bashdb", + "request": "launch", + "name": "Debug repo.sh", + "cwd": "${workspaceFolder}", + "program": "${workspaceFolder}/repro.sh", + "terminalKind": "integrated" + } + ] +} + \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..109a1ea --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "cSpell.words": [ + "CICD", + "qrng", + "unmarshalling", + ] +} \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c7f0ae0 --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +all: build test run + +build: + go build . + +test: + go test -v . + +run: + go run . diff --git a/README.md b/README.md new file mode 100755 index 0000000..7744411 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# Find Stone Beer + +This project is a Go application that once a day pings gets 256 random wiki pages, then picks one of those using a Quantum Random Number Generator, and then sends that article to my phone using Pushover. + +## Tech used + +- golang +- [ANU QRNG API](https://quantumnumbers.anu.edu.au/) for quantum random numbers +- [Wikipedia API](https://en.wikipedia.org/w/api.php?format=json&action=query&generator=random&grnnamespace=0&grnlimit=256) for getting random pages +- Pushover is used for delivering notifications to my phone +- GitLab CICD is used to perform a scheduled run of this app diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2815e68 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module quantum_inspiration + +go 1.21 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/main.go b/main.go new file mode 100644 index 0000000..a7a1994 --- /dev/null +++ b/main.go @@ -0,0 +1,16 @@ +package main + +import "fmt" + +func main() { + title, url, err := GetRandomWikiPage() + if err != nil { + panic(err) + } + + message := fmt.Sprintf("Quantum inspiration for the day: <a href='%s'>%s</a>", url, title) + err = SendPushoverMessage(message) + if err != nil { + panic(err) + } +} diff --git a/pushover.go b/pushover.go new file mode 100644 index 0000000..db71277 --- /dev/null +++ b/pushover.go @@ -0,0 +1,65 @@ +package main + +import ( + "fmt" + "net/http" + "net/url" + "os" +) + +const ( + pushoverTokenEnvVar = "PUSHOVER_QUANTUM_INSPIRATION_TOKEN" + pushoverUserTokenEnvVar = "PUSHOVER_USER_TOKEN" + maxMessageLength = 1024 + // this project runs daily, so this is the max TTL for now + // if this project is run more frequently, then this value should be changed + ttlDays = 2 + + // automatically calculated, do not change + ttlSeconds = ttlDays * 24 * 60 * 60 +) + +func CombineUpToMaxMessageLength(prefix string, moreInfo string, messages []string) string { + var combinedMessage = prefix + for _, message := range messages { + newMessage := combinedMessage + message + if len(newMessage)+len(moreInfo) > maxMessageLength { + combinedMessage += moreInfo + break + } + combinedMessage = newMessage + } + return combinedMessage +} + +func SendPushoverMessage(message string) error { + if len(message) > maxMessageLength { + return fmt.Errorf("message is too long: %d > %d", len(message), maxMessageLength) + } + postUrl := "https://api.pushover.net/1/messages.json" + + pushoverToken := os.Getenv(pushoverTokenEnvVar) + pushoverUser := os.Getenv(pushoverUserTokenEnvVar) + if pushoverUser == "" || pushoverToken == "" { + return fmt.Errorf("missing environment variable %s or %s", pushoverUserTokenEnvVar, pushoverTokenEnvVar) + } + + response, err := http.PostForm(postUrl, url.Values{ + "token": {pushoverToken}, + "user": {pushoverUser}, + "message": {message}, + "html": {"1"}, + "ttl": {fmt.Sprintf("%d", ttlSeconds)}, + }) + + if err != nil { + return err + } + defer response.Body.Close() + + isNot200 := response.StatusCode != 200 + if isNot200 { + return fmt.Errorf("pushover returned status code %d: %s", response.StatusCode, response.Status) + } + return nil +} diff --git a/qrng.go b/qrng.go new file mode 100644 index 0000000..cf97c28 --- /dev/null +++ b/qrng.go @@ -0,0 +1,67 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" +) + +const ( + qrngApiKeyEnvVar = "ANU_QRNG_KEY" +) + +type apiResponse struct { + Success bool `json:"success"` + Type string `json:"type"` + Length string `json:"length"` + Data []uint8 `json:"data"` +} + +func GetRandomNumbers(count int) ([]uint8, error) { + resp, err := getRandomFromQrng(count) + if err != nil { + return nil, err + } + return resp.Data, nil +} + +func getRandomFromQrng(count int) (apiResponse, error) { + API_URL := "https://api.quantumnumbers.anu.edu.au" + API_KEY, keyFound := os.LookupEnv(qrngApiKeyEnvVar) + if !keyFound { + fmt.Printf("Error: API key not found in environment variable %s", qrngApiKeyEnvVar) + return apiResponse{}, fmt.Errorf("API key not found") + } + + client := &http.Client{} + req, err := http.NewRequest("GET", fmt.Sprintf("%s?length=%d&type=uint8", API_URL, count), nil) + if err != nil { + fmt.Println("Error creating request:", err) + return apiResponse{}, err + } + req.Header.Set("x-api-key", API_KEY) + + resp, err := client.Do(req) + if err != nil { + fmt.Println("Error executing request:", err) + return apiResponse{}, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + fmt.Println("Error reading response:", err) + return apiResponse{}, err + } + + var response apiResponse + err = json.Unmarshal(body, &response) + if err != nil { + fmt.Println("Error unmarshalling JSON:", err) + return apiResponse{}, err + } + + return response, nil +} diff --git a/wiki.go b/wiki.go new file mode 100644 index 0000000..437c610 --- /dev/null +++ b/wiki.go @@ -0,0 +1,69 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "net/http" +) + +func GetRandomWikiPage() (title string, url string, err error) { + random256Pages, err := getRandomWikipediaPages(256) + if err != nil { + return "", "", fmt.Errorf("error getting random Wikipedia pages: %v", err) + } + + randomNumbers, err := GetRandomNumbers(1) + if err != nil { + return "", "", fmt.Errorf("error getting random numbers: %v", err) + } + + titles := []string{} + for title := range random256Pages { + titles = append(titles, title) + } + random := randomNumbers[0] + randomTitle := titles[random] + randomPage := random256Pages[randomTitle] + return randomTitle, randomPage, nil +} + +type WikipediaResponse struct { + BatchComplete string `json:"batchcomplete"` + Query struct { + Pages map[string]struct { + PageID int `json:"pageid"` + Ns int `json:"ns"` + Title string `json:"title"` + } `json:"pages"` + } `json:"query"` +} + +func getRandomWikipediaPages(count int) (nameToUrlMap map[string]string, err error) { + url := fmt.Sprintf("https://en.wikipedia.org/w/api.php?format=json&action=query&generator=random&grnnamespace=0&grnlimit=%d", count) + resp, err := http.Get(url) + if err != nil { + return nil, fmt.Errorf("error fetching Wikipedia pages: %v", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error reading Wikipedia response: %v", err) + } + + var wikiResponse WikipediaResponse + err = json.Unmarshal(body, &wikiResponse) + if err != nil { + return nil, fmt.Errorf("error unmarshalling Wikipedia JSON: %v", err) + } + + nameToUrlMap = map[string]string{} + for pageID := range wikiResponse.Query.Pages { + title := wikiResponse.Query.Pages[pageID].Title + url := fmt.Sprintf("http://en.wikipedia.org/?curid=%s", pageID) + nameToUrlMap[title] = url + } + + return nameToUrlMap, nil +}