Bläddra i källkod

Add very initial ChanServ and NickServ virtual clients

As well, add channel registration and re-applying founder privs on the first client joining the channel. I'm going to re-architect our modes system to better acocunt for this sort of change.
tags/v0.7.0
Daniel Oaks 7 år sedan
förälder
incheckning
b33b217fab
10 ändrade filer med 343 tillägg och 31 borttagningar
  1. 0
    9
      irc/accountreg.go
  2. 9
    0
      irc/accounts.go
  3. 22
    4
      irc/channel.go
  4. 90
    0
      irc/channelreg.go
  5. 119
    0
      irc/chanserv.go
  6. 16
    0
      irc/commands.go
  7. 20
    0
      irc/help.go
  8. 10
    2
      irc/nickname.go
  9. 24
    0
      irc/nickserv.go
  10. 33
    16
      irc/server.go

irc/registration.go → irc/accountreg.go Visa fil

16
 	"github.com/tidwall/buntdb"
16
 	"github.com/tidwall/buntdb"
17
 )
17
 )
18
 
18
 
19
-const (
20
-	keyAccountExists      = "account.exists %s"
21
-	keyAccountVerified    = "account.verified %s"
22
-	keyAccountName        = "account.name %s" // stores the 'preferred name' of the account, not casemapped
23
-	keyAccountRegTime     = "account.registered.time %s"
24
-	keyAccountCredentials = "account.credentials %s"
25
-	keyCertToAccount      = "account.creds.certfp %s"
26
-)
27
-
28
 var (
19
 var (
29
 	errAccountCreation     = errors.New("Account could not be created")
20
 	errAccountCreation     = errors.New("Account could not be created")
30
 	errCertfpAlreadyExists = errors.New("An account already exists with your certificate")
21
 	errCertfpAlreadyExists = errors.New("An account already exists with your certificate")

+ 9
- 0
irc/accounts.go Visa fil

17
 	"github.com/tidwall/buntdb"
17
 	"github.com/tidwall/buntdb"
18
 )
18
 )
19
 
19
 
20
+const (
21
+	keyAccountExists      = "account.exists %s"
22
+	keyAccountVerified    = "account.verified %s"
23
+	keyAccountName        = "account.name %s" // stores the 'preferred name' of the account, not casemapped
24
+	keyAccountRegTime     = "account.registered.time %s"
25
+	keyAccountCredentials = "account.credentials %s"
26
+	keyCertToAccount      = "account.creds.certfp %s"
27
+)
28
+
20
 var (
29
 var (
21
 	// EnabledSaslMechanisms contains the SASL mechanisms that exist and that we support.
30
 	// EnabledSaslMechanisms contains the SASL mechanisms that exist and that we support.
22
 	// This can be moved to some other data structure/place if we need to load/unload mechs later.
31
 	// This can be moved to some other data structure/place if we need to load/unload mechs later.

+ 22
- 4
irc/channel.go Visa fil

14
 	"sync"
14
 	"sync"
15
 
15
 
16
 	"github.com/DanielOaks/girc-go/ircmsg"
16
 	"github.com/DanielOaks/girc-go/ircmsg"
17
+	"github.com/tidwall/buntdb"
17
 )
18
 )
18
 
19
 
