nctalk: Images are not embedded in message when send from other messengers
Describe the bug Matterbridge: I'm using the master branch. Nextcloud: 27.1.8
When I post an image to whatsapp its not embedded into the nctalk message. Instead the message holds a link to the matterbridge mediaserver. Same behaviour when send from telegram.
When I post an image in nctalk, then its transfered to whatsapp nicely.
Sending images between telegram and whatsapp works great.
Text messages are bridged perfectly.
UPDATE I tested v1.26.0 (from source) and sending images from telegram to nctalk. It doesn't work.
To Reproduce
Nothings special.
Expected behavior Send embedded images/media from whatsapp/other messengers to nctalk, too.
Screenshots/debug logs
Environment (please complete the following information):
I'm running Matterbridge in Docker build with the served Dockerfile_whatsappmulti
LOG
matterbridge-1 | time="2024-05-05T13:23:01Z" level=debug msg="Receiving message &proto.Message{state:impl.MessageState{NoUnkeyedLiterals:pragma.NoUnkeyedLiterals{}, DoNotCompare:pragma.DoNotCompare{}, DoNotCopy:pragma.DoNotCopy{}, atomicMessageInfo:(*impl.MessageInfo)(0xc0006b67c0)}, sizeCache:0, unknownFields:[]uint8(nil), Conversation:(*string)(nil), SenderKeyDistributionMessage:(*proto.SenderKeyDistributionMessage)(nil), ImageMessage:(*proto.ImageMessage)(0xc00044d500), ContactMessage:(*proto.ContactMessage)(nil), LocationMessage:(*proto.LocationMessage)(nil), ExtendedTextMessage:(*proto.ExtendedTextMessage)(nil), DocumentMessage:(*proto.DocumentMessage)(nil), AudioMessage:(*proto.AudioMessage)(nil), VideoMessage:(*proto.VideoMessage)(nil), Call:(*proto.Call)(nil), Chat:(*proto.Chat)(nil), ProtocolMessage:(*proto.ProtocolMessage)(nil), ContactsArrayMessage:(*proto.ContactsArrayMessage)(nil), HighlyStructuredMessage:(*proto.HighlyStructuredMessage)(nil), FastRatchetKeySenderKeyDistributionMessage:(*proto.SenderKeyDistributionMessage)(nil), SendPaymentMessage:(*proto.SendPaymentMessage)(nil), LiveLocationMessage:(*proto.LiveLocationMessage)(nil), RequestPaymentMessage:(*proto.RequestPaymentMessage)(nil), DeclinePaymentRequestMessage:(*proto.DeclinePaymentRequestMessage)(nil), CancelPaymentRequestMessage:(*proto.CancelPaymentRequestMessage)(nil), TemplateMessage:(*proto.TemplateMessage)(nil), StickerMessage:(*proto.StickerMessage)(nil), GroupInviteMessage:(*proto.GroupInviteMessage)(nil), TemplateButtonReplyMessage:(*proto.TemplateButtonReplyMessage)(nil), ProductMessage:(*proto.ProductMessage)(nil), DeviceSentMessage:(*proto.DeviceSentMessage)(nil), MessageContextInfo:(*proto.MessageContextInfo)(nil), ListMessage:(*proto.ListMessage)(nil), ViewOnceMessage:(*proto.FutureProofMessage)(nil), OrderMessage:(*proto.OrderMessage)(nil), ListResponseMessage:(*proto.ListResponseMessage)(nil), EphemeralMessage:(*proto.FutureProofMessage)(nil), InvoiceMessage:(*proto.InvoiceMessage)(nil), ButtonsMessage:(*proto.ButtonsMessage)(nil), ButtonsResponseMessage:(*proto.ButtonsResponseMessage)(nil), PaymentInviteMessage:(*proto.PaymentInviteMessage)(nil), InteractiveMessage:(*proto.InteractiveMessage)(nil), ReactionMessage:(*proto.ReactionMessage)(nil), StickerSyncRmrMessage:(*proto.StickerSyncRMRMessage)(nil), InteractiveResponseMessage:(*proto.InteractiveResponseMessage)(nil), PollCreationMessage:(*proto.PollCreationMessage)(nil), PollUpdateMessage:(*proto.PollUpdateMessage)(nil), KeepInChatMessage:(*proto.KeepInChatMessage)(nil), DocumentWithCaptionMessage:(*proto.FutureProofMessage)(nil), RequestPhoneNumberMessage:(*proto.RequestPhoneNumberMessage)(nil), ViewOnceMessageV2:(*proto.FutureProofMessage)(nil), EncReactionMessage:(*proto.EncReactionMessage)(nil), EditedMessage:(*proto.FutureProofMessage)(nil), ViewOnceMessageV2Extension:(*proto.FutureProofMessage)(nil), PollCreationMessageV2:(*proto.PollCreationMessage)(nil), ScheduledCallCreationMessage:(*proto.ScheduledCallCreationMessage)(nil), GroupMentionedMessage:(*proto.FutureProofMessage)(nil), PinInChatMessage:(*proto.PinInChatMessage)(nil), PollCreationMessageV3:(*proto.PollCreationMessage)(nil), ScheduledCallEditMessage:(*proto.ScheduledCallEditMessage)(nil), PtvMessage:(*proto.VideoMessage)(nil), BotInvokeMessage:(*proto.FutureProofMessage)(nil), EncCommentMessage:(*proto.EncCommentMessage)(nil)}" func=handleMessage file="bridge/whatsappmulti/handlers.go:107" prefix=whatsapp
matterbridge-1 | time="2024-05-05T13:23:01Z" level=debug msg="Trying to download 3A8D508A4F3211EC3799.jpg with type image/jpeg" func=handleImageMessage file="bridge/whatsappmulti/handlers.go:242" prefix=whatsapp
matterbridge-1 | time="2024-05-05T13:23:01Z" level=debug msg="Download OK "3A8D508A4F3211EC3799.jpg" 176946" func=HandleDownloadData2 file="bridge/helper/helper.go:183" prefix=whatsapp
matterbridge-1 | time="2024-05-05T13:23:01Z" level=debug msg="<= Sending message from [email protected] on whatsapp.XXX to gateway" func=handleImageMessage file="bridge/whatsappmulti/handlers.go:254" prefix=whatsapp
matterbridge-1 | time="2024-05-05T13:23:01Z" level=debug msg="<= Message is config.Message{Text:"", Channel:"XXX", Username:"XXX P", UserID:"[email protected]", Avatar:"xxx", Account:"whatsapp.XXX", Event:"", Protocol:"whatsapp", Gateway:"", ParentID:"", Timestamp:time.Date(1, time.January, 1, 0, 0, 0, 0, time.UTC), ID:"[email protected]/3A8D508A4F3211EC3799", Extra:map[string][]interface {}{"file":[]interface {}{config.FileInfo{Name:"3A8D508A4F3211EC3799.jpg", Data:(*[]uint8)(0xc0000fe000), Comment:"", URL:"", Size:0, Avatar:false, SHA:"", NativeID:""}}}}" func=handleImageMessage file="bridge/whatsappmulti/handlers.go:255" prefix=whatsapp
matterbridge-1 | time="2024-05-05T13:23:01Z" level=debug msg="mediaserver path placing file: /mediaserver/104d0511/3A8D508A4F3211EC3799.jpg" func=handleFilesLocal file="gateway/handlers.go:158" prefix=gateway
matterbridge-1 | time="2024-05-05T13:23:01Z" level=debug msg="mediaserver download URL = XXX/3A8D508A4F3211EC3799.jpg" func=handleFiles file="gateway/handlers.go:112" prefix=gateway
matterbridge-1 | time="2024-05-05T13:23:01Z" level=debug msg="=> Sending config.Message{Text:"", Channel:"1xxx", Username:"XXX P", UserID:"[email protected]", Avatar:"xxx", Account:"whatsapp.XXX", Event:"", Protocol:"whatsapp", Gateway:"matterbottest", ParentID:"", Timestamp:time.Date(2024, time.May, 5, 13, 23, 1, 899666261, time.Local), ID:"[email protected]/3A8D508A4F3211EC3799", Extra:map[string][]interface {}{"file":[]interface {}{config.FileInfo{Name:"3A8D508A4F3211EC3799.jpg", Data:(*[]uint8)(0xc0000fe000), Comment:"", URL:"XXX/3A8D508A4F3211EC3799.jpg", Size:0, Avatar:false, SHA:"104d0511", NativeID:""}}}} from whatsapp.matterbot (xxx) to telegram.XXX (XXX)" func=SendMessage file="gateway/gateway.go:499" prefix=gateway
matterbridge-1 | time="2024-05-05T13:23:01Z" level=debug msg="=> Receiving config.Message{Text:"", Channel:"XXX", Username:"[whatsapp] <XXX P> ", UserID:"[email protected]", Avatar:"xxx", Account:"whatsapp.XXX", Event:"", Protocol:"whatsapp", Gateway:"matterbottest", ParentID:"", Timestamp:time.Date(2024, time.May, 5, 13, 23, 1, 899666261, time.Local), ID:"", Extra:map[string][]interface {}{"file":[]interface {}{config.FileInfo{Name:"3A8D508A4F3211EC3799.jpg", Data:(*[]uint8)(0xc0000fe000), Comment:"", URL:"XXX/3A8D508A4F3211EC3799.jpg", Size:0, Avatar:false, SHA:"104d0511", NativeID:""}}}}" func=Send file="bridge/telegram/telegram.go:121" prefix=telegram
matterbridge-1 | time="2024-05-05T13:23:01Z" level=debug msg="mID telegram.XXX: 44" func=SendMessage file="gateway/gateway.go:518" prefix=gateway
matterbridge-1 | time="2024-05-05T13:23:01Z" level=debug msg="=> Send from whatsapp.matterbot (XXX) to telegram.XXX (-XXX) took 71.451016ms" func=func1 file="gateway/gateway.go:508" prefix=gateway
matterbridge-1 | time="2024-05-05T13:23:01Z" level=debug msg="=> Sending config.Message{Text:"", Channel:"XXX", Username:"XXX P", UserID:"[email protected]", Avatar:"xxx", Account:"whatsapp.matterbot", Event:"", Protocol:"whatsapp", Gateway:"matterbottest", ParentID:"", Timestamp:time.Date(2024, time.May, 5, 13, 23, 1, 899666261, time.Local), ID:"[email protected]/3A8D508A4F3211EC3799", Extra:map[string][]interface {}{"file":[]interface {}{config.FileInfo{Name:"3A8D508A4F3211EC3799.jpg", Data:(*[]uint8)(0xc0000fe000), Comment:"", URL:"XXX/3A8D508A4F3211EC3799.jpg", Size:0, Avatar:false, SHA:"104d0511", NativeID:""}}}} from whatsapp.XXX (XXX) to nctalk.0 (xzhmhowg)" func=SendMessage file="gateway/gateway.go:499" prefix=gateway
matterbridge-1 | time="2024-05-05T13:23:05Z" level=error msg="Could not send message to room xzhmhowg from [whatsapp] <XXX P> : unexpected return code" func=Send file="bridge/nctalk/nctalk.go:128" prefix=nctalk
matterbridge-1 | time="2024-05-05T13:23:05Z" level=debug msg="=> Send from whatsapp.matterbot (XXX) to nctalk.0 (xzhmhowg) took 3.112660696s" func=func1 file="gateway/gateway.go:508" prefix=gateway
Additional context
[general]
MediaDownloadPath="xxx"
MediaServerDownload="xxx"
[telegram]
[telegram.xxx]
Token = "xxx"
PrefixMessagesWithNick = true
RemoteNickFormat = "[{PROTOCOL}] <{NICK}> "
UseFullName=true
[nctalk]
[nctalk.0]
SeparateDisplayName = true
Server = "xxx"
Login = "bridge-bot"
Password = "xxx"
PrefixMessagesWithNick = true
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
[whatsapp]
[whatsapp.xxx]
Number="xxx"
RemoteNickFormat="[{PROTOCOL}] @{NICK}: "
SessionFile="/data/xxx"
[[gateway]]
name = "matterbottest"
enable = true
[[gateway.inout]]
account = "telegram.xxx"
channel = "xxx"
[[gateway.inout]]
account = "nctalk.0"
channel = "xxxx"
[[gateway.inout]]
account = "whatsapp.xxx"
channel = "xxx"
We've been having this same issue, though with Discord and Matrix instead. Figured out that images below 1MB show fine, but anything above just spits out an invalid link.
Hi everyone,
I’ve added basic file upload support for the nctalk.go bridge. Please excuse any rough edges in the code – I used AI assistance to help me with the implementation. 🙂
To test it:
- Clone the repo
- Copy and paste the modified
nctalk.gointobridge/nctalk/(I can't upload the .go-file directly) - Rebuild the Docker image with:
docker build -t matterbridge .
- Run it with your config
I’ve tested it successfully with Telegram ↔︎ Nctalk, and file transfers are working for me.
Show modified nctalk.go
package nctalk
import (
"context"
"crypto/tls"
"encoding/json"
"fmt"
"strconv"
"strings"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/monaco-io/request"
"gomod.garykim.dev/nc-talk/ocs"
"gomod.garykim.dev/nc-talk/room"
"gomod.garykim.dev/nc-talk/user"
)
type Btalk struct {
user *user.TalkUser
rooms []Broom
*bridge.Config
}
func New(cfg *bridge.Config) bridge.Bridger {
return &Btalk{Config: cfg}
}
type Broom struct {
room *room.TalkRoom
ctx context.Context
ctxCancel context.CancelFunc
}
func (b *Btalk) Connect() error {
b.Log.Info("Connecting")
tconfig := &user.TalkUserConfig{
TLSConfig: &tls.Config{
InsecureSkipVerify: b.GetBool("SkipTLSVerify"), //nolint:gosec
},
}
var err error
b.user, err = user.NewUser(b.GetString("Server"), b.GetString("Login"), b.GetString("Password"), tconfig)
if err != nil {
b.Log.Error("Config could not be used")
return err
}
_, err = b.user.Capabilities()
if err != nil {
b.Log.Error("Cannot Connect")
return err
}
b.Log.Info("Connected")
return nil
}
func (b *Btalk) Disconnect() error {
for _, r := range b.rooms {
r.ctxCancel()
}
return nil
}
func (b *Btalk) JoinChannel(channel config.ChannelInfo) error {
tr, err := room.NewTalkRoom(b.user, channel.Name)
if err != nil {
return err
}
newRoom := Broom{
room: tr,
}
newRoom.ctx, newRoom.ctxCancel = context.WithCancel(context.Background())
c, err := newRoom.room.ReceiveMessages(newRoom.ctx)
if err != nil {
return err
}
b.rooms = append(b.rooms, newRoom)
go func() {
for msg := range c {
msg := msg
if msg.Error != nil {
b.Log.Errorf("Fatal message poll error: %s\n", msg.Error)
return
}
// Ignore messages that are from the bot user
if msg.ActorID == b.user.User || msg.ActorType == "bridged" {
continue
}
// Handle deleting messages
if msg.MessageType == ocs.MessageSystem && msg.Parent != nil && msg.Parent.MessageType == ocs.MessageDelete {
b.handleDeletingMessage(&msg, &newRoom)
continue
}
// Handle sending messages
if msg.MessageType == ocs.MessageComment {
b.handleSendingMessage(&msg, &newRoom)
continue
}
}
}()
return nil
}
func (b *Btalk) Send(msg config.Message) (string, error) {
r := b.getRoom(msg.Channel)
if r == nil {
b.Log.Errorf("Could not find room for %v", msg.Channel)
return "", nil
}
// Standard Message Send
if msg.Event == "" {
// Handle sending files if they are included
err := b.handleSendingFile(&msg, r)
if err != nil {
b.Log.Errorf("Could not send files in message to room %v from %v: %v", msg.Channel, msg.Username, err)
return "", nil
}
sentMessage, err := b.sendText(r, &msg, msg.Text)
if err != nil {
b.Log.Errorf("Could not send message to room %v from %v: %v", msg.Channel, msg.Username, err)
return "", nil
}
return strconv.Itoa(sentMessage.ID), nil
}
// Message Deletion
if msg.Event == config.EventMsgDelete {
messageID, err := strconv.Atoi(msg.ID)
if err != nil {
return "", err
}
data, err := r.room.DeleteMessage(messageID)
if err != nil {
return "", err
}
return strconv.Itoa(data.ID), nil
}
// Message is not a type that is currently supported
return "", nil
}
func (b *Btalk) getRoom(token string) *Broom {
for _, r := range b.rooms {
if r.room.Token == token {
return &r
}
}
return nil
}
func (b *Btalk) sendText(r *Broom, msg *config.Message, text string) (*ocs.TalkRoomMessageData, error) {
messageToSend := &room.Message{Message: msg.Username + text}
if b.GetBool("SeparateDisplayName") {
messageToSend.Message = text
messageToSend.ActorDisplayName = msg.Username
}
return r.room.SendComplexMessage(messageToSend)
}
func (b *Btalk) handleFiles(mmsg *config.Message, message *ocs.TalkRoomMessageData) error {
for _, parameter := range message.MessageParameters {
if parameter.Type == ocs.ROSTypeFile {
// Get the file
file, err := b.user.DownloadFile(parameter.Path)
if err != nil {
return err
}
if mmsg.Extra == nil {
mmsg.Extra = make(map[string][]interface{})
}
mmsg.Extra["file"] = append(mmsg.Extra["file"], config.FileInfo{
Name: parameter.Name,
Data: file,
Size: int64(len(*file)),
Avatar: false,
})
}
}
return nil
}
func (b *Btalk) handleSendingFile(msg *config.Message, r *Broom) error {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
// Try to upload file first
var filePath string
var err error
if fi.Data != nil {
filePath, err = b.uploadFileToNextcloud(&fi)
if err != nil {
b.Log.Errorf("Failed to upload file %s: %v", fi.Name, err)
}
}
// Create message with file information
messageText := ""
if fi.Comment != "" {
messageText = fi.Comment + " "
}
// If upload was successful, share file to chat
if filePath != "" {
err = b.shareFileToChat(r, messageText, fi.Name, filePath, msg.Username)
if err != nil {
b.Log.Errorf("Failed to share file to chat: %v", err)
// Fallback to simple message with file link
fileLink := b.user.NextcloudURL + "/remote.php/dav/files/" + b.user.User + filePath
messageText += "[" + fi.Name + "](" + fileLink + ")"
messageToSend := &room.Message{
Message: messageText,
}
if b.GetBool("SeparateDisplayName") {
messageToSend.Message = messageText
messageToSend.ActorDisplayName = msg.Username
}
_, err = r.room.SendComplexMessage(messageToSend)
if err != nil {
return err
}
}
} else if fi.URL != "" {
// If we have a URL (from media server), use it with markdown link
messageText += "[" + fi.Name + "](" + fi.URL + ")"
messageToSend := &room.Message{
Message: messageText,
}
if b.GetBool("SeparateDisplayName") {
messageToSend.Message = messageText
messageToSend.ActorDisplayName = msg.Username
}
_, err = r.room.SendComplexMessage(messageToSend)
if err != nil {
return err
}
} else {
// No data or URL, just send filename
messageText += fi.Name
messageToSend := &room.Message{
Message: messageText,
}
if b.GetBool("SeparateDisplayName") {
messageToSend.Message = messageText
messageToSend.ActorDisplayName = msg.Username
}
_, err = r.room.SendComplexMessage(messageToSend)
if err != nil {
return err
}
}
}
return nil
}
// shareFileToChat shares a file to the chat using the Nextcloud Files Sharing API
func (b *Btalk) shareFileToChat(r *Broom, messageText, fileName, filePath, username string) error {
url := b.user.NextcloudURL + "/ocs/v2.php/apps/files_sharing/api/v1/shares"
// Prepare request data for file sharing
requestData := map[string]interface{}{
"shareWith": r.room.Token,
"shareType": 10, // Talk room share type
"path": filePath,
}
// Convert to JSON
jsonData, err := json.Marshal(requestData)
if err != nil {
return err
}
b.Log.Debugf("Sharing file to chat: %s", string(jsonData))
// Send request
client := b.user.RequestClient(request.Client{
URL: url,
Method: "POST",
Body: jsonData,
Header: map[string]string{
"Content-Type": "application/json",
},
})
res, err := client.Do()
if err != nil {
return err
}
b.Log.Debugf("Share file response status: %d, body: %s", res.StatusCode(), string(res.Data))
if res.StatusCode() != 200 {
return fmt.Errorf("failed to share file to chat, status: %d", res.StatusCode())
}
// Send a message to the chat about the shared file
messageText += fileName
messageToSend := &room.Message{
Message: messageText,
}
if b.GetBool("SeparateDisplayName") {
messageToSend.Message = messageText
messageToSend.ActorDisplayName = username
}
_, err = r.room.SendComplexMessage(messageToSend)
if err != nil {
return err
}
return nil
}
// uploadFileToNextcloud uploads a file to Nextcloud via WebDAV
func (b *Btalk) uploadFileToNextcloud(fi *config.FileInfo) (string, error) {
// Generate a unique filename to avoid conflicts
filename := fi.Name
if filename == "" {
filename = "uploaded_file"
}
b.Log.Debugf("Starting upload for file: %s, size: %d bytes", filename, len(*fi.Data))
// Use the attachments folder from capabilities if available
uploadPath := "/"
capabilities, err := b.user.Capabilities()
if err == nil && capabilities.AttachmentsFolder != "" {
uploadPath = capabilities.AttachmentsFolder
b.Log.Debugf("Using attachments folder: %s", uploadPath)
} else {
b.Log.Debugf("No attachments folder configured, using root: %s", uploadPath)
}
// Ensure the path ends with /
if !strings.HasSuffix(uploadPath, "/") {
uploadPath += "/"
}
// Create the full file path
filePath := uploadPath + filename
// Construct the WebDAV URL using the same pattern as DownloadFile
webdavURL := b.user.NextcloudURL + "/remote.php/dav/files/" + b.user.User + filePath
b.Log.Debugf("WebDAV URL: %s", webdavURL)
// Create the HTTP request using the same client pattern as DownloadFile
client := b.user.RequestClient(request.Client{
URL: webdavURL,
Method: "PUT",
Body: *fi.Data,
Header: map[string]string{
"Content-Type": "application/octet-stream",
},
})
// Execute the request
res, err := client.Do()
if err != nil {
b.Log.Errorf("Upload request failed: %v", err)
return "", err
}
b.Log.Debugf("Upload response status: %d", res.StatusCode())
// Check if upload was successful
if res.StatusCode() != 201 && res.StatusCode() != 204 {
b.Log.Errorf("Upload failed with status code: %d, response: %s", res.StatusCode(), string(res.Data))
return "", fmt.Errorf("upload failed with status code: %d", res.StatusCode())
}
b.Log.Debugf("Upload successful, file path: %s", filePath)
return filePath, nil
}
func (b *Btalk) handleSendingMessage(msg *ocs.TalkRoomMessageData, r *Broom) {
remoteMessage := config.Message{
Text: formatRichObjectString(msg.Message, msg.MessageParameters),
Channel: r.room.Token,
Username: DisplayName(msg, b.guestSuffix()),
UserID: msg.ActorID,
Account: b.Account,
}
// It is possible for the ID to not be set on older versions of Talk so we only set it if
// the ID is not blank
if msg.ID != 0 {
remoteMessage.ID = strconv.Itoa(msg.ID)
}
// Handle Files
err := b.handleFiles(&remoteMessage, msg)
if err != nil {
b.Log.Errorf("Error handling file: %#v", msg)
return
}
b.Log.Debugf("<= Message is %#v", remoteMessage)
b.Remote <- remoteMessage
}
func (b *Btalk) handleDeletingMessage(msg *ocs.TalkRoomMessageData, r *Broom) {
remoteMessage := config.Message{
Event: config.EventMsgDelete,
Text: config.EventMsgDelete,
Channel: r.room.Token,
ID: strconv.Itoa(msg.Parent.ID),
Account: b.Account,
}
b.Log.Debugf("<= Message being deleted is %#v", remoteMessage)
b.Remote <- remoteMessage
}
func (b *Btalk) guestSuffix() string {
guestSuffix := " (Guest)"
if b.IsKeySet("GuestSuffix") {
guestSuffix = b.GetString("GuestSuffix")
}
return guestSuffix
}
// Spec: https://github.com/nextcloud/server/issues/1706#issue-182308785
func formatRichObjectString(message string, parameters map[string]ocs.RichObjectString) string {
for id, parameter := range parameters {
text := parameter.Name
switch parameter.Type {
case ocs.ROSTypeUser, ocs.ROSTypeGroup:
text = "@" + text
case ocs.ROSTypeFile:
if parameter.Link != "" {
text = parameter.Name
}
}
message = strings.ReplaceAll(message, "{"+id+"}", text)
}
return message
}
func DisplayName(msg *ocs.TalkRoomMessageData, suffix string) string {
if msg.ActorType == ocs.ActorGuest {
if msg.ActorDisplayName == "" {
return "Guest"
}
return msg.ActorDisplayName + suffix
}
return msg.ActorDisplayName
}