summaryrefslogtreecommitdiffstats
path: root/vendor/github.com/lrstanley/girc/state.go
blob: 36dcc82b37af9a5d6212fb7a46e6d54eeefc529e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
// Copyright (c) Liam Stanley <me@liamstanley.io>. All rights reserved. Use
// of this source code is governed by the MIT license that can be found in
// the LICENSE file.

package girc

import (
	"sort"
	"sync"
	"time"
)

// state represents the actively-changing variables within the client
// runtime. Note that everything within the state should be guarded by the
// embedded sync.RWMutex.
type state struct {
	sync.RWMutex
	// nick, ident, and host are the internal trackers for our user.
	nick, ident, host string
	// channels represents all channels we're active in.
	channels map[string]*Channel
	// users represents all of users that we're tracking.
	users map[string]*User
	// enabledCap are the capabilities which are enabled for this connection.
	enabledCap []string
	// tmpCap are the capabilties which we share with the server during the
	// last capability check. These will get sent once we have received the
	// last capability list command from the server.
	tmpCap []string
	// serverOptions are the standard capabilities and configurations
	// supported by the server at connection time. This also includes
	// RPL_ISUPPORT entries.
	serverOptions map[string]string
	// motd is the servers message of the day.
	motd string
}

// notify sends state change notifications so users can update their refs
// when state changes.
func (s *state) notify(c *Client, ntype string) {
	c.RunHandlers(&Event{Command: ntype})
}

// reset resets the state back to it's original form.
func (s *state) reset() {
	s.Lock()
	s.nick = ""
	s.ident = ""
	s.host = ""
	s.channels = make(map[string]*Channel)
	s.users = make(map[string]*User)
	s.serverOptions = make(map[string]string)
	s.enabledCap = []string{}
	s.motd = ""
	s.Unlock()
}

// User represents an IRC user and the state attached to them.
type User struct {
	// Nick is the users current nickname. rfc1459 compliant.
	Nick string `json:"nick"`
	// Ident is the users username/ident. Ident is commonly prefixed with a
	// "~", which indicates that they do not have a identd server setup for
	// authentication.
	Ident string `json:"ident"`
	// Host is the visible host of the users connection that the server has
	// provided to us for their connection. May not always be accurate due to
	// many networks spoofing/hiding parts of the hostname for privacy
	// reasons.
	Host string `json:"host"`

	// ChannelList is a sorted list of all channels that we are currently
	// tracking the user in. Each channel name is rfc1459 compliant. See
	// User.Channels() for a shorthand if you're looking for the *Channel
	// version of the channel list.
	ChannelList []string `json:"channels"`

	// FirstSeen represents the first time that the user was seen by the
	// client for the given channel. Only usable if from state, not in past.
	FirstSeen time.Time `json:"first_seen"`
	// LastActive represents the last time that we saw the user active,
	// which could be during nickname change, message, channel join, etc.
	// Only usable if from state, not in past.
	LastActive time.Time `json:"last_active"`

	// Perms are the user permissions applied to this user that affect the given
	// channel. This supports non-rfc style modes like Admin, Owner, and HalfOp.
	Perms *UserPerms `json:"perms"`

	// Extras are things added on by additional tracking methods, which may
	// or may not work on the IRC server in mention.
	Extras struct {
		// Name is the users "realname" or full name. Commonly contains links
		// to the IRC client being used, or something of non-importance. May
		// also be empty if unsupported by the server/tracking is disabled.
		Name string `json:"name"`
		// Account refers to the account which the user is authenticated as.
		// This differs between each network (e.g. usually Nickserv, but
		// could also be something like Undernet). May also be empty if
		// unsupported by the server/tracking is disabled.
		Account string `json:"account"`
		// Away refers to the away status of the user. An empty string
		// indicates that they are active, otherwise the string is what they
		// set as their away message. May also be empty if unsupported by the
		// server/tracking is disabled.
		Away string `json:"away"`
	} `json:"extras"`
}

// Channels returns a reference of *Channels that the client knows the user
// is in. If you're just looking for the namme of the channels, use
// User.ChannelList.
func (u User) Channels(c *Client) []*Channel {
	if c == nil {
		panic("nil Client provided")
	}

	channels := []*Channel{}

	c.state.RLock()
	for i := 0; i < len(u.ChannelList); i++ {
		ch := c.state.lookupChannel(u.ChannelList[i])
		if ch != nil {
			channels = append(channels, ch)
		}
	}
	c.state.RUnlock()

	return channels
}

