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. 24
      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. 18
      build/web/bundleWeb.sh
  39. 2
      config/constants.go
  40. 4
      config/verifyInstall.go
  41. 2
      contrib/owncast_for_windows.md
  42. 12
      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. 139
      core/chat/events/events.go
  53. 4
      core/chat/events/eventtype.go
  54. 17
      core/chat/events/userPartEvent.go
  55. 25
      core/chat/messageRendering_test.go
  56. 3
      core/chat/persistence.go
  57. 80
      core/chat/server.go
  58. 3
      core/core.go
  59. 111
      core/data/config.go
  60. 2
      core/data/configEntry.go
  61. 6
      core/data/data_test.go
  62. 84
      core/data/emoji.go
  63. 10
      core/data/types.go
  64. 6
      core/rtmp/rtmp.go
  65. 16
      core/storageproviders/local.go
  66. 13
      core/storageproviders/rewriteLocalPlaylist.go
  67. 66
      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. 15
      core/webhooks/webhooks.go
  75. 7
      core/webhooks/webhooks_test.go
  76. 13
      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. 19
      models/s3Storage.go
  87. 36
      models/user.go
  88. 10
      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 @@ @@ -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 @@ @@ -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.
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
Fixes # (issue)
@ -16,4 +14,4 @@ Some things you might want to mention: @@ -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?
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 @@ @@ -1 +0,0 @@
name: Go config

4
.github/codeql/javascript.yml

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

2
.github/workflows/actions-lint.yml

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

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

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

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

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

10
.github/workflows/browser-testing.yml

@ -20,9 +20,9 @@ jobs: @@ -20,9 +20,9 @@ jobs:
concurrent_skipping: 'same_content_newer'
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- uses: actions/setup-node@v4
- uses: actions/setup-node@v3
with:
node-version: 18.9.0
@ -38,13 +38,13 @@ jobs: @@ -38,13 +38,13 @@ jobs:
${{ runner.os }}-build-
${{ runner.os }}-
- uses: actions/setup-go@v5
- uses: actions/setup-go@v4
with:
go-version: '1.21'
go-version: '1.20'
cache: true
- 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
uses: nick-fields/retry@v2

2
.github/workflows/build-storybook.yml

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

44
.github/workflows/bundle-web.yml

@ -0,0 +1,44 @@ @@ -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: @@ -29,7 +29,7 @@ jobs:
concurrent_skipping: 'same_content_newer'
- name: Check out code
if: ${{ github.actor != 'renovate[bot]' && github.actor != 'renovate' }}
uses: actions/checkout@v4
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 }}

9
.github/workflows/codeql-analysis.yml

@ -37,14 +37,13 @@ jobs: @@ -37,14 +37,13 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v2
with:
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.
# 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.
@ -53,7 +52,7 @@ jobs: @@ -53,7 +52,7 @@ jobs:
# 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)
- name: Autobuild
uses: github/codeql-action/autobuild@v3
uses: github/codeql-action/autobuild@v2
# ℹ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@ -67,4 +66,4 @@ jobs: @@ -67,4 +66,4 @@ jobs:
# make release
- 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: @@ -19,7 +19,7 @@ jobs:
container:
image: aquasec/trivy
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
- name: Check critical issues
run: trivy config --exit-code 1 --severity "HIGH,CRITICAL" ./Dockerfile

4
.github/workflows/container.yaml

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

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

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

8
.github/workflows/go-lint.yml

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

28
.github/workflows/go-tests.yaml

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

6
.github/workflows/hls-tests.yml

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

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

@ -1,185 +0,0 @@ @@ -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 @@ @@ -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: @@ -18,9 +18,9 @@ jobs:
concurrent_skipping: 'same_content_newer'
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- uses: actions/setup-node@v4
- uses: actions/setup-node@v3
with:
node-version: 18.9.0

6
.github/workflows/screenshots.yml

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

4
.github/workflows/shellcheck.yml

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

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

@ -0,0 +1,50 @@ @@ -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 @@ -42,7 +42,6 @@ test/automated/browser/screenshots
lefthook.yml
test/automated/browser/cypress/screenshots
test/automated/browser/cypress/videos
web/style-definitions/build/
web/public/sw.js
web/public/workbox-*.js

4
.golangci.yml

@ -5,7 +5,7 @@ run: @@ -5,7 +5,7 @@ run:
# Define the Go version limit.
# 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
go: '1.21'
go: '1.20'
issues:
# The linter has a default list of ignorable errors. Turning this on will enable that list.
@ -69,7 +69,7 @@ linters-settings: @@ -69,7 +69,7 @@ linters-settings:
gosimple:
# Select the Go version to target. The default is '1.13'.
go: '1.21'
go: '1.20'
# https://staticcheck.io/docs/options#checks
checks: ['all']

2
Dockerfile

@ -22,7 +22,7 @@ ENV NAME=${NAME} @@ -22,7 +22,7 @@ ENV NAME=${NAME}
RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -ldflags "-extldflags \"-static\" -s -w -X github.com/owncast/owncast/config.GitCommit=$GIT_COMMIT -X github.com/owncast/owncast/config.VersionNumber=$VERSION -X github.com/owncast/owncast/config.BuildPlatform=$NAME" -o owncast .
# Create the image by copying the result of the build into a new alpine image
FROM alpine:3.19.0
FROM alpine:3.18.0
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

24
Earthfile