19
 type Channel struct {
20
 type Channel struct {
277
 	client.channels.Add(channel)
278
 	client.channels.Add(channel)
278
 	channel.members.Add(client)
279
 	channel.members.Add(client)
279
 	if len(channel.members) == 1 {
280
 	if len(channel.members) == 1 {
280
-		channel.createdTime = time.Now()
281
-		// // we should only do this on registered channels
282
-		// channel.members[client][ChannelFounder] = true
283
-		channel.members[client][ChannelOperator] = true
281
+		client.server.registeredChannelsMutex.Lock()
282
+		defer client.server.registeredChannelsMutex.Unlock()
283
+		client.server.store.Update(func(tx *buntdb.Tx) error {
284
+			chanReg := client.server.loadChannelNoMutex(tx, channel.nameCasefolded)
285
+
286
+			if chanReg == nil {
287
+				channel.createdTime = time.Now()
288
+				channel.members[client][ChannelOperator] = true
289
+			} else {
290
+				// we should only do this on registered channels
291
+				if client.account != nil && client.account.Name == chanReg.Founder {
292
+					channel.members[client][ChannelFounder] = true
293
+				}
294
+				channel.topic = chanReg.Topic
295
+				channel.topicSetBy = chanReg.TopicSetBy
296
+				channel.topicSetTime = chanReg.TopicSetTime
297
+				channel.name = chanReg.Name
298
+				channel.createdTime = chanReg.RegisteredAt
299
+			}
300
+			return nil
301
+		})
284
 	}
302
 	}
285
 
303
 
286
 	if client.capabilities[ExtendedJoin] {
304
 	if client.capabilities[ExtendedJoin] {

+ 90
- 0
irc/channelreg.go Visa fil

1
+// Copyright (c) 2016- Daniel Oaks <daniel@danieloaks.net>
2
+// released under the MIT license
3
+
4
+package irc
5
+
6
+import (
7
+	"errors"
8
+	"fmt"
9
+	"strconv"
10
+	"time"
11
+
12
+	"github.com/tidwall/buntdb"
13
+)
14
+
15
+const (
16
+	keyChannelExists       = "channel.exists %s"
17
+	keyChannelName         = "channel.name %s" // stores the 'preferred name' of the channel, not casemapped
18
+	keyChannelRegTime      = "channel.registered.time %s"
19
+	keyChannelFounder      = "channel.founder %s"
20
+	keyChannelTopic        = "channel.topic %s"
21
+	keyChannelTopicSetBy   = "channel.topic.setby %s"
22
+	keyChannelTopicSetTime = "channel.topic.settime %s"
23
+)
24
+
25
+var (
26
+	errChanExists = errors.New("Channel already exists")
27
+)
28
+
29
+// RegisteredChannel holds details about a given registered channel.
30
+type RegisteredChannel struct {
31
+	// Name of the channel.
32
+	Name string
33
+	// RegisteredAt represents the time that the channel was registered.
34
+	RegisteredAt time.Time
35
+	// Founder indicates the founder of the channel.
36
+	Founder string
37
+	// Topic represents the channel topic.
38
+	Topic string
39
+	// TopicSetBy represents the host that set the topic.
40
+	TopicSetBy string
41
+	// TopicSetTime represents the time the topic was set.
42
+	TopicSetTime time.Time
43
+}
44
+
45
+// loadChannelNoMutex loads a channel from the store.
46
+func (server *Server) loadChannelNoMutex(tx *buntdb.Tx, channelKey string) *RegisteredChannel {
47
+	// return loaded chan if it already exists
48
+	if server.registeredChannels[channelKey] != nil {
49
+		return server.registeredChannels[channelKey]
50
+	}
51
+	_, err := tx.Get(fmt.Sprintf(keyChannelExists, channelKey))
52
+	if err == buntdb.ErrNotFound {
53
+		// chan does not already exist, return
54
+		return nil
55
+	}
56
+
57
+	// channel exists, load it
58
+	name, _ := tx.Get(fmt.Sprintf(keyChannelName, channelKey))
59
+	regTime, _ := tx.Get(fmt.Sprintf(keyChannelRegTime, channelKey))
60
+	regTimeInt, _ := strconv.ParseInt(regTime, 10, 64)
61
+	founder, _ := tx.Get(fmt.Sprintf(keyChannelFounder, channelKey))
62
+	topic, _ := tx.Get(fmt.Sprintf(keyChannelTopic, channelKey))
63
+	topicSetBy, _ := tx.Get(fmt.Sprintf(keyChannelTopicSetBy, channelKey))
64
+	topicSetTime, _ := tx.Get(fmt.Sprintf(keyChannelTopicSetTime, channelKey))
65
+	topicSetTimeInt, _ := strconv.ParseInt(topicSetTime, 10, 64)
66
+
67
+	chanInfo := RegisteredChannel{
68
+		Name:         name,
69
+		RegisteredAt: time.Unix(regTimeInt, 0),
70
+		Founder:      founder,
71
+		Topic:        topic,
72
+		TopicSetBy:   topicSetBy,
73
+		TopicSetTime: time.Unix(topicSetTimeInt, 0),
74
+	}
75
+	server.registeredChannels[channelKey] = &chanInfo
76
+
77
+	return &chanInfo
78
+}
79
+
80
+// saveChannelNoMutex saves a channel to the store.
81
+func (server *Server) saveChannelNoMutex(tx *buntdb.Tx, channelKey string, channelInfo RegisteredChannel) {
82
+	tx.Set(fmt.Sprintf(keyChannelExists, channelKey), "1", nil)
83
+	tx.Set(fmt.Sprintf(keyChannelName, channelKey), channelInfo.Name, nil)
84
+	tx.Set(fmt.Sprintf(keyChannelRegTime, channelKey), strconv.FormatInt(channelInfo.RegisteredAt.Unix(), 10), nil)
85
+	tx.Set(fmt.Sprintf(keyChannelFounder, channelKey), channelInfo.Founder, nil)
86
+	tx.Set(fmt.Sprintf(keyChannelTopic, channelKey), channelInfo.Topic, nil)
87
+	tx.Set(fmt.Sprintf(keyChannelTopicSetBy, channelKey), channelInfo.TopicSetBy, nil)
88
+	tx.Set(fmt.Sprintf(keyChannelTopicSetTime, channelKey), strconv.FormatInt(channelInfo.TopicSetTime.Unix(), 10), nil)
89
+	server.registeredChannels[channelKey] = &channelInfo
90
+}

+ 119
- 0
irc/chanserv.go Visa fil

1
+// Copyright (c) 2017 Daniel Oaks <daniel@danieloaks.net>
2
+// released under the MIT license
3
+
4
+package irc
5
+
6
+import (
7
+	"fmt"
8
+	"strings"
9
+	"time"
10
+
11
+	"github.com/DanielOaks/girc-go/ircmsg"
12
+	"github.com/tidwall/buntdb"
13
+)
14
+
15
+// csHandler handles the /CS and /CHANSERV commands
16
+func csHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
17
+	server.chanservReceivePrivmsg(client, strings.Join(msg.Params, " "))
18
+	return false
19
+}
20
+
21
+func (server *Server) chanservReceiveNotice(client *Client, message string) {
22
+	// do nothing
23
+}
24
+
25
+// ChanServNotice sends the client a notice from ChanServ.
26
+func (client *Client) ChanServNotice(text string) {
27
+	client.Send(nil, fmt.Sprintf("ChanServ!services@%s", client.server.name), "NOTICE", client.nick, text)
28
+}
29
+
30
+func (server *Server) chanservReceivePrivmsg(client *Client, message string) {
31
+	var params []string
32
+	for _, p := range strings.Split(message, " ") {
33
+		if len(p) > 0 {
34
+			params = append(params, p)
35
+		}
36
+	}
37
+	if len(params) < 1 {
38
+		client.ChanServNotice("You need to run a command")
39
+		//TODO(dan): dump CS help here
40
+		return
41
+	}
42
+
43
+	command := strings.ToLower(params[0])
44
+	server.logger.Debug("chanserv", fmt.Sprintf("Client %s ran command %s", client.nick, command))
45
+
46
+	if command == "register" {
47
+		if len(params) < 2 {
48
+			client.ChanServNotice("Syntax: REGISTER <channel>")
49
+			return
50
+		}
51
+
52
+		server.registeredChannelsMutex.Lock()
53
+		defer server.registeredChannelsMutex.Unlock()
54
+
55
+		channelName := params[1]
56
+		channelKey, err := CasefoldChannel(channelName)
57
+		if err != nil {
58
+			client.ChanServNotice("Channel name is not valid")
59
+			return
60
+		}
61
+
62
+		channelInfo := server.channels.Get(channelKey)
63
+		if channelInfo == nil {
64
+			client.ChanServNotice("You must be an oper on the channel to register it")
65
+			return
66
+		}
67
+
68
+		if !channelInfo.ClientIsAtLeast(client, ChannelOperator) {
69
+			client.ChanServNotice("You must be an oper on the channel to register it")
70
+			return
71
+		}
72
+
73
+		server.store.Update(func(tx *buntdb.Tx) error {
74
+			currentChan := server.loadChannelNoMutex(tx, channelKey)
75
+			if currentChan != nil {
76
+				client.ChanServNotice("Channel is already registered")
77
+				return nil
78
+			}
79
+
80
+			account := client.account
81
+			if account == nil {
82
+				client.ChanServNotice("You must be logged in to register a channel")
83
+				return nil
84
+			}
85
+
86
+			chanRegInfo := RegisteredChannel{
87
+				Name:         channelName,
88
+				RegisteredAt: time.Now(),
89
+				Founder:      account.Name,
90
+				Topic:        channelInfo.topic,
91
+				TopicSetBy:   channelInfo.topicSetBy,
92
+				TopicSetTime: channelInfo.topicSetTime,
93
+			}
94
+			server.saveChannelNoMutex(tx, channelKey, chanRegInfo)
95
+
96
+			client.ChanServNotice(fmt.Sprintf("Channel %s successfully registered", channelName))
97
+
98
+			server.logger.Info("chanserv", fmt.Sprintf("Client %s registered channel %s", client.nick, channelName))
99
+
100
+			channelInfo.membersMutex.Lock()
101
+			defer channelInfo.membersMutex.Unlock()
102
+
103
+			// give them founder privs
104
+			change := channelInfo.applyModeMemberNoMutex(client, ChannelFounder, Add, client.nickCasefolded)
105
+			if change != nil {
106
+				//TODO(dan): we should change the name of String and make it return a slice here
107
+				//TODO(dan): unify this code with code in modes.go
108
+				args := append([]string{channelName}, strings.Split(change.String(), " ")...)
109
+				for member := range channelInfo.members {
110
+					member.Send(nil, fmt.Sprintf("ChanServ!services@%s", client.server.name), "MODE", args...)
111
+				}
112
+			}
113
+
114
+			return nil
115
+		})
116
+	} else {
117
+		client.ChanServNotice("Sorry, I don't know that command")
118
+	}
119
+}

+ 16
- 0
irc/commands.go Visa fil

74
 		usablePreReg: true,
74
 		usablePreReg: true,
75
 		minParams:    1,
75
 		minParams:    1,
76
 	},
76
 	},
77
+	"CHANSERV": {
78
+		handler:   csHandler,
79
+		minParams: 1,
80
+	},
81
+	"CS": {
82
+		handler:   csHandler,
83
+		minParams: 1,
84
+	},
77
 	"DEBUG": {
85
 	"DEBUG": {
78
 		handler:   debugHandler,
86
 		handler:   debugHandler,
79
 		minParams: 1,
87
 		minParams: 1,
143
 		usablePreReg: true,
151
 		usablePreReg: true,
144
 		minParams:    1,
152
 		minParams:    1,
145
 	},
153
 	},
154
+	"NICKSERV": {
155
+		handler:   nsHandler,
156
+		minParams: 1,
157
+	},
146
 	"NOTICE": {
158
 	"NOTICE": {
147
 		handler:   noticeHandler,
159
 		handler:   noticeHandler,
148
 		minParams: 2,
160
 		minParams: 2,
155
 		handler:   npcaHandler,
167
 		handler:   npcaHandler,
156
 		minParams: 3,
168
 		minParams: 3,
157
 	},
169
 	},
170
+	"NS": {
171
+		handler:   nsHandler,
172
+		minParams: 1,
173
+	},
158
 	"OPER": {
174
 	"OPER": {
159
 		handler:   operHandler,
175
 		handler:   operHandler,
160
 		minParams: 2,
176
 		minParams: 2,

+ 20
- 0
irc/help.go Visa fil

84
 Used in capability negotiation. See the IRCv3 specs for more info:
84
 Used in capability negotiation. See the IRCv3 specs for more info:
85
 http://ircv3.net/specs/core/capability-negotiation-3.1.html
85
 http://ircv3.net/specs/core/capability-negotiation-3.1.html
86
 http://ircv3.net/specs/core/capability-negotiation-3.2.html`,
86
 http://ircv3.net/specs/core/capability-negotiation-3.2.html`,
87
+	},
88
+	"chanserv": {
89
+		text: `CHANSERV <subcommand> [params]
90
+
91
+ChanServ controls channel registrations.`,
92
+	},
93
+	"cs": {
94
+		text: `CS <subcommand> [params]
95
+
96
+ChanServ controls channel registrations.`,
87
 	},
97
 	},
88
 	"debug": {
98
 	"debug": {
89
 		oper: true,
99
 		oper: true,
239
 		text: `NICK <newnick>
249
 		text: `NICK <newnick>
240
 
250
 
241
 Sets your nickname to the new given one.`,
251
 Sets your nickname to the new given one.`,
252
+	},
253
+	"nickserv": {
254
+		text: `NICKSERV <subcommand> [params]
255
+
256
+NickServ controls accounts and user registrations.`,
242
 	},
257
 	},
243
 	"notice": {
258
 	"notice": {
244
 		text: `NOTICE <target>{,<target>} <text to be sent>
259
 		text: `NOTICE <target>{,<target>} <text to be sent>
258
 The NPC command is used to send an action to the target as the source.
273
 The NPC command is used to send an action to the target as the source.
259
 
274
 
260
 Requires the roleplay mode (+E) to be set on the target.`,
275
 Requires the roleplay mode (+E) to be set on the target.`,
276
+	},
277
+	"ns": {
278
+		text: `NS <subcommand> [params]
279
+
280
+NickServ controls accounts and user registrations.`,
261
 	},
281
 	},
262
 	"oper": {
282
 	"oper": {
263
 		text: `OPER <name> <password>
283
 		text: `OPER <name> <password>

+ 10
- 2
irc/nickname.go Visa fil

11
 	"github.com/DanielOaks/girc-go/ircmsg"
11
 	"github.com/DanielOaks/girc-go/ircmsg"
12
 )
12
 )
13
 
13
 
14
+var (
15
+	restrictedNicknames = map[string]bool{
16
+		"=scene=":  true, // used for rp commands
17
+		"chanserv": true,
18
+		"nickserv": true,
19
+	}
20
+)
21
+
14
 // NICK <nickname>
22
 // NICK <nickname>
15
 func nickHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
23
 func nickHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
16
 	if !client.authorized {
24
 	if !client.authorized {
26
 		return false
34
 		return false
27
 	}
35
 	}
28
 
36
 
29
-	if err != nil || len(nicknameRaw) > server.limits.NickLen || nickname == "=scene=" {
37
+	if err != nil || len(nicknameRaw) > server.limits.NickLen || restrictedNicknames[nickname] {
30
 		client.Send(nil, server.name, ERR_ERRONEUSNICKNAME, client.nick, nicknameRaw, "Erroneous nickname")
38
 		client.Send(nil, server.name, ERR_ERRONEUSNICKNAME, client.nick, nicknameRaw, "Erroneous nickname")
31
 		return false
39
 		return false
32
 	}
40
 	}
70
 		return false
78
 		return false
71
 	}
