Compare commits
18 Commits
untagged-0
...
v0.0.13
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d6151c867 | ||
|
|
3a7f69531c | ||
|
|
a74ea4ef40 | ||
|
|
c2a7d9f50b | ||
|
|
e64dd2d206 | ||
|
|
f39c1184e5 | ||
|
|
a9cd28dbec | ||
|
|
4b5a1fcc3f | ||
|
|
96274ad541 | ||
|
|
717bbcf2e7 | ||
|
|
29972bb4e7 | ||
|
|
625b0b0099 | ||
|
|
27afb93e13 | ||
|
|
eb1121e9fa | ||
|
|
e65abb6073 | ||
|
|
29ec8a9091 | ||
|
|
ba7861cda9 | ||
|
|
1cbe2a56fb |
@@ -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)
|
||||
@@ -1 +0,0 @@
|
||||
test/automated/api/node_modules
|
||||
@@ -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
116
.gitattributes
vendored
@@ -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
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/bug-report-feature-request.md
vendored
Normal file
4
.github/ISSUE_TEMPLATE/bug-report-feature-request.md
vendored
Normal 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.
|
||||
---
|
||||
@@ -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
17
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal 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.
|
||||
@@ -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! 🎉
|
||||
1
.github/codeql/go.yml
vendored
1
.github/codeql/go.yml
vendored
@@ -1 +0,0 @@
|
||||
name: Go config
|
||||
4
.github/codeql/javascript.yml
vendored
4
.github/codeql/javascript.yml
vendored
@@ -1,4 +0,0 @@
|
||||
name: Javascript config
|
||||
|
||||
paths-ignore:
|
||||
- static/web
|
||||
31
.github/stale.yml
vendored
Normal file
31
.github/stale.yml
vendored
Normal 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
|
||||
28
.github/workflows/actions-lint.yml
vendored
28
.github/workflows/actions-lint.yml
vendored
@@ -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
|
||||
42
.github/workflows/auto-comment-on-label.yaml
vendored
42
.github/workflows/auto-comment-on-label.yaml
vendored
@@ -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
19
.github/workflows/automated-browser.yml
vendored
Normal 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
|
||||
61
.github/workflows/automated-end-to-end-api.yaml
vendored
61
.github/workflows/automated-end-to-end-api.yaml
vendored
@@ -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
|
||||
|
||||
|
||||
61
.github/workflows/browser-testing.yml
vendored
61
.github/workflows/browser-testing.yml
vendored
@@ -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
|
||||
61
.github/workflows/build-storybook.yml
vendored
61
.github/workflows/build-storybook.yml
vendored
@@ -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
21
.github/workflows/bundle-admin.yml
vendored
Normal 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 }}
|
||||
65
.github/workflows/chromatic.yml
vendored
65
.github/workflows/chromatic.yml
vendored
@@ -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
|
||||
73
.github/workflows/codeql-analysis.yml
vendored
73
.github/workflows/codeql-analysis.yml
vendored
@@ -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
|
||||
|
||||
36
.github/workflows/container-lint.yml
vendored
36
.github/workflows/container-lint.yml
vendored
@@ -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
|
||||
61
.github/workflows/container.yaml
vendored
61
.github/workflows/container.yaml
vendored
@@ -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
|
||||
53
.github/workflows/css-lint.yaml
vendored
53
.github/workflows/css-lint.yaml
vendored
@@ -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
40
.github/workflows/docker-nightly.yaml
vendored
Normal 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
|
||||
@@ -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 }}
|
||||
|
||||
38
.github/workflows/go-lint.yml
vendored
38
.github/workflows/go-lint.yml
vendored
@@ -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
|
||||
89
.github/workflows/go-tests.yaml
vendored
89
.github/workflows/go-tests.yaml
vendored
@@ -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 ./...
|
||||
72
.github/workflows/hls-tests.yml
vendored
72
.github/workflows/hls-tests.yml
vendored
@@ -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
|
||||
|
||||
222
.github/workflows/javascript-format-test-build.yml
vendored
222
.github/workflows/javascript-format-test-build.yml
vendored
@@ -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
|
||||
29
.github/workflows/javascript-formatting.yml
vendored
Normal file
29
.github/workflows/javascript-formatting.yml
vendored
Normal 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 }}
|
||||
33
.github/workflows/javascript-packages.yaml
vendored
Normal file
33
.github/workflows/javascript-packages.yaml
vendored
Normal 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
26
.github/workflows/lint.yml
vendored
Normal 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
|
||||
|
||||
38
.github/workflows/shellcheck.yml
vendored
38
.github/workflows/shellcheck.yml
vendored
@@ -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
|
||||
49
.github/workflows/stale.yml
vendored
49
.github/workflows/stale.yml
vendored
@@ -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
42
.github/workflows/test.yaml
vendored
Normal 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 ./...
|
||||
|
||||
62
.github/workflows/translations.yml
vendored
62
.github/workflows/translations.yml
vendored
@@ -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
8
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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 .
|
||||
@@ -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.*$
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# Ignore artifacts:
|
||||
build/javascript
|
||||
webroot/js/web_modules
|
||||
static/
|
||||
28
.vscode/settings.json
vendored
Normal file
28
.vscode/settings.json
vendored
Normal 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"
|
||||
]
|
||||
}
|
||||
22
Dockerfile
22
Dockerfile
@@ -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
104
Earthfile
@@ -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 ./...
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -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
147
README.md
@@ -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 you’re currently using one of those it’s 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 you’re currently using one of those it’s 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.
|
||||
We’ve 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 [](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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
//go:build !fixture
|
||||
// +build !fixture
|
||||
|
||||
package persistence
|
||||
|
||||
func addFollowersFixtureData() {
|
||||
// no-op
|
||||
}
|
||||
@@ -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{},
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
35
activitypub/router.go
Normal 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))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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
41
build/admin/bundleAdmin.sh
Executable 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."
|
||||
@@ -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}"
|
||||
@@ -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
|
||||
16
build/javascript/README.md
Normal file
16
build/javascript/README.md
Normal 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
2210
build/javascript/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
41
build/javascript/package.json
Normal file
41
build/javascript/package.json
Normal 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"
|
||||
}
|
||||
7
build/javascript/postcss.config.js
Normal file
7
build/javascript/postcss.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require('cssnano')({
|
||||
preset: 'default',
|
||||
}),
|
||||
],
|
||||
};
|
||||
7
build/javascript/tailwind.config.js
Normal file
7
build/javascript/tailwind.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
purge: {
|
||||
enabled: true,
|
||||
mode: 'layers',
|
||||
content: ['../../webroot/js/**'],
|
||||
},
|
||||
};
|
||||
118
build/release/build.sh
Executable file
118
build/release/build.sh
Executable 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
14
build/release/docker-nightly.sh
Executable 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}"
|
||||
@@ -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."
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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 ->
|
||||

|
||||
|
||||
## 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
Reference in New Issue
Block a user