| 1 | package gateway |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "os" |
| 6 | "regexp" |
| 7 | "strings" |
| 8 | "time" |
| 9 | |
| 10 | "github.com/d5/tengo/v2" |
| 11 | "github.com/d5/tengo/v2/stdlib" |
| 12 | lru "github.com/hashicorp/golang-lru" |
| 13 | // "github.com/kyokomi/emoji/v2" |
| 14 | "github.com/matterbridge-org/matterbridge/bridge" |
| 15 | "github.com/matterbridge-org/matterbridge/bridge/config" |
| 16 | "github.com/matterbridge-org/matterbridge/internal" |
| 17 | "github.com/sirupsen/logrus" |
| 18 | ) |
| 19 | |
| 20 | type Gateway struct { |
| 21 | config.Config |
| 22 | |
| 23 | Router *Router |
| 24 | MyConfig *config.Gateway |
| 25 | Bridges map[string]*bridge.Bridge |
| 26 | Channels map[string]*config.ChannelInfo |
| 27 | ChannelOptions map[string]config.ChannelOptions |
| 28 | Message chan config.Message |
| 29 | Name string |
| 30 | Messages *lru.Cache |
| 31 | |
| 32 | logger *logrus.Entry |
| 33 | } |
| 34 | |
| 35 | type BrMsgID struct { |
| 36 | br *bridge.Bridge |
| 37 | ID string |
| 38 | ChannelID string |
| 39 | } |
| 40 | |
| 41 | const apiProtocol = "api" |
| 42 | |
| 43 | // New creates a new Gateway object associated with the specified router and |
| 44 | // following the given configuration. |
| 45 | func New(rootLogger *logrus.Logger, cfg *config.Gateway, r *Router) *Gateway { |
| 46 | logger := rootLogger.WithFields(logrus.Fields{"prefix": "gateway"}) |
| 47 | |
| 48 | cache, _ := lru.New(5000) |
| 49 | gw := &Gateway{ |
| 50 | Channels: make(map[string]*config.ChannelInfo), |
| 51 | Message: r.Message, |
| 52 | Router: r, |
| 53 | Bridges: make(map[string]*bridge.Bridge), |
| 54 | Config: r.Config, |
| 55 | Messages: cache, |
| 56 | logger: logger, |
| 57 | } |
| 58 | if err := gw.AddConfig(cfg); err != nil { |
| 59 | logger.Errorf("Failed to add configuration to gateway: %#v", err) |
| 60 | } |
| 61 | return gw |
| 62 | } |
| 63 | |
| 64 | // FindCanonicalMsgID returns the ID under which a message was stored in the cache. |
| 65 | func (gw *Gateway) FindCanonicalMsgID(protocol string, mID string) string { |
| 66 | ID := protocol + " " + mID |
| 67 | if gw.Messages.Contains(ID) { |
| 68 | return ID |
| 69 | } |
| 70 | |
| 71 | // If not keyed, iterate through cache for downstream, and infer upstream. |
| 72 | for _, mid := range gw.Messages.Keys() { |
| 73 | v, _ := gw.Messages.Peek(mid) |
| 74 | ids := v.([]*BrMsgID) |
| 75 | for _, downstreamMsgObj := range ids { |
| 76 | if ID == downstreamMsgObj.ID { |
| 77 | return mid.(string) |
| 78 | } |
| 79 | } |
| 80 | } |
| 81 | return "" |
| 82 | } |
| 83 | |
| 84 | // AddBridge sets up a new bridge on startup. |
| 85 | // |
| 86 | // It's added in the gateway object with the specified configuration, and is |
| 87 | // not triggered again on config change. |
| 88 | func (gw *Gateway) AddBridge(cfg *config.Bridge) error { |
| 89 | br := gw.Router.getBridge(cfg.Account) |
| 90 | if br == nil { |
| 91 | gw.checkConfig(cfg) |
| 92 | br = bridge.New(cfg) |
| 93 | br.Config = gw.Router.Config |
| 94 | br.General = &gw.BridgeValues().General |
| 95 | br.Log = gw.logger.WithFields(logrus.Fields{"prefix": br.Protocol}) |
| 96 | |
| 97 | // Instantiate bridge's HTTP client |
| 98 | http_client, err := br.NewHttpClient(br.GetString("http_proxy")) |
| 99 | if err != nil { |
| 100 | br.Log.Fatalf("config failure for account %s, HTTP settings incorrect", br.Account) |
| 101 | } |
| 102 | |
| 103 | br.HttpClient = http_client |
| 104 | |
| 105 | brconfig := &bridge.Config{ |
| 106 | Remote: gw.Message, |
| 107 | Bridge: br, |
| 108 | } |
| 109 | // add the actual bridger for this protocol to this bridge using the bridgeMap |
| 110 | if _, ok := gw.Router.BridgeMap[br.Protocol]; !ok { |
| 111 | gw.logger.Fatalf("Incorrect protocol %s specified in gateway configuration %s, exiting.", br.Protocol, cfg.Account) |
| 112 | } |
| 113 | br.Bridger = gw.Router.BridgeMap[br.Protocol](brconfig) |
| 114 | } |
| 115 | gw.mapChannelsToBridge(br) |
| 116 | gw.Bridges[cfg.Account] = br |
| 117 | return nil |
| 118 | } |
| 119 | |
| 120 | // checkConfig checks a bridge config, on startup. |
| 121 | // |
| 122 | // This is not triggered when config is reloaded from disk. |
| 123 | func (gw *Gateway) checkConfig(cfg *config.Bridge) { |
| 124 | match := false |
| 125 | for _, key := range gw.Router.Config.Viper().AllKeys() { |
| 126 | if strings.HasPrefix(key, strings.ToLower(cfg.Account)) { |
| 127 | match = true |
| 128 | break |
| 129 | } |
| 130 | } |
| 131 | if !match { |
| 132 | gw.logger.Fatalf("Account %s defined in gateway %s but no configuration found, exiting.", cfg.Account, gw.Name) |
| 133 | } |
| 134 | } |
| 135 | |
| 136 | // AddConfig associates a new configuration with the gateway object. |
| 137 | func (gw *Gateway) AddConfig(cfg *config.Gateway) error { |
| 138 | gw.Name = cfg.Name |
| 139 | gw.MyConfig = cfg |
| 140 | if err := gw.mapChannels(); err != nil { |
| 141 | gw.logger.Errorf("mapChannels() failed: %s", err) |
| 142 | } |
| 143 | for _, br := range append(gw.MyConfig.In, append(gw.MyConfig.InOut, gw.MyConfig.Out...)...) { |
| 144 | br := br // scopelint |
| 145 | err := gw.AddBridge(&br) |
| 146 | if err != nil { |
| 147 | return err |
| 148 | } |
| 149 | } |
| 150 | return nil |
| 151 | } |
| 152 | |
| 153 | func (gw *Gateway) mapChannelsToBridge(br *bridge.Bridge) { |
| 154 | for ID, channel := range gw.Channels { |
| 155 | if br.Account == channel.Account { |
| 156 | br.Channels[ID] = *channel |
| 157 | } |
| 158 | } |
| 159 | } |
| 160 | |
| 161 | func (gw *Gateway) reconnectBridge(br *bridge.Bridge) { |
| 162 | if err := br.Disconnect(); err != nil { |
| 163 | gw.logger.Errorf("Disconnect() %s failed: %s", br.Account, err) |
| 164 | } |
| 165 | time.Sleep(time.Second * 5) |
| 166 | RECONNECT: |
| 167 | gw.logger.Infof("Reconnecting %s", br.Account) |
| 168 | err := br.Connect() |
| 169 | if err != nil { |
| 170 | gw.logger.Errorf("Reconnection failed: %s. Trying again in 60 seconds", err) |
| 171 | time.Sleep(time.Second * 60) |
| 172 | goto RECONNECT |
| 173 | } |
| 174 | br.Joined = make(map[string]bool) |
| 175 | if err := br.JoinChannels(); err != nil { |
| 176 | gw.logger.Errorf("JoinChannels() %s failed: %s", br.Account, err) |
| 177 | } |
| 178 | } |
| 179 | |
| 180 | func (gw *Gateway) mapChannelConfig(cfg []config.Bridge, direction string) { |
| 181 | for _, br := range cfg { |
| 182 | if isAPI(br.Account) { |
| 183 | br.Channel = apiProtocol |
| 184 | } |
| 185 | // make sure to lowercase irc channels in config #348 |
| 186 | if strings.HasPrefix(br.Account, "irc.") { |
| 187 | br.Channel = strings.ToLower(br.Channel) |
| 188 | } |
| 189 | if strings.HasPrefix(br.Account, "mattermost.") && strings.HasPrefix(br.Channel, "#") { |
| 190 | gw.logger.Errorf("Mattermost channels do not start with a #: remove the # in %s", br.Channel) |
| 191 | os.Exit(1) |
| 192 | } |
| 193 | if strings.HasPrefix(br.Account, "zulip.") && !strings.Contains(br.Channel, "/topic:") { |
| 194 | gw.logger.Errorf("Breaking change, since matterbridge 1.14.0 zulip channels need to specify the topic with channel/topic:mytopic in %s of %s", br.Channel, br.Account) |
| 195 | os.Exit(1) |
| 196 | } |
| 197 | ID := br.Channel + br.Account |
| 198 | if _, ok := gw.Channels[ID]; !ok { |
| 199 | channel := &config.ChannelInfo{ |
| 200 | Name: br.Channel, |
| 201 | Direction: direction, |
| 202 | ID: ID, |
| 203 | Options: br.Options, |
| 204 | Account: br.Account, |
| 205 | SameChannel: make(map[string]bool), |
| 206 | } |
| 207 | channel.SameChannel[gw.Name] = br.SameChannel |
| 208 | gw.Channels[channel.ID] = channel |
| 209 | } else { |
| 210 | // if we already have a key and it's not our current direction it means we have a bidirectional inout |
| 211 | if gw.Channels[ID].Direction != direction { |
| 212 | gw.Channels[ID].Direction = "inout" |
| 213 | } |
| 214 | } |
| 215 | gw.Channels[ID].SameChannel[gw.Name] = br.SameChannel |
| 216 | } |
| 217 | } |
| 218 | |
| 219 | func (gw *Gateway) mapChannels() error { |
| 220 | gw.mapChannelConfig(gw.MyConfig.In, "in") |
| 221 | gw.mapChannelConfig(gw.MyConfig.Out, "out") |
| 222 | gw.mapChannelConfig(gw.MyConfig.InOut, "inout") |
| 223 | return nil |
| 224 | } |
| 225 | |
| 226 | func (gw *Gateway) getDestChannel(msg *config.Message, dest bridge.Bridge) []config.ChannelInfo { |
| 227 | var channels []config.ChannelInfo |
| 228 | |
| 229 | // for messages received from the api check that the gateway is the specified one |
| 230 | if msg.Protocol == apiProtocol && gw.Name != msg.Gateway { |
| 231 | return channels |
| 232 | } |
| 233 | |
| 234 | // discord join/leave is for the whole bridge, isn't a per channel join/leave |
| 235 | if msg.Event == config.EventJoinLeave && getProtocol(msg) == "discord" && msg.Channel == "" { |
| 236 | for _, channel := range gw.Channels { |
| 237 | if channel.Account == dest.Account && strings.Contains(channel.Direction, "out") && |
| 238 | gw.validGatewayDest(msg) { |
| 239 | channels = append(channels, *channel) |
| 240 | } |
| 241 | } |
| 242 | return channels |
| 243 | } |
| 244 | |
| 245 | // if source channel is in only, do nothing |
| 246 | for _, channel := range gw.Channels { |
| 247 | // lookup the channel from the message |
| 248 | if channel.ID == getChannelID(msg) { |
| 249 | // we only have destinations if the original message is from an "in" (sending) channel |
| 250 | if !strings.Contains(channel.Direction, "in") { |
| 251 | return channels |
| 252 | } |
| 253 | continue |
| 254 | } |
| 255 | } |
| 256 | for _, channel := range gw.Channels { |
| 257 | if _, ok := gw.Channels[getChannelID(msg)]; !ok { |
| 258 | continue |
| 259 | } |
| 260 | |
| 261 | // do samechannelgateway logic |
| 262 | if channel.SameChannel[msg.Gateway] { |
| 263 | if msg.Channel == channel.Name && msg.Account != dest.Account { |
| 264 | channels = append(channels, *channel) |
| 265 | } |
| 266 | continue |
| 267 | } |
| 268 | if strings.Contains(channel.Direction, "out") && channel.Account == dest.Account && gw.validGatewayDest(msg) { |
| 269 | channels = append(channels, *channel) |
| 270 | } |
| 271 | } |
| 272 | return channels |
| 273 | } |
| 274 | |
| 275 | func (gw *Gateway) getDestMsgID(msgID string, dest *bridge.Bridge, channel *config.ChannelInfo) string { |
| 276 | if res, ok := gw.Messages.Get(msgID); ok { |
| 277 | IDs := res.([]*BrMsgID) |
| 278 | for _, id := range IDs { |
| 279 | // check protocol, bridge name and channelname |
| 280 | // for people that reuse the same bridge multiple times. see #342 |
| 281 | if dest.Protocol == id.br.Protocol && dest.Name == id.br.Name && channel.ID == id.ChannelID { |
| 282 | return strings.Replace(id.ID, dest.Protocol+" ", "", 1) |
| 283 | } |
| 284 | } |
| 285 | } |
| 286 | return "" |
| 287 | } |
| 288 | |
| 289 | // ignoreTextEmpty returns true if we need to ignore a message with an empty text. |
| 290 | func (gw *Gateway) ignoreTextEmpty(msg *config.Message) bool { |
| 291 | if msg.Text != "" { |
| 292 | return false |
| 293 | } |
| 294 | if msg.Event == config.EventUserTyping { |
| 295 | return false |
| 296 | } |
| 297 | // we have an attachment or actual bytes, do not ignore |
| 298 | if msg.Extra != nil && |
| 299 | (msg.Extra["attachments"] != nil || |
| 300 | len(msg.Extra["file"]) > 0 || |
| 301 | len(msg.Extra[config.EventFileFailureSize]) > 0) { |
| 302 | return false |
| 303 | } |
| 304 | gw.logger.Debugf("ignoring empty message %#v from %s", msg, msg.Account) |
| 305 | return true |
| 306 | } |
| 307 | |
| 308 | func (gw *Gateway) ignoreMessage(msg *config.Message) bool { |
| 309 | // if we don't have the bridge, ignore it |
| 310 | if _, ok := gw.Bridges[msg.Account]; !ok { |
| 311 | return true |
| 312 | } |
| 313 | |
| 314 | igNicks := strings.Fields(gw.Bridges[msg.Account].GetString("IgnoreNicks")) |
| 315 | igMessages := strings.Fields(gw.Bridges[msg.Account].GetString("IgnoreMessages")) |
| 316 | if gw.ignoreTextEmpty(msg) || gw.ignoreText(msg.Username, igNicks) || gw.ignoreText(msg.Text, igMessages) || gw.ignoreFilesComment(msg.Extra, igMessages) { |
| 317 | return true |
| 318 | } |
| 319 | |
| 320 | return false |
| 321 | } |
| 322 | |
| 323 | // ignoreFilesComment returns true if we need to ignore a file with matched comment. |
| 324 | func (gw *Gateway) ignoreFilesComment(extra map[string][]interface{}, igMessages []string) bool { |
| 325 | if extra == nil { |
| 326 | return false |
| 327 | } |
| 328 | for _, f := range extra["file"] { |
| 329 | fi, ok := f.(config.FileInfo) |
| 330 | if !ok { |
| 331 | continue |
| 332 | } |
| 333 | if gw.ignoreText(fi.Comment, igMessages) { |
| 334 | return true |
| 335 | } |
| 336 | } |
| 337 | return false |
| 338 | } |
| 339 | |
| 340 | func (gw *Gateway) modifyUsername(msg *config.Message, dest *bridge.Bridge) string { |
| 341 | if dest.GetBool("StripNick") { |
| 342 | re := regexp.MustCompile("[^a-zA-Z0-9]+") |
| 343 | msg.Username = re.ReplaceAllString(msg.Username, "") |
| 344 | } |
| 345 | nick := dest.GetString("RemoteNickFormat") |
| 346 | |
| 347 | // loop to replace nicks |
| 348 | br := gw.Bridges[msg.Account] |
| 349 | for _, outer := range br.GetStringSlice2D("ReplaceNicks") { |
| 350 | search := outer[0] |
| 351 | replace := outer[1] |
| 352 | // TODO move compile to bridge init somewhere |
| 353 | re, err := regexp.Compile(search) |
| 354 | if err != nil { |
| 355 | gw.logger.Errorf("regexp in %s failed: %s", msg.Account, err) |
| 356 | break |
| 357 | } |
| 358 | msg.Username = re.ReplaceAllString(msg.Username, replace) |
| 359 | } |
| 360 | |
| 361 | if len(msg.Username) > 0 { |
| 362 | // fix utf-8 issue #193 |
| 363 | i := 0 |
| 364 | for index := range msg.Username { |
| 365 | if i == 1 { |
| 366 | i = index |
| 367 | break |
| 368 | } |
| 369 | i++ |
| 370 | } |
| 371 | nick = strings.ReplaceAll(nick, "{NOPINGNICK}", msg.Username[:i]+"\u200b"+msg.Username[i:]) |
| 372 | } |
| 373 | |
| 374 | nick = strings.ReplaceAll(nick, "{BRIDGE}", br.Name) |
| 375 | nick = strings.ReplaceAll(nick, "{PROTOCOL}", br.Protocol) |
| 376 | nick = strings.ReplaceAll(nick, "{GATEWAY}", gw.Name) |
| 377 | nick = strings.ReplaceAll(nick, "{LABEL}", br.GetString("Label")) |
| 378 | nick = strings.ReplaceAll(nick, "{NICK}", msg.Username) |
| 379 | nick = strings.ReplaceAll(nick, "{USERID}", msg.UserID) |
| 380 | nick = strings.ReplaceAll(nick, "{CHANNEL}", msg.Channel) |
| 381 | tengoNick, err := gw.modifyUsernameTengo(msg, br) |
| 382 | if err != nil { |
| 383 | gw.logger.Errorf("modifyUsernameTengo error: %s", err) |
| 384 | } |
| 385 | nick = strings.ReplaceAll(nick, "{TENGO}", tengoNick) |
| 386 | return nick |
| 387 | } |
| 388 | |
| 389 | func (gw *Gateway) modifyAvatar(msg *config.Message, dest *bridge.Bridge) string { |
| 390 | iconurl := dest.GetString("IconURL") |
| 391 | iconurl = strings.ReplaceAll(iconurl, "{NICK}", msg.Username) |
| 392 | if msg.Avatar == "" { |
| 393 | msg.Avatar = iconurl |
| 394 | } |
| 395 | return msg.Avatar |
| 396 | } |
| 397 | |
| 398 | func (gw *Gateway) modifyMessage(msg *config.Message) { |
| 399 | if gw.BridgeValues().General.TengoModifyMessage != "" { |
| 400 | gw.logger.Warnf("General TengoModifyMessage=%s is deprecated and will be removed in v1.20.0, please move to Tengo InMessage=%s", gw.BridgeValues().General.TengoModifyMessage, gw.BridgeValues().General.TengoModifyMessage) |
| 401 | } |
| 402 | |
| 403 | if err := modifyInMessageTengo(gw.BridgeValues().General.TengoModifyMessage, msg); err != nil { |
| 404 | gw.logger.Errorf("TengoModifyMessage failed: %s", err) |
| 405 | } |
| 406 | |
| 407 | inMessage := gw.BridgeValues().Tengo.InMessage |
| 408 | if inMessage == "" { |
| 409 | inMessage = gw.BridgeValues().Tengo.Message |
| 410 | if inMessage != "" { |
| 411 | gw.logger.Warnf("Tengo Message=%s is deprecated and will be removed in v1.20.0, please move to Tengo InMessage=%s", inMessage, inMessage) |
| 412 | } |
| 413 | } |
| 414 | |
| 415 | if err := modifyInMessageTengo(inMessage, msg); err != nil { |
| 416 | gw.logger.Errorf("Tengo.Message failed: %s", err) |
| 417 | } |
| 418 | |
| 419 | // replace :emoji: to unicode |
| 420 | // emoji.ReplacePadding = "" |
| 421 | // msg.Text = emoji.Sprint(msg.Text) |
| 422 | |
| 423 | br := gw.Bridges[msg.Account] |
| 424 | // loop to replace messages |
| 425 | for _, outer := range br.GetStringSlice2D("ReplaceMessages") { |
| 426 | search := outer[0] |
| 427 | replace := outer[1] |
| 428 | // TODO move compile to bridge init somewhere |
| 429 | re, err := regexp.Compile(search) |
| 430 | if err != nil { |
| 431 | gw.logger.Errorf("regexp in %s failed: %s", msg.Account, err) |
| 432 | break |
| 433 | } |
| 434 | msg.Text = re.ReplaceAllString(msg.Text, replace) |
| 435 | } |
| 436 | |
| 437 | gw.handleExtractNicks(msg) |
| 438 | |
| 439 | // messages from api have Gateway specified, don't overwrite |
| 440 | if msg.Protocol != apiProtocol { |
| 441 | msg.Gateway = gw.Name |
| 442 | } |
| 443 | } |
| 444 | |
| 445 | // SendMessage sends a message (with specified parentID) to the channel on the selected |
| 446 | // destination bridge and returns a message ID or an error. |
| 447 | func (gw *Gateway) SendMessage( |
| 448 | rmsg *config.Message, |
| 449 | dest *bridge.Bridge, |
| 450 | channel *config.ChannelInfo, |
| 451 | canonicalParentMsgID string, |
| 452 | ) (string, error) { |
| 453 | msg := *rmsg |
| 454 | // Only send the avatar download event to ourselves. |
| 455 | if msg.Event == config.EventAvatarDownload { |
| 456 | if channel.ID != getChannelID(rmsg) { |
| 457 | return "", nil |
| 458 | } |
| 459 | } else { |
| 460 | // do not send to ourself for any other event |
| 461 | if channel.ID == getChannelID(rmsg) { |
| 462 | return "", nil |
| 463 | } |
| 464 | } |
| 465 | |
| 466 | // Only send irc notices to irc |
| 467 | if msg.Event == config.EventNoticeIRC && dest.Protocol != "irc" { |
| 468 | return "", nil |
| 469 | } |
| 470 | |
| 471 | // Too noisy to log like other events |
| 472 | debugSendMessage := "" |
| 473 | if msg.Event != config.EventUserTyping { |
| 474 | debugSendMessage = fmt.Sprintf("=> Sending %#v from %s (%s) to %s (%s)", msg, msg.Account, rmsg.Channel, dest.Account, channel.Name) |
| 475 | } |
| 476 | |
| 477 | msg.Channel = channel.Name |
| 478 | msg.Avatar = gw.modifyAvatar(rmsg, dest) |
| 479 | msg.Username = gw.modifyUsername(rmsg, dest) |
| 480 | |
| 481 | // exclude file delete event as the msg ID here is the native file ID that needs to be deleted |
| 482 | if msg.Event != config.EventFileDelete { |
| 483 | msg.ID = gw.getDestMsgID(rmsg.Protocol+" "+rmsg.ID, dest, channel) |
| 484 | } |
| 485 | |
| 486 | // for api we need originchannel as channel |
| 487 | if dest.Protocol == apiProtocol { |
| 488 | msg.Channel = rmsg.Channel |
| 489 | } |
| 490 | |
| 491 | msg.ParentID = gw.getDestMsgID(canonicalParentMsgID, dest, channel) |
| 492 | if msg.ParentID == "" { |
| 493 | msg.ParentID = strings.Replace(canonicalParentMsgID, dest.Protocol+" ", "", 1) |
| 494 | } |
| 495 | |
| 496 | // if the parentID is still empty and we have a parentID set in the original message |
| 497 | // this means that we didn't find it in the cache so set it to a "msg-parent-not-found" constant |
| 498 | if msg.ParentID == "" && rmsg.ParentID != "" { |
| 499 | msg.ParentID = config.ParentIDNotFound |
| 500 | } |
| 501 | |
| 502 | drop, err := gw.modifyOutMessageTengo(rmsg, &msg, dest) |
| 503 | if err != nil { |
| 504 | gw.logger.Errorf("modifySendMessageTengo: %s", err) |
| 505 | } |
| 506 | |
| 507 | if drop { |
| 508 | gw.logger.Debugf("=> Tengo dropping %#v from %s (%s) to %s (%s)", msg, msg.Account, rmsg.Channel, dest.Account, channel.Name) |
| 509 | return "", nil |
| 510 | } |
| 511 | |
| 512 | if debugSendMessage != "" { |
| 513 | gw.logger.Debug(debugSendMessage) |
| 514 | } |
| 515 | // if we are using mattermost plugin account, send messages to MattermostPlugin channel |
| 516 | // that can be picked up by the mattermost matterbridge plugin |
| 517 | if dest.Account == "mattermost.plugin" { |
| 518 | gw.Router.MattermostPlugin <- msg |
| 519 | } |
| 520 | |
| 521 | defer func(t time.Time) { |
| 522 | gw.logger.Debugf("=> Send from %s (%s) to %s (%s) took %s", msg.Account, rmsg.Channel, dest.Account, channel.Name, time.Since(t)) |
| 523 | }(time.Now()) |
| 524 | |
| 525 | mID, err := dest.Send(msg) |
| 526 | if err != nil { |
| 527 | return mID, err |
| 528 | } |
| 529 | |
| 530 | // append the message ID (mID) from this bridge (dest) to our brMsgIDs slice |
| 531 | if mID != "" { |
| 532 | gw.logger.Debugf("mID %s: %s", dest.Account, mID) |
| 533 | return mID, nil |
| 534 | // brMsgIDs = append(brMsgIDs, &BrMsgID{dest, dest.Protocol + " " + mID, channel.ID}) |
| 535 | } |
| 536 | return "", nil |
| 537 | } |
| 538 | |
| 539 | func (gw *Gateway) validGatewayDest(msg *config.Message) bool { |
| 540 | return msg.Gateway == gw.Name |
| 541 | } |
| 542 | |
| 543 | func getChannelID(msg *config.Message) string { |
| 544 | return msg.Channel + msg.Account |
| 545 | } |
| 546 | |
| 547 | func isAPI(account string) bool { |
| 548 | return strings.HasPrefix(account, "api.") |
| 549 | } |
| 550 | |
| 551 | // ignoreText returns true if text matches any of the input regexes. |
| 552 | func (gw *Gateway) ignoreText(text string, input []string) bool { |
| 553 | for _, entry := range input { |
| 554 | if entry == "" { |
| 555 | continue |
| 556 | } |
| 557 | // TODO do not compile regexps everytime |
| 558 | re, err := regexp.Compile(entry) |
| 559 | if err != nil { |
| 560 | gw.logger.Errorf("incorrect regexp %s", entry) |
| 561 | continue |
| 562 | } |
| 563 | if re.MatchString(text) { |
| 564 | gw.logger.Debugf("matching %s. ignoring %s", entry, text) |
| 565 | return true |
| 566 | } |
| 567 | } |
| 568 | return false |
| 569 | } |
| 570 | |
| 571 | func getProtocol(msg *config.Message) string { |
| 572 | p := strings.Split(msg.Account, ".") |
| 573 | return p[0] |
| 574 | } |
| 575 | |
| 576 | func modifyInMessageTengo(filename string, msg *config.Message) error { |
| 577 | if filename == "" { |
| 578 | return nil |
| 579 | } |
| 580 | |
| 581 | res, err := os.ReadFile(filename) //nolint:gosec |
| 582 | if err != nil { |
| 583 | return err |
| 584 | } |
| 585 | s := tengo.NewScript(res) |
| 586 | s.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...)) |
| 587 | _ = s.Add("msgText", msg.Text) |
| 588 | _ = s.Add("msgUsername", msg.Username) |
| 589 | _ = s.Add("msgUserID", msg.UserID) |
| 590 | _ = s.Add("msgAccount", msg.Account) |
| 591 | _ = s.Add("msgChannel", msg.Channel) |
| 592 | c, err := s.Compile() |
| 593 | if err != nil { |
| 594 | return err |
| 595 | } |
| 596 | if err := c.Run(); err != nil { |
| 597 | return err |
| 598 | } |
| 599 | msg.Text = c.Get("msgText").String() |
| 600 | msg.Username = c.Get("msgUsername").String() |
| 601 | return nil |
| 602 | } |
| 603 | |
| 604 | func (gw *Gateway) modifyUsernameTengo(msg *config.Message, br *bridge.Bridge) (string, error) { |
| 605 | filename := gw.BridgeValues().Tengo.RemoteNickFormat |
| 606 | if filename == "" { |
| 607 | return "", nil |
| 608 | } |
| 609 | |
| 610 | res, err := os.ReadFile(filename) //nolint:gosec |
| 611 | if err != nil { |
| 612 | return "", err |
| 613 | } |
| 614 | s := tengo.NewScript(res) |
| 615 | s.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...)) |
| 616 | _ = s.Add("result", "") |
| 617 | _ = s.Add("msgText", msg.Text) |
| 618 | _ = s.Add("msgUsername", msg.Username) |
| 619 | _ = s.Add("msgUserID", msg.UserID) |
| 620 | _ = s.Add("nick", msg.Username) |
| 621 | _ = s.Add("msgAccount", msg.Account) |
| 622 | _ = s.Add("msgChannel", msg.Channel) |
| 623 | _ = s.Add("channel", msg.Channel) |
| 624 | _ = s.Add("msgProtocol", msg.Protocol) |
| 625 | _ = s.Add("remoteAccount", br.Account) |
| 626 | _ = s.Add("protocol", br.Protocol) |
| 627 | _ = s.Add("bridge", br.Name) |
| 628 | _ = s.Add("gateway", gw.Name) |
| 629 | c, err := s.Compile() |
| 630 | if err != nil { |
| 631 | return "", err |
| 632 | } |
| 633 | if err := c.Run(); err != nil { |
| 634 | return "", err |
| 635 | } |
| 636 | return c.Get("result").String(), nil |
| 637 | } |
| 638 | |
| 639 | func (gw *Gateway) modifyOutMessageTengo(origmsg *config.Message, msg *config.Message, br *bridge.Bridge) (bool, error) { |
| 640 | filename := gw.BridgeValues().Tengo.OutMessage |
| 641 | var ( |
| 642 | res []byte |
| 643 | err error |
| 644 | drop bool |
| 645 | ) |
| 646 | |
| 647 | if filename == "" { |
| 648 | res, err = internal.Asset("tengo/outmessage.tengo") |
| 649 | if err != nil { |
| 650 | return drop, err |
| 651 | } |
| 652 | } else { |
| 653 | res, err = os.ReadFile(filename) //nolint:gosec |
| 654 | if err != nil { |
| 655 | return drop, err |
| 656 | } |
| 657 | } |
| 658 | |
| 659 | s := tengo.NewScript(res) |
| 660 | |
| 661 | s.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...)) |
| 662 | _ = s.Add("inAccount", origmsg.Account) |
| 663 | _ = s.Add("inProtocol", origmsg.Protocol) |
| 664 | _ = s.Add("inChannel", origmsg.Channel) |
| 665 | _ = s.Add("inGateway", origmsg.Gateway) |
| 666 | _ = s.Add("inEvent", origmsg.Event) |
| 667 | _ = s.Add("outAccount", br.Account) |
| 668 | _ = s.Add("outProtocol", br.Protocol) |
| 669 | _ = s.Add("outChannel", msg.Channel) |
| 670 | _ = s.Add("outGateway", gw.Name) |
| 671 | _ = s.Add("outEvent", msg.Event) |
| 672 | _ = s.Add("msgText", msg.Text) |
| 673 | _ = s.Add("msgUsername", msg.Username) |
| 674 | _ = s.Add("msgUserID", msg.UserID) |
| 675 | _ = s.Add("msgDrop", drop) |
| 676 | c, err := s.Compile() |
| 677 | if err != nil { |
| 678 | return drop, err |
| 679 | } |
| 680 | |
| 681 | if err := c.Run(); err != nil { |
| 682 | return drop, err |
| 683 | } |
| 684 | |
| 685 | drop = c.Get("msgDrop").Bool() |
| 686 | msg.Text = c.Get("msgText").String() |
| 687 | msg.Username = c.Get("msgUsername").String() |
| 688 | |
| 689 | return drop, nil |
| 690 | } |
| 691 | |