18 Commits

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

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

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

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

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

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

* Add util for ungraceful sql execs

* Fix messages schema + add indexes

* Add migration to drop+recreate messages table

* Create index only if does not exist

* Fix typo

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

View File

@@ -18,7 +18,12 @@ func handleUndoInboxRequest(c context.Context, activity vocab.ActivityStreamsUnd
return err
}
} else {
log.Traceln("Undo", iter.GetType().GetTypeName(), "ignored")
t := iter.GetType()
if t != nil {
log.Traceln("Undo", t.GetTypeName(), "ignored")
} else {
log.Traceln("Undo (no type) ignored")
}
return nil
}
}

View File

@@ -22,7 +22,7 @@ import (
func handle(request apmodels.InboxRequest) {
if verified, err := Verify(request.Request); err != nil {
log.Errorln("Error in attempting to verify request", err)
log.Debugln("Error in attempting to verify request", err)
return
} else if !verified {
log.Debugln("Request failed verification", err)

View File

@@ -173,6 +173,11 @@ func SendPublicMessage(textContent string) error {
activity, _, note, noteID := createBaseOutboundMessage(textContent)
note.SetActivityStreamsTag(tagProp)
if !data.GetFederationIsPrivate() {
note = apmodels.MakeNotePublic(note)
activity = apmodels.MakeActivityPublic(activity)
}
b, err := apmodels.Serialize(activity)
if err != nil {
log.Errorln("unable to serialize custom fediverse message activity", err)

View File

@@ -24,19 +24,10 @@ func createFederationFollowersTable() {
"approved_at" TIMESTAMP,
"disabled_at" TIMESTAMP,
"request_object" BLOB,
PRIMARY KEY (iri));
CREATE INDEX iri_index ON ap_followers (iri);
CREATE INDEX approved_at_index ON ap_followers (approved_at);`
stmt, err := _datastore.DB.Prepare(createTableSQL)
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
_, err = stmt.Exec()
if err != nil {
log.Warnln("error executing sql creating followers table", createTableSQL, err)
}
PRIMARY KEY (iri));`
_datastore.MustExec(createTableSQL)
_datastore.MustExec(`CREATE INDEX IF NOT EXISTS idx_iri ON ap_followers (iri);`)
_datastore.MustExec(`CREATE INDEX IF NOT EXISTS idx_approved_at ON ap_followers (approved_at);`)
}
// GetFollowerCount will return the number of followers we're keeping track of.

View File

@@ -202,17 +202,10 @@ func createFederatedActivitiesTable() {
"actor" TEXT NOT NULL,
"type" TEXT NOT NULL,
"timestamp" TIMESTAMP NOT NULL
);
CREATE INDEX iri_actor_index ON ap_accepted_activities (iri,actor);`
);`
stmt, err := _datastore.DB.Prepare(createTableSQL)
if err != nil {
log.Fatal("error creating inbox table", err)
}
defer stmt.Close()
if _, err := stmt.Exec(); err != nil {
log.Fatal("error creating inbound federated activities table", err)
}
_datastore.MustExec(createTableSQL)
_datastore.MustExec(`CREATE INDEX IF NOT EXISTS idx_iri_actor_index ON ap_accepted_activities (iri,actor);`)
}
func createFederationOutboxTable() {
@@ -223,20 +216,12 @@ func createFederationOutboxTable() {
"type" TEXT NOT NULL,
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
"live_notification" BOOLEAN DEFAULT FALSE,
PRIMARY KEY (iri));
CREATE INDEX iri ON ap_outbox (iri);
CREATE INDEX type ON ap_outbox (type);
CREATE INDEX live_notification ON ap_outbox (live_notification);`
PRIMARY KEY (iri));`
stmt, err := _datastore.DB.Prepare(createTableSQL)
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
_, err = stmt.Exec()
if err != nil {
log.Warnln("error executing sql creating outbox table", createTableSQL, err)
}
_datastore.MustExec(createTableSQL)
_datastore.MustExec(`CREATE INDEX IF NOT EXISTS idx_iri ON ap_outbox (iri);`)
_datastore.MustExec(`CREATE INDEX IF NOT EXISTS idx_type ON ap_outbox (type);`)
_datastore.MustExec(`CREATE INDEX IF NOT EXISTS idx_live_notification ON ap_outbox (live_notification);`)
}
// GetOutboxPostCount will return the number of posts in the outbox.

View File

@@ -3,6 +3,7 @@ package fediverse
import (
"crypto/rand"
"io"
"strings"
"time"
)
@@ -19,27 +20,37 @@ type OTPRegistration struct {
// to be active at a time.
var pendingAuthRequests = make(map[string]OTPRegistration)
const registrationTimeout = time.Minute * 10
// RegisterFediverseOTP will start the OTP flow for a user, creating a new
// code and returning it to be sent to a destination.
func RegisterFediverseOTP(accessToken, userID, userDisplayName, account string) OTPRegistration {
func RegisterFediverseOTP(accessToken, userID, userDisplayName, account string) (OTPRegistration, bool) {
request, requestExists := pendingAuthRequests[accessToken]
// If a request is already registered and has not expired then return that
// existing request.
if requestExists && time.Since(request.Timestamp) < registrationTimeout {
return request, false
}
code, _ := createCode()
r := OTPRegistration{
Code: code,
UserID: userID,
UserDisplayName: userDisplayName,
Account: account,
Account: strings.ToLower(account),
Timestamp: time.Now(),
}
pendingAuthRequests[accessToken] = r
return r
return r, true
}
// ValidateFediverseOTP will verify a OTP code for a auth request.
func ValidateFediverseOTP(accessToken, code string) (bool, *OTPRegistration) {
request, ok := pendingAuthRequests[accessToken]
if !ok || request.Code != code || time.Since(request.Timestamp) > time.Minute*10 {
if !ok || request.Code != code || time.Since(request.Timestamp) > registrationTimeout {
return false, nil
}

View File

@@ -1,6 +1,9 @@
package fediverse
import "testing"
import (
"strings"
"testing"
)
const (
accessToken = "fake-access-token"
@@ -10,7 +13,11 @@ const (
)
func TestOTPFlowValidation(t *testing.T) {
r := RegisterFediverseOTP(accessToken, userID, userDisplayName, account)
r, success := RegisterFediverseOTP(accessToken, userID, userDisplayName, account)
if !success {
t.Error("Registration should be permitted.")
}
if r.Code == "" {
t.Error("Code is empty")
@@ -41,3 +48,31 @@ func TestOTPFlowValidation(t *testing.T) {
t.Error("UserDisplayName is not set correctly")
}
}
func TestSingleOTPFlowRequest(t *testing.T) {
r1, _ := RegisterFediverseOTP(accessToken, userID, userDisplayName, account)
r2, s2 := RegisterFediverseOTP(accessToken, userID, userDisplayName, account)
if r1.Code != r2.Code {
t.Error("Only one registration should be permitted.")
}
if s2 {
t.Error("Second registration should not be permitted.")
}
}
func TestAccountCaseInsensitive(t *testing.T) {
account := "Account"
accessToken := "another-fake-access-token"
r1, _ := RegisterFediverseOTP(accessToken, userID, userDisplayName, account)
_, reg1 := ValidateFediverseOTP(accessToken, r1.Code)
// Simulate second auth with account in different case
r2, _ := RegisterFediverseOTP(accessToken, userID, userDisplayName, strings.ToUpper(account))
_, reg2 := ValidateFediverseOTP(accessToken, r2.Code)
if reg1.Account != reg2.Account {
t.Errorf("Account names should be case-insensitive: %s %s", reg1.Account, reg2.Account)
}
}

View File

@@ -6,7 +6,6 @@ import (
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/core/user"
log "github.com/sirupsen/logrus"
"github.com/owncast/owncast/db"
)
@@ -24,18 +23,9 @@ func Setup(db *data.Datastore) {
"type" TEXT NOT NULL,
"timestamp" DATE DEFAULT CURRENT_TIMESTAMP NOT NULL,
FOREIGN KEY(user_id) REFERENCES users(id)
);CREATE INDEX auth_token ON auth (token);`
stmt, err := db.DB.Prepare(createTableSQL)
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
_, err = stmt.Exec()
if err != nil {
log.Fatalln(err)
}
);`
_datastore.MustExec(createTableSQL)
_datastore.MustExec(`CREATE INDEX IF NOT EXISTS idx_auth_token ON auth (token);`)
}
// AddAuth will add an external authentication token and type for a user.

View File

@@ -4,7 +4,7 @@ import "path/filepath"
const (
// StaticVersionNumber is the version of Owncast that is used when it's not overwritten via build-time settings.
StaticVersionNumber = "0.0.12" // Shown when you build from develop
StaticVersionNumber = "0.0.13" // Shown when you build from develop
// WebRoot is the web server root directory.
WebRoot = "webroot"
// FfmpegSuggestedVersion is the version of ffmpeg we suggest.
@@ -13,6 +13,8 @@ const (
DataDirectory = "data"
// EmojiDir is relative to the webroot.
EmojiDir = "/img/emoji"
// MaxChatDisplayNameLength is the maximum length of a chat display name.
MaxChatDisplayNameLength = 30
)
var (

View File

@@ -37,7 +37,7 @@ func GetViewersOverTime(w http.ResponseWriter, r *http.Request) {
// GetActiveViewers returns currently connected clients.
func GetActiveViewers(w http.ResponseWriter, r *http.Request) {
c := core.GetActiveViewers()
viewers := []models.Viewer{}
viewers := make([]models.Viewer, 0, len(c))
for _, v := range c {
viewers = append(viewers, *v)
}

View File

@@ -7,7 +7,6 @@ import (
"github.com/owncast/owncast/activitypub"
"github.com/owncast/owncast/auth"
"github.com/owncast/owncast/auth/fediverse"
fediverseauth "github.com/owncast/owncast/auth/fediverse"
"github.com/owncast/owncast/controllers"
"github.com/owncast/owncast/core/chat"
@@ -29,7 +28,12 @@ func RegisterFediverseOTPRequest(u user.User, w http.ResponseWriter, r *http.Req
}
accessToken := r.URL.Query().Get("accessToken")
reg := fediverseauth.RegisterFediverseOTP(accessToken, u.ID, u.DisplayName, req.FediverseAccount)
reg, success := fediverseauth.RegisterFediverseOTP(accessToken, u.ID, u.DisplayName, req.FediverseAccount)
if !success {
controllers.WriteSimpleResponse(w, false, "Could not register auth request. One may already be pending. Try again later.")
return
}
msg := fmt.Sprintf("<p>This is an automated message from %s. If you did not request this message please ignore or block. Your requested one-time code is:</p><p>%s</p>", data.GetServerName(), reg.Code)
if err := activitypub.SendDirectFederatedMessage(msg, reg.Account); err != nil {
controllers.WriteSimpleResponse(w, false, "Could not send code to fediverse: "+err.Error())
@@ -52,7 +56,7 @@ func VerifyFediverseOTPRequest(w http.ResponseWriter, r *http.Request) {
return
}
accessToken := r.URL.Query().Get("accessToken")
valid, authRegistration := fediverse.ValidateFediverseOTP(accessToken, req.Code)
valid, authRegistration := fediverseauth.ValidateFediverseOTP(accessToken, req.Code)
if !valid {
w.WriteHeader(http.StatusForbidden)
return
@@ -94,5 +98,4 @@ func VerifyFediverseOTPRequest(w http.ResponseWriter, r *http.Request) {
}
controllers.WriteSimpleResponse(w, true, "")
w.WriteHeader(http.StatusOK)
}

View File

@@ -6,6 +6,7 @@ import (
"strings"
"time"
"github.com/owncast/owncast/config"
"github.com/owncast/owncast/core/chat/events"
"github.com/owncast/owncast/core/data"
"github.com/owncast/owncast/core/user"
@@ -25,12 +26,17 @@ func (s *Server) userNameChanged(eventData chatClientEvent) {
// Check if name is on the blocklist
blocklist := data.GetForbiddenUsernameList()
// Names have a max length
if len(proposedUsername) > config.MaxChatDisplayNameLength {
proposedUsername = proposedUsername[:config.MaxChatDisplayNameLength]
}
for _, blockedName := range blocklist {
normalizedName := strings.TrimSpace(blockedName)
normalizedName = strings.ToLower(normalizedName)
if strings.Contains(normalizedName, proposedUsername) {
// Denied.
log.Debugln(eventData.client.User.DisplayName, "blocked from changing name to", proposedUsername, "due to blocked name", normalizedName)
log.Debugln(logSanitize(eventData.client.User.DisplayName), "blocked from changing name to", logSanitize(proposedUsername), "due to blocked name", normalizedName)
message := fmt.Sprintf("You cannot change your name to **%s**.", proposedUsername)
s.sendActionToClient(eventData.client, message)
@@ -59,7 +65,7 @@ func (s *Server) userNameChanged(eventData chatClientEvent) {
oldName := savedUser.DisplayName
// Save the new name
if err := user.ChangeUsername(eventData.client.User.ID, receivedEvent.NewName); err != nil {
if err := user.ChangeUsername(eventData.client.User.ID, proposedUsername); err != nil {
log.Errorln("error changing username", err)
}
@@ -69,7 +75,7 @@ func (s *Server) userNameChanged(eventData chatClientEvent) {
eventData.client.User.NameChangedAt = &now
// Send chat event letting everyone about about the name change
savedUser.DisplayName = receivedEvent.NewName
savedUser.DisplayName = proposedUsername
broadcastEvent := events.NameChangeBroadcast{
Oldname: oldName,
@@ -132,3 +138,11 @@ func (s *Server) userMessageSent(eventData chatClientEvent) {
eventData.client.MessageCount++
_lastSeenCache[event.User.ID] = time.Now()
}
func logSanitize(userValue string) string {
// strip carriage return and newline from user-submitted values to prevent log injection
sanitizedValue := strings.Replace(userValue, "\n", "", -1)
sanitizedValue = strings.Replace(sanitizedValue, "\r", "", -1)
return fmt.Sprintf("userSuppliedValue(%s)", sanitizedValue)
}

View File

@@ -1,7 +1,7 @@
package chat
import (
"fmt"
"database/sql"
"strings"
"time"
@@ -207,20 +207,14 @@ type rowData struct {
userType *string
}
func getChat(query string) []interface{} {
func getChat(rows *sql.Rows) ([]interface{}, error) {
history := make([]interface{}, 0)
rows, err := _datastore.DB.Query(query)
if err != nil || rows.Err() != nil {
log.Errorln("error fetching chat history", err)
return history
}
defer rows.Close()
for rows.Next() {
row := rowData{}
// Convert a database row into a chat event
if err = rows.Scan(
if err := rows.Scan(
&row.id,
&row.userID,
&row.body,
@@ -241,9 +235,7 @@ func getChat(query string) []interface{} {
&row.userScopes,
&row.userType,
); err != nil {
log.Errorln(err)
log.Errorln("There is a problem converting query to chat objects. Please report this:", query)
break
return nil, err
}
var message interface{}
@@ -266,7 +258,7 @@ func getChat(query string) []interface{} {
history = append(history, message)
}
return history
return history, nil
}
var _historyCache *[]interface{}
@@ -277,20 +269,89 @@ func GetChatModerationHistory() []interface{} {
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"
result := getChat(query)
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 := fmt.Sprintf("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 %d", maxBacklogNumber)
m := getChat(query)
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 {
@@ -307,10 +368,40 @@ func SetMessageVisibilityForUserID(userID string, visible bool) error {
_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)
query := fmt.Sprintf("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 '%s'", userID)
messages := getChat(query)
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
@@ -320,6 +411,11 @@ func SetMessageVisibilityForUserID(userID string, visible bool) error {
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)
}

View File

@@ -355,7 +355,7 @@ func (s *Server) eventReceived(event chatClientEvent) {
s.userNameChanged(event)
default:
log.Debugln(eventType, "event not found:", typecheck)
log.Debugln(logSanitize(fmt.Sprint(eventType)), "event not found:", logSanitize(fmt.Sprint(typecheck)))
}
}

View File

@@ -17,7 +17,7 @@ import (
)
const (
schemaVersion = 5
schemaVersion = 6
)
var (

View File

@@ -13,30 +13,26 @@ import (
func CreateMessagesTable(db *sql.DB) {
createTableSQL := `CREATE TABLE IF NOT EXISTS messages (
"id" string NOT NULL,
"user_id" INTEGER,
"user_id" TEXT,
"body" TEXT,
"eventType" TEXT,
"hidden_at" DATETIME,
"timestamp" DATETIME,
"title" TEXT,
"subtitle" TEXT,
"image" TEXT,
"link" TEXT,
"title" TEXT,
"subtitle" TEXT,
"image" TEXT,
"link" TEXT,
PRIMARY KEY (id)
);CREATE INDEX index ON messages (id, user_id, hidden_at, timestamp);
CREATE INDEX id ON messages (id);
CREATE INDEX user_id ON messages (user_id);
CREATE INDEX hidden_at ON messages (hidden_at);
CREATE INDEX timestamp ON messages (timestamp);`
);`
MustExec(createTableSQL, db)
stmt, err := db.Prepare(createTableSQL)
if err != nil {
log.Fatal("error creating chat messages table", err)
}
defer stmt.Close()
if _, err := stmt.Exec(); err != nil {
log.Fatal("error creating chat messages table", err)
}
// Create indexes
MustExec(`CREATE INDEX IF NOT EXISTS user_id_hidden_at_timestamp ON messages (id, user_id, hidden_at, timestamp);`, db)
MustExec(`CREATE INDEX IF NOT EXISTS idx_id ON messages (id);`, db)
MustExec(`CREATE INDEX IF NOT EXISTS idx_user_id ON messages (user_id);`, db)
MustExec(`CREATE INDEX IF NOT EXISTS idx_hidden_at ON messages (hidden_at);`, db)
MustExec(`CREATE INDEX IF NOT EXISTS idx_timestamp ON messages (timestamp);`, db)
MustExec(`CREATE INDEX IF NOT EXISTS idx_messages_hidden_at_timestamp on messages(hidden_at, timestamp);`, db)
}
// GetMessagesCount will return the number of messages in the database.

View File

@@ -29,6 +29,8 @@ func migrateDatabaseSchema(db *sql.DB, from, to int) error {
migrateToSchema4(db)
case 4:
migrateToSchema5(db)
case 5:
migrateToSchema6(db)
default:
log.Fatalln("missing database migration step")
}
@@ -42,6 +44,16 @@ func migrateDatabaseSchema(db *sql.DB, from, to int) error {
return nil
}
func migrateToSchema6(db *sql.DB) {
// Fix chat messages table schema. Since chat is ephemeral we can drop
// the table and recreate it.
// Drop the old messages table
MustExec(`DROP TABLE messages`, db)
// Recreate it
CreateMessagesTable(db)
}
// nolint:cyclop
func migrateToSchema5(db *sql.DB) {
// Create the access tokens table.

View File

@@ -118,18 +118,9 @@ func (ds *Datastore) Setup() {
"key" string NOT NULL PRIMARY KEY,
"value" BLOB,
"timestamp" DATE DEFAULT CURRENT_TIMESTAMP NOT NULL
);CREATE INDEX IF NOT EXISTS messages_timestamp_index ON messages(timestamp);`
);`
stmt, err := ds.DB.Prepare(createTableSQL)
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
_, err = stmt.Exec()
if err != nil {
log.Fatalln(err)
}
ds.MustExec(createTableSQL)
if !HasPopulatedDefaults() {
PopulateDefaults()
@@ -173,3 +164,16 @@ func (ds *Datastore) Reset() {
func GetDatastore() *Datastore {
return _datastore
}
// MustExec will execute a SQL statement on a provided database instance.
func (ds *Datastore) MustExec(s string) {
stmt, err := ds.DB.Prepare(s)
if err != nil {
log.Panic(err)
}
defer stmt.Close()
_, err = stmt.Exec()
if err != nil {
log.Warnln(err)
}
}

View File

@@ -41,20 +41,12 @@ func createUsersTable(db *sql.DB) {
"type" TEXT DEFAULT 'STANDARD',
"last_used" DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
);CREATE INDEX user_id_disabled_at_index ON users (id, disabled_at);
CREATE INDEX user_id_index ON users (id);
CREATE INDEX user_id_disabled_index ON users (id, disabled_at);
CREATE INDEX user_disabled_at_index ON USERS (disabled_at);`
);`
stmt, err := db.Prepare(createTableSQL)
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
_, err = stmt.Exec()
if err != nil {
log.Warnln(err)
}
MustExec(createTableSQL, db)
MustExec(`CREATE INDEX IF NOT EXISTS idx_user_id ON users (id);`, db)
MustExec(`CREATE INDEX IF NOT EXISTS idx_user_id_disabled ON users (id, disabled_at);`, db)
MustExec(`CREATE INDEX IF NOT EXISTS idx_user_disabled_at ON users (disabled_at);`, db)
}
// GetUsersCount will return the number of users in the database.

20
core/data/utils.go Normal file
View File

@@ -0,0 +1,20 @@
package data
import (
"database/sql"
log "github.com/sirupsen/logrus"
)
// MustExec will execute a SQL statement on a provided database instance.
func MustExec(s string, db *sql.DB) {
stmt, err := db.Prepare(s)
if err != nil {
log.Panic(err)
}
defer stmt.Close()
_, err = stmt.Exec()
if err != nil {
log.Warnln(err)
}
}

File diff suppressed because one or more lines are too long

View File

@@ -20,6 +20,18 @@ func GetSocialHandle(platform string) *SocialHandle {
// GetAllSocialHandles will return a list of all the social platforms we support.
func GetAllSocialHandles() map[string]SocialHandle {
socialHandlePlatforms := map[string]SocialHandle{
"fediverse": {
Platform: "Fediverse",
Icon: "/img/platformlogos/fediverse.svg",
},
"matrix": {
Platform: "Matrix",
Icon: "/img/platformlogos/matrix.svg",
},
"xmpp": {
Platform: "XMPP",
Icon: "/img/platformlogos/xmpp.svg",
},
"bandcamp": {
Platform: "Bandcamp",
Icon: "/img/platformlogos/bandcamp.svg",

View File

@@ -17,18 +17,10 @@ func createNotificationsTable(db *sql.DB) {
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"channel" TEXT NOT NULL,
"destination" TEXT NOT NULL,
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
CREATE INDEX channel_index ON notifications (channel);`
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP);`
stmt, err := db.Prepare(createTableSQL)
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
_, err = stmt.Exec()
if err != nil {
log.Warnln("error executing sql creating followers table", createTableSQL, err)
}
data.MustExec(createTableSQL, db)
data.MustExec(`CREATE INDEX IF NOT EXISTS idx_channel ON notifications (channel);`, db)
}
// AddNotification saves a new user notification destination.

View File

@@ -2,7 +2,7 @@ openapi: 3.0.1
info:
title: Owncast
description: Owncast is a self-hosted live video and web chat server for use with existing popular broadcasting software.
version: '0.0.12'
version: '0.0.13'
contact:
name: Gabe Kangas
email: gabek@real-ity.com

View File

@@ -1,9 +0,0 @@
{
"version": "1",
"packages": [{
"schema": "db/schema.sql",
"queries": "db/query.sql",
"name": "db",
"path": "db"
}]
}

6
sqlc.yaml Normal file
View File

@@ -0,0 +1,6 @@
version: 1
packages:
- path: db
name: db
schema: 'db/schema.sql'
queries: 'db/query.sql'

View File

@@ -0,0 +1,162 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="196.52mm"
height="196.52mm"
viewBox="0 0 196.52 196.52"
version="1.1"
id="svg8"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
sodipodi:docname="fediverse.svg"
inkscape:export-filename="/home/nestor/Pictures/Fediversal/Logo_penta_connectat-imbrincat_retallats-color-512x.png"
inkscape:export-xdpi="66.175453"
inkscape:export-ydpi="66.175453"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.71293238"
inkscape:cx="49.093015"
inkscape:cy="446.04511"
inkscape:document-units="mm"
inkscape:current-layer="layer3"
showgrid="false"
inkscape:snap-smooth-nodes="true"
inkscape:snap-midpoints="true"
inkscape:snap-global="false"
inkscape:window-width="3834"
inkscape:window-height="1568"
inkscape:window-x="0"
inkscape:window-y="26"
inkscape:window-maximized="1"
fit-margin-top="5"
fit-margin-left="5"
fit-margin-right="5"
fit-margin-bottom="5"
inkscape:showpageshadow="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#d1d1d1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Linies"
style="display:inline"
transform="translate(6.6789703,-32.495842)">
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#a730b8;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12.4769;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 30.433048,105.79682 a 20.675157,20.675157 0 0 1 -8.842632,8.8016 l 48.544801,48.73413 11.703623,-5.93124 z m 64.032011,64.27936 -11.703623,5.93123 24.597704,24.69384 a 20.675158,20.675158 0 0 1 8.8444,-8.80277 z"
id="path9722"
inkscape:connector-curvature="0" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#5496be;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12.4769;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 150.63096,125.07992 -27.48274,13.92806 2.02632,12.9615 31.09575,-15.75978 a 20.675158,20.675158 0 0 1 -5.63933,-11.12978 z m -43.43711,22.01339 -64.98098,32.93215 a 20.675157,20.675157 0 0 1 5.640504,11.13096 l 61.366786,-31.10103 z"
id="path9729"
inkscape:connector-curvature="0" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#ce3d1a;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12.4769;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="M 86.296497,65.972733 54.94108,127.18595 64.198706,136.47934 97.397557,71.668922 A 20.675157,20.675157 0 0 1 86.296497,65.972733 Z M 46.785998,143.10691 30.903725,174.1124 a 20.675158,20.675158 0 0 1 11.099891,5.6956 L 56.04304,152.39971 Z"
id="path9713"
inkscape:connector-curvature="0" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#d0188f;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12.4769;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 21.320788,114.7344 a 20.675157,20.675157 0 0 1 -10.328521,2.15938 20.675157,20.675157 0 0 1 -1.9958343,-0.20867 l 9.2740393,59.31996 a 20.675158,20.675158 0 0 1 10.328518,-2.15937 20.675158,20.675158 0 0 1 1.994664,0.20867 z"
id="path1015"
inkscape:connector-curvature="0" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#5b36e9;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12.4769;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 47.902612,191.45008 a 20.675157,20.675157 0 0 1 0.216288,4.16459 20.675157,20.675157 0 0 1 -2.181058,8.15683 l 59.309998,9.51729 a 20.675158,20.675158 0 0 1 -0.21687,-4.16516 20.675158,20.675158 0 0 1 2.18163,-8.15626 z"
id="path1674"
inkscape:connector-curvature="0" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#30b873;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12.4769;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 156.47896,136.42659 -27.37547,53.44382 a 20.675157,20.675157 0 0 1 11.10223,5.69678 l 27.37489,-53.44325 a 20.675158,20.675158 0 0 1 -11.10165,-5.69735 z"
id="path1676"
inkscape:connector-curvature="0" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#ebe305;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12.4769;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 119.14248,60.83866 a 20.675157,20.675157 0 0 1 -8.84438,8.802775 l 42.39493,42.558465 a 20.675158,20.675158 0 0 1 8.8438,-8.80277 z"
id="path1678"
inkscape:connector-curvature="0" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#f47601;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12.4769;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="M 80.447909,54.626661 26.855786,81.785842 a 20.675158,20.675158 0 0 1 5.639919,11.129782 L 86.08724,65.755272 A 20.675157,20.675157 0 0 1 80.447909,54.626661 Z"
id="path1680"
inkscape:connector-curvature="0" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#57c115;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12.4769;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 110.20021,69.69067 a 20.675157,20.675157 0 0 1 -10.498502,2.245535 20.675157,20.675157 0 0 1 -1.809441,-0.189912 l 4.747213,30.398237 12.9533,2.07848 z m -4.80173,50.12623 11.22415,71.87057 a 20.675158,20.675158 0 0 1 10.17788,-2.08376 20.675158,20.675158 0 0 1 2.16289,0.23739 l -10.61104,-67.9463 z"
id="path9758"
inkscape:connector-curvature="0" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#dbb210;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:12.4769;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 32.533803,93.143636 a 20.675157,20.675157 0 0 1 0.227426,4.23023 20.675157,20.675157 0 0 1 -2.148818,8.097644 l 30.421682,4.88613 5.979296,-11.676661 z m 52.13965,8.373124 -5.979885,11.67783 71.879942,11.5442 a 20.675158,20.675158 0 0 1 -0.20867,-4.11711 20.675158,20.675158 0 0 1 2.20568,-8.20021 z"
id="path9760"
inkscape:connector-curvature="0" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Nodes"
style="display:inline;opacity:1"
transform="translate(6.6789703,-32.495842)">
<circle
style="fill:#ffca00;fill-opacity:0.995968;stroke:none;stroke-width:0.300108;stroke-opacity:0.960784"
id="path817"
cx="103.46658"
cy="45.731655"
transform="rotate(3.1178174)"
r="18.795599" />
<circle
id="path819"
style="fill:#64ff00;fill-opacity:0.995968;stroke:none;stroke-width:0.300108;stroke-opacity:0.960784"
cx="177.37808"
cy="112.26514"
transform="rotate(3.1178174)"
r="18.795599" />
<circle
id="path823"
style="fill:#00a3ff;fill-opacity:0.995968;stroke:none;stroke-width:0.300108;stroke-opacity:0.960784"
cx="136.92497"
cy="203.10144"
transform="rotate(3.1178174)"
r="18.795599" />
<circle
style="fill:#9500ff;fill-opacity:0.995968;stroke:none;stroke-width:0.300108;stroke-opacity:0.960784"
id="path825"
cx="38.012062"
cy="192.70789"
transform="rotate(3.1178174)"
r="18.795599" />
<circle
id="path827"
style="fill:#ff0000;fill-opacity:0.995968;stroke:none;stroke-width:0.300108;stroke-opacity:0.960784"
cx="17.333637"
cy="95.447998"
transform="rotate(3.1178174)"
r="18.795599" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 520 520" style="enable-background:new 0 0 520 520;" xml:space="preserve">
<path d="M13.7,11.9v496.2h35.7V520H0V0h49.4v11.9H13.7z"/>
<path d="M166.3,169.2v25.1h0.7c6.7-9.6,14.8-17,24.2-22.2c9.4-5.3,20.3-7.9,32.5-7.9c11.7,0,22.4,2.3,32.1,6.8
c9.7,4.5,17,12.6,22.1,24c5.5-8.1,13-15.3,22.4-21.5c9.4-6.2,20.6-9.3,33.5-9.3c9.8,0,18.9,1.2,27.3,3.6c8.4,2.4,15.5,6.2,21.5,11.5
c6,5.3,10.6,12.1,14,20.6c3.3,8.5,5,18.7,5,30.7v124.1h-50.9V249.6c0-6.2-0.2-12.1-0.7-17.6c-0.5-5.5-1.8-10.3-3.9-14.3
c-2.2-4.1-5.3-7.3-9.5-9.7c-4.2-2.4-9.9-3.6-17-3.6c-7.2,0-13,1.4-17.4,4.1c-4.4,2.8-7.9,6.3-10.4,10.8c-2.5,4.4-4.2,9.4-5,15.1
c-0.8,5.6-1.3,11.3-1.3,17v103.3h-50.9v-104c0-5.5-0.1-10.9-0.4-16.3c-0.2-5.4-1.3-10.3-3.1-14.9c-1.8-4.5-4.8-8.2-9-10.9
c-4.2-2.7-10.3-4.1-18.5-4.1c-2.4,0-5.6,0.5-9.5,1.6c-3.9,1.1-7.8,3.1-11.5,6.1c-3.7,3-6.9,7.3-9.5,12.9c-2.6,5.6-3.9,13-3.9,22.1
v107.6h-50.9V169.2H166.3z"/>
<path d="M506.3,508.1V11.9h-35.7V0H520v520h-49.4v-11.9H506.3z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 13.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
<svg:svg
version="1.1"
xml:space="preserve"
viewBox="0 0 200 200"
width="200px"
height="200px"
x="0px"
y="0px"
enable-background="new 0 0 200 200"
class="TridactylThemeDark"
id="svg36"
sodipodi:docname="xmpp.svg"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:xhtml="http://www.w3.org/1999/xhtml"><svg:defs
id="defs40" /><sodipodi:namedview
id="namedview38"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#505050"
showgrid="false"
inkscape:zoom="3.337544"
inkscape:cx="134.53006"
inkscape:cy="118.05088"
inkscape:window-width="3834"
inkscape:window-height="1568"
inkscape:window-x="0"
inkscape:window-y="26"
inkscape:window-maximized="1"
inkscape:current-layer="svg36" />
<svg:linearGradient
id="SVGID_right_"
y2="1.279e-13"
gradientUnits="userSpaceOnUse"
x2="-1073.2"
gradientTransform="matrix(1.1429608,0,0,1.1429608,1352.3375,27.297)"
y1="126.85"
x1="-1073.2">
<svg:stop
stop-color="#1b3967"
offset=".011"
id="stop2" />
<svg:stop
stop-color="#13b5ea"
offset=".467"
id="stop4" />
<svg:stop
stop-color="#002b5c"
offset=".9945"
id="stop6" />
</svg:linearGradient>
<svg:linearGradient
id="SVGID_left_"
y2="1.279e-13"
gradientUnits="userSpaceOnUse"
x2="-1073.2"
gradientTransform="matrix(-1.1429608,0,0,1.1429608,-1152.3376,27.295856)"
y1="126.85"
x1="-1073.2">
<svg:stop
stop-color="#1b3967"
offset=".011"
id="stop9" />
<svg:stop
stop-color="#13b5ea"
offset=".467"
id="stop11" />
<svg:stop
stop-color="#002b5c"
offset=".9945"
id="stop13" />
</svg:linearGradient>
<svg:path
d="m 158.17335,43.514472 c 0.088,1.500707 -2.04247,1.106386 -2.04247,2.620809 0,44.062284 -53.21398,111.285539 -104.795782,124.274139 v 1.87332 C 119.85902,165.9736 198.27413,94.789982 200,27.298143 l -41.83123,16.217472 z"
style="fill:url(#SVGID_right_);stroke-width:1.14296"
id="path16" />
<svg:path
d="m 137.44918,48.935535 c 0.0868,1.500708 0.13715,3.005988 0.13715,4.522696 0,44.062284 -35.08773,103.434549 -86.667269,116.421999 v 1.87332 C 118.40404,168.56469 171.85574,99.719572 171.85574,46.942212 c 0,-2.714532 -0.1463,-5.405062 -0.42403,-8.064732 l -33.98023,10.05577 z"
style="fill:#e96d1f;stroke-width:1.14296"
id="path18" />
<svg:path
d="m 171.75858,38.249995 -8.70592,3.111139 c 0.0469,1.099528 0.0755,2.576234 0.0755,3.686048 0,47.111704 -42.59929,112.243348 -99.748468,122.433978 -3.708919,1.24354 -8.61565,2.37393 -12.494848,3.35002 v 1.87217 C 125.46753,166.34848 177.86772,90.563313 171.7643,38.245422 Z"
style="fill:#d9541e;stroke-width:1.14296"
id="path20" />
<svg:path
d="m 41.826653,43.513329 c -0.088,1.500708 2.042471,1.106387 2.042471,2.620809 0,44.062284 53.21398,111.285542 104.795786,124.274152 v 1.87331 C 80.140986,165.97245 1.7258709,94.78884 0,27.297 l 41.831225,16.217472 z"
style="fill:url(#SVGID_left_);stroke-width:1.14296"
id="path22" />
<svg:path
d="m 62.550819,48.934392 c -0.08687,1.500708 -0.137155,3.005988 -0.137155,4.522697 0,44.062284 35.087749,103.434541 86.667286,116.422011 v 1.87331 C 81.595976,168.56354 28.144269,99.718429 28.144269,46.941069 c 0,-2.714533 0.146298,-5.405062 0.424038,-8.064732 l 33.980226,10.05577 z"
style="fill:#a0ce67;stroke-width:1.14296"
id="path24" />
<svg:path
d="m 28.241419,38.248851 8.705933,3.111139 c -0.04686,1.099529 -0.07543,2.576234 -0.07543,3.68605 0,47.111704 42.599295,112.24334 99.748468,122.43398 3.7089,1.24353 8.61563,2.37393 12.49485,3.35002 v 1.87216 C 74.532478,166.34735 22.132294,90.56217 28.235705,38.24428 Z"
style="fill:#439639;stroke-width:1.14296"
id="path26" />
<script /><xhtml:iframe
class="cleanslate hidden"
src="xmpp_files/commandline.html"
id="cmdline_iframe"
loading="lazy"
style="height: 0px !important;" /></svg:svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -347,8 +347,9 @@ export default class ChatInput extends Component {
>
<${ContentEditable}
id="message-input"
aria-role="textbox"
class="appearance-none block w-full bg-transparent text-sm text-gray-700 h-full focus:outline-none"
placeholderText=${placeholderText}
aria-placeholder=${placeholderText}
innerRef=${this.formMessageInput}
html=${inputHTML}
disabled=${!inputEnabled}

View File

@@ -33,7 +33,7 @@ function SocialIcon(props) {
return html`
<a class=${itemClass} target="_blank" rel="me" href=${url}>
<span
class="platform-icon rounded-lg bg-no-repeat"
class="platform-icon bg-no-repeat"
style=${style}
title="Find me on ${name}"
></span>

View File

@@ -62,7 +62,7 @@
/* If the div is empty then show the placeholder */
#message-input:empty:before {
content: attr(placeholderText);
content: attr(aria-placeholder);
pointer-events: none;
position: absolute; /* Fixes firefox positioning caret on the right */
display: block; /* For Firefox */