@ -6,10 +6,10 @@ ARG version=develop @@ -6,10 +6,10 @@ ARG version=develop
WORKDIR /build
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:
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:
BUILD --platform=linux/amd64 --platform=linux/386 --platform=linux/arm64 --platform=linux/arm/v7 +docker
@ -36,6 +36,7 @@ build: @@ -36,6 +36,7 @@ build:
FROM --platform=linux/amd64 +code
RUN echo $EARTHLY_GIT_HASH
RUN echo "Finding CC configuration for $TARGETPLATFORM"
IF [ "$TARGETPLATFORM" = "linux/amd64" ]
ARG NAME=linux-64bit
@ -58,10 +59,6 @@ build: @@ -58,10 +59,6 @@ build:
ARG NAME=macOS-64bit
ARG CC=o64-clang
ARG CXX=o64-clang++
ELSE IF [ "$TARGETPLATFORM" = "darwin/arm64" ]
ARG NAME=macOS-arm64
ARG CC=o64-clang
ARG CXX=o64-clang++
ELSE
RUN echo "Failed to find CC configuration for $TARGETPLATFORM"
ARG --required CC
@ -79,13 +76,10 @@ build: @@ -79,13 +76,10 @@ build:
# 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
# Decrease the size of the shipped binary. But only for non-Apple platforms.
# See https://github.com/upx/upx/issues/612
IF [ "$GOOS" != "darwin" ]
RUN upx --best --lzma owncast
# Test the binary
RUN upx -t owncast
END
# Decrease the size of the shipped binary
RUN upx --best --lzma owncast
# Test the binary
RUN upx -t owncast
SAVE ARTIFACT owncast owncast
@ -103,8 +97,6 @@ package: @@ -103,8 +97,6 @@ package:
ARG NAME=linux-arm7
ELSE IF [ "$TARGETPLATFORM" = "darwin/amd64" ]
ARG NAME=macOS-64bit
ELSE IF [ "$TARGETPLATFORM" = "darwin/arm64" ]
ARG NAME=macOS-arm64
ELSE
ARG NAME=custom
END
@ -112,7 +104,7 @@ package: @@ -112,7 +104,7 @@ package:
COPY (+build/owncast --platform $TARGETPLATFORM) /build/dist/owncast
ENV ZIPNAME owncast-$version-$NAME.zip
RUN cd /build/dist && zip -r -q -8 /build/dist/owncast.zip .
SAVE ARTIFACT --keep-ts /build/dist/owncast.zip owncast.zip AS LOCAL dist/$ZIPNAME
SAVE ARTIFACT /build/dist/owncast.zip owncast.zip AS LOCAL dist/$ZIPNAME
docker:
# 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 @@ -48,8 +48,8 @@ Owncast is an open source, self-hosted, decentralized, single user live video st
<div>
<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">
<img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/owncast/owncast?style=for-the-badge">
<a href="https://hub.docker.com/r/gabekangas/owncast">
<img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/gabekangas/owncast?style=for-the-badge">
</a>
<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">
@ -95,7 +95,7 @@ The Owncast backend is a service written in Go. @@ -95,7 +95,7 @@ The Owncast backend is a service written in Go.
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/)
- [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. `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.

8
activitypub/inbox/like.go

@ -13,14 +13,6 @@ import ( @@ -13,14 +13,6 @@ import (
func handleLikeRequest(c context.Context, activity vocab.ActivityStreamsLike) error {
object := activity.GetActivityStreamsObject()
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()
actorIRI := actorReference.At(0).GetIRI().String()

10
activitypub/inbox/workerpool.go

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

2
activitypub/outbox/outbox.go

@ -60,7 +60,7 @@ func SendLive() error { @@ -60,7 +60,7 @@ func SendLive() error {
if title := data.GetStreamTitle(); 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)

8
activitypub/webfinger/webfinger.go

@ -2,13 +2,10 @@ package webfinger @@ -2,13 +2,10 @@ package webfinger
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/owncast/owncast/utils"
)
// GetWebfingerLinks will return webfinger data for an account.
@ -21,11 +18,6 @@ func GetWebfingerLinks(account string) ([]map[string]interface{}, error) { @@ -21,11 +18,6 @@ func GetWebfingerLinks(account string) ([]map[string]interface{}, error) {
accountComponents := strings.Split(account, "@")
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.
requestURL, err := url.Parse("https://" + fediverseServer)
if err != nil {

9
activitypub/workerpool/outbound.go

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

18
auth/indieauth/client.go

@ -12,7 +12,6 @@ import ( @@ -12,7 +12,6 @@ import (
"time"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/utils"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)
@ -47,27 +46,10 @@ func setupExpiredRequestPruner() { @@ -47,27 +46,10 @@ func setupExpiredRequestPruner() {
// StartAuthFlow will begin the IndieAuth flow by generating an auth request.
func StartAuthFlow(authHost, userID, accessToken, displayName string) (*url.URL, error) {
// Limit the number of pending requests
if len(pendingAuthRequests) >= maxPendingRequests {
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()
if serverURL == "" {
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 { @@ -40,7 +40,7 @@ type ServerProfileResponse struct {
var pendingServerAuthRequests = map[string]ServerAuthRequest{}
const maxPendingRequests = 100
const maxPendingRequests = 1000
// StartServerAuth will handle the authentication for the admin user of this
// Owncast server. Initiated via a GET of the auth endpoint.

6
auth/persistence.go

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

18
build/web/bundleWeb.sh

@ -5,29 +5,17 @@ set -o errexit @@ -5,29 +5,17 @@ set -o errexit
set -o nounset
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
cd "$(git rev-parse --show-toplevel)"
cd web
if [ ! "$OFFLINE" ]; then
echo "Installing npm modules for the owncast web..."
npm --silent install 2>/dev/null
fi
echo "Installing npm modules for the owncast web..."
npm --silent install 2>/dev/null
echo "Building owncast web..."
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..."

2
config/constants.go

@ -4,7 +4,7 @@ import "path/filepath" @@ -4,7 +4,7 @@ import "path/filepath"
const (
// 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 = "v4.1.5" // Requires the v
// DataDirectory is the directory we save data to.

4
config/verifyInstall.go

@ -29,8 +29,8 @@ func VerifyFFMpegPath(path string) error { @@ -29,8 +29,8 @@ func VerifyFFMpegPath(path string) error {
}
mode := stat.Mode()
// source: https://stackoverflow.com/a/60128480
if mode&0o111 == 0 {
//source: https://stackoverflow.com/a/60128480
if mode&0111 == 0 {
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 -> @@ -43,7 +43,7 @@ Here is the list for all the prerequisites required ->
- npm (Node Package Manager) is installed as `sudo apt install npm`.
- Node.js is installed (LTS Version) `sudo apt install nodejs`.
- [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

12
controllers/admin/chat.go

@ -31,7 +31,6 @@ func UpdateMessageVisibility(w http.ResponseWriter, r *http.Request) { @@ -31,7 +31,6 @@ func UpdateMessageVisibility(w http.ResponseWriter, r *http.Request) {
}
if r.Method != controllers.POST {
// nolint:goconst
controllers.WriteSimpleResponse(w, false, r.Method+" not supported")
return
}
@ -165,17 +164,12 @@ func UpdateUserEnabled(w http.ResponseWriter, r *http.Request) { @@ -165,17 +164,12 @@ func UpdateUserEnabled(w http.ResponseWriter, r *http.Request) {
disconnectedUser := user.GetUserByID(request.UserID)
_ = 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.
for _, client := range clients {
ipAddress := client.IPAddress
if ipAddress != localIP4Address && ipAddress != localIP6Address {
reason := fmt.Sprintf("Banning of %s", disconnectedUser.DisplayName)
if err := data.BanIPAddress(ipAddress, reason); err != nil {
log.Errorln("error banning IP address: ", err)
}
reason := fmt.Sprintf("Banning of %s", disconnectedUser.DisplayName)
if err := data.BanIPAddress(ipAddress, reason); err != nil {
log.Errorln("error banning IP address: ", err)
}
}
}

21
controllers/admin/config.go

@ -5,7 +5,6 @@ import ( @@ -5,7 +5,6 @@ import (
"fmt"
"net"
"net/http"
"net/netip"
"os"
"path/filepath"
"reflect"
@ -407,14 +406,6 @@ func SetServerURL(w http.ResponseWriter, r *http.Request) { @@ -407,14 +406,6 @@ func SetServerURL(w http.ResponseWriter, r *http.Request) {
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
serverURL := strings.TrimRight(rawValue, "/")
@ -859,18 +850,6 @@ func SetStreamKeys(w http.ResponseWriter, r *http.Request) { @@ -859,18 +850,6 @@ func SetStreamKeys(w http.ResponseWriter, r *http.Request) {
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 {
controllers.WriteSimpleResponse(w, false, err.Error())
return

2
controllers/admin/serverConfig.go

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

1
controllers/chat.go

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

14
controllers/config.go

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

3
controllers/emoji.go

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

98
controllers/index.go

@ -4,18 +4,13 @@ import ( @@ -4,18 +4,13 @@ import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"path/filepath"
"strings"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/core"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/router/middleware"
"github.com/owncast/owncast/static"
"github.com/owncast/owncast/utils"
log "github.com/sirupsen/logrus"
)
// IndexHandler handles the default index route.
@ -29,13 +24,6 @@ func IndexHandler(w http.ResponseWriter, r *http.Request) { @@ -29,13 +24,6 @@ func IndexHandler(w http.ResponseWriter, r *http.Request) {
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
middleware.SetCachingHeaders(w, r)
@ -63,7 +51,6 @@ func renderIndexHtml(w http.ResponseWriter, nonce string) { @@ -63,7 +51,6 @@ func renderIndexHtml(w http.ResponseWriter, nonce string) {
Image string
StatusJSON string
ServerConfigJSON string
EmbedVideo string
Nonce string
}
@ -84,14 +71,13 @@ func renderIndexHtml(w http.ResponseWriter, nonce string) { @@ -84,14 +71,13 @@ func renderIndexHtml(w http.ResponseWriter, nonce string) {
content := serverSideContent{
Name: data.GetServerName(),
Summary: data.GetServerSummary(),
RequestedURL: fmt.Sprintf("%s%s", data.GetServerURL(), "/"),
RequestedURL: data.GetServerURL(),
TagsString: strings.Join(data.GetServerMetadataTags(), ","),
ThumbnailURL: "thumbnail.jpg",
Thumbnail: "thumbnail.jpg",
Image: "logo/external",
ThumbnailURL: "/thumbnail.jpg",
Thumbnail: "/thumbnail.jpg",
Image: "/logo/external",
StatusJSON: string(sb),
ServerConfigJSON: string(cb),
EmbedVideo: "embed/video",
Nonce: nonce,
}
@ -105,79 +91,3 @@ func renderIndexHtml(w http.ResponseWriter, nonce string) { @@ -105,79 +91,3 @@ func renderIndexHtml(w http.ResponseWriter, nonce string) {
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 ( @@ -13,8 +13,8 @@ import (
"github.com/gorilla/websocket"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/core/chat/events"
"github.com/owncast/owncast/core/user"
"github.com/owncast/owncast/geoip"
"github.com/owncast/owncast/models"
)
// Client represents a single chat client.
@ -23,7 +23,7 @@ type Client struct { @@ -23,7 +23,7 @@ type Client struct {
timeoutTimer *time.Timer
rateLimiter *rate.Limiter
conn *websocket.Conn
User *user.User `json:"user"`
User *models.User `json:"user"`
server *Server
Geo *geoip.GeoDetails `json:"geo"`
// Buffered channel of outbound messages.

19
core/chat/events.go

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

4
core/chat/events/connectedClientInfo.go

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

139
core/chat/events/events.go

@ -4,23 +4,16 @@ import ( @@ -4,23 +4,16 @@ import (
"bytes"
"regexp"
"strings"
"sync"
"text/template"
"time"
"github.com/microcosm-cc/bluemonday"
"github.com/owncast/owncast/models"
"github.com/teris-io/shortid"
"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/renderer/html"
"github.com/yuin/goldmark/util"
"mvdan.cc/xurls"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/core/user"
log "github.com/sirupsen/logrus"
)
@ -42,9 +35,9 @@ type Event struct { @@ -42,9 +35,9 @@ type Event struct {
// UserEvent is an event with an associated user.
type UserEvent struct {
User *user.User `json:"user"`
HiddenAt *time.Time `json:"hiddenAt,omitempty"`
ClientID uint `json:"clientId,omitempty"`
User *models.User `json:"user"`
HiddenAt *time.Time `json:"hiddenAt,omitempty"`
ClientID uint `json:"clientId,omitempty"`
}
// MessageEvent is an event that has a message body.
@ -73,105 +66,6 @@ func (e *UserMessageEvent) SetDefaults() { @@ -73,105 +66,6 @@ func (e *UserMessageEvent) SetDefaults() {
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
// the message into something safe and renderable for clients.
func (m *MessageEvent) RenderAndSanitizeMessageBody() {
@ -204,11 +98,6 @@ func RenderAndSanitize(raw string) string { @@ -204,11 +98,6 @@ func RenderAndSanitize(raw string) string {
// RenderMarkdown will return HTML rendered from the string body of a chat message.
func RenderMarkdown(raw string) string {
loadEmoji()
emojiMu.Lock()
defer emojiMu.Unlock()
markdown := goldmark.New(
goldmark.WithRendererOptions(
html.WithUnsafe(),
@ -223,16 +112,6 @@ func RenderMarkdown(raw string) string { @@ -223,16 +112,6 @@ func RenderMarkdown(raw string) string {
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])
}),
),
),
)
@ -246,9 +125,9 @@ func RenderMarkdown(raw string) string { @@ -246,9 +125,9 @@ func RenderMarkdown(raw string) string {
}
var (
_sanitizeReSrcMatch = regexp.MustCompile(`(?i)^/img/emoji/[^\.%]*.[A-Z]*$`)
_sanitizeReClassMatch = regexp.MustCompile(`(?i)^(emoji)[A-Z_]*?$`)
_sanitizeNonEmptyMatch = regexp.MustCompile(`^.+$`)
_sanitizeReSrcMatch = regexp.MustCompile(`(?i)^/img/emoji/[^\.%]*.[A-Z]*$`)
_sanitizeReAltTitleMatch = regexp.MustCompile(`:\S+:`)
_sanitizeReClassMatch = regexp.MustCompile(`(?i)^(emoji)[A-Z_]*?$`)
)
func sanitize(raw string) string {
@ -270,11 +149,11 @@ func sanitize(raw string) string { @@ -270,11 +149,11 @@ func sanitize(raw string) string {
// Allow breaks
p.AllowElements("br")
p.AllowElements("p")
p.AllowElementsContent("p")
// Allow img tags from the the local emoji directory only
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")
// Allow bold

4
core/chat/events/eventtype.go

@ -8,8 +8,6 @@ const ( @@ -8,8 +8,6 @@ const (
MessageSent EventType = "CHAT"
// UserJoined is the event sent when a chat user join action takes place.
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 EventType = "NAME_CHANGE"
// UserColorChanged is the event sent when a chat user color change takes place.
@ -35,7 +33,7 @@ const ( @@ -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 EventType = "CHAT_ACTION"
// 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 EventType = "ERROR_MAX_CONNECTIONS_EXCEEDED"
// ErrorUserDisabled is an error returned when the connecting user has been previously banned/disabled.

17
core/chat/events/userPartEvent.go

@ -1,17 +0,0 @@ @@ -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,
}
}

25
core/chat/messageRendering_test.go

@ -10,17 +10,20 @@ import ( @@ -10,17 +10,20 @@ import (
// and fully rendered HTML out of it.
func TestRenderAndSanitize(t *testing.T) {
messageContent := `
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>
## blah blah blah
[test link](http://owncast.online)
<img class="emoji" src="/img/emoji/bananadance.gif">`
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>
## blah blah blah
[test link](http://owncast.online)
<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>.
Here is an iframe</p>
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
blah blah blah
<p><a href="http://owncast.online" rel="nofollow noreferrer noopener" target="_blank">test link</a>
<img class="emoji" src="/img/emoji/bananadance.gif"></p>`
<a href="http://owncast.online" rel="nofollow noreferrer noopener" target="_blank">test link</a>
<img class="emoji" src="/img/emoji/bananadance.gif">`
result := events.RenderAndSanitize(messageContent)
if result != expected {
@ -31,7 +34,7 @@ blah blah blah @@ -31,7 +34,7 @@ blah blah blah
// Test to make sure we block remote images in chat messages.
func TestBlockRemoteImages(t *testing.T) {
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)
if result != expected {
@ -42,7 +45,7 @@ func TestBlockRemoteImages(t *testing.T) { @@ -42,7 +45,7 @@ func TestBlockRemoteImages(t *testing.T) {
// Test to make sure emoji images are allowed in chat messages.
func TestAllowEmojiImages(t *testing.T) {
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)
if result != expected {

3
core/chat/persistence.go

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

80
core/chat/server.go

@ -14,14 +14,18 @@ import ( @@ -14,14 +14,18 @@ import (
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/core/chat/events"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/core/user"
"github.com/owncast/owncast/core/webhooks"
"github.com/owncast/owncast/geoip"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/storage"
"github.com/owncast/owncast/utils"
)
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.
type Server struct {
clients map[uint]*Client
@ -35,10 +39,7 @@ type Server struct { @@ -35,10 +39,7 @@ type Server struct {
// unregister requests from clients.
unregister chan uint // the ChatClient id
geoipClient *geoip.Client
// a map of user IDs and timers that fire for chat part messages.
userPartedTimers map[string]*time.Ticker
geoipClient *geoip.Client
seq uint
maxSocketConnectionLimit int64
@ -57,7 +58,6 @@ func NewChat() *Server { @@ -57,7 +58,6 @@ func NewChat() *Server {
unregister: make(chan uint),
maxSocketConnectionLimit: maximumConcurrentConnectionLimit,
geoipClient: geoip.NewClient(),
userPartedTimers: map[string]*time.Ticker{},
}
return server
@ -68,8 +68,7 @@ func (s *Server) Run() { @@ -68,8 +68,7 @@ func (s *Server) Run() {
for {
select {
case clientID := <-s.unregister:
if client, ok := s.clients[clientID]; ok {
s.handleClientDisconnected(client)
if _, ok := s.clients[clientID]; ok {
s.mu.Lock()
delete(s.clients, clientID)
s.mu.Unlock()
@ -82,7 +81,7 @@ func (s *Server) Run() { @@ -82,7 +81,7 @@ func (s *Server) Run() {
}
// Addclient registers new connection as a User.
func (s *Server) Addclient(conn *websocket.Conn, user *user.User, accessToken string, userAgent string, ipAddress string) *Client {
func (s *Server) Addclient(conn *websocket.Conn, user *models.User, accessToken string, userAgent string, ipAddress string) *Client {
client := &Client{
server: s,
conn: conn,
@ -94,22 +93,18 @@ func (s *Server) Addclient(conn *websocket.Conn, user *user.User, accessToken st @@ -94,22 +93,18 @@ func (s *Server) Addclient(conn *websocket.Conn, user *user.User, accessToken st
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()
if previouslyLastSeen, ok := _lastSeenCache[user.ID]; ok && time.Since(previouslyLastSeen) < time.Minute*10 {
shouldSendJoinedMessages = false
}
s.mu.Lock()
{
// 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
}
client.Id = s.seq
s.clients[client.Id] = client
s.seq++
_lastSeenCache[user.ID] = time.Now()
}
s.mu.Unlock()
@ -149,43 +144,16 @@ func (s *Server) sendUserJoinedMessage(c *Client) { @@ -149,43 +144,16 @@ func (s *Server) sendUserJoinedMessage(c *Client) {
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 {
log.Debugln("Deleting", 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.
@ -232,8 +200,10 @@ func (s *Server) HandleClientConnection(w http.ResponseWriter, r *http.Request) @@ -232,8 +200,10 @@ func (s *Server) HandleClientConnection(w http.ResponseWriter, r *http.Request)
return
}
userRepository := storage.GetUserRepository()
// A user is required to use the websocket
user := user.GetUserByToken(accessToken)
user := userRepository.GetUserByToken(accessToken)
if user == nil {
// Send error that registration is required
_ = conn.WriteJSON(events.EventPayload{
@ -328,8 +298,10 @@ func SendConnectedClientInfoToUser(userID string) error { @@ -328,8 +298,10 @@ func SendConnectedClientInfoToUser(userID string) error {
return err
}
userRepository := storage.GetUserRepository()
// Get an updated reference to the user.
user := user.GetUserByID(userID)
user := userRepository.GetUserByID(userID)
if user == nil {
return fmt.Errorf("user not found")
}

3
core/core.go

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

111
core/data/config.go

@ -16,54 +16,53 @@ import ( @@ -16,54 +16,53 @@ import (
)
const (
extraContentKey = "extra_page_content"
streamTitleKey = "stream_title"
adminPasswordKey = "admin_password_key"
logoPathKey = "logo_path"
logoUniquenessKey = "logo_uniqueness"
serverSummaryKey = "server_summary"
serverWelcomeMessageKey = "server_welcome_message"
serverNameKey = "server_name"
serverURLKey = "server_url"
httpPortNumberKey = "http_port_number"
httpListenAddressKey = "http_listen_address"
websocketHostOverrideKey = "websocket_host_override"
rtmpPortNumberKey = "rtmp_port_number"
serverMetadataTagsKey = "server_metadata_tags"
directoryEnabledKey = "directory_enabled"
directoryRegistrationKeyKey = "directory_registration_key"
socialHandlesKey = "social_handles"
peakViewersSessionKey = "peak_viewers_session"
peakViewersOverallKey = "peak_viewers_overall"
lastDisconnectTimeKey = "last_disconnect_time"
ffmpegPathKey = "ffmpeg_path"
nsfwKey = "nsfw"
s3StorageConfigKey = "s3_storage_config"
videoLatencyLevel = "video_latency_level"
videoStreamOutputVariantsKey = "video_stream_output_variants"
chatDisabledKey = "chat_disabled"
externalActionsKey = "external_actions"
customStylesKey = "custom_styles"
customJavascriptKey = "custom_javascript"
videoCodecKey = "video_codec"
blockedUsernamesKey = "blocked_usernames"
publicKeyKey = "public_key"
privateKeyKey = "private_key"
serverInitDateKey = "server_init_date"
federationEnabledKey = "federation_enabled"
federationUsernameKey = "federation_username"
federationPrivateKey = "federation_private"
federationGoLiveMessageKey = "federation_go_live_message"
federationShowEngagementKey = "federation_show_engagement"
federationBlockedDomainsKey = "federation_blocked_domains"
suggestedUsernamesKey = "suggested_usernames"
chatJoinMessagesEnabledKey = "chat_join_messages_enabled"
chatEstablishedUsersOnlyModeKey = "chat_established_users_only_mode"
notificationsEnabledKey = "notifications_enabled"
discordConfigurationKey = "discord_configuration"
browserPushConfigurationKey = "browser_push_configuration"
browserPushPublicKeyKey = "browser_push_public_key"
// nolint:gosec
extraContentKey = "extra_page_content"
streamTitleKey = "stream_title"
adminPasswordKey = "admin_password_key"
logoPathKey = "logo_path"
logoUniquenessKey = "logo_uniqueness"
serverSummaryKey = "server_summary"
serverWelcomeMessageKey = "server_welcome_message"
serverNameKey = "server_name"
serverURLKey = "server_url"
httpPortNumberKey = "http_port_number"
httpListenAddressKey = "http_listen_address"
websocketHostOverrideKey = "websocket_host_override"
rtmpPortNumberKey = "rtmp_port_number"
serverMetadataTagsKey = "server_metadata_tags"
directoryEnabledKey = "directory_enabled"
directoryRegistrationKeyKey = "directory_registration_key"
socialHandlesKey = "social_handles"
peakViewersSessionKey = "peak_viewers_session"
peakViewersOverallKey = "peak_viewers_overall"
lastDisconnectTimeKey = "last_disconnect_time"
ffmpegPathKey = "ffmpeg_path"
nsfwKey = "nsfw"
s3StorageConfigKey = "s3_storage_config"
videoLatencyLevel = "video_latency_level"
videoStreamOutputVariantsKey = "video_stream_output_variants"
chatDisabledKey = "chat_disabled"
externalActionsKey = "external_actions"
customStylesKey = "custom_styles"
customJavascriptKey = "custom_javascript"
videoCodecKey = "video_codec"
blockedUsernamesKey = "blocked_usernames"
publicKeyKey = "public_key"
privateKeyKey = "private_key"
serverInitDateKey = "server_init_date"
federationEnabledKey = "federation_enabled"
federationUsernameKey = "federation_username"
federationPrivateKey = "federation_private"
federationGoLiveMessageKey = "federation_go_live_message"
federationShowEngagementKey = "federation_show_engagement"
federationBlockedDomainsKey = "federation_blocked_domains"
suggestedUsernamesKey = "suggested_usernames"
chatJoinMessagesEnabledKey = "chat_join_messages_enabled"
chatEstablishedUsersOnlyModeKey = "chat_established_users_only_mode"
notificationsEnabledKey = "notifications_enabled"
discordConfigurationKey = "discord_configuration"
browserPushConfigurationKey = "browser_push_configuration"
browserPushPublicKeyKey = "browser_push_public_key"
browserPushPrivateKeyKey = "browser_push_private_key"
hasConfiguredInitialNotificationsKey = "has_configured_initial_notifications"
hideViewerCountKey = "hide_viewer_count"
@ -620,19 +619,19 @@ func VerifySettings() error { @@ -620,19 +619,19 @@ func VerifySettings() error {
}
// 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 {
quality models.StreamOutputVariant
index int
quality models.StreamOutputVariant
}
if len(qualities) < 2 {
return 0, qualities[0].IsVideoPassthrough
return 0
}
indexedQualities := make([]IndexedQuality, 0)
for index, quality := range qualities {
indexedQuality := IndexedQuality{quality, index}
indexedQuality := IndexedQuality{index, quality}
indexedQualities = append(indexedQualities, indexedQuality)
}
@ -648,9 +647,7 @@ func FindHighestVideoQualityIndex(qualities []models.StreamOutputVariant) (int, @@ -648,9 +647,7 @@ func FindHighestVideoQualityIndex(qualities []models.StreamOutputVariant) (int,
return indexedQualities[a].quality.VideoBitrate > indexedQualities[b].quality.VideoBitrate
})
// nolint:gosec
selectedQuality := indexedQualities[0]
return selectedQuality.index, selectedQuality.quality.IsVideoPassthrough
return indexedQualities[0].index
}
// GetForbiddenUsernameList will return the blocked usernames as a comma separated string.
@ -817,8 +814,8 @@ func SetChatJoinMessagesEnabled(enabled bool) error { @@ -817,8 +814,8 @@ func SetChatJoinMessagesEnabled(enabled bool) error {
return _datastore.SetBool(chatJoinMessagesEnabledKey, enabled)
}
// GetChatJoinPartMessagesEnabled will return if chat join messages are enabled.
func GetChatJoinPartMessagesEnabled() bool {
// GetChatJoinMessagesEnabled will return if chat join messages are enabled.
func GetChatJoinMessagesEnabled() bool {
enabled, err := _datastore.GetBool(chatJoinMessagesEnabledKey)
if err != nil {
return true

2
core/data/configEntry.go

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

6
core/data/data_test.go

@ -20,8 +20,6 @@ func TestString(t *testing.T) { @@ -20,8 +20,6 @@ func TestString(t *testing.T) {
const testKey = "test string key"
const testValue = "test string value"
fmt.Println(testKey, testValue)
if err := _datastore.SetString(testKey, testValue); err != nil {
panic(err)
}
@ -89,7 +87,7 @@ func TestCustomType(t *testing.T) { @@ -89,7 +87,7 @@ func TestCustomType(t *testing.T) {
}
// 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)
}
@ -121,7 +119,7 @@ func TestStringMap(t *testing.T) { @@ -121,7 +119,7 @@ func TestStringMap(t *testing.T) {
}
// 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)
}

84
core/data/emoji.go

@ -6,8 +6,6 @@ import ( @@ -6,8 +6,6 @@ import (
"io/fs"
"os"
"path/filepath"
"sync"
"time"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/models"
@ -17,81 +15,29 @@ import ( @@ -17,81 +15,29 @@ import (
log "github.com/sirupsen/logrus"
)
var (
emojiCacheMu sync.Mutex
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)
if emojiFS == nil {
return modTime, fmt.Errorf("unable to open custom emoji directory")
}
emojiCacheData = make([]models.CustomEmoji, 0)
walkFunction := func(path string, d os.DirEntry, err error) error {
if d == nil || d.IsDir() {
return nil
}
// GetEmojiList returns a list of custom emoji from the emoji directory.
func GetEmojiList() []models.CustomEmoji {
emojiFS := os.DirFS(config.CustomEmojiPath)
emojiPath := filepath.Join(config.EmojiDir, path)
fileName := d.Name()
fileBase := fileName[:len(fileName)-len(filepath.Ext(fileName))]
singleEmoji := models.CustomEmoji{Name: fileBase, URL: emojiPath}
emojiCacheData = append(emojiCacheData, singleEmoji)
return nil
}
emojiResponse := make([]models.CustomEmoji, 0)
if err := fs.WalkDir(emojiFS, ".", walkFunction); err != nil {
log.Errorln("unable to fetch emojis: " + err.Error())
}
walkFunction := func(path string, d os.DirEntry, err error) error {
if d.IsDir() {
return nil
}
}
return modTime, nil
}
// GetEmojiList returns a list of custom emoji from the emoji directory.
func GetEmojiList() []models.CustomEmoji {
_, err := UpdateEmojiList(false)
if err != nil {
emojiPath := filepath.Join(config.EmojiDir, path)
singleEmoji := models.CustomEmoji{Name: d.Name(), URL: emojiPath}
emojiResponse = append(emojiResponse, singleEmoji)
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)
if err := fs.WalkDir(emojiFS, ".", walkFunction); err != nil {
log.Errorln("unable to fetch emojis: " + err.Error())
return emojiResponse
}
return emojiData
return emojiResponse
}
// 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) { @@ -11,7 +11,7 @@ func (ds *Datastore) GetStringSlice(key string) ([]string, error) {
// SetStringSlice will set the string slice value for a key.
func (ds *Datastore) SetStringSlice(key string, value []string) error {
configEntry := ConfigEntry{value, key}
configEntry := ConfigEntry{key, value}
return ds.Save(configEntry)
}
@ -26,7 +26,7 @@ func (ds *Datastore) GetString(key string) (string, error) { @@ -26,7 +26,7 @@ func (ds *Datastore) GetString(key string) (string, error) {
// SetString will set the string value for a key.
func (ds *Datastore) SetString(key string, value string) error {
configEntry := ConfigEntry{value, key}
configEntry := ConfigEntry{key, value}
return ds.Save(configEntry)
}
@ -41,7 +41,7 @@ func (ds *Datastore) GetNumber(key string) (float64, error) { @@ -41,7 +41,7 @@ func (ds *Datastore) GetNumber(key string) (float64, error) {
// SetNumber will set the numeric value for a key.
func (ds *Datastore) SetNumber(key string, value float64) error {
configEntry := ConfigEntry{value, key}
configEntry := ConfigEntry{key, value}
return ds.Save(configEntry)
}
@ -56,7 +56,7 @@ func (ds *Datastore) GetBool(key string) (bool, error) { @@ -56,7 +56,7 @@ func (ds *Datastore) GetBool(key string) (bool, error) {
// SetBool will set the boolean value for a key.
func (ds *Datastore) SetBool(key string, value bool) error {
configEntry := ConfigEntry{value, key}
configEntry := ConfigEntry{key, value}
return ds.Save(configEntry)
}
@ -71,6 +71,6 @@ func (ds *Datastore) GetStringMap(key string) (map[string]string, error) { @@ -71,6 +71,6 @@ func (ds *Datastore) GetStringMap(key string) (map[string]string, error) {
// SetStringMap will set the string map value for a key.
func (ds *Datastore) SetStringMap(key string, value map[string]string) error {
configEntry := ConfigEntry{value, key}
configEntry := ConfigEntry{key, value}
return ds.Save(configEntry)
}

6
core/rtmp/rtmp.go

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

16
core/storageproviders/local.go

@ -13,9 +13,7 @@ import ( @@ -13,9 +13,7 @@ import (
)
// LocalStorage represents an instance of the local storage provider for HLS video.
type LocalStorage struct {
host string
}
type LocalStorage struct{}
// NewLocalStorage returns a new LocalStorage instance.
func NewLocalStorage() *LocalStorage {
@ -24,7 +22,6 @@ func NewLocalStorage() *LocalStorage { @@ -24,7 +22,6 @@ func NewLocalStorage() *LocalStorage {
// Setup configures this storage provider.
func (s *LocalStorage) Setup() error {
s.host = data.GetVideoServingEndpoint()
return nil
}
@ -45,15 +42,8 @@ func (s *LocalStorage) VariantPlaylistWritten(localFilePath string) { @@ -45,15 +42,8 @@ func (s *LocalStorage) VariantPlaylistWritten(localFilePath string) {
// MasterPlaylistWritten is called when the master hls playlist is written.
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 {
log.Warnln(err)
}
if _, err := s.Save(localFilePath, 0); err != nil {
log.Warnln(err)
}
}

13
core/storageproviders/rewriteLocalPlaylist.go

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

66
core/storageproviders/s3Storage.go

@ -8,7 +8,6 @@ import ( @@ -8,7 +8,6 @@ import (
"path/filepath"
"sort"
"strings"
"sync"
"time"
"github.com/owncast/owncast/core/data"
@ -27,37 +26,30 @@ import ( @@ -27,37 +26,30 @@ import (
// S3Storage is the s3 implementation of a storage provider.
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
s3Secret string
s3Client *s3.S3
host string
s3Bucket string
s3Region string
s3Endpoint string
s3ServingEndpoint string
s3Region string
s3Bucket string
s3AccessKey string
s3Secret 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.
func NewS3Storage() *S3Storage {
return &S3Storage{
queuedPlaylistUpdates: make(map[string]string),
lock: sync.Mutex{},
}
}
@ -81,7 +73,6 @@ func (s *S3Storage) Setup() error { @@ -81,7 +73,6 @@ func (s *S3Storage) Setup() error {
s.s3AccessKey = s3Config.AccessKey
s.s3Secret = s3Config.Secret
s.s3ACL = s3Config.ACL
s.s3PathPrefix = s3Config.PathPrefix
s.s3ForcePathStyle = s3Config.ForcePathStyle
s.sess = s.connectAWS()
@ -116,7 +107,6 @@ func (s *S3Storage) SegmentWritten(localFilePath string) { @@ -116,7 +107,6 @@ func (s *S3Storage) SegmentWritten(localFilePath string) {
// so the segments and the HLS playlist referencing
// them are in sync.
playlistPath := filepath.Join(filepath.Dir(localFilePath), "stream.m3u8")
if _, err := s.Save(playlistPath, 0); err != nil {
s.queuedPlaylistUpdates[playlistPath] = playlistPath
if pErr, ok := err.(*os.PathError); ok {
@ -131,8 +121,6 @@ func (s *S3Storage) VariantPlaylistWritten(localFilePath string) { @@ -131,8 +121,6 @@ func (s *S3Storage) VariantPlaylistWritten(localFilePath string) {
// 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
// yet exist. See SegmentWritten.
s.lock.Lock()
defer s.lock.Unlock()
if _, ok := s.queuedPlaylistUpdates[localFilePath]; ok {
if _, err := s.Save(localFilePath, 0); err != nil {
log.Errorln(err)
@ -145,7 +133,7 @@ func (s *S3Storage) VariantPlaylistWritten(localFilePath string) { @@ -145,7 +133,7 @@ func (s *S3Storage) VariantPlaylistWritten(localFilePath string) {
// MasterPlaylistWritten is called when the master hls playlist is written.
func (s *S3Storage) MasterPlaylistWritten(localFilePath string) {
// 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)
}
}
@ -163,12 +151,6 @@ func (s *S3Storage) Save(filePath string, retryCount int) (string, error) { @@ -163,12 +151,6 @@ func (s *S3Storage) Save(filePath string, retryCount int) (string, error) {
// Build the remote path by adding the "hls" path prefix.
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)
cacheControlHeader := fmt.Sprintf("max-age=%d", maxAgeSeconds)
@ -202,15 +184,9 @@ func (s *S3Storage) Save(filePath string, retryCount int) (string, error) { @@ -202,15 +184,9 @@ func (s *S3Storage) Save(filePath string, retryCount int) (string, error) {
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)
}
// Upload success. Remove the local file.
s.removeLocalFile(filePath)
return response.Location, nil
}
@ -224,9 +200,7 @@ func (s *S3Storage) Cleanup() error { @@ -224,9 +200,7 @@ func (s *S3Storage) Cleanup() error {
return err
}
if len(keys) > 0 {
s.deleteObjects(keys)
}
s.deleteObjects(keys)
return nil
}
@ -267,23 +241,11 @@ func (s *S3Storage) getDeletableVideoSegmentsWithOffset(offset int) ([]s3object, @@ -267,23 +241,11 @@ func (s *S3Storage) getDeletableVideoSegmentsWithOffset(offset int) ([]s3object,
return nil, err
}
if offset > len(objectsToDelete)-1 {
offset = len(objectsToDelete) - 1
}
objectsToDelete = objectsToDelete[offset : len(objectsToDelete)-1]
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) {
keys := make([]*s3.ObjectIdentifier, len(objects))
for i, object := range objects {
@ -339,6 +301,6 @@ func (s *S3Storage) retrieveAllVideoSegments() ([]s3object, error) { @@ -339,6 +301,6 @@ func (s *S3Storage) retrieveAllVideoSegments() ([]s3object, error) {
}
type s3object struct {
lastModified time.Time
key string
lastModified time.Time
}

3
core/streamState.go

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

8
core/transcoder/thumbnailGenerator.go

@ -25,7 +25,7 @@ func StopThumbnailGenerator() { @@ -25,7 +25,7 @@ func StopThumbnailGenerator() {
}
// 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
// recent video segment.
_timer = time.NewTicker(20 * time.Second)
@ -36,11 +36,7 @@ func StartThumbnailGenerator(chunkPath string, variantIndex int, isVideoPassthro @@ -36,11 +36,7 @@ func StartThumbnailGenerator(chunkPath string, variantIndex int, isVideoPassthro
select {
case <-_timer.C:
if err := fireThumbnailGenerator(chunkPath, variantIndex); err != nil {
logMsg := "Unable to generate thumbnail: " + err.Error()
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)
log.Errorln("Unable to generate thumbnail:", err)
}
case <-quit:
log.Debug("thumbnail generator has stopped")

10
core/transcoder/utils.go

@ -13,10 +13,8 @@ import ( @@ -13,10 +13,8 @@ import (
log "github.com/sirupsen/logrus"
)
var (
_lastTranscoderLogMessage = ""
l = &sync.RWMutex{}
)
var _lastTranscoderLogMessage = ""
var l = &sync.RWMutex{}
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",
@ -102,14 +100,14 @@ func createVariantDirectories() { @@ -102,14 +100,14 @@ func createVariantDirectories() {
if len(data.GetStreamOutputVariants()) != 0 {
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)
}
}
} else {
dir := path.Join(config.HLSStoragePath, strconv.Itoa(0))
log.Traceln("Creating", dir)
if err := os.MkdirAll(dir, 0o750); err != nil {
if err := os.MkdirAll(dir, 0750); err != nil {
log.Fatalln(err)
}
}

311
core/user/externalAPIUser.go

@ -1,311 +0,0 @@ @@ -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 @@ @@ -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) { @@ -43,16 +43,6 @@ func SendChatEventUserJoined(event events.UserJoinedEvent) {
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
// messages has changed.
func SendChatEventSetMessageVisibility(event events.SetMessageVisibilityEvent) {

15
core/webhooks/webhooks.go

@ -5,7 +5,6 @@ import ( @@ -5,7 +5,6 @@ import (
"time"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/core/user"
"github.com/owncast/owncast/models"
)
@ -17,13 +16,13 @@ type WebhookEvent struct { @@ -17,13 +16,13 @@ type WebhookEvent struct {
// WebhookChatMessage represents a single chat message sent as a webhook payload.
type WebhookChatMessage struct {
User *user.User `json:"user,omitempty"`
Timestamp *time.Time `json:"timestamp,omitempty"`
Body string `json:"body,omitempty"`
RawBody string `json:"rawBody,omitempty"`
ID string `json:"id,omitempty"`
ClientID uint `json:"clientId,omitempty"`
Visible bool `json:"visible"`
User *models.User `json:"user,omitempty"`
Timestamp *time.Time `json:"timestamp,omitempty"`
Body string `json:"body,omitempty"`
RawBody string `json:"rawBody,omitempty"`
ID string `json:"id,omitempty"`
ClientID uint `json:"clientId,omitempty"`
Visible bool `json:"visible"`
}
// SendEventToWebhooks will send a single webhook event to all webhook destinations.

7
core/webhooks/webhooks_test.go

@ -12,7 +12,6 @@ import ( @@ -12,7 +12,6 @@ import (
"testing"
"time"
"github.com/owncast/owncast/core/chat/events"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/models"
jsonpatch "gopkg.in/evanphx/json-patch.v5"
@ -52,7 +51,7 @@ func TestMain(m *testing.M) { @@ -52,7 +51,7 @@ func TestMain(m *testing.M) {
// this test ensures that `SendToWebhooks` without a `WaitGroup` doesn't panic.
func TestPublicSend(t *testing.T) {
// 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
wg.Add(eventsCount)
@ -85,7 +84,7 @@ func TestPublicSend(t *testing.T) { @@ -85,7 +84,7 @@ func TestPublicSend(t *testing.T) {
// Make sure that events are only sent to interested endpoints.
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{}
var lock sync.Mutex
@ -268,7 +267,7 @@ func TestParallel(t *testing.T) { @@ -268,7 +267,7 @@ func TestParallel(t *testing.T) {
myId := atomic.AddUint32(&calls, 1)
// 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)
return
}

13
core/webhooks/workerpool.go

@ -4,7 +4,6 @@ import ( @@ -4,7 +4,6 @@ import (
"bytes"
"encoding/json"
"net/http"
"runtime"
"sync"
log "github.com/sirupsen/logrus"
@ -13,14 +12,16 @@ import ( @@ -13,14 +12,16 @@ import (
"github.com/owncast/owncast/models"
)
// webhookWorkerPoolSize defines the number of concurrent HTTP webhook requests.
var webhookWorkerPoolSize = runtime.GOMAXPROCS(0)
const (
// webhookWorkerPoolSize defines the number of concurrent HTTP webhook requests.
webhookWorkerPoolSize = 10
)
// Job struct bundling the webhook and the payload in one struct.
type Job struct {
wg *sync.WaitGroup
payload WebhookEvent
webhook models.Webhook
payload WebhookEvent
wg *sync.WaitGroup
}
var (
@ -46,7 +47,7 @@ func initWorkerPool() { @@ -46,7 +47,7 @@ func initWorkerPool() {
func addToQueue(webhook models.Webhook, payload WebhookEvent, wg *sync.WaitGroup) {
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) {

2
db/query.sql

@ -101,7 +101,7 @@ UPDATE users SET authenticated_at = CURRENT_TIMESTAMP WHERE id = $1; @@ -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;
-- 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
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 @@ @@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.19.1
// sqlc v1.15.0
// source: query.sql
package db
@ -667,7 +667,7 @@ func (q *Queries) GetUserDisplayNameByToken(ctx context.Context, token string) ( @@ -667,7 +667,7 @@ func (q *Queries) GetUserDisplayNameByToken(ctx context.Context, token string) (
}
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) {

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

60
go.mod

@ -1,26 +1,26 @@ @@ -1,26 +1,26 @@
module github.com/owncast/owncast
go 1.21
go 1.20
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/httpsig v1.1.0
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/gorilla/websocket v1.5.1
github.com/gorilla/websocket v1.5.0
github.com/grafov/m3u8 v0.12.0
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
github.com/mattn/go-sqlite3 v1.14.19
github.com/microcosm-cc/bluemonday v1.0.26
github.com/mattn/go-sqlite3 v1.14.17
github.com/microcosm-cc/bluemonday v1.0.24
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/schollz/sqlite3dump v1.3.1
github.com/sirupsen/logrus v1.9.3
github.com/teris-io/shortid v0.0.0-20220617161101-71ec9f2aa569
github.com/yuin/goldmark v1.6.0
golang.org/x/mod v0.14.0
golang.org/x/time v0.5.0
github.com/yuin/goldmark v1.5.4
golang.org/x/mod v0.10.0
golang.org/x/time v0.3.0
)
require (
@ -28,55 +28,53 @@ require ( @@ -28,55 +28,53 @@ require (
github.com/lestrrat-go/strftime v1.0.4 // indirect
github.com/mvdan/xurls v1.1.0 // indirect
github.com/pkg/errors v0.9.1
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/net v0.19.0
golang.org/x/sys v0.15.0 // indirect
github.com/tklauser/go-sysconf v0.3.11 // indirect
github.com/tklauser/numcpus v0.6.0 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect
golang.org/x/crypto v0.1.0 // indirect
golang.org/x/net v0.10.0
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 (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // 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/matttproud/golang_protobuf_extensions v1.0.4 // 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/common v0.44.0 // indirect
github.com/prometheus/procfs v0.11.1 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
golang.org/x/text v0.9.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
)
require (
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 (
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/go-test/deep v1.0.4 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/oschwald/maxminddb-golang v1.11.0 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/oschwald/maxminddb-golang v1.10.0 // indirect
github.com/shoenig/go-m1cpu v0.1.4 // indirect
)
require (
github.com/CAFxX/httpcompression v0.0.9
github.com/CAFxX/httpcompression v0.0.8
github.com/andybalholm/cascadia v1.3.2
github.com/mssola/user_agent v0.6.0
github.com/yuin/goldmark-emoji v1.0.2
gopkg.in/evanphx/json-patch.v5 v5.7.0
gopkg.in/evanphx/json-patch.v5 v5.6.0
mvdan.cc/xurls v1.1.0
)

156
go.sum

@ -1,13 +1,13 @@ @@ -1,13 +1,13 @@
github.com/CAFxX/httpcompression v0.0.9 h1:0ue2X8dOLEpxTm8tt+OdHcgA+gbDge0OqFQWGKSqgrg=
github.com/CAFxX/httpcompression v0.0.9/go.mod h1:XX8oPZA+4IDcfZ0A71Hz0mZsv/YJOgYygkFhizVPilM=
github.com/SherClockHolmes/webpush-go v1.3.0 h1:CAu3FvEE9QS4drc3iKNgpBWFfGqNthKlZhp5QpYnu6k=
github.com/SherClockHolmes/webpush-go v1.3.0/go.mod h1:AxRHmJuYwKGG1PVgYzToik1lphQvDnqFYDqimHvwhIw=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/CAFxX/httpcompression v0.0.8 h1:UBWojERnpCS6X7whJkGGZeCC3ruZBRwkwkcnfGfb0ko=
github.com/CAFxX/httpcompression v0.0.8/go.mod h1:bVd1taHK1vYb5SWe9lwNDCqrfj2ka+C1Zx7JHzxuHnU=
github.com/SherClockHolmes/webpush-go v1.2.0 h1:sGv0/ZWCvb1HUH+izLqrb2i68HuqD/0Y+AmGQfyqKJA=
github.com/SherClockHolmes/webpush-go v1.2.0/go.mod h1:w6X47YApe/B9wUz2Wh8xukxlyupaxSSEbu6yKJcHN2w=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
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/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/aws/aws-sdk-go v1.49.6 h1:yNldzF5kzLBRvKlKz1S0bkvc2+04R1kt13KfBWQBfFA=
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 h1:CX8O0gK+cGrgUyv7bgJ6QQP9mQg7u5mweHdNzULH47c=
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/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
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 @@ -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/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.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.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
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-20230829110029-ed738e842d2f/go.mod h1:nOPhAkwVliJdNTkj3gXpljmWhjc4wCaVqbMJcPKWP4s=
github.com/google/brotli/go/cbrotli v0.0.0-20210623081221-ce222e317e36 h1:qg5qEpjk1P1EMnInOCpxOpWSPRsspXJDT7P80y/JfFA=
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.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.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/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
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/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080=
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 @@ -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/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
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.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/klauspost/compress v1.14.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.16.4 h1:91KN02FnsOYhuunwU4ssRe8lc2JosWmizWa91B5v1PU=
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/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is=
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 @@ -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/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.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI=
github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
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/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
github.com/mssola/user_agent v0.6.0 h1:uwPR4rtWlCHRFyyP9u2KOV0u8iQXmS7Z7feTrstQwk4=
github.com/mssola/user_agent v0.6.0/go.mod h1:TTPno8LPY3wAIEKRpAtkdMT0f8SE24pLRGPahjCH4uw=
github.com/microcosm-cc/bluemonday v1.0.24 h1:NGQoPtwGVcbGkKfvyYk1yRqknzBuoMiUrO6R7uFTPlw=
github.com/microcosm-cc/bluemonday v1.0.24/go.mod h1:ArQySAMps0790cHSkdPEJ7bGkF2VePWH773hsJNSHf8=
github.com/mvdan/xurls v1.1.0 h1:OpuDelGQ1R1ueQ6sSryzi6P+1RtBpfQHM8fJwlE45ww=
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/go.mod h1:1xUrK3s1MXSlU6dn96xHerHx/MdO4BGmsAHEUbsaOxU=
github.com/nareix/joy5 v0.0.0-20210317075623-2c912ca30590 h1:PnxRU8L8Y2q82vFC2QdNw23Dm2u6WrjecIdpXjiYbXM=
github.com/nareix/joy5 v0.0.0-20210317075623-2c912ca30590/go.mod h1:XmAOs6UJXpNXRwKk+KY/nv5kL6xXYXyellk+A1pTlko=
github.com/oschwald/geoip2-golang v1.9.0 h1:uvD3O6fXAXs+usU+UGExshpdP13GAqp4GBrzN7IgKZc=
github.com/oschwald/geoip2-golang v1.9.0/go.mod h1:BHK6TvDyATVQhKNbQBdrj9eAvuwOMi2zSFXizL3K81Y=
github.com/oschwald/maxminddb-golang v1.11.0 h1:aSXMqYR/EPNjGE8epgqwDay+P30hCBZIveY0WZbAWh0=
github.com/oschwald/maxminddb-golang v1.11.0/go.mod h1:YmVI+H0zh3ySFR3w+oz8PCfglAFj3PuCmui13+P9zDg=
github.com/oschwald/geoip2-golang v1.8.0 h1:KfjYB8ojCEn/QLqsDU0AzrJ3R5Qa9vFlx3z6SLNcKTs=
github.com/oschwald/geoip2-golang v1.8.0/go.mod h1:R7bRvYjOeaoenAp9sKRS8GX5bJWcZ0laWO5+DauEktw=
github.com/oschwald/maxminddb-golang v1.10.0 h1:Xp1u0ZhqkSuopaKmk1WwHtjF0H9Hd9181uj2MQ5Vndg=
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/go.mod h1:v4QoPaAzjWZ8zN2VFVGL5ep9C02mst0hQYHUpQwso4Q=
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.12 h1:44l88ehTZAUGW4VlO1QC4zkilL99M6Y9MXNwEs0uzP8=
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.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
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 @@ -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/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM=
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI=
github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
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/go.mod h1:GEXHk5HgEKCvEIIrSpFI3ozzG5xOKA2DVlEX/gGnewM=
github.com/schollz/sqlite3dump v1.3.1 h1:QXizJ7XEJ7hggjqjZ3YRtF3+javm8zKtzNByYtEkPRA=
github.com/schollz/sqlite3dump v1.3.1/go.mod h1:mzSTjZpJH4zAb1FN3iNlhWPbbdyeBpOaTW0hukyMHyI=
github.com/shirou/gopsutil/v3 v3.23.11 h1:i3jP9NjCPUz7FiZKxlMnODZkdSIp2gnzfrvsu9CuWEQ=
github.com/shirou/gopsutil/v3 v3.23.11/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/shirou/gopsutil/v3 v3.23.3 h1:Syt5vVZXUDXPEXpIBt5ziWsJ4LdSAAxF4l/xZeQgSEE=
github.com/shirou/gopsutil/v3 v3.23.3/go.mod h1:lSBNN6t3+D6W5e5nXTxc8KIMMVxAcS+6IJlffjRRlMU=
github.com/shoenig/go-m1cpu v0.1.4 h1:SZPIgRM2sEF9NJy50mRHu9PKGwxyyTTJIWvCtgVbozs=
github.com/shoenig/go-m1cpu v0.1.4/go.mod h1:Wwvst4LR89UxjeFtLRMrpgRiyY4xPsejnVZym39dbAQ=
github.com/shoenig/test v0.6.3 h1:GVXWJFk9PiOjN0KoJ7VrJGH6uLPnqxR7/fe3HUPfE0c=
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/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
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 @@ -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.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.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
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/go.mod h1:2Ly+NIftZN4de9zRmENdYbvPQeaVIYKWpLFStLFEBgI=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/valyala/gozstd v1.20.1 h1:xPnnnvjmaDDitMFfDxmQ4vpx0+3CdTg2o3lALvXTU/g=
github.com/valyala/gozstd v1.20.1/go.mod h1:y5Ew47GLlP37EkTB+B4s7r6A5rdaeB7ftbl9zoYiIPQ=
github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms=
github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4=
github.com/valyala/gozstd v1.11.0 h1:VV6qQFt+4sBBj9OJ7eKVvsFAMy59Urcs9Lgd+o5FOw0=
github.com/valyala/gozstd v1.11.0/go.mod h1:y5Ew47GLlP37EkTB+B4s7r6A5rdaeB7ftbl9zoYiIPQ=
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.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s=
github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY=
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU=
github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
github.com/yusufpapurcu/wmi v1.2.2/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-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-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
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.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
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-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-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.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.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.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-20190423024810-112230192c58/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 @@ -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-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.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.6.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.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-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.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
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/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.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.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/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/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
@ -207,14 +202,13 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T @@ -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=
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.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
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/evanphx/json-patch.v5 v5.7.0 h1:dGKGylPlZ/jus2g1YqhhyzfH0gPy2R8/MYUpW/OslTY=
gopkg.in/evanphx/json-patch.v5 v5.7.0/go.mod h1:/kvTRh1TVm5wuM6OkHxqXtE/1nUZZpihg29RtuIyfvk=
gopkg.in/evanphx/json-patch.v5 v5.6.0 h1:BMT6KIwBD9CaU91PJCZIe46bDmBWa9ynTQgJIOpfQBk=
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.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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
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) { @@ -34,7 +34,7 @@ func Setup(enableDebugOptions bool, enableVerboseLogging bool) {
// Create the logging directory if needed
loggingDirectory := filepath.Dir(getLogFilePath())
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)
}
}

6
metrics/metrics.go

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

19
models/externalAPIUser.go

@ -0,0 +1,19 @@ @@ -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"`
}

19
models/s3Storage.go

@ -2,20 +2,17 @@ package models @@ -2,20 +2,17 @@ package models
// S3 is the storage configuration.
type S3 struct {
Endpoint string `json:"endpoint,omitempty"`
AccessKey string `json:"accessKey,omitempty"`
Secret string `json:"secret,omitempty"`
Bucket string `json:"bucket,omitempty"`
Region string `json:"region,omitempty"`
ACL string `json:"acl,omitempty"`
// PathPrefix is an optional prefix for object storage.
PathPrefix string `json:"pathPrefix,omitempty"`
Enabled bool `json:"enabled"`
Endpoint string `json:"endpoint,omitempty"`
AccessKey string `json:"accessKey,omitempty"`
Secret string `json:"secret,omitempty"`
Bucket string `json:"bucket,omitempty"`
Region string `json:"region,omitempty"`
ACL string `json:"acl,omitempty"`
ForcePathStyle bool `json:"forcePathStyle"`
// 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
// to allow the migration to take place without data loss.
ServingEndpoint string `json:"-"`
Enabled bool `json:"enabled"`
ForcePathStyle bool `json:"forcePathStyle"`
}

36
models/user.go

@ -0,0 +1,36 @@ @@ -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
}

