From eafdbff4d5db77c9b8667f34197dd2657cf19a98 Mon Sep 17 00:00:00 2001 From: Aram Peres <6775216+aramperes@users.noreply.github.com> Date: Fri, 21 Jul 2023 21:25:18 -0400 Subject: [PATCH 1/9] Add Helm chart --- .github/workflows/release.yml | 19 ++++++ helm/ts-activity/.helmignore | 23 +++++++ helm/ts-activity/Chart.yaml | 8 +++ helm/ts-activity/templates/_helpers.tpl | 62 +++++++++++++++++++ helm/ts-activity/templates/deployment.yaml | 61 ++++++++++++++++++ .../ts-activity/templates/serviceaccount.yaml | 12 ++++ helm/ts-activity/values.yaml | 53 ++++++++++++++++ 7 files changed, 238 insertions(+) create mode 100644 helm/ts-activity/.helmignore create mode 100644 helm/ts-activity/Chart.yaml create mode 100644 helm/ts-activity/templates/_helpers.tpl create mode 100644 helm/ts-activity/templates/deployment.yaml create mode 100644 helm/ts-activity/templates/serviceaccount.yaml create mode 100644 helm/ts-activity/values.yaml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3d979d5..75aafdc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,3 +32,22 @@ jobs: push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + + - name: Get the release version from the tag + if: env.VERSION == '' + run: | + echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV + echo "version is: ${{ env.VERSION }}" + + - name: Update Helm chart version + shell: bash + run: sed -i 's/0\.0\.0/${{ env.VERSION }}/g' helm/ts-activity/Chart.yaml + + - name: Build and push Helm chart + uses: goodsmileduck/helm-push-action@ec9f29cbf16a4773438b3ea98790aa5b5ca3e749 + env: + SOURCE_DIR: './helm' + CHART_FOLDER: 'ts-activity' + CHARTMUSEUM_URL: 'https://charts.momoperes.ca' + CHARTMUSEUM_USER: '${{ secrets.CHARTMUSEUM_USER }}' + CHARTMUSEUM_PASSWORD: ${{ secrets.CHARTMUSEUM_PASSWORD }} diff --git a/helm/ts-activity/.helmignore b/helm/ts-activity/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/helm/ts-activity/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/ts-activity/Chart.yaml b/helm/ts-activity/Chart.yaml new file mode 100644 index 0000000..07bc879 --- /dev/null +++ b/helm/ts-activity/Chart.yaml @@ -0,0 +1,8 @@ +apiVersion: v2 +name: ts-activity +description: Post TeamSpeak events to Discord + +type: application + +version: "0.0.0" +appVersion: "0.0.0" diff --git a/helm/ts-activity/templates/_helpers.tpl b/helm/ts-activity/templates/_helpers.tpl new file mode 100644 index 0000000..9665e88 --- /dev/null +++ b/helm/ts-activity/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "ts-activity.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "ts-activity.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "ts-activity.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "ts-activity.labels" -}} +helm.sh/chart: {{ include "ts-activity.chart" . }} +{{ include "ts-activity.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "ts-activity.selectorLabels" -}} +app.kubernetes.io/name: {{ include "ts-activity.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "ts-activity.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "ts-activity.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/helm/ts-activity/templates/deployment.yaml b/helm/ts-activity/templates/deployment.yaml new file mode 100644 index 0000000..9667033 --- /dev/null +++ b/helm/ts-activity/templates/deployment.yaml @@ -0,0 +1,61 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "ts-activity.fullname" . }} + labels: + {{- include "ts-activity.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: 1 + {{- end }} + selector: + matchLabels: + {{- include "ts-activity.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "ts-activity.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "ts-activity.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default "latest" }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/helm/ts-activity/templates/serviceaccount.yaml b/helm/ts-activity/templates/serviceaccount.yaml new file mode 100644 index 0000000..f102664 --- /dev/null +++ b/helm/ts-activity/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "ts-activity.serviceAccountName" . }} + labels: + {{- include "ts-activity.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/helm/ts-activity/values.yaml b/helm/ts-activity/values.yaml new file mode 100644 index 0000000..332054c --- /dev/null +++ b/helm/ts-activity/values.yaml @@ -0,0 +1,53 @@ +# Default values for ts-activity. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +image: + repository: aramperes/ts-activity + pullPolicy: IfNotPresent + # Overrides the image tag whose default is latest. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +nodeSelector: {} + +tolerations: [] + +affinity: {} From 976b8dc0a92104d423822c4811177c85853a9198 Mon Sep 17 00:00:00 2001 From: Aram Peres <6775216+aramperes@users.noreply.github.com> Date: Fri, 21 Jul 2023 22:00:56 -0400 Subject: [PATCH 2/9] Simplify client map --- cmd.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cmd.go b/cmd.go index 97ff074..19c0e87 100644 --- a/cmd.go +++ b/cmd.go @@ -50,7 +50,7 @@ func main() { log.Fatal(err) } - clientMap := make(map[int]string) + clientMap := make(map[string]string) log.Println("Online clients:") for _, client := range cl { @@ -58,7 +58,7 @@ func main() { continue } log.Println("-", client) - clientMap[client.ID] = client.Nickname + clientMap[strconv.Itoa(client.ID)] = client.Nickname } // Listen for client updates @@ -73,9 +73,9 @@ func main() { continue } - clientId, err := strconv.Atoi(event.Data["clid"]) - if err != nil { - log.Println("Failed to get client ID:", err) + clientId, ok := event.Data["clid"] + if !ok { + log.Println("User has no client id", event.Data) continue } @@ -92,9 +92,9 @@ func main() { ClientConnected(discord, clientNick) } } else if event.Type == "clientleftview" { - clientId, err := strconv.Atoi(event.Data["clid"]) - if err != nil { - log.Println("Failed to get client ID:", err) + clientId, ok := event.Data["clid"] + if !ok { + log.Println("User has no client id", event.Data) continue } From 71618fc8c36df154ff1a7b454a5220d92f4cc71b Mon Sep 17 00:00:00 2001 From: Aram Peres <6775216+aramperes@users.noreply.github.com> Date: Fri, 21 Jul 2023 23:24:55 -0400 Subject: [PATCH 3/9] Add configs to Helm chart --- .gitignore | 1 + helm/ts-activity/Chart.yaml | 2 +- helm/ts-activity/templates/deployment.yaml | 36 +++++++++++++--------- helm/ts-activity/values.yaml | 13 +++++++- 4 files changed, 35 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index 7a6353d..3e26fff 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .envrc +/values-dev.yaml diff --git a/helm/ts-activity/Chart.yaml b/helm/ts-activity/Chart.yaml index 07bc879..378029b 100644 --- a/helm/ts-activity/Chart.yaml +++ b/helm/ts-activity/Chart.yaml @@ -5,4 +5,4 @@ description: Post TeamSpeak events to Discord type: application version: "0.0.0" -appVersion: "0.0.0" +appVersion: "v0.0.0" diff --git a/helm/ts-activity/templates/deployment.yaml b/helm/ts-activity/templates/deployment.yaml index 9667033..edfd5fd 100644 --- a/helm/ts-activity/templates/deployment.yaml +++ b/helm/ts-activity/templates/deployment.yaml @@ -5,9 +5,7 @@ metadata: labels: {{- include "ts-activity.labels" . | nindent 4 }} spec: - {{- if not .Values.autoscaling.enabled }} replicas: 1 - {{- end }} selector: matchLabels: {{- include "ts-activity.selectorLabels" . | nindent 6 }} @@ -31,20 +29,28 @@ spec: - name: {{ .Chart.Name }} securityContext: {{- toYaml .Values.securityContext | nindent 12 }} - image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default "latest" }}" + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} - ports: - - name: http - containerPort: {{ .Values.service.port }} - protocol: TCP - livenessProbe: - httpGet: - path: / - port: http - readinessProbe: - httpGet: - path: / - port: http + env: + - name: TS_DISCORD_USERNAME + value: "{{ .Values.config.discordUsername | default "Jeff" }}" + - name: TS_QUERY_ADDR + value: "{{ .Values.config.serverQueryAddr | required "must provide serverQueryAddr" }}" + - name: TS_QUERY_USER + valueFrom: + secretKeyRef: + key: username + name: "{{ .Values.config.serverQuerySecret | required "must provide serverQuerySecret" }}" + - name: TS_QUERY_PASS + valueFrom: + secretKeyRef: + key: password + name: "{{ .Values.config.serverQuerySecret | required "must provide serverQuerySecret" }}" + - name: TS_DISCORD_WEBHOOK + valueFrom: + secretKeyRef: + key: discord + name: "{{ .Values.config.webhookSecret | required "must provide webhookSecret" }}" resources: {{- toYaml .Values.resources | nindent 12 }} {{- with .Values.nodeSelector }} diff --git a/helm/ts-activity/values.yaml b/helm/ts-activity/values.yaml index 332054c..20c0371 100644 --- a/helm/ts-activity/values.yaml +++ b/helm/ts-activity/values.yaml @@ -5,9 +5,20 @@ image: repository: aramperes/ts-activity pullPolicy: IfNotPresent - # Overrides the image tag whose default is latest. + # Overrides the image tag whose default is the chart version. tag: "" +config: + # Discord username displayed in webhook messages. + # Defaults to 'Jeff' + discordUsername: "" + # Address to plain ServerQuery. Usually :10011 + serverQueryAddr: "" + # Secret containing 'username' and 'password' for ServerQuery. + serverQuerySecret: "" + # Secret containing 'discord' with the Webhook URL. + webhookSecret: "" + imagePullSecrets: [] nameOverride: "" fullnameOverride: "" From db407c0ef5bfafc654226b0b33358113b1c40afe Mon Sep 17 00:00:00 2001 From: Aram Peres <6775216+aramperes@users.noreply.github.com> Date: Fri, 21 Jul 2023 23:29:25 -0400 Subject: [PATCH 4/9] Fix appVersion --- .github/workflows/release.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 75aafdc..2ce1da7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,7 +41,9 @@ jobs: - name: Update Helm chart version shell: bash - run: sed -i 's/0\.0\.0/${{ env.VERSION }}/g' helm/ts-activity/Chart.yaml + run: | + v="$(echo '${{ env.VERSION }}' | cut -d 'v' -f2)" + sed -i "s/0\.0\.0/$v/g" helm/ts-activity/Chart.yaml - name: Build and push Helm chart uses: goodsmileduck/helm-push-action@ec9f29cbf16a4773438b3ea98790aa5b5ca3e749 From 7e93beb13225d8a0383ff834baf68fbe8a9d753b Mon Sep 17 00:00:00 2001 From: Aram Peres <6775216+aramperes@users.noreply.github.com> Date: Fri, 21 Jul 2023 23:45:13 -0400 Subject: [PATCH 5/9] Create README.md --- README.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..e0b40c6 --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# ts-activity + +This program will post notifications to Discord when someone joins or leaves your TeamSpeak server. + +![image](https://github.com/aramperes/ts-activity/assets/6775216/bab942c3-b7d7-4b5e-8d14-4d69383bc856) + + +## Configuration + +You will have to create ServerQuery credentials on an account that has permissions to login & view clients in the server. You can do this from the `Tools -> ServerQuery Login` menu in TeamSpeak 3. + +This program is configured using environment variables: + +- `TS_QUERY_ADDR`: Address to the TeamSpeak ServerQuery port. Example: `127.0.0.1:10011` +- `TS_QUERY_USER`: The username you selected for ServerQuery in the setup +- `TS_QUERY_PASS`: The password TeamSpeak generated for ServerQuery in the setup +- `TS_DISCORD_WEBHOOK`: Webhook URL for Discord. You can create this from the channel "Integrations" page + +## Run it +[![docker hub](https://img.shields.io/docker/v/aramperes/ts-activity?color=%232496ed&label=docker%20hub&logo=docker&logoColor=fff&sort=semver)](https://hub.docker.com/r/aramperes/ts-activity) + +To build and run locally: + +```sh +go mod download +go run . +``` + +Or, using the [Docker image](https://hub.docker.com/r/aramperes/ts-activity): + +```sh +docker run --rm --name ts-activity \ + -e TS_DISCORD_WEBHOOK='https://discord.com/api/webhooks/...' \ + -e TS_QUERY_ADDR=127.0.0.1:10011 \ + -e TS_QUERY_USER=Jeff \ + -e TS_QUERY_PASS=******* \ + aramperes/ts-activity +``` + +There is also a Helm chart. You can create a `Secret` containing `username`, `password`, and `discord`, and then: + +```sh +helm repo add momoperes https://charts.momoperes.ca +helm repo update + +helm upgrade --install ts-activity momoperes/ts-activity \ + --set config.serverQueryAddr=teamspeak:10011 \ + --set config.discordUsername=Jeff \ + --set serverQuerySecret=ts-activity \ + --set webhookSecret=ts-activity +``` From 71ed01df586d2e141048fb6f719c271ca3d703f8 Mon Sep 17 00:00:00 2001 From: Aram Peres <6775216+aramperes@users.noreply.github.com> Date: Sat, 22 Jul 2023 12:33:20 -0400 Subject: [PATCH 6/9] Clean-up --- README.md | 4 ++-- cmd.go | 24 ++++++++++++------------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index e0b40c6..36035c4 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,6 @@ helm repo update helm upgrade --install ts-activity momoperes/ts-activity \ --set config.serverQueryAddr=teamspeak:10011 \ --set config.discordUsername=Jeff \ - --set serverQuerySecret=ts-activity \ - --set webhookSecret=ts-activity + --set config.serverQuerySecret=ts-activity \ + --set config.webhookSecret=ts-activity ``` diff --git a/cmd.go b/cmd.go index 19c0e87..674fe48 100644 --- a/cmd.go +++ b/cmd.go @@ -73,7 +73,7 @@ func main() { continue } - clientId, ok := event.Data["clid"] + clientID, ok := event.Data["clid"] if !ok { log.Println("User has no client id", event.Data) continue @@ -81,36 +81,36 @@ func main() { clientNick, ok := event.Data["client_nickname"] if !ok { - log.Println("User has no nickname:", clientId) + log.Println("User has no nickname:", clientID) continue } - _, previous := clientMap[clientId] - clientMap[clientId] = clientNick + _, previous := clientMap[clientID] + clientMap[clientID] = clientNick if !previous { - ClientConnected(discord, clientNick) + clientConnected(discord, clientNick) } } else if event.Type == "clientleftview" { - clientId, ok := event.Data["clid"] + clientID, ok := event.Data["clid"] if !ok { log.Println("User has no client id", event.Data) continue } - clientNick, ok := clientMap[clientId] + clientNick, ok := clientMap[clientID] if !ok { - log.Println("Unknown user left:", clientId) + log.Println("Unknown user left:", clientID) continue } - delete(clientMap, clientId) - ClientDisconnected(discord, clientNick) + delete(clientMap, clientID) + clientDisconnected(discord, clientNick) } } } -func ClientConnected(discord string, nick string) { +func clientConnected(discord string, nick string) { bot := os.Getenv("TS_DISCORD_USERNAME") if bot == "" { bot = "Jeff" @@ -127,7 +127,7 @@ func ClientConnected(discord string, nick string) { } } -func ClientDisconnected(discord string, nick string) { +func clientDisconnected(discord string, nick string) { bot := os.Getenv("TS_DISCORD_USERNAME") if bot == "" { bot = "Jeff" From 9fbbd2cd226f1d37623b4d8f94ddf5a58acc8104 Mon Sep 17 00:00:00 2001 From: Aram Peres <6775216+aramperes@users.noreply.github.com> Date: Sat, 22 Jul 2023 19:20:55 -0400 Subject: [PATCH 7/9] Refactoring & allow Discord avatar, custom server ID --- README.md | 3 + cmd.go | 119 ++++++++++++++------- helm/ts-activity/templates/deployment.yaml | 10 +- helm/ts-activity/values.yaml | 6 +- 4 files changed, 99 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 36035c4..36c5f2c 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,10 @@ This program is configured using environment variables: - `TS_QUERY_ADDR`: Address to the TeamSpeak ServerQuery port. Example: `127.0.0.1:10011` - `TS_QUERY_USER`: The username you selected for ServerQuery in the setup - `TS_QUERY_PASS`: The password TeamSpeak generated for ServerQuery in the setup +- `TS_QUERY_SERVER_ID`: Virtual server ID to monitor. Defaults to `1` - `TS_DISCORD_WEBHOOK`: Webhook URL for Discord. You can create this from the channel "Integrations" page +- `TS_DISCORD_AVATAR`: Optional URL for Discord bot avatar +- `TS_DISCORD_USERNAME`: Optional nickname for Discord bot ## Run it [![docker hub](https://img.shields.io/docker/v/aramperes/ts-activity?color=%232496ed&label=docker%20hub&logo=docker&logoColor=fff&sort=semver)](https://hub.docker.com/r/aramperes/ts-activity) diff --git a/cmd.go b/cmd.go index 674fe48..54e4b9e 100644 --- a/cmd.go +++ b/cmd.go @@ -1,6 +1,7 @@ package main import ( + "errors" "fmt" "log" "os" @@ -10,26 +11,83 @@ import ( "github.com/multiplay/go-ts3" ) +// App holds the configuration +type App struct { + discordURL string + discordUsername string + discordAvatarURL *string + tsQueryAddr string + tsQueryUser string + tsQueryPass string + tsQueryServerID int +} + +func appFromEnv() (*App, error) { + discordURL := os.Getenv("TS_DISCORD_WEBHOOK") + if discordURL == "" { + return nil, errors.New("must configure: TS_DISCORD_WEBHOOK") + } + discordUsername := os.Getenv("TS_DISCORD_USERNAME") + if discordUsername == "" { + discordUsername = "TeamSpeak" + } + + var discordAvatarURL *string = nil + if val, ok := os.LookupEnv("TS_DISCORD_AVATAR"); ok { + discordAvatarURL = &val + } + + tsQueryAddr := os.Getenv("TS_QUERY_ADDR") + if tsQueryAddr == "" { + return nil, errors.New("must configure: TS_QUERY_ADDR") + } + tsQueryUser := os.Getenv("TS_QUERY_USER") + if tsQueryUser == "" { + return nil, errors.New("must configure: TS_QUERY_USER") + } + tsQueryPass := os.Getenv("TS_QUERY_PASS") + if tsQueryPass == "" { + return nil, errors.New("must configure: TS_QUERY_PASS") + } + tsQueryServerID := 1 + if val, ok := os.LookupEnv("TS_QUERY_SERVER_ID"); ok { + val, err := strconv.Atoi(val) + if err == nil { + tsQueryServerID = val + } else { + return nil, errors.New("invalid TS_QUERY_SERVER_ID, must be int") + } + } + + return &App{ + discordURL: discordURL, + discordUsername: discordUsername, + discordAvatarURL: discordAvatarURL, + tsQueryAddr: tsQueryAddr, + tsQueryUser: tsQueryUser, + tsQueryPass: tsQueryPass, + tsQueryServerID: tsQueryServerID, + }, nil +} + func main() { - discord := os.Getenv("TS_DISCORD_WEBHOOK") - if discord == "" { - log.Fatal("Must configure: TS_DISCORD_WEBHOOK") + app, err := appFromEnv() + if err != nil { + log.Fatal(err) } // Connect and login - c, err := ts3.NewClient(os.Getenv("TS_QUERY_ADDR")) + c, err := ts3.NewClient(app.tsQueryAddr) if err != nil { log.Fatal(err) } defer c.Close() - user := os.Getenv("TS_QUERY_USER") - pass := os.Getenv("TS_QUERY_PASS") - if err := c.Login(user, pass); err != nil { + if err := c.Login(app.tsQueryUser, app.tsQueryPass); err != nil { log.Fatal(err) } - if err := c.Use(1); err != nil { + if err := c.Use(app.tsQueryServerID); err != nil { log.Fatal(err) } @@ -66,7 +124,6 @@ func main() { for { event := <-notifs - log.Println("=>", event) if event.Type == "cliententerview" { if event.Data["client_type"] != "0" { @@ -89,7 +146,7 @@ func main() { clientMap[clientID] = clientNick if !previous { - clientConnected(discord, clientNick) + app.clientConnected(clientNick) } } else if event.Type == "clientleftview" { clientID, ok := event.Data["clid"] @@ -105,41 +162,29 @@ func main() { } delete(clientMap, clientID) - clientDisconnected(discord, clientNick) + app.clientDisconnected(clientNick) } } } -func clientConnected(discord string, nick string) { - bot := os.Getenv("TS_DISCORD_USERNAME") - if bot == "" { - bot = "Jeff" +func (app *App) sendWebhook(content string) { + message := discordwebhook.Message{ + Username: &app.discordUsername, + Content: &content, + AvatarUrl: app.discordAvatarURL, } + if err := discordwebhook.SendMessage(app.discordURL, message); err != nil { + log.Println("Failed to log Discord message:", err) + } +} + +func (app *App) clientConnected(nick string) { content := fmt.Sprintf("Client connected: %s", nick) - message := discordwebhook.Message{ - Username: &bot, - Content: &content, - } - - if err := discordwebhook.SendMessage(discord, message); err != nil { - log.Println("Failed to log Discord message:", err) - } + app.sendWebhook(content) } -func clientDisconnected(discord string, nick string) { - bot := os.Getenv("TS_DISCORD_USERNAME") - if bot == "" { - bot = "Jeff" - } - +func (app *App) clientDisconnected(nick string) { content := fmt.Sprintf("Client disconnected: %s", nick) - message := discordwebhook.Message{ - Username: &bot, - Content: &content, - } - - if err := discordwebhook.SendMessage(discord, message); err != nil { - log.Println("Failed to log Discord message:", err) - } + app.sendWebhook(content) } diff --git a/helm/ts-activity/templates/deployment.yaml b/helm/ts-activity/templates/deployment.yaml index edfd5fd..ef8efe5 100644 --- a/helm/ts-activity/templates/deployment.yaml +++ b/helm/ts-activity/templates/deployment.yaml @@ -32,8 +32,16 @@ spec: image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} env: + {{- with .Values.config.discordUsername }} - name: TS_DISCORD_USERNAME - value: "{{ .Values.config.discordUsername | default "Jeff" }}" + value: {{ . | quote }} + {{- end }} + {{- with .Values.config.discordAvatar }} + - name: TS_DISCORD_AVATAR + value: {{ . | quote }} + {{- end }} + - name: TS_QUERY_SERVER_ID + value: {{ .Values.config.serverQueryId | quote }} - name: TS_QUERY_ADDR value: "{{ .Values.config.serverQueryAddr | required "must provide serverQueryAddr" }}" - name: TS_QUERY_USER diff --git a/helm/ts-activity/values.yaml b/helm/ts-activity/values.yaml index 20c0371..3aad80e 100644 --- a/helm/ts-activity/values.yaml +++ b/helm/ts-activity/values.yaml @@ -5,19 +5,23 @@ image: repository: aramperes/ts-activity pullPolicy: IfNotPresent - # Overrides the image tag whose default is the chart version. + # Overrides the image tag whose default is the chart appVersion. tag: "" config: # Discord username displayed in webhook messages. # Defaults to 'Jeff' discordUsername: "" + # Discord avatar displayed in webhook messages. + discordAvatar: "" # Address to plain ServerQuery. Usually :10011 serverQueryAddr: "" # Secret containing 'username' and 'password' for ServerQuery. serverQuerySecret: "" # Secret containing 'discord' with the Webhook URL. webhookSecret: "" + # TeamSpeak virtual server ID. + serverQueryId: 1 imagePullSecrets: [] nameOverride: "" From abfc92385a370d077d86b65f30b64bd5c279b0b1 Mon Sep 17 00:00:00 2001 From: Aram Peres <6775216+aramperes@users.noreply.github.com> Date: Wed, 6 Nov 2024 22:17:58 -0500 Subject: [PATCH 8/9] Spotlight feature --- cmd.go | 143 +++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 129 insertions(+), 14 deletions(-) diff --git a/cmd.go b/cmd.go index 54e4b9e..647ba60 100644 --- a/cmd.go +++ b/cmd.go @@ -5,7 +5,9 @@ import ( "fmt" "log" "os" + "slices" "strconv" + "strings" "github.com/gtuk/discordwebhook" "github.com/multiplay/go-ts3" @@ -13,13 +15,15 @@ import ( // App holds the configuration type App struct { - discordURL string - discordUsername string - discordAvatarURL *string - tsQueryAddr string - tsQueryUser string - tsQueryPass string - tsQueryServerID int + discordURL string + discordUsername string + discordAvatarURL *string + tsQueryAddr string + tsQueryUser string + tsQueryPass string + tsQueryServerID int + spotLightGfxFormat string + spotLightIDMap map[string]int } func appFromEnv() (*App, error) { @@ -58,15 +62,33 @@ func appFromEnv() (*App, error) { return nil, errors.New("invalid TS_QUERY_SERVER_ID, must be int") } } + spotLightGfxFormat := os.Getenv("TS_SPOTLIGHT_GFX_FMT") + + // TODO: Load from environment variable + spotLightIDMap := make(map[string]int) + spotLightIDMap["rb+mT/4bh37gHzQYqTgBiEHG2IA="] = 0 + spotLightIDMap["sA3fHhvqmlSuFYtMoVYseRQI2DE="] = 0 + spotLightIDMap["9K6JV7kWaRU+4HFRkXrBZNjSmRA="] = 1 + spotLightIDMap["pFclzBx0w2UmwPd91VvaXJjYCYA="] = 2 + spotLightIDMap["tvjlpKqvcyQSCCVkT0TJ28uwdaQ="] = 3 + spotLightIDMap["SLLvtjVBmSoIzpMhlxnLa9CWoOU="] = 4 + spotLightIDMap["7EU/Up++D9+8SQk0sNchEuKPufw="] = 5 + spotLightIDMap["SyldxnLYWHJOUj3HnEsXGF6B0T4="] = 5 + spotLightIDMap["Mc/TdoNhddKdGtB55DSZrYk3NWc="] = 6 + spotLightIDMap["xOWMWG/TpkbV8XjahqqoQLsHHpA="] = 7 + spotLightIDMap["G4kg1LKJElM5LIpoeA6gN7DMl0c="] = 7 + spotLightIDMap["wuQ907NtzqL4uxhLk3P/TCpkXF0="] = 8 return &App{ - discordURL: discordURL, - discordUsername: discordUsername, - discordAvatarURL: discordAvatarURL, - tsQueryAddr: tsQueryAddr, - tsQueryUser: tsQueryUser, - tsQueryPass: tsQueryPass, - tsQueryServerID: tsQueryServerID, + discordURL: discordURL, + discordUsername: discordUsername, + discordAvatarURL: discordAvatarURL, + tsQueryAddr: tsQueryAddr, + tsQueryUser: tsQueryUser, + tsQueryPass: tsQueryPass, + tsQueryServerID: tsQueryServerID, + spotLightGfxFormat: spotLightGfxFormat, + spotLightIDMap: spotLightIDMap, }, nil } @@ -109,6 +131,8 @@ func main() { } clientMap := make(map[string]string) + clientDatabaseIDs := make(map[string]string) + clientUniqueIDs := make(map[string]string) log.Println("Online clients:") for _, client := range cl { @@ -117,8 +141,20 @@ func main() { } log.Println("-", client) clientMap[strconv.Itoa(client.ID)] = client.Nickname + clientDatabaseIDs[strconv.Itoa(client.ID)] = strconv.Itoa(client.DatabaseID) + + uid, err := getClientUniqueId(c, strconv.Itoa(client.DatabaseID)) + if err != nil { + log.Fatal(err) + } + + clientUniqueIDs[strconv.Itoa(client.ID)] = uid + log.Println(" - UID:", uid) } + // Update the banner on startup with the currently online users. + app.updateSpotLight(c, mapValues(clientUniqueIDs)) + // Listen for client updates notifs := c.Notifications() @@ -136,6 +172,12 @@ func main() { continue } + clientDBID, ok := event.Data["client_database_id"] + if !ok { + log.Println("User has no client database id", event.Data) + continue + } + clientNick, ok := event.Data["client_nickname"] if !ok { log.Println("User has no nickname:", clientID) @@ -147,6 +189,16 @@ func main() { if !previous { app.clientConnected(clientNick) + + clientDatabaseIDs[clientID] = clientDBID + uid, err := getClientUniqueId(c, clientDBID) + if err != nil { + log.Fatal(err) + } + + clientUniqueIDs[clientID] = uid + + app.updateSpotLight(c, mapValues(clientUniqueIDs)) } } else if event.Type == "clientleftview" { clientID, ok := event.Data["clid"] @@ -162,6 +214,9 @@ func main() { } delete(clientMap, clientID) + delete(clientDatabaseIDs, clientID) + delete(clientUniqueIDs, clientID) + app.updateSpotLight(c, mapValues(clientUniqueIDs)) app.clientDisconnected(clientNick) } } @@ -188,3 +243,63 @@ func (app *App) clientDisconnected(nick string) { content := fmt.Sprintf("Client disconnected: %s", nick) app.sendWebhook(content) } + +func (app *App) updateSpotLight(c *ts3.Client, connectedUIDs []string) { + if app.spotLightGfxFormat == "" { + return + } + + spotLightIDs := make([]int, 0) + for _, uid := range connectedUIDs { + spotLightID, ok := app.spotLightIDMap[uid] + if ok { + spotLightIDs = append(spotLightIDs, spotLightID) + } + } + + if len(spotLightIDs) == 0 { + updateBanner(c, fmt.Sprintf(app.spotLightGfxFormat, "empty")) + return + } + + slices.Sort(spotLightIDs) + slices.Compact(spotLightIDs) + + spotLightIDStrings := make([]string, len(spotLightIDs)) + for idx, id := range spotLightIDs { + spotLightIDStrings[idx] = strconv.Itoa(id) + } + joined := strings.Join(spotLightIDStrings, "_") + + updateBanner(c, fmt.Sprintf(app.spotLightGfxFormat, joined)) +} + +func updateBanner(c *ts3.Client, gfx string) { + err := c.Server.Edit(ts3.NewArg("virtualserver_hostbanner_gfx_url", gfx)) + if err != nil { + log.Println("Failed to update banner:", gfx, err) + } else { + log.Println("Updated banner:", gfx) + } +} + +func getClientUniqueId(c *ts3.Client, dbID string) (string, error) { + var uid = struct { + UID string `ms:"cluid"` + }{} + _, err := c.ExecCmd(ts3.NewCmd("clientgetnamefromdbid").WithArgs(ts3.NewArg("cldbid", dbID)).WithResponse(&uid)) + + if err != nil { + return "", err + } + + return uid.UID, nil +} + +func mapValues(m map[string]string) []string { + v := make([]string, 0, len(m)) + for _, val := range m { + v = append(v, val) + } + return v +} From 7aba36c06484af99c24e630d78216b0f60c23e60 Mon Sep 17 00:00:00 2001 From: Aram Peres <6775216+aramperes@users.noreply.github.com> Date: Wed, 6 Nov 2024 22:21:20 -0500 Subject: [PATCH 9/9] Remove broken Helm chart repo --- .github/workflows/release.yml | 15 --------------- README.md | 3 --- 2 files changed, 18 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2ce1da7..cb22183 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,18 +38,3 @@ jobs: run: | echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV echo "version is: ${{ env.VERSION }}" - - - name: Update Helm chart version - shell: bash - run: | - v="$(echo '${{ env.VERSION }}' | cut -d 'v' -f2)" - sed -i "s/0\.0\.0/$v/g" helm/ts-activity/Chart.yaml - - - name: Build and push Helm chart - uses: goodsmileduck/helm-push-action@ec9f29cbf16a4773438b3ea98790aa5b5ca3e749 - env: - SOURCE_DIR: './helm' - CHART_FOLDER: 'ts-activity' - CHARTMUSEUM_URL: 'https://charts.momoperes.ca' - CHARTMUSEUM_USER: '${{ secrets.CHARTMUSEUM_USER }}' - CHARTMUSEUM_PASSWORD: ${{ secrets.CHARTMUSEUM_PASSWORD }} diff --git a/README.md b/README.md index 36c5f2c..f10f5d5 100644 --- a/README.md +++ b/README.md @@ -43,9 +43,6 @@ docker run --rm --name ts-activity \ There is also a Helm chart. You can create a `Secret` containing `username`, `password`, and `discord`, and then: ```sh -helm repo add momoperes https://charts.momoperes.ca -helm repo update - helm upgrade --install ts-activity momoperes/ts-activity \ --set config.serverQueryAddr=teamspeak:10011 \ --set config.discordUsername=Jeff \