From c83b753c9de744226c4896855836a11f16e4ca67 Mon Sep 17 00:00:00 2001 From: shanedabes Date: Sun, 14 Nov 2021 15:52:54 +0000 Subject: [PATCH] add retroachievements gowon module --- .github/renovate.json | 5 + .github/workflows/release.yaml | 166 +++++++++++++++++++++++++++++ .gitignore | 1 + Dockerfile | 11 ++ README.md | 2 +- demo/README.md | 9 ++ demo/kube/.gitignore | 1 + demo/kube/configmap-mosquitto.yaml | 10 ++ demo/kube/configmap-tiny.yaml | 26 +++++ demo/kube/deploy-gowon.yaml | 30 ++++++ demo/kube/deploy-ircd.yaml | 22 ++++ demo/kube/deploy-mosquitto.yaml | 30 ++++++ demo/kube/deploy-tiny.yaml | 27 +++++ demo/kube/svc-ircd.yaml | 12 +++ demo/kube/svc-mosquitto.yaml | 12 +++ demo/mqtt.sh | 32 ++++++ demo/skaffold.yaml | 13 +++ demo/tiny.sh | 3 + go.mod | 18 ++++ go.sum | 37 +++++++ main.go | 152 ++++++++++++++++++++++++++ retroachivements.go | 75 +++++++++++++ 22 files changed, 693 insertions(+), 1 deletion(-) create mode 100644 .github/renovate.json create mode 100644 .github/workflows/release.yaml create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 demo/README.md create mode 100644 demo/kube/.gitignore create mode 100644 demo/kube/configmap-mosquitto.yaml create mode 100644 demo/kube/configmap-tiny.yaml create mode 100644 demo/kube/deploy-gowon.yaml create mode 100644 demo/kube/deploy-ircd.yaml create mode 100644 demo/kube/deploy-mosquitto.yaml create mode 100644 demo/kube/deploy-tiny.yaml create mode 100644 demo/kube/svc-ircd.yaml create mode 100644 demo/kube/svc-mosquitto.yaml create mode 100755 demo/mqtt.sh create mode 100644 demo/skaffold.yaml create mode 100755 demo/tiny.sh create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 retroachivements.go diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 0000000..f45d8f1 --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,5 @@ +{ + "extends": [ + "config:base" + ] +} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..ec8f62e --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,166 @@ +--- +name: Release + +on: + workflow_dispatch: + push: + branches: + - main + paths: + - '*.go' + - Dockerfile + - go.mod + - go.sum + pull_request: + paths: + - '*.go' + - Dockerfile + - go.mod + - go.sum + +jobs: + hadolint: + name: Run hadolint + runs-on: ubuntu-20.04 + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: hadolint + uses: reviewdog/action-hadolint@v1.25.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + reporter: github-pr-review + filter_mode: diff_context + fail_on_error: true + + gotest: + name: Go test + runs-on: ubuntu-20.04 + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.17 + + - name: Test + run: go test -v ./... + + + build: + name: Build + runs-on: ubuntu-20.04 + needs: + - hadolint + - gotest + if: "!contains(github.event.head_commit.message, '[ci-skip]')" + + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Install GitVersion + uses: gittools/actions/gitversion/setup@v0.9.10 + with: + versionSpec: '5.x' + + - name: GitVersion + id: gitversion + uses: gittools/actions/gitversion/execute@v0.9.10 + + - name: Prepare + id: prep + run: | + if test -f "./goss.yaml"; then + echo ::set-output name=goss::true + else + echo ::set-output name=goss::false + fi + + if [ "${{github.event_name}}" == "pull_request" ]; then + echo ::set-output name=push::false + echo ::set-output name=cache_from::"type=local,src=/tmp/.buildx-cache" + echo ::set-output name=cache_to::"" + else + echo ::set-output name=push::true + echo ::set-output name=cache_from::"type=local,src=/tmp/.buildx-cache" + echo ::set-output name=cache_to::"type=local,dest=/tmp/.buildx-cache,mode=max" + fi + + echo ::set-output name=gitversionf::${GITVERSION_FULLSEMVER/+/-} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + with: + platforms: amd64,arm64 + + - name: Login to GHCR + uses: docker/login-action@v1 + if: github.event_name != 'pull_request' + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Install and configure Buildx + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v1 + with: + install: true + version: latest + driver-opts: image=moby/buildkit:latest + + - name: Cache Docker layers + uses: actions/cache@v2 + with: + path: /tmp/.buildx-cache + key: docker-cache + restore-keys: docker-cache + + # Install the GOSS testing framework + - name: Set up goss/dgoss + uses: e1himself/goss-installation-action@v1.0.4 + if: ${{ steps.prep.outputs.goss == 'true' }} + with: + version: 'v0.3.16' + + # Creates a local build to run tests on + - name: Build and Load local test-container + if: ${{ steps.prep.outputs.goss == 'true' }} + uses: docker/build-push-action@v2 + with: + context: . + file: ./Dockerfile + load: true + tags: | + ghcr.io/${{ github.repository_owner }}/${{ matrix.container }}:test + cache-from: ${{ steps.prep.outputs.cache_from }} + cache-to: ${{ steps.prep.outputs.cache_to }} + + # Run GOSS tests if included with the container + - name: Run GOSS tests + if: ${{ steps.prep.outputs.goss == 'true' }} + env: + GOSS_FILE: ./goss.yaml + run: | + dgoss run ghcr.io/${{ github.repository_owner }}/${{ matrix.container }}:test + + # Push if not a PR, otherwise just test the build process for all requested platforms + - name: Build and Push + uses: docker/build-push-action@v2 + with: + context: . + platforms: linux/amd64,linux/arm64 + file: ./Dockerfile + push: ${{ steps.prep.outputs.push }} + tags: | + ghcr.io/${{ github.repository_owner }}/gowon-retroachievements:latest + ghcr.io/${{ github.repository_owner }}/gowon-retroachievements:${{ steps.prep.outputs.gitversionf }} + cache-from: ${{ steps.prep.outputs.cache_from }} + cache-to: ${{ steps.prep.outputs.cache_to }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..31bbf45 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +kv.db diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..04c73af --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM golang:alpine as build-env +COPY . /src +WORKDIR /src +RUN go build -o gowon-retroachievements + +FROM alpine:3.14.2 +RUN mkdir /data +ENV GOWON_RA_KV_PATH /data/kv.db +WORKDIR /app +COPY --from=build-env /src/gowon-retroachievements /app/ +ENTRYPOINT ["./gowon-retroachievements"] diff --git a/README.md b/README.md index 02c9b3d..ff24b19 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# gowon-retroachievements \ No newline at end of file +# gowon-retroachievements diff --git a/demo/README.md b/demo/README.md new file mode 100644 index 0000000..7450594 --- /dev/null +++ b/demo/README.md @@ -0,0 +1,9 @@ +# Bot demo + +These files can be used to setup a test instance for demo or testing purposes using skaffold. + +To run an ircd and bot instance in a kubernetes cluster (like k3d) cd to this directory and run: + + skaffold dev --tail + +An [tiny](https://github.com/osa1/tiny) deployment is included. To use it run `tiny.sh`. diff --git a/demo/kube/.gitignore b/demo/kube/.gitignore new file mode 100644 index 0000000..19bddac --- /dev/null +++ b/demo/kube/.gitignore @@ -0,0 +1 @@ +deploy-retroachievements.yaml diff --git a/demo/kube/configmap-mosquitto.yaml b/demo/kube/configmap-mosquitto.yaml new file mode 100644 index 0000000..ee90b3f --- /dev/null +++ b/demo/kube/configmap-mosquitto.yaml @@ -0,0 +1,10 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: mosquitto-config +data: + mosquitto.conf: | + listener 1883 + protocol mqtt + allow_anonymous true diff --git a/demo/kube/configmap-tiny.yaml b/demo/kube/configmap-tiny.yaml new file mode 100644 index 0000000..b570ce0 --- /dev/null +++ b/demo/kube/configmap-tiny.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: tiny-config +data: + tiny.yml: | + --- + servers: + - addr: oragono + port: 6667 + tls: false + + realname: tester + nicks: + - tester + + join: + - '#gowon' + + defaults: + nicks: + - tester + realname: tester + join: [] + tls: false diff --git a/demo/kube/deploy-gowon.yaml b/demo/kube/deploy-gowon.yaml new file mode 100644 index 0000000..89034e7 --- /dev/null +++ b/demo/kube/deploy-gowon.yaml @@ -0,0 +1,30 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: gowon + labels: + app.kubernetes.io/name: gowon +spec: + selector: + matchLabels: + app.kubernetes.io/name: gowon + template: + metadata: + labels: + app.kubernetes.io/name: gowon + spec: + containers: + - name: gowon + image: ghcr.io/gowon-irc/gowon:0.1.0-66 + env: + - name: GOWON_SERVER + value: oragono:6667 + - name: GOWON_BROKER + value: mosquitto:1883 + - name: GOWON_NICK + value: gowon + - name: GOWON_USER + value: gowon + - name: GOWON_CHANNELS + value: "#gowon" diff --git a/demo/kube/deploy-ircd.yaml b/demo/kube/deploy-ircd.yaml new file mode 100644 index 0000000..5dba942 --- /dev/null +++ b/demo/kube/deploy-ircd.yaml @@ -0,0 +1,22 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: oragono + labels: + app.kubernetes.io/name: oragono +spec: + selector: + matchLabels: + app.kubernetes.io/name: oragono + template: + metadata: + labels: + app.kubernetes.io/name: oragono + spec: + containers: + - name: oragono + image: oragono/oragono + ports: + - containerPort: 6667 + name: ircd diff --git a/demo/kube/deploy-mosquitto.yaml b/demo/kube/deploy-mosquitto.yaml new file mode 100644 index 0000000..57eda11 --- /dev/null +++ b/demo/kube/deploy-mosquitto.yaml @@ -0,0 +1,30 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mosquitto + labels: + app.kubernetes.io/name: mosquitto +spec: + selector: + matchLabels: + app.kubernetes.io/name: mosquitto + template: + metadata: + labels: + app.kubernetes.io/name: mosquitto + spec: + containers: + - name: mosquitto + image: eclipse-mosquitto + ports: + - containerPort: 1883 + name: mqtt + volumeMounts: + - name: mosquitto-config + mountPath: /mosquitto/config/mosquitto.conf + subPath: mosquitto.conf + volumes: + - name: mosquitto-config + configMap: + name: mosquitto-config diff --git a/demo/kube/deploy-tiny.yaml b/demo/kube/deploy-tiny.yaml new file mode 100644 index 0000000..3b907a0 --- /dev/null +++ b/demo/kube/deploy-tiny.yaml @@ -0,0 +1,27 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: tiny + labels: + app.kubernetes.io/name: tiny +spec: + selector: + matchLabels: + app.kubernetes.io/name: tiny + template: + metadata: + labels: + app.kubernetes.io/name: tiny + spec: + containers: + - name: tiny + image: ghcr.io/shanedabes/tiny:v0.9.0 + volumeMounts: + - name: tiny-config + mountPath: /tiny.yml + subPath: tiny.yml + volumes: + - name: tiny-config + configMap: + name: tiny-config diff --git a/demo/kube/svc-ircd.yaml b/demo/kube/svc-ircd.yaml new file mode 100644 index 0000000..83c2c0d --- /dev/null +++ b/demo/kube/svc-ircd.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: oragono +spec: + ports: + - name: ircd + port: 6667 + targetPort: ircd + selector: + app.kubernetes.io/name: oragono diff --git a/demo/kube/svc-mosquitto.yaml b/demo/kube/svc-mosquitto.yaml new file mode 100644 index 0000000..8e38531 --- /dev/null +++ b/demo/kube/svc-mosquitto.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: mosquitto +spec: + ports: + - name: mqtt + port: 1883 + targetPort: mqtt + selector: + app.kubernetes.io/name: mosquitto diff --git a/demo/mqtt.sh b/demo/mqtt.sh new file mode 100755 index 0000000..f215282 --- /dev/null +++ b/demo/mqtt.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash + +BROKER_HOST="${BROKER_HOST:-localhost}" +BROKER_PORT="${BROKER_PORT:-1883}" + +ACTION="${1}" + +mqtt_msg() { + cat <<-EOF + {"module":"gowon","msg":".ra $@","nick":"tester","dest":"#gowon","command":"ra","args":"${@}"} + EOF +} + +pub() { + mqtt_msg "${@}" | mosquitto_pub -h "${BROKER_HOST}" -p "${BROKER_PORT}" -t "/gowon/input" -s +} + +sub() { + mosquitto_sub -h "${BROKER_HOST}" -p "${BROKER_PORT}" -t "/gowon/output" | jq -r '.msg' +} + +case "${ACTION}" in + pub) + pub "${@:2}" + ;; + sub) + sub "${@:2}" + ;; + *) + echo "First argument must be either pub or sub" >&2 + ;; +esac diff --git a/demo/skaffold.yaml b/demo/skaffold.yaml new file mode 100644 index 0000000..5a8514a --- /dev/null +++ b/demo/skaffold.yaml @@ -0,0 +1,13 @@ +--- +apiVersion: skaffold/v2beta23 +kind: Config +build: + artifacts: + - image: retroachievements + context: ./.. + docker: + dockerfile: Dockerfile +deploy: + kubectl: + manifests: + - "kube/*" diff --git a/demo/tiny.sh b/demo/tiny.sh new file mode 100755 index 0000000..1f77108 --- /dev/null +++ b/demo/tiny.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env sh + +kubectl exec -it $(kubectl get po -l app.kubernetes.io/name=tiny -o name) -- ./tiny -c tiny.yml diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0c0dd27 --- /dev/null +++ b/go.mod @@ -0,0 +1,18 @@ +module github.com/gowon-irc/gowon-retroachievements + +go 1.17 + +require ( + github.com/ShiraazMoollatjie/retroachievementsgo v0.0.0-20200114192927-16283851521e + github.com/boltdb/bolt v1.3.1 + github.com/eclipse/paho.mqtt.golang v1.3.5 + github.com/gowon-irc/go-gowon v0.0.0-20211113200040-cb1a970bd247 + github.com/jessevdk/go-flags v1.5.0 +) + +require ( + github.com/gorilla/websocket v1.4.2 // indirect + github.com/pkg/errors v0.9.1 // indirect + golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 // indirect + golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..41ea158 --- /dev/null +++ b/go.sum @@ -0,0 +1,37 @@ +github.com/ShiraazMoollatjie/retroachievementsgo v0.0.0-20200114192927-16283851521e h1:wt4kIdoE0ql985Wo0xhoPi8gzIT+zAeejaOgx5yKrGM= +github.com/ShiraazMoollatjie/retroachievementsgo v0.0.0-20200114192927-16283851521e/go.mod h1:ywNbLMgMP8b+Q1n1w4gHDjEdRfdWzgD9geeRr2UgCfE= +github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= +github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/eclipse/paho.mqtt.golang v1.3.4/go.mod h1:eTzb4gxwwyWpqBUHGQZ4ABAV7+Jgm1PklsYT/eo8Hcc= +github.com/eclipse/paho.mqtt.golang v1.3.5 h1:sWtmgNxYM9P2sP+xEItMozsR3w0cqZFlqnNN1bdl41Y= +github.com/eclipse/paho.mqtt.golang v1.3.5/go.mod h1:eTzb4gxwwyWpqBUHGQZ4ABAV7+Jgm1PklsYT/eo8Hcc= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gowon-irc/go-gowon v0.0.0-20211113200040-cb1a970bd247 h1:RkvE88EywA3Tpoe3FK/fHPvKD0LtET4msIKlY75t+ls= +github.com/gowon-irc/go-gowon v0.0.0-20211113200040-cb1a970bd247/go.mod h1:iY2WKgdQI1tsyd+lYFioxAnb5+8FQlJ9vqCTAUoq8QQ= +github.com/gowon-irc/gowon v0.0.0-20211012014610-ece6c2510654/go.mod h1:Qx8xz3LnjqnCxG6gTrKW6IWDdl4FTLvQVmBZEI5BAzk= +github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= +github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/thoj/go-ircevent v0.0.0-20210419090348-35410aa86c49/go.mod h1:I0ZT9x8wStY6VOxtNOrLpnDURFs7HS0z1e1vhuKUEVc= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 h1:Jcxah/M+oLZ/R4/z5RzfPzGbPXnVDPkEDtf2JnuxN+U= +golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4 h1:EZ2mChiOa8udjfp6rRmswTbtZN/QzUQp4ptM4rnjHvc= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..133cbbf --- /dev/null +++ b/main.go @@ -0,0 +1,152 @@ +package main + +import ( + "fmt" + "log" + "os" + "os/signal" + "strings" + "syscall" + "time" + + "github.com/boltdb/bolt" + mqtt "github.com/eclipse/paho.mqtt.golang" + "github.com/gowon-irc/go-gowon" + "github.com/jessevdk/go-flags" +) + +type Options struct { + Prefix string `short:"P" long:"prefix" env:"GOWON_PREFIX" default:"." description:"prefix for commands"` + Broker string `short:"b" long:"broker" env:"GOWON_BROKER" default:"localhost:1883" description:"mqtt broker"` + UserName string `short:"u" long:"username" env:"GOWON_RA_USERNAME" required:"true" description:"retroachievements username"` + APIKey string `short:"k" long:"api-key" env:"GOWON_RA_API_KEY" required:"true" description:"retroachievements api key"` + KVPath string `short:"K" long:"kv-path" env:"GOWON_RA_KV_PATH" default:"kv.db" description:"path to kv db"` +} + +const ( + moduleName = "retroachievements" + mqttConnectRetryInternal = 5 + mqttDisconnectTimeout = 1000 +) + +func setUser(kv *bolt.DB, nick, user []byte) error { + err := kv.Update(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte("retroachievements")) + return b.Put([]byte(nick), []byte(user)) + }) + return err +} + +func getUser(kv *bolt.DB, nick []byte) (user []byte, err error) { + err = kv.View(func(tx *bolt.Tx) error { + b := tx.Bucket([]byte("retroachievements")) + v := b.Get([]byte(nick)) + user = v + return nil + }) + return user, err +} + +func genRaHandler(apiUser, apiKey string, kv *bolt.DB) func(m gowon.Message) (string, error) { + return func(m gowon.Message) (string, error) { + fields := strings.Fields(m.Args) + + if len(fields) >= 2 && fields[0] == "set" { + err := setUser(kv, []byte(m.Nick), []byte(fields[1])) + if err != nil { + return "", err + } + return fmt.Sprintf("set %s's user to %s", m.Nick, fields[1]), nil + } + + if len(fields) >= 1 { + user := strings.Fields(m.Args)[0] + return ra(apiUser, apiKey, user) + } + + user, err := getUser(kv, []byte(m.Nick)) + if err != nil { + return "", err + } + + if len(user) > 0 { + return ra(apiUser, apiKey, string(user)) + } + + return "Error: username needed", nil + } +} + +func defaultPublishHandler(c mqtt.Client, msg mqtt.Message) { + log.Printf("unexpected message: %s\n", msg) +} + +func onConnectionLostHandler(c mqtt.Client, err error) { + log.Println("connection to broker lost") +} + +func onRecconnectingHandler(c mqtt.Client, opts *mqtt.ClientOptions) { + log.Println("attempting to reconnect to broker") +} + +func onConnectHandler(c mqtt.Client) { + log.Println("connected to broker") +} + +func main() { + log.Printf("%s starting\n", moduleName) + + opts := Options{} + if _, err := flags.Parse(&opts); err != nil { + log.Fatal(err) + } + + mqttOpts := mqtt.NewClientOptions() + mqttOpts.AddBroker(fmt.Sprintf("tcp://%s", opts.Broker)) + mqttOpts.SetClientID(fmt.Sprintf("gowon_%s", moduleName)) + mqttOpts.SetConnectRetry(true) + mqttOpts.SetConnectRetryInterval(mqttConnectRetryInternal * time.Second) + mqttOpts.SetAutoReconnect(true) + + mqttOpts.DefaultPublishHandler = defaultPublishHandler + mqttOpts.OnConnectionLost = onConnectionLostHandler + mqttOpts.OnReconnecting = onRecconnectingHandler + mqttOpts.OnConnect = onConnectHandler + + kv, err := bolt.Open(opts.KVPath, 0666, nil) + if err != nil { + log.Fatal(err) + } + defer kv.Close() + + err = kv.Update(func(tx *bolt.Tx) error { + _, err := tx.CreateBucketIfNotExists([]byte("retroachievements")) + return err + }) + if err != nil { + log.Fatal(err) + } + + mr := gowon.NewMessageRouter() + + mr.AddCommand("ra", genRaHandler(opts.UserName, opts.APIKey, kv)) + mr.Subscribe(mqttOpts, moduleName) + + log.Print("connecting to broker") + + c := mqtt.NewClient(mqttOpts) + if token := c.Connect(); token.Wait() && token.Error() != nil { + panic(token.Error()) + } + + log.Print("connected to broker") + + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) + + <-sigs + + log.Println("signal caught, exiting") + c.Disconnect(mqttDisconnectTimeout) + log.Println("shutdown complete") +} diff --git a/retroachivements.go b/retroachivements.go new file mode 100644 index 0000000..f564a0a --- /dev/null +++ b/retroachivements.go @@ -0,0 +1,75 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "time" +) + +const ( + raAchievementsURL = "https://ra.hfc-essentials.com/user_by_range.php?user=%s&key=%s&member=%s&mode=json" +) + +var ( + achievementStartTime = time.Date(2010, 10, 2, 6, 0, 0, 0, time.UTC) +) + +type AchievementsByDateResp struct { + AchievementList [][]Achievement `json:"achievement"` +} + +type Achievement struct { + Date string `json:"Date"` + HardcoreMode string `json:"HardcoreMode"` + AchievementID string `json:"AchievementID"` + Title string `json:"Title"` + Description string `json:"Description"` + BadgeName string `json:"BadgeName"` + Points string `json:"Points"` + Author string `json:"Author"` + GameTitle string `json:"GameTitle"` + GameIcon string `json:"GameIcon"` + GameID string `json:"GameID"` + ConsoleName string `json:"ConsoleName"` + CumulScore int `json:"CumulScore"` + BadgeURL string `json:"BadgeURL"` + GameURL string `json:"GameURL"` +} + +func formatAchievement(user string, a Achievement) string { + return fmt.Sprintf("%s's last retro achievement: %s (%s) - %s (%s)", user, a.Title, a.Description, a.GameTitle, a.ConsoleName) +} + +func ra(apiUser, apiKey, user string) (string, error) { + url := fmt.Sprintf(raAchievementsURL, apiUser, apiKey, user) + + j := &AchievementsByDateResp{} + + res, err := http.Get(url) + if err != nil { + return "", err + } + + defer res.Body.Close() + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return "", err + } + + err = json.Unmarshal(body, &j) + + if err != nil { + return "", err + } + + if len(j.AchievementList[0]) == 0 { + return fmt.Sprintf("No achievements found for user %s", user), nil + } + + out := formatAchievement(user, j.AchievementList[0][len(j.AchievementList[0])-1]) + + return out, nil +}