10
openapi.yaml

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

23
router/middleware/auth.go

@ -6,16 +6,16 @@ import ( @@ -6,16 +6,16 @@ import (
"strings"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/core/user"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/utils"
log "github.com/sirupsen/logrus"
)
// 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.
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
// the stream key as the password and and a hardcoded "admin" for username.
@ -25,9 +25,11 @@ func RequireAdminAuth(handler http.HandlerFunc) http.HandlerFunc { @@ -25,9 +25,11 @@ func RequireAdminAuth(handler http.HandlerFunc) http.HandlerFunc {
password := data.GetAdminPassword()
realm := "Owncast Authenticated Request"
// Alow CORS only for localhost:3000 to support Owncast development.
validAdminHost := "http://localhost:3000"
w.Header().Set("Access-Control-Allow-Origin", validAdminHost)
// The following line is kind of a work around.
// If you want HTTP Basic Auth + Cors it requires _explicit_ origins to be provided in the
// 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-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization")
@ -67,13 +69,10 @@ func RequireExternalAPIAccessToken(scope string, handler ExternalAccessTokenHand @@ -67,13 +69,10 @@ func RequireExternalAPIAccessToken(scope string, handler ExternalAccessTokenHand
return
}
authHeader := r.Header.Get("Authorization")
token := ""
if strings.HasPrefix(strings.ToLower(authHeader), "bearer ") {
token = authHeader[len("bearer "):]
}
authHeader := strings.Split(r.Header.Get("Authorization"), "Bearer ")
token := strings.Join(authHeader, "")
if token == "" {
if len(authHeader) == 0 || token == "" {
log.Warnln("invalid access token")
accessDenied(w)
return

83
static/metadata.html.tmpl vendored

@ -1,83 +0,0 @@ @@ -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 { @@ -76,14 +76,3 @@ func getFileSystemStaticFileOrDefault(path string, defaultData []byte) []byte {
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 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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