Separate plugin interface to reduce file size
This commit is contained in:
parent
d572522a96
commit
ef8fffaff8
9 changed files with 254 additions and 31 deletions
|
@ -22,6 +22,7 @@ import (
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
"maubot.xyz"
|
"maubot.xyz"
|
||||||
"maubot.xyz/config"
|
"maubot.xyz/config"
|
||||||
flag "maunium.net/go/mauflag"
|
flag "maunium.net/go/mauflag"
|
||||||
|
|
|
@ -19,7 +19,6 @@ package database
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
|
||||||
log "maunium.net/go/maulogger"
|
log "maunium.net/go/maulogger"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
48
interfaces/interfaces.go
Normal file
48
interfaces/interfaces.go
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
package interfaces
|
||||||
|
|
||||||
|
type Plugin interface {
|
||||||
|
Start()
|
||||||
|
Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventHandler func(*Event) bool
|
||||||
|
|
||||||
|
type MatrixClient interface {
|
||||||
|
AddEventHandler(string, EventHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventFuncs interface {
|
||||||
|
Reply(text string) (string, error)
|
||||||
|
SendMessage(text string) (string, error)
|
||||||
|
SendEvent(content map[string]interface{}) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Event struct {
|
||||||
|
EventFuncs
|
||||||
|
|
||||||
|
StateKey string `json:"state_key,omitempty"` // The state key for the event. Only present on State Events.
|
||||||
|
Sender string `json:"sender"` // The user ID of the sender of the event
|
||||||
|
Type string `json:"type"` // The event type
|
||||||
|
Timestamp int64 `json:"origin_server_ts"` // The unix timestamp when this message was sent by the origin server
|
||||||
|
ID string `json:"event_id"` // The unique ID of this event
|
||||||
|
RoomID string `json:"room_id"` // The room the event was sent to. May be nil (e.g. for presence)
|
||||||
|
Content map[string]interface{} `json:"content"` // The JSON content of the event.
|
||||||
|
Redacts string `json:"redacts,omitempty"` // The event ID that was redacted if a m.room.redaction event
|
||||||
|
Unsigned Unsigned `json:"unsigned,omitempty"` // Unsigned content set by own homeserver.
|
||||||
|
}
|
||||||
|
|
||||||
|
type Unsigned struct {
|
||||||
|
PrevContent map[string]interface{} `json:"prev_content,omitempty"`
|
||||||
|
PrevSender string `json:"prev_sender,omitempty"`
|
||||||
|
ReplacesState string `json:"replaces_state,omitempty"`
|
||||||
|
Age int64 `json:"age"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PluginCreatorFunc func(client MatrixClient) Plugin
|
||||||
|
|
||||||
|
type PluginCreator struct {
|
||||||
|
Create PluginCreatorFunc
|
||||||
|
Name string
|
||||||
|
Version string
|
||||||
|
Path string
|
||||||
|
}
|
|
@ -17,6 +17,7 @@
|
||||||
package matrix
|
package matrix
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"maubot.xyz/interfaces"
|
||||||
"maunium.net/go/gomatrix"
|
"maunium.net/go/gomatrix"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -25,6 +26,30 @@ type Event struct {
|
||||||
Client *Client
|
Client *Client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (evt *Event) Interface() *interfaces.Event {
|
||||||
|
var stateKey string
|
||||||
|
if evt.StateKey != nil {
|
||||||
|
stateKey = *evt.StateKey
|
||||||
|
}
|
||||||
|
return &interfaces.Event{
|
||||||
|
EventFuncs: evt,
|
||||||
|
StateKey: stateKey,
|
||||||
|
Sender: evt.Sender,
|
||||||
|
Type: evt.Type,
|
||||||
|
Timestamp: evt.Timestamp,
|
||||||
|
ID: evt.ID,
|
||||||
|
RoomID: evt.RoomID,
|
||||||
|
Content: evt.Content,
|
||||||
|
Redacts: evt.Redacts,
|
||||||
|
Unsigned: interfaces.Unsigned{
|
||||||
|
PrevContent: evt.Unsigned.PrevContent,
|
||||||
|
PrevSender: evt.Unsigned.PrevSender,
|
||||||
|
ReplacesState: evt.Unsigned.ReplacesState,
|
||||||
|
Age: evt.Unsigned.Age,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (evt *Event) Reply(text string) (string, error) {
|
func (evt *Event) Reply(text string) (string, error) {
|
||||||
return evt.SendEvent(
|
return evt.SendEvent(
|
||||||
SetReply(
|
SetReply(
|
||||||
|
|
|
@ -18,6 +18,7 @@ package matrix
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"maubot.xyz/database"
|
"maubot.xyz/database"
|
||||||
|
"maubot.xyz/interfaces"
|
||||||
"maunium.net/go/gomatrix"
|
"maunium.net/go/gomatrix"
|
||||||
log "maunium.net/go/maulogger"
|
log "maunium.net/go/maulogger"
|
||||||
)
|
)
|
||||||
|
@ -39,22 +40,33 @@ func NewClient(db *database.MatrixClient) (*Client, error) {
|
||||||
DB: db,
|
DB: db,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
client.Syncer = NewMaubotSyncer(client, client.Store)
|
||||||
|
|
||||||
client.AddEventHandler(gomatrix.StateMember, client.onJoin)
|
client.AddEventHandler(gomatrix.StateMember, client.onJoin)
|
||||||
|
|
||||||
return client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) AddEventHandler(evt string, handler gomatrix.OnEventListener) {
|
func (client *Client) ParseEvent(evt *gomatrix.Event) *Event {
|
||||||
client.Syncer.(*gomatrix.DefaultSyncer).OnEventType(evt, handler)
|
return &Event{
|
||||||
|
Client: client,
|
||||||
|
Event: evt,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) onJoin(evt *gomatrix.Event) {
|
func (client *Client) AddEventHandler(evt string, handler interfaces.EventHandler) {
|
||||||
if !client.DB.AutoJoinRooms || evt.StateKey == nil || *evt.StateKey != client.DB.UserID {
|
client.Syncer.(*MaubotSyncer).OnEventType(evt, handler)
|
||||||
return
|
}
|
||||||
|
|
||||||
|
func (client *Client) onJoin(evt *interfaces.Event) bool {
|
||||||
|
if !client.DB.AutoJoinRooms || evt.StateKey != client.DB.UserID {
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
if membership, _ := evt.Content["membership"].(string); membership == "invite" {
|
if membership, _ := evt.Content["membership"].(string); membership == "invite" {
|
||||||
client.JoinRoom(evt.RoomID)
|
client.JoinRoom(evt.RoomID)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) JoinRoom(roomID string) {
|
func (client *Client) JoinRoom(roomID string) {
|
||||||
|
|
152
matrix/sync.go
Normal file
152
matrix/sync.go
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
package matrix
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"runtime/debug"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"maubot.xyz/interfaces"
|
||||||
|
"maunium.net/go/gomatrix"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MaubotSyncer struct {
|
||||||
|
Client *Client
|
||||||
|
Store gomatrix.Storer
|
||||||
|
listeners map[string][]interfaces.EventHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDefaultSyncer returns an instantiated DefaultSyncer
|
||||||
|
func NewMaubotSyncer(client *Client, store gomatrix.Storer) *MaubotSyncer {
|
||||||
|
return &MaubotSyncer{
|
||||||
|
Client: client,
|
||||||
|
Store: store,
|
||||||
|
listeners: make(map[string][]interfaces.EventHandler),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProcessResponse processes the /sync response in a way suitable for bots. "Suitable for bots" means a stream of
|
||||||
|
// unrepeating events. Returns a fatal error if a listener panics.
|
||||||
|
func (s *MaubotSyncer) ProcessResponse(res *gomatrix.RespSync, since string) (err error) {
|
||||||
|
if !s.shouldProcessResponse(res, since) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
err = fmt.Errorf("ProcessResponse panicked! userID=%s since=%s panic=%s\n%s", s.Client.UserID, since, r, debug.Stack())
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for roomID, roomData := range res.Rooms.Join {
|
||||||
|
room := s.getOrCreateRoom(roomID)
|
||||||
|
for _, event := range roomData.State.Events {
|
||||||
|
event.RoomID = roomID
|
||||||
|
room.UpdateState(event)
|
||||||
|
s.notifyListeners(event)
|
||||||
|
}
|
||||||
|
for _, event := range roomData.Timeline.Events {
|
||||||
|
event.RoomID = roomID
|
||||||
|
s.notifyListeners(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for roomID, roomData := range res.Rooms.Invite {
|
||||||
|
room := s.getOrCreateRoom(roomID)
|
||||||
|
for _, event := range roomData.State.Events {
|
||||||
|
event.RoomID = roomID
|
||||||
|
room.UpdateState(event)
|
||||||
|
s.notifyListeners(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for roomID, roomData := range res.Rooms.Leave {
|
||||||
|
room := s.getOrCreateRoom(roomID)
|
||||||
|
for _, event := range roomData.Timeline.Events {
|
||||||
|
if event.StateKey != nil {
|
||||||
|
event.RoomID = roomID
|
||||||
|
room.UpdateState(event)
|
||||||
|
s.notifyListeners(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnEventType allows callers to be notified when there are new events for the given event type.
|
||||||
|
// There are no duplicate checks.
|
||||||
|
func (s *MaubotSyncer) OnEventType(eventType string, callback interfaces.EventHandler) {
|
||||||
|
_, exists := s.listeners[eventType]
|
||||||
|
if !exists {
|
||||||
|
s.listeners[eventType] = []interfaces.EventHandler{}
|
||||||
|
}
|
||||||
|
s.listeners[eventType] = append(s.listeners[eventType], callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
// shouldProcessResponse returns true if the response should be processed. May modify the response to remove
|
||||||
|
// stuff that shouldn't be processed.
|
||||||
|
func (s *MaubotSyncer) shouldProcessResponse(resp *gomatrix.RespSync, since string) bool {
|
||||||
|
if since == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// This is a horrible hack because /sync will return the most recent messages for a room
|
||||||
|
// as soon as you /join it. We do NOT want to process those events in that particular room
|
||||||
|
// because they may have already been processed (if you toggle the bot in/out of the room).
|
||||||
|
//
|
||||||
|
// Work around this by inspecting each room's timeline and seeing if an m.room.member event for us
|
||||||
|
// exists and is "join" and then discard processing that room entirely if so.
|
||||||
|
// TODO: We probably want to process messages from after the last join event in the timeline.
|
||||||
|
for roomID, roomData := range resp.Rooms.Join {
|
||||||
|
for i := len(roomData.Timeline.Events) - 1; i >= 0; i-- {
|
||||||
|
e := roomData.Timeline.Events[i]
|
||||||
|
if e.Type == "m.room.member" && e.StateKey != nil && *e.StateKey == s.Client.UserID {
|
||||||
|
m := e.Content["membership"]
|
||||||
|
mship, ok := m.(string)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if mship == "join" {
|
||||||
|
_, ok := resp.Rooms.Join[roomID]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
delete(resp.Rooms.Join, roomID) // don't re-process messages
|
||||||
|
delete(resp.Rooms.Invite, roomID) // don't re-process invites
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// getOrCreateRoom must only be called by the Sync() goroutine which calls ProcessResponse()
|
||||||
|
func (s *MaubotSyncer) getOrCreateRoom(roomID string) *gomatrix.Room {
|
||||||
|
room := s.Store.LoadRoom(roomID)
|
||||||
|
if room == nil {
|
||||||
|
room = gomatrix.NewRoom(roomID)
|
||||||
|
s.Store.SaveRoom(room)
|
||||||
|
}
|
||||||
|
return room
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MaubotSyncer) notifyListeners(mxEvent *gomatrix.Event) {
|
||||||
|
event := s.Client.ParseEvent(mxEvent)
|
||||||
|
listeners, exists := s.listeners[event.Type]
|
||||||
|
if !exists {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, fn := range listeners {
|
||||||
|
if !fn(event.Interface()) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnFailedSync always returns a 10 second wait period between failed /syncs, never a fatal error.
|
||||||
|
func (s *MaubotSyncer) OnFailedSync(res *gomatrix.RespSync, err error) (time.Duration, error) {
|
||||||
|
return 10 * time.Second, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFilterJSON returns a filter with a timeline limit of 50.
|
||||||
|
func (s *MaubotSyncer) GetFilterJSON(userID string) json.RawMessage {
|
||||||
|
return json.RawMessage(`{"room":{"timeline":{"limit":50}}}`)
|
||||||
|
}
|
|
@ -22,6 +22,7 @@ import (
|
||||||
|
|
||||||
"maubot.xyz/config"
|
"maubot.xyz/config"
|
||||||
"maubot.xyz/database"
|
"maubot.xyz/database"
|
||||||
|
"maubot.xyz/interfaces"
|
||||||
"maubot.xyz/matrix"
|
"maubot.xyz/matrix"
|
||||||
log "maunium.net/go/maulogger"
|
log "maunium.net/go/maulogger"
|
||||||
)
|
)
|
||||||
|
@ -30,7 +31,7 @@ type Bot struct {
|
||||||
Config *config.MainConfig
|
Config *config.MainConfig
|
||||||
Database *database.Database
|
Database *database.Database
|
||||||
Clients map[string]*matrix.Client
|
Clients map[string]*matrix.Client
|
||||||
PluginCreators map[string]*PluginCreator
|
PluginCreators map[string]*interfaces.PluginCreator
|
||||||
Plugins map[string]*PluginWrapper
|
Plugins map[string]*PluginWrapper
|
||||||
Server *http.Server
|
Server *http.Server
|
||||||
}
|
}
|
||||||
|
@ -40,7 +41,7 @@ func New(config *config.MainConfig) *Bot {
|
||||||
Config: config,
|
Config: config,
|
||||||
Clients: make(map[string]*matrix.Client),
|
Clients: make(map[string]*matrix.Client),
|
||||||
Plugins: make(map[string]*PluginWrapper),
|
Plugins: make(map[string]*PluginWrapper),
|
||||||
PluginCreators: make(map[string]*PluginCreator),
|
PluginCreators: make(map[string]*interfaces.PluginCreator),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
28
plugin.go
28
plugin.go
|
@ -21,30 +21,16 @@ import (
|
||||||
"plugin"
|
"plugin"
|
||||||
|
|
||||||
"maubot.xyz/database"
|
"maubot.xyz/database"
|
||||||
"maubot.xyz/matrix"
|
"maubot.xyz/interfaces"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Plugin interface {
|
|
||||||
Start()
|
|
||||||
Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
type PluginWrapper struct {
|
type PluginWrapper struct {
|
||||||
Plugin
|
interfaces.Plugin
|
||||||
Creator *PluginCreator
|
Creator *interfaces.PluginCreator
|
||||||
DB *database.Plugin
|
DB *database.Plugin
|
||||||
}
|
}
|
||||||
|
|
||||||
type PluginCreatorFunc func(bot *Bot, info *database.Plugin, client *matrix.Client) Plugin
|
func LoadPlugin(path string) (*interfaces.PluginCreator, error) {
|
||||||
|
|
||||||
type PluginCreator struct {
|
|
||||||
Create PluginCreatorFunc
|
|
||||||
Name string
|
|
||||||
Version string
|
|
||||||
Path string
|
|
||||||
}
|
|
||||||
|
|
||||||
func LoadPlugin(path string) (*PluginCreator, error) {
|
|
||||||
rawPlugin, err := plugin.Open(path)
|
rawPlugin, err := plugin.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to open: %v", err)
|
return nil, fmt.Errorf("failed to open: %v", err)
|
||||||
|
@ -52,7 +38,7 @@ func LoadPlugin(path string) (*PluginCreator, error) {
|
||||||
|
|
||||||
pluginCreatorSymbol, err := rawPlugin.Lookup("Plugin")
|
pluginCreatorSymbol, err := rawPlugin.Lookup("Plugin")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
pluginCreator, ok := pluginCreatorSymbol.(*PluginCreator)
|
pluginCreator, ok := pluginCreatorSymbol.(*interfaces.PluginCreator)
|
||||||
if ok {
|
if ok {
|
||||||
pluginCreator.Path = path
|
pluginCreator.Path = path
|
||||||
return pluginCreator, nil
|
return pluginCreator, nil
|
||||||
|
@ -63,7 +49,7 @@ func LoadPlugin(path string) (*PluginCreator, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("symbol \"Create\" not found: %v", err)
|
return nil, fmt.Errorf("symbol \"Create\" not found: %v", err)
|
||||||
}
|
}
|
||||||
pluginCreatorFunc, ok := pluginCreatorFuncSymbol.(PluginCreatorFunc)
|
pluginCreatorFunc, ok := pluginCreatorFuncSymbol.(interfaces.PluginCreatorFunc)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("symbol \"Create\" does not implement maubot.PluginCreator")
|
return nil, fmt.Errorf("symbol \"Create\" does not implement maubot.PluginCreator")
|
||||||
}
|
}
|
||||||
|
@ -86,7 +72,7 @@ func LoadPlugin(path string) (*PluginCreator, error) {
|
||||||
return nil, fmt.Errorf("symbol \"Version\" is not a string")
|
return nil, fmt.Errorf("symbol \"Version\" is not a string")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &PluginCreator{
|
return &interfaces.PluginCreator{
|
||||||
Create: pluginCreatorFunc,
|
Create: pluginCreatorFunc,
|
||||||
Name: name,
|
Name: name,
|
||||||
Version: version,
|
Version: version,
|
||||||
|
|
|
@ -86,10 +86,9 @@ func (bot *Bot) createPlugins() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
log.Debugf("Created plugin %s (type %s v%s)\n", plugin.ID, creator.Name, creator.Version)
|
log.Debugf("Created plugin %s (type %s v%s)\n", plugin.ID, creator.Name, creator.Version)
|
||||||
bot.Plugins[plugin.ID] = &PluginWrapper{
|
bot.Plugins[plugin.ID] = &PluginWrapper{
|
||||||
Plugin: creator.Create(bot, plugin, client),
|
Plugin: creator.Create(client),
|
||||||
Creator: creator,
|
Creator: creator,
|
||||||
DB: plugin,
|
DB: plugin,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue