summaryrefslogtreecommitdiffstats
path: root/bridge/mumble/helpers.go
blob: c828df2ca494a69f222ce3e9a4b9e8403da85ce6 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
package bmumble

import (
	"fmt"
	"mime"
	"net/http"
	"regexp"
	"strings"

	"github.com/42wim/matterbridge/bridge/config"
	"github.com/mattn/godown"
	"github.com/vincent-petithory/dataurl"
)

type MessagePart struct {
	Text          string
	FileExtension string
	Image         []byte
}

func (b *Bmumble) decodeImage(uri string, parts *[]MessagePart) error {
	// Decode the data:image/... URI
	image, err := dataurl.DecodeString(uri)
	if err != nil {
		b.Log.WithError(err).Info("No image extracted")
		return err
	}
	// Determine the file extensions for that image
	ext, err := mime.ExtensionsByType(image.MediaType.ContentType())
	if err != nil || len(ext) == 0 {
		b.Log.WithError(err).Infof("No file extension registered for MIME type '%s'", image.MediaType.ContentType())
		return err
	}
	// Add the image to the MessagePart slice
	*parts = append(*parts, MessagePart{"", ext[0], image.Data})
	return nil
}

func (b *Bmumble) tokenize(t *string) ([]MessagePart, error) {
	// `^(.*?)` matches everything before the image
	// `!\[[^\]]*\]\(` matches the `![alt](` part of markdown images
	// `(data:image\/[^)]+)` 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 := regexp.MustCompile(`^(?ms)(.*?)!\[[^\]]*\]\((data:image\/[^)]+)\)(.*)$`)
	remaining := *t
	var parts []MessagePart
	for {
		tokens := p.FindStringSubmatch(remaining)
		if tokens == nil {
			// no match -> remaining string is non-image text
			pre := strings.TrimSpace(remaining)
			if len(pre) > 0 {
				parts = append(parts, MessagePart{pre, "", nil})
			}
			return parts, nil
		}

		// tokens[1] is the text before the image
		if len(tokens[1]) > 0 {
			pre := strings.TrimSpace(tokens[1])
			parts = append(parts, MessagePart{pre, "", nil})
		}
		// tokens[2] is the image URL
		uri, err := dataurl.UnescapeToString(strings.TrimSpace(strings.ReplaceAll(tokens[2], " ", "")))
		if err != nil {
			b.Log.WithError(err).Info("URL unescaping failed")
			remaining = strings.TrimSpace(tokens[3])
			continue
		}
		err = b.decodeImage(uri, &parts)
		if err != nil {
			b.Log.WithError(err).Info("Decoding the image failed")
		}
		// tokens[3] is the text after the image, processed in the next iteration
		remaining = strings.TrimSpace(tokens[3])
	}
}

func (b *Bmumble) convertHTMLtoMarkdown(html string) ([]MessagePart, error) {
	var sb strings.Builder
	err := godown.Convert(&sb, strings.NewReader(html), nil)
	if err != nil {
		return nil, err
	}
	markdown := sb.String()
	b.Log.Debugf("### to markdown: %s", markdown)
	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
	}
	// Create a separate message for each file
	for _, f := range msg.Extra["file"] {
		fi := f.(config.FileInfo)
		imsg := config.Message{
			Channel:   msg.Channel,
			Username:  msg.Username,
			UserID:    msg.UserID,
			Account:   msg.Account,
			Protocol:  msg.Protocol,
			Timestamp: msg.Timestamp,
			Event:     "mumble_image",
		}
		// If no data is present for the file, send a link instead
		if fi.Data == nil || len(*fi.Data) == 0 {
			if len(fi.URL) > 0 {
				imsg.Text = fmt.Sprintf(`<a href="%s">%s</a>`, fi.URL, fi.URL)
				messages = append(messages, imsg)
			} else {
				b.Log.Infof("Not forwarding file without local data")
			}
			continue
		}
		mimeType := http.DetectContentType(*fi.Data)
		// Mumble only supports images natively, send a link instead
		if !strings.HasPrefix(mimeType, "image/") {
			if len(fi.URL) > 0 {
				imsg.Text = fmt.Sprintf(`<a href="%s">%s</a>`, fi.URL, fi.URL)
				messages = append(messages, imsg)
			} else {
				b.Log.Infof("Not forwarding file of type %s", mimeType)
			}
			continue
		}
		mimeType = strings.TrimSpace(strings.Split(mimeType, ";")[0])
		// Build data:image/...;base64,... style image URL and embed image directly into the message
		du := dataurl.New(*fi.Data, mimeType)
		dataURL, 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.Text = fmt.Sprintf(`<img src="%s"/>`, dataURL)
		messages = append(messages, imsg)
	}
	// Remove files from original message
	msg.Extra["file"] = nil
	return messages
}