From 2d277a15f5b70a6188aa2871333e089f2f95ceec Mon Sep 17 00:00:00 2001
From: Wim <wim@42.be>
Date: Fri, 19 Apr 2019 18:27:31 +0200
Subject: 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:
-- 
cgit v1.2.3