diff --git a/.github/workflows/auto-comment-on-label.yaml b/.github/workflows/auto-comment-on-label.yaml index b0b3cffec..fc793c9a1 100644 --- a/.github/workflows/auto-comment-on-label.yaml +++ b/.github/workflows/auto-comment-on-label.yaml @@ -11,7 +11,7 @@ jobs: issues: write steps: - name: Add comment - uses: peter-evans/create-or-update-comment@5e08535dbf140c83ed0490e2f480ce1b8f2334e0 + uses: peter-evans/create-or-update-comment@da12db3f2752cdc278e9c3bdb4c427e7d7cd1c2f with: issue-number: ${{ github.event.issue.number }} body: | diff --git a/.github/workflows/automated-end-to-end-api.yaml b/.github/workflows/automated-end-to-end-api.yaml index cce4c58b8..d42c9a2cc 100644 --- a/.github/workflows/automated-end-to-end-api.yaml +++ b/.github/workflows/automated-end-to-end-api.yaml @@ -35,7 +35,7 @@ jobs: - uses: actions/checkout@v4 - name: Run API tests - uses: nick-fields/retry@v2 + uses: nick-fields/retry@v3 with: timeout_minutes: 10 max_attempts: 3 diff --git a/.github/workflows/browser-testing.yml b/.github/workflows/browser-testing.yml index b38290970..fb9ed2f1f 100644 --- a/.github/workflows/browser-testing.yml +++ b/.github/workflows/browser-testing.yml @@ -40,14 +40,14 @@ jobs: - uses: actions/setup-go@v5 with: - go-version: '1.21' + 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@v2 + uses: nick-fields/retry@v3 with: timeout_minutes: 20 max_attempts: 3 diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml index ca2336b13..7b9ea24d9 100644 --- a/.github/workflows/chromatic.yml +++ b/.github/workflows/chromatic.yml @@ -43,7 +43,7 @@ jobs: - name: Publish to Chromatic if: ${{ github.actor != 'renovate[bot]' && github.actor != 'renovate' }} - uses: chromaui/action@v1 + uses: chromaui/action@v11 # Chromatic GitHub Action options with: workingDir: web diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 52ddf668a..8392c1938 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -9,16 +9,16 @@ # 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/**' pull_request: # The branches below must be a subset of the branches above - branches: [ develop ] + branches: [develop] paths-ignore: - 'static/**' @@ -30,41 +30,46 @@ 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: Checkout repository - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v4 - # 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 + - 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@v3 + # 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 - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/container.yaml b/.github/workflows/container.yaml index d8275833b..7993e1db3 100644 --- a/.github/workflows/container.yaml +++ b/.github/workflows/container.yaml @@ -49,7 +49,7 @@ jobs: EARTHLY_BUILD_TAG: 'nightly' EARTHLY_BUILD_BRANCH: 'develop' EARTHLY_PUSH: true - uses: nick-fields/retry@v2 + uses: nick-fields/retry@v3 with: timeout_minutes: 20 max_attempts: 3 diff --git a/.github/workflows/go-lint.yml b/.github/workflows/go-lint.yml index 169025fd2..e3b8fdd1a 100644 --- a/.github/workflows/go-lint.yml +++ b/.github/workflows/go-lint.yml @@ -28,11 +28,11 @@ jobs: - uses: actions/setup-go@v5 with: - go-version: '1.21' + go-version: '1.22' cache: true - uses: actions/checkout@v4 - name: golangci-lint - uses: golangci/golangci-lint-action@v4 + uses: golangci/golangci-lint-action@v5 with: only-new-issues: true args: --timeout=3m diff --git a/.github/workflows/go-tests.yaml b/.github/workflows/go-tests.yaml index d77b526cb..9c8293247 100644 --- a/.github/workflows/go-tests.yaml +++ b/.github/workflows/go-tests.yaml @@ -12,7 +12,7 @@ jobs: test: strategy: matrix: - go-version: [1.20.x, 1.21.x] + go-version: [1.21.x, 1.22.x] os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: diff --git a/.github/workflows/hls-tests.yml b/.github/workflows/hls-tests.yml index 1dd2856aa..47429da9d 100644 --- a/.github/workflows/hls-tests.yml +++ b/.github/workflows/hls-tests.yml @@ -27,7 +27,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: '1.21' + go-version: '1.22' cache: true - name: Cache node modules @@ -43,14 +43,14 @@ jobs: ${{ runner.os }}- - name: Local stroage - uses: nick-fields/retry@v2 + uses: nick-fields/retry@v3 with: timeout_minutes: 10 max_attempts: 3 command: cd test/automated/hls && ./run.sh - name: S3 storage - uses: nick-fields/retry@v2 + uses: nick-fields/retry@v3 with: timeout_minutes: 10 max_attempts: 3 diff --git a/.github/workflows/javascript-format-build.yml b/.github/workflows/javascript-format-build.yml index 34f47bdf4..5ef0dd8ae 100644 --- a/.github/workflows/javascript-format-build.yml +++ b/.github/workflows/javascript-format-build.yml @@ -39,7 +39,7 @@ jobs: - name: Get changed files id: changed-files-yaml - uses: tj-actions/changed-files@v41 + uses: tj-actions/changed-files@v44 with: path: 'web' files_ignore: | @@ -72,6 +72,9 @@ jobs: if: steps.changed-files-yaml.outputs.src_any_changed == 'true' run: npx prettier --write ${{ 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' uses: EndBug/add-and-commit@v9 @@ -80,6 +83,7 @@ jobs: 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: diff --git a/.github/workflows/screenshots.yml b/.github/workflows/screenshots.yml index 56a8af096..c4c87c174 100644 --- a/.github/workflows/screenshots.yml +++ b/.github/workflows/screenshots.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: '1.21' + go-version: '1.22' cache: true - name: Cache node modules @@ -33,7 +33,7 @@ jobs: ${{ runner.os }}- - name: Automate screenshots - uses: nick-fields/retry@v2 + uses: nick-fields/retry@v3 with: timeout_minutes: 10 max_attempts: 4 diff --git a/.golangci.yml b/.golangci.yml index faf434e4d..a12706bf9 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -5,7 +5,7 @@ run: # Define the Go version limit. # Mainly related to generics support in go1.18. # Default: use Go version from the go.mod file, fallback on the env var `GOVERSION`, fallback on 1.18 - go: '1.21' + go: '1.22' issues: # The linter has a default list of ignorable errors. Turning this on will enable that list. @@ -69,7 +69,7 @@ linters-settings: gosimple: # Select the Go version to target. The default is '1.13'. - go: '1.21' + go: '1.22' # https://staticcheck.io/docs/options#checks checks: ['all'] diff --git a/README.md b/README.md index fdf08c871..991f6478b 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ The Owncast backend is a service written in Go. 1. Ensure you have prerequisites installed. - C compiler, such as [GCC compiler](https://gcc.gnu.org/install/download.html) or a [Musl-compatible compiler](https://musl.libc.org/) - [ffmpeg](https://ffmpeg.org/download.html) -1. Install the [Go toolchain](https://golang.org/dl/) (1.21 or above). +1. Install the [Go toolchain](https://golang.org/dl/) (1.22 or above). 1. Clone the repo. `git clone https://github.com/owncast/owncast` 1. `go run main.go` will run from the source. 1. Visit `http://yourserver:8080` to access the web interface or `http://yourserver:8080/admin` to access the admin. diff --git a/activitypub/activitypub.go b/activitypub/activitypub.go index e8be9f961..a6aa99be5 100644 --- a/activitypub/activitypub.go +++ b/activitypub/activitypub.go @@ -17,7 +17,6 @@ func Start(datastore *data.Datastore) { persistence.Setup(datastore) workerpool.InitOutboundWorkerPool() inbox.InitInboxWorkerPool() - StartRouter() // Generate the keys for signing federated activity if needed. if data.GetPrivateKey() == "" { diff --git a/activitypub/router.go b/activitypub/router.go deleted file mode 100644 index a7dc81d75..000000000 --- a/activitypub/router.go +++ /dev/null @@ -1,35 +0,0 @@ -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)) -} diff --git a/config/constants.go b/config/constants.go index 36fa69109..c7f8e24f1 100644 --- a/config/constants.go +++ b/config/constants.go @@ -4,7 +4,7 @@ import "path/filepath" const ( // StaticVersionNumber is the version of Owncast that is used when it's not overwritten via build-time settings. - StaticVersionNumber = "0.1.3" // Shown when you build from develop + StaticVersionNumber = "0.2.0" // Shown when you build from develop // FfmpegSuggestedVersion is the version of ffmpeg we suggest. FfmpegSuggestedVersion = "v4.1.5" // Requires the v // DataDirectory is the directory we save data to. diff --git a/controllers/admin/config.go b/controllers/admin/config.go index 5fd78e7df..0648803ed 100644 --- a/controllers/admin/config.go +++ b/controllers/admin/config.go @@ -802,6 +802,42 @@ func SetVideoServingEndpoint(w http.ResponseWriter, r *http.Request) { controllers.WriteSimpleResponse(w, true, "custom video serving endpoint updated") } +// SetChatSpamProtectionEnabled will enable or disable the chat spam protection. +func SetChatSpamProtectionEnabled(w http.ResponseWriter, r *http.Request) { + if !requirePOST(w, r) { + return + } + + configValue, success := getValueFromRequest(w, r) + if !success { + return + } + + if err := data.SetChatSpamProtectionEnabled(configValue.Value.(bool)); err != nil { + controllers.WriteSimpleResponse(w, false, err.Error()) + return + } + controllers.WriteSimpleResponse(w, true, "chat spam protection changed") +} + +// SetChatSlurFilterEnabled will enable or disable the chat slur filter. +func SetChatSlurFilterEnabled(w http.ResponseWriter, r *http.Request) { + if !requirePOST(w, r) { + return + } + + configValue, success := getValueFromRequest(w, r) + if !success { + return + } + + if err := data.SetChatSlurFilterEnabled(configValue.Value.(bool)); err != nil { + controllers.WriteSimpleResponse(w, false, err.Error()) + return + } + controllers.WriteSimpleResponse(w, true, "chat message slur filter changed") +} + func requirePOST(w http.ResponseWriter, r *http.Request) bool { if r.Method != controllers.POST { controllers.WriteSimpleResponse(w, false, r.Method+" not supported") diff --git a/controllers/admin/serverConfig.go b/controllers/admin/serverConfig.go index 2429fdfa1..43e2720df 100644 --- a/controllers/admin/serverConfig.go +++ b/controllers/admin/serverConfig.go @@ -49,20 +49,22 @@ func GetServerConfig(w http.ResponseWriter, r *http.Request) { CustomJavascript: data.GetCustomJavascript(), AppearanceVariables: data.GetCustomColorVariableValues(), }, - FFmpegPath: ffmpeg, - AdminPassword: data.GetAdminPassword(), - StreamKeys: data.GetStreamKeys(), - StreamKeyOverridden: config.TemporaryStreamKey != "", - WebServerPort: config.WebServerPort, - WebServerIP: config.WebServerIP, - RTMPServerPort: data.GetRTMPPortNumber(), - ChatDisabled: data.GetChatDisabled(), - ChatJoinMessagesEnabled: data.GetChatJoinPartMessagesEnabled(), - SocketHostOverride: data.GetWebsocketOverrideHost(), - VideoServingEndpoint: data.GetVideoServingEndpoint(), - ChatEstablishedUserMode: data.GetChatEstbalishedUsersOnlyMode(), - HideViewerCount: data.GetHideViewerCount(), - DisableSearchIndexing: data.GetDisableSearchIndexing(), + FFmpegPath: ffmpeg, + AdminPassword: data.GetAdminPassword(), + StreamKeys: data.GetStreamKeys(), + StreamKeyOverridden: config.TemporaryStreamKey != "", + WebServerPort: config.WebServerPort, + WebServerIP: config.WebServerIP, + RTMPServerPort: data.GetRTMPPortNumber(), + ChatDisabled: data.GetChatDisabled(), + ChatJoinMessagesEnabled: data.GetChatJoinPartMessagesEnabled(), + SocketHostOverride: data.GetWebsocketOverrideHost(), + VideoServingEndpoint: data.GetVideoServingEndpoint(), + ChatEstablishedUserMode: data.GetChatEstbalishedUsersOnlyMode(), + ChatSpamProtectionEnabled: data.GetChatSpamProtectionEnabled(), + ChatSlurFilterEnabled: data.GetChatSlurFilterEnabled(), + HideViewerCount: data.GetHideViewerCount(), + DisableSearchIndexing: data.GetDisableSearchIndexing(), VideoSettings: videoSettings{ VideoQualityVariants: videoQualityVariants, LatencyLevel: data.GetStreamLatencyLevel().Level, @@ -100,31 +102,33 @@ func GetServerConfig(w http.ResponseWriter, r *http.Request) { } type serverConfigAdminResponse struct { - InstanceDetails webConfigResponse `json:"instanceDetails"` - Notifications notificationsConfigResponse `json:"notifications"` - YP yp `json:"yp"` - FFmpegPath string `json:"ffmpegPath"` - AdminPassword string `json:"adminPassword"` - SocketHostOverride string `json:"socketHostOverride,omitempty"` - WebServerIP string `json:"webServerIP"` - VideoCodec string `json:"videoCodec"` - VideoServingEndpoint string `json:"videoServingEndpoint"` - S3 models.S3 `json:"s3"` - Federation federationConfigResponse `json:"federation"` - SupportedCodecs []string `json:"supportedCodecs"` - ExternalActions []models.ExternalAction `json:"externalActions"` - ForbiddenUsernames []string `json:"forbiddenUsernames"` - SuggestedUsernames []string `json:"suggestedUsernames"` - StreamKeys []models.StreamKey `json:"streamKeys"` - VideoSettings videoSettings `json:"videoSettings"` - RTMPServerPort int `json:"rtmpServerPort"` - WebServerPort int `json:"webServerPort"` - ChatDisabled bool `json:"chatDisabled"` - ChatJoinMessagesEnabled bool `json:"chatJoinMessagesEnabled"` - ChatEstablishedUserMode bool `json:"chatEstablishedUserMode"` - DisableSearchIndexing bool `json:"disableSearchIndexing"` - StreamKeyOverridden bool `json:"streamKeyOverridden"` - HideViewerCount bool `json:"hideViewerCount"` + InstanceDetails webConfigResponse `json:"instanceDetails"` + Notifications notificationsConfigResponse `json:"notifications"` + YP yp `json:"yp"` + FFmpegPath string `json:"ffmpegPath"` + AdminPassword string `json:"adminPassword"` + SocketHostOverride string `json:"socketHostOverride,omitempty"` + WebServerIP string `json:"webServerIP"` + VideoCodec string `json:"videoCodec"` + VideoServingEndpoint string `json:"videoServingEndpoint"` + S3 models.S3 `json:"s3"` + Federation federationConfigResponse `json:"federation"` + SupportedCodecs []string `json:"supportedCodecs"` + ExternalActions []models.ExternalAction `json:"externalActions"` + ForbiddenUsernames []string `json:"forbiddenUsernames"` + SuggestedUsernames []string `json:"suggestedUsernames"` + StreamKeys []models.StreamKey `json:"streamKeys"` + VideoSettings videoSettings `json:"videoSettings"` + RTMPServerPort int `json:"rtmpServerPort"` + WebServerPort int `json:"webServerPort"` + ChatDisabled bool `json:"chatDisabled"` + ChatJoinMessagesEnabled bool `json:"chatJoinMessagesEnabled"` + ChatEstablishedUserMode bool `json:"chatEstablishedUserMode"` + ChatSpamProtectionEnabled bool `json:"chatSpamProtectionEnabled"` + ChatSlurFilterEnabled bool `json:"chatSlurFilterEnabled"` + DisableSearchIndexing bool `json:"disableSearchIndexing"` + StreamKeyOverridden bool `json:"streamKeyOverridden"` + HideViewerCount bool `json:"hideViewerCount"` } type videoSettings struct { diff --git a/controllers/auth/fediverse/fediverse.go b/controllers/auth/fediverse/fediverse.go index 6f7002bff..4803055ae 100644 --- a/controllers/auth/fediverse/fediverse.go +++ b/controllers/auth/fediverse/fediverse.go @@ -39,7 +39,7 @@ func RegisterFediverseOTPRequest(u user.User, w http.ResponseWriter, r *http.Req return } - msg := fmt.Sprintf("
This is an automated message from %s. If you did not request this message please ignore or block. Your requested one-time code is:
%s
", data.GetServerName(), reg.Code) + msg := fmt.Sprintf("One-time code from %s: %s. If you did not request this message please ignore or block.
", data.GetServerName(), reg.Code) if err := activitypub.SendDirectFederatedMessage(msg, reg.Account); err != nil { controllers.WriteSimpleResponse(w, false, "Could not send code to fediverse: "+err.Error()) return diff --git a/controllers/auth/indieauth/server.go b/controllers/auth/indieauth/server.go index 92cb4562f..68078e8de 100644 --- a/controllers/auth/indieauth/server.go +++ b/controllers/auth/indieauth/server.go @@ -13,18 +13,18 @@ import ( func HandleAuthEndpoint(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { // Require the GET request for IndieAuth to be behind admin login. - f := middleware.RequireAdminAuth(handleAuthEndpointGet) + f := middleware.RequireAdminAuth(HandleAuthEndpointGet) f(w, r) return } else if r.Method == http.MethodPost { - handleAuthEndpointPost(w, r) + HandleAuthEndpointPost(w, r) } else { w.WriteHeader(http.StatusMethodNotAllowed) return } } -func handleAuthEndpointGet(w http.ResponseWriter, r *http.Request) { +func HandleAuthEndpointGet(w http.ResponseWriter, r *http.Request) { clientID := r.URL.Query().Get("client_id") redirectURI := r.URL.Query().Get("redirect_uri") codeChallenge := r.URL.Query().Get("code_challenge") @@ -57,7 +57,7 @@ func handleAuthEndpointGet(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, u.String(), http.StatusTemporaryRedirect) } -func handleAuthEndpointPost(w http.ResponseWriter, r *http.Request) { +func HandleAuthEndpointPost(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { controllers.WriteSimpleResponse(w, false, err.Error()) return diff --git a/controllers/config.go b/controllers/config.go index ceec87692..a8ea9f599 100644 --- a/controllers/config.go +++ b/controllers/config.go @@ -16,26 +16,27 @@ import ( ) type webConfigResponse struct { - AppearanceVariables map[string]string `json:"appearanceVariables"` - Name string `json:"name"` - CustomStyles string `json:"customStyles"` - StreamTitle string `json:"streamTitle,omitempty"` // What's going on with the current stream - OfflineMessage string `json:"offlineMessage"` - Logo string `json:"logo"` - Version string `json:"version"` - SocketHostOverride string `json:"socketHostOverride,omitempty"` - ExtraPageContent string `json:"extraPageContent"` - Summary string `json:"summary"` - Tags []string `json:"tags"` - SocialHandles []models.SocialHandle `json:"socialHandles"` - ExternalActions []models.ExternalAction `json:"externalActions"` - Notifications notificationsConfigResponse `json:"notifications"` - Federation federationConfigResponse `json:"federation"` - MaxSocketPayloadSize int `json:"maxSocketPayloadSize"` - HideViewerCount bool `json:"hideViewerCount"` - ChatDisabled bool `json:"chatDisabled"` - NSFW bool `json:"nsfw"` - Authentication authenticationConfigResponse `json:"authentication"` + AppearanceVariables map[string]string `json:"appearanceVariables"` + Name string `json:"name"` + CustomStyles string `json:"customStyles"` + StreamTitle string `json:"streamTitle,omitempty"` // What's going on with the current stream + OfflineMessage string `json:"offlineMessage"` + Logo string `json:"logo"` + Version string `json:"version"` + SocketHostOverride string `json:"socketHostOverride,omitempty"` + ExtraPageContent string `json:"extraPageContent"` + Summary string `json:"summary"` + Tags []string `json:"tags"` + SocialHandles []models.SocialHandle `json:"socialHandles"` + ExternalActions []models.ExternalAction `json:"externalActions"` + Notifications notificationsConfigResponse `json:"notifications"` + Federation federationConfigResponse `json:"federation"` + MaxSocketPayloadSize int `json:"maxSocketPayloadSize"` + HideViewerCount bool `json:"hideViewerCount"` + ChatDisabled bool `json:"chatDisabled"` + ChatSpamProtectionDisabled bool `json:"chatSpamProtectionDisabled"` + NSFW bool `json:"nsfw"` + Authentication authenticationConfigResponse `json:"authentication"` } type federationConfigResponse struct { @@ -118,26 +119,27 @@ func getConfigResponse() webConfigResponse { } return webConfigResponse{ - Name: data.GetServerName(), - Summary: serverSummary, - OfflineMessage: offlineMessage, - Logo: "/logo", - Tags: data.GetServerMetadataTags(), - Version: config.GetReleaseString(), - NSFW: data.GetNSFW(), - SocketHostOverride: data.GetWebsocketOverrideHost(), - ExtraPageContent: pageContent, - StreamTitle: data.GetStreamTitle(), - SocialHandles: socialHandles, - ChatDisabled: data.GetChatDisabled(), - ExternalActions: data.GetExternalActions(), - CustomStyles: data.GetCustomStyles(), - MaxSocketPayloadSize: config.MaxSocketPayloadSize, - Federation: federationResponse, - Notifications: notificationsResponse, - Authentication: authenticationResponse, - AppearanceVariables: data.GetCustomColorVariableValues(), - HideViewerCount: data.GetHideViewerCount(), + Name: data.GetServerName(), + Summary: serverSummary, + OfflineMessage: offlineMessage, + Logo: "/logo", + Tags: data.GetServerMetadataTags(), + Version: config.GetReleaseString(), + NSFW: data.GetNSFW(), + SocketHostOverride: data.GetWebsocketOverrideHost(), + ExtraPageContent: pageContent, + StreamTitle: data.GetStreamTitle(), + SocialHandles: socialHandles, + ChatDisabled: data.GetChatDisabled(), + ChatSpamProtectionDisabled: data.GetChatSpamProtectionEnabled(), + ExternalActions: data.GetExternalActions(), + CustomStyles: data.GetCustomStyles(), + MaxSocketPayloadSize: config.MaxSocketPayloadSize, + Federation: federationResponse, + Notifications: notificationsResponse, + Authentication: authenticationResponse, + AppearanceVariables: data.GetCustomColorVariableValues(), + HideViewerCount: data.GetHideViewerCount(), } } diff --git a/core/chat/chatclient.go b/core/chat/chatclient.go index fac4ed491..df344f17b 100644 --- a/core/chat/chatclient.go +++ b/core/chat/chatclient.go @@ -13,19 +13,21 @@ import ( "github.com/gorilla/websocket" "github.com/owncast/owncast/config" "github.com/owncast/owncast/core/chat/events" + "github.com/owncast/owncast/core/data" "github.com/owncast/owncast/core/user" "github.com/owncast/owncast/geoip" ) // Client represents a single chat client. type Client struct { - ConnectedAt time.Time `json:"connectedAt"` - timeoutTimer *time.Timer - rateLimiter *rate.Limiter - conn *websocket.Conn - User *user.User `json:"user"` - server *Server - Geo *geoip.GeoDetails `json:"geo"` + ConnectedAt time.Time `json:"connectedAt"` + timeoutTimer *time.Timer + rateLimiter *rate.Limiter + messageFilter *ChatMessageFilter + conn *websocket.Conn + User *user.User `json:"user"` + server *Server + Geo *geoip.GeoDetails `json:"geo"` // Buffered channel of outbound messages. send chan []byte accessToken string @@ -90,6 +92,7 @@ func (c *Client) readPump() { // Allow 3 messages every two seconds. limit := rate.Every(2 * time.Second / 3) c.rateLimiter = rate.NewLimiter(limit, 1) + c.messageFilter = NewMessageFilter() defer func() { c.close() @@ -129,6 +132,12 @@ func (c *Client) readPump() { continue } + // Check if this message passes the optional language filter + if data.GetChatSlurFilterEnabled() && !c.messageFilter.Allow(string(message)) { + c.sendAction("Sorry, that message contained language that is not allowed in this chat.") + continue + } + message = bytes.TrimSpace(bytes.ReplaceAll(message, newline, space)) c.handleEvent(message) } @@ -200,7 +209,13 @@ func (c *Client) close() { } func (c *Client) passesRateLimit() bool { - return c.User.IsModerator() || (c.rateLimiter.Allow() && !c.inTimeout) + // If spam rate limiting is disabled, or the user is a moderator, always + // allow the message. + if !data.GetChatSpamProtectionEnabled() || c.User.IsModerator() { + return true + } + + return (c.rateLimiter.Allow() && !c.inTimeout) } func (c *Client) startChatRejectionTimeout() { diff --git a/core/chat/messageFilter.go b/core/chat/messageFilter.go new file mode 100644 index 000000000..86d11ec02 --- /dev/null +++ b/core/chat/messageFilter.go @@ -0,0 +1,18 @@ +package chat + +import ( + goaway "github.com/TwiN/go-away" +) + +// ChatMessageFilter is a allow/deny chat message filter. +type ChatMessageFilter struct{} + +// NewMessageFilter will return an instance of the chat message filter. +func NewMessageFilter() *ChatMessageFilter { + return &ChatMessageFilter{} +} + +// Allow will test if this message should be allowed to be sent. +func (*ChatMessageFilter) Allow(message string) bool { + return !goaway.IsProfane(message) +} diff --git a/core/chat/messageFilter_test.go b/core/chat/messageFilter_test.go new file mode 100644 index 000000000..37ca31823 --- /dev/null +++ b/core/chat/messageFilter_test.go @@ -0,0 +1,39 @@ +package chat + +import ( + "testing" +) + +func TestFiltering(t *testing.T) { + filter := NewMessageFilter() + + filteredTestMessages := []string{ + "Hello, fucking world!", + "Suck my dick", + "Eat my ass", + "fuck this shit", + "@$$h073", + "F u C k th1$ $h!t", + "u r fag", + "fucking sucks", + } + + unfilteredTestMessages := []string{ + "bass fish", + "assumptions", + } + + for _, m := range filteredTestMessages { + result := filter.Allow(m) + if result { + t.Errorf("%s should be seen as a filtered profane message", m) + } + } + + for _, m := range unfilteredTestMessages { + result := filter.Allow(m) + if !result { + t.Errorf("%s should not be filtered", m) + } + } +} diff --git a/core/core.go b/core/core.go index 5b6e28262..ef7165d5b 100644 --- a/core/core.go +++ b/core/core.go @@ -104,7 +104,7 @@ func transitionToOfflineVideoStreamContent() { _transcoder.SetLatencyLevel(models.GetLatencyLevel(4)) _transcoder.SetIsEvent(true) - offlineFilePath, err := saveOfflineClipToDisk("offline.ts") + offlineFilePath, err := saveOfflineClipToDisk("offline-v2.ts") if err != nil { log.Fatalln("unable to save offline clip:", err) } diff --git a/core/data/config.go b/core/data/config.go index 47cc860af..ca440dc56 100644 --- a/core/data/config.go +++ b/core/data/config.go @@ -59,6 +59,8 @@ const ( suggestedUsernamesKey = "suggested_usernames" chatJoinMessagesEnabledKey = "chat_join_messages_enabled" chatEstablishedUsersOnlyModeKey = "chat_established_users_only_mode" + chatSpamProtectionEnabledKey = "chat_spam_protection_enabled" + chatSlurFilterEnabledKey = "chat_slur_filter_enabled" notificationsEnabledKey = "notifications_enabled" discordConfigurationKey = "discord_configuration" browserPushConfigurationKey = "browser_push_configuration" @@ -528,6 +530,36 @@ func GetChatEstbalishedUsersOnlyMode() bool { return false } +// SetChatSpamProtectionEnabled will enable chat spam protection if set to true. +func SetChatSpamProtectionEnabled(enabled bool) error { + return _datastore.SetBool(chatSpamProtectionEnabledKey, enabled) +} + +// GetChatSpamProtectionEnabled will return if chat spam protection is enabled. +func GetChatSpamProtectionEnabled() bool { + enabled, err := _datastore.GetBool(chatSpamProtectionEnabledKey) + if err == nil { + return enabled + } + + return true +} + +// SetChatSlurFilterEnabled will enable the chat slur filter. +func SetChatSlurFilterEnabled(enabled bool) error { + return _datastore.SetBool(chatSlurFilterEnabledKey, enabled) +} + +// GetChatSlurFilterEnabled will return if the chat slur filter is enabled. +func GetChatSlurFilterEnabled() bool { + enabled, err := _datastore.GetBool(chatSlurFilterEnabledKey) + if err == nil { + return enabled + } + + return false +} + // GetExternalActions will return the registered external actions. func GetExternalActions() []models.ExternalAction { configEntry, err := _datastore.Get(externalActionsKey) diff --git a/core/offlineState.go b/core/offlineState.go index e1257c37f..4fb1c7e04 100644 --- a/core/offlineState.go +++ b/core/offlineState.go @@ -36,7 +36,7 @@ func appendOfflineToVariantPlaylist(index int, playlistFilePath string) { _, _ = atomicWriteTmpPlaylistFile.WriteString("#EXT-X-DISCONTINUITY\n") // If "offline" content gets changed then change the duration below _, _ = atomicWriteTmpPlaylistFile.WriteString("#EXTINF:8.000000,\n") - _, _ = atomicWriteTmpPlaylistFile.WriteString("offline.ts\n") + _, _ = atomicWriteTmpPlaylistFile.WriteString("offline-v2.ts\n") _, _ = atomicWriteTmpPlaylistFile.WriteString("#EXT-X-ENDLIST\n") if err := atomicWriteTmpPlaylistFile.Close(); err != nil { diff --git a/core/streamState.go b/core/streamState.go index 52b8b844e..cb6f10f5a 100644 --- a/core/streamState.go +++ b/core/streamState.go @@ -93,7 +93,7 @@ func SetStreamAsDisconnected() { _stats.LastConnectTime = nil _broadcaster = nil - offlineFilename := "offline.ts" + offlineFilename := "offline-v2.ts" offlineFilePath, err := saveOfflineClipToDisk(offlineFilename) if err != nil { diff --git a/docs/api/index.html b/docs/api/index.html index 3d9ac4567..32c7188be 100644 --- a/docs/api/index.html +++ b/docs/api/index.html @@ -2261,7 +2261,7 @@ data-styled.g138[id="sc-iIgjPs"]{content:"jIUTls,"}/*!sc*/ 55.627 l 55.6165,55.627 -231.245496,231.24803 c -127.185,127.1864 -231.5279,231.248 -231.873,231.248 -0.3451,0 -104.688, -104.0616 -231.873,-231.248 z - " fill="currentColor">Download OpenAPI specification:Download
Owncast is a self-hosted live video and web chat server for use with existing popular broadcasting software.
Take note that only APIs listed specifically for external use, 3rd parties or for integration purposes are encouraged for external use. Internal APIs may change at any time and are used by the server and frontend itself.
Download OpenAPI specification:Download
Owncast is a self-hosted live video and web chat server for use with existing popular broadcasting software.
Take note that only APIs listed specifically for external use, 3rd parties or for integration purposes are encouraged for external use. Internal APIs may change at any time and are used by the server and frontend itself.
{- "broadcaster": {
- "remoteAddr": "172.217.164.110",
- "time": "2020-10-06T23:20:44.588649-07:00",
- "streamDetails": {
- "width": 640,
- "height": 480,
- "frameRate": 24,
- "videoBitrate": 1500,
- "videoCodec": "mp4a",
- "audioBitrate": 256,
- "audioCodec": "aac",
- "encoder": "obs-output module (libobs version 25.0.8)"
}
}, - "online": true,
- "viewerCount": 3,
- "overallPeakViewerCount": 4,
- "sessionPeakViewerCount": 4,
- "versionNumber": "0.0.3"
}Disconnect the active inbound stream, if one exists, and terminate the broadcast.
@@ -2442,11 +2442,11 @@ data-styled.g138[id="sc-iIgjPs"]{content:"jIUTls,"}/*!sc*/{- "lastConnectTime": "2020-10-03T21:36:22-05:00",
- "lastDisconnectTime": null,
- "online": true,
- "overallMaxViewerCount": 420,
- "sessionMaxViewerCount": 12,
- "viewerCount": 7
}Information to be used in the Yellow Pages service, a global directory of Owncast servers.
{- "name": "string",
- "description": "string",
- "logo": "string",
- "nsfw": true,
- "tags": [
- "string"
], - "online": true,
- "viewerCount": 0,
- "overallMaxViewerCount": 0,
- "sessionMaxViewerCount": 0,
- "lastConnectTime": "2019-08-24T14:15:22Z"
}[- {
- "name": "John Smith",
- "timestamp": "2019-08-24T14:15:22Z",
- "disabledAt": "2019-08-24T14:15:22Z"
}
]| account | string The fediverse username@server.tld account that wants to perform the remote follow action. +Response samples
Content type application/json [Return the information needed to redirect a user to a fediverse server to perform a remote follow action.Request Body schema: application/json
ResponsesRequest samples
Content type application/json {Response samples
Content type application/json 404This page could not be found. |