mirror of
https://github.com/owncast/owncast.git
synced 2024-11-22 04:40:37 +03:00
Merge branch 'develop' into antd-update-5_12
This commit is contained in:
commit
ae3c345f75
722 changed files with 60280 additions and 73337 deletions
1
.earthlyignore
Normal file
1
.earthlyignore
Normal file
|
@ -0,0 +1 @@
|
|||
test/automated/api/node_modules
|
|
@ -1,12 +1,17 @@
|
|||
# Read first
|
||||
|
||||
Please include a summary of the change and which issue number is fixed, including relevant motivation and context. Feel free to mark this as a Draft or WIP and write up some details later.
|
||||
If this is an unsolicited change, or there is no existing issue filed for it, please open a GitHub issue before creating a pull request. This will allow us to discuss the motivations and the big picture behind the change first. It's possible there may be other solutions that should be discussed for what you think should be built. It is possible your change will be rejected unless some discussion around your proposal happens first. While creating this PR means you probably already did the work, it still makes sense to file an issue now, and into the future when you have proposed changes.
|
||||
|
||||
If there is no issue filed for this particular change it's highly recommended you file one. While creating this PR means you probably already did the work, in the future make sure an issue is filed beforehand so changes, fixes and features can be discussed ahead of time.
|
||||
## Description
|
||||
|
||||
# Description
|
||||
Please include a summary of the change and which issue number is fixed, including relevant motivation and context. Feel free to mark this as a Draft or WIP and write up some details later and start a conversation, even if your PR is not ready for review.
|
||||
|
||||
Fixes # (issue)
|
||||
|
||||
## Screenshot Examples or Logs
|
||||
|
||||
If this is a frontend change, please include a screenshot of the change. If this is a backend change, please include relevant logs or examples of the change in action if applicable.
|
||||
|
||||
---
|
||||
|
||||
Some things you might want to mention:
|
||||
|
@ -16,4 +21,4 @@ Some things you might want to mention:
|
|||
3. If you're fixing something, what was wrong? How should we stop from having this issue happen again?
|
||||
4. If this is a new feature or addition to functionality, why should it be added? What are the use cases? Who was asking for this functionality?
|
||||
|
||||
If this is an unsolicited change or have no issue associated please do your best to detail the motivations behind this PR, and think about filing an issue to discuss changes ahead of time in the future.
|
||||
Thank you so much for contributing to Owncast! 🎉
|
||||
|
|
31
.github/stale.yml
vendored
31
.github/stale.yml
vendored
|
@ -1,31 +0,0 @@
|
|||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 60
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 7
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- backlog
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. If this
|
||||
was a feature request that others have shown no interest in then it's
|
||||
likely to not get implemented due to lack of interest. If others also
|
||||
want to see this feature then now is the time to say something!
|
||||
Thank you for your contributions.
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
||||
exemptMilestones: true
|
||||
|
||||
# Since old PRs are less useful than old issues ping them sooner.
|
||||
pulls:
|
||||
daysUntilStale: 30
|
||||
markComment: >
|
||||
This pull request has not had any activity in 30 days. Since things move fast it's best
|
||||
to get PRs merged in. If this PR addresses a previously filed issue that needs to be
|
||||
resolved please work to get it merged in, or allow somebody else to work on a fix.
|
||||
This PR will be closed if no further activity occurs. Thank you for your contributions!
|
||||
exemptLabels:
|
||||
- bot
|
10
.github/workflows/actions-lint.yml
vendored
10
.github/workflows/actions-lint.yml
vendored
|
@ -13,7 +13,15 @@ jobs:
|
|||
name: GitHub actions
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Check out pull request code
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'pull_request'
|
||||
with:
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'push'
|
||||
|
||||
- uses: docker://rhysd/actionlint:latest
|
||||
with:
|
||||
|
|
2
.github/workflows/auto-comment-on-label.yaml
vendored
2
.github/workflows/auto-comment-on-label.yaml
vendored
|
@ -11,7 +11,7 @@ jobs:
|
|||
issues: write
|
||||
steps:
|
||||
- name: Add comment
|
||||
uses: peter-evans/create-or-update-comment@9bb5d837b91928730a318c22b99b9f42a0f005e1
|
||||
uses: peter-evans/create-or-update-comment@6f2ce0ef39607bc6465c7fdf93368dc08095f938
|
||||
with:
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
body: |
|
||||
|
|
24
.github/workflows/automated-end-to-end-api.yaml
vendored
24
.github/workflows/automated-end-to-end-api.yaml
vendored
|
@ -18,23 +18,43 @@ jobs:
|
|||
with:
|
||||
concurrent_skipping: 'same_content_newer'
|
||||
|
||||
- name: Check out pull request code
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'pull_request'
|
||||
with:
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'push'
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files-yaml
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
files_yaml: |
|
||||
src:
|
||||
- '**/*.{go,mod,sum}'
|
||||
|
||||
- uses: earthly/actions-setup@v1
|
||||
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
||||
with:
|
||||
version: 'latest' # or pin to an specific version, e.g. "v0.6.10"
|
||||
|
||||
- name: Earthly version
|
||||
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
||||
run: earthly --version
|
||||
|
||||
- name: Set up QEMU
|
||||
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
||||
id: qemu
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
image: tonistiigi/binfmt:latest
|
||||
platforms: all
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Run API tests
|
||||
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
|
|
11
.github/workflows/browser-testing.yml
vendored
11
.github/workflows/browser-testing.yml
vendored
|
@ -19,12 +19,19 @@ jobs:
|
|||
with:
|
||||
concurrent_skipping: 'same_content_newer'
|
||||
|
||||
- name: Checkout
|
||||
- name: Check out pull request code
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'pull_request'
|
||||
with:
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'push'
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: latest
|
||||
node-version: '22.9.0'
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v4
|
||||
|
|
9
.github/workflows/build-storybook.yml
vendored
9
.github/workflows/build-storybook.yml
vendored
|
@ -11,8 +11,15 @@ jobs:
|
|||
if: github.repository == 'owncast/owncast'
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
- name: Check out pull request code
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'pull_request'
|
||||
with:
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'push'
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v4
|
||||
|
|
30
.github/workflows/chromatic.yml
vendored
30
.github/workflows/chromatic.yml
vendored
|
@ -27,23 +27,37 @@ jobs:
|
|||
uses: fkirc/skip-duplicate-actions@v5
|
||||
with:
|
||||
concurrent_skipping: 'same_content_newer'
|
||||
- name: Check out code
|
||||
if: ${{ github.actor != 'renovate[bot]' && github.actor != 'renovate' }}
|
||||
|
||||
- name: Check out pull request code
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'pull_request' || github.event_name == 'pull_request_target'
|
||||
with:
|
||||
# Make sure the actual branch is checked out when running on pull requests
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'push'
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files-yaml
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
path: 'web'
|
||||
files_ignore: |
|
||||
static/**
|
||||
web/next.config.js
|
||||
files_yaml: |
|
||||
src:
|
||||
- '**/*.{js,ts,tsx,jsx,md}'
|
||||
|
||||
- name: Install dependencies
|
||||
if: ${{ github.actor != 'renovate[bot]' && github.actor != 'renovate' }}
|
||||
if: ${{ github.actor != 'renovate[bot]' && github.actor != 'renovate' && steps.changed-files-yaml.outputs.src_any_changed == 'true'}}
|
||||
run: npm install
|
||||
|
||||
- name: Publish to Chromatic
|
||||
if: ${{ github.actor != 'renovate[bot]' && github.actor != 'renovate' }}
|
||||
|
||||
if: ${{ github.actor != 'renovate[bot]' && github.actor != 'renovate' && steps.changed-files-yaml.outputs.src_any_changed == 'true' }}
|
||||
uses: chromaui/action@v11
|
||||
|
||||
# Chromatic GitHub Action options
|
||||
with:
|
||||
workingDir: web
|
||||
|
|
9
.github/workflows/codeql-analysis.yml
vendored
9
.github/workflows/codeql-analysis.yml
vendored
|
@ -36,8 +36,15 @@ jobs:
|
|||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
- name: Check out pull request code
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'pull_request'
|
||||
with:
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'push'
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
|
|
10
.github/workflows/container-lint.yml
vendored
10
.github/workflows/container-lint.yml
vendored
|
@ -19,7 +19,15 @@ jobs:
|
|||
container:
|
||||
image: aquasec/trivy
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Check out pull request code
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'pull_request'
|
||||
with:
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'push'
|
||||
|
||||
- name: Check critical issues
|
||||
run: trivy config --exit-code 1 --severity "HIGH,CRITICAL" ./Dockerfile
|
||||
|
|
9
.github/workflows/container.yaml
vendored
9
.github/workflows/container.yaml
vendored
|
@ -37,10 +37,15 @@ jobs:
|
|||
image: tonistiigi/binfmt:latest
|
||||
platforms: all
|
||||
|
||||
- name: Checkout repo
|
||||
- name: Check out pull request code
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'pull_request'
|
||||
with:
|
||||
fetch-depth: 0
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'push'
|
||||
|
||||
- name: Build and push
|
||||
if: ${{ github.event_name == 'schedule' && env.GH_CR_PAT != null }}
|
||||
|
|
53
.github/workflows/css-lint.yaml
vendored
Normal file
53
.github/workflows/css-lint.yaml
vendored
Normal file
|
@ -0,0 +1,53 @@
|
|||
name: CSS Lint and Formatting
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'web/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'web/**'
|
||||
|
||||
jobs:
|
||||
css-lint:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./web
|
||||
|
||||
steps:
|
||||
- name: Check out pull request code
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'pull_request'
|
||||
with:
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'push'
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files-yaml
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
path: 'web'
|
||||
files_yaml: |
|
||||
src:
|
||||
- '**/*.{css,scss}'
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22.9.0'
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
||||
run: npm install
|
||||
|
||||
- name: Run Prettier
|
||||
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
||||
run: npx prettier --check ${{ steps.changed-files-yaml.outputs.src_all_changed_files }}
|
||||
|
||||
- name: Run Stylelint
|
||||
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
||||
run: npx stylelint ${{ steps.changed-files-yaml.outputs.src_all_changed_files }}
|
|
@ -2,19 +2,26 @@ name: javascript-packages
|
|||
on:
|
||||
push:
|
||||
paths:
|
||||
- spec/openapi.yaml
|
||||
- openapi.yaml
|
||||
|
||||
jobs:
|
||||
run:
|
||||
name: Generate API Documentation
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
- name: Check out pull request code
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'pull_request'
|
||||
with:
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'push'
|
||||
|
||||
- name: Run redoc on openapi.yaml
|
||||
run: |
|
||||
npx redoc-cli bundle spec/openapi.yaml -o docs/api/index.html --options '{"hideHostname": true, "pathInMiddlePanel": true}'
|
||||
npx @redocly/cli --config docs/api/redocly.yaml build-docs openapi.yaml -o docs/api/index.html
|
||||
|
||||
- name: Commit changes
|
||||
uses: EndBug/add-and-commit@v9
|
||||
|
|
21
.github/workflows/go-tests.yaml
vendored
21
.github/workflows/go-tests.yaml
vendored
|
@ -18,7 +18,16 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files-yaml
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
files_yaml: |
|
||||
src:
|
||||
- '**/*.{go,mod,sum}'
|
||||
|
||||
- uses: actions/cache@v4
|
||||
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
||||
with:
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
|
@ -28,12 +37,14 @@ jobs:
|
|||
go-test-
|
||||
|
||||
- name: Install go
|
||||
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '^1'
|
||||
cache: true
|
||||
|
||||
- name: Run tests
|
||||
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
||||
run: go test ./...
|
||||
|
||||
test-bsds:
|
||||
|
@ -49,6 +60,14 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files-yaml
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
files_yaml: |
|
||||
src:
|
||||
- '**/*.{go,mod,sum}'
|
||||
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
|
@ -59,10 +78,12 @@ jobs:
|
|||
go-test-
|
||||
|
||||
- name: Install go
|
||||
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '^1'
|
||||
cache: true
|
||||
|
||||
- name: Run tests
|
||||
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
||||
run: go test ./...
|
||||
|
|
23
.github/workflows/hls-tests.yml
vendored
23
.github/workflows/hls-tests.yml
vendored
|
@ -24,8 +24,27 @@ jobs:
|
|||
uses: fkirc/skip-duplicate-actions@v5
|
||||
with:
|
||||
concurrent_skipping: 'same_content_newer'
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Check out pull request code
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'pull_request'
|
||||
with:
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'push'
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files-yaml
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
files_yaml: |
|
||||
src:
|
||||
- '**/*.{go,mod,sum}'
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
||||
with:
|
||||
go-version: '1.22'
|
||||
cache: true
|
||||
|
@ -43,6 +62,7 @@ jobs:
|
|||
${{ runner.os }}-
|
||||
|
||||
- name: Local stroage
|
||||
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
|
@ -50,6 +70,7 @@ jobs:
|
|||
command: cd test/automated/hls && ./run.sh
|
||||
|
||||
- name: S3 storage
|
||||
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
||||
uses: nick-fields/retry@v3
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
|
|
|
@ -28,18 +28,25 @@ jobs:
|
|||
cancel_others: 'true'
|
||||
skip_after_successful_duplicate: 'true'
|
||||
|
||||
- name: Checkout
|
||||
- name: Check out pull request code
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'pull_request'
|
||||
with:
|
||||
# Make sure the actual branch is checked out when running on pull requests
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: true
|
||||
ref: ${{github.event.pull_request.head.ref}}
|
||||
|
||||
- name: Setup Nodejs
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22.9.0'
|
||||
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'push'
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files-yaml
|
||||
uses: tj-actions/changed-files@v44
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
path: 'web'
|
||||
files_ignore: |
|
||||
|
@ -47,7 +54,7 @@ jobs:
|
|||
web/next.config.js
|
||||
files_yaml: |
|
||||
src:
|
||||
- '**/*.{js,ts,tsx,jsx,css,md}'
|
||||
- '**/*.{js,ts,tsx,jsx,md}'
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v4
|
||||
|
@ -64,19 +71,27 @@ jobs:
|
|||
- name: Install Dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Lint
|
||||
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
||||
- name: Lint and fix
|
||||
if: steps.changed-files-yaml.outputs.src_any_changed == 'true' && github.event_name != 'pull_request'
|
||||
run: npx eslint --fix ${{ steps.changed-files-yaml.outputs.src_all_changed_files }}
|
||||
|
||||
- name: Prettier
|
||||
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
||||
- name: Lint
|
||||
if: steps.changed-files-yaml.outputs.src_any_changed == 'true' && github.event_name == 'pull_request'
|
||||
run: npx eslint ${{ steps.changed-files-yaml.outputs.src_all_changed_files }}
|
||||
|
||||
- name: Prettier formatting
|
||||
if: steps.changed-files-yaml.outputs.src_any_changed == 'true' && github.event_name == 'pull_request'
|
||||
run: npx prettier --write ${{ steps.changed-files-yaml.outputs.src_all_changed_files }}
|
||||
|
||||
- name: Prettier check
|
||||
if: steps.changed-files-yaml.outputs.src_any_changed == 'true' && github.event_name != 'pull_request'
|
||||
run: npx prettier ${{ steps.changed-files-yaml.outputs.src_all_changed_files }}
|
||||
|
||||
- name: Debug changed files output
|
||||
run: 'pwd && echo "Changed files: ${{ steps.changed-files-yaml.outputs.src_all_changed_files }}"'
|
||||
|
||||
- name: Commit changes
|
||||
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
|
||||
if: steps.changed-files-yaml.outputs.src_any_changed == 'true' && github.event_name != 'pull_request'
|
||||
uses: EndBug/add-and-commit@v9
|
||||
with:
|
||||
author_name: Owncast
|
||||
|
@ -101,13 +116,20 @@ jobs:
|
|||
cancel_others: 'true'
|
||||
skip_after_successful_duplicate: 'true'
|
||||
|
||||
- name: Checkout
|
||||
- name: Check out pull request code
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'pull_request'
|
||||
with:
|
||||
# Make sure the actual branch is checked out when running on pull requests
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'push'
|
||||
|
||||
- name: Setup Nodejs
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22.9.0'
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v4
|
||||
|
@ -127,6 +149,10 @@ jobs:
|
|||
- name: Check for unused JS code and dependencies
|
||||
run: npx knip --include dependencies,files,exports
|
||||
|
||||
- name: Run tests
|
||||
working-directory: ./web
|
||||
run: npm test
|
||||
|
||||
# After any formatting and linting is complete we can run the build
|
||||
# and bundle step. This both will verify that the build is successful as
|
||||
# well as commiting the updated static files into the repository for use.
|
||||
|
@ -143,6 +169,11 @@ jobs:
|
|||
cancel_others: 'true'
|
||||
skip_after_successful_duplicate: 'true'
|
||||
|
||||
- name: Setup Nodejs
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22.9.0'
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
|
@ -155,13 +186,15 @@ jobs:
|
|||
${{ runner.os }}-build-
|
||||
${{ runner.os }}-
|
||||
|
||||
- name: Checkout
|
||||
- name: Check out pull request code
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'pull_request'
|
||||
with:
|
||||
# Make sure the actual branch is checked out when running on pull requests
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'push'
|
||||
|
||||
- name: Bundle web app (next.js build)
|
||||
run: build/web/bundleWeb.sh
|
45
.github/workflows/javascript-tests.yml
vendored
45
.github/workflows/javascript-tests.yml
vendored
|
@ -1,45 +0,0 @@
|
|||
name: Javascript Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'web/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'web/**'
|
||||
|
||||
jobs:
|
||||
jest-run:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: skip_check
|
||||
uses: fkirc/skip-duplicate-actions@v5
|
||||
with:
|
||||
concurrent_skipping: 'same_content_newer'
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 18.9.0
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: cache-node-modules-javascript-tests
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('web/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
${{ runner.os }}-build-
|
||||
${{ runner.os }}-
|
||||
|
||||
- name: Install Dependencies
|
||||
working-directory: ./web
|
||||
run: npm install
|
||||
|
||||
- name: Run tests
|
||||
working-directory: ./web
|
||||
run: npm test
|
11
.github/workflows/screenshots.yml
vendored
11
.github/workflows/screenshots.yml
vendored
|
@ -14,7 +14,16 @@ jobs:
|
|||
Screenshots:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Check out pull request code
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'pull_request'
|
||||
with:
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'push'
|
||||
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.22'
|
||||
|
|
10
.github/workflows/shellcheck.yml
vendored
10
.github/workflows/shellcheck.yml
vendored
|
@ -20,7 +20,15 @@ jobs:
|
|||
container:
|
||||
image: docker.io/ubuntu:24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Check out pull request code
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'pull_request'
|
||||
with:
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
if: github.event_name == 'push'
|
||||
|
||||
- name: Install shellcheck
|
||||
run: apt update && apt install -y shellcheck bash && shellcheck --version
|
||||
|
|
46
.github/workflows/stale.yml
vendored
Normal file
46
.github/workflows/stale.yml
vendored
Normal file
|
@ -0,0 +1,46 @@
|
|||
name: 'Close stale issues and PRs'
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 */2 * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
exempt-all-milestones: true
|
||||
|
||||
days-before-issue-stale: 60
|
||||
days-before-issue-close: 67
|
||||
exempt-issue-labels: backlog,long-lived,bot
|
||||
stale-issue-message: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. If this
|
||||
was a feature request that others have shown no interest in, then it's
|
||||
unlikely to get implemented due to lack of interest. If others also
|
||||
want to see this feature then now is the time to say something! If this
|
||||
is a bug report or you have questions that still need answering, please say
|
||||
something. Feel free to drop by [our chat](https://owncast.rocket.chat) if
|
||||
you'd like to discuss in real-time with people.
|
||||
close-issue-message: >
|
||||
This issue has been automatically closed due to inactivity. This isn't done
|
||||
to be a jerk, or because the project doesn't care. But simply to keep the focus
|
||||
on things that are actively discussed, and has continued interest from the community and
|
||||
Owncast developers. Feel free to to comment if there is still discussion to be
|
||||
had, or if you plan to work on it. Feel free to drop by [our chat](https://owncast.rocket.chat)
|
||||
if you'd like to discuss in real-time with people. Thank you for being involved!
|
||||
|
||||
days-before-pr-stale: 30
|
||||
days-before-pr-close: 37
|
||||
stale-pr-message: >
|
||||
This pull request has not had any activity in 30 days. If it has been abandoned
|
||||
no future actions are necessary, it will be automatically closed. If this is a PR
|
||||
with no clear plan on how to move forward on it getting into the project, then
|
||||
further discussion is needed. Now is a good time to discuss if this is still
|
||||
something that should be worked on. If this PR is idle simply because nobody
|
||||
has reviewed it, then feel free to ping somebody. However, if this PR is not linked to an
|
||||
existing issue regarding something that was previously determined to be important, then even
|
||||
more discussion needs to take place before it can get anywhere.
|
||||
This PR will be closed if no further activity occurs. Thank you for your contributions!
|
||||
close-pr-message: 'This PR was closed because it has been stalled for 10 days with no activity.'
|
|
@ -28,7 +28,6 @@ linters:
|
|||
- bodyclose
|
||||
- dupl
|
||||
- errcheck
|
||||
- exportloopref
|
||||
- goconst
|
||||
- godot
|
||||
- godox
|
||||
|
@ -49,7 +48,7 @@ linters:
|
|||
- cyclop
|
||||
- gosimple
|
||||
- unused
|
||||
- exportloopref
|
||||
- copyloopvar
|
||||
- gocritic
|
||||
- forbidigo
|
||||
- unparam
|
||||
|
@ -67,12 +66,6 @@ linters-settings:
|
|||
# should ignore tests
|
||||
skip-tests: true
|
||||
|
||||
gosimple:
|
||||
# Select the Go version to target. The default is '1.13'.
|
||||
go: '1.22'
|
||||
# https://staticcheck.io/docs/options#checks
|
||||
checks: ['all']
|
||||
|
||||
gocritic:
|
||||
disabled-checks:
|
||||
- ifElseChain
|
||||
|
|
|
@ -22,7 +22,7 @@ ENV NAME=${NAME}
|
|||
RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -ldflags "-extldflags \"-static\" -s -w -X github.com/owncast/owncast/config.GitCommit=$GIT_COMMIT -X github.com/owncast/owncast/config.VersionNumber=$VERSION -X github.com/owncast/owncast/config.BuildPlatform=$NAME" -o owncast .
|
||||
|
||||
# Create the image by copying the result of the build into a new alpine image
|
||||
FROM alpine:3.20.0
|
||||
FROM alpine:3.20.3
|
||||
RUN apk update && apk add --no-cache ffmpeg ffmpeg-libs ca-certificates && update-ca-certificates
|
||||
|
||||
RUN addgroup -g 101 -S owncast && adduser -u 101 -S owncast -G owncast
|
||||
|
|
10
Earthfile
10
Earthfile
|
@ -1,6 +1,6 @@
|
|||
VERSION --new-platform 0.6
|
||||
|
||||
FROM --platform=linux/amd64 alpine:3.15.5
|
||||
FROM --platform=linux/amd64 alpine:3.20.3
|
||||
ARG version=develop
|
||||
|
||||
WORKDIR /build
|
||||
|
@ -87,7 +87,7 @@ build:
|
|||
RUN upx -t owncast
|
||||
END
|
||||
|
||||
SAVE ARTIFACT owncast owncast
|
||||
SAVE ARTIFACT --keep-ts owncast owncast
|
||||
|
||||
package:
|
||||
RUN apk add --update --no-cache zip >> /dev/null
|
||||
|
@ -109,7 +109,7 @@ package:
|
|||
ARG NAME=custom
|
||||
END
|
||||
|
||||
COPY (+build/owncast --platform $TARGETPLATFORM) /build/dist/owncast
|
||||
COPY --keep-ts (+build/owncast --platform $TARGETPLATFORM) /build/dist/owncast
|
||||
ENV ZIPNAME owncast-$version-$NAME.zip
|
||||
RUN cd /build/dist && zip -r -q -8 /build/dist/owncast.zip .
|
||||
SAVE ARTIFACT --keep-ts /build/dist/owncast.zip owncast.zip AS LOCAL dist/$ZIPNAME
|
||||
|
@ -119,11 +119,11 @@ docker:
|
|||
# in as space separated strings using the full account/repo:tag format.
|
||||
# https://github.com/earthly/earthly/blob/aea38448fa9c0064b1b70d61be717ae740689fb9/docs/earthfile/earthfile.md#assigning-multiple-image-names
|
||||
ARG TARGETPLATFORM
|
||||
FROM --platform=$TARGETPLATFORM alpine:3.15.5
|
||||
FROM --platform=$TARGETPLATFORM alpine:3.20.3
|
||||
RUN apk update && apk add --no-cache ffmpeg ffmpeg-libs ca-certificates unzip && update-ca-certificates
|
||||
RUN addgroup -g 101 -S owncast && adduser -u 101 -S owncast -G owncast
|
||||
WORKDIR /app
|
||||
COPY --platform=$TARGETPLATFORM +package/owncast.zip /app
|
||||
COPY --keep-ts --platform=$TARGETPLATFORM +package/owncast.zip /app
|
||||
RUN unzip -x owncast.zip && mkdir data
|
||||
|
||||
# temporarily disable until we figure out how to move forward
|
||||
|
|
|
@ -284,6 +284,12 @@ func writeResponse(payload interface{}, w http.ResponseWriter) error {
|
|||
|
||||
// HostMetaController points to webfinger.
|
||||
func HostMetaController(w http.ResponseWriter, r *http.Request) {
|
||||
if !data.GetFederationEnabled() {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
log.Debugln("host meta request rejected! Federation is not enabled")
|
||||
return
|
||||
}
|
||||
|
||||
serverURL := data.GetServerURL()
|
||||
if serverURL == "" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
|
|
|
@ -45,8 +45,8 @@ func GetFederationFollowers(limit int, offset int) ([]models.Follower, int, erro
|
|||
}
|
||||
|
||||
followersResult, err := _datastore.GetQueries().GetFederationFollowersWithOffset(ctx, db.GetFederationFollowersWithOffsetParams{
|
||||
Limit: int32(limit),
|
||||
Offset: int32(offset),
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
|
|
|
@ -237,7 +237,7 @@ func GetOutbox(limit int, offset int) (vocab.ActivityStreamsOrderedCollection, e
|
|||
orderedItems := streams.NewActivityStreamsOrderedItemsProperty()
|
||||
rows, err := _datastore.GetQueries().GetOutboxWithOffset(
|
||||
context.Background(),
|
||||
db.GetOutboxWithOffsetParams{Limit: int32(limit), Offset: int32(offset)},
|
||||
db.GetOutboxWithOffsetParams{Limit: limit, Offset: offset},
|
||||
)
|
||||
if err != nil {
|
||||
return collection, err
|
||||
|
@ -309,8 +309,8 @@ func SaveInboundFediverseActivity(objectIRI string, actorIRI string, eventType s
|
|||
func GetInboundActivities(limit int, offset int) ([]models.FederatedActivity, int, error) {
|
||||
ctx := context.Background()
|
||||
rows, err := _datastore.GetQueries().GetInboundActivitiesWithOffset(ctx, db.GetInboundActivitiesWithOffsetParams{
|
||||
Limit: int32(limit),
|
||||
Offset: int32(offset),
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/owncast/owncast/core/data"
|
||||
"github.com/owncast/owncast/core/user"
|
||||
|
||||
"github.com/owncast/owncast/db"
|
||||
)
|
||||
|
||||
var _datastore *data.Datastore
|
||||
|
@ -27,41 +21,3 @@ func Setup(db *data.Datastore) {
|
|||
_datastore.MustExec(createTableSQL)
|
||||
_datastore.MustExec(`CREATE INDEX IF NOT EXISTS idx_auth_token ON auth (token);`)
|
||||
}
|
||||
|
||||
// AddAuth will add an external authentication token and type for a user.
|
||||
func AddAuth(userID, authToken string, authType Type) error {
|
||||
return _datastore.GetQueries().AddAuthForUser(context.Background(), db.AddAuthForUserParams{
|
||||
UserID: userID,
|
||||
Token: authToken,
|
||||
Type: string(authType),
|
||||
})
|
||||
}
|
||||
|
||||
// GetUserByAuth will return an existing user given auth details if a user
|
||||
// has previously authenticated with that method.
|
||||
func GetUserByAuth(authToken string, authType Type) *user.User {
|
||||
u, err := _datastore.GetQueries().GetUserByAuth(context.Background(), db.GetUserByAuthParams{
|
||||
Token: authToken,
|
||||
Type: string(authType),
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var scopes []string
|
||||
if u.Scopes.Valid {
|
||||
scopes = strings.Split(u.Scopes.String, ",")
|
||||
}
|
||||
|
||||
return &user.User{
|
||||
ID: u.ID,
|
||||
DisplayName: u.DisplayName,
|
||||
DisplayColor: int(u.DisplayColor),
|
||||
CreatedAt: u.CreatedAt.Time,
|
||||
DisabledAt: &u.DisabledAt.Time,
|
||||
PreviousNames: strings.Split(u.PreviousNames.String, ","),
|
||||
NameChangedAt: &u.NamechangedAt.Time,
|
||||
AuthenticatedAt: &u.AuthenticatedAt.Time,
|
||||
Scopes: scopes,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,27 +4,24 @@
|
|||
|
||||
# setup
|
||||
package="generated"
|
||||
folderPath="handler/generated"
|
||||
specPath="spec/openapi.yaml"
|
||||
folderPath="webserver/handlers/generated"
|
||||
specPath="openapi.yaml"
|
||||
|
||||
# validate scripts are installed
|
||||
if ! command -v swagger-cli &> /dev/null
|
||||
then
|
||||
echo "Please install \`swagger-cli\` before running this script"
|
||||
if ! command -v redocly &>/dev/null; then
|
||||
echo "Please install \`redocly cli\` before running this script: npm install -g @redocly/cli"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v oapi-codegen &> /dev/null
|
||||
then
|
||||
if ! command -v oapi-codegen &>/dev/null; then
|
||||
echo "Please install \`oapi-codegen\` before running this script"
|
||||
echo "Hint: run \`go install github.com/deepmap/oapi-codegen/v2/cmd/oapi-codegen@latest\` to install"
|
||||
echo "Hint: run \`go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest\` to install"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# validate schema
|
||||
swagger-cli validate $specPath
|
||||
if [ $? -ne 0 ];
|
||||
then
|
||||
npx redocly lint $specPath
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Open API specification is not valid"
|
||||
exit 1
|
||||
fi
|
|
@ -1,13 +0,0 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/owncast/owncast/core/rtmp"
|
||||
)
|
||||
|
||||
// DisconnectInboundConnection will force-disconnect an inbound stream.
|
||||
func DisconnectInboundConnection(w http.ResponseWriter, r *http.Request) {
|
||||
rtmp.Disconnect()
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/owncast/owncast/controllers"
|
||||
"github.com/owncast/owncast/core/data"
|
||||
)
|
||||
|
||||
// SetCustomColorVariableValues sets the custom color variables.
|
||||
func SetCustomColorVariableValues(w http.ResponseWriter, r *http.Request) {
|
||||
if !requirePOST(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
type request struct {
|
||||
Value map[string]string `json:"value"`
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
var values request
|
||||
|
||||
if err := decoder.Decode(&values); err != nil {
|
||||
controllers.WriteSimpleResponse(w, false, "unable to update appearance variable values")
|
||||
return
|
||||
}
|
||||
|
||||
if err := data.SetCustomColorVariableValues(values.Value); err != nil {
|
||||
controllers.WriteSimpleResponse(w, false, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
controllers.WriteSimpleResponse(w, true, "custom appearance variables updated")
|
||||
}
|
|
@ -1,102 +0,0 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/owncast/owncast/config"
|
||||
"github.com/owncast/owncast/controllers"
|
||||
"github.com/owncast/owncast/core/user"
|
||||
"github.com/owncast/owncast/utils"
|
||||
)
|
||||
|
||||
type deleteExternalAPIUserRequest struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type createExternalAPIUserRequest struct {
|
||||
Name string `json:"name"`
|
||||
Scopes []string `json:"scopes"`
|
||||
}
|
||||
|
||||
// CreateExternalAPIUser will generate a 3rd party access token.
|
||||
func CreateExternalAPIUser(w http.ResponseWriter, r *http.Request) {
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
var request createExternalAPIUserRequest
|
||||
if err := decoder.Decode(&request); err != nil {
|
||||
controllers.BadRequestHandler(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify all the scopes provided are valid
|
||||
if !user.HasValidScopes(request.Scopes) {
|
||||
controllers.BadRequestHandler(w, errors.New("one or more invalid scopes provided"))
|
||||
return
|
||||
}
|
||||
|
||||
token, err := utils.GenerateAccessToken()
|
||||
if err != nil {
|
||||
controllers.InternalErrorHandler(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
color := utils.GenerateRandomDisplayColor(config.MaxUserColor)
|
||||
|
||||
if err := user.InsertExternalAPIUser(token, request.Name, color, request.Scopes); err != nil {
|
||||
controllers.InternalErrorHandler(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
controllers.WriteResponse(w, user.ExternalAPIUser{
|
||||
AccessToken: token,
|
||||
DisplayName: request.Name,
|
||||
DisplayColor: color,
|
||||
Scopes: request.Scopes,
|
||||
CreatedAt: time.Now(),
|
||||
LastUsedAt: nil,
|
||||
})
|
||||
}
|
||||
|
||||
// GetExternalAPIUsers will return all 3rd party access tokens.
|
||||
func GetExternalAPIUsers(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
tokens, err := user.GetExternalAPIUser()
|
||||
if err != nil {
|
||||
controllers.InternalErrorHandler(w, err)
|
||||
return
|
||||
}
|
||||
controllers.WriteResponse(w, tokens)
|
||||
}
|
||||
|
||||
// DeleteExternalAPIUser will return a single 3rd party access token.
|
||||
func DeleteExternalAPIUser(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if r.Method != controllers.POST {
|
||||
controllers.WriteSimpleResponse(w, false, r.Method+" not supported")
|
||||
return
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
var request deleteExternalAPIUserRequest
|
||||
if err := decoder.Decode(&request); err != nil {
|
||||
controllers.BadRequestHandler(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if request.Token == "" {
|
||||
controllers.BadRequestHandler(w, errors.New("must provide a token"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := user.DeleteExternalAPIUser(request.Token); err != nil {
|
||||
controllers.InternalErrorHandler(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
controllers.WriteSimpleResponse(w, true, "deleted token")
|
||||
}
|
|
@ -1,99 +0,0 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/owncast/owncast/config"
|
||||
"github.com/owncast/owncast/core/chat"
|
||||
"github.com/owncast/owncast/core/user"
|
||||
"github.com/owncast/owncast/router/middleware"
|
||||
"github.com/owncast/owncast/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// ExternalGetChatMessages gets all of the chat messages.
|
||||
func ExternalGetChatMessages(integration user.ExternalAPIUser, w http.ResponseWriter, r *http.Request) {
|
||||
middleware.EnableCors(w)
|
||||
getChatMessages(w, r)
|
||||
}
|
||||
|
||||
// GetChatMessages gets all of the chat messages.
|
||||
func GetChatMessages(u user.User, w http.ResponseWriter, r *http.Request) {
|
||||
middleware.EnableCors(w)
|
||||
getChatMessages(w, r)
|
||||
}
|
||||
|
||||
func getChatMessages(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
messages := chat.GetChatHistory()
|
||||
|
||||
if err := json.NewEncoder(w).Encode(messages); err != nil {
|
||||
log.Debugln(err)
|
||||
}
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
if err := json.NewEncoder(w).Encode(j{"error": "method not implemented (PRs are accepted)"}); err != nil {
|
||||
InternalErrorHandler(w, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterAnonymousChatUser will register a new user.
|
||||
func RegisterAnonymousChatUser(w http.ResponseWriter, r *http.Request) {
|
||||
middleware.EnableCors(w)
|
||||
|
||||
if r.Method == "OPTIONS" {
|
||||
// All OPTIONS requests should have a wildcard CORS header.
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method != http.MethodPost {
|
||||
// nolint:goconst
|
||||
WriteSimpleResponse(w, false, r.Method+" not supported")
|
||||
return
|
||||
}
|
||||
|
||||
type registerAnonymousUserRequest struct {
|
||||
DisplayName string `json:"displayName"`
|
||||
}
|
||||
|
||||
type registerAnonymousUserResponse struct {
|
||||
ID string `json:"id"`
|
||||
AccessToken string `json:"accessToken"`
|
||||
DisplayName string `json:"displayName"`
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
var request registerAnonymousUserRequest
|
||||
if err := decoder.Decode(&request); err != nil { //nolint
|
||||
// this is fine. register a new user anyway.
|
||||
}
|
||||
|
||||
if request.DisplayName == "" {
|
||||
request.DisplayName = r.Header.Get("X-Forwarded-User")
|
||||
}
|
||||
|
||||
proposedNewDisplayName := utils.MakeSafeStringOfLength(request.DisplayName, config.MaxChatDisplayNameLength)
|
||||
newUser, accessToken, err := user.CreateAnonymousUser(proposedNewDisplayName)
|
||||
if err != nil {
|
||||
WriteSimpleResponse(w, false, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
response := registerAnonymousUserResponse{
|
||||
ID: newUser.ID,
|
||||
AccessToken: accessToken,
|
||||
DisplayName: newUser.DisplayName,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
middleware.DisableCache(w)
|
||||
|
||||
WriteResponse(w, response)
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package controllers
|
||||
|
||||
// POST is the HTTP POST method.
|
||||
const POST = "POST"
|
||||
|
||||
// GET is the HTTP GET method.
|
||||
const GET = "GET"
|
|
@ -1,52 +0,0 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/owncast/owncast/metrics"
|
||||
"github.com/owncast/owncast/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// ReportPlaybackMetrics will accept playback metrics from a client and save
|
||||
// them for future video health reporting.
|
||||
func ReportPlaybackMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != POST {
|
||||
WriteSimpleResponse(w, false, r.Method+" not supported")
|
||||
return
|
||||
}
|
||||
|
||||
type reportPlaybackMetricsRequest struct {
|
||||
Bandwidth float64 `json:"bandwidth"`
|
||||
Latency float64 `json:"latency"`
|
||||
Errors float64 `json:"errors"`
|
||||
DownloadDuration float64 `json:"downloadDuration"`
|
||||
QualityVariantChanges float64 `json:"qualityVariantChanges"`
|
||||
}
|
||||
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
var request reportPlaybackMetricsRequest
|
||||
if err := decoder.Decode(&request); err != nil {
|
||||
log.Errorln("error decoding playback metrics payload:", err)
|
||||
WriteSimpleResponse(w, false, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
clientID := utils.GenerateClientIDFromRequest(r)
|
||||
|
||||
metrics.RegisterPlaybackErrorCount(clientID, request.Errors)
|
||||
if request.Bandwidth != 0.0 {
|
||||
metrics.RegisterPlayerBandwidth(clientID, request.Bandwidth)
|
||||
}
|
||||
|
||||
if request.Latency != 0.0 {
|
||||
metrics.RegisterPlayerLatency(clientID, request.Latency)
|
||||
}
|
||||
|
||||
if request.DownloadDuration != 0.0 {
|
||||
metrics.RegisterPlayerSegmentDownloadDuration(clientID, request.DownloadDuration)
|
||||
}
|
||||
|
||||
metrics.RegisterQualityVariantChangesCount(clientID, request.QualityVariantChanges)
|
||||
}
|
|
@ -14,8 +14,8 @@ import (
|
|||
"github.com/owncast/owncast/config"
|
||||
"github.com/owncast/owncast/core/chat/events"
|
||||
"github.com/owncast/owncast/core/data"
|
||||
"github.com/owncast/owncast/core/user"
|
||||
"github.com/owncast/owncast/geoip"
|
||||
"github.com/owncast/owncast/models"
|
||||
"github.com/owncast/owncast/services/geoip"
|
||||
)
|
||||
|
||||
// Client represents a single chat client.
|
||||
|
@ -25,7 +25,7 @@ type Client struct {
|
|||
rateLimiter *rate.Limiter
|
||||
messageFilter *ChatMessageFilter
|
||||
conn *websocket.Conn
|
||||
User *user.User `json:"user"`
|
||||
User *models.User `json:"user"`
|
||||
server *Server
|
||||
Geo *geoip.GeoDetails `json:"geo"`
|
||||
// Buffered channel of outbound messages.
|
||||
|
|
|
@ -10,14 +10,14 @@ import (
|
|||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func setSystemConcurrentConnectionLimit(limit int64) {
|
||||
func setSystemConcurrentConnectionLimit(limit uint64) {
|
||||
var rLimit syscall.Rlimit
|
||||
if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
originalLimit := rLimit.Cur
|
||||
rLimit.Cur = uint64(limit)
|
||||
rLimit.Cur = limit
|
||||
if err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func setSystemConcurrentConnectionLimit(limit int64) {
|
||||
func setSystemConcurrentConnectionLimit(limit uint64) {
|
||||
var rLimit syscall.Rlimit
|
||||
if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit); err != nil {
|
||||
log.Fatalln(err)
|
||||
|
|
|
@ -3,4 +3,4 @@
|
|||
|
||||
package chat
|
||||
|
||||
func setSystemConcurrentConnectionLimit(limit int64) {}
|
||||
func setSystemConcurrentConnectionLimit(limit uint64) {}
|
||||
|
|
|
@ -9,8 +9,8 @@ import (
|
|||
"github.com/owncast/owncast/config"
|
||||
"github.com/owncast/owncast/core/chat/events"
|
||||
"github.com/owncast/owncast/core/data"
|
||||
"github.com/owncast/owncast/core/user"
|
||||
"github.com/owncast/owncast/core/webhooks"
|
||||
"github.com/owncast/owncast/persistence/userrepository"
|
||||
"github.com/owncast/owncast/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
@ -46,12 +46,14 @@ func (s *Server) userNameChanged(eventData chatClientEvent) {
|
|||
}
|
||||
}
|
||||
|
||||
userRepository := userrepository.Get()
|
||||
|
||||
// Check if the name is not already assigned to a registered user.
|
||||
if available, err := user.IsDisplayNameAvailable(proposedUsername); err != nil {
|
||||
if available, err := userRepository.IsDisplayNameAvailable(proposedUsername); err != nil {
|
||||
log.Errorln("error checking if name is available", err)
|
||||
return
|
||||
} else if !available {
|
||||
message := fmt.Sprintf("The name **%s** has been already registered. If this is your name, please authenticate.", proposedUsername)
|
||||
message := fmt.Sprintf("The name **%s** has already been registered. If this is your name, please authenticate.", proposedUsername)
|
||||
s.sendActionToClient(eventData.client, message)
|
||||
|
||||
// Resend the client's user so their username is in sync.
|
||||
|
@ -60,7 +62,7 @@ func (s *Server) userNameChanged(eventData chatClientEvent) {
|
|||
return
|
||||
}
|
||||
|
||||
savedUser := user.GetUserByToken(eventData.client.accessToken)
|
||||
savedUser := userRepository.GetUserByToken(eventData.client.accessToken)
|
||||
oldName := savedUser.DisplayName
|
||||
|
||||
// Check that the new name is different from old.
|
||||
|
@ -70,7 +72,7 @@ func (s *Server) userNameChanged(eventData chatClientEvent) {
|
|||
}
|
||||
|
||||
// Save the new name
|
||||
if err := user.ChangeUsername(eventData.client.User.ID, proposedUsername); err != nil {
|
||||
if err := userRepository.ChangeUsername(eventData.client.User.ID, proposedUsername); err != nil {
|
||||
log.Errorln("error changing username", err)
|
||||
}
|
||||
|
||||
|
@ -103,6 +105,8 @@ func (s *Server) userNameChanged(eventData chatClientEvent) {
|
|||
}
|
||||
|
||||
func (s *Server) userColorChanged(eventData chatClientEvent) {
|
||||
userRepository := userrepository.Get()
|
||||
|
||||
var receivedEvent events.ColorChangeEvent
|
||||
if err := json.Unmarshal(eventData.data, &receivedEvent); err != nil {
|
||||
log.Errorln("error unmarshalling to ColorChangeEvent", err)
|
||||
|
@ -116,7 +120,7 @@ func (s *Server) userColorChanged(eventData chatClientEvent) {
|
|||
}
|
||||
|
||||
// Save the new color
|
||||
if err := user.ChangeUserColor(eventData.client.User.ID, receivedEvent.NewColor); err != nil {
|
||||
if err := userRepository.ChangeUserColor(eventData.client.User.ID, receivedEvent.NewColor); err != nil {
|
||||
log.Errorln("error changing user display color", err)
|
||||
}
|
||||
|
||||
|
@ -126,6 +130,8 @@ func (s *Server) userColorChanged(eventData chatClientEvent) {
|
|||
}
|
||||
|
||||
func (s *Server) userMessageSent(eventData chatClientEvent) {
|
||||
userRepository := userrepository.Get()
|
||||
|
||||
var event events.UserMessageEvent
|
||||
if err := json.Unmarshal(eventData.data, &event); err != nil {
|
||||
log.Errorln("error unmarshalling to UserMessageEvent", err)
|
||||
|
@ -148,7 +154,7 @@ func (s *Server) userMessageSent(eventData chatClientEvent) {
|
|||
}
|
||||
}
|
||||
|
||||
event.User = user.GetUserByToken(eventData.client.accessToken)
|
||||
event.User = userRepository.GetUserByToken(eventData.client.accessToken)
|
||||
|
||||
// Guard against nil users
|
||||
if event.User == nil {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
package events
|
||||
|
||||
import "github.com/owncast/owncast/core/user"
|
||||
import "github.com/owncast/owncast/models"
|
||||
|
||||
// ConnectedClientInfo represents the information about a connected client.
|
||||
type ConnectedClientInfo struct {
|
||||
User *user.User `json:"user"`
|
||||
User *models.User `json:"user"`
|
||||
Event
|
||||
}
|
||||
|
|
|
@ -17,10 +17,10 @@ import (
|
|||
"github.com/yuin/goldmark/extension"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/util"
|
||||
"mvdan.cc/xurls"
|
||||
"mvdan.cc/xurls/v2"
|
||||
|
||||
"github.com/owncast/owncast/core/data"
|
||||
"github.com/owncast/owncast/core/user"
|
||||
"github.com/owncast/owncast/models"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
|
@ -30,21 +30,21 @@ type EventPayload map[string]interface{}
|
|||
// OutboundEvent represents an event that is sent out to all listeners of the chat server.
|
||||
type OutboundEvent interface {
|
||||
GetBroadcastPayload() EventPayload
|
||||
GetMessageType() EventType
|
||||
GetMessageType() models.EventType
|
||||
}
|
||||
|
||||
// Event is any kind of event. A type is required to be specified.
|
||||
type Event struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Type EventType `json:"type,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Type models.EventType `json:"type,omitempty"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
// UserEvent is an event with an associated user.
|
||||
type UserEvent struct {
|
||||
User *user.User `json:"user"`
|
||||
HiddenAt *time.Time `json:"hiddenAt,omitempty"`
|
||||
ClientID uint `json:"clientId,omitempty"`
|
||||
User *models.User `json:"user"`
|
||||
HiddenAt *time.Time `json:"hiddenAt,omitempty"`
|
||||
ClientID uint `json:"clientId,omitempty"`
|
||||
}
|
||||
|
||||
// MessageEvent is an event that has a message body.
|
||||
|
@ -220,7 +220,7 @@ func RenderMarkdown(raw string) string {
|
|||
[]byte("https:"),
|
||||
}),
|
||||
extension.WithLinkifyURLRegexp(
|
||||
xurls.Strict,
|
||||
xurls.Strict(),
|
||||
),
|
||||
),
|
||||
emoji.New(
|
||||
|
|
|
@ -8,8 +8,9 @@ import (
|
|||
|
||||
"github.com/owncast/owncast/core/chat/events"
|
||||
"github.com/owncast/owncast/core/data"
|
||||
"github.com/owncast/owncast/core/user"
|
||||
"github.com/owncast/owncast/models"
|
||||
"github.com/owncast/owncast/persistence/tables"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
|
@ -22,7 +23,7 @@ const (
|
|||
|
||||
func setupPersistence() {
|
||||
_datastore = data.GetDatastore()
|
||||
data.CreateMessagesTable(_datastore.DB)
|
||||
tables.CreateMessagesTable(_datastore.DB)
|
||||
data.CreateBanIPTable(_datastore.DB)
|
||||
|
||||
chatDataPruner := time.NewTicker(5 * time.Minute)
|
||||
|
@ -104,7 +105,7 @@ func makeUserMessageEventFromRowData(row rowData) events.UserMessageEvent {
|
|||
isBot := (row.userType != nil && *row.userType == "API")
|
||||
scopeSlice := strings.Split(scopes, ",")
|
||||
|
||||
u := user.User{
|
||||
u := models.User{
|
||||
ID: *row.userID,
|
||||
DisplayName: displayName,
|
||||
DisplayColor: displayColor,
|
||||
|
|
|
@ -14,9 +14,10 @@ import (
|
|||
"github.com/owncast/owncast/config"
|
||||
"github.com/owncast/owncast/core/chat/events"
|
||||
"github.com/owncast/owncast/core/data"
|
||||
"github.com/owncast/owncast/core/user"
|
||||
"github.com/owncast/owncast/core/webhooks"
|
||||
"github.com/owncast/owncast/geoip"
|
||||
"github.com/owncast/owncast/models"
|
||||
"github.com/owncast/owncast/persistence/userrepository"
|
||||
"github.com/owncast/owncast/services/geoip"
|
||||
"github.com/owncast/owncast/utils"
|
||||
)
|
||||
|
||||
|
@ -40,7 +41,7 @@ type Server struct {
|
|||
// a map of user IDs and timers that fire for chat part messages.
|
||||
userPartedTimers map[string]*time.Ticker
|
||||
seq uint
|
||||
maxSocketConnectionLimit int64
|
||||
maxSocketConnectionLimit uint64
|
||||
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
@ -82,7 +83,7 @@ func (s *Server) Run() {
|
|||
}
|
||||
|
||||
// Addclient registers new connection as a User.
|
||||
func (s *Server) Addclient(conn *websocket.Conn, user *user.User, accessToken string, userAgent string, ipAddress string) *Client {
|
||||
func (s *Server) Addclient(conn *websocket.Conn, user *models.User, accessToken string, userAgent string, ipAddress string) *Client {
|
||||
client := &Client{
|
||||
server: s,
|
||||
conn: conn,
|
||||
|
@ -214,7 +215,7 @@ func (s *Server) HandleClientConnection(w http.ResponseWriter, r *http.Request)
|
|||
}
|
||||
|
||||
// Limit concurrent chat connections
|
||||
if int64(len(s.clients)) >= s.maxSocketConnectionLimit {
|
||||
if uint64(len(s.clients)) >= s.maxSocketConnectionLimit {
|
||||
log.Warnln("rejecting incoming client connection as it exceeds the max client count of", s.maxSocketConnectionLimit)
|
||||
_, _ = w.Write([]byte(events.ErrorMaxConnectionsExceeded))
|
||||
return
|
||||
|
@ -239,8 +240,11 @@ func (s *Server) HandleClientConnection(w http.ResponseWriter, r *http.Request)
|
|||
return
|
||||
}
|
||||
|
||||
userRepository := userrepository.Get()
|
||||
|
||||
// A user is required to use the websocket
|
||||
user := user.GetUserByToken(accessToken)
|
||||
user := userRepository.GetUserByToken(accessToken)
|
||||
|
||||
if user == nil {
|
||||
// Send error that registration is required
|
||||
_ = conn.WriteJSON(events.EventPayload{
|
||||
|
@ -335,8 +339,10 @@ func SendConnectedClientInfoToUser(userID string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
userRepository := userrepository.Get()
|
||||
|
||||
// Get an updated reference to the user.
|
||||
user := user.GetUserByID(userID)
|
||||
user := userRepository.GetUserByID(userID)
|
||||
if user == nil {
|
||||
return fmt.Errorf("user not found")
|
||||
}
|
||||
|
|
|
@ -9,14 +9,14 @@ import (
|
|||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func getMaximumConcurrentConnectionLimit() int64 {
|
||||
func getMaximumConcurrentConnectionLimit() uint64 {
|
||||
var rLimit syscall.Rlimit
|
||||
if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
|
||||
// Return the limit to 70% of max so the machine doesn't die even if it's maxed out for some reason.
|
||||
proposedLimit := int64(float32(rLimit.Max) * 0.7)
|
||||
proposedLimit := uint64(float32(rLimit.Max) * 0.7)
|
||||
|
||||
return proposedLimit
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
package chat
|
||||
|
||||
func getMaximumConcurrentConnectionLimit() int64 {
|
||||
func getMaximumConcurrentConnectionLimit() uint64 {
|
||||
// The maximum limit I can find for windows is 16,777,216
|
||||
// (essentially unlimited, but add the 0.7 multiplier as well to be
|
||||
// consistent with other systems)
|
||||
|
|
|
@ -13,10 +13,10 @@ import (
|
|||
"github.com/owncast/owncast/core/data"
|
||||
"github.com/owncast/owncast/core/rtmp"
|
||||
"github.com/owncast/owncast/core/transcoder"
|
||||
"github.com/owncast/owncast/core/user"
|
||||
"github.com/owncast/owncast/core/webhooks"
|
||||
"github.com/owncast/owncast/models"
|
||||
"github.com/owncast/owncast/notifications"
|
||||
"github.com/owncast/owncast/persistence/tables"
|
||||
"github.com/owncast/owncast/utils"
|
||||
"github.com/owncast/owncast/yp"
|
||||
)
|
||||
|
@ -56,7 +56,7 @@ func Start() error {
|
|||
log.Errorln("storage error", err)
|
||||
}
|
||||
|
||||
user.SetupUsers()
|
||||
tables.SetupUsers(data.GetDatastore().DB)
|
||||
auth.Setup(data.GetDatastore())
|
||||
|
||||
fileWriter.SetupFileWriterReceiverService(&handler)
|
||||
|
|
|
@ -115,7 +115,11 @@ func GetAdminPassword() string {
|
|||
|
||||
// SetAdminPassword will set the admin password.
|
||||
func SetAdminPassword(key string) error {
|
||||
return _datastore.SetString(adminPasswordKey, key)
|
||||
hashed_pass, err := utils.HashPassword(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return _datastore.SetString(adminPasswordKey, hashed_pass)
|
||||
}
|
||||
|
||||
// GetLogoPath will return the path for the logo, relative to webroot.
|
||||
|
|
|
@ -11,6 +11,8 @@ import (
|
|||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/owncast/owncast/persistence/tables"
|
||||
|
||||
"github.com/owncast/owncast/config"
|
||||
"github.com/owncast/owncast/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
@ -74,8 +76,8 @@ func SetupPersistence(file string) error {
|
|||
_, _ = db.Exec("pragma wal_checkpoint(full)")
|
||||
|
||||
createWebhooksTable()
|
||||
createUsersTable(db)
|
||||
createAccessTokenTable(db)
|
||||
tables.CreateUsersTable(db)
|
||||
tables.CreateAccessTokenTable(db)
|
||||
|
||||
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS config (
|
||||
"key" string NOT NULL PRIMARY KEY,
|
||||
|
@ -108,7 +110,7 @@ func SetupPersistence(file string) error {
|
|||
|
||||
// is database schema outdated?
|
||||
if version < schemaVersion {
|
||||
if err := migrateDatabaseSchema(db, version, schemaVersion); err != nil {
|
||||
if err := tables.MigrateDatabaseSchema(db, version, schemaVersion); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
datastoreValuesVersion = 3
|
||||
datastoreValuesVersion = 4
|
||||
datastoreValueVersionKey = "DATA_STORE_VERSION"
|
||||
)
|
||||
|
||||
|
@ -27,6 +27,8 @@ func migrateDatastoreValues(datastore *Datastore) {
|
|||
migrateToDatastoreValues2(datastore)
|
||||
case 2:
|
||||
migrateToDatastoreValues3ServingEndpoint3(datastore)
|
||||
case 3:
|
||||
migrateToDatastoreValues4(datastore)
|
||||
default:
|
||||
log.Fatalln("missing datastore values migration step")
|
||||
}
|
||||
|
@ -58,7 +60,8 @@ func migrateToDatastoreValues1(datastore *Datastore) {
|
|||
|
||||
func migrateToDatastoreValues2(datastore *Datastore) {
|
||||
oldAdminPassword, _ := datastore.GetString("stream_key")
|
||||
_ = SetAdminPassword(oldAdminPassword)
|
||||
// Avoids double hashing the password
|
||||
_ = datastore.SetString("admin_password_key", oldAdminPassword)
|
||||
_ = SetStreamKeys([]models.StreamKey{
|
||||
{Key: oldAdminPassword, Comment: "Default stream key"},
|
||||
})
|
||||
|
@ -73,3 +76,11 @@ func migrateToDatastoreValues3ServingEndpoint3(_ *Datastore) {
|
|||
|
||||
_ = SetVideoServingEndpoint(s3Config.ServingEndpoint)
|
||||
}
|
||||
|
||||
func migrateToDatastoreValues4(datastore *Datastore) {
|
||||
unhashed_pass, _ := datastore.GetString("admin_password_key")
|
||||
err := SetAdminPassword(unhashed_pass)
|
||||
if err != nil {
|
||||
log.Fatalln("error migrating admin password:", err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,32 +9,6 @@ import (
|
|||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// CreateMessagesTable will create the chat messages table if needed.
|
||||
func CreateMessagesTable(db *sql.DB) {
|
||||
createTableSQL := `CREATE TABLE IF NOT EXISTS messages (
|
||||
"id" string NOT NULL,
|
||||
"user_id" TEXT,
|
||||
"body" TEXT,
|
||||
"eventType" TEXT,
|
||||
"hidden_at" DATETIME,
|
||||
"timestamp" DATETIME,
|
||||
"title" TEXT,
|
||||
"subtitle" TEXT,
|
||||
"image" TEXT,
|
||||
"link" TEXT,
|
||||
PRIMARY KEY (id)
|
||||
);`
|
||||
MustExec(createTableSQL, db)
|
||||
|
||||
// Create indexes
|
||||
MustExec(`CREATE INDEX IF NOT EXISTS user_id_hidden_at_timestamp ON messages (id, user_id, hidden_at, timestamp);`, db)
|
||||
MustExec(`CREATE INDEX IF NOT EXISTS idx_id ON messages (id);`, db)
|
||||
MustExec(`CREATE INDEX IF NOT EXISTS idx_user_id ON messages (user_id);`, db)
|
||||
MustExec(`CREATE INDEX IF NOT EXISTS idx_hidden_at ON messages (hidden_at);`, db)
|
||||
MustExec(`CREATE INDEX IF NOT EXISTS idx_timestamp ON messages (timestamp);`, db)
|
||||
MustExec(`CREATE INDEX IF NOT EXISTS idx_messages_hidden_at_timestamp on messages(hidden_at, timestamp);`, db)
|
||||
}
|
||||
|
||||
// GetMessagesCount will return the number of messages in the database.
|
||||
func GetMessagesCount() int64 {
|
||||
query := `SELECT COUNT(*) FROM messages`
|
||||
|
|
|
@ -8,8 +8,8 @@ import (
|
|||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/owncast/owncast/core/data"
|
||||
"github.com/owncast/owncast/geoip"
|
||||
"github.com/owncast/owncast/models"
|
||||
"github.com/owncast/owncast/services/geoip"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
|
@ -229,7 +229,9 @@ func (t *Transcoder) getString() string {
|
|||
"-hls_segment_filename", localListenerAddress + "/%v/stream-" + t.segmentIdentifier + "-%d.ts", // Send HLS segments back to us over HTTP
|
||||
"-max_muxing_queue_size", "400", // Workaround for Too many packets error: https://trac.ffmpeg.org/ticket/6375?cversion=0
|
||||
|
||||
"-method PUT", // HLS results sent back to us will be over PUTs
|
||||
"-method PUT", // HLS results sent back to us will be over PUTs
|
||||
"-http_persistent", "1", // Ensures persistent HTTP connections
|
||||
|
||||
localListenerAddress + "/%v/stream.m3u8", // Send HLS playlists back to us over HTTP
|
||||
}
|
||||
|
||||
|
|
|
@ -42,7 +42,7 @@ func TestFFmpegNvencCommand(t *testing.T) {
|
|||
cmd := transcoder.getString()
|
||||
|
||||
expectedLogPath := filepath.Join("data", "logs", "transcoder.log")
|
||||
expected := `FFREPORT=file="` + expectedLogPath + `":level=32 ` + transcoder.ffmpegPath + ` -hide_banner -loglevel warning -hwaccel cuda -fflags +genpts -flags +cgop -i fakecontent.flv -map v:0 -c:v:0 h264_nvenc -b:v:0 1008k -maxrate:v:0 1088k -g:v:0 90 -keyint_min:v:0 90 -r:v:0 30 -tune:v:0 ll -map a:0? -c:a:0 copy -preset p3 -map v:0 -c:v:1 h264_nvenc -b:v:1 3308k -maxrate:v:1 3572k -g:v:1 72 -keyint_min:v:1 72 -r:v:1 24 -tune:v:1 ll -map a:0? -c:a:1 copy -preset p5 -map v:0 -c:v:2 copy -map a:0? -c:a:2 copy -preset p1 -var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2 " -f hls -hls_time 3 -hls_list_size 10 -hls_flags program_date_time+independent_segments+omit_endlist -segment_format_options mpegts_flags=mpegts_copyts=1 -pix_fmt yuv420p -sc_threshold 0 -master_pl_name stream.m3u8 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdoieGg-%d.ts -max_muxing_queue_size 400 -method PUT http://127.0.0.1:8123/%v/stream.m3u8`
|
||||
expected := `FFREPORT=file="` + expectedLogPath + `":level=32 ` + transcoder.ffmpegPath + ` -hide_banner -loglevel warning -hwaccel cuda -fflags +genpts -flags +cgop -i fakecontent.flv -map v:0 -c:v:0 h264_nvenc -b:v:0 1008k -maxrate:v:0 1088k -g:v:0 90 -keyint_min:v:0 90 -r:v:0 30 -tune:v:0 ll -map a:0? -c:a:0 copy -preset p3 -map v:0 -c:v:1 h264_nvenc -b:v:1 3308k -maxrate:v:1 3572k -g:v:1 72 -keyint_min:v:1 72 -r:v:1 24 -tune:v:1 ll -map a:0? -c:a:1 copy -preset p5 -map v:0 -c:v:2 copy -map a:0? -c:a:2 copy -preset p1 -var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2 " -f hls -hls_time 3 -hls_list_size 10 -hls_flags program_date_time+independent_segments+omit_endlist -segment_format_options mpegts_flags=mpegts_copyts=1 -pix_fmt yuv420p -sc_threshold 0 -master_pl_name stream.m3u8 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdoieGg-%d.ts -max_muxing_queue_size 400 -method PUT -http_persistent 1 http://127.0.0.1:8123/%v/stream.m3u8`
|
||||
|
||||
if cmd != expected {
|
||||
t.Errorf("ffmpeg command does not match expected.\nGot %s\n, want: %s", cmd, expected)
|
||||
|
|
|
@ -42,7 +42,7 @@ func TestFFmpegOmxCommand(t *testing.T) {
|
|||
cmd := transcoder.getString()
|
||||
|
||||
expectedLogPath := filepath.Join("data", "logs", "transcoder.log")
|
||||
expected := `FFREPORT=file="` + expectedLogPath + `":level=32 ` + transcoder.ffmpegPath + ` -hide_banner -loglevel warning -fflags +genpts -flags +cgop -i fakecontent.flv -map v:0 -c:v:0 h264_omx -b:v:0 1008k -maxrate:v:0 1088k -g:v:0 90 -keyint_min:v:0 90 -r:v:0 30 -map a:0? -c:a:0 copy -preset veryfast -map v:0 -c:v:1 h264_omx -b:v:1 3308k -maxrate:v:1 3572k -g:v:1 72 -keyint_min:v:1 72 -r:v:1 24 -map a:0? -c:a:1 copy -preset fast -map v:0 -c:v:2 copy -map a:0? -c:a:2 copy -preset ultrafast -var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2 " -f hls -hls_time 3 -hls_list_size 10 -hls_flags program_date_time+independent_segments+omit_endlist -segment_format_options mpegts_flags=mpegts_copyts=1 -tune zerolatency -pix_fmt yuv420p -sc_threshold 0 -master_pl_name stream.m3u8 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdFsdfzGg-%d.ts -max_muxing_queue_size 400 -method PUT http://127.0.0.1:8123/%v/stream.m3u8`
|
||||
expected := `FFREPORT=file="` + expectedLogPath + `":level=32 ` + transcoder.ffmpegPath + ` -hide_banner -loglevel warning -fflags +genpts -flags +cgop -i fakecontent.flv -map v:0 -c:v:0 h264_omx -b:v:0 1008k -maxrate:v:0 1088k -g:v:0 90 -keyint_min:v:0 90 -r:v:0 30 -map a:0? -c:a:0 copy -preset veryfast -map v:0 -c:v:1 h264_omx -b:v:1 3308k -maxrate:v:1 3572k -g:v:1 72 -keyint_min:v:1 72 -r:v:1 24 -map a:0? -c:a:1 copy -preset fast -map v:0 -c:v:2 copy -map a:0? -c:a:2 copy -preset ultrafast -var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2 " -f hls -hls_time 3 -hls_list_size 10 -hls_flags program_date_time+independent_segments+omit_endlist -segment_format_options mpegts_flags=mpegts_copyts=1 -tune zerolatency -pix_fmt yuv420p -sc_threshold 0 -master_pl_name stream.m3u8 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdFsdfzGg-%d.ts -max_muxing_queue_size 400 -method PUT -http_persistent 1 http://127.0.0.1:8123/%v/stream.m3u8`
|
||||
|
||||
if cmd != expected {
|
||||
t.Errorf("ffmpeg command does not match expected.\nGot %s\n, want: %s", cmd, expected)
|
||||
|
|
|
@ -42,7 +42,7 @@ func TestFFmpegVaapiCommand(t *testing.T) {
|
|||
cmd := transcoder.getString()
|
||||
|
||||
expectedLogPath := filepath.Join("data", "logs", "transcoder.log")
|
||||
expected := `FFREPORT=file="` + expectedLogPath + `":level=32 ` + transcoder.ffmpegPath + ` -hide_banner -loglevel warning -hwaccel vaapi -hwaccel_output_format vaapi -vaapi_device /dev/dri/renderD128 -fflags +genpts -flags +cgop -i fakecontent.flv -map v:0 -c:v:0 h264_vaapi -b:v:0 1008k -maxrate:v:0 1088k -g:v:0 90 -keyint_min:v:0 90 -r:v:0 30 -map a:0? -c:a:0 copy -preset veryfast -map v:0 -c:v:1 h264_vaapi -b:v:1 3308k -maxrate:v:1 3572k -g:v:1 72 -keyint_min:v:1 72 -r:v:1 24 -map a:0? -c:a:1 copy -preset fast -map v:0 -c:v:2 copy -map a:0? -c:a:2 copy -preset ultrafast -var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2 " -f hls -hls_time 3 -hls_list_size 10 -hls_flags program_date_time+independent_segments+omit_endlist -segment_format_options mpegts_flags=mpegts_copyts=1 -pix_fmt vaapi_vld -sc_threshold 0 -master_pl_name stream.m3u8 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdofFGg-%d.ts -max_muxing_queue_size 400 -method PUT http://127.0.0.1:8123/%v/stream.m3u8`
|
||||
expected := `FFREPORT=file="` + expectedLogPath + `":level=32 ` + transcoder.ffmpegPath + ` -hide_banner -loglevel warning -hwaccel vaapi -hwaccel_output_format vaapi -vaapi_device /dev/dri/renderD128 -fflags +genpts -flags +cgop -i fakecontent.flv -map v:0 -c:v:0 h264_vaapi -b:v:0 1008k -maxrate:v:0 1088k -g:v:0 90 -keyint_min:v:0 90 -r:v:0 30 -map a:0? -c:a:0 copy -preset veryfast -map v:0 -c:v:1 h264_vaapi -b:v:1 3308k -maxrate:v:1 3572k -g:v:1 72 -keyint_min:v:1 72 -r:v:1 24 -map a:0? -c:a:1 copy -preset fast -map v:0 -c:v:2 copy -map a:0? -c:a:2 copy -preset ultrafast -var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2 " -f hls -hls_time 3 -hls_list_size 10 -hls_flags program_date_time+independent_segments+omit_endlist -segment_format_options mpegts_flags=mpegts_copyts=1 -pix_fmt vaapi_vld -sc_threshold 0 -master_pl_name stream.m3u8 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdofFGg-%d.ts -max_muxing_queue_size 400 -method PUT -http_persistent 1 http://127.0.0.1:8123/%v/stream.m3u8`
|
||||
|
||||
if cmd != expected {
|
||||
t.Errorf("ffmpeg command does not match expected.\nGot %s\n, want: %s", cmd, expected)
|
||||
|
|
|
@ -42,7 +42,7 @@ func TestFFmpegVideoToolboxCommand(t *testing.T) {
|
|||
cmd := transcoder.getString()
|
||||
|
||||
expectedLogPath := filepath.Join("data", "logs", "transcoder.log")
|
||||
expected := `FFREPORT=file="` + expectedLogPath + `":level=32 ` + transcoder.ffmpegPath + ` -hide_banner -loglevel warning -fflags +genpts -flags +cgop -i fakecontent.flv -map v:0 -c:v:0 h264_videotoolbox -b:v:0 1008k -maxrate:v:0 1088k -g:v:0 90 -keyint_min:v:0 90 -r:v:0 30 -realtime true -map a:0? -c:a:0 copy -preset veryfast -map v:0 -c:v:1 h264_videotoolbox -b:v:1 3308k -maxrate:v:1 3572k -g:v:1 72 -keyint_min:v:1 72 -r:v:1 24 -map a:0? -c:a:1 copy -preset fast -map v:0 -c:v:2 copy -map a:0? -c:a:2 copy -preset ultrafast -var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2 " -f hls -hls_time 3 -hls_list_size 10 -hls_flags program_date_time+independent_segments+omit_endlist -segment_format_options mpegts_flags=mpegts_copyts=1 -pix_fmt nv12 -sc_threshold 0 -master_pl_name stream.m3u8 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdFsdfzGg-%d.ts -max_muxing_queue_size 400 -method PUT http://127.0.0.1:8123/%v/stream.m3u8`
|
||||
expected := `FFREPORT=file="` + expectedLogPath + `":level=32 ` + transcoder.ffmpegPath + ` -hide_banner -loglevel warning -fflags +genpts -flags +cgop -i fakecontent.flv -map v:0 -c:v:0 h264_videotoolbox -b:v:0 1008k -maxrate:v:0 1088k -g:v:0 90 -keyint_min:v:0 90 -r:v:0 30 -realtime true -map a:0? -c:a:0 copy -preset veryfast -map v:0 -c:v:1 h264_videotoolbox -b:v:1 3308k -maxrate:v:1 3572k -g:v:1 72 -keyint_min:v:1 72 -r:v:1 24 -map a:0? -c:a:1 copy -preset fast -map v:0 -c:v:2 copy -map a:0? -c:a:2 copy -preset ultrafast -var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2 " -f hls -hls_time 3 -hls_list_size 10 -hls_flags program_date_time+independent_segments+omit_endlist -segment_format_options mpegts_flags=mpegts_copyts=1 -pix_fmt nv12 -sc_threshold 0 -master_pl_name stream.m3u8 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdFsdfzGg-%d.ts -max_muxing_queue_size 400 -method PUT -http_persistent 1 http://127.0.0.1:8123/%v/stream.m3u8`
|
||||
|
||||
if cmd != expected {
|
||||
t.Errorf("ffmpeg command does not match expected.\nGot %s\n, want: %s", cmd, expected)
|
||||
|
|
|
@ -42,7 +42,7 @@ func TestFFmpegx264Command(t *testing.T) {
|
|||
cmd := transcoder.getString()
|
||||
|
||||
expectedLogPath := filepath.Join("data", "logs", "transcoder.log")
|
||||
expected := `FFREPORT=file="` + expectedLogPath + `":level=32 ` + transcoder.ffmpegPath + ` -hide_banner -loglevel warning -fflags +genpts -flags +cgop -i fakecontent.flv -map v:0 -c:v:0 libx264 -b:v:0 1008k -maxrate:v:0 1088k -g:v:0 90 -keyint_min:v:0 90 -r:v:0 30 -x264-params:v:0 "scenecut=0:open_gop=0" -bufsize:v:0 1088k -profile:v:0 high -map a:0? -c:a:0 copy -preset veryfast -map v:0 -c:v:1 libx264 -b:v:1 3308k -maxrate:v:1 3572k -g:v:1 72 -keyint_min:v:1 72 -r:v:1 24 -x264-params:v:1 "scenecut=0:open_gop=0" -bufsize:v:1 3572k -profile:v:1 high -map a:0? -c:a:1 copy -preset fast -map v:0 -c:v:2 copy -map a:0? -c:a:2 copy -preset ultrafast -var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2 " -f hls -hls_time 3 -hls_list_size 10 -hls_flags program_date_time+independent_segments+omit_endlist -segment_format_options mpegts_flags=mpegts_copyts=1 -tune zerolatency -pix_fmt yuv420p -sc_threshold 0 -master_pl_name stream.m3u8 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdofFGg-%d.ts -max_muxing_queue_size 400 -method PUT http://127.0.0.1:8123/%v/stream.m3u8`
|
||||
expected := `FFREPORT=file="` + expectedLogPath + `":level=32 ` + transcoder.ffmpegPath + ` -hide_banner -loglevel warning -fflags +genpts -flags +cgop -i fakecontent.flv -map v:0 -c:v:0 libx264 -b:v:0 1008k -maxrate:v:0 1088k -g:v:0 90 -keyint_min:v:0 90 -r:v:0 30 -x264-params:v:0 "scenecut=0:open_gop=0" -bufsize:v:0 1088k -profile:v:0 high -map a:0? -c:a:0 copy -preset veryfast -map v:0 -c:v:1 libx264 -b:v:1 3308k -maxrate:v:1 3572k -g:v:1 72 -keyint_min:v:1 72 -r:v:1 24 -x264-params:v:1 "scenecut=0:open_gop=0" -bufsize:v:1 3572k -profile:v:1 high -map a:0? -c:a:1 copy -preset fast -map v:0 -c:v:2 copy -map a:0? -c:a:2 copy -preset ultrafast -var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2 " -f hls -hls_time 3 -hls_list_size 10 -hls_flags program_date_time+independent_segments+omit_endlist -segment_format_options mpegts_flags=mpegts_copyts=1 -tune zerolatency -pix_fmt yuv420p -sc_threshold 0 -master_pl_name stream.m3u8 -hls_segment_filename http://127.0.0.1:8123/%v/stream-jdofFGg-%d.ts -max_muxing_queue_size 400 -method PUT -http_persistent 1 http://127.0.0.1:8123/%v/stream.m3u8`
|
||||
|
||||
if cmd != expected {
|
||||
t.Errorf("ffmpeg command does not match expected.\nGot %s\n, want: %s", cmd, expected)
|
||||
|
|
|
@ -1,311 +0,0 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/owncast/owncast/utils"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/teris-io/shortid"
|
||||
)
|
||||
|
||||
// ExternalAPIUser represents a single 3rd party integration that uses an access token.
|
||||
// This struct mostly matches the User struct so they can be used interchangeably.
|
||||
type ExternalAPIUser struct {
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
LastUsedAt *time.Time `json:"lastUsedAt,omitempty"`
|
||||
ID string `json:"id"`
|
||||
AccessToken string `json:"accessToken"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Type string `json:"type,omitempty"` // Should be API
|
||||
Scopes []string `json:"scopes"`
|
||||
DisplayColor int `json:"displayColor"`
|
||||
IsBot bool `json:"isBot"`
|
||||
}
|
||||
|
||||
const (
|
||||
// ScopeCanSendChatMessages will allow sending chat messages as itself.
|
||||
ScopeCanSendChatMessages = "CAN_SEND_MESSAGES"
|
||||
// ScopeCanSendSystemMessages will allow sending chat messages as the system.
|
||||
ScopeCanSendSystemMessages = "CAN_SEND_SYSTEM_MESSAGES"
|
||||
// ScopeHasAdminAccess will allow performing administrative actions on the server.
|
||||
ScopeHasAdminAccess = "HAS_ADMIN_ACCESS"
|
||||
)
|
||||
|
||||
// For a scope to be seen as "valid" it must live in this slice.
|
||||
var validAccessTokenScopes = []string{
|
||||
ScopeCanSendChatMessages,
|
||||
ScopeCanSendSystemMessages,
|
||||
ScopeHasAdminAccess,
|
||||
}
|
||||
|
||||
// InsertExternalAPIUser will add a new API user to the database.
|
||||
func InsertExternalAPIUser(token string, name string, color int, scopes []string) error {
|
||||
log.Traceln("Adding new API user")
|
||||
|
||||
_datastore.DbLock.Lock()
|
||||
defer _datastore.DbLock.Unlock()
|
||||
|
||||
scopesString := strings.Join(scopes, ",")
|
||||
id := shortid.MustGenerate()
|
||||
|
||||
tx, err := _datastore.DB.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stmt, err := tx.Prepare("INSERT INTO users(id, display_name, display_color, scopes, type, previous_names) values(?, ?, ?, ?, ?, ?)")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
if _, err = stmt.Exec(id, name, color, scopesString, "API", name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := addAccessTokenForUser(token, id); err != nil {
|
||||
return errors.Wrap(err, "unable to save access token for new external api user")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteExternalAPIUser will delete a token from the database.
|
||||
func DeleteExternalAPIUser(token string) error {
|
||||
log.Traceln("Deleting access token")
|
||||
|
||||
_datastore.DbLock.Lock()
|
||||
defer _datastore.DbLock.Unlock()
|
||||
|
||||
tx, err := _datastore.DB.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stmt, err := tx.Prepare("UPDATE users SET disabled_at = CURRENT_TIMESTAMP WHERE id = (SELECT user_id FROM user_access_tokens WHERE token = ?)")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
result, err := stmt.Exec(token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rowsDeleted, _ := result.RowsAffected(); rowsDeleted == 0 {
|
||||
tx.Rollback() //nolint
|
||||
return errors.New(token + " not found")
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetExternalAPIUserForAccessTokenAndScope will determine if a specific token has access to perform a scoped action.
|
||||
func GetExternalAPIUserForAccessTokenAndScope(token string, scope string) (*ExternalAPIUser, error) {
|
||||
// This will split the scopes from comma separated to individual rows
|
||||
// so we can efficiently find if a token supports a single scope.
|
||||
// This is SQLite specific, so if we ever support other database
|
||||
// backends we need to support other methods.
|
||||
query := `SELECT
|
||||
id,
|
||||
scopes,
|
||||
display_name,
|
||||
display_color,
|
||||
created_at,
|
||||
last_used
|
||||
FROM
|
||||
user_access_tokens
|
||||
INNER JOIN (
|
||||
WITH RECURSIVE split(
|
||||
id,
|
||||
scopes,
|
||||
display_name,
|
||||
display_color,
|
||||
created_at,
|
||||
last_used,
|
||||
disabled_at,
|
||||
scope,
|
||||
rest
|
||||
) AS (
|
||||
SELECT
|
||||
id,
|
||||
scopes,
|
||||
display_name,
|
||||
display_color,
|
||||
created_at,
|
||||
last_used,
|
||||
disabled_at,
|
||||
'',
|
||||
scopes || ','
|
||||
FROM
|
||||
users AS u
|
||||
UNION ALL
|
||||
SELECT
|
||||
id,
|
||||
scopes,
|
||||
display_name,
|
||||
display_color,
|
||||
created_at,
|
||||
last_used,
|
||||
disabled_at,
|
||||
substr(rest, 0, instr(rest, ',')),
|
||||
substr(rest, instr(rest, ',') + 1)
|
||||
FROM
|
||||
split
|
||||
WHERE
|
||||
rest <> ''
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
display_name,
|
||||
display_color,
|
||||
created_at,
|
||||
last_used,
|
||||
disabled_at,
|
||||
scopes,
|
||||
scope
|
||||
FROM
|
||||
split
|
||||
WHERE
|
||||
scope <> ''
|
||||
) ON user_access_tokens.user_id = id
|
||||
WHERE
|
||||
disabled_at IS NULL
|
||||
AND token = ?
|
||||
AND scope = ?;`
|
||||
|
||||
row := _datastore.DB.QueryRow(query, token, scope)
|
||||
integration, err := makeExternalAPIUserFromRow(row)
|
||||
|
||||
return integration, err
|
||||
}
|
||||
|
||||
// GetIntegrationNameForAccessToken will return the integration name associated with a specific access token.
|
||||
func GetIntegrationNameForAccessToken(token string) *string {
|
||||
name, err := _datastore.GetQueries().GetUserDisplayNameByToken(context.Background(), token)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &name
|
||||
}
|
||||
|
||||
// GetExternalAPIUser will return all API users with access tokens.
|
||||
func GetExternalAPIUser() ([]ExternalAPIUser, error) { //nolint
|
||||
query := "SELECT id, token, display_name, display_color, scopes, created_at, last_used FROM users, user_access_tokens WHERE user_access_tokens.user_id = id AND type IS 'API' AND disabled_at IS NULL"
|
||||
|
||||
rows, err := _datastore.DB.Query(query)
|
||||
if err != nil {
|
||||
return []ExternalAPIUser{}, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
integrations, err := makeExternalAPIUsersFromRows(rows)
|
||||
|
||||
return integrations, err
|
||||
}
|
||||
|
||||
// SetExternalAPIUserAccessTokenAsUsed will update the last used timestamp for a token.
|
||||
func SetExternalAPIUserAccessTokenAsUsed(token string) error {
|
||||
tx, err := _datastore.DB.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stmt, err := tx.Prepare("UPDATE users SET last_used = CURRENT_TIMESTAMP WHERE id = (SELECT user_id FROM user_access_tokens WHERE token = ?)")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
if _, err := stmt.Exec(token); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func makeExternalAPIUserFromRow(row *sql.Row) (*ExternalAPIUser, error) {
|
||||
var id string
|
||||
var displayName string
|
||||
var displayColor int
|
||||
var scopes string
|
||||
var createdAt time.Time
|
||||
var lastUsedAt *time.Time
|
||||
|
||||
err := row.Scan(&id, &scopes, &displayName, &displayColor, &createdAt, &lastUsedAt)
|
||||
if err != nil {
|
||||
log.Debugln("unable to convert row to api user", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
integration := ExternalAPIUser{
|
||||
ID: id,
|
||||
DisplayName: displayName,
|
||||
DisplayColor: displayColor,
|
||||
CreatedAt: createdAt,
|
||||
Scopes: strings.Split(scopes, ","),
|
||||
LastUsedAt: lastUsedAt,
|
||||
}
|
||||
|
||||
return &integration, nil
|
||||
}
|
||||
|
||||
func makeExternalAPIUsersFromRows(rows *sql.Rows) ([]ExternalAPIUser, error) {
|
||||
integrations := make([]ExternalAPIUser, 0)
|
||||
|
||||
for rows.Next() {
|
||||
var id string
|
||||
var accessToken string
|
||||
var displayName string
|
||||
var displayColor int
|
||||
var scopes string
|
||||
var createdAt time.Time
|
||||
var lastUsedAt *time.Time
|
||||
|
||||
err := rows.Scan(&id, &accessToken, &displayName, &displayColor, &scopes, &createdAt, &lastUsedAt)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
integration := ExternalAPIUser{
|
||||
ID: id,
|
||||
AccessToken: accessToken,
|
||||
DisplayName: displayName,
|
||||
DisplayColor: displayColor,
|
||||
CreatedAt: createdAt,
|
||||
Scopes: strings.Split(scopes, ","),
|
||||
LastUsedAt: lastUsedAt,
|
||||
IsBot: true,
|
||||
}
|
||||
integrations = append(integrations, integration)
|
||||
}
|
||||
|
||||
return integrations, nil
|
||||
}
|
||||
|
||||
// HasValidScopes will verify that all the scopes provided are valid.
|
||||
func HasValidScopes(scopes []string) bool {
|
||||
for _, scope := range scopes {
|
||||
_, foundInSlice := utils.FindInSlice(validAccessTokenScopes, scope)
|
||||
if !foundInSlice {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
|
@ -1,93 +0,0 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/owncast/owncast/core/data"
|
||||
)
|
||||
|
||||
const (
|
||||
tokenName = "test token name"
|
||||
token = "test-token-123"
|
||||
)
|
||||
|
||||
var testScopes = []string{"test-scope"}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if err := data.SetupPersistence(":memory:"); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
SetupUsers()
|
||||
|
||||
m.Run()
|
||||
}
|
||||
|
||||
func TestCreateExternalAPIUser(t *testing.T) {
|
||||
if err := InsertExternalAPIUser(token, tokenName, 0, testScopes); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
user := GetUserByToken(token)
|
||||
if user == nil {
|
||||
t.Fatal("api user not found after creating")
|
||||
}
|
||||
|
||||
if user.DisplayName != tokenName {
|
||||
t.Errorf("expected display name %q, got %q", tokenName, user.DisplayName)
|
||||
}
|
||||
|
||||
if user.Scopes[0] != testScopes[0] {
|
||||
t.Errorf("expected scopes %q, got %q", testScopes, user.Scopes)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteExternalAPIUser(t *testing.T) {
|
||||
if err := DeleteExternalAPIUser(token); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyTokenDisabled(t *testing.T) {
|
||||
users, err := GetExternalAPIUser()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(users) > 0 {
|
||||
t.Fatal("disabled user returned in list of all API users")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyGetUserTokenDisabled(t *testing.T) {
|
||||
user := GetUserByToken(token)
|
||||
if user == nil {
|
||||
t.Fatal("user not returned in GetUserByToken after disabling")
|
||||
}
|
||||
|
||||
if user.DisabledAt == nil {
|
||||
t.Fatal("user returned in GetUserByToken after disabling")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyGetExternalAPIUserForAccessTokenAndScopeTokenDisabled(t *testing.T) {
|
||||
user, _ := GetExternalAPIUserForAccessTokenAndScope(token, testScopes[0])
|
||||
|
||||
if user != nil {
|
||||
t.Fatal("user returned in GetExternalAPIUserForAccessTokenAndScope after disabling")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateAdditionalAPIUser(t *testing.T) {
|
||||
if err := InsertExternalAPIUser("ignore-me", "token-to-be-ignored", 0, testScopes); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgainVerifyGetExternalAPIUserForAccessTokenAndScopeTokenDisabled(t *testing.T) {
|
||||
user, _ := GetExternalAPIUserForAccessTokenAndScope(token, testScopes[0])
|
||||
|
||||
if user != nil {
|
||||
t.Fatal("user returned in TestAgainVerifyGetExternalAPIUserForAccessTokenAndScopeTokenDisabled after disabling")
|
||||
}
|
||||
}
|
|
@ -1,473 +0,0 @@
|
|||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/owncast/owncast/config"
|
||||
"github.com/owncast/owncast/core/data"
|
||||
"github.com/owncast/owncast/db"
|
||||
"github.com/owncast/owncast/utils"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/teris-io/shortid"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var _datastore *data.Datastore
|
||||
|
||||
const (
|
||||
moderatorScopeKey = "MODERATOR"
|
||||
minSuggestedUsernamePoolLength = 10
|
||||
)
|
||||
|
||||
// User represents a single chat user.
|
||||
type User struct {
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
DisabledAt *time.Time `json:"disabledAt,omitempty"`
|
||||
NameChangedAt *time.Time `json:"nameChangedAt,omitempty"`
|
||||
AuthenticatedAt *time.Time `json:"-"`
|
||||
ID string `json:"id"`
|
||||
DisplayName string `json:"displayName"`
|
||||
PreviousNames []string `json:"previousNames"`
|
||||
Scopes []string `json:"scopes,omitempty"`
|
||||
DisplayColor int `json:"displayColor"`
|
||||
IsBot bool `json:"isBot"`
|
||||
Authenticated bool `json:"authenticated"`
|
||||
}
|
||||
|
||||
// IsEnabled will return if this single user is enabled.
|
||||
func (u *User) IsEnabled() bool {
|
||||
return u.DisabledAt == nil
|
||||
}
|
||||
|
||||
// IsModerator will return if the user has moderation privileges.
|
||||
func (u *User) IsModerator() bool {
|
||||
_, hasModerationScope := utils.FindInSlice(u.Scopes, moderatorScopeKey)
|
||||
return hasModerationScope
|
||||
}
|
||||
|
||||
// SetupUsers will perform the initial initialization of the user package.
|
||||
func SetupUsers() {
|
||||
_datastore = data.GetDatastore()
|
||||
}
|
||||
|
||||
func generateDisplayName() string {
|
||||
suggestedUsernamesList := data.GetSuggestedUsernamesList()
|
||||
|
||||
if len(suggestedUsernamesList) >= minSuggestedUsernamePoolLength {
|
||||
index := utils.RandomIndex(len(suggestedUsernamesList))
|
||||
return suggestedUsernamesList[index]
|
||||
} else {
|
||||
return utils.GeneratePhrase()
|
||||
}
|
||||
}
|
||||
|
||||
// CreateAnonymousUser will create a new anonymous user with the provided display name.
|
||||
func CreateAnonymousUser(displayName string) (*User, string, error) {
|
||||
// Try to assign a name that was requested.
|
||||
if displayName != "" {
|
||||
// If name isn't available then generate a random one.
|
||||
if available, _ := IsDisplayNameAvailable(displayName); !available {
|
||||
displayName = generateDisplayName()
|
||||
}
|
||||
} else {
|
||||
displayName = generateDisplayName()
|
||||
}
|
||||
|
||||
displayColor := utils.GenerateRandomDisplayColor(config.MaxUserColor)
|
||||
|
||||
id := shortid.MustGenerate()
|
||||
user := &User{
|
||||
ID: id,
|
||||
DisplayName: displayName,
|
||||
DisplayColor: displayColor,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Create new user.
|
||||
if err := create(user); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// Assign it an access token.
|
||||
accessToken, err := utils.GenerateAccessToken()
|
||||
if err != nil {
|
||||
log.Errorln("Unable to create access token for new user")
|
||||
return nil, "", err
|
||||
}
|
||||
if err := addAccessTokenForUser(accessToken, id); err != nil {
|
||||
return nil, "", errors.Wrap(err, "unable to save access token for new user")
|
||||
}
|
||||
|
||||
return user, accessToken, nil
|
||||
}
|
||||
|
||||
// IsDisplayNameAvailable will check if the proposed name is available for use.
|
||||
func IsDisplayNameAvailable(displayName string) (bool, error) {
|
||||
if available, err := _datastore.GetQueries().IsDisplayNameAvailable(context.Background(), displayName); err != nil {
|
||||
return false, errors.Wrap(err, "unable to check if display name is available")
|
||||
} else if available != 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// ChangeUsername will change the user associated to userID from one display name to another.
|
||||
func ChangeUsername(userID string, username string) error {
|
||||
_datastore.DbLock.Lock()
|
||||
defer _datastore.DbLock.Unlock()
|
||||
|
||||
if err := _datastore.GetQueries().ChangeDisplayName(context.Background(), db.ChangeDisplayNameParams{
|
||||
DisplayName: username,
|
||||
ID: userID,
|
||||
PreviousNames: sql.NullString{String: fmt.Sprintf(",%s", username), Valid: true},
|
||||
NamechangedAt: sql.NullTime{Time: time.Now(), Valid: true},
|
||||
}); err != nil {
|
||||
return errors.Wrap(err, "unable to change display name")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ChangeUserColor will change the user associated to userID from one display name to another.
|
||||
func ChangeUserColor(userID string, color int) error {
|
||||
_datastore.DbLock.Lock()
|
||||
defer _datastore.DbLock.Unlock()
|
||||
|
||||
if err := _datastore.GetQueries().ChangeDisplayColor(context.Background(), db.ChangeDisplayColorParams{
|
||||
DisplayColor: int32(color),
|
||||
ID: userID,
|
||||
}); err != nil {
|
||||
return errors.Wrap(err, "unable to change display color")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func addAccessTokenForUser(accessToken, userID string) error {
|
||||
return _datastore.GetQueries().AddAccessTokenForUser(context.Background(), db.AddAccessTokenForUserParams{
|
||||
Token: accessToken,
|
||||
UserID: userID,
|
||||
})
|
||||
}
|
||||
|
||||
func create(user *User) error {
|
||||
_datastore.DbLock.Lock()
|
||||
defer _datastore.DbLock.Unlock()
|
||||
|
||||
tx, err := _datastore.DB.Begin()
|
||||
if err != nil {
|
||||
log.Debugln(err)
|
||||
}
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
stmt, err := tx.Prepare("INSERT INTO users(id, display_name, display_color, previous_names, created_at) values(?, ?, ?, ?, ?)")
|
||||
if err != nil {
|
||||
log.Debugln(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
_, err = stmt.Exec(user.ID, user.DisplayName, user.DisplayColor, user.DisplayName, user.CreatedAt)
|
||||
if err != nil {
|
||||
log.Errorln("error creating new user", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// SetEnabled will set the enabled status of a single user by ID.
|
||||
func SetEnabled(userID string, enabled bool) error {
|
||||
_datastore.DbLock.Lock()
|
||||
defer _datastore.DbLock.Unlock()
|
||||
|
||||
tx, err := _datastore.DB.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer tx.Rollback() //nolint
|
||||
|
||||
var stmt *sql.Stmt
|
||||
if !enabled {
|
||||
stmt, err = tx.Prepare("UPDATE users SET disabled_at=DATETIME('now', 'localtime') WHERE id IS ?")
|
||||
} else {
|
||||
stmt, err = tx.Prepare("UPDATE users SET disabled_at=null WHERE id IS ?")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer stmt.Close()
|
||||
|
||||
if _, err := stmt.Exec(userID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// GetUserByToken will return a user by an access token.
|
||||
func GetUserByToken(token string) *User {
|
||||
u, err := _datastore.GetQueries().GetUserByAccessToken(context.Background(), token)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var scopes []string
|
||||
if u.Scopes.Valid {
|
||||
scopes = strings.Split(u.Scopes.String, ",")
|
||||
}
|
||||
|
||||
var disabledAt *time.Time
|
||||
if u.DisabledAt.Valid {
|
||||
disabledAt = &u.DisabledAt.Time
|
||||
}
|
||||
|
||||
var authenticatedAt *time.Time
|
||||
if u.AuthenticatedAt.Valid {
|
||||
authenticatedAt = &u.AuthenticatedAt.Time
|
||||
}
|
||||
|
||||
return &User{
|
||||
ID: u.ID,
|
||||
DisplayName: u.DisplayName,
|
||||
DisplayColor: int(u.DisplayColor),
|
||||
CreatedAt: u.CreatedAt.Time,
|
||||
DisabledAt: disabledAt,
|
||||
PreviousNames: strings.Split(u.PreviousNames.String, ","),
|
||||
NameChangedAt: &u.NamechangedAt.Time,
|
||||
AuthenticatedAt: authenticatedAt,
|
||||
Authenticated: authenticatedAt != nil,
|
||||
Scopes: scopes,
|
||||
}
|
||||
}
|
||||
|
||||
// SetAccessTokenToOwner will reassign an access token to be owned by a
|
||||
// different user. Used for logging in with external auth.
|
||||
func SetAccessTokenToOwner(token, userID string) error {
|
||||
return _datastore.GetQueries().SetAccessTokenToOwner(context.Background(), db.SetAccessTokenToOwnerParams{
|
||||
UserID: userID,
|
||||
Token: token,
|
||||
})
|
||||
}
|
||||
|
||||
// SetUserAsAuthenticated will mark that a user has been authenticated
|
||||
// in some way.
|
||||
func SetUserAsAuthenticated(userID string) error {
|
||||
return errors.Wrap(_datastore.GetQueries().SetUserAsAuthenticated(context.Background(), userID), "unable to set user as authenticated")
|
||||
}
|
||||
|
||||
// SetModerator will add or remove moderator status for a single user by ID.
|
||||
func SetModerator(userID string, isModerator bool) error {
|
||||
if isModerator {
|
||||
return addScopeToUser(userID, moderatorScopeKey)
|
||||
}
|
||||
|
||||
return removeScopeFromUser(userID, moderatorScopeKey)
|
||||
}
|
||||
|
||||
func addScopeToUser(userID string, scope string) error {
|
||||
u := GetUserByID(userID)
|
||||
if u == nil {
|
||||
return errors.New("user not found when modifying scope")
|
||||
}
|
||||
|
||||
scopesString := u.Scopes
|
||||
scopes := utils.StringSliceToMap(scopesString)
|
||||
scopes[scope] = true
|
||||
|
||||
scopesSlice := utils.StringMapKeys(scopes)
|
||||
|
||||
return setScopesOnUser(userID, scopesSlice)
|
||||
}
|
||||
|
||||
func removeScopeFromUser(userID string, scope string) error {
|
||||
u := GetUserByID(userID)
|
||||
scopesString := u.Scopes
|
||||
scopes := utils.StringSliceToMap(scopesString)
|
||||
delete(scopes, scope)
|
||||
|
||||
scopesSlice := utils.StringMapKeys(scopes)
|
||||
|
||||
return setScopesOnUser(userID, scopesSlice)
|
||||
}
|
||||
|
||||
func setScopesOnUser(userID string, scopes []string) error {
|
||||
_datastore.DbLock.Lock()
|
||||
defer _datastore.DbLock.Unlock()
|
||||
|
||||
tx, err := _datastore.DB.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer tx.Rollback() //nolint
|
||||
|
||||
scopesSliceString := strings.TrimSpace(strings.Join(scopes, ","))
|
||||
stmt, err := tx.Prepare("UPDATE users SET scopes=? WHERE id IS ?")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer stmt.Close()
|
||||
|
||||
var val *string
|
||||
if scopesSliceString == "" {
|
||||
val = nil
|
||||
} else {
|
||||
val = &scopesSliceString
|
||||
}
|
||||
|
||||
if _, err := stmt.Exec(val, userID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// GetUserByID will return a user by a user ID.
|
||||
func GetUserByID(id string) *User {
|
||||
_datastore.DbLock.Lock()
|
||||
defer _datastore.DbLock.Unlock()
|
||||
|
||||
query := "SELECT id, display_name, display_color, created_at, disabled_at, previous_names, namechanged_at, scopes FROM users WHERE id = ?"
|
||||
row := _datastore.DB.QueryRow(query, id)
|
||||
if row == nil {
|
||||
log.Errorln(row)
|
||||
return nil
|
||||
}
|
||||
return getUserFromRow(row)
|
||||
}
|
||||
|
||||
// GetDisabledUsers will return back all the currently disabled users that are not API users.
|
||||
func GetDisabledUsers() []*User {
|
||||
query := "SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at FROM users WHERE disabled_at IS NOT NULL AND type IS NOT 'API'"
|
||||
|
||||
rows, err := _datastore.DB.Query(query)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
return nil
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
users := getUsersFromRows(rows)
|
||||
|
||||
sort.Slice(users, func(i, j int) bool {
|
||||
return users[i].DisabledAt.Before(*users[j].DisabledAt)
|
||||
})
|
||||
|
||||
return users
|
||||
}
|
||||
|
||||
// GetModeratorUsers will return a list of users with moderator access.
|
||||
func GetModeratorUsers() []*User {
|
||||
query := `SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at FROM (
|
||||
WITH RECURSIVE split(id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at, scope, rest) AS (
|
||||
SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at, '', scopes || ',' FROM users
|
||||
UNION ALL
|
||||
SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at,
|
||||
substr(rest, 0, instr(rest, ',')),
|
||||
substr(rest, instr(rest, ',')+1)
|
||||
FROM split
|
||||
WHERE rest <> '')
|
||||
SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at, scope
|
||||
FROM split
|
||||
WHERE scope <> ''
|
||||
ORDER BY created_at
|
||||
) AS token WHERE token.scope = ?`
|
||||
|
||||
rows, err := _datastore.DB.Query(query, moderatorScopeKey)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
return nil
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
users := getUsersFromRows(rows)
|
||||
|
||||
return users
|
||||
}
|
||||
|
||||
func getUsersFromRows(rows *sql.Rows) []*User {
|
||||
users := make([]*User, 0)
|
||||
|
||||
for rows.Next() {
|
||||
var id string
|
||||
var displayName string
|
||||
var displayColor int
|
||||
var createdAt time.Time
|
||||
var disabledAt *time.Time
|
||||
var previousUsernames string
|
||||
var userNameChangedAt *time.Time
|
||||
var scopesString *string
|
||||
|
||||
if err := rows.Scan(&id, &displayName, &scopesString, &displayColor, &createdAt, &disabledAt, &previousUsernames, &userNameChangedAt); err != nil {
|
||||
log.Errorln("error creating collection of users from results", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
var scopes []string
|
||||
if scopesString != nil {
|
||||
scopes = strings.Split(*scopesString, ",")
|
||||
}
|
||||
|
||||
user := &User{
|
||||
ID: id,
|
||||
DisplayName: displayName,
|
||||
DisplayColor: displayColor,
|
||||
CreatedAt: createdAt,
|
||||
DisabledAt: disabledAt,
|
||||
PreviousNames: strings.Split(previousUsernames, ","),
|
||||
NameChangedAt: userNameChangedAt,
|
||||
Scopes: scopes,
|
||||
}
|
||||
users = append(users, user)
|
||||
}
|
||||
|
||||
sort.Slice(users, func(i, j int) bool {
|
||||
return users[i].CreatedAt.Before(users[j].CreatedAt)
|
||||
})
|
||||
|
||||
return users
|
||||
}
|
||||
|
||||
func getUserFromRow(row *sql.Row) *User {
|
||||
var id string
|
||||
var displayName string
|
||||
var displayColor int
|
||||
var createdAt time.Time
|
||||
var disabledAt *time.Time
|
||||
var previousUsernames string
|
||||
var userNameChangedAt *time.Time
|
||||
var scopesString *string
|
||||
|
||||
if err := row.Scan(&id, &displayName, &displayColor, &createdAt, &disabledAt, &previousUsernames, &userNameChangedAt, &scopesString); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var scopes []string
|
||||
if scopesString != nil {
|
||||
scopes = strings.Split(*scopesString, ",")
|
||||
}
|
||||
|
||||
return &User{
|
||||
ID: id,
|
||||
DisplayName: displayName,
|
||||
DisplayColor: displayColor,
|
||||
CreatedAt: createdAt,
|
||||
DisabledAt: disabledAt,
|
||||
PreviousNames: strings.Split(previousUsernames, ","),
|
||||
NameChangedAt: userNameChangedAt,
|
||||
Scopes: scopes,
|
||||
}
|
||||
}
|
|
@ -5,13 +5,12 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/owncast/owncast/core/chat/events"
|
||||
"github.com/owncast/owncast/core/user"
|
||||
"github.com/owncast/owncast/models"
|
||||
)
|
||||
|
||||
func TestSendChatEvent(t *testing.T) {
|
||||
timestamp := time.Unix(72, 6).UTC()
|
||||
user := user.User{
|
||||
user := models.User{
|
||||
ID: "user id",
|
||||
DisplayName: "display name",
|
||||
DisplayColor: 4,
|
||||
|
@ -64,7 +63,7 @@ func TestSendChatEvent(t *testing.T) {
|
|||
|
||||
func TestSendChatEventUsernameChanged(t *testing.T) {
|
||||
timestamp := time.Unix(72, 6).UTC()
|
||||
user := user.User{
|
||||
user := models.User{
|
||||
ID: "user id",
|
||||
DisplayName: "display name",
|
||||
DisplayColor: 4,
|
||||
|
@ -112,7 +111,7 @@ func TestSendChatEventUsernameChanged(t *testing.T) {
|
|||
|
||||
func TestSendChatEventUserJoined(t *testing.T) {
|
||||
timestamp := time.Unix(72, 6).UTC()
|
||||
user := user.User{
|
||||
user := models.User{
|
||||
ID: "user id",
|
||||
DisplayName: "display name",
|
||||
DisplayColor: 4,
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/owncast/owncast/core/data"
|
||||
"github.com/owncast/owncast/core/user"
|
||||
"github.com/owncast/owncast/models"
|
||||
)
|
||||
|
||||
|
@ -17,13 +16,13 @@ type WebhookEvent struct {
|
|||
|
||||
// WebhookChatMessage represents a single chat message sent as a webhook payload.
|
||||
type WebhookChatMessage struct {
|
||||
User *user.User `json:"user,omitempty"`
|
||||
Timestamp *time.Time `json:"timestamp,omitempty"`
|
||||
Body string `json:"body,omitempty"`
|
||||
RawBody string `json:"rawBody,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
ClientID uint `json:"clientId,omitempty"`
|
||||
Visible bool `json:"visible"`
|
||||
User *models.User `json:"user,omitempty"`
|
||||
Timestamp *time.Time `json:"timestamp,omitempty"`
|
||||
Body string `json:"body,omitempty"`
|
||||
RawBody string `json:"rawBody,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
ClientID uint `json:"clientId,omitempty"`
|
||||
Visible bool `json:"visible"`
|
||||
}
|
||||
|
||||
// SendEventToWebhooks will send a single webhook event to all webhook destinations.
|
||||
|
|
|
@ -158,7 +158,7 @@ UPDATE users SET display_color = $1 WHERE id = $2
|
|||
`
|
||||
|
||||
type ChangeDisplayColorParams struct {
|
||||
DisplayColor int32
|
||||
DisplayColor int
|
||||
ID string
|
||||
}
|
||||
|
||||
|
@ -253,8 +253,8 @@ SELECT iri, inbox, name, username, image, created_at FROM ap_followers WHERE app
|
|||
`
|
||||
|
||||
type GetFederationFollowersWithOffsetParams struct {
|
||||
Limit int32
|
||||
Offset int32
|
||||
Limit int
|
||||
Offset int
|
||||
}
|
||||
|
||||
type GetFederationFollowersWithOffsetRow struct {
|
||||
|
@ -365,8 +365,8 @@ SELECT iri, actor, type, timestamp FROM ap_accepted_activities ORDER BY timestam
|
|||
`
|
||||
|
||||
type GetInboundActivitiesWithOffsetParams struct {
|
||||
Limit int32
|
||||
Offset int32
|
||||
Limit int
|
||||
Offset int
|
||||
}
|
||||
|
||||
type GetInboundActivitiesWithOffsetRow struct {
|
||||
|
@ -514,8 +514,8 @@ SELECT value FROM ap_outbox LIMIT $1 OFFSET $2
|
|||
`
|
||||
|
||||
type GetOutboxWithOffsetParams struct {
|
||||
Limit int32
|
||||
Offset int32
|
||||
Limit int
|
||||
Offset int
|
||||
}
|
||||
|
||||
func (q *Queries) GetOutboxWithOffset(ctx context.Context, arg GetOutboxWithOffsetParams) ([][]byte, error) {
|
||||
|
|
8832
docs/api/index.html
8832
docs/api/index.html
File diff suppressed because one or more lines are too long
2
docs/api/redocly.yaml
Normal file
2
docs/api/redocly.yaml
Normal file
|
@ -0,0 +1,2 @@
|
|||
decorators:
|
||||
remove-x-internal: on
|
114
go.mod
114
go.mod
|
@ -1,92 +1,80 @@
|
|||
module github.com/owncast/owncast
|
||||
|
||||
go 1.22
|
||||
go 1.22.0
|
||||
|
||||
toolchain go1.23.1
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go v1.53.5
|
||||
github.com/CAFxX/httpcompression v0.0.9
|
||||
github.com/SherClockHolmes/webpush-go v1.3.0
|
||||
github.com/TwiN/go-away v1.6.13
|
||||
github.com/andybalholm/cascadia v1.3.2
|
||||
github.com/aws/aws-sdk-go v1.55.5
|
||||
github.com/go-chi/chi/v5 v5.1.0
|
||||
github.com/go-fed/activity v1.0.1-0.20210803212804-d866ba75dd0f
|
||||
github.com/go-fed/httpsig v1.1.0
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/grafov/m3u8 v0.12.0
|
||||
github.com/jellydator/ttlcache/v3 v3.3.0
|
||||
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
|
||||
github.com/mattn/go-sqlite3 v1.14.22
|
||||
github.com/microcosm-cc/bluemonday v1.0.26
|
||||
github.com/mattn/go-sqlite3 v1.14.24
|
||||
github.com/microcosm-cc/bluemonday v1.0.27
|
||||
github.com/mssola/user_agent v0.6.0
|
||||
github.com/nakabonne/tstorage v0.3.6
|
||||
github.com/nareix/joy5 v0.0.0-20210317075623-2c912ca30590
|
||||
github.com/oschwald/geoip2-golang v1.9.0
|
||||
github.com/oapi-codegen/runtime v1.1.1
|
||||
github.com/oschwald/geoip2-golang v1.11.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.20.5
|
||||
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5
|
||||
github.com/schollz/sqlite3dump v1.3.1
|
||||
github.com/shirou/gopsutil/v4 v4.24.9
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/teris-io/shortid v0.0.0-20220617161101-71ec9f2aa569
|
||||
github.com/yuin/goldmark v1.7.1
|
||||
golang.org/x/mod v0.17.0
|
||||
golang.org/x/time v0.5.0
|
||||
github.com/yuin/goldmark v1.7.8
|
||||
github.com/yuin/goldmark-emoji v1.0.4
|
||||
golang.org/x/crypto v0.28.0
|
||||
golang.org/x/mod v0.21.0
|
||||
golang.org/x/net v0.30.0
|
||||
golang.org/x/time v0.7.0
|
||||
gopkg.in/evanphx/json-patch.v5 v5.9.0
|
||||
mvdan.cc/xurls/v2 v2.5.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/jonboulle/clockwork v0.2.2 // indirect
|
||||
github.com/lestrrat-go/strftime v1.0.4 // indirect
|
||||
github.com/mvdan/xurls v1.1.0 // indirect
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
golang.org/x/crypto v0.23.0 // indirect
|
||||
golang.org/x/net v0.25.0
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
)
|
||||
|
||||
require github.com/prometheus/client_golang v1.19.1
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/klauspost/compress v1.16.7 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.48.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/nakabonne/tstorage v0.3.6
|
||||
github.com/shirou/gopsutil/v3 v3.24.4
|
||||
)
|
||||
|
||||
require github.com/SherClockHolmes/webpush-go v1.3.0
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/ebitengine/purego v0.8.0 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-test/deep v1.0.4 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/google/uuid v1.5.0 // indirect
|
||||
github.com/gorilla/css v1.0.0 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/oschwald/maxminddb-golang v1.11.0 // indirect
|
||||
github.com/jonboulle/clockwork v0.2.2 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/lestrrat-go/strftime v1.0.4 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/oschwald/maxminddb-golang v1.13.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
golang.org/x/sync v0.3.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.55.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/text v0.19.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/CAFxX/httpcompression v0.0.9
|
||||
github.com/TwiN/go-away v1.6.13
|
||||
github.com/andybalholm/cascadia v1.3.2
|
||||
github.com/go-chi/chi/v5 v5.0.12
|
||||
github.com/jellydator/ttlcache/v3 v3.2.0
|
||||
github.com/mssola/user_agent v0.6.0
|
||||
github.com/oapi-codegen/runtime v1.1.1
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/yuin/goldmark-emoji v1.0.2
|
||||
gopkg.in/evanphx/json-patch.v5 v5.9.0
|
||||
mvdan.cc/xurls v1.1.0
|
||||
)
|
||||
|
||||
replace github.com/go-fed/activity => github.com/owncast/activity v1.0.1-0.20211229051252-7821289d4026
|
||||
|
|
174
go.sum
174
go.sum
|
@ -11,27 +11,23 @@ github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsVi
|
|||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
|
||||
github.com/aws/aws-sdk-go v1.50.33 h1:/SKPJ7ZVPCFOYZyTKo5YdjeUEeOn2J2M0qfDTXWAoEU=
|
||||
github.com/aws/aws-sdk-go v1.50.33/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
|
||||
github.com/aws/aws-sdk-go v1.51.17 h1:Cfa40lCdjv9OxC3X1Ks3a6O1Tu3gOANSyKHOSw/zuWU=
|
||||
github.com/aws/aws-sdk-go v1.51.17/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
|
||||
github.com/aws/aws-sdk-go v1.51.23 h1:/3TEdsEE/aHmdKGw2NrOp7Sdea76zfffGkTTSXTsDxY=
|
||||
github.com/aws/aws-sdk-go v1.51.23/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
|
||||
github.com/aws/aws-sdk-go v1.53.5 h1:1OcVWMjGlwt7EU5OWmmEEXqaYfmX581EK317QJZXItM=
|
||||
github.com/aws/aws-sdk-go v1.53.5/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
|
||||
github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
|
||||
github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/dave/jennifer v1.3.0/go.mod h1:fIb+770HOpJ2fmN9EPPKOqm1vMGhB+TwXKMZhrIygKg=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
|
||||
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/ebitengine/purego v0.8.0 h1:JbqvnEzRvPpxhCJzJJ2y0RbiZ8nyjccVUrSM3q+GvvE=
|
||||
github.com/ebitengine/purego v0.8.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
||||
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-fed/httpsig v0.1.1-0.20190914113940-c2de3672e5b5/go.mod h1:T56HUNYZUQ1AGUzhAYPugZfp36sKApVnGBgKlIY+aIE=
|
||||
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
|
||||
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
|
||||
|
@ -46,19 +42,18 @@ github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb
|
|||
github.com/google/brotli/go/cbrotli v0.0.0-20230829110029-ed738e842d2f h1:jopqB+UTSdJGEJT8tEqYyE29zN91fi2827oLET8tl7k=
|
||||
github.com/google/brotli/go/cbrotli v0.0.0-20230829110029-ed738e842d2f/go.mod h1:nOPhAkwVliJdNTkj3gXpljmWhjc4wCaVqbMJcPKWP4s=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grafov/m3u8 v0.12.0 h1:T6iTwTsSEtMcwkayef+FJO8kj+Sglr4Lh81Zj8Ked/4=
|
||||
github.com/grafov/m3u8 v0.12.0/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080=
|
||||
github.com/jellydator/ttlcache/v3 v3.2.0 h1:6lqVJ8X3ZaUwvzENqPAobDsXNExfUJd61u++uW8a3LE=
|
||||
github.com/jellydator/ttlcache/v3 v3.2.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4=
|
||||
github.com/jellydator/ttlcache/v3 v3.3.0 h1:BdoC9cE81qXfrxeb9eoJi9dWrdhSuwXMAnHTbnBm4Wc=
|
||||
github.com/jellydator/ttlcache/v3 v3.3.0/go.mod h1:bj2/e0l4jRnQdrnSTaGTsh4GSXvMjQcy41i7th0GVGw=
|
||||
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||
|
@ -66,13 +61,16 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfC
|
|||
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
|
||||
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
|
||||
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
|
||||
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
|
||||
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8=
|
||||
github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is=
|
||||
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible h1:Y6sqxHMyB1D2YSzWkLibYKgg+SwmyFU9dF2hn6MdTj4=
|
||||
|
@ -84,22 +82,24 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2
|
|||
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
|
||||
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||
github.com/mssola/user_agent v0.6.0 h1:uwPR4rtWlCHRFyyP9u2KOV0u8iQXmS7Z7feTrstQwk4=
|
||||
github.com/mssola/user_agent v0.6.0/go.mod h1:TTPno8LPY3wAIEKRpAtkdMT0f8SE24pLRGPahjCH4uw=
|
||||
github.com/mvdan/xurls v1.1.0 h1:OpuDelGQ1R1ueQ6sSryzi6P+1RtBpfQHM8fJwlE45ww=
|
||||
github.com/mvdan/xurls v1.1.0/go.mod h1:tQlNn3BED8bE/15hnSL2HLkDeLWpNPAwtw7wkEq44oU=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/nakabonne/tstorage v0.3.6 h1:usp7pTohax8mynnFiUSUQ2QVBCKLCkYx3gmb3+rJo54=
|
||||
github.com/nakabonne/tstorage v0.3.6/go.mod h1:1xUrK3s1MXSlU6dn96xHerHx/MdO4BGmsAHEUbsaOxU=
|
||||
github.com/nareix/joy5 v0.0.0-20210317075623-2c912ca30590 h1:PnxRU8L8Y2q82vFC2QdNw23Dm2u6WrjecIdpXjiYbXM=
|
||||
github.com/nareix/joy5 v0.0.0-20210317075623-2c912ca30590/go.mod h1:XmAOs6UJXpNXRwKk+KY/nv5kL6xXYXyellk+A1pTlko=
|
||||
github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro=
|
||||
github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
|
||||
github.com/oschwald/geoip2-golang v1.9.0 h1:uvD3O6fXAXs+usU+UGExshpdP13GAqp4GBrzN7IgKZc=
|
||||
github.com/oschwald/geoip2-golang v1.9.0/go.mod h1:BHK6TvDyATVQhKNbQBdrj9eAvuwOMi2zSFXizL3K81Y=
|
||||
github.com/oschwald/maxminddb-golang v1.11.0 h1:aSXMqYR/EPNjGE8epgqwDay+P30hCBZIveY0WZbAWh0=
|
||||
github.com/oschwald/maxminddb-golang v1.11.0/go.mod h1:YmVI+H0zh3ySFR3w+oz8PCfglAFj3PuCmui13+P9zDg=
|
||||
github.com/oschwald/geoip2-golang v1.11.0 h1:hNENhCn1Uyzhf9PTmquXENiWS6AlxAEnBII6r8krA3w=
|
||||
github.com/oschwald/geoip2-golang v1.11.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo=
|
||||
github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU=
|
||||
github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o=
|
||||
github.com/owncast/activity v1.0.1-0.20211229051252-7821289d4026 h1:E1nxiX44BcMQTSSs8MHLm2rXnqXNedYZkFI31gXMsJc=
|
||||
github.com/owncast/activity v1.0.1-0.20211229051252-7821289d4026/go.mod h1:v4QoPaAzjWZ8zN2VFVGL5ep9C02mst0hQYHUpQwso4Q=
|
||||
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
|
||||
|
@ -111,32 +111,24 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
|
||||
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
|
||||
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
|
||||
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
|
||||
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
|
||||
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/prometheus/client_golang v1.20.0 h1:jBzTZ7B099Rg24tny+qngoynol8LtVYlA2bqx3vEloI=
|
||||
github.com/prometheus/client_golang v1.20.0/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
|
||||
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 h1:mZHayPoR0lNmnHyvtYjDeq0zlVHn9K/ZXoy17ylucdo=
|
||||
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5/go.mod h1:GEXHk5HgEKCvEIIrSpFI3ozzG5xOKA2DVlEX/gGnewM=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/schollz/sqlite3dump v1.3.1 h1:QXizJ7XEJ7hggjqjZ3YRtF3+javm8zKtzNByYtEkPRA=
|
||||
github.com/schollz/sqlite3dump v1.3.1/go.mod h1:mzSTjZpJH4zAb1FN3iNlhWPbbdyeBpOaTW0hukyMHyI=
|
||||
github.com/shirou/gopsutil/v3 v3.24.2 h1:kcR0erMbLg5/3LcInpw0X/rrPSqq4CDPyI6A6ZRC18Y=
|
||||
github.com/shirou/gopsutil/v3 v3.24.2/go.mod h1:tSg/594BcA+8UdQU2XcW803GWYgdtauFFPgJCJKZlVk=
|
||||
github.com/shirou/gopsutil/v3 v3.24.3 h1:eoUGJSmdfLzJ3mxIhmOAhgKEKgQkeOwKpz1NbhVnuPE=
|
||||
github.com/shirou/gopsutil/v3 v3.24.3/go.mod h1:JpND7O217xa72ewWz9zN2eIIkPWsDN/3pl0H8Qt0uwg=
|
||||
github.com/shirou/gopsutil/v3 v3.24.4 h1:dEHgzZXt4LMNm+oYELpzl9YCqV65Yr/6SfrvgRBtXeU=
|
||||
github.com/shirou/gopsutil/v3 v3.24.4/go.mod h1:lTd2mdiOspcqLgAnr9/nGi71NkeMpWKdmhuxm9GusH8=
|
||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
|
||||
github.com/shirou/gopsutil/v4 v4.24.9 h1:KIV+/HaHD5ka5f570RZq+2SaeFsb/pq+fp2DGNWYoOI=
|
||||
github.com/shirou/gopsutil/v4 v4.24.9/go.mod h1:3fkaHNeYsUFCGZ8+9vZVWtbyM1k2eRnlL+bWO8Bxa/Q=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/spf13/cobra v0.0.4-0.20190109003409-7547e83b2d85/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
|
@ -145,7 +137,6 @@ github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKk
|
|||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
|
@ -163,35 +154,35 @@ github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9f
|
|||
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/valyala/gozstd v1.20.1 h1:xPnnnvjmaDDitMFfDxmQ4vpx0+3CdTg2o3lALvXTU/g=
|
||||
github.com/valyala/gozstd v1.20.1/go.mod h1:y5Ew47GLlP37EkTB+B4s7r6A5rdaeB7ftbl9zoYiIPQ=
|
||||
github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA=
|
||||
github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U=
|
||||
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s=
|
||||
github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY=
|
||||
github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg=
|
||||
github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/yuin/goldmark v1.7.7 h1:5m9rrB1sW3JUMToKFQfb+FGt1U7r57IHu5GrYrG2nqU=
|
||||
github.com/yuin/goldmark v1.7.7/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
||||
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/yuin/goldmark-emoji v1.0.3 h1:aLRkLHOuBR2czCY4R8olwMjID+tENfhyFDMCRhbIQY4=
|
||||
github.com/yuin/goldmark-emoji v1.0.3/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
|
||||
github.com/yuin/goldmark-emoji v1.0.4 h1:vCwMkPZSNefSUnOW2ZKRUjBSD5Ok3W78IXhGxxAEF90=
|
||||
github.com/yuin/goldmark-emoji v1.0.4/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
|
||||
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
|
||||
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
|
||||
golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
|
@ -200,17 +191,15 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
|||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
|
||||
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
|
||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180525142821-c11f84a56e43/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
@ -225,37 +214,38 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
|
||||
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
|
||||
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
|
||||
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
|
||||
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
@ -267,5 +257,5 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
mvdan.cc/xurls v1.1.0 h1:kj0j2lonKseISJCiq1Tfk+iTv65dDGCl0rTbanXJGGc=
|
||||
mvdan.cc/xurls v1.1.0/go.mod h1:TNWuhvo+IqbUCmtUIb/3LJSQdrzel8loVpgFm0HikbI=
|
||||
mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=
|
||||
mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE=
|
||||
|
|
2
main.go
2
main.go
|
@ -12,8 +12,8 @@ import (
|
|||
"github.com/owncast/owncast/core"
|
||||
"github.com/owncast/owncast/core/data"
|
||||
"github.com/owncast/owncast/metrics"
|
||||
"github.com/owncast/owncast/router"
|
||||
"github.com/owncast/owncast/utils"
|
||||
"github.com/owncast/owncast/webserver/router"
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
|
@ -3,9 +3,9 @@ package metrics
|
|||
import (
|
||||
"time"
|
||||
|
||||
"github.com/shirou/gopsutil/v3/cpu"
|
||||
"github.com/shirou/gopsutil/v3/disk"
|
||||
"github.com/shirou/gopsutil/v3/mem"
|
||||
"github.com/shirou/gopsutil/v4/cpu"
|
||||
"github.com/shirou/gopsutil/v4/disk"
|
||||
"github.com/shirou/gopsutil/v4/mem"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/owncast/owncast/core"
|
||||
"github.com/owncast/owncast/core/chat"
|
||||
"github.com/owncast/owncast/core/data"
|
||||
"github.com/owncast/owncast/persistence/userrepository"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
|
@ -30,6 +31,7 @@ func startViewerCollectionMetrics() {
|
|||
func collectViewerCount() {
|
||||
// Don't collect metrics for viewers if there's no stream active.
|
||||
if !core.GetStatus().Online {
|
||||
activeViewerCount.Set(0)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -59,7 +61,8 @@ func collectChatClientCount() {
|
|||
currentChatMessageCount.Set(float64(cmc))
|
||||
|
||||
// Total user count
|
||||
uc := data.GetUsersCount()
|
||||
userRepository := userrepository.Get()
|
||||
uc := userRepository.GetUsersCount()
|
||||
// Insert user count into Prometheus collector.
|
||||
chatUserCount.Set(float64(uc))
|
||||
|
||||
|
|
11
models/auth.go
Normal file
11
models/auth.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
package models
|
||||
|
||||
// Type represents a form of authentication.
|
||||
type AuthType string
|
||||
|
||||
// The different auth types we support.
|
||||
const (
|
||||
// IndieAuth https://indieauth.spec.indieweb.org/.
|
||||
IndieAuth AuthType = "indieauth"
|
||||
Fediverse AuthType = "fediverse"
|
||||
)
|
12
models/chatAccessScopes.go
Normal file
12
models/chatAccessScopes.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
package models
|
||||
|
||||
const (
|
||||
// ScopeCanSendChatMessages will allow sending chat messages as itself.
|
||||
ScopeCanSendChatMessages = "CAN_SEND_MESSAGES"
|
||||
// ScopeCanSendSystemMessages will allow sending chat messages as the system.
|
||||
ScopeCanSendSystemMessages = "CAN_SEND_SYSTEM_MESSAGES"
|
||||
// ScopeHasAdminAccess will allow performing administrative actions on the server.
|
||||
ScopeHasAdminAccess = "HAS_ADMIN_ACCESS"
|
||||
|
||||
ModeratorScopeKey = "MODERATOR"
|
||||
)
|
|
@ -4,7 +4,7 @@ import (
|
|||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/owncast/owncast/geoip"
|
||||
"github.com/owncast/owncast/services/geoip"
|
||||
"github.com/owncast/owncast/utils"
|
||||
)
|
||||
|
||||
|
|
19
models/externalAPIUser.go
Normal file
19
models/externalAPIUser.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// ExternalAPIUser represents a single 3rd party integration that uses an access token.
|
||||
// This struct mostly matches the User struct so they can be used interchangeably.
|
||||
type ExternalAPIUser struct {
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
LastUsedAt *time.Time `json:"lastUsedAt,omitempty"`
|
||||
ID string `json:"id"`
|
||||
AccessToken string `json:"accessToken"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Type string `json:"type,omitempty"` // Should be API
|
||||
Scopes []string `json:"scopes"`
|
||||
DisplayColor int `json:"displayColor"`
|
||||
IsBot bool `json:"isBot"`
|
||||
}
|
|
@ -105,7 +105,7 @@ func GetAllSocialHandles() map[string]SocialHandle {
|
|||
Icon: "/img/platformlogos/twitch.svg",
|
||||
},
|
||||
"twitter": {
|
||||
Platform: "Twitter",
|
||||
Platform: "X",
|
||||
Icon: "/img/platformlogos/twitter.svg",
|
||||
},
|
||||
"youtube": {
|
||||
|
|
36
models/user.go
Normal file
36
models/user.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/owncast/owncast/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
moderatorScopeKey = "MODERATOR"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
DisabledAt *time.Time `json:"disabledAt,omitempty"`
|
||||
NameChangedAt *time.Time `json:"nameChangedAt,omitempty"`
|
||||
AuthenticatedAt *time.Time `json:"-"`
|
||||
ID string `json:"id"`
|
||||
DisplayName string `json:"displayName"`
|
||||
PreviousNames []string `json:"previousNames"`
|
||||
Scopes []string `json:"scopes,omitempty"`
|
||||
DisplayColor int `json:"displayColor"`
|
||||
IsBot bool `json:"isBot"`
|
||||
Authenticated bool `json:"authenticated"`
|
||||
}
|
||||
|
||||
// IsEnabled will return if this single user is enabled.
|
||||
func (u *User) IsEnabled() bool {
|
||||
return u.DisabledAt == nil
|
||||
}
|
||||
|
||||
// IsModerator will return if the user has moderation privileges.
|
||||
func (u *User) IsModerator() bool {
|
||||
_, hasModerationScope := utils.FindInSlice(u.Scopes, moderatorScopeKey)
|
||||
return hasModerationScope
|
||||
}
|
|
@ -4,7 +4,7 @@ import (
|
|||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/owncast/owncast/geoip"
|
||||
"github.com/owncast/owncast/services/geoip"
|
||||
"github.com/owncast/owncast/utils"
|
||||
)
|
||||
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
package notifications
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/owncast/owncast/config"
|
||||
"github.com/owncast/owncast/core/data"
|
||||
"github.com/owncast/owncast/db"
|
||||
"github.com/owncast/owncast/models"
|
||||
"github.com/owncast/owncast/persistence/tables"
|
||||
|
||||
"github.com/owncast/owncast/notifications/browser"
|
||||
"github.com/owncast/owncast/notifications/discord"
|
||||
"github.com/pkg/errors"
|
||||
|
@ -21,7 +25,7 @@ type Notifier struct {
|
|||
|
||||
// Setup will perform any pre-use setup for the notifier.
|
||||
func Setup(datastore *data.Datastore) {
|
||||
createNotificationsTable(datastore.DB)
|
||||
tables.CreateNotificationsTable(datastore.DB)
|
||||
initializeBrowserPushIfNeeded()
|
||||
}
|
||||
|
||||
|
@ -150,3 +154,31 @@ func (n *Notifier) Notify() {
|
|||
n.notifyDiscord()
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveNotificationForChannel removes a notification destination.
|
||||
func RemoveNotificationForChannel(channel, destination string) error {
|
||||
log.Debugln("Removing notification for channel", channel)
|
||||
return data.GetDatastore().GetQueries().RemoveNotificationDestinationForChannel(context.Background(), db.RemoveNotificationDestinationForChannelParams{
|
||||
Channel: channel,
|
||||
Destination: destination,
|
||||
})
|
||||
}
|
||||
|
||||
// GetNotificationDestinationsForChannel will return a collection of
|
||||
// destinations to notify for a given channel.
|
||||
func GetNotificationDestinationsForChannel(channel string) ([]string, error) {
|
||||
result, err := data.GetDatastore().GetQueries().GetNotificationDestinationsForChannel(context.Background(), channel)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to query notification destinations for channel "+channel)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// AddNotification saves a new user notification destination.
|
||||
func AddNotification(channel, destination string) error {
|
||||
return data.GetDatastore().GetQueries().AddNotification(context.Background(), db.AddNotificationParams{
|
||||
Channel: channel,
|
||||
Destination: destination,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
package notifications
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/owncast/owncast/core/data"
|
||||
"github.com/owncast/owncast/db"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func createNotificationsTable(db *sql.DB) {
|
||||
log.Traceln("Creating federation followers table...")
|
||||
|
||||
createTableSQL := `CREATE TABLE IF NOT EXISTS notifications (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"channel" TEXT NOT NULL,
|
||||
"destination" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP);`
|
||||
|
||||
data.MustExec(createTableSQL, db)
|
||||
data.MustExec(`CREATE INDEX IF NOT EXISTS idx_channel ON notifications (channel);`, db)
|
||||
}
|
||||
|
||||
// AddNotification saves a new user notification destination.
|
||||
func AddNotification(channel, destination string) error {
|
||||
return data.GetDatastore().GetQueries().AddNotification(context.Background(), db.AddNotificationParams{
|
||||
Channel: channel,
|
||||
Destination: destination,
|
||||
})
|
||||
}
|
||||
|
||||
// RemoveNotificationForChannel removes a notification destination.
|
||||
func RemoveNotificationForChannel(channel, destination string) error {
|
||||
log.Debugln("Removing notification for channel", channel)
|
||||
return data.GetDatastore().GetQueries().RemoveNotificationDestinationForChannel(context.Background(), db.RemoveNotificationDestinationForChannelParams{
|
||||
Channel: channel,
|
||||
Destination: destination,
|
||||
})
|
||||
}
|
||||
|
||||
// GetNotificationDestinationsForChannel will return a collection of
|
||||
// destinations to notify for a given channel.
|
||||
func GetNotificationDestinationsForChannel(channel string) ([]string, error) {
|
||||
result, err := data.GetDatastore().GetQueries().GetNotificationDestinationsForChannel(context.Background(), channel)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to query notification destinations for channel "+channel)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
6556
openapi.yaml
6556
openapi.yaml
File diff suppressed because one or more lines are too long
33
persistence/tables/messages.go
Normal file
33
persistence/tables/messages.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
package tables
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/owncast/owncast/utils"
|
||||
)
|
||||
|
||||
// CreateMessagesTable will create the chat messages table if needed.
|
||||
func CreateMessagesTable(db *sql.DB) {
|
||||
createTableSQL := `CREATE TABLE IF NOT EXISTS messages (
|
||||
"id" string NOT NULL,
|
||||
"user_id" TEXT,
|
||||
"body" TEXT,
|
||||
"eventType" TEXT,
|
||||
"hidden_at" DATETIME,
|
||||
"timestamp" DATETIME,
|
||||
"title" TEXT,
|
||||
"subtitle" TEXT,
|
||||
"image" TEXT,
|
||||
"link" TEXT,
|
||||
PRIMARY KEY (id)
|
||||
);`
|
||||
utils.MustExec(createTableSQL, db)
|
||||
|
||||
// Create indexes
|
||||
utils.MustExec(`CREATE INDEX IF NOT EXISTS user_id_hidden_at_timestamp ON messages (id, user_id, hidden_at, timestamp);`, db)
|
||||
utils.MustExec(`CREATE INDEX IF NOT EXISTS idx_id ON messages (id);`, db)
|
||||
utils.MustExec(`CREATE INDEX IF NOT EXISTS idx_user_id ON messages (user_id);`, db)
|
||||
utils.MustExec(`CREATE INDEX IF NOT EXISTS idx_hidden_at ON messages (hidden_at);`, db)
|
||||
utils.MustExec(`CREATE INDEX IF NOT EXISTS idx_timestamp ON messages (timestamp);`, db)
|
||||
utils.MustExec(`CREATE INDEX IF NOT EXISTS idx_messages_hidden_at_timestamp on messages(hidden_at, timestamp);`, db)
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package data
|
||||
package tables
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
@ -12,7 +12,7 @@ import (
|
|||
"github.com/teris-io/shortid"
|
||||
)
|
||||
|
||||
func migrateDatabaseSchema(db *sql.DB, from, to int) error {
|
||||
func MigrateDatabaseSchema(db *sql.DB, from, to int) error {
|
||||
log.Printf("Migrating database from version %d to %d", from, to)
|
||||
dbBackupFile := filepath.Join(config.BackupDirectory, fmt.Sprintf("owncast-v%d.bak", from))
|
||||
utils.Backup(db, dbBackupFile)
|
||||
|
@ -94,7 +94,7 @@ func migrateToSchema6(db *sql.DB) {
|
|||
// Fix chat messages table schema. Since chat is ephemeral we can drop
|
||||
// the table and recreate it.
|
||||
// Drop the old messages table
|
||||
MustExec(`DROP TABLE messages`, db)
|
||||
utils.MustExec(`DROP TABLE messages`, db)
|
||||
|
||||
// Recreate it
|
||||
CreateMessagesTable(db)
|
||||
|
@ -103,7 +103,7 @@ func migrateToSchema6(db *sql.DB) {
|
|||
// nolint:cyclop
|
||||
func migrateToSchema5(db *sql.DB) {
|
||||
// Create the access tokens table.
|
||||
createAccessTokenTable(db)
|
||||
CreateAccessTokenTable(db)
|
||||
|
||||
// 1. Authenticated bool added to the users table.
|
||||
// 2. Access tokens are now stored in their own table.
|
21
persistence/tables/notifications.go
Normal file
21
persistence/tables/notifications.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
package tables
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/owncast/owncast/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func CreateNotificationsTable(db *sql.DB) {
|
||||
log.Traceln("Creating federation followers table...")
|
||||
|
||||
createTableSQL := `CREATE TABLE IF NOT EXISTS notifications (
|
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"channel" TEXT NOT NULL,
|
||||
"destination" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP);`
|
||||
|
||||
utils.MustExec(createTableSQL, db)
|
||||
utils.MustExec(`CREATE INDEX IF NOT EXISTS idx_channel ON notifications (channel);`, db)
|
||||
}
|
|
@ -1,12 +1,18 @@
|
|||
package data
|
||||
package tables
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/owncast/owncast/utils"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func createAccessTokenTable(db *sql.DB) {
|
||||
func SetupUsers(db *sql.DB) {
|
||||
CreateUsersTable(db)
|
||||
CreateAccessTokenTable(db)
|
||||
}
|
||||
|
||||
func CreateAccessTokenTable(db *sql.DB) {
|
||||
createTableSQL := `CREATE TABLE IF NOT EXISTS user_access_tokens (
|
||||
"token" TEXT NOT NULL PRIMARY KEY,
|
||||
"user_id" TEXT NOT NULL,
|
||||
|
@ -25,7 +31,7 @@ func createAccessTokenTable(db *sql.DB) {
|
|||
}
|
||||
}
|
||||
|
||||
func createUsersTable(db *sql.DB) {
|
||||
func CreateUsersTable(db *sql.DB) {
|
||||
log.Traceln("Creating users table...")
|
||||
|
||||
createTableSQL := `CREATE TABLE IF NOT EXISTS users (
|
||||
|
@ -43,25 +49,8 @@ func createUsersTable(db *sql.DB) {
|
|||
PRIMARY KEY (id)
|
||||
);`
|
||||
|
||||
MustExec(createTableSQL, db)
|
||||
MustExec(`CREATE INDEX IF NOT EXISTS idx_user_id ON users (id);`, db)
|
||||
MustExec(`CREATE INDEX IF NOT EXISTS idx_user_id_disabled ON users (id, disabled_at);`, db)
|
||||
MustExec(`CREATE INDEX IF NOT EXISTS idx_user_disabled_at ON users (disabled_at);`, db)
|
||||
}
|
||||
|
||||
// GetUsersCount will return the number of users in the database.
|
||||
func GetUsersCount() int64 {
|
||||
query := `SELECT COUNT(*) FROM users`
|
||||
rows, err := _db.Query(query)
|
||||
if err != nil || rows.Err() != nil {
|
||||
return 0
|
||||
}
|
||||
defer rows.Close()
|
||||
var count int64
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(&count); err != nil {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
return count
|
||||
utils.MustExec(createTableSQL, db)
|
||||
utils.MustExec(`CREATE INDEX IF NOT EXISTS idx_user_id ON users (id);`, db)
|
||||
utils.MustExec(`CREATE INDEX IF NOT EXISTS idx_user_id_disabled ON users (id, disabled_at);`, db)
|
||||
utils.MustExec(`CREATE INDEX IF NOT EXISTS idx_user_disabled_at ON users (disabled_at);`, db)
|
||||
}
|
806
persistence/userrepository/userrepository.go
Normal file
806
persistence/userrepository/userrepository.go
Normal file
|
@ -0,0 +1,806 @@
|
|||
package userrepository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/owncast/owncast/config"
|
||||
"github.com/owncast/owncast/core/data"
|
||||
"github.com/owncast/owncast/db"
|
||||
|
||||
"github.com/owncast/owncast/models"
|
||||
"github.com/owncast/owncast/utils"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/teris-io/shortid"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type UserRepository interface {
|
||||
ChangeUserColor(userID string, color int) error
|
||||
ChangeUsername(userID string, username string) error
|
||||
CreateAnonymousUser(displayName string) (*models.User, string, error)
|
||||
DeleteExternalAPIUser(token string) error
|
||||
GetDisabledUsers() []*models.User
|
||||
GetExternalAPIUser() ([]models.ExternalAPIUser, error)
|
||||
GetExternalAPIUserForAccessTokenAndScope(token string, scope string) (*models.ExternalAPIUser, error)
|
||||
GetModeratorUsers() []*models.User
|
||||
GetUserByID(id string) *models.User
|
||||
GetUserByToken(token string) *models.User
|
||||
InsertExternalAPIUser(token string, name string, color int, scopes []string) error
|
||||
IsDisplayNameAvailable(displayName string) (bool, error)
|
||||
SetAccessTokenToOwner(token, userID string) error
|
||||
SetEnabled(userID string, enabled bool) error
|
||||
SetModerator(userID string, isModerator bool) error
|
||||
SetUserAsAuthenticated(userID string) error
|
||||
HasValidScopes(scopes []string) bool
|
||||
GetUserByAuth(authToken string, authType models.AuthType) *models.User
|
||||
AddAuth(userID, authToken string, authType models.AuthType) error
|
||||
SetExternalAPIUserAccessTokenAsUsed(token string) error
|
||||
GetUsersCount() int
|
||||
}
|
||||
|
||||
type SqlUserRepository struct {
|
||||
datastore *data.Datastore
|
||||
}
|
||||
|
||||
// NOTE: This is temporary during the transition period.
|
||||
var temporaryGlobalInstance UserRepository
|
||||
|
||||
// Get will return the user repository.
|
||||
func Get() UserRepository {
|
||||
if temporaryGlobalInstance == nil {
|
||||
i := New(data.GetDatastore())
|
||||
temporaryGlobalInstance = i
|
||||
}
|
||||
return temporaryGlobalInstance
|
||||
}
|
||||
|
||||
// New will create a new instance of the UserRepository.
|
||||
func New(datastore *data.Datastore) UserRepository {
|
||||
r := SqlUserRepository{
|
||||
datastore: datastore,
|
||||
}
|
||||
|
||||
return &r
|
||||
}
|
||||
|
||||
// CreateAnonymousUser will create a new anonymous user with the provided display name.
|
||||
func (r *SqlUserRepository) CreateAnonymousUser(displayName string) (*models.User, string, error) {
|
||||
if displayName == "" {
|
||||
return nil, "", errors.New("display name cannot be empty")
|
||||
}
|
||||
|
||||
// Try to assign a name that was requested.
|
||||
// If name isn't available then generate a random one.
|
||||
if available, _ := r.IsDisplayNameAvailable(displayName); !available {
|
||||
rand, _ := utils.GenerateRandomString(3)
|
||||
displayName += rand
|
||||
}
|
||||
|
||||
displayColor := utils.GenerateRandomDisplayColor(config.MaxUserColor)
|
||||
|
||||
id := shortid.MustGenerate()
|
||||
user := &models.User{
|
||||
ID: id,
|
||||
DisplayName: displayName,
|
||||
DisplayColor: displayColor,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Create new user.
|
||||
if err := r.create(user); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// Assign it an access token.
|
||||
accessToken, err := utils.GenerateAccessToken()
|
||||
if err != nil {
|
||||
log.Errorln("Unable to create access token for new user")
|
||||
return nil, "", err
|
||||
}
|
||||
if err := r.addAccessTokenForUser(accessToken, id); err != nil {
|
||||
return nil, "", errors.Wrap(err, "unable to save access token for new user")
|
||||
}
|
||||
|
||||
return user, accessToken, nil
|
||||
}
|
||||
|
||||
// IsDisplayNameAvailable will check if the proposed name is available for use.
|
||||
func (r *SqlUserRepository) IsDisplayNameAvailable(displayName string) (bool, error) {
|
||||
if available, err := r.datastore.GetQueries().IsDisplayNameAvailable(context.Background(), displayName); err != nil {
|
||||
return false, errors.Wrap(err, "unable to check if display name is available")
|
||||
} else if available != 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// ChangeUsername will change the user associated to userID from one display name to another.
|
||||
func (r *SqlUserRepository) ChangeUsername(userID string, username string) error {
|
||||
r.datastore.DbLock.Lock()
|
||||
defer r.datastore.DbLock.Unlock()
|
||||
|
||||
if err := r.datastore.GetQueries().ChangeDisplayName(context.Background(), db.ChangeDisplayNameParams{
|
||||
DisplayName: username,
|
||||
ID: userID,
|
||||
PreviousNames: sql.NullString{String: fmt.Sprintf(",%s", username), Valid: true},
|
||||
NamechangedAt: sql.NullTime{Time: time.Now(), Valid: true},
|
||||
}); err != nil {
|
||||
return errors.Wrap(err, "unable to change display name")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ChangeUserColor will change the user associated to userID from one display name to another.
|
||||
func (r *SqlUserRepository) ChangeUserColor(userID string, color int) error {
|
||||
r.datastore.DbLock.Lock()
|
||||
defer r.datastore.DbLock.Unlock()
|
||||
|
||||
if err := r.datastore.GetQueries().ChangeDisplayColor(context.Background(), db.ChangeDisplayColorParams{
|
||||
DisplayColor: color,
|
||||
ID: userID,
|
||||
}); err != nil {
|
||||
return errors.Wrap(err, "unable to change display color")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SqlUserRepository) addAccessTokenForUser(accessToken, userID string) error {
|
||||
return r.datastore.GetQueries().AddAccessTokenForUser(context.Background(), db.AddAccessTokenForUserParams{
|
||||
Token: accessToken,
|
||||
UserID: userID,
|
||||
})
|
||||
}
|
||||
|
||||
func (r *SqlUserRepository) create(user *models.User) error {
|
||||
r.datastore.DbLock.Lock()
|
||||
defer r.datastore.DbLock.Unlock()
|
||||
|
||||
tx, err := r.datastore.DB.Begin()
|
||||
if err != nil {
|
||||
log.Debugln(err)
|
||||
}
|
||||
defer func() {
|
||||
_ = tx.Rollback()
|
||||
}()
|
||||
|
||||
stmt, err := tx.Prepare("INSERT INTO users(id, display_name, display_color, previous_names, created_at) values(?, ?, ?, ?, ?)")
|
||||
if err != nil {
|
||||
log.Debugln(err)
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
_, err = stmt.Exec(user.ID, user.DisplayName, user.DisplayColor, user.DisplayName, user.CreatedAt)
|
||||
if err != nil {
|
||||
log.Errorln("error creating new user", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// SetEnabled will set the enabled status of a single user by ID.
|
||||
func (r *SqlUserRepository) SetEnabled(userID string, enabled bool) error {
|
||||
r.datastore.DbLock.Lock()
|
||||
defer r.datastore.DbLock.Unlock()
|
||||
|
||||
tx, err := r.datastore.DB.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer tx.Rollback() //nolint
|
||||
|
||||
var stmt *sql.Stmt
|
||||
if !enabled {
|
||||
stmt, err = tx.Prepare("UPDATE users SET disabled_at=DATETIME('now', 'localtime') WHERE id IS ?")
|
||||
} else {
|
||||
stmt, err = tx.Prepare("UPDATE users SET disabled_at=null WHERE id IS ?")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer stmt.Close()
|
||||
|
||||
if _, err := stmt.Exec(userID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// GetUserByToken will return a user by an access token.
|
||||
func (r *SqlUserRepository) GetUserByToken(token string) *models.User {
|
||||
u, err := r.datastore.GetQueries().GetUserByAccessToken(context.Background(), token)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var scopes []string
|
||||
if u.Scopes.Valid {
|
||||
scopes = strings.Split(u.Scopes.String, ",")
|
||||
}
|
||||
|
||||
var disabledAt *time.Time
|
||||
if u.DisabledAt.Valid {
|
||||
disabledAt = &u.DisabledAt.Time
|
||||
}
|
||||
|
||||
var authenticatedAt *time.Time
|
||||
if u.AuthenticatedAt.Valid {
|
||||
authenticatedAt = &u.AuthenticatedAt.Time
|
||||
}
|
||||
|
||||
return &models.User{
|
||||
ID: u.ID,
|
||||
DisplayName: u.DisplayName,
|
||||
DisplayColor: int(u.DisplayColor),
|
||||
CreatedAt: u.CreatedAt.Time,
|
||||
DisabledAt: disabledAt,
|
||||
PreviousNames: strings.Split(u.PreviousNames.String, ","),
|
||||
NameChangedAt: &u.NamechangedAt.Time,
|
||||
AuthenticatedAt: authenticatedAt,
|
||||
Authenticated: authenticatedAt != nil,
|
||||
Scopes: scopes,
|
||||
}
|
||||
}
|
||||
|
||||
// SetAccessTokenToOwner will reassign an access token to be owned by a
|
||||
// different user. Used for logging in with external auth.
|
||||
func (r *SqlUserRepository) SetAccessTokenToOwner(token, userID string) error {
|
||||
return r.datastore.GetQueries().SetAccessTokenToOwner(context.Background(), db.SetAccessTokenToOwnerParams{
|
||||
UserID: userID,
|
||||
Token: token,
|
||||
})
|
||||
}
|
||||
|
||||
// SetUserAsAuthenticated will mark that a user has been authenticated
|
||||
// in some way.
|
||||
func (r *SqlUserRepository) SetUserAsAuthenticated(userID string) error {
|
||||
return errors.Wrap(r.datastore.GetQueries().SetUserAsAuthenticated(context.Background(), userID), "unable to set user as authenticated")
|
||||
}
|
||||
|
||||
// AddAuth will add an external authentication token and type for a user.
|
||||
func (r *SqlUserRepository) AddAuth(userID, authToken string, authType models.AuthType) error {
|
||||
return r.datastore.GetQueries().AddAuthForUser(context.Background(), db.AddAuthForUserParams{
|
||||
UserID: userID,
|
||||
Token: authToken,
|
||||
Type: string(authType),
|
||||
})
|
||||
}
|
||||
|
||||
// GetUserByAuth will return an existing user given auth details if a user
|
||||
// has previously authenticated with that method.
|
||||
func (r *SqlUserRepository) GetUserByAuth(authToken string, authType models.AuthType) *models.User {
|
||||
u, err := r.datastore.GetQueries().GetUserByAuth(context.Background(), db.GetUserByAuthParams{
|
||||
Token: authToken,
|
||||
Type: string(authType),
|
||||
})
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var scopes []string
|
||||
if u.Scopes.Valid {
|
||||
scopes = strings.Split(u.Scopes.String, ",")
|
||||
}
|
||||
|
||||
return &models.User{
|
||||
ID: u.ID,
|
||||
DisplayName: u.DisplayName,
|
||||
DisplayColor: int(u.DisplayColor),
|
||||
CreatedAt: u.CreatedAt.Time,
|
||||
DisabledAt: &u.DisabledAt.Time,
|
||||
PreviousNames: strings.Split(u.PreviousNames.String, ","),
|
||||
NameChangedAt: &u.NamechangedAt.Time,
|
||||
AuthenticatedAt: &u.AuthenticatedAt.Time,
|
||||
Scopes: scopes,
|
||||
}
|
||||
}
|
||||
|
||||
// SetModerator will add or remove moderator status for a single user by ID.
|
||||
func (r *SqlUserRepository) SetModerator(userID string, isModerator bool) error {
|
||||
if isModerator {
|
||||
return r.addScopeToUser(userID, models.ModeratorScopeKey)
|
||||
}
|
||||
|
||||
return r.removeScopeFromUser(userID, models.ModeratorScopeKey)
|
||||
}
|
||||
|
||||
func (r *SqlUserRepository) addScopeToUser(userID string, scope string) error {
|
||||
u := r.GetUserByID(userID)
|
||||
if u == nil {
|
||||
return errors.New("user not found when modifying scope")
|
||||
}
|
||||
|
||||
scopesString := u.Scopes
|
||||
scopes := utils.StringSliceToMap(scopesString)
|
||||
scopes[scope] = true
|
||||
|
||||
scopesSlice := utils.StringMapKeys(scopes)
|
||||
|
||||
return r.setScopesOnUser(userID, scopesSlice)
|
||||
}
|
||||
|
||||
func (r *SqlUserRepository) removeScopeFromUser(userID string, scope string) error {
|
||||
u := r.GetUserByID(userID)
|
||||
scopesString := u.Scopes
|
||||
scopes := utils.StringSliceToMap(scopesString)
|
||||
delete(scopes, scope)
|
||||
|
||||
scopesSlice := utils.StringMapKeys(scopes)
|
||||
|
||||
return r.setScopesOnUser(userID, scopesSlice)
|
||||
}
|
||||
|
||||
func (r *SqlUserRepository) setScopesOnUser(userID string, scopes []string) error {
|
||||
r.datastore.DbLock.Lock()
|
||||
defer r.datastore.DbLock.Unlock()
|
||||
|
||||
tx, err := r.datastore.DB.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer tx.Rollback() //nolint
|
||||
|
||||
scopesSliceString := strings.TrimSpace(strings.Join(scopes, ","))
|
||||
stmt, err := tx.Prepare("UPDATE users SET scopes=? WHERE id IS ?")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer stmt.Close()
|
||||
|
||||
var val *string
|
||||
if scopesSliceString == "" {
|
||||
val = nil
|
||||
} else {
|
||||
val = &scopesSliceString
|
||||
}
|
||||
|
||||
if _, err := stmt.Exec(val, userID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// GetUserByID will return a user by a user ID.
|
||||
func (r *SqlUserRepository) GetUserByID(id string) *models.User {
|
||||
r.datastore.DbLock.Lock()
|
||||
defer r.datastore.DbLock.Unlock()
|
||||
|
||||
query := "SELECT id, display_name, display_color, created_at, disabled_at, previous_names, namechanged_at, scopes FROM users WHERE id = ?"
|
||||
row := r.datastore.DB.QueryRow(query, id)
|
||||
if row == nil {
|
||||
log.Errorln(row)
|
||||
return nil
|
||||
}
|
||||
return r.getUserFromRow(row)
|
||||
}
|
||||
|
||||
// GetDisabledUsers will return back all the currently disabled users that are not API users.
|
||||
func (r *SqlUserRepository) GetDisabledUsers() []*models.User {
|
||||
query := "SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at FROM users WHERE disabled_at IS NOT NULL AND type IS NOT 'API'"
|
||||
|
||||
rows, err := r.datastore.DB.Query(query)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
return nil
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
users := r.getUsersFromRows(rows)
|
||||
|
||||
sort.Slice(users, func(i, j int) bool {
|
||||
return users[i].DisabledAt.Before(*users[j].DisabledAt)
|
||||
})
|
||||
|
||||
return users
|
||||
}
|
||||
|
||||
// GetModeratorUsers will return a list of users with moderator access.
|
||||
func (r *SqlUserRepository) GetModeratorUsers() []*models.User {
|
||||
query := `SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at FROM (
|
||||
WITH RECURSIVE split(id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at, scope, rest) AS (
|
||||
SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at, '', scopes || ',' FROM users
|
||||
UNION ALL
|
||||
SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at,
|
||||
substr(rest, 0, instr(rest, ',')),
|
||||
substr(rest, instr(rest, ',')+1)
|
||||
FROM split
|
||||
WHERE rest <> '')
|
||||
SELECT id, display_name, scopes, display_color, created_at, disabled_at, previous_names, namechanged_at, scope
|
||||
FROM split
|
||||
WHERE scope <> ''
|
||||
ORDER BY created_at
|
||||
) AS token WHERE token.scope = ?`
|
||||
|
||||
rows, err := r.datastore.DB.Query(query, models.ModeratorScopeKey)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
return nil
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
users := r.getUsersFromRows(rows)
|
||||
|
||||
return users
|
||||
}
|
||||
|
||||
func (r *SqlUserRepository) getUsersFromRows(rows *sql.Rows) []*models.User {
|
||||
users := make([]*models.User, 0)
|
||||
|
||||
for rows.Next() {
|
||||
var id string
|
||||
var displayName string
|
||||
var displayColor int
|
||||
var createdAt time.Time
|
||||
var disabledAt *time.Time
|
||||
var previousUsernames string
|
||||
var userNameChangedAt *time.Time
|
||||
var scopesString *string
|
||||
|
||||
if err := rows.Scan(&id, &displayName, &scopesString, &displayColor, &createdAt, &disabledAt, &previousUsernames, &userNameChangedAt); err != nil {
|
||||
log.Errorln("error creating collection of users from results", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
var scopes []string
|
||||
if scopesString != nil {
|
||||
scopes = strings.Split(*scopesString, ",")
|
||||
}
|
||||
|
||||
user := &models.User{
|
||||
ID: id,
|
||||
DisplayName: displayName,
|
||||
DisplayColor: displayColor,
|
||||
CreatedAt: createdAt,
|
||||
DisabledAt: disabledAt,
|
||||
PreviousNames: strings.Split(previousUsernames, ","),
|
||||
NameChangedAt: userNameChangedAt,
|
||||
Scopes: scopes,
|
||||
}
|
||||
users = append(users, user)
|
||||
}
|
||||
|
||||
sort.Slice(users, func(i, j int) bool {
|
||||
return users[i].CreatedAt.Before(users[j].CreatedAt)
|
||||
})
|
||||
|
||||
return users
|
||||
}
|
||||
|
||||
func (r *SqlUserRepository) getUserFromRow(row *sql.Row) *models.User {
|
||||
var id string
|
||||
var displayName string
|
||||
var displayColor int
|
||||
var createdAt time.Time
|
||||
var disabledAt *time.Time
|
||||
var previousUsernames string
|
||||
var userNameChangedAt *time.Time
|
||||
var scopesString *string
|
||||
|
||||
if err := row.Scan(&id, &displayName, &displayColor, &createdAt, &disabledAt, &previousUsernames, &userNameChangedAt, &scopesString); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var scopes []string
|
||||
if scopesString != nil {
|
||||
scopes = strings.Split(*scopesString, ",")
|
||||
}
|
||||
|
||||
return &models.User{
|
||||
ID: id,
|
||||
DisplayName: displayName,
|
||||
DisplayColor: displayColor,
|
||||
CreatedAt: createdAt,
|
||||
DisabledAt: disabledAt,
|
||||
PreviousNames: strings.Split(previousUsernames, ","),
|
||||
NameChangedAt: userNameChangedAt,
|
||||
Scopes: scopes,
|
||||
}
|
||||
}
|
||||
|
||||
// InsertExternalAPIUser will add a new API user to the database.
|
||||
func (r *SqlUserRepository) InsertExternalAPIUser(token string, name string, color int, scopes []string) error {
|
||||
log.Traceln("Adding new API user")
|
||||
|
||||
r.datastore.DbLock.Lock()
|
||||
defer r.datastore.DbLock.Unlock()
|
||||
|
||||
scopesString := strings.Join(scopes, ",")
|
||||
id := shortid.MustGenerate()
|
||||
|
||||
tx, err := r.datastore.DB.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stmt, err := tx.Prepare("INSERT INTO users(id, display_name, display_color, scopes, type, previous_names) values(?, ?, ?, ?, ?, ?)")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
if _, err = stmt.Exec(id, name, color, scopesString, "API", name); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := r.addAccessTokenForUser(token, id); err != nil {
|
||||
return errors.Wrap(err, "unable to save access token for new external api user")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteExternalAPIUser will delete a token from the database.
|
||||
func (r *SqlUserRepository) DeleteExternalAPIUser(token string) error {
|
||||
log.Traceln("Deleting access token")
|
||||
|
||||
r.datastore.DbLock.Lock()
|
||||
defer r.datastore.DbLock.Unlock()
|
||||
|
||||
tx, err := r.datastore.DB.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stmt, err := tx.Prepare("UPDATE users SET disabled_at = CURRENT_TIMESTAMP WHERE id = (SELECT user_id FROM user_access_tokens WHERE token = ?)")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
result, err := stmt.Exec(token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rowsDeleted, _ := result.RowsAffected(); rowsDeleted == 0 {
|
||||
tx.Rollback() //nolint
|
||||
return errors.New(token + " not found")
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetExternalAPIUserForAccessTokenAndScope will determine if a specific token has access to perform a scoped action.
|
||||
func (r *SqlUserRepository) GetExternalAPIUserForAccessTokenAndScope(token string, scope string) (*models.ExternalAPIUser, error) {
|
||||
// This will split the scopes from comma separated to individual rows
|
||||
// so we can efficiently find if a token supports a single scope.
|
||||
// This is SQLite specific, so if we ever support other database
|
||||
// backends we need to support other methods.
|
||||
query := `SELECT
|
||||
id,
|
||||
scopes,
|
||||
display_name,
|
||||
display_color,
|
||||
created_at,
|
||||
last_used
|
||||
FROM
|
||||
user_access_tokens
|
||||
INNER JOIN (
|
||||
WITH RECURSIVE split(
|
||||
id,
|
||||
scopes,
|
||||
display_name,
|
||||
display_color,
|
||||
created_at,
|
||||
last_used,
|
||||
disabled_at,
|
||||
scope,
|
||||
rest
|
||||
) AS (
|
||||
SELECT
|
||||
id,
|
||||
scopes,
|
||||
display_name,
|
||||
display_color,
|
||||
created_at,
|
||||
last_used,
|
||||
disabled_at,
|
||||
'',
|
||||
scopes || ','
|
||||
FROM
|
||||
users AS u
|
||||
UNION ALL
|
||||
SELECT
|
||||
id,
|
||||
scopes,
|
||||
display_name,
|
||||
display_color,
|
||||
created_at,
|
||||
last_used,
|
||||
disabled_at,
|
||||
substr(rest, 0, instr(rest, ',')),
|
||||
substr(rest, instr(rest, ',') + 1)
|
||||
FROM
|
||||
split
|
||||
WHERE
|
||||
rest <> ''
|
||||
)
|
||||
SELECT
|
||||
id,
|
||||
display_name,
|
||||
display_color,
|
||||
created_at,
|
||||
last_used,
|
||||
disabled_at,
|
||||
scopes,
|
||||
scope
|
||||
FROM
|
||||
split
|
||||
WHERE
|
||||
scope <> ''
|
||||
) ON user_access_tokens.user_id = id
|
||||
WHERE
|
||||
disabled_at IS NULL
|
||||
AND token = ?
|
||||
AND scope = ?;`
|
||||
|
||||
row := r.datastore.DB.QueryRow(query, token, scope)
|
||||
integration, err := r.makeExternalAPIUserFromRow(row)
|
||||
|
||||
return integration, err
|
||||
}
|
||||
|
||||
// GetIntegrationNameForAccessToken will return the integration name associated with a specific access token.
|
||||
func (r *SqlUserRepository) GetIntegrationNameForAccessToken(token string) *string {
|
||||
name, err := r.datastore.GetQueries().GetUserDisplayNameByToken(context.Background(), token)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &name
|
||||
}
|
||||
|
||||
// GetExternalAPIUser will return all API users with access tokens.
|
||||
func (r *SqlUserRepository) GetExternalAPIUser() ([]models.ExternalAPIUser, error) { //nolint
|
||||
query := "SELECT id, token, display_name, display_color, scopes, created_at, last_used FROM users, user_access_tokens WHERE user_access_tokens.user_id = id AND type IS 'API' AND disabled_at IS NULL"
|
||||
|
||||
rows, err := r.datastore.DB.Query(query)
|
||||
if err != nil {
|
||||
return []models.ExternalAPIUser{}, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
integrations, err := r.makeExternalAPIUsersFromRows(rows)
|
||||
|
||||
return integrations, err
|
||||
}
|
||||
|
||||
// SetExternalAPIUserAccessTokenAsUsed will update the last used timestamp for a token.
|
||||
func (r *SqlUserRepository) SetExternalAPIUserAccessTokenAsUsed(token string) error {
|
||||
tx, err := r.datastore.DB.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stmt, err := tx.Prepare("UPDATE users SET last_used = CURRENT_TIMESTAMP WHERE id = (SELECT user_id FROM user_access_tokens WHERE token = ?)")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
if _, err := stmt.Exec(token); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SqlUserRepository) makeExternalAPIUserFromRow(row *sql.Row) (*models.ExternalAPIUser, error) {
|
||||
var id string
|
||||
var displayName string
|
||||
var displayColor int
|
||||
var scopes string
|
||||
var createdAt time.Time
|
||||
var lastUsedAt *time.Time
|
||||
|
||||
err := row.Scan(&id, &scopes, &displayName, &displayColor, &createdAt, &lastUsedAt)
|
||||
if err != nil {
|
||||
log.Debugln("unable to convert row to api user", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
integration := models.ExternalAPIUser{
|
||||
ID: id,
|
||||
DisplayName: displayName,
|
||||
DisplayColor: displayColor,
|
||||
CreatedAt: createdAt,
|
||||
Scopes: strings.Split(scopes, ","),
|
||||
LastUsedAt: lastUsedAt,
|
||||
}
|
||||
|
||||
return &integration, nil
|
||||
}
|
||||
|
||||
func (r *SqlUserRepository) makeExternalAPIUsersFromRows(rows *sql.Rows) ([]models.ExternalAPIUser, error) {
|
||||
integrations := make([]models.ExternalAPIUser, 0)
|
||||
|
||||
for rows.Next() {
|
||||
var id string
|
||||
var accessToken string
|
||||
var displayName string
|
||||
var displayColor int
|
||||
var scopes string
|
||||
var createdAt time.Time
|
||||
var lastUsedAt *time.Time
|
||||
|
||||
err := rows.Scan(&id, &accessToken, &displayName, &displayColor, &scopes, &createdAt, &lastUsedAt)
|
||||
if err != nil {
|
||||
log.Errorln(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
integration := models.ExternalAPIUser{
|
||||
ID: id,
|
||||
AccessToken: accessToken,
|
||||
DisplayName: displayName,
|
||||
DisplayColor: displayColor,
|
||||
CreatedAt: createdAt,
|
||||
Scopes: strings.Split(scopes, ","),
|
||||
LastUsedAt: lastUsedAt,
|
||||
IsBot: true,
|
||||
}
|
||||
integrations = append(integrations, integration)
|
||||
}
|
||||
|
||||
return integrations, nil
|
||||
}
|
||||
|
||||
// HasValidScopes will verify that all the scopes provided are valid.
|
||||
func (r *SqlUserRepository) HasValidScopes(scopes []string) bool {
|
||||
// For a scope to be seen as "valid" it must live in this slice.
|
||||
validAccessTokenScopes := []string{
|
||||
models.ScopeCanSendChatMessages,
|
||||
models.ScopeCanSendSystemMessages,
|
||||
models.ScopeHasAdminAccess,
|
||||
}
|
||||
|
||||
for _, scope := range scopes {
|
||||
_, foundInSlice := utils.FindInSlice(validAccessTokenScopes, scope)
|
||||
if !foundInSlice {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// GetUsersCount will return the number of users in the database.
|
||||
func (r *SqlUserRepository) GetUsersCount() int {
|
||||
query := `SELECT COUNT(*) FROM users`
|
||||
rows, err := r.datastore.DB.Query(query)
|
||||
if err != nil || rows.Err() != nil {
|
||||
return 0
|
||||
}
|
||||
defer rows.Close()
|
||||
var count int
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(&count); err != nil {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
8
redocly.yaml
Normal file
8
redocly.yaml
Normal file
|
@ -0,0 +1,8 @@
|
|||
extends:
|
||||
- recommended
|
||||
|
||||
rules:
|
||||
operation-summary: off
|
||||
security-defined: off
|
||||
operation-4xx-response: off
|
||||
no-server-example.com: off
|
|
@ -18,6 +18,13 @@
|
|||
"major": {
|
||||
"dependencyDashboardApproval": true
|
||||
},
|
||||
"dockerfile": {
|
||||
"fileMatch": [
|
||||
"(^|/)Earthfile$",
|
||||
"(^|/|\\.)Dockerfile$",
|
||||
"(^|/)Dockerfile[^/]*$"
|
||||
]
|
||||
},
|
||||
"packageRules": [
|
||||
{
|
||||
"description": "Automatically merge minor and patch-level updates",
|
||||
|
|
4413
spec/openapi.yaml
4413
spec/openapi.yaml
File diff suppressed because it is too large
Load diff
4
static/web/404.html
vendored
4
static/web/404.html
vendored
File diff suppressed because one or more lines are too long
4
static/web/404/index.html
vendored
4
static/web/404/index.html
vendored
File diff suppressed because one or more lines are too long
|
@ -1 +1 @@
|
|||
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[9349],{47035:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"}}]},name:"check-circle",theme:"filled"}},89349:function(e,t,r){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var n,a=(n=r(20487))&&n.__esModule?n:{default:n};t.default=a,e.exports=a},20487:function(e,t,r){var n=r(64836),a=r(18698);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var u=n(r(42122)),f=function(e,t){if(e&&e.__esModule)return e;if(null===e||"object"!=a(e)&&"function"!=typeof e)return{default:e};var r=c(void 0);if(r&&r.has(e))return r.get(e);var n={__proto__:null},u=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var f in e)if("default"!==f&&({}).hasOwnProperty.call(e,f)){var l=u?Object.getOwnPropertyDescriptor(e,f):null;l&&(l.get||l.set)?Object.defineProperty(n,f,l):n[f]=e[f]}return n.default=e,r&&r.set(e,n),n}(r(67294)),l=n(r(47035)),o=n(r(3908));function c(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,r=new WeakMap;return(c=function(e){return e?r:t})(e)}var i=f.forwardRef(function(e,t){return f.createElement(o.default,(0,u.default)((0,u.default)({},e),{},{ref:t,icon:l.default}))});t.default=i}}]);
|
||||
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[1031],{94210:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"}}]},name:"check-circle",theme:"filled"}},91031:function(e,t,r){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var n,a=(n=r(18522))&&n.__esModule?n:{default:n};t.default=a,e.exports=a},18522:function(e,t,r){var n=r(64836),a=r(18698);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var u=n(r(42122)),f=function(e,t){if(e&&e.__esModule)return e;if(null===e||"object"!=a(e)&&"function"!=typeof e)return{default:e};var r=c(void 0);if(r&&r.has(e))return r.get(e);var n={__proto__:null},u=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var f in e)if("default"!==f&&({}).hasOwnProperty.call(e,f)){var l=u?Object.getOwnPropertyDescriptor(e,f):null;l&&(l.get||l.set)?Object.defineProperty(n,f,l):n[f]=e[f]}return n.default=e,r&&r.set(e,n),n}(r(67294)),l=n(r(94210)),o=n(r(3247));function c(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,r=new WeakMap;return(c=function(e){return e?r:t})(e)}var i=f.forwardRef(function(e,t){return f.createElement(o.default,(0,u.default)((0,u.default)({},e),{},{ref:t,icon:l.default}))});t.default=i}}]);
|
1
static/web/_next/static/chunks/1071.a77505a640429fb1.js
vendored
Normal file
1
static/web/_next/static/chunks/1071.a77505a640429fb1.js
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[1071],{1399:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default={icon:function(e,t){return{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm288.5 682.8L277.7 224C258 240 240 258 224 277.7l522.8 522.8C682.8 852.7 601 884 512 884c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372c0 89-31.3 170.8-83.5 234.8z",fill:e}},{tag:"path",attrs:{d:"M512 140c-205.4 0-372 166.6-372 372s166.6 372 372 372c89 0 170.8-31.3 234.8-83.5L224 277.7c16-19.7 34-37.7 53.7-53.7l522.8 522.8C852.7 682.8 884 601 884 512c0-205.4-166.6-372-372-372z",fill:t}}]}},name:"stop",theme:"twotone"}},71071:function(e,t,r){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var n,a=(n=r(98678))&&n.__esModule?n:{default:n};t.default=a,e.exports=a},98678:function(e,t,r){var n=r(64836),a=r(18698);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var u=n(r(42122)),f=function(e,t){if(e&&e.__esModule)return e;if(null===e||"object"!=a(e)&&"function"!=typeof e)return{default:e};var r=c(void 0);if(r&&r.has(e))return r.get(e);var n={__proto__:null},u=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var f in e)if("default"!==f&&({}).hasOwnProperty.call(e,f)){var l=u?Object.getOwnPropertyDescriptor(e,f):null;l&&(l.get||l.set)?Object.defineProperty(n,f,l):n[f]=e[f]}return n.default=e,r&&r.set(e,n),n}(r(67294)),l=n(r(1399)),o=n(r(3247));function c(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,r=new WeakMap;return(c=function(e){return e?r:t})(e)}var i=f.forwardRef(function(e,t){return f.createElement(o.default,(0,u.default)((0,u.default)({},e),{},{ref:t,icon:l.default}))});t.default=i}}]);
|
|
@ -1 +0,0 @@
|
|||
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[108],{65089:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M832 464h-68V240c0-70.7-57.3-128-128-128H388c-70.7 0-128 57.3-128 128v224h-68c-17.7 0-32 14.3-32 32v384c0 17.7 14.3 32 32 32h640c17.7 0 32-14.3 32-32V496c0-17.7-14.3-32-32-32zM332 240c0-30.9 25.1-56 56-56h248c30.9 0 56 25.1 56 56v224H332V240zm460 600H232V536h560v304zM484 701v53c0 4.4 3.6 8 8 8h40c4.4 0 8-3.6 8-8v-53a48.01 48.01 0 10-56 0z"}}]},name:"lock",theme:"outlined"}},30108:function(e,t,r){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var n,u=(n=r(34811))&&n.__esModule?n:{default:n};t.default=u,e.exports=u},34811:function(e,t,r){var n=r(64836),u=r(18698);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a=n(r(42122)),f=function(e,t){if(e&&e.__esModule)return e;if(null===e||"object"!=u(e)&&"function"!=typeof e)return{default:e};var r=l(void 0);if(r&&r.has(e))return r.get(e);var n={__proto__:null},a=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var f in e)if("default"!==f&&({}).hasOwnProperty.call(e,f)){var o=a?Object.getOwnPropertyDescriptor(e,f):null;o&&(o.get||o.set)?Object.defineProperty(n,f,o):n[f]=e[f]}return n.default=e,r&&r.set(e,n),n}(r(67294)),o=n(r(65089)),c=n(r(3908));function l(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,r=new WeakMap;return(l=function(e){return e?r:t})(e)}var i=f.forwardRef(function(e,t){return f.createElement(c.default,(0,a.default)((0,a.default)({},e),{},{ref:t,icon:o.default}))});t.default=i}}]);
|
1
static/web/_next/static/chunks/1115-3cc893267bfc23c9.js
vendored
Normal file
1
static/web/_next/static/chunks/1115-3cc893267bfc23c9.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
static/web/_next/static/chunks/1117-9ad7734a1a33285f.js
vendored
Normal file
1
static/web/_next/static/chunks/1117-9ad7734a1a33285f.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -1 +1 @@
|
|||
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[784],{49281:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M942.2 486.2Q889.47 375.11 816.7 305l-50.88 50.88C807.31 395.53 843.45 447.4 874.7 512 791.5 684.2 673.4 766 512 766q-72.67 0-133.87-22.38L323 798.75Q408 838 512 838q288.3 0 430.2-300.3a60.29 60.29 0 000-51.5zm-63.57-320.64L836 122.88a8 8 0 00-11.32 0L715.31 232.2Q624.86 186 512 186q-288.3 0-430.2 300.3a60.3 60.3 0 000 51.5q56.69 119.4 136.5 191.41L112.48 835a8 8 0 000 11.31L155.17 889a8 8 0 0011.31 0l712.15-712.12a8 8 0 000-11.32zM149.3 512C232.6 339.8 350.7 258 512 258c54.54 0 104.13 9.36 149.12 28.39l-70.3 70.3a176 176 0 00-238.13 238.13l-83.42 83.42C223.1 637.49 183.3 582.28 149.3 512zm246.7 0a112.11 112.11 0 01146.2-106.69L401.31 546.2A112 112 0 01396 512z"}},{tag:"path",attrs:{d:"M508 624c-3.46 0-6.87-.16-10.25-.47l-52.82 52.82a176.09 176.09 0 00227.42-227.42l-52.82 52.82c.31 3.38.47 6.79.47 10.25a111.94 111.94 0 01-112 112z"}}]},name:"eye-invisible",theme:"outlined"}},40784:function(e,t,a){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r,n=(r=a(27184))&&r.__esModule?r:{default:r};t.default=n,e.exports=n},27184:function(e,t,a){var r=a(64836),n=a(18698);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var u=r(a(42122)),l=function(e,t){if(e&&e.__esModule)return e;if(null===e||"object"!=n(e)&&"function"!=typeof e)return{default:e};var a=i(void 0);if(a&&a.has(e))return a.get(e);var r={__proto__:null},u=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var l in e)if("default"!==l&&({}).hasOwnProperty.call(e,l)){var f=u?Object.getOwnPropertyDescriptor(e,l):null;f&&(f.get||f.set)?Object.defineProperty(r,l,f):r[l]=e[l]}return r.default=e,a&&a.set(e,r),r}(a(67294)),f=r(a(49281)),o=r(a(3908));function i(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,a=new WeakMap;return(i=function(e){return e?a:t})(e)}var c=l.forwardRef(function(e,t){return l.createElement(o.default,(0,u.default)((0,u.default)({},e),{},{ref:t,icon:f.default}))});t.default=c}}]);
|
||||
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[112],{34662:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M942.2 486.2Q889.47 375.11 816.7 305l-50.88 50.88C807.31 395.53 843.45 447.4 874.7 512 791.5 684.2 673.4 766 512 766q-72.67 0-133.87-22.38L323 798.75Q408 838 512 838q288.3 0 430.2-300.3a60.29 60.29 0 000-51.5zm-63.57-320.64L836 122.88a8 8 0 00-11.32 0L715.31 232.2Q624.86 186 512 186q-288.3 0-430.2 300.3a60.3 60.3 0 000 51.5q56.69 119.4 136.5 191.41L112.48 835a8 8 0 000 11.31L155.17 889a8 8 0 0011.31 0l712.15-712.12a8 8 0 000-11.32zM149.3 512C232.6 339.8 350.7 258 512 258c54.54 0 104.13 9.36 149.12 28.39l-70.3 70.3a176 176 0 00-238.13 238.13l-83.42 83.42C223.1 637.49 183.3 582.28 149.3 512zm246.7 0a112.11 112.11 0 01146.2-106.69L401.31 546.2A112 112 0 01396 512z"}},{tag:"path",attrs:{d:"M508 624c-3.46 0-6.87-.16-10.25-.47l-52.82 52.82a176.09 176.09 0 00227.42-227.42l-52.82 52.82c.31 3.38.47 6.79.47 10.25a111.94 111.94 0 01-112 112z"}}]},name:"eye-invisible",theme:"outlined"}},80112:function(e,t,a){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var r,n=(r=a(45532))&&r.__esModule?r:{default:r};t.default=n,e.exports=n},45532:function(e,t,a){var r=a(64836),n=a(18698);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var u=r(a(42122)),l=function(e,t){if(e&&e.__esModule)return e;if(null===e||"object"!=n(e)&&"function"!=typeof e)return{default:e};var a=i(void 0);if(a&&a.has(e))return a.get(e);var r={__proto__:null},u=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var l in e)if("default"!==l&&({}).hasOwnProperty.call(e,l)){var f=u?Object.getOwnPropertyDescriptor(e,l):null;f&&(f.get||f.set)?Object.defineProperty(r,l,f):r[l]=e[l]}return r.default=e,a&&a.set(e,r),r}(a(67294)),f=r(a(34662)),o=r(a(3247));function i(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,a=new WeakMap;return(i=function(e){return e?a:t})(e)}var c=l.forwardRef(function(e,t){return l.createElement(o.default,(0,u.default)((0,u.default)({},e),{},{ref:t,icon:f.default}))});t.default=c}}]);
|
1
static/web/_next/static/chunks/1183-aa425286604b823d.js
vendored
Normal file
1
static/web/_next/static/chunks/1183-aa425286604b823d.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -1 +0,0 @@
|
|||
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[1189],{92262:function(e,t){Object.defineProperty(t,"__esModule",{value:!0}),t.default={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M881.7 187.4l-45.1-45.1a8.03 8.03 0 00-11.3 0L667.8 299.9l-54.7-54.7a7.94 7.94 0 00-13.5 4.7L576.1 439c-.6 5.2 3.7 9.5 8.9 8.9l189.2-23.5c6.6-.8 9.3-8.8 4.7-13.5l-54.7-54.7 157.6-157.6c3-3 3-8.1-.1-11.2zM439 576.1l-189.2 23.5c-6.6.8-9.3 8.9-4.7 13.5l54.7 54.7-157.5 157.5a8.03 8.03 0 000 11.3l45.1 45.1c3.1 3.1 8.2 3.1 11.3 0l157.6-157.6 54.7 54.7a7.94 7.94 0 0013.5-4.7L447.9 585a7.9 7.9 0 00-8.9-8.9z"}}]},name:"shrink",theme:"outlined"}},91189:function(e,t,r){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a,n=(a=r(85e3))&&a.__esModule?a:{default:a};t.default=n,e.exports=n},85e3:function(e,t,r){var a=r(64836),n=r(18698);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var u=a(r(42122)),l=function(e,t){if(e&&e.__esModule)return e;if(null===e||"object"!=n(e)&&"function"!=typeof e)return{default:e};var r=c(void 0);if(r&&r.has(e))return r.get(e);var a={__proto__:null},u=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var l in e)if("default"!==l&&({}).hasOwnProperty.call(e,l)){var f=u?Object.getOwnPropertyDescriptor(e,l):null;f&&(f.get||f.set)?Object.defineProperty(a,l,f):a[l]=e[l]}return a.default=e,r&&r.set(e,a),a}(r(67294)),f=a(r(92262)),o=a(r(3908));function c(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,r=new WeakMap;return(c=function(e){return e?r:t})(e)}var i=l.forwardRef(function(e,t){return l.createElement(o.default,(0,u.default)((0,u.default)({},e),{},{ref:t,icon:f.default}))});t.default=i}}]);
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue