Compare commits
2 Commits
8aceb259ca
...
7b76f9ed23
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b76f9ed23 | ||
|
|
0b94ce7fe5 |
@@ -157,3 +157,19 @@ tier = 1
|
||||
poll_interval_minutes = 60
|
||||
direct_route = "music"
|
||||
enabled = true
|
||||
|
||||
[[sources]]
|
||||
name = "Naked Capitalism"
|
||||
feed_url = "https://www.nakedcapitalism.com/feed"
|
||||
tier = 1
|
||||
poll_interval_minutes = 60
|
||||
direct_route = "finance"
|
||||
enabled = true
|
||||
|
||||
[[sources]]
|
||||
name = "Wolf Street"
|
||||
feed_url = "https://wolfstreet.com/feed/"
|
||||
tier = 1
|
||||
poll_interval_minutes = 60
|
||||
direct_route = "finance"
|
||||
enabled = true
|
||||
|
||||
@@ -13,8 +13,8 @@ import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -142,30 +142,45 @@ func New(cfg config.MatrixConfig) (*Client, error) {
|
||||
"room", evt.RoomID, "event_id", evt.ID, "sender", evt.Sender, "err", err)
|
||||
}
|
||||
|
||||
// LoginAs enables the cryptohelper to re-login if the token expires
|
||||
ch.LoginAs = &mautrix.ReqLogin{
|
||||
Type: mautrix.AuthTypePassword,
|
||||
Identifier: mautrix.UserIdentifier{
|
||||
Type: mautrix.IdentifierTypeUser,
|
||||
User: cfg.UserID,
|
||||
},
|
||||
Password: cfg.Password,
|
||||
InitialDeviceDisplayName: cfg.DisplayName,
|
||||
}
|
||||
|
||||
// IMPORTANT: do NOT set ch.LoginAs here. We already performed an explicit
|
||||
// password login above (or restored a saved token), so mx is fully
|
||||
// authenticated against device.json's device. When LoginAs is set AND the
|
||||
// crypto store is fresh, cryptohelper.Init() performs a SECOND login
|
||||
// (StoreCredentials=true), minting a separate device and pinning the olm
|
||||
// account to it — while device.json still records the first device. That
|
||||
// split-brain is exactly the "device never verifies" bug: the device the
|
||||
// token authenticates as is not the device the crypto account belongs to,
|
||||
// and each cold start leaks another orphan device. Token-expiry recovery is
|
||||
// already handled by the isTokenValid() check + re-login path above.
|
||||
if err := ch.Init(context.Background()); err != nil {
|
||||
return nil, fmt.Errorf("crypto helper init: %w", err)
|
||||
}
|
||||
|
||||
mx.Crypto = ch
|
||||
|
||||
// Bootstrap cross-signing only if not already set up.
|
||||
// Ensure our CURRENT device is cross-signed. We bootstrap (generate + upload
|
||||
// fresh cross-signing keys, then self-sign) whenever either no keys exist yet,
|
||||
// or keys exist on the server but our device is not signed by them. The latter
|
||||
// is exactly the post-wipe state: when crypto.db is deleted the private
|
||||
// self-signing key is lost (it is never persisted locally without an SSSS
|
||||
// passphrase, and we discard the random recovery key), so the lingering server
|
||||
// master key is useless to us and must be replaced.
|
||||
//
|
||||
// CRITICAL: do NOT gate this on IsDeviceTrusted(mach.OwnIdentity()).
|
||||
// OwnIdentity() hard-codes Trust=id.TrustStateVerified, and ResolveTrustContext
|
||||
// short-circuits on that, so the call is a tautology that ALWAYS returns true —
|
||||
// it makes this branch unreachable and the device never gets signed.
|
||||
// GetOwnVerificationStatus does a real check: is our own device key signed by
|
||||
// our self-signing key?
|
||||
mach := ch.Machine()
|
||||
existingKeys, err := mach.GetOwnCrossSigningPublicKeys(context.Background())
|
||||
hasKeys, deviceCrossSigned, err := mach.GetOwnVerificationStatus(context.Background())
|
||||
if err != nil {
|
||||
slog.Warn("cross-signing: failed to fetch existing keys", "err", err)
|
||||
slog.Warn("cross-signing: failed to check verification status", "err", err)
|
||||
}
|
||||
if !hasKeys || !deviceCrossSigned {
|
||||
if hasKeys {
|
||||
slog.Warn("cross-signing: keys exist but current device is not cross-signed, resetting cross-signing")
|
||||
}
|
||||
if existingKeys == nil || existingKeys.MasterKey == "" {
|
||||
_, _, err = mach.GenerateAndUploadCrossSigningKeys(context.Background(), func(ui *mautrix.RespUserInteractive) interface{} {
|
||||
return map[string]interface{}{
|
||||
"type": mautrix.AuthTypePassword,
|
||||
@@ -195,7 +210,7 @@ func New(cfg config.MatrixConfig) (*Client, error) {
|
||||
slog.Info("cross-signing: master key signed")
|
||||
}
|
||||
} else {
|
||||
slog.Info("cross-signing: already configured, skipping bootstrap")
|
||||
slog.Info("cross-signing: already configured and current device cross-signed, skipping bootstrap")
|
||||
}
|
||||
|
||||
slog.Info("E2EE initialized",
|
||||
|
||||
@@ -39,6 +39,7 @@ var channels = []Channel{
|
||||
{Slug: "anime", Title: "Anime", Theme: "anime", Emoji: "🌸", Blurb: "Series, studios, and the wider world of anime and manga."},
|
||||
{Slug: "foss", Title: "FOSS", Theme: "foss", Emoji: "🐧", Blurb: "Kernel, distros, and the wider free and open source software world."},
|
||||
{Slug: "kids", Title: "Kids", Theme: "kids", Emoji: "🧒", Blurb: "World news written for younger readers — short, clear, and curious."},
|
||||
{Slug: "finance", Title: "Finance", Theme: "finance", Emoji: "💰", Blurb: "Markets, money, and the business of the world. Read-only — these stories don't post to Matrix."},
|
||||
}
|
||||
|
||||
// SourceInfo is the trimmed view of a configured feed for the settings panel.
|
||||
|
||||
@@ -77,6 +77,7 @@ html[data-phase="night"] {
|
||||
.bg-theme-anime { background-color: #ec5e8a; }
|
||||
.bg-theme-foss { background-color: #d97706; }
|
||||
.bg-theme-kids { background-color: #14b8a6; }
|
||||
.bg-theme-finance { background-color: #10b981; }
|
||||
|
||||
.text-theme-gaming { color: #2d8a5a; }
|
||||
.text-theme-tech { color: #2f7fb8; }
|
||||
@@ -86,6 +87,7 @@ html[data-phase="night"] {
|
||||
.text-theme-anime { color: #c33a6a; }
|
||||
.text-theme-foss { color: #b8530a; }
|
||||
.text-theme-kids { color: #0f766e; }
|
||||
.text-theme-finance { color: #059669; }
|
||||
|
||||
.decoration-theme-gaming { text-decoration-color: #4caf7d; }
|
||||
.decoration-theme-tech { text-decoration-color: #5aa9e6; }
|
||||
@@ -95,6 +97,7 @@ html[data-phase="night"] {
|
||||
.decoration-theme-anime { text-decoration-color: #ec5e8a; }
|
||||
.decoration-theme-foss { text-decoration-color: #d97706; }
|
||||
.decoration-theme-kids { text-decoration-color: #14b8a6; }
|
||||
.decoration-theme-finance { text-decoration-color: #10b981; }
|
||||
|
||||
.border-theme-gaming { border-color: #4caf7d; }
|
||||
.border-theme-tech { border-color: #5aa9e6; }
|
||||
@@ -104,6 +107,7 @@ html[data-phase="night"] {
|
||||
.border-theme-anime { border-color: #ec5e8a; }
|
||||
.border-theme-foss { border-color: #d97706; }
|
||||
.border-theme-kids { border-color: #14b8a6; }
|
||||
.border-theme-finance { border-color: #10b981; }
|
||||
|
||||
/* "Posted to Matrix" glow — soft pulsing aura in the channel's theme color. */
|
||||
.glow-theme-gaming { box-shadow: 0 0 0 2px rgba(76,175,125,0.35), 0 0 24px 4px rgba(76,175,125,0.45); animation: pete-glow-pulse 2.4s ease-in-out infinite; }
|
||||
@@ -114,6 +118,7 @@ html[data-phase="night"] {
|
||||
.glow-theme-anime { box-shadow: 0 0 0 2px rgba(236,94,138,0.35), 0 0 24px 4px rgba(236,94,138,0.45); animation: pete-glow-pulse 2.4s ease-in-out infinite; }
|
||||
.glow-theme-foss { box-shadow: 0 0 0 2px rgba(217,119,6,0.35), 0 0 24px 4px rgba(217,119,6,0.45); animation: pete-glow-pulse 2.4s ease-in-out infinite; }
|
||||
.glow-theme-kids { box-shadow: 0 0 0 2px rgba(20,184,166,0.35), 0 0 24px 4px rgba(20,184,166,0.45); animation: pete-glow-pulse 2.4s ease-in-out infinite; }
|
||||
.glow-theme-finance { box-shadow: 0 0 0 2px rgba(16,185,129,0.35), 0 0 24px 4px rgba(16,185,129,0.45); animation: pete-glow-pulse 2.4s ease-in-out infinite; }
|
||||
|
||||
@keyframes pete-glow-pulse {
|
||||
0%, 100% { filter: brightness(1); }
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user