18 Commits

Author SHA1 Message Date
Owncast
1d6151c867 Commit updated API documentation 2022-11-22 01:44:19 +00:00
Gabe Kangas
3a7f69531c Bump version for 0.0.13 release 2022-11-21 12:34:56 -08:00
Albin Larsson
a74ea4ef40 improve chat input accessibility (#2353)
Sets aria-role="textbox" and aria-placeholder
2022-11-21 12:12:02 -08:00
Gabe Kangas
c2a7d9f50b Explicitly set AP post as public. Closes #2112 2022-11-21 12:11:54 -08:00
John Regan
e64dd2d206 activitypub: ensure Undo request has valid type (#2317) 2022-11-21 12:11:41 -08:00
M. Ajmal Moochingal
f39c1184e5 Using prepared statements for SQL queries. (#2257)
* using prepared statements for sql query for fixing sql injection

* returning error in getChat instead of logging
2022-11-21 12:11:28 -08:00
Florian Lehner
a9cd28dbec preallocate memory (#2201)
**What this PR does / why we need it:**

Preallocate memory instead of enforcing an incremental growth. This will result in less work for the garbage collector.
2022-11-21 12:11:04 -08:00
Matt Owens
4b5a1fcc3f remove extra w.WriteHeader call (#2158) 2022-11-21 12:10:36 -08:00
Matt Owens
96274ad541 Treat fediverse usernames as case-insensitive (#2155)
* treat fediverse usernames as case-insensitive for auth

* add test for case insensitive, clean up duplicate import in federverse auth controller

* fix test, there was an issue with state when all the tests were run
2022-11-21 12:10:17 -08:00
Matt Owens
717bbcf2e7 Sanitize user submitted values before logging (#2134)
* strip line breaks from user-submitted values before logging

* finish comment
2022-11-21 12:09:47 -08:00
cel
29972bb4e7 Add Fediverse, Matrix and XMPP social links (#2044) 2022-11-21 12:09:20 -08:00
Gabe Kangas
625b0b0099 Limit chat display names to 30 characters. Closes #1919 2022-11-21 12:09:03 -08:00
Gabe Kangas
27afb93e13 Function has been re-exported for use 2022-11-21 12:08:36 -08:00
Gabe Kangas
eb1121e9fa Fix creating table indexes 2022-11-21 12:08:28 -08:00
Gabe Kangas
e65abb6073 Add util for ungraceful sql execs 2022-11-21 12:08:21 -08:00
Gabe Kangas
29ec8a9091 Messages table fixes to improve query performance (#2026)
* Move to yaml sqlc config

* Add util for ungraceful sql execs

* Fix messages schema + add indexes

* Add migration to drop+recreate messages table

* Create index only if does not exist

* Fix typo

* Unexport function
2022-11-21 12:07:54 -08:00
Gabe Kangas
ba7861cda9 Limit OTP requests to one per expiry window. Closes #2000 2022-11-21 12:07:43 -08:00
Gabe Kangas
1cbe2a56fb Do not log inactionable error. Closes #1992 2022-11-21 12:07:32 -08:00
1897 changed files with 50154 additions and 151355 deletions

View File

@@ -1,70 +0,0 @@
# Owncast Design Guidelines & Resources
A collection of design contribution guidelines and resources for the Owncast interface.
> **All participating designers are highly encouraged to shape and evolve these guidelines!**
> It is a work in progress and as we have design contributors we can work to solidify the process, tools and resources.
## 👋 Welcome
Owncast is a live streaming and chat server targeted to anybody who has live streaming needs. This means anybody from corporate events, government meetings, game streamers, musicians, churches, TV stations, and more.
Read the detailed [product definition](https://github.com/owncast/owncast/blob/develop/docs/product-definition.md) to learn more.
## 🚢 How to contribute to product design
1. Check out open [issues](https://github.com/owncast/owncast/issues) here on GitHub (we label them with `needs design`)
2. Feel free to open an issue on your own if you find something you would like to contribute to the project.
3. Add your contributions to an issue and we promise we will review your contribution carefully and foster discussions
**We encourage you to:**
- Get in touch with the team by joining our [Community Chat](https://owncast.rocket.chat).
- Check out our [Contributor Guide](https://owncast.online/help) and
[Code of Conduct](https://github.com/owncast/owncast/blob/develop/CODE_OF_CONDUCT.md)
## 🎭 Target audience
Owncast is a live streaming and chat server targeted to anybody who has live streaming needs. This means anything from corporate events, government meetings, game streams, concerts, TV stations, and more.
## 🧑‍🎨 Product design opportunities
Owncast is a constantly moving project with features both old and new. This allows for design contributions to be both big or small.
You may not know how much time you can dedicate to the project, or if you'll be able to see something through to the end, so be honest about that. Take on projects that you'll be able to see completed.
- So maybe start small by finding rough edges and improvements to existing features without requiring complete rewrites. As a small project the bandwidth for rebuilding existing designs is limited, but tweaks are appreciated. This is especially great if you don't know how much time or energy you'll be able to provide the project. If you think you have a week to help, but might not be around in a month small projects are better.
- If you think you'll be around longer term, learn about future new features and start thinking about the design challenges of those so we can build them your feedback and design contributions in mind. See your designs put in the world through brand new functionality!
- Not everything has to be a a feature. Think big picture. What can we start doing now to put the project in a better place six months from now, or a year?
## 💅 Design relevant materials
A collection of design relevant information and materials can be found under the "style" section of "Storybook" here:
http://owncast.online/components
### Fonts
https://owncast.online/components/?path=%2Fdocs%2Fowncast-styles-typography--page
Body text: Inter
Display/Header text: Poppins
### Colors
https://owncast.online/components/?path=%2Fdocs%2Fowncast-styles-colors-components--page
### Design Files, Screenshots, etc
We do not currently have any design files that fully represent the state of
the Owncast interface. However, going forward it would be nice to resolve this
and collaborate on designs.
We do have a [PenPot organization](https://design.penpot.app/#/dashboard/team/8373f780-f255-11ec-b774-f940e3befd53/projects). Please ask for access.
## 🎓 License
All design work is licensed under the
[MIT](https://mit-license.org/)
[(Back to top)](#-table-of-contents)

View File

@@ -1 +0,0 @@
test/automated/api/node_modules

View File

@@ -14,13 +14,3 @@ quote_type = single
curly_bracket_next_line = true
spaces_around_operators = true
spaces_around_brackets = true
[*.{yml,yaml}]
indent_style = space
indent_size = 2
[*.{md,mdx}]
trim_trailing_whitespace = false
[*.go]
indent_style = tab

116
.gitattributes vendored
View File

@@ -1,65 +1,57 @@
# Recreate this file via
# find static -type d -print0 | xargs -0 -I {} echo "{}/* linguist-vendored" | xclip -selection clipboard
webroot/js/web_modules/* linguist-vendored
webroot/js/web_modules/@joeattardi/* linguist-vendored
webroot/js/web_modules/@justinribeiro/* linguist-vendored
webroot/js/web_modules/@videojs/http-streaming/dist/* linguist-vendored
webroot/js/web_modules/@videojs/themes/fantasy/* linguist-vendored
webroot/js/web_modules/common/* linguist-vendored
webroot/js/web_modules/markjs/dist/* linguist-vendored
webroot/js/web_modules/tailwindcss/dist/* linguist-vendored
webroot/js/web_modules/videojs/* linguist-vendored
webroot/js/web_modules/micromodal/dist/* linguist-vendored
static/* linguist-vendored
static/admin/* linguist-vendored
docs/api/* linguist-documentation
static/* linguist-vendored
static/web/* linguist-vendored
static/web/admin/* linguist-vendored
static/web/admin/federation/* linguist-vendored
static/web/admin/federation/actions/* linguist-vendored
static/web/admin/federation/followers/* linguist-vendored
static/web/admin/logs/* linguist-vendored
static/web/admin/config-social-items/* linguist-vendored
static/web/admin/config/* linguist-vendored
static/web/admin/config/general/* linguist-vendored
static/web/admin/config/server/* linguist-vendored
static/web/admin/config-chat/* linguist-vendored
static/web/admin/config-federation/* linguist-vendored
static/web/admin/viewer-info/* linguist-vendored
static/web/admin/access-tokens/* linguist-vendored
static/web/admin/actions/* linguist-vendored
static/web/admin/help/* linguist-vendored
static/web/admin/webhooks/* linguist-vendored
static/web/admin/chat/* linguist-vendored
static/web/admin/chat/messages/* linguist-vendored
static/web/admin/chat/users/* linguist-vendored
static/web/admin/chat/emojis/* linguist-vendored
static/web/admin/upgrade/* linguist-vendored
static/web/admin/config-notify/* linguist-vendored
static/web/admin/hardware-info/* linguist-vendored
static/web/admin/config-video/* linguist-vendored
static/web/admin/stream-health/* linguist-vendored
static/web/404/* linguist-vendored
static/web/_next/* linguist-vendored
static/web/_next/static/* linguist-vendored
static/web/_next/static/l-3emuM7cUz2zU2fzzpRq/* linguist-vendored
static/web/_next/static/media/* linguist-vendored
static/web/_next/static/chunks/* linguist-vendored
static/web/_next/static/chunks/pages/* linguist-vendored
static/web/_next/static/chunks/pages/admin/* linguist-vendored
static/web/_next/static/chunks/pages/admin/federation/* linguist-vendored
static/web/_next/static/chunks/pages/admin/config/* linguist-vendored
static/web/_next/static/chunks/pages/admin/chat/* linguist-vendored
static/web/_next/static/chunks/pages/embed/* linguist-vendored
static/web/_next/static/chunks/pages/embed/chat/* linguist-vendored
static/web/_next/static/css/* linguist-vendored
static/web/_next/static/OQyHVua-s5F40yEopTtjx/* linguist-vendored
static/web/_next/OQyHVua-s5F40yEopTtjx/* linguist-vendored
static/web/embed/* linguist-vendored
static/web/embed/chat/* linguist-vendored
static/web/embed/chat/readonly/* linguist-vendored
static/web/embed/chat/readwrite/* linguist-vendored
static/web/embed/video/* linguist-vendored
static/web/fonts/* linguist-vendored
static/web/fonts/inter/* linguist-vendored
static/web/styles/* linguist-vendored
static/web/styles/admin/* linguist-vendored
static/web/img/* linguist-vendored
static/web/img/favicon/* linguist-vendored
static/web/img/platformlogos/* linguist-vendored
static/img/* linguist-vendored
static/img/emoji/* linguist-vendored
static/img/emoji/dog/* linguist-vendored
static/img/emoji/conigliolo96/* linguist-vendored
static/img/emoji/mutant/* linguist-vendored
static/img/emoji/blob/* linguist-vendored
static/admin/* linguist-vendored
static/admin/logs/* linguist-vendored
static/admin/config-social-items/* linguist-vendored
static/admin/offline-notice/* linguist-vendored
static/admin/config-chat/* linguist-vendored
static/admin/404/* linguist-vendored
static/admin/_next/* linguist-vendored
static/admin/_next/static/* linguist-vendored
static/admin/_next/static/chunks/* linguist-vendored
static/admin/_next/static/chunks/pages/* linguist-vendored
static/admin/_next/static/chunks/pages/chat/* linguist-vendored
static/admin/_next/static/b1nOF3ZgELnezD8dvvt2B/* linguist-vendored
static/admin/_next/static/css/* linguist-vendored
static/admin/_next/quK9VwW_avTP773Ot9m2x/* linguist-vendored
static/admin/viewer-info/* linguist-vendored
static/admin/access-tokens/* linguist-vendored
static/admin/config-storage/* linguist-vendored
static/admin/config-public-details/* linguist-vendored
static/admin/config-server-details/* linguist-vendored
static/admin/actions/* linguist-vendored
static/admin/help/* linguist-vendored
static/admin/webhooks/* linguist-vendored
static/admin/chat/* linguist-vendored
static/admin/chat/messages/* linguist-vendored
static/admin/chat/users/* linguist-vendored
static/admin/upgrade/* linguist-vendored
static/admin/hardware-info/* linguist-vendored
static/admin/config-video/* linguist-vendored
webroot/js/web_modules/* linguist-vendored
webroot/js/web_modules/micromodal/* linguist-vendored
webroot/js/web_modules/micromodal/dist/* linguist-vendored
webroot/js/web_modules/common/* linguist-vendored
webroot/js/web_modules/@videojs/* linguist-vendored
webroot/js/web_modules/@videojs/http-streaming/* linguist-vendored
webroot/js/web_modules/@videojs/http-streaming/dist/* linguist-vendored
webroot/js/web_modules/@videojs/themes/* linguist-vendored
webroot/js/web_modules/@videojs/themes/fantasy/* linguist-vendored
webroot/js/web_modules/markjs/* linguist-vendored
webroot/js/web_modules/markjs/dist/* linguist-vendored
webroot/js/web_modules/@joeattardi/* linguist-vendored
webroot/js/web_modules/tailwindcss/* linguist-vendored
webroot/js/web_modules/tailwindcss/dist/* linguist-vendored
webroot/js/web_modules/videojs/* linguist-vendored

View File

@@ -0,0 +1,4 @@
---
name: Bug report or feature request
about: Having problems or have ideas? We'd love to know what you think and help you out.
---

View File

@@ -1,15 +0,0 @@
name: Bug report or feature request
description: Submit a bug you encountered or share an idea you have for the project.
body:
- type: markdown
attributes:
value: |
Thanks for helping by reporting issues and sharing ideas you might have!
While no idea is a bad idea, some might make more sense for Owncast than others.
Take a look at the [Owncast product definition](https://github.com/owncast/owncast/blob/develop/docs/product-definition.md) to see what our focus is and how your requests might align.
- type: textarea
id: issue-body
attributes:
label: Share your bug report, feature request, or comment.
description: Please include as much detail as possible.

17
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,17 @@
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.
# Description
Fixes # (issue)
---
Some things you might want to mention:
1. Why are you making the change?
2. Explain how it works and decisions you made.
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.

View File

@@ -1,24 +0,0 @@
# Read first
If this is an unsolicited change, or there is no existing issue filed for it, please open a GitHub issue before creating a pull request. This will allow us to discuss the motivations and the big picture behind the change first. It's possible there may be other solutions that should be discussed for what you think should be built. It is possible your change will be rejected unless some discussion around your proposal happens first. While creating this PR means you probably already did the work, it still makes sense to file an issue now, and into the future when you have proposed changes.
## Description
Please include a summary of the change and which issue number is fixed, including relevant motivation and context. Feel free to mark this as a Draft or WIP and write up some details later and start a conversation, even if your PR is not ready for review.
Fixes # (issue)
## Screenshot Examples or Logs
If this is a frontend change, please include a screenshot of the change. If this is a backend change, please include relevant logs or examples of the change in action if applicable.
---
Some things you might want to mention:
1. Why are you making the change?
2. Explain how it works and decisions you made.
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?
Thank you so much for contributing to Owncast! 🎉

View File

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

View File

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

31
.github/stale.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 60
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- backlog
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. If this
was a feature request that others have shown no interest in then it's
likely to not get implemented due to lack of interest. If others also
want to see this feature then now is the time to say something!
Thank you for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false
exemptMilestones: true
# Since old PRs are less useful than old issues ping them sooner.
pulls:
daysUntilStale: 30
markComment: >
This pull request has not had any activity in 30 days. Since things move fast it's best
to get PRs merged in. If this PR addresses a previously filed issue that needs to be
resolved please work to get it merged in, or allow somebody else to work on a fix.
This PR will be closed if no further activity occurs. Thank you for your contributions!
exemptLabels:
- bot

View File

@@ -1,28 +0,0 @@
name: Lint
on:
push:
paths:
- '.github/workflows/*'
pull_request:
paths:
- '.github/workflows/*'
jobs:
actionlint:
name: GitHub actions
runs-on: ubuntu-latest
steps:
- name: Check out pull request code
uses: actions/checkout@v4
if: github.event_name == 'pull_request'
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Check out repository code
uses: actions/checkout@v4
if: github.event_name == 'push'
- uses: docker://rhysd/actionlint:latest
with:
args: -shellcheck= -color

View File

@@ -1,42 +0,0 @@
name: Add comment on good first issues
on:
issues:
types:
- labeled
jobs:
add-comment:
if: github.event.label.name == 'good first issue' || github.event.label.name == 'help wanted' || github.event.label.name == 'hacktoberfest'
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Add comment
uses: peter-evans/create-or-update-comment@fdb73c443d3a4f66832374f01fb9a713fad84937
with:
issue-number: ${{ github.event.issue.number }}
body: |
## Good First Issue
This item was marked as a good first issue because of the following:
- It's self contained as a single feature or change.
- Is clear when it's complete.
- You do not need deep knowledge of Owncast to accomplish it.
### Next Steps
1. Comment on this issue before starting work so it can be assigned to you. Also, this issue may have been filed with limited detail or changes may have occurred that are worth sharing with you before you start work.
2. Drop by our [community chat](https://owncast.rocket.chat/) if you'd like to be involved in more real-time discussion around Owncast to talk about this change.
3. Follow the project's getting started tips to make sure you can [build and run the project from source](https://owncast.online/development).
### Notes
- Development takes place on the `develop` branch.
- We use Storybook for testing and developing React components. `npm run storybook`. A hosted version [is available for viewing](https://owncast.online/components).
- If you need to install the Go programming language to run the Owncast backend it's simple from [here](https://go.dev/dl/).
- Active contributors get an Owncast t-shirt! Ask about it if you feel like you've been contributing and haven't yet been given one.
### New to Git?
If you're brand new to Git you may want a short primer about the Fork -> Commit -> Pull Request workflow that enables changes to get made collaboratively using git. Visit the [First Contributions](https://github.com/firstcontributions/first-contributions) project to learn step-by-step how to commit a change to a Git repository such as this one.

19
.github/workflows/automated-browser.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
name: Automated browser tests
on: [push, pull_request]
jobs:
browser:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
stable: 'false'
go-version: '1.17.2'
- name: Run browser tests
run: cd test/automated/browser && ./run.sh
- uses: actions/upload-artifact@v3
with:
name: screenshots-${{ github.run_id }}
path: test/automated/browser/screenshots/*.png

View File

@@ -3,60 +3,21 @@ name: Automated API tests
on:
push:
paths-ignore:
- 'web/**'
- 'webroot/**'
pull_request:
paths-ignore:
- 'web/**'
- 'webroot/**'
jobs:
test:
api:
runs-on: ubuntu-latest
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
concurrent_skipping: 'same_content_newer'
- name: Check out pull request code
uses: actions/checkout@v4
if: github.event_name == 'pull_request'
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Check out repository code
uses: actions/checkout@v4
if: github.event_name == 'push'
- name: Get changed files
id: changed-files-yaml
uses: tj-actions/changed-files@v45
with:
files_yaml: |
src:
- '**/*.{go,mod,sum}'
- uses: earthly/actions-setup@v1
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
with:
version: 'latest' # or pin to an specific version, e.g. "v0.6.10"
- name: Earthly version
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
run: earthly --version
- name: Set up QEMU
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
id: qemu
uses: docker/setup-qemu-action@v3
with:
image: tonistiigi/binfmt:latest
platforms: all
stable: 'false'
go-version: '1.17.2'
- name: Run API tests
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: earthly +api-tests
run: cd test/automated/api && ./run.sh

View File

@@ -1,61 +0,0 @@
name: Browser Tests
on:
push:
paths:
- 'web/**'
- 'test/automated/browser/**'
pull_request:
paths:
- 'web/**'
- 'test/automated/browser/**'
jobs:
cypress-run:
runs-on: ubuntu-latest
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
concurrent_skipping: 'same_content_newer'
- name: Check out pull request code
uses: actions/checkout@v4
if: github.event_name == 'pull_request'
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Check out repository code
uses: actions/checkout@v4
if: github.event_name == 'push'
- uses: actions/setup-node@v4
with:
node-version: '22.9.0'
- name: Cache node modules
uses: actions/cache@v4
env:
cache-name: cache-node-modules-browser-tests
with:
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('test/automated/browser/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- uses: actions/setup-go@v5
with:
go-version: '1.22'
cache: true
- name: Install Google Chrome
run: sudo apt-get update && sudo apt-get install google-chrome-stable
- name: Run Browser tests
uses: nick-fields/retry@v3
with:
timeout_minutes: 20
max_attempts: 3
command: cd test/automated/browser && ./run.sh

View File

@@ -1,61 +0,0 @@
name: Build and Deploy Components+Style Guide
on:
push:
branches:
- develop
paths: ['web/stories/**', 'web/components/**', 'web/.storybook/**'] # Trigger the action only when files change in the folders defined here
jobs:
build-and-deploy:
runs-on: ubuntu-latest
if: github.repository == 'owncast/owncast'
steps:
- name: Check out pull request code
uses: actions/checkout@v4
if: github.event_name == 'pull_request'
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Check out repository code
uses: actions/checkout@v4
if: github.event_name == 'push'
- name: Cache node modules
uses: actions/cache@v4
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 and Build
run: | # Install npm packages and build the Storybook files
cd web
npm install --include-dev --force
cd .storybook/tools
./generate-stories.sh
cd -
npm run build-storybook -- -o ../docs/components
- name: Commit changes
uses: EndBug/add-and-commit@v9
with:
author_name: Owncast
author_email: owncast@owncast.online
message: 'Commit updated Storybook stories'
add: '*.stories.*'
pull: '--rebase --autostash'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Dispatch event to web site
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.BUNDLE_STORYBOOK_OWNCAST_ONLINE }}
repository: owncast/owncast.github.io
event-type: bundle-components-library

21
.github/workflows/bundle-admin.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: Bundle admin (owncast/owncast-admin)
on:
repository_dispatch:
types: [bundle-admin-event]
jobs:
bundle:
runs-on: ubuntu-latest
steps:
- name: Bundle admin
uses: actions/checkout@v3
- run: build/admin/bundleAdmin.sh
- name: Commit changes
uses: EndBug/add-and-commit@v9
with:
author_name: Owncast
author_email: owncast@owncast.online
message: "Update admin to ${{ github.event.client_payload.sha }}"
add: "static/admin"
env:
GITHUB_TOKEN: ${{ secrets.GH_CR_PAT }}

View File

@@ -1,65 +0,0 @@
# .github/workflows/chromatic.yml
# Workflow name
name: 'Chromatic'
on:
push:
paths:
- web/**
pull_request_target:
paths:
- web/**
# List of jobs
jobs:
chromatic-deployment:
# Operating System
runs-on: ubuntu-latest
if: github.repository == 'owncast/owncast'
defaults:
run:
working-directory: ./web
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
concurrent_skipping: 'same_content_newer'
- name: Check out pull request code
uses: actions/checkout@v4
if: github.event_name == 'pull_request' || github.event_name == 'pull_request_target'
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Check out repository code
uses: actions/checkout@v4
if: github.event_name == 'push'
- name: Get changed files
id: changed-files-yaml
uses: tj-actions/changed-files@v45
with:
path: 'web'
files_ignore: |
static/**
web/next.config.js
files_yaml: |
src:
- '**/*.{js,ts,tsx,jsx,md}'
- name: Install dependencies
if: ${{ github.actor != 'renovate[bot]' && github.actor != 'renovate' && steps.changed-files-yaml.outputs.src_any_changed == 'true'}}
run: npm install
- name: Publish to Chromatic
if: ${{ github.actor != 'renovate[bot]' && github.actor != 'renovate' && steps.changed-files-yaml.outputs.src_any_changed == 'true' }}
uses: chromaui/action@v11
# Chromatic GitHub Action options
with:
workingDir: web
projectToken: f47410569b62
onlyChanged: true

View File

@@ -9,16 +9,18 @@
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: 'CodeQL'
name: "CodeQL"
on:
push:
branches: [develop]
branches: [ develop ]
paths-ignore:
- 'static/**'
- 'webroot/js/web_modules/**'
- 'build/javascript/**'
pull_request:
# The branches below must be a subset of the branches above
branches: [develop]
branches: [ develop ]
paths-ignore:
- 'static/**'
@@ -30,53 +32,40 @@ jobs:
strategy:
fail-fast: false
matrix:
language: ['go', 'javascript']
language: [ 'go', 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Check out pull request code
uses: actions/checkout@v4
if: github.event_name == 'pull_request'
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Checkout repository
uses: actions/checkout@v3
- name: Check out repository code
uses: actions/checkout@v4
if: github.event_name == 'push'
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# 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.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
- uses: actions/setup-go@v5
with:
go-version: '1.22'
cache: true
# 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@v1
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
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.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# 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
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
#- run: |
# make bootstrap
# make release
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@@ -1,36 +0,0 @@
name: Lint
on:
push:
branches:
- develop
paths:
- 'Dockerfile'
pull_request:
branches:
- develop
paths:
- 'Dockerfile'
jobs:
trivy:
name: Dockerfile
runs-on: ubuntu-latest
container:
image: aquasec/trivy
steps:
- name: Check out pull request code
uses: actions/checkout@v4
if: github.event_name == 'pull_request'
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Check out repository code
uses: actions/checkout@v4
if: github.event_name == 'push'
- name: Check critical issues
run: trivy config --exit-code 1 --severity "HIGH,CRITICAL" ./Dockerfile
- name: Check non-critical issues
run: trivy config --severity "LOW,MEDIUM" ./Dockerfile

View File

@@ -1,61 +0,0 @@
# See https://docs.earthly.dev/ci-integration/vendor-specific-guides/gh-actions-integration
# for details.
name: Build development container
on:
schedule:
- cron: '0 2 * * *'
push:
branches:
- develop
pull_request:
branches:
- develop
jobs:
Earthly:
runs-on: ubuntu-latest
steps:
- name: Set up Earthly
uses: earthly/actions-setup@v1
with:
version: 'latest' # or pin to an specific version, e.g. "v0.6.10"
- name: Log Earthly version
run: earthly --version
- name: Authenticate to GitHub Container Registry
if: ${{ github.event_name == 'schedule' && env.GH_CR_PAT != null }}
env:
GH_CR_PAT: ${{ secrets.GH_CR_PAT }}
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
with:
image: tonistiigi/binfmt:latest
platforms: all
- name: Check out pull request code
uses: actions/checkout@v4
if: github.event_name == 'pull_request'
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Check out repository code
uses: actions/checkout@v4
if: github.event_name == 'push' || github.event_name == 'schedule'
- name: Build and push
if: ${{ github.event_name == 'schedule' && env.GH_CR_PAT != null }}
env:
GH_CR_PAT: ${{ secrets.GH_CR_PAT }}
EARTHLY_BUILD_TAG: 'nightly'
EARTHLY_BUILD_BRANCH: 'develop'
EARTHLY_PUSH: true
uses: nick-fields/retry@v3
with:
timeout_minutes: 20
max_attempts: 3
command: ./build/develop/container.sh

View File

@@ -1,53 +0,0 @@
name: CSS Lint and Formatting
on:
push:
paths:
- 'web/**'
pull_request:
paths:
- 'web/**'
jobs:
css-lint:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./web
steps:
- name: Check out pull request code
uses: actions/checkout@v4
if: github.event_name == 'pull_request'
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Check out repository code
uses: actions/checkout@v4
if: github.event_name == 'push'
- name: Get changed files
id: changed-files-yaml
uses: tj-actions/changed-files@v45
with:
path: 'web'
files_yaml: |
src:
- '**/*.{css,scss}'
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22.9.0'
- name: Install dependencies
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
run: npm install
- name: Run Prettier
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
run: npx prettier --check ${{ steps.changed-files-yaml.outputs.src_all_changed_files }}
- name: Run Stylelint
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
run: npx stylelint ${{ steps.changed-files-yaml.outputs.src_all_changed_files }}

40
.github/workflows/docker-nightly.yaml vendored Normal file
View File

@@ -0,0 +1,40 @@
# See https://docs.earthly.dev/ci-integration/vendor-specific-guides/gh-actions-integration
# for details.
name: Build nightly docker
on:
workflow_dispatch:
schedule:
- cron: '0 2 * * *'
jobs:
Docker:
runs-on: ubuntu-latest
steps:
- uses: earthly/actions-setup@v1
with:
version: 'latest' # or pin to an specific version, e.g. "v0.6.10"
- name: Earthly version
run: earthly --version
- name: Log into GitHub Container Registry
env:
GH_CR_PAT: ${{ secrets.GH_CR_PAT }}
run: echo "${{ secrets.GH_CR_PAT }}" | docker login https://ghcr.io -u ${{ github.actor }} --password-stdin
if: env.GH_CR_PAT != null
- name: Set up QEMU
id: qemu
uses: docker/setup-qemu-action@v1
with:
image: tonistiigi/binfmt:latest
platforms: all
- uses: actions/checkout@v3
- name: Checkout and build
if: env.GH_CR_PAT != null
env:
GH_CR_PAT: ${{ secrets.GH_CR_PAT }}
run: cd build/release && ./docker-nightly.sh

View File

@@ -9,26 +9,19 @@ jobs:
name: Generate API Documentation
runs-on: ubuntu-latest
steps:
- name: Check out pull request code
uses: actions/checkout@v4
if: github.event_name == 'pull_request'
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Checkout repo
uses: actions/checkout@v3
- name: Check out repository code
uses: actions/checkout@v4
if: github.event_name == 'push'
- name: Run redoc on openapi.yaml
run: |
npx redoc-cli bundle openapi.yaml -o docs/api/index.html --options '{"hideHostname": true, "pathInMiddlePanel": true}'
- name: Run redoc on openapi.yaml
run: |
npx @redocly/cli --config docs/api/redocly.yaml build-docs openapi.yaml -o docs/api/index.html
- name: Commit changes
uses: EndBug/add-and-commit@v9
with:
author_name: Owncast
author_email: owncast@owncast.online
message: 'Commit updated API documentation'
add: 'docs/api/index.html'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Commit changes
uses: EndBug/add-and-commit@v9
with:
author_name: Owncast
author_email: owncast@owncast.online
message: "Commit updated API documentation"
add: "docs/api/index.html"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,38 +0,0 @@
name: Lint
on:
push:
paths-ignore:
- 'web/**'
pull_request:
paths-ignore:
- 'web/**'
permissions:
contents: read
jobs:
golangci:
name: Go linter
if: ${{ github.actor != 'dependabot[bot]' }}
runs-on: ubuntu-latest
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
with:
concurrent_skipping: 'same_content_newer'
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-go@v5
with:
go-version: '1.22'
cache: true
- uses: actions/checkout@v4
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
only-new-issues: true
args: --timeout=3m

View File

@@ -1,89 +0,0 @@
name: Go Tests
on:
push:
paths-ignore:
- 'web/**'
pull_request:
paths-ignore:
- 'web/**'
jobs:
test:
strategy:
matrix:
go-version: [1.21.x, 1.22.x]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Get changed files
id: changed-files-yaml
uses: tj-actions/changed-files@v45
with:
files_yaml: |
src:
- '**/*.{go,mod,sum}'
- uses: actions/cache@v4
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: go-test-${{ github.sha }}
restore-keys: |
go-test-
- name: Install go
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
uses: actions/setup-go@v5
with:
go-version: '^1'
cache: true
- name: Run tests
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
run: go test ./...
test-bsds:
runs-on: macos-latest
strategy:
matrix:
os:
- name: freebsd
version: 12.2
- name: openbsd
version: 6.8
steps:
- uses: actions/checkout@v4
- name: Get changed files
id: changed-files-yaml
uses: tj-actions/changed-files@v45
with:
files_yaml: |
src:
- '**/*.{go,mod,sum}'
- uses: actions/cache@v4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: go-test-${{ github.sha }}
restore-keys: |
go-test-
- name: Install go
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
uses: actions/setup-go@v5
with:
go-version: '^1'
cache: true
- name: Run tests
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
run: go test ./...

View File

@@ -1,13 +1,13 @@
name: HLS tests
name: Automated HLS tests
on:
push:
paths-ignore:
- 'web/**'
- 'webroot/**'
pull_request:
paths-ignore:
- 'web/**'
- 'webroot/**'
env:
S3_BUCKET: ${{ secrets.S3_BUCKET }}
S3_ACCESS_KEY: ${{ secrets.S3_ACCESS_KEY }}
@@ -16,63 +16,15 @@ env:
S3_SECRET: ${{ secrets.S3_SECRET }}
jobs:
tests:
api:
runs-on: ubuntu-latest
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
concurrent_skipping: 'same_content_newer'
stable: 'false'
go-version: '1.17.2'
- name: Run HLS tests
run: cd test/automated/hls && ./run.sh
- name: Check out pull request code
uses: actions/checkout@v4
if: github.event_name == 'pull_request'
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Check out repository code
uses: actions/checkout@v4
if: github.event_name == 'push'
- name: Get changed files
id: changed-files-yaml
uses: tj-actions/changed-files@v45
with:
files_yaml: |
src:
- '**/*.{go,mod,sum}'
- uses: actions/setup-go@v5
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
with:
go-version: '1.22'
cache: true
- name: Cache node modules
uses: actions/cache@v4
env:
cache-name: cache-node-modules-hls-tests
with:
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('test/automated/hls/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Local stroage
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: cd test/automated/hls && ./run.sh
- name: S3 storage
if: steps.changed-files-yaml.outputs.src_any_changed == 'true'
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: cd test/automated/hls && ./run-s3.sh

View File

@@ -1,222 +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: Check out pull request code
uses: actions/checkout@v4
if: github.event_name == 'pull_request'
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
ref: ${{github.event.pull_request.head.ref}}
- name: Setup Nodejs
uses: actions/setup-node@v4
with:
node-version: '22.9.0'
- name: Check out repository code
uses: actions/checkout@v4
if: github.event_name == 'push'
- name: Get changed files
id: changed-files-yaml
uses: tj-actions/changed-files@v45
with:
path: 'web'
files_ignore: |
static/**
web/next.config.js
files_yaml: |
src:
- '**/*.{js,ts,tsx,jsx,md}'
- name: Cache node modules
uses: actions/cache@v4
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 and fix
if: steps.changed-files-yaml.outputs.src_any_changed == 'true' && github.event_name != 'pull_request'
run: npx eslint --fix ${{ steps.changed-files-yaml.outputs.src_all_changed_files }}
- name: Lint
if: steps.changed-files-yaml.outputs.src_any_changed == 'true' && github.event_name == 'pull_request'
run: npx eslint ${{ steps.changed-files-yaml.outputs.src_all_changed_files }}
- name: Prettier formatting
if: steps.changed-files-yaml.outputs.src_any_changed == 'true' && github.event_name == 'pull_request'
run: npx prettier --write ${{ steps.changed-files-yaml.outputs.src_all_changed_files }}
- name: Prettier check
if: steps.changed-files-yaml.outputs.src_any_changed == 'true' && github.event_name != 'pull_request'
run: npx prettier ${{ steps.changed-files-yaml.outputs.src_all_changed_files }}
- name: Debug changed files output
run: 'pwd && echo "Changed files: ${{ steps.changed-files-yaml.outputs.src_all_changed_files }}"'
- name: Commit changes
if: steps.changed-files-yaml.outputs.src_any_changed == 'true' && github.event_name != 'pull_request'
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 }}
cwd: './web' # Ensure this is the correct relative directory
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: Check out pull request code
uses: actions/checkout@v4
if: github.event_name == 'pull_request'
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Check out repository code
uses: actions/checkout@v4
if: github.event_name == 'push'
- name: Setup Nodejs
uses: actions/setup-node@v4
with:
node-version: '22.9.0'
- name: Cache node modules
uses: actions/cache@v4
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
- name: Run tests
working-directory: ./web
run: npm test
# After any formatting and linting is complete we can run the build
# and bundle step. This both will verify that the build is successful as
# well as commiting the updated static files into the repository for use.
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: Setup Nodejs
uses: actions/setup-node@v4
with:
node-version: '22.9.0'
- name: Cache node modules
uses: actions/cache@v4
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: Check out pull request code
uses: actions/checkout@v4
if: github.event_name == 'pull_request'
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Check out repository code
uses: actions/checkout@v4
if: github.event_name == 'push'
- name: 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

View File

@@ -0,0 +1,29 @@
name: Format Javascript
# This action works with pull requests and pushes
on:
push:
branches:
- develop
jobs:
prettier:
runs-on: ubuntu-latest
if: ${{ github.actor != 'dependabot[bot]' }}
steps:
- name: Checkout
uses: actions/checkout@v3
with:
# Make sure the actual branch is checked out when running on pull requests
ref: ${{ github.head_ref }}
fetch-depth: 0
- name: Prettify code
uses: creyD/prettier_action@v4.2
with:
# This part is also where you can pass other options, for example:
prettier_options: --write webroot/**/*.{js,md}
only_changed: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -0,0 +1,33 @@
name: javascript-packages
on:
push:
paths:
- build/javascript/package.json
jobs:
run:
if: ${{ github.actor != 'dependabot[bot]' }}
name: npm run build
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v3
with:
# Make sure the actual branch is checked out when running on pull requests
ref: ${{ github.head_ref }}
- name: Build dependencies
uses: actions/setup-node@v3
with:
node-version: '12'
- run: cd build/javascript && npm run build
- name: Commit changes
uses: EndBug/add-and-commit@v9
with:
author_name: Owncast
author_email: owncast@owncast.online
message: "Commit updated Javascript packages"
add: "build/javascript/package* webroot/js/web_modules"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

26
.github/workflows/lint.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: lint
on:
push:
pull_request:
permissions:
contents: read
jobs:
golangci:
name: Go linter
if: ${{ github.actor != 'dependabot[bot]' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: actions/setup-go@v3
- uses: actions/checkout@v3
- name: golangci-lint
uses: golangci/golangci-lint-action@v3
with:
only-new-issues: true
args: --timeout=3m

View File

@@ -1,38 +0,0 @@
name: Lint
on:
push:
branches:
- develop
paths:
- '**.sh'
pull_request:
branches:
- develop
paths:
- '**.sh'
jobs:
shellcheck:
runs-on: ubuntu-latest
env:
LANG: C.UTF-8
container:
image: docker.io/ubuntu:24.04
steps:
- name: Check out pull request code
uses: actions/checkout@v4
if: github.event_name == 'pull_request'
with:
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Check out repository code
uses: actions/checkout@v4
if: github.event_name == 'push'
- name: Install shellcheck
run: apt update && apt install -y shellcheck bash && shellcheck --version
- name: Check shell scripts
run: shopt -s globstar && ls **/*.sh && shellcheck -x -P "SCRIPTDIR" --severity=info **/*.sh
shell: bash

View File

@@ -1,49 +0,0 @@
name: 'Close stale issues and PRs'
on:
schedule:
- cron: '30 */2 * * *'
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
exempt-all-milestones: true
days-before-issue-stale: 60
days-before-issue-close: 67
exempt-issue-labels: backlog,long-lived,bot
exempt-all-issue-milestones: true
stale-issue-message: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. If this
was a feature request that others have shown no interest in, then it's
unlikely to get implemented due to lack of interest. If others also
want to see this feature then now is the time to say something! If this
is a bug report or you have questions that still need answering, please say
something. Feel free to drop by [our chat](https://owncast.rocket.chat) if
you'd like to discuss in real-time with people.
close-issue-message: >
This issue has been automatically closed due to inactivity. This isn't done
to be a jerk, or because the project doesn't care. But simply to keep the focus
on things that are actively discussed, and has continued interest from the community and
Owncast developers. Feel free to to comment if there is still discussion to be
had, or if you plan to work on it. Feel free to drop by [our chat](https://owncast.rocket.chat)
if you'd like to discuss in real-time with people. Thank you for being involved!
days-before-pr-stale: 30
days-before-pr-close: 37
exempt-pr-labels: backlog,long-lived,bot
exempt-all-pr-milestones: true
stale-pr-message: >
This pull request has not had any activity in 30 days. If it has been abandoned
no future actions are necessary, it will be automatically closed. If this is a PR
with no clear plan on how to move forward on it getting into the project, then
further discussion is needed. Now is a good time to discuss if this is still
something that should be worked on. If this PR is idle simply because nobody
has reviewed it, then feel free to ping somebody. However, if this PR is not linked to an
existing issue regarding something that was previously determined to be important, then even
more discussion needs to take place before it can get anywhere.
This PR will be closed if no further activity occurs. Thank you for your contributions!
close-pr-message: 'This PR was closed because it has been stalled for 10 days with no activity.'

42
.github/workflows/test.yaml vendored Normal file
View File

@@ -0,0 +1,42 @@
name: Tests
on: [push, pull_request]
jobs:
test:
strategy:
matrix:
go-version: [1.16.x, 1.17.x]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- name: Install go
uses: actions/setup-go@v3
with:
go-version: "^1"
- name: Run tests
run: go test ./...
test-bsds:
runs-on: macos-10.15
strategy:
matrix:
os:
- name: freebsd
version: 12.2
- name: openbsd
version: 6.8
steps:
- uses: actions/checkout@v3
- name: Install go
uses: actions/setup-go@v3
with:
go-version: "^1"
- name: Run tests
run: go test ./...

View File

@@ -1,62 +0,0 @@
name: Translation job
on:
schedule:
# Run the workflow every hour
- cron: "0 * * * *"
push:
paths:
- 'web/i18n/en/translation.json'
- 'web/**/*.tsx'
- 'web/**/*.js'
- 'crowdin.yml'
- '.github/workflows/translations.yml'
- 'web/i18next-parser.config.mjs'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
generate-translations:
defaults:
run:
working-directory: ./web
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
if: ${{ github.actor != 'renovate[bot]' && github.actor != 'renovate' }}
run: npm install
- name: Generate translation files
run: npm run translate
- name: Crowdin upload sources/download translations
uses: crowdin/github-action@v1
with:
upload_sources: true
download_translations: true
create_pull_request: true
pull_request_title: 'New Translations'
localization_branch_name: translations
pull_request_base_branch_name: 'develop'
commit_message: 'Updated translations'
config: crowdin.yml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
- name: Commit changes
uses: EndBug/add-and-commit@v9
with:
author_name: Owncast
author_email: owncast@owncast.online
message: 'Commit updated translations'
add: 'web/i18n/**'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

8
.gitignore vendored
View File

@@ -4,7 +4,6 @@
*.dll
*.so
*.dylib
.DS_Store
# Test binary, built with `go test -c`
*.test
@@ -28,7 +27,6 @@ webroot/preview.gif
webroot/hls
webroot/static/content.md
hls/
!test/automated/hls/
dist/
data/
transcoder.log
@@ -41,9 +39,3 @@ backup/
test/test.db
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

View File

@@ -1,5 +0,0 @@
# Automatic workspace preparation for gitpod instances
tasks:
- init: sudo apt-get install ffmpeg -y && go get && go build ./... && go test ./...
command: go run .

View File

@@ -4,8 +4,8 @@ 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.22'
# Default: use Go version from the go.mod file, fallback on the env var `GOVERSION`, fallback on 1.17
go: '1.17'
issues:
# The linter has a default list of ignorable errors. Turning this on will enable that list.
@@ -28,6 +28,7 @@ linters:
- bodyclose
- dupl
- errcheck
- exportloopref
- goconst
- godot
- godox
@@ -47,8 +48,10 @@ linters:
- nakedret
- cyclop
- gosimple
- varcheck
- unused
- copyloopvar
- deadcode
- exportloopref
- gocritic
- forbidigo
- unparam
@@ -66,20 +69,21 @@ linters-settings:
# should ignore tests
skip-tests: true
gosimple:
# Select the Go version to target. The default is '1.13'.
go: "1.17"
# https://staticcheck.io/docs/options#checks
checks: ["all"]
gocritic:
disabled-checks:
- ifElseChain
- exitAfterDefer
revive:
rules:
- name: package-comments
disabled: true
forbidigo:
# Forbid the following identifiers (identifiers are written using regexp):
forbid:
# Logging via Print bypasses our logging framework.
# Logging via Print bypasses our logging framework.
- ^(fmt\.Print(|f|ln)|print|println)
- ^panic.*$

View File

@@ -1,4 +1,3 @@
# Ignore artifacts:
build/javascript
webroot/js/web_modules
static/

28
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,28 @@
{
"cSpell.words": [
"Debugln",
"Errorln",
"Fediverse",
"Ffmpeg",
"ffmpegpath",
"ffmpg",
"geoip",
"gosec",
"mattn",
"Mbps",
"nolint",
"Owncast",
"ppid",
"preact",
"RTMP",
"rtmpserverport",
"sqlite",
"Tracef",
"Traceln",
"upgrader",
"Upgrader",
"videojs",
"Warnf",
"Warnln"
]
}

View File

@@ -1,16 +1,9 @@
# IMPORTANT: This Dockerfile has been provided for the sake of convenience.
# Currently, functionality of the containers built based on this file
# is not a part of our continuous testing. Although, patches to keep it
# up to date are always welcome.
#
# See Earthfile for the recipes used in official builds.
# Perform a build
FROM golang:alpine AS build
RUN apk update && apk add --no-cache git gcc build-base linux-headers
RUN mkdir /build
ADD . /build
WORKDIR /build
COPY . /build
RUN apk update && apk add --no-cache git gcc build-base linux-headers
ARG VERSION=dev
ENV VERSION=${VERSION}
@@ -22,16 +15,13 @@ 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.21.2
FROM alpine
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
# Copy owncast assets
WORKDIR /app
COPY --from=build /build/owncast /app/owncast
COPY --from=build /build/webroot /app/webroot
RUN mkdir /app/data
RUN chown -R owncast:owncast /app
USER owncast
ENTRYPOINT ["/app/owncast"]
EXPOSE 8080 1935

104
Earthfile
View File

@@ -1,15 +1,15 @@
VERSION --new-platform 0.6
FROM --platform=linux/amd64 alpine:3.21.2
FROM --platform=linux/amd64 alpine:latest
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
@@ -17,14 +17,14 @@ docker-all:
crosscompiler:
# This image is missing a few platforms, so we'll add them locally
FROM --platform=linux/amd64 bdwyertech/go-crosscompile
RUN apk add --update --no-cache tar gzip upx >> /dev/null
RUN curl -sfL "https://owncast-infra.nyc3.cdn.digitaloceanspaces.com/build/armv7l-linux-musleabihf-cross.tgz" | tar zxf - -C /usr/ --strip-components=1
RUN curl -sfL "https://owncast-infra.nyc3.cdn.digitaloceanspaces.com/build/i686-linux-musl-cross.tgz" | tar zxf - -C /usr/ --strip-components=1
RUN curl -sfL "https://owncast-infra.nyc3.cdn.digitaloceanspaces.com/build/x86_64-linux-musl-cross.tgz" | tar zxf - -C /usr/ --strip-components=1
RUN curl -sfL "https://musl.cc/armv7l-linux-musleabihf-cross.tgz" | tar zxf - -C /usr/ --strip-components=1
RUN curl -sfL "https://musl.cc/i686-linux-musl-cross.tgz" | tar zxf - -C /usr/ --strip-components=1
RUN curl -sfL "https://musl.cc/x86_64-linux-musl-cross.tgz" | tar zxf - -C /usr/ --strip-components=1
code:
FROM --platform=linux/amd64 +crosscompiler
COPY . /build
# GIT CLONE --branch=$version git@github.com:owncast/owncast.git /build
build:
ARG EARTHLY_GIT_HASH # provided by Earthly
@@ -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:
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
@@ -77,17 +74,21 @@ build:
WORKDIR /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
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" -o owncast main.go
COPY +tailwind/prod-tailwind.min.css /build/dist/webroot/js/web_modules/tailwindcss/dist/tailwind.min.css
# 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
SAVE ARTIFACT owncast owncast
SAVE ARTIFACT webroot webroot
SAVE ARTIFACT README.md README.md
SAVE ARTIFACT --keep-ts owncast owncast
tailwind:
FROM +code
WORKDIR /build/build/javascript
RUN apk add --update --no-cache npm >> /dev/null
ENV NODE_ENV=production
RUN cd /build/build/javascript && npm install --quiet --no-progress >> /dev/null && npm install -g cssnano postcss postcss-cli --quiet --no-progress --save-dev >> /dev/null && ./node_modules/.bin/tailwind build > /build/tailwind.min.css
RUN npx postcss /build/tailwind.min.css > /build/prod-tailwind.min.css
SAVE ARTIFACT /build/prod-tailwind.min.css prod-tailwind.min.css
package:
RUN apk add --update --no-cache zip >> /dev/null
@@ -103,66 +104,37 @@ 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
COPY --keep-ts (+build/owncast --platform $TARGETPLATFORM) /build/dist/owncast
COPY (+build/webroot --platform $TARGETPLATFORM) /build/dist/webroot
COPY (+build/owncast --platform $TARGETPLATFORM) /build/dist/owncast
COPY (+build/README.md --platform $TARGETPLATFORM) /build/dist/README.md
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
# in as space separated strings using the full account/repo:tag format.
# https://github.com/earthly/earthly/blob/aea38448fa9c0064b1b70d61be717ae740689fb9/docs/earthfile/earthfile.md#assigning-multiple-image-names
ARG image=ghcr.io/owncast/owncast
ARG tag=develop
ARG TARGETPLATFORM
FROM --platform=$TARGETPLATFORM alpine:3.21.2
FROM --platform=$TARGETPLATFORM alpine:latest
RUN apk update && apk add --no-cache ffmpeg ffmpeg-libs ca-certificates unzip && update-ca-certificates
RUN addgroup -g 101 -S owncast && adduser -u 101 -S owncast -G owncast
WORKDIR /app
COPY --keep-ts --platform=$TARGETPLATFORM +package/owncast.zip /app
COPY --platform=$TARGETPLATFORM +package/owncast.zip /app
RUN unzip -x owncast.zip && mkdir data
# temporarily disable until we figure out how to move forward
# RUN chown -R owncast:owncast /app
# USER owncast
ENTRYPOINT ["/app/owncast"]
EXPOSE 8080 1935
ARG images=ghcr.io/owncast/owncast:testing
RUN echo "Saving images: ${images}"
# Tag this image with the list of names
# passed along.
FOR --no-cache i IN ${images}
SAVE IMAGE --push "${i}"
END
dockerfile:
FROM DOCKERFILE -f Dockerfile .
unit-tests:
FROM --platform=linux/amd64 bdwyertech/go-crosscompile
COPY . /build
WORKDIR /build
RUN go test ./...
SAVE IMAGE --push $image:$tag
api-tests:
FROM --platform=linux/amd64 bdwyertech/go-crosscompile
RUN apk add npm font-noto && fc-cache -f
COPY . /build
WORKDIR /build/test/automated/api
RUN npm install
RUN ./run.sh
FROM --platform=linux/amd64 +code
WORKDIR /build
RUN apk add npm ffmpeg
RUN cd test/automated/api && npm install && ./run.sh
hls-tests:
FROM --platform=linux/amd64 bdwyertech/go-crosscompile
RUN apk add npm font-noto && fc-cache -f
COPY . /build
WORKDIR /build/test/automated/hls
RUN npm install
RUN ./run.sh
unit-tests:
FROM --platform=linux/amd64 +code
WORKDIR /build
RUN go test ./...

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2020-2023 Gabe Kangas
Copyright (c) 2020 Gabe Kangas
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

147
README.md
View File

@@ -1,48 +1,37 @@
<br />
<p align="center">
<a href="https://github.com/owncast/owncast" alt="Owncast">
<img src="https://owncast.online/images/logo.png" alt="Owncast Logo" width="200">
</a>
</p>
<p align="center">
<strong>Take control over your content and stream it yourself.</strong>
</p>
<br/>
<p align="center">
<a href="https://github.com/owncast/owncast/blob/develop/LICENSE">
<img src="https://img.shields.io/badge/License-MIT-green.svg" alt="License" />
<img src="https://owncast.online/images/logo.png" alt="Logo" width="200">
</a>
</p>
<br/>
<p align="center">
<a href="https://owncast.online"><strong>Explore the docs »</strong></a>
<br />
<a href="https://watch.owncast.online/">View Demo</a>
·
<a href="https://owncast.online/faq/">FAQ</a>
·
<a href="https://github.com/owncast/owncast/issues">Report Bug</a>
<p align="center">
<strong>Take control over your content and stream it yourself.</strong>
<br />
<a href="http://owncast.online"><strong>Explore the docs »</strong></a>
<br />
<a href="https://watch.owncast.online/">View Demo</a>
·
<a href="https://broadcast.owncast.online/">Use Our Server for Testing</a>
·
<a href="https://owncast.online/faq/">FAQ</a>
·
<a href="https://github.com/owncast/owncast/issues">Report Bug</a>
</p>
</p>
<!-- TABLE OF CONTENTS -->
## Table of Contents
- 📒 [About the Project](#about-the-project)
- 🚀 [Getting Started](#getting-started)
- 👨‍💻 [Use with your broadcasting software](#use-with-your-existing-broadcasting-software)
- 🛠 [Building from source](#building-from-source)
- 🚨 [Important note about source code and the develop branch](#important-note-about-source-code-and-the-develop-branch)
- 🗄️ [Backend](#backend)
- ⚛️ [Frontend](#frontend)
- 👏 [Contributing](#contributing)
- 💵 [Donors](#donors)
- 📝 [License](#license)
- [About the Project](#about-the-project)
- [Getting Started](#getting-started)
- [Use with your broadcasting software](#use-with-your-existing-broadcasting-software)
- [Building from source](#building-from-source)
- [Contributing](#contributing)
- [License](#license)
- [Contact](#contact)
<!-- ABOUT THE PROJECT -->
@@ -55,12 +44,12 @@
</a>
</p>
Owncast is an open source, self-hosted, decentralized, single user live video streaming and chat server for running your own live streams similar in style to the large mainstream options. It offers complete ownership over your content, interface, moderation and audience. <a href="https://watch.owncast.online">Visit the demo</a> for an example.
Owncast is an open source, self-hosted, decentralized, single user live video streaming and chat server for running your own live streams similar in style to the large mainstream options. It offers complete ownership over your content, interface, moderation and audience. <a href="https://watch.owncast.online">Visit the demo</a> for an example.
<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">
@@ -70,6 +59,7 @@ Owncast is an open source, self-hosted, decentralized, single user live video st
</a>
</div>
---
<!-- GETTING STARTED -->
@@ -80,45 +70,29 @@ The goal is to have a single service that you can run and it works out of the bo
## Use with your existing broadcasting software
In general, Owncast is compatible with any software that uses `RTMP` to broadcast to a remote server. `RTMP` is what all the major live streaming services use, so if youre currently using one of those its likely that you can point your existing software at your Owncast instance instead.
In general Owncast is compatible with any software that uses `RTMP` to broadcast to a remote server. `RTMP` is what all the major live streaming services use, so if youre currently using one of those its likely that you can point your existing software at your Owncast instance instead.
OBS, Streamlabs, Restream and many others have been used with Owncast. [Read more about compatibility with existing software](https://owncast.online/docs/broadcasting/).
## Building from Source
Owncast consists of two projects.
1. The Owncast backend is written in Go.
1. The frontend is written in React.
[Read more about running from source](https://owncast.online/development/).
### Important note about source code and the develop branch
The `develop` branch is always the most up-to-date state of development and this may not be what you always want. If you want to run the latest released stable version, check out the tag related to that release. For example, if you'd only like the source prior to the v0.1.0 development cycle you can check out the `v0.0.13` tag.
> Note: Currently Owncast does not natively support Windows servers. However, Windows Users can use Windows Subsystem for Linux (WSL2) to install Owncast. For details visit [this document](https://github.com/owncast/owncast/blob/develop/contrib/owncast_for_windows.md).
### Backend
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.22 or above).
1. Ensure you have the gcc compiler installed.
1. Install the [Go toolchain](https://golang.org/dl/) (1.16 or above).
1. Clone the repo. `git clone https://github.com/owncast/owncast`
1. `go run main.go` will run from the source.
1. `go run main.go` will run from source.
1. Visit `http://yourserver:8080` to access the web interface or `http://yourserver:8080/admin` to access the admin.
1. Point your [broadcasting software](https://owncast.online/docs/broadcasting/) at your new server and start streaming.
### Frontend
There is also a supplied `Dockerfile` so you can spin it up from source with little effort. [Read more about running from source](https://owncast.online/docs/building/).
The frontend is the web interface that includes the player, chat, embed components, and other UI.
### Bundling in latest admin from source
1. This project lives in the `web` directory.
1. Run `npm install` to install the Javascript dependencies.
1. Run `npm run dev`
The admin ui is built at: https://github.com/owncast/owncast-admin it is bundled into the final binary using pkger.
To bundle in the latest admin UI:
1. From the owncast directory run the packager script: `./build/admin/bundleAdmin.sh`
1. Compile or run like above. `go run main.go`
## Contributing
@@ -128,56 +102,31 @@ And while we have a small team of kind, talented and thoughtful volunteers, we h
We abide by our [Code of Conduct](https://owncast.online/contribute/) and feel strongly about open, appreciative, and empathetic people joining us.
Weve been very lucky to have this so far, so maybe you can help us with your skills and passion, too!
If you're new to the project, maybe you'd be interested in looking at [![Good First Issue](https://img.shields.io/github/issues/owncast/owncast/good%20first%20issue.svg)](https://github.com/owncast/owncast/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22).
There is a larger, more detailed, and more up-to-date [guide for helping contribute to Owncast on our website](https://owncast.online/help/).
### Donors
The Owncast project is possible thanks to the people who make a donation to support us and our work.
Thank you to all our donors who help keep Owncast running by donating on OpenCollective. You can support this project by [becoming a backer/sponsor](https://opencollective.com/owncast#suppor).
### Architecture
Owncast consists of two repositories with two standalone projects. [The repo you're looking at now](https://github.com/owncast/owncast) is the core repository with the backend and frontend. [owncast/owncast-admin](https://github.com/owncast/owncast-admin) is an additional web project that is built separately and used for configuration and management of an Owncast server.
### Suggestions when working with the Owncast codebase
1. Install [golangci-lint](https://golangci-lint.run/usage/install/) for helpful warnings and suggestions [directly in your editor](https://golangci-lint.run/usage/integrations/) when writing Go.
1. If using VSCode install the [lit-html](https://marketplace.visualstudio.com/items?itemName=bierner.lit-html) extension to aid in syntax highlighting of our frontend HTML + Preact.
1. Run the project with `go run main.go`.
<div>
<a href="https://opencollective.com/owncast#support">
<img alt="GitHub issues by-label" src="https://opencollective.com/owncast/tiers/backers.svg?avatarHeight=36&width=600" alt="Backer button">
</a>
</div>
<!-- LICENSE -->
## License
Distributed under the MIT License. See `LICENSE` for more information.
## Support
<ul style="font-size:21px; color:black; ">
<li>Browser testing via <a
href="https://www.lambdatest.com/" target="_blank"><img
src="https://www.lambdatest.com/support/img/logo.svg"
style="vertical-align: middle;margin-left:5px" width="147" height="26"
/></a></li>
<li>Project chat provided by
<a href="https://rocket.chat" target="_blank">
<img src="https://owncast.online/images/sponsors/rocketchat.png" width="147" height="26" style="vertical-align: middle;margin-left:5px">
</a>
</li>
<li>CDN services by
<a href="https://fastly.com" target="_blank">
<img src="https://owncast.online/images/sponsors/fastly.png" height="26" style="vertical-align: middle;margin-left:5px">
</a>
</li>
<li>UI testing with Chromatic
<a href="https://chromatic.com" target="_blank">
<img src="https://owncast.online/images/sponsors/chromatic.png" height="26" style="vertical-align: middle;margin-left:5px">
</a>
</li>
</ul>
<!-- CONTACT -->
## Contact
Project chat: [Join us on Rocket.Chat](https://owncast.rocket.chat/home) if you want to contribute, follow along, or if you have questions.
Gabe Kangas - [@gabek@social.gabekangas.com](https://social.gabekangas.com/gabek) - email [gabek@real-ity.com](mailto:gabek@real-ity.com)
Gabe Kangas - [@gabek@fosstodon.org](https://fosstodon.org/@gabek) - email [gabek@real-ity.com](mailto:gabek@real-ity.com)
Project Link: [https://github.com/owncast/owncast](https://github.com/owncast/owncast)

View File

@@ -1,14 +1,11 @@
package activitypub
import (
"math"
"github.com/owncast/owncast/activitypub/crypto"
"github.com/owncast/owncast/activitypub/inbox"
"github.com/owncast/owncast/activitypub/outbox"
"github.com/owncast/owncast/activitypub/persistence"
"github.com/owncast/owncast/activitypub/workerpool"
"github.com/owncast/owncast/persistence/configrepository"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/models"
@@ -17,35 +14,22 @@ import (
// Start will initialize and start the federation support.
func Start(datastore *data.Datastore) {
configRepository := configrepository.Get()
persistence.Setup(datastore)
outboundWorkerPoolSize := getOutboundWorkerPoolSize()
workerpool.InitOutboundWorkerPool(outboundWorkerPoolSize)
workerpool.InitOutboundWorkerPool()
inbox.InitInboxWorkerPool()
StartRouter()
// Generate the keys for signing federated activity if needed.
if configRepository.GetPrivateKey() == "" {
if data.GetPrivateKey() == "" {
privateKey, publicKey, err := crypto.GenerateKeys()
_ = configRepository.SetPrivateKey(string(privateKey))
_ = configRepository.SetPublicKey(string(publicKey))
_ = data.SetPrivateKey(string(privateKey))
_ = data.SetPublicKey(string(publicKey))
if err != nil {
log.Errorln("Unable to get private key", err)
}
}
}
func getOutboundWorkerPoolSize() int {
var followerCount int64
fc, err := persistence.GetFollowerCount()
if err != nil {
log.Errorln("Unable to get follower count", err)
fc = 50 // Arbitrary fallback value.
}
followerCount = int64(math.Max(float64(fc), 50))
return int(followerCount * 5)
}
// SendLive will send a "Go Live" message to followers.
func SendLive() error {
return outbox.SendLive()

View File

@@ -6,7 +6,7 @@ import (
"github.com/go-fed/activity/streams"
"github.com/go-fed/activity/streams/vocab"
"github.com/owncast/owncast/persistence/configrepository"
"github.com/owncast/owncast/core/data"
)
// PrivacyAudience represents the audience for an activity.
@@ -87,10 +87,8 @@ func MakeActivityDirect(activity vocab.ActivityStreamsCreate, toIRI *url.URL) vo
// MakeActivityPublic sets the required properties to make this activity
// seen as public.
func MakeActivityPublic(activity vocab.ActivityStreamsCreate) vocab.ActivityStreamsCreate {
configRepository := configrepository.Get()
// TO the public if we're not treating ActivityPub as "private".
if !configRepository.GetFederationIsPrivate() {
if !data.GetFederationIsPrivate() {
public, _ := url.Parse(PUBLIC)
to := streams.NewActivityStreamsToProperty()
@@ -123,9 +121,7 @@ func MakeUpdateActivity(activityID *url.URL) vocab.ActivityStreamsUpdate {
activity.SetJSONLDId(id)
// CC the public if we're not treating ActivityPub as "private".
configRepository := configrepository.Get()
if !configRepository.GetFederationIsPrivate() {
if !data.GetFederationIsPrivate() {
public, _ := url.Parse(PUBLIC)
cc := streams.NewActivityStreamsCcProperty()
cc.AppendIRI(public)

View File

@@ -9,33 +9,33 @@ import (
"github.com/go-fed/activity/streams"
"github.com/go-fed/activity/streams/vocab"
"github.com/owncast/owncast/activitypub/crypto"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/persistence/configrepository"
log "github.com/sirupsen/logrus"
)
// ActivityPubActor represents a single actor in handling ActivityPub activity.
type ActivityPubActor struct {
// RequestObject is the actual follow request object.
RequestObject vocab.ActivityStreamsFollow
// W3IDSecurityV1PublicKey is the public key of the actor.
W3IDSecurityV1PublicKey vocab.W3IDSecurityV1PublicKeyProperty
// ActorIRI is the IRI of the remote actor.
ActorIri *url.URL
// FollowRequestIRI is the unique identifier of the follow request.
FollowRequestIri *url.URL
// Inbox is the inbox URL of the remote follower
Inbox *url.URL
// Image is the avatar image of the Actor.
Image *url.URL
// DisabledAt is the time, if any, this follower was blocked/removed.
DisabledAt *time.Time
// Name is the display name of the follower.
Name string
// Username is the account username of the remote actor.
Username string
// FullUsername is the username@account.tld representation of the user.
FullUsername string
// Image is the avatar image of the Actor.
Image *url.URL
// RequestObject is the actual follow request object.
RequestObject vocab.ActivityStreamsFollow
// W3IDSecurityV1PublicKey is the public key of the actor.
W3IDSecurityV1PublicKey vocab.W3IDSecurityV1PublicKeyProperty
// DisabledAt is the time, if any, this follower was blocked/removed.
DisabledAt *time.Time
}
// DeleteRequest represents a request for delete.
@@ -101,13 +101,11 @@ func MakeActorPropertyWithID(idIRI *url.URL) vocab.ActivityStreamsActorProperty
// MakeServiceForAccount will create a new local actor service with the the provided username.
func MakeServiceForAccount(accountName string) vocab.ActivityStreamsService {
configRepository := configrepository.Get()
actorIRI := MakeLocalIRIForAccount(accountName)
person := streams.NewActivityStreamsService()
nameProperty := streams.NewActivityStreamsNameProperty()
nameProperty.AppendXMLSchemaString(configRepository.GetServerName())
nameProperty.AppendXMLSchemaString(data.GetServerName())
person.SetActivityStreamsName(nameProperty)
preferredUsernameProperty := streams.NewActivityStreamsPreferredUsernameProperty()
@@ -121,7 +119,7 @@ func MakeServiceForAccount(accountName string) vocab.ActivityStreamsService {
person.SetActivityStreamsInbox(inboxProp)
needsFollowApprovalProperty := streams.NewActivityStreamsManuallyApprovesFollowersProperty()
needsFollowApprovalProperty.Set(configRepository.GetFederationIsPrivate())
needsFollowApprovalProperty.Set(data.GetFederationIsPrivate())
person.SetActivityStreamsManuallyApprovesFollowers(needsFollowApprovalProperty)
outboxIRI := MakeLocalIRIForResource("/user/" + accountName + "/outbox")
@@ -154,7 +152,7 @@ func MakeServiceForAccount(accountName string) vocab.ActivityStreamsService {
publicKeyProp.AppendW3IDSecurityV1PublicKey(publicKeyType)
person.SetW3IDSecurityV1PublicKey(publicKeyProp)
if t, err := configRepository.GetServerInitTime(); t != nil {
if t, err := data.GetServerInitTime(); t != nil {
publishedDateProp := streams.NewActivityStreamsPublishedProperty()
publishedDateProp.Set(t.Time)
person.SetActivityStreamsPublished(publishedDateProp)
@@ -165,8 +163,8 @@ func MakeServiceForAccount(accountName string) vocab.ActivityStreamsService {
// Profile properties
// Avatar
uniquenessString := configRepository.GetLogoUniquenessString()
userAvatarURLString := configRepository.GetServerURL() + "/logo/external"
uniquenessString := data.GetLogoUniquenessString()
userAvatarURLString := data.GetServerURL() + "/logo/external"
userAvatarURL, err := url.Parse(userAvatarURLString)
userAvatarURL.RawQuery = "uc=" + uniquenessString
if err != nil {
@@ -197,14 +195,14 @@ func MakeServiceForAccount(accountName string) vocab.ActivityStreamsService {
// Profile bio
summaryProperty := streams.NewActivityStreamsSummaryProperty()
summaryProperty.AppendXMLSchemaString(configRepository.GetServerSummary())
summaryProperty.AppendXMLSchemaString(data.GetServerSummary())
person.SetActivityStreamsSummary(summaryProperty)
// Links
if serverURL := configRepository.GetServerURL(); serverURL != "" {
if serverURL := data.GetServerURL(); serverURL != "" {
addMetadataLinkToProfile(person, "Stream", serverURL)
}
for _, link := range configRepository.GetSocialHandles() {
for _, link := range data.GetSocialHandles() {
addMetadataLinkToProfile(person, link.Platform, link.URL)
}
@@ -222,7 +220,7 @@ func MakeServiceForAccount(accountName string) vocab.ActivityStreamsService {
// Tags
tagProp := streams.NewActivityStreamsTagProperty()
for _, tagString := range configRepository.GetServerMetadataTags() {
for _, tagString := range data.GetServerMetadataTags() {
hashtag := MakeHashtag(tagString)
tagProp.AppendTootHashtag(hashtag)
}
@@ -231,7 +229,7 @@ func MakeServiceForAccount(accountName string) vocab.ActivityStreamsService {
// Work around an issue where a single attachment will not serialize
// as an array, so add another item to the mix.
if len(configRepository.GetSocialHandles()) == 1 {
if len(data.GetSocialHandles()) == 1 {
addMetadataLinkToProfile(person, "Owncast", "https://owncast.online")
}

View File

@@ -9,7 +9,6 @@ import (
"github.com/go-fed/activity/streams"
"github.com/go-fed/activity/streams/vocab"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/persistence/configrepository"
)
func makeFakeService() vocab.ActivityStreamsService {
@@ -56,11 +55,9 @@ func TestMain(m *testing.M) {
if err != nil {
panic(err)
}
data.SetupPersistence(dbFile.Name())
configRepository := configrepository.Get()
configRepository.SetServerURL("https://my.cool.site.biz")
data.SetServerURL("https://my.cool.site.biz")
m.Run()
}
@@ -156,7 +153,7 @@ func TestMakeServiceForAccount(t *testing.T) {
t.Errorf("actor.Followers = %v, want %v", person.GetActivityStreamsFollowers().GetIRI().String(), expectedFollowers)
}
expectedName := "New Owncast Server"
expectedName := "Owncast"
if person.GetActivityStreamsName().Begin().GetXMLSchemaString() != expectedName {
t.Errorf("actor.Name = %v, want %v", person.GetActivityStreamsName().Begin().GetXMLSchemaString(), expectedName)
}
@@ -171,7 +168,7 @@ func TestMakeServiceForAccount(t *testing.T) {
t.Errorf("actor.Avatar = %v, want %v", person.GetActivityStreamsIcon().At(0).GetActivityStreamsImage().GetActivityStreamsUrl().Begin().GetIRI().String(), expectedAvatar)
}
expectedSummary := "This is a new live video streaming server powered by Owncast."
expectedSummary := "Welcome to your new Owncast server! This description can be changed in the admin. Visit https://owncast.online/docs/configuration/ to learn more."
if person.GetActivityStreamsSummary().At(0).GetXMLSchemaString() != expectedSummary {
t.Errorf("actor.Summary = %v, want %v", person.GetActivityStreamsSummary().At(0).GetXMLSchemaString(), expectedSummary)
}

View File

@@ -4,11 +4,10 @@ import (
"encoding/json"
"net/url"
"path"
"path/filepath"
"github.com/go-fed/activity/streams"
"github.com/go-fed/activity/streams/vocab"
"github.com/owncast/owncast/persistence/configrepository"
"github.com/owncast/owncast/core/data"
log "github.com/sirupsen/logrus"
)
@@ -27,9 +26,7 @@ func MakeRemoteIRIForResource(resourcePath string, host string) (*url.URL, error
// MakeLocalIRIForResource will create an IRI for the local server.
func MakeLocalIRIForResource(resourcePath string) *url.URL {
configRepository := configrepository.Get()
host := configRepository.GetServerURL()
host := data.GetServerURL()
u, err := url.Parse(host)
if err != nil {
log.Errorln("unable to parse local IRI url", host, err)
@@ -43,9 +40,7 @@ func MakeLocalIRIForResource(resourcePath string) *url.URL {
// MakeLocalIRIForAccount will return a full IRI for the local server account username.
func MakeLocalIRIForAccount(account string) *url.URL {
configRepository := configrepository.Get()
host := configRepository.GetServerURL()
host := data.GetServerURL()
u, err := url.Parse(host)
if err != nil {
log.Errorln("unable to parse local IRI account server url", err)
@@ -65,56 +60,3 @@ func Serialize(obj vocab.Type) ([]byte, error) {
return b, err
}
// MakeLocalIRIForStreamURL will return a full IRI for the local server stream url.
func MakeLocalIRIForStreamURL() *url.URL {
configRepository := configrepository.Get()
host := configRepository.GetServerURL()
u, err := url.Parse(host)
if err != nil {
log.Errorln("unable to parse local IRI stream url", err)
return nil
}
u.Path = path.Join(u.Path, "/hls/stream.m3u8")
return u
}
// MakeLocalIRIforLogo will return a full IRI for the local server logo.
func MakeLocalIRIforLogo() *url.URL {
configRepository := configrepository.Get()
host := configRepository.GetServerURL()
u, err := url.Parse(host)
if err != nil {
log.Errorln("unable to parse local IRI stream url", err)
return nil
}
u.Path = path.Join(u.Path, "/logo/external")
return u
}
// GetLogoType will return the rel value for the webfinger response and
// the default static image is of type png.
func GetLogoType() string {
configRepository := configrepository.Get()
imageFilename := configRepository.GetLogoPath()
if imageFilename == "" {
return "image/png"
}
logoType := "image/jpeg"
if filepath.Ext(imageFilename) == ".svg" {
logoType = "image/svg+xml"
} else if filepath.Ext(imageFilename) == ".gif" {
logoType = "image/gif"
} else if filepath.Ext(imageFilename) == ".png" {
logoType = "image/png"
}
return logoType
}

View File

@@ -26,9 +26,7 @@ type Link struct {
// MakeWebfingerResponse will create a new Webfinger response.
func MakeWebfingerResponse(account string, inbox string, host string) WebfingerResponse {
accountIRI := MakeLocalIRIForAccount(account)
streamIRI := MakeLocalIRIForStreamURL()
logoIRI := MakeLocalIRIforLogo()
logoType := GetLogoType()
return WebfingerResponse{
Subject: fmt.Sprintf("acct:%s@%s", account, host),
Aliases: []string{
@@ -45,16 +43,6 @@ func MakeWebfingerResponse(account string, inbox string, host string) WebfingerR
Type: "text/html",
Href: accountIRI.String(),
},
{
Rel: "http://webfinger.net/rel/avatar",
Type: logoType,
Href: logoIRI.String(),
},
{
Rel: "alternate",
Type: "application/x-mpegURL",
Href: streamIRI.String(),
},
},
}
}

View File

@@ -9,14 +9,12 @@ import (
"github.com/owncast/owncast/activitypub/apmodels"
"github.com/owncast/owncast/activitypub/crypto"
"github.com/owncast/owncast/activitypub/requests"
"github.com/owncast/owncast/persistence/configrepository"
"github.com/owncast/owncast/core/data"
)
// ActorHandler handles requests for a single actor.
func ActorHandler(w http.ResponseWriter, r *http.Request) {
configRepository := configrepository.Get()
if !configRepository.GetFederationEnabled() {
if !data.GetFederationEnabled() {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
@@ -24,7 +22,7 @@ func ActorHandler(w http.ResponseWriter, r *http.Request) {
pathComponents := strings.Split(r.URL.Path, "/")
accountName := pathComponents[3]
if _, valid := configRepository.GetFederatedInboxMap()[accountName]; !valid {
if _, valid := data.GetFederatedInboxMap()[accountName]; !valid {
// User is not valid
w.WriteHeader(http.StatusNotFound)
return

View File

@@ -16,7 +16,7 @@ import (
"github.com/owncast/owncast/activitypub/crypto"
"github.com/owncast/owncast/activitypub/persistence"
"github.com/owncast/owncast/activitypub/requests"
"github.com/owncast/owncast/persistence/configrepository"
"github.com/owncast/owncast/core/data"
)
const (
@@ -145,9 +145,7 @@ func getFollowersPage(page string, r *http.Request) (vocab.ActivityStreamsOrdere
}
func createPageURL(r *http.Request, page *string) (*url.URL, error) {
configRepository := configrepository.Get()
domain := configRepository.GetServerURL()
domain := data.GetServerURL()
if domain == "" {
return nil, errors.New("unable to get server URL")
}

View File

@@ -7,7 +7,7 @@ import (
"github.com/owncast/owncast/activitypub/apmodels"
"github.com/owncast/owncast/activitypub/inbox"
"github.com/owncast/owncast/persistence/configrepository"
"github.com/owncast/owncast/core/data"
log "github.com/sirupsen/logrus"
)
@@ -22,9 +22,7 @@ func InboxHandler(w http.ResponseWriter, r *http.Request) {
}
func acceptInboxRequest(w http.ResponseWriter, r *http.Request) {
configRepository := configrepository.Get()
if !configRepository.GetFederationEnabled() {
if !data.GetFederationEnabled() {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
@@ -41,7 +39,7 @@ func acceptInboxRequest(w http.ResponseWriter, r *http.Request) {
// The account this request is for must match the account name we have set
// for federation.
if forLocalAccount != configRepository.GetFederationUsername() {
if forLocalAccount != data.GetFederationUsername() {
w.WriteHeader(http.StatusNotFound)
return
}

View File

@@ -10,7 +10,7 @@ import (
"github.com/owncast/owncast/activitypub/persistence"
"github.com/owncast/owncast/activitypub/requests"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/persistence/configrepository"
"github.com/owncast/owncast/core/data"
log "github.com/sirupsen/logrus"
)
@@ -25,14 +25,12 @@ func NodeInfoController(w http.ResponseWriter, r *http.Request) {
Links []links `json:"links"`
}
configRepository := configrepository.Get()
if !configRepository.GetFederationEnabled() {
if !data.GetFederationEnabled() {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
serverURL := configRepository.GetServerURL()
serverURL := data.GetServerURL()
if serverURL == "" {
w.WriteHeader(http.StatusNotFound)
return
@@ -91,9 +89,7 @@ func NodeInfoV2Controller(w http.ResponseWriter, r *http.Request) {
Metadata metadata `json:"metadata"`
}
configRepository := configrepository.Get()
if !configRepository.GetFederationEnabled() {
if !data.GetFederationEnabled() {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
@@ -121,7 +117,7 @@ func NodeInfoV2Controller(w http.ResponseWriter, r *http.Request) {
OpenRegistrations: false,
Protocols: []string{"activitypub"},
Metadata: metadata{
ChatEnabled: !configRepository.GetChatDisabled(),
ChatEnabled: !data.GetChatDisabled(),
},
}
@@ -158,23 +154,21 @@ func XNodeInfo2Controller(w http.ResponseWriter, r *http.Request) {
LocalComments int `json:"localComments"`
}
type response struct {
Server Server `json:"server"`
Organization Organization `json:"organization"`
Version string `json:"version"`
Server Server `json:"server"`
Services Services `json:"services"`
Protocols []string `json:"protocols"`
Usage Usage `json:"usage"`
Version string `json:"version"`
OpenRegistrations bool `json:"openRegistrations"`
Usage Usage `json:"usage"`
}
configRepository := configrepository.Get()
if !configRepository.GetFederationEnabled() {
if !data.GetFederationEnabled() {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
serverURL := configRepository.GetServerURL()
serverURL := data.GetServerURL()
if serverURL == "" {
w.WriteHeader(http.StatusNotFound)
return
@@ -184,7 +178,7 @@ func XNodeInfo2Controller(w http.ResponseWriter, r *http.Request) {
res := &response{
Organization: Organization{
Name: configRepository.GetServerName(),
Name: data.GetServerName(),
Contact: serverURL,
},
Server: Server{
@@ -230,22 +224,20 @@ func InstanceV1Controller(w http.ResponseWriter, r *http.Request) {
ShortDescription string `json:"short_description"`
Description string `json:"description"`
Version string `json:"version"`
Stats Stats `json:"stats"`
Thumbnail string `json:"thumbnail"`
Languages []string `json:"languages"`
Stats Stats `json:"stats"`
Registrations bool `json:"registrations"`
ApprovalRequired bool `json:"approval_required"`
InvitesEnabled bool `json:"invites_enabled"`
}
configRepository := configrepository.Get()
if !configRepository.GetFederationEnabled() {
if !data.GetFederationEnabled() {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
serverURL := configRepository.GetServerURL()
serverURL := data.GetServerURL()
if serverURL == "" {
w.WriteHeader(http.StatusNotFound)
return
@@ -262,9 +254,9 @@ func InstanceV1Controller(w http.ResponseWriter, r *http.Request) {
res := response{
URI: serverURL,
Title: configRepository.GetServerName(),
ShortDescription: configRepository.GetServerSummary(),
Description: configRepository.GetServerSummary(),
Title: data.GetServerName(),
ShortDescription: data.GetServerSummary(),
Description: data.GetServerSummary(),
Version: config.GetReleaseString(),
Stats: Stats{
UserCount: 1,
@@ -283,9 +275,7 @@ func InstanceV1Controller(w http.ResponseWriter, r *http.Request) {
}
func writeResponse(payload interface{}, w http.ResponseWriter) error {
configRepository := configrepository.Get()
accountName := configRepository.GetDefaultFederationUsername()
accountName := data.GetDefaultFederationUsername()
actorIRI := apmodels.MakeLocalIRIForAccount(accountName)
publicKey := crypto.GetPublicKey(actorIRI)
@@ -294,15 +284,7 @@ func writeResponse(payload interface{}, w http.ResponseWriter) error {
// HostMetaController points to webfinger.
func HostMetaController(w http.ResponseWriter, r *http.Request) {
configRepository := configrepository.Get()
if !configRepository.GetFederationEnabled() {
w.WriteHeader(http.StatusMethodNotAllowed)
log.Debugln("host meta request rejected! Federation is not enabled")
return
}
serverURL := configRepository.GetServerURL()
serverURL := data.GetServerURL()
if serverURL == "" {
w.WriteHeader(http.StatusNotFound)
return

View File

@@ -8,33 +8,31 @@ import (
"github.com/owncast/owncast/activitypub/crypto"
"github.com/owncast/owncast/activitypub/persistence"
"github.com/owncast/owncast/activitypub/requests"
"github.com/owncast/owncast/persistence/configrepository"
"github.com/owncast/owncast/core/data"
log "github.com/sirupsen/logrus"
)
// ObjectHandler handles requests for a single federated ActivityPub object.
func ObjectHandler(w http.ResponseWriter, r *http.Request) {
configRepository := configrepository.Get()
if !configRepository.GetFederationEnabled() {
if !data.GetFederationEnabled() {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
// If private federation mode is enabled do not allow access to objects.
if configRepository.GetFederationIsPrivate() {
if data.GetFederationIsPrivate() {
w.WriteHeader(http.StatusNotFound)
return
}
iri := strings.Join([]string{strings.TrimSuffix(configRepository.GetServerURL(), "/"), r.URL.Path}, "")
iri := strings.Join([]string{strings.TrimSuffix(data.GetServerURL(), "/"), r.URL.Path}, "")
object, _, _, err := persistence.GetObjectByIRI(iri)
if err != nil {
w.WriteHeader(http.StatusNotFound)
return
}
accountName := configRepository.GetDefaultFederationUsername()
accountName := data.GetDefaultFederationUsername()
actorIRI := apmodels.MakeLocalIRIForAccount(accountName)
publicKey := crypto.GetPublicKey(actorIRI)

View File

@@ -6,64 +6,53 @@ import (
"strings"
"github.com/owncast/owncast/activitypub/apmodels"
"github.com/owncast/owncast/persistence/configrepository"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/utils"
log "github.com/sirupsen/logrus"
)
// WebfingerHandler will handle webfinger lookup requests.
func WebfingerHandler(w http.ResponseWriter, r *http.Request) {
configRepository := configrepository.Get()
if !configRepository.GetFederationEnabled() {
if !data.GetFederationEnabled() {
w.WriteHeader(http.StatusMethodNotAllowed)
log.Debugln("webfinger request rejected! Federation is not enabled")
return
}
instanceHostURL := configRepository.GetServerURL()
if instanceHostURL == "" {
w.WriteHeader(http.StatusNotFound)
log.Warnln("webfinger request rejected! Federation is enabled but server URL is empty.")
return
}
instanceHostString := utils.GetHostnameFromURLString(instanceHostURL)
if instanceHostString == "" {
w.WriteHeader(http.StatusNotFound)
log.Warnln("webfinger request rejected! Federation is enabled but server URL is not set properly. data.GetServerURL(): " + configRepository.GetServerURL())
return
}
resource := r.URL.Query().Get("resource")
preAcct, account, foundAcct := strings.Cut(resource, "acct:")
resourceComponents := strings.Split(resource, ":")
if !foundAcct || preAcct != "" {
w.WriteHeader(http.StatusBadRequest)
log.Debugln("webfinger request rejected! Malformed resource in query: " + resource)
return
var account string
if len(resourceComponents) == 2 {
account = resourceComponents[1]
} else {
account = resourceComponents[0]
}
userComponents := strings.Split(account, "@")
if len(userComponents) != 2 {
w.WriteHeader(http.StatusBadRequest)
log.Debugln("webfinger request rejected! Malformed account in query: " + account)
if len(userComponents) < 2 {
return
}
host := userComponents[1]
user := userComponents[0]
if _, valid := configRepository.GetFederatedInboxMap()[user]; !valid {
if _, valid := data.GetFederatedInboxMap()[user]; !valid {
// User is not valid
w.WriteHeader(http.StatusNotFound)
log.Debugln("webfinger request rejected! Invalid user: " + user)
log.Debugln("webfinger request rejected")
return
}
// If the webfinger request doesn't match our server then it
// should be rejected.
if instanceHostString != host {
instanceHostString := data.GetServerURL()
if instanceHostString == "" {
w.WriteHeader(http.StatusNotImplemented)
return
}
instanceHostString = utils.GetHostnameFromURLString(instanceHostString)
if instanceHostString == "" || instanceHostString != host {
w.WriteHeader(http.StatusNotImplemented)
log.Debugln("webfinger request rejected! Invalid query host: " + host + " instanceHostString: " + instanceHostString)
return
}

View File

@@ -8,15 +8,13 @@ import (
"errors"
"net/url"
"github.com/owncast/owncast/persistence/configrepository"
"github.com/owncast/owncast/core/data"
log "github.com/sirupsen/logrus"
)
// GetPublicKey will return the public key for the provided actor.
func GetPublicKey(actorIRI *url.URL) PublicKey {
configRepository := configrepository.Get()
key := configRepository.GetPublicKey()
key := data.GetPublicKey()
idURL, err := url.Parse(actorIRI.String() + "#main-key")
if err != nil {
log.Errorln("unable to parse actor iri string", idURL, err)
@@ -31,9 +29,7 @@ func GetPublicKey(actorIRI *url.URL) PublicKey {
// GetPrivateKey will return the internal server private key.
func GetPrivateKey() *rsa.PrivateKey {
configRepository := configrepository.Get()
key := configRepository.GetPrivateKey()
key := data.GetPrivateKey()
block, _ := pem.Decode([]byte(key))
if block == nil {

View File

@@ -7,19 +7,17 @@ import (
"github.com/owncast/owncast/activitypub/resolvers"
"github.com/owncast/owncast/core/chat"
"github.com/owncast/owncast/core/chat/events"
"github.com/owncast/owncast/persistence/configrepository"
"github.com/owncast/owncast/core/data"
)
func handleEngagementActivity(eventType events.EventType, isLiveNotification bool, actorReference vocab.ActivityStreamsActorProperty, action string) error {
configRepository := configrepository.Get()
// Do nothing if displaying engagement actions has been turned off.
if !configRepository.GetFederationShowEngagement() {
if !data.GetFederationShowEngagement() {
return nil
}
// Do nothing if chat is disabled
if configRepository.GetChatDisabled() {
if data.GetChatDisabled() {
return nil
}
@@ -38,11 +36,11 @@ func handleEngagementActivity(eventType events.EventType, isLiveNotification boo
if isLiveNotification && action == events.FediverseEngagementLike {
suffix = "liked that this stream went live."
} else if action == events.FediverseEngagementLike {
suffix = fmt.Sprintf("liked a post from %s.", configRepository.GetServerName())
suffix = fmt.Sprintf("liked a post from %s.", data.GetServerName())
} else if isLiveNotification && action == events.FediverseEngagementRepost {
suffix = "shared this stream with their followers."
} else if action == events.FediverseEngagementRepost {
suffix = fmt.Sprintf("shared a post from %s.", configRepository.GetServerName())
suffix = fmt.Sprintf("shared a post from %s.", data.GetServerName())
} else if action == events.FediverseEngagementFollow {
suffix = "followed this stream."
} else {

View File

@@ -10,15 +10,13 @@ import (
"github.com/owncast/owncast/activitypub/requests"
"github.com/owncast/owncast/activitypub/resolvers"
"github.com/owncast/owncast/core/chat/events"
"github.com/owncast/owncast/persistence/configrepository"
"github.com/owncast/owncast/core/data"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)
func handleFollowInboxRequest(c context.Context, activity vocab.ActivityStreamsFollow) error {
configRepository := configrepository.Get()
follow, err := resolvers.MakeFollowRequest(c, activity)
if err != nil {
log.Errorln("unable to create follow inbox request", err)
@@ -29,7 +27,7 @@ func handleFollowInboxRequest(c context.Context, activity vocab.ActivityStreamsF
return fmt.Errorf("unable to handle request")
}
approved := !configRepository.GetFederationIsPrivate()
approved := !data.GetFederationIsPrivate()
followRequest := *follow
@@ -38,7 +36,7 @@ func handleFollowInboxRequest(c context.Context, activity vocab.ActivityStreamsF
return err
}
localAccountName := configRepository.GetDefaultFederationUsername()
localAccountName := data.GetDefaultFederationUsername()
if approved {
if err := requests.SendFollowAccept(follow.Inbox, activity, localAccountName); err != nil {

View File

@@ -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()

View File

@@ -15,7 +15,7 @@ import (
"github.com/owncast/owncast/activitypub/apmodels"
"github.com/owncast/owncast/activitypub/persistence"
"github.com/owncast/owncast/activitypub/resolvers"
"github.com/owncast/owncast/persistence/configrepository"
"github.com/owncast/owncast/core/data"
log "github.com/sirupsen/logrus"
)
@@ -131,9 +131,7 @@ func Verify(request *http.Request) (bool, error) {
}
func isBlockedDomain(domain string) bool {
configRepository := configrepository.Get()
blockedDomains := configRepository.GetBlockedFederatedDomains()
blockedDomains := data.GetBlockedFederatedDomains()
for _, blockedDomain := range blockedDomains {
if strings.Contains(domain, blockedDomain) {

View File

@@ -9,7 +9,6 @@ import (
"github.com/owncast/owncast/activitypub/apmodels"
"github.com/owncast/owncast/activitypub/persistence"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/persistence/configrepository"
)
func makeFakePerson() vocab.ActivityStreamsPerson {
@@ -50,24 +49,21 @@ func makeFakePerson() vocab.ActivityStreamsPerson {
func TestMain(m *testing.M) {
data.SetupPersistence(":memory:")
configRepository := configrepository.Get()
configRepository.SetServerURL("https://my.cool.site.biz")
data.SetServerURL("https://my.cool.site.biz")
persistence.Setup(data.GetDatastore())
m.Run()
}
func TestBlockedDomains(t *testing.T) {
configRepository := configrepository.Get()
person := makeFakePerson()
configRepository.SetBlockedFederatedDomains([]string{"freedom.eagle", "guns.life"})
data.SetBlockedFederatedDomains([]string{"freedom.eagle", "guns.life"})
if len(configRepository.GetBlockedFederatedDomains()) != 2 {
if len(data.GetBlockedFederatedDomains()) != 2 {
t.Error("Blocked federated domains is not set correctly")
}
for _, domain := range configRepository.GetBlockedFederatedDomains() {
for _, domain := range data.GetBlockedFederatedDomains() {
if domain == person.GetJSONLDId().GetIRI().Host {
return
}

View File

@@ -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() {
queue = make(chan Job)
// start workers
for i := 1; i <= workerPoolSize; i++ {
for i := 1; i <= InboxWorkerPoolSize; i++ {
go worker(i, queue)
}
}

View File

@@ -16,10 +16,10 @@ import (
"github.com/owncast/owncast/activitypub/resolvers"
"github.com/owncast/owncast/activitypub/webfinger"
"github.com/owncast/owncast/activitypub/workerpool"
"github.com/owncast/owncast/persistence/configrepository"
"github.com/pkg/errors"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/utils"
log "github.com/sirupsen/logrus"
"github.com/teris-io/shortid"
@@ -27,9 +27,7 @@ import (
// SendLive will send all followers the message saying you started a live stream.
func SendLive() error {
configRepository := configrepository.Get()
textContent := configRepository.GetFederationGoLiveMessage()
textContent := data.GetFederationGoLiveMessage()
// If the message is empty then do not send it.
if textContent == "" {
@@ -40,7 +38,7 @@ func SendLive() error {
reg := regexp.MustCompile("[^a-zA-Z0-9]+")
tagProp := streams.NewActivityStreamsTagProperty()
for _, tagString := range configRepository.GetServerMetadataTags() {
for _, tagString := range data.GetServerMetadataTags() {
tagWithoutSpecialCharacters := reg.ReplaceAllString(tagString, "")
hashtag := apmodels.MakeHashtag(tagWithoutSpecialCharacters)
tagProp.AppendTootHashtag(hashtag)
@@ -59,15 +57,15 @@ func SendLive() error {
tagsString := strings.Join(tagStrings, " ")
var streamTitle string
if title := configRepository.GetStreamTitle(); title != "" {
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, configRepository.GetServerURL(), configRepository.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)
// To the public if we're not treating ActivityPub as "private".
if !configRepository.GetFederationIsPrivate() {
if !data.GetFederationIsPrivate() {
note = apmodels.MakeNotePublic(note)
activity = apmodels.MakeActivityPublic(activity)
}
@@ -75,12 +73,12 @@ func SendLive() error {
note.SetActivityStreamsTag(tagProp)
// Attach an image along with the Federated message.
previewURL, err := url.Parse(configRepository.GetServerURL())
previewURL, err := url.Parse(data.GetServerURL())
if err == nil {
var imageToAttach string
var mediaType string
previewGif := filepath.Join(config.TempDir, "preview.gif")
thumbnailJpg := filepath.Join(config.TempDir, "thumbnail.jpg")
previewGif := filepath.Join(config.WebRoot, "preview.gif")
thumbnailJpg := filepath.Join(config.WebRoot, "thumbnail.jpg")
uniquenessString := shortid.MustGenerate()
if utils.DoesFileExists(previewGif) {
imageToAttach = "preview.gif"
@@ -96,7 +94,7 @@ func SendLive() error {
}
}
if configRepository.GetNSFW() {
if data.GetNSFW() {
// Mark content as sensitive.
sensitive := streams.NewActivityStreamsSensitiveProperty()
sensitive.AppendXMLSchemaBoolean(true)
@@ -153,8 +151,6 @@ func SendDirectMessageToAccount(textContent, account string) error {
// SendPublicMessage will send a public message to all followers.
func SendPublicMessage(textContent string) error {
configRepository := configrepository.Get()
originalContent := textContent
textContent = utils.RenderSimpleMarkdown(textContent)
@@ -177,7 +173,7 @@ func SendPublicMessage(textContent string) error {
activity, _, note, noteID := createBaseOutboundMessage(textContent)
note.SetActivityStreamsTag(tagProp)
if !configRepository.GetFederationIsPrivate() {
if !data.GetFederationIsPrivate() {
note = apmodels.MakeNotePublic(note)
activity = apmodels.MakeActivityPublic(activity)
}
@@ -201,8 +197,7 @@ func SendPublicMessage(textContent string) error {
// nolint: unparam
func createBaseOutboundMessage(textContent string) (vocab.ActivityStreamsCreate, string, vocab.ActivityStreamsNote, string) {
configRepository := configrepository.Get()
localActor := apmodels.MakeLocalIRIForAccount(configRepository.GetDefaultFederationUsername())
localActor := apmodels.MakeLocalIRIForAccount(data.GetDefaultFederationUsername())
noteID := shortid.MustGenerate()
noteIRI := apmodels.MakeLocalIRIForResource(noteID)
id := shortid.MustGenerate()
@@ -223,8 +218,7 @@ func getHashtagLinkHTMLFromTagString(baseHashtag string) string {
// SendToFollowers will send an arbitrary payload to all follower inboxes.
func SendToFollowers(payload []byte) error {
configRepository := configrepository.Get()
localActor := apmodels.MakeLocalIRIForAccount(configRepository.GetDefaultFederationUsername())
localActor := apmodels.MakeLocalIRIForAccount(data.GetDefaultFederationUsername())
followers, _, err := persistence.GetFederationFollowers(-1, 0)
if err != nil {
@@ -247,8 +241,7 @@ func SendToFollowers(payload []byte) error {
// SendToUser will send a payload to a single specific inbox.
func SendToUser(inbox *url.URL, payload []byte) error {
configRepository := configrepository.Get()
localActor := apmodels.MakeLocalIRIForAccount(configRepository.GetDefaultFederationUsername())
localActor := apmodels.MakeLocalIRIForAccount(data.GetDefaultFederationUsername())
req, err := requests.CreateSignedRequest(payload, inbox, localActor)
if err != nil {
@@ -262,10 +255,8 @@ func SendToUser(inbox *url.URL, payload []byte) error {
// UpdateFollowersWithAccountUpdates will send an update to all followers alerting of a profile update.
func UpdateFollowersWithAccountUpdates() error {
configRepository := configrepository.Get()
// Don't do anything if federation is disabled.
if !configRepository.GetFederationEnabled() {
if !data.GetFederationEnabled() {
return nil
}
@@ -274,7 +265,7 @@ func UpdateFollowersWithAccountUpdates() error {
activity := apmodels.MakeUpdateActivity(objectID)
actor := streams.NewActivityStreamsPerson()
actorID := apmodels.MakeLocalIRIForAccount(configRepository.GetDefaultFederationUsername())
actorID := apmodels.MakeLocalIRIForAccount(data.GetDefaultFederationUsername())
actorIDProperty := streams.NewJSONLDIdProperty()
actorIDProperty.Set(actorID)
actor.SetJSONLDId(actorIDProperty)

View File

@@ -45,8 +45,8 @@ func GetFederationFollowers(limit int, offset int) ([]models.Follower, int, erro
}
followersResult, err := _datastore.GetQueries().GetFederationFollowersWithOffset(ctx, db.GetFederationFollowersWithOffsetParams{
Limit: limit,
Offset: offset,
Limit: int32(limit),
Offset: int32(offset),
})
if err != nil {
return nil, 0, err

View File

@@ -1,36 +0,0 @@
//go:build fixture
// +build fixture
package persistence
import (
"encoding/json"
"fmt"
"os"
"github.com/owncast/owncast/models"
log "github.com/sirupsen/logrus"
)
func addFollowersFixtureData() {
log.Println("Adding followers fixture data...")
file, err := os.Open("./test/fixture/followers_fixture.json")
if err != nil {
fmt.Println("Error opening file:", err)
return
}
defer file.Close()
var followers []models.Follower
decoder := json.NewDecoder(file)
err = decoder.Decode(&followers)
if err != nil {
fmt.Println("Error decoding JSON:", err)
return
}
// Iterate over the followers array
for _, follower := range followers {
createFollow(follower.ActorIRI, follower.Inbox, "", follower.Name, follower.Username, follower.Image, nil, true)
}
}

View File

@@ -1,8 +0,0 @@
//go:build !fixture
// +build !fixture
package persistence
func addFollowersFixtureData() {
// no-op
}

View File

@@ -1,107 +0,0 @@
package persistence
import (
"os"
"testing"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/utils"
)
func TestMain(m *testing.M) {
setup()
code := m.Run()
os.Exit(code)
}
var followers = []models.Follower{}
func setup() {
data.SetupPersistence(":memory:")
_datastore = data.GetDatastore()
createFederationFollowersTable()
number := 100
for i := 0; i < number; i++ {
u := createFakeFollower()
createFollow(u.ActorIRI, u.Inbox, "https://fake.fediverse.server/some/request", u.Name, u.Username, u.Image, nil, true)
followers = append(followers, u)
}
}
func TestQueryFollowers(t *testing.T) {
f, total, err := GetFederationFollowers(10, 0)
if err != nil {
t.Errorf("Error querying followers: %s", err)
}
if len(f) != 10 {
t.Errorf("Expected 10 followers, got %d", len(f))
}
if total != 100 {
t.Errorf("Expected 100 followers, got %d", total)
}
}
func TestQueryFollowersWithOffset(t *testing.T) {
f, total, err := GetFederationFollowers(10, 10)
if err != nil {
t.Errorf("Error querying followers: %s", err)
}
if len(f) != 10 {
t.Errorf("Expected 10 followers, got %d", len(f))
}
if total != 100 {
t.Errorf("Expected 100 followers, got %d", total)
}
}
func TestQueryFollowersWithOffsetAndLimit(t *testing.T) {
f, total, err := GetFederationFollowers(10, 90)
if err != nil {
t.Errorf("Error querying followers: %s", err)
}
if len(f) != 10 {
t.Errorf("Expected 10 followers, got %d", len(f))
}
if total != 100 {
t.Errorf("Expected 100 followers, got %d", total)
}
}
func TestQueryFollowersWithPagination(t *testing.T) {
f, _, err := GetFederationFollowers(15, 10)
if err != nil {
t.Errorf("Error querying followers: %s", err)
}
comparisonFollowers := followers[10:25]
if len(f) != len(comparisonFollowers) {
t.Errorf("Expected %d followers, got %d", len(comparisonFollowers), len(f))
}
for i, follower := range f {
if follower.ActorIRI != comparisonFollowers[i].ActorIRI {
t.Errorf("Expected %s, got %s", comparisonFollowers[i].ActorIRI, follower.ActorIRI)
}
}
}
func createFakeFollower() models.Follower {
user, _ := utils.GenerateRandomString(10)
return models.Follower{
ActorIRI: "https://freedom.eagle/user/" + user,
Inbox: "https://fake.fediverse.server/user/" + user + "/inbox",
Image: "https://fake.fediverse.server/user/" + user + "/avatar.png",
Name: user,
Username: user,
Timestamp: utils.NullTime{},
}
}

View File

@@ -27,7 +27,6 @@ func Setup(datastore *data.Datastore) {
createFederationFollowersTable()
createFederationOutboxTable()
createFederatedActivitiesTable()
addFollowersFixtureData()
}
// AddFollow will save a follow to the datastore.
@@ -237,7 +236,7 @@ func GetOutbox(limit int, offset int) (vocab.ActivityStreamsOrderedCollection, e
orderedItems := streams.NewActivityStreamsOrderedItemsProperty()
rows, err := _datastore.GetQueries().GetOutboxWithOffset(
context.Background(),
db.GetOutboxWithOffsetParams{Limit: limit, Offset: offset},
db.GetOutboxWithOffsetParams{Limit: int32(limit), Offset: int32(offset)},
)
if err != nil {
return collection, err
@@ -278,6 +277,12 @@ func AddToOutbox(iri string, itemData []byte, typeString string, isLiveNotificat
return tx.Commit()
}
// GetObjectByID will return a string representation of a single object by the ID.
func GetObjectByID(id string) (string, error) {
value, err := _datastore.GetQueries().GetObjectFromOutboxByID(context.Background(), id)
return string(value), err
}
// GetObjectByIRI will return a string representation of a single object by the IRI.
func GetObjectByIRI(iri string) (string, bool, time.Time, error) {
row, err := _datastore.GetQueries().GetObjectFromOutboxByIRI(context.Background(), iri)
@@ -309,8 +314,8 @@ func SaveInboundFediverseActivity(objectIRI string, actorIRI string, eventType s
func GetInboundActivities(limit int, offset int) ([]models.FederatedActivity, int, error) {
ctx := context.Background()
rows, err := _datastore.GetQueries().GetInboundActivitiesWithOffset(ctx, db.GetInboundActivitiesWithOffsetParams{
Limit: limit,
Offset: offset,
Limit: int32(limit),
Offset: int32(offset),
})
if err != nil {
return nil, 0, err

View File

@@ -3,14 +3,14 @@ package resolvers
import (
"context"
"encoding/json"
"io"
"io/ioutil"
"net/http"
"github.com/go-fed/activity/streams"
"github.com/go-fed/activity/streams/vocab"
"github.com/owncast/owncast/activitypub/apmodels"
"github.com/owncast/owncast/activitypub/crypto"
"github.com/owncast/owncast/persistence/configrepository"
"github.com/owncast/owncast/core/data"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)
@@ -47,12 +47,11 @@ func Resolve(c context.Context, data []byte, callbacks ...interface{}) error {
// ResolveIRI will resolve an IRI ahd call the correct callback for the resolved type.
func ResolveIRI(c context.Context, iri string, callbacks ...interface{}) error {
configRepository := configrepository.Get()
log.Debugln("Resolving", iri)
req, _ := http.NewRequest(http.MethodGet, iri, nil)
actor := apmodels.MakeLocalIRIForAccount(configRepository.GetDefaultFederationUsername())
actor := apmodels.MakeLocalIRIForAccount(data.GetDefaultFederationUsername())
if err := crypto.SignRequest(req, nil, actor); err != nil {
return err
}
@@ -64,7 +63,7 @@ func ResolveIRI(c context.Context, iri string, callbacks ...interface{}) error {
defer response.Body.Close()
data, err := io.ReadAll(response.Body)
data, err := ioutil.ReadAll(response.Body)
if err != nil {
return err
}

35
activitypub/router.go Normal file
View File

@@ -0,0 +1,35 @@
package activitypub
import (
"net/http"
"github.com/owncast/owncast/activitypub/controllers"
"github.com/owncast/owncast/router/middleware"
)
// StartRouter will start the federation specific http router.
func StartRouter() {
// WebFinger
http.HandleFunc("/.well-known/webfinger", controllers.WebfingerHandler)
// Host Metadata
http.HandleFunc("/.well-known/host-meta", controllers.HostMetaController)
// Nodeinfo v1
http.HandleFunc("/.well-known/nodeinfo", controllers.NodeInfoController)
// x-nodeinfo v2
http.HandleFunc("/.well-known/x-nodeinfo2", controllers.XNodeInfo2Controller)
// Nodeinfo v2
http.HandleFunc("/nodeinfo/2.0", controllers.NodeInfoV2Controller)
// Instance details
http.HandleFunc("/api/v1/instance", controllers.InstanceV1Controller)
// Single ActivityPub Actor
http.HandleFunc("/federation/user/", middleware.RequireActivityPubOrRedirect(controllers.ActorHandler))
// Single AP object
http.HandleFunc("/federation/", middleware.RequireActivityPubOrRedirect(controllers.ObjectHandler))
}

View File

@@ -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) {
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 {
@@ -37,14 +29,7 @@ func GetWebfingerLinks(account string) ([]map[string]interface{}, error) {
query.Add("resource", fmt.Sprintf("acct:%s", account))
requestURL.RawQuery = query.Encode()
// Do not support redirects.
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
response, err := client.Get(requestURL.String())
response, err := http.DefaultClient.Get(requestURL.String())
if err != nil {
return nil, err
}

View File

@@ -6,6 +6,11 @@ import (
log "github.com/sirupsen/logrus"
)
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 {
request *http.Request
@@ -14,24 +19,19 @@ type Job struct {
var queue chan Job
// InitOutboundWorkerPool starts n go routines that await ActivityPub jobs.
func InitOutboundWorkerPool(workerPoolSize int) {
queue = make(chan Job, workerPoolSize)
func InitOutboundWorkerPool() {
queue = make(chan Job)
// start workers
for i := 1; i <= workerPoolSize; i++ {
for i := 1; i <= ActivityPubWorkerPoolSize; i++ {
go worker(i, queue)
}
}
// AddToOutboundQueue will queue up an outbound http request.
func AddToOutboundQueue(req *http.Request) {
select {
case queue <- Job{req}:
default:
log.Debugln("Outbound ActivityPub job queue is full")
queue <- Job{req} // will block until received by a worker at this point
}
log.Tracef("Queued request for ActivityPub destination %s", req.RequestURI)
queue <- Job{req}
}
func worker(workerID int, queue <-chan Job) {

View File

@@ -2,73 +2,35 @@ package fediverse
import (
"crypto/rand"
"errors"
"io"
"strings"
"sync"
"time"
log "github.com/sirupsen/logrus"
)
// OTPRegistration represents a single OTP request.
type OTPRegistration struct {
Timestamp time.Time
UserID string
UserDisplayName string
Code string
Account string
Timestamp time.Time
}
// Key by access token to limit one OTP request for a person
// to be active at a time.
var (
pendingAuthRequests = make(map[string]OTPRegistration)
lock = sync.Mutex{}
)
var pendingAuthRequests = make(map[string]OTPRegistration)
const (
registrationTimeout = time.Minute * 10
maxPendingRequests = 1000
)
func init() {
go setupExpiredRequestPruner()
}
// Clear out any pending requests that have been pending for greater than
// the specified timeout value.
func setupExpiredRequestPruner() {
pruneExpiredRequestsTimer := time.NewTicker(registrationTimeout)
for range pruneExpiredRequestsTimer.C {
lock.Lock()
log.Debugln("Pruning expired OTP requests.")
for k, v := range pendingAuthRequests {
if time.Since(v.Timestamp) > registrationTimeout {
delete(pendingAuthRequests, k)
}
}
lock.Unlock()
}
}
const registrationTimeout = time.Minute * 10
// RegisterFediverseOTP will start the OTP flow for a user, creating a new
// code and returning it to be sent to a destination.
func RegisterFediverseOTP(accessToken, userID, userDisplayName, account string) (OTPRegistration, bool, error) {
func RegisterFediverseOTP(accessToken, userID, userDisplayName, account string) (OTPRegistration, bool) {
request, requestExists := pendingAuthRequests[accessToken]
// If a request is already registered and has not expired then return that
// existing request.
if requestExists && time.Since(request.Timestamp) < registrationTimeout {
return request, false, nil
}
lock.Lock()
defer lock.Unlock()
if len(pendingAuthRequests)+1 > maxPendingRequests {
return request, false, errors.New("Please try again later. Too many pending requests.")
return request, false
}
code, _ := createCode()
@@ -81,7 +43,7 @@ func RegisterFediverseOTP(accessToken, userID, userDisplayName, account string)
}
pendingAuthRequests[accessToken] = r
return r, true, nil
return r, true
}
// ValidateFediverseOTP will verify a OTP code for a auth request.
@@ -92,9 +54,6 @@ func ValidateFediverseOTP(accessToken, code string) (bool, *OTPRegistration) {
return false, nil
}
lock.Lock()
defer lock.Unlock()
delete(pendingAuthRequests, accessToken)
return true, &request
}

View File

@@ -3,8 +3,6 @@ package fediverse
import (
"strings"
"testing"
"github.com/owncast/owncast/utils"
)
const (
@@ -15,10 +13,7 @@ const (
)
func TestOTPFlowValidation(t *testing.T) {
r, success, err := RegisterFediverseOTP(accessToken, userID, userDisplayName, account)
if err != nil {
t.Error(err)
}
r, success := RegisterFediverseOTP(accessToken, userID, userDisplayName, account)
if !success {
t.Error("Registration should be permitted.")
@@ -55,8 +50,8 @@ func TestOTPFlowValidation(t *testing.T) {
}
func TestSingleOTPFlowRequest(t *testing.T) {
r1, _, _ := RegisterFediverseOTP(accessToken, userID, userDisplayName, account)
r2, s2, _ := RegisterFediverseOTP(accessToken, userID, userDisplayName, account)
r1, _ := RegisterFediverseOTP(accessToken, userID, userDisplayName, account)
r2, s2 := RegisterFediverseOTP(accessToken, userID, userDisplayName, account)
if r1.Code != r2.Code {
t.Error("Only one registration should be permitted.")
@@ -70,42 +65,14 @@ func TestSingleOTPFlowRequest(t *testing.T) {
func TestAccountCaseInsensitive(t *testing.T) {
account := "Account"
accessToken := "another-fake-access-token"
r1, _, _ := RegisterFediverseOTP(accessToken, userID, userDisplayName, account)
r1, _ := RegisterFediverseOTP(accessToken, userID, userDisplayName, account)
_, reg1 := ValidateFediverseOTP(accessToken, r1.Code)
// Simulate second auth with account in different case
r2, _, _ := RegisterFediverseOTP(accessToken, userID, userDisplayName, strings.ToUpper(account))
r2, _ := RegisterFediverseOTP(accessToken, userID, userDisplayName, strings.ToUpper(account))
_, reg2 := ValidateFediverseOTP(accessToken, r2.Code)
if reg1.Account != reg2.Account {
t.Errorf("Account names should be case-insensitive: %s %s", reg1.Account, reg2.Account)
}
}
func TestLimitGlobalPendingRequests(t *testing.T) {
for i := 0; i < maxPendingRequests-1; i++ {
at, _ := utils.GenerateRandomString(10)
uid, _ := utils.GenerateRandomString(10)
account, _ := utils.GenerateRandomString(10)
_, success, error := RegisterFediverseOTP(at, uid, "userDisplayName", account)
if !success {
t.Error("Registration should be permitted.", i, " of ", len(pendingAuthRequests))
}
if error != nil {
t.Error(error)
}
}
// This one should fail
at, _ := utils.GenerateRandomString(10)
uid, _ := utils.GenerateRandomString(10)
account, _ := utils.GenerateRandomString(10)
_, success, error := RegisterFediverseOTP(at, uid, "userDisplayName", account)
if success {
t.Error("Registration should not be permitted.")
}
if error == nil {
t.Error("Error should be returned.")
}
}

View File

@@ -8,69 +8,17 @@ import (
"net/url"
"strconv"
"strings"
"sync"
"time"
"github.com/owncast/owncast/persistence/configrepository"
"github.com/owncast/owncast/utils"
"github.com/owncast/owncast/core/data"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)
var (
pendingAuthRequests = make(map[string]*Request)
lock = sync.Mutex{}
)
const registrationTimeout = time.Minute * 10
func init() {
go setupExpiredRequestPruner()
}
// Clear out any pending requests that have been pending for greater than
// the specified timeout value.
func setupExpiredRequestPruner() {
pruneExpiredRequestsTimer := time.NewTicker(registrationTimeout)
for range pruneExpiredRequestsTimer.C {
lock.Lock()
log.Debugln("Pruning expired IndieAuth requests.")
for k, v := range pendingAuthRequests {
if time.Since(v.Timestamp) > registrationTimeout {
delete(pendingAuthRequests, k)
}
}
lock.Unlock()
}
}
var pendingAuthRequests = make(map[string]*Request)
// StartAuthFlow will begin the IndieAuth flow by generating an auth request.
func StartAuthFlow(authHost, userID, accessToken, displayName string) (*url.URL, error) {
configRepository := configrepository.Get()
// 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 := configRepository.GetServerURL()
serverURL := data.GetServerURL()
if serverURL == "" {
return nil, errors.New("Owncast server URL must be set when using auth")
}
@@ -100,13 +48,7 @@ func HandleCallbackCode(code, state string) (*Request, *Response, error) {
data.Set("redirect_uri", request.Callback.String())
data.Set("code_verifier", request.CodeVerifier)
// Do not support redirects.
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
client := &http.Client{}
r, err := http.NewRequest("POST", request.Endpoint.String(), strings.NewReader(data.Encode())) // URL-encoded payload
if err != nil {
return nil, nil, err

View File

@@ -7,7 +7,6 @@ import (
"net/http"
"net/url"
"strings"
"time"
"github.com/andybalholm/cascadia"
"github.com/pkg/errors"
@@ -64,7 +63,6 @@ func createAuthRequest(authDestination, userID, displayName, accessToken, baseSe
State: state,
Redirect: &redirect,
Callback: &callbackURL,
Timestamp: time.Now(),
}, nil
}
@@ -74,10 +72,6 @@ func getAuthEndpointFromURL(urlstring string) (*url.URL, error) {
return nil, errors.Wrap(err, "unable to parse URL")
}
if htmlDocScrapeURL.Scheme != "https" {
return nil, fmt.Errorf("url must be https")
}
r, err := http.Get(htmlDocScrapeURL.String()) // nolint:gosec
if err != nil {
return nil, err

View File

@@ -1,35 +0,0 @@
package indieauth
import (
"testing"
"github.com/owncast/owncast/utils"
)
func TestLimitGlobalPendingRequests(t *testing.T) {
// Simulate 10 pending requests
for i := 0; i < maxPendingRequests-1; i++ {
cid, _ := utils.GenerateRandomString(10)
redirectURL, _ := utils.GenerateRandomString(10)
cc, _ := utils.GenerateRandomString(10)
state, _ := utils.GenerateRandomString(10)
me, _ := utils.GenerateRandomString(10)
_, err := StartServerAuth(cid, redirectURL, cc, state, me)
if err != nil {
t.Error("Registration should be permitted.", i, " of ", len(pendingAuthRequests), err)
}
}
// This should throw an error
cid, _ := utils.GenerateRandomString(10)
redirectURL, _ := utils.GenerateRandomString(10)
cc, _ := utils.GenerateRandomString(10)
state, _ := utils.GenerateRandomString(10)
me, _ := utils.GenerateRandomString(10)
_, err := StartServerAuth(cid, redirectURL, cc, state, me)
if err == nil {
t.Error("Registration should not be permitted.")
}
}

View File

@@ -1,22 +1,18 @@
package indieauth
import (
"net/url"
"time"
)
import "net/url"
// Request represents a single in-flight IndieAuth request.
type Request struct {
Timestamp time.Time
Endpoint *url.URL
Redirect *url.URL // Outbound redirect URL to continue auth flow
Callback *url.URL // Inbound URL to get auth flow results
Me *url.URL
UserID string
DisplayName string
CurrentAccessToken string
Endpoint *url.URL
Redirect *url.URL // Outbound redirect URL to continue auth flow
Callback *url.URL // Inbound URL to get auth flow results
ClientID string
CodeVerifier string
CodeChallenge string
State string
Me *url.URL
}

View File

@@ -2,9 +2,8 @@ package indieauth
import (
"fmt"
"time"
"github.com/owncast/owncast/persistence/configrepository"
"github.com/owncast/owncast/core/data"
"github.com/pkg/errors"
"github.com/teris-io/shortid"
)
@@ -12,7 +11,6 @@ import (
// ServerAuthRequest is n inbound request to authenticate against
// this Owncast instance.
type ServerAuthRequest struct {
Timestamp time.Time
ClientID string
RedirectURI string
CodeChallenge string
@@ -40,16 +38,10 @@ type ServerProfileResponse struct {
var pendingServerAuthRequests = map[string]ServerAuthRequest{}
const maxPendingRequests = 100
// StartServerAuth will handle the authentication for the admin user of this
// Owncast server. Initiated via a GET of the auth endpoint.
// https://indieweb.org/authorization-endpoint
func StartServerAuth(clientID, redirectURI, codeChallenge, state, me string) (*ServerAuthRequest, error) {
if len(pendingServerAuthRequests)+1 >= maxPendingRequests {
return nil, errors.New("Please try again later. Too many pending requests.")
}
code := shortid.MustGenerate()
r := ServerAuthRequest{
@@ -59,7 +51,6 @@ func StartServerAuth(clientID, redirectURI, codeChallenge, state, me string) (*S
State: state,
Me: me,
Code: code,
Timestamp: time.Now(),
}
pendingServerAuthRequests[code] = r
@@ -70,8 +61,6 @@ func StartServerAuth(clientID, redirectURI, codeChallenge, state, me string) (*S
// CompleteServerAuth will verify that the values provided in the final step
// of the IndieAuth flow are correct, and return some basic profile info.
func CompleteServerAuth(code, redirectURI, clientID string, codeVerifier string) (*ServerProfileResponse, error) {
configRepository := configrepository.Get()
request, pending := pendingServerAuthRequests[code]
if !pending {
return nil, errors.New("no pending authentication request")
@@ -91,11 +80,11 @@ func CompleteServerAuth(code, redirectURI, clientID string, codeVerifier string)
}
response := ServerProfileResponse{
Me: configRepository.GetServerURL(),
Me: data.GetServerURL(),
Profile: ServerProfile{
Name: configRepository.GetServerName(),
URL: configRepository.GetServerURL(),
Photo: fmt.Sprintf("%s/%s", configRepository.GetServerURL(), configRepository.GetLogoPath()),
Name: data.GetServerName(),
URL: data.GetServerURL(),
Photo: fmt.Sprintf("%s/%s", data.GetServerURL(), data.GetLogoPath()),
},
}

View File

@@ -1,7 +1,13 @@
package auth
import (
"context"
"strings"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/core/user"
"github.com/owncast/owncast/db"
)
var _datastore *data.Datastore
@@ -21,3 +27,41 @@ func Setup(db *data.Datastore) {
_datastore.MustExec(createTableSQL)
_datastore.MustExec(`CREATE INDEX IF NOT EXISTS idx_auth_token ON auth (token);`)
}
// AddAuth will add an external authentication token and type for a user.
func AddAuth(userID, authToken string, authType Type) error {
return _datastore.GetQueries().AddAuthForUser(context.Background(), db.AddAuthForUserParams{
UserID: userID,
Token: authToken,
Type: string(authType),
})
}
// GetUserByAuth will return an existing user given auth details if a user
// has previously authenticated with that method.
func GetUserByAuth(authToken string, authType Type) *user.User {
u, err := _datastore.GetQueries().GetUserByAuth(context.Background(), db.GetUserByAuthParams{
Token: authToken,
Type: string(authType),
})
if err != nil {
return nil
}
var scopes []string
if u.Scopes.Valid {
scopes = strings.Split(u.Scopes.String, ",")
}
return &user.User{
ID: u.ID,
DisplayName: u.DisplayName,
DisplayColor: int(u.DisplayColor),
CreatedAt: u.CreatedAt.Time,
DisabledAt: &u.DisabledAt.Time,
PreviousNames: strings.Split(u.PreviousNames.String, ","),
NameChangedAt: &u.NamechangedAt.Time,
AuthenticatedAt: &u.AuthenticatedAt.Time,
Scopes: scopes,
}
}

41
build/admin/bundleAdmin.sh Executable file
View File

@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# shellcheck disable=SC2059
set -o errexit
set -o nounset
set -o pipefail
INSTALL_TEMP_DIRECTORY="$(mktemp -d)"
PROJECT_SOURCE_DIR=$(pwd)
cd $INSTALL_TEMP_DIRECTORY
shutdown () {
rm -rf "$INSTALL_TEMP_DIRECTORY"
}
trap shutdown INT TERM ABRT EXIT
echo "Cloning owncast admin into $INSTALL_TEMP_DIRECTORY..."
git clone https://github.com/owncast/owncast-admin 2> /dev/null
cd owncast-admin
echo "Installing npm modules for the owncast admin..."
npm --silent install 2> /dev/null
echo "Building owncast admin..."
rm -rf .next
(node_modules/.bin/next build && node_modules/.bin/next export) | grep info
echo "Copying admin to project directory..."
ADMIN_BUILD_DIR=$(pwd)
cd $PROJECT_SOURCE_DIR
mkdir -p admin 2> /dev/null
cd admin
# Remove the old one
rm -rf $PROJECT_SOURCE_DIR/static/admin
# Copy over the new one
mv ${ADMIN_BUILD_DIR}/out $PROJECT_SOURCE_DIR/static/admin
shutdown
echo "Done."

View File

@@ -1,24 +0,0 @@
#!/bin/sh
set -e
# Development container builder
#
# Must authenticate first: https://docs.github.com/en/packages/using-github-packages-with-your-projects-ecosystem/configuring-docker-for-use-with-github-packages#authenticating-to-github-packages
# env vars:
# $EARTHLY_BUILD_BRANCH: git branch to checkout
# $EARTHLY_BUILD_TAG: tag for container image
EARTHLY_IMAGE_NAME="owncast"
BUILD_TAG=${EARTHLY_BUILD_TAG:-develop}
DATE=$(date +"%Y%m%d")
VERSION="${DATE}-${BUILD_TAG}"
echo "Building container image ${EARTHLY_IMAGE_NAME}:${BUILD_TAG} ..."
# Change to the root directory of the repository
cd "$(git rev-parse --show-toplevel)" || exit
if [ -n "${EARTHLY_BUILD_BRANCH}" ]; then
git checkout "${EARTHLY_BUILD_BRANCH}" || exit
fi
earthly --ci +docker-all --images="ghcr.io/owncast/${EARTHLY_IMAGE_NAME}:${BUILD_TAG}" --version="${VERSION}"

View File

@@ -1,38 +0,0 @@
#!/bin/bash
# go install github.com/deepmap/oapi-codegen/v2/cmd/oapi-codegen@latest
# setup
package="generated"
folderPath="webserver/handlers/generated"
specPath="openapi.yaml"
# validate scripts are installed
if ! command -v redocly &>/dev/null; then
echo "Please install \`redocly cli\` before running this script: npm install -g @redocly/cli"
exit 1
fi
if ! command -v oapi-codegen &>/dev/null; then
echo "Please install \`oapi-codegen\` before running this script"
echo "Hint: run \`go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest\` to install"
exit 1
fi
# validate schema
npx redocly lint $specPath
if [ $? -ne 0 ]; then
echo "Open API specification is not valid"
exit 1
fi
# cleanup
rm -r $folderPath
mkdir -p $folderPath
# codegen
oapi-codegen -generate types -o $folderPath/$package-types.gen.go -package $package $specPath
oapi-codegen -generate "chi-server" -o $folderPath/$package.gen.go -package $package $specPath
# go
go mod tidy

View File

@@ -0,0 +1,16 @@
## Third party web dependencies
Owncast's web frontend utilizes a few third party Javascript and CSS dependencies that we ship with the application.
To add, remove, or update one of these components:
1. Perform your `npm install/uninstall/etc`, or edit the `package.json` file to reflect the change you want to make.
2. Edit the `snowpack` `install` block of `package.json` to specify what files you want to add to the Owncast project. This can be an entire library (such as `preact`) or a single file (such as `video.js/dist/video.min.js`). These paths point to files that live in `node_modules`.
3. Run `npm run build`. This will download the requested module from NPM, package up the assets you specified, and then copy them to the Owncast web app in the `webroot/js/web_modules` directory.
4. Your new web dependency is now available for use in your web code.
## VideoJS versions
Currently Videojs version 7.8.3 and http-streaming version 2.2.0 are hardcoded because these are versions that have been found to work properly with our HLS stream. Other versions have had issues with things like discontinuities causing a loading spinner.
So if you update videojs or vhs make sure you do an end-to-end test of a stream and make sure the "this stream is offline" ending video displays properly.

2210
build/javascript/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,41 @@
{
"name": "owncast-dependencies",
"version": "1.0.0",
"description": "Javascript dependencies for Owncast web app",
"main": "index.js",
"dependencies": {
"@joeattardi/emoji-button": "^4.6.2",
"@videojs/themes": "^1.0.1",
"htm": "^3.1.0",
"mark.js": "^8.11.1",
"micromodal": "^0.4.10",
"preact": "10.6.6",
"tailwindcss": "^1.9.6",
"video.js": "7.17.0"
},
"devDependencies": {
"cssnano": "5.1.0",
"postcss": "8.4.7",
"postcss-cli": "9.1.0"
},
"snowpack": {
"install": [
"@videojs/themes/fantasy/*",
"video.js/dist/video-js.min.css",
"video.js/dist/video.min.js",
"@joeattardi/emoji-button",
"htm",
"preact",
"preact/hooks",
"mark.js/dist/mark.es6.min.js",
"tailwindcss/dist/tailwind.min.css",
"micromodal/dist/micromodal.min.js"
]
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "npm install && npx snowpack@2.18.4 install && cp node_modules/video.js/dist/video-js.min.css web_modules/videojs && rm -rf ../../webroot/js/web_modules && cp -R web_modules ../../webroot/js"
},
"author": "Owncast",
"license": "ISC"
}

View File

@@ -0,0 +1,7 @@
module.exports = {
plugins: [
require('cssnano')({
preset: 'default',
}),
],
};

View File

@@ -0,0 +1,7 @@
module.exports = {
purge: {
enabled: true,
mode: 'layers',
content: ['../../webroot/js/**'],
},
};

118
build/release/build.sh Executable file
View File

@@ -0,0 +1,118 @@
#!/bin/sh
# Human readable names of binary distributions
DISTRO=(macOS-64bit linux-64bit linux-32bit linux-arm7 linux-arm64)
# Operating systems for the respective distributions
OS=(darwin linux linux linux linux)
# Architectures for the respective distributions
ARCH=(amd64 amd64 386 arm-7 arm64)
# Version
VERSION=$1
SHOULD_RELEASE=$2
# Build info
GIT_COMMIT=$(git rev-list -1 HEAD)
GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [[ -z "${VERSION}" ]]; then
echo "Version must be specified when running build"
exit
fi
BUILD_TEMP_DIRECTORY="$(mktemp -d)"
cd $BUILD_TEMP_DIRECTORY
echo "Cloning owncast into $BUILD_TEMP_DIRECTORY..."
git clone https://github.com/owncast/owncast 2> /dev/null
cd owncast
echo "Changing to branch: $GIT_BRANCH"
git checkout $GIT_BRANCH
[[ -z "${VERSION}" ]] && VERSION='unknownver' || VERSION="${VERSION}"
# Change to the root directory of the repository
cd $(git rev-parse --show-toplevel)
echo "Cleaning working directories..."
rm -rf ./webroot/hls/* ./hls/* ./webroot/thumbnail.jpg
echo "Creating version ${VERSION} from commit ${GIT_COMMIT}"
# Create production build of Tailwind CSS
pushd build/javascript >> /dev/null
# Install the tailwind & postcss CLIs
npm install --quiet --no-progress
# Run the tailwind CLI and pipe it to postcss for minification.
# Save it to a temp directory that we will reference below.
NODE_ENV="production" ./node_modules/.bin/tailwind build | ./node_modules/.bin/postcss > "${TMPDIR}tailwind.min.css"
popd
mkdir -p dist
build() {
NAME=$1
OS=$2
ARCH=$3
VERSION=$4
GIT_COMMIT=$5
echo "Building ${NAME} (${OS}/${ARCH}) release from ${GIT_BRANCH} ${GIT_COMMIT}..."
mkdir -p dist/${NAME}
mkdir -p dist/${NAME}/data
cp -R webroot/ dist/${NAME}/webroot/
# Copy the production pruned+minified css to the build's directory.
cp "${TMPDIR}tailwind.min.css" ./dist/${NAME}/webroot/js/web_modules/tailwindcss/dist/tailwind.min.css
cp README.md dist/${NAME}
pushd dist/${NAME} >> /dev/null
CGO_ENABLED=1 ~/go/bin/xgo -go latest --branch ${GIT_BRANCH} -ldflags "-s -w -X github.com/owncast/owncast/config.GitCommit=${GIT_COMMIT} -X github.com/owncast/owncast/config.BuildVersion=${VERSION} -X github.com/owncast/owncast/config.BuildPlatform=${NAME}" -tags enable_updates -targets "${OS}/${ARCH}" github.com/owncast/owncast
mv owncast-*-${ARCH} owncast
zip -r -q -8 ../owncast-$VERSION-$NAME.zip .
popd >> /dev/null
rm -rf dist/${NAME}/
}
for i in "${!DISTRO[@]}"; do
build ${DISTRO[$i]} ${OS[$i]} ${ARCH[$i]} $VERSION $GIT_COMMIT
done
echo "Build archives are available in $BUILD_TEMP_DIRECTORY/owncast/dist"
ls -alh "$BUILD_TEMP_DIRECTORY/owncast/dist"
# Use the second argument "release" to create an actual release.
if [ "$SHOULD_RELEASE" != "release" ]; then
echo "Not uploading a release."
exit
fi
# Create the tag
git tag -a "v${VERSION}" -m "Release build v${VERSION}"
# On macOS open the Github page for new releases so they can be uploaded
if test -f "/usr/bin/open"; then
open "https://github.com/owncast/owncast/releases/new"
open dist
fi
# Docker build
# Must authenticate first: https://docs.github.com/en/packages/using-github-packages-with-your-projects-ecosystem/configuring-docker-for-use-with-github-packages#authenticating-to-github-packages
DOCKER_IMAGE="owncast-${VERSION}"
echo "Building Docker image ${DOCKER_IMAGE}..."
# Change to the root directory of the repository
cd $(git rev-parse --show-toplevel)
# Docker build
docker build --build-arg NAME=docker --build-arg VERSION=${VERSION} --build-arg GIT_COMMIT=$GIT_COMMIT -t gabekangas/owncast:$VERSION -t gabekangas/owncast:latest -t owncast .
# Dockerhub
# You must be authenticated via `docker login` with your Dockerhub credentials first.
docker push "gabekangas/owncast:${VERSION}"

14
build/release/docker-nightly.sh Executable file
View File

@@ -0,0 +1,14 @@
#!/bin/sh
# Docker build
# Must authenticate first: https://docs.github.com/en/packages/using-github-packages-with-your-projects-ecosystem/configuring-docker-for-use-with-github-packages#authenticating-to-github-packages
DOCKER_IMAGE="owncast"
DATE=$(date +"%Y%m%d")
VERSION="${DATE}-nightly"
echo "Building Docker image ${DOCKER_IMAGE}..."
# Change to the root directory of the repository
cd $(git rev-parse --show-toplevel)
earthly --ci --push +docker-all --image="ghcr.io/owncast/${DOCKER_IMAGE}" --tag=nightly --version="${VERSION}"

View File

@@ -1,40 +0,0 @@
#!/usr/bin/env bash
# shellcheck disable=SC2059
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 "Building owncast web..."
rm -rf .next
node_modules/.bin/next build | grep info
echo "Copying web project to dist directory..."
# Remove the old one
rm -rf ../static/web
# Copy over the new one
mv ./out ../static/web
echo "Done."

View File

@@ -40,9 +40,6 @@ var BuildPlatform = "dev"
// EnableAutoUpdate will explicitly enable in-place auto-updates via the admin.
var EnableAutoUpdate = false
// A temporary stream key that can be set via the command line.
var TemporaryStreamKey = ""
// GetCommit will return an identifier used for identifying the point in time this build took place.
func GetCommit() string {
if GitCommit == "" {

View File

@@ -4,16 +4,15 @@ import "path/filepath"
const (
// StaticVersionNumber is the version of Owncast that is used when it's not overwritten via build-time settings.
StaticVersionNumber = "0.2.2" // Shown when you build from develop
StaticVersionNumber = "0.0.13" // Shown when you build from develop
// WebRoot is the web server root directory.
WebRoot = "webroot"
// FfmpegSuggestedVersion is the version of ffmpeg we suggest.
FfmpegSuggestedVersion = "v4.1.5" // Requires the v
// DataDirectory is the directory we save data to.
DataDirectory = "data"
// EmojiDir defines the URL route prefix for emoji requests.
EmojiDir = "/img/emoji/"
// MaxUserColor is the largest color value available to assign to users.
// They start at 0 and can be treated as IDs more than colors themselves.
MaxUserColor = 7
// EmojiDir is relative to the webroot.
EmojiDir = "/img/emoji"
// MaxChatDisplayNameLength is the maximum length of a chat display name.
MaxChatDisplayNameLength = 30
)
@@ -24,10 +23,4 @@ var (
// HLSStoragePath is the directory HLS video is written to.
HLSStoragePath = filepath.Join(DataDirectory, "hls")
// CustomEmojiPath is the emoji directory.
CustomEmojiPath = filepath.Join(DataDirectory, "emoji")
// PublicFilesPath is the optional directory for hosting public files.
PublicFilesPath = filepath.Join(DataDirectory, "public")
)

View File

@@ -8,70 +8,47 @@ import (
// Defaults will hold default configuration values.
type Defaults struct {
PageBodyContent string
FederationGoLiveMessage string
Name string
Title string
Summary string
ServerWelcomeMessage string
Logo string
YPServer string
Title string
Tags []string
PageBodyContent string
DatabaseFilePath string
FederationUsername string
WebServerIP string
Name string
AdminPassword string
StreamKeys []models.StreamKey
StreamVariants []models.StreamOutputVariant
Tags []string
RTMPServerPort int
SegmentsInPlaylist int
SegmentLengthSeconds int
WebServerPort int
ChatEstablishedUserModeTimeDuration time.Duration
WebServerPort int
WebServerIP string
RTMPServerPort int
StreamKey string
YPEnabled bool
YPServer string
SegmentLengthSeconds int
SegmentsInPlaylist int
StreamVariants []models.StreamOutputVariant
FederationUsername string
FederationGoLiveMessage string
ChatEstablishedUserModeTimeDuration time.Duration
}
// GetDefaults will return default configuration values.
func GetDefaults() Defaults {
return Defaults{
Name: "New Owncast Server",
Summary: "This is a new live video streaming server powered by Owncast.",
Name: "Owncast",
Title: "My Owncast Server",
Summary: "This is brief summary of whom you are or what your stream is. You can edit this description in the admin.",
ServerWelcomeMessage: "",
Logo: "logo.svg",
AdminPassword: "abc123",
StreamKeys: []models.StreamKey{
{Key: "abc123", Comment: "Default stream key"},
},
Tags: []string{
"owncast",
"streaming",
},
PageBodyContent: `
# Welcome to Owncast!
- This is a live stream powered by [Owncast](https://owncast.online), a free and open source live streaming server.
- To discover more examples of streams, visit [Owncast's directory](https://directory.owncast.online).
- If you're the owner of this server you should visit the admin and customize the content on this page.
<hr/>
<video id="video" controls preload="metadata" style="width: 60vw; max-width: 600px; min-width: 200px;" poster="https://videos.owncast.online/t/xaJ3xNn9Y6pWTdB25m9ai3">
<source src="https://assets.owncast.tv/video/owncast-embed.mp4" type="video/mp4" />
</video>
`,
PageBodyContent: "# This is your page content that can be edited from the admin.",
DatabaseFilePath: "data/owncast.db",
@@ -81,6 +58,7 @@ func GetDefaults() Defaults {
WebServerPort: 8080,
WebServerIP: "0.0.0.0",
RTMPServerPort: 1935,
StreamKey: "abc123",
ChatEstablishedUserModeTimeDuration: time.Minute * 15,

View File

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

View File

@@ -1,7 +0,0 @@
# Contrib
This directory contains unmaintained, unsupported, and unorganized contributions by the Owncast community.
It is a place to put scripts, config files and examples that might be useful to share with others without expectation that they're an official part of the project.
[Read Drew DeVault's description of the contrib directory](https://drewdevault.com/2020/06/06/Add-a-contrib-directory.html) for details and background.

View File

@@ -1,19 +0,0 @@
[Unit]
Description=Owncast Service
[Service]
Type=simple
WorkingDirectory=[path to owncast directory]
ReadWritePaths=[path to owncast directory]
ExecStart=[path to owncast directory]/owncast
Restart=always
RestartSec=5
User=[user to run owncast as]
Group=[group to run owncast as]
NoNewPrivileges=true
SecureBits=noroot
ProtectSystem=strict
ProtectHome=read-only
[Install]
WantedBy=multi-user.target

View File

@@ -1,82 +0,0 @@
# Owncast on Windows
> Note: Owncast currently **does not natively support the Windows Operating System**, however it is possible to run Owncast on Windows using the Windows Subsystem for Linux (WSL2).
This document is a user-contributed document and the Owncast project does not actively maintain Windows support. Hopefully this can be helpful in pointing people in the right direction.
This document list out the steps in detail to install and run Owncast in Windows using Windows Subsystem for Linux, specifically **WSL2**.
Below are steps both for local development, contributing to the project and running it in production.
---
## Required: Installing WSL2 in Windows
There are lots of tutorials available online (videos and docs both) on how to install WSL2.
Here are the official documents from Microsoft -> [Install Linux on Windows with WSL](https://learn.microsoft.com/en-us/windows/wsl/setup/environment)
Some points to remember ->
- Preferable method to install WSL2 is by using the `wsl --install `. If you are facing issues with this method you can look at - [Manual installation steps for older versions of WSL](https://learn.microsoft.com/en-us/windows/wsl/install-manual)
- Make sure you have enabled the Virtual Machine feature. (ignore if used wsl --install method)
- Make sure you have WSL2
- Installed your Linux distribution of choice and make sure you installed the latest available version (Preferably Ubuntu)
### Setting up WSL2 and the distribution of your choice
After basic setup, you can look into setting WSL2 for development. Here is the link for a detailed document by Microsoft - [https://learn.microsoft.com/en-us/windows/wsl/setup/environment](https://learn.microsoft.com/en-us/windows/wsl/setup/environment)
---
## Installing Owncast under WSL2
Once you're running WSL2 in Windows you can install Owncast the same way you would on any Linux distribution by following the [Quickstart](https://owncast.online/quickstart/) guide.
## Contributing to Owncast by performing local development
If you want to use your Windows machine to contribute to Owncast, you'll need to do so under WSL2 and make sure the following prerequisites are installed.
### Make sure all the prerequisites are installed in WSL2
Here is the list for all the prerequisites required ->
- C compiler, such as [GCC compiler](https://gcc.gnu.org/install/download.html) or a [Musl-compatible compiler](https://musl.libc.org/)
- 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).
### Read more
Once your local development environment is setup, you can read more about how to contribute to Owncast [by reading the development document](https://owncast.online/development/).
## Some possible issues you can face while setting up WSL2
### You have an older version of Nodejs installed in the WSL2
To solve this issue you can look at nvm. Here is one tutorial - [Node-Version-Manager](https://www.digitalocean.com/community/tutorials/how-to-install-node-js-on-ubuntu-20-04#option-3-installing-node-using-the-node-version-manager).
### The broadcasting Software failed to connect to the server
This issue arises when you try to use `rtmp://localhost:1935/live` for example in OBS.
To solve this issue you need to find the correct IP address for the WSL2 you are running and use that instead of localhost.
You can use the below commands to find that ->
Note: you can use either of these, whichever works for you.
- In WSL2 Terminal -
`ip addr show eth0 | grep -oP '(?<=inet\s)\d+(\.\d+){3}'`
- In Windows Terminal -
`wsl -- ip -o -4 -json addr list eth0`
In this result look for "local": X.X.X.X
After finding the IP address in your broadcasting software make the server point to
`rtmp://<your version of IP address>:1935/live`
Example in OBS-Studio ->
![image](https://user-images.githubusercontent.com/73140257/228762798-a0c56695-c692-4295-b11b-f2e85e867ce7.png)
## More resources
- [Windows Subsystem for Linux](https://learn.microsoft.com/en-us/windows/wsl/)
- [Owncast development documentation](https://owncast.online/development/)
- [Owncast quickstart guide](https://owncast.online/quickstart/)
- [Owncast README](https://github.com/owncast/owncast/blob/develop/README.md#building-from-source)

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