36a485ea26
Backend extends MatrixService with Start()/Stop()/ListRooms()/LoadTimeline()/
SendText()/SendMarkdown(). On login the service initialises the crypto store
(cryptohelper, Olm/Megolm via goolm build tag) and a sync loop that fans
events out through Wails events ("matrix:event", "matrix:error"). Pickle
key is 32 random bytes hex-encoded in the OS keyring alongside the access
token, so the crypto SQLite store survives restarts.
Vendors 4 fresh helpers from fn_registry/functions/infra/:
matrix_crypto_init.go (//go:build goolm || libolm)
matrix_sync_service.go
matrix_message_send.go
matrix_room_list.go
Plus the existing 3 (mas_oidc_loopback, keyring_token_store, matrix_client_init).
go-sqlite3 driver pulled explicitly via sqlite_driver.go.
Frontend rewires HomeScreen as a 3-zone AppShell (sidebar / timeline /
composer). useMatrixRooms polls + reacts to the sync stream; useMatrixTimeline
loads the last 50 events of the selected room and appends live ones. New
components: RoomList, Timeline, EventBubble, Composer. Composer supports
plain text (default) and a markdown toggle; Enter sends, Shift+Enter newline.
wails.json now passes "build:tags": "goolm" by default. Tested with
wails build -tags goolm on linux/amd64 and windows/amd64.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
85 lines
3.0 KiB
Go
85 lines
3.0 KiB
Go
package infra
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
keyring "github.com/zalando/go-keyring"
|
|
)
|
|
|
|
// ErrNotFound is returned by Load when no token exists for the given account.
|
|
var ErrNotFound = errors.New("token not found in keyring")
|
|
|
|
// Token holds OAuth/OIDC credentials that need to survive app restarts.
|
|
type Token struct {
|
|
AccessToken string `json:"access_token"`
|
|
RefreshToken string `json:"refresh_token,omitempty"`
|
|
ExpiresAt time.Time `json:"expires_at,omitempty"` // zero = never expires
|
|
UserID string `json:"user_id"`
|
|
DeviceID string `json:"device_id,omitempty"`
|
|
HomeserverURL string `json:"homeserver_url"`
|
|
Issuer string `json:"issuer,omitempty"` // MAS/OIDC issuer URL
|
|
ClientID string `json:"client_id,omitempty"` // MAS client_id used
|
|
// PickleKeyHex is the 32-byte hex-encoded key used by cryptohelper to
|
|
// pickle Olm/Megolm sessions at-rest in the SQLite crypto store.
|
|
// MUST persist across restarts. If lost, the crypto store is unusable
|
|
// and a fresh device login is required.
|
|
PickleKeyHex string `json:"pickle_key_hex,omitempty"`
|
|
}
|
|
|
|
// KeyringTokenStore persists tokens in the OS keyring (Secret Service on Linux,
|
|
// Keychain on macOS, Credential Manager on Windows).
|
|
type KeyringTokenStore struct {
|
|
// Service is the keyring namespace. Keep it stable across app versions.
|
|
// Example: "fn_registry.matrix_client_pc"
|
|
Service string
|
|
}
|
|
|
|
// NewKeyringTokenStore returns a store scoped to the given service name.
|
|
func NewKeyringTokenStore(service string) *KeyringTokenStore {
|
|
return &KeyringTokenStore{Service: service}
|
|
}
|
|
|
|
// Save serialises t to JSON and writes it to the keyring under (service, account).
|
|
// Overwrites silently if an entry already exists.
|
|
// account is typically the user ID, e.g. "@user:homeserver.example.com".
|
|
func (s *KeyringTokenStore) Save(account string, t Token) error {
|
|
b, err := json.Marshal(t)
|
|
if err != nil {
|
|
return fmt.Errorf("keyring save: marshal: %w", err)
|
|
}
|
|
if err := keyring.Set(s.Service, account, string(b)); err != nil {
|
|
return fmt.Errorf("keyring save: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Load retrieves and deserialises the token stored under (service, account).
|
|
// Returns ErrNotFound if no entry exists. Callers should check with errors.Is.
|
|
func (s *KeyringTokenStore) Load(account string) (*Token, error) {
|
|
raw, err := keyring.Get(s.Service, account)
|
|
if err != nil {
|
|
if errors.Is(err, keyring.ErrNotFound) {
|
|
return nil, ErrNotFound
|
|
}
|
|
return nil, fmt.Errorf("keyring load: %w", err)
|
|
}
|
|
var t Token
|
|
if err := json.Unmarshal([]byte(raw), &t); err != nil {
|
|
return nil, fmt.Errorf("keyring load: unmarshal: %w", err)
|
|
}
|
|
return &t, nil
|
|
}
|
|
|
|
// Delete removes the token for account from the keyring.
|
|
// Idempotent: if no entry exists, returns nil.
|
|
func (s *KeyringTokenStore) Delete(account string) error {
|
|
err := keyring.Delete(s.Service, account)
|
|
if err != nil && !errors.Is(err, keyring.ErrNotFound) {
|
|
return fmt.Errorf("keyring delete: %w", err)
|
|
}
|
|
return nil
|
|
}
|