Mumble bridge: Add HTML/Markdown conversion and image handling
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
package bmumble
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"layeh.com/gumble/gumble"
|
||||
"layeh.com/gumble/gumbleutil"
|
||||
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
"github.com/42wim/matterbridge/bridge/helper"
|
||||
)
|
||||
|
||||
func (b *Bmumble) handleServerConfig(event *gumble.ServerConfigEvent) {
|
||||
@@ -12,15 +15,32 @@ func (b *Bmumble) handleServerConfig(event *gumble.ServerConfigEvent) {
|
||||
}
|
||||
|
||||
func (b *Bmumble) handleTextMessage(event *gumble.TextMessageEvent) {
|
||||
rmsg := config.Message{
|
||||
Text: event.TextMessage.Message,
|
||||
Channel: event.Client.Self.Channel.Name,
|
||||
Username: event.TextMessage.Sender.Name,
|
||||
UserID: event.TextMessage.Sender.Name + "@" + b.Host,
|
||||
Account: b.Account,
|
||||
// Convert Mumble HTML messages to markdown
|
||||
parts, err := b.convertHTMLtoMarkdown(event.TextMessage.Message)
|
||||
if err != nil {
|
||||
b.Log.Error(err)
|
||||
}
|
||||
for i, part := range parts {
|
||||
// Construct matterbridge message and pass on to the gateway
|
||||
rmsg := config.Message{
|
||||
Channel: event.Client.Self.Channel.Name,
|
||||
Username: event.TextMessage.Sender.Name,
|
||||
UserID: event.TextMessage.Sender.Name + "@" + b.Host,
|
||||
Account: b.Account,
|
||||
}
|
||||
if part.Image == nil {
|
||||
rmsg.Text = part.Text
|
||||
} else {
|
||||
rmsg.Extra = make(map[string][]interface{})
|
||||
if err = helper.HandleDownloadSize(b.Log, &rmsg, "image"+strconv.Itoa(i)+part.FileExtension, int64(len(part.Image)), b.General); err != nil {
|
||||
b.Log.WithError(err).Warn("not including image in message")
|
||||
continue
|
||||
}
|
||||
helper.HandleDownloadData(b.Log, &rmsg, "image"+strconv.Itoa(i)+part.FileExtension, "", "", &part.Image, b.General)
|
||||
}
|
||||
b.Log.Debugf("<= Remote message is %+v", rmsg)
|
||||
b.Remote <- rmsg
|
||||
}
|
||||
b.Log.Debugf("<= Remote message is %+v", rmsg)
|
||||
b.Remote <- rmsg
|
||||
}
|
||||
|
||||
func (b *Bmumble) handleConnect(event *gumble.ConnectEvent) {
|
||||
|
||||
119
bridge/mumble/helpers.go
Normal file
119
bridge/mumble/helpers.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package bmumble
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"mime"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/42wim/matterbridge/bridge/config"
|
||||
md "github.com/JohannesKaufmann/html-to-markdown"
|
||||
"github.com/vincent-petithory/dataurl"
|
||||
)
|
||||
|
||||
type MessagePart struct {
|
||||
Text string
|
||||
FileExtension string
|
||||
Image []byte
|
||||
}
|
||||
|
||||
func (b *Bmumble) tokenize(t *string) ([]MessagePart, error) {
|
||||
// `^(.*?)` matches everyting before the image
|
||||
// `!\[[^\]]*\]\(` matches the `]+)` matches the data: URI used by Mumble
|
||||
// `\)` matches the closing parenthesis after the URI
|
||||
// `(.*)$` matches the remaining text to be examined in the next iteration
|
||||
p, err := regexp.Compile(`^(.*?)!\[[^\]]*\]\((data:image\/[^)]+)\)(.*)$`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
remaining := *t
|
||||
var parts []MessagePart
|
||||
for {
|
||||
tokens := p.FindStringSubmatch(remaining)
|
||||
|
||||
if tokens == nil {
|
||||
b.Log.Debugf("Last text token: %s", remaining)
|
||||
// no match -> remaining string is non-image text
|
||||
if len(remaining) > 0 {
|
||||
parts = append(parts, MessagePart{remaining, "", nil})
|
||||
}
|
||||
return parts, nil
|
||||
}
|
||||
if len(tokens[1]) > 0 {
|
||||
parts = append(parts, MessagePart{tokens[1], "", nil})
|
||||
}
|
||||
uri, err := dataurl.UnescapeToString(strings.ReplaceAll(tokens[2], " ", ""))
|
||||
if err != nil {
|
||||
b.Log.WithError(err).Info("URL unescaping failed")
|
||||
} else {
|
||||
b.Log.Debugf("Raw data: URL: %s", uri)
|
||||
image, err := dataurl.DecodeString(uri)
|
||||
if err == nil {
|
||||
ext, err := mime.ExtensionsByType(image.MediaType.ContentType())
|
||||
if ext != nil && len(ext) > 0 {
|
||||
parts = append(parts, MessagePart{"", ext[0], image.Data})
|
||||
} else {
|
||||
b.Log.WithError(err).Infof("No file extension registered for MIME type '%s'", image.MediaType.ContentType())
|
||||
}
|
||||
} else {
|
||||
b.Log.WithError(err).Info("No image extracted")
|
||||
}
|
||||
}
|
||||
remaining = tokens[3]
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bmumble) convertHTMLtoMarkdown(html string) ([]MessagePart, error) {
|
||||
converter := md.NewConverter("", true, nil)
|
||||
markdown, err := converter.ConvertString(html)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b.tokenize(&markdown)
|
||||
}
|
||||
|
||||
func (b *Bmumble) extractFiles(msg *config.Message) []config.Message {
|
||||
var messages []config.Message
|
||||
if msg.Extra == nil || len(msg.Extra["file"]) == 0 {
|
||||
return messages
|
||||
}
|
||||
for _, f := range msg.Extra["file"] {
|
||||
fi := f.(config.FileInfo)
|
||||
if fi.Data == nil || len(*fi.Data) == 0 {
|
||||
// Mumble needs the raw data
|
||||
b.Log.Info("Not forwarding file without local data")
|
||||
continue
|
||||
}
|
||||
mimeType := http.DetectContentType(*fi.Data)
|
||||
if !strings.HasPrefix(mimeType, "image/") {
|
||||
// Mumble only supports images
|
||||
b.Log.Infof("Not forwarding file of type %s", mimeType)
|
||||
continue
|
||||
}
|
||||
mimeType = strings.TrimSpace(strings.Split(mimeType, ";")[0])
|
||||
// Build image message
|
||||
du := dataurl.New(*fi.Data, mimeType)
|
||||
url, err := du.MarshalText()
|
||||
if err != nil {
|
||||
b.Log.WithError(err).Infof("Image Serialization into data URL failed (type: %s, length: %d)", mimeType, len(*fi.Data))
|
||||
continue
|
||||
}
|
||||
imsg := config.Message{
|
||||
Text: fmt.Sprintf(`<img src="%s"/>`, url),
|
||||
Channel: msg.Channel,
|
||||
Username: msg.Username,
|
||||
UserID: msg.UserID,
|
||||
Account: msg.Account,
|
||||
Protocol: msg.Protocol,
|
||||
Timestamp: msg.Timestamp,
|
||||
Extra: make(map[string][]interface{}),
|
||||
Event: "mumble_image",
|
||||
}
|
||||
messages = append(messages, imsg)
|
||||
}
|
||||
// Remove files from original message
|
||||
msg.Extra["file"] = nil
|
||||
return messages
|
||||
}
|
||||
@@ -90,7 +90,11 @@ func (b *Bmumble) Send(msg config.Message) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
attachments := b.extractFiles(&msg)
|
||||
b.local <- msg
|
||||
for _, a := range attachments {
|
||||
b.local <- a
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
@@ -213,8 +217,28 @@ func (b *Bmumble) doSend() {
|
||||
func (b *Bmumble) processMessage(msg *config.Message) {
|
||||
b.Log.Debugf("Processing message %s", msg.Text)
|
||||
|
||||
allowHTML := true
|
||||
if b.serverConfig.AllowHTML != nil {
|
||||
allowHTML = *b.serverConfig.AllowHTML
|
||||
}
|
||||
|
||||
// If this is a specially generated image message, send it unmodified
|
||||
if msg.Event == "mumble_image" {
|
||||
if allowHTML {
|
||||
b.Log.Debugf("Sending image message: %s%s", msg.Username, msg.Text)
|
||||
b.client.Self.Channel.Send(msg.Username+msg.Text, false)
|
||||
} else {
|
||||
b.Log.Info("Can't send image, server does not allow HTML messages")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Don't process empty messages
|
||||
if len(msg.Text) == 0 {
|
||||
return
|
||||
}
|
||||
// If HTML is allowed, convert markdown into HTML, otherwise strip markdown
|
||||
if allowHTML := b.serverConfig.AllowHTML; allowHTML == nil || !*allowHTML {
|
||||
if allowHTML {
|
||||
msg.Text = helper.ParseMarkdown(msg.Text)
|
||||
} else {
|
||||
msg.Text = stripmd.Strip(msg.Text)
|
||||
|
||||
Reference in New Issue
Block a user