mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 00:52:40 +00:00
Add appservice auth mode behind AUTH_MODE; land device grant
Two things, entangled in the working tree because 20d1f92 was mislabeled
(its message claimed "device grant" but it only deleted registration.yaml.example
and left the failed appservice+/sync hybrid in client.go):
1. Land the MAS OAuth 2.0 device grant that was running in prod but never
committed: masauth.go + client.go rewrite. Bot stays a normal user so /sync
works; refresh token persisted in data/mas_auth.json.
2. Add "appservice" as an alternate transport behind AUTH_MODE (default
"masdevice" = unchanged device-grant path, instant rollback):
- internal/bot/session.go: Session abstraction unifying both transports
(OnEventType/Run/Stop); NewSession dispatches on AuthMode.
- internal/bot/appservice.go: as_token auth (MAS-durable, no login/MFA/expiry),
cryptohelper MSC4190 device creation, crypto machine fed from Synapse
transaction pushes (to-device/device-lists/OTK) instead of /sync, inbound
decrypt+redispatch, and lazy per-room StateStore backfill (encryption +
members) so replies in encrypted rooms encrypt to the right recipients.
- main.go: NewSession/sess.OnEventType/sess.Run in place of the /sync loop.
- .env.example: AUTH_MODE, AS_REGISTRATION, AS_LISTEN_HOST/PORT, HOMESERVER_DOMAIN.
The appservice path fixes the earlier hybrid's fatal flaw (Synapse forbids AS
users from /sync) by using the transaction/push model. Bot-side only; Synapse
registration + experimental_features (msc2409/msc3202/msc4190) and E2EE cutover
are the next steps. Build + vet green (CGO=1 -tags goolm).
This commit is contained in:
122
internal/bot/session.go
Normal file
122
internal/bot/session.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package bot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/appservice"
|
||||
"maunium.net/go/mautrix/event"
|
||||
)
|
||||
|
||||
// Session is the bot's live Matrix connection plus its event source. It hides
|
||||
// the difference between the two transports the bot can run under:
|
||||
//
|
||||
// - masdevice: a normal Matrix user authenticated via the MAS OAuth device
|
||||
// grant, receiving events over /sync (mautrix DefaultSyncer).
|
||||
// - appservice: a Synapse appservice authenticated by its as_token, receiving
|
||||
// events pushed over the appservice transaction API (mautrix EventProcessor).
|
||||
//
|
||||
// Callers register the same three handlers (message/member/reaction) via
|
||||
// OnEventType and call Run regardless of mode; the plugin layer never knows which
|
||||
// transport is in use.
|
||||
type Session struct {
|
||||
Client *mautrix.Client
|
||||
mode string
|
||||
|
||||
// masdevice
|
||||
syncer *mautrix.DefaultSyncer
|
||||
|
||||
// appservice
|
||||
as *appservice.AppService
|
||||
ep *appservice.EventProcessor
|
||||
}
|
||||
|
||||
// EventHandler matches both DefaultSyncer.OnEventType and EventProcessor.On.
|
||||
type EventHandler = func(ctx context.Context, evt *event.Event)
|
||||
|
||||
// NewSession builds a Session for the configured auth mode. Default and
|
||||
// "masdevice" use the MAS device grant; "appservice" uses the transaction model.
|
||||
func NewSession(cfg Config) (*Session, error) {
|
||||
switch cfg.AuthMode {
|
||||
case "appservice":
|
||||
return newAppserviceSession(cfg)
|
||||
case "", "masdevice":
|
||||
return newMASDeviceSession(cfg)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown AUTH_MODE %q (want appservice or masdevice)", cfg.AuthMode)
|
||||
}
|
||||
}
|
||||
|
||||
// newMASDeviceSession wraps the existing device-grant client (NewClient) and its
|
||||
// DefaultSyncer. This is the retained rollback path — unchanged behaviour.
|
||||
func newMASDeviceSession(cfg Config) (*Session, error) {
|
||||
client, err := NewClient(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Session{
|
||||
Client: client,
|
||||
mode: "masdevice",
|
||||
syncer: client.Syncer.(*mautrix.DefaultSyncer),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// OnEventType registers a handler for the given event type against whichever
|
||||
// event source backs this session.
|
||||
func (s *Session) OnEventType(evtType event.Type, fn EventHandler) {
|
||||
switch s.mode {
|
||||
case "appservice":
|
||||
s.ep.On(evtType, fn)
|
||||
default:
|
||||
s.syncer.OnEventType(evtType, fn)
|
||||
}
|
||||
}
|
||||
|
||||
// Run blocks, delivering events to registered handlers, until ctx is cancelled.
|
||||
func (s *Session) Run(ctx context.Context) error {
|
||||
switch s.mode {
|
||||
case "appservice":
|
||||
return s.runAppservice(ctx)
|
||||
default:
|
||||
return s.runSync(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop halts the event source. Safe to call once.
|
||||
func (s *Session) Stop() {
|
||||
switch s.mode {
|
||||
case "appservice":
|
||||
if s.ep != nil {
|
||||
s.ep.Stop()
|
||||
}
|
||||
if s.as != nil {
|
||||
s.as.Stop()
|
||||
}
|
||||
default:
|
||||
s.Client.StopSync()
|
||||
}
|
||||
}
|
||||
|
||||
// runSync is the device-grant /sync loop (moved verbatim from main.go): restart
|
||||
// on transient failure, exit on context cancel.
|
||||
func (s *Session) runSync(ctx context.Context) error {
|
||||
for {
|
||||
err := s.Client.SyncWithContext(ctx)
|
||||
if ctx.Err() != nil {
|
||||
return nil // shutdown requested
|
||||
}
|
||||
if err != nil {
|
||||
slog.Error("sync stopped, restarting in 5s", "err", err)
|
||||
} else {
|
||||
slog.Warn("sync returned without error, restarting in 5s")
|
||||
}
|
||||
select {
|
||||
case <-time.After(5 * time.Second):
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user