| 1 | package bmumble |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "mime" |
| 6 | "net/http" |
| 7 | "regexp" |
| 8 | "strings" |
| 9 | |
| 10 | "github.com/matterbridge-org/matterbridge/bridge/config" |
| 11 | "github.com/mattn/godown" |
| 12 | "github.com/vincent-petithory/dataurl" |
| 13 | ) |
| 14 | |
| 15 | type MessagePart struct { |
| 16 | Text string |
| 17 | FileExtension string |
| 18 | Image []byte |
| 19 | } |
| 20 | |
| 21 | func (b *Bmumble) decodeImage(uri string, parts *[]MessagePart) error { |
| 22 | // Decode the data:image/... URI |
| 23 | image, err := dataurl.DecodeString(uri) |
| 24 | if err != nil { |
| 25 | b.Log.WithError(err).Info("No image extracted") |
| 26 | return err |
| 27 | } |
| 28 | // Determine the file extensions for that image |
| 29 | ext, err := mime.ExtensionsByType(image.MediaType.ContentType()) |
| 30 | if err != nil || len(ext) == 0 { |
| 31 | b.Log.WithError(err).Infof("No file extension registered for MIME type '%s'", image.MediaType.ContentType()) |
| 32 | return err |
| 33 | } |
| 34 | // Add the image to the MessagePart slice |
| 35 | *parts = append(*parts, MessagePart{"", ext[0], image.Data}) |
| 36 | return nil |
| 37 | } |
| 38 | |
| 39 | func (b *Bmumble) tokenize(t *string) ([]MessagePart, error) { |
| 40 | // `^(.*?)` matches everything before the image |
| 41 | // `!\[[^\]]*\]\(` matches the `]+)` matches the data: URI used by Mumble |
| 43 | // `\)` matches the closing parenthesis after the URI |
| 44 | // `(.*)$` matches the remaining text to be examined in the next iteration |
| 45 | p := regexp.MustCompile(`^(?ms)(.*?)!\[[^\]]*\]\((data:image\/[^)]+)\)(.*)$`) |
| 46 | remaining := *t |
| 47 | var parts []MessagePart |
| 48 | for { |
| 49 | tokens := p.FindStringSubmatch(remaining) |
| 50 | if tokens == nil { |
| 51 | // no match -> remaining string is non-image text |
| 52 | pre := strings.TrimSpace(remaining) |
| 53 | if len(pre) > 0 { |
| 54 | parts = append(parts, MessagePart{pre, "", nil}) |
| 55 | } |
| 56 | return parts, nil |
| 57 | } |
| 58 | |
| 59 | // tokens[1] is the text before the image |
| 60 | if len(tokens[1]) > 0 { |
| 61 | pre := strings.TrimSpace(tokens[1]) |
| 62 | parts = append(parts, MessagePart{pre, "", nil}) |
| 63 | } |
| 64 | // tokens[2] is the image URL |
| 65 | uri, err := dataurl.UnescapeToString(strings.TrimSpace(strings.ReplaceAll(tokens[2], " ", ""))) |
| 66 | if err != nil { |
| 67 | b.Log.WithError(err).Info("URL unescaping failed") |
| 68 | remaining = strings.TrimSpace(tokens[3]) |
| 69 | continue |
| 70 | } |
| 71 | err = b.decodeImage(uri, &parts) |
| 72 | if err != nil { |
| 73 | b.Log.WithError(err).Info("Decoding the image failed") |
| 74 | } |
| 75 | // tokens[3] is the text after the image, processed in the next iteration |
| 76 | remaining = strings.TrimSpace(tokens[3]) |
| 77 | } |
| 78 | } |
| 79 | |
| 80 | func (b *Bmumble) convertHTMLtoMarkdown(html string) ([]MessagePart, error) { |
| 81 | var sb strings.Builder |
| 82 | err := godown.Convert(&sb, strings.NewReader(html), nil) |
| 83 | if err != nil { |
| 84 | return nil, err |
| 85 | } |
| 86 | markdown := sb.String() |
| 87 | b.Log.Debugf("### to markdown: %s", markdown) |
| 88 | return b.tokenize(&markdown) |
| 89 | } |
| 90 | |
| 91 | func (b *Bmumble) extractFiles(msg *config.Message) []config.Message { |
| 92 | var messages []config.Message |
| 93 | if msg.Extra == nil || len(msg.Extra["file"]) == 0 { |
| 94 | return messages |
| 95 | } |
| 96 | // Create a separate message for each file |
| 97 | for _, f := range msg.Extra["file"] { |
| 98 | fi := f.(config.FileInfo) |
| 99 | imsg := config.Message{ |
| 100 | Channel: msg.Channel, |
| 101 | Username: msg.Username, |
| 102 | UserID: msg.UserID, |
| 103 | Account: msg.Account, |
| 104 | Protocol: msg.Protocol, |
| 105 | Timestamp: msg.Timestamp, |
| 106 | Event: "mumble_image", |
| 107 | } |
| 108 | // If no data is present for the file, send a link instead |
| 109 | if fi.Data == nil || len(*fi.Data) == 0 { |
| 110 | if len(fi.URL) > 0 { |
| 111 | imsg.Text = fmt.Sprintf(`<a href="%s">%s</a>`, fi.URL, fi.URL) |
| 112 | messages = append(messages, imsg) |
| 113 | } else { |
| 114 | b.Log.Infof("Not forwarding file without local data") |
| 115 | } |
| 116 | continue |
| 117 | } |
| 118 | mimeType := http.DetectContentType(*fi.Data) |
| 119 | // Mumble only supports images natively, send a link instead |
| 120 | if !strings.HasPrefix(mimeType, "image/") { |
| 121 | if len(fi.URL) > 0 { |
| 122 | imsg.Text = fmt.Sprintf(`<a href="%s">%s</a>`, fi.URL, fi.URL) |
| 123 | messages = append(messages, imsg) |
| 124 | } else { |
| 125 | b.Log.Infof("Not forwarding file of type %s", mimeType) |
| 126 | } |
| 127 | continue |
| 128 | } |
| 129 | mimeType = strings.TrimSpace(strings.Split(mimeType, ";")[0]) |
| 130 | // Build data:image/...;base64,... style image URL and embed image directly into the message |
| 131 | du := dataurl.New(*fi.Data, mimeType) |
| 132 | dataURL, err := du.MarshalText() |
| 133 | if err != nil { |
| 134 | b.Log.WithError(err).Infof("Image Serialization into data URL failed (type: %s, length: %d)", mimeType, len(*fi.Data)) |
| 135 | continue |
| 136 | } |
| 137 | imsg.Text = fmt.Sprintf(`<img src="%s"/>`, dataURL) |
| 138 | messages = append(messages, imsg) |
| 139 | } |
| 140 | // Remove files from original message |
| 141 | msg.Extra["file"] = nil |
| 142 | return messages |
| 143 | } |
| 144 | |