Compare commits

...

73 Commits
v0.2.2 ... main

Author SHA1 Message Date
stryan 5d413e3806 don't clear channel ID on auto toggle 2022-03-02 17:06:35 -05:00
Tyler Stiene 7010fd277a table of options for the readme 2022-02-01 01:44:57 -05:00
Tyler Stiene 1e46cbbc6e announce patchcord.io 2021-12-19 22:11:06 -05:00
Tyler Stiene 52c3132424 chore: seperate binary and docker actions 2021-12-15 01:11:31 -05:00
Tyler Stiene 4b941100fc chore: abandon goreleaser docker build 2021-12-15 00:55:21 -05:00
Tyler Stiene 23a4b6fa66 chore: update .gitignore 2021-12-15 00:09:57 -05:00
Tyler Stiene 07ee7b36c5 chore: build improvements 2021-12-15 00:07:04 -05:00
Tyler Stiene f520fc67c7 fix: allow discordSendPCM to exit when discord disconnects
chore: update action and docker build
2021-12-14 23:37:02 -05:00
Tyler Stiene c0ed696543 chore: build on main branch 2021-12-14 01:18:47 -05:00
Tyler Stiene c28a9cd5f8 fix github action 2021-12-14 00:59:51 -05:00
Tyler Stiene c410c49095 Merge branch 'v0.5.0' into main 2021-12-13 00:17:30 -05:00
Tyler Stiene 08b4eb3771 flag to disable discord bot status 2021-12-13 00:10:48 -05:00
Tyler Stiene 3d24e1dbc4 build and deploy improvements 2021-12-12 23:54:32 -05:00
Tyler Stiene 3e96b27564 allow discord send loop to exit with ctx done 2021-12-07 22:59:04 -05:00
Tyler Stiene df2a477147
Merge pull request #28 from Brottweiler/patch-1
Tweak grammar when no users are in mumble
2021-12-05 17:50:03 -05:00
Christoffer Tibell 682887b936
Tweak grammar when no users are in mumble
The former capitalized "No" looks bad since the bot adds "Listening to" before it.
2021-12-05 13:40:50 +01:00
Tyler Stiene 7dea05bdb4 remove recover statement from constant bridge loop 2021-11-20 20:22:14 -05:00
Tyler Stiene 404af876db dsiable sleepct pausing untill additional testing 2021-11-20 17:12:04 -05:00
Tyler Stiene fd4884cdb6 from to discord buffer warning 2021-11-20 16:58:39 -05:00
Tyler Stiene c94362581a refactor global variables into duplex structs to fix issue on bridge restart
disable timer pausing in discord send untill better notification method can be developed
2021-11-19 00:57:20 -05:00
Tyler Stiene 8ca66fb500 refactor wait group
added debug message for bridge cancel
2021-11-17 23:58:54 -05:00
Tyler Stiene 0f57c5d33a improve discord buffer packet drop message
kill the bridge if unable to send to discord for 5 seconds
2021-11-04 01:21:41 -04:00
Tyler Stiene 52ea304bd1
Merge pull request #26 from stryan/main
minor README clarification
2021-11-04 00:59:38 -04:00
stryan fb5940bfb8 minor README clarification 2021-11-03 13:33:39 -04:00
Tyler Stiene 649fe9f33e fix sleepct pausing 2021-09-13 00:50:23 -04:00
Tyler Stiene 5c5ccb72c9 Merge branch 'sleepct-pause' into v0.5.0 2021-09-11 13:39:37 -04:00
Tyler Stiene f7f492670c correct buffer discord buffer axis 2021-08-23 00:35:01 -04:00
Tyler Stiene 6658c8534c add prometheus metrics
add grafana and promethus docker-container
add example grafana dashboard
sleepct now returns delta time from target
2021-08-23 00:00:39 -04:00
Tyler Stiene cb4d2349c5 wip allow a sleepct ticker to pause and be signaled 2021-08-09 23:07:27 -04:00
Tyler Stiene 6a188336b9 install libopus-dev in github action 2021-05-14 01:13:26 -04:00
Tyler Stiene dbe46c87fc readd missing files 2021-05-13 02:25:42 -04:00
Tyler Stiene 78aab791d2 fix issue with discordRecievePCM 2021-05-13 02:16:50 -04:00
Tyler Stiene 2888928c51
Merge pull request #23 from Stieneee/v0.4.0
V0.4.0
2021-05-13 01:36:39 -04:00
Tyler Stiene 72fe283778 update discord go for discord RTR extended header fix 2021-05-13 01:30:40 -04:00
Tyler Stiene 27f8e5ba65 always use system installed opus lib
trim white space from string env vars
2021-04-24 18:32:54 -04:00
Tyler Stiene a963d2caa1 restrucutre into go standard folder strucutre 2021-04-24 14:36:34 -04:00
Tyler Stiene 5bc118c97c
Merge pull request #21 from Stieneee/issue-20
Issue 20 - Mumble to Discord Buffer and Silence Frames
2021-04-24 13:23:09 -04:00
Tyler Stiene 9246563a35 openbsd readme update 2021-04-24 13:22:09 -04:00
Tyler Stiene 235dc44f41 recover from panic on shutdown 2021-04-24 13:17:11 -04:00
Tyler Stiene a1f6a60b89 to mumble jitter buffer and silence 2021-04-19 23:25:45 -04:00
Tyler Stiene c7f79ba01d remove temp debug message 2021-04-19 22:17:09 -04:00
Tyler Stiene 29ff1d797f chore move tickerct to seperate repo 2021-04-19 22:07:36 -04:00
Tyler Stiene c959774c23 unloaded and loaded ticker test 2021-04-18 17:39:25 -04:00
Tyler Stiene d18691d0f0 idlejitter test 2021-04-18 17:17:16 -04:00
Tyler Stiene cc387ba3b9 sleepct and tests 2021-04-18 00:30:27 -04:00
Tyler Stiene bad460e57c tickerct 2021-04-15 00:43:12 -04:00
Tyler Stiene b4a1a793a7 chore: fix image 2021-04-12 02:02:34 -04:00
Tyler Stiene 9fb5dc5af5 chore: switch to png 2021-04-12 02:01:04 -04:00
Tyler Stiene 87f1e1a87e chore: audio-flow.svg 2021-04-12 01:44:19 -04:00
Tyler Stiene a18df28213 chore: doc update for mumble serve issue closes #17 2021-04-10 16:19:02 -04:00
Tyler Stiene 026cdff797 add configurable buffer for mumble -> discord stream fixes #20
add silence to -> discord streams as suggested by disocrd docs closes #11
2021-04-10 15:34:02 -04:00
Tyler Stiene dcc3fd48b4 correct debug message and add timing 2021-04-08 01:14:24 -04:00
Tyler Stiene 910eaa5954 speaking debug messages 2021-04-08 00:49:49 -04:00
Tyler Stiene 480fa533a2 fix race data related to reading bridge state 2021-04-07 01:24:17 -04:00
Tyler Stiene 177553f3a4 address static check suggestions 2021-04-06 22:34:38 -04:00
Tyler Stiene a8f2574370 update to discordgo v0.23.2 2021-04-03 01:32:04 -04:00
Tyler Stiene 66a778b350 Merge branch 'main' of github.com:Stieneee/mumble-discord-bridge into main 2021-03-09 00:06:00 -05:00
Tyler Stiene 1ed35f6a39 chore: doc update 2021-03-09 00:05:04 -05:00
Tyler Stiene f3ba7a6121
Merge pull request #19 from 2xsaiko/client-cert
Allow using a Mumble client certificate
2021-03-08 22:17:00 -05:00
2xsaiko 3d9f11ee56 Cleanup 2021-03-05 20:15:17 +01:00
2xsaiko 80d6b9f8e6 Apparently this uses tabs 2021-03-05 20:12:34 +01:00
2xsaiko 83e33b85bc Add option to specify client certificate 2021-02-25 22:22:06 +01:00
Tyler Stiene 3267dc5f2d
Merge pull request #18 from stryan/better-manual
Better manual
2021-02-13 21:04:16 -05:00
stryan e85f521cb3 kill bridge only when connected 2021-02-11 14:37:34 -05:00
stryan aac90dd113 give feedback on commands 2021-02-09 18:04:11 -05:00
stryan 027d970c11 store channelid in state so other functions can use it 2021-02-09 17:46:24 -05:00
stryan 8e5bb4b479 announce auto status change, respect what voice channel used for manual link 2021-02-09 17:46:24 -05:00
Tyler Stiene 16a66719e1
Merge pull request #15 from stryan/new-config
support nested mumble channel, add debug toggle
2021-02-08 22:17:32 -05:00
Tyler Stiene 77719237f8
Merge pull request #13 from jorgror/patch-1
Info from issue #12
2021-02-08 22:05:50 -05:00
stryan 0a2a61b7df add debug, update doc 2021-02-08 13:25:48 -05:00
stryan bcf0b30d1e use string slice for mumble channel 2021-02-08 13:16:09 -05:00
jorgror 25b7699466
Removed extra lines 2021-02-07 18:57:19 +01:00
jorgror e89cd0fff7
Info from issue #12 2021-02-07 18:53:54 +01:00
31 changed files with 3014 additions and 686 deletions

33
.github/workflows/build-docker.yml vendored Normal file
View File

@ -0,0 +1,33 @@
name: 'Docker'
on:
create:
tags:
- v*
jobs:
docker-build-push:
runs-on: ubuntu-latest
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Log in to Docker Hub
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
if: startsWith(github.ref, 'refs/tags/')
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
push: true
tags: stieneee/mumble-discord-bridge:latest,stieneee/mumble-discord-bridge:${{ github.ref_name }},ghcr.io/stieneee/mumble-discord-bridge:latest,ghcr.io/stieneee/mumble-discord-bridge:${{ github.ref_name }}

View File