79
 	}
72
 
80
 
73
-	if oerr != nil || err != nil || len(strings.TrimSpace(msg.Params[1])) > server.limits.NickLen || nickname == "=scene=" {
81
+	if oerr != nil || err != nil || len(strings.TrimSpace(msg.Params[1])) > server.limits.NickLen || restrictedNicknames[nickname] {
74
 		client.Send(nil, server.name, ERR_ERRONEUSNICKNAME, client.nick, msg.Params[0], "Erroneous nickname")
82
 		client.Send(nil, server.name, ERR_ERRONEUSNICKNAME, client.nick, msg.Params[0], "Erroneous nickname")
75
 		return false
83
 		return false
76
 	}
84
 	}

+ 24
- 0
irc/nickserv.go Visa fil

1
+// Copyright (c) 2017 Daniel Oaks <daniel@danieloaks.net>
2
+// released under the MIT license
3
+
4
+package irc
5
+
6
+import (
7
+	"strings"
8
+
9
+	"github.com/DanielOaks/girc-go/ircmsg"
10
+)
11
+
12
+// nsHandler handles the /NS and /NICKSERV commands
13
+func nsHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
14
+	server.nickservReceivePrivmsg(client, strings.Join(msg.Params, " "))
15
+	return false
16
+}
17
+
18
+func (server *Server) nickservReceiveNotice(client *Client, message string) {
19
+	// do nothing
20
+}
21
+
22
+func (server *Server) nickservReceivePrivmsg(client *Client, message string) {
23
+	client.Notice("NickServ is not yet implemented, sorry!")
24
+}

