├── .github ├── CODEOWNERS ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── build.yml │ ├── release.yml │ └── release_pkg.yml ├── .gitignore ├── .go-version ├── .goreleaser.yml ├── .mergify.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── assets ├── charts │ ├── chart.drawio │ └── chart.png └── img │ └── gopher.png ├── beaver.go ├── bin └── release.sh ├── cache └── .gitignore ├── cmd ├── api.go ├── root.go └── version.go ├── config.dist.yml ├── config.toml ├── core ├── api │ ├── channel.go │ ├── client.go │ └── config.go ├── controller │ ├── channel.go │ ├── client.go │ ├── config.go │ ├── health.go │ ├── home.go │ ├── metrics.go │ └── socket.go ├── driver │ └── redis.go ├── middleware │ ├── auth.go │ ├── correlation.go │ ├── cors.go │ ├── log.go │ └── metric.go └── util │ ├── helpers.go │ ├── map.go │ ├── token.go │ └── validator.go ├── deployment ├── .gitkeep ├── docker │ └── .gitkeep └── linux │ └── .gitkeep ├── go.mod ├── go.sum ├── renovate.json ├── techstack.md ├── techstack.yml └── web ├── static ├── .gitkeep ├── css │ └── app.css ├── img │ └── logo.png └── js │ └── beaver.js └── template └── index.tmpl /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Docs: https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners 2 | * @clivern 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: # clivern 2 | custom: clivern.com/sponsor/ 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **Development or production environment** 11 | - OS: [e.g. Ubuntu 18.04] 12 | - Go 1.13 13 | 14 | **Additional context** 15 | Add any other context about the problem here. 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Additional context** 14 | Add any other context or screenshots about the feature request here. 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | go: ['1.18', '1.19', '1.20.4'] 14 | name: Go ${{ matrix.go }} run 15 | steps: 16 | - uses: actions/checkout@v3 17 | - name: Setup go 18 | uses: actions/setup-go@v3 19 | with: 20 | go-version: ${{ matrix.go }} 21 | 22 | - name: Get dependencies 23 | run: | 24 | export PATH=${PATH}:`go env GOPATH`/bin 25 | make install_revive 26 | 27 | - name: Run make ci 28 | run: | 29 | export PATH=${PATH}:`go env GOPATH`/bin 30 | go get -t . 31 | make ci 32 | make integration 33 | git status 34 | git diff > diff.log 35 | cat diff.log 36 | git clean -fd 37 | git reset --hard 38 | make verify 39 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | goreleaser: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - 13 | name: Checkout 14 | uses: actions/checkout@v3 15 | with: 16 | fetch-depth: 0 17 | - 18 | name: Set up Go 19 | uses: actions/setup-go@v3 20 | with: 21 | go-version: 1.19 22 | - 23 | name: Run GoReleaser 24 | uses: goreleaser/goreleaser-action@v4 25 | with: 26 | version: latest 27 | args: release --rm-dist 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | -------------------------------------------------------------------------------- /.github/workflows/release_pkg.yml: -------------------------------------------------------------------------------- 1 | name: ReleasePkg 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - 13 | name: Checkout 14 | uses: actions/checkout@v3 15 | with: 16 | fetch-depth: 0 17 | - 18 | name: Set up Go 19 | uses: actions/setup-go@v3 20 | with: 21 | go-version: 1.19 22 | 23 | - name: Update checksum database 24 | run: | 25 | ./bin/release.sh 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # dist dir 15 | dist 16 | 17 | sync 18 | 19 | *.db 20 | 21 | config.prod.yml -------------------------------------------------------------------------------- /.go-version: -------------------------------------------------------------------------------- 1 | 1.20.4 2 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # This is an example goreleaser.yaml file with some sane defaults. 2 | # Make sure to check the documentation at http://goreleaser.com 3 | --- 4 | archives: 5 | - 6 | replacements: 7 | 386: i386 8 | amd64: x86_64 9 | darwin: Darwin 10 | linux: Linux 11 | windows: Windows 12 | files: 13 | - LICENSE 14 | - README.md 15 | - config.dist.yml 16 | before: 17 | hooks: 18 | - "go mod download" 19 | - "go generate ./..." 20 | builds: 21 | - 22 | env: 23 | - CGO_ENABLED=0 24 | goos: 25 | - linux 26 | - darwin 27 | - windows 28 | 29 | changelog: 30 | filters: 31 | exclude: 32 | - "^docs:" 33 | - "^test:" 34 | sort: asc 35 | checksum: 36 | name_template: checksums.txt 37 | snapshot: 38 | name_template: "{{ .Tag }}-next" 39 | project_name: beaver 40 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | --- 2 | pull_request_rules: 3 | - 4 | actions: 5 | merge: 6 | method: squash 7 | conditions: 8 | - author!=Clivern 9 | - approved-reviews-by=Clivern 10 | - label=release 11 | name: "Automatic Merge 🚀" 12 | - 13 | actions: 14 | merge: 15 | method: merge 16 | conditions: 17 | - author=Clivern 18 | - label=release 19 | name: "Automatic Merge 🚀" 20 | - 21 | actions: 22 | merge: 23 | method: squash 24 | conditions: 25 | - "author=renovate[bot]" 26 | - label=release 27 | name: "Automatic Merge for Renovate PRs 🚀" 28 | - 29 | actions: 30 | comment: 31 | message: "Nice! PR merged successfully." 32 | conditions: 33 | - merged 34 | name: "Merge Done 🚀" 35 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at hello@clivern.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | - With issues: 4 | - Use the search tool before opening a new issue. 5 | - Please provide source code and commit sha if you found a bug. 6 | - Review existing issues and provide feedback or react to them. 7 | 8 | - With pull requests: 9 | - Open your pull request against `main` 10 | - Your pull request should have no more than two commits, if not you should squash them. 11 | - It should pass all tests in the available continuous integrations systems such as TravisCI. 12 | - You should add/modify tests to cover your proposed code changes. 13 | - If your pull request contains a new feature, please document it on the README. 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Clivern 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GO ?= go 2 | GOFMT ?= $(GO)fmt 3 | NPM ?= npm 4 | NPX ?= npx 5 | RHINO ?= rhino 6 | pkgs = ./... 7 | PKGER ?= pkger 8 | 9 | 10 | help: Makefile 11 | @echo 12 | @echo " Choose a command run in Beaver:" 13 | @echo 14 | @sed -n 's/^##//p' $< | column -t -s ':' | sed -e 's/^/ /' 15 | @echo 16 | 17 | 18 | ## install_revive: Install revive for linting. 19 | .PHONY: install_revive 20 | install_revive: 21 | @echo ">> ============= Install Revive ============= <<" 22 | $(GO) install github.com/mgechev/revive@latest 23 | 24 | 25 | ## style: Check code style. 26 | .PHONY: style 27 | style: 28 | @echo ">> ============= Checking Code Style ============= <<" 29 | @fmtRes=$$($(GOFMT) -d $$(find . -path ./vendor -prune -o -name '*.go' -print)); \ 30 | if [ -n "$${fmtRes}" ]; then \ 31 | echo "gofmt checking failed!"; echo "$${fmtRes}"; echo; \ 32 | echo "Please ensure you are using $$($(GO) version) for formatting code."; \ 33 | exit 1; \ 34 | fi 35 | 36 | 37 | ## check_license: Check if license header on all files. 38 | .PHONY: check_license 39 | check_license: 40 | @echo ">> ============= Checking License Header ============= <<" 41 | @licRes=$$(for file in $$(find . -type f -iname '*.go' ! -path './vendor/*') ; do \ 42 | awk 'NR<=3' $$file | grep -Eq "(Copyright|generated|GENERATED)" || echo $$file; \ 43 | done); \ 44 | if [ -n "$${licRes}" ]; then \ 45 | echo "license header checking failed:"; echo "$${licRes}"; \ 46 | exit 1; \ 47 | fi 48 | 49 | 50 | ## test_short: Run test cases with short flag. 51 | .PHONY: test_short 52 | test_short: 53 | @echo ">> ============= Running Short Tests ============= <<" 54 | $(GO) clean -testcache 55 | $(GO) test -mod=readonly -short $(pkgs) 56 | 57 | 58 | ## test: Run test cases. 59 | .PHONY: test 60 | test: 61 | @echo ">> ============= Running All Tests ============= <<" 62 | $(GO) clean -testcache 63 | $(GO) test -mod=readonly -run=Unit -bench=. -benchmem -v -cover $(pkgs) 64 | 65 | 66 | ## integration: Run integration test cases (Requires redis) 67 | .PHONY: integration 68 | integration: 69 | @echo ">> ============= Running All Tests ============= <<" 70 | $(GO) clean -testcache 71 | $(GO) test -mod=readonly -run=Integration -bench=. -benchmem -v -cover $(pkgs) 72 | 73 | 74 | ## lint: Lint the code. 75 | .PHONY: lint 76 | lint: 77 | @echo ">> ============= Lint All Files ============= <<" 78 | revive -config config.toml -exclude vendor/... -formatter friendly ./... 79 | 80 | 81 | ## verify: Verify dependencies 82 | .PHONY: verify 83 | verify: 84 | @echo ">> ============= List Dependencies ============= <<" 85 | $(GO) list -m all 86 | @echo ">> ============= Verify Dependencies ============= <<" 87 | $(GO) mod verify 88 | 89 | 90 | ## format: Format the code. 91 | .PHONY: format 92 | format: 93 | @echo ">> ============= Formatting Code ============= <<" 94 | $(GO) fmt $(pkgs) 95 | 96 | 97 | ## vet: Examines source code and reports suspicious constructs. 98 | .PHONY: vet 99 | vet: 100 | @echo ">> ============= Vetting Code ============= <<" 101 | $(GO) vet $(pkgs) 102 | 103 | 104 | ## coverage: Create HTML coverage report 105 | .PHONY: coverage 106 | coverage: 107 | @echo ">> ============= Coverage ============= <<" 108 | rm -f coverage.html cover.out 109 | $(GO) test -mod=readonly -coverprofile=cover.out $(pkgs) 110 | go tool cover -html=cover.out -o coverage.html 111 | 112 | 113 | ## run: Run the API Server 114 | .PHONY: run 115 | run: 116 | @echo ">> ============= Run Tower ============= <<" 117 | $(GO) run beaver.go api -c config.dist.yml 118 | 119 | 120 | ## ci: Run all CI tests. 121 | .PHONY: ci 122 | ci: style check_license test vet lint 123 | @echo "\n==> All quality checks passed" 124 | 125 | 126 | .PHONY: help 127 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

Beaver

4 |

A Real Time Messaging Server.

5 |

6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |

25 |

26 |
27 |

28 | 29 |