@ -1,29 +1,54 @@
name: 'CI'
on:
push:
tags:
- 'v*'
branches:
- main
pull_request:
branches:
- main
jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
-
name: Checkout
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
-
name: Set up Go
- name: Install libopus-dev zip
run: sudo apt update && sudo apt-get -y install libopus-dev zip
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.15
-
name: Run GoReleaser
go-version: 1.17
- name: go-license install
run: go install github.com/google/go-licenses@latest
- name: go-license save
run: go-licenses save ./cmd/mumble-discord-bridge --force --save_path="./LICENSES" && zip -r -9 LICENSES.zip ./LICENSES
- name: Run GoReleaser Build
uses: goreleaser/goreleaser-action@v2
with:
version: latest
args: build --rm-dist --skip-validate
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Run GoReleaser Release
uses: goreleaser/goreleaser-action@v2
if: startsWith(github.ref, 'refs/tags/')
with:
version: latest
args: release --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload assets
uses: actions/upload-artifact@v2
with:
name: mdb
path: dist/*

12
.gitignore vendored
View File

@ -1,6 +1,12 @@
.env
main
mumble-discord-bridge
dist
bridge
.prof
*.prof
*.out
*.test
cert.pem
*.gob
docker-compose.yml
mdb-local
LICENSES
LICENSES.zip

View File

@ -7,7 +7,8 @@ before:
# you may remove this if you don't need go generate
# - go generate ./...
builds:
- ldflags: '-s -w -linkmode external -extldflags "-static" -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser'
- main: ./cmd/mumble-discord-bridge
ldflags: '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser'
env:
- CGO_ENABLED=1
flags:
@ -18,12 +19,37 @@ builds:
# - darwin
goarch:
- amd64
# dockers:
# - goos: linux
# goarch: amd64
# image_templates:
# - "ghcr.io/stieneee/mumble-discord-bridge:latest"
# - "ghcr.io/stieneee/mumble-discord-bridge:{{ .Tag }}"
# - "stieneee/mumble-discord-bridge:latest"
# - "stieneee/mumble-discord-bridge:{{ .Tag }}"
# skip_push: "false"
# dockerfile: Dockerfile
# use: docker
# build_flag_templates:
# - "--pull"
# - "--label=org.opencontainers.image.created={{.Date}}"
# - "--label=org.opencontainers.image.title={{.ProjectName}}"
# - "--label=org.opencontainers.image.revision={{.FullCommit}}"
# - "--label=org.opencontainers.image.version={{.Version}}"
# - "--platform=linux/amd64"
# push_flags:
# - --tls-verify=false
# extra_files:
# - config.yml
archives:
# archives:
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ .Tag }}-next"
release:
extra_files:
- glob: "./LICENSES.zip"
changelog:
sort: asc
filters:

View File

@ -2,18 +2,28 @@
# Stage 1
FROM golang:1.15 as builder
FROM golang:1.17 as builder
WORKDIR /go/src/app
COPY . .
RUN curl -sfL https://install.goreleaser.com/github.com/goreleaser/goreleaser.sh | sh
RUN ./bin/goreleaser build --skip-validate
RUN apt update && apt install -y libopus-dev
RUN go install github.com/goreleaser/goreleaser@latest
RUN go install github.com/google/go-licenses@latest
RUN goreleaser build --skip-validate
RUN go-licenses save ./cmd/mumble-discord-bridge --force --save_path="./dist/LICENSES"
# Stage 2
FROM alpine:latest as static
FROM alpine:latest as final
WORKDIR /opt/
RUN apk add opus
RUN mkdir /lib64 && ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2
COPY --from=builder /go/src/app/dist/LICENSES .
COPY --from=builder /go/src/app/dist/mumble-discord-bridge_linux_amd64/mumble-discord-bridge .
# FROM ubuntu:latest as final
# WORKDIR /opt/
# RUN apt update && apt install -y libopus0 ca-certificates && apt clean
# COPY --from=builder /go/src/app/dist/mumble-discord-bridge_linux_amd64/mumble-discord-bridge .
# Entry Point
CMD ["/opt/mumble-discord-bridge"]

View File

@ -1,22 +1,49 @@
GOFILES=main.go mumble.go discord.go bridge.go config.go mumble-handlers.go discord-handlers.go
GOFILES=$(shell find ./ -type f -name '*.go')
LATEST_TAG=$(shell git describe --tags `git rev-list --tags --max-count=1`)
mumble-discord-bridge: $(GOFILES)
goreleaser build --skip-validate --rm-dist
mumble-discord-bridge: $(GOFILES) .goreleaser.yml
goreleaser build --skip-validate --rm-dist --single-target --auto-snapshot
dev: $(GOFILES)
goreleaser build --skip-validate --rm-dist && sudo ./dist/mumble-discord-bridge_linux_amd64/mumble-discord-bridge
release:
rm -rf LICENSES.zip LICENSES
go-licenses save ./cmd/mumble-discord-bridge --save_path="./LICENSES"
zip -r -9 LICENSES.zip ./LICENSES
goreleaser release --rm-dist
dev-profile: $(GOFILES)
goreleaser build --skip-validate --rm-dist && sudo ./dist/mumble-discord-bridge_linux_amd64/mumble-discord-bridge -cpuprofile cpu.prof
dev: $(GOFILES) .goreleaser.yml
goreleaser build --skip-validate --rm-dist --single-target --snapshot && sudo ./dist/mumble-discord-bridge_linux_amd64/mumble-discord-bridge
dev-race: $(GOFILES) .goreleaser.yml
go run -race ./cmd/mumble-discord-bridge
dev-profile: $(GOFILES) .goreleaser.yml
goreleaser build --skip-validate --rm-dist --single-target --snapshot && sudo ./dist/mumble-discord-bridge_linux_amd64/mumble-discord-bridge -cpuprofile cpu.prof
test-chart: SHELL:=/bin/bash
test-chart:
go test ./test &
until pidof test.test; do continue; done;
psrecord --plot docs/test-cpu-memory.png $$(pidof mumble-discord-bridge.test)
docker-latest:
docker build -t stieneee/mumble-discord-bridge:latest .
docker build -t stieneee/mumble-discord-bridge:latest -t stieneee/mumble-discord-bridge:$(LATEST_TAG) -t ghcr.io/stieneee/mumble-discord-bridge:latest -t ghcr.io/stieneee/mumble-discord-bridge:$(LATEST_TAG) .
docker-latest-run:
docker run --env-file .env -it stieneee/mumble-discord-bridge:latest
docker-release:
docker push stieneee/mumble-discord-bridge:latest
docker push stieneee/mumble-discord-bridge:$(LATEST_TAG)
docker push ghcr.io/stieneee/mumble-discord-bridge:latest
docker push ghcr.io/stieneee/mumble-discord-bridge:$(LATEST_TAG)
docker-next:
docker build -t stieneee/mumble-discord-bridge:next .
docker build -t stieneee/mumble-discord-bridge:next -t ghcr.io/stieneee/mumble-discord-bridge:next .
docker push stieneee/mumble-discord-bridge:next
docker push ghcr.io/stieneee/mumble-discord-bridge:next
clean:
rm -f mumble-discord-bridge
rm -rf dist
rm -rf LICENSES.zip LICENSES
.PHONY: docker-latest docker-latest-push clean
.PHONY: release dev dev-profile dev-race test-chart docker-latest docker-latest-release docker-release docker-next clean

147
README.md
View File

@ -4,67 +4,52 @@ Mumble Discord Bridge is an open source Go application to bridge the audio betwe
It was built with the hope that people can continue to use the voice application of their choice.
## PatchCord.io
Mumble Discord Bridge can be hosted on any server or computer and ships in a Docker container for convenience.
If hosting this application yourself seem like an difficult task please consider [PatchCord.io](https://patchcord.io).
The site even offers a free tier for those who want to try out Mumble Discord Bridge.
## Usage
Several configuration variables must be set for the binary to function correctly.
All variables can be set using flags or in the environment.
The binary will also attempt to load .env file located in the working directory.
See the help documentation for all the options
```bash
Usage of ./mumble-discord-bridge:
-discord-cid string
DISCORD_CID, discord cid, required
-discord-command string
DISCORD_COMMAND, Discord command string, env alt DISCORD_COMMAND, optional, (defaults mumble-discord) (default "mumble-discord")
-discord-disable-text
DISCORD_DISABLE_TEXT, disable sending direct messages to discord, (default false)
-discord-gid string
DISCORD_GID, discord gid, required
-discord-token string
DISCORD_TOKEN, discord bot token, required
-mode string
MODE, [constant, manual, auto] determine which mode the bridge starts in, (default constant) (default "constant")
-mumble-address string
MUMBLE_ADDRESS, mumble server address, example example.com, required
-mumble-channel string
MUMBLE_CHANNEL, mumble channel to start in, optional
-mumble-disable-text
MUMBLE_DISABLE_TEXT, disable sending text to mumble, (default false)
-mumble-insecure
MUMBLE_INSECURE, mumble insecure, optional
-mumble-password string
MUMBLE_PASSWORD, mumble password, optional
-mumble-port int
MUMBLE_PORT, mumble port, (default 64738) (default 64738)
-mumble-username string
MUMBLE_USERNAME, mumble username, (default: discord) (default "Discord")
-nice
NICE, whether the bridge should automatically try to 'nice' itself, (default false)
mumble-discord-bridge --help
```
The bridge can be run with the follow modes:
```bash
```text
auto
The bridge starts up but does not connect immediately. It can be either manually linked (see below) or will join the voice channels when there's at least one person on each side.
The bridge will leave both voice channels once there is no one on either end
manual
The bridge starts up but does not connect immediately. It will join the voice channels when issued the link command and will leave with the unlink command
constant
constant (default)
The bridge starts up and immediately connects to both Discord and Mumble voice channels. It can not be controlled in this mode and quits when the program is stopped
```
In "auto" or "manual" modes, the bridge can be controlled in Discord with the following commands:
```bash
```text
!DISCORD_COMMAND link
Commands the bridge to join the Discord channel the user is in and the Mumble server
Commands the bridge to join the Discord channel the user is in and the Mumble server
!DISCORD_COMMAND unlink
Commands the bridge to leave the Discord channel the user is in and the Mumble server
Commands the bridge to leave the Discord channel the user is in and the Mumble server
!DISCORD_COMMAND refresh
Commands the bridge to unlink, then link again.
Commands the bridge to unlink, then link again.
!DISCORD_COMMAND auto
Toggle between manual and auto mode
Toggle between manual and auto mode
```
## Setup
### Creating a Discord Bot
@ -74,7 +59,34 @@ The guide below provides information on how to setup a Discord bot.
[Create a Discord Bot](https://discordpy.readthedocs.io/en/latest/discord.html)
Individual Discord servers need to invite the bot before it can connect.
Individual Discord servers need to invite the bot before it can connect.
The bot requires the following permissions:
* View Channels
* See Messages
* Read Message History
* Voice Channel Connect
* Voice Channel Speak
* Voice Channel Use Voice Activity
Permission integer 36768768.
### Finding Discord CID and GID
Discord GID is a unique ID linked to one Discord Server, also called Guild. CID is similarly a unique ID for a Discord Channel. To find these you need to set Discord into developer Mode.
[Instructions to enable Discord Developer Mode](https://discordia.me/en/developer-mode)
Then you can get the GID by right-clicking your server and selecting Copy-ID. Similarly the CID can be found right clicking the voice channel and selecting Copy ID.
### Generating Mumble Client (Optional)
Optionally you can specify a client certificate for mumble [Mumble Certificates](https://wiki.mumble.info/wiki/Mumble_Certificates)
If you don't have a client certificate, you can generate one with this command:
``` bash
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout cert.pem -out cert.pem -subj "/CN=mumble-discord-bridge"
```
### Binary
@ -110,6 +122,33 @@ docker docker run -e MUMBLE_ADDRESS=example.com -e MUMBLE_PASSWORD=optional -e D
docker stop mumble-discord-bridge && docker rm mumble-discord-bridge
```
### MDB Bridge Options
The following options can be set using environment variables or with command line options.
| Environment Option | Flag | Type | Default | Description |
|----------------------------|-----------------------------|---------|------------------|--------------------------------------------------------------------------------------------------------------------------------|
| DEBUG_LEVEL | -debug-level | int | 1 | discord debug level |
| DISCORD_CID | -discord-cid | string | "" | discord cid, required |
| DISCORD_COMMAND | -discord-command | string | "mumble-discord" | discord command string, env alt DISCORD_COMMAND, optional |
| DISCORD_DISABLE_BOT_STATUS | -discord-disable-bot-status | boolean | false | disable updating bot status |
| DISCORD_DISABLE_TEXT | -discord-disable-text | boolean | false | disable sending direct messages to discord |
| DISCORD_GID | -discord-gid | string | "" | discord gid, required |
| DISCORD_TOKEN | -discord-token | string | "" | discord bot token, required |
| MODE | -mode | string | "constant" | [constant, manual, auto] determine which mode the bridge starts in |
| MUMBLE_ADDRESS | -mumble-address | string | "" | mumble server address, example example.com, required |
| MUMBLE_CERTIFICATE | -mumble-certificate | string | "" | client certificate to use when connecting to the Mumble server |
| MUMBLE_CHANNEL | -mumble-channel | string | "" | mumble channel to start in, using '/' to separate nested channels, optional |
| MUMBLE_DISABLE_TEXT | -mumble-disable-text | boolean | false | disable sending text to mumble |
| MUMBLE_INSECURE | -mumble-insecure | boolean | false | mumble insecure, ignore ssl certificates issues |
| MUMBLE_PASSWORD | -mumble-password | string | "" | mumble password |
| MUMBLE_PORT | -mumble-port | int | 64738 | mumble port |
| MUMBLE_USERNAME | -mumble-username | string | "Discord" | mumble username |
| PROMETHEUS_ENABLE | -prometheus-enable | boolean | false | enable prometheus metrics |
| PROMETHEUS_PORT | -prometheus-port | int | 9559 | prometheus metrics port |
| TO_DISCORD_BUFFER | -to-discord-buffer | int | 50 | jitter buffer from Mumble to Discord to absorb timing issues related to network, OS and hardware quality. (Increments of 10ms) |
| TO_MUMBLE_BUFFER | -to-mumble-buffer | int | 50 | jitter buffer from Discord to Mumble to absorb timing issues related to network, OS and hardware quality. (Increments of 10ms) |****
### Mumbler Server Setting
To ensure compatibility please edit your murmur configuration file with the following
@ -127,11 +166,32 @@ A simple go build command is all that is needed.
Ensure the opus library is installed.
```bash
go build -o mumble-discord-bridge *.go
#or
make mumble-discord-bridge
go install github.com/goreleaser/goreleaser@latest
goreleaser build --skip-validate --rm-dist --single-target --auto-snapshot
```
### OpenBSD Users
OpenBSD users should consider compiling a custom kernel to use 1000 ticks for the best possible performance.
See [issue 20](https://github.com/Stieneee/mumble-discord-bridge/issues/20) for the latest discussion about this topic.
## Jitter Buffer
The bridge implements simple jitter buffers that attempt to compensate for network, OS and hardware related jitter.
These jitter buffers are configurable in both directions.
A jitter buffer will slightly the delay the transmission of audio in order to have audio packets buffered for the next time step.
The Mumble client itself includes a jitter buffer for similar reasons.
A default jitter of 50ms should be adequate for most scenarios.
A warning will be logged if short burst or audio are seen.
A single warning can be ignored multiple warnings in short time spans would suggest the need for a larger jitter buffer.
## Monitoring the Bridge (Optional)
The bridge can be started with a Prometheus metrics endpoint enabled.
The example folder contains the a docker-compose file that will spawn the bridge, Prometheus and Grafana configured to serve a single a pre-configured dashboard.
![Mumble Discord Bridge Grafana Dashboard](example/grafana-dashboard.png "Grafana Dashboard")
## Known Issues
Currently there is an issue opening the discord voice channel.
@ -141,6 +201,9 @@ Audio leveling from Discord needs to be improved.
Delays in connecting to Mumble (such as from external authentication plugins) may result in extra error messages on initial connection.
There is an issue seen with Mumble-Server (murmur) 1.3.0 in which the bridge will loose the ability to send messages client after prolonged periods of connectivity.
This issue has been appears to be resolved by murmur 1.3.4.
## License
Distributed under the MIT License. See LICENSE for more information.
@ -154,5 +217,5 @@ Please consider opening an issue to discuss features and ideas.
The project would not have been possible without:
- [gumble](https://github.com/layeh/gumble)
- [discordgo](https://github.com/bwmarrin/discordgo)
* [gumble](https://github.com/layeh/gumble)
* [discordgo](https://github.com/bwmarrin/discordgo)

View File

@ -6,35 +6,15 @@ import (
"log"
"os"
"strconv"
"layeh.com/gumble/gumble"
)
type bridgeMode int
const (
bridgeModeAuto bridgeMode = iota
bridgeModeManual
bridgeModeConstant
"strings"
)
//BridgeConfig holds configuration information set at startup
//It should not change during runtime
type BridgeConfig struct {
MumbleConfig *gumble.Config
MumbleAddr string
MumbleInsecure bool
MumbleChannel string
MumbleDisableText bool
Command string
GID string
CID string
DiscordDisableText bool
}
func lookupEnvOrString(key string, defaultVal string) string {
if val, ok := os.LookupEnv(key); ok {
return val
return strings.TrimSpace(val)
}
return defaultVal
}

View File

@ -4,18 +4,20 @@ import (
"flag"
"fmt"
"log"
"math"
"os"
"os/signal"
"runtime/pprof"
"strconv"
"strings"
"syscall"
"time"
"github.com/bwmarrin/discordgo"
"github.com/joho/godotenv"
"layeh.com/gumble/gumble"
"layeh.com/gumble/gumbleutil"
_ "layeh.com/gumble/opus"
"github.com/stieneee/gumble/gumble"
"github.com/stieneee/gumble/gumbleutil"
"github.com/stieneee/mumble-discord-bridge/internal/bridge"
)
var (
@ -38,15 +40,22 @@ func main() {
mumbleUsername := flag.String("mumble-username", lookupEnvOrString("MUMBLE_USERNAME", "Discord"), "MUMBLE_USERNAME, mumble username, (default: discord)")
mumblePassword := flag.String("mumble-password", lookupEnvOrString("MUMBLE_PASSWORD", ""), "MUMBLE_PASSWORD, mumble password, optional")
mumbleInsecure := flag.Bool("mumble-insecure", lookupEnvOrBool("MUMBLE_INSECURE", false), " MUMBLE_INSECURE, mumble insecure, optional")
mumbleChannel := flag.String("mumble-channel", lookupEnvOrString("MUMBLE_CHANNEL", ""), "MUMBLE_CHANNEL, mumble channel to start in, optional")
mumbleCertificate := flag.String("mumble-certificate", lookupEnvOrString("MUMBLE_CERTIFICATE", ""), "MUMBLE_CERTIFICATE, client certificate to use when connecting to the Mumble server")
mumbleChannel := flag.String("mumble-channel", lookupEnvOrString("MUMBLE_CHANNEL", ""), "MUMBLE_CHANNEL, mumble channel to start in, using '/' to separate nested channels, optional")
mumbleSendBuffer := flag.Int("to-mumble-buffer", lookupEnvOrInt("TO_MUMBLE_BUFFER", 50), "TO_MUMBLE_BUFFER, Jitter buffer from Discord to Mumble to absorb timing issues related to network, OS and hardware quality. (Increments of 10ms)")
mumbleDisableText := flag.Bool("mumble-disable-text", lookupEnvOrBool("MUMBLE_DISABLE_TEXT", false), "MUMBLE_DISABLE_TEXT, disable sending text to mumble, (default false)")
discordToken := flag.String("discord-token", lookupEnvOrString("DISCORD_TOKEN", ""), "DISCORD_TOKEN, discord bot token, required")
discordGID := flag.String("discord-gid", lookupEnvOrString("DISCORD_GID", ""), "DISCORD_GID, discord gid, required")
discordCID := flag.String("discord-cid", lookupEnvOrString("DISCORD_CID", ""), "DISCORD_CID, discord cid, required")
discordSendBuffer := flag.Int("to-discord-buffer", lookupEnvOrInt("TO_DISCORD_BUFFER", 50), "TO_DISCORD_BUFFER, Jitter buffer from Mumble to Discord to absorb timing issues related to network, OS and hardware quality. (Increments of 10ms)")
discordCommand := flag.String("discord-command", lookupEnvOrString("DISCORD_COMMAND", "mumble-discord"), "DISCORD_COMMAND, Discord command string, env alt DISCORD_COMMAND, optional, (defaults mumble-discord)")
discordDisableText := flag.Bool("discord-disable-text", lookupEnvOrBool("DISCORD_DISABLE_TEXT", false), "DISCORD_DISABLE_TEXT, disable sending direct messages to discord, (default false)")
discordDisableBotStatus := flag.Bool("discord-disable-bot-status", lookupEnvOrBool("DISCORD_DISABLE_BOT_STATUS", false), "DISCORD_DISABLE_BOT_STATUS, disable updating bot status, (default false)")
mode := flag.String("mode", lookupEnvOrString("MODE", "constant"), "MODE, [constant, manual, auto] determine which mode the bridge starts in, (default constant)")
nice := flag.Bool("nice", lookupEnvOrBool("NICE", false), "NICE, whether the bridge should automatically try to 'nice' itself, (default false)")
debug := flag.Int("debug-level", lookupEnvOrInt("DEBUG", 1), "DEBUG_LEVEL, Discord debug level, optional, (default 1)")
promEnable := flag.Bool("prometheus-enable", lookupEnvOrBool("PROMETHEUS_ENABLE", false), "PROMETHEUS_ENABLE, Enable prometheus metrics")
promPort := flag.Int("prometheus-port", lookupEnvOrInt("PROMETHEUS_PORT", 9559), "PROMETHEUS_PORT, Prometheus metrics port, optional, (default 9559)")
cpuprofile := flag.String("cpuprofile", "", "write cpu profile to `file`")
@ -79,6 +88,10 @@ func main() {
}
}
if *promEnable {
go bridge.StartPromServer(*promPort)
}
// Optional CPU Profiling
if *cpuprofile != "" {
f, err := os.Create(*cpuprofile)
@ -92,38 +105,61 @@ func main() {
defer pprof.StopCPUProfile()
}
// Buffer Math
if *discordSendBuffer < 10 {
*discordSendBuffer = 10
}
if *mumbleSendBuffer < 10 {
*mumbleSendBuffer = 10
}
var discordStartStreamingCount int = int(math.Round(float64(*discordSendBuffer) / 10.0))
log.Println("To Discord Jitter Buffer: ", discordStartStreamingCount*10, " ms")
var mumbleStartStreamCount int = int(math.Round(float64(*mumbleSendBuffer) / 10.0))
log.Println("To Mumble Jitter Buffer: ", mumbleStartStreamCount*10, " ms")
// BRIDGE SETUP
Bridge := &BridgeState{
BridgeConfig: &BridgeConfig{
Bridge := &bridge.BridgeState{
BridgeConfig: &bridge.BridgeConfig{
// MumbleConfig: config,
MumbleAddr: *mumbleAddr + ":" + strconv.Itoa(*mumblePort),
MumbleInsecure: *mumbleInsecure,
MumbleChannel: *mumbleChannel,
MumbleDisableText: *mumbleDisableText,
Command: *discordCommand,
GID: *discordGID,
CID: *discordCID,
DiscordDisableText: *discordDisableText,
MumbleAddr: *mumbleAddr + ":" + strconv.Itoa(*mumblePort),
MumbleInsecure: *mumbleInsecure,
MumbleCertificate: *mumbleCertificate,
MumbleChannel: strings.Split(*mumbleChannel, "/"),
MumbleStartStreamCount: mumbleStartStreamCount,
MumbleDisableText: *mumbleDisableText,
Command: *discordCommand,
GID: *discordGID,
CID: *discordCID,
DiscordStartStreamingCount: discordStartStreamingCount,
DiscordDisableText: *discordDisableText,
DiscordDisableBotStatus: *discordDisableBotStatus,
Version: version,
},
Connected: false,
DiscordUsers: make(map[string]discordUser),
DiscordUsers: make(map[string]bridge.DiscordUser),
MumbleUsers: make(map[string]bool),
}
bridge.PromApplicationStartTime.SetToCurrentTime()
// MUMBLE SETUP
Bridge.BridgeConfig.MumbleConfig = gumble.NewConfig()
Bridge.BridgeConfig.MumbleConfig.Username = *mumbleUsername
Bridge.BridgeConfig.MumbleConfig.Password = *mumblePassword
Bridge.BridgeConfig.MumbleConfig.AudioInterval = time.Millisecond * 10
Bridge.MumbleListener = &MumbleListener{
Bridge.MumbleListener = &bridge.MumbleListener{
Bridge: Bridge,
}
Bridge.BridgeConfig.MumbleConfig.Attach(gumbleutil.Listener{
Connect: Bridge.MumbleListener.mumbleConnect,
UserChange: Bridge.MumbleListener.mumbleUserChange,
Connect: Bridge.MumbleListener.MumbleConnect,
UserChange: Bridge.MumbleListener.MumbleUserChange,
// ChannelChange: Bridge.MumbleListener.MumbleChannelChange,
})
// DISCORD SETUP
@ -135,17 +171,17 @@ func main() {
return
}
Bridge.DiscordSession.LogLevel = 1
Bridge.DiscordSession.LogLevel = *debug
Bridge.DiscordSession.StateEnabled = true
Bridge.DiscordSession.Identify.Intents = discordgo.MakeIntent(discordgo.IntentsAllWithoutPrivileged)
Bridge.DiscordSession.ShouldReconnectOnError = true
// register handlers
Bridge.DiscordListener = &DiscordListener{
Bridge.DiscordListener = &bridge.DiscordListener{
Bridge: Bridge,
}
Bridge.DiscordSession.AddHandler(Bridge.DiscordListener.messageCreate)
Bridge.DiscordSession.AddHandler(Bridge.DiscordListener.guildCreate)
Bridge.DiscordSession.AddHandler(Bridge.DiscordListener.voiceUpdate)
Bridge.DiscordSession.AddHandler(Bridge.DiscordListener.MessageCreate)
Bridge.DiscordSession.AddHandler(Bridge.DiscordListener.GuildCreate)
Bridge.DiscordSession.AddHandler(Bridge.DiscordListener.VoiceUpdate)
// Open Discord websocket
err = Bridge.DiscordSession.Open()
@ -162,18 +198,22 @@ func main() {
case "auto":
log.Println("bridge starting in automatic mode")
Bridge.AutoChanDie = make(chan bool)
Bridge.Mode = bridgeModeAuto
Bridge.Mode = bridge.BridgeModeAuto
Bridge.DiscordChannelID = Bridge.BridgeConfig.CID
go Bridge.AutoBridge()
case "manual":
log.Println("bridge starting in manual mode")
Bridge.Mode = bridgeModeManual
Bridge.Mode = bridge.BridgeModeManual
case "constant":
log.Println("bridge starting in constant mode")
Bridge.Mode = bridgeModeConstant
Bridge.Mode = bridge.BridgeModeConstant
Bridge.DiscordChannelID = Bridge.BridgeConfig.CID
go func() {
for {
Bridge.startBridge()
log.Println("Bridge died. Restarting")
Bridge.StartBridge()
log.Println("Bridge died")
time.Sleep(5 * time.Second)
log.Println("Restarting")
}
}()
default:
@ -181,20 +221,25 @@ func main() {
log.Fatalln("invalid bridge mode set")
}
go Bridge.discordStatusUpdate()
go Bridge.DiscordStatusUpdate()
// Shutdown on OS signal
sc := make(chan os.Signal, 1)
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill)
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
<-sc
// Signal the bridge to exit cleanly
Bridge.BridgeDie <- true
log.Println("OS Signal. Bot shutting down")
time.AfterFunc(30*time.Second, func() {
os.Exit(99)
})
// Wait or the bridge to exit cleanly
Bridge.BridgeMutex.Lock()
if Bridge.Connected {
//TODO BridgeDie occasionally panics on send to closed channel
Bridge.BridgeDie <- true
Bridge.WaitExit.Wait()
}
Bridge.BridgeMutex.Unlock()
}

View File

@ -1,261 +0,0 @@
package main
import (
"context"
"fmt"
"log"
"sync"
"time"
"github.com/bwmarrin/discordgo"
"layeh.com/gopus"
"layeh.com/gumble/gumble"
_ "layeh.com/gumble/opus"
)
type fromDiscord struct {
decoder *gopus.Decoder
pcm chan []int16
streaming bool
}
// DiscordDuplex Handle discord voice stream
type DiscordDuplex struct {
Bridge *BridgeState
discordMutex sync.Mutex
discordMixerMutex sync.Mutex
fromDiscordMap map[uint32]fromDiscord
}
// OnError gets called by dgvoice when an error is encountered.
// By default logs to STDERR
var OnError = func(str string, err error) {
prefix := "dgVoice: " + str
if err != nil {
log.Println(prefix + ": " + err.Error())
} else {
log.Println(prefix)
}
}
// SendPCM will receive on the provied channel encode
// received PCM data into Opus then send that to Discordgo
func (dd *DiscordDuplex) discordSendPCM(ctx context.Context, wg *sync.WaitGroup, cancel context.CancelFunc, pcm <-chan []int16) {
const channels int = 1
const frameRate int = 48000 // audio sampling rate
const frameSize int = 960 // uint16 size of each audio frame
const maxBytes int = (frameSize * 2) * 2 // max size of opus data
streaming := false
opusEncoder, err := gopus.NewEncoder(frameRate, channels, gopus.Audio)
if err != nil {
OnError("NewEncoder Error", err)
panic(err)
}
ticker := time.NewTicker(20 * time.Millisecond)
lastReady := true
var readyTimeout *time.Timer
wg.Add(1)
for {
select {
case <-ctx.Done():
wg.Done()
return
default:
}
<-ticker.C
if len(pcm) > 1 {
if !streaming {
dd.Bridge.DiscordVoice.Speaking(true)
streaming = true
}
r1 := <-pcm
r2 := <-pcm
// try encoding pcm frame with Opus
opus, err := opusEncoder.Encode(append(r1, r2...), frameSize, maxBytes)
if err != nil {
OnError("Encoding Error", err)
continue
}
if dd.Bridge.DiscordVoice.Ready == false || dd.Bridge.DiscordVoice.OpusSend == nil {
if lastReady == true {
OnError(fmt.Sprintf("Discordgo not ready for opus packets. %+v : %+v", dd.Bridge.DiscordVoice.Ready, dd.Bridge.DiscordVoice.OpusSend), nil)
readyTimeout = time.AfterFunc(30*time.Second, func() {
log.Println("set ready timeout")
cancel()
})
lastReady = false
}
continue
} else if lastReady == false {
fmt.Println("Discordgo ready to send opus packets")
lastReady = true
readyTimeout.Stop()
}
dd.Bridge.DiscordVoice.OpusSend <- opus
} else {
if streaming {
dd.Bridge.DiscordVoice.Speaking(false)
streaming = false
}
}
}
}
// ReceivePCM will receive on the the Discordgo OpusRecv channel and decode
// the opus audio into PCM then send it on the provided channel.
func (dd *DiscordDuplex) discordReceivePCM(ctx context.Context, wg *sync.WaitGroup, cancel context.CancelFunc) {
var err error
lastReady := true
var readyTimeout *time.Timer
wg.Add(1)
for {
if dd.Bridge.DiscordVoice.Ready == false || dd.Bridge.DiscordVoice.OpusRecv == nil {
if lastReady == true {
OnError(fmt.Sprintf("Discordgo not to receive opus packets. %+v : %+v", dd.Bridge.DiscordVoice.Ready, dd.Bridge.DiscordVoice.OpusSend), nil)
readyTimeout = time.AfterFunc(30*time.Second, func() {
log.Println("set ready timeout")
cancel()
})
lastReady = false
}
continue
} else if lastReady == false {
fmt.Println("Discordgo ready to receive packets")
lastReady = true
readyTimeout.Stop()
}
var ok bool
var p *discordgo.Packet
select {
case <-ctx.Done():
wg.Done()
return
case p, ok = <-dd.Bridge.DiscordVoice.OpusRecv:
}
if !ok {
log.Println("Opus not ok")
continue
}
dd.discordMutex.Lock()
_, ok = dd.fromDiscordMap[p.SSRC]
dd.discordMutex.Unlock()
if !ok {
newStream := fromDiscord{}
newStream.pcm = make(chan []int16, 100)
newStream.streaming = false
newStream.decoder, err = gopus.NewDecoder(48000, 1)
if err != nil {
OnError("error creating opus decoder", err)
continue
}
dd.discordMutex.Lock()
dd.fromDiscordMap[p.SSRC] = newStream
dd.discordMutex.Unlock()
}
dd.discordMutex.Lock()
p.PCM, err = dd.fromDiscordMap[p.SSRC].decoder.Decode(p.Opus, 960, false)
dd.discordMutex.Unlock()
if err != nil {
OnError("Error decoding opus data", err)
continue
}
if len(p.PCM) != 960 {
log.Println("Opus size error")
continue
}
dd.discordMutex.Lock()
select {
case dd.fromDiscordMap[p.SSRC].pcm <- p.PCM[0:480]:
default:
log.Println("fromDiscordMap buffer full. Dropping packet")
dd.discordMutex.Unlock()
continue
}
select {
case dd.fromDiscordMap[p.SSRC].pcm <- p.PCM[480:960]:
default:
log.Println("fromDiscordMap buffer full. Dropping packet")
}
dd.discordMutex.Unlock()
}
}
func (dd *DiscordDuplex) fromDiscordMixer(ctx context.Context, wg *sync.WaitGroup, toMumble chan<- gumble.AudioBuffer) {
ticker := time.NewTicker(10 * time.Millisecond)
sendAudio := false
wg.Add(1)
for {
select {
case <-ctx.Done():
wg.Done()
return
case <-ticker.C:
}
dd.discordMutex.Lock()
sendAudio = false
internalMixerArr := make([][]int16, 0)
// Work through each channel
for i := range dd.fromDiscordMap {
if len(dd.fromDiscordMap[i].pcm) > 0 {
sendAudio = true
if dd.fromDiscordMap[i].streaming == false {
x := dd.fromDiscordMap[i]
x.streaming = true
dd.fromDiscordMap[i] = x
}
x1 := (<-dd.fromDiscordMap[i].pcm)
internalMixerArr = append(internalMixerArr, x1)
} else {
if dd.fromDiscordMap[i].streaming == true {
x := dd.fromDiscordMap[i]
x.streaming = false
dd.fromDiscordMap[i] = x
}
}
}
dd.discordMutex.Unlock()
outBuf := make([]int16, 480)
for i := 0; i < len(outBuf); i++ {
for j := 0; j < len(internalMixerArr); j++ {
outBuf[i] += (internalMixerArr[j])[i]
}
}
if sendAudio {
select {
case toMumble <- outBuf:
default:
log.Println("toMumble buffer full. Dropping packet")
}
}
}
}

BIN
docs/audio-flow.drawio.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

BIN
docs/test-cpu-memory.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -1,3 +1,6 @@
# This a basic docker-compose file to run an instance an instance of Mumble-Discord-Bridge
# docker-compose -f ./docker-compose.yml up -d
version: "3"
services:

View File

@ -0,0 +1,69 @@
# This docker compose file contians an exmaple of staring Mumble-Discord-Bridge with Prometheus and Grafana
# The monitoring folder is need to provide the nesscary default configs for Promethus and Grafana
# Prometheus port 9090
# Grafana port 3030
version: '3.8'
volumes:
prometheus_data: {}
grafana_data: {}
services:
services:
mumble-discord-bridge:
image: stieneee/mumble-discord-bridge
restart: unless-stopped
networks:
- mdb
environment:
- MUMBLE_ADDRESS=example.com"
- MUMBLE_USERNAME=discord-bridge
- MUMBLE_PASSWORD=password
- DISCORD_TOKEN=token
- DISCORD_GID=gid
- DISCORD_CID=cid
prometheus:
image: prom/prometheus:latest
container_name: prometheus
volumes:
- ./prometheus:/etc/prometheus
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--web.console.libraries=/etc/prometheus/console_libraries'
- '--web.console.templates=/etc/prometheus/consoles'
- '--storage.tsdb.retention.time=200h'
- '--web.enable-lifecycle'
restart: unless-stopped
expose:
- 9090
ports:
- 9090:9090
depends_on:
- mumble-discord-bridge
grafana:
image: grafana/grafana:latest
container_name: grafana
volumes:
# - grafana_data:/var/lib/grafana
- ./grafana/provisioning/dashboards:/etc/grafana/provisioning/dashboards
- ./grafana/provisioning/datasources:/etc/grafana/provisioning/datasources
environment:
# - GF_SECURITY_ADMIN_USER=${ADMIN_USER:-admin}
# - GF_SECURITY_ADMIN_PASSWORD=${ADMIN_PASSWORD:-admin}
- GF_USERS_ALLOW_SIGN_UP=false
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_NAME=Main Org.
- GF_AUTH_ANONYMOUS_ORG_ROLE=Editor
restart: unless-stopped
expose:
- 3000
ports:
- 3030:3000
depends_on:
- prometheus

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

View File

@ -0,0 +1,24 @@
apiVersion: 1
providers:
# <string> an unique provider name. Required
- name: 'MDB'
# <int> Org id. Default to 1
orgId: 1
# <string> name of the dashboard folder.
folder: ''
# <string> folder UID. will be automatically generated if not specified
folderUid: ''
# <string> provider type. Default to 'file'
type: file
# <bool> disable dashboard deletion
disableDeletion: false
# <int> how often Grafana will scan for changed dashboards
updateIntervalSeconds: 30
# <bool> allow updating provisioned dashboards from the UI
allowUiUpdates: true
options:
# <string, required> path to dashboard files on disk. Required when using the 'file' type
path: /etc/grafana/provisioning/dashboards
# <bool> use folder names from filesystem to create folders in Grafana
foldersFromFilesStructure: false

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
orgId: 1
url: http://prometheus:9090
basicAuth: false
isDefault: true
editable: true

View File

@ -0,0 +1,12 @@
global:
scrape_interval: 15s
evaluation_interval: 15s
# A scrape configuration containing exactly one endpoint to scrape.
scrape_configs:
- job_name: 'mdb'
scrape_interval: 1s
static_configs:
- targets: [
'mumble-discord-bridge:9559',
]

12
go.mod
View File

@ -3,13 +3,11 @@ module github.com/stieneee/mumble-discord-bridge
go 1.15
require (
github.com/bwmarrin/discordgo v0.22.0
github.com/golang/protobuf v1.4.3 // indirect
github.com/gorilla/websocket v1.4.2 // indirect
github.com/bwmarrin/discordgo v0.23.3-0.20210512035133-7d7206b01bb5
github.com/joho/godotenv v1.3.0
github.com/prometheus/client_golang v1.11.0
github.com/stieneee/gopus v0.0.0-20210424193312-6d10f6090335
github.com/stieneee/gumble v0.0.0-20210424210604-732f48b5e0de
github.com/stieneee/tickerct v0.0.0-20210420020607-d1b092aa40e9
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect
golang.org/x/sys v0.0.0-20210108172913-0df2131ae363 // indirect
google.golang.org/protobuf v1.25.0 // indirect
layeh.com/gopus v0.0.0-20161224163843-0ebf989153aa
layeh.com/gumble v0.0.0-20200818122324-146f9205029b
)

179
go.sum
View File

@ -1,17 +1,29 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/bwmarrin/discordgo v0.22.0 h1:uBxY1HmlVCsW1IuaPjpCGT6A2DBwRn0nvOguQIxDdFM=
github.com/bwmarrin/discordgo v0.22.0/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/dchote/go-openal v0.0.0-20171116030048-f4a9a141d372 h1:tz3KnXWtRZR0RWOfcMNOw+HHezWLQa7vfSOWTtKjchI=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bwmarrin/discordgo v0.23.3-0.20210512035133-7d7206b01bb5 h1:VtiZMSjY2N6XpM1luSchBVX76QURpS0HA7BffVuHOCo=
github.com/bwmarrin/discordgo v0.23.3-0.20210512035133-7d7206b01bb5/go.mod h1:OMKxbTmkKofBjBi4/yidO3ItxbJ6PUfEUkjchM4En8c=
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dchote/go-openal v0.0.0-20171116030048-f4a9a141d372/go.mod h1:74z+CYu2/mx4N+mcIS/rsvfAxBPBV9uv8zRAnwyFkdI=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
@ -19,76 +31,129 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
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/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ=
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/stieneee/gopus v0.0.0-20210424193312-6d10f6090335 h1:yzwz6AqGKysli5du4CrQ48BMGUCSkrl7V7Kbo9VaG8w=
github.com/stieneee/gopus v0.0.0-20210424193312-6d10f6090335/go.mod h1:tAKYr3fSBJGold7c9DMPlhupn9oy8hTgl3cZ0hoyRQs=
github.com/stieneee/gumble v0.0.0-20210424210604-732f48b5e0de h1:4dWOeXRnba4jHVa3KuWf7i/GOIAlBMR3euVTUXOey2I=
github.com/stieneee/gumble v0.0.0-20210424210604-732f48b5e0de/go.mod h1:hVIsmrlrudlx2HJbsDkIZI4crkv6NHSau0ldEWbQI/Y=
github.com/stieneee/tickerct v0.0.0-20210420020607-d1b092aa40e9 h1:0l2H6Oj6JHMmkqm9xaBMQA5MOGhPT+Nn/thlTUcD9Iw=
github.com/stieneee/tickerct v0.0.0-20210420020607-d1b092aa40e9/go.mod h1:54+oZlabriEpT52rPAjAeEWUFgYqv325LrS3hNxHGFE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210108172913-0df2131ae363 h1:wHn06sgWHMO1VsQ8F+KzDJx/JzqfsNLnc+oEi07qD7s=
golang.org/x/sys v0.0.0-20210108172913-0df2131ae363/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 h1:JWgyZ1qgdTaF3N3oxC+MdTV7qvEEgHo3otj+HB5CM7Q=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
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=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
layeh.com/gopus v0.0.0-20161224163843-0ebf989153aa h1:WNU4LYsgD2UHxgKgB36mL6iMAMOvr127alafSlgBbiA=
layeh.com/gopus v0.0.0-20161224163843-0ebf989153aa/go.mod h1:AOef7vHz0+v4sWwJnr0jSyHiX/1NgsMoaxl+rEPz/I0=
layeh.com/gumble v0.0.0-20200818122324-146f9205029b h1:Kne6wkHqbqrygRsqs5XUNhSs84DFG5TYMeCkCbM56sY=
layeh.com/gumble v0.0.0-20200818122324-146f9205029b/go.mod h1:tWPVA9ZAfImNwabjcd9uDE+Mtz0Hfs7a7G3vxrnrwyc=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -1,4 +1,4 @@
package main
package bridge
import (
"context"
@ -6,20 +6,46 @@ import (
"fmt"
"log"
"net"
"os"
"strconv"
"sync"
"time"
"github.com/bwmarrin/discordgo"
"layeh.com/gumble/gumble"
"github.com/stieneee/gumble/gumble"
)
type discordUser struct {
type DiscordUser struct {
username string
seen bool
dm *discordgo.Channel
}
type BridgeMode int
const (
BridgeModeAuto BridgeMode = iota
BridgeModeManual
BridgeModeConstant
)
type BridgeConfig struct {
MumbleConfig *gumble.Config
MumbleAddr string
MumbleInsecure bool
MumbleCertificate string
MumbleChannel []string
MumbleStartStreamCount int
MumbleDisableText bool
Command string
GID string
CID string
DiscordStartStreamingCount int
DiscordDisableText bool
DiscordDisableBotStatus bool
Version string
}
//BridgeState manages dynamic information about the bridge during runtime
type BridgeState struct {
// The configuration data for this bridge
@ -34,11 +60,14 @@ type BridgeState struct {
// Wait for bridge to exit cleanly
WaitExit *sync.WaitGroup
// Bridge State Mutex
BridgeMutex sync.Mutex
// Bridge connection
Connected bool
// The bridge mode constant, auto, manual. Default is constant.
Mode bridgeMode
Mode BridgeMode
// Discord session. This is created and outside the bridge state
DiscordSession *discordgo.Session
@ -50,7 +79,7 @@ type BridgeState struct {
MumbleClient *gumble.Client
// Map of Discord users tracked by this bridge.
DiscordUsers map[string]discordUser
DiscordUsers map[string]DiscordUser
DiscordUsersMutex sync.Mutex
// Map of Mumble users tracked by this bridge
@ -70,10 +99,13 @@ type BridgeState struct {
// Mumble Duplex and Event Listener
MumbleStream *MumbleDuplex
MumbleListener *MumbleListener
// Discord Voice channel to join
DiscordChannelID string
}
// startBridge established the voice connection
func (b *BridgeState) startBridge() {
func (b *BridgeState) StartBridge() {
b.lock.Lock()
defer b.lock.Unlock()
@ -88,9 +120,17 @@ func (b *BridgeState) startBridge() {
var err error
promBridgeStarts.Inc()
promBridgeStartTime.SetToCurrentTime()
// DISCORD Connect Voice
log.Println("Attempting to join Discord voice channel")
b.DiscordVoice, err = b.DiscordSession.ChannelVoiceJoin(b.BridgeConfig.GID, b.BridgeConfig.CID, false, false)
if b.DiscordChannelID == "" {
log.Println("Tried to start bridge but no Discord channel specified")
return
}
b.DiscordVoice, err = b.DiscordSession.ChannelVoiceJoin(b.BridgeConfig.GID, b.DiscordChannelID, false, false)
if err != nil {
log.Println(err)
b.DiscordVoice.Disconnect()
@ -102,7 +142,7 @@ func (b *BridgeState) startBridge() {
// MUMBLE Connect
b.MumbleStream = &MumbleDuplex{}
b.MumbleStream = NewMumbleDuplex()
det := b.BridgeConfig.MumbleConfig.AudioListeners.Attach(b.MumbleStream)
defer det.Detach()
@ -111,6 +151,16 @@ func (b *BridgeState) startBridge() {
tlsConfig.InsecureSkipVerify = true
}
if b.BridgeConfig.MumbleCertificate != "" {
keyFile := b.BridgeConfig.MumbleCertificate
if certificate, err := tls.LoadX509KeyPair(keyFile, keyFile); err != nil {
fmt.Fprintf(os.Stderr, "%s: %s\n", os.Args[0], err)
os.Exit(1)
} else {
tlsConfig.Certificates = append(tlsConfig.Certificates, certificate)
}
}
log.Println("Attempting to join Mumble")
b.MumbleClient, err = gumble.DialWithDialer(new(net.Dialer), b.BridgeConfig.MumbleAddr, b.BridgeConfig.MumbleConfig, &tlsConfig)
@ -131,26 +181,40 @@ func (b *BridgeState) startBridge() {
defer close(toDiscord)
defer close(toMumble)
// From Discord
b.DiscordStream = NewDiscordDuplex(b)
// Start Passing Between
// From Mumble
go b.MumbleStream.fromMumbleMixer(ctx, &wg, toDiscord)
wg.Add(1)
go func() {
defer wg.Done()
b.MumbleStream.fromMumbleMixer(ctx, cancel, toDiscord)
}()
// From Discord
b.DiscordStream = &DiscordDuplex{
Bridge: b,
fromDiscordMap: make(map[uint32]fromDiscord),
}
go b.DiscordStream.discordReceivePCM(ctx, &wg, cancel)
go b.DiscordStream.fromDiscordMixer(ctx, &wg, toMumble)
wg.Add(1)
go func() {
defer wg.Done()
b.DiscordStream.discordReceivePCM(ctx, cancel)
}()
wg.Add(1)
go func() {
defer wg.Done()
b.DiscordStream.fromDiscordMixer(ctx, toMumble)
}()
// To Discord
go b.DiscordStream.discordSendPCM(ctx, &wg, cancel, toDiscord)
// Monitor Mumble
wg.Add(1)
go func() {
wg.Add(1)
defer wg.Done()
b.DiscordStream.discordSendPCM(ctx, cancel, toDiscord)
}()
// Monitor
wg.Add(1)
go func() {
defer wg.Done()
ticker := time.NewTicker(500 * time.Millisecond)
for {
select {
@ -164,13 +228,14 @@ func (b *BridgeState) startBridge() {
cancel()
}
case <-ctx.Done():
wg.Done()
return
}
}
}()
b.BridgeMutex.Lock()
b.Connected = true
b.BridgeMutex.Unlock()
// Hold until cancelled or external die request
select {
@ -181,16 +246,19 @@ func (b *BridgeState) startBridge() {
cancel()
}
b.BridgeMutex.Lock()
b.Connected = false
b.BridgeMutex.Unlock()
wg.Wait()
log.Println("Terminating Bridge")
b.MumbleUsersMutex.Lock()
b.MumbleUsers = make(map[string]bool)
b.MumbleUsersMutex.Unlock()
b.DiscordUsers = make(map[string]discordUser)
b.DiscordUsers = make(map[string]DiscordUser)
}
func (b *BridgeState) discordStatusUpdate() {
func (b *BridgeState) DiscordStatusUpdate() {
m, _ := time.ParseDuration("30s")
for {
time.Sleep(3 * time.Second)
@ -201,13 +269,17 @@ func (b *BridgeState) discordStatusUpdate() {
log.Printf("error pinging mumble server %v\n", err)
b.DiscordSession.UpdateListeningStatus("an error pinging mumble")
} else {
promMumblePing.Set(float64(resp.Ping.Milliseconds()))
b.MumbleUsersMutex.Lock()
b.BridgeMutex.Lock()
b.MumbleUserCount = resp.ConnectedUsers
if b.Connected {
b.MumbleUserCount = b.MumbleUserCount - 1
}
if b.MumbleUserCount == 0 {
status = "No users in Mumble"
status = "no users in Mumble"
} else {
if len(b.MumbleUsers) > 0 {
status = fmt.Sprintf("%v/%v users in Mumble\n", len(b.MumbleUsers), b.MumbleUserCount)
@ -215,9 +287,18 @@ func (b *BridgeState) discordStatusUpdate() {
status = fmt.Sprintf("%v users in Mumble\n", b.MumbleUserCount)
}
}
b.BridgeMutex.Unlock()
b.MumbleUsersMutex.Unlock()
b.DiscordSession.UpdateListeningStatus(status)
if !b.BridgeConfig.DiscordDisableBotStatus {
b.DiscordSession.UpdateListeningStatus(status)
}
}
discordHeartBeat := b.DiscordSession.LastHeartbeatAck.Sub(b.DiscordSession.LastHeartbeatSent).Milliseconds()
if discordHeartBeat > 0 {
promDiscordHeartBeat.Set(float64(discordHeartBeat))
}
}
}
@ -225,29 +306,31 @@ func (b *BridgeState) discordStatusUpdate() {
// when there is at least one user on both, starts up the bridge
// when there are no users on either side, kills the bridge
func (b *BridgeState) AutoBridge() {
log.Println("beginning auto mode")
log.Println("Beginning auto mode")
ticker := time.NewTicker(3 * time.Second)
for {
select {
case <-ticker.C:
case <-b.AutoChanDie:
log.Println("ending automode")
log.Println("Ending automode")
return
}
b.MumbleUsersMutex.Lock()
b.DiscordUsersMutex.Lock()
b.BridgeMutex.Lock()
if !b.Connected && b.MumbleUserCount > 0 && len(b.DiscordUsers) > 0 {
log.Println("users detected in mumble and discord, bridging")
go b.startBridge()
log.Println("Users detected in mumble and discord, bridging")
go b.StartBridge()
}
if b.Connected && b.MumbleUserCount == 0 && len(b.DiscordUsers) <= 1 {
log.Println("no one online, killing bridge")
log.Println("No one online, killing bridge")
b.BridgeDie <- true
}
b.BridgeMutex.Unlock()
b.MumbleUsersMutex.Unlock()
b.DiscordUsersMutex.Unlock()
}

View File

@ -1,4 +1,4 @@
package main
package bridge
import (
"fmt"
@ -15,16 +15,16 @@ type DiscordListener struct {
Bridge *BridgeState
}
func (l *DiscordListener) guildCreate(s *discordgo.Session, event *discordgo.GuildCreate) {
func (l *DiscordListener) GuildCreate(s *discordgo.Session, event *discordgo.GuildCreate) {
log.Println("CREATE event registered")
if event.ID != l.Bridge.BridgeConfig.GID {
log.Println("received GuildCreate from a guild not in config")
log.Println("Received GuildCreate from a guild not in config")
return
}
for _, vs := range event.VoiceStates {
if vs.ChannelID == l.Bridge.BridgeConfig.CID {
if vs.ChannelID == l.Bridge.DiscordChannelID {
if s.State.User.ID == vs.UserID {
// Ignore bot
continue
@ -32,7 +32,7 @@ func (l *DiscordListener) guildCreate(s *discordgo.Session, event *discordgo.Gui
u, err := s.User(vs.UserID)
if err != nil {
log.Println("error looking up username")
log.Println("Error looking up username")
}
dm, err := s.UserChannelCreate(u.ID)
@ -41,7 +41,7 @@ func (l *DiscordListener) guildCreate(s *discordgo.Session, event *discordgo.Gui
}
l.Bridge.DiscordUsersMutex.Lock()
l.Bridge.DiscordUsers[vs.UserID] = discordUser{
l.Bridge.DiscordUsers[vs.UserID] = DiscordUser{
username: u.Username,
seen: true,
dm: dm,
@ -49,21 +49,19 @@ func (l *DiscordListener) guildCreate(s *discordgo.Session, event *discordgo.Gui
l.Bridge.DiscordUsersMutex.Unlock()
// If connected to mumble inform users of Discord users
l.Bridge.BridgeMutex.Lock()
if l.Bridge.Connected && !l.Bridge.BridgeConfig.MumbleDisableText {
l.Bridge.MumbleClient.Do(func() {
l.Bridge.MumbleClient.Self.Channel.Send(fmt.Sprintf("%v has joined Discord\n", u.Username), false)
})
}
l.Bridge.BridgeMutex.Unlock()
}
}
}
func (l *DiscordListener) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
if l.Bridge.Mode == bridgeModeConstant {
return
}
func (l *DiscordListener) MessageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
// Ignore all messages created by the bot itself
if m.Author.ID == s.State.User.ID {
@ -83,12 +81,27 @@ func (l *DiscordListener) messageCreate(s *discordgo.Session, m *discordgo.Messa
return
}
prefix := "!" + l.Bridge.BridgeConfig.Command
if l.Bridge.Mode == BridgeModeConstant && strings.HasPrefix(m.Content, prefix) {
l.Bridge.DiscordSession.ChannelMessageSend(m.ChannelID, "Constant mode enabled, manual commands can not be entered")
return
}
l.Bridge.BridgeMutex.Lock()
bridgeConnected := l.Bridge.Connected
l.Bridge.BridgeMutex.Unlock()
if strings.HasPrefix(m.Content, prefix+" link") {
// Look for the message sender in that guild's current voice states.
for _, vs := range g.VoiceStates {
if bridgeConnected {
l.Bridge.DiscordSession.ChannelMessageSend(m.ChannelID, "Bridge already running, unlink first")
return
}
if vs.UserID == m.Author.ID {
log.Printf("Trying to join GID %v and VID %v\n", g.ID, vs.ChannelID)
go l.Bridge.startBridge()
l.Bridge.DiscordChannelID = vs.ChannelID
go l.Bridge.StartBridge()
return
}
}
@ -96,8 +109,12 @@ func (l *DiscordListener) messageCreate(s *discordgo.Session, m *discordgo.Messa
if strings.HasPrefix(m.Content, prefix+" unlink") {
// Look for the message sender in that guild's current voice states.
if !bridgeConnected {
l.Bridge.DiscordSession.ChannelMessageSend(m.ChannelID, "Bridge is not currently running")
return
}
for _, vs := range g.VoiceStates {
if vs.UserID == m.Author.ID {
if vs.UserID == m.Author.ID && vs.ChannelID == l.Bridge.DiscordChannelID {
log.Printf("Trying to leave GID %v and VID %v\n", g.ID, vs.ChannelID)
l.Bridge.BridgeDie <- true
return
@ -107,6 +124,10 @@ func (l *DiscordListener) messageCreate(s *discordgo.Session, m *discordgo.Messa
if strings.HasPrefix(m.Content, prefix+" refresh") {
// Look for the message sender in that guild's current voice states.
if !bridgeConnected {
l.Bridge.DiscordSession.ChannelMessageSend(m.ChannelID, "Bridge is not currently running")
return
}
for _, vs := range g.VoiceStates {
if vs.UserID == m.Author.ID {
log.Printf("Trying to refresh GID %v and VID %v\n", g.ID, vs.ChannelID)
@ -114,25 +135,28 @@ func (l *DiscordListener) messageCreate(s *discordgo.Session, m *discordgo.Messa
time.Sleep(5 * time.Second)
go l.Bridge.startBridge()
go l.Bridge.StartBridge()
return
}
}
}
if strings.HasPrefix(m.Content, prefix+" auto") {
if l.Bridge.Mode != bridgeModeAuto {
l.Bridge.Mode = bridgeModeAuto
if l.Bridge.Mode != BridgeModeAuto {
l.Bridge.DiscordSession.ChannelMessageSend(m.ChannelID, "Auto mode enabled")
l.Bridge.Mode = BridgeModeAuto
l.Bridge.DiscordChannelID = l.Bridge.BridgeConfig.CID
l.Bridge.AutoChanDie = make(chan bool)
go l.Bridge.AutoBridge()
} else {
l.Bridge.DiscordSession.ChannelMessageSend(m.ChannelID, "Auto mode disabled")
l.Bridge.AutoChanDie <- true
l.Bridge.Mode = bridgeModeManual
l.Bridge.Mode = BridgeModeManual
}
}
}
func (l *DiscordListener) voiceUpdate(s *discordgo.Session, event *discordgo.VoiceStateUpdate) {
func (l *DiscordListener) VoiceUpdate(s *discordgo.Session, event *discordgo.VoiceStateUpdate) {
l.Bridge.DiscordUsersMutex.Lock()
defer l.Bridge.DiscordUsersMutex.Unlock()
@ -140,7 +164,7 @@ func (l *DiscordListener) voiceUpdate(s *discordgo.Session, event *discordgo.Voi
g, err := s.State.Guild(l.Bridge.BridgeConfig.GID)
if err != nil {
log.Println("error finding guild")
log.Println("Error finding guild")
panic(err)
}
@ -152,7 +176,7 @@ func (l *DiscordListener) voiceUpdate(s *discordgo.Session, event *discordgo.Voi
// Sync the channel voice states to the local discordUsersMap
for _, vs := range g.VoiceStates {
if vs.ChannelID == l.Bridge.BridgeConfig.CID {
if vs.ChannelID == l.Bridge.DiscordChannelID {
if s.State.User.ID == vs.UserID {
// Ignore bot
continue
@ -162,7 +186,7 @@ func (l *DiscordListener) voiceUpdate(s *discordgo.Session, event *discordgo.Voi
u, err := s.User(vs.UserID)
if err != nil {
log.Println("error looking up username")
log.Println("Error looking up username")
continue
}
@ -171,16 +195,18 @@ func (l *DiscordListener) voiceUpdate(s *discordgo.Session, event *discordgo.Voi
if err != nil {
log.Println("Error creating private channel for", u.Username)
}
l.Bridge.DiscordUsers[vs.UserID] = discordUser{
l.Bridge.DiscordUsers[vs.UserID] = DiscordUser{
username: u.Username,
seen: true,
dm: dm,
}
l.Bridge.BridgeMutex.Lock()
if l.Bridge.Connected && !l.Bridge.BridgeConfig.MumbleDisableText {
l.Bridge.MumbleClient.Do(func() {
l.Bridge.MumbleClient.Self.Channel.Send(fmt.Sprintf("%v has joined Discord\n", u.Username), false)
})
}
l.Bridge.BridgeMutex.Unlock()
} else {
du := l.Bridge.DiscordUsers[vs.UserID]
du.seen = true
@ -192,15 +218,21 @@ func (l *DiscordListener) voiceUpdate(s *discordgo.Session, event *discordgo.Voi
// Remove users that are no longer connected
for id := range l.Bridge.DiscordUsers {
if l.Bridge.DiscordUsers[id].seen == false {
if !l.Bridge.DiscordUsers[id].seen {
log.Println("User left Discord channel " + l.Bridge.DiscordUsers[id].username)
l.Bridge.BridgeMutex.Lock()
if l.Bridge.Connected && !l.Bridge.BridgeConfig.MumbleDisableText {
l.Bridge.MumbleClient.Do(func() {
l.Bridge.MumbleClient.Self.Channel.Send(fmt.Sprintf("%v has left Discord channel\n", l.Bridge.DiscordUsers[id].username), false)
})
}
delete(l.Bridge.DiscordUsers, id)
l.Bridge.BridgeMutex.Unlock()
}
}
l.Bridge.BridgeMutex.Lock()
promDiscordUsers.Set(float64(len(l.Bridge.DiscordUsers)))
l.Bridge.BridgeMutex.Unlock()
}
}

421
internal/bridge/discord.go Normal file
View File

@ -0,0 +1,421 @@
package bridge
import (
"context"
"fmt"
"log"
"sync"
"time"
"github.com/bwmarrin/discordgo"
"github.com/stieneee/gopus"
"github.com/stieneee/gumble/gumble"
"github.com/stieneee/mumble-discord-bridge/pkg/sleepct"
)
type fromDiscord struct {
decoder *gopus.Decoder
pcm chan []int16
receiving bool // is used to to track the assumption that we are streaming a continuos stream form discord
streaming bool // The buffer streaming is streaming out
lastSequence uint16
lastTimeStamp uint32
}
// DiscordDuplex Handle discord voice stream
type DiscordDuplex struct {
Bridge *BridgeState
discordMutex sync.Mutex
fromDiscordMap map[uint32]fromDiscord
discordSendSleepTick sleepct.SleepCT
discordReceiveSleepTick sleepct.SleepCT
}
func NewDiscordDuplex(b *BridgeState) *DiscordDuplex {
return &DiscordDuplex{
Bridge: b,
fromDiscordMap: make(map[uint32]fromDiscord),
discordSendSleepTick: sleepct.SleepCT{},
discordReceiveSleepTick: sleepct.SleepCT{},
}
}
// OnError gets called by dgvoice when an error is encountered.
// By default logs to STDERR
var OnError = func(str string, err error) {
prefix := "dgVoice: " + str
if err != nil {
log.Println(prefix + ": " + err.Error())
} else {
log.Println(prefix)
}
}
// SendPCM will receive on the provied channel encode
// received PCM data with Opus then send that to Discordgo
func (dd *DiscordDuplex) discordSendPCM(ctx context.Context, cancel context.CancelFunc, pcm <-chan []int16) {
const channels int = 1
const frameRate int = 48000 // audio sampling rate
const frameSize int = 960 // uint16 size of each audio frame
const maxBytes int = (frameSize * 2) * 2 // max size of opus data
streaming := false
opusEncoder, err := gopus.NewEncoder(frameRate, channels, gopus.Audio)
if err != nil {
OnError("NewEncoder Error", err)
panic(err)
}
// Generate Opus Silence Frame
opusSilence := []byte{0xf8, 0xff, 0xfe}
dd.discordSendSleepTick.Start(20 * time.Millisecond)
lastReady := true
var readyTimeout *time.Timer
var speakingStart time.Time
// Spy on the PCM channel to notify
// TODO determine a method to notify a paused sleepct
// pcm := make(chan []int16, 10)
// go func() {
// for {
// t, ok := <-pcmIn
// if !ok {
// close(pcm)
// return
// } else {
// dd.discordSendSleepTick.Notify()
// pcm <- t
// }
// }
// }()
internalSend := func(opus []byte) {
dd.Bridge.DiscordVoice.RWMutex.RLock()
if !dd.Bridge.DiscordVoice.Ready || dd.Bridge.DiscordVoice.OpusSend == nil {
if lastReady {
OnError(fmt.Sprintf("Discordgo not ready for opus packets. %+v : %+v", dd.Bridge.DiscordVoice.Ready, dd.Bridge.DiscordVoice.OpusSend), nil)
readyTimeout = time.AfterFunc(30*time.Second, func() {
log.Println("Debug: Set ready timeout")
cancel()
})
lastReady = false
}
} else if !lastReady {
fmt.Println("Discordgo ready to send opus packets")
lastReady = true
readyTimeout.Stop()
} else {
select {
case dd.Bridge.DiscordVoice.OpusSend <- opus:
case <-ctx.Done():
}
promDiscordSentPackets.Inc()
}
dd.Bridge.DiscordVoice.RWMutex.RUnlock()
}
defer log.Println("Stopping Discord send PCM")
for {
select {
case <-ctx.Done():
return
default:
}
// if we are not streaming try to pause
// promTimerDiscordSend.Observe(float64(dd.discordSendSleepTick.SleepNextTarget(ctx, !streaming)))
promTimerDiscordSend.Observe(float64(dd.discordSendSleepTick.SleepNextTarget(ctx, false)))
if (len(pcm) > 1 && streaming) || (len(pcm) > dd.Bridge.BridgeConfig.DiscordStartStreamingCount && !streaming) {
if !streaming {
speakingStart = time.Now()
done := make(chan bool, 1)
go func() {
// This call will prevent discordSendPCM from exiting if the discord connection is lost
dd.Bridge.DiscordVoice.Speaking(true)
done <- true
}()
select {
case <-done:
case <-time.After(5 * time.Second):
fmt.Println("Discord speaking timeout :(")
cancel()
return
case <-ctx.Done():
return
}
streaming = true
}
r1 := <-pcm
r2 := <-pcm
// try encoding pcm frame with Opus
opus, err := opusEncoder.Encode(append(r1, r2...), frameSize, maxBytes)
if err != nil {
OnError("Encoding Error", err)
continue
}
internalSend(opus)
} else {
if streaming {
// Check to see if there is a short speaking cycle.
// It is possible that short speaking cycle is the result of a short input to mumble (Not a problem). ie a quick tap of push to talk button.
// Or when timing delays are introduced via network, hardware or kernel delays (Problem).
// The problem delays result in choppy or stuttering sounds, especially when the silence frames are introduced into the opus frames below.
// Multiple short cycle delays can result in a discord rate limiter being trigger due to of multiple JSON speaking/not-speaking state changes
if time.Since(speakingStart).Milliseconds() < 50 {
log.Println("Warning: Short Mumble to Discord speaking cycle. Consider increaseing the size of the to Discord jitter buffer.")
}
// Send silence as suggested by Discord Documentation.
// We want to do this after alerting the user of possible short speaking cycles
for i := 0; i < 5; i++ {
internalSend(opusSilence)
// promTimerDiscordSend.Observe(float64(dd.discordSendSleepTick.SleepNextTarget(ctx, true)))
promTimerDiscordSend.Observe(float64(dd.discordSendSleepTick.SleepNextTarget(ctx, false)))
}
dd.Bridge.DiscordVoice.Speaking(false)
streaming = false
}
}
}
}
// ReceivePCM will receive on the the Discordgo OpusRecv channel and decode
// the opus audio into PCM then send it on the provided channel.
func (dd *DiscordDuplex) discordReceivePCM(ctx context.Context, cancel context.CancelFunc) {
var err error
lastReady := true
var readyTimeout *time.Timer
var zeros [480]int16
for i := 0; i < 480; i++ {
zeros[i] = 0
}
for {
dd.Bridge.DiscordVoice.RWMutex.RLock()
if !dd.Bridge.DiscordVoice.Ready || dd.Bridge.DiscordVoice.OpusRecv == nil {
if lastReady {
OnError(fmt.Sprintf("Discordgo not to receive opus packets. %+v : %+v", dd.Bridge.DiscordVoice.Ready, dd.Bridge.DiscordVoice.OpusSend), nil)
readyTimeout = time.AfterFunc(30*time.Second, func() {
log.Println("Debug: Set ready timeout")
cancel()
})
lastReady = false
}
continue
} else if !lastReady {
fmt.Println("Discordgo ready to receive packets")
lastReady = true
readyTimeout.Stop()
}
dd.Bridge.DiscordVoice.RWMutex.RUnlock()
var ok bool
var p *discordgo.Packet
select {
case <-ctx.Done():
log.Println("Stopping Discord receive PCM")
return
case p, ok = <-dd.Bridge.DiscordVoice.OpusRecv:
}
if !ok {
log.Println("Opus not ok")
continue
}
dd.discordMutex.Lock()
_, ok = dd.fromDiscordMap[p.SSRC]
if !ok {
newStream := fromDiscord{}
newStream.pcm = make(chan []int16, 100)
newStream.receiving = false
newStream.streaming = false
newStream.decoder, err = gopus.NewDecoder(48000, 1) // Decode into mono
if err != nil {
OnError("error creating opus decoder", err)
dd.discordMutex.Unlock()
continue
}
dd.fromDiscordMap[p.SSRC] = newStream
}
s := dd.fromDiscordMap[p.SSRC]
deltaT := int(p.Timestamp - s.lastTimeStamp)
if p.Sequence-s.lastSequence != 1 {
s.decoder.ResetState()
}
// oldReceiving := s.receiving
if !s.receiving || deltaT < 1 || deltaT > 960*10 {
// First packet assume deltaT
// fmt.Println("replacing", deltaT, 960)
deltaT = 960
s.receiving = true
}
s.lastTimeStamp = p.Timestamp
s.lastSequence = p.Sequence
dd.fromDiscordMap[p.SSRC] = s
dd.discordMutex.Unlock()
p.PCM, err = s.decoder.Decode(p.Opus, deltaT, false)
if err != nil {
OnError("Error decoding opus data", err)
continue
}
// fmt.Println(p.SSRC, p.Type, deltaT, p.Sequence, p.Sequence-s.lastSequence, oldReceiving, s.streaming, len(p.Opus), len(p.PCM))
promDiscordReceivedPackets.Inc()
// Push data into pcm channel in 10ms chunks of mono pcm data
dd.discordMutex.Lock()
for l := 0; l < len(p.PCM); l = l + 480 {
var next []int16
u := l + 480
next = p.PCM[l:u]
select {
case dd.fromDiscordMap[p.SSRC].pcm <- next:
default:
log.Println("From Discord buffer full. Dropping packet")
}
}
dd.discordMutex.Unlock()
dd.discordReceiveSleepTick.Notify()
}
}
func (dd *DiscordDuplex) fromDiscordMixer(ctx context.Context, toMumble chan<- gumble.AudioBuffer) {
mumbleSilence := gumble.AudioBuffer{}
for i := 3; i < 480; i++ {
mumbleSilence = append(mumbleSilence, 0x00)
}
var speakingStart time.Time
dd.discordReceiveSleepTick.Start(10 * time.Millisecond)
sendAudio := false
toMumbleStreaming := false
for {
select {
case <-ctx.Done():
log.Println("Stopping from Discord mixer")
return
default:
}
// if didn't send audio try to pause
// promTimerDiscordMixer.Observe(float64(dd.discordReceiveSleepTick.SleepNextTarget(ctx, !sendAudio)))
// TODO Additional pause testing
promTimerDiscordMixer.Observe(float64(dd.discordReceiveSleepTick.SleepNextTarget(ctx, false)))
dd.discordMutex.Lock()
sendAudio = false
internalMixerArr := make([][]int16, 0)
streamingCount := 0
// Work through each channel
for i := range dd.fromDiscordMap {
bufferLength := len(dd.fromDiscordMap[i].pcm)
isStreaming := dd.fromDiscordMap[i].streaming
if (bufferLength > 0 && isStreaming) || (bufferLength > dd.Bridge.BridgeConfig.MumbleStartStreamCount && !isStreaming) {
if !toMumbleStreaming {
speakingStart = time.Now()
toMumbleStreaming = true
}
sendAudio = true
if !isStreaming {
x := dd.fromDiscordMap[i]
x.streaming = true
dd.fromDiscordMap[i] = x
}
streamingCount++
x1 := (<-dd.fromDiscordMap[i].pcm)
internalMixerArr = append(internalMixerArr, x1)
} else {
if dd.fromDiscordMap[i].streaming {
x := dd.fromDiscordMap[i]
x.streaming = false
x.receiving = false // toggle this here is not optimal but there is no better location atm.
dd.fromDiscordMap[i] = x
}
}
}
promDiscordArraySize.Set(float64(len(dd.fromDiscordMap)))
promDiscordStreaming.Set(float64(streamingCount))
dd.discordMutex.Unlock()
mumbleTimeoutSend := func(outBuf []int16) {
timeout := make(chan bool, 1)
go func() {
time.Sleep(10 * time.Millisecond)
timeout <- true
}()
select {
case toMumble <- outBuf:
promSentMumblePackets.Inc()
case <-timeout:
log.Println("To Mumble timeout. Dropping packet")
promToMumbleDropped.Inc()
}
}
if sendAudio {
// Regular send mixed audio
outBuf := make([]int16, 480)
for j := 0; j < len(internalMixerArr); j++ {
for i := 0; i < len(internalMixerArr[j]); i++ {
outBuf[i] += (internalMixerArr[j])[i]
}
}
mumbleTimeoutSend(outBuf)
} else if !sendAudio && toMumbleStreaming {
// Send opus silence to mumble
// See note above about jitter buffer warning
if time.Since(speakingStart).Milliseconds() < 50 {
log.Println("Warning: Short Discord to Mumble speaking cycle. Consider increaseing the size of the to Mumble jitter buffer.", time.Since(speakingStart).Milliseconds())
}
for i := 0; i < 5; i++ {
mumbleTimeoutSend(mumbleSilence)
promTimerDiscordMixer.Observe(float64(dd.discordReceiveSleepTick.SleepNextTarget(ctx, false)))
}
toMumbleStreaming = false
}
}
}

View File

@ -0,0 +1,91 @@
package bridge
import (
"fmt"
"log"
"strings"
"time"
"github.com/stieneee/gumble/gumble"
)
// MumbleListener Handle mumble events
type MumbleListener struct {
Bridge *BridgeState
}
func (l *MumbleListener) updateUsers() {
l.Bridge.MumbleUsersMutex.Lock()
l.Bridge.MumbleUsers = make(map[string]bool)
for _, user := range l.Bridge.MumbleClient.Self.Channel.Users {
//note, this might be too slow for really really big channels?
//event listeners block while processing
//also probably bad to rebuild the set every user change.
if user.Name != l.Bridge.MumbleClient.Self.Name {
l.Bridge.MumbleUsers[user.Name] = true
}
}
promMumbleUsers.Set(float64(len(l.Bridge.MumbleUsers)))
l.Bridge.MumbleUsersMutex.Unlock()
}
func (l *MumbleListener) MumbleConnect(e *gumble.ConnectEvent) {
//join specified channel
startingChannel := e.Client.Channels.Find(l.Bridge.BridgeConfig.MumbleChannel...)
if startingChannel != nil {
e.Client.Self.Move(startingChannel)
}
// l.updateUsers() // patch below
// This is an ugly patch Mumble Client state is slow to update
time.AfterFunc(5*time.Second, func() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("Failed to mumble user list %v \n", r)
}
}()
l.updateUsers()
})
}
func (l *MumbleListener) MumbleUserChange(e *gumble.UserChangeEvent) {
l.updateUsers()
if e.Type.Has(gumble.UserChangeConnected) {
log.Println("User connected to mumble " + e.User.Name)
if !l.Bridge.BridgeConfig.MumbleDisableText {
e.User.Send("Mumble-Discord-Bridge v" + l.Bridge.BridgeConfig.Version)
// Tell the user who is connected to discord
l.Bridge.DiscordUsersMutex.Lock()
if len(l.Bridge.DiscordUsers) == 0 {
e.User.Send("No users connected to Discord")
} else {
s := "Connected to Discord: "
arr := []string{}
for u := range l.Bridge.DiscordUsers {
arr = append(arr, l.Bridge.DiscordUsers[u].username)
}
s = s + strings.Join(arr[:], ",")
e.User.Send(s)
}
l.Bridge.DiscordUsersMutex.Unlock()
}
// Send discord a notice
l.Bridge.discordSendMessageAll(e.User.Name + " has joined mumble")
}
if e.Type.Has(gumble.UserChangeDisconnected) {
l.Bridge.discordSendMessageAll(e.User.Name + " has left mumble")
log.Println("User disconnected from mumble " + e.User.Name)
}
}

143
internal/bridge/mumble.go Normal file
View File

@ -0,0 +1,143 @@
package bridge
import (
"context"
"log"
"strconv"
"sync"
"time"
"github.com/stieneee/gumble/gumble"
_ "github.com/stieneee/gumble/opus"
"github.com/stieneee/mumble-discord-bridge/pkg/sleepct"
)
// MumbleDuplex - listener and outgoing
type MumbleDuplex struct {
mutex sync.Mutex
fromMumbleArr []chan gumble.AudioBuffer
mumbleStreamingArr []bool
mumbleSleepTick sleepct.SleepCT
}
func NewMumbleDuplex() *MumbleDuplex {
return &MumbleDuplex{
fromMumbleArr: make([]chan gumble.AudioBuffer, 0),
mumbleStreamingArr: make([]bool, 0),
mumbleSleepTick: sleepct.SleepCT{},
}
}
// OnAudioStream - Spawn routines to handle incoming packets
func (m *MumbleDuplex) OnAudioStream(e *gumble.AudioStreamEvent) {
// hold a reference ot the channel in the closure
streamChan := make(chan gumble.AudioBuffer, 100)
m.mutex.Lock()
m.fromMumbleArr = append(m.fromMumbleArr, streamChan)
m.mumbleStreamingArr = append(m.mumbleStreamingArr, false)
m.mutex.Unlock()
promMumbleArraySize.Set(float64(len(m.fromMumbleArr)))
go func() {
name := e.User.Name
log.Println("New mumble audio stream", name)
for p := range e.C {
// log.Println("audio packet", p.Sender.Name, len(p.AudioBuffer))
// 480 per 10ms
for i := 0; i < len(p.AudioBuffer)/480; i++ {
streamChan <- p.AudioBuffer[480*i : 480*(i+1)]
}
promReceivedMumblePackets.Inc()
m.mumbleSleepTick.Notify()
}
log.Println("Mumble audio stream ended", name)
}()
}
func (m *MumbleDuplex) fromMumbleMixer(ctx context.Context, cancel context.CancelFunc, toDiscord chan []int16) {
m.mumbleSleepTick.Start(10 * time.Millisecond)
sendAudio := false
droppingPackets := false
droppingPacketCount := 0
for {
select {
case <-ctx.Done():
log.Println("Stopping From Mumble Mixer")
return
default:
}
promTimerMumbleMixer.Observe(float64(m.mumbleSleepTick.SleepNextTarget(ctx, false)))
m.mutex.Lock()
sendAudio = false
internalMixerArr := make([]gumble.AudioBuffer, 0)
streamingCount := 0
// Work through each channel
for i := 0; i < len(m.fromMumbleArr); i++ {
if len(m.fromMumbleArr[i]) > 0 {
sendAudio = true
if !m.mumbleStreamingArr[i] {
m.mumbleStreamingArr[i] = true
streamingCount++
// log.Println("Mumble starting", i)
}
x1 := (<-m.fromMumbleArr[i])
internalMixerArr = append(internalMixerArr, x1)
} else {
if m.mumbleStreamingArr[i] {
m.mumbleStreamingArr[i] = false
// log.Println("Mumble stopping", i)
}
}
}
m.mutex.Unlock()
promMumbleStreaming.Set(float64(streamingCount))
if sendAudio {
outBuf := make([]int16, 480)
for i := 0; i < len(outBuf); i++ {
for j := 0; j < len(internalMixerArr); j++ {
outBuf[i] += (internalMixerArr[j])[i]
}
}
promToDiscordBufferSize.Set(float64(len(toDiscord)))
select {
case toDiscord <- outBuf:
{
if droppingPackets {
log.Println("Discord buffer ok, total packets dropped " + strconv.Itoa(droppingPacketCount))
droppingPackets = false
}
}
default:
if !droppingPackets {
log.Println("Error: toDiscord buffer full. Dropping packets")
droppingPackets = true
droppingPacketCount = 0
}
droppingPacketCount++
promToDiscordDropped.Inc()
if droppingPacketCount > 250 {
log.Println("Discord Timeout")
cancel()
}
}
}
}
}

141
internal/bridge/prom.go Normal file
View File

@ -0,0 +1,141 @@
package bridge
import (
"log"
"net/http"
"strconv"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
// Bridge General
PromApplicationStartTime = promauto.NewGauge(prometheus.GaugeOpts{
Name: "mdb_bridge_start_time",
Help: "The time the application started",
})
promBridgeStarts = promauto.NewCounter(prometheus.CounterOpts{
Name: "mdb_bridge_starts_count",
Help: "The number of times the bridge start routine has been called",
})
promBridgeStartTime = promauto.NewGauge(prometheus.GaugeOpts{
Name: "mdb_bridge_starts_time",
Help: "The time the current bridge instance started",
})
// MUMBLE
promMumblePing = promauto.NewGauge(prometheus.GaugeOpts{
Name: "mdb_mumble_ping",
Help: "Mumble ping",
})
promMumbleUsers = promauto.NewGauge(prometheus.GaugeOpts{
Name: "mdb_mumble_users_gauge",
Help: "The number of connected Mumble users",
})
promReceivedMumblePackets = promauto.NewCounter(prometheus.CounterOpts{
Name: "mdb_mumble_received_count",
Help: "The count of Mumble audio packets received",
})
promSentMumblePackets = promauto.NewCounter(prometheus.CounterOpts{
Name: "mdb_mumble_sent_count",
Help: "The count of audio packets sent to mumble",
})
// promToMumbleBufferSize = promauto.NewGauge(prometheus.GaugeOpts{
// Name: "mdb_to_mumble_buffer_gauge",
// Help: "",
// })
promToMumbleDropped = promauto.NewCounter(prometheus.CounterOpts{
Name: "mdb_to_mumble_dropped",
Help: "The number of packets timeouts to mumble",
})
promMumbleArraySize = promauto.NewGauge(prometheus.GaugeOpts{
Name: "mdb_to_mumble_array_size_gauge",
Help: "The array size of mumble streams",
})
promMumbleStreaming = promauto.NewGauge(prometheus.GaugeOpts{ //SUMMARY?
Name: "mdb_mumble_streaming_gauge",
Help: "The number of active audio streams streaming audio from mumble",
})
// DISCORD
// TODO Discrod Ping
promDiscordHeartBeat = promauto.NewGauge(prometheus.GaugeOpts{
Name: "mdb_discord_latency",
Help: "Discord heartbeat latency",
})
promDiscordUsers = promauto.NewGauge(prometheus.GaugeOpts{
Name: "mdb_discord_users_gauge",
Help: "The number of Connected Discord users",
})
promDiscordReceivedPackets = promauto.NewCounter(prometheus.CounterOpts{
Name: "mdb_discord_received_count",
Help: "The number of received packets from Discord",
})
promDiscordSentPackets = promauto.NewCounter(prometheus.CounterOpts{
Name: "mdb_discord_sent_count",
Help: "The number of packets sent to Discord",
})
promToDiscordBufferSize = promauto.NewGauge(prometheus.GaugeOpts{
Name: "mdb_discord_buffer_gauge",
Help: "The buffer size for packets to Discord",
})
promToDiscordDropped = promauto.NewCounter(prometheus.CounterOpts{
Name: "mdb_to_discord_dropped",
Help: "The count of packets dropped to discord",
})
promDiscordArraySize = promauto.NewGauge(prometheus.GaugeOpts{
Name: "mdb_discord_array_size_gauge",
Help: "The discord receiving array size",
})
promDiscordStreaming = promauto.NewGauge(prometheus.GaugeOpts{
Name: "mdb_discord_streaming_gauge",
Help: "The number of active audio streams streaming from discord",
})
// Sleep Timer Performance
promTimerDiscordSend = promauto.NewHistogram(prometheus.HistogramOpts{
Name: "mdb_timer_discord_send",
Help: "Timer performance for Discord send",
Buckets: []float64{1000, 2000, 5000, 10000, 20000},
})
promTimerDiscordMixer = promauto.NewHistogram(prometheus.HistogramOpts{
Name: "mdb_timer_discord_mixer",
Help: "Timer performance for the Discord mixer",
Buckets: []float64{1000, 2000, 5000, 10000, 20000},
})
promTimerMumbleMixer = promauto.NewHistogram(prometheus.HistogramOpts{
Name: "mdb_timer_mumble_mixer",
Help: "Timer performance for the Mumble mixer",
Buckets: []float64{1000, 2000, 5000, 10000, 20000},
})
)
func StartPromServer(port int) {
log.Println("Starting Metrics Server")
http.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(":"+strconv.Itoa(port), nil)
}

View File

@ -1,74 +0,0 @@
package main
import (
"log"
"strings"
"layeh.com/gumble/gumble"
)
// MumbleListener Handle mumble events
type MumbleListener struct {
Bridge *BridgeState
}
func (l *MumbleListener) mumbleConnect(e *gumble.ConnectEvent) {
if l.Bridge.BridgeConfig.MumbleChannel != "" {
//join specified channel
startingChannel := e.Client.Channels.Find(l.Bridge.BridgeConfig.MumbleChannel)
if startingChannel != nil {
e.Client.Self.Move(startingChannel)
}
}
}
func (l *MumbleListener) mumbleUserChange(e *gumble.UserChangeEvent) {
l.Bridge.MumbleUsersMutex.Lock()
if e.Type.Has(gumble.UserChangeConnected) || e.Type.Has(gumble.UserChangeChannel) || e.Type.Has(gumble.UserChangeDisconnected) {
l.Bridge.MumbleUsers = make(map[string]bool)
for _, user := range l.Bridge.MumbleClient.Self.Channel.Users {
//note, this might be too slow for really really big channels?
//event listeners block while processing
//also probably bad to rebuild the set every user change.
if user.Name != l.Bridge.MumbleClient.Self.Name {
l.Bridge.MumbleUsers[user.Name] = true
}
}
}
l.Bridge.MumbleUsersMutex.Unlock()
if e.Type.Has(gumble.UserChangeConnected) {
log.Println("User connected to mumble " + e.User.Name)
if !l.Bridge.BridgeConfig.MumbleDisableText {
e.User.Send("Mumble-Discord-Bridge v" + version)
// Tell the user who is connected to discord
if len(l.Bridge.DiscordUsers) == 0 {
e.User.Send("No users connected to Discord")
} else {
s := "Connected to Discord: "
arr := []string{}
l.Bridge.DiscordUsersMutex.Lock()
for u := range l.Bridge.DiscordUsers {
arr = append(arr, l.Bridge.DiscordUsers[u].username)
}
s = s + strings.Join(arr[:], ",")
l.Bridge.DiscordUsersMutex.Unlock()
e.User.Send(s)
}
}
// Send discord a notice
l.Bridge.discordSendMessageAll(e.User.Name + " has joined mumble")
}
if e.Type.Has(gumble.UserChangeDisconnected) {
l.Bridge.discordSendMessageAll(e.User.Name + " has left mumble")
log.Println("User disconnected from mumble " + e.User.Name)
}
}

106
mumble.go
View File

@ -1,106 +0,0 @@
package main
import (
"context"
"log"
"sync"
"time"
"layeh.com/gumble/gumble"
_ "layeh.com/gumble/opus"
)
var mutex sync.Mutex
var fromMumbleArr []chan gumble.AudioBuffer
var mumbleStreamingArr []bool
// MumbleDuplex - listenera and outgoing
type MumbleDuplex struct{}
// OnAudioStream - Spawn routines to handle incoming packets
func (m MumbleDuplex) OnAudioStream(e *gumble.AudioStreamEvent) {
// hold a reference ot the channel in the closure
localMumbleArray := make(chan gumble.AudioBuffer, 100)
mutex.Lock()
fromMumbleArr = append(fromMumbleArr, localMumbleArray)
mumbleStreamingArr = append(mumbleStreamingArr, false)
mutex.Unlock()
go func() {
// TODO kill go routine on cleanup
log.Println("new mumble audio stream", e.User.Name)
for {
select {
case p := <-e.C:
// log.Println("audio packet", p.Sender.Name, len(p.AudioBuffer))
// 480 per 10ms
for i := 0; i < len(p.AudioBuffer)/480; i++ {
localMumbleArray <- p.AudioBuffer[480*i : 480*(i+1)]
}
}
}
}()
return
}
func (m MumbleDuplex) fromMumbleMixer(ctx context.Context, wg *sync.WaitGroup, toDiscord chan []int16) {
ticker := time.NewTicker(10 * time.Millisecond)
sendAudio := false
wg.Add(1)
for {
select {
case <-ctx.Done():
wg.Done()
return
default:
}
<-ticker.C
mutex.Lock()
sendAudio = false
internalMixerArr := make([]gumble.AudioBuffer, 0)
// Work through each channel
for i := 0; i < len(fromMumbleArr); i++ {
if len(fromMumbleArr[i]) > 0 {
sendAudio = true
if mumbleStreamingArr[i] == false {
mumbleStreamingArr[i] = true
// log.Println("mumble starting", i)
}
x1 := (<-fromMumbleArr[i])
internalMixerArr = append(internalMixerArr, x1)
} else {
if mumbleStreamingArr[i] == true {
mumbleStreamingArr[i] = false
// log.Println("mumble stopping", i)
}
}
}
mutex.Unlock()
outBuf := make([]int16, 480)
for i := 0; i < len(outBuf); i++ {
for j := 0; j < len(internalMixerArr); j++ {
outBuf[i] += (internalMixerArr[j])[i]
}
}
if sendAudio {
select {
case toDiscord <- outBuf:
default:
log.Println("toDiscord buffer full. Dropping packet")
}
}
}
}

92
pkg/sleepct/sleepct.go Normal file
View File

@ -0,0 +1,92 @@
package sleepct
import (
"context"
"fmt"
"time"
)
// SleepCT - Sleep constant time step crates a sleep based ticker.
// designed to maintain a consistent sleep/tick interval.
// The sleeper can be paused waiting to be signaled from another go routine.
// This allows for the pausing of loops that do not have work to complete
type SleepCT struct {
d time.Duration // desired duration between targets
t time.Time // last time target
resume chan bool
wake time.Time // last wake time
drift int64 // last wake drift microseconds
}
func (s *SleepCT) Start(d time.Duration) {
s.resume = make(chan bool, 2)
if s.t.IsZero() {
s.d = d
s.t = time.Now()
} else {
panic("SleepCT already started")
}
}
// Sleep to the next target duration.
// If pause it set to true will sleep the duration and wait to be notified.
// The notification channel will be cleared when the thread wakes.
// SleepNextTarget should not be call more than once concurrently.
func (s *SleepCT) SleepNextTarget(ctx context.Context, pause bool) int64 {
now := time.Now()
// if target is zero safety net
if s.t.IsZero() {
fmt.Println("SleepCT reset")
s.t = now.Add(-s.d)
}
// Sleep to Next Target
s.t = s.t.Add(s.d)
// Compute the desired sleep time to reach the target
d := time.Until(s.t)
// Sleep
time.Sleep(d)
// record the wake time
s.wake = time.Now()
s.drift = s.wake.Sub(s.t).Microseconds()
// fmt.Println(s.t.UnixMilli(), d.Milliseconds(), wake.UnixMilli(), drift, pause, len(s.resume))
// external pause control
if pause {
// don't pause if the notification channel has something
if len(s.resume) == 0 {
// fmt.Println("pause")
select {
case <-s.resume:
case <-ctx.Done():
// fmt.Println("sleepct ctx exit")
}
// if we did pause set the last sleep target to now
s.t = time.Now()
}
}
// Drain the resume channel
select {
case <-s.resume:
default:
}
// return the drift for monitoring purposes
return s.drift
}
// Notify attempts to resume a paused sleeper.
// It is safe to call notify from other processes and as often as desired.
func (s *SleepCT) Notify() {
select {
case s.resume <- true:
default:
}
}

198
test/timing_test.go Normal file
View File

@ -0,0 +1,198 @@
package main
import (
"context"
"fmt"
"math"
"math/rand"
"sort"
"sync"
"testing"
"time"
"github.com/stieneee/mumble-discord-bridge/pkg/sleepct"
"github.com/stieneee/tickerct"
)
const testCount int64 = 10000
const maxSleepInterval time.Duration = 15 * time.Millisecond
const tickerInterval time.Duration = 10 * time.Millisecond
const testDuration time.Duration = time.Duration(testCount * 10 * int64(time.Millisecond))
func testTickerBaseCase(wg *sync.WaitGroup, test *testing.T) {
wg.Add(1)
go func(interval time.Duration) {
now := time.Now()
start := now
// start the ticker
t := time.NewTicker(interval)
var i int64
for i = 0; i < testCount; i++ {
now = <-t.C
// fmt.Println(now)
}
t.Stop()
fmt.Println("Ticker (unloaded) after", testDuration, "drifts", time.Since(start)-testDuration)
wg.Done()
}(tickerInterval)
}
func TestTickerBaseCase(t *testing.T) {
wg := sync.WaitGroup{}
testTickerBaseCase(&wg, t)
wg.Wait()
}
func testTickerLoaded(wg *sync.WaitGroup) {
wg.Add(1)
go func(interval time.Duration) {
now := time.Now()
start := now
// start the ticker
t := time.NewTicker(interval)
var i int64
for i = 0; i < testCount; i++ {
if i+1 < testCount {
time.Sleep(time.Duration(float64(maxSleepInterval) * rand.Float64()))
}
now = <-t.C
// fmt.Println(now)
}
t.Stop()
fmt.Println("Ticker (loaded) after", testDuration, "drifts", time.Since(start)-testDuration)
wg.Done()
}(tickerInterval)
}
func TestTicker(t *testing.T) {
wg := sync.WaitGroup{}
testTickerLoaded(&wg)
wg.Wait()
}
func testTickerCT(wg *sync.WaitGroup) {
wg.Add(1)
go func(interval time.Duration) {
now := time.Now()
start := now
// start the ticker
t := tickerct.NewTickerCT(interval)
var i int64
for i = 0; i < testCount; i++ {
if i+1 < testCount {
time.Sleep(time.Duration(float64(maxSleepInterval) * rand.Float64()))
}
now = <-t.C
// fmt.Println(now)
}
t.Stop()
fmt.Println("TickerCT (loaded) after", testDuration, "drifts", time.Since(start)-testDuration)
wg.Done()
}(tickerInterval)
}
func TestTickerCT(t *testing.T) {
wg := sync.WaitGroup{}
testTickerCT(&wg)
wg.Wait()
}
func testSleepCT(wg *sync.WaitGroup) {
wg.Add(1)
go func(interval time.Duration) {
now := time.Now()
start := now
// start the ticker
s := sleepct.SleepCT{}
s.Start(interval)
var i int64
for i = 0; i < testCount; i++ {
if i+1 < testCount {
time.Sleep(time.Duration(float64(maxSleepInterval) * rand.Float64()))
}
s.SleepNextTarget(context.TODO(), false)
}
fmt.Println("SleepCT (loaded) after", testDuration, "drifts", time.Since(start)-testDuration)
wg.Done()
}(tickerInterval)
}
func TestSleepCT(t *testing.T) {
wg := sync.WaitGroup{}
testSleepCT(&wg)
wg.Wait()
}
func testSleepCTPause(wg *sync.WaitGroup) {
wg.Add(1)
go func(interval time.Duration) {
now := time.Now()
start := now
// start the ticker
s := sleepct.SleepCT{}
s.Start(interval)
var i int64
for i = 0; i < testCount; i++ {
if i+1 < testCount {
time.Sleep(time.Duration(float64(maxSleepInterval) * rand.Float64()))
}
s.Notify()
s.SleepNextTarget(context.TODO(), true)
}
fmt.Println("SleepCT Pause (loaded) after", testDuration, "drifts", time.Since(start)-testDuration)
wg.Done()
}(tickerInterval)
}
func TestSleepCTPause(t *testing.T) {
wg := sync.WaitGroup{}
testSleepCTPause(&wg)
wg.Wait()
}
func TestIdleJitter(t *testing.T) {
wg := sync.WaitGroup{}
const testSize = 100000
const sleepTarget = time.Millisecond
res := make([]time.Duration, testSize)
for i := 0; i < testSize; i++ {
start := time.Now()
target := start.Add(sleepTarget)
time.Sleep(sleepTarget)
res[i] = time.Since(target)
}
sort.Slice(res, func(i, j int) bool {
return res[i] < res[j]
})
var total float64 = 0
for i := 0; i < testSize; i++ {
total += float64(res[i])
}
avg := time.Duration(total / testSize)
nineFive := int64(math.Round(testSize * 0.95))
nineNine := int64(math.Round(testSize * 0.99))
nineNineNine := int64(math.Round(testSize * 0.999))
fmt.Println("IdleJitter test", testSize, sleepTarget)
fmt.Println("IdleJitter results min/avg/95/99/99.9/max", res[0], avg, res[nineFive], res[nineNine], res[nineNineNine], res[testSize-1])
wg.Wait()
}