Thumbnail

rani/matterbridge.git

Clone URL: https://git.buni.party/rani/matterbridge.git

Viewing file on branch master

1package config
2
3import (
4 "bytes"
5 "errors"
6 "fmt"
7 "os"
8 "path/filepath"
9 "regexp"
10 "strings"
11 "sync"
12 "time"
13
14 "github.com/fsnotify/fsnotify"
15 "github.com/sirupsen/logrus"
16 "github.com/spf13/viper"
17)
18
19const (
20 EventJoinLeave = "join_leave"
21 EventTopicChange = "topic_change"
22 EventFailure = "failure"
23 EventFileFailureSize = "file_failure_size"
24 EventAvatarDownload = "avatar_download"
25 EventRejoinChannels = "rejoin_channels"
26 EventUserAction = "user_action"
27 EventMsgDelete = "msg_delete"
28 EventFileDelete = "file_delete"
29 EventAPIConnected = "api_connected"
30 EventUserTyping = "user_typing"
31 EventGetChannelMembers = "get_channel_members"
32 EventNoticeIRC = "notice_irc"
33)
34
35const ParentIDNotFound = "msg-parent-not-found"
36
37type Message struct {
38 Text string `json:"text"`
39 Channel string `json:"channel"`
40 Username string `json:"username"`
41 UserID string `json:"userid"` // userid on the bridge
42 Avatar string `json:"avatar"`
43 Account string `json:"account"`
44 Event string `json:"event"`
45 Protocol string `json:"protocol"`
46 Gateway string `json:"gateway"`
47 ParentID string `json:"parent_id"`
48 Timestamp time.Time `json:"timestamp"`
49 ID string `json:"id"`
50 Extra map[string][]interface{}
51}
52
53func (m Message) ParentNotFound() bool {
54 return m.ParentID == ParentIDNotFound
55}
56
57func (m Message) ParentValid() bool {
58 return m.ParentID != "" && !m.ParentNotFound()
59}
60
61// GetFileInfos extracts typed FileInfo list from the message.
62//
63// This method is guaranteed not to fail. The inner type casting should never
64// fail, but will simply produce a warning if that's the case.
65func (m Message) GetFileInfos(log *logrus.Entry) *[]*FileInfo {
66 var fileInfos []*FileInfo
67
68 for _, file := range m.Extra["file"] {
69 fileInfo, ok := file.(FileInfo)
70 if !ok {
71 // This should never happen, unless a bridge receiving an external message
72 // produces an invalid Extra field where the File is not valid FileInfo.
73 // TODO: log more information about the message for debugging.
74 log.Warn(FileCastError())
75 continue
76 }
77
78 fileInfos = append(fileInfos, &fileInfo)
79 }
80
81 return &fileInfos
82}
83
84// FileInfo is an attachment contained in a message.
85//
86// When receiving an attachment (eg. an image), a bridge should populate the
87// Data/Size fields.
88//
89// When the media server is enabled, for services that don't support file upload
90// (such as IRC), the gateway router will upload the file to the media server
91// and populate the URL/SHA fields. The Data/Size fields are not removed
92// in this process. See handleFiles in gateway/handlers.go
93type FileInfo struct {
94 Name string
95 Data *[]byte
96 Comment string
97 URL string
98 Size int64
99 Avatar bool
100 SHA string
101 NativeID string
102}
103
104var errFileCast = errors.New("failed to cast config.FileInfo")
105
106func FileCastError() error {
107 return fmt.Errorf("%w", errFileCast)
108}
109
110type ChannelInfo struct {
111 Name string
112 Account string
113 Direction string
114 ID string
115 SameChannel map[string]bool
116 Options ChannelOptions
117}
118
119type ChannelMember struct {
120 Username string
121 Nick string
122 UserID string
123 ChannelID string
124 ChannelName string
125}
126
127type ChannelMembers []ChannelMember
128
129type Protocol struct {
130 AllowMention []string // discord
131 BindAddress string // mattermost, slack // DEPRECATED
132 Buffer int // api
133 Charset string // irc
134 ClientID string // msteams
135 ColorNicks bool // only irc for now
136 Debug bool // general
137 DebugLevel int // only for irc now
138 DeviceID string // matrix
139 DisableWebPagePreview bool // telegram
140 EditSuffix string // mattermost, slack, discord, telegram
141 EditDisable bool // mattermost, slack, discord, telegram
142 HTMLDisable bool // matrix
143 IconURL string // mattermost, slack
144 IgnoreFailureOnStart bool // general
145 IgnoreNicks string // all protocols
146 IgnoreMessages string // all protocols
147 Jid string // xmpp
148 JoinDelay string // all protocols
149 Label string // all protocols
150 Login string // mattermost, matrix
151 LogFile string // general
152 MediaDownloadBlackList []string
153 MediaDownloadPath string // Write upload to a file on the same server.
154 MediaDownloadSize int // all protocols
155 MediaServerDownload string
156 MediaConvertTgs string // telegram
157 MediaConvertWebPToPNG bool // telegram
158 MessageDelay int // IRC, time in millisecond to wait between messages
159 MessageFormat string // telegram
160 MessageLength int // IRC, max length of a message allowed
161 MessageQueue int // IRC, size of message queue for flood control
162 MessageSplit bool // IRC, split long messages with newlines on MessageLength instead of clipping
163 MessageSplitMaxCount int // discord, split long messages into at most this many messages instead of clipping (MessageLength=1950 cannot be configured)
164 Muc string // xmpp
165 MxID string // matrix
166 Name string // all protocols
167 Nick string // all protocols
168 NickFormatter string // mattermost, slack
169 NickServNick string // IRC
170 NickServUsername string // IRC
171 NickServPassword string // IRC
172 NicksPerRow int // mattermost, slack
173 NoHomeServerSuffix bool // matrix
174 NoSendJoinPart bool // all protocols
175 NoTLS bool // mattermost, xmpp
176 Password string // IRC,mattermost,XMPP,matrix
177 PickleKey string // matrix
178 PrefixMessagesWithNick bool // mattemost, slack
179 PreserveThreading bool // slack
180 Protocol string // all protocols
181 QuoteDisable bool // telegram, discord
182 QuoteFormat string // telegram, discord
183 QuoteLengthLimit int // telegram, discord
184 RealName string // IRC
185 RecoveryKey string // matrix
186 RejoinDelay int // IRC
187 ReplaceMessages [][]string // all protocols
188 ReplaceNicks [][]string // all protocols
189 RemoteNickFormat string // all protocols
190 RunCommands []string // IRC
191 Server string // IRC,mattermost,XMPP,discord,matrix
192 SessionFile string // msteams,whatsapp
193 ShowJoinPart bool // all protocols
194 ShowTopicChange bool // slack
195 ShowUserTyping bool // slack
196 ShowEmbeds bool // discord
197 SkipTLSVerify bool // IRC, mattermost
198 SkipVersionCheck bool // mattermost
199 StripNick bool // all protocols
200 StripMarkdown bool // irc
201 SyncTopic bool // slack
202 TengoModifyMessage string // general
203 Team string // mattermost
204 TeamID string // msteams
205 TenantID string // msteams
206 Token string // slack, discord, api, matrix
207 Topic string // zulip
208 URL string // mattermost, slack // DEPRECATED
209 UseAPI bool // mattermost, slack
210 UseLocalAvatar []string // discord
211 UseSASL bool // IRC
212 UseTLS bool // IRC
213 UseDiscriminator bool // discord
214 UseFirstName bool // telegram
215 UseUserName bool // discord, matrix, mattermost
216 UsePerProtocolJID bool // xmpp
217 UseInsecureURL bool // telegram
218 UserName string // IRC
219 VerboseJoinPart bool // IRC
220 WebhookBindAddress string // mattermost, slack
221 WebhookURL string // mattermost, slack
222}
223
224type ChannelOptions struct {
225 Key string // irc, xmpp
226 WebhookURL string // discord
227 Topic string // zulip
228}
229
230type Bridge struct {
231 Account string
232 Channel string
233 Options ChannelOptions
234 SameChannel bool
235}
236
237type Gateway struct {
238 Name string
239 Enable bool
240 In []Bridge
241 Out []Bridge
242 InOut []Bridge
243}
244
245type Tengo struct {
246 InMessage string
247 Message string
248 RemoteNickFormat string
249 OutMessage string
250}
251
252type SameChannelGateway struct {
253 Name string
254 Enable bool
255 Channels []string
256 Accounts []string
257}
258
259type BridgeValues struct {
260 API map[string]Protocol
261 IRC map[string]Protocol
262 Mattermost map[string]Protocol
263 Matrix map[string]Protocol
264 Slack map[string]Protocol
265 SlackLegacy map[string]Protocol
266 Steam map[string]Protocol
267 XMPP map[string]Protocol
268 Discord map[string]Protocol
269 Telegram map[string]Protocol
270 Rocketchat map[string]Protocol
271 SSHChat map[string]Protocol
272 WhatsApp map[string]Protocol // TODO is this struct used? Search for "SlackLegacy" for example didn't return any results
273 Zulip map[string]Protocol
274 Keybase map[string]Protocol
275 Mumble map[string]Protocol
276 General Protocol
277 Tengo Tengo
278 Gateway []Gateway
279 SameChannelGateway []SameChannelGateway
280}
281
282type Config interface {
283 Viper() *viper.Viper
284 BridgeValues() *BridgeValues
285 IsKeySet(key string) bool
286 GetBool(key string) (bool, bool)
287 GetInt(key string) (int, bool)
288 GetString(key string) (string, bool)
289 GetStringSlice(key string) ([]string, bool)
290 GetStringSlice2D(key string) ([][]string, bool)
291 IsFilenameBlacklisted(filename string) bool
292}
293
294type config struct {
295 sync.RWMutex
296
297 logger *logrus.Entry
298 v *viper.Viper
299 cv *BridgeValues
300 MediaDownloadBlackListRegexes *[]*regexp.Regexp
301}
302
303// NewConfig instantiates a new configuration based on the specified configuration file path.
304func NewConfig(rootLogger *logrus.Logger, cfgfile string) Config {
305 logger := rootLogger.WithFields(logrus.Fields{"prefix": "config"})
306
307 viper.SetConfigFile(cfgfile)
308
309 input, err := os.ReadFile(cfgfile) //nolint:gosec
310 if err != nil {
311 logger.Fatalf("Failed to read configuration file: %#v", err)
312 }
313
314 cfgtype := detectConfigType(cfgfile)
315 mycfg := newConfigFromString(logger, input, cfgtype)
316 if mycfg.cv.General.LogFile != "" {
317 logfile, err := os.OpenFile(mycfg.cv.General.LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
318 if err == nil {
319 logger.Info("Opening log file ", mycfg.cv.General.LogFile)
320 rootLogger.Out = logfile
321 } else {
322 logger.Warn("Failed to open ", mycfg.cv.General.LogFile)
323 }
324 }
325 if mycfg.cv.General.MediaDownloadSize == 0 {
326 mycfg.cv.General.MediaDownloadSize = 1000000
327 }
328
329 // Precompile MediaBlackList regexes so we make sure they're correct,
330 // and they don't have to be compiled on every file attachment, because
331 // that's a slow operation.
332 mycfg.compileMediaDownloadBlackListRegexes()
333
334 viper.WatchConfig()
335 viper.OnConfigChange(func(e fsnotify.Event) {
336 logger.Println("Config file changed:", e.Name)
337 })
338 return mycfg
339}
340
341// detectConfigType detects JSON and YAML formats, defaults to TOML.
342func detectConfigType(cfgfile string) string {
343 fileExt := filepath.Ext(cfgfile)
344 switch fileExt {
345 case ".json":
346 return "json"
347 case ".yaml", ".yml":
348 return "yaml"
349 }
350 return "toml"
351}
352
353// NewConfigFromString instantiates a new configuration based on the specified string.
354func NewConfigFromString(rootLogger *logrus.Logger, input []byte) Config {
355 logger := rootLogger.WithFields(logrus.Fields{"prefix": "config"})
356 return newConfigFromString(logger, input, "toml")
357}
358
359func newConfigFromString(logger *logrus.Entry, input []byte, cfgtype string) *config {
360 viper.SetConfigType(cfgtype)
361 viper.SetEnvPrefix("matterbridge")
362 viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
363 viper.AutomaticEnv()
364
365 if err := viper.ReadConfig(bytes.NewBuffer(input)); err != nil {
366 logger.Fatalf("Failed to parse the configuration: %s", err)
367 }
368
369 cfg := &BridgeValues{}
370 if err := viper.Unmarshal(cfg); err != nil {
371 logger.Fatalf("Failed to load the configuration: %s", err)
372 }
373 return &config{
374 logger: logger,
375 v: viper.GetViper(),
376 cv: cfg,
377 }
378}
379
380func (c *config) BridgeValues() *BridgeValues {
381 return c.cv
382}
383
384func (c *config) Viper() *viper.Viper {
385 return c.v
386}
387
388func (c *config) IsKeySet(key string) bool {
389 c.RLock()
390 defer c.RUnlock()
391 return c.v.IsSet(key)
392}
393
394func (c *config) GetBool(key string) (bool, bool) {
395 c.RLock()
396 defer c.RUnlock()
397 return c.v.GetBool(key), c.v.IsSet(key)
398}
399
400func (c *config) GetInt(key string) (int, bool) {
401 c.RLock()
402 defer c.RUnlock()
403 return c.v.GetInt(key), c.v.IsSet(key)
404}
405
406func (c *config) GetString(key string) (string, bool) {
407 c.RLock()
408 defer c.RUnlock()
409 return c.v.GetString(key), c.v.IsSet(key)
410}
411
412func (c *config) GetStringSlice(key string) ([]string, bool) {
413 c.RLock()
414 defer c.RUnlock()
415 return c.v.GetStringSlice(key), c.v.IsSet(key)
416}
417
418func (c *config) GetStringSlice2D(key string) ([][]string, bool) {
419 c.RLock()
420 defer c.RUnlock()
421
422 res, ok := c.v.Get(key).([]interface{})
423 if !ok {
424 return nil, false
425 }
426 var result [][]string
427 for _, entry := range res {
428 result2 := []string{}
429 for _, entry2 := range entry.([]interface{}) {
430 result2 = append(result2, entry2.(string))
431 }
432 result = append(result, result2)
433 }
434 return result, true
435}
436
437// IsFilenameBlackListed checks if a given file name matches the
438// configured blacklist. This is useful to filter potentially-harmful
439// files that could be served over HTTP (eg. `.html` with XSS).
440func (c *config) IsFilenameBlacklisted(filename string) bool {
441 c.RLock()
442 defer c.RUnlock()
443
444 for _, re := range *c.MediaDownloadBlackListRegexes {
445 if re.MatchString(filename) {
446 return true
447 }
448 }
449
450 return false
451}
452
453func (c *config) compileMediaDownloadBlackListRegexes() {
454 regexes := []*regexp.Regexp{}
455
456 // TODO: apparently c.cv.General does not get updated when config reloads
457 // see https://github.com/matterbridge-org/matterbridge/issues/57
458 // for _, regex := range c.cv.General.MediaDownloadBlackList {
459 for _, regex := range c.v.GetStringSlice("general.MediaDownloadBlackList") {
460 c.logger.Debugf("Found blacklist regex %s", regex)
461
462 re, err := regexp.Compile(regex)
463 if err != nil {
464 c.logger.Errorf("incorrect regexp %s for MediaDownloadBlackList", regex)
465 continue
466 }
467
468 regexes = append(regexes, re)
469 }
470
471 c.MediaDownloadBlackListRegexes = &regexes
472 c.logger.Debug("Successfully applied new `MediaDownloadBlackList` regexes")
473}
474
475func GetIconURL(msg *Message, iconURL string) string {
476 info := strings.Split(msg.Account, ".")
477 protocol := info[0]
478 name := info[1]
479 iconURL = strings.ReplaceAll(iconURL, "{NICK}", msg.Username)
480 iconURL = strings.ReplaceAll(iconURL, "{BRIDGE}", name)
481 iconURL = strings.ReplaceAll(iconURL, "{PROTOCOL}", protocol)
482 return iconURL
483}
484
485type TestConfig struct {
486 Config
487
488 Overrides map[string]interface{}
489}
490
491func (c *TestConfig) IsKeySet(key string) bool {
492 _, ok := c.Overrides[key]
493 return ok || c.Config.IsKeySet(key)
494}
495
496func (c *TestConfig) GetBool(key string) (bool, bool) {
497 val, ok := c.Overrides[key]
498 if ok {
499 return val.(bool), true
500 }
501 return c.Config.GetBool(key)
502}
503
504func (c *TestConfig) GetInt(key string) (int, bool) {
505 if val, ok := c.Overrides[key]; ok {
506 return val.(int), true
507 }
508 return c.Config.GetInt(key)
509}
510
511func (c *TestConfig) GetString(key string) (string, bool) {
512 if val, ok := c.Overrides[key]; ok {
513 return val.(string), true
514 }
515 return c.Config.GetString(key)
516}
517
518func (c *TestConfig) GetStringSlice(key string) ([]string, bool) {
519 if val, ok := c.Overrides[key]; ok {
520 return val.([]string), true
521 }
522 return c.Config.GetStringSlice(key)
523}
524
525func (c *TestConfig) GetStringSlice2D(key string) ([][]string, bool) {
526 if val, ok := c.Overrides[key]; ok {
527 return val.([][]string), true
528 }
529 return c.Config.GetStringSlice2D(key)
530}
531