Thumbnail

rani/matterbridge.git

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

Viewing file on branch master

1package birc
2
3import (
4 "crypto/tls"
5 "errors"
6 "fmt"
7 "hash/crc32"
8 "io"
9 "net"
10 "sort"
11 "strconv"
12 "strings"
13 "time"
14
15 "github.com/lrstanley/girc"
16 "github.com/matterbridge-org/matterbridge/bridge"
17 "github.com/matterbridge-org/matterbridge/bridge/config"
18 "github.com/matterbridge-org/matterbridge/bridge/helper"
19 stripmd "github.com/writeas/go-strip-markdown"
20
21 // We need to import the 'data' package as an implicit dependency.
22 // See: https://godoc.org/github.com/paulrosania/go-charset/charset
23 _ "github.com/paulrosania/go-charset/data"
24)
25
26type Birc struct {
27 i *girc.Client
28 Nick string
29 names map[string][]string
30 connected chan error
31 Local chan config.Message // local queue for flood control
32 FirstConnection, authDone bool
33 MessageDelay, MessageQueue, MessageLength int
34 channels map[string]bool
35
36 *bridge.Config
37}
38
39func New(cfg *bridge.Config) bridge.Bridger {
40 b := &Birc{}
41 b.Config = cfg
42 b.Nick = b.GetString("Nick")
43 b.names = make(map[string][]string)
44 b.connected = make(chan error)
45 b.channels = make(map[string]bool)
46
47 if b.GetInt("MessageDelay") == 0 {
48 b.MessageDelay = 1300
49 } else {
50 b.MessageDelay = b.GetInt("MessageDelay")
51 }
52 if b.GetInt("MessageQueue") == 0 {
53 b.MessageQueue = 30
54 } else {
55 b.MessageQueue = b.GetInt("MessageQueue")
56 }
57 if b.GetInt("MessageLength") == 0 {
58 b.MessageLength = 400
59 } else {
60 b.MessageLength = b.GetInt("MessageLength")
61 }
62 b.FirstConnection = true
63 return b
64}
65
66func (b *Birc) Command(msg *config.Message) string {
67 if msg.Text == "!users" {
68 b.i.Handlers.Add(girc.RPL_NAMREPLY, b.storeNames)
69 b.i.Handlers.Add(girc.RPL_ENDOFNAMES, b.endNames)
70 b.i.Cmd.SendRaw("NAMES " + msg.Channel) //nolint:errcheck
71 }
72 return ""
73}
74
75func (b *Birc) Connect() error {
76 if b.GetBool("UseSASL") && b.GetString("TLSClientCertificate") != "" {
77 return errors.New("you can't enable SASL and TLSClientCertificate at the same time")
78 }
79
80 b.Local = make(chan config.Message, b.MessageQueue+10)
81 b.Log.Infof("Connecting %s", b.GetString("Server"))
82
83 i, err := b.getClient()
84 if err != nil {
85 return err
86 }
87
88 if b.GetBool("UseSASL") {
89 i.Config.SASL = &girc.SASLPlain{
90 User: b.GetString("NickServNick"),
91 Pass: b.GetString("NickServPassword"),
92 }
93 }
94
95 i.Handlers.Add(girc.RPL_WELCOME, b.handleNewConnection)
96 i.Handlers.Add(girc.RPL_ENDOFMOTD, b.handleOtherAuth)
97 i.Handlers.Add(girc.ERR_NOMOTD, b.handleOtherAuth)
98 i.Handlers.Add(girc.ALL_EVENTS, b.handleOther)
99 b.i = i
100
101 go b.doConnect()
102
103 err = <-b.connected
104 if err != nil {
105 return fmt.Errorf("connection failed %s", err)
106 }
107 b.Log.Info("Connection succeeded")
108 b.FirstConnection = false
109 if b.GetInt("DebugLevel") == 0 {
110 i.Handlers.Clear(girc.ALL_EVENTS)
111 }
112 go b.doSend()
113 return nil
114}
115
116func (b *Birc) Disconnect() error {
117 b.i.Close()
118 close(b.Local)
119 return nil
120}
121
122func (b *Birc) JoinChannel(channel config.ChannelInfo) error {
123 b.channels[channel.Name] = true
124 // need to check if we have nickserv auth done before joining channels
125 for {
126 if b.authDone {
127 break
128 }
129 time.Sleep(time.Second)
130 }
131 if channel.Options.Key != "" {
132 b.Log.Debugf("using key %s for channel %s", channel.Options.Key, channel.Name)
133 b.i.Cmd.JoinKey(channel.Name, channel.Options.Key)
134 } else {
135 b.i.Cmd.Join(channel.Name)
136 }
137 return nil
138}
139
140func (b *Birc) Send(msg config.Message) (string, error) {
141 // ignore delete messages
142 if msg.Event == config.EventMsgDelete {
143 return "", nil
144 }
145
146 b.Log.Debugf("=> Receiving %#v", msg)
147
148 // we can be in between reconnects #385
149 if !b.i.IsConnected() {
150 b.Log.Error("Not connected to server, dropping message")
151 return "", nil
152 }
153
154 // Execute a command
155 if strings.HasPrefix(msg.Text, "!") {
156 b.Command(&msg)
157 }
158
159 // convert to specified charset
160 if err := b.handleCharset(&msg); err != nil {
161 return "", err
162 }
163
164 // handle files, return if we're done here
165 if ok := b.handleFiles(&msg); ok {
166 return "", nil
167 }
168
169 var msgLines []string
170 if b.GetBool("StripMarkdown") {
171 msg.Text = stripmd.Strip(msg.Text)
172 }
173
174 if b.GetBool("MessageSplit") {
175 msgLines = helper.GetSubLines(msg.Text, b.MessageLength, b.GetString("MessageClipped"))
176 } else {
177 msgLines = helper.GetSubLines(msg.Text, 0, b.GetString("MessageClipped"))
178 }
179 for i := range msgLines {
180 if len(b.Local) >= b.MessageQueue {
181 b.Log.Debugf("flooding, dropping message (queue at %d)", len(b.Local))
182 return "", nil
183 }
184
185 msg.Text = msgLines[i]
186 b.Local <- msg
187 }
188 return "", nil
189}
190
191func (b *Birc) doConnect() {
192 for {
193 if err := b.i.Connect(); err != nil {
194 b.Log.Errorf("disconnect: error: %s", err)
195 if b.FirstConnection {
196 b.connected <- err
197 return
198 }
199 } else {
200 b.Log.Info("disconnect: client requested quit")
201 }
202 b.Log.Info("reconnecting in 30 seconds...")
203 time.Sleep(30 * time.Second)
204 b.i.Handlers.Clear(girc.RPL_WELCOME)
205 b.i.Handlers.Add(girc.RPL_WELCOME, func(client *girc.Client, event girc.Event) {
206 b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: "", Account: b.Account, Event: config.EventRejoinChannels}
207 // set our correct nick on reconnect if necessary
208 b.Nick = event.Source.Name
209 })
210 }
211}
212
213// Sanitize nicks for RELAYMSG: replace IRC characters with special meanings with "-"
214func sanitizeNick(nick string) string {
215 sanitize := func(r rune) rune {
216 if strings.ContainsRune("!+%@&#$:'\"?*,. ", r) {
217 return '-'
218 }
219 return r
220 }
221 return strings.Map(sanitize, nick)
222}
223
224func (b *Birc) doSend() {
225 rate := time.Millisecond * time.Duration(b.MessageDelay)
226 throttle := time.NewTicker(rate)
227 for msg := range b.Local {
228 <-throttle.C
229 username := msg.Username
230 // Optional support for the proposed RELAYMSG extension, described at
231 // https://github.com/jlu5/ircv3-specifications/blob/master/extensions/relaymsg.md
232 // nolint:nestif
233 if (b.i.HasCapability("overdrivenetworks.com/relaymsg") || b.i.HasCapability("draft/relaymsg")) &&
234 b.GetBool("UseRelayMsg") {
235 username = sanitizeNick(username)
236 text := msg.Text
237
238 // Work around girc chomping leading commas on single word messages?
239 if strings.HasPrefix(text, ":") && !strings.ContainsRune(text, ' ') {
240 text = ":" + text
241 }
242
243 if msg.Event == config.EventUserAction {
244 b.i.Cmd.SendRawf("RELAYMSG %s %s :\x01ACTION %s\x01", msg.Channel, username, text) //nolint:errcheck
245 } else {
246 b.Log.Debugf("Sending RELAYMSG to channel %s: nick=%s", msg.Channel, username)
247 b.i.Cmd.SendRawf("RELAYMSG %s %s :%s", msg.Channel, username, text) //nolint:errcheck
248 }
249 } else {
250 if b.GetBool("Colornicks") {
251 checksum := crc32.ChecksumIEEE([]byte(msg.Username))
252 colorCode := checksum%14 + 2 // quick fix - prevent white or black color codes
253 username = fmt.Sprintf("\x03%02d%s\x0F", colorCode, msg.Username)
254 }
255 switch msg.Event {
256 case config.EventUserAction:
257 b.i.Cmd.Action(msg.Channel, username+msg.Text)
258 case config.EventNoticeIRC:
259 b.Log.Debugf("Sending notice to channel %s", msg.Channel)
260 b.i.Cmd.Notice(msg.Channel, username+msg.Text)
261 default:
262 b.Log.Debugf("Sending to channel %s", msg.Channel)
263 b.i.Cmd.Message(msg.Channel, username+msg.Text)
264 }
265 }
266 }
267}
268
269// validateInput validates the server/port/nick configuration. Returns a *girc.Client if successful
270func (b *Birc) getClient() (*girc.Client, error) {
271 server, portstr, err := net.SplitHostPort(b.GetString("Server"))
272 if err != nil {
273 return nil, err
274 }
275 port, err := strconv.Atoi(portstr)
276 if err != nil {
277 return nil, err
278 }
279 user := b.GetString("UserName")
280 if user == "" {
281 user = b.GetString("Nick")
282 }
283 // fix strict user handling of girc
284 for !girc.IsValidUser(user) {
285 if len(user) == 1 || len(user) == 0 {
286 user = "matterbridge"
287 break
288 }
289 user = user[1:]
290 }
291 realName := b.GetString("RealName")
292 if realName == "" {
293 realName = b.GetString("Nick")
294 }
295
296 debug := io.Discard
297 if b.GetInt("DebugLevel") == 2 {
298 debug = b.Log.Writer()
299 }
300
301 pingDelay, err := time.ParseDuration(b.GetString("pingdelay"))
302 if err != nil || pingDelay == 0 {
303 pingDelay = time.Minute
304 }
305
306 b.Log.Debugf("setting pingdelay to %s", pingDelay)
307
308 tlsConfig, err := b.getTLSConfig()
309 if err != nil {
310 return nil, err
311 }
312
313 i := girc.New(girc.Config{
314 Server: server,
315 ServerPass: b.GetString("Password"),
316 Port: port,
317 Nick: b.GetString("Nick"),
318 User: user,
319 Name: realName,
320 SSL: b.GetBool("UseTLS"),
321 Bind: b.GetString("Bind"),
322 TLSConfig: tlsConfig,
323 PingDelay: pingDelay,
324 // skip gIRC internal rate limiting, since we have our own throttling
325 AllowFlood: true,
326 Debug: debug,
327 SupportedCaps: map[string][]string{"overdrivenetworks.com/relaymsg": nil, "draft/relaymsg": nil},
328 })
329 return i, nil
330}
331
332func (b *Birc) endNames(client *girc.Client, event girc.Event) {
333 channel := event.Params[1]
334 sort.Strings(b.names[channel])
335 maxNamesPerPost := (300 / b.nicksPerRow()) * b.nicksPerRow()
336 for len(b.names[channel]) > maxNamesPerPost {
337 b.Remote <- config.Message{
338 Username: b.Nick, Text: b.formatnicks(b.names[channel][0:maxNamesPerPost]),
339 Channel: channel, Account: b.Account,
340 }
341 b.names[channel] = b.names[channel][maxNamesPerPost:]
342 }
343 b.Remote <- config.Message{
344 Username: b.Nick, Text: b.formatnicks(b.names[channel]),
345 Channel: channel, Account: b.Account,
346 }
347 b.names[channel] = nil
348 b.i.Handlers.Clear(girc.RPL_NAMREPLY)
349 b.i.Handlers.Clear(girc.RPL_ENDOFNAMES)
350}
351
352func (b *Birc) skipPrivMsg(event girc.Event) bool {
353 // Our nick can be changed
354 b.Nick = b.i.GetNick()
355
356 // freenode doesn't send 001 as first reply
357 if event.Command == "NOTICE" && len(event.Params) != 2 {
358 return true
359 }
360 // don't forward queries to the bot
361 if event.Params[0] == b.Nick {
362 return true
363 }
364 // don't forward message from ourself
365 if event.Source != nil {
366 if event.Source.Name == b.Nick {
367 return true
368 }
369 }
370 // don't forward messages we sent via RELAYMSG
371 if relayedNick, ok := event.Tags.Get("draft/relaymsg"); ok && relayedNick == b.Nick {
372 return true
373 }
374 // This is the old name of the cap sent in spoofed messages; I've kept this in
375 // for compatibility reasons
376 if relayedNick, ok := event.Tags.Get("relaymsg"); ok && relayedNick == b.Nick {
377 return true
378 }
379 return false
380}
381
382func (b *Birc) nicksPerRow() int {
383 return 4
384}
385
386func (b *Birc) storeNames(client *girc.Client, event girc.Event) {
387 channel := event.Params[2]
388 b.names[channel] = append(
389 b.names[channel],
390 strings.Split(strings.TrimSpace(event.Last()), " ")...)
391}
392
393func (b *Birc) formatnicks(nicks []string) string {
394 return strings.Join(nicks, ", ") + " currently on IRC"
395}
396
397func (b *Birc) getTLSConfig() (*tls.Config, error) {
398 server, _, _ := net.SplitHostPort(b.GetString("server"))
399
400 tlsConfig := &tls.Config{
401 InsecureSkipVerify: b.GetBool("skiptlsverify"), //nolint:gosec
402 ServerName: server,
403 }
404
405 if filename := b.GetString("TLSClientCertificate"); filename != "" {
406 cert, err := tls.LoadX509KeyPair(filename, filename)
407 if err != nil {
408 return nil, err
409 }
410
411 tlsConfig.Certificates = []tls.Certificate{cert}
412 }
413
414 return tlsConfig, nil
415}
416