Compare commits

..
44 Commits
Author SHA1 Message Date
Owncast 11af286501 Bundle embedded web app 2025-01-23 03:56:45 +00:00
613d928149 Updated translations (#4176)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-01-22 19:51:53 -08:00
renovate[bot]andGitHub 7ecc7d33d4 chore(deps): update peter-evans/create-or-update-comment digest to 2ef7be1 2025-01-22 06:37:22 +00:00
Owncast 9c8c18919f Bundle embedded web app 2025-01-22 01:40:34 +00:00
69fd525b8d Updated translations (#4169)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-01-21 17:35:40 -08:00
Owncast 1df783d0ff Bundle embedded web app 2025-01-21 23:25:56 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8a5afae48a chore(deps): bump undici from 6.21.0 to 6.21.1 in /web (#4173)
Bumps [undici](https://github.com/nodejs/undici) from 6.21.0 to 6.21.1.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v6.21.0...v6.21.1)

---
updated-dependencies:
- dependency-name: undici
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-21 15:20:48 -08:00
Owncast 15e0a2eb73 Bundle embedded web app 2025-01-21 07:34:12 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
bf685c5702 fix(deps): update dependency yaml to v2.7.0 (#4172)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-20 23:29:02 -08:00
Owncast d1fd6bc8cb Bundle embedded web app 2025-01-21 07:21:16 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
6c8daab1bd chore(deps): update dependency typescript to v5.7.3 (#4171)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-20 23:16:05 -08:00
Owncast 3e1f06913f Bundle embedded web app 2025-01-21 07:12:47 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
1e80c6dff8 chore(deps): update dependency chromatic to v11.25.0 (#4170)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-20 23:07:28 -08:00
Owncast 6482fc9f59 Bundle embedded web app 2025-01-21 03:45:30 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
31d6fc94fe chore(deps): update dependency eslint-plugin-storybook to v0.11.2 (#4165)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-20 19:40:22 -08:00
Owncast 8e89c71a34 Bundle embedded web app 2025-01-21 03:24:24 +00:00
1ad9376c4b Updated translations (#4168)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-01-20 19:19:37 -08:00
Owncast 3ddc5a8d50 Bundle embedded web app 2025-01-21 02:49:11 +00:00
6fd3aad81d Updated translations (#4162)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-01-20 18:44:02 -08:00
Owncast 520183c39f Bundle embedded web app 2025-01-21 02:07:32 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
86078d8b90 chore(deps): update dependency knip to v5.42.2 (#4166)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-20 18:02:35 -08:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
caa27d6fdb chore(deps): update dependency sass to v1.83.4 (#4167)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-20 18:01:32 -08:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
24b1dd2ed8 chore(deps): update dependency @types/node to v22.10.7 (#4164)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-20 18:01:03 -08:00
Gabe KangasandGitHub c1f4096e11 chore(go): new chat message db repository. Closes #3081 (#4161) 2025-01-20 16:32:25 -08:00
Owncast db0ddfe009 Bundle embedded web app 2025-01-21 00:04:05 +00:00
9ea94e9963 Updated translations (#4160)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-01-20 15:58:45 -08:00
Owncast c72ae603c1 Bundle embedded web app 2025-01-20 20:48:24 +00:00
5e8373fcac Updated translations (#4156)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-01-20 12:43:32 -08:00
Owncast b48b3cbc35 Bundle embedded web app 2025-01-20 02:57:04 +00:00
515e378b4b Updated translations (#4153)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-01-19 18:52:08 -08:00
Owncast 583668f750 Bundle embedded web app 2025-01-19 23:20:23 +00:00
d109c0cd7d Updated translations (#4152)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-01-19 15:15:29 -08:00
Owncast 7949670061 Bundle embedded web app 2025-01-19 22:07:46 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
9a7a072050 chore(deps): update dependency eslint-plugin-react to v7.37.4 (#4144)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-19 14:02:25 -08:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
eb608414f6 chore(deps): update dependency eslint-plugin-prettier to v5.2.2 (#4143)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-19 14:00:57 -08:00
Owncast 0d38420a7a Bundle embedded web app 2025-01-19 21:50:27 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
d81c148f68 chore(deps): update dependency stylelint to v16.13.2 (#4145)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-19 13:45:23 -08:00
Owncast 89b4e39542 Bundle embedded web app 2025-01-19 21:45:08 +00:00
4ad771ac3d Updated translations (#4151)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-01-19 13:40:21 -08:00
Gabe Kangas 0e8ff0bea3 chore(i18n): update translation commit message 2025-01-19 13:31:20 -08:00
Gabe Kangas ce976a5f0b chore(ci): tweak i18n workflows 2025-01-19 13:20:32 -08:00
Owncast 5e64b6ea41 Bundle embedded web app 2025-01-19 20:20:54 +00:00
49f7c12b7e Updated translations (#4148)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-01-19 12:15:38 -08:00
Vibhanshu JainandGitHub 9dcee79432 Update translation.json (#4150) 2025-01-19 12:10:58 -08:00
79 changed files with 1164 additions and 876 deletions
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
issues: write issues: write
steps: steps:
- name: Add comment - name: Add comment
uses: peter-evans/create-or-update-comment@fdb73c443d3a4f66832374f01fb9a713fad84937 uses: peter-evans/create-or-update-comment@2ef7be1cfae84b02f7b4e47ce45c1f93762d1d92
with: with:
issue-number: ${{ github.event.issue.number }} issue-number: ${{ github.event.issue.number }}
body: | body: |
+6 -1
View File
@@ -3,7 +3,12 @@ on:
push: push:
branches: branches:
- develop - develop
paths: ['web/stories/**', 'web/components/**', 'web/.storybook/**'] # Trigger the action only when files change in the folders defined here paths: [
'web/stories/**',
'web/components/**',
'web/.storybook/**',
'web/i18n/**',
] # Trigger the action only when files change in the folders defined here
jobs: jobs:
build-and-deploy: build-and-deploy:
+10 -10
View File
@@ -3,7 +3,7 @@ name: Translation job
on: on:
schedule: schedule:
# Run the workflow every hour # Run the workflow every hour
- cron: "0 * * * *" - cron: '0 * * * *'
push: push:
paths: paths:
- 'web/i18n/en/translation.json' - 'web/i18n/en/translation.json'
@@ -51,12 +51,12 @@ jobs:
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
- name: Commit changes # - name: Commit changes
uses: EndBug/add-and-commit@v9 # uses: EndBug/add-and-commit@v9
with: # with:
author_name: Owncast # author_name: Owncast
author_email: owncast@owncast.online # author_email: owncast@owncast.online
message: 'Commit updated translations' # message: 'Commit updated translations'
add: 'web/i18n/**' # add: 'web/i18n/**'
env: # env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+7 -3
View File
@@ -8,6 +8,7 @@ import (
"github.com/owncast/owncast/config" "github.com/owncast/owncast/config"
"github.com/owncast/owncast/core/chat/events" "github.com/owncast/owncast/core/chat/events"
"github.com/owncast/owncast/models" "github.com/owncast/owncast/models"
"github.com/owncast/owncast/persistence/chatmessagerepository"
"github.com/owncast/owncast/persistence/configrepository" "github.com/owncast/owncast/persistence/configrepository"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promauto"
@@ -103,7 +104,8 @@ func SendSystemMessage(text string, ephemeral bool) error {
} }
if !ephemeral { if !ephemeral {
saveEvent(message.ID, nil, message.Body, message.GetMessageType(), nil, message.Timestamp, nil, nil, nil, nil) chatMessageRepository := chatmessagerepository.Get()
chatMessageRepository.SaveEvent(message.ID, nil, message.Body, message.GetMessageType(), nil, message.Timestamp, nil, nil, nil, nil)
} }
return nil return nil
@@ -131,7 +133,8 @@ func SendFediverseAction(eventType string, userAccountName string, image *string
return err return err
} }
saveFederatedAction(message) chatMessageRepository := chatmessagerepository.Get()
chatMessageRepository.SaveFederatedAction(message)
return nil return nil
} }
@@ -152,7 +155,8 @@ func SendSystemAction(text string, ephemeral bool) error {
} }
if !ephemeral { if !ephemeral {
saveEvent(message.ID, nil, message.Body, message.GetMessageType(), nil, message.Timestamp, nil, nil, nil, nil) chatMessageRepository := chatmessagerepository.Get()
chatMessageRepository.SaveEvent(message.ID, nil, message.Body, message.GetMessageType(), nil, message.Timestamp, nil, nil, nil, nil)
} }
return nil return nil
+3 -2
View File
@@ -9,6 +9,7 @@ import (
"github.com/owncast/owncast/config" "github.com/owncast/owncast/config"
"github.com/owncast/owncast/core/chat/events" "github.com/owncast/owncast/core/chat/events"
"github.com/owncast/owncast/core/webhooks" "github.com/owncast/owncast/core/webhooks"
"github.com/owncast/owncast/persistence/chatmessagerepository"
"github.com/owncast/owncast/persistence/configrepository" "github.com/owncast/owncast/persistence/configrepository"
"github.com/owncast/owncast/persistence/userrepository" "github.com/owncast/owncast/persistence/userrepository"
"github.com/owncast/owncast/utils" "github.com/owncast/owncast/utils"
@@ -172,8 +173,8 @@ func (s *Server) userMessageSent(eventData chatClientEvent) {
// Send chat message sent webhook // Send chat message sent webhook
webhooks.SendChatEvent(&event) webhooks.SendChatEvent(&event)
chatMessagesSentCounter.Inc() chatMessagesSentCounter.Inc()
chatMessageRepository := chatmessagerepository.Get()
SaveUserMessage(event) chatMessageRepository.SaveUserMessage(event)
eventData.client.MessageCount++ eventData.client.MessageCount++
} }
+3 -1
View File
@@ -5,13 +5,15 @@ import (
"github.com/owncast/owncast/core/chat/events" "github.com/owncast/owncast/core/chat/events"
"github.com/owncast/owncast/core/webhooks" "github.com/owncast/owncast/core/webhooks"
"github.com/owncast/owncast/persistence/chatmessagerepository"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
// SetMessagesVisibility will set the visibility of multiple messages by ID. // SetMessagesVisibility will set the visibility of multiple messages by ID.
func SetMessagesVisibility(messageIDs []string, visibility bool) error { func SetMessagesVisibility(messageIDs []string, visibility bool) error {
// Save new message visibility // Save new message visibility
if err := saveMessageVisibility(messageIDs, visibility); err != nil { chatMessageRepository := chatmessagerepository.Get()
if err := chatMessageRepository.SetMessageVisibilityForMessageIDs(messageIDs, visibility); err != nil {
log.Errorln(err) log.Errorln(err)
return err return err
} }
+1 -462
View File
@@ -1,25 +1,17 @@
package chat package chat
import ( import (
"context"
"database/sql"
"strings"
"time" "time"
"github.com/owncast/owncast/core/chat/events"
"github.com/owncast/owncast/core/data" "github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/models"
"github.com/owncast/owncast/persistence/authrepository" "github.com/owncast/owncast/persistence/authrepository"
"github.com/owncast/owncast/persistence/tables" "github.com/owncast/owncast/persistence/tables"
log "github.com/sirupsen/logrus"
) )
var _datastore *data.Datastore var _datastore *data.Datastore
const ( const (
maxBacklogHours = 2 // Keep backlog max hours worth of messages maxBacklogHours = 2 // Keep backlog max hours worth of messages
maxBacklogNumber = 50 // Return max number of messages in history request
) )
func setupPersistence() { func setupPersistence() {
@@ -37,456 +29,3 @@ func setupPersistence() {
} }
}() }()
} }
// SaveUserMessage will save a single chat event to the messages database.
func SaveUserMessage(event events.UserMessageEvent) {
saveEvent(event.ID, &event.User.ID, event.Body, event.Type, event.HiddenAt, event.Timestamp, nil, nil, nil, nil)
}
func saveFederatedAction(event events.FediverseEngagementEvent) {
saveEvent(event.ID, nil, event.Body, event.Type, nil, event.Timestamp, event.Image, &event.Link, &event.UserAccountName, nil)
}
// nolint: unparam
func saveEvent(id string, userID *string, body string, eventType string, hidden *time.Time, timestamp time.Time, image *string, link *string, title *string, subtitle *string) {
defer func() {
_historyCache = nil
}()
tx, err := _datastore.DB.Begin()
if err != nil {
log.Errorln("error saving", eventType, err)
return
}
defer tx.Rollback() // nolint
stmt, err := tx.Prepare("INSERT INTO messages(id, user_id, body, eventType, hidden_at, timestamp, image, link, title, subtitle) values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
if err != nil {
log.Errorln("error saving", eventType, err)
return
}
defer stmt.Close()
if _, err = stmt.Exec(id, userID, body, eventType, hidden, timestamp, image, link, title, subtitle); err != nil {
log.Errorln("error saving", eventType, err)
return
}
if err = tx.Commit(); err != nil {
log.Errorln("error saving", eventType, err)
return
}
}
func makeUserMessageEventFromRowData(row rowData) events.UserMessageEvent {
scopes := ""
if row.userScopes != nil {
scopes = *row.userScopes
}
previousUsernames := []string{}
if row.previousUsernames != nil {
previousUsernames = strings.Split(*row.previousUsernames, ",")
}
displayName := ""
if row.userDisplayName != nil {
displayName = *row.userDisplayName
}
displayColor := 0
if row.userDisplayColor != nil {
displayColor = *row.userDisplayColor
}
createdAt := time.Time{}
if row.userCreatedAt != nil {
createdAt = *row.userCreatedAt
}
isBot := (row.userType != nil && *row.userType == "API")
scopeSlice := strings.Split(scopes, ",")
u := models.User{
ID: *row.userID,
DisplayName: displayName,
DisplayColor: displayColor,
CreatedAt: createdAt,
DisabledAt: row.userDisabledAt,
NameChangedAt: row.userNameChangedAt,
PreviousNames: previousUsernames,
AuthenticatedAt: row.userAuthenticatedAt,
Authenticated: row.userAuthenticatedAt != nil,
Scopes: scopeSlice,
IsBot: isBot,
}
message := events.UserMessageEvent{
Event: events.Event{
Type: row.eventType,
ID: row.id,
Timestamp: row.timestamp,
},
UserEvent: events.UserEvent{
User: &u,
HiddenAt: row.hiddenAt,
},
MessageEvent: events.MessageEvent{
Body: row.body,
RawBody: row.body,
},
}
return message
}
func makeSystemMessageChatEventFromRowData(row rowData) events.SystemMessageEvent {
message := events.SystemMessageEvent{
Event: events.Event{
Type: row.eventType,
ID: row.id,
Timestamp: row.timestamp,
},
MessageEvent: events.MessageEvent{
Body: row.body,
RawBody: row.body,
},
}
return message
}
func makeActionMessageChatEventFromRowData(row rowData) events.ActionEvent {
message := events.ActionEvent{
Event: events.Event{
Type: row.eventType,
ID: row.id,
Timestamp: row.timestamp,
},
MessageEvent: events.MessageEvent{
Body: row.body,
RawBody: row.body,
},
}
return message
}
func makeFederatedActionChatEventFromRowData(row rowData) events.FediverseEngagementEvent {
message := events.FediverseEngagementEvent{
Event: events.Event{
Type: row.eventType,
ID: row.id,
Timestamp: row.timestamp,
},
MessageEvent: events.MessageEvent{
Body: row.body,
RawBody: row.body,
},
Image: row.image,
Link: *row.link,
UserAccountName: *row.title,
}
return message
}
type rowData struct {
timestamp time.Time
image *string
previousUsernames *string
userDisplayName *string
userDisplayColor *int
userID *string
title *string
subtitle *string
link *string
userType *string
userScopes *string
hiddenAt *time.Time
userCreatedAt *time.Time
userDisabledAt *time.Time
userAuthenticatedAt *time.Time
userNameChangedAt *time.Time
body string
eventType models.EventType
id string
}
func getChat(rows *sql.Rows) ([]interface{}, error) {
history := make([]interface{}, 0)
for rows.Next() {
row := rowData{}
// Convert a database row into a chat event
if err := rows.Scan(
&row.id,
&row.userID,
&row.body,
&row.title,
&row.subtitle,
&row.image,
&row.link,
&row.eventType,
&row.hiddenAt,
&row.timestamp,
&row.userDisplayName,
&row.userDisplayColor,
&row.userCreatedAt,
&row.userDisabledAt,
&row.previousUsernames,
&row.userNameChangedAt,
&row.userAuthenticatedAt,
&row.userScopes,
&row.userType,
); err != nil {
return nil, err
}
var message interface{}
switch row.eventType {
case events.MessageSent:
message = makeUserMessageEventFromRowData(row)
case events.SystemMessageSent:
message = makeSystemMessageChatEventFromRowData(row)
case events.ChatActionSent:
message = makeActionMessageChatEventFromRowData(row)
case events.FediverseEngagementFollow:
message = makeFederatedActionChatEventFromRowData(row)
case events.FediverseEngagementLike:
message = makeFederatedActionChatEventFromRowData(row)
case events.FediverseEngagementRepost:
message = makeFederatedActionChatEventFromRowData(row)
}
history = append(history, message)
}
return history, nil
}
var _historyCache *[]interface{}
// GetChatModerationHistory will return all the chat messages suitable for moderation purposes.
func GetChatModerationHistory() []interface{} {
if _historyCache != nil {
return *_historyCache
}
tx, err := _datastore.DB.Begin()
if err != nil {
log.Errorln("error fetching chat moderation history", err)
return nil
}
defer tx.Rollback() // nolint
// Get all messages regardless of visibility
query := "SELECT messages.id, user_id, body, title, subtitle, image, link, eventType, hidden_at, timestamp, display_name, display_color, created_at, disabled_at, previous_names, namechanged_at, authenticated_at, scopes, type FROM messages INNER JOIN users ON messages.user_id = users.id ORDER BY timestamp DESC"
stmt, err := tx.Prepare(query)
if err != nil {
log.Errorln("error fetching chat moderation history", err)
return nil
}
rows, err := stmt.Query()
if err != nil {
log.Errorln("error fetching chat moderation history", err)
return nil
}
defer stmt.Close()
defer rows.Close()
result, err := getChat(rows)
if err != nil {
log.Errorln(err)
log.Errorln("There is a problem enumerating chat message rows. Please report this:", query)
return nil
}
_historyCache = &result
if err = tx.Commit(); err != nil {
log.Errorln("error fetching chat moderation history", err)
return nil
}
return result
}
// GetChatHistory will return all the chat messages suitable for returning as user-facing chat history.
func GetChatHistory() []interface{} {
tx, err := _datastore.DB.Begin()
if err != nil {
log.Errorln("error fetching chat history", err)
return nil
}
defer tx.Rollback() // nolint
// Get all visible messages
query := "SELECT messages.id, messages.user_id, messages.body, messages.title, messages.subtitle, messages.image, messages.link, messages.eventType, messages.hidden_at, messages.timestamp, users.display_name, users.display_color, users.created_at, users.disabled_at, users.previous_names, users.namechanged_at, users.authenticated_at, users.scopes, users.type FROM users JOIN messages ON users.id = messages.user_id WHERE hidden_at IS NULL AND disabled_at IS NULL ORDER BY timestamp DESC LIMIT ?"
stmt, err := tx.Prepare(query)
if err != nil {
log.Errorln("error fetching chat history", err)
return nil
}
rows, err := stmt.Query(maxBacklogNumber)
if err != nil {
log.Errorln("error fetching chat history", err)
return nil
}
defer stmt.Close()
defer rows.Close()
m, err := getChat(rows)
if err != nil {
log.Errorln(err)
log.Errorln("There is a problem enumerating chat message rows. Please report this:", query)
return nil
}
if err = tx.Commit(); err != nil {
log.Errorln("error fetching chat history", err)
return nil
}
// Invert order of messages
for i, j := 0, len(m)-1; i < j; i, j = i+1, j-1 {
m[i], m[j] = m[j], m[i]
}
return m
}
// GetMessagesFromUser returns chat messages that were sent by a specific user.
func GetMessagesFromUser(userID string) ([]events.UserMessageEvent, error) {
query, err := _datastore.GetQueries().GetMessagesFromUser(context.Background(), sql.NullString{String: userID, Valid: true})
if err != nil {
return nil, err
}
results := make([]events.UserMessageEvent, len(query))
for i, row := range query {
results[i] = events.UserMessageEvent{
Event: events.Event{
Timestamp: row.Timestamp.Time,
ID: row.ID,
},
MessageEvent: events.MessageEvent{
Body: row.Body.String,
},
}
}
return results, nil
}
// SetMessageVisibilityForUserID will bulk change the visibility of messages for a user
// and then send out visibility changed events to chat clients.
func SetMessageVisibilityForUserID(userID string, visible bool) error {
defer func() {
_historyCache = nil
}()
tx, err := _datastore.DB.Begin()
if err != nil {
log.Errorln("error while setting message visibility", err)
return nil
}
defer tx.Rollback() // nolint
query := "SELECT messages.id, user_id, body, title, subtitle, image, link, eventType, hidden_at, timestamp, display_name, display_color, created_at, disabled_at, previous_names, namechanged_at, authenticated_at, scopes, type FROM messages INNER JOIN users ON messages.user_id = users.id WHERE user_id IS ?"
stmt, err := tx.Prepare(query)
if err != nil {
log.Errorln("error while setting message visibility", err)
return nil
}
rows, err := stmt.Query(userID)
if err != nil {
log.Errorln("error while setting message visibility", err)
return nil
}
defer stmt.Close()
defer rows.Close()
// Get a list of IDs to send to the connected clients to hide
ids := make([]string, 0)
messages, err := getChat(rows)
if err != nil {
log.Errorln(err)
log.Errorln("There is a problem enumerating chat message rows. Please report this:", query)
return nil
}
if len(messages) == 0 {
return nil
}
for _, message := range messages {
ids = append(ids, message.(events.UserMessageEvent).ID)
}
if err = tx.Commit(); err != nil {
log.Errorln("error while setting message visibility ", err)
return nil
}
// Tell the clients to hide/show these messages.
return SetMessagesVisibility(ids, visible)
}
func saveMessageVisibility(messageIDs []string, visible bool) error {
defer func() {
_historyCache = nil
}()
_datastore.DbLock.Lock()
defer _datastore.DbLock.Unlock()
tx, err := _datastore.DB.Begin()
if err != nil {
return err
}
// nolint:gosec
stmt, err := tx.Prepare("UPDATE messages SET hidden_at=? WHERE id IN (?" + strings.Repeat(",?", len(messageIDs)-1) + ")")
if err != nil {
return err
}
defer stmt.Close()
var hiddenAt *time.Time
if !visible {
now := time.Now()
hiddenAt = &now
} else {
hiddenAt = nil
}
args := make([]interface{}, len(messageIDs)+1)
args[0] = hiddenAt
for i, id := range messageIDs {
args[i+1] = id
}
if _, err = stmt.Exec(args...); err != nil {
return err
}
if err = tx.Commit(); err != nil {
return err
}
return nil
}
+1
View File
@@ -75,6 +75,7 @@ func SetupPersistence(file string) error {
_, _ = db.Exec("pragma temp_store = memory") _, _ = db.Exec("pragma temp_store = memory")
_, _ = db.Exec("pragma wal_checkpoint(full)") _, _ = db.Exec("pragma wal_checkpoint(full)")
tables.CreateConfigTable(db)
tables.CreateWebhooksTable(db) tables.CreateWebhooksTable(db)
tables.CreateUsersTable(db) tables.CreateUsersTable(db)
tables.CreateAccessTokenTable(db) tables.CreateAccessTokenTable(db)
+1 -9
View File
@@ -107,19 +107,11 @@ func (ds *Datastore) Save(e models.ConfigEntry) error {
return nil return nil
} }
// Setup will create the datastore table and perform initial initialization. // Setup will perform initial initialization.
func (ds *Datastore) Setup() { func (ds *Datastore) Setup() {
ds.cache = make(map[string][]byte) ds.cache = make(map[string][]byte)
ds.DB = GetDatabase() ds.DB = GetDatabase()
ds.DbLock = &sync.Mutex{} ds.DbLock = &sync.Mutex{}
createTableSQL := `CREATE TABLE IF NOT EXISTS datastore (
"key" string NOT NULL PRIMARY KEY,
"value" BLOB,
"timestamp" DATE DEFAULT CURRENT_TIMESTAMP NOT NULL
);`
ds.MustExec(createTableSQL)
} }
// Reset will delete all config entries in the datastore and start over. // Reset will delete all config entries in the datastore and start over.
-18
View File
@@ -1,18 +0,0 @@
package data
// GetMessagesCount will return the number of messages in the database.
func GetMessagesCount() int64 {
query := `SELECT COUNT(*) FROM messages`
rows, err := _db.Query(query)
if err != nil || rows.Err() != nil {
return 0
}
defer rows.Close()
var count int64
for rows.Next() {
if err := rows.Scan(&count); err != nil {
return 0
}
}
return count
}
+1 -1
View File
@@ -5,7 +5,7 @@ pull_request_labels:
- crowdin - crowdin
- i18n - i18n
- translation - translation
commit_message: Updated translations commit_message: 'Updated translations for %language%'
files: files:
- source: /web/i18n/en/translation.json - source: /web/i18n/en/translation.json
translation: /web/i18n/%two_letters_code%/translation.json translation: /web/i18n/%two_letters_code%/translation.json
+3 -2
View File
@@ -6,7 +6,7 @@ import (
"github.com/nakabonne/tstorage" "github.com/nakabonne/tstorage"
"github.com/owncast/owncast/core" "github.com/owncast/owncast/core"
"github.com/owncast/owncast/core/chat" "github.com/owncast/owncast/core/chat"
"github.com/owncast/owncast/core/data" "github.com/owncast/owncast/persistence/chatmessagerepository"
"github.com/owncast/owncast/persistence/userrepository" "github.com/owncast/owncast/persistence/userrepository"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@@ -56,7 +56,8 @@ func collectChatClientCount() {
activeChatClientCount.Set(float64(count)) activeChatClientCount.Set(float64(count))
// Total message count // Total message count
cmc := data.GetMessagesCount() chatMessageRepository := chatmessagerepository.Get()
cmc := chatMessageRepository.GetMessagesCount()
// Insert message count into Prometheus collector. // Insert message count into Prometheus collector.
currentChatMessageCount.Set(float64(cmc)) currentChatMessageCount.Set(float64(cmc))
@@ -0,0 +1,523 @@
package chatmessagerepository
import (
"context"
"database/sql"
"errors"
"strings"
"time"
"github.com/owncast/owncast/core/chat/events"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/models"
log "github.com/sirupsen/logrus"
)
const maxBacklogNumber = 50 // Return max number of messages in history request
type ChatMessageRepository interface {
SaveUserMessage(event events.UserMessageEvent)
SaveFederatedAction(event events.FediverseEngagementEvent)
SaveEvent(id string, userID *string, body string, eventType string, hidden *time.Time, timestamp time.Time, image *string, link *string, title *string, subtitle *string)
GetChatModerationHistory() []interface{}
GetChatHistory() []interface{}
GetMessagesFromUser(userID string) ([]events.UserMessageEvent, error)
GetMessageIdsForUserID(userID string) ([]string, error)
SetMessageVisibilityForMessageIDs(messageIDs []string, visible bool) error
GetMessagesCount() int64
}
type SqlChatMessageRepository struct {
datastore *data.Datastore
}
// NOTE: This is temporary during the transition period.
var temporaryGlobalInstance ChatMessageRepository
// Get will return the user repository.
func Get() ChatMessageRepository {
if temporaryGlobalInstance == nil {
i := New(data.GetDatastore())
temporaryGlobalInstance = i
}
return temporaryGlobalInstance
}
// New will create a new instance of the UserRepository.
func New(datastore *data.Datastore) ChatMessageRepository {
r := SqlChatMessageRepository{
datastore: datastore,
}
return &r
}
// SaveUserMessage will save a single chat event to the messages database.
func (r *SqlChatMessageRepository) SaveUserMessage(event events.UserMessageEvent) {
r.SaveEvent(event.ID, &event.User.ID, event.Body, event.Type, event.HiddenAt, event.Timestamp, nil, nil, nil, nil)
}
func (r *SqlChatMessageRepository) SaveFederatedAction(event events.FediverseEngagementEvent) {
r.SaveEvent(event.ID, nil, event.Body, event.Type, nil, event.Timestamp, event.Image, &event.Link, &event.UserAccountName, nil)
}
// nolint: unparam
func (r *SqlChatMessageRepository) SaveEvent(id string, userID *string, body string, eventType string, hidden *time.Time, timestamp time.Time, image *string, link *string, title *string, subtitle *string) {
defer func() {
_historyCache = nil
}()
tx, err := r.datastore.DB.Begin()
if err != nil {
log.Errorln("error saving", eventType, err)
return
}
defer tx.Rollback() // nolint
stmt, err := tx.Prepare("INSERT INTO messages(id, user_id, body, eventType, hidden_at, timestamp, image, link, title, subtitle) values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
if err != nil {
log.Errorln("error saving", eventType, err)
return
}
defer stmt.Close()
if _, err = stmt.Exec(id, userID, body, eventType, hidden, timestamp, image, link, title, subtitle); err != nil {
log.Errorln("error saving", eventType, err)
return
}
if err = tx.Commit(); err != nil {
log.Errorln("error saving", eventType, err)
return
}
}
func makeUserMessageEventFromRowData(row rowData) events.UserMessageEvent {
scopes := ""
if row.userScopes != nil {
scopes = *row.userScopes
}
previousUsernames := []string{}
if row.previousUsernames != nil {
previousUsernames = strings.Split(*row.previousUsernames, ",")
}
displayName := ""
if row.userDisplayName != nil {
displayName = *row.userDisplayName
}
displayColor := 0
if row.userDisplayColor != nil {
displayColor = *row.userDisplayColor
}
createdAt := time.Time{}
if row.userCreatedAt != nil {
createdAt = *row.userCreatedAt
}
isBot := (row.userType != nil && *row.userType == "API")
scopeSlice := strings.Split(scopes, ",")
u := models.User{
ID: *row.userID,
DisplayName: displayName,
DisplayColor: displayColor,
CreatedAt: createdAt,
DisabledAt: row.userDisabledAt,
NameChangedAt: row.userNameChangedAt,
PreviousNames: previousUsernames,
AuthenticatedAt: row.userAuthenticatedAt,
Authenticated: row.userAuthenticatedAt != nil,
Scopes: scopeSlice,
IsBot: isBot,
}
message := events.UserMessageEvent{
Event: events.Event{
Type: row.eventType,
ID: row.id,
Timestamp: row.timestamp,
},
UserEvent: events.UserEvent{
User: &u,
HiddenAt: row.hiddenAt,
},
MessageEvent: events.MessageEvent{
Body: row.body,
RawBody: row.body,
},
}
return message
}
func makeSystemMessageChatEventFromRowData(row rowData) events.SystemMessageEvent {
message := events.SystemMessageEvent{
Event: events.Event{
Type: row.eventType,
ID: row.id,
Timestamp: row.timestamp,
},
MessageEvent: events.MessageEvent{
Body: row.body,
RawBody: row.body,
},
}
return message
}
func makeActionMessageChatEventFromRowData(row rowData) events.ActionEvent {
message := events.ActionEvent{
Event: events.Event{
Type: row.eventType,
ID: row.id,
Timestamp: row.timestamp,
},
MessageEvent: events.MessageEvent{
Body: row.body,
RawBody: row.body,
},
}
return message
}
func makeFederatedActionChatEventFromRowData(row rowData) events.FediverseEngagementEvent {
message := events.FediverseEngagementEvent{
Event: events.Event{
Type: row.eventType,
ID: row.id,
Timestamp: row.timestamp,
},
MessageEvent: events.MessageEvent{
Body: row.body,
RawBody: row.body,
},
Image: row.image,
Link: *row.link,
UserAccountName: *row.title,
}
return message
}
type rowData struct {
timestamp time.Time
image *string
previousUsernames *string
userDisplayName *string
userDisplayColor *int
userID *string
title *string
subtitle *string
link *string
userType *string
userScopes *string
hiddenAt *time.Time
userCreatedAt *time.Time
userDisabledAt *time.Time
userAuthenticatedAt *time.Time
userNameChangedAt *time.Time
body string
eventType models.EventType
id string
}
func getChat(rows *sql.Rows) ([]interface{}, error) {
history := make([]interface{}, 0)
for rows.Next() {
row := rowData{}
// Convert a database row into a chat event
if err := rows.Scan(
&row.id,
&row.userID,
&row.body,
&row.title,
&row.subtitle,
&row.image,
&row.link,
&row.eventType,
&row.hiddenAt,
&row.timestamp,
&row.userDisplayName,
&row.userDisplayColor,
&row.userCreatedAt,
&row.userDisabledAt,
&row.previousUsernames,
&row.userNameChangedAt,
&row.userAuthenticatedAt,
&row.userScopes,
&row.userType,
); err != nil {
return nil, err
}
var message interface{}
switch row.eventType {
case events.MessageSent:
message = makeUserMessageEventFromRowData(row)
case events.SystemMessageSent:
message = makeSystemMessageChatEventFromRowData(row)
case events.ChatActionSent:
message = makeActionMessageChatEventFromRowData(row)
case events.FediverseEngagementFollow:
message = makeFederatedActionChatEventFromRowData(row)
case events.FediverseEngagementLike:
message = makeFederatedActionChatEventFromRowData(row)
case events.FediverseEngagementRepost:
message = makeFederatedActionChatEventFromRowData(row)
}
history = append(history, message)
}
return history, nil
}
var _historyCache *[]interface{}
// GetChatModerationHistory will return all the chat messages suitable for moderation purposes.
func (r *SqlChatMessageRepository) GetChatModerationHistory() []interface{} {
if _historyCache != nil {
return *_historyCache
}
tx, err := r.datastore.DB.Begin()
if err != nil {
log.Errorln("error fetching chat moderation history", err)
return nil
}
defer tx.Rollback() // nolint
// Get all messages regardless of visibility
query := "SELECT messages.id, user_id, body, title, subtitle, image, link, eventType, hidden_at, timestamp, display_name, display_color, created_at, disabled_at, previous_names, namechanged_at, authenticated_at, scopes, type FROM messages INNER JOIN users ON messages.user_id = users.id ORDER BY timestamp DESC"
stmt, err := tx.Prepare(query)
if err != nil {
log.Errorln("error fetching chat moderation history", err)
return nil
}
rows, err := stmt.Query()
if err != nil {
log.Errorln("error fetching chat moderation history", err)
return nil
}
defer stmt.Close()
defer rows.Close()
result, err := getChat(rows)
if err != nil {
log.Errorln(err)
log.Errorln("There is a problem enumerating chat message rows. Please report this:", query)
return nil
}
_historyCache = &result
if err = tx.Commit(); err != nil {
log.Errorln("error fetching chat moderation history", err)
return nil
}
return result
}
// GetChatHistory will return all the chat messages suitable for returning as user-facing chat history.
func (r *SqlChatMessageRepository) GetChatHistory() []interface{} {
tx, err := r.datastore.DB.Begin()
if err != nil {
log.Errorln("error fetching chat history", err)
return nil
}
defer tx.Rollback() // nolint
// Get all visible messages
query := "SELECT messages.id, messages.user_id, messages.body, messages.title, messages.subtitle, messages.image, messages.link, messages.eventType, messages.hidden_at, messages.timestamp, users.display_name, users.display_color, users.created_at, users.disabled_at, users.previous_names, users.namechanged_at, users.authenticated_at, users.scopes, users.type FROM users JOIN messages ON users.id = messages.user_id WHERE hidden_at IS NULL AND disabled_at IS NULL ORDER BY timestamp DESC LIMIT ?"
stmt, err := tx.Prepare(query)
if err != nil {
log.Errorln("error fetching chat history", err)
return nil
}
rows, err := stmt.Query(maxBacklogNumber)
if err != nil {
log.Errorln("error fetching chat history", err)
return nil
}
defer stmt.Close()
defer rows.Close()
m, err := getChat(rows)
if err != nil {
log.Errorln(err)
log.Errorln("There is a problem enumerating chat message rows. Please report this:", query)
return nil
}
if err = tx.Commit(); err != nil {
log.Errorln("error fetching chat history", err)
return nil
}
// Invert order of messages
for i, j := 0, len(m)-1; i < j; i, j = i+1, j-1 {
m[i], m[j] = m[j], m[i]
}
return m
}
// GetMessagesFromUser returns chat messages that were sent by a specific user.
func (r *SqlChatMessageRepository) GetMessagesFromUser(userID string) ([]events.UserMessageEvent, error) {
query, err := r.datastore.GetQueries().GetMessagesFromUser(context.Background(), sql.NullString{String: userID, Valid: true})
if err != nil {
return nil, err
}
results := make([]events.UserMessageEvent, len(query))
for i, row := range query {
results[i] = events.UserMessageEvent{
Event: events.Event{
Timestamp: row.Timestamp.Time,
ID: row.ID,
},
MessageEvent: events.MessageEvent{
Body: row.Body.String,
},
}
}
return results, nil
}
// GetMessageIdsForUserID will return the chat message IDs for a specific user.
func (r *SqlChatMessageRepository) GetMessageIdsForUserID(userID string) ([]string, error) {
defer func() {
_historyCache = nil
}()
tx, err := r.datastore.DB.Begin()
if err != nil {
return nil, errors.New("error while setting message visibility")
}
defer tx.Rollback() // nolint
query := "SELECT messages.id, user_id, body, title, subtitle, image, link, eventType, hidden_at, timestamp, display_name, display_color, created_at, disabled_at, previous_names, namechanged_at, authenticated_at, scopes, type FROM messages INNER JOIN users ON messages.user_id = users.id WHERE user_id IS ?"
stmt, err := tx.Prepare(query)
if err != nil {
return nil, errors.New("error while setting message visibility")
}
rows, err := stmt.Query(userID)
if err != nil {
log.Errorln("error while setting message visibility", err)
return nil, errors.New("error while setting message visibility")
}
defer stmt.Close()
defer rows.Close()
// Get a list of IDs to send to the connected clients to hide
ids := make([]string, 0)
messages, err := getChat(rows)
if err != nil {
log.Errorln(err)
return nil, errors.New("There is a problem enumerating chat message rows. Please report this: " + query)
}
if len(messages) == 0 {
return []string{}, nil
}
for _, message := range messages {
ids = append(ids, message.(events.UserMessageEvent).ID)
}
if err = tx.Commit(); err != nil {
return nil, errors.New("error while setting message visibility")
}
// Tell the clients to hide/show these messages.
return ids, nil
}
func (r *SqlChatMessageRepository) SetMessageVisibilityForMessageIDs(messageIDs []string, visible bool) error {
defer func() {
_historyCache = nil
}()
if len(messageIDs) == 0 {
return nil
}
r.datastore.DbLock.Lock()
defer r.datastore.DbLock.Unlock()
tx, err := r.datastore.DB.Begin()
if err != nil {
return err
}
// nolint:gosec
stmt, err := tx.Prepare("UPDATE messages SET hidden_at=? WHERE id IN (?" + strings.Repeat(",?", len(messageIDs)-1) + ")")
if err != nil {
return err
}
defer stmt.Close()
var hiddenAt *time.Time
if !visible {
now := time.Now()
hiddenAt = &now
} else {
hiddenAt = nil
}
args := make([]interface{}, len(messageIDs)+1)
args[0] = hiddenAt
for i, id := range messageIDs {
args[i+1] = id
}
if _, err = stmt.Exec(args...); err != nil {
return err
}
if err = tx.Commit(); err != nil {
return err
}
return nil
}
// GetMessagesCount will return the number of messages in the database.
func (r *SqlChatMessageRepository) GetMessagesCount() int64 {
query := `SELECT COUNT(*) FROM messages`
rows, err := r.datastore.DB.Query(query)
if err != nil || rows.Err() != nil {
return 0
}
defer rows.Close()
var count int64
for rows.Next() {
if err := rows.Scan(&count); err != nil {
return 0
}
}
return count
}
+17
View File
@@ -0,0 +1,17 @@
package tables
import (
"database/sql"
"github.com/owncast/owncast/utils"
)
func CreateConfigTable(db *sql.DB) {
createTableSQL := `CREATE TABLE IF NOT EXISTS datastore (
"key" string NOT NULL PRIMARY KEY,
"value" BLOB,
"timestamp" DATE DEFAULT CURRENT_TIMESTAMP NOT NULL
);`
utils.MustExec(createTableSQL, db)
}
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
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
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
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
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
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+24 -2
View File
@@ -22,6 +22,11 @@ const establishedUserFailedChatMessage = {
type: 'CHAT', type: 'CHAT',
}; };
const establishedUserSucceedChatMessage = {
body: 'this message should send ' + Math.floor(Math.random() * 100),
type: 'CHAT',
};
test('send a chat message', async () => { test('send a chat message', async () => {
const registration = await registerChat(); const registration = await registerChat();
const accessToken = registration.accessToken; const accessToken = registration.accessToken;
@@ -36,7 +41,7 @@ test('verify admin can make API call to mark message as hidden', async () => {
`ws://localhost:8080/ws?accessToken=${accessToken}`, `ws://localhost:8080/ws?accessToken=${accessToken}`,
{ {
origin: 'http://localhost:8080', origin: 'http://localhost:8080',
} },
); );
// Verify the visibility change comes through the websocket // Verify the visibility change comes through the websocket
@@ -70,7 +75,7 @@ test('verify message has become hidden', async () => {
return obj.id === messageId; return obj.id === messageId;
}); });
expect(message.length).toBe(1); expect(message.length).toBe(1);
// expect(message[0].hiddenAt).toBeTruthy(); expect(message[0].hiddenAt).toBeTruthy();
}); });
test('enable established chat user mode', async () => { test('enable established chat user mode', async () => {
@@ -97,3 +102,20 @@ test('verify rejected message is not in the chat feed', async () => {
test('disable established chat user mode', async () => { test('disable established chat user mode', async () => {
await sendAdminRequest('config/chat/establishedusermode', false); await sendAdminRequest('config/chat/establishedusermode', false);
}); });
test('send a message after established user mode is disabled', async () => {
const registration = await registerChat();
const accessToken = registration.accessToken;
await sendChatMessage(establishedUserSucceedChatMessage, accessToken);
});
test('verify message is in the chat feed', async () => {
const res = await getAdminResponse('chat/messages');
const message = res.body.filter((obj) => {
return obj.body === establishedUserSucceedChatMessage.body;
});
expect(message.length).toBe(0);
});
Regular → Executable
View File
+1 -1
View File
@@ -53,7 +53,7 @@
"the next time goes live": "the next time goes live", "the next time goes live": "the next time goes live",
"Follow": "Follow", "Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live", "on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live ago", "Last live ago": "آخر بث مباشر {{timeAgo}}",
"Time": "Time", "Time": "Time",
"User": "User", "User": "User",
"Visible messages": "Visible messages", "Visible messages": "Visible messages",
+1 -1
View File
@@ -53,7 +53,7 @@
"the next time goes live": "the next time goes live", "the next time goes live": "the next time goes live",
"Follow": "Follow", "Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live", "on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live ago", "Last live ago": "গত {{timeAgo}} এর শেষ লাইভ",
"Time": "Time", "Time": "Time",
"User": "User", "User": "User",
"Visible messages": "Visible messages", "Visible messages": "Visible messages",
+1 -1
View File
@@ -53,7 +53,7 @@
"the next time goes live": "the next time goes live", "the next time goes live": "the next time goes live",
"Follow": "Follow", "Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live", "on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live ago", "Last live ago": "Letzter Live- {{timeAgo}} vor",
"Time": "Time", "Time": "Time",
"User": "User", "User": "User",
"Visible messages": "Visible messages", "Visible messages": "Visible messages",
+1 -1
View File
@@ -53,7 +53,7 @@
"the next time goes live": "the next time goes live", "the next time goes live": "the next time goes live",
"Follow": "Follow", "Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live", "on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live ago", "Last live ago": "Τελευταία ζωντανή σύνδεση {{timeAgo}} πριν",
"Time": "Time", "Time": "Time",
"User": "User", "User": "User",
"Visible messages": "Visible messages", "Visible messages": "Visible messages",
+1 -1
View File
@@ -53,7 +53,7 @@
"the next time goes live": "the next time goes live", "the next time goes live": "the next time goes live",
"Follow": "Follow", "Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live", "on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live ago", "Last live ago": "Last live {{timeAgo}} ago",
"Time": "Time", "Time": "Time",
"User": "User", "User": "User",
"Visible messages": "Visible messages", "Visible messages": "Visible messages",
+1 -1
View File
@@ -53,7 +53,7 @@
"the next time goes live": "the next time goes live", "the next time goes live": "the next time goes live",
"Follow": "Follow", "Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live", "on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live ago", "Last live ago": "Última versión de {{timeAgo}} hace",
"Time": "Time", "Time": "Time",
"User": "User", "User": "User",
"Visible messages": "Visible messages", "Visible messages": "Visible messages",
+128
View File
@@ -0,0 +1,128 @@
{
"Notify": "Jakinarazi",
"Level": "Maila",
"Info": "Informazioa",
"Warning": "Abisua",
"Error": "Errorea",
"Timestamp": "Denbora-zigilua",
"Message": "Mezua",
"Logs": "Erregistroak",
"Link": "Esteka",
"No news": "Zaharrak berri.",
"News & Updates from Owncast": "Owncast-en albiste eta eguneraketak",
"Use your broadcasting software": "Erabili zure igorpen-softwarea",
"Learn how to point your existing software to your new server and start streaming your content": "Ikasi lehendik duzun softwarea zerbitzari berrira bideratzen eta hasi zure edukia erreproduzitzen.",
"View": "Ikusi",
"Overridden via command line": "Komando-lerroaren bidez gainidatzi da.",
"Embed your video onto other sites": "Txertatu zure bideoa beste webgune batzuetan",
"Learn how you can add your Owncast stream to other sites you control": "Ikasi nola gehi dezakezun zure Owncasteko streama kontrolatzen dituzun beste gune batzuetara.",
"Chat is disabled": "Txata desgaituta dago",
"Chat will continue to be disabled until you begin a live stream": "Txata desgaituta egongo da zuzeneko igorpen bat hasi arte.",
"Find an audience on the Owncast Directory": "Aurkitu zure entzulegoa Owncast direktorioan",
"List yourself in the Owncast Directory and show off your stream": {
" Enable it in": "Zerrendatu zeure burua Owncast direktorioan eta erakutsi zure streama. Gaitu hemen"
},
"settings": "Ezarpenak",
"Add your Owncast instance to the Fediverse": "Gehitu zure Owncast instantzia fedibertsora",
"Enable Owncast social features": "Gaitu Owncasten sare sozialen funtzioak",
"to have your instance join the Fediverse, allowing people to follow, share and engage with your live stream": "zure instantzia fedibertsoan sartzeko, jendeari zure zuzeneko igorpenarekin jarraitzeko, partekatzeko eta parte hartzeko aukera emanez.",
"No stream is active": "Ez da aktibo dagoen zuzenekorik",
"You should start one": "Bat abiatu zenezake",
"Healthy Stream": "Zuzenekoaren osasuna",
"Yes": "Bai",
"No": "Ez",
"Playback Health": "Erreprodukzioaren osasuna",
"Stream health represents": "Zuzenekoaren osasunak ordezkatzen du",
"of all known players": {
" Other player status is unknown": "erreproduzitzaile ezagun guztietatik. Beste erreproduzitzaileen egoera ezezaguna da."
},
"TROUBLESHOOT": "Arazoen konponketa",
"Powered by Owncast": "Owncast-i esker",
"Documentation": "Dokumentazioa",
"Contribute": "Egin ekarpena",
"Source": "Iturria",
"Skip to player": "Joan erreproduktorera",
"Skip to offline message": "Joan lineaz kanpoko mezura",
"Skip to page content": "Joan orriaren edukira",
"Skip to footer": "Joan orriaren oinera",
"Chat will be available when the stream is live": "Txata erabilgarri egongo da zuzenekoaren igorpena abiatutakoan.",
"Chat is offline": "Txata lineaz kanpo dago",
"Stay updated!": "Eguneraketak jarraitu!",
"Click and never miss future streams!": "Egin klik eta ez itzazu ahaztu etorkizuneko zuzenekoak!",
"This stream is offline": "Zuzenekoa bukatu da.",
"the next time goes live": "zuzenekoa abiatzen den hurrengoan",
"Follow": "Jarraitu",
"on the Fediverse to see the next time goes live": "fedibertsoan hurrengo zuzenekoaren berri izan dezazun",
"Last live ago": "Azken zuzenekoa duela {{timeAgo}}",
"Time": "Denbora",
"User": "Erabiltzailea",
"Visible messages": "Ikus daitezkeen mezuak",
"Hidden messages": "Ezkututako mezuak",
"Chat Messages": "Txateko mezuak",
"Manage the messages from viewers that show up on your stream": "Kudeatu zuzenekoan agertzen diren ikusleen mezuak.",
"Show": "Erakutsi",
"Hide": "Ezkutatu",
"Visit the": "Bisitatu",
"documentation": "dokumentazioa",
"to configure additional details about your viewers": "zure ikusleei buruzko xehetasun gehiago konfiguratzeko.",
"When a stream is active and chat is enabled, connected chat clients will be displayed here": "Zuzeneko bat aktibo dagoenean eta txata gaituta dagoenean, konektatutako txat-bezeroak hemen bistaratuko dira.",
"Connected": "Konektatuta",
"offline": "lineaz kanpo",
"Banned Users": "Debekatutako erabiltzaileak",
"IP Bans": "IP debekuak",
"Moderators": "Moderatzaileak",
"Hardware Info": "Hardware-ari buruzko informazioa",
"Please wait": "Itxaron mesedez",
"No hardware details have been collected yet": "Ez dira oraindik hardware-ari buruzko xehetasunak jaso.",
"CPU": "PUZ",
"Memory": "Memoria",
"Disk": "Biltegiratzea",
"used": "erabilita",
"I want to configure my owncast instance": "Nire Owncast instantzia konfiguratu nahi dut",
"Learn more": "Ikasi gehiago",
"Help configuring my broadcasting software": "Laguntza emanaldietarako software-a konfiguratzen",
"I want to embed my stream into another site": "Nire zuzenekoa beste gune batean txertatu nahi dut",
"I want to customize my website": "Nire webgunea pertsonalizatu nahi dut",
"I want to tweak my video output": "Nire bideo irteera aldatu nahi dut",
"I want to use an external storage provider": "Kanpoko biltegiratze hornitzaile bat erabili nahi dut",
"I found a bug": "Arazo bat topatu dut",
"If you found a bug, then please": "Arazo bat topatu baduzu, mesedez,",
"let us know": "jakinaraziguzu",
"I have a general question": "Galdera orokor bat daukat",
"Most general questions are answered in our": "Galdera orokor gehienak erantzuten dira gure",
"FAQ": "FAQ (Maiz Egindako Galderak)",
"or exist in our": "edo existitzen dira gure",
"discussions": "eztabaidak",
"I want to build add-ons for Owncast": "Owncast-erako gehigarriak sortu nahi ditut",
"You can build your own bots, overlays, tools and add-ons with our": "Zure bot-ak, gainjartzeak, tresnak eta gehigarriak eraiki ditzakezu gure",
"developer APIs": "garatzaileentzako APIak.",
"How can we help you?": "Nola lagun zaitzakegu?",
"Troubleshooting": "Arazoen konponketa",
"Fix your problems": "Arazoak konpondu",
"Read the Docs": "Irakurri dokumentazioa",
"Common tasks": "Ataza arruntak",
"Other": "Beste edozein",
"Outbound Video Stream": "Bideoaren igorpena kanpora atera",
"Outbound Audio Stream": "Soinuaren igorpena kanpora atera",
"Stream started": "Zuzeneko igorpena abiatu da",
"Viewers": "Ikusleak",
"Peak viewer count": "Ikusle kopuruaren goren maila lortu",
"Outbound Stream Details": "Igorpenaren xehetasunak kanpora atera",
"Inbound Stream Details": "Igorpenaren xehetasunak barrura ekarri",
"Input": "Sarrera",
"Inbound Video Stream": "Bideoaren igorpena barrura sartu",
"Inbound Audio Stream": "Soinuaren igorpena barrura sartu",
"Current stream": "Oraingo igorpena",
"Last 12 hours": "Azken 12 orduetan",
"Last 24 hours": "Azken 24 orduetan",
"Last 7 days": "Azken 7 egunetan",
"Last 30 days": "Azken 30 egunetan",
"Last 3 months": "Azken 3 hilabeteetan",
"Last 6 months": "Azken 6 hilabeteetan",
"Viewer Info": "Ikus-entzuleen informazioa",
"Current viewers": "Uneko ikusleak",
"Max viewers this stream": "Igorpen honek izan duen gehiengo ikus-entzule kopurua",
"Max viewers last stream": "Azken igorpenak izan duen gehiengo ikus-entzule kopurua",
"max viewers": "gehiengo ikus-entzule kopurua",
"No viewer data has been collected yet": "Oraindik ez da ikus-entzuleei buruzko daturik jaso."
}
+29 -29
View File
@@ -1,16 +1,16 @@
{ {
"Notify": "Notify", "Notify": "Notifier",
"Level": "Level", "Level": "Niveau",
"Info": "Info", "Info": "Info",
"Warning": "Warning", "Warning": "Alerte",
"Error": "Error", "Error": "Erreur",
"Timestamp": "Timestamp", "Timestamp": "Horodatage",
"Message": "Message", "Message": "Message",
"Logs": "Logs", "Logs": "Journaux",
"Link": "Link", "Link": "Link",
"No news": "No news.", "No news": "No news.",
"News & Updates from Owncast": "News & Updates from Owncast", "News & Updates from Owncast": "News & Updates from Owncast",
"Use your broadcasting software": "Use your broadcasting software", "Use your broadcasting software": "Utilisez votre logiciel de diffusion",
"Learn how to point your existing software to your new server and start streaming your content": "Learn how to point your existing software to your new server and start streaming your content.", "Learn how to point your existing software to your new server and start streaming your content": "Learn how to point your existing software to your new server and start streaming your content.",
"View": "View", "View": "View",
"Overridden via command line": "Overridden via command line.", "Overridden via command line": "Overridden via command line.",
@@ -22,15 +22,15 @@
"List yourself in the Owncast Directory and show off your stream": { "List yourself in the Owncast Directory and show off your stream": {
" Enable it in": "List yourself in the Owncast Directory and show off your stream. Enable it in" " Enable it in": "List yourself in the Owncast Directory and show off your stream. Enable it in"
}, },
"settings": "settings.", "settings": "paramètres.",
"Add your Owncast instance to the Fediverse": "Add your Owncast instance to the Fediverse", "Add your Owncast instance to the Fediverse": "Add your Owncast instance to the Fediverse",
"Enable Owncast social features": "Enable Owncast social features", "Enable Owncast social features": "Enable Owncast social features",
"to have your instance join the Fediverse, allowing people to follow, share and engage with your live stream": "to have your instance join the Fediverse, allowing people to follow, share and engage with your live stream.", "to have your instance join the Fediverse, allowing people to follow, share and engage with your live stream": "to have your instance join the Fediverse, allowing people to follow, share and engage with your live stream.",
"No stream is active": "No stream is active", "No stream is active": "Aucune diffusion en cours",
"You should start one": "You should start one.", "You should start one": "You should start one.",
"Healthy Stream": "Healthy Stream", "Healthy Stream": "Healthy Stream",
"Yes": "Yes", "Yes": "Oui",
"No": "No", "No": "Non",
"Playback Health": "Playback Health", "Playback Health": "Playback Health",
"Stream health represents": "Stream health represents", "Stream health represents": "Stream health represents",
"of all known players": { "of all known players": {
@@ -39,53 +39,53 @@
"TROUBLESHOOT": "TROUBLESHOOT", "TROUBLESHOOT": "TROUBLESHOOT",
"Powered by Owncast": "Powered by Owncast", "Powered by Owncast": "Powered by Owncast",
"Documentation": "Documentation", "Documentation": "Documentation",
"Contribute": "Contribute", "Contribute": "Contribuer",
"Source": "Source", "Source": "Source",
"Skip to player": "Skip to player", "Skip to player": "Skip to player",
"Skip to offline message": "Skip to offline message", "Skip to offline message": "Skip to offline message",
"Skip to page content": "Skip to page content", "Skip to page content": "Skip to page content",
"Skip to footer": "Skip to footer", "Skip to footer": "Skip to footer",
"Chat will be available when the stream is live": "Chat will be available when the stream is live.", "Chat will be available when the stream is live": "Chat will be available when the stream is live.",
"Chat is offline": "Chat is offline", "Chat is offline": "Clavardage hors ligne",
"Stay updated!": "Stay updated!", "Stay updated!": "Stay updated!",
"Click and never miss future streams!": "Click and never miss future streams!", "Click and never miss future streams!": "Click and never miss future streams!",
"This stream is offline": "This stream is offline.", "This stream is offline": "This stream is offline.",
"the next time goes live": "the next time goes live", "the next time goes live": "the next time goes live",
"Follow": "Follow", "Follow": "Suivre",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live", "on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live ago", "Last live ago": "Dernière diffusion il y a {{timeAgo}}",
"Time": "Time", "Time": "Time",
"User": "User", "User": "Utilisateur",
"Visible messages": "Visible messages", "Visible messages": "Visible messages",
"Hidden messages": "Hidden messages", "Hidden messages": "Hidden messages",
"Chat Messages": "Chat Messages", "Chat Messages": "Chat Messages",
"Manage the messages from viewers that show up on your stream": "Manage the messages from viewers that show up on your stream.", "Manage the messages from viewers that show up on your stream": "Manage the messages from viewers that show up on your stream.",
"Show": "Show", "Show": "Afficher",
"Hide": "Hide", "Hide": "Masquer",
"Visit the": "Visit the", "Visit the": "Visit the",
"documentation": "documentation", "documentation": "documentation",
"to configure additional details about your viewers": "to configure additional details about your viewers.", "to configure additional details about your viewers": "to configure additional details about your viewers.",
"When a stream is active and chat is enabled, connected chat clients will be displayed here": "When a stream is active and chat is enabled, connected chat clients will be displayed here.", "When a stream is active and chat is enabled, connected chat clients will be displayed here": "When a stream is active and chat is enabled, connected chat clients will be displayed here.",
"Connected": "Connected", "Connected": "Connecté",
"offline": "offline", "offline": "offline",
"Banned Users": "Banned Users", "Banned Users": "Utilisateurs Bannis",
"IP Bans": "IP Bans", "IP Bans": "IP Bannies",
"Moderators": "Moderators", "Moderators": "Modérateurs",
"Hardware Info": "Hardware Info", "Hardware Info": "Hardware Info",
"Please wait": "Please wait", "Please wait": "Veuillez patienter",
"No hardware details have been collected yet": "No hardware details have been collected yet.", "No hardware details have been collected yet": "No hardware details have been collected yet.",
"CPU": "CPU", "CPU": "UCT",
"Memory": "Memory", "Memory": "Mémoire",
"Disk": "Disk", "Disk": "Disque",
"used": "used", "used": "used",
"I want to configure my owncast instance": "I want to configure my owncast instance", "I want to configure my owncast instance": "Je veux configurer mon instance Owncast",
"Learn more": "Learn more", "Learn more": "Learn more",
"Help configuring my broadcasting software": "Help configuring my broadcasting software", "Help configuring my broadcasting software": "Help configuring my broadcasting software",
"I want to embed my stream into another site": "I want to embed my stream into another site", "I want to embed my stream into another site": "I want to embed my stream into another site",
"I want to customize my website": "I want to customize my website", "I want to customize my website": "I want to customize my website",
"I want to tweak my video output": "I want to tweak my video output", "I want to tweak my video output": "I want to tweak my video output",
"I want to use an external storage provider": "I want to use an external storage provider", "I want to use an external storage provider": "I want to use an external storage provider",
"I found a bug": "I found a bug", "I found a bug": "J'ai trouvé un bogue",
"If you found a bug, then please": "If you found a bug, then please", "If you found a bug, then please": "If you found a bug, then please",
"let us know": "let us know", "let us know": "let us know",
"I have a general question": "I have a general question", "I have a general question": "I have a general question",
@@ -105,7 +105,7 @@
"Outbound Video Stream": "Outbound Video Stream", "Outbound Video Stream": "Outbound Video Stream",
"Outbound Audio Stream": "Outbound Audio Stream", "Outbound Audio Stream": "Outbound Audio Stream",
"Stream started": "Stream started", "Stream started": "Stream started",
"Viewers": "Viewers", "Viewers": "Spectateurs",
"Peak viewer count": "Peak viewer count", "Peak viewer count": "Peak viewer count",
"Outbound Stream Details": "Outbound Stream Details", "Outbound Stream Details": "Outbound Stream Details",
"Inbound Stream Details": "Inbound Stream Details", "Inbound Stream Details": "Inbound Stream Details",
+1 -1
View File
@@ -53,7 +53,7 @@
"the next time goes live": "the next time goes live", "the next time goes live": "the next time goes live",
"Follow": "Follow", "Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live", "on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live ago", "Last live ago": "An t-ainm beo deireanach {{timeAgo}} ó shin",
"Time": "Time", "Time": "Time",
"User": "User", "User": "User",
"Visible messages": "Visible messages", "Visible messages": "Visible messages",
+1 -1
View File
@@ -53,7 +53,7 @@
"the next time goes live": "the next time goes live", "the next time goes live": "the next time goes live",
"Follow": "Follow", "Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live", "on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live ago", "Last live ago": "पिछली बार लाइव {{timeAgo}} पहले",
"Time": "Time", "Time": "Time",
"User": "User", "User": "User",
"Visible messages": "Visible messages", "Visible messages": "Visible messages",
+1 -1
View File
@@ -53,7 +53,7 @@
"the next time goes live": "the next time goes live", "the next time goes live": "the next time goes live",
"Follow": "Follow", "Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live", "on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live ago", "Last live ago": "Zadnji put uživo {{timeAgo}} prije",
"Time": "Time", "Time": "Time",
"User": "User", "User": "User",
"Visible messages": "Visible messages", "Visible messages": "Visible messages",
+116 -116
View File
@@ -1,128 +1,128 @@
{ {
"Notify": "Notify", "Notify": "Notifica",
"Level": "Level", "Level": "Livello",
"Info": "Info", "Info": "Info",
"Warning": "Warning", "Warning": "Attenzione",
"Error": "Error", "Error": "Errore",
"Timestamp": "Timestamp", "Timestamp": "Data",
"Message": "Message", "Message": "Messaggio",
"Logs": "Logs", "Logs": "Registri",
"Link": "Link", "Link": "Collegamento",
"No news": "No news.", "No news": "Nessuna notizia.",
"News & Updates from Owncast": "News & Updates from Owncast", "News & Updates from Owncast": "Notizie & Aggiornamenti da Owncast",
"Use your broadcasting software": "Use your broadcasting software", "Use your broadcasting software": "Usa il tuo software di trasmissione",
"Learn how to point your existing software to your new server and start streaming your content": "Learn how to point your existing software to your new server and start streaming your content.", "Learn how to point your existing software to your new server and start streaming your content": "Scopri come puntare il software esistente sul tuo nuovo server e iniziare a trasmettere i tuoi contenuti.",
"View": "View", "View": "Visualizza",
"Overridden via command line": "Overridden via command line.", "Overridden via command line": "Sovrascrivi tramite riga di comando.",
"Embed your video onto other sites": "Embed your video onto other sites", "Embed your video onto other sites": "Incorpora il tuo video in altri siti",
"Learn how you can add your Owncast stream to other sites you control": "Learn how you can add your Owncast stream to other sites you control.", "Learn how you can add your Owncast stream to other sites you control": "Scopri come puoi aggiungere il tuo flusso Owncast ad altri siti che controlli.",
"Chat is disabled": "Chat is disabled", "Chat is disabled": "La chat è disattivata",
"Chat will continue to be disabled until you begin a live stream": "Chat will continue to be disabled until you begin a live stream.", "Chat will continue to be disabled until you begin a live stream": "La chat continuerà ad essere disabilitata fino a quando non inizi un live stream.",
"Find an audience on the Owncast Directory": "Find an audience on the Owncast Directory", "Find an audience on the Owncast Directory": "Trova un pubblico nella directory di Owncast",
"List yourself in the Owncast Directory and show off your stream": { "List yourself in the Owncast Directory and show off your stream": {
" Enable it in": "List yourself in the Owncast Directory and show off your stream. Enable it in" " Enable it in": "Elenca te stesso nella directory di Owncast e mostra il tuo flusso. Abilitalo in"
}, },
"settings": "settings.", "settings": "impostazioni.",
"Add your Owncast instance to the Fediverse": "Add your Owncast instance to the Fediverse", "Add your Owncast instance to the Fediverse": "Aggiungi la tua istanza Owncast al Fediverso",
"Enable Owncast social features": "Enable Owncast social features", "Enable Owncast social features": "Abilita funzionalità sociali di Owncast",
"to have your instance join the Fediverse, allowing people to follow, share and engage with your live stream": "to have your instance join the Fediverse, allowing people to follow, share and engage with your live stream.", "to have your instance join the Fediverse, allowing people to follow, share and engage with your live stream": "per far partecipare la tua istanza a Fediverse, permettendo alle persone di seguire, condividere e impegnarsi con il tuo live stream.",
"No stream is active": "No stream is active", "No stream is active": "Nessun flusso attivo",
"You should start one": "You should start one.", "You should start one": "Dovreste iniziarne uno.",
"Healthy Stream": "Healthy Stream", "Healthy Stream": "Flusso Sano",
"Yes": "Yes", "Yes": "Si",
"No": "No", "No": "No",
"Playback Health": "Playback Health", "Playback Health": "Salute Riproduzione",
"Stream health represents": "Stream health represents", "Stream health represents": "Salute Flusso rappresenta",
"of all known players": { "of all known players": {
" Other player status is unknown": "of all known players. Other player status is unknown." " Other player status is unknown": "di tutti i lettori conosciuti. Lo stato di altri lettori è sconosciuto."
}, },
"TROUBLESHOOT": "TROUBLESHOOT", "TROUBLESHOOT": "TROUBLESHOOT",
"Powered by Owncast": "Powered by Owncast", "Powered by Owncast": "Fornito da Owncast",
"Documentation": "Documentation", "Documentation": "Documentazione",
"Contribute": "Contribute", "Contribute": "Contribuisci",
"Source": "Source", "Source": "Fonte",
"Skip to player": "Skip to player", "Skip to player": "Vai al lettore video",
"Skip to offline message": "Skip to offline message", "Skip to offline message": "Salta al messaggio fuori rete",
"Skip to page content": "Skip to page content", "Skip to page content": "Salta al contenuto della pagina",
"Skip to footer": "Skip to footer", "Skip to footer": "Salta al piè di pagina",
"Chat will be available when the stream is live": "Chat will be available when the stream is live.", "Chat will be available when the stream is live": "La chat sarà disponibile quando lo stream è in diretta.",
"Chat is offline": "Chat is offline", "Chat is offline": "La chat è offline",
"Stay updated!": "Stay updated!", "Stay updated!": "Resta Aggiornato!",
"Click and never miss future streams!": "Click and never miss future streams!", "Click and never miss future streams!": "Clicca e non perderti mai le dirette future!",
"This stream is offline": "This stream is offline.", "This stream is offline": "Questa diretta è offline.",
"the next time goes live": "the next time goes live", "the next time goes live": "la prossima volta che andrà in diretta",
"Follow": "Follow", "Follow": "Segui",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live", "on the Fediverse to see the next time goes live": "sul Fediverso per vedere la prossima volta che andrà in diretta",
"Last live ago": "Last live ago", "Last live ago": "Ultima diretta {{timeAgo}} fa",
"Time": "Time", "Time": "Tempo",
"User": "User", "User": "Utente",
"Visible messages": "Visible messages", "Visible messages": "Messaggi visibili",
"Hidden messages": "Hidden messages", "Hidden messages": "Messaggi nascosti",
"Chat Messages": "Chat Messages", "Chat Messages": "Messaggi chat",
"Manage the messages from viewers that show up on your stream": "Manage the messages from viewers that show up on your stream.", "Manage the messages from viewers that show up on your stream": "Gestisci i messaggi dagli spettatori che appaiono sul tuo flusso.",
"Show": "Show", "Show": "Mostra",
"Hide": "Hide", "Hide": "Nascondi",
"Visit the": "Visit the", "Visit the": "Visita il",
"documentation": "documentation", "documentation": "documentazione",
"to configure additional details about your viewers": "to configure additional details about your viewers.", "to configure additional details about your viewers": "per configurare ulteriori dettagli sui tuoi spettatori.",
"When a stream is active and chat is enabled, connected chat clients will be displayed here": "When a stream is active and chat is enabled, connected chat clients will be displayed here.", "When a stream is active and chat is enabled, connected chat clients will be displayed here": "Quando uno stream è attivo e la chat è abilitata, i client di chat connessi verranno visualizzati qui.",
"Connected": "Connected", "Connected": "Connesso",
"offline": "offline", "offline": "offline",
"Banned Users": "Banned Users", "Banned Users": "Utenti bloccati",
"IP Bans": "IP Bans", "IP Bans": "IP Bannati",
"Moderators": "Moderators", "Moderators": "Moderatori",
"Hardware Info": "Hardware Info", "Hardware Info": "Informazioni Hardware",
"Please wait": "Please wait", "Please wait": "Attendere prego",
"No hardware details have been collected yet": "No hardware details have been collected yet.", "No hardware details have been collected yet": "Non sono stati ancora raccolti dettagli hardware.",
"CPU": "CPU", "CPU": "CPU",
"Memory": "Memory", "Memory": "Memoria",
"Disk": "Disk", "Disk": "Disco",
"used": "used", "used": "usato",
"I want to configure my owncast instance": "I want to configure my owncast instance", "I want to configure my owncast instance": "Voglio configurare la mia istanza owncast",
"Learn more": "Learn more", "Learn more": "Per saperne di più",
"Help configuring my broadcasting software": "Help configuring my broadcasting software", "Help configuring my broadcasting software": "Aiuta a configurare il mio software di trasmissione",
"I want to embed my stream into another site": "I want to embed my stream into another site", "I want to embed my stream into another site": "Voglio incorporare il mio flusso in un altro sito",
"I want to customize my website": "I want to customize my website", "I want to customize my website": "Voglio personalizzare il mio sito web",
"I want to tweak my video output": "I want to tweak my video output", "I want to tweak my video output": "Voglio modificare la mia uscita video",
"I want to use an external storage provider": "I want to use an external storage provider", "I want to use an external storage provider": "Voglio usare un provider di archiviazione esterno",
"I found a bug": "I found a bug", "I found a bug": "Ho trovato un bug",
"If you found a bug, then please": "If you found a bug, then please", "If you found a bug, then please": "Se hai trovato un bug, per favore",
"let us know": "let us know", "let us know": "faccelo sapere",
"I have a general question": "I have a general question", "I have a general question": "Ho una domanda generale",
"Most general questions are answered in our": "Most general questions are answered in our", "Most general questions are answered in our": "Le domande più generali sono risposte nel nostro",
"FAQ": "FAQ", "FAQ": "FAQ",
"or exist in our": "or exist in our", "or exist in our": "o esiste nel nostro",
"discussions": "discussions", "discussions": "discussioni",
"I want to build add-ons for Owncast": "I want to build add-ons for Owncast", "I want to build add-ons for Owncast": "Voglio costruire componenti aggiuntivi per Owncast",
"You can build your own bots, overlays, tools and add-ons with our": "You can build your own bots, overlays, tools and add-ons with our", "You can build your own bots, overlays, tools and add-ons with our": "È possibile costruire i propri bot, sovrapposizioni, strumenti e componenti aggiuntivi con il nostro",
"developer APIs": "developer APIs.", "developer APIs": "API per sviluppatori.",
"How can we help you?": "How can we help you?", "How can we help you?": "Come possiamo aiutarti?",
"Troubleshooting": "Troubleshooting", "Troubleshooting": "Risoluzione problemi",
"Fix your problems": "Fix your problems", "Fix your problems": "Risolvi i tuoi problemi",
"Read the Docs": "Read the Docs", "Read the Docs": "Leggi la documentazione",
"Common tasks": "Common tasks", "Common tasks": "Attività comuni",
"Other": "Other", "Other": "Altro",
"Outbound Video Stream": "Outbound Video Stream", "Outbound Video Stream": "Flusso Video In Uscita",
"Outbound Audio Stream": "Outbound Audio Stream", "Outbound Audio Stream": "Flusso Audio In Uscita",
"Stream started": "Stream started", "Stream started": "Stream avviato",
"Viewers": "Viewers", "Viewers": "Spettatori",
"Peak viewer count": "Peak viewer count", "Peak viewer count": "Picco conteggio spettatore",
"Outbound Stream Details": "Outbound Stream Details", "Outbound Stream Details": "Dettagli Stream In Uscita",
"Inbound Stream Details": "Inbound Stream Details", "Inbound Stream Details": "Dettagli Stream In Entrata",
"Input": "Input", "Input": "Ingresso",
"Inbound Video Stream": "Inbound Video Stream", "Inbound Video Stream": "Flusso Video In Entrata",
"Inbound Audio Stream": "Inbound Audio Stream", "Inbound Audio Stream": "Flusso Audio In Entrata",
"Current stream": "Current stream", "Current stream": "Flusso corrente",
"Last 12 hours": "Last 12 hours", "Last 12 hours": "Ultime 12 ore",
"Last 24 hours": "Last 24 hours", "Last 24 hours": "Ultime 24 ore",
"Last 7 days": "Last 7 days", "Last 7 days": "Ultimi 7 giorni",
"Last 30 days": "Last 30 days", "Last 30 days": "Ultimi 30 giorni",
"Last 3 months": "Last 3 months", "Last 3 months": "Ultimi 3 mesi",
"Last 6 months": "Last 6 months", "Last 6 months": "Ultimi 6 mesi",
"Viewer Info": "Viewer Info", "Viewer Info": "Informazioni Spettatore",
"Current viewers": "Current viewers", "Current viewers": "Spettatori attuali",
"Max viewers this stream": "Max viewers this stream", "Max viewers this stream": "Massimo spettatori di questo flusso",
"Max viewers last stream": "Max viewers last stream", "Max viewers last stream": "Massimo spettatori ultimo flusso",
"max viewers": "max viewers", "max viewers": "massimo spettatori",
"No viewer data has been collected yet": "No viewer data has been collected yet." "No viewer data has been collected yet": "Non sono stati ancora raccolti dati degli spettatori."
} }
+1 -1
View File
@@ -53,7 +53,7 @@
"the next time goes live": "the next time goes live", "the next time goes live": "the next time goes live",
"Follow": "Follow", "Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live", "on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live ago", "Last live ago": "前回のライブ {{timeAgo}} 前",
"Time": "Time", "Time": "Time",
"User": "User", "User": "User",
"Visible messages": "Visible messages", "Visible messages": "Visible messages",
+1 -1
View File
@@ -53,7 +53,7 @@
"the next time goes live": "the next time goes live", "the next time goes live": "the next time goes live",
"Follow": "Follow", "Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live", "on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live ago", "Last live ago": "마지막 라이브 {{timeAgo}} 전",
"Time": "Time", "Time": "Time",
"User": "User", "User": "User",
"Visible messages": "Visible messages", "Visible messages": "Visible messages",
+1 -1
View File
@@ -53,7 +53,7 @@
"the next time goes live": "the next time goes live", "the next time goes live": "the next time goes live",
"Follow": "Follow", "Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live", "on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live ago", "Last live ago": "Siaran langsung terakhir {{timeAgo}} yang lalu",
"Time": "Time", "Time": "Time",
"User": "User", "User": "User",
"Visible messages": "Visible messages", "Visible messages": "Visible messages",
+21 -21
View File
@@ -5,14 +5,14 @@
"Warning": "Waarschuwing", "Warning": "Waarschuwing",
"Error": "Fout", "Error": "Fout",
"Timestamp": "Timestamp", "Timestamp": "Timestamp",
"Message": "Message", "Message": "Bericht",
"Logs": "Logs", "Logs": "Logs",
"Link": "Link", "Link": "Link",
"No news": "No news.", "No news": "Geen nieuws.",
"News & Updates from Owncast": "News & Updates from Owncast", "News & Updates from Owncast": "News & Updates from Owncast",
"Use your broadcasting software": "Use your broadcasting software", "Use your broadcasting software": "Use your broadcasting software",
"Learn how to point your existing software to your new server and start streaming your content": "Learn how to point your existing software to your new server and start streaming your content.", "Learn how to point your existing software to your new server and start streaming your content": "Learn how to point your existing software to your new server and start streaming your content.",
"View": "View", "View": "Bekijk",
"Overridden via command line": "Overridden via command line.", "Overridden via command line": "Overridden via command line.",
"Embed your video onto other sites": "Embed your video onto other sites", "Embed your video onto other sites": "Embed your video onto other sites",
"Learn how you can add your Owncast stream to other sites you control": "Learn how you can add your Owncast stream to other sites you control.", "Learn how you can add your Owncast stream to other sites you control": "Learn how you can add your Owncast stream to other sites you control.",
@@ -29,8 +29,8 @@
"No stream is active": "No stream is active", "No stream is active": "No stream is active",
"You should start one": "You should start one.", "You should start one": "You should start one.",
"Healthy Stream": "Healthy Stream", "Healthy Stream": "Healthy Stream",
"Yes": "Yes", "Yes": "Ja",
"No": "No", "No": "Nee",
"Playback Health": "Playback Health", "Playback Health": "Playback Health",
"Stream health represents": "Stream health represents", "Stream health represents": "Stream health represents",
"of all known players": { "of all known players": {
@@ -53,7 +53,7 @@
"the next time goes live": "the next time goes live", "the next time goes live": "the next time goes live",
"Follow": "Follow", "Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live", "on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live ago", "Last live ago": "Laatste live {{timeAgo}} geleden",
"Time": "Time", "Time": "Time",
"User": "User", "User": "User",
"Visible messages": "Visible messages", "Visible messages": "Visible messages",
@@ -66,18 +66,18 @@
"documentation": "documentation", "documentation": "documentation",
"to configure additional details about your viewers": "to configure additional details about your viewers.", "to configure additional details about your viewers": "to configure additional details about your viewers.",
"When a stream is active and chat is enabled, connected chat clients will be displayed here": "When a stream is active and chat is enabled, connected chat clients will be displayed here.", "When a stream is active and chat is enabled, connected chat clients will be displayed here": "When a stream is active and chat is enabled, connected chat clients will be displayed here.",
"Connected": "Connected", "Connected": "Verbonden",
"offline": "offline", "offline": "offline",
"Banned Users": "Banned Users", "Banned Users": "Verbannen gebruikers",
"IP Bans": "IP Bans", "IP Bans": "IP Bans",
"Moderators": "Moderators", "Moderators": "Moderators",
"Hardware Info": "Hardware Info", "Hardware Info": "Hardware Info",
"Please wait": "Please wait", "Please wait": "Even geduld...",
"No hardware details have been collected yet": "No hardware details have been collected yet.", "No hardware details have been collected yet": "No hardware details have been collected yet.",
"CPU": "CPU", "CPU": "CPU",
"Memory": "Memory", "Memory": "Geheugen",
"Disk": "Disk", "Disk": "Schijfruimte",
"used": "used", "used": "gebruikt",
"I want to configure my owncast instance": "I want to configure my owncast instance", "I want to configure my owncast instance": "I want to configure my owncast instance",
"Learn more": "Learn more", "Learn more": "Learn more",
"Help configuring my broadcasting software": "Help configuring my broadcasting software", "Help configuring my broadcasting software": "Help configuring my broadcasting software",
@@ -99,26 +99,26 @@
"How can we help you?": "How can we help you?", "How can we help you?": "How can we help you?",
"Troubleshooting": "Troubleshooting", "Troubleshooting": "Troubleshooting",
"Fix your problems": "Fix your problems", "Fix your problems": "Fix your problems",
"Read the Docs": "Read the Docs", "Read the Docs": "Lees de documentatie",
"Common tasks": "Common tasks", "Common tasks": "Common tasks",
"Other": "Other", "Other": "Other",
"Outbound Video Stream": "Outbound Video Stream", "Outbound Video Stream": "Outbound Video Stream",
"Outbound Audio Stream": "Outbound Audio Stream", "Outbound Audio Stream": "Outbound Audio Stream",
"Stream started": "Stream started", "Stream started": "Stream started",
"Viewers": "Viewers", "Viewers": "Kijkers",
"Peak viewer count": "Peak viewer count", "Peak viewer count": "Peak viewer count",
"Outbound Stream Details": "Outbound Stream Details", "Outbound Stream Details": "Outbound Stream Details",
"Inbound Stream Details": "Inbound Stream Details", "Inbound Stream Details": "Inbound Stream Details",
"Input": "Input", "Input": "Input",
"Inbound Video Stream": "Inbound Video Stream", "Inbound Video Stream": "Inbound Video Stream",
"Inbound Audio Stream": "Inbound Audio Stream", "Inbound Audio Stream": "Inbound Audio Stream",
"Current stream": "Current stream", "Current stream": "Huidige stream",
"Last 12 hours": "Last 12 hours", "Last 12 hours": "Laatste 12 uur",
"Last 24 hours": "Last 24 hours", "Last 24 hours": "Laatste 24 uur",
"Last 7 days": "Last 7 days", "Last 7 days": "Afgelopen 7 dagen",
"Last 30 days": "Last 30 days", "Last 30 days": "Afgelopen 30 dagen",
"Last 3 months": "Last 3 months", "Last 3 months": "Afgelopen 3 maanden",
"Last 6 months": "Last 6 months", "Last 6 months": "Afgelopen 6 maanden",
"Viewer Info": "Viewer Info", "Viewer Info": "Viewer Info",
"Current viewers": "Current viewers", "Current viewers": "Current viewers",
"Max viewers this stream": "Max viewers this stream", "Max viewers this stream": "Max viewers this stream",
+1 -1
View File
@@ -53,7 +53,7 @@
"the next time goes live": "the next time goes live", "the next time goes live": "the next time goes live",
"Follow": "Follow", "Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live", "on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live ago", "Last live ago": "Sist live {{timeAgo}} siden",
"Time": "Time", "Time": "Time",
"User": "User", "User": "User",
"Visible messages": "Visible messages", "Visible messages": "Visible messages",
+1 -1
View File
@@ -53,7 +53,7 @@
"the next time goes live": "the next time goes live", "the next time goes live": "the next time goes live",
"Follow": "Follow", "Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live", "on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live ago", "Last live ago": "ਪਿਛਲੀਆਂ ਲਾਈਵ {{timeAgo}} ਪਹਿਲਾਂ",
"Time": "Time", "Time": "Time",
"User": "User", "User": "User",
"Visible messages": "Visible messages", "Visible messages": "Visible messages",
+1 -1
View File
@@ -53,7 +53,7 @@
"the next time goes live": "the next time goes live", "the next time goes live": "the next time goes live",
"Follow": "Follow", "Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live", "on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live ago", "Last live ago": "Ostatnio na żywo {{timeAgo}} temu",
"Time": "Time", "Time": "Time",
"User": "User", "User": "User",
"Visible messages": "Visible messages", "Visible messages": "Visible messages",
+1 -1
View File
@@ -53,7 +53,7 @@
"the next time goes live": "the next time goes live", "the next time goes live": "the next time goes live",
"Follow": "Follow", "Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live", "on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live ago", "Last live ago": "Última transmissão ao vivo {{timeAgo}} ago",
"Time": "Time", "Time": "Time",
"User": "User", "User": "User",
"Visible messages": "Visible messages", "Visible messages": "Visible messages",
+1 -1
View File
@@ -53,7 +53,7 @@
"the next time goes live": "the next time goes live", "the next time goes live": "the next time goes live",
"Follow": "Follow", "Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live", "on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live ago", "Last live ago": "Последняя живая {{timeAgo}} назад",
"Time": "Time", "Time": "Time",
"User": "User", "User": "User",
"Visible messages": "Visible messages", "Visible messages": "Visible messages",
+1 -1
View File
@@ -53,7 +53,7 @@
"the next time goes live": "the next time goes live", "the next time goes live": "the next time goes live",
"Follow": "Follow", "Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live", "on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live ago", "Last live ago": "Senast live {{timeAgo}} sedan",
"Time": "Time", "Time": "Time",
"User": "User", "User": "User",
"Visible messages": "Visible messages", "Visible messages": "Visible messages",
+1 -1
View File
@@ -53,7 +53,7 @@
"the next time goes live": "the next time goes live", "the next time goes live": "the next time goes live",
"Follow": "Follow", "Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live", "on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live ago", "Last live ago": "ถ่ายทอดสดล่าสุดเมื่อ {{timeAgo}} ที่ผ่านมา",
"Time": "Time", "Time": "Time",
"User": "User", "User": "User",
"Visible messages": "Visible messages", "Visible messages": "Visible messages",
+1 -1
View File
@@ -53,7 +53,7 @@
"the next time goes live": "the next time goes live", "the next time goes live": "the next time goes live",
"Follow": "Follow", "Follow": "Follow",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live", "on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live ago", "Last live ago": "Lần phát trực tiếp cuối cùng {{timeAgo}} trước",
"Time": "Time", "Time": "Time",
"User": "User", "User": "User",
"Visible messages": "Visible messages", "Visible messages": "Visible messages",
+53 -53
View File
@@ -1,22 +1,22 @@
{ {
"Notify": "Notify", "Notify": "通知",
"Level": "Level", "Level": "水平",
"Info": "Info", "Info": "資訊",
"Warning": "Warning", "Warning": "警告",
"Error": "Error", "Error": "錯誤",
"Timestamp": "Timestamp", "Timestamp": "時間戳記",
"Message": "Message", "Message": "訊息",
"Logs": "Logs", "Logs": "紀錄",
"Link": "Link", "Link": "Link",
"No news": "No news.", "No news": "沒有新聞。",
"News & Updates from Owncast": "News & Updates from Owncast", "News & Updates from Owncast": "News & Updates from Owncast",
"Use your broadcasting software": "Use your broadcasting software", "Use your broadcasting software": "Use your broadcasting software",
"Learn how to point your existing software to your new server and start streaming your content": "Learn how to point your existing software to your new server and start streaming your content.", "Learn how to point your existing software to your new server and start streaming your content": "Learn how to point your existing software to your new server and start streaming your content.",
"View": "View", "View": "查看",
"Overridden via command line": "Overridden via command line.", "Overridden via command line": "Overridden via command line.",
"Embed your video onto other sites": "Embed your video onto other sites", "Embed your video onto other sites": "Embed your video onto other sites",
"Learn how you can add your Owncast stream to other sites you control": "Learn how you can add your Owncast stream to other sites you control.", "Learn how you can add your Owncast stream to other sites you control": "Learn how you can add your Owncast stream to other sites you control.",
"Chat is disabled": "Chat is disabled", "Chat is disabled": "聊天已禁用。",
"Chat will continue to be disabled until you begin a live stream": "Chat will continue to be disabled until you begin a live stream.", "Chat will continue to be disabled until you begin a live stream": "Chat will continue to be disabled until you begin a live stream.",
"Find an audience on the Owncast Directory": "Find an audience on the Owncast Directory", "Find an audience on the Owncast Directory": "Find an audience on the Owncast Directory",
"List yourself in the Owncast Directory and show off your stream": { "List yourself in the Owncast Directory and show off your stream": {
@@ -29,83 +29,83 @@
"No stream is active": "No stream is active", "No stream is active": "No stream is active",
"You should start one": "You should start one.", "You should start one": "You should start one.",
"Healthy Stream": "Healthy Stream", "Healthy Stream": "Healthy Stream",
"Yes": "Yes", "Yes": "是",
"No": "No", "No": "否",
"Playback Health": "Playback Health", "Playback Health": "Playback Health",
"Stream health represents": "Stream health represents", "Stream health represents": "Stream health represents",
"of all known players": { "of all known players": {
" Other player status is unknown": "of all known players. Other player status is unknown." " Other player status is unknown": "of all known players. Other player status is unknown."
}, },
"TROUBLESHOOT": "TROUBLESHOOT", "TROUBLESHOOT": "排除故障",
"Powered by Owncast": "Powered by Owncast", "Powered by Owncast": "由Owncast提供支援",
"Documentation": "Documentation", "Documentation": "文件",
"Contribute": "Contribute", "Contribute": "Contribute",
"Source": "Source", "Source": "源導",
"Skip to player": "Skip to player", "Skip to player": "Skip to player",
"Skip to offline message": "Skip to offline message", "Skip to offline message": "跳到至離線訊息",
"Skip to page content": "Skip to page content", "Skip to page content": "Skip to page content",
"Skip to footer": "Skip to footer", "Skip to footer": "跳到至頁腳",
"Chat will be available when the stream is live": "Chat will be available when the stream is live.", "Chat will be available when the stream is live": "Chat will be available when the stream is live.",
"Chat is offline": "Chat is offline", "Chat is offline": "聊天室已離線。",
"Stay updated!": "Stay updated!", "Stay updated!": "Stay updated!",
"Click and never miss future streams!": "Click and never miss future streams!", "Click and never miss future streams!": "Click and never miss future streams!",
"This stream is offline": "This stream is offline.", "This stream is offline": "實況已離線。",
"the next time goes live": "the next time goes live", "the next time goes live": "the next time goes live",
"Follow": "Follow", "Follow": "追隨",
"on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live", "on the Fediverse to see the next time goes live": "on the Fediverse to see the next time goes live",
"Last live ago": "Last live ago", "Last live ago": "上次實況時間:{{timeAgo}}前",
"Time": "Time", "Time": "時間",
"User": "User", "User": "用戶",
"Visible messages": "Visible messages", "Visible messages": "Visible messages",
"Hidden messages": "Hidden messages", "Hidden messages": "Hidden messages",
"Chat Messages": "Chat Messages", "Chat Messages": "聊天訊息",
"Manage the messages from viewers that show up on your stream": "Manage the messages from viewers that show up on your stream.", "Manage the messages from viewers that show up on your stream": "Manage the messages from viewers that show up on your stream.",
"Show": "Show", "Show": "顯示",
"Hide": "Hide", "Hide": "隱藏",
"Visit the": "Visit the", "Visit the": "Visit the",
"documentation": "documentation", "documentation": "documentation",
"to configure additional details about your viewers": "to configure additional details about your viewers.", "to configure additional details about your viewers": "to configure additional details about your viewers.",
"When a stream is active and chat is enabled, connected chat clients will be displayed here": "When a stream is active and chat is enabled, connected chat clients will be displayed here.", "When a stream is active and chat is enabled, connected chat clients will be displayed here": "When a stream is active and chat is enabled, connected chat clients will be displayed here.",
"Connected": "Connected", "Connected": "已連接",
"offline": "offline", "offline": "已離線",
"Banned Users": "Banned Users", "Banned Users": "被遮蔽的用戶",
"IP Bans": "IP Bans", "IP Bans": "IP 封鎖",
"Moderators": "Moderators", "Moderators": "版主",
"Hardware Info": "Hardware Info", "Hardware Info": "硬體資訊",
"Please wait": "Please wait", "Please wait": "請稍後",
"No hardware details have been collected yet": "No hardware details have been collected yet.", "No hardware details have been collected yet": "No hardware details have been collected yet.",
"CPU": "CPU", "CPU": "中央處理器",
"Memory": "Memory", "Memory": "記憶體",
"Disk": "Disk", "Disk": "磁碟",
"used": "used", "used": "使用情況",
"I want to configure my owncast instance": "I want to configure my owncast instance", "I want to configure my owncast instance": "I want to configure my owncast instance",
"Learn more": "Learn more", "Learn more": "了解更多",
"Help configuring my broadcasting software": "Help configuring my broadcasting software", "Help configuring my broadcasting software": "Help configuring my broadcasting software",
"I want to embed my stream into another site": "I want to embed my stream into another site", "I want to embed my stream into another site": "I want to embed my stream into another site",
"I want to customize my website": "I want to customize my website", "I want to customize my website": "I want to customize my website",
"I want to tweak my video output": "I want to tweak my video output", "I want to tweak my video output": "I want to tweak my video output",
"I want to use an external storage provider": "I want to use an external storage provider", "I want to use an external storage provider": "I want to use an external storage provider",
"I found a bug": "I found a bug", "I found a bug": "报告 Bug",
"If you found a bug, then please": "If you found a bug, then please", "If you found a bug, then please": "如果您發現 Bug,請",
"let us know": "let us know", "let us know": "告知我們",
"I have a general question": "I have a general question", "I have a general question": "我有一個一般性問題",
"Most general questions are answered in our": "Most general questions are answered in our", "Most general questions are answered in our": "Most general questions are answered in our",
"FAQ": "FAQ", "FAQ": "常見問題",
"or exist in our": "or exist in our", "or exist in our": "or exist in our",
"discussions": "discussions", "discussions": "discussions",
"I want to build add-ons for Owncast": "I want to build add-ons for Owncast", "I want to build add-ons for Owncast": "I want to build add-ons for Owncast",
"You can build your own bots, overlays, tools and add-ons with our": "You can build your own bots, overlays, tools and add-ons with our", "You can build your own bots, overlays, tools and add-ons with our": "You can build your own bots, overlays, tools and add-ons with our",
"developer APIs": "developer APIs.", "developer APIs": "developer APIs.",
"How can we help you?": "How can we help you?", "How can we help you?": "我們該怎樣幫助你?",
"Troubleshooting": "Troubleshooting", "Troubleshooting": "疑難排解",
"Fix your problems": "Fix your problems", "Fix your problems": "Fix your problems",
"Read the Docs": "Read the Docs", "Read the Docs": "閱讀文件",
"Common tasks": "Common tasks", "Common tasks": "Common tasks",
"Other": "Other", "Other": "其他",
"Outbound Video Stream": "Outbound Video Stream", "Outbound Video Stream": "Outbound Video Stream",
"Outbound Audio Stream": "Outbound Audio Stream", "Outbound Audio Stream": "Outbound Audio Stream",
"Stream started": "Stream started", "Stream started": "實況已開始",
"Viewers": "Viewers", "Viewers": "瀏覽者",
"Peak viewer count": "Peak viewer count", "Peak viewer count": "Peak viewer count",
"Outbound Stream Details": "Outbound Stream Details", "Outbound Stream Details": "Outbound Stream Details",
"Inbound Stream Details": "Inbound Stream Details", "Inbound Stream Details": "Inbound Stream Details",
@@ -119,8 +119,8 @@
"Last 30 days": "Last 30 days", "Last 30 days": "Last 30 days",
"Last 3 months": "Last 3 months", "Last 3 months": "Last 3 months",
"Last 6 months": "Last 6 months", "Last 6 months": "Last 6 months",
"Viewer Info": "Viewer Info", "Viewer Info": "觀看人訊息",
"Current viewers": "Current viewers", "Current viewers": "目前收看人數",
"Max viewers this stream": "Max viewers this stream", "Max viewers this stream": "Max viewers this stream",
"Max viewers last stream": "Max viewers last stream", "Max viewers last stream": "Max viewers last stream",
"max viewers": "max viewers", "max viewers": "max viewers",
+133 -75
View File
@@ -57,7 +57,7 @@
"workbox-precaching": "^7.0.0", "workbox-precaching": "^7.0.0",
"workbox-window": "^7.0.0", "workbox-window": "^7.0.0",
"xstate": "4.38.3", "xstate": "4.38.3",
"yaml": "2.6.0" "yaml": "2.7.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.26.0", "@babel/core": "7.26.0",
@@ -86,7 +86,7 @@
"@types/classnames": "2.3.4", "@types/classnames": "2.3.4",
"@types/jest": "^29.5.0", "@types/jest": "^29.5.0",
"@types/markdown-it": "14.1.2", "@types/markdown-it": "14.1.2",
"@types/node": "22.10.6", "@types/node": "22.10.7",
"@types/prop-types": "15.7.14", "@types/prop-types": "15.7.14",
"@types/react": "18.3.18", "@types/react": "18.3.18",
"@types/react-linkify": "1.0.4", "@types/react-linkify": "1.0.4",
@@ -96,7 +96,7 @@
"@typescript-eslint/eslint-plugin": "8.13.0", "@typescript-eslint/eslint-plugin": "8.13.0",
"@typescript-eslint/parser": "8.13.0", "@typescript-eslint/parser": "8.13.0",
"babel-loader": "9.2.1", "babel-loader": "9.2.1",
"chromatic": "11.24.0", "chromatic": "11.25.0",
"css-loader": "7.1.2", "css-loader": "7.1.2",
"cypress": "^13.2.0", "cypress": "^13.2.0",
"eslint": "8.57.1", "eslint": "8.57.1",
@@ -106,9 +106,9 @@
"eslint-plugin-import": "^2.27.5", "eslint-plugin-import": "^2.27.5",
"eslint-plugin-jsx-a11y": "6.10.2", "eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-prettier": "^5.0.0", "eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-react": "7.37.2", "eslint-plugin-react": "7.37.4",
"eslint-plugin-react-hooks": "4.6.2", "eslint-plugin-react-hooks": "4.6.2",
"eslint-plugin-storybook": "0.11.0", "eslint-plugin-storybook": "0.11.2",
"handlebars": "^4.7.7", "handlebars": "^4.7.7",
"html-webpack-plugin": "5.6.3", "html-webpack-plugin": "5.6.3",
"install": "^0.13.0", "install": "^0.13.0",
@@ -121,7 +121,7 @@
"postcss": "^8.4.31", "postcss": "^8.4.31",
"prettier": "3.4.2", "prettier": "3.4.2",
"resolve-url-loader": "^5.0.0", "resolve-url-loader": "^5.0.0",
"sass": "1.80.6", "sass": "1.83.4",
"sass-loader": "^14.0.0", "sass-loader": "^14.0.0",
"storybook": "^8.3.6", "storybook": "^8.3.6",
"storybook-addon-fetch-mock": "^2.0.1", "storybook-addon-fetch-mock": "^2.0.1",
@@ -132,7 +132,7 @@
"stylelint-config-standard": "^36.0.0", "stylelint-config-standard": "^36.0.0",
"stylelint-config-standard-scss": "^14.0.0", "stylelint-config-standard-scss": "^14.0.0",
"ts-jest": "^29.1.0", "ts-jest": "^29.1.0",
"typescript": "5.6.3" "typescript": "5.7.3"
} }
}, },
"node_modules/@adobe/css-tools": { "node_modules/@adobe/css-tools": {
@@ -5305,6 +5305,41 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@keyv/serialize": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.0.2.tgz",
"integrity": "sha512-+E/LyaAeuABniD/RvUezWVXKpeuvwLEA9//nE9952zBaOdBd2mQ3pPoM8cUe2X6IcMByfuSLzmYqnYshG60+HQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"buffer": "^6.0.3"
}
},
"node_modules/@keyv/serialize/node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"node_modules/@kurkle/color": { "node_modules/@kurkle/color": {
"version": "0.3.4", "version": "0.3.4",
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
@@ -9128,9 +9163,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "22.10.6", "version": "22.10.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.6.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz",
"integrity": "sha512-qNiuwC4ZDAUNcY47xgaSuS92cjf8JbSUoaKS77bmLG1rU7MlATVSiw/IlrjtIyyskXBZ8KkNfjK/P5na7rgXbQ==", "integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~6.20.0" "undici-types": "~6.20.0"
@@ -11692,6 +11727,27 @@
"node": ">=10.16.0" "node": ">=10.16.0"
} }
}, },
"node_modules/cacheable": {
"version": "1.8.7",
"resolved": "https://registry.npmjs.org/cacheable/-/cacheable-1.8.7.tgz",
"integrity": "sha512-AbfG7dAuYNjYxFUtL1lAqmlWdxczCJ47w7cFjhGcnGnUdwSo6VgmSojfoW3tUI12HUkgTJ5kqj78yyq6TsFtlg==",
"dev": true,
"license": "MIT",
"dependencies": {
"hookified": "^1.6.0",
"keyv": "^5.2.3"
}
},
"node_modules/cacheable/node_modules/keyv": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-5.2.3.tgz",
"integrity": "sha512-AGKecUfzrowabUv0bH1RIR5Vf7w+l4S3xtQAypKaUpTdIR1EbrAcTxHCrpo9Q+IWeUlFE2palRtgIQcgm+PQJw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@keyv/serialize": "^1.0.2"
}
},
"node_modules/cachedir": { "node_modules/cachedir": {
"version": "2.4.0", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz", "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz",
@@ -12130,9 +12186,9 @@
} }
}, },
"node_modules/chromatic": { "node_modules/chromatic": {
"version": "11.24.0", "version": "11.25.0",
"resolved": "https://registry.npmjs.org/chromatic/-/chromatic-11.24.0.tgz", "resolved": "https://registry.npmjs.org/chromatic/-/chromatic-11.25.0.tgz",
"integrity": "sha512-IocIxnxera27bRA51HHtfV9/Daqgj0E4BWYV12/nlf7qPJW1T82raQ5ypxZDJA4bXQywxOzpG2e6WWYvz/AwHA==", "integrity": "sha512-P2BVe0rRLS9WM+eSG3u1SRg0Mi2vopsdPs2FiXwUiPqZ6hs9fe66d3Pnt7CfQ22v2jThuPEXYjYEeuL75a16Bw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
@@ -15717,9 +15773,9 @@
} }
}, },
"node_modules/eslint-plugin-prettier": { "node_modules/eslint-plugin-prettier": {
"version": "5.2.1", "version": "5.2.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.2.tgz",
"integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", "integrity": "sha512-1yI3/hf35wmlq66C8yOyrujQnel+v5l1Vop5Cl2I6ylyNTT1JbuUUnV3/41PzwTzcyDp/oF0jWE3HXvcH5AQOQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -15748,29 +15804,29 @@
} }
}, },
"node_modules/eslint-plugin-react": { "node_modules/eslint-plugin-react": {
"version": "7.37.2", "version": "7.37.4",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.2.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.4.tgz",
"integrity": "sha512-EsTAnj9fLVr/GZleBLFbj/sSuXeWmp1eXIN60ceYnZveqEaUCyW4X+Vh4WTdUhCkW4xutXYqTXCUSyqD4rB75w==", "integrity": "sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"array-includes": "^3.1.8", "array-includes": "^3.1.8",
"array.prototype.findlast": "^1.2.5", "array.prototype.findlast": "^1.2.5",
"array.prototype.flatmap": "^1.3.2", "array.prototype.flatmap": "^1.3.3",
"array.prototype.tosorted": "^1.1.4", "array.prototype.tosorted": "^1.1.4",
"doctrine": "^2.1.0", "doctrine": "^2.1.0",
"es-iterator-helpers": "^1.1.0", "es-iterator-helpers": "^1.2.1",
"estraverse": "^5.3.0", "estraverse": "^5.3.0",
"hasown": "^2.0.2", "hasown": "^2.0.2",
"jsx-ast-utils": "^2.4.1 || ^3.0.0", "jsx-ast-utils": "^2.4.1 || ^3.0.0",
"minimatch": "^3.1.2", "minimatch": "^3.1.2",
"object.entries": "^1.1.8", "object.entries": "^1.1.8",
"object.fromentries": "^2.0.8", "object.fromentries": "^2.0.8",
"object.values": "^1.2.0", "object.values": "^1.2.1",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"resolve": "^2.0.0-next.5", "resolve": "^2.0.0-next.5",
"semver": "^6.3.1", "semver": "^6.3.1",
"string.prototype.matchall": "^4.0.11", "string.prototype.matchall": "^4.0.12",
"string.prototype.repeat": "^1.0.0" "string.prototype.repeat": "^1.0.0"
}, },
"engines": { "engines": {
@@ -15859,9 +15915,9 @@
} }
}, },
"node_modules/eslint-plugin-storybook": { "node_modules/eslint-plugin-storybook": {
"version": "0.11.0", "version": "0.11.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-0.11.0.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-0.11.2.tgz",
"integrity": "sha512-MvPJgF+ORwgK04a1CY5itO4pwdAOFIRqczlNEHL62+4Ocvj1d61GWRqIdeX1BNCKno6fdPC6TksUHCZMGsq26g==", "integrity": "sha512-0Z4DUklJrC+GHjCRXa7PYfPzWC15DaVnwaOYenpgXiCEijXPZkLKCms+rHhtoRcWccP7Z8DpOOaP1gc3P9oOwg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -15873,7 +15929,7 @@
"node": ">= 18" "node": ">= 18"
}, },
"peerDependencies": { "peerDependencies": {
"eslint": ">=6" "eslint": ">=8"
} }
}, },
"node_modules/eslint-scope": { "node_modules/eslint-scope": {
@@ -18734,6 +18790,13 @@
"minimalistic-crypto-utils": "^1.0.1" "minimalistic-crypto-utils": "^1.0.1"
} }
}, },
"node_modules/hookified": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/hookified/-/hookified-1.7.0.tgz",
"integrity": "sha512-XQdMjqC1AyeOzfs+17cnIk7Wdfu1hh2JtcyNfBf5u9jHrT3iZUlGHxLTntFBuk5lwkqJ6l3+daeQdHK5yByHVA==",
"dev": true,
"license": "MIT"
},
"node_modules/html-entities": { "node_modules/html-entities": {
"version": "2.5.2", "version": "2.5.2",
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz",
@@ -19195,9 +19258,9 @@
} }
}, },
"node_modules/immutable": { "node_modules/immutable": {
"version": "4.3.7", "version": "5.0.3",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz",
"integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", "integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==",
"devOptional": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },
@@ -22430,9 +22493,9 @@
} }
}, },
"node_modules/knip": { "node_modules/knip": {
"version": "5.42.1", "version": "5.42.2",
"resolved": "https://registry.npmjs.org/knip/-/knip-5.42.1.tgz", "resolved": "https://registry.npmjs.org/knip/-/knip-5.42.2.tgz",
"integrity": "sha512-xTnwo0I5TLAEU1BNqi8EwnvxI/5yJUJmBiXJdnS0+2FDE6WQII3upoImuUbx1GxrftHmY+prqfL6XuO6JEtYBw==", "integrity": "sha512-hVtZ6V59COFz3Y0/BHrWMlPAx82EdX/xFHXbutIRSNfJFPMGmIpxLBWTg35F4XQJGwlu5uWiJf8rBYYkmlYWWQ==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -36929,14 +36992,14 @@
} }
}, },
"node_modules/sass": { "node_modules/sass": {
"version": "1.80.6", "version": "1.83.4",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.80.6.tgz", "resolved": "https://registry.npmjs.org/sass/-/sass-1.83.4.tgz",
"integrity": "sha512-ccZgdHNiBF1NHBsWvacvT5rju3y1d/Eu+8Ex6c21nHp2lZGLBEtuwc415QfiI1PJa1TpCo3iXwwSRjRpn2Ckjg==", "integrity": "sha512-B1bozCeNQiOgDcLd33e2Cs2U60wZwjUUXzh900ZyQF5qUasvMdDZYbQ566LJu7cqR+sAHlAfO6RMkaID5s6qpA==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"chokidar": "^4.0.0", "chokidar": "^4.0.0",
"immutable": "^4.0.0", "immutable": "^5.0.2",
"source-map-js": ">=0.6.2 <2.0.0" "source-map-js": ">=0.6.2 <2.0.0"
}, },
"bin": { "bin": {
@@ -37007,13 +37070,13 @@
} }
}, },
"node_modules/sass/node_modules/readdirp": { "node_modules/sass/node_modules/readdirp": {
"version": "4.0.2", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz",
"integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", "integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 14.16.0" "node": ">= 14.18.0"
}, },
"funding": { "funding": {
"type": "individual", "type": "individual",
@@ -38268,9 +38331,9 @@
} }
}, },
"node_modules/stylelint": { "node_modules/stylelint": {
"version": "16.12.0", "version": "16.13.2",
"resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.12.0.tgz", "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.13.2.tgz",
"integrity": "sha512-F8zZ3L/rBpuoBZRvI4JVT20ZanPLXfQLzMOZg1tzPflRVh9mKpOZ8qcSIhh1my3FjAjZWG4T2POwGnmn6a6hbg==", "integrity": "sha512-wDlgh0mRO9RtSa3TdidqHd0nOG8MmUyVKl+dxA6C1j8aZRzpNeEgdhFmU5y4sZx4Fc6r46p0fI7p1vR5O2DZqA==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -38293,16 +38356,16 @@
"colord": "^2.9.3", "colord": "^2.9.3",
"cosmiconfig": "^9.0.0", "cosmiconfig": "^9.0.0",
"css-functions-list": "^3.2.3", "css-functions-list": "^3.2.3",
"css-tree": "^3.0.1", "css-tree": "^3.1.0",
"debug": "^4.3.7", "debug": "^4.3.7",
"fast-glob": "^3.3.2", "fast-glob": "^3.3.3",
"fastest-levenshtein": "^1.0.16", "fastest-levenshtein": "^1.0.16",
"file-entry-cache": "^9.1.0", "file-entry-cache": "^10.0.5",
"global-modules": "^2.0.0", "global-modules": "^2.0.0",
"globby": "^11.1.0", "globby": "^11.1.0",
"globjoin": "^0.1.4", "globjoin": "^0.1.4",
"html-tags": "^3.3.1", "html-tags": "^3.3.1",
"ignore": "^6.0.2", "ignore": "^7.0.1",
"imurmurhash": "^0.1.4", "imurmurhash": "^0.1.4",
"is-plain-object": "^5.0.0", "is-plain-object": "^5.0.0",
"known-css-properties": "^0.35.0", "known-css-properties": "^0.35.0",
@@ -38477,30 +38540,25 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/stylelint/node_modules/file-entry-cache": { "node_modules/stylelint/node_modules/file-entry-cache": {
"version": "9.1.0", "version": "10.0.5",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-9.1.0.tgz", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.0.5.tgz",
"integrity": "sha512-/pqPFG+FdxWQj+/WSuzXSDaNzxgTLr/OrR1QuqfEZzDakpdYE70PwUxL7BPUa8hpjbvY1+qvCl8k+8Tq34xJgg==", "integrity": "sha512-umpQsJrBNsdMDgreSryMEXvJh66XeLtZUwA8Gj7rHGearGufUFv6rB/bcXRFsiGWw/VeSUgUofF4Rf2UKEOrTA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"flat-cache": "^5.0.0" "flat-cache": "^6.1.5"
},
"engines": {
"node": ">=18"
} }
}, },
"node_modules/stylelint/node_modules/flat-cache": { "node_modules/stylelint/node_modules/flat-cache": {
"version": "5.0.0", "version": "6.1.5",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-5.0.0.tgz", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.5.tgz",
"integrity": "sha512-JrqFmyUl2PnPi1OvLyTVHnQvwQ0S+e6lGSwu8OkAZlSaNIZciTY2H/cOOROxsBA1m/LZNHDsqAgDZt6akWcjsQ==", "integrity": "sha512-QR+2kN38f8nMfiIQ1LHYjuDEmZNZVjxuxY+HufbS3BW0EX01Q5OnH7iduOYRutmgiXb797HAKcXUeXrvRjjgSQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"flatted": "^3.3.1", "cacheable": "^1.8.7",
"keyv": "^4.5.4" "flatted": "^3.3.2",
}, "hookified": "^1.6.0"
"engines": {
"node": ">=18"
} }
}, },
"node_modules/stylelint/node_modules/globby": { "node_modules/stylelint/node_modules/globby": {
@@ -38535,9 +38593,9 @@
} }
}, },
"node_modules/stylelint/node_modules/ignore": { "node_modules/stylelint/node_modules/ignore": {
"version": "6.0.2", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-6.0.2.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.3.tgz",
"integrity": "sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==", "integrity": "sha512-bAH5jbK/F3T3Jls4I0SO1hmPR0dKU0a7+SY6n1yzRtG54FLO8d6w/nxLFX2Nb7dBu6cCWXPaAME6cYqFUMmuCA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -39798,9 +39856,9 @@
} }
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.6.3", "version": "5.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
@@ -39907,9 +39965,9 @@
} }
}, },
"node_modules/undici": { "node_modules/undici": {
"version": "6.21.0", "version": "6.21.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.0.tgz", "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz",
"integrity": "sha512-BUgJXc752Kou3oOIuU1i+yZZypyZRqNPW0vqoMPl8VaoalSfeR0D8/t4iAS3yirs79SSMTxTag+ZC86uswv+Cw==", "integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=18.17" "node": ">=18.17"
@@ -41901,9 +41959,9 @@
"license": "ISC" "license": "ISC"
}, },
"node_modules/yaml": { "node_modules/yaml": {
"version": "2.6.0", "version": "2.7.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.0.tgz", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz",
"integrity": "sha512-a6ae//JvKDEra2kdi1qzCyrJW/WZCgFi8ydDV+eXExl95t+5R+ijnqHJbz9tmMh8FUjx3iv2fCQ4dclAQlO2UQ==", "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==",
"license": "ISC", "license": "ISC",
"bin": { "bin": {
"yaml": "bin.mjs" "yaml": "bin.mjs"
+7 -7
View File
@@ -68,7 +68,7 @@
"workbox-precaching": "^7.0.0", "workbox-precaching": "^7.0.0",
"workbox-window": "^7.0.0", "workbox-window": "^7.0.0",
"xstate": "4.38.3", "xstate": "4.38.3",
"yaml": "2.6.0" "yaml": "2.7.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.26.0", "@babel/core": "7.26.0",
@@ -97,7 +97,7 @@
"@types/classnames": "2.3.4", "@types/classnames": "2.3.4",
"@types/jest": "^29.5.0", "@types/jest": "^29.5.0",
"@types/markdown-it": "14.1.2", "@types/markdown-it": "14.1.2",
"@types/node": "22.10.6", "@types/node": "22.10.7",
"@types/prop-types": "15.7.14", "@types/prop-types": "15.7.14",
"@types/react": "18.3.18", "@types/react": "18.3.18",
"@types/react-linkify": "1.0.4", "@types/react-linkify": "1.0.4",
@@ -107,7 +107,7 @@
"@typescript-eslint/eslint-plugin": "8.13.0", "@typescript-eslint/eslint-plugin": "8.13.0",
"@typescript-eslint/parser": "8.13.0", "@typescript-eslint/parser": "8.13.0",
"babel-loader": "9.2.1", "babel-loader": "9.2.1",
"chromatic": "11.24.0", "chromatic": "11.25.0",
"css-loader": "7.1.2", "css-loader": "7.1.2",
"cypress": "^13.2.0", "cypress": "^13.2.0",
"eslint": "8.57.1", "eslint": "8.57.1",
@@ -117,9 +117,9 @@
"eslint-plugin-import": "^2.27.5", "eslint-plugin-import": "^2.27.5",
"eslint-plugin-jsx-a11y": "6.10.2", "eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-prettier": "^5.0.0", "eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-react": "7.37.2", "eslint-plugin-react": "7.37.4",
"eslint-plugin-react-hooks": "4.6.2", "eslint-plugin-react-hooks": "4.6.2",
"eslint-plugin-storybook": "0.11.0", "eslint-plugin-storybook": "0.11.2",
"handlebars": "^4.7.7", "handlebars": "^4.7.7",
"html-webpack-plugin": "5.6.3", "html-webpack-plugin": "5.6.3",
"install": "^0.13.0", "install": "^0.13.0",
@@ -132,7 +132,7 @@
"postcss": "^8.4.31", "postcss": "^8.4.31",
"prettier": "3.4.2", "prettier": "3.4.2",
"resolve-url-loader": "^5.0.0", "resolve-url-loader": "^5.0.0",
"sass": "1.80.6", "sass": "1.83.4",
"sass-loader": "^14.0.0", "sass-loader": "^14.0.0",
"storybook": "^8.3.6", "storybook": "^8.3.6",
"storybook-addon-fetch-mock": "^2.0.1", "storybook-addon-fetch-mock": "^2.0.1",
@@ -143,7 +143,7 @@
"stylelint-config-standard": "^36.0.0", "stylelint-config-standard": "^36.0.0",
"stylelint-config-standard-scss": "^14.0.0", "stylelint-config-standard-scss": "^14.0.0",
"ts-jest": "^29.1.0", "ts-jest": "^29.1.0",
"typescript": "5.6.3" "typescript": "5.7.3"
}, },
"overrides": { "overrides": {
"@emoji-mart/react": { "@emoji-mart/react": {
+15 -5
View File
@@ -4,7 +4,6 @@ package admin
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
@@ -13,11 +12,13 @@ import (
"github.com/owncast/owncast/core/chat/events" "github.com/owncast/owncast/core/chat/events"
"github.com/owncast/owncast/models" "github.com/owncast/owncast/models"
"github.com/owncast/owncast/persistence/authrepository" "github.com/owncast/owncast/persistence/authrepository"
"github.com/owncast/owncast/persistence/chatmessagerepository"
"github.com/owncast/owncast/persistence/configrepository" "github.com/owncast/owncast/persistence/configrepository"
"github.com/owncast/owncast/persistence/userrepository" "github.com/owncast/owncast/persistence/userrepository"
"github.com/owncast/owncast/utils" "github.com/owncast/owncast/utils"
"github.com/owncast/owncast/webserver/handlers/generated" "github.com/owncast/owncast/webserver/handlers/generated"
webutils "github.com/owncast/owncast/webserver/utils" webutils "github.com/owncast/owncast/webserver/utils"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@@ -146,13 +147,20 @@ func UpdateUserEnabled(w http.ResponseWriter, r *http.Request) {
func updateUserStatus(request generated.UpdateUserEnabledJSONBody) error { func updateUserStatus(request generated.UpdateUserEnabledJSONBody) error {
userRepository := userrepository.Get() userRepository := userrepository.Get()
chatMessageRepository := chatmessagerepository.Get()
if err := userRepository.SetEnabled(*request.UserId, *request.Enabled); err != nil { if err := userRepository.SetEnabled(*request.UserId, *request.Enabled); err != nil {
log.Errorln("error changing user enabled status", err) log.Errorln("error changing user enabled status", err)
return err return err
} }
if !*request.Enabled { messageIDs, err := chatMessageRepository.GetMessageIdsForUserID(*request.UserId)
if err := chat.SetMessageVisibilityForUserID(*request.UserId, *request.Enabled); err != nil { if err != nil {
return errors.Wrap(err, "error fetching user messages")
}
if !*request.Enabled && len(messageIDs) > 0 {
if err := chat.SetMessagesVisibility(messageIDs, *request.Enabled); err != nil {
log.Errorln("error changing user messages visibility", err) log.Errorln("error changing user messages visibility", err)
return err return err
} }
@@ -247,7 +255,8 @@ func GetModerators(w http.ResponseWriter, r *http.Request) {
func GetChatMessages(w http.ResponseWriter, r *http.Request) { func GetChatMessages(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
messages := chat.GetChatModerationHistory() chatMessageRepository := chatmessagerepository.Get()
messages := chatMessageRepository.GetChatModerationHistory()
webutils.WriteResponse(w, messages) webutils.WriteResponse(w, messages)
} }
@@ -343,7 +352,8 @@ func SendIntegrationChatMessage(integration models.ExternalAPIUser, w http.Respo
return return
} }
chat.SaveUserMessage(event) chatMessageRepository := chatmessagerepository.Get()
chatMessageRepository.SaveUserMessage(event)
webutils.WriteSimpleResponse(w, true, "sent") webutils.WriteSimpleResponse(w, true, "sent")
} }
+3 -2
View File
@@ -5,8 +5,8 @@ import (
"net/http" "net/http"
"github.com/owncast/owncast/config" "github.com/owncast/owncast/config"
"github.com/owncast/owncast/core/chat"
"github.com/owncast/owncast/models" "github.com/owncast/owncast/models"
"github.com/owncast/owncast/persistence/chatmessagerepository"
"github.com/owncast/owncast/persistence/configrepository" "github.com/owncast/owncast/persistence/configrepository"
"github.com/owncast/owncast/persistence/userrepository" "github.com/owncast/owncast/persistence/userrepository"
"github.com/owncast/owncast/utils" "github.com/owncast/owncast/utils"
@@ -33,7 +33,8 @@ func getChatMessages(w http.ResponseWriter, r *http.Request) {
switch r.Method { switch r.Method {
case http.MethodGet: case http.MethodGet:
messages := chat.GetChatHistory() chatMessageRepository := chatmessagerepository.Get()
messages := chatMessageRepository.GetChatHistory()
if err := json.NewEncoder(w).Encode(messages); err != nil { if err := json.NewEncoder(w).Encode(messages); err != nil {
log.Debugln(err) log.Debugln(err)
+3 -1
View File
@@ -9,6 +9,7 @@ import (
"github.com/owncast/owncast/core/chat" "github.com/owncast/owncast/core/chat"
"github.com/owncast/owncast/core/chat/events" "github.com/owncast/owncast/core/chat/events"
"github.com/owncast/owncast/models" "github.com/owncast/owncast/models"
"github.com/owncast/owncast/persistence/chatmessagerepository"
"github.com/owncast/owncast/persistence/userrepository" "github.com/owncast/owncast/persistence/userrepository"
"github.com/owncast/owncast/webserver/utils" "github.com/owncast/owncast/webserver/utils"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@@ -58,7 +59,8 @@ func GetUserDetails(w http.ResponseWriter, r *http.Request) {
clients[i] = client clients[i] = client
} }
messages, err := chat.GetMessagesFromUser(uid) chatMessagesRepository := chatmessagerepository.Get()
messages, err := chatMessagesRepository.GetMessagesFromUser(uid)
if err != nil { if err != nil {
log.Errorln(err) log.Errorln(err)
} }