Compare commits

..

1 Commits

Author SHA1 Message Date
Gabe Kangas cff76707f0
WIP user repository 2 years ago
  1. 23
      .github/PULL_REQUEST_TEMPLATE/atomic-refactor-pull-request-for-issue-2119.md
  2. 4
      .github/PULL_REQUEST_TEMPLATE/standard-pull-request.md
  3. 1
      .github/codeql/go.yml
  4. 4
      .github/codeql/javascript.yml
  5. 2
      .github/workflows/actions-lint.yml
  6. 2
      .github/workflows/auto-comment-on-label.yaml
  7. 4
      .github/workflows/automated-end-to-end-api.yaml
  8. 10
      .github/workflows/browser-testing.yml
  9. 2
      .github/workflows/build-storybook.yml
  10. 44
      .github/workflows/bundle-web.yml
  11. 2
      .github/workflows/chromatic.yml
  12. 9
      .github/workflows/codeql-analysis.yml
  13. 2
      .github/workflows/container-lint.yml
  14. 4
      .github/workflows/container.yaml
  15. 2
      .github/workflows/generate-api-documentation.yaml
  16. 8
      .github/workflows/go-lint.yml
  17. 28
      .github/workflows/go-tests.yaml
  18. 6
      .github/workflows/hls-tests.yml
  19. 185
      .github/workflows/javascript-format-build.yml
  20. 96
      .github/workflows/javascript-formatting.yml
  21. 4
      .github/workflows/javascript-tests.yml
  22. 6
      .github/workflows/screenshots.yml
  23. 4
      .github/workflows/shellcheck.yml
  24. 50
      .github/workflows/test-webapp-build.yaml
  25. 1
      .gitignore
  26. 4
      .golangci.yml
  27. 2
      Dockerfile
  28. 18
      Earthfile
  29. 6
      README.md
  30. 8
      activitypub/inbox/like.go
  31. 10
      activitypub/inbox/workerpool.go
  32. 2
      activitypub/outbox/outbox.go
  33. 8
      activitypub/webfinger/webfinger.go
  34. 9
      activitypub/workerpool/outbound.go
  35. 18
      auth/indieauth/client.go
  36. 2
      auth/indieauth/server.go
  37. 6
      auth/persistence.go
  38. 14
      build/web/bundleWeb.sh
  39. 2
      config/constants.go
  40. 2
      config/verifyInstall.go
  41. 2
      contrib/owncast_for_windows.md
  42. 6
      controllers/admin/chat.go
  43. 21
      controllers/admin/config.go
  44. 2
      controllers/admin/serverConfig.go
  45. 1
      controllers/chat.go
  46. 14
      controllers/config.go
  47. 3
      controllers/emoji.go
  48. 98
      controllers/index.go
  49. 4
      core/chat/chatclient.go
  50. 19
      core/chat/events.go
  51. 4
      core/chat/events/connectedClientInfo.go
  52. 131
      core/chat/events/events.go
  53. 4
      core/chat/events/eventtype.go
  54. 17
      core/chat/events/userPartEvent.go
  55. 17
      core/chat/messageRendering_test.go
  56. 3
      core/chat/persistence.go
  57. 78
      core/chat/server.go
  58. 3
      core/core.go
  59. 17
      core/data/config.go
  60. 2
      core/data/configEntry.go
  61. 6
      core/data/data_test.go
  62. 70
      core/data/emoji.go
  63. 10
      core/data/types.go
  64. 6
      core/rtmp/rtmp.go
  65. 12
      core/storageproviders/local.go
  66. 13
      core/storageproviders/rewriteLocalPlaylist.go
  67. 64
      core/storageproviders/s3Storage.go
  68. 3
      core/streamState.go
  69. 8
      core/transcoder/thumbnailGenerator.go
  70. 10
      core/transcoder/utils.go
  71. 311
      core/user/externalAPIUser.go
  72. 473
      core/user/user.go
  73. 10
      core/webhooks/chat.go
  74. 3
      core/webhooks/webhooks.go
  75. 7
      core/webhooks/webhooks_test.go
  76. 11
      core/webhooks/workerpool.go
  77. 2
      db/query.sql
  78. 4
      db/query.sql.go
  79. 14
      docs/api/index.html
  80. 2
      geoip/geoip.go
  81. 60
      go.mod
  82. 156
      go.sum
  83. 2
      logging/logging.go
  84. 6
      metrics/metrics.go
  85. 19
      models/externalAPIUser.go
  86. 7
      models/s3Storage.go
  87. 36
      models/user.go
  88. 8
      openapi.yaml
  89. 23
      router/middleware/auth.go
  90. 83
      static/metadata.html.tmpl
  91. 11
      static/static.go
  92. 6
      static/web/404.html
  93. 6
      static/web/404/index.html
  94. 1
      static/web/_next/static/7FO45oyNxons-CT00qbSN/_buildManifest.js
  95. 1
      static/web/_next/static/_EwXAWpD9Ghec1YlZX6x3/_buildManifest.js
  96. 0
      static/web/_next/static/_EwXAWpD9Ghec1YlZX6x3/_ssgManifest.js
  97. 1
      static/web/_next/static/chunks/1008.34cc20ecda8c2f89.js
  98. 1
      static/web/_next/static/chunks/1008.65d0bc27255efb44.js
  99. 1
      static/web/_next/static/chunks/1010.398d7f6d64350bec.js
  100. 1
      static/web/_next/static/chunks/1010.a223916b6c5495db.js
  101. Some files were not shown because too many files have changed in this diff Show More

23
.github/PULL_REQUEST_TEMPLATE/atomic-refactor-pull-request-for-issue-2119.md

