| 1 | package nctalk |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "crypto/tls" |
| 6 | "strconv" |
| 7 | "strings" |
| 8 | |
| 9 | "github.com/matterbridge-org/matterbridge/bridge" |
| 10 | "github.com/matterbridge-org/matterbridge/bridge/config" |
| 11 | |
| 12 | "gomod.garykim.dev/nc-talk/ocs" |
| 13 | "gomod.garykim.dev/nc-talk/room" |
| 14 | "gomod.garykim.dev/nc-talk/user" |
| 15 | ) |
| 16 | |
| 17 | type Btalk struct { |
| 18 | user *user.TalkUser |
| 19 | rooms []Broom |
| 20 | *bridge.Config |
| 21 | } |
| 22 | |
| 23 | func New(cfg *bridge.Config) bridge.Bridger { |
| 24 | return &Btalk{Config: cfg} |
| 25 | } |
| 26 | |
| 27 | type Broom struct { |
| 28 | room *room.TalkRoom |
| 29 | ctx context.Context |
| 30 | ctxCancel context.CancelFunc |
| 31 | } |
| 32 | |
| 33 | func (b *Btalk) Connect() error { |
| 34 | b.Log.Info("Connecting") |
| 35 | tconfig := &user.TalkUserConfig{ |
| 36 | TLSConfig: &tls.Config{ |
| 37 | InsecureSkipVerify: b.GetBool("SkipTLSVerify"), //nolint:gosec |
| 38 | }, |
| 39 | } |
| 40 | var err error |
| 41 | b.user, err = user.NewUser(b.GetString("Server"), b.GetString("Login"), b.GetString("Password"), tconfig) |
| 42 | if err != nil { |
| 43 | b.Log.Error("Config could not be used") |
| 44 | return err |
| 45 | } |
| 46 | _, err = b.user.Capabilities() |
| 47 | if err != nil { |
| 48 | b.Log.Error("Cannot Connect") |
| 49 | return err |
| 50 | } |
| 51 | b.Log.Info("Connected") |
| 52 | return nil |
| 53 | } |
| 54 | |
| 55 | func (b *Btalk) Disconnect() error { |
| 56 | for _, r := range b.rooms { |
| 57 | r.ctxCancel() |
| 58 | } |
| 59 | return nil |
| 60 | } |
| 61 | |
| 62 | func (b *Btalk) JoinChannel(channel config.ChannelInfo) error { |
| 63 | tr, err := room.NewTalkRoom(b.user, channel.Name) |
| 64 | if err != nil { |
| 65 | return err |
| 66 | } |
| 67 | newRoom := Broom{ |
| 68 | room: tr, |
| 69 | } |
| 70 | newRoom.ctx, newRoom.ctxCancel = context.WithCancel(context.Background()) |
| 71 | c, err := newRoom.room.ReceiveMessages(newRoom.ctx) |
| 72 | if err != nil { |
| 73 | return err |
| 74 | } |
| 75 | b.rooms = append(b.rooms, newRoom) |
| 76 | |
| 77 | go func() { |
| 78 | for msg := range c { |
| 79 | msg := msg |
| 80 | |
| 81 | if msg.Error != nil { |
| 82 | b.Log.Errorf("Fatal message poll error: %s\n", msg.Error) |
| 83 | |
| 84 | return |
| 85 | } |
| 86 | |
| 87 | // Ignore messages that are from the bot user |
| 88 | if msg.ActorID == b.user.User || msg.ActorType == "bridged" { |
| 89 | continue |
| 90 | } |
| 91 | |
| 92 | // Handle deleting messages |
| 93 | if msg.MessageType == ocs.MessageSystem && msg.Parent != nil && msg.Parent.MessageType == ocs.MessageDelete { |
| 94 | b.handleDeletingMessage(&msg, &newRoom) |
| 95 | continue |
| 96 | } |
| 97 | |
| 98 | // Handle sending messages |
| 99 | if msg.MessageType == ocs.MessageComment { |
| 100 | b.handleSendingMessage(&msg, &newRoom) |
| 101 | continue |
| 102 | } |
| 103 | |
| 104 | } |
| 105 | }() |
| 106 | return nil |
| 107 | } |
| 108 | |
| 109 | func (b *Btalk) Send(msg config.Message) (string, error) { |
| 110 | r := b.getRoom(msg.Channel) |
| 111 | if r == nil { |
| 112 | b.Log.Errorf("Could not find room for %v", msg.Channel) |
| 113 | return "", nil |
| 114 | } |
| 115 | |
| 116 | // Standard Message Send |
| 117 | if msg.Event == "" { |
| 118 | // Handle sending files if they are included |
| 119 | err := b.handleSendingFile(&msg, r) |
| 120 | if err != nil { |
| 121 | b.Log.Errorf("Could not send files in message to room %v from %v: %v", msg.Channel, msg.Username, err) |
| 122 | |
| 123 | return "", nil |
| 124 | } |
| 125 | |
| 126 | sentMessage, err := b.sendText(r, &msg, msg.Text) |
| 127 | if err != nil { |
| 128 | b.Log.Errorf("Could not send message to room %v from %v: %v", msg.Channel, msg.Username, err) |
| 129 | |
| 130 | return "", nil |
| 131 | } |
| 132 | return strconv.Itoa(sentMessage.ID), nil |
| 133 | } |
| 134 | |
| 135 | // Message Deletion |
| 136 | if msg.Event == config.EventMsgDelete { |
| 137 | messageID, err := strconv.Atoi(msg.ID) |
| 138 | if err != nil { |
| 139 | return "", err |
| 140 | } |
| 141 | data, err := r.room.DeleteMessage(messageID) |
| 142 | if err != nil { |
| 143 | return "", err |
| 144 | } |
| 145 | return strconv.Itoa(data.ID), nil |
| 146 | } |
| 147 | |
| 148 | // Message is not a type that is currently supported |
| 149 | return "", nil |
| 150 | } |
| 151 | |
| 152 | func (b *Btalk) getRoom(token string) *Broom { |
| 153 | for _, r := range b.rooms { |
| 154 | if r.room.Token == token { |
| 155 | return &r |
| 156 | } |
| 157 | } |
| 158 | return nil |
| 159 | } |
| 160 | |
| 161 | func (b *Btalk) sendText(r *Broom, msg *config.Message, text string) (*ocs.TalkRoomMessageData, error) { |
| 162 | messageToSend := &room.Message{Message: msg.Username + text} |
| 163 | |
| 164 | if b.GetBool("SeparateDisplayName") { |
| 165 | messageToSend.Message = text |
| 166 | messageToSend.ActorDisplayName = msg.Username |
| 167 | } |
| 168 | |
| 169 | return r.room.SendComplexMessage(messageToSend) |
| 170 | } |
| 171 | |
| 172 | func (b *Btalk) handleFiles(mmsg *config.Message, message *ocs.TalkRoomMessageData) error { |
| 173 | for _, parameter := range message.MessageParameters { |
| 174 | if parameter.Type == ocs.ROSTypeFile { |
| 175 | // Get the file |
| 176 | file, err := b.user.DownloadFile(parameter.Path) |
| 177 | if err != nil { |
| 178 | return err |
| 179 | } |
| 180 | |
| 181 | if mmsg.Extra == nil { |
| 182 | mmsg.Extra = make(map[string][]interface{}) |
| 183 | } |
| 184 | |
| 185 | mmsg.Extra["file"] = append(mmsg.Extra["file"], config.FileInfo{ |
| 186 | Name: parameter.Name, |
| 187 | Data: file, |
| 188 | Size: int64(len(*file)), |
| 189 | Avatar: false, |
| 190 | }) |
| 191 | } |
| 192 | } |
| 193 | |
| 194 | return nil |
| 195 | } |
| 196 | |
| 197 | func (b *Btalk) handleSendingFile(msg *config.Message, r *Broom) error { |
| 198 | for _, f := range msg.Extra["file"] { |
| 199 | fi := f.(config.FileInfo) |
| 200 | if fi.URL == "" { |
| 201 | continue |
| 202 | } |
| 203 | |
| 204 | message := "" |
| 205 | if fi.Comment != "" { |
| 206 | message += fi.Comment + " " |
| 207 | } |
| 208 | message += fi.URL |
| 209 | _, err := b.sendText(r, msg, message) |
| 210 | if err != nil { |
| 211 | return err |
| 212 | } |
| 213 | } |
| 214 | |
| 215 | return nil |
| 216 | } |
| 217 | |
| 218 | func (b *Btalk) handleSendingMessage(msg *ocs.TalkRoomMessageData, r *Broom) { |
| 219 | remoteMessage := config.Message{ |
| 220 | Text: formatRichObjectString(msg.Message, msg.MessageParameters), |
| 221 | Channel: r.room.Token, |
| 222 | Username: DisplayName(msg, b.guestSuffix()), |
| 223 | UserID: msg.ActorID, |
| 224 | Account: b.Account, |
| 225 | } |
| 226 | // It is possible for the ID to not be set on older versions of Talk so we only set it if |
| 227 | // the ID is not blank |
| 228 | if msg.ID != 0 { |
| 229 | remoteMessage.ID = strconv.Itoa(msg.ID) |
| 230 | } |
| 231 | |
| 232 | // Handle Files |
| 233 | err := b.handleFiles(&remoteMessage, msg) |
| 234 | if err != nil { |
| 235 | b.Log.Errorf("Error handling file: %#v", msg) |
| 236 | |
| 237 | return |
| 238 | } |
| 239 | |
| 240 | b.Log.Debugf("<= Message is %#v", remoteMessage) |
| 241 | b.Remote <- remoteMessage |
| 242 | } |
| 243 | |
| 244 | func (b *Btalk) handleDeletingMessage(msg *ocs.TalkRoomMessageData, r *Broom) { |
| 245 | remoteMessage := config.Message{ |
| 246 | Event: config.EventMsgDelete, |
| 247 | Text: config.EventMsgDelete, |
| 248 | Channel: r.room.Token, |
| 249 | ID: strconv.Itoa(msg.Parent.ID), |
| 250 | Account: b.Account, |
| 251 | } |
| 252 | b.Log.Debugf("<= Message being deleted is %#v", remoteMessage) |
| 253 | b.Remote <- remoteMessage |
| 254 | } |
| 255 | |
| 256 | func (b *Btalk) guestSuffix() string { |
| 257 | guestSuffix := " (Guest)" |
| 258 | if b.IsKeySet("GuestSuffix") { |
| 259 | guestSuffix = b.GetString("GuestSuffix") |
| 260 | } |
| 261 | |
| 262 | return guestSuffix |
| 263 | } |
| 264 | |
| 265 | // Spec: https://github.com/nextcloud/server/issues/1706#issue-182308785 |
| 266 | func formatRichObjectString(message string, parameters map[string]ocs.RichObjectString) string { |
| 267 | for id, parameter := range parameters { |
| 268 | text := parameter.Name |
| 269 | |
| 270 | switch parameter.Type { |
| 271 | case ocs.ROSTypeUser, ocs.ROSTypeGroup: |
| 272 | text = "@" + text |
| 273 | case ocs.ROSTypeFile: |
| 274 | if parameter.Link != "" { |
| 275 | text = parameter.Name |
| 276 | } |
| 277 | } |
| 278 | |
| 279 | message = strings.ReplaceAll(message, "{"+id+"}", text) |
| 280 | } |
| 281 | |
| 282 | return message |
| 283 | } |
| 284 | |
| 285 | func DisplayName(msg *ocs.TalkRoomMessageData, suffix string) string { |
| 286 | if msg.ActorType == ocs.ActorGuest { |
| 287 | if msg.ActorDisplayName == "" { |
| 288 | return "Guest" |
| 289 | } |
| 290 | |
| 291 | return msg.ActorDisplayName + suffix |
| 292 | } |
| 293 | |
| 294 | return msg.ActorDisplayName |
| 295 | } |
| 296 | |