+ 33
- 16
irc/server.go Visa fil

114
 	operclasses                  map[string]OperClass
114
 	operclasses                  map[string]OperClass
115
 	password                     []byte
115
 	password                     []byte
116
 	passwords                    *PasswordManager
116
 	passwords                    *PasswordManager
117
+	registeredChannels           map[string]*RegisteredChannel
118
+	registeredChannelsMutex      sync.RWMutex
117
 	rehashMutex                  sync.Mutex
119
 	rehashMutex                  sync.Mutex
118
 	rehashSignal                 chan os.Signal
120
 	rehashSignal                 chan os.Signal
119
 	restAPI                      *RestAPIConfig
121
 	restAPI                      *RestAPIConfig
185
 	}
187
 	}
186
 
188
 
187
 	server := &Server{
189
 	server := &Server{
188
-		accounts:                     make(map[string]*ClientAccount),
189
 		accountAuthenticationEnabled: config.Accounts.AuthenticationEnabled,
190
 		accountAuthenticationEnabled: config.Accounts.AuthenticationEnabled,
191
+		accounts:                     make(map[string]*ClientAccount),
190
 		channels:                     make(ChannelNameMap),
192
 		channels:                     make(ChannelNameMap),
193
+		checkIdent:                   config.Server.CheckIdent,
191
 		clients:                      NewClientLookupSet(),
194
 		clients:                      NewClientLookupSet(),
192
 		commands:                     make(chan Command),
195
 		commands:                     make(chan Command),
193
 		configFilename:               configFilename,
196
 		configFilename:               configFilename,
209
 				Rest: config.Limits.LineLen.Rest,
212
 				Rest: config.Limits.LineLen.Rest,
210
 			},
213
 			},