// Copy returns a deep copy of the user which can be modified without making
// changes to the actual state.
func (u *User) Copy() *User {
	if u == nil {
		return nil
	}

	nu := &User{}
	*nu = *u

	nu.Perms = u.Perms.Copy()
	_ = copy(nu.ChannelList, u.ChannelList)

	return nu
}

// addChannel adds the channel to the users channel list.
func (u *User) addChannel(name string) {
	if u.InChannel(name) {
		return
	}

	u.ChannelList = append(u.ChannelList, ToRFC1459(name))
	sort.Strings(u.ChannelList)

	u.Perms.set(name, Perms{})
}

// deleteChannel removes an existing channel from the users channel list.
func (u *User) deleteChannel(name string) {
	name = ToRFC1459(name)

	j := -1
	for i := 0; i < len(u.ChannelList); i++ {
		if u.ChannelList[i] == name {
			j = i
			break
		}
	}

	if j != -1 {
		u.ChannelList = append(u.ChannelList[:j], u.ChannelList[j+1:]...)
	}

	u.Perms.remove(name)
}

// InChannel checks to see if a user is in the given channel.
func (u *User) InChannel(name string) bool {
	name = ToRFC1459(name)

	for i := 0; i < len(u.ChannelList); i++ {
		if u.ChannelList[i] == name {
			return true
		}
	}

	return false
}

// Lifetime represents the amount of time that has passed since we have first
// seen the user.
func (u *User) Lifetime() time.Duration {
	return time.Since(u.FirstSeen)
}

// Active represents the the amount of time that has passed since we have
// last seen the user.
func (u *User) Active() time.Duration {
	return time.Since(u.LastActive)
}

// IsActive returns true if they were active within the last 30 minutes.
func (u *User) IsActive() bool {
	return u.Active() < (time.Minute * 30)
}

// Channel represents an IRC channel and the state attached to it.
type Channel struct {
	// Name of the channel. Must be rfc1459 compliant.
	Name string `json:"name"`
	// Topic of the channel.
	Topic string `json:"topic"`

	// UserList is a sorted list of all users we are currently tracking within
	// the channel. Each is the nickname, and is rfc1459 compliant.
	UserList []string `json:"user_list"`
	// Joined represents the first time that the client joined the channel.
	Joined time.Time `json:"joined"`
	// Modes are the known channel modes that the bot has captured.
	Modes CModes `json:"modes"`
}

// Users returns a reference of *Users that the client knows the channel has
// If you're just looking for just the name of the users, use Channnel.UserList.
func (ch Channel) Users(c *Client) []*User {
	if c == nil {
		panic("nil Client provided")
	}

	users := []*User{}

	c.state.RLock()
	for i := 0; i < len(ch.UserList); i++ {
		user := c.state.lookupUser(ch.UserList[i])
		if user != nil {
			users = append(users, user)
		}
	}
	c.state.RUnlock()

	return users
}

// Trusted returns a list of users which have voice or greater in the given
// channel. See Perms.IsTrusted() for more information.
func (ch Channel) Trusted(c *Client) []*User {
	if c == nil {
		panic("nil Client provided")
	}

	users := []*User{}

	c.state.RLock()
	for i := 0; i < len(ch.UserList); i++ {
		user := c.state.lookupUser(ch.UserList[i])
		if user == nil {
			continue
		}

		perms, ok := user.Perms.Lookup(ch.Name)
		if ok && perms.IsTrusted() {
			users = append(users, user)
		}
	}
	c.state.RUnlock()

	return users
}

// Admins returns a list of users which have half-op (if supported), or
// greater permissions (op, admin, owner, etc) in the given channel. See
// Perms.IsAdmin() for more information.
func (ch Channel) Admins(c *Client) []*User {
	if c == nil {
		panic("nil Client provided")
	}

	users := []*User{}

	c.state.RLock()
	for i := 0; i < len(ch.UserList); i++ {
		user := c.state.lookupUser(ch.UserList[i])
		if user == nil {
			continue
		}

		perms, ok := user.Perms.Lookup(ch.Name)
		if ok && perms.IsAdmin() {
			users = append(users, user)
		}
	}
	c.state.RUnlock()

	return users
}

// addUser adds a user to the users list.
func (ch *Channel) addUser(nick string) {
	if ch.UserIn(nick) {
		return
	}

	ch.UserList = append(ch.UserList, ToRFC1459(nick))
	sort.Strings(ch.UserList)
}

