mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 00:32:40 +00:00
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).
123 lines
3.2 KiB
Go
123 lines
3.2 KiB
Go
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
|
|
}
|
|
}
|
|
}
|