211
 		},
214
 		},
212
-		listeners:      make(map[string]ListenerInterface),
213
-		logger:         logger,
214
-		monitoring:     make(map[string][]Client),
215
-		name:           config.Server.Name,
216
-		nameCasefolded: casefoldedName,
217
-		networkName:    config.Network.Name,
218
-		newConns:       make(chan clientConn),
219
-		operclasses:    *operClasses,
220
-		operators:      opers,
221
-		signals:        make(chan os.Signal, len(ServerExitSignals)),
222
-		stsEnabled:     config.Server.STS.Enabled,
223
-		rehashSignal:   make(chan os.Signal, 1),
224
-		restAPI:        &config.Server.RestAPI,
225
-		whoWas:         NewWhoWasList(config.Limits.WhowasEntries),
226
-		checkIdent:     config.Server.CheckIdent,
215
+		listeners:          make(map[string]ListenerInterface),
216
+		logger:             logger,
217
+		monitoring:         make(map[string][]Client),
218
+		name:               config.Server.Name,
219
+		nameCasefolded:     casefoldedName,
220
+		networkName:        config.Network.Name,
221
+		newConns:           make(chan clientConn),
222
+		operators:          opers,
223
+		operclasses:        *operClasses,
224
+		registeredChannels: make(map[string]*RegisteredChannel),
225
+		rehashSignal:       make(chan os.Signal, 1),
226
+		restAPI:            &config.Server.RestAPI,
227
+		signals:            make(chan os.Signal, len(ServerExitSignals)),
228
+		stsEnabled:         config.Server.STS.Enabled,
229
+		whoWas:             NewWhoWasList(config.Limits.WhowasEntries),
227
 	}