30 | 31 | Beaver is a real-time messaging server. With beaver you can easily build scalable in-app notifications, realtime graphs, multiplayer games, chat applications, geotracking and more in web applications and mobile apps. 32 | 33 | 34 | ## Documentation 35 | 36 | #### Run Beaver on Ubuntu 37 | 38 | Download [the latest beaver binary](https://github.com/Clivern/Beaver/releases). Make it executable from everywhere. 39 | 40 | ```zsh 41 | $ export BEAVER_LATEST_VERSION=$(curl --silent "https://api.github.com/repos/Clivern/Beaver/releases/latest" | jq '.tag_name' | sed -E 's/.*"([^"]+)".*/\1/' | tr -d v) 42 | 43 | $ curl -sL https://github.com/Clivern/Beaver/releases/download/v{$BEAVER_LATEST_VERSION}/beaver_{$BEAVER_LATEST_VERSION}_Linux_x86_64.tar.gz | tar xz 44 | ``` 45 | 46 | Then install `redis` cluster or a single node. Update the following config file with redis configs. 47 | 48 | 49 | Create the configs file `config.yml` from `config.dist.yml`. Something like the following: 50 | 51 | ```yaml 52 | # App configs 53 | app: 54 | # Env mode (dev or prod) 55 | mode: ${BEAVER_APP_MODE:-prod} 56 | # HTTP port 57 | port: ${BEAVER_API_PORT:-8080} 58 | # Hostname 59 | hostname: ${BEAVER_API_HOSTNAME:-127.0.0.1} 60 | # TLS configs 61 | tls: 62 | status: ${BEAVER_API_TLS_STATUS:-off} 63 | pemPath: ${BEAVER_API_TLS_PEMPATH:-cert/server.pem} 64 | keyPath: ${BEAVER_API_TLS_KEYPATH:-cert/server.key} 65 | 66 | # API Configs 67 | api: 68 | key: ${BEAVER_API_KEY:-6c68b836-6f8e-465e-b59f-89c1db53afca} 69 | 70 | # Beaver Secret 71 | secret: ${BEAVER_SECRET:-sWUhHRcs4Aqa0MEnYwbuQln3EW8CZ0oD} 72 | 73 | # Runtime, Requests/Response and Beaver Metrics 74 | metrics: 75 | prometheus: 76 | # Route for the metrics endpoint 77 | endpoint: ${BEAVER_METRICS_PROM_ENDPOINT:-/metrics} 78 | 79 | # Application Database 80 | database: 81 | # Database driver 82 | driver: ${BEAVER_DB_DRIVER:-redis} 83 | 84 | # Redis Configs 85 | redis: 86 | # Redis address 87 | address: ${BEAVER_DB_REDIS_ADDR:-localhost:6379} 88 | # Redis password 89 | password: ${BEAVER_DB_REDIS_PASSWORD:- } 90 | # Redis database 91 | db: ${BEAVER_DB_REDIS_DB:-0} 92 | 93 | # Log configs 94 | log: 95 | # Log level, it can be debug, info, warn, error, panic, fatal 96 | level: ${BEAVER_LOG_LEVEL:-info} 97 | # Output can be stdout or abs path to log file /var/logs/beaver.log 98 | output: ${BEAVER_LOG_OUTPUT:-stdout} 99 | # Format can be json 100 | format: ${BEAVER_LOG_FORMAT:-json} 101 | ``` 102 | 103 | The run the `beaver` with `systemd` 104 | 105 | ```zsh 106 | $ beaver api -c /path/to/config.yml 107 | ``` 108 | 109 | ### API Endpoints 110 | 111 | Create a Config `app_name`: 112 | 113 | ```bash 114 | $ curl -X POST \ 115 | -H "Content-Type: application/json" \ 116 | -H "X-API-Key: 6c68b836-6f8e-465e-b59f-89c1db53afca" \ 117 | -d '{"key":"app_name","value":"Beaver"}' \ 118 | "http://localhost:8080/api/config" 119 | ``` 120 | 121 | Get a Config `app_name`: 122 | 123 | ```bash 124 | $ curl -X GET \ 125 | -H "Content-Type: application/json" \ 126 | -H "X-API-Key: 6c68b836-6f8e-465e-b59f-89c1db53afca" \ 127 | "http://localhost:8080/api/config/app_name" 128 | 129 | {"key":"app_name","value":"Beaver"} 130 | ``` 131 | 132 | Update a Config `app_name`: 133 | 134 | ```bash 135 | $ curl -X PUT \ 136 | -H "Content-Type: application/json" \ 137 | -H "X-API-Key: 6c68b836-6f8e-465e-b59f-89c1db53afca" \ 138 | -d '{"value":"Beaver"}' \ 139 | "http://localhost:8080/api/config/app_name" 140 | ``` 141 | 142 | Delete a Config `app_name`: 143 | 144 | ```bash 145 | $ curl -X DELETE \ 146 | -H "Content-Type: application/json" \ 147 | -H "X-API-Key: 6c68b836-6f8e-465e-b59f-89c1db53afca" \ 148 | "http://localhost:8080/api/config/app_name" 149 | ``` 150 | 151 | Create a Channel: 152 | 153 | ```bash 154 | # Private Channel 155 | $ curl -X POST \ 156 | -H 'Content-Type: application/json' \ 157 | -H 'X-API-Key: 6c68b836-6f8e-465e-b59f-89c1db53afca' \ 158 | -d '{"name": "app_x_chatroom_1", "type": "private"}' \ 159 | 'http://localhost:8080/api/channel' 160 | 161 | # Public Channel 162 | $ curl -X POST \ 163 | -H 'Content-Type: application/json' \ 164 | -H 'X-API-Key: 6c68b836-6f8e-465e-b59f-89c1db53afca' \ 165 | -d '{"name": "app_y_chatroom_1", "type": "public"}' \ 166 | 'http://localhost:8080/api/channel' 167 | 168 | # Presence Channel 169 | $ curl -X POST \ 170 | -H 'Content-Type: application/json' \ 171 | -H 'X-API-Key: 6c68b836-6f8e-465e-b59f-89c1db53afca' \ 172 | -d '{"name": "app_z_chatroom_5", "type": "presence"}' \ 173 | 'http://localhost:8080/api/channel' 174 | ``` 175 | 176 | Get a Channel: 177 | 178 | ```bash 179 | $ curl -X GET \ 180 | -H 'Content-Type: application/json' \ 181 | -H 'X-API-Key: 6c68b836-6f8e-465e-b59f-89c1db53afca' \ 182 | -d '' \ 183 | 'http://localhost:8080/api/channel/app_x_chatroom_1' 184 | { 185 | "created_at":1545573214, 186 | "listeners_count":0, 187 | "name":"app_x_chatroom_1", 188 | "subscribers_count":0, 189 | "type":"private", 190 | "updated_at":1545573214 191 | } 192 | 193 | $ curl -X GET \ 194 | -H 'Content-Type: application/json' \ 195 | -H 'X-API-Key: 6c68b836-6f8e-465e-b59f-89c1db53afca' \ 196 | -d '' \ 197 | 'http://localhost:8080/api/channel/app_y_chatroom_1' 198 | { 199 | "created_at":1545573219, 200 | "listeners_count":0, 201 | "name":"app_y_chatroom_1", 202 | "subscribers_count":0, 203 | "type":"public", 204 | "updated_at":1545573219 205 | } 206 | 207 | $ curl -X GET \ 208 | -H 'Content-Type: application/json' \ 209 | -H 'X-API-Key: 6c68b836-6f8e-465e-b59f-89c1db53afca' \ 210 | -d '' \ 211 | 'http://localhost:8080/api/channel/app_z_chatroom_5' 212 | { 213 | "created_at": 1545573225, 214 | "listeners": null, 215 | "listeners_count": 0, 216 | "name": "app_z_chatroom_5", 217 | "subscribers": null, 218 | "subscribers_count": 0, 219 | "type": "presence", 220 | "updated_at": 1545573225 221 | } 222 | ``` 223 | 224 | Update a Channel `app_y_chatroom_1`: 225 | 226 | ```bash 227 | $ curl -X PUT \ 228 | -H 'Content-Type: application/json' \ 229 | -H 'X-API-Key: 6c68b836-6f8e-465e-b59f-89c1db53afca' \ 230 | -d '{"type": "private"}' \ 231 | 'http://localhost:8080/api/channel/app_y_chatroom_1' 232 | ``` 233 | 234 | Delete a Channel `app_y_chatroom_1`: 235 | 236 | ```bash 237 | $ curl -X DELETE \ 238 | -H 'Content-Type: application/json' \ 239 | -H 'X-API-Key: 6c68b836-6f8e-465e-b59f-89c1db53afca' \ 240 | -d '' \ 241 | 'http://localhost:8080/api/channel/app_y_chatroom_1' 242 | ``` 243 | 244 | Create a Client and add to `app_x_chatroom_1` Channel: 245 | 246 | ```bash 247 | $ curl -X POST \ 248 | -H 'Content-Type: application/json' \ 249 | -H 'X-API-Key: 6c68b836-6f8e-465e-b59f-89c1db53afca' \ 250 | -d '{"channels": ["app_x_chatroom_1"]}' \ 251 | 'http://localhost:8080/api/client' 252 | { 253 | "channels": [ 254 | "app_x_chatroom_1" 255 | ], 256 | "created_at": 1545575142, 257 | "id": "69775af3-5f68-4725-8162-09cab63e8427", 258 | "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiNjk3NzVhZjMtNWY2OC00NzI1LTgxNjItMDljYWI2M2U4NDI3QDE1NDU1NzUxNDIiLCJ0aW1lc3RhbXAiOjE1NDU1NzUxNDJ9.EqL-nWwu5p7hJXWrKdZN3Ds2cxWVjNYmeP1mbl562nU", 259 | "updated_at": 1545575142 260 | } 261 | ``` 262 | 263 | Get a Client `69775af3-5f68-4725-8162-09cab63e8427`: 264 | 265 | ```bash 266 | $ curl -X GET \ 267 | -H 'Content-Type: application/json' \ 268 | -H 'X-API-Key: 6c68b836-6f8e-465e-b59f-89c1db53afca' \ 269 | -d '' \ 270 | 'http://localhost:8080/api/client/69775af3-5f68-4725-8162-09cab63e8427' 271 | { 272 | "channels": [ 273 | "app_x_chatroom_1" 274 | ], 275 | "created_at": 1545575142, 276 | "id": "69775af3-5f68-4725-8162-09cab63e8427", 277 | "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjoiNjk3NzVhZjMtNWY2OC00NzI1LTgxNjItMDljYWI2M2U4NDI3QDE1NDU1NzUxNDIiLCJ0aW1lc3RhbXAiOjE1NDU1NzUxNDJ9.EqL-nWwu5p7hJXWrKdZN3Ds2cxWVjNYmeP1mbl562nU", 278 | "updated_at": 1545575142 279 | } 280 | ``` 281 | 282 | Subscribe a Client `69775af3-5f68-4725-8162-09cab63e8427` to a Channel `app_z_chatroom_5`: 283 | 284 | ```bash 285 | $ curl -X PUT \ 286 | -H 'Content-Type: application/json' \ 287 | -H 'X-API-Key: 6c68b836-6f8e-465e-b59f-89c1db53afca' \ 288 | -d '{"channels": ["app_z_chatroom_5"]}' \ 289 | 'http://localhost:8080/api/client/69775af3-5f68-4725-8162-09cab63e8427/subscribe' 290 | ``` 291 | 292 | Unsubscribe a Client `69775af3-5f68-4725-8162-09cab63e8427` from a Channel `app_z_chatroom_5`: 293 | 294 | ```bash 295 | $ curl -X PUT \ 296 | -H 'Content-Type: application/json' \ 297 | -H 'X-API-Key: 6c68b836-6f8e-465e-b59f-89c1db53afca' \ 298 | -d '{"channels": ["app_z_chatroom_5"]}' \ 299 | 'http://localhost:8080/api/client/69775af3-5f68-4725-8162-09cab63e8427/unsubscribe' 300 | ``` 301 | 302 | Delete a Client: 303 | 304 | ```bash 305 | $ curl -X DELETE \ 306 | -H 'Content-Type: application/json' \ 307 | -H 'X-API-Key: 6c68b836-6f8e-465e-b59f-89c1db53afca' \ 308 | -d '' \ 309 | 'http://localhost:8080/api/client/69775af3-5f68-4725-8162-09cab63e8427' 310 | ``` 311 | 312 | Publish to a Channel `app_x_chatroom_1`: 313 | 314 | ```bash 315 | $ curl -X POST \ 316 | -H 'Content-Type: application/json' \ 317 | -H 'X-API-Key: 6c68b836-6f8e-465e-b59f-89c1db53afca' \ 318 | -d '{"channel": "app_x_chatroom_1", "data": "{\"message\": \"Hello World\"}"}' \ 319 | 'http://localhost:8080/api/publish' 320 | ``` 321 | 322 | Broadcast to Channels `["app_x_chatroom_1"]`: 323 | 324 | ```bash 325 | $ curl -X POST \ 326 | -H 'Content-Type: application/json' \ 327 | -H 'X-API-Key: 6c68b836-6f8e-465e-b59f-89c1db53afca' \ 328 | -d '{"channels": ["app_x_chatroom_1"], "data": "{\"message\": \"Hello World\"}"}' \ 329 | 'http://localhost:8080/api/broadcast' 330 | ``` 331 | 332 | Sample Frontend Client 333 | 334 | ```js 335 | function Socket(url){ 336 | ws = new WebSocket(url); 337 | ws.onmessage = function(e) { console.log(e); }; 338 | ws.onclose = function(){ 339 | // Try to reconnect in 5 seconds 340 | setTimeout(function(){Socket(url)}, 5000); 341 | }; 342 | } 343 | 344 | Socket("ws://localhost:8080/ws/$ID/$TOKEN"); 345 | ``` 346 | 347 | 348 | ## Client: 349 | 350 | - [Go Client](https://github.com/domgolonka/beavergo) Thanks [@domgolonka](https://github.com/domgolonka) 351 | 352 | 353 | ## Versioning 354 | 355 | For transparency into our release cycle and in striving to maintain backward compatibility, Beaver is maintained under the [Semantic Versioning guidelines](https://semver.org/) and release process is predictable and business-friendly. 356 | 357 | See the [Releases section of our GitHub project](https://github.com/clivern/beaver/releases) for changelogs for each release version of Beaver. It contains summaries of the most noteworthy changes made in each release. 358 | 359 | 360 | ## Bug tracker 361 | 362 | If you have any suggestions, bug reports, or annoyances please report them to our issue tracker at https://github.com/clivern/beaver/issues 363 | 364 | 365 | ## Security Issues 366 | 367 | If you discover a security vulnerability within Beaver, please send an email to [hello@clivern.com](mailto:hello@clivern.com) 368 | 369 | 370 | ## Contributing 371 | 372 | We are an open source, community-driven project so please feel free to join us. see the [contributing guidelines](CONTRIBUTING.md) for more details. 373 | 374 | 375 | ## License 376 | 377 | © 2018, Clivern. Released under [MIT License](https://opensource.org/licenses/mit-license.php). 378 | 379 | **Beaver** is authored and maintained by [@Clivern](http://github.com/clivern). 380 | -------------------------------------------------------------------------------- /assets/charts/chart.drawio: -------------------------------------------------------------------------------- 1 | 7VpbU9s6EP41PJbxNZdHkkDpmXImc+jQ9omRbcXRQbZ8ZAWS/vqzsuWLLBNCm0CaKcwk1upia79v17urnLnTZP2Ro2x5wyJMzxwrWp+5szPHsW1rAF9SsiklQ3dYCmJOIjWoEdySH1gJLSVdkQjn2kDBGBUk04UhS1McCk2GOGdP+rAFo/pdMxRjQ3AbImpKv5JILEvpyLca+TUm8VLUG1Y9CaoGK0G+RBF7aoncyzN3yhkT5VWynmIqlVfppZx39Uxv/WAcp2KXCdMRtZL1X3ff58F8lrPN6mpIP7iOejixqXaMI1CAajIulixmKaKXjXTC2SqNsFzWglYz5jNjGQhtEP6LhdgoNNFKMBAtRUJVL14T8U1Nl9ff5fW5r1qzdatrtqkaqeCbb9UCstGaJZvNtKJVzVsQSqeMMl7szl0sFk4YgjwXnD3gVk80CAb+AHpMxSpd52zFQ7xFmxVpBeIxFlsG+uU4qerWHRRuHzFLMGwBBnBMkSCPOheRonRcj2tQhwsF/CtIUD32I6IrdSuDFQ3mUv9PSyLwbYYKbTyB5ev47lHnj5gLvN6qJNXrDdU2lKfxKsN7auzWHijZsmWzY+tAevVPQa322DkytbqD9/RZtuaznO1Oy0AMD55BbDgOLOuXnI+zo+9xjsv5VPHAMaD5G4LpHReYo+MBc/hKNA8YF+yKZvUi3h+axdQLztGmNSBjJBV5a+W5FLRd/lBz+a7fiSw7433X2jYeLsonaHhVb+Xnqea8/HJFeVamBAuylvyaZJgTuC+WyMItIIfA80bUphSiJE7hOgSsi746epfciVC+rAm7YKlo0cVzhsPJBOQUBZhOUPgQFwTXuQZ/aqpite3Im8Crn6Txl4L3LghIUqQn1feMJDEoi5IAPkmQwCfKMkpCIANL87J5n2MOr/fz/DHeUwTg6fBC+mNEAH0BwOBQAcDQgP4fHBHYvnVbbN0gAmxUvICvVAfokV6ojoREUemdcE5+oKBYSsKtjAfW9Sdn/kyuBQ4pVyhWoKp79yRpr1Y/5Mya+n3bNtQ/7lG/cyj1jwz1XzQkhA7JeZxGp4WCbfm6EYx7wmD3LWEY/3GA0gFGSCD4ClkKYOYFB+8FRyTF0X2EM8o2ONqfL/QdnQaeZ9LA72GBfygW2OZ7cIJR4QStv1mET8sK/Y4VvrsvtAd/rFBa4QqCDhl/BLLius/gw/P14GNk2tubxh62GXwYgLOVoOB/pnVJugcf+L+St53EHEUEN30pS3FfGlJP0FCvTPWzRHkOple8g91ZwIRgSY8tCwmqyat2lanDhrYNy1sadAZOZrIzWcfyDOAcPeXeObiKIiv6FMrnkZ6jvNJHJSwgFN+HlBQM0ZIypYg9cMjtRFDvzSFnh3orBFAX8vhCgkRRnpMyN0VcmOJfKhHWdXXr3B977dq6fW4NRy9U14tWy3nVebZasZ2Rb03Hn4V5f0X3nWslO77LX5eEm1lzt15td/hW7lzN2pJ+e2N9IbsbbJSaMRbaWyZeGeSx0XnUobN9BHT+2fLszobwbnT29kVnq0Pnrh8+NJ09g85fMUQ61rR8S51UQO15ekBt9+Qzo7cMqB3z0OymCBJOFAC3SuAqAFzPAOBNMxrHzGiA/jkLH/Auqtc9s4rhesK63SHqOxTVjwx0TGSvQCoOHvh7SkO8kQaT23MK6r0lTK5Z/plyjERjJ5COTWVjiSANodAaoETqLg3yrFDKgErsAg5XsbxCsmZnzVf5sqgdCLJoCsunZHJ1YF9hafsGlr0n2u6hwPTMhGDO2SOR5RurLudcs1woTJUjtL6ArcmSaw1u+QmSTzMT4C9MzlVZqfwdVXv10ysWOd2SrW2eW/Qa7eFwNiPl39i3DvfkW31bt8e+86VD+dZFEjjXKZrf3l2x//I7P5/f3H3YIZ8xiiAtmHqI3gWkKqh1ay19BbYkDxFY5tUUhUt8X5x03YN3iFah0MpshsJ7YHneVjy9RFK/715IHjzn1RhAs/nRYxm0Nz8ddS//Bw== -------------------------------------------------------------------------------- /assets/charts/chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clivern/Beaver/bd68afd9d44386b789e46d303f7f4a865ac4cc68/assets/charts/chart.png -------------------------------------------------------------------------------- /assets/img/gopher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clivern/Beaver/bd68afd9d44386b789e46d303f7f4a865ac4cc68/assets/img/gopher.png -------------------------------------------------------------------------------- /beaver.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "github.com/clivern/beaver/cmd" 9 | ) 10 | 11 | var ( 12 | version = "dev" 13 | commit = "none" 14 | date = "unknown" 15 | builtBy = "unknown" 16 | ) 17 | 18 | func main() { 19 | cmd.Version = version 20 | cmd.Commit = commit 21 | cmd.Date = date 22 | cmd.BuiltBy = builtBy 23 | 24 | cmd.Execute() 25 | } 26 | -------------------------------------------------------------------------------- /bin/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Fetch latest version 4 | export LATEST_VERSION=$(curl --silent "https://api.github.com/repos/clivern/beaver/releases/latest" | jq '.tag_name' | sed -E 's/.*"([^"]+)".*/\1/') 5 | 6 | # Update go checksum database (sum.golang.org) immediately after release 7 | curl --silent https://sum.golang.org/lookup/github.com/clivern/beaver@{$LATEST_VERSION} 8 | -------------------------------------------------------------------------------- /cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /cmd/api.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package cmd 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "io" 11 | "io/ioutil" 12 | "net/http" 13 | "os" 14 | "path/filepath" 15 | "strconv" 16 | "strings" 17 | 18 | "github.com/clivern/beaver/core/controller" 19 | "github.com/clivern/beaver/core/middleware" 20 | "github.com/clivern/beaver/core/util" 21 | 22 | "github.com/drone/envsubst" 23 | "github.com/gin-gonic/gin" 24 | log "github.com/sirupsen/logrus" 25 | "github.com/spf13/cobra" 26 | "github.com/spf13/viper" 27 | ) 28 | 29 | var towerCmd = &cobra.Command{ 30 | Use: "api", 31 | Short: "Start beaver api server", 32 | Run: func(cmd *cobra.Command, args []string) { 33 | configUnparsed, err := ioutil.ReadFile(config) 34 | 35 | if err != nil { 36 | panic(fmt.Sprintf( 37 | "Error while reading config file [%s]: %s", 38 | config, 39 | err.Error(), 40 | )) 41 | } 42 | 43 | configParsed, err := envsubst.EvalEnv(string(configUnparsed)) 44 | 45 | if err != nil { 46 | panic(fmt.Sprintf( 47 | "Error while parsing config file [%s]: %s", 48 | config, 49 | err.Error(), 50 | )) 51 | } 52 | 53 | viper.SetConfigType("yaml") 54 | err = viper.ReadConfig(bytes.NewBufferString(configParsed)) 55 | 56 | if err != nil { 57 | panic(fmt.Sprintf( 58 | "Error while loading configs [%s]: %s", 59 | config, 60 | err.Error(), 61 | )) 62 | } 63 | 64 | viper.SetDefault("app.name", util.GenerateUUID4()) 65 | 66 | if viper.GetString("app.log.output") != "stdout" { 67 | dir, _ := filepath.Split(viper.GetString("app.log.output")) 68 | 69 | if !util.DirExists(dir) { 70 | if _, err := util.EnsureDir(dir, 775); err != nil { 71 | panic(fmt.Sprintf( 72 | "Directory [%s] creation failed with error: %s", 73 | dir, 74 | err.Error(), 75 | )) 76 | } 77 | } 78 | 79 | if !util.FileExists(viper.GetString("app.log.output")) { 80 | f, err := os.Create(viper.GetString("app.log.output")) 81 | if err != nil { 82 | panic(fmt.Sprintf( 83 | "Error while creating log file [%s]: %s", 84 | viper.GetString("app.log.output"), 85 | err.Error(), 86 | )) 87 | } 88 | defer f.Close() 89 | } 90 | } 91 | 92 | if viper.GetString("app.log.output") == "stdout" { 93 | gin.DefaultWriter = os.Stdout 94 | log.SetOutput(os.Stdout) 95 | } else { 96 | f, _ := os.Create(viper.GetString("app.log.output")) 97 | gin.DefaultWriter = io.MultiWriter(f) 98 | log.SetOutput(f) 99 | } 100 | 101 | lvl := strings.ToLower(viper.GetString("app.log.level")) 102 | level, err := log.ParseLevel(lvl) 103 | 104 | if err != nil { 105 | level = log.InfoLevel 106 | } 107 | 108 | log.SetLevel(level) 109 | 110 | if viper.GetString("app.mode") == "prod" { 111 | gin.SetMode(gin.ReleaseMode) 112 | gin.DefaultWriter = ioutil.Discard 113 | gin.DisableConsoleColor() 114 | } 115 | 116 | if viper.GetString("app.log.format") == "json" { 117 | log.SetFormatter(&log.JSONFormatter{}) 118 | } else { 119 | log.SetFormatter(&log.TextFormatter{}) 120 | } 121 | 122 | r := gin.Default() 123 | 124 | r.Use(middleware.Cors()) 125 | r.Use(middleware.Correlation()) 126 | r.Use(middleware.Logger()) 127 | r.Use(middleware.Metric()) 128 | r.Use(middleware.Auth()) 129 | 130 | r.GET("/favicon.ico", func(c *gin.Context) { 131 | c.String(http.StatusNoContent, "") 132 | }) 133 | 134 | r.GET("/", controller.Home) 135 | 136 | r.GET("/_health", controller.Health) 137 | 138 | r.GET( 139 | viper.GetString("app.metrics.prometheus.endpoint"), 140 | gin.WrapH(controller.Metrics()), 141 | ) 142 | 143 | r.GET("/api/channel/:name", controller.GetChannelByName) 144 | r.POST("/api/channel", controller.CreateChannel) 145 | r.DELETE("/api/channel/:name", controller.DeleteChannelByName) 146 | r.PUT("/api/channel/:name", controller.UpdateChannelByName) 147 | 148 | r.GET("/api/client/:id", controller.GetClientByID) 149 | r.POST("/api/client", controller.CreateClient) 150 | r.DELETE("/api/client/:id", controller.DeleteClientByID) 151 | r.PUT("/api/client/:id/unsubscribe", controller.Unsubscribe) 152 | r.PUT("/api/client/:id/subscribe", controller.Subscribe) 153 | 154 | r.GET("/api/config/:key", controller.GetConfigByKey) 155 | r.POST("/api/config", controller.CreateConfig) 156 | r.DELETE("/api/config/:key", controller.DeleteConfigByKey) 157 | r.PUT("/api/config/:key", controller.UpdateConfigByKey) 158 | 159 | socket := &controller.Websocket{} 160 | socket.Init() 161 | 162 | r.GET("/ws/:id/:token", func(c *gin.Context) { 163 | socket.HandleConnections( 164 | c.Writer, 165 | c.Request, 166 | c.Param("id"), 167 | c.Param("token"), 168 | ) 169 | }) 170 | 171 | r.POST("/api/broadcast", func(c *gin.Context) { 172 | rawBody, err := c.GetRawData() 173 | if err != nil { 174 | c.JSON(http.StatusBadRequest, gin.H{ 175 | "status": "error", 176 | "error": "Invalid request", 177 | }) 178 | return 179 | } 180 | socket.BroadcastAction(c, rawBody) 181 | }) 182 | 183 | r.POST("/api/publish", func(c *gin.Context) { 184 | rawBody, err := c.GetRawData() 185 | if err != nil { 186 | c.JSON(http.StatusBadRequest, gin.H{ 187 | "status": "error", 188 | "error": "Invalid request", 189 | }) 190 | return 191 | } 192 | socket.PublishAction(c, rawBody) 193 | }) 194 | 195 | go socket.HandleMessages() 196 | 197 | var runerr error 198 | 199 | if viper.GetBool("app.tls.status") { 200 | runerr = r.RunTLS( 201 | fmt.Sprintf(":%s", strconv.Itoa(viper.GetInt("app.port"))), 202 | viper.GetString("app.tls.pemPath"), 203 | viper.GetString("app.tls.keyPath"), 204 | ) 205 | } else { 206 | runerr = r.Run( 207 | fmt.Sprintf(":%s", strconv.Itoa(viper.GetInt("app.port"))), 208 | ) 209 | } 210 | 211 | if runerr != nil { 212 | panic(runerr.Error()) 213 | } 214 | }, 215 | } 216 | 217 | func init() { 218 | towerCmd.Flags().StringVarP( 219 | &config, 220 | "config", 221 | "c", 222 | "config.prod.yml", 223 | "Absolute path to config file (required)", 224 | ) 225 | towerCmd.MarkFlagRequired("config") 226 | rootCmd.AddCommand(towerCmd) 227 | } 228 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package cmd 6 | 7 | import ( 8 | "fmt" 9 | "os" 10 | 11 | "github.com/spf13/cobra" 12 | ) 13 | 14 | var config string 15 | 16 | var rootCmd = &cobra.Command{ 17 | Use: "beaver", 18 | Short: `🐺 A Real time messaging system to build a scalable in-app notifications, 19 | multiplayer games, chat apps in web and mobile apps. 20 | 21 | If you have any suggestions, bug reports, or annoyances please report 22 | them to our issue tracker at `, 23 | } 24 | 25 | // Execute runs cmd tool 26 | func Execute() { 27 | if err := rootCmd.Execute(); err != nil { 28 | fmt.Println(err) 29 | os.Exit(1) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package cmd 6 | 7 | import ( 8 | "fmt" 9 | 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | var ( 14 | // Version buildinfo item 15 | Version = "dev" 16 | // Commit buildinfo item 17 | Commit = "none" 18 | // Date buildinfo item 19 | Date = "unknown" 20 | // BuiltBy buildinfo item 21 | BuiltBy = "unknown" 22 | ) 23 | 24 | var versionCmd = &cobra.Command{ 25 | Use: "version", 26 | Short: "Print the version number", 27 | Run: func(cmd *cobra.Command, args []string) { 28 | fmt.Println( 29 | fmt.Sprintf( 30 | `Current Beaver Version %v Commit %v, Built @%v By %v.`, 31 | Version, 32 | Commit, 33 | Date, 34 | BuiltBy, 35 | ), 36 | ) 37 | }, 38 | } 39 | 40 | func init() { 41 | rootCmd.AddCommand(versionCmd) 42 | } 43 | -------------------------------------------------------------------------------- /config.dist.yml: -------------------------------------------------------------------------------- 1 | # App configs 2 | app: 3 | # Env mode (dev or prod) 4 | mode: ${BEAVER_APP_MODE:-dev} 5 | # HTTP port 6 | port: ${BEAVER_API_PORT:-8080} 7 | # Hostname 8 | hostname: ${BEAVER_API_HOSTNAME:-127.0.0.1} 9 | # TLS configs 10 | tls: 11 | status: ${BEAVER_API_TLS_STATUS:-off} 12 | pemPath: ${BEAVER_API_TLS_PEMPATH:-cert/server.pem} 13 | keyPath: ${BEAVER_API_TLS_KEYPATH:-cert/server.key} 14 | 15 | # API Configs 16 | api: 17 | key: ${BEAVER_API_KEY:-6c68b836-6f8e-465e-b59f-89c1db53afca} 18 | 19 | # Beaver Secret 20 | secret: ${BEAVER_SECRET:-sWUhHRcs4Aqa0MEnYwbuQln3EW8CZ0oD} 21 | 22 | # Runtime, Requests/Response and Beaver Metrics 23 | metrics: 24 | prometheus: 25 | # Route for the metrics endpoint 26 | endpoint: ${BEAVER_METRICS_PROM_ENDPOINT:-/metrics} 27 | 28 | # Application Database 29 | database: 30 | # Database driver 31 | driver: ${BEAVER_DB_DRIVER:-redis} 32 | 33 | # Redis Configs 34 | redis: 35 | # Redis address 36 | address: ${BEAVER_DB_REDIS_ADDR:-localhost:6379} 37 | # Redis password 38 | password: ${BEAVER_DB_REDIS_PASSWORD:- } 39 | # Redis database 40 | db: ${BEAVER_DB_REDIS_DB:-0} 41 | 42 | # Log configs 43 | log: 44 | # Log level, it can be debug, info, warn, error, panic, fatal 45 | level: ${BEAVER_LOG_LEVEL:-info} 46 | # Output can be stdout or abs path to log file /var/logs/beaver.log 47 | output: ${BEAVER_LOG_OUTPUT:-stdout} 48 | # Format can be json 49 | format: ${BEAVER_LOG_FORMAT:-json} 50 | -------------------------------------------------------------------------------- /config.toml: -------------------------------------------------------------------------------- 1 | ignoreGeneratedHeader = false 2 | severity = "warning" 3 | confidence = 0.8 4 | errorCode = 0 5 | warningCode = 0 6 | 7 | [rule.blank-imports] 8 | [rule.context-as-argument] 9 | [rule.context-keys-type] 10 | [rule.dot-imports] 11 | [rule.error-return] 12 | [rule.error-strings] 13 | [rule.error-naming] 14 | [rule.exported] 15 | [rule.if-return] 16 | [rule.increment-decrement] 17 | [rule.var-naming] 18 | [rule.var-declaration] 19 | [rule.package-comments] 20 | [rule.range] 21 | [rule.receiver-naming] 22 | [rule.time-naming] 23 | [rule.unexported-return] 24 | [rule.indent-error-flow] 25 | [rule.errorf] 26 | [rule.empty-block] 27 | [rule.superfluous-else] 28 | [rule.unused-parameter] 29 | [rule.unreachable-code] 30 | [rule.redefines-builtin-id] -------------------------------------------------------------------------------- /core/api/channel.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package api 6 | 7 | import ( 8 | "encoding/json" 9 | "fmt" 10 | 11 | "github.com/clivern/beaver/core/driver" 12 | "github.com/clivern/beaver/core/util" 13 | 14 | "github.com/go-redis/redis" 15 | log "github.com/sirupsen/logrus" 16 | ) 17 | 18 | // ChannelsHashPrefix is the hash prefix 19 | const ChannelsHashPrefix string = "beaver.channel" 20 | 21 | // Channel struct 22 | type Channel struct { 23 | Driver *driver.Redis 24 | } 25 | 26 | // ChannelResult struct 27 | type ChannelResult struct { 28 | Name string `json:"name"` 29 | Type string `json:"type"` 30 | CreatedAt int64 `json:"created_at"` 31 | UpdatedAt int64 `json:"updated_at"` 32 | } 33 | 34 | // LoadFromJSON load object from json 35 | func (c *ChannelResult) LoadFromJSON(data []byte) (bool, error) { 36 | err := json.Unmarshal(data, &c) 37 | if err != nil { 38 | return false, err 39 | } 40 | return true, nil 41 | } 42 | 43 | // ConvertToJSON converts object to json 44 | func (c *ChannelResult) ConvertToJSON() (string, error) { 45 | data, err := json.Marshal(&c) 46 | if err != nil { 47 | return "", err 48 | } 49 | return string(data), nil 50 | } 51 | 52 | // Init initialize the redis connection 53 | func (c *Channel) Init() bool { 54 | c.Driver = driver.NewRedisDriver() 55 | 56 | result, err := c.Driver.Connect() 57 | 58 | if !result { 59 | log.Errorf( 60 | `Error while connecting to redis: %s`, 61 | err.Error(), 62 | ) 63 | return false 64 | } 65 | 66 | log.Infof(`Redis connection established`) 67 | 68 | return true 69 | } 70 | 71 | // CreateChannel creates a channel 72 | func (c *Channel) CreateChannel(channel ChannelResult) (bool, error) { 73 | exists, err := c.Driver.HExists(ChannelsHashPrefix, channel.Name) 74 | 75 | if err != nil { 76 | log.Errorf( 77 | `Error while creating channel %s: %s`, 78 | channel.Name, 79 | err.Error(), 80 | ) 81 | return false, fmt.Errorf( 82 | `Error while creating channel %s`, 83 | channel.Name, 84 | ) 85 | } 86 | 87 | if exists { 88 | log.Warningf( 89 | `Trying to create existent channel %s`, 90 | channel.Name, 91 | ) 92 | return false, fmt.Errorf( 93 | `Trying to create existent channel %s`, 94 | channel.Name, 95 | ) 96 | } 97 | 98 | result, err := channel.ConvertToJSON() 99 | 100 | if err != nil { 101 | log.Errorf( 102 | `Something wrong with channel %s data: %s`, 103 | channel.Name, 104 | err.Error(), 105 | ) 106 | return false, fmt.Errorf( 107 | `Something wrong with channel %s data`, 108 | channel.Name, 109 | ) 110 | } 111 | 112 | _, err = c.Driver.HSet(ChannelsHashPrefix, channel.Name, result) 113 | 114 | if err != nil { 115 | log.Errorf( 116 | `Error while creating channel %s: %s`, 117 | channel.Name, 118 | err.Error(), 119 | ) 120 | return false, fmt.Errorf( 121 | `Error while creating channel %s`, 122 | channel.Name, 123 | ) 124 | } 125 | 126 | log.Infof( 127 | `Channel %s with type %s got created`, 128 | channel.Name, 129 | channel.Type, 130 | ) 131 | 132 | return true, nil 133 | } 134 | 135 | // GetChannelByName gets a channel by name 136 | func (c *Channel) GetChannelByName(name string) (ChannelResult, error) { 137 | var channelResult ChannelResult 138 | 139 | exists, err := c.Driver.HExists(ChannelsHashPrefix, name) 140 | 141 | if err != nil { 142 | log.Errorf( 143 | `Error while getting channel %s: %s`, 144 | name, 145 | err.Error(), 146 | ) 147 | return channelResult, fmt.Errorf( 148 | `Error while getting channel %s`, 149 | name, 150 | ) 151 | } 152 | 153 | if !exists { 154 | log.Warningf( 155 | `Trying to get non existent channel %s`, 156 | name, 157 | ) 158 | return channelResult, fmt.Errorf( 159 | `Trying to get non existent channel %s`, 160 | name, 161 | ) 162 | } 163 | 164 | value, err := c.Driver.HGet(ChannelsHashPrefix, name) 165 | 166 | if err != nil { 167 | log.Errorf( 168 | `Error while getting channel %s: %s`, 169 | name, 170 | err.Error(), 171 | ) 172 | return channelResult, fmt.Errorf( 173 | `Error while getting channel %s`, 174 | name, 175 | ) 176 | } 177 | 178 | _, err = channelResult.LoadFromJSON([]byte(value)) 179 | 180 | if err != nil { 181 | log.Errorf( 182 | `Error while getting channel %s: %s`, 183 | name, 184 | err.Error(), 185 | ) 186 | return channelResult, fmt.Errorf( 187 | `Error while getting channel %s`, 188 | name, 189 | ) 190 | } 191 | 192 | return channelResult, nil 193 | } 194 | 195 | // UpdateChannelByName updates a channel by name 196 | func (c *Channel) UpdateChannelByName(channel ChannelResult) (bool, error) { 197 | exists, err := c.Driver.HExists(ChannelsHashPrefix, channel.Name) 198 | 199 | if err != nil { 200 | log.Errorf( 201 | `Error while updating channel %s: %s`, 202 | channel.Name, 203 | err.Error(), 204 | ) 205 | return false, fmt.Errorf( 206 | `Error while updating channel %s`, 207 | channel.Name, 208 | ) 209 | } 210 | 211 | if !exists { 212 | log.Warningf( 213 | `Trying to create non existent channel %s`, 214 | channel.Name, 215 | ) 216 | return false, fmt.Errorf( 217 | `Trying to create non existent channel %s`, 218 | channel.Name, 219 | ) 220 | } 221 | 222 | result, err := channel.ConvertToJSON() 223 | 224 | if err != nil { 225 | log.Errorf( 226 | `Something wrong with channel %s data: %s`, 227 | channel.Name, 228 | err.Error(), 229 | ) 230 | return false, fmt.Errorf( 231 | `Something wrong with channel %s data`, 232 | channel.Name, 233 | ) 234 | } 235 | 236 | _, err = c.Driver.HSet(ChannelsHashPrefix, channel.Name, result) 237 | 238 | if err != nil { 239 | log.Errorf( 240 | `Error while updating channel %s: %s`, 241 | channel.Name, 242 | err.Error(), 243 | ) 244 | return false, fmt.Errorf( 245 | `Error while updating channel %s`, 246 | channel.Name, 247 | ) 248 | } 249 | 250 | log.Infof( 251 | `Channel %s got updated to type %s`, 252 | channel.Name, 253 | channel.Type, 254 | ) 255 | 256 | return true, nil 257 | } 258 | 259 | // DeleteChannelByName deletes a channel with name 260 | func (c *Channel) DeleteChannelByName(name string) (bool, error) { 261 | deleted, err := c.Driver.HDel(ChannelsHashPrefix, name) 262 | 263 | if err != nil { 264 | log.Errorf( 265 | `Error while deleting channel %s: %s`, 266 | name, 267 | err.Error(), 268 | ) 269 | return false, fmt.Errorf( 270 | `Error while deleting channel %s`, 271 | name, 272 | ) 273 | } 274 | 275 | if deleted <= 0 { 276 | log.Warningf( 277 | `Trying to delete non existent channel %s`, 278 | name, 279 | ) 280 | return false, fmt.Errorf( 281 | `Trying to delete non existent channel %s`, 282 | name, 283 | ) 284 | } 285 | 286 | c.Driver.HTruncate(fmt.Sprintf("%s.listeners", name)) 287 | c.Driver.HTruncate(fmt.Sprintf("%s.subscribers", name)) 288 | 289 | log.Infof( 290 | `Channel %s got deleted`, 291 | name, 292 | ) 293 | 294 | return true, nil 295 | } 296 | 297 | // CountListeners counts channel listeners 298 | func (c *Channel) CountListeners(name string) int64 { 299 | 300 | count, err := c.Driver.HLen(fmt.Sprintf("%s.listeners", name)) 301 | 302 | if err != nil { 303 | log.Errorf( 304 | `Error while counting %s listeners %s`, 305 | name, 306 | err.Error(), 307 | ) 308 | 309 | return 0 310 | } 311 | 312 | return count 313 | 314 | } 315 | 316 | // CountSubscribers counts channel subscribers 317 | func (c *Channel) CountSubscribers(name string) int64 { 318 | 319 | count, err := c.Driver.HLen(fmt.Sprintf("%s.subscribers", name)) 320 | 321 | if err != nil { 322 | log.Errorf( 323 | `Error while counting %s subscribers %s`, 324 | name, 325 | err.Error(), 326 | ) 327 | return 0 328 | } 329 | 330 | return count 331 | } 332 | 333 | // ChannelsExist checks if channels exist 334 | func (c *Channel) ChannelsExist(channels []string) (bool, error) { 335 | for _, channel := range channels { 336 | exists, err := c.Driver.HExists(ChannelsHashPrefix, channel) 337 | 338 | if err != nil { 339 | log.Errorf( 340 | `Error while getting channel %s: %s`, 341 | channel, 342 | err.Error(), 343 | ) 344 | 345 | return false, fmt.Errorf( 346 | `Error while getting channel %s`, 347 | channel, 348 | ) 349 | } 350 | 351 | if !exists { 352 | log.Infof( 353 | `Channel %s not exist`, 354 | channel, 355 | ) 356 | 357 | return false, fmt.Errorf( 358 | `Channel %s not exist`, 359 | channel, 360 | ) 361 | } 362 | } 363 | 364 | return true, nil 365 | } 366 | 367 | // ChannelExist checks if channel exists 368 | func (c *Channel) ChannelExist(channel string) (bool, error) { 369 | return c.ChannelsExist([]string{channel}) 370 | } 371 | 372 | // ChannelScan get clients under channel listeners (connected clients) 373 | func (c *Channel) ChannelScan(channel string) *redis.ScanCmd { 374 | return c.Driver.HScan(fmt.Sprintf("%s.listeners", channel), 0, "", 0) 375 | } 376 | 377 | // GetListeners gets a list of listeners with channel name 378 | func (c *Channel) GetListeners(channel string) []string { 379 | var result []string 380 | var key string 381 | validate := util.Validator{} 382 | 383 | iter := c.Driver.HScan(fmt.Sprintf("%s.listeners", channel), 0, "", 0).Iterator() 384 | 385 | for iter.Next() { 386 | key = iter.Val() 387 | if key != "" && validate.IsUUID4(key) { 388 | result = append(result, key) 389 | } 390 | } 391 | 392 | return result 393 | } 394 | 395 | // GetSubscribers gets a list of subscribers with channel name 396 | func (c *Channel) GetSubscribers(channel string) []string { 397 | var result []string 398 | var key string 399 | validate := util.Validator{} 400 | 401 | iter := c.Driver.HScan(fmt.Sprintf("%s.subscribers", channel), 0, "", 0).Iterator() 402 | 403 | for iter.Next() { 404 | key = iter.Val() 405 | if key != "" && validate.IsUUID4(key) { 406 | result = append(result, key) 407 | } 408 | } 409 | 410 | return result 411 | } 412 | -------------------------------------------------------------------------------- /core/api/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package api 6 | 7 | import ( 8 | "encoding/json" 9 | "fmt" 10 | "time" 11 | 12 | "github.com/clivern/beaver/core/driver" 13 | "github.com/clivern/beaver/core/util" 14 | 15 | log "github.com/sirupsen/logrus" 16 | "github.com/spf13/viper" 17 | ) 18 | 19 | // ClientsHashPrefix is the hash prefix 20 | const ClientsHashPrefix string = "beaver.client" 21 | 22 | // Client struct 23 | type Client struct { 24 | Driver *driver.Redis 25 | } 26 | 27 | // ClientResult struct 28 | type ClientResult struct { 29 | ID string `json:"id"` 30 | Token string `json:"token"` 31 | Channels []string `json:"channels"` 32 | CreatedAt int64 `json:"created_at"` 33 | UpdatedAt int64 `json:"updated_at"` 34 | } 35 | 36 | // LoadFromJSON load object from json 37 | func (c *ClientResult) LoadFromJSON(data []byte) (bool, error) { 38 | err := json.Unmarshal(data, &c) 39 | if err != nil { 40 | return false, err 41 | } 42 | return true, nil 43 | } 44 | 45 | // ConvertToJSON converts object to json 46 | func (c *ClientResult) ConvertToJSON() (string, error) { 47 | data, err := json.Marshal(&c) 48 | if err != nil { 49 | return "", err 50 | } 51 | return string(data), nil 52 | } 53 | 54 | // GenerateClient generates client ID & Token 55 | func (c *ClientResult) GenerateClient() (bool, error) { 56 | 57 | now := time.Now().Unix() 58 | c.ID = util.GenerateUUID4() 59 | 60 | token, err := util.GenerateJWTToken( 61 | fmt.Sprintf("%s@%d", c.ID, now), 62 | now, 63 | viper.GetString("app.secret"), 64 | ) 65 | 66 | if err != nil { 67 | return false, err 68 | } 69 | 70 | c.Token = token 71 | c.CreatedAt = now 72 | c.UpdatedAt = now 73 | 74 | return true, nil 75 | } 76 | 77 | // Init initialize the redis connection 78 | func (c *Client) Init() bool { 79 | c.Driver = driver.NewRedisDriver() 80 | 81 | result, err := c.Driver.Connect() 82 | if !result { 83 | log.Errorf( 84 | `Error while connecting to redis: %s`, 85 | err.Error(), 86 | ) 87 | return false 88 | } 89 | 90 | log.Infof(`Redis connection established`) 91 | 92 | return true 93 | } 94 | 95 | // CreateClient creates a client 96 | func (c *Client) CreateClient(client ClientResult) (bool, error) { 97 | 98 | exists, err := c.Driver.HExists(ClientsHashPrefix, client.ID) 99 | 100 | if err != nil { 101 | log.Errorf( 102 | `Error while creating client %s: %s`, 103 | client.ID, 104 | err.Error(), 105 | ) 106 | 107 | return false, fmt.Errorf( 108 | `Error while creating client %s`, 109 | client.ID, 110 | ) 111 | } 112 | 113 | if exists { 114 | log.Warningf( 115 | `Trying to create existent client %s`, 116 | client.ID, 117 | ) 118 | 119 | return false, fmt.Errorf( 120 | `Trying to create existent client %s`, 121 | client.ID, 122 | ) 123 | } 124 | 125 | result, err := client.ConvertToJSON() 126 | 127 | if err != nil { 128 | log.Errorf( 129 | `Something wrong with client %s data: %s`, 130 | client.ID, 131 | err.Error(), 132 | ) 133 | return false, fmt.Errorf( 134 | `Something wrong with client %s data`, 135 | client.ID, 136 | ) 137 | } 138 | 139 | _, err = c.Driver.HSet(ClientsHashPrefix, client.ID, result) 140 | 141 | if err != nil { 142 | log.Errorf( 143 | `Error while creating client %s: %s`, 144 | client.ID, 145 | err.Error(), 146 | ) 147 | return false, fmt.Errorf( 148 | `Error while creating client %s`, 149 | client.ID, 150 | ) 151 | } 152 | 153 | log.Infof( 154 | `Client %s got created`, 155 | client.ID, 156 | ) 157 | 158 | for _, channel := range client.Channels { 159 | ok, err := c.AddToChannel(client.ID, channel) 160 | if !ok || err != nil { 161 | return false, err 162 | } 163 | } 164 | 165 | return true, nil 166 | } 167 | 168 | // GetClientByID gets a client by ID 169 | func (c *Client) GetClientByID(ID string) (ClientResult, error) { 170 | 171 | var clientResult ClientResult 172 | 173 | exists, err := c.Driver.HExists(ClientsHashPrefix, ID) 174 | 175 | if err != nil { 176 | log.Errorf( 177 | `Error while getting client %s: %s`, 178 | ID, 179 | err.Error(), 180 | ) 181 | return clientResult, fmt.Errorf( 182 | "Error while getting client %s", 183 | ID, 184 | ) 185 | } 186 | 187 | if !exists { 188 | log.Warningf( 189 | `Trying to get non existent client %s`, 190 | ID, 191 | ) 192 | return clientResult, fmt.Errorf( 193 | "Trying to get non existent client %s", 194 | ID, 195 | ) 196 | } 197 | 198 | value, err := c.Driver.HGet(ClientsHashPrefix, ID) 199 | 200 | if err != nil { 201 | log.Errorf( 202 | `Error while getting client %s: %s`, 203 | ID, 204 | err.Error(), 205 | ) 206 | return clientResult, fmt.Errorf( 207 | "Error while getting client %s", 208 | ID, 209 | ) 210 | } 211 | 212 | _, err = clientResult.LoadFromJSON([]byte(value)) 213 | 214 | if err != nil { 215 | log.Errorf( 216 | `Error while getting client %s: %s`, 217 | ID, 218 | err.Error(), 219 | ) 220 | return clientResult, fmt.Errorf( 221 | "Error while getting client %s", 222 | ID, 223 | ) 224 | } 225 | 226 | return clientResult, nil 227 | } 228 | 229 | // UpdateClientByID updates a client by ID 230 | func (c *Client) UpdateClientByID(client ClientResult) (bool, error) { 231 | 232 | exists, err := c.Driver.HExists(ClientsHashPrefix, client.ID) 233 | 234 | if err != nil { 235 | log.Errorf( 236 | `Error while updating client %s: %s`, 237 | client.ID, 238 | err.Error(), 239 | ) 240 | return false, fmt.Errorf( 241 | `Error while updating client %s`, 242 | client.ID, 243 | ) 244 | } 245 | 246 | if !exists { 247 | log.Warningf( 248 | `Trying to create non existent client %s`, 249 | client.ID, 250 | ) 251 | return false, fmt.Errorf( 252 | `Trying to create non existent client %s`, 253 | client.ID, 254 | ) 255 | } 256 | 257 | result, err := client.ConvertToJSON() 258 | 259 | if err != nil { 260 | log.Errorf( 261 | `Something wrong with client %s data: %s`, 262 | client.ID, 263 | err.Error(), 264 | ) 265 | return false, fmt.Errorf( 266 | `Something wrong with client %s data`, 267 | client.ID, 268 | ) 269 | } 270 | 271 | _, err = c.Driver.HSet(ClientsHashPrefix, client.ID, result) 272 | 273 | if err != nil { 274 | log.Errorf( 275 | `Error while updating client %s: %s`, 276 | client.ID, 277 | err.Error(), 278 | ) 279 | return false, fmt.Errorf( 280 | `Error while updating client %s`, 281 | client.ID, 282 | ) 283 | } 284 | 285 | log.Infof( 286 | `Client %s got updated`, 287 | client.ID, 288 | ) 289 | 290 | return true, nil 291 | } 292 | 293 | // DeleteClientByID deletes a client with ID 294 | func (c *Client) DeleteClientByID(ID string) (bool, error) { 295 | 296 | client, err := c.GetClientByID(ID) 297 | 298 | if err != nil { 299 | log.Errorf( 300 | `Error while deleting client %s: %s`, 301 | ID, 302 | err.Error(), 303 | ) 304 | return false, fmt.Errorf( 305 | `Error while deleting client %s`, 306 | ID, 307 | ) 308 | } 309 | 310 | for _, channel := range client.Channels { 311 | ok, err := c.RemoveFromChannel(ID, channel) 312 | if !ok || err != nil { 313 | return false, err 314 | } 315 | } 316 | 317 | // Remove client from clients 318 | _, err = c.Driver.HDel(ClientsHashPrefix, ID) 319 | 320 | if err != nil { 321 | log.Errorf( 322 | `Error while deleting client %s: %s`, 323 | ID, 324 | err.Error(), 325 | ) 326 | return false, fmt.Errorf( 327 | `Error while deleting client %s`, 328 | ID, 329 | ) 330 | } 331 | 332 | log.Infof( 333 | `Client %s got deleted`, 334 | ID, 335 | ) 336 | 337 | return true, nil 338 | } 339 | 340 | // Unsubscribe from channels 341 | func (c *Client) Unsubscribe(ID string, channels []string) (bool, error) { 342 | 343 | validator := util.Validator{} 344 | clientResult, err := c.GetClientByID(ID) 345 | 346 | if err != nil { 347 | return false, err 348 | } 349 | 350 | for i, channel := range clientResult.Channels { 351 | if validator.IsIn(channel, channels) { 352 | ok, err := c.RemoveFromChannel(ID, channel) 353 | if !ok || err != nil { 354 | return false, err 355 | } 356 | clientResult.Channels = util.Unset(clientResult.Channels, i) 357 | } 358 | } 359 | 360 | return c.UpdateClientByID(clientResult) 361 | } 362 | 363 | // Subscribe to channels 364 | func (c *Client) Subscribe(ID string, channels []string) (bool, error) { 365 | 366 | validator := util.Validator{} 367 | clientResult, err := c.GetClientByID(ID) 368 | 369 | if err != nil { 370 | return false, err 371 | } 372 | 373 | for _, channel := range channels { 374 | if !validator.IsIn(channel, clientResult.Channels) { 375 | ok, err := c.AddToChannel(ID, channel) 376 | if !ok || err != nil { 377 | return false, err 378 | } 379 | clientResult.Channels = append(clientResult.Channels, channel) 380 | } 381 | } 382 | 383 | return c.UpdateClientByID(clientResult) 384 | } 385 | 386 | // AddToChannel adds a client to a channel 387 | func (c *Client) AddToChannel(ID string, channel string) (bool, error) { 388 | // Remove client from channel subscribers 389 | _, err := c.Driver.HSet(fmt.Sprintf("%s.subscribers", channel), ID, "") 390 | 391 | if err != nil { 392 | log.Errorf( 393 | `Error while adding client %s to channel %s: %s`, 394 | ID, 395 | fmt.Sprintf("%s.subscribers", channel), 396 | err.Error(), 397 | ) 398 | return false, fmt.Errorf( 399 | `Error while adding client %s to channel %s`, 400 | ID, 401 | fmt.Sprintf("%s.subscribers", channel), 402 | ) 403 | } 404 | 405 | log.Infof( 406 | `Client %s added to channel %s`, 407 | ID, 408 | channel, 409 | ) 410 | 411 | return true, nil 412 | } 413 | 414 | // RemoveFromChannel removes a client from a channel 415 | func (c *Client) RemoveFromChannel(ID string, channel string) (bool, error) { 416 | // Remove client from channel subscribers 417 | _, err := c.Driver.HDel(fmt.Sprintf("%s.subscribers", channel), ID) 418 | 419 | if err != nil { 420 | log.Errorf( 421 | `Error while removing client %s from channel %s: %s`, 422 | ID, 423 | fmt.Sprintf("%s.subscribers", channel), 424 | err.Error(), 425 | ) 426 | return false, fmt.Errorf( 427 | `Error while removing client %s from %s`, 428 | ID, 429 | fmt.Sprintf("%s.subscribers", channel), 430 | ) 431 | } 432 | 433 | // Delete from listeners if stale 434 | _, err = c.Driver.HDel(fmt.Sprintf("%s.listeners", channel), ID) 435 | 436 | if err != nil { 437 | log.Errorf( 438 | `Error while removing client %s from channel %s: %s`, 439 | ID, 440 | fmt.Sprintf("%s.listeners", channel), 441 | err.Error(), 442 | ) 443 | return false, fmt.Errorf( 444 | `Error while removing client %s from %s`, 445 | ID, 446 | fmt.Sprintf("%s.listeners", channel), 447 | ) 448 | } 449 | 450 | log.Infof( 451 | `Client %s removed from channel %s`, 452 | ID, 453 | channel, 454 | ) 455 | 456 | return true, nil 457 | } 458 | 459 | // Connect a client 460 | func (c *Client) Connect(clientResult ClientResult) (bool, error) { 461 | for _, channel := range clientResult.Channels { 462 | // Remove client from channel listeners 463 | _, err := c.Driver.HSet(fmt.Sprintf("%s.listeners", channel), clientResult.ID, "") 464 | 465 | if err != nil { 466 | log.Errorf( 467 | `Error while adding client %s to channel %s: %s`, 468 | clientResult.ID, 469 | fmt.Sprintf("%s.listeners", channel), 470 | err.Error(), 471 | ) 472 | return false, fmt.Errorf( 473 | `Error while adding client %s to channel %s`, 474 | clientResult.ID, 475 | fmt.Sprintf("%s.listeners", channel), 476 | ) 477 | } 478 | } 479 | 480 | log.Infof( 481 | `Client %s connected to all subscribed channels`, 482 | clientResult.ID, 483 | ) 484 | 485 | return true, nil 486 | } 487 | 488 | // Disconnect a client 489 | func (c *Client) Disconnect(clientResult ClientResult) (bool, error) { 490 | for _, channel := range clientResult.Channels { 491 | // Remove client from channel listeners 492 | _, err := c.Driver.HDel(fmt.Sprintf("%s.listeners", channel), clientResult.ID) 493 | 494 | if err != nil { 495 | log.Errorf( 496 | `Error while removing client %s from channel %s: %s`, 497 | clientResult.ID, 498 | fmt.Sprintf("%s.listeners", channel), 499 | err.Error(), 500 | ) 501 | return false, fmt.Errorf( 502 | "Error while removing client %s from %s", 503 | clientResult.ID, 504 | fmt.Sprintf("%s.listeners", channel), 505 | ) 506 | } 507 | } 508 | 509 | log.Infof( 510 | `Client %s disconnected from all subscribed channels`, 511 | clientResult.ID, 512 | ) 513 | 514 | return true, nil 515 | } 516 | -------------------------------------------------------------------------------- /core/api/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package api 6 | 7 | import ( 8 | "encoding/json" 9 | "fmt" 10 | 11 | "github.com/clivern/beaver/core/driver" 12 | 13 | log "github.com/sirupsen/logrus" 14 | ) 15 | 16 | // ConfigsHashPrefix is the hash prefix 17 | const ConfigsHashPrefix string = "beaver.config" 18 | 19 | // Config struct 20 | type Config struct { 21 | Driver *driver.Redis 22 | } 23 | 24 | // ConfigResult struct 25 | type ConfigResult struct { 26 | Key string `json:"key"` 27 | Value string `json:"value"` 28 | } 29 | 30 | // LoadFromJSON load object from json 31 | func (c *ConfigResult) LoadFromJSON(data []byte) (bool, error) { 32 | err := json.Unmarshal(data, &c) 33 | 34 | if err != nil { 35 | return false, err 36 | } 37 | 38 | return true, nil 39 | } 40 | 41 | // ConvertToJSON converts object to json 42 | func (c *ConfigResult) ConvertToJSON() (string, error) { 43 | data, err := json.Marshal(&c) 44 | 45 | if err != nil { 46 | return "", err 47 | } 48 | 49 | return string(data), nil 50 | } 51 | 52 | // Init initialize the redis connection 53 | func (c *Config) Init() bool { 54 | c.Driver = driver.NewRedisDriver() 55 | 56 | result, err := c.Driver.Connect() 57 | 58 | if !result { 59 | log.Errorf( 60 | `Error while connecting to redis: %s`, 61 | err.Error(), 62 | ) 63 | return false 64 | } 65 | 66 | log.Infof(`Redis connection established`) 67 | 68 | return true 69 | } 70 | 71 | // CreateConfig creates a config 72 | func (c *Config) CreateConfig(key string, value string) (bool, error) { 73 | exists, err := c.Driver.HExists(ConfigsHashPrefix, key) 74 | 75 | if err != nil { 76 | log.Errorf( 77 | `Error while creating config %s: %s`, 78 | key, 79 | err.Error(), 80 | ) 81 | 82 | return false, fmt.Errorf( 83 | `Error while creating config %s`, 84 | key, 85 | ) 86 | } 87 | 88 | if exists { 89 | log.Warningf( 90 | `Trying to create existent config %s`, 91 | key, 92 | ) 93 | 94 | return false, fmt.Errorf( 95 | `Trying to create existent config %s`, 96 | key, 97 | ) 98 | } 99 | 100 | _, err = c.Driver.HSet(ConfigsHashPrefix, key, value) 101 | 102 | if err != nil { 103 | log.Errorf( 104 | `Error while creating config %s: %s`, 105 | key, 106 | err.Error(), 107 | ) 108 | 109 | return false, fmt.Errorf( 110 | `Error while creating config %s`, 111 | key, 112 | ) 113 | } 114 | 115 | log.Infof( 116 | `Config %s got created`, 117 | key, 118 | ) 119 | 120 | return true, nil 121 | } 122 | 123 | // GetConfigByKey gets a config value with key 124 | func (c *Config) GetConfigByKey(key string) (string, error) { 125 | 126 | exists, err := c.Driver.HExists(ConfigsHashPrefix, key) 127 | 128 | if err != nil { 129 | log.Errorf( 130 | `Error while getting config %s: %s`, 131 | key, 132 | err.Error(), 133 | ) 134 | 135 | return "", fmt.Errorf( 136 | `Error while getting config %s`, 137 | key, 138 | ) 139 | } 140 | 141 | if !exists { 142 | log.Warningf( 143 | `Trying to get non existent config %s`, 144 | key, 145 | ) 146 | 147 | return "", fmt.Errorf( 148 | `Trying to get non existent config %s`, 149 | key, 150 | ) 151 | } 152 | 153 | value, err := c.Driver.HGet(ConfigsHashPrefix, key) 154 | 155 | if err != nil { 156 | log.Errorf( 157 | `Error while getting config %s: %s`, 158 | key, 159 | err.Error(), 160 | ) 161 | 162 | return "", fmt.Errorf( 163 | `Error while getting config %s`, 164 | key, 165 | ) 166 | } 167 | 168 | return value, nil 169 | } 170 | 171 | // UpdateConfigByKey updates a config with key 172 | func (c *Config) UpdateConfigByKey(key string, value string) (bool, error) { 173 | 174 | exists, err := c.Driver.HExists(ConfigsHashPrefix, key) 175 | 176 | if err != nil { 177 | log.Errorf( 178 | `Error while updating config %s: %s`, 179 | key, 180 | err.Error(), 181 | ) 182 | 183 | return false, fmt.Errorf( 184 | `Error while updating config %s`, 185 | key, 186 | ) 187 | } 188 | 189 | if !exists { 190 | log.Warningf( 191 | `Trying to update non existent config %s`, 192 | key, 193 | ) 194 | 195 | return false, fmt.Errorf( 196 | `Trying to update non existent config %s`, 197 | key, 198 | ) 199 | } 200 | 201 | _, err = c.Driver.HSet(ConfigsHashPrefix, key, value) 202 | 203 | if err != nil { 204 | log.Errorf( 205 | `Error while updating config %s: %s`, 206 | key, 207 | err.Error(), 208 | ) 209 | 210 | return false, fmt.Errorf( 211 | `Error while updating config %s`, 212 | key, 213 | ) 214 | } 215 | 216 | log.Infof( 217 | `Config %s got updated`, 218 | key, 219 | ) 220 | 221 | return true, nil 222 | 223 | } 224 | 225 | // DeleteConfigByKey deletes a config with key 226 | func (c *Config) DeleteConfigByKey(key string) (bool, error) { 227 | 228 | deleted, err := c.Driver.HDel(ConfigsHashPrefix, key) 229 | 230 | if err != nil { 231 | log.Errorf( 232 | `Error while deleting config %s: %s`, 233 | key, 234 | err.Error(), 235 | ) 236 | 237 | return false, fmt.Errorf( 238 | `Error while deleting config %s`, 239 | key, 240 | ) 241 | } 242 | 243 | if deleted <= 0 { 244 | log.Warningf( 245 | `Trying to delete non existent config %s`, 246 | key, 247 | ) 248 | 249 | return false, fmt.Errorf( 250 | `Trying to delete non existent config %s`, 251 | key, 252 | ) 253 | } 254 | 255 | log.Infof( 256 | `Config %s got deleted`, 257 | key, 258 | ) 259 | 260 | return true, nil 261 | } 262 | -------------------------------------------------------------------------------- /core/controller/channel.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package controller 6 | 7 | import ( 8 | "net/http" 9 | "time" 10 | 11 | "github.com/clivern/beaver/core/api" 12 | "github.com/clivern/beaver/core/util" 13 | 14 | "github.com/gin-gonic/gin" 15 | ) 16 | 17 | // GetChannelByName controller 18 | func GetChannelByName(c *gin.Context) { 19 | 20 | var channelResult api.ChannelResult 21 | validate := util.Validator{} 22 | 23 | name := c.Param("name") 24 | 25 | if validate.IsEmpty(name) || !validate.IsSlug(name, 3, 60) { 26 | c.JSON(http.StatusBadRequest, gin.H{ 27 | "status": "error", 28 | "error": "Channel name must be alphanumeric with length from 3 to 60", 29 | }) 30 | return 31 | } 32 | 33 | channel := api.Channel{} 34 | 35 | if !channel.Init() { 36 | c.JSON(http.StatusServiceUnavailable, gin.H{ 37 | "status": "error", 38 | "error": "Internal server error", 39 | }) 40 | return 41 | } 42 | 43 | channelResult, err := channel.GetChannelByName(name) 44 | 45 | if err != nil { 46 | c.JSON(http.StatusNotFound, gin.H{ 47 | "status": "error", 48 | "error": err.Error(), 49 | }) 50 | return 51 | } 52 | 53 | if channelResult.Type == "presence" { 54 | c.JSON(http.StatusOK, gin.H{ 55 | "name": channelResult.Name, 56 | "type": channelResult.Type, 57 | "subscribers_count": channel.CountSubscribers(name), 58 | "listeners_count": channel.CountListeners(name), 59 | "subscribers": channel.GetSubscribers(name), 60 | "listeners": channel.GetListeners(name), 61 | "created_at": channelResult.CreatedAt, 62 | "updated_at": channelResult.UpdatedAt, 63 | }) 64 | return 65 | } 66 | 67 | c.JSON(http.StatusOK, gin.H{ 68 | "name": channelResult.Name, 69 | "type": channelResult.Type, 70 | "subscribers_count": channel.CountSubscribers(name), 71 | "listeners_count": channel.CountListeners(name), 72 | "created_at": channelResult.CreatedAt, 73 | "updated_at": channelResult.UpdatedAt, 74 | }) 75 | } 76 | 77 | // CreateChannel controller 78 | func CreateChannel(c *gin.Context) { 79 | 80 | var channelResult api.ChannelResult 81 | validate := util.Validator{} 82 | 83 | channel := api.Channel{} 84 | 85 | rawBody, err := c.GetRawData() 86 | 87 | if err != nil { 88 | c.JSON(http.StatusBadRequest, gin.H{ 89 | "status": "error", 90 | "error": "Invalid request", 91 | }) 92 | return 93 | } 94 | 95 | ok, err := channelResult.LoadFromJSON(rawBody) 96 | 97 | if !ok || err != nil { 98 | c.JSON(http.StatusBadRequest, gin.H{ 99 | "status": "error", 100 | "error": "Invalid request", 101 | }) 102 | return 103 | } 104 | 105 | if validate.IsEmpty(channelResult.Name) || !validate.IsSlug(channelResult.Name, 3, 60) { 106 | c.JSON(http.StatusBadRequest, gin.H{ 107 | "status": "error", 108 | "error": "Channel name must be alphanumeric with length from 3 to 60", 109 | }) 110 | return 111 | } 112 | 113 | if !validate.IsIn(channelResult.Type, []string{"public", "private", "presence"}) { 114 | c.JSON(http.StatusBadRequest, gin.H{ 115 | "status": "error", 116 | "error": "Channel type must be public, private or presence", 117 | }) 118 | return 119 | } 120 | 121 | if !channel.Init() { 122 | c.JSON(http.StatusServiceUnavailable, gin.H{ 123 | "status": "error", 124 | "error": "Internal server error", 125 | }) 126 | return 127 | } 128 | 129 | channelResult.CreatedAt = time.Now().Unix() 130 | channelResult.UpdatedAt = time.Now().Unix() 131 | 132 | ok, err = channel.CreateChannel(channelResult) 133 | 134 | if !ok || err != nil { 135 | c.JSON(http.StatusBadRequest, gin.H{ 136 | "status": "error", 137 | "error": err.Error(), 138 | }) 139 | return 140 | } 141 | 142 | c.Status(http.StatusCreated) 143 | } 144 | 145 | // DeleteChannelByName controller 146 | func DeleteChannelByName(c *gin.Context) { 147 | 148 | validate := util.Validator{} 149 | 150 | name := c.Param("name") 151 | 152 | if validate.IsEmpty(name) || !validate.IsSlug(name, 3, 60) { 153 | c.JSON(http.StatusBadRequest, gin.H{ 154 | "status": "error", 155 | "error": "Channel name must be alphanumeric with length from 3 to 60", 156 | }) 157 | return 158 | } 159 | 160 | channel := api.Channel{} 161 | 162 | if !channel.Init() { 163 | c.JSON(http.StatusServiceUnavailable, gin.H{ 164 | "status": "error", 165 | "error": "Internal server error", 166 | }) 167 | return 168 | } 169 | 170 | _, err := channel.DeleteChannelByName(name) 171 | 172 | if err != nil { 173 | c.JSON(http.StatusNotFound, gin.H{ 174 | "status": "error", 175 | "error": err.Error(), 176 | }) 177 | return 178 | } 179 | 180 | c.Status(http.StatusNoContent) 181 | } 182 | 183 | // UpdateChannelByName controller 184 | func UpdateChannelByName(c *gin.Context) { 185 | 186 | var channelResult api.ChannelResult 187 | var currentChannelResult api.ChannelResult 188 | validate := util.Validator{} 189 | 190 | channel := api.Channel{} 191 | 192 | rawBody, err := c.GetRawData() 193 | 194 | if err != nil { 195 | c.JSON(http.StatusBadRequest, gin.H{ 196 | "status": "error", 197 | "error": "Invalid request", 198 | }) 199 | return 200 | } 201 | 202 | channelResult.LoadFromJSON(rawBody) 203 | channelResult.Name = c.Param("name") 204 | 205 | if validate.IsEmpty(channelResult.Name) || !validate.IsSlug(channelResult.Name, 3, 60) { 206 | c.JSON(http.StatusBadRequest, gin.H{ 207 | "status": "error", 208 | "error": "Channel name must be alphanumeric with length from 3 to 60", 209 | }) 210 | return 211 | } 212 | 213 | if !validate.IsIn(channelResult.Type, []string{"public", "private", "presence"}) { 214 | c.JSON(http.StatusBadRequest, gin.H{ 215 | "status": "error", 216 | "error": "Channel type must be public, private or presence", 217 | }) 218 | return 219 | } 220 | 221 | if !channel.Init() { 222 | c.JSON(http.StatusServiceUnavailable, gin.H{ 223 | "status": "error", 224 | "error": "Internal server error", 225 | }) 226 | return 227 | } 228 | 229 | currentChannelResult, err = channel.GetChannelByName(channelResult.Name) 230 | 231 | if err != nil { 232 | c.JSON(http.StatusNotFound, gin.H{ 233 | "status": "error", 234 | "error": err.Error(), 235 | }) 236 | return 237 | } 238 | 239 | // Update type & updated_at 240 | currentChannelResult.Type = channelResult.Type 241 | currentChannelResult.UpdatedAt = time.Now().Unix() 242 | 243 | ok, err := channel.UpdateChannelByName(currentChannelResult) 244 | 245 | if !ok || err != nil { 246 | c.JSON(http.StatusBadRequest, gin.H{ 247 | "status": "error", 248 | "error": err.Error(), 249 | }) 250 | return 251 | } 252 | 253 | c.Status(http.StatusOK) 254 | } 255 | -------------------------------------------------------------------------------- /core/controller/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package controller 6 | 7 | import ( 8 | "net/http" 9 | 10 | "github.com/clivern/beaver/core/api" 11 | "github.com/clivern/beaver/core/util" 12 | 13 | "github.com/gin-gonic/gin" 14 | ) 15 | 16 | // GetClientByID controller 17 | func GetClientByID(c *gin.Context) { 18 | 19 | var clientResult api.ClientResult 20 | validate := util.Validator{} 21 | 22 | ID := c.Param("id") 23 | 24 | if validate.IsEmpty(ID) || !validate.IsUUID4(ID) { 25 | c.JSON(http.StatusBadRequest, gin.H{ 26 | "status": "error", 27 | "error": "Client ID is invalid.", 28 | }) 29 | return 30 | } 31 | 32 | client := api.Client{} 33 | 34 | if !client.Init() { 35 | c.JSON(http.StatusServiceUnavailable, gin.H{ 36 | "status": "error", 37 | "error": "Internal server error", 38 | }) 39 | return 40 | } 41 | 42 | clientResult, err := client.GetClientByID(ID) 43 | 44 | if err != nil { 45 | c.JSON(http.StatusNotFound, gin.H{ 46 | "status": "error", 47 | "error": err.Error(), 48 | }) 49 | return 50 | } 51 | 52 | c.JSON(http.StatusOK, gin.H{ 53 | "id": clientResult.ID, 54 | "token": clientResult.Token, 55 | "channels": clientResult.Channels, 56 | "created_at": clientResult.CreatedAt, 57 | "updated_at": clientResult.UpdatedAt, 58 | }) 59 | } 60 | 61 | // CreateClient controller 62 | func CreateClient(c *gin.Context) { 63 | 64 | var clientResult api.ClientResult 65 | validate := util.Validator{} 66 | 67 | client := api.Client{} 68 | channel := api.Channel{} 69 | 70 | rawBody, err := c.GetRawData() 71 | 72 | if err != nil { 73 | c.JSON(http.StatusBadRequest, gin.H{ 74 | "status": "error", 75 | "error": "Invalid request", 76 | }) 77 | return 78 | } 79 | 80 | ok, err := clientResult.LoadFromJSON(rawBody) 81 | 82 | if !ok || err != nil { 83 | c.JSON(http.StatusBadRequest, gin.H{ 84 | "status": "error", 85 | "error": "Invalid request", 86 | }) 87 | return 88 | } 89 | 90 | if !validate.IsSlugs(clientResult.Channels, 3, 60) { 91 | c.JSON(http.StatusBadRequest, gin.H{ 92 | "status": "error", 93 | "error": "Provided client channels are invalid.", 94 | }) 95 | return 96 | } 97 | 98 | if !client.Init() || !channel.Init() { 99 | c.JSON(http.StatusServiceUnavailable, gin.H{ 100 | "status": "error", 101 | "error": "Internal server error", 102 | }) 103 | return 104 | } 105 | 106 | ok, err = channel.ChannelsExist(clientResult.Channels) 107 | 108 | if !ok || err != nil { 109 | c.JSON(http.StatusBadRequest, gin.H{ 110 | "status": "error", 111 | "error": err.Error(), 112 | }) 113 | return 114 | } 115 | 116 | ok, err = clientResult.GenerateClient() 117 | 118 | if !ok || err != nil { 119 | c.JSON(http.StatusBadRequest, gin.H{ 120 | "status": "error", 121 | "error": err.Error(), 122 | }) 123 | return 124 | } 125 | 126 | ok, err = client.CreateClient(clientResult) 127 | 128 | if !ok || err != nil { 129 | c.JSON(http.StatusBadRequest, gin.H{ 130 | "status": "error", 131 | "error": err.Error(), 132 | }) 133 | return 134 | } 135 | 136 | c.JSON(http.StatusCreated, gin.H{ 137 | "id": clientResult.ID, 138 | "token": clientResult.Token, 139 | "channels": clientResult.Channels, 140 | "created_at": clientResult.CreatedAt, 141 | "updated_at": clientResult.UpdatedAt, 142 | }) 143 | } 144 | 145 | // DeleteClientByID controller 146 | func DeleteClientByID(c *gin.Context) { 147 | 148 | validate := util.Validator{} 149 | ID := c.Param("id") 150 | 151 | if validate.IsEmpty(ID) || !validate.IsUUID4(ID) { 152 | c.JSON(http.StatusBadRequest, gin.H{ 153 | "status": "error", 154 | "error": "Client ID is invalid.", 155 | }) 156 | return 157 | } 158 | 159 | client := api.Client{} 160 | 161 | if !client.Init() { 162 | c.JSON(http.StatusServiceUnavailable, gin.H{ 163 | "status": "error", 164 | "error": "Internal server error", 165 | }) 166 | return 167 | } 168 | 169 | _, err := client.DeleteClientByID(ID) 170 | 171 | if err != nil { 172 | c.JSON(http.StatusNotFound, gin.H{ 173 | "status": "error", 174 | "error": err.Error(), 175 | }) 176 | return 177 | } 178 | 179 | c.Status(http.StatusNoContent) 180 | } 181 | 182 | // Unsubscribe controller 183 | func Unsubscribe(c *gin.Context) { 184 | 185 | var clientResult api.ClientResult 186 | validate := util.Validator{} 187 | ID := c.Param("id") 188 | 189 | if validate.IsEmpty(ID) || !validate.IsUUID4(ID) { 190 | c.JSON(http.StatusBadRequest, gin.H{ 191 | "status": "error", 192 | "error": "Client ID is invalid.", 193 | }) 194 | return 195 | } 196 | 197 | client := api.Client{} 198 | channel := api.Channel{} 199 | 200 | rawBody, err := c.GetRawData() 201 | 202 | if err != nil { 203 | c.JSON(http.StatusBadRequest, gin.H{ 204 | "status": "error", 205 | "error": "Invalid request", 206 | }) 207 | return 208 | } 209 | 210 | ok, err := clientResult.LoadFromJSON(rawBody) 211 | 212 | if !ok || err != nil { 213 | c.JSON(http.StatusBadRequest, gin.H{ 214 | "status": "error", 215 | "error": "Invalid request", 216 | }) 217 | return 218 | } 219 | 220 | if !validate.IsSlugs(clientResult.Channels, 3, 60) { 221 | c.JSON(http.StatusBadRequest, gin.H{ 222 | "status": "error", 223 | "error": "Provided client channels are invalid.", 224 | }) 225 | return 226 | } 227 | 228 | if !client.Init() || !channel.Init() { 229 | c.JSON(http.StatusServiceUnavailable, gin.H{ 230 | "status": "error", 231 | "error": "Internal server error", 232 | }) 233 | return 234 | } 235 | 236 | ok, err = channel.ChannelsExist(clientResult.Channels) 237 | 238 | if !ok || err != nil { 239 | c.JSON(http.StatusBadRequest, gin.H{ 240 | "status": "error", 241 | "error": err.Error(), 242 | }) 243 | return 244 | } 245 | 246 | ok, err = client.Unsubscribe(ID, clientResult.Channels) 247 | 248 | if !ok || err != nil { 249 | c.JSON(http.StatusBadRequest, gin.H{ 250 | "status": "error", 251 | "error": err.Error(), 252 | }) 253 | return 254 | } 255 | 256 | c.Status(http.StatusOK) 257 | } 258 | 259 | // Subscribe controller 260 | func Subscribe(c *gin.Context) { 261 | 262 | var clientResult api.ClientResult 263 | validate := util.Validator{} 264 | ID := c.Param("id") 265 | 266 | if validate.IsEmpty(ID) || !validate.IsUUID4(ID) { 267 | c.JSON(http.StatusBadRequest, gin.H{ 268 | "status": "error", 269 | "error": "Client ID is invalid.", 270 | }) 271 | return 272 | } 273 | 274 | client := api.Client{} 275 | channel := api.Channel{} 276 | 277 | rawBody, err := c.GetRawData() 278 | 279 | if err != nil { 280 | c.JSON(http.StatusBadRequest, gin.H{ 281 | "status": "error", 282 | "error": "Invalid request", 283 | }) 284 | return 285 | } 286 | 287 | ok, err := clientResult.LoadFromJSON(rawBody) 288 | 289 | if !ok || err != nil { 290 | c.JSON(http.StatusBadRequest, gin.H{ 291 | "status": "error", 292 | "error": "Invalid request", 293 | }) 294 | return 295 | } 296 | 297 | if !validate.IsSlugs(clientResult.Channels, 3, 60) { 298 | c.JSON(http.StatusBadRequest, gin.H{ 299 | "status": "error", 300 | "error": "Provided client channels are invalid.", 301 | }) 302 | return 303 | } 304 | 305 | if !client.Init() || !channel.Init() { 306 | c.JSON(http.StatusServiceUnavailable, gin.H{ 307 | "status": "error", 308 | "error": "Internal server error", 309 | }) 310 | return 311 | } 312 | 313 | ok, err = channel.ChannelsExist(clientResult.Channels) 314 | 315 | if !ok || err != nil { 316 | c.JSON(http.StatusBadRequest, gin.H{ 317 | "status": "error", 318 | "error": err.Error(), 319 | }) 320 | return 321 | } 322 | 323 | ok, err = client.Subscribe(ID, clientResult.Channels) 324 | 325 | if !ok || err != nil { 326 | c.JSON(http.StatusBadRequest, gin.H{ 327 | "status": "error", 328 | "error": err.Error(), 329 | }) 330 | return 331 | } 332 | 333 | c.Status(http.StatusOK) 334 | } 335 | -------------------------------------------------------------------------------- /core/controller/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package controller 6 | 7 | import ( 8 | "net/http" 9 | 10 | "github.com/clivern/beaver/core/api" 11 | "github.com/clivern/beaver/core/util" 12 | 13 | "github.com/gin-gonic/gin" 14 | ) 15 | 16 | // GetConfigByKey controller 17 | func GetConfigByKey(c *gin.Context) { 18 | key := c.Param("key") 19 | validate := util.Validator{} 20 | 21 | if validate.IsEmpty(key) || !validate.IsSlug(key, 3, 60) { 22 | c.JSON(http.StatusBadRequest, gin.H{ 23 | "status": "error", 24 | "error": "Config key must be alphanumeric with length from 3 to 60", 25 | }) 26 | return 27 | } 28 | 29 | config := api.Config{} 30 | 31 | if !config.Init() { 32 | c.JSON(http.StatusServiceUnavailable, gin.H{ 33 | "status": "error", 34 | "error": "Internal server error", 35 | }) 36 | return 37 | } 38 | 39 | value, err := config.GetConfigByKey(key) 40 | 41 | if err != nil { 42 | c.JSON(http.StatusNotFound, gin.H{ 43 | "status": "error", 44 | "error": err.Error(), 45 | }) 46 | return 47 | } 48 | 49 | c.JSON(http.StatusOK, gin.H{ 50 | "key": key, 51 | "value": value, 52 | }) 53 | } 54 | 55 | // CreateConfig controller 56 | func CreateConfig(c *gin.Context) { 57 | 58 | validate := util.Validator{} 59 | var configRequest api.ConfigResult 60 | 61 | config := api.Config{} 62 | 63 | rawBody, err := c.GetRawData() 64 | 65 | if err != nil { 66 | c.JSON(http.StatusBadRequest, gin.H{ 67 | "status": "error", 68 | "error": "Invalid request", 69 | }) 70 | return 71 | } 72 | 73 | ok, err := configRequest.LoadFromJSON(rawBody) 74 | 75 | if !ok || err != nil { 76 | c.JSON(http.StatusBadRequest, gin.H{ 77 | "status": "error", 78 | "error": "Invalid request", 79 | }) 80 | return 81 | } 82 | 83 | if validate.IsEmpty(configRequest.Key) || !validate.IsSlug(configRequest.Key, 3, 60) { 84 | c.JSON(http.StatusBadRequest, gin.H{ 85 | "status": "error", 86 | "error": "Config key must be alphanumeric with length from 3 to 60", 87 | }) 88 | return 89 | } 90 | 91 | if validate.IsEmpty(configRequest.Value) { 92 | c.JSON(http.StatusBadRequest, gin.H{ 93 | "status": "error", 94 | "error": "Config value must not be empty", 95 | }) 96 | return 97 | } 98 | 99 | if !config.Init() { 100 | c.JSON(http.StatusServiceUnavailable, gin.H{ 101 | "status": "error", 102 | "error": "Internal server error", 103 | }) 104 | return 105 | } 106 | 107 | _, err = config.CreateConfig(configRequest.Key, configRequest.Value) 108 | 109 | if err != nil { 110 | c.JSON(http.StatusBadRequest, gin.H{ 111 | "status": "error", 112 | "error": err.Error(), 113 | }) 114 | return 115 | } 116 | 117 | c.Status(http.StatusCreated) 118 | } 119 | 120 | // DeleteConfigByKey controller 121 | func DeleteConfigByKey(c *gin.Context) { 122 | key := c.Param("key") 123 | validate := util.Validator{} 124 | 125 | if validate.IsEmpty(key) || !validate.IsSlug(key, 3, 60) { 126 | c.JSON(http.StatusBadRequest, gin.H{ 127 | "status": "error", 128 | "error": "Config key must be alphanumeric with length from 3 to 60", 129 | }) 130 | return 131 | } 132 | 133 | config := api.Config{} 134 | 135 | if !config.Init() { 136 | c.JSON(http.StatusServiceUnavailable, gin.H{ 137 | "status": "error", 138 | "error": "Internal server error", 139 | }) 140 | return 141 | } 142 | 143 | _, err := config.DeleteConfigByKey(key) 144 | 145 | if err != nil { 146 | c.JSON(http.StatusNotFound, gin.H{ 147 | "status": "error", 148 | "error": err.Error(), 149 | }) 150 | return 151 | } 152 | 153 | c.Status(http.StatusNoContent) 154 | } 155 | 156 | // UpdateConfigByKey controller 157 | func UpdateConfigByKey(c *gin.Context) { 158 | 159 | var configRequest api.ConfigResult 160 | validate := util.Validator{} 161 | 162 | config := api.Config{} 163 | 164 | rawBody, err := c.GetRawData() 165 | 166 | if err != nil { 167 | c.JSON(http.StatusBadRequest, gin.H{ 168 | "status": "error", 169 | "error": "Invalid request", 170 | }) 171 | return 172 | } 173 | 174 | configRequest.LoadFromJSON(rawBody) 175 | configRequest.Key = c.Param("key") 176 | 177 | if validate.IsEmpty(configRequest.Key) || !validate.IsSlug(configRequest.Key, 3, 60) { 178 | c.JSON(http.StatusBadRequest, gin.H{ 179 | "status": "error", 180 | "error": "Config key must be alphanumeric with length from 3 to 60", 181 | }) 182 | return 183 | } 184 | 185 | if validate.IsEmpty(configRequest.Value) { 186 | c.JSON(http.StatusBadRequest, gin.H{ 187 | "status": "error", 188 | "error": "Config value must not be empty", 189 | }) 190 | return 191 | } 192 | 193 | if !config.Init() { 194 | c.JSON(http.StatusServiceUnavailable, gin.H{ 195 | "status": "error", 196 | "error": "Internal server error", 197 | }) 198 | return 199 | } 200 | 201 | _, err = config.UpdateConfigByKey(configRequest.Key, configRequest.Value) 202 | 203 | if err != nil { 204 | c.JSON(http.StatusBadRequest, gin.H{ 205 | "status": "error", 206 | "error": err.Error(), 207 | }) 208 | return 209 | } 210 | 211 | c.Status(http.StatusOK) 212 | } 213 | -------------------------------------------------------------------------------- /core/controller/health.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package controller 6 | 7 | import ( 8 | "net/http" 9 | 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | // Health controller 14 | func Health(c *gin.Context) { 15 | c.JSON(http.StatusOK, gin.H{ 16 | "status": "ok", 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /core/controller/home.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package controller 6 | 7 | import ( 8 | "net/http" 9 | 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | // Home controller 14 | func Home(c *gin.Context) { 15 | homeTpl := ` Beaver
Beaver