@ -0,0 +1,23 @@
<!-- this template is for changes relating to #2119. You might want to use the standard template. -->
# Description
<!-- do not remove -->
This PR is for updating/adding a component following the atomic design pattern set out in #2119.
<!-- mention the component you changed, and describe any design choices if necessary -->
---
Extra Info
<!-- fill these in -->
- Component name in [kanban board](https://collab.owncast.tv/kanban/#/2/kanban/edit/omLI2N+LcnP+elmdT7qW9GHD/): `________`
Checklist:
- [] The component follows the [design guide](../../web/components/_COMPONENT_HOW_TO.md).
- [] Moved the component to the correct `atoms` / `molecules` / `organisms` / `templates` directory.
- [] Added an explanation to this PR for any major changes you made.
- [] Replaced any [`defaultProps`](https://www.reactjstutorials.com/react-basics/17/react-default-props) with default args.
- [] Added a (short) JSDoc description to the component.
- [] Removed the component's Storybook description text with if it's not needed.

4
.github/PULL_REQUEST_TEMPLATE/standard-pull-request.md

@ -1,8 +1,6 @@
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. 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 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
Fixes # (issue) Fixes # (issue)
@ -16,4 +14,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? 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? 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. If this is an unsolicited change or have no issue associated please do your best to detail the motivations behind this PR.

1
.github/codeql/go.yml

@ -1 +0,0 @@
name: Go config

4
.github/codeql/javascript.yml

@ -1,4 +0,0 @@
name: Javascript config
paths-ignore:
- static/web

2
.github/workflows/actions-lint.yml

@ -13,7 +13,7 @@ jobs:
name: GitHub actions name: GitHub actions
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: docker://rhysd/actionlint:latest - uses: docker://rhysd/actionlint:latest
with: with:

2
.github/workflows/auto-comment-on-label.yaml

@ -11,7 +11,7 @@ jobs:
issues: write issues: write
steps: steps:
- name: Add comment - name: Add comment
uses: peter-evans/create-or-update-comment@0f44b017d10caeea6a4c1b410ba0521ad8a02815 uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa
with: with:
issue-number: ${{ github.event.issue.number }} issue-number: ${{ github.event.issue.number }}
body: | body: |

4
.github/workflows/automated-end-to-end-api.yaml

@ -27,12 +27,12 @@ jobs:
- name: Set up QEMU - name: Set up QEMU
id: qemu id: qemu
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v2
with: with:
image: tonistiigi/binfmt:latest image: tonistiigi/binfmt:latest
platforms: all platforms: all
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: Run API tests - name: Run API tests
uses: nick-fields/retry@v2 uses: nick-fields/retry@v2

10
.github/workflows/browser-testing.yml

@ -20,9 +20,9 @@ jobs:
concurrent_skipping: 'same_content_newer' concurrent_skipping: 'same_content_newer'
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v3
- uses: actions/setup-node@v4 - uses: actions/setup-node@v3
with: with:
node-version: 18.9.0 node-version: 18.9.0
@ -38,13 +38,13 @@ jobs:
${{ runner.os }}-build- ${{ runner.os }}-build-
${{ runner.os }}- ${{ runner.os }}-
- uses: actions/setup-go@v5 - uses: actions/setup-go@v4
with: with:
go-version: '1.21' go-version: '1.20'
cache: true cache: true
- name: Install Google Chrome - name: Install Google Chrome
run: sudo apt-get update && sudo apt-get install google-chrome-stable run: sudo apt-get install google-chrome-stable
- name: Run Browser tests - name: Run Browser tests
uses: nick-fields/retry@v2 uses: nick-fields/retry@v2

2
.github/workflows/build-storybook.yml

@ -12,7 +12,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: Cache node modules - name: Cache node modules
uses: actions/cache@v3 uses: actions/cache@v3

44
.github/workflows/bundle-web.yml

@ -0,0 +1,44 @@
name: Build and bundle web app into Owncast
on:
push:
branches:
- develop
paths:
- 'web/**'
- '!**.md'
jobs:
bundle:
runs-on: ubuntu-latest
if: github.repository == 'owncast/owncast'
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
concurrent_skipping: 'same_content_newer'
- name: Cache node modules
uses: actions/cache@v3
env:
cache-name: cache-node-modules-bundle-web-app
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: Bundle web app (next.js build)
uses: actions/checkout@v3
- run: build/web/bundleWeb.sh
- name: Commit changes
uses: EndBug/add-and-commit@v9
with:
pull: --rebase --autostash
message: 'Bundle embedded web app'
add: 'static/web'
author_name: Owncast
author_email: owncast@owncast.online

2
.github/workflows/chromatic.yml

@ -29,7 +29,7 @@ jobs:
concurrent_skipping: 'same_content_newer' concurrent_skipping: 'same_content_newer'
- name: Check out code - name: Check out code
if: ${{ github.actor != 'renovate[bot]' && github.actor != 'renovate' }} if: ${{ github.actor != 'renovate[bot]' && github.actor != 'renovate' }}
uses: actions/checkout@v4 uses: actions/checkout@v3
with: with:
# Make sure the actual branch is checked out when running on pull requests # Make sure the actual branch is checked out when running on pull requests
ref: ${{ github.event.pull_request.head.ref }} ref: ${{ github.event.pull_request.head.ref }}

9
.github/workflows/codeql-analysis.yml

@ -37,14 +37,13 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v3 uses: github/codeql-action/init@v2
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
config-file: ./.github/codeql/${{ matrix.language }}.yml
# If you wish to specify custom queries, you can do so here or in a config file. # If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file. # By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file. # Prefix the list here with "+" to use these queries and those in the config file.
@ -53,7 +52,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@v3 uses: github/codeql-action/autobuild@v2
# ℹ Command-line programs to run using the OS shell. # ℹ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
@ -67,4 +66,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3 uses: github/codeql-action/analyze@v2

2
.github/workflows/container-lint.yml

@ -19,7 +19,7 @@ jobs:
container: container:
image: aquasec/trivy image: aquasec/trivy
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: Check critical issues - name: Check critical issues
run: trivy config --exit-code 1 --severity "HIGH,CRITICAL" ./Dockerfile run: trivy config --exit-code 1 --severity "HIGH,CRITICAL" ./Dockerfile

4
.github/workflows/container.yaml

@ -32,13 +32,13 @@ jobs:
run: echo "${{ secrets.GH_CR_PAT }}" | docker login https://ghcr.io -u ${{ github.actor }} --password-stdin run: echo "${{ secrets.GH_CR_PAT }}" | docker login https://ghcr.io -u ${{ github.actor }} --password-stdin
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v2
with: with:
image: tonistiigi/binfmt:latest image: tonistiigi/binfmt:latest
platforms: all platforms: all
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v4 uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0

2
.github/workflows/generate-api-documentation.yaml

@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repo - name: Checkout repo
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: Run redoc on openapi.yaml - name: Run redoc on openapi.yaml
run: | run: |

8
.github/workflows/go-lint.yml

@ -22,15 +22,15 @@ jobs:
with: with:
concurrent_skipping: 'same_content_newer' concurrent_skipping: 'same_content_newer'
- uses: actions/checkout@v4 - uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: actions/setup-go@v5 - uses: actions/setup-go@v4
with: with:
go-version: '1.21' go-version: '1.20'
cache: true cache: true
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: golangci-lint - name: golangci-lint
uses: golangci/golangci-lint-action@v3 uses: golangci/golangci-lint-action@v3
with: with:

28
.github/workflows/go-tests.yaml

@ -12,23 +12,14 @@ jobs:
test: test:
strategy: strategy:
matrix: matrix:
go-version: [1.20.x, 1.21.x] go-version: [1.19.x, 1.20.x]
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: actions/cache@v3
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: go-test-${{ github.sha }}
restore-keys: |
go-test-
- name: Install go - name: Install go
uses: actions/setup-go@v5 uses: actions/setup-go@v4
with: with:
go-version: '^1' go-version: '^1'
cache: true cache: true
@ -47,19 +38,10 @@ jobs:
version: 6.8 version: 6.8
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: actions/cache@v3
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: go-test-${{ github.sha }}
restore-keys: |
go-test-
- name: Install go - name: Install go
uses: actions/setup-go@v5 uses: actions/setup-go@v4
with: with:
go-version: '^1' go-version: '^1'
cache: true cache: true

6
.github/workflows/hls-tests.yml

@ -24,10 +24,10 @@ jobs:
uses: fkirc/skip-duplicate-actions@v5 uses: fkirc/skip-duplicate-actions@v5
with: with:
concurrent_skipping: 'same_content_newer' concurrent_skipping: 'same_content_newer'
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: actions/setup-go@v5 - uses: actions/setup-go@v4
with: with:
go-version: '1.21' go-version: '1.20'
cache: true cache: true
- name: Cache node modules - name: Cache node modules

185
.github/workflows/javascript-format-build.yml

@ -1,185 +0,0 @@
name: Javascript
# This action works with pull requests and pushes
on:
push:
paths:
- web/**
- '!**.md'
pull_request:
paths:
- web/**
- '!**.md'
jobs:
formatting:
name: Code formatting
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./web
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
concurrent_skipping: 'same_content_newer'
cancel_others: 'true'
skip_after_successful_duplicate: 'true'
- name: Checkout
uses: actions/checkout@v4
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
- name: Get changed files
id: changed-files-yaml
uses: tj-actions/changed-files@v41
with:
path: 'web'
files_ignore: |
static/**
web/next.config.js
files_yaml: |
src:
- '**/*.{js,ts,tsx,jsx,css,md}'
- name: Cache node modules
uses: actions/cache@v3
env:
cache-name: cache-node-modules-bundle-web-app
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
run: npm install
- name: Lint
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
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'
run: npx prettier --write ${{ steps.changed-files-yaml.outputs.src_all_changed_files }}
- name: Commit changes
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
uses: EndBug/add-and-commit@v9
with:
author_name: Owncast
author_email: owncast@owncast.online
message: 'Javascript formatting autofixes'
add: ${{ steps.changed-files-yaml.outputs.src_all_changed_files }}
pull: '--rebase --autostash'
unused-code:
name: Test for unused code
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./web
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
concurrent_skipping: 'same_content_newer'
cancel_others: 'true'
skip_after_successful_duplicate: 'true'
- name: Checkout
uses: actions/checkout@v4
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: Cache node modules
uses: actions/cache@v3
env:
cache-name: cache-node-modules-bundle-web-app
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
run: npm install
- name: Check for unused JS code and dependencies
run: npx knip --include dependencies,files,exports
# 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.
web-bundle:
name: Build and bundle web project
runs-on: ubuntu-latest
if: github.repository == 'owncast/owncast'
needs: [formatting, unused-code]
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
concurrent_skipping: 'same_content_newer'
cancel_others: 'true'
skip_after_successful_duplicate: 'true'
- name: Cache node modules
uses: actions/cache@v3
env:
cache-name: cache-node-modules-bundle-web-app
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: Checkout
uses: actions/checkout@v4
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: Bundle web app (next.js build)
run: build/web/bundleWeb.sh
- name: Rebase
if: ${{ github.ref == 'refs/heads/develop' }}
run: |
git add static/web
git pull --rebase --autostash
# Only commit built web project files on develop.
- name: Commit changes
if: ${{ github.ref == 'refs/heads/develop' }}
uses: EndBug/add-and-commit@v9
with:
message: 'Bundle embedded web app'
add: 'static/web'
author_name: Owncast
author_email: owncast@owncast.online
- name: Push changes
if: ${{ github.ref == 'refs/heads/develop' }}
run: |
git pull --rebase --autostash
git push

96
.github/workflows/javascript-formatting.yml

@ -0,0 +1,96 @@
name: Lint
# This action works with pull requests and pushes
on:
push:
paths:
- web/**
pull_request_target:
paths:
- web/**
jobs:
prettier:
name: Javascript prettier
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./web
if: ${{ github.actor != 'dependabot[bot]' }}
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
concurrent_skipping: 'same_content_newer'
- name: Checkout
uses: actions/checkout@v3
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: Prettify code
uses: creyD/prettier_action@v4.3
with:
# This part is also where you can pass other options, for example:
prettier_options: --write **/*.{js,ts,jsx,tsx,css,md}
only_changed: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
linter:
name: Javascript linter
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./web
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
concurrent_skipping: 'same_content_newer'
- name: Checkout
uses: actions/checkout@v3
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: Install Dependencies
run: npm install
- name: Lint
run: npm run lint
unused-code:
name: Test for unused code
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./web
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
concurrent_skipping: 'same_content_newer'
- name: Checkout
uses: actions/checkout@v3
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: Install Dependencies
run: npm install
- name: Check for unused JS code and dependencies
run: npx knip --include dependencies,files,exports

4
.github/workflows/javascript-tests.yml

@ -18,9 +18,9 @@ jobs:
concurrent_skipping: 'same_content_newer' concurrent_skipping: 'same_content_newer'
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v3
- uses: actions/setup-node@v4 - uses: actions/setup-node@v3
with: with:
node-version: 18.9.0 node-version: 18.9.0

6
.github/workflows/screenshots.yml

@ -14,10 +14,10 @@ jobs:
Screenshots: Screenshots:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- uses: actions/setup-go@v5 - uses: actions/setup-go@v4
with: with:
go-version: '1.21' go-version: '1.20'
cache: true cache: true
- name: Cache node modules - name: Cache node modules

4
.github/workflows/shellcheck.yml

@ -18,9 +18,9 @@ jobs:
env: env:
LANG: C.UTF-8 LANG: C.UTF-8
container: container:
image: docker.io/ubuntu:24.04 image: docker.io/ubuntu:23.10
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: Install shellcheck - name: Install shellcheck
run: apt update && apt install -y shellcheck bash && shellcheck --version run: apt update && apt install -y shellcheck bash && shellcheck --version

50
.github/workflows/test-webapp-build.yaml

@ -0,0 +1,50 @@
name: Webapp Test Build
# This action works with pull requests and pushes
on:
push:
paths:
- web/**
pull_request:
paths:
- web/**
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./web
name: Build webapp
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
concurrent_skipping: 'same_content_newer'
- name: Checkout
uses: actions/checkout@v3
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: Cache node modules
uses: actions/cache@v3
env:
cache-name: cache-node-modules-bundle-web-app
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
run: npm install
- name: Build
run: npm run build

1
.gitignore vendored

@ -42,7 +42,6 @@ test/automated/browser/screenshots
lefthook.yml lefthook.yml
test/automated/browser/cypress/screenshots test/automated/browser/cypress/screenshots
test/automated/browser/cypress/videos test/automated/browser/cypress/videos
web/style-definitions/build/
web/public/sw.js web/public/sw.js
web/public/workbox-*.js web/public/workbox-*.js

4
.golangci.yml

@ -5,7 +5,7 @@ run:
# Define the Go version limit. # Define the Go version limit.
# Mainly related to generics support in go1.18. # Mainly related to generics support in go1.18.
# Default: use Go version from the go.mod file, fallback on the env var `GOVERSION`, fallback on 1.18 # Default: use Go version from the go.mod file, fallback on the env var `GOVERSION`, fallback on 1.18
go: '1.21' go: '1.20'
issues: issues:
# The linter has a default list of ignorable errors. Turning this on will enable that list. # The linter has a default list of ignorable errors. Turning this on will enable that list.
@ -69,7 +69,7 @@ linters-settings:
gosimple: gosimple:
# Select the Go version to target. The default is '1.13'. # Select the Go version to target. The default is '1.13'.
go: '1.21' go: '1.20'
# https://staticcheck.io/docs/options#checks # https://staticcheck.io/docs/options#checks
checks: ['all'] checks: ['all']

2
Dockerfile

@ -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 . 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 # Create the image by copying the result of the build into a new alpine image
FROM alpine:3.19.0 FROM alpine:3.18.0
RUN apk update && apk add --no-cache ffmpeg ffmpeg-libs ca-certificates && update-ca-certificates 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 RUN addgroup -g 101 -S owncast && adduser -u 101 -S owncast -G owncast

18
Earthfile

@ -6,10 +6,10 @@ ARG version=develop
WORKDIR /build WORKDIR /build
build-all: build-all:
BUILD --platform=linux/amd64 --platform=linux/386 --platform=linux/arm64 --platform=linux/arm/v7 --platform=darwin/amd64 --platform=darwin/arm64 +build BUILD --platform=linux/amd64 --platform=linux/386 --platform=linux/arm64 --platform=linux/arm/v7 --platform=darwin/amd64 +build
package-all: package-all:
BUILD --platform=linux/amd64 --platform=linux/386 --platform=linux/arm64 --platform=linux/arm/v7 --platform=darwin/amd64 --platform=darwin/arm64 +package BUILD --platform=linux/amd64 --platform=linux/386 --platform=linux/arm64 --platform=linux/arm/v7 --platform=darwin/amd64 +package
docker-all: docker-all:
BUILD --platform=linux/amd64 --platform=linux/386 --platform=linux/arm64 --platform=linux/arm/v7 +docker BUILD --platform=linux/amd64 --platform=linux/386 --platform=linux/arm64 --platform=linux/arm/v7 +docker
@ -36,6 +36,7 @@ build:
FROM --platform=linux/amd64 +code FROM --platform=linux/amd64 +code
RUN echo $EARTHLY_GIT_HASH
RUN echo "Finding CC configuration for $TARGETPLATFORM" RUN echo "Finding CC configuration for $TARGETPLATFORM"
IF [ "$TARGETPLATFORM" = "linux/amd64" ] IF [ "$TARGETPLATFORM" = "linux/amd64" ]
ARG NAME=linux-64bit ARG NAME=linux-64bit
@ -58,10 +59,6 @@ build:
ARG NAME=macOS-64bit ARG NAME=macOS-64bit
ARG CC=o64-clang ARG CC=o64-clang
ARG CXX=o64-clang++ ARG CXX=o64-clang++
ELSE IF [ "$TARGETPLATFORM" = "darwin/arm64" ]
ARG NAME=macOS-arm64
ARG CC=o64-clang
ARG CXX=o64-clang++
ELSE ELSE
RUN echo "Failed to find CC configuration for $TARGETPLATFORM" RUN echo "Failed to find CC configuration for $TARGETPLATFORM"
ARG --required CC ARG --required CC
@ -79,13 +76,10 @@ build:
# MacOSX disallows static executables, so we omit the static flag on this platform # MacOSX disallows static executables, so we omit the static flag on this platform
RUN go build -a -installsuffix cgo -ldflags "$([ "$GOOS"z != darwinz ] && echo "-linkmode external -extldflags -static ") -s -w -X github.com/owncast/owncast/config.GitCommit=$EARTHLY_GIT_HASH -X github.com/owncast/owncast/config.VersionNumber=$version -X github.com/owncast/owncast/config.BuildPlatform=$NAME" -tags sqlite_omit_load_extension -o owncast main.go RUN go build -a -installsuffix cgo -ldflags "$([ "$GOOS"z != darwinz ] && echo "-linkmode external -extldflags -static ") -s -w -X github.com/owncast/owncast/config.GitCommit=$EARTHLY_GIT_HASH -X github.com/owncast/owncast/config.VersionNumber=$version -X github.com/owncast/owncast/config.BuildPlatform=$NAME" -tags sqlite_omit_load_extension -o owncast main.go
# Decrease the size of the shipped binary. But only for non-Apple platforms. # Decrease the size of the shipped binary
# See https://github.com/upx/upx/issues/612
IF [ "$GOOS" != "darwin" ]
RUN upx --best --lzma owncast RUN upx --best --lzma owncast
# Test the binary # Test the binary
RUN upx -t owncast RUN upx -t owncast
END
SAVE ARTIFACT owncast owncast SAVE ARTIFACT owncast owncast
@ -103,8 +97,6 @@ package:
ARG NAME=linux-arm7 ARG NAME=linux-arm7
ELSE IF [ "$TARGETPLATFORM" = "darwin/amd64" ] ELSE IF [ "$TARGETPLATFORM" = "darwin/amd64" ]
ARG NAME=macOS-64bit ARG NAME=macOS-64bit
ELSE IF [ "$TARGETPLATFORM" = "darwin/arm64" ]
ARG NAME=macOS-arm64
ELSE ELSE
ARG NAME=custom ARG NAME=custom
END END
@ -112,7 +104,7 @@ package:
COPY (+build/owncast --platform $TARGETPLATFORM) /build/dist/owncast COPY (+build/owncast --platform $TARGETPLATFORM) /build/dist/owncast
ENV ZIPNAME owncast-$version-$NAME.zip ENV ZIPNAME owncast-$version-$NAME.zip
RUN cd /build/dist && zip -r -q -8 /build/dist/owncast.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 SAVE ARTIFACT /build/dist/owncast.zip owncast.zip AS LOCAL dist/$ZIPNAME
docker: docker:
# Multiple image names can be tagged at once. They should all be passed # Multiple image names can be tagged at once. They should all be passed

6
README.md

@ -48,8 +48,8 @@ Owncast is an open source, self-hosted, decentralized, single user live video st
<div> <div>
<img alt="GitHub all releases" src="https://img.shields.io/github/downloads/owncast/owncast/total?style=for-the-badge"> <img alt="GitHub all releases" src="https://img.shields.io/github/downloads/owncast/owncast/total?style=for-the-badge">
<a href="https://hub.docker.com/r/owncast/owncast"> <a href="https://hub.docker.com/r/gabekangas/owncast">
<img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/owncast/owncast?style=for-the-badge"> <img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/gabekangas/owncast?style=for-the-badge">
</a> </a>
<a href="https://github.com/owncast/owncast/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22"> <a href="https://github.com/owncast/owncast/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22">
<img alt="GitHub issues by-label" src="https://img.shields.io/github/issues-raw/owncast/owncast/good%20first%20issue?style=for-the-badge"> <img alt="GitHub issues by-label" src="https://img.shields.io/github/issues-raw/owncast/owncast/good%20first%20issue?style=for-the-badge">
@ -95,7 +95,7 @@ The Owncast backend is a service written in Go.
1. Ensure you have prerequisites installed. 1. Ensure you have prerequisites installed.
- C compiler, such as [GCC compiler](https://gcc.gnu.org/install/download.html) or a [Musl-compatible compiler](https://musl.libc.org/) - C compiler, such as [GCC compiler](https://gcc.gnu.org/install/download.html) or a [Musl-compatible compiler](https://musl.libc.org/)
- [ffmpeg](https://ffmpeg.org/download.html) - [ffmpeg](https://ffmpeg.org/download.html)
1. Install the [Go toolchain](https://golang.org/dl/) (1.21 or above). 1. Install the [Go toolchain](https://golang.org/dl/) (1.20 or above).
1. Clone the repo. `git clone https://github.com/owncast/owncast` 1. Clone the repo. `git clone https://github.com/owncast/owncast`
1. `go run main.go` will run from the source. 1. `go run main.go` will run from the source.
1. Visit `http://yourserver:8080` to access the web interface or `http://yourserver:8080/admin` to access the admin. 1. Visit `http://yourserver:8080` to access the web interface or `http://yourserver:8080/admin` to access the admin.

8
activitypub/inbox/like.go

@ -13,14 +13,6 @@ import (
func handleLikeRequest(c context.Context, activity vocab.ActivityStreamsLike) error { func handleLikeRequest(c context.Context, activity vocab.ActivityStreamsLike) error {
object := activity.GetActivityStreamsObject() object := activity.GetActivityStreamsObject()
actorReference := activity.GetActivityStreamsActor() actorReference := activity.GetActivityStreamsActor()
if object.Len() < 1 {
return errors.New("like activity is missing object")
}
if actorReference.Len() < 1 {
return errors.New("like activity is missing actor")
}
objectIRI := object.At(0).GetIRI().String() objectIRI := object.At(0).GetIRI().String()
actorIRI := actorReference.At(0).GetIRI().String() actorIRI := actorReference.At(0).GetIRI().String()

10
activitypub/inbox/workerpool.go

@ -1,14 +1,14 @@
package inbox package inbox
import ( import (
"runtime"
"github.com/owncast/owncast/activitypub/apmodels" "github.com/owncast/owncast/activitypub/apmodels"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
// workerPoolSize defines the number of concurrent ActivityPub handlers. const (
var workerPoolSize = runtime.GOMAXPROCS(0) // InboxWorkerPoolSize defines the number of concurrent ActivityPub handlers.
InboxWorkerPoolSize = 10
)
// Job struct bundling the ActivityPub and the payload in one struct. // Job struct bundling the ActivityPub and the payload in one struct.
type Job struct { type Job struct {
@ -22,7 +22,7 @@ func InitInboxWorkerPool() {
queue = make(chan Job) queue = make(chan Job)
// start workers // start workers
for i := 1; i <= workerPoolSize; i++ { for i := 1; i <= InboxWorkerPoolSize; i++ {
go worker(i, queue) go worker(i, queue)
} }
} }

2
activitypub/outbox/outbox.go

@ -60,7 +60,7 @@ func SendLive() error {
if title := data.GetStreamTitle(); title != "" { if title := data.GetStreamTitle(); title != "" {
streamTitle = fmt.Sprintf("<p>%s</p>", title) streamTitle = fmt.Sprintf("<p>%s</p>", title)
} }
textContent = fmt.Sprintf("<p>%s</p>%s<p>%s</p><p><a href=\"%s\">%s</a></p>", textContent, streamTitle, tagsString, data.GetServerURL(), data.GetServerURL()) textContent = fmt.Sprintf("<p>%s</p>%s<p>%s</p><a href=\"%s\">%s</a>", textContent, streamTitle, tagsString, data.GetServerURL(), data.GetServerURL())
activity, _, note, noteID := createBaseOutboundMessage(textContent) activity, _, note, noteID := createBaseOutboundMessage(textContent)

8
activitypub/webfinger/webfinger.go

@ -2,13 +2,10 @@ package webfinger
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
"github.com/owncast/owncast/utils"
) )
// GetWebfingerLinks will return webfinger data for an account. // GetWebfingerLinks will return webfinger data for an account.
@ -21,11 +18,6 @@ func GetWebfingerLinks(account string) ([]map[string]interface{}, error) {
accountComponents := strings.Split(account, "@") accountComponents := strings.Split(account, "@")
fediverseServer := accountComponents[1] fediverseServer := accountComponents[1]
// Reject any requests to our internal network or loopback.
if utils.IsHostnameInternal(fediverseServer) {
return nil, errors.New("unable to use provided host as a valid fediverse server")
}
// HTTPS is required. // HTTPS is required.
requestURL, err := url.Parse("https://" + fediverseServer) requestURL, err := url.Parse("https://" + fediverseServer)
if err != nil { if err != nil {

9
activitypub/workerpool/outbound.go

@ -2,13 +2,14 @@ package workerpool
import ( import (
"net/http" "net/http"
"runtime"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
// workerPoolSize defines the number of concurrent HTTP ActivityPub requests. const (
var workerPoolSize = runtime.GOMAXPROCS(0) // ActivityPubWorkerPoolSize defines the number of concurrent HTTP ActivityPub requests.
ActivityPubWorkerPoolSize = 10
)
// Job struct bundling the ActivityPub and the payload in one struct. // Job struct bundling the ActivityPub and the payload in one struct.
type Job struct { type Job struct {
@ -22,7 +23,7 @@ func InitOutboundWorkerPool() {
queue = make(chan Job) queue = make(chan Job)
// start workers // start workers
for i := 1; i <= workerPoolSize; i++ { for i := 1; i <= ActivityPubWorkerPoolSize; i++ {
go worker(i, queue) go worker(i, queue)
} }
} }

18
auth/indieauth/client.go

@ -12,7 +12,6 @@ import (
"time" "time"
"github.com/owncast/owncast/core/data" "github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/utils"
"github.com/pkg/errors" "github.com/pkg/errors"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -47,27 +46,10 @@ func setupExpiredRequestPruner() {
// StartAuthFlow will begin the IndieAuth flow by generating an auth request. // StartAuthFlow will begin the IndieAuth flow by generating an auth request.
func StartAuthFlow(authHost, userID, accessToken, displayName string) (*url.URL, error) { func StartAuthFlow(authHost, userID, accessToken, displayName string) (*url.URL, error) {
// Limit the number of pending requests
if len(pendingAuthRequests) >= maxPendingRequests { if len(pendingAuthRequests) >= maxPendingRequests {
return nil, errors.New("Please try again later. Too many pending requests.") return nil, errors.New("Please try again later. Too many pending requests.")
} }
// Reject any requests to our internal network or loopback
if utils.IsHostnameInternal(authHost) {
return nil, errors.New("unable to use provided host")
}
// Santity check the server URL
u, err := url.ParseRequestURI(authHost)
if err != nil {
return nil, errors.New("unable to parse server URL")
}
// Limit to only secured connections
if u.Scheme != "https" {
return nil, errors.New("only servers secured with https are supported")
}
serverURL := data.GetServerURL() serverURL := data.GetServerURL()
if serverURL == "" { if serverURL == "" {
return nil, errors.New("Owncast server URL must be set when using auth") return nil, errors.New("Owncast server URL must be set when using auth")

2
auth/indieauth/server.go

@ -40,7 +40,7 @@ type ServerProfileResponse struct {
var pendingServerAuthRequests = map[string]ServerAuthRequest{} var pendingServerAuthRequests = map[string]ServerAuthRequest{}
const maxPendingRequests = 100 const maxPendingRequests = 1000
// StartServerAuth will handle the authentication for the admin user of this // StartServerAuth will handle the authentication for the admin user of this
// Owncast server. Initiated via a GET of the auth endpoint. // Owncast server. Initiated via a GET of the auth endpoint.

6
auth/persistence.go

@ -5,7 +5,7 @@ import (
"strings" "strings"
"github.com/owncast/owncast/core/data" "github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/core/user" "github.com/owncast/owncast/models"
"github.com/owncast/owncast/db" "github.com/owncast/owncast/db"
) )
@ -39,7 +39,7 @@ func AddAuth(userID, authToken string, authType Type) error {
// GetUserByAuth will return an existing user given auth details if a user // GetUserByAuth will return an existing user given auth details if a user
// has previously authenticated with that method. // has previously authenticated with that method.
func GetUserByAuth(authToken string, authType Type) *user.User { func GetUserByAuth(authToken string, authType Type) *models.User {
u, err := _datastore.GetQueries().GetUserByAuth(context.Background(), db.GetUserByAuthParams{ u, err := _datastore.GetQueries().GetUserByAuth(context.Background(), db.GetUserByAuthParams{
Token: authToken, Token: authToken,
Type: string(authType), Type: string(authType),
@ -53,7 +53,7 @@ func GetUserByAuth(authToken string, authType Type) *user.User {
scopes = strings.Split(u.Scopes.String, ",") scopes = strings.Split(u.Scopes.String, ",")
} }
return &user.User{ return &models.User{
ID: u.ID, ID: u.ID,
DisplayName: u.DisplayName, DisplayName: u.DisplayName,
DisplayColor: int(u.DisplayColor), DisplayColor: int(u.DisplayColor),

14
build/web/bundleWeb.sh

@ -5,29 +5,17 @@ set -o errexit
set -o nounset set -o nounset
set -o pipefail set -o pipefail
OFFLINE=
while [[ $# -gt 0 ]]; do
case $1 in
--offline)
OFFLINE=1
;;
esac
shift
done
# Change to the root directory of the repository # Change to the root directory of the repository
cd "$(git rev-parse --show-toplevel)" cd "$(git rev-parse --show-toplevel)"
cd web cd web
if [ ! "$OFFLINE" ]; then
echo "Installing npm modules for the owncast web..." echo "Installing npm modules for the owncast web..."
npm --silent install 2>/dev/null npm --silent install 2>/dev/null
fi
echo "Building owncast web..." echo "Building owncast web..."
rm -rf .next rm -rf .next
node_modules/.bin/next build | grep info (node_modules/.bin/next build && node_modules/.bin/next export) | grep info
echo "Copying web project to dist directory..." echo "Copying web project to dist directory..."

2
config/constants.go

@ -4,7 +4,7 @@ import "path/filepath"
const ( const (
// StaticVersionNumber is the version of Owncast that is used when it's not overwritten via build-time settings. // StaticVersionNumber is the version of Owncast that is used when it's not overwritten via build-time settings.
StaticVersionNumber = "0.1.3" // Shown when you build from develop StaticVersionNumber = "0.1.1" // Shown when you build from develop
// FfmpegSuggestedVersion is the version of ffmpeg we suggest. // FfmpegSuggestedVersion is the version of ffmpeg we suggest.
FfmpegSuggestedVersion = "v4.1.5" // Requires the v FfmpegSuggestedVersion = "v4.1.5" // Requires the v
// DataDirectory is the directory we save data to. // DataDirectory is the directory we save data to.

2
config/verifyInstall.go

@ -30,7 +30,7 @@ func VerifyFFMpegPath(path string) error {
mode := stat.Mode() mode := stat.Mode()
//source: https://stackoverflow.com/a/60128480 //source: https://stackoverflow.com/a/60128480
if mode&0o111 == 0 { if mode&0111 == 0 {
return errors.New("ffmpeg path is not executable") return errors.New("ffmpeg path is not executable")
} }

2
contrib/owncast_for_windows.md

@ -43,7 +43,7 @@ Here is the list for all the prerequisites required ->
- npm (Node Package Manager) is installed as `sudo apt install npm`. - npm (Node Package Manager) is installed as `sudo apt install npm`.
- Node.js is installed (LTS Version) `sudo apt install nodejs`. - Node.js is installed (LTS Version) `sudo apt install nodejs`.
- [ffmpeg](https://ffmpeg.org/download.html) - [ffmpeg](https://ffmpeg.org/download.html)
- Install the [Go toolchain](https://golang.org/dl/) (1.21 or above). - Install the [Go toolchain](https://golang.org/dl/) (1.20 or above).
### Read more ### Read more

6
controllers/admin/chat.go

@ -31,7 +31,6 @@ func UpdateMessageVisibility(w http.ResponseWriter, r *http.Request) {
} }
if r.Method != controllers.POST { if r.Method != controllers.POST {
// nolint:goconst
controllers.WriteSimpleResponse(w, false, r.Method+" not supported") controllers.WriteSimpleResponse(w, false, r.Method+" not supported")
return return
} }
@ -165,20 +164,15 @@ func UpdateUserEnabled(w http.ResponseWriter, r *http.Request) {
disconnectedUser := user.GetUserByID(request.UserID) disconnectedUser := user.GetUserByID(request.UserID)
_ = chat.SendSystemAction(fmt.Sprintf("**%s** has been removed from chat.", disconnectedUser.DisplayName), true) _ = chat.SendSystemAction(fmt.Sprintf("**%s** has been removed from chat.", disconnectedUser.DisplayName), true)
localIP4Address := "127.0.0.1"
localIP6Address := "::1"
// Ban this user's IP address. // Ban this user's IP address.
for _, client := range clients { for _, client := range clients {
ipAddress := client.IPAddress ipAddress := client.IPAddress
if ipAddress != localIP4Address && ipAddress != localIP6Address {
reason := fmt.Sprintf("Banning of %s", disconnectedUser.DisplayName) reason := fmt.Sprintf("Banning of %s", disconnectedUser.DisplayName)
if err := data.BanIPAddress(ipAddress, reason); err != nil { if err := data.BanIPAddress(ipAddress, reason); err != nil {
log.Errorln("error banning IP address: ", err) log.Errorln("error banning IP address: ", err)
} }
} }
} }
}
controllers.WriteSimpleResponse(w, true, fmt.Sprintf("%s enabled: %t", request.UserID, request.Enabled)) controllers.WriteSimpleResponse(w, true, fmt.Sprintf("%s enabled: %t", request.UserID, request.Enabled))
} }

21
controllers/admin/config.go

@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
"net/netip"
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
@ -407,14 +406,6 @@ func SetServerURL(w http.ResponseWriter, r *http.Request) {
return return
} }
// Block Private IP URLs
ipAddr, ipErr := netip.ParseAddr(utils.GetHostnameWithoutPortFromURLString(rawValue))
if ipErr == nil && ipAddr.IsPrivate() {
controllers.WriteSimpleResponse(w, false, "Server URL cannot be private")
return
}
// Trim any trailing slash // Trim any trailing slash
serverURL := strings.TrimRight(rawValue, "/") serverURL := strings.TrimRight(rawValue, "/")
@ -859,18 +850,6 @@ func SetStreamKeys(w http.ResponseWriter, r *http.Request) {
return return
} }
if len(streamKeys.Value) == 0 {
controllers.WriteSimpleResponse(w, false, "must provide at least one valid stream key")
return
}
for _, streamKey := range streamKeys.Value {
if streamKey.Key == "" {
controllers.WriteSimpleResponse(w, false, "stream key cannot be empty")
return
}
}
if err := data.SetStreamKeys(streamKeys.Value); err != nil { if err := data.SetStreamKeys(streamKeys.Value); err != nil {
controllers.WriteSimpleResponse(w, false, err.Error()) controllers.WriteSimpleResponse(w, false, err.Error())
return return

2
controllers/admin/serverConfig.go

@ -57,7 +57,7 @@ func GetServerConfig(w http.ResponseWriter, r *http.Request) {
WebServerIP: config.WebServerIP, WebServerIP: config.WebServerIP,
RTMPServerPort: data.GetRTMPPortNumber(), RTMPServerPort: data.GetRTMPPortNumber(),
ChatDisabled: data.GetChatDisabled(), ChatDisabled: data.GetChatDisabled(),
ChatJoinMessagesEnabled: data.GetChatJoinPartMessagesEnabled(), ChatJoinMessagesEnabled: data.GetChatJoinMessagesEnabled(),
SocketHostOverride: data.GetWebsocketOverrideHost(), SocketHostOverride: data.GetWebsocketOverrideHost(),
VideoServingEndpoint: data.GetVideoServingEndpoint(), VideoServingEndpoint: data.GetVideoServingEndpoint(),
ChatEstablishedUserMode: data.GetChatEstbalishedUsersOnlyMode(), ChatEstablishedUserMode: data.GetChatEstbalishedUsersOnlyMode(),

1
controllers/chat.go

@ -54,7 +54,6 @@ func RegisterAnonymousChatUser(w http.ResponseWriter, r *http.Request) {
} }
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
// nolint:goconst
WriteSimpleResponse(w, false, r.Method+" not supported") WriteSimpleResponse(w, false, r.Method+" not supported")
return return
} }

14
controllers/config.go

@ -17,25 +17,25 @@ import (
type webConfigResponse struct { type webConfigResponse struct {
AppearanceVariables map[string]string `json:"appearanceVariables"` AppearanceVariables map[string]string `json:"appearanceVariables"`
Name string `json:"name"` Notifications notificationsConfigResponse `json:"notifications"`
CustomStyles string `json:"customStyles"` CustomStyles string `json:"customStyles"`
StreamTitle string `json:"streamTitle,omitempty"` // What's going on with the current stream Summary string `json:"summary"`
OfflineMessage string `json:"offlineMessage"` OfflineMessage string `json:"offlineMessage"`
Logo string `json:"logo"` Logo string `json:"logo"`
Version string `json:"version"` Version string `json:"version"`
SocketHostOverride string `json:"socketHostOverride,omitempty"` SocketHostOverride string `json:"socketHostOverride,omitempty"`
ExtraPageContent string `json:"extraPageContent"` ExtraPageContent string `json:"extraPageContent"`
Summary string `json:"summary"` StreamTitle string `json:"streamTitle,omitempty"` // What's going on with the current stream
Tags []string `json:"tags"` Name string `json:"name"`
Federation federationConfigResponse `json:"federation"`
SocialHandles []models.SocialHandle `json:"socialHandles"` SocialHandles []models.SocialHandle `json:"socialHandles"`
ExternalActions []models.ExternalAction `json:"externalActions"` ExternalActions []models.ExternalAction `json:"externalActions"`
Notifications notificationsConfigResponse `json:"notifications"` Tags []string `json:"tags"`
Federation federationConfigResponse `json:"federation"`
MaxSocketPayloadSize int `json:"maxSocketPayloadSize"` MaxSocketPayloadSize int `json:"maxSocketPayloadSize"`
HideViewerCount bool `json:"hideViewerCount"`
ChatDisabled bool `json:"chatDisabled"` ChatDisabled bool `json:"chatDisabled"`
NSFW bool `json:"nsfw"` NSFW bool `json:"nsfw"`
Authentication authenticationConfigResponse `json:"authentication"` Authentication authenticationConfigResponse `json:"authentication"`
HideViewerCount bool `json:"hideViewerCount"`
} }
type federationConfigResponse struct { type federationConfigResponse struct {

3
controllers/emoji.go

@ -8,13 +8,11 @@ import (
"github.com/owncast/owncast/config" "github.com/owncast/owncast/config"
"github.com/owncast/owncast/core/data" "github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/router/middleware"
) )
// GetCustomEmojiList returns a list of emoji via the API. // GetCustomEmojiList returns a list of emoji via the API.
func GetCustomEmojiList(w http.ResponseWriter, r *http.Request) { func GetCustomEmojiList(w http.ResponseWriter, r *http.Request) {
emojiList := data.GetEmojiList() emojiList := data.GetEmojiList()
middleware.SetCachingHeaders(w, r)
if err := json.NewEncoder(w).Encode(emojiList); err != nil { if err := json.NewEncoder(w).Encode(emojiList); err != nil {
InternalErrorHandler(w, err) InternalErrorHandler(w, err)
@ -27,6 +25,5 @@ func GetCustomEmojiImage(w http.ResponseWriter, r *http.Request) {
r.URL.Path = path r.URL.Path = path
emojiFS := os.DirFS(config.CustomEmojiPath) emojiFS := os.DirFS(config.CustomEmojiPath)
middleware.SetCachingHeaders(w, r)
http.FileServer(http.FS(emojiFS)).ServeHTTP(w, r) http.FileServer(http.FS(emojiFS)).ServeHTTP(w, r)
} }

98
controllers/index.go

@ -4,18 +4,13 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/core"
"github.com/owncast/owncast/core/data" "github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/router/middleware" "github.com/owncast/owncast/router/middleware"
"github.com/owncast/owncast/static" "github.com/owncast/owncast/static"
"github.com/owncast/owncast/utils" "github.com/owncast/owncast/utils"
log "github.com/sirupsen/logrus"
) )
// IndexHandler handles the default index route. // IndexHandler handles the default index route.
@ -29,13 +24,6 @@ func IndexHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
// For search engine bots and social scrapers return a special
// server-rendered page.
if utils.IsUserAgentABot(r.UserAgent()) && isIndexRequest {
handleScraperMetadataPage(w, r)
return
}
// Set a cache control max-age header // Set a cache control max-age header
middleware.SetCachingHeaders(w, r) middleware.SetCachingHeaders(w, r)
@ -63,7 +51,6 @@ func renderIndexHtml(w http.ResponseWriter, nonce string) {
Image string Image string
StatusJSON string StatusJSON string
ServerConfigJSON string ServerConfigJSON string
EmbedVideo string
Nonce string Nonce string
} }
@ -84,14 +71,13 @@ func renderIndexHtml(w http.ResponseWriter, nonce string) {
content := serverSideContent{ content := serverSideContent{
Name: data.GetServerName(), Name: data.GetServerName(),
Summary: data.GetServerSummary(), Summary: data.GetServerSummary(),
RequestedURL: fmt.Sprintf("%s%s", data.GetServerURL(), "/"), RequestedURL: data.GetServerURL(),
TagsString: strings.Join(data.GetServerMetadataTags(), ","), TagsString: strings.Join(data.GetServerMetadataTags(), ","),
ThumbnailURL: "thumbnail.jpg", ThumbnailURL: "/thumbnail.jpg",
Thumbnail: "thumbnail.jpg", Thumbnail: "/thumbnail.jpg",
Image: "logo/external", Image: "/logo/external",
StatusJSON: string(sb), StatusJSON: string(sb),
ServerConfigJSON: string(cb), ServerConfigJSON: string(cb),
EmbedVideo: "embed/video",
Nonce: nonce, Nonce: nonce,
} }
@ -105,79 +91,3 @@ func renderIndexHtml(w http.ResponseWriter, nonce string) {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
} }
} }
// MetadataPage represents a server-rendered web page for bots and web scrapers.
type MetadataPage struct {
RequestedURL string
Image string
Thumbnail string
TagsString string
Summary string
Name string
Tags []string
SocialHandles []models.SocialHandle
}
// Return a basic HTML page with server-rendered metadata from the config
// to give to Opengraph clients and web scrapers (bots, web crawlers, etc).
func handleScraperMetadataPage(w http.ResponseWriter, r *http.Request) {
tmpl, err := static.GetBotMetadataTemplate()
if err != nil {
log.Errorln(err)
w.WriteHeader(http.StatusInternalServerError)
return
}
scheme := "http"
if siteURL := data.GetServerURL(); siteURL != "" {
if parsed, err := url.Parse(siteURL); err == nil && parsed.Scheme != "" {
scheme = parsed.Scheme
}
}
fullURL, err := url.Parse(fmt.Sprintf("%s://%s%s", scheme, r.Host, r.URL.Path))
if err != nil {
log.Errorln(err)
}
imageURL, err := url.Parse(fmt.Sprintf("%s://%s%s", scheme, r.Host, "/logo/external"))
if err != nil {
log.Errorln(err)
}
status := core.GetStatus()
// If the thumbnail does not exist or we're offline then just use the logo image
var thumbnailURL string
if status.Online && utils.DoesFileExists(filepath.Join(config.DataDirectory, "tmp", "thumbnail.jpg")) {
thumbnail, err := url.Parse(fmt.Sprintf("%s://%s%s", scheme, r.Host, "/thumbnail.jpg"))
if err != nil {
log.Errorln(err)
thumbnailURL = imageURL.String()
} else {
thumbnailURL = thumbnail.String()
}
} else {
thumbnailURL = imageURL.String()
}
tagsString := strings.Join(data.GetServerMetadataTags(), ",")
metadata := MetadataPage{
Name: data.GetServerName(),
RequestedURL: fullURL.String(),
Image: imageURL.String(),
Summary: data.GetServerSummary(),
Thumbnail: thumbnailURL,
TagsString: tagsString,
Tags: data.GetServerMetadataTags(),
SocialHandles: data.GetSocialHandles(),
}
// Set a cache header
middleware.SetCachingHeaders(w, r)
w.Header().Set("Content-Type", "text/html")
if err := tmpl.Execute(w, metadata); err != nil {
log.Errorln(err)
}
}

4
core/chat/chatclient.go

@ -13,8 +13,8 @@ import (
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/owncast/owncast/config" "github.com/owncast/owncast/config"
"github.com/owncast/owncast/core/chat/events" "github.com/owncast/owncast/core/chat/events"
"github.com/owncast/owncast/core/user"
"github.com/owncast/owncast/geoip" "github.com/owncast/owncast/geoip"
"github.com/owncast/owncast/models"
) )
// Client represents a single chat client. // Client represents a single chat client.
@ -23,7 +23,7 @@ type Client struct {
timeoutTimer *time.Timer timeoutTimer *time.Timer
rateLimiter *rate.Limiter rateLimiter *rate.Limiter
conn *websocket.Conn conn *websocket.Conn
User *user.User `json:"user"` User *models.User `json:"user"`
server *Server server *Server
Geo *geoip.GeoDetails `json:"geo"` Geo *geoip.GeoDetails `json:"geo"`
// Buffered channel of outbound messages. // Buffered channel of outbound messages.

19
core/chat/events.go

@ -9,8 +9,8 @@ import (
"github.com/owncast/owncast/config" "github.com/owncast/owncast/config"
"github.com/owncast/owncast/core/chat/events" "github.com/owncast/owncast/core/chat/events"
"github.com/owncast/owncast/core/data" "github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/core/user"
"github.com/owncast/owncast/core/webhooks" "github.com/owncast/owncast/core/webhooks"
"github.com/owncast/owncast/storage"
"github.com/owncast/owncast/utils" "github.com/owncast/owncast/utils"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -26,6 +26,7 @@ func (s *Server) userNameChanged(eventData chatClientEvent) {
// Check if name is on the blocklist // Check if name is on the blocklist
blocklist := data.GetForbiddenUsernameList() blocklist := data.GetForbiddenUsernameList()
userRepository := storage.GetUserRepository()
// Names have a max length // Names have a max length
proposedUsername = utils.MakeSafeStringOfLength(proposedUsername, config.MaxChatDisplayNameLength) proposedUsername = utils.MakeSafeStringOfLength(proposedUsername, config.MaxChatDisplayNameLength)
@ -47,7 +48,7 @@ func (s *Server) userNameChanged(eventData chatClientEvent) {
} }
// Check if the name is not already assigned to a registered user. // 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) log.Errorln("error checking if name is available", err)
return return
} else if !available { } else if !available {
@ -60,17 +61,11 @@ func (s *Server) userNameChanged(eventData chatClientEvent) {
return return
} }
savedUser := user.GetUserByToken(eventData.client.accessToken) savedUser := userRepository.GetUserByToken(eventData.client.accessToken)
oldName := savedUser.DisplayName oldName := savedUser.DisplayName
// Check that the new name is different from old.
if proposedUsername == oldName {
eventData.client.sendConnectedClientInfo()
return
}
// Save the new name // 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) log.Errorln("error changing username", err)
} }
@ -114,9 +109,10 @@ func (s *Server) userColorChanged(eventData chatClientEvent) {
log.Errorln("invalid color requested when changing user display color") log.Errorln("invalid color requested when changing user display color")
return return
} }
userRepository := storage.GetUserRepository()
// Save the new color // 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) log.Errorln("error changing user display color", err)
} }
@ -167,6 +163,7 @@ func (s *Server) userMessageSent(eventData chatClientEvent) {
SaveUserMessage(event) SaveUserMessage(event)
eventData.client.MessageCount++ eventData.client.MessageCount++
_lastSeenCache[event.User.ID] = time.Now()
} }
func logSanitize(userValue string) string { func logSanitize(userValue string) string {

4
core/chat/events/connectedClientInfo.go

@ -1,9 +1,9 @@
package events package events
import "github.com/owncast/owncast/core/user" import "github.com/owncast/owncast/models"
// ConnectedClientInfo represents the information about a connected client. // ConnectedClientInfo represents the information about a connected client.
type ConnectedClientInfo struct { type ConnectedClientInfo struct {
User *user.User `json:"user"`
Event Event
User *models.User `json:"user"`
} }

131
core/chat/events/events.go

@ -4,23 +4,16 @@ import (
"bytes" "bytes"
"regexp" "regexp"
"strings" "strings"
"sync"
"text/template"
"time" "time"
"github.com/microcosm-cc/bluemonday" "github.com/microcosm-cc/bluemonday"
"github.com/owncast/owncast/models"
"github.com/teris-io/shortid" "github.com/teris-io/shortid"
"github.com/yuin/goldmark" "github.com/yuin/goldmark"
emoji "github.com/yuin/goldmark-emoji"
emojiAst "github.com/yuin/goldmark-emoji/ast"
emojiDef "github.com/yuin/goldmark-emoji/definition"
"github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/renderer/html" "github.com/yuin/goldmark/renderer/html"
"github.com/yuin/goldmark/util"
"mvdan.cc/xurls" "mvdan.cc/xurls"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/core/user"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -42,7 +35,7 @@ type Event struct {
// UserEvent is an event with an associated user. // UserEvent is an event with an associated user.
type UserEvent struct { type UserEvent struct {
User *user.User `json:"user"` User *models.User `json:"user"`
HiddenAt *time.Time `json:"hiddenAt,omitempty"` HiddenAt *time.Time `json:"hiddenAt,omitempty"`
ClientID uint `json:"clientId,omitempty"` ClientID uint `json:"clientId,omitempty"`
} }
@ -73,105 +66,6 @@ func (e *UserMessageEvent) SetDefaults() {
e.RenderAndSanitizeMessageBody() e.RenderAndSanitizeMessageBody()
} }
// implements the emojiDef.Emojis interface but uses case-insensitive search.
// the .children field isn't currently used, but could be used in a future
// implementation of say, emoji packs where a child represents a pack.
type emojis struct {
list []emojiDef.Emoji
names map[string]*emojiDef.Emoji
children []emojiDef.Emojis
}
// return a new Emojis set.
func newEmojis(emotes ...emojiDef.Emoji) emojiDef.Emojis {
self := &emojis{
list: emotes,
names: map[string]*emojiDef.Emoji{},
children: []emojiDef.Emojis{},
}
for i := range self.list {
emoji := &self.list[i]
for _, s := range emoji.ShortNames {
self.names[s] = emoji
}
}
return self
}
func (self *emojis) Get(shortName string) (*emojiDef.Emoji, bool) {
v, ok := self.names[strings.ToLower(shortName)]
if ok {
return v, ok
}
for _, child := range self.children {
v, ok := child.Get(shortName)
if ok {
return v, ok
}
}
return nil, false
}
func (self *emojis) Add(emotes emojiDef.Emojis) {
self.children = append(self.children, emotes)
}
func (self *emojis) Clone() emojiDef.Emojis {
clone := &emojis{
list: self.list,
names: self.names,
children: make([]emojiDef.Emojis, len(self.children)),
}
copy(clone.children, self.children)
return clone
}
var (
emojiMu sync.Mutex
emojiDefs = newEmojis()
emojiHTML = make(map[string]string)
emojiModTime time.Time
emojiHTMLFormat = `<img src="{{ .URL }}" class="emoji" alt=":{{ .Name }}:" title=":{{ .Name }}:">`
emojiHTMLTemplate = template.Must(template.New("emojiHTML").Parse(emojiHTMLFormat))
)
func loadEmoji() {
modTime, err := data.UpdateEmojiList(false)
if err != nil {
return
}
if modTime.After(emojiModTime) {
emojiMu.Lock()
defer emojiMu.Unlock()
emojiHTML = make(map[string]string)
emojiList := data.GetEmojiList()
emojiArr := make([]emojiDef.Emoji, 0)
for i := 0; i < len(emojiList); i++ {
var buf bytes.Buffer
err := emojiHTMLTemplate.Execute(&buf, emojiList[i])
if err != nil {
return
}
emojiHTML[strings.ToLower(emojiList[i].Name)] = buf.String()
emoji := emojiDef.NewEmoji(emojiList[i].Name, nil, strings.ToLower(emojiList[i].Name))
emojiArr = append(emojiArr, emoji)
}
emojiDefs = newEmojis(emojiArr...)
}
}
// RenderAndSanitizeMessageBody will turn markdown into HTML, sanitize raw user-supplied HTML and standardize // RenderAndSanitizeMessageBody will turn markdown into HTML, sanitize raw user-supplied HTML and standardize
// the message into something safe and renderable for clients. // the message into something safe and renderable for clients.
func (m *MessageEvent) RenderAndSanitizeMessageBody() { func (m *MessageEvent) RenderAndSanitizeMessageBody() {
@ -204,11 +98,6 @@ func RenderAndSanitize(raw string) string {
// RenderMarkdown will return HTML rendered from the string body of a chat message. // RenderMarkdown will return HTML rendered from the string body of a chat message.
func RenderMarkdown(raw string) string { func RenderMarkdown(raw string) string {
loadEmoji()
emojiMu.Lock()
defer emojiMu.Unlock()
markdown := goldmark.New( markdown := goldmark.New(
goldmark.WithRendererOptions( goldmark.WithRendererOptions(
html.WithUnsafe(), html.WithUnsafe(),
@ -223,16 +112,6 @@ func RenderMarkdown(raw string) string {
xurls.Strict, xurls.Strict,
), ),
), ),
emoji.New(
emoji.WithEmojis(
emojiDefs,
),
emoji.WithRenderingMethod(emoji.Func),
emoji.WithRendererFunc(func(w util.BufWriter, source []byte, n *emojiAst.Emoji, config *emoji.RendererConfig) {
baseName := n.Value.ShortNames[0]
_, _ = w.WriteString(emojiHTML[baseName])
}),
),
), ),
) )
@ -247,8 +126,8 @@ func RenderMarkdown(raw string) string {
var ( var (
_sanitizeReSrcMatch = regexp.MustCompile(`(?i)^/img/emoji/[^\.%]*.[A-Z]*$`) _sanitizeReSrcMatch = regexp.MustCompile(`(?i)^/img/emoji/[^\.%]*.[A-Z]*$`)
_sanitizeReAltTitleMatch = regexp.MustCompile(`:\S+:`)
_sanitizeReClassMatch = regexp.MustCompile(`(?i)^(emoji)[A-Z_]*?$`) _sanitizeReClassMatch = regexp.MustCompile(`(?i)^(emoji)[A-Z_]*?$`)
_sanitizeNonEmptyMatch = regexp.MustCompile(`^.+$`)
) )
func sanitize(raw string) string { func sanitize(raw string) string {
@ -270,11 +149,11 @@ func sanitize(raw string) string {
// Allow breaks // Allow breaks
p.AllowElements("br") p.AllowElements("br")
p.AllowElements("p") p.AllowElementsContent("p")
// Allow img tags from the the local emoji directory only // Allow img tags from the the local emoji directory only
p.AllowAttrs("src").Matching(_sanitizeReSrcMatch).OnElements("img") p.AllowAttrs("src").Matching(_sanitizeReSrcMatch).OnElements("img")
p.AllowAttrs("alt", "title").Matching(_sanitizeNonEmptyMatch).OnElements("img") p.AllowAttrs("alt", "title").Matching(_sanitizeReAltTitleMatch).OnElements("img")
p.AllowAttrs("class").Matching(_sanitizeReClassMatch).OnElements("img") p.AllowAttrs("class").Matching(_sanitizeReClassMatch).OnElements("img")
// Allow bold // Allow bold

4
core/chat/events/eventtype.go

@ -8,8 +8,6 @@ const (
MessageSent EventType = "CHAT" MessageSent EventType = "CHAT"
// UserJoined is the event sent when a chat user join action takes place. // UserJoined is the event sent when a chat user join action takes place.
UserJoined EventType = "USER_JOINED" UserJoined EventType = "USER_JOINED"
// UserParted is the event sent when a chat user part action takes place.
UserParted EventType = "USER_PARTED"
// UserNameChanged is the event sent when a chat username change takes place. // UserNameChanged is the event sent when a chat username change takes place.
UserNameChanged EventType = "NAME_CHANGE" UserNameChanged EventType = "NAME_CHANGE"
// UserColorChanged is the event sent when a chat user color change takes place. // UserColorChanged is the event sent when a chat user color change takes place.
@ -35,7 +33,7 @@ const (
// ChatActionSent is a generic chat action that can be used for anything that doesn't need specific handling or formatting. // ChatActionSent is a generic chat action that can be used for anything that doesn't need specific handling or formatting.
ChatActionSent EventType = "CHAT_ACTION" ChatActionSent EventType = "CHAT_ACTION"
// ErrorNeedsRegistration is an error returned when the client needs to perform registration. // ErrorNeedsRegistration is an error returned when the client needs to perform registration.
ErrorNeedsRegistration EventType = "ERROR_NEEDS_REGISTRATION" // nolint:gosec ErrorNeedsRegistration EventType = "ERROR_NEEDS_REGISTRATION"
// ErrorMaxConnectionsExceeded is an error returned when the server determined it should not handle more connections. // ErrorMaxConnectionsExceeded is an error returned when the server determined it should not handle more connections.
ErrorMaxConnectionsExceeded EventType = "ERROR_MAX_CONNECTIONS_EXCEEDED" ErrorMaxConnectionsExceeded EventType = "ERROR_MAX_CONNECTIONS_EXCEEDED"
// ErrorUserDisabled is an error returned when the connecting user has been previously banned/disabled. // ErrorUserDisabled is an error returned when the connecting user has been previously banned/disabled.

17
core/chat/events/userPartEvent.go

@ -1,17 +0,0 @@
package events
// UserPartEvent is the event fired when a user leaves chat.
type UserPartEvent struct {
Event
UserEvent
}
// GetBroadcastPayload will return the object to send to all chat users.
func (e *UserPartEvent) GetBroadcastPayload() EventPayload {
return EventPayload{
"type": UserParted,
"id": e.ID,
"timestamp": e.Timestamp,
"user": e.User,
}
}

17
core/chat/messageRendering_test.go

@ -12,15 +12,18 @@ func TestRenderAndSanitize(t *testing.T) {
messageContent := ` messageContent := `
Test one two three! I go to http://yahoo.com and search for _sports_ and **answers**. Test one two three! I go to http://yahoo.com and search for _sports_ and **answers**.
Here is an iframe <iframe src="http://yahoo.com"></iframe> Here is an iframe <iframe src="http://yahoo.com"></iframe>
## blah blah blah ## blah blah blah
[test link](http://owncast.online) [test link](http://owncast.online)
<img class="emoji" src="/img/emoji/bananadance.gif">` <img class="emoji" alt="bananadance.gif" width="600px" src="/img/emoji/bananadance.gif">
<script src="http://hackers.org/hack.js"></script>
`
expected := `<p>Test one two three! I go to <a href="http://yahoo.com" rel="nofollow noreferrer noopener" target="_blank">http://yahoo.com</a> and search for <em>sports</em> and <strong>answers</strong>. expected := `Test one two three! I go to <a href="http://yahoo.com" rel="nofollow noreferrer noopener" target="_blank">http://yahoo.com</a> and search for <em>sports</em> and <strong>answers</strong>.
Here is an iframe</p> Here is an iframe
blah blah blah blah blah blah
<p><a href="http://owncast.online" rel="nofollow noreferrer noopener" target="_blank">test link</a> <a href="http://owncast.online" rel="nofollow noreferrer noopener" target="_blank">test link</a>
<img class="emoji" src="/img/emoji/bananadance.gif"></p>` <img class="emoji" src="/img/emoji/bananadance.gif">`
result := events.RenderAndSanitize(messageContent) result := events.RenderAndSanitize(messageContent)
if result != expected { if result != expected {
@ -31,7 +34,7 @@ blah blah blah
// Test to make sure we block remote images in chat messages. // Test to make sure we block remote images in chat messages.
func TestBlockRemoteImages(t *testing.T) { func TestBlockRemoteImages(t *testing.T) {
messageContent := `<img src="https://via.placeholder.com/img/emoji/350x150"> test ![](https://via.placeholder.com/img/emoji/350x150)` messageContent := `<img src="https://via.placeholder.com/img/emoji/350x150"> test ![](https://via.placeholder.com/img/emoji/350x150)`
expected := `<p> test </p>` expected := `test`
result := events.RenderAndSanitize(messageContent) result := events.RenderAndSanitize(messageContent)
if result != expected { if result != expected {
@ -42,7 +45,7 @@ func TestBlockRemoteImages(t *testing.T) {
// Test to make sure emoji images are allowed in chat messages. // Test to make sure emoji images are allowed in chat messages.
func TestAllowEmojiImages(t *testing.T) { func TestAllowEmojiImages(t *testing.T) {
messageContent := `<img alt=":beerparrot:" title=":beerparrot:" src="/img/emoji/beerparrot.gif"> test ![](/img/emoji/beerparrot.gif)` messageContent := `<img alt=":beerparrot:" title=":beerparrot:" src="/img/emoji/beerparrot.gif"> test ![](/img/emoji/beerparrot.gif)`
expected := `<p><img alt=":beerparrot:" title=":beerparrot:" src="/img/emoji/beerparrot.gif"> test <img src="/img/emoji/beerparrot.gif"></p>` expected := `<img alt=":beerparrot:" title=":beerparrot:" src="/img/emoji/beerparrot.gif"> test <img src="/img/emoji/beerparrot.gif">`
result := events.RenderAndSanitize(messageContent) result := events.RenderAndSanitize(messageContent)
if result != expected { if result != expected {

3
core/chat/persistence.go

@ -8,7 +8,6 @@ import (
"github.com/owncast/owncast/core/chat/events" "github.com/owncast/owncast/core/chat/events"
"github.com/owncast/owncast/core/data" "github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/core/user"
"github.com/owncast/owncast/models" "github.com/owncast/owncast/models"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -104,7 +103,7 @@ func makeUserMessageEventFromRowData(row rowData) events.UserMessageEvent {
isBot := (row.userType != nil && *row.userType == "API") isBot := (row.userType != nil && *row.userType == "API")
scopeSlice := strings.Split(scopes, ",") scopeSlice := strings.Split(scopes, ",")
u := user.User{ u := models.User{
ID: *row.userID, ID: *row.userID,
DisplayName: displayName, DisplayName: displayName,
DisplayColor: displayColor, DisplayColor: displayColor,

78
core/chat/server.go

@ -14,14 +14,18 @@ import (
"github.com/owncast/owncast/config" "github.com/owncast/owncast/config"
"github.com/owncast/owncast/core/chat/events" "github.com/owncast/owncast/core/chat/events"
"github.com/owncast/owncast/core/data" "github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/core/user"
"github.com/owncast/owncast/core/webhooks" "github.com/owncast/owncast/core/webhooks"
"github.com/owncast/owncast/geoip" "github.com/owncast/owncast/geoip"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/storage"
"github.com/owncast/owncast/utils" "github.com/owncast/owncast/utils"
) )
var _server *Server var _server *Server
// a map of user IDs and when they last were active.
var _lastSeenCache = map[string]time.Time{}
// Server represents an instance of the chat server. // Server represents an instance of the chat server.
type Server struct { type Server struct {
clients map[uint]*Client clients map[uint]*Client
@ -36,9 +40,6 @@ type Server struct {
unregister chan uint // the ChatClient id unregister chan uint // the ChatClient id
geoipClient *geoip.Client geoipClient *geoip.Client
// a map of user IDs and timers that fire for chat part messages.
userPartedTimers map[string]*time.Ticker
seq uint seq uint
maxSocketConnectionLimit int64 maxSocketConnectionLimit int64
@ -57,7 +58,6 @@ func NewChat() *Server {
unregister: make(chan uint), unregister: make(chan uint),
maxSocketConnectionLimit: maximumConcurrentConnectionLimit, maxSocketConnectionLimit: maximumConcurrentConnectionLimit,
geoipClient: geoip.NewClient(), geoipClient: geoip.NewClient(),
userPartedTimers: map[string]*time.Ticker{},
} }
return server return server
@ -68,8 +68,7 @@ func (s *Server) Run() {
for { for {
select { select {
case clientID := <-s.unregister: case clientID := <-s.unregister:
if client, ok := s.clients[clientID]; ok { if _, ok := s.clients[clientID]; ok {
s.handleClientDisconnected(client)
s.mu.Lock() s.mu.Lock()
delete(s.clients, clientID) delete(s.clients, clientID)
s.mu.Unlock() s.mu.Unlock()
@ -82,7 +81,7 @@ func (s *Server) Run() {
} }
// Addclient registers new connection as a User. // 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{ client := &Client{
server: s, server: s,
conn: conn, conn: conn,
@ -94,22 +93,18 @@ func (s *Server) Addclient(conn *websocket.Conn, user *user.User, accessToken st
ConnectedAt: time.Now(), ConnectedAt: time.Now(),
} }
shouldSendJoinedMessages := data.GetChatJoinPartMessagesEnabled() // Do not send user re-joined broadcast message if they've been active within 10 minutes.
shouldSendJoinedMessages := data.GetChatJoinMessagesEnabled()
s.mu.Lock() if previouslyLastSeen, ok := _lastSeenCache[user.ID]; ok && time.Since(previouslyLastSeen) < time.Minute*10 {
{
// If there is a pending disconnect timer then clear it.
// Do not send user joined message if enough time hasn't passed where the
// user chat part message hasn't been sent yet.
if ticker, ok := s.userPartedTimers[user.ID]; ok {
ticker.Stop()
delete(s.userPartedTimers, user.ID)
shouldSendJoinedMessages = false shouldSendJoinedMessages = false
} }
s.mu.Lock()
{
client.Id = s.seq client.Id = s.seq
s.clients[client.Id] = client s.clients[client.Id] = client
s.seq++ s.seq++
_lastSeenCache[user.ID] = time.Now()
} }
s.mu.Unlock() s.mu.Unlock()
@ -149,43 +144,16 @@ func (s *Server) sendUserJoinedMessage(c *Client) {
webhooks.SendChatEventUserJoined(userJoinedEvent) webhooks.SendChatEventUserJoined(userJoinedEvent)
} }
func (s *Server) handleClientDisconnected(c *Client) { // ClientClosed is fired when a client disconnects or connection is dropped.
func (s *Server) ClientClosed(c *Client) {
s.mu.Lock()
defer s.mu.Unlock()
c.close()
if _, ok := s.clients[c.Id]; ok { if _, ok := s.clients[c.Id]; ok {
log.Debugln("Deleting", c.Id) log.Debugln("Deleting", c.Id)
delete(s.clients, c.Id) delete(s.clients, c.Id)
} }
additionalClientCheck, _ := GetClientsForUser(c.User.ID)
if len(additionalClientCheck) > 0 {
// This user is still connected to chat with another client.
return
}
s.userPartedTimers[c.User.ID] = time.NewTicker(10 * time.Second)
go func() {
<-s.userPartedTimers[c.User.ID].C
s.sendUserPartedMessage(c)
}()
}
func (s *Server) sendUserPartedMessage(c *Client) {
s.userPartedTimers[c.User.ID].Stop()
delete(s.userPartedTimers, c.User.ID)
userPartEvent := events.UserPartEvent{}
userPartEvent.SetDefaults()
userPartEvent.User = c.User
userPartEvent.ClientID = c.Id
// If part messages are disabled.
if data.GetChatJoinPartMessagesEnabled() {
if err := s.Broadcast(userPartEvent.GetBroadcastPayload()); err != nil {
log.Errorln("error sending chat part message", err)
}
}
// Send chat user joined webhook
webhooks.SendChatEventUserParted(userPartEvent)
} }
// HandleClientConnection is fired when a single client connects to the websocket. // HandleClientConnection is fired when a single client connects to the websocket.
@ -232,8 +200,10 @@ func (s *Server) HandleClientConnection(w http.ResponseWriter, r *http.Request)
return return
} }
userRepository := storage.GetUserRepository()
// A user is required to use the websocket // A user is required to use the websocket
user := user.GetUserByToken(accessToken) user := userRepository.GetUserByToken(accessToken)
if user == nil { if user == nil {
// Send error that registration is required // Send error that registration is required
_ = conn.WriteJSON(events.EventPayload{ _ = conn.WriteJSON(events.EventPayload{
@ -328,8 +298,10 @@ func SendConnectedClientInfoToUser(userID string) error {
return err return err
} }
userRepository := storage.GetUserRepository()
// Get an updated reference to the user. // Get an updated reference to the user.
user := user.GetUserByID(userID) user := userRepository.GetUserByID(userID)
if user == nil { if user == nil {
return fmt.Errorf("user not found") return fmt.Errorf("user not found")
} }

3
core/core.go

@ -13,7 +13,6 @@ import (
"github.com/owncast/owncast/core/data" "github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/core/rtmp" "github.com/owncast/owncast/core/rtmp"
"github.com/owncast/owncast/core/transcoder" "github.com/owncast/owncast/core/transcoder"
"github.com/owncast/owncast/core/user"
"github.com/owncast/owncast/core/webhooks" "github.com/owncast/owncast/core/webhooks"
"github.com/owncast/owncast/models" "github.com/owncast/owncast/models"
"github.com/owncast/owncast/notifications" "github.com/owncast/owncast/notifications"
@ -56,7 +55,7 @@ func Start() error {
log.Errorln("storage error", err) log.Errorln("storage error", err)
} }
user.SetupUsers() // user.SetupUsers()
auth.Setup(data.GetDatastore()) auth.Setup(data.GetDatastore())
fileWriter.SetupFileWriterReceiverService(&handler) fileWriter.SetupFileWriterReceiverService(&handler)

17
core/data/config.go

@ -63,7 +63,6 @@ const (
discordConfigurationKey = "discord_configuration" discordConfigurationKey = "discord_configuration"
browserPushConfigurationKey = "browser_push_configuration" browserPushConfigurationKey = "browser_push_configuration"
browserPushPublicKeyKey = "browser_push_public_key" browserPushPublicKeyKey = "browser_push_public_key"
// nolint:gosec
browserPushPrivateKeyKey = "browser_push_private_key" browserPushPrivateKeyKey = "browser_push_private_key"
hasConfiguredInitialNotificationsKey = "has_configured_initial_notifications" hasConfiguredInitialNotificationsKey = "has_configured_initial_notifications"
hideViewerCountKey = "hide_viewer_count" hideViewerCountKey = "hide_viewer_count"
@ -620,19 +619,19 @@ func VerifySettings() error {
} }
// FindHighestVideoQualityIndex will return the highest quality from a slice of variants. // FindHighestVideoQualityIndex will return the highest quality from a slice of variants.
func FindHighestVideoQualityIndex(qualities []models.StreamOutputVariant) (int, bool) { func FindHighestVideoQualityIndex(qualities []models.StreamOutputVariant) int {
type IndexedQuality struct { type IndexedQuality struct {
quality models.StreamOutputVariant
index int index int
quality models.StreamOutputVariant
} }
if len(qualities) < 2 { if len(qualities) < 2 {
return 0, qualities[0].IsVideoPassthrough return 0
} }
indexedQualities := make([]IndexedQuality, 0) indexedQualities := make([]IndexedQuality, 0)
for index, quality := range qualities { for index, quality := range qualities {
indexedQuality := IndexedQuality{quality, index} indexedQuality := IndexedQuality{index, quality}
indexedQualities = append(indexedQualities, indexedQuality) indexedQualities = append(indexedQualities, indexedQuality)
} }
@ -648,9 +647,7 @@ func FindHighestVideoQualityIndex(qualities []models.StreamOutputVariant) (int,
return indexedQualities[a].quality.VideoBitrate > indexedQualities[b].quality.VideoBitrate return indexedQualities[a].quality.VideoBitrate > indexedQualities[b].quality.VideoBitrate
}) })
// nolint:gosec return indexedQualities[0].index
selectedQuality := indexedQualities[0]
return selectedQuality.index, selectedQuality.quality.IsVideoPassthrough
} }
// GetForbiddenUsernameList will return the blocked usernames as a comma separated string. // GetForbiddenUsernameList will return the blocked usernames as a comma separated string.
@ -817,8 +814,8 @@ func SetChatJoinMessagesEnabled(enabled bool) error {
return _datastore.SetBool(chatJoinMessagesEnabledKey, enabled) return _datastore.SetBool(chatJoinMessagesEnabledKey, enabled)
} }
// GetChatJoinPartMessagesEnabled will return if chat join messages are enabled. // GetChatJoinMessagesEnabled will return if chat join messages are enabled.
func GetChatJoinPartMessagesEnabled() bool { func GetChatJoinMessagesEnabled() bool {
enabled, err := _datastore.GetBool(chatJoinMessagesEnabledKey) enabled, err := _datastore.GetBool(chatJoinMessagesEnabledKey)
if err != nil { if err != nil {
return true return true

2
core/data/configEntry.go

@ -8,8 +8,8 @@ import (
// ConfigEntry is the actual object saved to the database. // ConfigEntry is the actual object saved to the database.
// The Value is encoded using encoding/gob. // The Value is encoded using encoding/gob.
type ConfigEntry struct { type ConfigEntry struct {
Value interface{}
Key string Key string
Value interface{}
} }
func (c *ConfigEntry) getStringSlice() ([]string, error) { func (c *ConfigEntry) getStringSlice() ([]string, error) {

6
core/data/data_test.go

@ -20,8 +20,6 @@ func TestString(t *testing.T) {
const testKey = "test string key" const testKey = "test string key"
const testValue = "test string value" const testValue = "test string value"
fmt.Println(testKey, testValue)
if err := _datastore.SetString(testKey, testValue); err != nil { if err := _datastore.SetString(testKey, testValue); err != nil {
panic(err) panic(err)
} }
@ -89,7 +87,7 @@ func TestCustomType(t *testing.T) {
} }
// Save config entry to the database // Save config entry to the database
if err := _datastore.Save(ConfigEntry{&testStruct, testKey}); err != nil { if err := _datastore.Save(ConfigEntry{testKey, &testStruct}); err != nil {
t.Error(err) t.Error(err)
} }
@ -121,7 +119,7 @@ func TestStringMap(t *testing.T) {
} }
// Save config entry to the database // Save config entry to the database
if err := _datastore.Save(ConfigEntry{&testMap, testKey}); err != nil { if err := _datastore.Save(ConfigEntry{testKey, &testMap}); err != nil {
t.Error(err) t.Error(err)
} }

70
core/data/emoji.go

@ -6,8 +6,6 @@ import (
"io/fs" "io/fs"
"os" "os"
"path/filepath" "path/filepath"
"sync"
"time"
"github.com/owncast/owncast/config" "github.com/owncast/owncast/config"
"github.com/owncast/owncast/models" "github.com/owncast/owncast/models"
@ -17,81 +15,29 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
var ( // GetEmojiList returns a list of custom emoji from the emoji directory.
emojiCacheMu sync.Mutex func GetEmojiList() []models.CustomEmoji {
emojiCacheData = make([]models.CustomEmoji, 0)
emojiCacheModTime time.Time
)
// UpdateEmojiList will update the cache (if required) and
// return the modifiation time.
func UpdateEmojiList(force bool) (time.Time, error) {
var modTime time.Time
emojiPathInfo, err := os.Stat(config.CustomEmojiPath)
if err != nil {
return modTime, err
}
modTime = emojiPathInfo.ModTime()
if modTime.After(emojiCacheModTime) || force {
emojiCacheMu.Lock()
defer emojiCacheMu.Unlock()
// double-check that another thread didn't update this while waiting.
if modTime.After(emojiCacheModTime) || force {
emojiCacheModTime = modTime
if force {
emojiCacheModTime = time.Now()
}
emojiFS := os.DirFS(config.CustomEmojiPath) emojiFS := os.DirFS(config.CustomEmojiPath)
if emojiFS == nil {
return modTime, fmt.Errorf("unable to open custom emoji directory")
}
emojiCacheData = make([]models.CustomEmoji, 0) emojiResponse := make([]models.CustomEmoji, 0)
walkFunction := func(path string, d os.DirEntry, err error) error { walkFunction := func(path string, d os.DirEntry, err error) error {
if d == nil || d.IsDir() { if d.IsDir() {
return nil return nil
} }
emojiPath := filepath.Join(config.EmojiDir, path) emojiPath := filepath.Join(config.EmojiDir, path)
fileName := d.Name() singleEmoji := models.CustomEmoji{Name: d.Name(), URL: emojiPath}
fileBase := fileName[:len(fileName)-len(filepath.Ext(fileName))] emojiResponse = append(emojiResponse, singleEmoji)
singleEmoji := models.CustomEmoji{Name: fileBase, URL: emojiPath}
emojiCacheData = append(emojiCacheData, singleEmoji)
return nil return nil
} }
if err := fs.WalkDir(emojiFS, ".", walkFunction); err != nil { if err := fs.WalkDir(emojiFS, ".", walkFunction); err != nil {
log.Errorln("unable to fetch emojis: " + err.Error()) log.Errorln("unable to fetch emojis: " + err.Error())
return emojiResponse
} }
}
}
return modTime, nil
}
// GetEmojiList returns a list of custom emoji from the emoji directory.
func GetEmojiList() []models.CustomEmoji {
_, err := UpdateEmojiList(false)
if err != nil {
return nil
}
// Lock to make sure this doesn't get updated in the middle of reading
emojiCacheMu.Lock()
defer emojiCacheMu.Unlock()
// return a copy of cache data, ensures underlying slice isn't affected
// by future update
emojiData := make([]models.CustomEmoji, len(emojiCacheData))
copy(emojiData, emojiCacheData)
return emojiData return emojiResponse
} }
// SetupEmojiDirectory sets up the custom emoji directory by copying all built-in // SetupEmojiDirectory sets up the custom emoji directory by copying all built-in

10
core/data/types.go

@ -11,7 +11,7 @@ func (ds *Datastore) GetStringSlice(key string) ([]string, error) {
// SetStringSlice will set the string slice value for a key. // SetStringSlice will set the string slice value for a key.
func (ds *Datastore) SetStringSlice(key string, value []string) error { func (ds *Datastore) SetStringSlice(key string, value []string) error {
configEntry := ConfigEntry{value, key} configEntry := ConfigEntry{key, value}
return ds.Save(configEntry) return ds.Save(configEntry)
} }
@ -26,7 +26,7 @@ func (ds *Datastore) GetString(key string) (string, error) {
// SetString will set the string value for a key. // SetString will set the string value for a key.
func (ds *Datastore) SetString(key string, value string) error { func (ds *Datastore) SetString(key string, value string) error {
configEntry := ConfigEntry{value, key} configEntry := ConfigEntry{key, value}
return ds.Save(configEntry) return ds.Save(configEntry)
} }
@ -41,7 +41,7 @@ func (ds *Datastore) GetNumber(key string) (float64, error) {
// SetNumber will set the numeric value for a key. // SetNumber will set the numeric value for a key.
func (ds *Datastore) SetNumber(key string, value float64) error { func (ds *Datastore) SetNumber(key string, value float64) error {
configEntry := ConfigEntry{value, key} configEntry := ConfigEntry{key, value}
return ds.Save(configEntry) return ds.Save(configEntry)
} }
@ -56,7 +56,7 @@ func (ds *Datastore) GetBool(key string) (bool, error) {
// SetBool will set the boolean value for a key. // SetBool will set the boolean value for a key.
func (ds *Datastore) SetBool(key string, value bool) error { func (ds *Datastore) SetBool(key string, value bool) error {
configEntry := ConfigEntry{value, key} configEntry := ConfigEntry{key, value}
return ds.Save(configEntry) return ds.Save(configEntry)
} }
@ -71,6 +71,6 @@ func (ds *Datastore) GetStringMap(key string) (map[string]string, error) {
// SetStringMap will set the string map value for a key. // SetStringMap will set the string map value for a key.
func (ds *Datastore) SetStringMap(key string, value map[string]string) error { func (ds *Datastore) SetStringMap(key string, value map[string]string) error {
configEntry := ConfigEntry{value, key} configEntry := ConfigEntry{key, value}
return ds.Save(configEntry) return ds.Save(configEntry)
} }

6
core/rtmp/rtmp.go

@ -73,7 +73,7 @@ func HandleConn(c *rtmp.Conn, nc net.Conn) {
} }
if _hasInboundRTMPConnection { if _hasInboundRTMPConnection {
log.Errorln("stream already running; can not overtake an existing stream from", nc.RemoteAddr().String()) log.Errorln("stream already running; can not overtake an existing stream")
_ = nc.Close() _ = nc.Close()
return return
} }
@ -94,14 +94,14 @@ func HandleConn(c *rtmp.Conn, nc net.Conn) {
} }
if !accessGranted { if !accessGranted {
log.Errorln("invalid streaming key; rejecting incoming stream from", nc.RemoteAddr().String()) log.Errorln("invalid streaming key; rejecting incoming stream")
_ = nc.Close() _ = nc.Close()
return return
} }
rtmpOut, rtmpIn := io.Pipe() rtmpOut, rtmpIn := io.Pipe()
_pipe = rtmpIn _pipe = rtmpIn
log.Infoln("Inbound stream connected from", nc.RemoteAddr().String()) log.Infoln("Inbound stream connected.")
_setStreamAsConnected(rtmpOut) _setStreamAsConnected(rtmpOut)
_hasInboundRTMPConnection = true _hasInboundRTMPConnection = true

12
core/storageproviders/local.go

@ -13,9 +13,7 @@ import (
) )
// LocalStorage represents an instance of the local storage provider for HLS video. // LocalStorage represents an instance of the local storage provider for HLS video.
type LocalStorage struct { type LocalStorage struct{}
host string
}
// NewLocalStorage returns a new LocalStorage instance. // NewLocalStorage returns a new LocalStorage instance.
func NewLocalStorage() *LocalStorage { func NewLocalStorage() *LocalStorage {
@ -24,7 +22,6 @@ func NewLocalStorage() *LocalStorage {
// Setup configures this storage provider. // Setup configures this storage provider.
func (s *LocalStorage) Setup() error { func (s *LocalStorage) Setup() error {
s.host = data.GetVideoServingEndpoint()
return nil return nil
} }
@ -45,17 +42,10 @@ func (s *LocalStorage) VariantPlaylistWritten(localFilePath string) {
// MasterPlaylistWritten is called when the master hls playlist is written. // MasterPlaylistWritten is called when the master hls playlist is written.
func (s *LocalStorage) MasterPlaylistWritten(localFilePath string) { func (s *LocalStorage) MasterPlaylistWritten(localFilePath string) {
// If we're using a remote serving endpoint, we need to rewrite the master playlist
if s.host != "" {
if err := rewritePlaylistLocations(localFilePath, s.host, ""); err != nil {
log.Warnln(err)
}
} else {
if _, err := s.Save(localFilePath, 0); err != nil { if _, err := s.Save(localFilePath, 0); err != nil {
log.Warnln(err) log.Warnln(err)
} }
} }
}
// Save will save a local filepath using the storage provider. // Save will save a local filepath using the storage provider.
func (s *LocalStorage) Save(filePath string, retryCount int) (string, error) { func (s *LocalStorage) Save(filePath string, retryCount int) (string, error) {

13
core/storageproviders/rewriteLocalPlaylist.go

@ -12,8 +12,8 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
// rewritePlaylistLocations will take a local playlist and rewrite it to have absolute URLs to a specified location. // rewriteRemotePlaylist will take a local playlist and rewrite it to have absolute URLs to remote locations.
func rewritePlaylistLocations(localFilePath, remoteServingEndpoint, pathPrefix string) error { func rewriteRemotePlaylist(localFilePath, remoteServingEndpoint string) error {
f, err := os.Open(localFilePath) // nolint f, err := os.Open(localFilePath) // nolint
if err != nil { if err != nil {
log.Fatalln(err) log.Fatalln(err)
@ -25,14 +25,7 @@ func rewritePlaylistLocations(localFilePath, remoteServingEndpoint, pathPrefix s
} }
for _, item := range p.Variants { for _, item := range p.Variants {
// Determine the final path to this playlist. item.URI = remoteServingEndpoint + filepath.Join("/hls", item.URI)
var finalPath string
if pathPrefix != "" {
finalPath = filepath.Join(pathPrefix, "/hls")
} else {
finalPath = "/hls"
}
item.URI = remoteServingEndpoint + filepath.Join(finalPath, item.URI)
} }
publicPath := filepath.Join(config.HLSStoragePath, filepath.Base(localFilePath)) publicPath := filepath.Join(config.HLSStoragePath, filepath.Base(localFilePath))

64
core/storageproviders/s3Storage.go

@ -8,7 +8,6 @@ import (
"path/filepath" "path/filepath"
"sort" "sort"
"strings" "strings"
"sync"
"time" "time"
"github.com/owncast/owncast/core/data" "github.com/owncast/owncast/core/data"
@ -27,37 +26,30 @@ import (
// S3Storage is the s3 implementation of a storage provider. // S3Storage is the s3 implementation of a storage provider.
type S3Storage struct { type S3Storage struct {
// If we try to upload a playlist but it is not yet on disk
// then keep a reference to it here.
queuedPlaylistUpdates map[string]string
s3Client *s3.S3
uploader *s3manager.Uploader
sess *session.Session sess *session.Session
s3Secret string s3Client *s3.S3
host string
s3Bucket string s3Endpoint string
s3Region string
s3ServingEndpoint string s3ServingEndpoint string
s3Region string
s3Bucket string
s3AccessKey string s3AccessKey string
s3Secret string
s3ACL string s3ACL string
s3PathPrefix string s3ForcePathStyle bool
s3Endpoint string
host string
lock sync.Mutex // If we try to upload a playlist but it is not yet on disk
// then keep a reference to it here.
queuedPlaylistUpdates map[string]string
s3ForcePathStyle bool uploader *s3manager.Uploader
} }
// NewS3Storage returns a new S3Storage instance. // NewS3Storage returns a new S3Storage instance.
func NewS3Storage() *S3Storage { func NewS3Storage() *S3Storage {
return &S3Storage{ return &S3Storage{
queuedPlaylistUpdates: make(map[string]string), queuedPlaylistUpdates: make(map[string]string),
lock: sync.Mutex{},
} }
} }
@ -81,7 +73,6 @@ func (s *S3Storage) Setup() error {
s.s3AccessKey = s3Config.AccessKey s.s3AccessKey = s3Config.AccessKey
s.s3Secret = s3Config.Secret s.s3Secret = s3Config.Secret
s.s3ACL = s3Config.ACL s.s3ACL = s3Config.ACL
s.s3PathPrefix = s3Config.PathPrefix
s.s3ForcePathStyle = s3Config.ForcePathStyle s.s3ForcePathStyle = s3Config.ForcePathStyle
s.sess = s.connectAWS() s.sess = s.connectAWS()
@ -116,7 +107,6 @@ func (s *S3Storage) SegmentWritten(localFilePath string) {
// so the segments and the HLS playlist referencing // so the segments and the HLS playlist referencing
// them are in sync. // them are in sync.
playlistPath := filepath.Join(filepath.Dir(localFilePath), "stream.m3u8") playlistPath := filepath.Join(filepath.Dir(localFilePath), "stream.m3u8")
if _, err := s.Save(playlistPath, 0); err != nil { if _, err := s.Save(playlistPath, 0); err != nil {
s.queuedPlaylistUpdates[playlistPath] = playlistPath s.queuedPlaylistUpdates[playlistPath] = playlistPath
if pErr, ok := err.(*os.PathError); ok { if pErr, ok := err.(*os.PathError); ok {
@ -131,8 +121,6 @@ func (s *S3Storage) VariantPlaylistWritten(localFilePath string) {
// We are uploading the variant playlist after uploading the segment // We are uploading the variant playlist after uploading the segment
// to make sure we're not referring to files in a playlist that don't // to make sure we're not referring to files in a playlist that don't
// yet exist. See SegmentWritten. // yet exist. See SegmentWritten.
s.lock.Lock()
defer s.lock.Unlock()
if _, ok := s.queuedPlaylistUpdates[localFilePath]; ok { if _, ok := s.queuedPlaylistUpdates[localFilePath]; ok {
if _, err := s.Save(localFilePath, 0); err != nil { if _, err := s.Save(localFilePath, 0); err != nil {
log.Errorln(err) log.Errorln(err)
@ -145,7 +133,7 @@ func (s *S3Storage) VariantPlaylistWritten(localFilePath string) {
// MasterPlaylistWritten is called when the master hls playlist is written. // MasterPlaylistWritten is called when the master hls playlist is written.
func (s *S3Storage) MasterPlaylistWritten(localFilePath string) { func (s *S3Storage) MasterPlaylistWritten(localFilePath string) {
// Rewrite the playlist to use absolute remote S3 URLs // Rewrite the playlist to use absolute remote S3 URLs
if err := rewritePlaylistLocations(localFilePath, s.host, s.s3PathPrefix); err != nil { if err := rewriteRemotePlaylist(localFilePath, s.host); err != nil {
log.Warnln(err) log.Warnln(err)
} }
} }
@ -163,12 +151,6 @@ func (s *S3Storage) Save(filePath string, retryCount int) (string, error) {
// Build the remote path by adding the "hls" path prefix. // Build the remote path by adding the "hls" path prefix.
remotePath := strings.Join([]string{"hls", normalizedPath}, "") remotePath := strings.Join([]string{"hls", normalizedPath}, "")
// If a custom path prefix is set prepend it.
if s.s3PathPrefix != "" {
prefix := strings.TrimPrefix(s.s3PathPrefix, "/")
remotePath = strings.Join([]string{prefix, remotePath}, "/")
}
maxAgeSeconds := utils.GetCacheDurationSecondsForPath(filePath) maxAgeSeconds := utils.GetCacheDurationSecondsForPath(filePath)
cacheControlHeader := fmt.Sprintf("max-age=%d", maxAgeSeconds) cacheControlHeader := fmt.Sprintf("max-age=%d", maxAgeSeconds)
@ -202,15 +184,9 @@ func (s *S3Storage) Save(filePath string, retryCount int) (string, error) {
return s.Save(filePath, retryCount+1) return s.Save(filePath, retryCount+1)
} }
// Upload failure. Remove the local file.
s.removeLocalFile(filePath)
return "", fmt.Errorf("Giving up uploading %s to object storage %s", filePath, s.s3Endpoint) return "", fmt.Errorf("Giving up uploading %s to object storage %s", filePath, s.s3Endpoint)
} }
// Upload success. Remove the local file.
s.removeLocalFile(filePath)
return response.Location, nil return response.Location, nil
} }
@ -224,9 +200,7 @@ func (s *S3Storage) Cleanup() error {
return err return err
} }
if len(keys) > 0 {
s.deleteObjects(keys) s.deleteObjects(keys)
}
return nil return nil
} }
@ -267,23 +241,11 @@ func (s *S3Storage) getDeletableVideoSegmentsWithOffset(offset int) ([]s3object,
return nil, err return nil, err
} }
if offset > len(objectsToDelete)-1 {
offset = len(objectsToDelete) - 1
}
objectsToDelete = objectsToDelete[offset : len(objectsToDelete)-1] objectsToDelete = objectsToDelete[offset : len(objectsToDelete)-1]
return objectsToDelete, nil return objectsToDelete, nil
} }
func (s *S3Storage) removeLocalFile(filePath string) {
cleanFilepath := filepath.Clean(filePath)
if err := os.Remove(cleanFilepath); err != nil {
log.Errorln(err)
}
}
func (s *S3Storage) deleteObjects(objects []s3object) { func (s *S3Storage) deleteObjects(objects []s3object) {
keys := make([]*s3.ObjectIdentifier, len(objects)) keys := make([]*s3.ObjectIdentifier, len(objects))
for i, object := range objects { for i, object := range objects {
@ -339,6 +301,6 @@ func (s *S3Storage) retrieveAllVideoSegments() ([]s3object, error) {
} }
type s3object struct { type s3object struct {
lastModified time.Time
key string key string
lastModified time.Time
} }

3
core/streamState.go

@ -69,8 +69,7 @@ func setStreamAsConnected(rtmpOut *io.PipeReader) {
}() }()
go webhooks.SendStreamStatusEvent(models.StreamStarted) go webhooks.SendStreamStatusEvent(models.StreamStarted)
selectedThumbnailVideoQualityIndex, isVideoPassthrough := data.FindHighestVideoQualityIndex(_currentBroadcast.OutputSettings) transcoder.StartThumbnailGenerator(segmentPath, data.FindHighestVideoQualityIndex(_currentBroadcast.OutputSettings))
transcoder.StartThumbnailGenerator(segmentPath, selectedThumbnailVideoQualityIndex, isVideoPassthrough)
_ = chat.SendSystemAction("Stay tuned, the stream is **starting**!", true) _ = chat.SendSystemAction("Stay tuned, the stream is **starting**!", true)
chat.SendAllWelcomeMessage() chat.SendAllWelcomeMessage()

8
core/transcoder/thumbnailGenerator.go

@ -25,7 +25,7 @@ func StopThumbnailGenerator() {
} }
// StartThumbnailGenerator starts generating thumbnails. // StartThumbnailGenerator starts generating thumbnails.
func StartThumbnailGenerator(chunkPath string, variantIndex int, isVideoPassthrough bool) { func StartThumbnailGenerator(chunkPath string, variantIndex int) {
// Every 20 seconds create a thumbnail from the most // Every 20 seconds create a thumbnail from the most
// recent video segment. // recent video segment.
_timer = time.NewTicker(20 * time.Second) _timer = time.NewTicker(20 * time.Second)
@ -36,11 +36,7 @@ func StartThumbnailGenerator(chunkPath string, variantIndex int, isVideoPassthro
select { select {
case <-_timer.C: case <-_timer.C:
if err := fireThumbnailGenerator(chunkPath, variantIndex); err != nil { if err := fireThumbnailGenerator(chunkPath, variantIndex); err != nil {
logMsg := "Unable to generate thumbnail: " + err.Error() log.Errorln("Unable to generate thumbnail:", err)
if isVideoPassthrough {
logMsg += ". Video Passthrough is enabled. You should disable it to fix this, and other, streaming errors. https://owncast.online/troubleshoot"
}
log.Errorln("Unable to generate thumbnail:", logMsg)
} }
case <-quit: case <-quit:
log.Debug("thumbnail generator has stopped") log.Debug("thumbnail generator has stopped")

10
core/transcoder/utils.go

@ -13,10 +13,8 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
var ( var _lastTranscoderLogMessage = ""
_lastTranscoderLogMessage = "" var l = &sync.RWMutex{}
l = &sync.RWMutex{}
)
var errorMap = map[string]string{ var errorMap = map[string]string{
"Unrecognized option 'vaapi_device'": "you are likely trying to utilize a vaapi codec, but your version of ffmpeg or your hardware doesn't support it. change your codec to libx264 and restart your stream", "Unrecognized option 'vaapi_device'": "you are likely trying to utilize a vaapi codec, but your version of ffmpeg or your hardware doesn't support it. change your codec to libx264 and restart your stream",
@ -102,14 +100,14 @@ func createVariantDirectories() {
if len(data.GetStreamOutputVariants()) != 0 { if len(data.GetStreamOutputVariants()) != 0 {
for index := range data.GetStreamOutputVariants() { for index := range data.GetStreamOutputVariants() {
if err := os.MkdirAll(path.Join(config.HLSStoragePath, strconv.Itoa(index)), 0o750); err != nil { if err := os.MkdirAll(path.Join(config.HLSStoragePath, strconv.Itoa(index)), 0750); err != nil {
log.Fatalln(err) log.Fatalln(err)
} }
} }
} else { } else {
dir := path.Join(config.HLSStoragePath, strconv.Itoa(0)) dir := path.Join(config.HLSStoragePath, strconv.Itoa(0))
log.Traceln("Creating", dir) log.Traceln("Creating", dir)
if err := os.MkdirAll(dir, 0o750); err != nil { if err := os.MkdirAll(dir, 0750); err != nil {
log.Fatalln(err) log.Fatalln(err)
} }
} }

311
core/user/externalAPIUser.go

@ -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
}

473
core/user/user.go

@ -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,
}
}

10
core/webhooks/chat.go

@ -43,16 +43,6 @@ func SendChatEventUserJoined(event events.UserJoinedEvent) {
SendEventToWebhooks(webhookEvent) SendEventToWebhooks(webhookEvent)
} }
// SendChatEventUserParted sends a webhook notifying that a user has parted.
func SendChatEventUserParted(event events.UserPartEvent) {
webhookEvent := WebhookEvent{
Type: events.UserParted,
EventData: event,
}
SendEventToWebhooks(webhookEvent)
}
// SendChatEventSetMessageVisibility sends a webhook notifying that the visibility of one or more // SendChatEventSetMessageVisibility sends a webhook notifying that the visibility of one or more
// messages has changed. // messages has changed.
func SendChatEventSetMessageVisibility(event events.SetMessageVisibilityEvent) { func SendChatEventSetMessageVisibility(event events.SetMessageVisibilityEvent) {

3
core/webhooks/webhooks.go

@ -5,7 +5,6 @@ import (
"time" "time"
"github.com/owncast/owncast/core/data" "github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/core/user"
"github.com/owncast/owncast/models" "github.com/owncast/owncast/models"
) )
@ -17,7 +16,7 @@ type WebhookEvent struct {
// WebhookChatMessage represents a single chat message sent as a webhook payload. // WebhookChatMessage represents a single chat message sent as a webhook payload.
type WebhookChatMessage struct { type WebhookChatMessage struct {
User *user.User `json:"user,omitempty"` User *models.User `json:"user,omitempty"`
Timestamp *time.Time `json:"timestamp,omitempty"` Timestamp *time.Time `json:"timestamp,omitempty"`
Body string `json:"body,omitempty"` Body string `json:"body,omitempty"`
RawBody string `json:"rawBody,omitempty"` RawBody string `json:"rawBody,omitempty"`

7
core/webhooks/webhooks_test.go

@ -12,7 +12,6 @@ import (
"testing" "testing"
"time" "time"
"github.com/owncast/owncast/core/chat/events"
"github.com/owncast/owncast/core/data" "github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/models" "github.com/owncast/owncast/models"
jsonpatch "gopkg.in/evanphx/json-patch.v5" jsonpatch "gopkg.in/evanphx/json-patch.v5"
@ -52,7 +51,7 @@ func TestMain(m *testing.M) {
// this test ensures that `SendToWebhooks` without a `WaitGroup` doesn't panic. // this test ensures that `SendToWebhooks` without a `WaitGroup` doesn't panic.
func TestPublicSend(t *testing.T) { func TestPublicSend(t *testing.T) {
// Send enough events to be sure at least one worker delivers a second event. // Send enough events to be sure at least one worker delivers a second event.
eventsCount := webhookWorkerPoolSize + 1 const eventsCount = webhookWorkerPoolSize + 1
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(eventsCount) wg.Add(eventsCount)
@ -85,7 +84,7 @@ func TestPublicSend(t *testing.T) {
// Make sure that events are only sent to interested endpoints. // Make sure that events are only sent to interested endpoints.
func TestRouting(t *testing.T) { func TestRouting(t *testing.T) {
eventTypes := []models.EventType{models.ChatActionSent, models.UserJoined, events.UserParted} eventTypes := []models.EventType{models.ChatActionSent, models.UserJoined}
calls := map[models.EventType]int{} calls := map[models.EventType]int{}
var lock sync.Mutex var lock sync.Mutex
@ -268,7 +267,7 @@ func TestParallel(t *testing.T) {
myId := atomic.AddUint32(&calls, 1) myId := atomic.AddUint32(&calls, 1)
// We made it to the pool size + 1 event, so we're done with the test. // We made it to the pool size + 1 event, so we're done with the test.
if myId == uint32(webhookWorkerPoolSize)+1 { if myId == webhookWorkerPoolSize+1 {
close(finished) close(finished)
return return
} }

11
core/webhooks/workerpool.go

@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"net/http" "net/http"
"runtime"
"sync" "sync"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -13,14 +12,16 @@ import (
"github.com/owncast/owncast/models" "github.com/owncast/owncast/models"
) )
const (
// webhookWorkerPoolSize defines the number of concurrent HTTP webhook requests. // webhookWorkerPoolSize defines the number of concurrent HTTP webhook requests.
var webhookWorkerPoolSize = runtime.GOMAXPROCS(0) webhookWorkerPoolSize = 10
)
// Job struct bundling the webhook and the payload in one struct. // Job struct bundling the webhook and the payload in one struct.
type Job struct { type Job struct {
wg *sync.WaitGroup
payload WebhookEvent
webhook models.Webhook webhook models.Webhook
payload WebhookEvent
wg *sync.WaitGroup
} }
var ( var (
@ -46,7 +47,7 @@ func initWorkerPool() {
func addToQueue(webhook models.Webhook, payload WebhookEvent, wg *sync.WaitGroup) { func addToQueue(webhook models.Webhook, payload WebhookEvent, wg *sync.WaitGroup) {
log.Tracef("Queued Event %s for Webhook %s", payload.Type, webhook.URL) log.Tracef("Queued Event %s for Webhook %s", payload.Type, webhook.URL)
queue <- Job{wg, payload, webhook} queue <- Job{webhook, payload, wg}
} }
func worker(workerID int, queue <-chan Job) { func worker(workerID int, queue <-chan Job) {

2
db/query.sql

@ -101,7 +101,7 @@ UPDATE users SET authenticated_at = CURRENT_TIMESTAMP WHERE id = $1;
SELECT id, body, hidden_at, timestamp FROM messages WHERE eventType = 'CHAT' AND user_id = $1 ORDER BY TIMESTAMP DESC; SELECT id, body, hidden_at, timestamp FROM messages WHERE eventType = 'CHAT' AND user_id = $1 ORDER BY TIMESTAMP DESC;
-- name: IsDisplayNameAvailable :one -- name: IsDisplayNameAvailable :one
SELECT count(*) FROM users WHERE display_name = $1 AND ( type='API' OR authenticated_at IS NOT NULL ) AND disabled_at IS NULL; SELECT count(*) FROM users WHERE display_name = $1 AND authenticated_at is not null AND disabled_at is NULL;
-- name: ChangeDisplayName :exec -- name: ChangeDisplayName :exec
UPDATE users SET display_name = $1, previous_names = previous_names || $2, namechanged_at = $3 WHERE id = $4; UPDATE users SET display_name = $1, previous_names = previous_names || $2, namechanged_at = $3 WHERE id = $4;

4
db/query.sql.go

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.19.1 // sqlc v1.15.0
// source: query.sql // source: query.sql
package db package db
@ -667,7 +667,7 @@ func (q *Queries) GetUserDisplayNameByToken(ctx context.Context, token string) (
} }
const isDisplayNameAvailable = `-- name: IsDisplayNameAvailable :one const isDisplayNameAvailable = `-- name: IsDisplayNameAvailable :one
SELECT count(*) FROM users WHERE display_name = $1 AND ( type='API' OR authenticated_at IS NOT NULL ) AND disabled_at IS NULL SELECT count(*) FROM users WHERE display_name = $1 AND authenticated_at is not null AND disabled_at is NULL
` `
func (q *Queries) IsDisplayNameAvailable(ctx context.Context, displayName string) (int64, error) { func (q *Queries) IsDisplayNameAvailable(ctx context.Context, displayName string) (int64, error) {

14
docs/api/index.html

File diff suppressed because one or more lines are too long

2
geoip/geoip.go

@ -76,7 +76,7 @@ func (c *Client) fetchGeoForIP(ip string) *GeoDetails {
// If no country is available then exit // If no country is available then exit
// If we believe this IP to be anonymous then no reason to report it // If we believe this IP to be anonymous then no reason to report it
if record.Country.IsoCode != "" && !record.Traits.IsAnonymousProxy { if record.Country.IsoCode != "" && !record.Traits.IsAnonymousProxy {
regionName := "Unknown" var regionName = "Unknown"
if len(record.Subdivisions) > 0 { if len(record.Subdivisions) > 0 {
if region, ok := record.Subdivisions[0].Names["en"]; ok { if region, ok := record.Subdivisions[0].Names["en"]; ok {
regionName = region regionName = region

60
go.mod

@ -1,26 +1,26 @@
module github.com/owncast/owncast module github.com/owncast/owncast
go 1.21 go 1.20
require ( require (
github.com/aws/aws-sdk-go v1.49.6 github.com/aws/aws-sdk-go v1.44.273
github.com/go-fed/activity v1.0.1-0.20210803212804-d866ba75dd0f github.com/go-fed/activity v1.0.1-0.20210803212804-d866ba75dd0f
github.com/go-fed/httpsig v1.1.0 github.com/go-fed/httpsig v1.1.0
github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-ole/go-ole v1.2.6 // indirect
github.com/gorilla/websocket v1.5.1 github.com/gorilla/websocket v1.5.0
github.com/grafov/m3u8 v0.12.0 github.com/grafov/m3u8 v0.12.0
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
github.com/mattn/go-sqlite3 v1.14.19 github.com/mattn/go-sqlite3 v1.14.17
github.com/microcosm-cc/bluemonday v1.0.26 github.com/microcosm-cc/bluemonday v1.0.24
github.com/nareix/joy5 v0.0.0-20210317075623-2c912ca30590 github.com/nareix/joy5 v0.0.0-20210317075623-2c912ca30590
github.com/oschwald/geoip2-golang v1.9.0 github.com/oschwald/geoip2-golang v1.8.0
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5
github.com/schollz/sqlite3dump v1.3.1 github.com/schollz/sqlite3dump v1.3.1
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
github.com/teris-io/shortid v0.0.0-20220617161101-71ec9f2aa569 github.com/teris-io/shortid v0.0.0-20220617161101-71ec9f2aa569
github.com/yuin/goldmark v1.6.0 github.com/yuin/goldmark v1.5.4
golang.org/x/mod v0.14.0 golang.org/x/mod v0.10.0
golang.org/x/time v0.5.0 golang.org/x/time v0.3.0
) )
require ( require (
@ -28,55 +28,53 @@ require (
github.com/lestrrat-go/strftime v1.0.4 // indirect github.com/lestrrat-go/strftime v1.0.4 // indirect
github.com/mvdan/xurls v1.1.0 // indirect github.com/mvdan/xurls v1.1.0 // indirect
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/go-sysconf v0.3.11 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect github.com/tklauser/numcpus v0.6.0 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect
golang.org/x/crypto v0.17.0 // indirect golang.org/x/crypto v0.1.0 // indirect
golang.org/x/net v0.19.0 golang.org/x/net v0.10.0
golang.org/x/sys v0.15.0 // indirect golang.org/x/sys v0.8.0 // indirect
) )
require github.com/prometheus/client_golang v1.17.0 require github.com/prometheus/client_golang v1.15.1
require ( require (
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect github.com/golang/protobuf v1.5.3 // indirect
github.com/klauspost/compress v1.16.7 // indirect github.com/klauspost/compress v1.16.4 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.44.0 // indirect github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.11.1 // indirect github.com/prometheus/procfs v0.9.0 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/text v0.9.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect google.golang.org/protobuf v1.30.0 // indirect
) )
require ( require (
github.com/nakabonne/tstorage v0.3.6 github.com/nakabonne/tstorage v0.3.6
github.com/shirou/gopsutil/v3 v3.23.11 github.com/shirou/gopsutil/v3 v3.23.3
) )
require github.com/SherClockHolmes/webpush-go v1.3.0 require github.com/SherClockHolmes/webpush-go v1.2.0
require ( require (
github.com/andybalholm/brotli v1.0.5 // indirect github.com/andybalholm/brotli v1.0.4 // indirect
github.com/aymerick/douceur v0.2.0 // indirect github.com/aymerick/douceur v0.2.0 // indirect
github.com/go-test/deep v1.0.4 // indirect github.com/go-test/deep v1.0.4 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/gorilla/css v1.0.0 // indirect github.com/gorilla/css v1.0.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/oschwald/maxminddb-golang v1.11.0 // indirect github.com/oschwald/maxminddb-golang v1.10.0 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/shoenig/go-m1cpu v0.1.4 // indirect
) )
require ( require (
github.com/CAFxX/httpcompression v0.0.9 github.com/CAFxX/httpcompression v0.0.8
github.com/andybalholm/cascadia v1.3.2 github.com/andybalholm/cascadia v1.3.2
github.com/mssola/user_agent v0.6.0 gopkg.in/evanphx/json-patch.v5 v5.6.0
github.com/yuin/goldmark-emoji v1.0.2
gopkg.in/evanphx/json-patch.v5 v5.7.0
mvdan.cc/xurls v1.1.0 mvdan.cc/xurls v1.1.0
) )

156
go.sum

@ -1,13 +1,13 @@
github.com/CAFxX/httpcompression v0.0.9 h1:0ue2X8dOLEpxTm8tt+OdHcgA+gbDge0OqFQWGKSqgrg= github.com/CAFxX/httpcompression v0.0.8 h1:UBWojERnpCS6X7whJkGGZeCC3ruZBRwkwkcnfGfb0ko=
github.com/CAFxX/httpcompression v0.0.9/go.mod h1:XX8oPZA+4IDcfZ0A71Hz0mZsv/YJOgYygkFhizVPilM= github.com/CAFxX/httpcompression v0.0.8/go.mod h1:bVd1taHK1vYb5SWe9lwNDCqrfj2ka+C1Zx7JHzxuHnU=
github.com/SherClockHolmes/webpush-go v1.3.0 h1:CAu3FvEE9QS4drc3iKNgpBWFfGqNthKlZhp5QpYnu6k= github.com/SherClockHolmes/webpush-go v1.2.0 h1:sGv0/ZWCvb1HUH+izLqrb2i68HuqD/0Y+AmGQfyqKJA=
github.com/SherClockHolmes/webpush-go v1.3.0/go.mod h1:AxRHmJuYwKGG1PVgYzToik1lphQvDnqFYDqimHvwhIw= github.com/SherClockHolmes/webpush-go v1.2.0/go.mod h1:w6X47YApe/B9wUz2Wh8xukxlyupaxSSEbu6yKJcHN2w=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/aws/aws-sdk-go v1.49.6 h1:yNldzF5kzLBRvKlKz1S0bkvc2+04R1kt13KfBWQBfFA= github.com/aws/aws-sdk-go v1.44.273 h1:CX8O0gK+cGrgUyv7bgJ6QQP9mQg7u5mweHdNzULH47c=
github.com/aws/aws-sdk-go v1.49.6/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= github.com/aws/aws-sdk-go v1.44.273/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= 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/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 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@ -30,20 +30,20 @@ github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keL
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/brotli/go/cbrotli v0.0.0-20230829110029-ed738e842d2f h1:jopqB+UTSdJGEJT8tEqYyE29zN91fi2827oLET8tl7k= github.com/google/brotli/go/cbrotli v0.0.0-20210623081221-ce222e317e36 h1:qg5qEpjk1P1EMnInOCpxOpWSPRsspXJDT7P80y/JfFA=
github.com/google/brotli/go/cbrotli v0.0.0-20230829110029-ed738e842d2f/go.mod h1:nOPhAkwVliJdNTkj3gXpljmWhjc4wCaVqbMJcPKWP4s= github.com/google/brotli/go/cbrotli v0.0.0-20210623081221-ce222e317e36/go.mod h1:nOPhAkwVliJdNTkj3gXpljmWhjc4wCaVqbMJcPKWP4s=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= 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/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.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/gorilla/websocket v1.5.0/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 h1:T6iTwTsSEtMcwkayef+FJO8kj+Sglr4Lh81Zj8Ked/4=
github.com/grafov/m3u8 v0.12.0/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080= github.com/grafov/m3u8 v0.12.0/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
@ -52,9 +52,10 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGw
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= github.com/klauspost/compress v1.14.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/compress v1.16.4 h1:91KN02FnsOYhuunwU4ssRe8lc2JosWmizWa91B5v1PU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/klauspost/compress v1.16.4/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8= 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/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= github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible h1:Y6sqxHMyB1D2YSzWkLibYKgg+SwmyFU9dF2hn6MdTj4=
@ -64,28 +65,26 @@ github.com/lestrrat-go/strftime v1.0.4/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR7
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI= github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58= github.com/microcosm-cc/bluemonday v1.0.24 h1:NGQoPtwGVcbGkKfvyYk1yRqknzBuoMiUrO6R7uFTPlw=
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs= github.com/microcosm-cc/bluemonday v1.0.24/go.mod h1:ArQySAMps0790cHSkdPEJ7bGkF2VePWH773hsJNSHf8=
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 h1:OpuDelGQ1R1ueQ6sSryzi6P+1RtBpfQHM8fJwlE45ww=
github.com/mvdan/xurls v1.1.0/go.mod h1:tQlNn3BED8bE/15hnSL2HLkDeLWpNPAwtw7wkEq44oU= github.com/mvdan/xurls v1.1.0/go.mod h1:tQlNn3BED8bE/15hnSL2HLkDeLWpNPAwtw7wkEq44oU=
github.com/nakabonne/tstorage v0.3.6 h1:usp7pTohax8mynnFiUSUQ2QVBCKLCkYx3gmb3+rJo54= github.com/nakabonne/tstorage v0.3.6 h1:usp7pTohax8mynnFiUSUQ2QVBCKLCkYx3gmb3+rJo54=
github.com/nakabonne/tstorage v0.3.6/go.mod h1:1xUrK3s1MXSlU6dn96xHerHx/MdO4BGmsAHEUbsaOxU= 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 h1:PnxRU8L8Y2q82vFC2QdNw23Dm2u6WrjecIdpXjiYbXM=
github.com/nareix/joy5 v0.0.0-20210317075623-2c912ca30590/go.mod h1:XmAOs6UJXpNXRwKk+KY/nv5kL6xXYXyellk+A1pTlko= github.com/nareix/joy5 v0.0.0-20210317075623-2c912ca30590/go.mod h1:XmAOs6UJXpNXRwKk+KY/nv5kL6xXYXyellk+A1pTlko=
github.com/oschwald/geoip2-golang v1.9.0 h1:uvD3O6fXAXs+usU+UGExshpdP13GAqp4GBrzN7IgKZc= github.com/oschwald/geoip2-golang v1.8.0 h1:KfjYB8ojCEn/QLqsDU0AzrJ3R5Qa9vFlx3z6SLNcKTs=
github.com/oschwald/geoip2-golang v1.9.0/go.mod h1:BHK6TvDyATVQhKNbQBdrj9eAvuwOMi2zSFXizL3K81Y= github.com/oschwald/geoip2-golang v1.8.0/go.mod h1:R7bRvYjOeaoenAp9sKRS8GX5bJWcZ0laWO5+DauEktw=
github.com/oschwald/maxminddb-golang v1.11.0 h1:aSXMqYR/EPNjGE8epgqwDay+P30hCBZIveY0WZbAWh0= github.com/oschwald/maxminddb-golang v1.10.0 h1:Xp1u0ZhqkSuopaKmk1WwHtjF0H9Hd9181uj2MQ5Vndg=
github.com/oschwald/maxminddb-golang v1.11.0/go.mod h1:YmVI+H0zh3ySFR3w+oz8PCfglAFj3PuCmui13+P9zDg= github.com/oschwald/maxminddb-golang v1.10.0/go.mod h1:Y2ELenReaLAZ0b400URyGwvYxHV1dLIxBuyOsyYjHK0=
github.com/owncast/activity v1.0.1-0.20211229051252-7821289d4026 h1:E1nxiX44BcMQTSSs8MHLm2rXnqXNedYZkFI31gXMsJc= 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/owncast/activity v1.0.1-0.20211229051252-7821289d4026/go.mod h1:v4QoPaAzjWZ8zN2VFVGL5ep9C02mst0hQYHUpQwso4Q=
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= github.com/pierrec/lz4/v4 v4.1.12 h1:44l88ehTZAUGW4VlO1QC4zkilL99M6Y9MXNwEs0uzP8=
github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.12/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -93,24 +92,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/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 h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI=
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM= github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI= github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY= github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 h1:mZHayPoR0lNmnHyvtYjDeq0zlVHn9K/ZXoy17ylucdo= 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/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5/go.mod h1:GEXHk5HgEKCvEIIrSpFI3ozzG5xOKA2DVlEX/gGnewM=
github.com/schollz/sqlite3dump v1.3.1 h1:QXizJ7XEJ7hggjqjZ3YRtF3+javm8zKtzNByYtEkPRA= github.com/schollz/sqlite3dump v1.3.1 h1:QXizJ7XEJ7hggjqjZ3YRtF3+javm8zKtzNByYtEkPRA=
github.com/schollz/sqlite3dump v1.3.1/go.mod h1:mzSTjZpJH4zAb1FN3iNlhWPbbdyeBpOaTW0hukyMHyI= github.com/schollz/sqlite3dump v1.3.1/go.mod h1:mzSTjZpJH4zAb1FN3iNlhWPbbdyeBpOaTW0hukyMHyI=
github.com/shirou/gopsutil/v3 v3.23.11 h1:i3jP9NjCPUz7FiZKxlMnODZkdSIp2gnzfrvsu9CuWEQ= github.com/shirou/gopsutil/v3 v3.23.3 h1:Syt5vVZXUDXPEXpIBt5ziWsJ4LdSAAxF4l/xZeQgSEE=
github.com/shirou/gopsutil/v3 v3.23.11/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM= github.com/shirou/gopsutil/v3 v3.23.3/go.mod h1:lSBNN6t3+D6W5e5nXTxc8KIMMVxAcS+6IJlffjRRlMU=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.4 h1:SZPIgRM2sEF9NJy50mRHu9PKGwxyyTTJIWvCtgVbozs=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/go-m1cpu v0.1.4/go.mod h1:Wwvst4LR89UxjeFtLRMrpgRiyY4xPsejnVZym39dbAQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= github.com/shoenig/test v0.6.3 h1:GVXWJFk9PiOjN0KoJ7VrJGH6uLPnqxR7/fe3HUPfE0c=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/shoenig/test v0.6.3/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 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= github.com/spf13/cobra v0.0.4-0.20190109003409-7547e83b2d85/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
@ -123,46 +122,42 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/teris-io/shortid v0.0.0-20220617161101-71ec9f2aa569 h1:xzABM9let0HLLqFypcxvLmlvEciCHL7+Lv+4vwZqecI= github.com/teris-io/shortid v0.0.0-20220617161101-71ec9f2aa569 h1:xzABM9let0HLLqFypcxvLmlvEciCHL7+Lv+4vwZqecI=
github.com/teris-io/shortid v0.0.0-20220617161101-71ec9f2aa569/go.mod h1:2Ly+NIftZN4de9zRmENdYbvPQeaVIYKWpLFStLFEBgI= github.com/teris-io/shortid v0.0.0-20220617161101-71ec9f2aa569/go.mod h1:2Ly+NIftZN4de9zRmENdYbvPQeaVIYKWpLFStLFEBgI=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4=
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/valyala/gozstd v1.11.0 h1:VV6qQFt+4sBBj9OJ7eKVvsFAMy59Urcs9Lgd+o5FOw0=
github.com/valyala/gozstd v1.20.1 h1:xPnnnvjmaDDitMFfDxmQ4vpx0+3CdTg2o3lALvXTU/g= github.com/valyala/gozstd v1.11.0/go.mod h1:y5Ew47GLlP37EkTB+B4s7r6A5rdaeB7ftbl9zoYiIPQ=
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.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68= github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU=
github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s= github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY= github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180527072434-ab813273cd59/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/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-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-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.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.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 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-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 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.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -177,28 +172,28 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.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 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.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.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.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-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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 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.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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.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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 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/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 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.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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
@ -207,14 +202,13 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/evanphx/json-patch.v5 v5.7.0 h1:dGKGylPlZ/jus2g1YqhhyzfH0gPy2R8/MYUpW/OslTY= gopkg.in/evanphx/json-patch.v5 v5.6.0 h1:BMT6KIwBD9CaU91PJCZIe46bDmBWa9ynTQgJIOpfQBk=
gopkg.in/evanphx/json-patch.v5 v5.7.0/go.mod h1:/kvTRh1TVm5wuM6OkHxqXtE/1nUZZpihg29RtuIyfvk= gopkg.in/evanphx/json-patch.v5 v5.6.0/go.mod h1:/kvTRh1TVm5wuM6OkHxqXtE/1nUZZpihg29RtuIyfvk=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
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.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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

2
logging/logging.go

@ -34,7 +34,7 @@ func Setup(enableDebugOptions bool, enableVerboseLogging bool) {
// Create the logging directory if needed // Create the logging directory if needed
loggingDirectory := filepath.Dir(getLogFilePath()) loggingDirectory := filepath.Dir(getLogFilePath())
if !utils.DoesFileExists(loggingDirectory) { if !utils.DoesFileExists(loggingDirectory) {
if err := os.Mkdir(loggingDirectory, 0o700); err != nil { if err := os.Mkdir(loggingDirectory, 0700); err != nil {
logger.Errorln("unable to create logs directory", loggingDirectory, err) logger.Errorln("unable to create logs directory", loggingDirectory, err)
} }
} }

6
metrics/metrics.go

@ -10,10 +10,8 @@ import (
) )
// How often we poll for updates. // How often we poll for updates.
const ( const hardwareMetricsPollingInterval = 2 * time.Minute
hardwareMetricsPollingInterval = 2 * time.Minute const playbackMetricsPollingInterval = 2 * time.Minute
playbackMetricsPollingInterval = 2 * time.Minute
)
const ( const (
// How often we poll for updates. // How often we poll for updates.

19
models/externalAPIUser.go

@ -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"`
}

7
models/s3Storage.go

@ -2,20 +2,17 @@ package models
// S3 is the storage configuration. // S3 is the storage configuration.
type S3 struct { type S3 struct {
Enabled bool `json:"enabled"`
Endpoint string `json:"endpoint,omitempty"` Endpoint string `json:"endpoint,omitempty"`
AccessKey string `json:"accessKey,omitempty"` AccessKey string `json:"accessKey,omitempty"`
Secret string `json:"secret,omitempty"` Secret string `json:"secret,omitempty"`
Bucket string `json:"bucket,omitempty"` Bucket string `json:"bucket,omitempty"`
Region string `json:"region,omitempty"` Region string `json:"region,omitempty"`
ACL string `json:"acl,omitempty"` ACL string `json:"acl,omitempty"`
ForcePathStyle bool `json:"forcePathStyle"`
// PathPrefix is an optional prefix for object storage.
PathPrefix string `json:"pathPrefix,omitempty"`
// This property is no longer used as of v0.1.1. See the standalone // This property is no longer used as of v0.1.1. See the standalone
// property that was pulled out of here instead. It's only left here // property that was pulled out of here instead. It's only left here
// to allow the migration to take place without data loss. // to allow the migration to take place without data loss.
ServingEndpoint string `json:"-"` ServingEndpoint string `json:"-"`
Enabled bool `json:"enabled"`
ForcePathStyle bool `json:"forcePathStyle"`
} }

36
models/user.go

@ -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
}

8
openapi.yaml

@ -356,7 +356,7 @@ components:
StreamKey: StreamKey:
type: object type: object
properties: properties:
key: id:
type: string type: string
description: The key used for authing a stream. description: The key used for authing a stream.
example: yklw5Imng example: yklw5Imng
@ -1086,9 +1086,6 @@ paths:
content: content:
application/json: application/json:
schema: schema:
type: object
properties:
value:
$ref: '#/components/schemas/StreamKeyArray' $ref: '#/components/schemas/StreamKeyArray'
/api/admin/config/pagecontent: /api/admin/config/pagecontent:
@ -1106,8 +1103,7 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/ConfigValue' $ref: '#/components/schemas/ConfigValue'
example: example: '# Welcome to my cool server!<br><br>I _hope_ you enjoy it.'
value: '# Welcome to my cool server!<br><br>I _hope_ you enjoy it.'
/api/admin/config/streamtitle: /api/admin/config/streamtitle:
post: post:

23
router/middleware/auth.go

@ -6,16 +6,16 @@ import (
"strings" "strings"
"github.com/owncast/owncast/core/data" "github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/core/user" "github.com/owncast/owncast/models"
"github.com/owncast/owncast/utils" "github.com/owncast/owncast/utils"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
// ExternalAccessTokenHandlerFunc is a function that is called after validing access. // ExternalAccessTokenHandlerFunc is a function that is called after validing access.
type ExternalAccessTokenHandlerFunc func(user.ExternalAPIUser, http.ResponseWriter, *http.Request) type ExternalAccessTokenHandlerFunc func(models.ExternalAPIUser, http.ResponseWriter, *http.Request)
// UserAccessTokenHandlerFunc is a function that is called after validing user access. // UserAccessTokenHandlerFunc is a function that is called after validing user access.
type UserAccessTokenHandlerFunc func(user.User, http.ResponseWriter, *http.Request) type UserAccessTokenHandlerFunc func(models.User, http.ResponseWriter, *http.Request)
// RequireAdminAuth wraps a handler requiring HTTP basic auth for it using the given // RequireAdminAuth wraps a handler requiring HTTP basic auth for it using the given
// the stream key as the password and and a hardcoded "admin" for username. // the stream key as the password and and a hardcoded "admin" for username.
@ -25,9 +25,11 @@ func RequireAdminAuth(handler http.HandlerFunc) http.HandlerFunc {
password := data.GetAdminPassword() password := data.GetAdminPassword()
realm := "Owncast Authenticated Request" realm := "Owncast Authenticated Request"
// Alow CORS only for localhost:3000 to support Owncast development. // The following line is kind of a work around.
validAdminHost := "http://localhost:3000" // If you want HTTP Basic Auth + Cors it requires _explicit_ origins to be provided in the
w.Header().Set("Access-Control-Allow-Origin", validAdminHost) // Access-Control-Allow-Origin header. So we just pull out the origin header and specify it.
// If we want to lock down admin APIs to not be CORS accessible for anywhere, this is where we would do that.
w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin"))
w.Header().Set("Access-Control-Allow-Credentials", "true") w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization") w.Header().Set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization")
@ -67,13 +69,10 @@ func RequireExternalAPIAccessToken(scope string, handler ExternalAccessTokenHand
return return
} }
authHeader := r.Header.Get("Authorization") authHeader := strings.Split(r.Header.Get("Authorization"), "Bearer ")
token := "" token := strings.Join(authHeader, "")
if strings.HasPrefix(strings.ToLower(authHeader), "bearer ") {
token = authHeader[len("bearer "):]
}
if token == "" { if len(authHeader) == 0 || token == "" {
log.Warnln("invalid access token") log.Warnln("invalid access token")
accessDenied(w) accessDenied(w)
return return

83
static/metadata.html.tmpl vendored

@ -1,83 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{{.Name}}</title>
<meta name="description" content="{{.Summary}}">
<meta property="og:title" content="{{.Name}}">
<meta property="og:site_name" content="{{.Name}}">
<meta property="og:url" content="{{.RequestedURL}}">
<meta property="og:description" content="{{.Summary}}">
<meta property="og:type" content="video.other">
<meta property="video:tag" content="{{.TagsString}}">
<meta property="og:image" content="{{.Thumbnail}}">
<meta property="og:image:url" content="{{.Thumbnail}}">
<meta property="og:image:alt" content="{{.Image}}">
<meta property="og:video" content='{{.RequestedURL}}embed/video' />
<meta property="og:video:secure_url" content='{{.RequestedURL}}embed/video' />
<meta property="og:video:height" content="315" />
<meta property="og:video:width" content="560" />
<meta property="og:video:type" content="text/html" />
<meta property="og:video:actor" content="{{.Name}}" />
<meta property="twitter:title" content="{{.Name}}">
<meta property="twitter:url" content="{{.RequestedURL}}">
<meta property="twitter:description" content="{{.Summary}}">
<meta property="twitter:image" content="{{.Image}}">
<meta property="twitter:card" content="player" />
<meta property="twitter:player" content='{{.RequestedURL}}embed/video' />
<meta property="twitter:player:width" content="560" />
<meta property="twitter:player:height" content="315" />
<link rel="apple-touch-icon" sizes="57x57" href="/img/favicon/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="/img/favicon/apple-icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="/img/favicon/apple-icon-72x72.png">
<link rel="apple-touch-icon" sizes="76x76" href="/img/favicon/apple-icon-76x76.png">
<link rel="apple-touch-icon" sizes="114x114" href="/img/favicon/apple-icon-114x114.png">
<link rel="apple-touch-icon" sizes="120x120" href="/img/favicon/apple-icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="/img/favicon/apple-icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="/img/favicon/apple-icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="/img/favicon/apple-icon-180x180.png">
<link rel="icon" type="image/png" sizes="192x192" href="/img/favicon/android-icon-192x192.png">
<link rel="icon" type="image/png" sizes="32x32" href="/img/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="96x96" href="/img/favicon/favicon-96x96.png">
<link rel="icon" type="image/png" sizes="16x16" href="/img/favicon/favicon-16x16.png">
<link rel="manifest" href="/manifest.json">
<link rel="authorization_endpoint" href="/api/auth/provider/indieauth">
<meta name="msapplication-TileColor" content="#ffffff">
<meta name="msapplication-TileImage" content="/img/favicon/ms-icon-144x144.png">
<meta name="theme-color" content="#ffffff">
</head>
<body>
<h1>{{.Name}}</h1>
<center>
<img src="{{.Thumbnail}}" width=10% />
</center>
<h3>{{.Summary}}</h3>
{{range .Tags}}
<li>{{.}}</li>
{{end}}
<br/>
<h3>Links for {{.Name}}:</h3>
{{range .SocialHandles}}
<li><a href="{{.URL}}">{{.Platform}}</a></li>
{{end}}
</body>
</html>

11
static/static.go vendored

@ -76,14 +76,3 @@ func getFileSystemStaticFileOrDefault(path string, defaultData []byte) []byte {
return data return data
} }
//go:embed metadata.html.tmpl
var botMetadataTemplate embed.FS
// GetBotMetadataTemplate will return the bot/scraper metadata template.
func GetBotMetadataTemplate() (*template.Template, error) {
name := "metadata.html.tmpl"
t, err := template.ParseFS(botMetadataTemplate, name)
tmpl := template.Must(t, err)
return tmpl, err
}

6
static/web/404.html vendored

File diff suppressed because one or more lines are too long

6
static/web/404/index.html vendored

File diff suppressed because one or more lines are too long

1
static/web/_next/static/7FO45oyNxons-CT00qbSN/_buildManifest.js

File diff suppressed because one or more lines are too long

1
static/web/_next/static/_EwXAWpD9Ghec1YlZX6x3/_buildManifest.js

File diff suppressed because one or more lines are too long

0
static/web/_next/static/7FO45oyNxons-CT00qbSN/_ssgManifest.js → static/web/_next/static/_EwXAWpD9Ghec1YlZX6x3/_ssgManifest.js

1
static/web/_next/static/chunks/1008.34cc20ecda8c2f89.js vendored

@ -0,0 +1 @@
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[1008],{98696: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:"M816 768h-24V428c0-141.1-104.3-257.7-240-277.1V112c0-22.1-17.9-40-40-40s-40 17.9-40 40v38.9c-135.7 19.4-240 136-240 277.1v340h-24c-17.7 0-32 14.3-32 32v32c0 4.4 3.6 8 8 8h216c0 61.8 50.2 112 112 112s112-50.2 112-112h216c4.4 0 8-3.6 8-8v-32c0-17.7-14.3-32-32-32zM512 888c-26.5 0-48-21.5-48-48h96c0 26.5-21.5 48-48 48zM304 768V428c0-55.6 21.6-107.8 60.9-147.1S456.4 220 512 220c55.6 0 107.8 21.6 147.1 60.9S720 372.4 720 428v340H304z"}}]},name:"bell",theme:"outlined"}},11008:function(e,t,r){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a,n=(a=r(25594))&&a.__esModule?a:{default:a};t.default=n,e.exports=n},25594: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)),f=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!==n(e)&&"function"!=typeof e)return{default:e};var r=o(t);if(r&&r.has(e))return r.get(e);var a={},u=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var f in e)if("default"!==f&&Object.prototype.hasOwnProperty.call(e,f)){var l=u?Object.getOwnPropertyDescriptor(e,f):null;l&&(l.get||l.set)?Object.defineProperty(a,f,l):a[f]=e[f]}return a.default=e,r&&r.set(e,a),a}(r(67294)),l=a(r(98696)),c=a(r(92074));function o(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,r=new WeakMap;return(o=function(e){return e?r:t})(e)}var i=function(e,t){return f.createElement(c.default,(0,u.default)((0,u.default)({},e),{},{ref:t,icon:l.default}))};i.displayName="BellOutlined";var d=f.forwardRef(i);t.default=d}}]);

1
static/web/_next/static/chunks/1008.65d0bc27255efb44.js vendored

@ -1 +0,0 @@
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[1008],{98696: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:"M816 768h-24V428c0-141.1-104.3-257.7-240-277.1V112c0-22.1-17.9-40-40-40s-40 17.9-40 40v38.9c-135.7 19.4-240 136-240 277.1v340h-24c-17.7 0-32 14.3-32 32v32c0 4.4 3.6 8 8 8h216c0 61.8 50.2 112 112 112s112-50.2 112-112h216c4.4 0 8-3.6 8-8v-32c0-17.7-14.3-32-32-32zM512 888c-26.5 0-48-21.5-48-48h96c0 26.5-21.5 48-48 48zM304 768V428c0-55.6 21.6-107.8 60.9-147.1S456.4 220 512 220c55.6 0 107.8 21.6 147.1 60.9S720 372.4 720 428v340H304z"}}]},name:"bell",theme:"outlined"}},11008:function(e,t,r){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var u=_interopRequireDefault(r(25594));function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}t.default=u,e.exports=u},25594:function(e,t,r){var u=r(64836),a=r(18698);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var n=u(r(42122)),l=_interopRequireWildcard(r(67294)),i=u(r(98696)),c=u(r(92074));function _getRequireWildcardCache(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,r=new WeakMap;return(_getRequireWildcardCache=function(e){return e?r:t})(e)}function _interopRequireWildcard(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!==a(e)&&"function"!=typeof e)return{default:e};var r=_getRequireWildcardCache(t);if(r&&r.has(e))return r.get(e);var u={},n=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var l in e)if("default"!==l&&Object.prototype.hasOwnProperty.call(e,l)){var i=n?Object.getOwnPropertyDescriptor(e,l):null;i&&(i.get||i.set)?Object.defineProperty(u,l,i):u[l]=e[l]}return u.default=e,r&&r.set(e,u),u}var BellOutlined=function(e,t){return l.createElement(c.default,(0,n.default)((0,n.default)({},e),{},{ref:t,icon:i.default}))};BellOutlined.displayName="BellOutlined";var f=l.forwardRef(BellOutlined);t.default=f}}]);

1
static/web/_next/static/chunks/1010.398d7f6d64350bec.js vendored

@ -1 +0,0 @@
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[1010],{90034: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:"M180 292h80v440h-80zm369 180h-74a3 3 0 00-3 3v74a3 3 0 003 3h74a3 3 0 003-3v-74a3 3 0 00-3-3zm215-108h80v296h-80z",fill:t}},{tag:"path",attrs:{d:"M904 296h-66v-96c0-4.4-3.6-8-8-8h-52c-4.4 0-8 3.6-8 8v96h-66c-4.4 0-8 3.6-8 8v416c0 4.4 3.6 8 8 8h66v96c0 4.4 3.6 8 8 8h52c4.4 0 8-3.6 8-8v-96h66c4.4 0 8-3.6 8-8V304c0-4.4-3.6-8-8-8zm-60 364h-80V364h80v296zM612 404h-66V232c0-4.4-3.6-8-8-8h-52c-4.4 0-8 3.6-8 8v172h-66c-4.4 0-8 3.6-8 8v200c0 4.4 3.6 8 8 8h66v172c0 4.4 3.6 8 8 8h52c4.4 0 8-3.6 8-8V620h66c4.4 0 8-3.6 8-8V412c0-4.4-3.6-8-8-8zm-60 145a3 3 0 01-3 3h-74a3 3 0 01-3-3v-74a3 3 0 013-3h74a3 3 0 013 3v74zM320 224h-66v-56c0-4.4-3.6-8-8-8h-52c-4.4 0-8 3.6-8 8v56h-66c-4.4 0-8 3.6-8 8v560c0 4.4 3.6 8 8 8h66v56c0 4.4 3.6 8 8 8h52c4.4 0 8-3.6 8-8v-56h66c4.4 0 8-3.6 8-8V232c0-4.4-3.6-8-8-8zm-60 508h-80V292h80v440z",fill:e}}]}},name:"sliders",theme:"twotone"}},71010:function(e,t,r){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a=_interopRequireDefault(r(54626));function _interopRequireDefault(e){return e&&e.__esModule?e:{default:e}}t.default=a,e.exports=a},54626:function(e,t,r){var a=r(64836),c=r(18698);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var n=a(r(42122)),u=_interopRequireWildcard(r(67294)),i=a(r(90034)),o=a(r(92074));function _getRequireWildcardCache(e){if("function"!=typeof WeakMap)return null;var t=new WeakMap,r=new WeakMap;return(_getRequireWildcardCache=function(e){return e?r:t})(e)}function _interopRequireWildcard(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!==c(e)&&"function"!=typeof e)return{default:e};var r=_getRequireWildcardCache(t);if(r&&r.has(e))return r.get(e);var a={},n=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var u in e)if("default"!==u&&Object.prototype.hasOwnProperty.call(e,u)){var i=n?Object.getOwnPropertyDescriptor(e,u):null;i&&(i.get||i.set)?Object.defineProperty(a,u,i):a[u]=e[u]}return a.default=e,r&&r.set(e,a),a}var SlidersTwoTone=function(e,t){return u.createElement(o.default,(0,n.default)((0,n.default)({},e),{},{ref:t,icon:i.default}))};SlidersTwoTone.displayName="SlidersTwoTone";var l=u.forwardRef(SlidersTwoTone);t.default=l}}]);

1
static/web/_next/static/chunks/1010.a223916b6c5495db.js vendored

@ -0,0 +1 @@
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[1010],{90034: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:"M180 292h80v440h-80zm369 180h-74a3 3 0 00-3 3v74a3 3 0 003 3h74a3 3 0 003-3v-74a3 3 0 00-3-3zm215-108h80v296h-80z",fill:t}},{tag:"path",attrs:{d:"M904 296h-66v-96c0-4.4-3.6-8-8-8h-52c-4.4 0-8 3.6-8 8v96h-66c-4.4 0-8 3.6-8 8v416c0 4.4 3.6 8 8 8h66v96c0 4.4 3.6 8 8 8h52c4.4 0 8-3.6 8-8v-96h66c4.4 0 8-3.6 8-8V304c0-4.4-3.6-8-8-8zm-60 364h-80V364h80v296zM612 404h-66V232c0-4.4-3.6-8-8-8h-52c-4.4 0-8 3.6-8 8v172h-66c-4.4 0-8 3.6-8 8v200c0 4.4 3.6 8 8 8h66v172c0 4.4 3.6 8 8 8h52c4.4 0 8-3.6 8-8V620h66c4.4 0 8-3.6 8-8V412c0-4.4-3.6-8-8-8zm-60 145a3 3 0 01-3 3h-74a3 3 0 01-3-3v-74a3 3 0 013-3h74a3 3 0 013 3v74zM320 224h-66v-56c0-4.4-3.6-8-8-8h-52c-4.4 0-8 3.6-8 8v56h-66c-4.4 0-8 3.6-8 8v560c0 4.4 3.6 8 8 8h66v56c0 4.4 3.6 8 8 8h52c4.4 0 8-3.6 8-8v-56h66c4.4 0 8-3.6 8-8V232c0-4.4-3.6-8-8-8zm-60 508h-80V292h80v440z",fill:e}}]}},name:"sliders",theme:"twotone"}},71010:function(e,t,r){Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var a,c=(a=r(54626))&&a.__esModule?a:{default:a};t.default=c,e.exports=c},54626:function(e,t,r){var a=r(64836),c=r(18698);Object.defineProperty(t,"__esModule",{value:!0}),t.default=void 0;var n=a(r(42122)),u=function(e,t){if(!t&&e&&e.__esModule)return e;if(null===e||"object"!==c(e)&&"function"!=typeof e)return{default:e};var r=l(t);if(r&&r.has(e))return r.get(e);var a={},n=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var u in e)if("default"!==u&&Object.prototype.hasOwnProperty.call(e,u)){var f=n?Object.getOwnPropertyDescriptor(e,u):null;f&&(f.get||f.set)?Object.defineProperty(a,u,f):a[u]=e[u]}return a.default=e,r&&r.set(e,a),a}(r(67294)),f=a(r(90034)),o=a(r(92074));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 h=function(e,t){return u.createElement(o.default,(0,n.default)((0,n.default)({},e),{},{ref:t,icon:f.default}))};h.displayName="SlidersTwoTone";var v=u.forwardRef(h);t.default=v}}]);

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save