230
 	}
228
 
231
 
229
 	// open data store
232
 	// open data store
949
 			channel.SplitPrivMsg(msgid, lowestPrefix, clientOnlyTags, client, splitMsg)
952
 			channel.SplitPrivMsg(msgid, lowestPrefix, clientOnlyTags, client, splitMsg)
950
 		} else {
953
 		} else {
951
 			target, err = CasefoldName(targetString)
954
 			target, err = CasefoldName(targetString)
955
+			if target == "chanserv" {
956
+				server.chanservReceivePrivmsg(client, message)
957
+				continue
958
+			} else if target == "nickserv" {
959
+				server.nickservReceivePrivmsg(client, message)
960
+				continue
961
+			}
952
 			user := server.clients.Get(target)
962
 			user := server.clients.Get(target)
953
 			if err != nil || user == nil {
963
 			if err != nil || user == nil {
954
 				if len(target) > 0 {
964
 				if len(target) > 0 {
1593
 			if err != nil {
1603
 			if err != nil {
1594
 				continue
1604
 				continue
1595
 			}
1605
 			}
1606
+			if target == "chanserv" {
1607
+				server.chanservReceiveNotice(client, message)
1608
+				continue
1609
+			} else if target == "nickserv" {
1610
+				server.nickservReceiveNotice(client, message)
1611
+				continue
1612
+			}
1596
 
1613
 
1597
 			user := server.clients.Get(target)
1614
 			user := server.clients.Get(target)
1598
 			if user == nil {
1615
 			if user == nil {

Laddar…
Avbryt
Spara