A Real Time Messaging Server.

` 16 | c.Writer.WriteHeader(http.StatusOK) 17 | c.Writer.Write([]byte(homeTpl)) 18 | return 19 | } 20 | -------------------------------------------------------------------------------- /core/controller/metrics.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package controller 6 | 7 | import ( 8 | "net/http" 9 | 10 | "github.com/prometheus/client_golang/prometheus/promhttp" 11 | ) 12 | 13 | // Metrics controller 14 | func Metrics() http.Handler { 15 | return promhttp.Handler() 16 | } 17 | -------------------------------------------------------------------------------- /core/controller/socket.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package controller 6 | 7 | import ( 8 | "encoding/json" 9 | "net/http" 10 | 11 | "github.com/clivern/beaver/core/api" 12 | "github.com/clivern/beaver/core/util" 13 | 14 | "github.com/gin-gonic/gin" 15 | "github.com/gorilla/websocket" 16 | log "github.com/sirupsen/logrus" 17 | ) 18 | 19 | // Message struct 20 | type Message struct { 21 | FromClient string `json:"from_client"` 22 | ToClient string `json:"to_client"` 23 | Channel string `json:"channel"` 24 | Data string `json:"data"` 25 | } 26 | 27 | // BroadcastRequest struct 28 | type BroadcastRequest struct { 29 | Channels []string `json:"channels"` 30 | Data string `json:"data"` 31 | } 32 | 33 | // PublishRequest struct 34 | type PublishRequest struct { 35 | Channel string `json:"channel"` 36 | Data string `json:"data"` 37 | } 38 | 39 | // Websocket Object 40 | type Websocket struct { 41 | Clients util.Map 42 | Broadcast chan Message 43 | Upgrader websocket.Upgrader 44 | } 45 | 46 | // IsValid checks if message is valid 47 | func (m *Message) IsValid() bool { 48 | validator := util.Validator{} 49 | return validator.IsJSON(m.Data) 50 | } 51 | 52 | // LoadFromJSON load object from json 53 | func (c *BroadcastRequest) LoadFromJSON(data []byte) (bool, error) { 54 | err := json.Unmarshal(data, &c) 55 | if err != nil { 56 | return false, err 57 | } 58 | return true, nil 59 | } 60 | 61 | // ConvertToJSON converts object to json 62 | func (c *BroadcastRequest) ConvertToJSON() (string, error) { 63 | data, err := json.Marshal(&c) 64 | if err != nil { 65 | return "", err 66 | } 67 | return string(data), nil 68 | } 69 | 70 | // LoadFromJSON load object from json 71 | func (c *PublishRequest) LoadFromJSON(data []byte) (bool, error) { 72 | err := json.Unmarshal(data, &c) 73 | if err != nil { 74 | return false, err 75 | } 76 | return true, nil 77 | } 78 | 79 | // ConvertToJSON converts object to json 80 | func (c *PublishRequest) ConvertToJSON() (string, error) { 81 | data, err := json.Marshal(&c) 82 | if err != nil { 83 | return "", err 84 | } 85 | return string(data), nil 86 | } 87 | 88 | // Init initialize the websocket object 89 | func (e *Websocket) Init() { 90 | e.Clients = util.NewMap() 91 | e.Broadcast = make(chan Message) 92 | e.Upgrader = websocket.Upgrader{ 93 | ReadBufferSize: 1024, 94 | WriteBufferSize: 1024, 95 | CheckOrigin: func(_ *http.Request) bool { 96 | return true 97 | }, 98 | } 99 | } 100 | 101 | // HandleConnections manage new clients 102 | func (e *Websocket) HandleConnections(w http.ResponseWriter, r *http.Request, ID string, token string) { 103 | 104 | var clientResult api.ClientResult 105 | validate := util.Validator{} 106 | 107 | // Validate client uuid & token 108 | if validate.IsEmpty(ID) || validate.IsEmpty(token) || !validate.IsUUID4(ID) { 109 | return 110 | } 111 | 112 | client := api.Client{} 113 | 114 | if !client.Init() { 115 | return 116 | } 117 | 118 | clientResult, err := client.GetClientByID(ID) 119 | 120 | if err != nil { 121 | return 122 | } 123 | 124 | // Ensure that client is alreay registered before 125 | if clientResult.ID != ID || clientResult.Token != token { 126 | return 127 | } 128 | 129 | ok, err := client.Connect(clientResult) 130 | 131 | if !ok || err != nil { 132 | return 133 | } 134 | 135 | // Upgrade initial GET request to a websocket 136 | ws, err := e.Upgrader.Upgrade(w, r, nil) 137 | 138 | if err != nil { 139 | log.Fatalf( 140 | `Error while upgrading the GET request to a websocket for client %s: %s`, 141 | ID, 142 | err.Error(), 143 | ) 144 | } 145 | 146 | // Make sure we close the connection when the function returns 147 | defer ws.Close() 148 | 149 | // Register our new client 150 | e.Clients.Set(ID, ws) 151 | 152 | log.Infof( 153 | `Client %s connected`, 154 | ID, 155 | ) 156 | 157 | for { 158 | var msg Message 159 | 160 | // Read in a new message as JSON and map it to a Message object 161 | err := ws.ReadJSON(&msg) 162 | 163 | if err != nil { 164 | e.Clients.Delete(ID) 165 | client.Disconnect(clientResult) 166 | log.Infof( 167 | `Client %s disconnected`, 168 | ID, 169 | ) 170 | break 171 | } 172 | 173 | msg.FromClient = ID 174 | 175 | if msg.IsValid() { 176 | // Send the newly received message to the broadcast channel 177 | e.Broadcast <- msg 178 | } 179 | } 180 | } 181 | 182 | // HandleMessages send messages to a specific connected client 183 | func (e *Websocket) HandleMessages() { 184 | 185 | validate := util.Validator{} 186 | 187 | for { 188 | // Grab the next message from the broadcast channel 189 | msg := <-e.Broadcast 190 | 191 | // Send to Client 192 | if msg.IsValid() && !validate.IsEmpty(msg.ToClient) && !validate.IsEmpty(msg.Channel) && validate.IsUUID4(msg.ToClient) { 193 | // Push message to that client if it still connected 194 | // or remove from clients if we can't deliver messages to 195 | // it anymore 196 | if client, ok := e.Clients.Get(msg.ToClient); ok { 197 | err := client.(*websocket.Conn).WriteJSON(msg) 198 | 199 | if err != nil { 200 | client.(*websocket.Conn).Close() 201 | e.Clients.Delete(msg.ToClient) 202 | } 203 | } 204 | } 205 | 206 | // Send to client Peers on a channel 207 | if msg.IsValid() && !validate.IsEmpty(msg.FromClient) && !validate.IsEmpty(msg.Channel) && validate.IsUUID4(msg.FromClient) { 208 | 209 | channel := api.Channel{} 210 | channel.Init() 211 | iter := channel.ChannelScan(msg.Channel).Iterator() 212 | 213 | for iter.Next() { 214 | 215 | if msg.FromClient == iter.Val() { 216 | continue 217 | } 218 | 219 | msg.ToClient = iter.Val() 220 | 221 | if msg.ToClient != "" && validate.IsUUID4(msg.ToClient) { 222 | if client, ok := e.Clients.Get(msg.ToClient); ok { 223 | err := client.(*websocket.Conn).WriteJSON(msg) 224 | if err != nil { 225 | client.(*websocket.Conn).Close() 226 | e.Clients.Delete(msg.ToClient) 227 | } 228 | } 229 | } 230 | } 231 | } 232 | } 233 | } 234 | 235 | // BroadcastAction controller 236 | func (e *Websocket) BroadcastAction(c *gin.Context, rawBody []byte) { 237 | 238 | var broadcastRequest BroadcastRequest 239 | var key string 240 | var msg Message 241 | 242 | validate := util.Validator{} 243 | 244 | broadcastRequest.LoadFromJSON(rawBody) 245 | 246 | if !validate.IsSlugs(broadcastRequest.Channels, 3, 60) { 247 | c.JSON(http.StatusBadRequest, gin.H{ 248 | "status": "error", 249 | "error": "Provided client channels are invalid.", 250 | }) 251 | return 252 | } 253 | 254 | channel := api.Channel{} 255 | 256 | if !channel.Init() { 257 | c.JSON(http.StatusServiceUnavailable, gin.H{ 258 | "status": "error", 259 | "error": "Internal server error", 260 | }) 261 | return 262 | } 263 | 264 | ok, err := channel.ChannelsExist(broadcastRequest.Channels) 265 | 266 | if !ok || err != nil { 267 | c.JSON(http.StatusBadRequest, gin.H{ 268 | "status": "error", 269 | "error": err.Error(), 270 | }) 271 | return 272 | } 273 | 274 | if !validate.IsJSON(broadcastRequest.Data) { 275 | c.JSON(http.StatusBadRequest, gin.H{ 276 | "status": "error", 277 | "error": "Message data is invalid JSON", 278 | }) 279 | return 280 | } 281 | 282 | for _, name := range broadcastRequest.Channels { 283 | // Push message to all subscribed clients 284 | iter := channel.ChannelScan(name).Iterator() 285 | 286 | for iter.Next() { 287 | key = iter.Val() 288 | if key != "" && validate.IsUUID4(key) { 289 | msg = Message{ 290 | ToClient: key, 291 | Data: broadcastRequest.Data, 292 | Channel: name, 293 | } 294 | 295 | e.Broadcast <- msg 296 | } 297 | } 298 | } 299 | 300 | c.Status(http.StatusOK) 301 | } 302 | 303 | // PublishAction controller 304 | func (e *Websocket) PublishAction(c *gin.Context, rawBody []byte) { 305 | 306 | var publishRequest PublishRequest 307 | var key string 308 | var msg Message 309 | 310 | validate := util.Validator{} 311 | 312 | publishRequest.LoadFromJSON(rawBody) 313 | 314 | if !validate.IsSlug(publishRequest.Channel, 3, 60) { 315 | c.JSON(http.StatusBadRequest, gin.H{ 316 | "status": "error", 317 | "error": "Provided client channel is invalid.", 318 | }) 319 | return 320 | } 321 | 322 | channel := api.Channel{} 323 | 324 | if !channel.Init() { 325 | c.JSON(http.StatusServiceUnavailable, gin.H{ 326 | "status": "error", 327 | "error": "Internal server error", 328 | }) 329 | return 330 | } 331 | 332 | ok, err := channel.ChannelExist(publishRequest.Channel) 333 | 334 | if !ok || err != nil { 335 | c.JSON(http.StatusBadRequest, gin.H{ 336 | "status": "error", 337 | "error": err.Error(), 338 | }) 339 | return 340 | } 341 | 342 | if !validate.IsJSON(publishRequest.Data) { 343 | c.JSON(http.StatusBadRequest, gin.H{ 344 | "status": "error", 345 | "error": "Message data is invalid JSON", 346 | }) 347 | return 348 | } 349 | 350 | // Push message to all subscribed clients 351 | iter := channel.ChannelScan(publishRequest.Channel).Iterator() 352 | 353 | for iter.Next() { 354 | key = iter.Val() 355 | if key != "" && validate.IsUUID4(key) { 356 | msg = Message{ 357 | ToClient: key, 358 | Data: publishRequest.Data, 359 | Channel: publishRequest.Channel, 360 | } 361 | 362 | e.Broadcast <- msg 363 | } 364 | } 365 | 366 | c.Status(http.StatusOK) 367 | } 368 | -------------------------------------------------------------------------------- /core/driver/redis.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package driver 6 | 7 | import ( 8 | "sync" 9 | "time" 10 | 11 | "github.com/go-redis/redis" 12 | "github.com/spf13/viper" 13 | ) 14 | 15 | var instance *redis.Client 16 | 17 | // Redis driver 18 | type Redis struct { 19 | sync.RWMutex 20 | Client *redis.Client 21 | Addr string 22 | Password string 23 | DB int 24 | } 25 | 26 | // NewRedisDriver create a new instance 27 | func NewRedisDriver() *Redis { 28 | return &Redis{ 29 | Addr: viper.GetString("app.database.redis.address"), 30 | Password: viper.GetString("app.database.redis.password"), 31 | DB: viper.GetInt("app.database.redis.db"), 32 | } 33 | } 34 | 35 | // Connect establish a redis connection 36 | func (r *Redis) Connect() (bool, error) { 37 | r.Lock() 38 | defer r.Unlock() 39 | 40 | // Reuse redis connections 41 | if instance == nil { 42 | r.Client = redis.NewClient(&redis.Options{ 43 | Addr: r.Addr, 44 | Password: r.Password, 45 | DB: r.DB, 46 | PoolSize: 10, 47 | PoolTimeout: 30 * time.Second, 48 | }) 49 | 50 | instance = r.Client 51 | 52 | _, err := r.Ping() 53 | 54 | if err != nil { 55 | return false, err 56 | } 57 | } else { 58 | r.Client = instance 59 | 60 | _, err := r.Ping() 61 | 62 | if err != nil { 63 | return false, err 64 | } 65 | } 66 | 67 | return true, nil 68 | } 69 | 70 | // Ping checks the redis connection 71 | func (r *Redis) Ping() (bool, error) { 72 | pong, err := r.Client.Ping().Result() 73 | 74 | if err != nil { 75 | return false, err 76 | } 77 | return pong == "PONG", nil 78 | } 79 | 80 | // Set sets a record 81 | func (r *Redis) Set(key, value string, expiration time.Duration) (bool, error) { 82 | result := r.Client.Set(key, value, expiration) 83 | 84 | if result.Err() != nil { 85 | return false, result.Err() 86 | } 87 | 88 | return result.Val() == "OK", nil 89 | } 90 | 91 | // Get gets a record value 92 | func (r *Redis) Get(key string) (string, error) { 93 | result := r.Client.Get(key) 94 | 95 | if result.Err() != nil { 96 | return "", result.Err() 97 | } 98 | 99 | return result.Val(), nil 100 | } 101 | 102 | // Exists deletes a record 103 | func (r *Redis) Exists(key string) (bool, error) { 104 | result := r.Client.Exists(key) 105 | 106 | if result.Err() != nil { 107 | return false, result.Err() 108 | } 109 | 110 | return result.Val() > 0, nil 111 | } 112 | 113 | // Del deletes a record 114 | func (r *Redis) Del(key string) (int64, error) { 115 | result := r.Client.Del(key) 116 | 117 | if result.Err() != nil { 118 | return 0, result.Err() 119 | } 120 | 121 | return result.Val(), nil 122 | } 123 | 124 | // HGet gets a record from hash 125 | func (r *Redis) HGet(key, field string) (string, error) { 126 | result := r.Client.HGet(key, field) 127 | 128 | if result.Err() != nil { 129 | return "", result.Err() 130 | } 131 | 132 | return result.Val(), nil 133 | } 134 | 135 | // HSet sets a record in hash 136 | func (r *Redis) HSet(key, field, value string) (bool, error) { 137 | result := r.Client.HSet(key, field, value) 138 | 139 | if result.Err() != nil { 140 | return false, result.Err() 141 | } 142 | 143 | return result.Val(), nil 144 | } 145 | 146 | // HExists checks if key exists on a hash 147 | func (r *Redis) HExists(key, field string) (bool, error) { 148 | result := r.Client.HExists(key, field) 149 | 150 | if result.Err() != nil { 151 | return false, result.Err() 152 | } 153 | 154 | return result.Val(), nil 155 | } 156 | 157 | // HDel deletes a hash record 158 | func (r *Redis) HDel(key, field string) (int64, error) { 159 | result := r.Client.HDel(key, field) 160 | 161 | if result.Err() != nil { 162 | return 0, result.Err() 163 | } 164 | 165 | return result.Val(), nil 166 | } 167 | 168 | // HLen count hash records 169 | func (r *Redis) HLen(key string) (int64, error) { 170 | result := r.Client.HLen(key) 171 | 172 | if result.Err() != nil { 173 | return 0, result.Err() 174 | } 175 | 176 | return result.Val(), nil 177 | } 178 | 179 | // HTruncate deletes a hash 180 | func (r *Redis) HTruncate(key string) (int64, error) { 181 | result := r.Client.Del(key) 182 | 183 | if result.Err() != nil { 184 | return 0, result.Err() 185 | } 186 | 187 | return result.Val(), nil 188 | } 189 | 190 | // HScan return an iterative obj for a hash 191 | func (r *Redis) HScan(key string, cursor uint64, match string, count int64) *redis.ScanCmd { 192 | return r.Client.HScan(key, cursor, match, count) 193 | } 194 | -------------------------------------------------------------------------------- /core/middleware/auth.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package middleware 6 | 7 | import ( 8 | "net/http" 9 | "strings" 10 | 11 | "github.com/gin-gonic/gin" 12 | "github.com/spf13/viper" 13 | ) 14 | 15 | // Auth middleware 16 | func Auth() gin.HandlerFunc { 17 | return func(c *gin.Context) { 18 | path := c.Request.URL.Path 19 | 20 | if strings.Contains(path, "/api/") { 21 | apiKey := c.GetHeader("x-api-key") 22 | if apiKey != viper.GetString("app.api.key") && viper.GetString("app.api.key") != "" { 23 | c.AbortWithStatus(http.StatusUnauthorized) 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /core/middleware/correlation.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package middleware 6 | 7 | import ( 8 | "strings" 9 | 10 | "github.com/clivern/beaver/core/util" 11 | 12 | "github.com/gin-gonic/gin" 13 | ) 14 | 15 | // Correlation middleware 16 | func Correlation() gin.HandlerFunc { 17 | return func(c *gin.Context) { 18 | corralationID := c.GetHeader("x-correlation-id") 19 | 20 | if strings.TrimSpace(corralationID) == "" { 21 | c.Request.Header.Add("X-Correlation-ID", util.GenerateUUID4()) 22 | } 23 | c.Next() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /core/middleware/cors.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package middleware 6 | 7 | import ( 8 | "net/http" 9 | 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | // Cors middleware 14 | func Cors() gin.HandlerFunc { 15 | return func(c *gin.Context) { 16 | c.Writer.Header().Set("Access-Control-Allow-Origin", "*") 17 | c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") 18 | c.Writer.Header().Set("Access-Control-Allow-Headers", "*") 19 | c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE") 20 | 21 | if c.Request.Method == "OPTIONS" { 22 | c.AbortWithStatus(http.StatusOK) 23 | return 24 | } 25 | 26 | c.Next() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /core/middleware/log.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package middleware 6 | 7 | import ( 8 | "bytes" 9 | "io/ioutil" 10 | 11 | "github.com/gin-gonic/gin" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | // Logger middleware 16 | // TODO Hide Secure Data from Logs 17 | func Logger() gin.HandlerFunc { 18 | return func(c *gin.Context) { 19 | // before request 20 | var bodyBytes []byte 21 | 22 | // Workaround for issue https://github.com/gin-gonic/gin/issues/1651 23 | if c.Request.Body != nil { 24 | bodyBytes, _ = ioutil.ReadAll(c.Request.Body) 25 | } 26 | 27 | c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes)) 28 | 29 | log.WithFields(log.Fields{ 30 | "correlation_id": c.GetHeader("x-correlation-id"), 31 | "http_method": c.Request.Method, 32 | "http_path": c.Request.URL.Path, 33 | "request_body": string(bodyBytes), 34 | }).Info("Request started") 35 | 36 | c.Next() 37 | 38 | // after request 39 | status := c.Writer.Status() 40 | size := c.Writer.Size() 41 | 42 | log.WithFields(log.Fields{ 43 | "correlation_id": c.GetHeader("x-correlation-id"), 44 | "http_status": status, 45 | "response_size": size, 46 | }).Info(`Request finished`) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /core/middleware/metric.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package middleware 6 | 7 | import ( 8 | "strconv" 9 | "time" 10 | 11 | "github.com/gin-gonic/gin" 12 | "github.com/prometheus/client_golang/prometheus" 13 | log "github.com/sirupsen/logrus" 14 | ) 15 | 16 | var ( 17 | httpRequests = prometheus.NewCounterVec( 18 | prometheus.CounterOpts{ 19 | Namespace: "beaver", 20 | Name: "total_http_requests", 21 | Help: "How many HTTP requests processed, partitioned by status code and HTTP method.", 22 | }, []string{"code", "method", "handler", "host", "url"}) 23 | 24 | requestDuration = prometheus.NewHistogramVec( 25 | prometheus.HistogramOpts{ 26 | Subsystem: "beaver", 27 | Name: "request_duration_seconds", 28 | Help: "The HTTP request latencies in seconds.", 29 | }, 30 | []string{"code", "method", "url"}, 31 | ) 32 | 33 | responseSize = prometheus.NewSummary( 34 | prometheus.SummaryOpts{ 35 | Namespace: "beaver", 36 | Name: "response_size_bytes", 37 | Help: "The HTTP response sizes in bytes.", 38 | }, 39 | ) 40 | ) 41 | 42 | func init() { 43 | prometheus.MustRegister(httpRequests) 44 | prometheus.MustRegister(requestDuration) 45 | prometheus.MustRegister(responseSize) 46 | } 47 | 48 | // Metric middleware 49 | func Metric() gin.HandlerFunc { 50 | return func(c *gin.Context) { 51 | // before request 52 | start := time.Now() 53 | 54 | c.Next() 55 | 56 | // after request 57 | elapsed := float64(time.Since(start)) / float64(time.Second) 58 | 59 | log.WithFields(log.Fields{ 60 | "correlation_id": c.GetHeader("x-correlation-id"), 61 | }).Info(`Collecting metrics`) 62 | 63 | // Collect Metrics 64 | httpRequests.WithLabelValues( 65 | strconv.Itoa(c.Writer.Status()), 66 | c.Request.Method, 67 | c.HandlerName(), 68 | c.Request.Host, 69 | c.Request.URL.Path, 70 | ).Inc() 71 | 72 | requestDuration.WithLabelValues( 73 | strconv.Itoa(c.Writer.Status()), 74 | c.Request.Method, 75 | c.Request.URL.Path, 76 | ).Observe(elapsed) 77 | 78 | responseSize.Observe(float64(c.Writer.Size())) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /core/util/helpers.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package util 6 | 7 | import ( 8 | "os" 9 | 10 | "github.com/satori/go.uuid" 11 | ) 12 | 13 | // GenerateUUID4 create a UUID 14 | func GenerateUUID4() string { 15 | u := uuid.Must(uuid.NewV4(), nil) 16 | return u.String() 17 | } 18 | 19 | // Unset remove element at position i 20 | func Unset(a []string, i int) []string { 21 | a[i] = a[len(a)-1] 22 | a[len(a)-1] = "" 23 | return a[:len(a)-1] 24 | } 25 | 26 | // FileExists reports whether the named file exists 27 | func FileExists(path string) bool { 28 | if fi, err := os.Stat(path); err == nil { 29 | if fi.Mode().IsRegular() { 30 | return true 31 | } 32 | } 33 | return false 34 | } 35 | 36 | // DirExists reports whether the dir exists 37 | func DirExists(path string) bool { 38 | if fi, err := os.Stat(path); err == nil { 39 | if fi.Mode().IsDir() { 40 | return true 41 | } 42 | } 43 | return false 44 | } 45 | 46 | // EnsureDir ensures that directory exists 47 | func EnsureDir(dirName string, mode int) (bool, error) { 48 | err := os.MkdirAll(dirName, os.FileMode(mode)) 49 | 50 | if err == nil || os.IsExist(err) { 51 | return true, nil 52 | } 53 | return false, err 54 | } 55 | -------------------------------------------------------------------------------- /core/util/map.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package util 6 | 7 | import ( 8 | "sync" 9 | ) 10 | 11 | // Map type 12 | type Map struct { 13 | sync.RWMutex 14 | items map[string]interface{} 15 | } 16 | 17 | // NewMap creates a new instance of Map 18 | func NewMap() Map { 19 | return Map{items: make(map[string]interface{})} 20 | } 21 | 22 | // Get a key from a concurrent map 23 | func (cm *Map) Get(key string) (interface{}, bool) { 24 | cm.Lock() 25 | defer cm.Unlock() 26 | 27 | value, ok := cm.items[key] 28 | 29 | return value, ok 30 | } 31 | 32 | // Set a key in a concurrent map 33 | func (cm *Map) Set(key string, value interface{}) { 34 | cm.Lock() 35 | defer cm.Unlock() 36 | 37 | cm.items[key] = value 38 | } 39 | 40 | // Delete deletes a key 41 | func (cm *Map) Delete(key string) { 42 | cm.Lock() 43 | defer cm.Unlock() 44 | 45 | delete(cm.items, key) 46 | } 47 | -------------------------------------------------------------------------------- /core/util/token.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package util 6 | 7 | import ( 8 | "github.com/dgrijalva/jwt-go" 9 | ) 10 | 11 | // GenerateJWTToken generate a jwt token for frontend 12 | func GenerateJWTToken(data string, timestamp int64, secret string) (string, error) { 13 | 14 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ 15 | "data": data, 16 | "timestamp": timestamp, 17 | }) 18 | 19 | tokenString, err := token.SignedString([]byte(secret)) 20 | 21 | if err != nil { 22 | return "", err 23 | } 24 | 25 | return tokenString, nil 26 | } 27 | -------------------------------------------------------------------------------- /core/util/validator.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Clivern. All rights reserved. 2 | // Use of this source code is governed by the MIT 3 | // license that can be found in the LICENSE file. 4 | 5 | package util 6 | 7 | import ( 8 | "encoding/json" 9 | "regexp" 10 | "strings" 11 | ) 12 | 13 | const ( 14 | // UUID3 regex expr 15 | UUID3 string = "^[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$" 16 | // UUID4 regex expr 17 | UUID4 string = "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" 18 | // UUID5 regex expr 19 | UUID5 string = "^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" 20 | // UUID regex expr 21 | UUID string = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" 22 | // SLUG regex expr 23 | SLUG string = "^[a-z0-9]+(?:_[a-z0-9]+)*$" 24 | ) 25 | 26 | // Validator util 27 | type Validator struct { 28 | } 29 | 30 | // IsIn checks if item in array 31 | func (v *Validator) IsIn(item string, list []string) bool { 32 | for _, a := range list { 33 | if a == item { 34 | return true 35 | } 36 | } 37 | return false 38 | } 39 | 40 | // IsSlug checks if string is a valid slug 41 | func (v *Validator) IsSlug(slug string, min int, max int) bool { 42 | 43 | if len(slug) < min { 44 | return false 45 | } 46 | 47 | if len(slug) > max { 48 | return false 49 | } 50 | 51 | if regexp.MustCompile(SLUG).MatchString(slug) { 52 | return true 53 | } 54 | 55 | return false 56 | } 57 | 58 | // IsSlugs checks if string is a valid slug 59 | func (v *Validator) IsSlugs(slugs []string, min int, max int) bool { 60 | 61 | for _, slug := range slugs { 62 | if !v.IsSlug(slug, min, max) { 63 | return false 64 | } 65 | } 66 | 67 | return true 68 | } 69 | 70 | // IsEmpty checks if item is empty 71 | func (v *Validator) IsEmpty(item string) bool { 72 | if strings.TrimSpace(item) == "" { 73 | return true 74 | } 75 | return false 76 | } 77 | 78 | // IsUUID validates a UUID 79 | func (v *Validator) IsUUID(uuid string) bool { 80 | if regexp.MustCompile(UUID).MatchString(uuid) { 81 | return true 82 | } 83 | 84 | return false 85 | } 86 | 87 | // IsUUID3 validates a UUID3 88 | func (v *Validator) IsUUID3(uuid string) bool { 89 | if regexp.MustCompile(UUID3).MatchString(uuid) { 90 | return true 91 | } 92 | 93 | return false 94 | } 95 | 96 | // IsUUID4 validates a UUID4 97 | func (v *Validator) IsUUID4(uuid string) bool { 98 | if regexp.MustCompile(UUID4).MatchString(uuid) { 99 | return true 100 | } 101 | 102 | return false 103 | } 104 | 105 | // IsUUID5 validates a UUID5 106 | func (v *Validator) IsUUID5(uuid string) bool { 107 | if regexp.MustCompile(UUID5).MatchString(uuid) { 108 | return true 109 | } 110 | 111 | return false 112 | } 113 | 114 | // IsJSON validates a JSON string 115 | func (v *Validator) IsJSON(str string) bool { 116 | var jsonStr map[string]interface{} 117 | err := json.Unmarshal([]byte(str), &jsonStr) 118 | return err == nil 119 | } 120 | -------------------------------------------------------------------------------- /deployment/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clivern/Beaver/bd68afd9d44386b789e46d303f7f4a865ac4cc68/deployment/.gitkeep -------------------------------------------------------------------------------- /deployment/docker/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clivern/Beaver/bd68afd9d44386b789e46d303f7f4a865ac4cc68/deployment/docker/.gitkeep -------------------------------------------------------------------------------- /deployment/linux/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clivern/Beaver/bd68afd9d44386b789e46d303f7f4a865ac4cc68/deployment/linux/.gitkeep -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/clivern/beaver 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 7 | github.com/drone/envsubst v1.0.3 8 | github.com/gin-gonic/gin v1.9.1 9 | github.com/go-redis/redis v6.15.9+incompatible 10 | github.com/gorilla/websocket v1.5.0 11 | github.com/prometheus/client_golang v1.18.0 12 | github.com/satori/go.uuid v1.2.0 13 | github.com/sirupsen/logrus v1.9.3 14 | github.com/spf13/cobra v1.7.0 15 | github.com/spf13/viper v1.16.0 16 | ) 17 | 18 | require ( 19 | github.com/beorn7/perks v1.0.1 // indirect 20 | github.com/bytedance/sonic v1.9.1 // indirect 21 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 22 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 23 | github.com/fsnotify/fsnotify v1.6.0 // indirect 24 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 25 | github.com/gin-contrib/sse v0.1.0 // indirect 26 | github.com/go-playground/locales v0.14.1 // indirect 27 | github.com/go-playground/universal-translator v0.18.1 // indirect 28 | github.com/go-playground/validator/v10 v10.14.0 // indirect 29 | github.com/goccy/go-json v0.10.2 // indirect 30 | github.com/golang/protobuf v1.5.3 // indirect 31 | github.com/hashicorp/hcl v1.0.0 // indirect 32 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 33 | github.com/json-iterator/go v1.1.12 // indirect 34 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 35 | github.com/leodido/go-urn v1.2.4 // indirect 36 | github.com/magiconair/properties v1.8.7 // indirect 37 | github.com/mattn/go-isatty v0.0.19 // indirect 38 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 39 | github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect 40 | github.com/mitchellh/mapstructure v1.5.0 // indirect 41 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 42 | github.com/modern-go/reflect2 v1.0.2 // indirect 43 | github.com/onsi/ginkgo v1.16.5 // indirect 44 | github.com/onsi/gomega v1.27.10 // indirect 45 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 46 | github.com/prometheus/client_model v0.5.0 // indirect 47 | github.com/prometheus/common v0.45.0 // indirect 48 | github.com/prometheus/procfs v0.12.0 // indirect 49 | github.com/spf13/afero v1.9.5 // indirect 50 | github.com/spf13/cast v1.5.1 // indirect 51 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 52 | github.com/spf13/pflag v1.0.5 // indirect 53 | github.com/subosito/gotenv v1.4.2 // indirect 54 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 55 | github.com/ugorji/go/codec v1.2.11 // indirect 56 | golang.org/x/arch v0.3.0 // indirect 57 | golang.org/x/crypto v0.14.0 // indirect 58 | golang.org/x/net v0.17.0 // indirect 59 | golang.org/x/sys v0.15.0 // indirect 60 | golang.org/x/text v0.13.0 // indirect 61 | google.golang.org/protobuf v1.31.0 // indirect 62 | gopkg.in/ini.v1 v1.67.0 // indirect 63 | gopkg.in/yaml.v3 v3.0.1 // indirect 64 | ) 65 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 7 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 8 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 9 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 10 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 11 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 12 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 13 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 14 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 15 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= 16 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= 17 | cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= 18 | cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= 19 | cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= 20 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 21 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 22 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 23 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 24 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 25 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 26 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 27 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 28 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 29 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 30 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 31 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 32 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 33 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 34 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 35 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 36 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 37 | cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= 38 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 39 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 40 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 41 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 42 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 43 | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= 44 | github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= 45 | github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= 46 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 47 | github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 48 | github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 49 | github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= 50 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= 51 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= 52 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 53 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 54 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 55 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 56 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 57 | github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 58 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 59 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 60 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 61 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 62 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 63 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 64 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 65 | github.com/drone/envsubst v1.0.3 h1:PCIBwNDYjs50AsLZPYdfhSATKaRg/FJmDc2D6+C2x8g= 66 | github.com/drone/envsubst v1.0.3/go.mod h1:N2jZmlMufstn1KEqvbHjw40h1KyTmnVzHcSc9bFiJ2g= 67 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 68 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 69 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 70 | github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= 71 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 72 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 73 | github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= 74 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 75 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 76 | github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= 77 | github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 78 | github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= 79 | github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= 80 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 81 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 82 | github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= 83 | github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= 84 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 85 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 86 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 87 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 88 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 89 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 90 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 91 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 92 | github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= 93 | github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= 94 | github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= 95 | github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= 96 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 97 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 98 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 99 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 100 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 101 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 102 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 103 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 104 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 105 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 106 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 107 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 108 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 109 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 110 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 111 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 112 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 113 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 114 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 115 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 116 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 117 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 118 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 119 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 120 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 121 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 122 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 123 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 124 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 125 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 126 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 127 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 128 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 129 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 130 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 131 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 132 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 133 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 134 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 135 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 136 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 137 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 138 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 139 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 140 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 141 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 142 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 143 | github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 144 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 145 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 146 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 147 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 148 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 149 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 150 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 151 | github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 152 | github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 153 | github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 154 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 155 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 156 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 157 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 158 | github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= 159 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 160 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 161 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 162 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 163 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 164 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 165 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 166 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 167 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 168 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 169 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 170 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 171 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 172 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 173 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 174 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 175 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 176 | github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= 177 | github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= 178 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 179 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 180 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 181 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 182 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 183 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 184 | github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= 185 | github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= 186 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 187 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 188 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 189 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 190 | github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= 191 | github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= 192 | github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= 193 | github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= 194 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 195 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 196 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 197 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 198 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 199 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 200 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 201 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 202 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 203 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 204 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 205 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 206 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 207 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 208 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 209 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 210 | github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= 211 | github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= 212 | github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= 213 | github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= 214 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 215 | github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= 216 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 217 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 218 | github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= 219 | github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= 220 | github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= 221 | github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= 222 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 223 | github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= 224 | github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= 225 | github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= 226 | github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= 227 | github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= 228 | github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= 229 | github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= 230 | github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= 231 | github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= 232 | github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= 233 | github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= 234 | github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= 235 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 236 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 237 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 238 | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= 239 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 240 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 241 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 242 | github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= 243 | github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= 244 | github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= 245 | github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= 246 | github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= 247 | github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= 248 | github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= 249 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= 250 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 251 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 252 | github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= 253 | github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg= 254 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 255 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 256 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 257 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 258 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 259 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 260 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 261 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 262 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 263 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 264 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 265 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 266 | github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= 267 | github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 268 | github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= 269 | github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= 270 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 271 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 272 | github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= 273 | github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 274 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 275 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 276 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 277 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 278 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 279 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 280 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 281 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 282 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 283 | go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= 284 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 285 | golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= 286 | golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 287 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 288 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 289 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 290 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 291 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 292 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 293 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 294 | golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= 295 | golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= 296 | golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= 297 | golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= 298 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 299 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 300 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 301 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 302 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 303 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 304 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 305 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 306 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 307 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 308 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 309 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 310 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 311 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 312 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 313 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 314 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 315 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 316 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 317 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 318 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 319 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 320 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 321 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 322 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 323 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 324 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 325 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 326 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 327 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 328 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 329 | golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 330 | golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 331 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 332 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 333 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 334 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 335 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 336 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 337 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 338 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 339 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 340 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 341 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 342 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 343 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 344 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 345 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 346 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 347 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 348 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 349 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 350 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 351 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 352 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 353 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 354 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 355 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 356 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 357 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 358 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 359 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 360 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 361 | golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 362 | golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 363 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 364 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 365 | golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= 366 | golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= 367 | golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= 368 | golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 369 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 370 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 371 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 372 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 373 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 374 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 375 | golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 376 | golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 377 | golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 378 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 379 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 380 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 381 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 382 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 383 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 384 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 385 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 386 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 387 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 388 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 389 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 390 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 391 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 392 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 393 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 394 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 395 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 396 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 397 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 398 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 399 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 400 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 401 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 402 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 403 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 404 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 405 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 406 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 407 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 408 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 409 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 410 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 411 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 412 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 413 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 414 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 415 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 416 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 417 | golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 418 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 419 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 420 | golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 421 | golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 422 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 423 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 424 | golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 425 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 426 | golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 427 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 428 | golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 429 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 430 | golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 431 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 432 | golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= 433 | golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 434 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 435 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 436 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 437 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 438 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 439 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 440 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 441 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 442 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 443 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 444 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 445 | golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= 446 | golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 447 | golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= 448 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 449 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 450 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 451 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 452 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 453 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 454 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 455 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 456 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 457 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 458 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 459 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 460 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 461 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 462 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 463 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 464 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 465 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 466 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 467 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 468 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 469 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 470 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 471 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 472 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 473 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 474 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 475 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 476 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 477 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 478 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 479 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 480 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 481 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 482 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 483 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 484 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 485 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 486 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 487 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 488 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 489 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 490 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 491 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 492 | golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= 493 | golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 494 | golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 495 | golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 496 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 497 | golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 498 | golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 499 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 500 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 501 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 502 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 503 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 504 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 505 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 506 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 507 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 508 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 509 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 510 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 511 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 512 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 513 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 514 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 515 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 516 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 517 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 518 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 519 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 520 | google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= 521 | google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= 522 | google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= 523 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 524 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 525 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 526 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 527 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 528 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 529 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 530 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 531 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 532 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 533 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 534 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 535 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 536 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 537 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 538 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 539 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 540 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 541 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 542 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 543 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 544 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 545 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 546 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 547 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 548 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 549 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 550 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 551 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 552 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 553 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 554 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 555 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 556 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 557 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 558 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 559 | google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 560 | google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 561 | google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 562 | google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 563 | google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 564 | google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 565 | google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 566 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 567 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 568 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 569 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 570 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 571 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 572 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 573 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 574 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 575 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 576 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 577 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 578 | google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 579 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 580 | google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= 581 | google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 582 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 583 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 584 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 585 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 586 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 587 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 588 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 589 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 590 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 591 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 592 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 593 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 594 | google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= 595 | google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 596 | google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= 597 | google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 598 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 599 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 600 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 601 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 602 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 603 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 604 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 605 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 606 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 607 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 608 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 609 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 610 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 611 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 612 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 613 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 614 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 615 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 616 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 617 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 618 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 619 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 620 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 621 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 622 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 623 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 624 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /techstack.md: -------------------------------------------------------------------------------- 1 | 36 |
37 | 38 | # Tech Stack File 39 | ![](https://img.stackshare.io/repo.svg "repo") [Clivern/Beaver](https://github.com/Clivern/Beaver)![](https://img.stackshare.io/public_badge.svg "public") 40 |

41 | |48
Tools used|12/21/23
Report generated| 42 | |------|------| 43 |
44 | 45 | ## Languages (2) 46 | 47 | 54 | 55 | 62 | 63 | 64 |
48 | Golang 49 |
50 | Golang 51 |
52 | 53 |
56 | JavaScript 57 |
58 | JavaScript 59 |
60 | 61 |
65 | 66 | ## Frameworks (2) 67 | 68 | 75 | 76 | 83 | 84 | 85 |
69 | Gin Gonic 70 |
71 | Gin Gonic 72 |
73 | 74 |
77 | Protobuf 78 |
79 | Protobuf 80 |
81 | 82 |
86 | 87 | ## Data (1) 88 | 89 | 96 | 97 | 98 |
90 | Redis 91 |
92 | Redis 93 |
94 | 95 |
99 | 100 | ## DevOps (3) 101 | 102 | 109 | 110 | 117 | 118 | 125 | 126 | 127 |
103 | Git 104 |
105 | Git 106 |
107 | 108 |
111 | GitHub Actions 112 |
113 | GitHub Actions 114 |
115 | 116 |
119 | Prometheus 120 |
121 | Prometheus 122 |
123 | 124 |
128 | 129 | ## Other (1) 130 | 131 | 138 | 139 | 140 |
132 | Shell 133 |
134 | Shell 135 |
136 | 137 |
141 | 142 | 143 | ## Open source packages (39) 144 | 145 | ## Go Packages (39) 146 | 147 | |NAME|VERSION|LAST UPDATED|LAST UPDATED BY|LICENSE|VULNERABILITIES| 148 | |:------|:------|:------|:------|:------|:------| 149 | |[afero](https://pkg.go.dev/github.com/spf13/afero)|v1.9.5|04/24/23|Clivern |Apache-2.0|N/A| 150 | |[assertion](https://pkg.go.dev/github.com/onsi/gomega/internal/assertion)|N/A|04/24/23|Clivern |MIT|N/A| 151 | |[cast](https://pkg.go.dev/github.com/spf13/cast)|v1.5.1|04/24/23|Clivern |MIT|N/A| 152 | |[client_model](https://pkg.go.dev/github.com/prometheus/client_model)|v0.0.0|04/24/23|Clivern |Apache-2.0|N/A| 153 | |[cobra](https://pkg.go.dev/github.com/spf13/cobra)|v1.7.0|04/24/23|Clivern |Apache-2.0|N/A| 154 | |[codec](https://pkg.go.dev/github.com/ugorji/go/codec)|v1.2.11|04/24/23|Clivern |MIT|N/A| 155 | |[common](https://pkg.go.dev/github.com/prometheus/common)|v0.42.0|04/24/23|Clivern |Apache-2.0|N/A| 156 | |[convert](https://pkg.go.dev/github.com/onsi/ginkgo/ginkgo/convert)|N/A|01/08/19|Clivern |MIT|N/A| 157 | |[crypto](https://pkg.go.dev/golang.org/x/crypto)|v0.0.0|04/24/23|Clivern |BSD-3-Clause|[CVE-2020-9283](https://github.com/advisories/GHSA-ffhg-7mh4-33c4) (Moderate)| 158 | |[fsnotify](https://pkg.go.dev/github.com/fsnotify/fsnotify)|v1.4.7|04/24/23|Clivern |BSD-3-Clause|N/A| 159 | |[gin](https://pkg.go.dev/github.com/gin-gonic/gin)|v1.9.1|01/08/19|Clivern |MIT|N/A| 160 | |[ginkgo](https://pkg.go.dev/github.com/onsi/ginkgo)|v1.6.0|04/24/23|Clivern |MIT|N/A| 161 | |[go-isatty](https://pkg.go.dev/github.com/mattn/go-isatty)|v0.0.19|04/24/23|Clivern |MIT|N/A| 162 | |[go.uuid](https://pkg.go.dev/github.com/satori/go.uuid)|v1.2.0|03/14/19|Clivern |MIT|N/A| 163 | |[golang_protobuf_extensions](https://pkg.go.dev/github.com/matttproud/golang_protobuf_extensions)|v1.0.4|04/24/23|Clivern |Apache-2.0|N/A| 164 | |[gomega](https://pkg.go.dev/github.com/onsi/gomega)|v1.7.1|04/24/23|Clivern |MIT|N/A| 165 | |[gotenv](https://pkg.go.dev/github.com/subosito/gotenv)|v1.4.2|04/24/23|Clivern |MIT|N/A| 166 | |[hcl](https://pkg.go.dev/github.com/hashicorp/hcl)|v1.0.0|01/08/19|Clivern |MPL-2.0|N/A| 167 | |[ini.v1](https://pkg.go.dev/gopkg.in/ini.v1)|v1.67.0|04/24/23|Clivern |Apache-2.0|N/A| 168 | |[json-iterator/go](https://pkg.go.dev/github.com/json-iterator/go)|v1.1.12|04/24/23|Clivern |MIT|N/A| 169 | |[jwalterweatherman](https://pkg.go.dev/github.com/spf13/jwalterweatherman)|v1.1.0|04/24/23|Clivern |MIT|N/A| 170 | |[jwt-go](https://pkg.go.dev/github.com/dgrijalva/jwt-go)|N/A|03/14/19|Clivern |MIT|N/A| 171 | |[locales](https://pkg.go.dev/github.com/go-playground/locales)|v0.14.1|04/24/23|Clivern |MIT|N/A| 172 | |[logrus](https://pkg.go.dev/github.com/sirupsen/logrus)|v1.9.3|04/24/23|Clivern |MIT|N/A| 173 | |[mapstructure](https://pkg.go.dev/github.com/mitchellh/mapstructure)|v1.5.0|04/24/23|Clivern |MIT|N/A| 174 | |[mimetype](https://pkg.go.dev/github.com/gabriel-vasile/mimetype)|v1.4.2|04/24/23|Clivern |MIT|N/A| 175 | |[mousetrap](https://pkg.go.dev/github.com/inconshreveable/mousetrap)|v1.1.0|04/24/23|Clivern |Apache-2.0|N/A| 176 | |[net](https://pkg.go.dev/golang.org/x/net)|v0.0.0|01/08/19|Clivern |BSD-3-Clause|N/A| 177 | |[perks](https://pkg.go.dev/github.com/beorn7/perks)|v1.0.1|04/24/23|Clivern |MIT|N/A| 178 | |[pflag](https://pkg.go.dev/github.com/spf13/pflag)|v1.0.5|04/24/23|Clivern |BSD-3-Clause|N/A| 179 | |[procfs](https://pkg.go.dev/github.com/prometheus/procfs)|v0.10.1|04/24/23|Clivern |Apache-2.0|N/A| 180 | |[properties](https://pkg.go.dev/github.com/magiconair/properties)|v1.8.7|04/24/23|Clivern |BSD-2-Clause|N/A| 181 | |[proto](https://pkg.go.dev/github.com/golang/protobuf/proto)|N/A|04/24/23|Clivern |BSD-3-Clause|N/A| 182 | |[reflect2](https://pkg.go.dev/github.com/modern-go/reflect2)|v1.0.2|04/24/23|Clivern |Apache-2.0|N/A| 183 | |[sys](https://pkg.go.dev/golang.org/x/sys)|v0.0.0|01/08/19|Clivern |BSD-3-Clause|N/A| 184 | |[text](https://pkg.go.dev/golang.org/x/text)|v0.0.0|04/24/23|Clivern |BSD-3-Clause|N/A| 185 | |[universal-translator](https://pkg.go.dev/github.com/go-playground/universal-translator)|v0.18.1|04/24/23|Clivern |MIT|N/A| 186 | |[viper](https://pkg.go.dev/github.com/spf13/viper)|v1.16.0|04/24/23|Clivern |MIT|N/A| 187 | |[websocket](https://pkg.go.dev/github.com/gorilla/websocket)|v1.5.0|02/15/22|renovate[bot] |BSD-3-Clause|N/A| 188 | 189 |
190 |
191 | 192 | Generated via [Stack File](https://github.com/marketplace/stack-file) 193 | -------------------------------------------------------------------------------- /techstack.yml: -------------------------------------------------------------------------------- 1 | repo_name: Clivern/Beaver 2 | report_id: 7bdb4c3f5f0b1a8883352e328dc9f3f5 3 | version: 0.1 4 | repo_type: Public 5 | timestamp: '2023-12-21T15:23:29+00:00' 6 | requested_by: Clivern 7 | provider: github 8 | branch: main 9 | detected_tools_count: 48 10 | tools: 11 | - name: Golang 12 | description: An open source programming language that makes it easy to build simple, 13 | reliable, and efficient software 14 | website_url: http://golang.org/ 15 | license: BSD-3-Clause 16 | open_source: true 17 | hosted_saas: false 18 | category: Languages & Frameworks 19 | sub_category: Languages 20 | image_url: https://img.stackshare.io/service/1005/O6AczwfV_400x400.png 21 | detection_source: Repo Metadata 22 | - name: JavaScript 23 | description: Lightweight, interpreted, object-oriented language with first-class 24 | functions 25 | website_url: https://developer.mozilla.org/en-US/docs/Web/JavaScript 26 | open_source: true 27 | hosted_saas: false 28 | category: Languages & Frameworks 29 | sub_category: Languages 30 | image_url: https://img.stackshare.io/service/1209/javascript.jpeg 31 | detection_source: Repo Metadata 32 | - name: Gin Gonic 33 | description: 'HTTP web framework written in Go ' 34 | website_url: https://gin-gonic.com/ 35 | license: MIT 36 | open_source: true 37 | hosted_saas: false 38 | category: Languages & Frameworks 39 | sub_category: Frameworks (Full Stack) 40 | image_url: https://img.stackshare.io/service/4221/7894478.png 41 | detection_source: go.mod 42 | last_updated_by: Clivern 43 | last_updated_on: 2023-04-24 21:31:39.000000000 Z 44 | - name: Protobuf 45 | description: Google's data interchange format 46 | website_url: https://developers.google.com/protocol-buffers/ 47 | open_source: true 48 | hosted_saas: false 49 | category: Languages & Frameworks 50 | sub_category: Serialization Frameworks 51 | image_url: https://img.stackshare.io/service/4393/ma2jqJKH_400x400.png 52 | detection_source: go.mod 53 | last_updated_by: Clivern 54 | last_updated_on: 2023-04-24 21:31:39.000000000 Z 55 | - name: Redis 56 | description: Open source (BSD licensed), in-memory data structure store 57 | website_url: http://redis.io/ 58 | license: BSD-3-Clause 59 | open_source: true 60 | hosted_saas: false 61 | category: Data Stores 62 | sub_category: In-Memory Databases 63 | image_url: https://img.stackshare.io/service/1031/default_cbce472cd134adc6688572f999e9122b9657d4ba.png 64 | detection_source: go.mod 65 | last_updated_by: Ahmed⠙ 66 | last_updated_on: 2020-12-31 20:42:54.000000000 Z 67 | - name: Git 68 | description: Fast, scalable, distributed revision control system 69 | website_url: http://git-scm.com/ 70 | open_source: true 71 | hosted_saas: false 72 | category: Build, Test, Deploy 73 | sub_category: Version Control System 74 | image_url: https://img.stackshare.io/service/1046/git.png 75 | detection_source: Repo Metadata 76 | - name: GitHub Actions 77 | description: Automate your workflow from idea to production 78 | website_url: https://github.com/features/actions 79 | open_source: false 80 | hosted_saas: true 81 | category: Build, Test, Deploy 82 | sub_category: Continuous Integration 83 | image_url: https://img.stackshare.io/service/11563/actions.png 84 | detection_source: ".github/workflows/build.yml" 85 | last_updated_by: renovate[bot] 86 | last_updated_on: 2022-03-02 08:41:47.000000000 Z 87 | - name: Prometheus 88 | description: An open-source service monitoring system and time series database, 89 | developed by SoundCloud 90 | website_url: http://prometheus.io/ 91 | license: Apache-2.0 92 | open_source: true 93 | hosted_saas: false 94 | category: Monitoring 95 | sub_category: Monitoring Tools 96 | image_url: https://img.stackshare.io/service/2501/default_3cf1b307194b26782be5cb209d30360580ae5b3c.png 97 | detection_source: go.mod 98 | last_updated_by: Clivern 99 | last_updated_on: 2023-04-24 21:31:39.000000000 Z 100 | - name: Shell 101 | description: A shell is a text-based terminal, used for manipulating programs and 102 | files. Shell scripts typically manage program execution. 103 | website_url: https://en.wikipedia.org/wiki/Shell_script 104 | open_source: false 105 | hosted_saas: false 106 | category: Languages & Frameworks 107 | sub_category: Languages 108 | image_url: https://img.stackshare.io/service/4631/default_c2062d40130562bdc836c13dbca02d318205a962.png 109 | detection_source: Repo Metadata 110 | - name: afero 111 | description: A FileSystem Abstraction System for Go 112 | package_url: https://pkg.go.dev/github.com/spf13/afero 113 | version: 1.9.5 114 | license: Apache-2.0 115 | open_source: true 116 | hosted_saas: false 117 | category: Libraries 118 | sub_category: Go Modules Packages 119 | image_url: https://img.stackshare.io/package/go-packages/image.png 120 | detection_source_url: https://github.com/Clivern/Beaver/blob/main/go.sum 121 | detection_source: go.mod 122 | last_updated_by: Clivern 123 | last_updated_on: 2023-04-24 21:31:39.000000000 Z 124 | - name: assertion 125 | description: Ginkgo's Preferred Matcher Library 126 | package_url: https://pkg.go.dev/github.com/onsi/gomega/internal/assertion 127 | license: MIT 128 | open_source: true 129 | hosted_saas: false 130 | category: Libraries 131 | sub_category: Go Modules Packages 132 | image_url: https://img.stackshare.io/package/go-packages/image.png 133 | detection_source: go.mod 134 | last_updated_by: Clivern 135 | last_updated_on: 2023-04-24 21:31:39.000000000 Z 136 | - name: cast 137 | description: Safe and easy casting from one type to another in Go 138 | package_url: https://pkg.go.dev/github.com/spf13/cast 139 | version: 1.5.1 140 | license: MIT 141 | open_source: true 142 | hosted_saas: false 143 | category: Libraries 144 | sub_category: Go Modules Packages 145 | image_url: https://img.stackshare.io/package/go-packages/image.png 146 | detection_source_url: https://github.com/Clivern/Beaver/blob/main/go.sum 147 | detection_source: go.mod 148 | last_updated_by: Clivern 149 | last_updated_on: 2023-04-24 21:31:39.000000000 Z 150 | - name: client_model 151 | description: Data model artifacts for Prometheus 152 | package_url: https://pkg.go.dev/github.com/prometheus/client_model 153 | version: 0.0.0 154 | license: Apache-2.0 155 | open_source: true 156 | hosted_saas: false 157 | category: Libraries 158 | sub_category: Go Modules Packages 159 | image_url: https://img.stackshare.io/package/go-packages/image.png 160 | detection_source_url: https://github.com/Clivern/Beaver/blob/main/go.sum 161 | detection_source: go.mod 162 | last_updated_by: Clivern 163 | last_updated_on: 2023-04-24 21:31:39.000000000 Z 164 | - name: cobra 165 | description: A Commander for modern Go CLI interactions 166 | package_url: https://pkg.go.dev/github.com/spf13/cobra 167 | version: 1.7.0 168 | license: Apache-2.0 169 | open_source: true 170 | hosted_saas: false 171 | category: Libraries 172 | sub_category: Go Modules Packages 173 | image_url: https://img.stackshare.io/package/go-packages/image.png 174 | detection_source_url: https://github.com/Clivern/Beaver/blob/main/go.sum 175 | detection_source: go.mod 176 | last_updated_by: Clivern 177 | last_updated_on: 2023-04-24 21:31:39.000000000 Z 178 | - name: codec 179 | description: Idiomatic codec and rpc lib for msgpack, cbor, json, etc 180 | package_url: https://pkg.go.dev/github.com/ugorji/go/codec 181 | version: 1.2.11 182 | license: MIT 183 | open_source: true 184 | hosted_saas: false 185 | category: Libraries 186 | sub_category: Go Modules Packages 187 | image_url: https://img.stackshare.io/package/go-packages/image.png 188 | detection_source_url: https://github.com/Clivern/Beaver/blob/main/go.sum 189 | detection_source: go.mod 190 | last_updated_by: Clivern 191 | last_updated_on: 2023-04-24 21:31:39.000000000 Z 192 | - name: common 193 | description: Go libraries shared across Prometheus components and libraries 194 | package_url: https://pkg.go.dev/github.com/prometheus/common 195 | version: 0.42.0 196 | license: Apache-2.0 197 | open_source: true 198 | hosted_saas: false 199 | category: Libraries 200 | sub_category: Go Modules Packages 201 | image_url: https://img.stackshare.io/package/go-packages/image.png 202 | detection_source_url: https://github.com/Clivern/Beaver/blob/main/go.sum 203 | detection_source: go.mod 204 | last_updated_by: Clivern 205 | last_updated_on: 2023-04-24 21:31:39.000000000 Z 206 | - name: convert 207 | description: BDD Testing Framework for Go 208 | package_url: https://pkg.go.dev/github.com/onsi/ginkgo/ginkgo/convert 209 | license: MIT 210 | open_source: true 211 | hosted_saas: false 212 | category: Libraries 213 | sub_category: Go Modules Packages 214 | image_url: https://img.stackshare.io/package/go-packages/image.png 215 | detection_source: go.mod 216 | last_updated_by: Clivern 217 | last_updated_on: 2019-01-08 22:10:24.000000000 Z 218 | - name: crypto 219 | description: Go supplementary cryptography libraries 220 | package_url: https://pkg.go.dev/golang.org/x/crypto 221 | version: 0.0.0 222 | license: BSD-3-Clause 223 | open_source: true 224 | hosted_saas: false 225 | category: Libraries 226 | sub_category: Go Modules Packages 227 | image_url: https://img.stackshare.io/package/go-packages/image.png 228 | detection_source_url: https://github.com/Clivern/Beaver/blob/main/go.sum 229 | detection_source: go.mod 230 | last_updated_by: Clivern 231 | last_updated_on: 2023-04-24 21:31:39.000000000 Z 232 | vulnerabilities: 233 | - name: Improper Verification of Cryptographic Signature in golang.org/x/crypto 234 | cve_id: CVE-2020-9283 235 | cve_url: https://github.com/advisories/GHSA-ffhg-7mh4-33c4 236 | detected_date: Aug 22 237 | severity: moderate 238 | first_patched: 0.0.0-20200220183623-bac4c82f6975 239 | - name: fsnotify 240 | description: Cross-platform file system notifications for Go 241 | package_url: https://pkg.go.dev/github.com/fsnotify/fsnotify 242 | version: 1.4.7 243 | license: BSD-3-Clause 244 | open_source: true 245 | hosted_saas: false 246 | category: Libraries 247 | sub_category: Go Modules Packages 248 | image_url: https://img.stackshare.io/package/go-packages/image.png 249 | detection_source_url: https://github.com/Clivern/Beaver/blob/main/go.sum 250 | detection_source: go.mod 251 | last_updated_by: Clivern 252 | last_updated_on: 2023-04-24 21:31:39.000000000 Z 253 | - name: gin 254 | description: Gin is a HTTP web framework written in Go 255 | package_url: https://pkg.go.dev/github.com/gin-gonic/gin 256 | version: 1.9.1 257 | license: MIT 258 | open_source: true 259 | hosted_saas: false 260 | category: Libraries 261 | sub_category: Go Modules Packages 262 | image_url: https://img.stackshare.io/package/go-packages/image.png 263 | detection_source_url: https://github.com/Clivern/Beaver/blob/main/go.sum 264 | detection_source: go.mod 265 | last_updated_by: Clivern 266 | last_updated_on: 2019-01-08 22:10:24.000000000 Z 267 | - name: ginkgo 268 | description: BDD Testing Framework for Go 269 | package_url: https://pkg.go.dev/github.com/onsi/ginkgo 270 | version: 1.6.0 271 | license: MIT 272 | open_source: true 273 | hosted_saas: false 274 | category: Libraries 275 | sub_category: Go Modules Packages 276 | image_url: https://img.stackshare.io/package/go-packages/image.png 277 | detection_source_url: https://github.com/Clivern/Beaver/blob/main/go.sum 278 | detection_source: go.mod 279 | last_updated_by: Clivern 280 | last_updated_on: 2023-04-24 21:31:39.000000000 Z 281 | - name: go-isatty 282 | description: Package isatty implements interface to isatty 283 | package_url: https://pkg.go.dev/github.com/mattn/go-isatty 284 | version: 0.0.19 285 | license: MIT 286 | open_source: true 287 | hosted_saas: false 288 | category: Libraries 289 | sub_category: Go Modules Packages 290 | image_url: https://img.stackshare.io/package/go-packages/image.png 291 | detection_source_url: https://github.com/Clivern/Beaver/blob/main/go.sum 292 | detection_source: go.mod 293 | last_updated_by: Clivern 294 | last_updated_on: 2023-04-24 21:31:39.000000000 Z 295 | - name: go.uuid 296 | description: UUID package for Go 297 | package_url: https://pkg.go.dev/github.com/satori/go.uuid 298 | version: 1.2.0 299 | license: MIT 300 | open_source: true 301 | hosted_saas: false 302 | category: Libraries 303 | sub_category: Go Modules Packages 304 | image_url: https://img.stackshare.io/package/go-packages/image.png 305 | detection_source_url: https://github.com/Clivern/Beaver/blob/main/go.sum 306 | detection_source: go.mod 307 | last_updated_by: Clivern 308 | last_updated_on: 2019-03-14 22:28:50.000000000 Z 309 | - name: golang_protobuf_extensions 310 | description: Support for streaming Protocol Buffer messages for the Go language 311 | package_url: https://pkg.go.dev/github.com/matttproud/golang_protobuf_extensions 312 | version: 1.0.4 313 | license: Apache-2.0 314 | open_source: true 315 | hosted_saas: false 316 | category: Libraries 317 | sub_category: Go Modules Packages 318 | image_url: https://img.stackshare.io/package/go-packages/image.png 319 | detection_source_url: https://github.com/Clivern/Beaver/blob/main/go.sum 320 | detection_source: go.mod 321 | last_updated_by: Clivern 322 | last_updated_on: 2023-04-24 21:31:39.000000000 Z 323 | - name: gomega 324 | description: Ginkgo's Preferred Matcher Library 325 | package_url: https://pkg.go.dev/github.com/onsi/gomega 326 | version: 1.7.1 327 | license: MIT 328 | open_source: true 329 | hosted_saas: false 330 | category: Libraries 331 | sub_category: Go Modules Packages 332 | image_url: https://img.stackshare.io/package/go-packages/image.png 333 | detection_source_url: https://github.com/Clivern/Beaver/blob/main/go.sum 334 | detection_source: go.mod 335 | last_updated_by: Clivern 336 | last_updated_on: 2023-04-24 21:31:39.000000000 Z 337 | - name: gotenv 338 | description: Load environment variables dynamically in Go 339 | package_url: https://pkg.go.dev/github.com/subosito/gotenv 340 | version: 1.4.2 341 | license: MIT 342 | open_source: true 343 | hosted_saas: false 344 | category: Libraries 345 | sub_category: Go Modules Packages 346 | image_url: https://img.stackshare.io/package/go-packages/image.png 347 | detection_source_url: https://github.com/Clivern/Beaver/blob/main/go.sum 348 | detection_source: go.mod 349 | last_updated_by: Clivern 350 | last_updated_on: 2023-04-24 21:31:39.000000000 Z 351 | - name: hcl 352 | description: HCL is the HashiCorp configuration language 353 | package_url: https://pkg.go.dev/github.com/hashicorp/hcl 354 | version: 1.0.0 355 | license: MPL-2.0 356 | open_source: true 357 | hosted_saas: false 358 | category: Libraries 359 | sub_category: Go Modules Packages 360 | image_url: https://img.stackshare.io/package/go-packages/image.png 361 | detection_source_url: https://github.com/Clivern/Beaver/blob/main/go.sum 362 | detection_source: go.mod 363 | last_updated_by: Clivern 364 | last_updated_on: 2019-01-08 22:10:24.000000000 Z 365 | - name: ini.v1 366 | description: Package ini provides INI file read and write functionality in Go 367 | package_url: https://pkg.go.dev/gopkg.in/ini.v1 368 | version: 1.67.0 369 | license: Apache-2.0 370 | open_source: true 371 | hosted_saas: false 372 | category: Libraries 373 | sub_category: Go Modules Packages 374 | image_url: https://img.stackshare.io/package/go-packages/image.png 375 | detection_source_url: https://github.com/Clivern/Beaver/blob/main/go.sum 376 | detection_source: go.mod 377 | last_updated_by: Clivern 378 | last_updated_on: 2023-04-24 21:31:39.000000000 Z 379 | - name: json-iterator/go 380 | description: A high-performance 100% compatible drop-in replacement of "encoding/json" 381 | package_url: https://pkg.go.dev/github.com/json-iterator/go 382 | version: 1.1.12 383 | license: MIT 384 | open_source: true 385 | hosted_saas: false 386 | category: Libraries 387 | sub_category: Go Modules Packages 388 | image_url: https://img.stackshare.io/package/go-packages/image.png 389 | detection_source_url: https://github.com/Clivern/Beaver/blob/main/go.sum 390 | detection_source: go.mod 391 | last_updated_by: Clivern 392 | last_updated_on: 2023-04-24 21:31:39.000000000 Z 393 | - name: jwalterweatherman 394 | description: So you always leave a note 395 | package_url: https://pkg.go.dev/github.com/spf13/jwalterweatherman 396 | version: 1.1.0 397 | license: MIT 398 | open_source: true 399 | hosted_saas: false 400 | category: Libraries 401 | sub_category: Go Modules Packages 402 | image_url: https://img.stackshare.io/package/go-packages/image.png 403 | detection_source_url: https://github.com/Clivern/Beaver/blob/main/go.sum 404 | detection_source: go.mod 405 | last_updated_by: Clivern 406 | last_updated_on: 2023-04-24 21:31:39.000000000 Z 407 | - name: jwt-go 408 | description: Golang implementation of JSON Web Tokens 409 | package_url: https://pkg.go.dev/github.com/dgrijalva/jwt-go 410 | license: MIT 411 | open_source: true 412 | hosted_saas: false 413 | category: Libraries 414 | sub_category: Go Modules Packages 415 | image_url: https://img.stackshare.io/package/go-packages/image.png 416 | detection_source: go.mod 417 | last_updated_by: Clivern 418 | last_updated_on: 2019-03-14 22:28:50.000000000 Z 419 | - name: locales 420 | description: ":earth_americas: a set of locales generated from the CLDR Project 421 | which can be used independently or within an i18n package; these were built for 422 | use with" 423 | package_url: https://pkg.go.dev/github.com/go-playground/locales 424 | version: 0.14.1 425 | license: MIT 426 | open_source: true 427 | hosted_saas: false 428 | category: Libraries 429 | sub_category: Go Modules Packages 430 | image_url: https://img.stackshare.io/package/go-packages/image.png 431 | detection_source_url: https://github.com/Clivern/Beaver/blob/main/go.sum 432 | detection_source: go.mod 433 | last_updated_by: Clivern 434 | last_updated_on: 2023-04-24 21:31:39.000000000 Z 435 | - name: logrus 436 | description: Structured, pluggable logging for Go 437 | package_url: https://pkg.go.dev/github.com/sirupsen/logrus 438 | version: 1.9.3 439 | license: MIT 440 | open_source: true 441 | hosted_saas: false 442 | category: Libraries 443 | sub_category: Go Modules Packages 444 | image_url: https://img.stackshare.io/package/go-packages/image.png 445 | detection_source_url: https://github.com/Clivern/Beaver/blob/main/go.sum 446 | detection_source: go.mod 447 | last_updated_by: Clivern 448 | last_updated_on: 2023-04-24 21:31:39.000000000 Z 449 | - name: mapstructure 450 | description: Go library for decoding generic map values into native Go structures 451 | package_url: https://pkg.go.dev/github.com/mitchellh/mapstructure 452 | version: 1.5.0 453 | license: MIT 454 | open_source: true 455 | hosted_saas: false 456 | category: Libraries 457 | sub_category: Go Modules Packages 458 | image_url: https://img.stackshare.io/package/go-packages/image.png 459 | detection_source_url: https://github.com/Clivern/Beaver/blob/main/go.sum 460 | detection_source: go.mod 461 | last_updated_by: Clivern 462 | last_updated_on: 2023-04-24 21:31:39.000000000 Z 463 | - name: mimetype 464 | description: A golang library for detecting the MIME type and file extension 465 | package_url: https://pkg.go.dev/github.com/gabriel-vasile/mimetype 466 | version: 1.4.2 467 | license: MIT 468 | open_source: true 469 | hosted_saas: false 470 | category: Libraries 471 | sub_category: Go Modules Packages 472 | image_url: https://img.stackshare.io/package/go-packages/image.png 473 | detection_source_url: https://github.com/Clivern/Beaver/blob/main/go.sum 474 | detection_source: go.mod 475 | last_updated_by: Clivern 476 | last_updated_on: 2023-04-24 21:31:39.000000000 Z 477 | - name: mousetrap 478 | description: Detect starting from Windows explorer 479 | package_url: https://pkg.go.dev/github.com/inconshreveable/mousetrap 480 | version: 1.1.0 481 | license: Apache-2.0 482 | open_source: true 483 | hosted_saas: false 484 | category: Libraries 485 | sub_category: Go Modules Packages 486 | image_url: https://img.stackshare.io/package/go-packages/image.png 487 | detection_source_url: https://github.com/Clivern/Beaver/blob/main/go.sum 488 | detection_source: go.mod 489 | last_updated_by: Clivern 490 | last_updated_on: 2023-04-24 21:31:39.000000000 Z 491 | - name: net 492 | description: Go supplementary network libraries 493 | package_url: https://pkg.go.dev/golang.org/x/net 494 | version: 0.0.0 495 | license: BSD-3-Clause 496 | open_source: true 497 | hosted_saas: false 498 | category: Libraries 499 | sub_category: Go Modules Packages 500 | image_url: https://img.stackshare.io/package/go-packages/image.png 501 | detection_source_url: https://github.com/Clivern/Beaver/blob/main/go.sum 502 | detection_source: go.mod 503 | last_updated_by: Clivern 504 | last_updated_on: 2019-01-08 22:10:24.000000000 Z 505 | - name: perks 506 | description: Effective Computation of Things 507 | package_url: https://pkg.go.dev/github.com/beorn7/perks 508 | version: 1.0.1 509 | license: MIT 510 | open_source: true 511 | hosted_saas: false 512 | category: Libraries 513 | sub_category: Go Modules Packages 514 | image_url: https://img.stackshare.io/package/go-packages/image.png 515 | detection_source_url: https://github.com/Clivern/Beaver/blob/main/go.sum 516 | detection_source: go.mod 517 | last_updated_by: Clivern 518 | last_updated_on: 2023-04-24 21:31:39.000000000 Z 519 | - name: pflag 520 | description: Drop-in replacement for Go's flag package 521 | package_url: https://pkg.go.dev/github.com/spf13/pflag 522 | version: 1.0.5 523 | license: BSD-3-Clause 524 | open_source: true 525 | hosted_saas: false 526 | category: Libraries 527 | sub_category: Go Modules Packages 528 | image_url: https://img.stackshare.io/package/go-packages/image.png 529 | detection_source_url: https://github.com/Clivern/Beaver/blob/main/go.sum 530 | detection_source: go.mod 531 | last_updated_by: Clivern 532 | last_updated_on: 2023-04-24 21:31:39.000000000 Z 533 | - name: procfs 534 | description: Procfs provides functions to retrieve system 535 | package_url: https://pkg.go.dev/github.com/prometheus/procfs 536 | version: 0.10.1 537 | license: Apache-2.0 538 | open_source: true 539 | hosted_saas: false 540 | category: Libraries 541 | sub_category: Go Modules Packages 542 | image_url: https://img.stackshare.io/package/go-packages/image.png 543 | detection_source_url: https://github.com/Clivern/Beaver/blob/main/go.sum 544 | detection_source: go.mod 545 | last_updated_by: Clivern 546 | last_updated_on: 2023-04-24 21:31:39.000000000 Z 547 | - name: properties 548 | description: Java properties scanner for Go 549 | package_url: https://pkg.go.dev/github.com/magiconair/properties 550 | version: 1.8.7 551 | license: BSD-2-Clause 552 | open_source: true 553 | hosted_saas: false 554 | category: Libraries 555 | sub_category: Go Modules Packages 556 | image_url: https://img.stackshare.io/package/go-packages/image.png 557 | detection_source_url: https://github.com/Clivern/Beaver/blob/main/go.sum 558 | detection_source: go.mod 559 | last_updated_by: Clivern 560 | last_updated_on: 2023-04-24 21:31:39.000000000 Z 561 | - name: proto 562 | description: Go support for Google's protocol buffers 563 | package_url: https://pkg.go.dev/github.com/golang/protobuf/proto 564 | license: BSD-3-Clause 565 | open_source: true 566 | hosted_saas: false 567 | category: Libraries 568 | sub_category: Go Modules Packages 569 | image_url: https://img.stackshare.io/package/go-packages/image.png 570 | detection_source: go.mod 571 | last_updated_by: Clivern 572 | last_updated_on: 2023-04-24 21:31:39.000000000 Z 573 | - name: reflect2 574 | description: Reflect api without runtime reflect.Value cost 575 | package_url: https://pkg.go.dev/github.com/modern-go/reflect2 576 | version: 1.0.2 577 | license: Apache-2.0 578 | open_source: true 579 | hosted_saas: false 580 | category: Libraries 581 | sub_category: Go Modules Packages 582 | image_url: https://img.stackshare.io/package/go-packages/image.png 583 | detection_source_url: https://github.com/Clivern/Beaver/blob/main/go.sum 584 | detection_source: go.mod 585 | last_updated_by: Clivern 586 | last_updated_on: 2023-04-24 21:31:39.000000000 Z 587 | - name: sys 588 | description: Go packages for low-level interaction with the operating system 589 | package_url: https://pkg.go.dev/golang.org/x/sys 590 | version: 0.0.0 591 | license: BSD-3-Clause 592 | open_source: true 593 | hosted_saas: false 594 | category: Libraries 595 | sub_category: Go Modules Packages 596 | image_url: https://img.stackshare.io/package/go-packages/image.png 597 | detection_source_url: https://github.com/Clivern/Beaver/blob/main/go.sum 598 | detection_source: go.mod 599 | last_updated_by: Clivern 600 | last_updated_on: 2019-01-08 22:10:24.000000000 Z 601 | - name: text 602 | description: Go text processing support 603 | package_url: https://pkg.go.dev/golang.org/x/text 604 | version: 0.0.0 605 | license: BSD-3-Clause 606 | open_source: true 607 | hosted_saas: false 608 | category: Libraries 609 | sub_category: Go Modules Packages 610 | image_url: https://img.stackshare.io/package/go-packages/image.png 611 | detection_source_url: https://github.com/Clivern/Beaver/blob/main/go.sum 612 | detection_source: go.mod 613 | last_updated_by: Clivern 614 | last_updated_on: 2023-04-24 21:31:39.000000000 Z 615 | - name: universal-translator 616 | description: ":speech_balloon: i18n Translator for Go/Golang using CLDR data + pluralization 617 | rules" 618 | package_url: https://pkg.go.dev/github.com/go-playground/universal-translator 619 | version: 0.18.1 620 | license: MIT 621 | open_source: true 622 | hosted_saas: false 623 | category: Libraries 624 | sub_category: Go Modules Packages 625 | image_url: https://img.stackshare.io/package/go-packages/image.png 626 | detection_source_url: https://github.com/Clivern/Beaver/blob/main/go.sum 627 | detection_source: go.mod 628 | last_updated_by: Clivern 629 | last_updated_on: 2023-04-24 21:31:39.000000000 Z 630 | - name: viper 631 | description: Go configuration with fangs 632 | package_url: https://pkg.go.dev/github.com/spf13/viper 633 | version: 1.16.0 634 | license: MIT 635 | open_source: true 636 | hosted_saas: false 637 | category: Libraries 638 | sub_category: Go Modules Packages 639 | image_url: https://img.stackshare.io/package/go-packages/image.png 640 | detection_source_url: https://github.com/Clivern/Beaver/blob/main/go.sum 641 | detection_source: go.mod 642 | last_updated_by: Clivern 643 | last_updated_on: 2023-04-24 21:31:39.000000000 Z 644 | - name: websocket 645 | description: A fast 646 | package_url: https://pkg.go.dev/github.com/gorilla/websocket 647 | version: 1.5.0 648 | license: BSD-3-Clause 649 | open_source: true 650 | hosted_saas: false 651 | category: Libraries 652 | sub_category: Go Modules Packages 653 | image_url: https://img.stackshare.io/package/go-packages/image.png 654 | detection_source_url: https://github.com/Clivern/Beaver/blob/main/go.sum 655 | detection_source: go.mod 656 | last_updated_by: renovate[bot] 657 | last_updated_on: 2022-02-15 19:12:44.000000000 Z 658 | -------------------------------------------------------------------------------- /web/static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clivern/Beaver/bd68afd9d44386b789e46d303f7f4a865ac4cc68/web/static/.gitkeep -------------------------------------------------------------------------------- /web/static/css/app.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clivern/Beaver/bd68afd9d44386b789e46d303f7f4a865ac4cc68/web/static/css/app.css -------------------------------------------------------------------------------- /web/static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Clivern/Beaver/bd68afd9d44386b789e46d303f7f4a865ac4cc68/web/static/img/logo.png -------------------------------------------------------------------------------- /web/static/js/beaver.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Beaver Client 3 | */ 4 | 5 | function Socket(url){ 6 | ws = new WebSocket(url); 7 | ws.onmessage = function(e) { console.log(e); }; 8 | ws.onclose = function(){ 9 | // Try to reconnect in 5 seconds 10 | setTimeout(function(){Socket(url)}, 5000); 11 | }; 12 | } -------------------------------------------------------------------------------- /web/template/index.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ .title }} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 59 | 60 | 61 |
62 |
63 | 64 |
65 | {{ .title }} 66 |
67 |

A Real Time Messaging Server.

68 |
69 |
70 | 71 | --------------------------------------------------------------------------------