// deleteUser removes an existing user from the users list.
func (ch *Channel) deleteUser(nick string) {
	nick = ToRFC1459(nick)

	j := -1
	for i := 0; i < len(ch.UserList); i++ {
		if ch.UserList[i] == nick {
			j = i
			break
		}
	}

	if j != -1 {
		ch.UserList = append(ch.UserList[:j], ch.UserList[j+1:]...)
	}
}

// Copy returns a deep copy of a given channel.
func (ch *Channel) Copy() *Channel {
	if ch == nil {
		return nil
	}

	nc := &Channel{}
	*nc = *ch

	_ = copy(nc.UserList, ch.UserList)

	// And modes.
	nc.Modes = ch.Modes.Copy()

	return nc
}

// Len returns the count of users in a given channel.
func (ch *Channel) Len() int {
	return len(ch.UserList)
}

// UserIn checks to see if a given user is in a channel.
func (ch *Channel) UserIn(name string) bool {
	name = ToRFC1459(name)

	for i := 0; i < len(ch.UserList); i++ {
		if ch.UserList[i] == name {
			return true
		}
	}

	return false
}

// Lifetime represents the amount of time that has passed since we have first
// joined the channel.
func (ch *Channel) Lifetime() time.Duration {
	return time.Since(ch.Joined)
}

// createChannel creates the channel in state, if not already done.
func (s *state) createChannel(name string) (ok bool) {
	supported := s.chanModes()
	prefixes, _ := parsePrefixes(s.userPrefixes())

	if _, ok := s.channels[ToRFC1459(name)]; ok {
		return false
	}

	s.channels[ToRFC1459(name)] = &Channel{
		Name:     name,
		UserList: []string{},
		Joined:   time.Now(),
		Modes:    NewCModes(supported, prefixes),
	}

	return true
}

// deleteChannel removes the channel from state, if not already done.
func (s *state) deleteChannel(name string) {
	name = ToRFC1459(name)

	_, ok := s.channels[name]
	if !ok {
		return
	}

	for _, user := range s.channels[name].UserList {
		s.users[user].deleteChannel(name)

		if len(s.users[user].ChannelList) == 0 {
			// Assume we were only tracking them in this channel, and they
			// should be removed from state.

			delete(s.users, user)
		}
	}

	delete(s.channels, name)
}

// lookupChannel returns a reference to a channel, nil returned if no results
// found.
func (s *state) lookupChannel(name string) *Channel {
	return s.channels[ToRFC1459(name)]
}

// lookupUser returns a reference to a user, nil returned if no results
// found.
func (s *state) lookupUser(name string) *User {
	return s.users[ToRFC1459(name)]
}

// createUser creates the user in state, if not already done.
func (s *state) createUser(nick string) (ok bool) {
	if _, ok := s.users[ToRFC1459(nick)]; ok {
		// User already exists.
		return false
	}

	s.users[ToRFC1459(nick)] = &User{
		Nick:       nick,
		FirstSeen:  time.Now(),
		LastActive: time.Now(),
		Perms:      &UserPerms{channels: make(map[string]Perms)},
	}

	return true
}

// deleteUser removes the user from channel state.
func (s *state) deleteUser(channelName, nick string) {
	user := s.lookupUser(nick)
	if user == nil {
		return
	}

	if channelName == "" {
		for i := 0; i < len(user.ChannelList); i++ {
			s.channels[user.ChannelList[i]].deleteUser(nick)
		}

		delete(s.users, ToRFC1459(nick))
		return
	}

	channel := s.lookupChannel(channelName)
	if channel == nil {
		return
	}

	user.deleteChannel(channelName)
	channel.deleteUser(nick)

	if len(user.ChannelList) == 0 {
		// This means they are no longer in any channels we track, delete
		// them from state.

		delete(s.users, ToRFC1459(nick))
	}
}

// renameUser renames the user in state, in all locations where relevant.
func (s *state) renameUser(from, to string) {
	from = ToRFC1459(from)

	// Update our nickname.
	if from == ToRFC1459(s.nick) {
		s.nick = to
	}

	user := s.lookupUser(from)
	if user == nil {
		return
	}

	delete(s.users, from)

	user.Nick = to
	user.LastActive = time.Now()
	s.users[ToRFC1459(to)] = user

	for i := 0; i < len(user.ChannelList); i++ {
		for j := 0; j < len(s.channels[user.ChannelList[i]].UserList); j++ {
			if s.channels[user.ChannelList[i]].UserList[j] == from {
				s.channels[user.ChannelList[i]].UserList[j] = ToRFC1459(to)

				sort.Strings(s.channels[user.ChannelList[i]].UserList)
				break
			}
		}
	}
}