Compare commits
46
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
601553c582 | ||
|
|
8b5fe84e62 | ||
|
|
33efafecd5 | ||
|
|
8ccec822b4 | ||
|
|
e738156fd7 | ||
|
|
a2981903c2 | ||
|
|
5d4fcf9686 | ||
|
|
50854f09b3 | ||
|
|
54104a5c3c | ||
|
|
715c267079 | ||
|
|
c48cc989f4 | ||
|
|
44970293f8 | ||
|
|
423ddb8e22 | ||
|
|
c593ed381d | ||
|
|
b6a39721dd | ||
|
|
159e24438d | ||
|
|
2c3e8fa3c1 | ||
|
|
ba8ce8bf66 | ||
|
|
84341875b7 | ||
|
|
d52911160d | ||
|
|
4ef036b0f4 | ||
|
|
eb3184bcfa | ||
|
|
5c9b2333f9 | ||
|
|
77c7c08587 | ||
|
|
d9a424ea79 | ||
|
|
826c7e8a9e | ||
|
|
3e6f6de032 | ||
|
|
3909c506a2 | ||
|
|
e9556d44da | ||
|
|
c85529370b | ||
|
|
285a722fac | ||
|
|
234c696af9 | ||
|
|
177840b94a | ||
|
|
35e684bd10 | ||
|
|
9b4adab8fc | ||
|
|
d74d1b55f0 | ||
|
|
4d52d7d1fa | ||
|
|
02559acc39 | ||
|
|
892fd3d89e | ||
|
|
0bb372d8cb | ||
|
|
ddc8e9bed0 | ||
|
|
5b2a3e3127 | ||
|
|
a476142f23 | ||
|
|
ad090a0f86 | ||
|
|
5365cc0325 | ||
|
|
37404aac5c |
@@ -1,57 +0,0 @@
|
||||
name: ActivityPub Federation Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- "activitypub/**"
|
||||
- "webserver/**"
|
||||
- "test/automated/activitypub/**"
|
||||
- ".github/workflows/activitypub-federation-tests.yaml"
|
||||
pull_request:
|
||||
paths:
|
||||
- "activitypub/**"
|
||||
- "webserver/**"
|
||||
- "test/automated/activitypub/**"
|
||||
- ".github/workflows/activitypub-federation-tests.yaml"
|
||||
|
||||
jobs:
|
||||
federation-test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- id: skip_check
|
||||
uses: fkirc/skip-duplicate-actions@v5
|
||||
with:
|
||||
concurrent_skipping: "same_content_newer"
|
||||
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "^1.23"
|
||||
cache: true
|
||||
|
||||
- name: Install Caddy
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y debian-keyring debian-archive-keyring apt-transport-https curl
|
||||
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
||||
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y caddy
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get install -y snac2 ffmpeg
|
||||
|
||||
- name: Run setup
|
||||
run: |
|
||||
cd test/automated/activitypub
|
||||
sudo ./setup.sh
|
||||
|
||||
- name: Run federation test
|
||||
run: |
|
||||
cd test/automated/activitypub
|
||||
CI=true USER_COUNT=20 ./run.sh
|
||||
@@ -1,47 +0,0 @@
|
||||
name: ActivityPub Follower Validation Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- "activitypub/**"
|
||||
- "config/**"
|
||||
- "main.go"
|
||||
- "test/automated/activitypub/**"
|
||||
- ".github/workflows/activitypub-follower-validation-tests.yaml"
|
||||
pull_request:
|
||||
paths:
|
||||
- "activitypub/**"
|
||||
- "config/**"
|
||||
- "main.go"
|
||||
- "test/automated/activitypub/**"
|
||||
- ".github/workflows/activitypub-follower-validation-tests.yaml"
|
||||
|
||||
jobs:
|
||||
follower-validation-test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- id: skip_check
|
||||
uses: fkirc/skip-duplicate-actions@v5
|
||||
with:
|
||||
concurrent_skipping: "same_content_newer"
|
||||
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "^1.23"
|
||||
cache: true
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y sqlite3 ffmpeg jq
|
||||
|
||||
- name: Run follower validation test
|
||||
timeout-minutes: 15
|
||||
run: |
|
||||
cd test/automated/activitypub
|
||||
./test-follower-validation.sh
|
||||
@@ -0,0 +1,130 @@
|
||||
name: ActivityPub Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- "activitypub/**"
|
||||
- "core/chat/**"
|
||||
- "config/**"
|
||||
- "main.go"
|
||||
- "webserver/**"
|
||||
- "test/automated/activitypub/**"
|
||||
- ".github/workflows/activitypub-tests.yaml"
|
||||
pull_request:
|
||||
paths:
|
||||
- "activitypub/**"
|
||||
- "core/chat/**"
|
||||
- "config/**"
|
||||
- "main.go"
|
||||
- "webserver/**"
|
||||
- "test/automated/activitypub/**"
|
||||
- ".github/workflows/activitypub-tests.yaml"
|
||||
|
||||
jobs:
|
||||
federation-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: skip_check
|
||||
uses: fkirc/skip-duplicate-actions@v5
|
||||
with:
|
||||
concurrent_skipping: "same_content_newer"
|
||||
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "^1.23"
|
||||
cache: true
|
||||
|
||||
- name: Install Caddy
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y debian-keyring debian-archive-keyring apt-transport-https curl
|
||||
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
||||
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y caddy
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get install -y snac2 ffmpeg
|
||||
|
||||
- name: Run setup
|
||||
run: |
|
||||
cd test/automated/activitypub
|
||||
sudo ./setup.sh
|
||||
|
||||
- name: Run federation test
|
||||
run: |
|
||||
cd test/automated/activitypub
|
||||
CI=true USER_COUNT=20 ./test-federation.sh
|
||||
|
||||
chat-sanitization-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: skip_check
|
||||
uses: fkirc/skip-duplicate-actions@v5
|
||||
with:
|
||||
concurrent_skipping: "same_content_newer"
|
||||
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "^1.23"
|
||||
cache: true
|
||||
|
||||
- name: Install Caddy
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y debian-keyring debian-archive-keyring apt-transport-https curl
|
||||
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
||||
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y caddy
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get install -y snac2 ffmpeg
|
||||
|
||||
- name: Run setup
|
||||
run: |
|
||||
cd test/automated/activitypub
|
||||
sudo ./setup.sh
|
||||
|
||||
- name: Run chat sanitization test
|
||||
run: |
|
||||
cd test/automated/activitypub
|
||||
CI=true ./test-chat-sanitization.sh
|
||||
|
||||
follower-validation-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: skip_check
|
||||
uses: fkirc/skip-duplicate-actions@v5
|
||||
with:
|
||||
concurrent_skipping: "same_content_newer"
|
||||
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "^1.23"
|
||||
cache: true
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y sqlite3 ffmpeg jq
|
||||
|
||||
- name: Run follower validation test
|
||||
timeout-minutes: 15
|
||||
run: |
|
||||
cd test/automated/activitypub
|
||||
./test-follower-validation.sh
|
||||
@@ -11,7 +11,7 @@ jobs:
|
||||
issues: write
|
||||
steps:
|
||||
- name: Add comment
|
||||
uses: peter-evans/create-or-update-comment@9143c495e69aa6ad64fa02b7515ccab6bd35aad1
|
||||
uses: peter-evans/create-or-update-comment@57232238742e38b2ccc27136ce596ccae7ca28b4
|
||||
with:
|
||||
issue-number: ${{ github.event.issue.number }}
|
||||
body: |
|
||||
|
||||
@@ -86,7 +86,7 @@ jobs:
|
||||
|
||||
- name: Publish to Chromatic
|
||||
if: ${{ github.actor != 'renovate[bot]' && github.actor != 'renovate' && steps.changed-files-yaml.outputs.src_any_changed == 'true' }}
|
||||
uses: chromaui/action@v15
|
||||
uses: chromaui/action@v16
|
||||
with:
|
||||
workingDir: web
|
||||
projectToken: f47410569b62
|
||||
|
||||
@@ -38,6 +38,7 @@ on:
|
||||
jobs:
|
||||
formatting:
|
||||
name: Code formatting
|
||||
if: github.actor != 'renovate[bot]' && github.actor != 'dependabot[bot]'
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
@@ -133,6 +134,7 @@ jobs:
|
||||
|
||||
unused-code:
|
||||
name: Test for unused code
|
||||
if: github.actor != 'renovate[bot]' && github.actor != 'dependabot[bot]'
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
@@ -197,7 +199,7 @@ jobs:
|
||||
web-bundle:
|
||||
name: Build and bundle web project
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'owncast/owncast'
|
||||
if: github.repository == 'owncast/owncast' && !cancelled()
|
||||
needs: [formatting, unused-code]
|
||||
steps:
|
||||
- id: skip_check
|
||||
|
||||
@@ -16,6 +16,8 @@ on:
|
||||
# Run the workflow every hour
|
||||
- cron: '0 * * * *'
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
paths:
|
||||
- 'web/i18n/en/translation.json'
|
||||
- 'web/**/*.tsx'
|
||||
|
||||
@@ -4,12 +4,24 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-fed/activity/streams/vocab"
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
"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"
|
||||
)
|
||||
|
||||
// sanitizeActorName strips HTML tags from the ActivityPub actor display name.
|
||||
// Falls back to the username if the display name is empty or entirely HTML.
|
||||
func sanitizeActorName(displayName, username string) string {
|
||||
strict := bluemonday.StrictPolicy()
|
||||
name := strict.Sanitize(displayName)
|
||||
if name == "" {
|
||||
name = strict.Sanitize(username)
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func handleEngagementActivity(eventType events.EventType, isLiveNotification bool, actorReference vocab.ActivityStreamsActorProperty, action string) error {
|
||||
configRepository := configrepository.Get()
|
||||
|
||||
@@ -30,10 +42,7 @@ func handleEngagementActivity(eventType events.EventType, isLiveNotification boo
|
||||
}
|
||||
|
||||
// Send chat message
|
||||
actorName := actor.Name
|
||||
if actorName == "" {
|
||||
actorName = actor.Username
|
||||
}
|
||||
actorName := sanitizeActorName(actor.Name, actor.Username)
|
||||
actorIRI := actor.ActorIriString()
|
||||
|
||||
userPrefix := fmt.Sprintf("%s ", actorName)
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
package inbox
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSanitizeActorName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
displayName string
|
||||
username string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "plain display name",
|
||||
displayName: "Alice",
|
||||
username: "alice",
|
||||
expected: "Alice",
|
||||
},
|
||||
{
|
||||
name: "display name with emoji",
|
||||
displayName: "Alice 🦊",
|
||||
username: "alice",
|
||||
expected: "Alice 🦊",
|
||||
},
|
||||
{
|
||||
name: "display name with unicode",
|
||||
displayName: "Ålice Böb",
|
||||
username: "alice",
|
||||
expected: "Ålice Böb",
|
||||
},
|
||||
{
|
||||
name: "empty display name falls back to username",
|
||||
displayName: "",
|
||||
username: "alice",
|
||||
expected: "alice",
|
||||
},
|
||||
{
|
||||
name: "script tag in display name",
|
||||
displayName: `<script>alert("xss")</script>`,
|
||||
username: "alice",
|
||||
expected: "alice",
|
||||
},
|
||||
{
|
||||
name: "iframe injection in display name",
|
||||
displayName: `<iframe src="https://evil.com" style="position:fixed;top:0;left:0;width:100%;height:100%"></iframe>`,
|
||||
username: "alice",
|
||||
expected: "alice",
|
||||
},
|
||||
{
|
||||
name: "img tag in display name",
|
||||
displayName: `<img src="https://evil.com/track.png">`,
|
||||
username: "alice",
|
||||
expected: "alice",
|
||||
},
|
||||
{
|
||||
name: "form injection in display name",
|
||||
displayName: `<form action="https://evil.com/steal"><input name="pw" type="password"></form>`,
|
||||
username: "alice",
|
||||
expected: "alice",
|
||||
},
|
||||
{
|
||||
name: "meta refresh in display name",
|
||||
displayName: `<meta http-equiv="refresh" content="0;url=https://evil.com">`,
|
||||
username: "alice",
|
||||
expected: "alice",
|
||||
},
|
||||
{
|
||||
name: "mixed text and HTML in display name",
|
||||
displayName: `Alice <script>alert(1)</script> Bob`,
|
||||
username: "alice",
|
||||
expected: "Alice Bob",
|
||||
},
|
||||
{
|
||||
name: "custom emoji HTML in display name",
|
||||
displayName: `Alice :blobcat: <img src="https://instance.com/emoji/blobcat.png" class="custom-emoji">`,
|
||||
username: "alice",
|
||||
expected: "Alice :blobcat: ",
|
||||
},
|
||||
{
|
||||
name: "HTML in both display name and username",
|
||||
displayName: `<script>alert(1)</script>`,
|
||||
username: `<b>alice</b>`,
|
||||
expected: "alice",
|
||||
},
|
||||
{
|
||||
name: "entirely HTML display name falls back to username",
|
||||
displayName: `<div></div>`,
|
||||
username: "alice",
|
||||
expected: "alice",
|
||||
},
|
||||
{
|
||||
name: "style tag in display name",
|
||||
displayName: `<style>body{display:none}</style>Alice`,
|
||||
username: "alice",
|
||||
expected: "Alice",
|
||||
},
|
||||
{
|
||||
name: "nested HTML tags",
|
||||
displayName: `<div><span><a href="https://evil.com">Click me</a></span></div>`,
|
||||
username: "alice",
|
||||
expected: "Click me",
|
||||
},
|
||||
{
|
||||
name: "event handler attributes",
|
||||
displayName: `<img src=x onerror="alert(1)">Alice`,
|
||||
username: "alice",
|
||||
expected: "Alice",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := sanitizeActorName(tt.displayName, tt.username)
|
||||
if result != tt.expected {
|
||||
t.Errorf("sanitizeActorName(%q, %q) = %q, want %q", tt.displayName, tt.username, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -126,7 +126,6 @@ func SendFediverseAction(eventType string, userAccountName string, image *string
|
||||
}
|
||||
|
||||
message.SetDefaults()
|
||||
message.RenderBody()
|
||||
|
||||
if err := Broadcast(&message); err != nil {
|
||||
log.Errorln("error sending system message", err)
|
||||
|
||||
@@ -16,12 +16,12 @@ require (
|
||||
github.com/hashicorp/go-retryablehttp v0.7.8
|
||||
github.com/jellydator/ttlcache/v3 v3.4.0
|
||||
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
|
||||
github.com/mattn/go-sqlite3 v1.14.37
|
||||
github.com/mattn/go-sqlite3 v1.14.38
|
||||
github.com/microcosm-cc/bluemonday v1.0.27
|
||||
github.com/mssola/user_agent v0.6.0
|
||||
github.com/nakabonne/tstorage v0.3.6
|
||||
github.com/nareix/joy5 v0.0.0-20210317075623-2c912ca30590
|
||||
github.com/oapi-codegen/runtime v1.3.0
|
||||
github.com/oapi-codegen/runtime v1.3.1
|
||||
github.com/oschwald/geoip2-golang v1.13.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
|
||||
@@ -94,6 +94,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
|
||||
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg=
|
||||
github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.38 h1:tDUzL85kMvOrvpCt8P64SbGgVFtJB11GPi2AdmITgb4=
|
||||
github.com/mattn/go-sqlite3 v1.14.38/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||
github.com/mssola/user_agent v0.6.0 h1:uwPR4rtWlCHRFyyP9u2KOV0u8iQXmS7Z7feTrstQwk4=
|
||||
@@ -106,6 +108,8 @@ github.com/nareix/joy5 v0.0.0-20210317075623-2c912ca30590 h1:PnxRU8L8Y2q82vFC2Qd
|
||||
github.com/nareix/joy5 v0.0.0-20210317075623-2c912ca30590/go.mod h1:XmAOs6UJXpNXRwKk+KY/nv5kL6xXYXyellk+A1pTlko=
|
||||
github.com/oapi-codegen/runtime v1.3.0 h1:vyK1zc0gDWWXgk2xoQa4+X4RNNc5SL2RbTpJS/4vMYA=
|
||||
github.com/oapi-codegen/runtime v1.3.0/go.mod h1:kOdeacKy7t40Rclb1je37ZLFboFxh+YLy0zaPCMibPY=
|
||||
github.com/oapi-codegen/runtime v1.3.1 h1:RgDY6J4OGQLbRXhG/Xpt3vSVqYpHQS7hN4m85+5xB9g=
|
||||
github.com/oapi-codegen/runtime v1.3.1/go.mod h1:kOdeacKy7t40Rclb1je37ZLFboFxh+YLy0zaPCMibPY=
|
||||
github.com/oschwald/geoip2-golang v1.13.0 h1:Q44/Ldc703pasJeP5V9+aFSZFmBN7DKHbNsSFzQATJI=
|
||||
github.com/oschwald/geoip2-golang v1.13.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo=
|
||||
github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU=
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
Vendored
+2
-2
File diff suppressed because one or more lines are too long
Vendored
+2
-2
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
|
||||
certs/
|
||||
README.md
|
||||
@@ -0,0 +1,36 @@
|
||||
FROM golang:1.25-bookworm
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
gcc make \
|
||||
curl libcurl4-openssl-dev libssl-dev \
|
||||
jq sqlite3 ffmpeg \
|
||||
libnss3-tools \
|
||||
lsof procps \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install mkcert (architecture-aware)
|
||||
RUN ARCH="$(dpkg --print-architecture)" && \
|
||||
curl -sL "https://github.com/FiloSottile/mkcert/releases/download/v1.4.4/mkcert-v1.4.4-linux-${ARCH}" -o /usr/local/bin/mkcert && \
|
||||
chmod +x /usr/local/bin/mkcert
|
||||
|
||||
# Install Caddy (architecture-aware)
|
||||
RUN ARCH="$(dpkg --print-architecture)" && \
|
||||
curl -sL "https://github.com/caddyserver/caddy/releases/download/v2.8.4/caddy_2.8.4_linux_${ARCH}.tar.gz" | tar -xz -C /usr/local/bin caddy
|
||||
|
||||
# Build and install snac2
|
||||
RUN git clone --depth 1 https://codeberg.org/grunfink/snac2.git /tmp/snac2-src \
|
||||
&& cd /tmp/snac2-src && make && cp snac /usr/local/bin/snac \
|
||||
&& rm -rf /tmp/snac2-src
|
||||
|
||||
# Install mkcert CA into system trust store so snac2 trusts the test certs
|
||||
RUN mkcert -install
|
||||
|
||||
# Allow git operations on the mounted repo
|
||||
RUN git config --global --add safe.directory /owncast
|
||||
|
||||
WORKDIR /owncast
|
||||
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
@@ -2,83 +2,29 @@
|
||||
|
||||
This test verifies Owncast's ActivityPub federation by having snac2 users follow the Owncast instance and confirming message delivery.
|
||||
|
||||
## One-Time Setup
|
||||
All test infrastructure (snac2, Caddy, mkcert, Go) runs inside a Docker container so you don't need to install anything on the host besides Docker.
|
||||
|
||||
### 1. Install mkcert
|
||||
## Prerequisites
|
||||
|
||||
- Docker installed and running
|
||||
|
||||
## Running the Tests
|
||||
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt install mkcert
|
||||
|
||||
# Or download directly
|
||||
curl -sLO https://github.com/FiloSottile/mkcert/releases/download/v1.4.4/mkcert-v1.4.4-linux-amd64
|
||||
chmod +x mkcert-v1.4.4-linux-amd64
|
||||
sudo mv mkcert-v1.4.4-linux-amd64 /usr/local/bin/mkcert
|
||||
```
|
||||
|
||||
### 2. Install the local CA
|
||||
|
||||
This installs a Certificate Authority into your system's trust store. All certificates generated by mkcert will be trusted.
|
||||
|
||||
```bash
|
||||
mkcert -install
|
||||
```
|
||||
|
||||
### 3. Generate certificates for the test domains
|
||||
|
||||
```bash
|
||||
cd test/automated/activitypub
|
||||
mkdir -p certs
|
||||
mkcert -cert-file certs/cert.pem -key-file certs/key.pem owncast.local snac.local localhost 127.0.0.1
|
||||
```
|
||||
|
||||
### 4. Add hosts entries
|
||||
|
||||
```bash
|
||||
sudo sh -c 'echo "127.0.0.1 owncast.local snac.local" >> /etc/hosts'
|
||||
```
|
||||
|
||||
### 5. Install snac2
|
||||
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt install snac2
|
||||
|
||||
# Or build from source
|
||||
git clone https://codeberg.org/grunfink/snac2.git
|
||||
cd snac2
|
||||
make
|
||||
sudo make install
|
||||
```
|
||||
|
||||
### 6. Install Caddy
|
||||
|
||||
Caddy is used as the HTTPS reverse proxy for TLS termination. It will be installed automatically by `setup.sh`, or you can install it manually:
|
||||
|
||||
```bash
|
||||
# Download binary directly
|
||||
curl -sL "https://github.com/caddyserver/caddy/releases/download/v2.8.4/caddy_2.8.4_linux_amd64.tar.gz" | sudo tar -xz -C /usr/local/bin caddy
|
||||
|
||||
# Or see: https://caddyserver.com/docs/install
|
||||
```
|
||||
|
||||
## Running the Test
|
||||
|
||||
```bash
|
||||
# Run with default 100 users
|
||||
# Run the federation test with default 100 users
|
||||
./run.sh
|
||||
|
||||
# Run with fewer users for quick testing
|
||||
USER_COUNT=10 ./run.sh
|
||||
|
||||
# Run the follower validation test
|
||||
./run.sh test-follower-validation.sh
|
||||
|
||||
# Keep servers running after test for debugging
|
||||
KEEP_RUNNING=true ./run.sh
|
||||
|
||||
# Adjust follow request throttling (default 0.1s)
|
||||
FOLLOW_DELAY=0.2 ./run.sh
|
||||
|
||||
# Run in CI mode (skip interactive prompts)
|
||||
CI=true ./run.sh
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
@@ -88,7 +34,7 @@ CI=true ./run.sh
|
||||
| `USER_COUNT` | 100 | Number of test users to create |
|
||||
| `FOLLOW_DELAY` | 0.1 | Delay in seconds between follow requests |
|
||||
| `KEEP_RUNNING` | false | Keep servers running after test for debugging |
|
||||
| `CI` | false | Skip interactive prompts for CI environments |
|
||||
| `CI` | false | Always true inside the container |
|
||||
| `PROXY_PORT` | 8443 | HTTPS proxy port |
|
||||
| `SNAC_PORT` | 9080 | snac2 HTTP port |
|
||||
| `OWNCAST_PORT` | 8080 | Owncast HTTP port |
|
||||
@@ -111,23 +57,37 @@ The test reports:
|
||||
- **Follow Success Rate**: Percentage of follow requests that succeeded
|
||||
- **Delivery Rate**: Percentage of registered followers who received the message
|
||||
|
||||
## Docker Image Details
|
||||
|
||||
The Docker image (`owncast-ap-test`) bundles all dependencies:
|
||||
- Go (for building Owncast)
|
||||
- snac2 (built from source)
|
||||
- Caddy (HTTPS reverse proxy)
|
||||
- mkcert (TLS certificates trusted by the container)
|
||||
- sqlite3, jq, curl
|
||||
|
||||
Go module and build caches are stored in named Docker volumes (`owncast-ap-test-gomod`, `owncast-ap-test-gobuild`) so repeated runs are faster.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Certificate errors from snac2
|
||||
### Docker build fails
|
||||
|
||||
Make sure you ran `mkcert -install` and generated the certificates. The CA must be in the system trust store for snac2 to trust the certificates.
|
||||
Make sure Docker is running. On macOS, Docker Desktop or a compatible runtime (colima, OrbStack, etc.) is required.
|
||||
|
||||
### Port already in use
|
||||
|
||||
Kill any leftover processes:
|
||||
If a previous container didn't shut down cleanly:
|
||||
```bash
|
||||
pkill -f "snac httpd /tmp"
|
||||
pkill -f "caddy run"
|
||||
docker ps -a | grep owncast-ap-test
|
||||
docker rm -f <container_id>
|
||||
```
|
||||
|
||||
### Hosts file not configured
|
||||
### Cleaning up Docker resources
|
||||
|
||||
Verify the entries exist:
|
||||
```bash
|
||||
grep -E 'owncast.local|snac.local' /etc/hosts
|
||||
# Remove the image
|
||||
docker rmi owncast-ap-test
|
||||
|
||||
# Remove Go caches
|
||||
docker volume rm owncast-ap-test-gomod owncast-ap-test-gobuild
|
||||
```
|
||||
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Generate mkcert certificates for the test domains.
|
||||
# These are placed in a container-local path so they don't leak into the
|
||||
# mounted source tree.
|
||||
export CERT_DIR="/tmp/test-certs"
|
||||
mkdir -p "${CERT_DIR}"
|
||||
mkcert -cert-file "${CERT_DIR}/cert.pem" \
|
||||
-key-file "${CERT_DIR}/key.pem" \
|
||||
owncast.local snac.local localhost 127.0.0.1
|
||||
|
||||
# Change CWD away from the mounted repo root so Owncast doesn't pick up the
|
||||
# host's (possibly wrong-architecture) ffmpeg binary via ./ffmpeg detection.
|
||||
# Stay inside the git repo so `git rev-parse --show-toplevel` still works.
|
||||
cd /owncast/test/automated/activitypub
|
||||
|
||||
# Run the specified test script (default: test-federation.sh)
|
||||
TEST_SCRIPT="${1:-test-federation.sh}"
|
||||
exec "/owncast/test/automated/activitypub/${TEST_SCRIPT}"
|
||||
@@ -1,14 +1,52 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Run the ActivityPub federation test
|
||||
# Run the ActivityPub federation test inside a Docker container.
|
||||
#
|
||||
# Usage:
|
||||
# ./run.sh # Run with 100 users
|
||||
# ./run.sh # Run federation test with 100 users
|
||||
# ./run.sh test-follower-validation.sh # Run follower validation test
|
||||
# USER_COUNT=50 ./run.sh # Run with 50 users
|
||||
# KEEP_RUNNING=true ./run.sh # Keep servers running after test
|
||||
#
|
||||
# Prerequisites:
|
||||
# Add to /etc/hosts: 127.0.0.1 owncast.local snac.local
|
||||
# Docker must be installed and running.
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
exec "${SCRIPT_DIR}/test-federation.sh" "$@"
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||
IMAGE_NAME="owncast-ap-test"
|
||||
|
||||
echo "Building Docker image..."
|
||||
docker build -t "${IMAGE_NAME}" "${SCRIPT_DIR}"
|
||||
|
||||
# Collect environment variables to pass through
|
||||
ENV_ARGS=()
|
||||
for var in USER_COUNT FOLLOW_DELAY KEEP_RUNNING CI PROXY_PORT SNAC_PORT OWNCAST_PORT CLEAR_SHARED_INBOX_PERCENT; do
|
||||
if [[ -n "${!var}" ]]; then
|
||||
ENV_ARGS+=("-e" "${var}=${!var}")
|
||||
fi
|
||||
done
|
||||
|
||||
# Always skip interactive prompts inside the container
|
||||
ENV_ARGS+=("-e" "CI=true")
|
||||
|
||||
# Port-forward when KEEP_RUNNING is set so the user can access the services
|
||||
EXTRA_ARGS=()
|
||||
if [[ "${KEEP_RUNNING}" == "true" ]]; then
|
||||
OWNCAST_PORT="${OWNCAST_PORT:-8080}"
|
||||
PROXY_PORT="${PROXY_PORT:-8443}"
|
||||
EXTRA_ARGS+=("-p" "${OWNCAST_PORT}:${OWNCAST_PORT}" "-p" "${PROXY_PORT}:${PROXY_PORT}")
|
||||
fi
|
||||
|
||||
echo "Running test in Docker container..."
|
||||
docker run --rm \
|
||||
--add-host owncast.local:127.0.0.1 \
|
||||
--add-host snac.local:127.0.0.1 \
|
||||
-v "${REPO_ROOT}:/owncast" \
|
||||
-v owncast-ap-test-gomod:/go/pkg/mod \
|
||||
-v owncast-ap-test-gobuild:/root/.cache/go-build \
|
||||
"${ENV_ARGS[@]}" \
|
||||
"${EXTRA_ARGS[@]}" \
|
||||
"${IMAGE_NAME}" \
|
||||
"$@"
|
||||
|
||||
+520
@@ -0,0 +1,520 @@
|
||||
#!/bin/bash
|
||||
# shellcheck disable=SC2317,SC2329 # cleanup() is invoked via trap, not direct call
|
||||
# shellcheck disable=SC2034 # SNAC_URL is unused but kept for consistency with other AP tests
|
||||
|
||||
# Chat Sanitization Test for Fediverse Engagement Events
|
||||
#
|
||||
# This test verifies that malicious HTML and markdown in ActivityPub actor
|
||||
# display names is sanitized before being rendered in chat messages.
|
||||
#
|
||||
# The test:
|
||||
# 1. Starts snac2 + Owncast + HTTPS proxy (same as federation test)
|
||||
# 2. Creates snac2 users with malicious display names (HTML, markdown)
|
||||
# 3. Enables engagement display in chat
|
||||
# 4. Has the malicious users follow Owncast
|
||||
# 5. Queries the chat messages from the database
|
||||
# 6. Verifies no HTML tags or markdown artifacts appear in stored messages
|
||||
#
|
||||
# Prerequisites:
|
||||
# Add to /etc/hosts: 127.0.0.1 owncast.local snac.local
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(git rev-parse --show-toplevel)"
|
||||
|
||||
# Configuration
|
||||
FOLLOW_DELAY="${FOLLOW_DELAY:-0.3}"
|
||||
CI="${CI:-false}"
|
||||
PROXY_PORT="${PROXY_PORT:-8443}"
|
||||
SNAC_PORT="${SNAC_PORT:-9080}"
|
||||
SNAC_HOSTNAME="snac.local"
|
||||
OWNCAST_PORT="${OWNCAST_PORT:-8080}"
|
||||
OWNCAST_HOSTNAME="owncast.local"
|
||||
ADMIN_USER="admin"
|
||||
ADMIN_PASS="abc123"
|
||||
FEDERATION_USERNAME="streamer"
|
||||
|
||||
# URLs
|
||||
SNAC_URL="https://${SNAC_HOSTNAME}:${PROXY_PORT}"
|
||||
OWNCAST_URL="https://${OWNCAST_HOSTNAME}:${PROXY_PORT}"
|
||||
|
||||
# Directories
|
||||
TEMP_DIR=""
|
||||
SNAC_DATA_DIR=""
|
||||
SNAC_BIN=""
|
||||
OWNCAST_DB=""
|
||||
|
||||
# PIDs
|
||||
SNAC_PID=""
|
||||
OWNCAST_PID=""
|
||||
PROXY_PID=""
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
log_test() { echo -e "${CYAN}[TEST]${NC} $1"; }
|
||||
|
||||
# Test users: username -> malicious display name
|
||||
# These simulate real-world attack payloads from the security advisory
|
||||
declare -A MALICIOUS_USERS
|
||||
MALICIOUS_USERS=(
|
||||
["htmlscript"]='<script>alert("xss")</script>'
|
||||
["htmliframe"]='<iframe src="https://evil.com"></iframe>'
|
||||
["htmlimg"]='<img src="https://evil.com/track.png">'
|
||||
["htmlform"]='<form action="https://evil.com"><input type="password"></form>'
|
||||
["htmlmeta"]='<meta http-equiv="refresh" content="0;url=https://evil.com">'
|
||||
["htmlstyle"]='<style>body{display:none}</style>Visible'
|
||||
["mdimage"]=''
|
||||
["mdlink"]='[Click me](https://evil.com)'
|
||||
["mixedhtml"]='Alice <script>alert(1)</script> Bob'
|
||||
["cleanuser"]='Legitimate User'
|
||||
)
|
||||
|
||||
# Patterns that must NOT appear in stored chat message bodies
|
||||
FORBIDDEN_PATTERNS=(
|
||||
'<script'
|
||||
'<iframe'
|
||||
'<img'
|
||||
'<form'
|
||||
'<meta'
|
||||
'<style'
|
||||
'<input'
|
||||
'src="https://evil'
|
||||
'action="https://evil'
|
||||
'onerror='
|
||||
'onload='
|
||||
)
|
||||
|
||||
kill_leftover_processes() {
|
||||
local killed=false
|
||||
if pkill -f "snac httpd /tmp" 2>/dev/null; then killed=true; fi
|
||||
if pkill -f "local-proxy.js" 2>/dev/null; then killed=true; fi
|
||||
|
||||
local proxy_pid
|
||||
proxy_pid=$(lsof -ti :"${PROXY_PORT}" 2>/dev/null) || true
|
||||
if [[ -n "${proxy_pid}" ]]; then
|
||||
kill "${proxy_pid}" 2>/dev/null || true
|
||||
killed=true
|
||||
fi
|
||||
|
||||
local snac_pid
|
||||
snac_pid=$(lsof -ti :"${SNAC_PORT}" 2>/dev/null) || true
|
||||
if [[ -n "${snac_pid}" ]]; then
|
||||
kill "${snac_pid}" 2>/dev/null || true
|
||||
killed=true
|
||||
fi
|
||||
|
||||
if [[ "${killed}" == "true" ]]; then
|
||||
log_info "Killed leftover processes from previous run"
|
||||
sleep 1
|
||||
fi
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
log_info "Cleaning up..."
|
||||
if [[ -n "${PROXY_PID}" ]] && kill -0 "${PROXY_PID}" 2>/dev/null; then
|
||||
kill "${PROXY_PID}" 2>/dev/null || true
|
||||
wait "${PROXY_PID}" 2>/dev/null || true
|
||||
fi
|
||||
if [[ -n "${OWNCAST_PID}" ]] && kill -0 "${OWNCAST_PID}" 2>/dev/null; then
|
||||
kill "${OWNCAST_PID}" 2>/dev/null || true
|
||||
wait "${OWNCAST_PID}" 2>/dev/null || true
|
||||
fi
|
||||
if [[ -n "${SNAC_PID}" ]] && kill -0 "${SNAC_PID}" 2>/dev/null; then
|
||||
kill "${SNAC_PID}" 2>/dev/null || true
|
||||
wait "${SNAC_PID}" 2>/dev/null || true
|
||||
fi
|
||||
if [[ -n "${TEMP_DIR}" ]] && [[ -d "${TEMP_DIR}" ]]; then
|
||||
rm -rf "${TEMP_DIR}"
|
||||
fi
|
||||
log_info "Cleanup complete."
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
setup_temp_dir() {
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
SNAC_DATA_DIR="${TEMP_DIR}/snac-data"
|
||||
OWNCAST_DB="${TEMP_DIR}/owncast.db"
|
||||
log_info "Temp directory: ${TEMP_DIR}"
|
||||
}
|
||||
|
||||
check_hosts_entry() {
|
||||
if ! grep -q "${OWNCAST_HOSTNAME}" /etc/hosts 2>/dev/null || ! grep -q "${SNAC_HOSTNAME}" /etc/hosts 2>/dev/null; then
|
||||
log_warn "Required /etc/hosts entries not found."
|
||||
log_warn "Please add: 127.0.0.1 ${OWNCAST_HOSTNAME} ${SNAC_HOSTNAME}"
|
||||
exit 1
|
||||
fi
|
||||
log_info "Hosts entries verified"
|
||||
}
|
||||
|
||||
install_snac2() {
|
||||
log_info "Setting up snac2..."
|
||||
if command -v snac &> /dev/null; then
|
||||
SNAC_BIN=$(command -v snac)
|
||||
log_info "Using system snac2: ${SNAC_BIN}"
|
||||
return
|
||||
fi
|
||||
|
||||
local snac_src="${TEMP_DIR}/snac2-src"
|
||||
log_info "Cloning snac2..."
|
||||
git clone --depth 1 https://codeberg.org/grunfink/snac2.git "${snac_src}" 2>/dev/null
|
||||
log_info "Building snac2..."
|
||||
pushd "${snac_src}" > /dev/null
|
||||
make
|
||||
SNAC_BIN="${snac_src}/snac"
|
||||
popd > /dev/null
|
||||
log_info "snac2 built: ${SNAC_BIN}"
|
||||
}
|
||||
|
||||
check_certs() {
|
||||
# CERT_DIR may be set by the Docker entrypoint; fall back to local certs/
|
||||
CERT_DIR="${CERT_DIR:-${SCRIPT_DIR}/certs}"
|
||||
if [[ ! -f "${CERT_DIR}/cert.pem" ]] || [[ ! -f "${CERT_DIR}/key.pem" ]]; then
|
||||
log_error "Certificates not found in ${CERT_DIR}. See README.md for setup."
|
||||
exit 1
|
||||
fi
|
||||
log_info "Using certificates from ${CERT_DIR}"
|
||||
}
|
||||
|
||||
init_snac2() {
|
||||
log_info "Initializing snac2..."
|
||||
local snac_host_port="${SNAC_HOSTNAME}:${PROXY_PORT}"
|
||||
printf "127.0.0.1\n%s\n%s\n\ntest@test.local\n" "${SNAC_PORT}" "${snac_host_port}" | \
|
||||
"${SNAC_BIN}" init "${SNAC_DATA_DIR}" > /dev/null 2>&1
|
||||
log_info "snac2 initialized"
|
||||
}
|
||||
|
||||
create_malicious_users() {
|
||||
log_info "Creating snac2 users with malicious display names..."
|
||||
|
||||
local run_id
|
||||
run_id=$(date +%s%N | sha256sum | head -c 8)
|
||||
|
||||
local created=0
|
||||
for username in "${!MALICIOUS_USERS[@]}"; do
|
||||
local displayname="${MALICIOUS_USERS[$username]}"
|
||||
local full_username="${username}${run_id}"
|
||||
|
||||
if printf "%s\n%s\n" "${full_username}" "${displayname}" | "${SNAC_BIN}" adduser "${SNAC_DATA_DIR}" > /dev/null 2>&1; then
|
||||
# Update the key to include the run_id so we can find them later
|
||||
MALICIOUS_USERS["${username}"]="${displayname}"
|
||||
# Store the full username for follow requests
|
||||
SNAC_FULL_USERNAMES["${username}"]="${full_username}"
|
||||
created=$((created + 1))
|
||||
log_info " Created user '${full_username}' with display name: ${displayname}"
|
||||
else
|
||||
log_error " Failed to create user '${full_username}'"
|
||||
fi
|
||||
done
|
||||
|
||||
log_info "Created ${created} users with malicious display names"
|
||||
}
|
||||
|
||||
start_proxy() {
|
||||
log_info "Starting HTTPS reverse proxy (Caddy)..."
|
||||
if ! command -v caddy &> /dev/null; then
|
||||
log_error "Caddy is not installed."
|
||||
return 1
|
||||
fi
|
||||
|
||||
export PROXY_PORT OWNCAST_PORT SNAC_PORT
|
||||
export CERT_FILE="${CERT_DIR}/cert.pem"
|
||||
export KEY_FILE="${CERT_DIR}/key.pem"
|
||||
|
||||
local caddy_log="${TEMP_DIR}/caddy.log"
|
||||
caddy run --config "${SCRIPT_DIR}/Caddyfile" --adapter caddyfile > "${caddy_log}" 2>&1 &
|
||||
PROXY_PID=$!
|
||||
sleep 2
|
||||
|
||||
if ! kill -0 "${PROXY_PID}" 2>/dev/null; then
|
||||
log_error "Caddy failed to start"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local max_attempts=10
|
||||
local attempt=0
|
||||
while [[ ${attempt} -lt ${max_attempts} ]]; do
|
||||
if curl -sk "https://127.0.0.1:${PROXY_PORT}/" > /dev/null 2>&1; then
|
||||
log_info "Caddy proxy is ready"
|
||||
return 0
|
||||
fi
|
||||
attempt=$((attempt + 1))
|
||||
sleep 1
|
||||
done
|
||||
log_error "Caddy proxy did not become ready"
|
||||
return 1
|
||||
}
|
||||
|
||||
start_snac2() {
|
||||
log_info "Starting snac2 server..."
|
||||
local snac_log="${TEMP_DIR}/snac2.log"
|
||||
DEBUG=0 "${SNAC_BIN}" httpd "${SNAC_DATA_DIR}" > "${snac_log}" 2>&1 &
|
||||
SNAC_PID=$!
|
||||
|
||||
local max_attempts=30
|
||||
local attempt=0
|
||||
while [[ ${attempt} -lt ${max_attempts} ]]; do
|
||||
if curl -s "http://127.0.0.1:${SNAC_PORT}/" > /dev/null 2>&1; then
|
||||
log_info "snac2 is ready"
|
||||
return 0
|
||||
fi
|
||||
attempt=$((attempt + 1))
|
||||
sleep 1
|
||||
done
|
||||
log_error "snac2 did not become ready"
|
||||
return 1
|
||||
}
|
||||
|
||||
build_owncast() {
|
||||
log_info "Building Owncast..."
|
||||
pushd "${REPO_ROOT}" > /dev/null
|
||||
CGO_ENABLED=1 go build -o owncast main.go
|
||||
popd > /dev/null
|
||||
log_info "Owncast built"
|
||||
}
|
||||
|
||||
start_owncast() {
|
||||
log_info "Starting Owncast..."
|
||||
OWNCAST_ALLOW_INTERNAL_FEDERATION=true \
|
||||
OWNCAST_INSECURE_SKIP_VERIFY=true \
|
||||
"${REPO_ROOT}/owncast" -database "${OWNCAST_DB}" &
|
||||
OWNCAST_PID=$!
|
||||
|
||||
local max_attempts=30
|
||||
local attempt=0
|
||||
while [[ ${attempt} -lt ${max_attempts} ]]; do
|
||||
if curl -s "http://localhost:${OWNCAST_PORT}/api/status" > /dev/null 2>&1; then
|
||||
log_info "Owncast is ready"
|
||||
return 0
|
||||
fi
|
||||
attempt=$((attempt + 1))
|
||||
sleep 1
|
||||
done
|
||||
log_error "Owncast did not become ready"
|
||||
return 1
|
||||
}
|
||||
|
||||
configure_owncast() {
|
||||
log_info "Configuring Owncast..."
|
||||
local base_url="http://localhost:${OWNCAST_PORT}"
|
||||
local auth
|
||||
auth=$(echo -n "${ADMIN_USER}:${ADMIN_PASS}" | base64)
|
||||
|
||||
# Set server URL
|
||||
curl -s -X POST "${base_url}/api/admin/config/serverurl" \
|
||||
-H "Authorization: Basic ${auth}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"value\": \"${OWNCAST_URL}\"}" > /dev/null
|
||||
|
||||
# Set federation username
|
||||
curl -s -X POST "${base_url}/api/admin/config/federation/username" \
|
||||
-H "Authorization: Basic ${auth}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"value\": \"${FEDERATION_USERNAME}\"}" > /dev/null
|
||||
|
||||
# Enable federation
|
||||
curl -s -X POST "${base_url}/api/admin/config/federation/enable" \
|
||||
-H "Authorization: Basic ${auth}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"value": true}' > /dev/null
|
||||
|
||||
# Disable private mode
|
||||
curl -s -X POST "${base_url}/api/admin/config/federation/private" \
|
||||
-H "Authorization: Basic ${auth}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"value": false}' > /dev/null
|
||||
|
||||
# Enable engagement display in chat
|
||||
curl -s -X POST "${base_url}/api/admin/config/federation/showengagement" \
|
||||
-H "Authorization: Basic ${auth}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"value": true}' > /dev/null
|
||||
|
||||
log_info "Owncast configured (engagement display enabled)"
|
||||
}
|
||||
|
||||
send_follow_requests() {
|
||||
log_info "Sending follow requests from malicious users..."
|
||||
local owncast_actor="${OWNCAST_URL}/federation/user/${FEDERATION_USERNAME}"
|
||||
local successful=0
|
||||
|
||||
for username in "${!MALICIOUS_USERS[@]}"; do
|
||||
local full_username="${SNAC_FULL_USERNAMES[$username]}"
|
||||
local follow_output
|
||||
follow_output=$("${SNAC_BIN}" follow "${SNAC_DATA_DIR}" "${full_username}" "${owncast_actor}" 2>&1)
|
||||
local follow_exit=$?
|
||||
|
||||
if [[ ${follow_exit} -eq 0 ]] && [[ ! "${follow_output}" =~ "cannot" ]]; then
|
||||
successful=$((successful + 1))
|
||||
log_info " ${full_username} followed Owncast"
|
||||
else
|
||||
log_warn " ${full_username} follow failed: ${follow_output}"
|
||||
fi
|
||||
|
||||
sleep "${FOLLOW_DELAY}"
|
||||
done
|
||||
|
||||
log_test "${successful}/${#MALICIOUS_USERS[@]} follow requests sent"
|
||||
|
||||
# Wait for follow requests to be processed
|
||||
local user_count=${#MALICIOUS_USERS[@]}
|
||||
local wait_time=$((15 + user_count))
|
||||
log_info "Waiting ${wait_time}s for engagement events to be processed..."
|
||||
sleep "${wait_time}"
|
||||
}
|
||||
|
||||
verify_chat_sanitization() {
|
||||
log_info "Verifying chat message sanitization..."
|
||||
|
||||
local passed=true
|
||||
local tests_run=0
|
||||
local tests_passed=0
|
||||
|
||||
# Query fediverse engagement messages from the database
|
||||
local messages
|
||||
messages=$(sqlite3 "${OWNCAST_DB}" \
|
||||
"SELECT body FROM messages WHERE eventType IN ('FEDIVERSE_ENGAGEMENT_FOLLOW', 'FEDIVERSE_ENGAGEMENT_LIKE', 'FEDIVERSE_ENGAGEMENT_REPOST');" 2>/dev/null)
|
||||
|
||||
if [[ -z "${messages}" ]]; then
|
||||
log_error "No fediverse engagement messages found in database"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local message_count
|
||||
message_count=$(echo "${messages}" | wc -l | tr -d ' ')
|
||||
log_info "Found ${message_count} engagement messages in chat"
|
||||
|
||||
# Test 1: No forbidden HTML patterns in any message
|
||||
for pattern in "${FORBIDDEN_PATTERNS[@]}"; do
|
||||
tests_run=$((tests_run + 1))
|
||||
if echo "${messages}" | grep -qi "${pattern}"; then
|
||||
log_error "FAIL: Found forbidden pattern '${pattern}' in chat messages:"
|
||||
echo "${messages}" | grep -i "${pattern}" | while read -r line; do
|
||||
log_error " Body: ${line}"
|
||||
done
|
||||
passed=false
|
||||
else
|
||||
tests_passed=$((tests_passed + 1))
|
||||
log_test "PASS: No '${pattern}' found in messages"
|
||||
fi
|
||||
done
|
||||
|
||||
# Test 2: Verify the clean user's message is present
|
||||
# Note: snac2 may not serve the display name in actor objects, so we check
|
||||
# for either the display name or the username fallback.
|
||||
tests_run=$((tests_run + 1))
|
||||
local clean_username="${SNAC_FULL_USERNAMES[cleanuser]}"
|
||||
if echo "${messages}" | grep -q "Legitimate User"; then
|
||||
tests_passed=$((tests_passed + 1))
|
||||
log_test "PASS: Clean display name 'Legitimate User' preserved correctly"
|
||||
elif echo "${messages}" | grep -q "${clean_username}"; then
|
||||
tests_passed=$((tests_passed + 1))
|
||||
log_test "PASS: Clean user present via username fallback '${clean_username}'"
|
||||
else
|
||||
log_error "FAIL: Clean user message not found in chat"
|
||||
passed=false
|
||||
fi
|
||||
|
||||
# Test 3: Verify that messages with stripped HTML fell back to expected content
|
||||
# Messages from users whose display names were entirely HTML should show
|
||||
# the follow action text but not the HTML
|
||||
tests_run=$((tests_run + 1))
|
||||
if echo "${messages}" | grep -q "followed this stream"; then
|
||||
tests_passed=$((tests_passed + 1))
|
||||
log_test "PASS: Follow action text present in messages"
|
||||
else
|
||||
log_error "FAIL: No 'followed this stream' text found in any message"
|
||||
passed=false
|
||||
fi
|
||||
|
||||
# Test 4: No markdown image syntax rendered as HTML img tags
|
||||
tests_run=$((tests_run + 1))
|
||||
if echo "${messages}" | grep -qi '<img.*src=.*evil'; then
|
||||
log_error "FAIL: Markdown image syntax was rendered as HTML img tag"
|
||||
passed=false
|
||||
else
|
||||
tests_passed=$((tests_passed + 1))
|
||||
log_test "PASS: No markdown-rendered img tags found"
|
||||
fi
|
||||
|
||||
# Test 5: No markdown link syntax rendered as HTML anchor tags with evil URLs
|
||||
tests_run=$((tests_run + 1))
|
||||
if echo "${messages}" | grep -qi '<a.*href=.*evil'; then
|
||||
log_error "FAIL: Markdown link syntax was rendered as HTML anchor tag"
|
||||
passed=false
|
||||
else
|
||||
tests_passed=$((tests_passed + 1))
|
||||
log_test "PASS: No markdown-rendered anchor tags with evil URLs found"
|
||||
fi
|
||||
|
||||
# Print all stored messages for inspection
|
||||
echo ""
|
||||
log_info "All stored engagement messages:"
|
||||
echo "${messages}" | while read -r line; do
|
||||
log_info " ${line}"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo "Chat Sanitization Test Results"
|
||||
echo "========================================"
|
||||
echo "Tests run: ${tests_run}"
|
||||
echo "Tests passed: ${tests_passed}"
|
||||
echo "========================================"
|
||||
|
||||
if [[ "${passed}" == "true" ]]; then
|
||||
echo -e "${GREEN}TEST PASSED${NC}"
|
||||
return 0
|
||||
else
|
||||
echo -e "${RED}TEST FAILED${NC}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# We need an associative array for the full usernames
|
||||
declare -A SNAC_FULL_USERNAMES
|
||||
|
||||
main() {
|
||||
kill_leftover_processes
|
||||
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo "Chat Sanitization Test"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# Setup infrastructure
|
||||
setup_temp_dir
|
||||
check_hosts_entry
|
||||
install_snac2
|
||||
check_certs
|
||||
init_snac2
|
||||
create_malicious_users
|
||||
start_proxy
|
||||
start_snac2
|
||||
build_owncast
|
||||
start_owncast
|
||||
configure_owncast
|
||||
sleep 2
|
||||
|
||||
# Send follows from malicious users
|
||||
send_follow_requests
|
||||
|
||||
# Verify results
|
||||
if verify_chat_sanitization; then
|
||||
exit 0
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -47,6 +47,7 @@ OWNCAST_URL="https://${OWNCAST_HOSTNAME}:${PROXY_PORT}"
|
||||
TEMP_DIR=""
|
||||
SNAC_DATA_DIR=""
|
||||
SNAC_BIN=""
|
||||
OWNCAST_BIN=""
|
||||
OWNCAST_DB=""
|
||||
|
||||
# PIDs and state
|
||||
@@ -55,6 +56,7 @@ OWNCAST_PID=""
|
||||
PROXY_PID=""
|
||||
TEST_STREAM_PID=""
|
||||
SNAC_USERNAMES=()
|
||||
CONFIRMED_FOLLOWERS=()
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
@@ -182,8 +184,8 @@ install_snac2() {
|
||||
}
|
||||
|
||||
check_certs() {
|
||||
# Use pre-generated mkcert certificates from the script directory
|
||||
CERT_DIR="${SCRIPT_DIR}/certs"
|
||||
# CERT_DIR may be set by the Docker entrypoint; fall back to local certs/
|
||||
CERT_DIR="${CERT_DIR:-${SCRIPT_DIR}/certs}"
|
||||
|
||||
if [[ ! -f "${CERT_DIR}/cert.pem" ]] || [[ ! -f "${CERT_DIR}/key.pem" ]]; then
|
||||
log_error "Certificates not found in ${CERT_DIR}"
|
||||
@@ -390,11 +392,12 @@ verify_snac_shared_inbox() {
|
||||
build_owncast() {
|
||||
log_info "Building Owncast..."
|
||||
|
||||
OWNCAST_BIN="${TEMP_DIR}/owncast"
|
||||
pushd "${REPO_ROOT}" > /dev/null
|
||||
CGO_ENABLED=1 go build -o owncast main.go
|
||||
CGO_ENABLED=1 go build -o "${OWNCAST_BIN}" main.go
|
||||
popd > /dev/null
|
||||
|
||||
log_info "Owncast built"
|
||||
log_info "Owncast built: ${OWNCAST_BIN}"
|
||||
}
|
||||
|
||||
start_owncast() {
|
||||
@@ -403,7 +406,7 @@ start_owncast() {
|
||||
# Start Owncast with test environment variables and debug flags
|
||||
OWNCAST_ALLOW_INTERNAL_FEDERATION=true \
|
||||
OWNCAST_INSECURE_SKIP_VERIFY=true \
|
||||
"${REPO_ROOT}/owncast" -database "${OWNCAST_DB}" &
|
||||
"${OWNCAST_BIN}" -database "${OWNCAST_DB}" &
|
||||
OWNCAST_PID=$!
|
||||
|
||||
log_info "Owncast started with PID ${OWNCAST_PID}"
|
||||
@@ -539,8 +542,8 @@ send_follow_requests() {
|
||||
log_info "snac2 queue has ${queue_count} pending items"
|
||||
|
||||
# Give snac2 background thread time to process all pending follows
|
||||
# Wait longer for more users
|
||||
local wait_time=$((10 + USER_COUNT / 10))
|
||||
# Each follow requires an HTTP round-trip; scale wait with user count
|
||||
local wait_time=$((10 + USER_COUNT / 5))
|
||||
log_info "Waiting ${wait_time}s for snac2 to process follow requests..."
|
||||
sleep "${wait_time}"
|
||||
}
|
||||
@@ -562,6 +565,28 @@ verify_followers() {
|
||||
echo "${count}"
|
||||
}
|
||||
|
||||
populate_confirmed_followers() {
|
||||
# Query Owncast API for actual registered followers and map their IRIs
|
||||
# back to snac2 usernames so we only check those inboxes for delivery.
|
||||
CONFIRMED_FOLLOWERS=()
|
||||
local auth
|
||||
auth=$(echo -n "${ADMIN_USER}:${ADMIN_PASS}" | base64)
|
||||
|
||||
local response
|
||||
response=$(curl -s "http://localhost:${OWNCAST_PORT}/api/admin/followers?limit=1000" \
|
||||
-H "Authorization: Basic ${auth}" 2>/dev/null || echo '{"results":[]}')
|
||||
|
||||
# Follower IRIs look like https://snac.local:8443/username
|
||||
while IFS= read -r iri; do
|
||||
if [[ -n "${iri}" ]]; then
|
||||
local username="${iri##*/}"
|
||||
CONFIRMED_FOLLOWERS+=("${username}")
|
||||
fi
|
||||
done < <(echo "${response}" | jq -r '.results[]?.link // empty' 2>/dev/null)
|
||||
|
||||
log_info "Confirmed ${#CONFIRMED_FOLLOWERS[@]} follower usernames from Owncast API"
|
||||
}
|
||||
|
||||
send_test_message() {
|
||||
log_info "Sending test message from Owncast..."
|
||||
|
||||
@@ -579,9 +604,16 @@ send_test_message() {
|
||||
}
|
||||
|
||||
check_snac_inboxes_count() {
|
||||
local users_with_messages=0
|
||||
# Only check users confirmed as followers by Owncast, not all snac2 users.
|
||||
# snac2's shared inbox distributes messages to all users it considers followers,
|
||||
# which may include users whose follow hasn't been registered by Owncast yet.
|
||||
local usernames_to_check=("${CONFIRMED_FOLLOWERS[@]}")
|
||||
if [[ ${#usernames_to_check[@]} -eq 0 ]]; then
|
||||
usernames_to_check=("${SNAC_USERNAMES[@]}")
|
||||
fi
|
||||
|
||||
for username in "${SNAC_USERNAMES[@]}"; do
|
||||
local users_with_messages=0
|
||||
for username in "${usernames_to_check[@]}"; do
|
||||
if user_has_message "${username}"; then
|
||||
users_with_messages=$((users_with_messages + 1))
|
||||
fi
|
||||
@@ -614,11 +646,16 @@ user_has_message() {
|
||||
check_snac_inboxes() {
|
||||
log_info "Checking snac2 user inboxes for delivered messages..." >&2
|
||||
|
||||
local usernames_to_check=("${CONFIRMED_FOLLOWERS[@]}")
|
||||
if [[ ${#usernames_to_check[@]} -eq 0 ]]; then
|
||||
usernames_to_check=("${SNAC_USERNAMES[@]}")
|
||||
fi
|
||||
|
||||
local users_with_messages=0
|
||||
local users_without_messages=()
|
||||
local total=${#SNAC_USERNAMES[@]}
|
||||
local total=${#usernames_to_check[@]}
|
||||
|
||||
for username in "${SNAC_USERNAMES[@]}"; do
|
||||
for username in "${usernames_to_check[@]}"; do
|
||||
if user_has_message "${username}"; then
|
||||
users_with_messages=$((users_with_messages + 1))
|
||||
else
|
||||
@@ -626,7 +663,7 @@ check_snac_inboxes() {
|
||||
fi
|
||||
done
|
||||
|
||||
log_test "${users_with_messages}/${total} users received the message" >&2
|
||||
log_test "${users_with_messages}/${total} confirmed followers received the message" >&2
|
||||
|
||||
if [[ ${#users_without_messages[@]} -gt 0 ]] && [[ ${#users_without_messages[@]} -le 10 ]]; then
|
||||
log_warn "Users missing messages: ${users_without_messages[*]}" >&2
|
||||
@@ -847,7 +884,7 @@ main() {
|
||||
|
||||
# Wait for all followers to be registered (with timeout)
|
||||
local followers=0
|
||||
local max_wait=60 # Maximum seconds to wait for followers
|
||||
local max_wait=$((60 + USER_COUNT)) # Scale timeout with user count
|
||||
local waited=0
|
||||
local check_interval=2
|
||||
|
||||
@@ -889,6 +926,7 @@ main() {
|
||||
echo "----------------------------------------"
|
||||
echo "STEP 4: Owncast sends message to followers"
|
||||
echo "----------------------------------------"
|
||||
populate_confirmed_followers
|
||||
send_test_message
|
||||
echo ""
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ TEST_START_TIME=""
|
||||
|
||||
# Directories
|
||||
TEMP_DIR=""
|
||||
OWNCAST_BIN=""
|
||||
OWNCAST_DB=""
|
||||
|
||||
# PIDs
|
||||
@@ -151,11 +152,12 @@ setup_temp_dir() {
|
||||
build_owncast() {
|
||||
log_info "Building Owncast..."
|
||||
|
||||
OWNCAST_BIN="${TEMP_DIR}/owncast"
|
||||
pushd "${REPO_ROOT}" > /dev/null
|
||||
CGO_ENABLED=1 go build -o owncast main.go
|
||||
CGO_ENABLED=1 go build -o "${OWNCAST_BIN}" main.go
|
||||
popd > /dev/null
|
||||
|
||||
log_info "Owncast built"
|
||||
log_info "Owncast built: ${OWNCAST_BIN}"
|
||||
}
|
||||
|
||||
start_owncast() {
|
||||
@@ -164,7 +166,7 @@ start_owncast() {
|
||||
# Start Owncast with test configuration
|
||||
OWNCAST_ALLOW_INTERNAL_FEDERATION=true \
|
||||
OWNCAST_INSECURE_SKIP_VERIFY=true \
|
||||
"${REPO_ROOT}/owncast" \
|
||||
"${OWNCAST_BIN}" \
|
||||
-database "${OWNCAST_DB}" \
|
||||
-followervalidationinterval "${VALIDATION_INTERVAL}" \
|
||||
-enableVerboseLogging &
|
||||
|
||||
Generated
+4
-3
@@ -3621,10 +3621,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
|
||||
Generated
+6
-6
@@ -3036,9 +3036,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
@@ -5903,9 +5903,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"dev": true
|
||||
},
|
||||
"pirates": {
|
||||
|
||||
Generated
+19
-36
@@ -5732,9 +5732,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
|
||||
"integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
@@ -7309,9 +7309,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/glob/node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
|
||||
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
@@ -8071,9 +8071,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/matcher-collection/node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
|
||||
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
@@ -8620,9 +8620,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
@@ -9149,35 +9149,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-parser": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
||||
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
||||
"version": "4.2.6",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz",
|
||||
"integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1"
|
||||
"debug": "~4.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-parser/node_modules/debug": {
|
||||
"version": "4.3.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/socketio-wildcard": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/socketio-wildcard/-/socketio-wildcard-2.0.0.tgz",
|
||||
@@ -9417,9 +9400,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby/node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
|
||||
+5
-5
@@ -13,7 +13,7 @@ require (
|
||||
require (
|
||||
4d63.com/gocheckcompilerdirectives v1.3.0 // indirect
|
||||
4d63.com/gochecknoglobals v0.2.2 // indirect
|
||||
cel.dev/expr v0.24.0 // indirect
|
||||
cel.dev/expr v0.25.1 // indirect
|
||||
codeberg.org/chavacava/garif v0.2.0 // indirect
|
||||
codeberg.org/polyfloyd/go-errorlint v1.9.0 // indirect
|
||||
dev.gaijin.team/go/exhaustruct/v4 v4.0.0 // indirect
|
||||
@@ -277,10 +277,10 @@ require (
|
||||
golang.org/x/term v0.41.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
golang.org/x/tools v0.43.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c // indirect
|
||||
google.golang.org/grpc v1.75.0 // indirect
|
||||
google.golang.org/protobuf v1.36.8 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
google.golang.org/grpc v1.79.3 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
|
||||
+22
-22
@@ -2,8 +2,8 @@
|
||||
4d63.com/gocheckcompilerdirectives v1.3.0/go.mod h1:ofsJ4zx2QAuIP/NO/NAh1ig6R1Fb18/GI7RVMwz7kAY=
|
||||
4d63.com/gochecknoglobals v0.2.2 h1:H1vdnwnMaZdQW/N+NrkT1SZMTBmcwHe9Vq8lJcYYTtU=
|
||||
4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0=
|
||||
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
|
||||
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
|
||||
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
|
||||
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
@@ -789,18 +789,18 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
|
||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
@@ -1138,10 +1138,10 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc
|
||||
google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7 h1:FiusG7LWj+4byqhbvmB+Q93B/mOxJLN2DTozDuZm4EU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:kXqgZtrWaf6qS3jZOCnCH7WYfrvFjkC51bM8fz3RsCA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c h1:qXWI/sQtv5UKboZ/zUk7h+mrf/lXORyI+n9DKDAusdg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
@@ -1154,8 +1154,8 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa
|
||||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
|
||||
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
|
||||
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
@@ -1169,8 +1169,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -2,4 +2,70 @@
|
||||
|
||||
.player {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
:global(.vjs-control-bar .vjs-control:hover .vjs-control-text) {
|
||||
/* This ensures the text isn't hidden by default VJS accessibility rules */
|
||||
display: block !important;
|
||||
visibility: visible !important;
|
||||
width: auto !important;
|
||||
height: auto !important;
|
||||
/* stylelint-disable-next-line property-no-deprecated */
|
||||
clip: auto !important; /* necessary to make the control text visible */
|
||||
margin: 0 !important;
|
||||
|
||||
/* Base tooltip styles */
|
||||
position: absolute;
|
||||
bottom: 45px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
white-space: nowrap;
|
||||
z-index: 1000;
|
||||
padding: 5px 10px;
|
||||
background: rgb(20 20 20 / 90%);
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
pointer-events: none; /* Prevents tooltip from flickering when mouse hits it */
|
||||
|
||||
/* If there's no text content, it stays invisible */
|
||||
&:empty {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Prevent the "Mute"/"Unmute" tooltip from showing when interacting with the volume slider.
|
||||
* Since the slider is part of the volume panel container, hovering it technically
|
||||
* counts as hovering the mute control context. We force-hide the tooltip
|
||||
* unless the cursor is explicitly over the mute button icon itself.
|
||||
*/
|
||||
:global(.vjs-volume-panel:hover .vjs-mute-control:not(:hover) .vjs-control-text) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Move the first and last element slightly right and left to prevent them from overflowing respectively */
|
||||
:global(.vjs-control:first-child:hover .vjs-control-text) {
|
||||
left: 0;
|
||||
transform: translateX(6px);
|
||||
|
||||
&::after {
|
||||
left: 15%;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.vjs-control:last-child:hover .vjs-control-text) {
|
||||
left: auto;
|
||||
right: 0;
|
||||
transform: translateX(-6px);
|
||||
|
||||
&::after {
|
||||
left: auto;
|
||||
right: 15%;
|
||||
transform: translateX(50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
import React, { FC } from 'react';
|
||||
import videojs from 'video.js';
|
||||
import type VideoJsPlayer from 'video.js/dist/types/player';
|
||||
import { useTranslation } from 'next-export-i18n';
|
||||
|
||||
import styles from './VideoJS.module.scss';
|
||||
|
||||
require('video.js/dist/video-js.css');
|
||||
|
||||
const SHORTCUT_SUFFIXES: Record<string, string> = {
|
||||
Play: ' (Space)',
|
||||
Pause: ' (Space)',
|
||||
Mute: ' (m)',
|
||||
Unmute: ' (m)',
|
||||
Fullscreen: ' (f)',
|
||||
'Non-Fullscreen': ' (f)',
|
||||
'Picture-in-Picture': ' (i)',
|
||||
'Exit Picture-in-Picture': ' (i)',
|
||||
};
|
||||
|
||||
export type VideoJSProps = {
|
||||
options: any;
|
||||
onReady: (player: VideoJsPlayer, vjsInstance: typeof videojs) => void;
|
||||
@@ -14,14 +26,30 @@ export type VideoJSProps = {
|
||||
export const VideoJS: FC<VideoJSProps> = ({ options, onReady }) => {
|
||||
const videoRef = React.useRef<HTMLVideoElement | null>(null);
|
||||
const playerRef = React.useRef<VideoJsPlayer | null>(null);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const addShortcutsToLanguage = (vjs: typeof videojs, langCode: string) => {
|
||||
const updates: Record<string, string> = {};
|
||||
Object.keys(SHORTCUT_SUFFIXES).forEach(key => {
|
||||
const currentLabel = key;
|
||||
const suffix = SHORTCUT_SUFFIXES[key];
|
||||
updates[key] = t(`${currentLabel}${suffix}`);
|
||||
});
|
||||
vjs.addLanguage(langCode, updates);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
// Make sure Video.js player is only initialized once
|
||||
if (!playerRef.current) {
|
||||
const videoElement = videoRef.current;
|
||||
|
||||
addShortcutsToLanguage(videojs, 'en');
|
||||
const finalOptions = {
|
||||
...options,
|
||||
noUITitleAttributes: true, // Prevents videojs from adding a title attribute to UI elements, thus preventing "double tooltips".
|
||||
};
|
||||
// eslint-disable-next-line no-multi-assign
|
||||
const player: VideoJsPlayer = (playerRef.current = videojs(videoElement, options, () => {
|
||||
const player: VideoJsPlayer = (playerRef.current = videojs(videoElement, finalOptions, () => {
|
||||
console.debug('player is ready');
|
||||
return onReady && onReady(player, videojs);
|
||||
}));
|
||||
|
||||
@@ -221,7 +221,8 @@
|
||||
"offlineBasic": "هذا البث غير متصل. تحقق قريباً!",
|
||||
"offlineFediverseOnly": "هذا البث غير متصل. <span class='follow-link'>تابع</span> {{fediverseAccount}} على فيديفيرس لرؤية المرة القادمة التي يذهب فيها {{streamer}} للحياة.",
|
||||
"offlineNotifyAndFediverse": "هذا البث غير متصل بالإنترنت. يمكنك أن تتلقى <span class='notify-link'>إشعارًا</span> في المرة القادمة {{streamer}} أو <span class='follow-link'>متابعة</span> {{fediverseAccount}} على موقع Fediverse.",
|
||||
"offlineNotifyOnly": "هذا البث غير متصل بالإنترنت. <span class='notify-link'>كن على علم</span> في المرة القادمة التي يتم فيها البث المباشر {{streamer}} ."
|
||||
"offlineNotifyOnly": "هذا البث غير متصل بالإنترنت. <span class='notify-link'>كن على علم</span> في المرة القادمة التي يتم فيها البث المباشر {{streamer}} .",
|
||||
"chatEmbedTitle": "محادثة {{name}}"
|
||||
},
|
||||
"Testing": {
|
||||
"itemCount": "لديك عناصر {{count}}",
|
||||
|
||||
@@ -221,7 +221,8 @@
|
||||
"offlineBasic": "এই স্ট্রিম অফলাইন। কিছুক্ষণ পরে চেক করুন!",
|
||||
"offlineFediverseOnly": "এই স্ট্রিম অফলাইন। <span class='follow-link'>ফলো করুন</span> {{fediverseAccount}} কে Fediverse এ পরবর্তী সময়ের জন্য যখন {{streamer}} লাইভ হবে তা দেখার জন্য।",
|
||||
"offlineNotifyAndFediverse": "এই স্ট্রিম অফলাইন। আপনি <span class='notify-link'>বিজ্ঞপ্তি গ্রহণ করতে পারবেন</span> পরবর্তী সময় {{streamer}} লাইভ হলে অথবা <span class='follow-link'>ফলো করতে পারবেন</span> {{fediverseAccount}} কে Fediverse এ।",
|
||||
"offlineNotifyOnly": "এই স্ট্রিম অফলাইন। <span class='notify-link'>বিজ্ঞপ্তি পান</span> পরবর্তী সময় {{streamer}} লাইভ হলে।"
|
||||
"offlineNotifyOnly": "এই স্ট্রিম অফলাইন। <span class='notify-link'>বিজ্ঞপ্তি পান</span> পরবর্তী সময় {{streamer}} লাইভ হলে।",
|
||||
"chatEmbedTitle": "{{name}} চ্যাট"
|
||||
},
|
||||
"Testing": {
|
||||
"itemCount": "আপনার {{count}} টি আইটেম রয়েছে",
|
||||
|
||||
@@ -221,7 +221,8 @@
|
||||
"offlineBasic": "Dieser Stream ist offline. Schauen Sie bald wieder!",
|
||||
"offlineFediverseOnly": "Dieser Stream ist offline. <span class='follow-link'>Folgen Sie</span> {{fediverseAccount}} auf der Feier, um das nächste Mal {{streamer}} live zu sehen.",
|
||||
"offlineNotifyAndFediverse": "Dieser Stream ist offline. Sie können <span class='notify-link'>benachrichtigt werden,</span> wenn {{streamer}} das nächste Mal live geht, oder <span class='follow-link'>folgen Sie</span> {{fediverseAccount}} auf dem Fediverse.",
|
||||
"offlineNotifyOnly": "Dieser Stream ist offline. <span class='notify-link'>Werde benachrichtigt</span> wenn {{streamer}} das nächste Mal live geht."
|
||||
"offlineNotifyOnly": "Dieser Stream ist offline. <span class='notify-link'>Werde benachrichtigt</span> wenn {{streamer}} das nächste Mal live geht.",
|
||||
"chatEmbedTitle": "{{name}} Chat"
|
||||
},
|
||||
"Testing": {
|
||||
"itemCount": "Sie haben {{count}} Elemente",
|
||||
|
||||
@@ -221,7 +221,8 @@
|
||||
"offlineBasic": "Αυτό το ρεύμα είναι εκτός σύνδεσης. Ελέγξτε ξανά σύντομα!",
|
||||
"offlineFediverseOnly": "Αυτό το ρεύμα είναι εκτός σύνδεσης. <span class='follow-link'>Ακολουθήστε</span> {{fediverseAccount}} στο Fediverse για να δείτε την επόμενη φορά που το {{streamer}} ζωντανά.",
|
||||
"offlineNotifyAndFediverse": "Αυτή η ροή είναι εκτός σύνδεσης. Μπορείτε να ειδοποιηθείτε <span class='notify-link'></span> την επόμενη φορά που το {{streamer}} πηγαίνει ζωντανά ή <span class='follow-link'>ακολουθήστε το</span> {{fediverseAccount}} στο Fediverse.",
|
||||
"offlineNotifyOnly": "Αυτή η ροή είναι εκτός σύνδεσης. <span class='notify-link'>Θα ειδοποιηθείτε</span> την επόμενη φορά που το {{streamer}} θα συνδεθεί."
|
||||
"offlineNotifyOnly": "Αυτή η ροή είναι εκτός σύνδεσης. <span class='notify-link'>Θα ειδοποιηθείτε</span> την επόμενη φορά που το {{streamer}} θα συνδεθεί.",
|
||||
"chatEmbedTitle": "{{name}} Συζήτηση"
|
||||
},
|
||||
"Testing": {
|
||||
"itemCount": "Έχετε {{count}} αντικείμενα",
|
||||
|
||||
@@ -221,7 +221,8 @@
|
||||
"offlineBasic": "This stream is offline. Check back soon!",
|
||||
"offlineFediverseOnly": "This stream is offline. <span class='follow-link'>Follow</span> {{fediverseAccount}} on the Fediverse to see the next time {{streamer}} goes live.",
|
||||
"offlineNotifyAndFediverse": "This stream is offline. You can <span class='notify-link'>be notified</span> the next time {{streamer}} goes live or <span class='follow-link'>follow</span> {{fediverseAccount}} on the Fediverse.",
|
||||
"offlineNotifyOnly": "This stream is offline. <span class='notify-link'>Be notified</span> the next time {{streamer}} goes live."
|
||||
"offlineNotifyOnly": "This stream is offline. <span class='notify-link'>Be notified</span> the next time {{streamer}} goes live.",
|
||||
"chatEmbedTitle": "{{name}} Chat"
|
||||
},
|
||||
"Testing": {
|
||||
"itemCount": "You have {{count}} items",
|
||||
|
||||
@@ -221,7 +221,8 @@
|
||||
"offlineBasic": "Esta corriente está fuera de línea. ¡Vuelve pronto!",
|
||||
"offlineFediverseOnly": "Este stream está fuera de línea. <span class='follow-link'>Sigue</span> {{fediverseAccount}} en el Fediverso para ver la próxima vez que {{streamer}} se ponga en directo.",
|
||||
"offlineNotifyAndFediverse": "Esta transmisión está desconectada. Puede <span class='notify-link'>recibir una notificación</span> la próxima vez que {{streamer}} salga en directo o <span class='follow-link'>seguir</span> {{fediverseAccount}} en Fediverse.",
|
||||
"offlineNotifyOnly": "Este stream está desconectado. <span class='notify-link'>Ser notificado</span> la próxima vez que {{streamer}} vaya en directo."
|
||||
"offlineNotifyOnly": "Este stream está desconectado. <span class='notify-link'>Ser notificado</span> la próxima vez que {{streamer}} vaya en directo.",
|
||||
"chatEmbedTitle": "Chat {{name}}"
|
||||
},
|
||||
"Testing": {
|
||||
"itemCount": "Tienes {{count}} elementos",
|
||||
|
||||
@@ -221,7 +221,8 @@
|
||||
"offlineBasic": "Torrente hau offline dago. Itzuli laster!",
|
||||
"offlineFediverseOnly": "Torrente hau offline dago. <span class='follow-link'>Jarraitzaile</span> {{fediverseAccount}} Fediversen hurrengoa {{streamer}} zuzenean joan denean ikusteko.",
|
||||
"offlineNotifyAndFediverse": "Torrente hau offline dago. <span class='notify-link'>Jakinarazi</span> hurrengoa {{streamer}} zuzenean joan denean edo <span class='follow-link'>jarraitu</span> {{fediverseAccount}} Fediversen.",
|
||||
"offlineNotifyOnly": "Torrente hau offline dago. <span class='notify-link'>Jakinarazi</span> hurrengoa {{streamer}} zuzenean joan denean."
|
||||
"offlineNotifyOnly": "Torrente hau offline dago. <span class='notify-link'>Jakinarazi</span> hurrengoa {{streamer}} zuzenean joan denean.",
|
||||
"chatEmbedTitle": "{{name}} Txata"
|
||||
},
|
||||
"Testing": {
|
||||
"itemCount": "{{count}} elementu dituzu",
|
||||
|
||||
@@ -221,7 +221,8 @@
|
||||
"offlineBasic": "Ce flux est hors ligne. Revenez bientôt !",
|
||||
"offlineFediverseOnly": "Ce flux est hors ligne. <span class='follow-link'>Suivez</span> {{fediverseAccount}} sur le Fédiverse pour voir la prochaine fois que {{streamer}} sera en live.",
|
||||
"offlineNotifyAndFediverse": "Ce flux est hors ligne. Vous pouvez <span class='notify-link'>être notifié</span> la prochaine fois que {{streamer}} sera en direct ou <span class='follow-link'>suivre</span> {{fediverseAccount}} sur le Fediverse.",
|
||||
"offlineNotifyOnly": "Ce flux est hors ligne. <span class='notify-link'>Être notifié</span> la prochaine fois que {{streamer}} sera en ligne."
|
||||
"offlineNotifyOnly": "Ce flux est hors ligne. <span class='notify-link'>Être notifié</span> la prochaine fois que {{streamer}} sera en ligne.",
|
||||
"chatEmbedTitle": "Chat {{name}}"
|
||||
},
|
||||
"Testing": {
|
||||
"itemCount": "Vous avez des objets {{count}}",
|
||||
|
||||
@@ -221,7 +221,8 @@
|
||||
"offlineBasic": "Tá an sruth seo as líne. Seiceáil arís go luath!",
|
||||
"offlineFediverseOnly": "Tá an sruth seo as líne. <span class='follow-link'>Lean</span> {{fediverseAccount}} ar an Fediverse chun a fheiceáil an uair a bhíonn {{streamer}} ar líne.",
|
||||
"offlineNotifyAndFediverse": "Tá an sruth seo as líne. Is féidir leat <span class='notify-link'>a bheith rabhaidh</span> an uair a bhíonn {{streamer}} ar líne nó <span class='follow-link'>lean</span> {{fediverseAccount}} ar an Fediverse.",
|
||||
"offlineNotifyOnly": "Tá an sruth seo as líne. <span class='notify-link'>Bí rabhaidh</span> an uair a bhíonn {{streamer}} ar líne."
|
||||
"offlineNotifyOnly": "Tá an sruth seo as líne. <span class='notify-link'>Bí rabhaidh</span> an uair a bhíonn {{streamer}} ar líne.",
|
||||
"chatEmbedTitle": "{{name}} Comhrá"
|
||||
},
|
||||
"Testing": {
|
||||
"itemCount": "Tá {{count}} réadanna agat",
|
||||
|
||||
@@ -221,7 +221,8 @@
|
||||
"offlineBasic": "यह स्ट्रीम ऑफ़लाइन है। कृपया जल्द ही वापस आएं!",
|
||||
"offlineFediverseOnly": "यह स्ट्रीम ऑफ़लाइन है। <span class='follow-link'>अनुसरण करें</span> {{fediverseAccount}} Fediverse पर अगली बार देखने के लिए जब {{streamer}} लाइव जाएं।",
|
||||
"offlineNotifyAndFediverse": "यह स्ट्रीम ऑफ़लाइन है। आप अगले बार जब {{streamer}} लाइव जाएं तो <span class='notify-link'>सूचित</span> हो सकते हैं या <span class='follow-link'>अनुसरण</span> कर सकते हैं {{fediverseAccount}} को Fediverse पर।",
|
||||
"offlineNotifyOnly": "यह स्ट्रीम ऑफ़लाइन है। <span class='notify-link'>सूचित हों</span> अगली बार जब {{streamer}} लाइव जाएं।"
|
||||
"offlineNotifyOnly": "यह स्ट्रीम ऑफ़लाइन है। <span class='notify-link'>सूचित हों</span> अगली बार जब {{streamer}} लाइव जाएं।",
|
||||
"chatEmbedTitle": "{{name}} चैट"
|
||||
},
|
||||
"Testing": {
|
||||
"itemCount": "आपके पास {{count}} आइटम हैं",
|
||||
|
||||
@@ -221,7 +221,8 @@
|
||||
"offlineBasic": "Ovaj stream je offline. Provjerite ponovno uskoro!",
|
||||
"offlineFediverseOnly": "Ovaj stream je offline. <span class='follow-link'>Pratite</span> {{fediverseAccount}} na Fediversu kako biste vidjeli kada {{streamer}} ponovno ide uživo.",
|
||||
"offlineNotifyAndFediverse": "Ovaj stream je offline. Možete <span class='notify-link'>biti obaviješteni</span> kada {{streamer}} ponovno ide uživo ili <span class='follow-link'>pratiti</span> {{fediverseAccount}} na Fediversu.",
|
||||
"offlineNotifyOnly": "Ovaj stream je offline. <span class='notify-link'>Budite obaviješteni</span> kada {{streamer}} ponovno ide uživo."
|
||||
"offlineNotifyOnly": "Ovaj stream je offline. <span class='notify-link'>Budite obaviješteni</span> kada {{streamer}} ponovno ide uživo.",
|
||||
"chatEmbedTitle": "{{name}} Čavrljanje"
|
||||
},
|
||||
"Testing": {
|
||||
"itemCount": "Imate {{count}} stavki",
|
||||
|
||||
@@ -221,7 +221,8 @@
|
||||
"offlineBasic": "Questo stream è offline. Riprova presto!",
|
||||
"offlineFediverseOnly": "Questo stream è offline. <span class='follow-link'>Segui</span> {{fediverseAccount}} sul Fediverse per vedere la prossima volta che {{streamer}} andrà in diretta.",
|
||||
"offlineNotifyAndFediverse": "Questo stream è offline. È possibile notificare <span class='notify-link'></span> la prossima volta che {{streamer}} va in diretta o <span class='follow-link'>seguire</span> {{fediverseAccount}} sul Fediverse.",
|
||||
"offlineNotifyOnly": "Questo stream è offline. <span class='notify-link'>Sii avvisato</span> la prossima volta che {{streamer}} andrà in diretta."
|
||||
"offlineNotifyOnly": "Questo stream è offline. <span class='notify-link'>Sii avvisato</span> la prossima volta che {{streamer}} andrà in diretta.",
|
||||
"chatEmbedTitle": "Chat {{name}}"
|
||||
},
|
||||
"Testing": {
|
||||
"itemCount": "Hai {{count}} oggetti",
|
||||
|
||||
@@ -221,7 +221,8 @@
|
||||
"offlineBasic": "このストリームはオフラインです。もう一度お試しください!",
|
||||
"offlineFediverseOnly": "このストリームはオフラインです。 <span class='follow-link'></span> {{fediverseAccount}} をFediverse でフォローして、次回 {{streamer}} がライブになるのを確認します。",
|
||||
"offlineNotifyAndFediverse": "このストリームはオフラインです。次回のライブ配信は {{streamer}} <span class='notify-link'>に通知さ</span>れるか、Fediverse で {{fediverseAccount}} を<span class='follow-link'>フォローして</span>ください。",
|
||||
"offlineNotifyOnly": "このストリームはオフラインです。 <span class='notify-link'></span> 次回 {{streamer}} がライブになるときに通知されます。"
|
||||
"offlineNotifyOnly": "このストリームはオフラインです。 <span class='notify-link'></span> 次回 {{streamer}} がライブになるときに通知されます。",
|
||||
"chatEmbedTitle": "{{name}} チャット"
|
||||
},
|
||||
"Testing": {
|
||||
"itemCount": "{{count}} 個のアイテムがあります",
|
||||
|
||||
@@ -221,7 +221,8 @@
|
||||
"offlineBasic": "이 스트림은 오프라인 상태입니다. 곧 다시 확인해주세요!",
|
||||
"offlineFediverseOnly": "이 스트림은 오프라인 상태입니다. 다음 번 {{streamer}} 라이브 방송을 보려면 Fediverse에서 {{fediverseAccount}} <span class='follow-link'>팔로우하세요</span>.",
|
||||
"offlineNotifyAndFediverse": "이 스트림은 오프라인 상태입니다. 다음 번에 {{streamer}} 가 생방송될 때 <span class='notify-link'>알림을</span> 받거나 페디버스에서 {{fediverseAccount}} 을 <span class='follow-link'>팔로우하세요</span>.",
|
||||
"offlineNotifyOnly": "이 스트림은 오프라인 상태입니다. 다음에 {{streamer}} 가 생방송될 때 <span class='notify-link'>알림을</span> 받으세요."
|
||||
"offlineNotifyOnly": "이 스트림은 오프라인 상태입니다. 다음에 {{streamer}} 가 생방송될 때 <span class='notify-link'>알림을</span> 받으세요.",
|
||||
"chatEmbedTitle": "{{name}} 채팅"
|
||||
},
|
||||
"Testing": {
|
||||
"itemCount": "귀하는 {{count}} 개 항목이 있습니다",
|
||||
|
||||
@@ -221,7 +221,8 @@
|
||||
"offlineBasic": "Saluran ini sedang offline. Periksa kembali sebentar lagi!",
|
||||
"offlineFediverseOnly": "Saluran ini sedang offline. <span class='follow-link'>Ikuti</span> {{fediverseAccount}} di Fediverse untuk melihat kali seterusnya {{streamer}} siaran langsung.",
|
||||
"offlineNotifyAndFediverse": "Saluran ini sedang offline. Anda boleh <span class='notify-link'>diberitahu</span> kali seterusnya {{streamer}} siaran langsung atau <span class='follow-link'>ikuti</span> {{fediverseAccount}} di Fediverse.",
|
||||
"offlineNotifyOnly": "Saluran ini sedang offline. <span class='notify-link'>Diberitahu</span> kali seterusnya {{streamer}} siaran langsung."
|
||||
"offlineNotifyOnly": "Saluran ini sedang offline. <span class='notify-link'>Diberitahu</span> kali seterusnya {{streamer}} siaran langsung.",
|
||||
"chatEmbedTitle": "{{name}} Sembang"
|
||||
},
|
||||
"Testing": {
|
||||
"itemCount": "Anda mempunyai {{count}} item",
|
||||
|
||||
@@ -221,7 +221,8 @@
|
||||
"offlineBasic": "Deze stream is offline. Kom snel terug!",
|
||||
"offlineFediverseOnly": "Deze stream is offline. <span class='follow-link'>Volg</span> {{fediverseAccount}} op de Fediverse om de volgende keer dat {{streamer}} live gaat te zien.",
|
||||
"offlineNotifyAndFediverse": "Deze stream is offline. U kunt <span class='notify-link'>een bericht ontvangen</span> wanneer {{streamer}} weer live gaat of {{fediverseAccount}} <span class='follow-link'>volgen</span> op de Fediverse.",
|
||||
"offlineNotifyOnly": "Deze stream is offline. <span class='notify-link'>Krijg een melding</span> de volgende keer dat {{streamer}} live gaat."
|
||||
"offlineNotifyOnly": "Deze stream is offline. <span class='notify-link'>Krijg een melding</span> de volgende keer dat {{streamer}} live gaat.",
|
||||
"chatEmbedTitle": "{{name}} chat"
|
||||
},
|
||||
"Testing": {
|
||||
"itemCount": "Je hebt {{count}} items",
|
||||
|
||||
@@ -221,7 +221,8 @@
|
||||
"offlineBasic": "Denne strømmen er frakoblet. Sjekk igjen snart!",
|
||||
"offlineFediverseOnly": "Denne strømmen er frakoblet. <span class='follow-link'>Følg</span> {{fediverseAccount}} på Fediverse for å se neste gang {{streamer}} går liv.",
|
||||
"offlineNotifyAndFediverse": "Denne strømmen er offline. Du kan <span class='notify-link'>bli varslet</span> neste gang {{streamer}} er live, eller <span class='follow-link'>følge</span> {{fediverseAccount}} på Fediverse.",
|
||||
"offlineNotifyOnly": "Denne strømmen er frakoblet. <span class='notify-link'>bli varslet</span> neste gang {{streamer}} går bor."
|
||||
"offlineNotifyOnly": "Denne strømmen er frakoblet. <span class='notify-link'>bli varslet</span> neste gang {{streamer}} går bor.",
|
||||
"chatEmbedTitle": "{{name}} Chat"
|
||||
},
|
||||
"Testing": {
|
||||
"itemCount": "Du har {{count}} objekter",
|
||||
|
||||
@@ -221,7 +221,8 @@
|
||||
"offlineBasic": "ਇਹ ਸਟ੍ਰੀਮ ਅਫਲਾਈਨ ਹੈ। ਜਲਦੀ ਹੀ ਵਾਪਸ ਆਓ!",
|
||||
"offlineFediverseOnly": "ਇਹ ਸਟ੍ਰੀਮ ਅਫਲਾਈਨ ਹੈ। ਜਅਦੋ {{streamer}} ਲਾਈਵ ਹੋਵੇਗਾ, ਗੇਰੀ ਮਨੱਤਾ ਤੋਂ <span class='follow-link'>ਜਾਣੋ</span>।",
|
||||
"offlineNotifyAndFediverse": "ਇਹ ਸਟ੍ਰੀਮ ਅਫਲਾਈਨ ਹੈ। ਜਦੋਂ {{streamer}} ਲਾਈਵ ਹੋਵੇਗਾ, ਤੁਸੀਂ <span class='notify-link'>ਜਾਣੋ</span> ਕਰ ਸਕਦੇ ਹੋ ਜਾਂ <span class='follow-link'>ਫਾਲੋ</span> {{fediverseAccount}} ਫੈਡੀਵਰਸ 'ਤੇ।",
|
||||
"offlineNotifyOnly": "ਇਹ ਸਟ੍ਰੀਮ ਅਫਲਾਈਨ ਹੈ। <span class='notify-link'>ਜਾਣੋ</span> ਜਦੋਂ {{streamer}} ਲਾਈਵ ਹੋਵੇਗਾ।"
|
||||
"offlineNotifyOnly": "ਇਹ ਸਟ੍ਰੀਮ ਅਫਲਾਈਨ ਹੈ। <span class='notify-link'>ਜਾਣੋ</span> ਜਦੋਂ {{streamer}} ਲਾਈਵ ਹੋਵੇਗਾ।",
|
||||
"chatEmbedTitle": "{{name}} ਚੈਟ"
|
||||
},
|
||||
"Testing": {
|
||||
"itemCount": "ਤੁਹਾਡੇ ਕੋਲ {{count}} ਆਈਟਮ ਹਨ",
|
||||
|
||||
@@ -221,7 +221,8 @@
|
||||
"offlineBasic": "Ten strumień jest offline. Sprawdź wkrótce!",
|
||||
"offlineFediverseOnly": "Ten strumień jest offline. <span class='follow-link'>Śledź</span> {{fediverseAccount}} na Fediwerse, aby zobaczyć kiedy następny {{streamer}} będzie na żywo.",
|
||||
"offlineNotifyAndFediverse": "Ten strumień jest offline. Możesz <span class='notify-link'>zostać powiadomiony</span> następnym razem, gdy {{streamer}} pojawi się lub <span class='follow-link'>podążaj za</span> {{fediverseAccount}} na Fediverse.",
|
||||
"offlineNotifyOnly": "Ta transmisja jest wyłączona. <span class='notify-link'>Otrzymaj powiadomienie</span>, gdy następnym razem strona {{streamer}} zostanie uruchomiona."
|
||||
"offlineNotifyOnly": "Ta transmisja jest wyłączona. <span class='notify-link'>Otrzymaj powiadomienie</span>, gdy następnym razem strona {{streamer}} zostanie uruchomiona.",
|
||||
"chatEmbedTitle": "Czat {{name}}"
|
||||
},
|
||||
"Testing": {
|
||||
"itemCount": "Masz {{count}} przedmiotów",
|
||||
|
||||
@@ -221,7 +221,8 @@
|
||||
"offlineBasic": "Esta transmissão está offline. Volte em breve!",
|
||||
"offlineFediverseOnly": "Esta transmissão está off-line. <span class='follow-link'>Siga</span> {{fediverseAccount}} no Fediverse para ver a próxima vez que {{streamer}} estiver ao vivo.",
|
||||
"offlineNotifyAndFediverse": "Esta transmissão está offline. <span class='notify-link'>Pode ser notificado</span> na próxima vez que {{streamer}} for ao ar ou <span class='follow-link'>siga</span> {{fediverseAccount}} no Fediverse.",
|
||||
"offlineNotifyOnly": "Esta transmissão está offline. U <span class='notify-link'>Seja notificado</span> na próxima vez que {{streamer}} for ao ar."
|
||||
"offlineNotifyOnly": "Esta transmissão está offline. U <span class='notify-link'>Seja notificado</span> na próxima vez que {{streamer}} for ao ar.",
|
||||
"chatEmbedTitle": "{{name}} Chat"
|
||||
},
|
||||
"Testing": {
|
||||
"itemCount": "Você tem {{count}} itens",
|
||||
|
||||
@@ -221,7 +221,8 @@
|
||||
"offlineBasic": "Этот поток оффлайн. Возвращайтесь в ближайшее время!",
|
||||
"offlineFediverseOnly": "Этот поток отключен. <span class='follow-link'>Следите за</span> {{fediverseAccount}} на Fediverse, чтобы узнать, когда {{streamer}} выйдет в эфир в следующий раз.",
|
||||
"offlineNotifyAndFediverse": "Этот поток отключен. Вы можете <span class='notify-link'>получить уведомление о</span> следующем выходе {{streamer}} в прямой эфир или <span class='follow-link'>следить за</span> {{fediverseAccount}} на Fediverse.",
|
||||
"offlineNotifyOnly": "Этот поток отключен. <span class='notify-link'>Получите уведомление</span>, когда {{streamer}} появится в следующий раз."
|
||||
"offlineNotifyOnly": "Этот поток отключен. <span class='notify-link'>Получите уведомление</span>, когда {{streamer}} появится в следующий раз.",
|
||||
"chatEmbedTitle": "{{name}} Чат"
|
||||
},
|
||||
"Testing": {
|
||||
"itemCount": "У вас есть {{count}} элементов",
|
||||
|
||||
@@ -221,7 +221,8 @@
|
||||
"offlineBasic": "Denna ström är offline. Kom tillbaka snart!",
|
||||
"offlineFediverseOnly": "Denna ström är offline. <span class='follow-link'>Följ</span> {{fediverseAccount}} på Fediverse för att se nästa gång {{streamer}} går live.",
|
||||
"offlineNotifyAndFediverse": "Denna ström är offline. Du kan <span class='notify-link'>meddelas</span> nästa gång {{streamer}} går live eller <span class='follow-link'>följ</span> {{fediverseAccount}} på Fediverse.",
|
||||
"offlineNotifyOnly": "Denna ström är offline. <span class='notify-link'>meddelas</span> nästa gång {{streamer}} går live."
|
||||
"offlineNotifyOnly": "Denna ström är offline. <span class='notify-link'>meddelas</span> nästa gång {{streamer}} går live.",
|
||||
"chatEmbedTitle": "{{name}} Chatt"
|
||||
},
|
||||
"Testing": {
|
||||
"itemCount": "Du har {{count}} objekt",
|
||||
|
||||
@@ -221,7 +221,8 @@
|
||||
"offlineBasic": "สตรีมนี้ออฟไลน์ กรุณาตรวจสอบอีกครั้งในภายหลัง!",
|
||||
"offlineFediverseOnly": "สตรีมนี้ออฟไลน์ <span class='follow-link'>ติดตาม</span> {{fediverseAccount}} บน Fediverse เพื่อดูเมื่อ {{streamer}} เริ่มถ่ายทอดสดครั้งต่อไป",
|
||||
"offlineNotifyAndFediverse": "สตรีมนี้ออฟไลน์ คุณสามารถ <span class='notify-link'>รับการแจ้งเตือน</span> เมื่อ {{streamer}} เริ่มถ่ายทอดสดอีกครั้งหรือ <span class='follow-link'>ติดตาม</span> {{fediverseAccount}} บน Fediverse",
|
||||
"offlineNotifyOnly": "สตรีมนี้ออฟไลน์ <span class='notify-link'>รับการแจ้งเตือน</span> เมื่อ {{streamer}} เริ่มถ่ายทอดสด"
|
||||
"offlineNotifyOnly": "สตรีมนี้ออฟไลน์ <span class='notify-link'>รับการแจ้งเตือน</span> เมื่อ {{streamer}} เริ่มถ่ายทอดสด",
|
||||
"chatEmbedTitle": "{{name}} แชนแนลแชท"
|
||||
},
|
||||
"Testing": {
|
||||
"itemCount": "คุณมี {{count}} รายการ",
|
||||
|
||||
@@ -221,7 +221,8 @@
|
||||
"offlineBasic": "Luồng này đang ngoại tuyến. Hãy quay lại sau!",
|
||||
"offlineFediverseOnly": "Luồng này đang ngoại tuyến. <span class='follow-link'>Theo dõi</span> {{fediverseAccount}} trên Fediverse để xem lần tới {{streamer}} trực tiếp.",
|
||||
"offlineNotifyAndFediverse": "Luồng này đang ngoại tuyến. Bạn có thể <span class='notify-link'>được thông báo</span> khi {{streamer}} trực tiếp hoặc <span class='follow-link'>theo dõi</span> {{fediverseAccount}} trên Fediverse.",
|
||||
"offlineNotifyOnly": "Luồng này đang ngoại tuyến. <span class='notify-link'>Được thông báo</span> khi {{streamer}} trực tiếp."
|
||||
"offlineNotifyOnly": "Luồng này đang ngoại tuyến. <span class='notify-link'>Được thông báo</span> khi {{streamer}} trực tiếp.",
|
||||
"chatEmbedTitle": "Chat {{name}}"
|
||||
},
|
||||
"Testing": {
|
||||
"itemCount": "Bạn có {{count}} mục",
|
||||
|
||||
@@ -221,7 +221,8 @@
|
||||
"offlineBasic": "此串流已離線。請稍後再回來查看!",
|
||||
"offlineFediverseOnly": "此串流已離線。請在 Fediverse<span class='follow-link'>上追</span>蹤 {{fediverseAccount}} ,以查看 {{streamer}} 的下次直播時間。",
|
||||
"offlineNotifyAndFediverse": "此串流已離線。您可以在 {{streamer}} 上線時<span class='notify-link'>收到通知,</span>或在 Fediverse<span class='follow-link'>上追蹤</span> {{fediverseAccount}}。",
|
||||
"offlineNotifyOnly": "此串流已離線。下次 {{streamer}} 上線時,您<span class='notify-link'>將會收到通知</span>。"
|
||||
"offlineNotifyOnly": "此串流已離線。下次 {{streamer}} 上線時,您<span class='notify-link'>將會收到通知</span>。",
|
||||
"chatEmbedTitle": "{{name}} 聊天"
|
||||
},
|
||||
"Testing": {
|
||||
"itemCount": "您有 {{count}} 項目",
|
||||
|
||||
@@ -59,6 +59,9 @@ module.exports = async phase => {
|
||||
productionBrowserSourceMaps: process.env.SOURCE_MAPS === 'true',
|
||||
trailingSlash: true,
|
||||
reactStrictMode: true,
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
|
||||
Generated
+1189
-1302
File diff suppressed because it is too large
Load Diff
+6
-6
@@ -41,8 +41,8 @@
|
||||
"@next/bundle-analyzer": "^14.0.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@uiw/codemirror-theme-bbedit": "4.25.8",
|
||||
"@uiw/react-codemirror": "4.25.8",
|
||||
"@uiw/codemirror-theme-bbedit": "4.25.9",
|
||||
"@uiw/react-codemirror": "4.25.9",
|
||||
"@xstate/react": "3.2.2",
|
||||
"antd": "4.24.16",
|
||||
"autoprefixer": "^10.4.14",
|
||||
@@ -105,10 +105,10 @@
|
||||
"@types/sanitize-html": "^2.9.0",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/video.js": "^7.3.50",
|
||||
"@typescript-eslint/eslint-plugin": "8.57.1",
|
||||
"@typescript-eslint/parser": "8.57.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.57.2",
|
||||
"@typescript-eslint/parser": "8.57.2",
|
||||
"babel-loader": "10.1.1",
|
||||
"chromatic": "15.3.0",
|
||||
"chromatic": "16.0.0",
|
||||
"css-loader": "7.1.4",
|
||||
"cypress": "^15.0.0",
|
||||
"eslint": "8.57.1",
|
||||
@@ -121,7 +121,7 @@
|
||||
"eslint-plugin-react": "7.37.5",
|
||||
"eslint-plugin-react-hooks": "4.6.2",
|
||||
"eslint-plugin-storybook": "9.1.20",
|
||||
"handlebars": "^4.7.7",
|
||||
"handlebars": "^4.7.9",
|
||||
"html-webpack-plugin": "5.6.6",
|
||||
"install": "^0.13.0",
|
||||
"jest-environment-jsdom": "^30.0.4",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import Head from 'next/head';
|
||||
import { useTranslation } from 'next-export-i18n';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { ChatMessage } from '../../../../interfaces/chat-message.model';
|
||||
import { ChatContainer } from '../../../../components/chat/ChatContainer/ChatContainer';
|
||||
@@ -6,22 +8,34 @@ import {
|
||||
ClientConfigStore,
|
||||
currentUserAtom,
|
||||
visibleChatMessagesSelector,
|
||||
clientConfigStateAtom,
|
||||
isChatAvailableSelector,
|
||||
} from '../../../../components/stores/ClientConfigStore';
|
||||
import { ClientConfig } from '../../../../interfaces/client-config.model';
|
||||
import { Theme } from '../../../../components/theme/Theme';
|
||||
import { ComponentError } from '../../../../components/ui/ComponentError/ComponentError';
|
||||
import { Localization } from '../../../../types/localization';
|
||||
|
||||
export default function ReadOnlyChatEmbed() {
|
||||
const currentUser = useRecoilValue(currentUserAtom);
|
||||
const messages = useRecoilValue<ChatMessage[]>(visibleChatMessagesSelector);
|
||||
const clientConfig = useRecoilValue<ClientConfig>(clientConfigStateAtom);
|
||||
const isChatAvailable = useRecoilValue(isChatAvailableSelector);
|
||||
|
||||
const { name } = clientConfig;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const pageTitle = name ? t(Localization.Frontend.chatEmbedTitle, { name }) : 'Chat';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title>{pageTitle}</title>
|
||||
</Head>
|
||||
<ErrorBoundary
|
||||
// eslint-disable-next-line react/no-unstable-nested-components
|
||||
fallbackRender={({ error }) => (
|
||||
<ComponentError componentName="ReadWriteChatEmbed" message={error.message} />
|
||||
<ComponentError componentName="ReadOnlyChatEmbed" message={error.message} />
|
||||
)}
|
||||
>
|
||||
<ClientConfigStore />
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/* eslint-disable react/no-unknown-property */
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { useEffect } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import Head from 'next/head';
|
||||
import { useTranslation } from 'next-export-i18n';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { ChatMessage } from '../../../../interfaces/chat-message.model';
|
||||
import { ChatContainer } from '../../../../components/chat/ChatContainer/ChatContainer';
|
||||
import {
|
||||
@@ -50,6 +51,8 @@ export default function ReadWriteChatEmbed() {
|
||||
|
||||
const headerText = online ? streamTitle || name : name;
|
||||
|
||||
const pageTitle = name ? t(Localization.Frontend.chatEmbedTitle, { name }) : 'Chat';
|
||||
|
||||
// This is a hack to force a specific body background color for just this page.
|
||||
useEffect(() => {
|
||||
document.body.classList.add('body-background');
|
||||
@@ -57,6 +60,9 @@ export default function ReadWriteChatEmbed() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Head>
|
||||
<title>{pageTitle}</title>
|
||||
</Head>
|
||||
<style jsx global>
|
||||
{`
|
||||
.body-background {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user