From 2d277a15f5b70a6188aa2871333e089f2f95ceec Mon Sep 17 00:00:00 2001 From: Wim Date: Fri, 19 Apr 2019 18:27:31 +0200 Subject: [PATCH] Add scripting (tengo) support for every outgoing message (#806) Adds a new key OutMessage under [tengo] table, which specifies the location of the script that will be invoked on each message being sent to a bridge and can be used to modify the Username and the Text of that message. The script will have the following global variables: read-only: inAccount, inProtocol, inChannel, inGateway outAccount, outProtocol, outChannel, outGateway read-write: msgText, msgUsername The script is reloaded on every message, so you can modify the script on the fly. The default script in https://github.com/42wim/matterbridge/tree/master/internal/tengo/outmessage.tengo is compiled in and will be executed if no script is specified. --- bridge/config/config.go | 2 + bridge/irc/handlers.go | 5 - contrib/outmessage-irccolors.tengo | 7 + gateway/gateway.go | 45 +++++ internal/bindata.go | 288 +++++++++++++++++++++++++++++ internal/tengo/outmessage.tengo | 19 ++ matterbridge.toml.sample | 24 ++- 7 files changed, 383 insertions(+), 7 deletions(-) create mode 100644 contrib/outmessage-irccolors.tengo create mode 100644 internal/bindata.go create mode 100644 internal/tengo/outmessage.tengo diff --git a/bridge/config/config.go b/bridge/config/config.go index 1af6f407..d8237555 100644 --- a/bridge/config/config.go +++ b/bridge/config/config.go @@ -168,8 +168,10 @@ type Gateway struct { } type Tengo struct { + InMessage string Message string RemoteNickFormat string + OutMessage string } type SameChannelGateway struct { diff --git a/bridge/irc/handlers.go b/bridge/irc/handlers.go index f0e54928..8f0acaad 100644 --- a/bridge/irc/handlers.go +++ b/bridge/irc/handlers.go @@ -4,7 +4,6 @@ import ( "bytes" "fmt" "io/ioutil" - "regexp" "strconv" "strings" "time" @@ -182,10 +181,6 @@ func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) { // strip action, we made an event if it was an action rmsg.Text += event.StripAction() - // strip IRC colors - re := regexp.MustCompile(`\x03(?:\d{1,2}(?:,\d{1,2})?)?|[[:cntrl:]]`) - rmsg.Text = re.ReplaceAllString(rmsg.Text, "") - // start detecting the charset mycharset := b.GetString("Charset") if mycharset == "" { diff --git a/contrib/outmessage-irccolors.tengo b/contrib/outmessage-irccolors.tengo new file mode 100644 index 00000000..fe63da43 --- /dev/null +++ b/contrib/outmessage-irccolors.tengo @@ -0,0 +1,7 @@ +// See https://github.com/42wim/matterbridge/issues/798 + +// if we're not sending to an irc bridge we strip the IRC colors +if outProtocol != "irc" { + re := text.re_compile(`\x03(?:\d{1,2}(?:,\d{1,2})?)?|[[:cntrl:]]`) + msgText=re.replace(msgText,"") +} diff --git a/gateway/gateway.go b/gateway/gateway.go index d46f75d6..a9c331bc 100644 --- a/gateway/gateway.go +++ b/gateway/gateway.go @@ -9,6 +9,7 @@ import ( "github.com/42wim/matterbridge/bridge" "github.com/42wim/matterbridge/bridge/config" + "github.com/42wim/matterbridge/internal" "github.com/d5/tengo/script" "github.com/d5/tengo/stdlib" lru "github.com/hashicorp/golang-lru" @@ -429,6 +430,11 @@ func (gw *Gateway) SendMessage( msg.ParentID = "msg-parent-not-found" } + err := gw.modifySendMessageTengo(rmsg, &msg, dest) + if err != nil { + gw.logger.Errorf("modifySendMessageTengo: %s", err) + } + // if we are using mattermost plugin account, send messages to MattermostPlugin channel // that can be picked up by the mattermost matterbridge plugin if dest.Account == "mattermost.plugin" { @@ -544,3 +550,42 @@ func (gw *Gateway) modifyUsernameTengo(msg *config.Message, br *bridge.Bridge) ( } return c.Get("result").String(), nil } + +func (gw *Gateway) modifySendMessageTengo(origmsg *config.Message, msg *config.Message, br *bridge.Bridge) error { + filename := gw.BridgeValues().Tengo.OutMessage + var res []byte + var err error + if filename == "" { + res, err = internal.Asset("tengo/outmessage.tengo") + if err != nil { + return err + } + } else { + res, err = ioutil.ReadFile(filename) + if err != nil { + return err + } + } + s := script.New(res) + s.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...)) + _ = s.Add("inAccount", origmsg.Account) + _ = s.Add("inProtocol", origmsg.Protocol) + _ = s.Add("inChannel", origmsg.Channel) + _ = s.Add("inGateway", origmsg.Gateway) + _ = s.Add("outAccount", br.Account) + _ = s.Add("outProtocol", br.Protocol) + _ = s.Add("outChannel", msg.Channel) + _ = s.Add("outGateway", gw.Name) + _ = s.Add("msgText", msg.Text) + _ = s.Add("msgUsername", msg.Username) + c, err := s.Compile() + if err != nil { + return err + } + if err := c.Run(); err != nil { + return err + } + msg.Text = c.Get("msgText").String() + msg.Username = c.Get("msgUsername").String() + return nil +} diff --git a/internal/bindata.go b/internal/bindata.go new file mode 100644 index 00000000..65295716 --- /dev/null +++ b/internal/bindata.go @@ -0,0 +1,288 @@ +// Code generated by go-bindata. DO NOT EDIT. +// sources: +// tengo/outmessage.tengo + +package internal + + +import ( + "bytes" + "compress/gzip" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + "time" +) + +func bindataRead(data []byte, name string) ([]byte, error) { + gz, err := gzip.NewReader(bytes.NewBuffer(data)) + if err != nil { + return nil, fmt.Errorf("Read %q: %v", name, err) + } + + var buf bytes.Buffer + _, err = io.Copy(&buf, gz) + clErr := gz.Close() + + if err != nil { + return nil, fmt.Errorf("Read %q: %v", name, err) + } + if clErr != nil { + return nil, err + } + + return buf.Bytes(), nil +} + + +type asset struct { + bytes []byte + info fileInfoEx +} + +type fileInfoEx interface { + os.FileInfo + MD5Checksum() string +} + +type bindataFileInfo struct { + name string + size int64 + mode os.FileMode + modTime time.Time + md5checksum string +} + +func (fi bindataFileInfo) Name() string { + return fi.name +} +func (fi bindataFileInfo) Size() int64 { + return fi.size +} +func (fi bindataFileInfo) Mode() os.FileMode { + return fi.mode +} +func (fi bindataFileInfo) ModTime() time.Time { + return fi.modTime +} +func (fi bindataFileInfo) MD5Checksum() string { + return fi.md5checksum +} +func (fi bindataFileInfo) IsDir() bool { + return false +} +func (fi bindataFileInfo) Sys() interface{} { + return nil +} + +var _bindataTengoOutmessagetengo = []byte( + "\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xc4\x91\x3d\x8f\xda\x40\x10\x86\xfb\xfd\x15\x13\x37\xb1\x2d\x07\xe7\xa3" + + "\xb3\x64\x59\x11\x45\x94\x2e\x8a\x92\x0a\xd0\xb1\xac\x07\x33\xd2\x7a\xc7\x1a\x8f\x31\x88\xe3\xbf\x9f\xcc\x01\x47" + + "\x7f\xc5\x75\xef\xae\x9e\x9d\x77\x1f\x4d\x9e\x9a\xbd\x15\xb2\x1b\x8f\x3d\xd8\xbd\x25\x3f\x45\x30\x82\xb6\xfe\xc2" + + "\xc1\x1f\x0b\x43\xe1\xa7\x73\x3c\x04\xcd\x80\xc2\x1f\x61\x65\xc7\x7e\xca\xf3\x9d\x0d\x01\x2f\xf1\x97\x55\x1c\xed" + + "\xd1\xf0\xa0\x77\x98\x07\x7d\xa3\x79\xd0\x3b\xce\x83\xde\xf8\xd7\x9e\x51\x48\xb1\x30\x6d\xdf\xfc\xc3\x83\x66\xd0" + + "\xf6\xcd\xff\x1e\x25\xd8\x16\x4d\x9a\x1b\xa3\x78\x50\x28\x4a\xa0\xb6\x63\xd1\x38\x9a\xce\x51\x62\x4c\x9e\x43\xaf" + + "\x42\x1d\x90\x38\x70\xec\x59\xfa\xe9\x8e\xb6\x30\xe2\x67\x41\x08\xac\xd0\x63\xa8\x29\x34\xa0\x0c\x36\x5c\xc0\x8d" + + "\x50\xdd\x20\x8c\x78\x7d\xac\x3b\x84\xdf\x7f\xe7\xb7\x01\xb4\x7d\xd0\x84\xb2\x84\x88\xc4\x45\x70\x32\x00\x00\x82" + + "\xd3\x3f\xa6\xfe\x99\xe0\x93\xe3\xb6\x23\x8f\xf1\x7a\x79\xf8\xfa\x23\xae\x8a\x65\x7d\xfa\x96\x7d\x3f\xc7\x55\x91" + + "\x5d\x63\x52\x25\xd5\xf3\x62\x51\xb8\xa0\xe2\x8b\xd5\x6a\x9d\x5c\xc6\x5c\x4d\x4b\xc1\x99\x60\xe7\xad\xc3\xf8\x26" + + "\x1f\x45\x89\x39\x9b\xf7\x6b\xe4\x29\x6d\x1f\x57\x00\x9f\x3e\xc6\x24\xcd\xcd\x4b\x00\x00\x00\xff\xff\x40\xb8\x54" + + "\xb8\x64\x02\x00\x00") + +func bindataTengoOutmessagetengoBytes() ([]byte, error) { + return bindataRead( + _bindataTengoOutmessagetengo, + "tengo/outmessage.tengo", + ) +} + + + +func bindataTengoOutmessagetengo() (*asset, error) { + bytes, err := bindataTengoOutmessagetengoBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{ + name: "tengo/outmessage.tengo", + size: 612, + md5checksum: "", + mode: os.FileMode(420), + modTime: time.Unix(1555622139, 0), + } + + a := &asset{bytes: bytes, info: info} + + return a, nil +} + + +// +// Asset loads and returns the asset for the given name. +// It returns an error if the asset could not be found or +// could not be loaded. +// +func Asset(name string) ([]byte, error) { + cannonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[cannonicalName]; ok { + a, err := f() + if err != nil { + return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) + } + return a.bytes, nil + } + return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrNotExist} +} + +// +// MustAsset is like Asset but panics when Asset would return an error. +// It simplifies safe initialization of global variables. +// nolint: deadcode +// +func MustAsset(name string) []byte { + a, err := Asset(name) + if err != nil { + panic("asset: Asset(" + name + "): " + err.Error()) + } + + return a +} + +// +// AssetInfo loads and returns the asset info for the given name. +// It returns an error if the asset could not be found or could not be loaded. +// +func AssetInfo(name string) (os.FileInfo, error) { + cannonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[cannonicalName]; ok { + a, err := f() + if err != nil { + return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) + } + return a.info, nil + } + return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrNotExist} +} + +// +// AssetNames returns the names of the assets. +// nolint: deadcode +// +func AssetNames() []string { + names := make([]string, 0, len(_bindata)) + for name := range _bindata { + names = append(names, name) + } + return names +} + +// +// _bindata is a table, holding each asset generator, mapped to its name. +// +var _bindata = map[string]func() (*asset, error){ + "tengo/outmessage.tengo": bindataTengoOutmessagetengo, +} + +// +// AssetDir returns the file names below a certain +// directory embedded in the file by go-bindata. +// For example if you run go-bindata on data/... and data contains the +// following hierarchy: +// data/ +// foo.txt +// img/ +// a.png +// b.png +// then AssetDir("data") would return []string{"foo.txt", "img"} +// AssetDir("data/img") would return []string{"a.png", "b.png"} +// AssetDir("foo.txt") and AssetDir("notexist") would return an error +// AssetDir("") will return []string{"data"}. +// +func AssetDir(name string) ([]string, error) { + node := _bintree + if len(name) != 0 { + cannonicalName := strings.Replace(name, "\\", "/", -1) + pathList := strings.Split(cannonicalName, "/") + for _, p := range pathList { + node = node.Children[p] + if node == nil { + return nil, &os.PathError{ + Op: "open", + Path: name, + Err: os.ErrNotExist, + } + } + } + } + if node.Func != nil { + return nil, &os.PathError{ + Op: "open", + Path: name, + Err: os.ErrNotExist, + } + } + rv := make([]string, 0, len(node.Children)) + for childName := range node.Children { + rv = append(rv, childName) + } + return rv, nil +} + + +type bintree struct { + Func func() (*asset, error) + Children map[string]*bintree +} + +var _bintree = &bintree{Func: nil, Children: map[string]*bintree{ + "tengo": {Func: nil, Children: map[string]*bintree{ + "outmessage.tengo": {Func: bindataTengoOutmessagetengo, Children: map[string]*bintree{}}, + }}, +}} + +// RestoreAsset restores an asset under the given directory +func RestoreAsset(dir, name string) error { + data, err := Asset(name) + if err != nil { + return err + } + info, err := AssetInfo(name) + if err != nil { + return err + } + err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) + if err != nil { + return err + } + err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) + if err != nil { + return err + } + return os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) +} + +// RestoreAssets restores an asset under the given directory recursively +func RestoreAssets(dir, name string) error { + children, err := AssetDir(name) + // File + if err != nil { + return RestoreAsset(dir, name) + } + // Dir + for _, child := range children { + err = RestoreAssets(dir, filepath.Join(name, child)) + if err != nil { + return err + } + } + return nil +} + +func _filePath(dir, name string) string { + cannonicalName := strings.Replace(name, "\\", "/", -1) + return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) +} diff --git a/internal/tengo/outmessage.tengo b/internal/tengo/outmessage.tengo new file mode 100644 index 00000000..d218088a --- /dev/null +++ b/internal/tengo/outmessage.tengo @@ -0,0 +1,19 @@ +/* +variables available +read-only: +inAccount, inProtocol, inChannel, inGateway +outAccount, outProtocol, outChannel, outGateway + +read-write: +msgText, msgUsername +*/ + +text := import("text") + +// start - strip irc colors +// if we're not sending to an irc bridge we strip the IRC colors +if inProtocol == "irc" { + re := text.re_compile(`\x03(?:\d{1,2}(?:,\d{1,2})?)?|[[:cntrl:]]`) + msgText=re.replace(msgText,"") +} +// end - strip irc colors diff --git a/matterbridge.toml.sample b/matterbridge.toml.sample index a53cec18..93881ad1 100644 --- a/matterbridge.toml.sample +++ b/matterbridge.toml.sample @@ -1452,7 +1452,7 @@ IgnoreFailureOnStart=false #https://github.com/d5/tengo/blob/master/docs/stdlib.md [tengo] -#Message allows you to specify the location of a tengo (https://github.com/d5/tengo/) script. +#InMessage allows you to specify the location of a tengo (https://github.com/d5/tengo/) script. #This script will receive every incoming message and can be used to modify the Username and the Text of that message. #The script will have the following global variables: #to modify: msgUsername and msgText @@ -1470,7 +1470,27 @@ IgnoreFailureOnStart=false # msgUsername="fakeuser" #} #OPTIONAL (default empty) -Message="example.tengo" +InMessage="example.tengo" + +#OutMessage allows you to specify the location of the script that +#will be invoked on each message being sent to a bridge and can be used to modify the Username +#and the Text of that message. +# +#The script will have the following global variables: +#read-only: +#inAccount, inProtocol, inChannel, inGateway +#outAccount, outProtocol, outChannel, outGateway +# +#read-write: +#msgText, msgUsername +# +#The script is reloaded on every message, so you can modify the script on the fly. +# +#The default script in https://github.com/42wim/matterbridge/tree/master/internal/tengo/outmessage.tengo +#is compiled in and will be executed if no script is specified. +#OPTIONAL (default empty) +OutMessage="example.tengo" + #RemoteNickFormat allows you to specify the location of a tengo (https://github.com/d5/tengo/) script. #The script will have the following global variables: