fuzzyworld

quantum-inspiration / commit

Initial commit of random wiki pages being pushed to a Pushover client.

e58f6fffa243c18f4ef3530d792a50320050631d · Pavel Safronov · 2023-09-06 00:05:33 -0700

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
+}