Pārlūkot izejas kodu

nickserv: implement GHOST, GROUP, DROP, and INFO

tags/v0.11.0-beta
Shivaram Lingamneni 6 gadus atpakaļ
vecāks
revīzija
a022befffe
6 mainītis faili ar 308 papildinājumiem un 84 dzēšanām
  1. 174
    70
      irc/accounts.go
  2. 5
    4
      irc/config.go
  3. 4
    0
      irc/errors.go
  4. 1
    8
      irc/handlers.go
  5. 121
    2
      irc/nickserv.go
  6. 3
    0
      oragono.yaml

+ 174
- 70
irc/accounts.go Parādīt failu

@@ -29,6 +29,7 @@ const (
29 29
 	keyAccountName             = "account.name %s" // stores the 'preferred name' of the account, not casemapped
30 30
 	keyAccountRegTime          = "account.registered.time %s"
31 31
 	keyAccountCredentials      = "account.credentials %s"
32
+	keyAccountAdditionalNicks  = "account.additionalnicks %s"
32 33
 	keyCertToAccount           = "account.creds.certfp %s"
33 34
 )
34 35
 
@@ -75,6 +76,12 @@ func (am *AccountManager) buildNickToAccountIndex() {
75 76
 			if _, err := tx.Get(fmt.Sprintf(keyAccountVerified, accountName)); err == nil {
76 77
 				result[accountName] = accountName
77 78
 			}
79
+			if rawNicks, err := tx.Get(fmt.Sprintf(keyAccountAdditionalNicks, accountName)); err == nil {
80
+				additionalNicks := unmarshalReservedNicks(rawNicks)
81
+				for _, nick := range additionalNicks {
82
+					result[nick] = accountName
83
+				}
84
+			}
78 85
 			return true
79 86
 		})
80 87
 		return err
@@ -91,7 +98,12 @@ func (am *AccountManager) buildNickToAccountIndex() {
91 98
 	return
92 99
 }
93 100
 
94
-func (am *AccountManager) NickToAccount(cfnick string) string {
101
+func (am *AccountManager) NickToAccount(nick string) string {
102
+	cfnick, err := CasefoldName(nick)
103
+	if err != nil {
104
+		return ""
105
+	}
106
+
95 107
 	am.RLock()
96 108
 	defer am.RUnlock()
97 109
 	return am.nickToAccount[cfnick]
@@ -325,13 +337,92 @@ func (am *AccountManager) Verify(client *Client, account string, code string) er
325 337
 	return nil
326 338
 }
327 339
 
328
-func (am *AccountManager) AuthenticateByPassphrase(client *Client, accountName string, passphrase string) error {
329
-	casefoldedAccount, err := CasefoldName(accountName)
340
+func marshalReservedNicks(nicks []string) string {
341
+	return strings.Join(nicks, ",")
342
+}
343
+
344
+func unmarshalReservedNicks(nicks string) (result []string) {
345
+	if nicks == "" {
346
+		return
347
+	}
348
+	return strings.Split(nicks, ",")
349
+}
350
+
351
+func (am *AccountManager) SetNickReserved(client *Client, nick string, reserve bool) error {
352
+	cfnick, err := CasefoldName(nick)
330 353
 	if err != nil {
331
-		return errAccountDoesNotExist
354
+		return errAccountNickReservationFailed
355
+	}
356
+
357
+	// sanity check so we don't persist bad data
358
+	account := client.Account()
359
+	if account == "" || cfnick == "" || !am.server.AccountConfig().NickReservation.Enabled {
360
+		return errAccountNickReservationFailed
332 361
 	}
333 362
 
334
-	account, err := am.LoadAccount(casefoldedAccount)
363
+	limit := am.server.AccountConfig().NickReservation.AdditionalNickLimit
364
+
365
+	am.serialCacheUpdateMutex.Lock()
366
+	defer am.serialCacheUpdateMutex.Unlock()
367
+
368
+	// the cache is in sync with the DB while we hold serialCacheUpdateMutex
369
+	accountForNick := am.NickToAccount(cfnick)
370
+	if reserve && accountForNick != "" {
371
+		return errNicknameReserved
372
+	} else if !reserve && accountForNick != account {
373
+		return errAccountNickReservationFailed
374
+	} else if !reserve && cfnick == account {
375
+		return errAccountCantDropPrimaryNick
376
+	}
377
+
378
+	nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, account)
379
+	err = am.server.store.Update(func(tx *buntdb.Tx) error {
380
+		rawNicks, err := tx.Get(nicksKey)
381
+		if err != nil && err != buntdb.ErrNotFound {
382
+			return err
383
+		}
384
+
385
+		nicks := unmarshalReservedNicks(rawNicks)
386
+
387
+		if reserve {
388
+			if len(nicks) >= limit {
389
+				return errAccountTooManyNicks
390
+			}
391
+			nicks = append(nicks, cfnick)
392
+		} else {
393
+			var newNicks []string
394
+			for _, reservedNick := range nicks {
395
+				if reservedNick != cfnick {
396
+					newNicks = append(newNicks, reservedNick)
397
+				}
398
+			}
399
+			nicks = newNicks
400
+		}
401
+
402
+		marshaledNicks := marshalReservedNicks(nicks)
403
+		_, _, err = tx.Set(nicksKey, string(marshaledNicks), nil)
404
+		return err
405
+	})
406
+
407
+	if err == errAccountTooManyNicks {
408
+		return err
409
+	} else if err != nil {
410
+		return errAccountNickReservationFailed
411
+	}
412
+
413
+	// success
414
+	am.Lock()
415
+	defer am.Unlock()
416
+	if reserve {
417
+		am.nickToAccount[cfnick] = account
418
+	} else {
419
+		delete(am.nickToAccount, cfnick)
420
+	}
421
+	return nil
422
+}
423
+
424
+func (am *AccountManager) AuthenticateByPassphrase(client *Client, accountName string, passphrase string) error {
425
+	account, err := am.LoadAccount(accountName)
335 426
 	if err != nil {
336 427
 		return err
337 428
 	}
@@ -350,7 +441,13 @@ func (am *AccountManager) AuthenticateByPassphrase(client *Client, accountName s
350 441
 	return nil
351 442
 }
352 443
 
353
-func (am *AccountManager) LoadAccount(casefoldedAccount string) (result ClientAccount, err error) {
444
+func (am *AccountManager) LoadAccount(accountName string) (result ClientAccount, err error) {
445
+	casefoldedAccount, err := CasefoldName(accountName)
446
+	if err != nil {
447
+		err = errAccountDoesNotExist
448
+		return
449
+	}
450
+
354 451
 	var raw rawClientAccount
355 452
 	am.server.store.View(func(tx *buntdb.Tx) error {
356 453
 		raw, err = am.loadRawAccount(tx, casefoldedAccount)
@@ -369,6 +466,7 @@ func (am *AccountManager) LoadAccount(casefoldedAccount string) (result ClientAc
369 466
 		err = errAccountDoesNotExist
370 467
 		return
371 468
 	}
469
+	result.AdditionalNicks = unmarshalReservedNicks(raw.AdditionalNicks)
372 470
 	result.Verified = raw.Verified
373 471
 	return
374 472
 }
@@ -380,6 +478,7 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string
380 478
 	credentialsKey := fmt.Sprintf(keyAccountCredentials, casefoldedAccount)
381 479
 	verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
382 480
 	callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
481
+	nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
383 482
 
384 483
 	_, e := tx.Get(accountKey)
385 484
 	if e == buntdb.ErrNotFound {
@@ -391,6 +490,7 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string
391 490
 	result.RegisteredAt, _ = tx.Get(registeredTimeKey)
392 491
 	result.Credentials, _ = tx.Get(credentialsKey)
393 492
 	result.Callback, _ = tx.Get(callbackKey)
493
+	result.AdditionalNicks, _ = tx.Get(nicksKey)
394 494
 
395 495
 	if _, e = tx.Get(verifiedKey); e == nil {
396 496
 		result.Verified = true
@@ -412,51 +512,56 @@ func (am *AccountManager) Unregister(account string) error {
412 512
 	callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
413 513
 	verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
414 514
 	verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
515
+	nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
415 516
 
416 517
 	var clients []*Client
417 518
 
418
-	func() {
419
-		var credText string
519
+	var credText string
520
+	var rawNicks string
420 521
 
421
-		am.serialCacheUpdateMutex.Lock()
422
-		defer am.serialCacheUpdateMutex.Unlock()
522
+	am.serialCacheUpdateMutex.Lock()
523
+	defer am.serialCacheUpdateMutex.Unlock()
423 524
 
424
-		am.server.store.Update(func(tx *buntdb.Tx) error {
425
-			tx.Delete(accountKey)
426
-			tx.Delete(accountNameKey)
427
-			tx.Delete(verifiedKey)
428
-			tx.Delete(registeredTimeKey)
429
-			tx.Delete(callbackKey)
430
-			tx.Delete(verificationCodeKey)
431
-			credText, err = tx.Get(credentialsKey)
432
-			tx.Delete(credentialsKey)
433
-			return nil
434
-		})
525
+	am.server.store.Update(func(tx *buntdb.Tx) error {
526
+		tx.Delete(accountKey)
527
+		tx.Delete(accountNameKey)
528
+		tx.Delete(verifiedKey)
529
+		tx.Delete(registeredTimeKey)
530
+		tx.Delete(callbackKey)
531
+		tx.Delete(verificationCodeKey)
532
+		rawNicks, _ = tx.Get(nicksKey)
533
+		tx.Delete(nicksKey)
534
+		credText, err = tx.Get(credentialsKey)
535
+		tx.Delete(credentialsKey)
536
+		return nil
537
+	})
435 538
 
436
-		if err == nil {
437
-			var creds AccountCredentials
438
-			if err = json.Unmarshal([]byte(credText), &creds); err == nil && creds.Certificate != "" {
439
-				certFPKey := fmt.Sprintf(keyCertToAccount, creds.Certificate)
440
-				am.server.store.Update(func(tx *buntdb.Tx) error {
441
-					if account, err := tx.Get(certFPKey); err == nil && account == casefoldedAccount {
442
-						tx.Delete(certFPKey)
443
-					}
444
-					return nil
445
-				})
446
-			}
539
+	if err == nil {
540
+		var creds AccountCredentials
541
+		if err = json.Unmarshal([]byte(credText), &creds); err == nil && creds.Certificate != "" {
542
+			certFPKey := fmt.Sprintf(keyCertToAccount, creds.Certificate)
543
+			am.server.store.Update(func(tx *buntdb.Tx) error {
544
+				if account, err := tx.Get(certFPKey); err == nil && account == casefoldedAccount {
545
+					tx.Delete(certFPKey)
546
+				}
547
+				return nil
548
+			})
447 549
 		}
550
+	}
448 551
 
449
-		am.Lock()
450
-		defer am.Unlock()
451
-		clients = am.accountToClients[casefoldedAccount]
452
-		delete(am.accountToClients, casefoldedAccount)
453
-		// TODO when registration of multiple nicks is fully implemented,
454
-		// save the nicks that were deleted from the store and delete them here:
455
-		delete(am.nickToAccount, casefoldedAccount)
456
-	}()
552
+	additionalNicks := unmarshalReservedNicks(rawNicks)
457 553
 
554
+	am.Lock()
555
+	defer am.Unlock()
556
+
557
+	clients = am.accountToClients[casefoldedAccount]
558
+	delete(am.accountToClients, casefoldedAccount)
559
+	delete(am.nickToAccount, casefoldedAccount)
560
+	for _, nick := range additionalNicks {
561
+		delete(am.nickToAccount, nick)
562
+	}
458 563
 	for _, client := range clients {
459
-		client.LogoutOfAccount()
564
+		am.logoutOfAccount(client)
460 565
 	}
461 566
 
462 567
 	if err != nil {
@@ -498,29 +603,25 @@ func (am *AccountManager) AuthenticateByCertFP(client *Client) error {
498 603
 }
499 604
 
500 605
 func (am *AccountManager) Login(client *Client, account string) {
501
-	client.LoginToAccount(account)
502
-
503
-	casefoldedAccount, _ := CasefoldName(account)
504 606
 	am.Lock()
505 607
 	defer am.Unlock()
608
+
609
+	am.loginToAccount(client, account)
610
+	casefoldedAccount := client.Account()
506 611
 	am.accountToClients[casefoldedAccount] = append(am.accountToClients[casefoldedAccount], client)
507 612
 }
508 613
 
509 614
 func (am *AccountManager) Logout(client *Client) {
510
-	casefoldedAccount := client.Account()
511
-	if casefoldedAccount == "" || casefoldedAccount == "*" {
512
-		return
513
-	}
514
-
515
-	client.LogoutOfAccount()
516
-
517 615
 	am.Lock()
518 616
 	defer am.Unlock()
519 617
 
520
-	if client.LoggedIntoAccount() {
618
+	casefoldedAccount := client.Account()
619
+	if casefoldedAccount == "" {
521 620
 		return
522 621
 	}
523 622
 
623
+	am.logoutOfAccount(client)
624
+
524 625
 	clients := am.accountToClients[casefoldedAccount]
525 626
 	if len(clients) <= 1 {
526 627
 		delete(am.accountToClients, casefoldedAccount)
@@ -559,42 +660,45 @@ type ClientAccount struct {
559 660
 	// Name of the account.
560 661
 	Name string
561 662
 	// RegisteredAt represents the time that the account was registered.
562
-	RegisteredAt time.Time
563
-	Credentials  AccountCredentials
564
-	Verified     bool
663
+	RegisteredAt    time.Time
664
+	Credentials     AccountCredentials
665
+	Verified        bool
666
+	AdditionalNicks []string
565 667
 }
566 668
 
567 669
 // convenience for passing around raw serialized account data
568 670
 type rawClientAccount struct {
569
-	Name         string
570
-	RegisteredAt string
571
-	Credentials  string
572
-	Callback     string
573
-	Verified     bool
671
+	Name            string
672
+	RegisteredAt    string
673
+	Credentials     string
674
+	Callback        string
675
+	Verified        bool
676
+	AdditionalNicks string
574 677
 }
575 678
 
576
-// LoginToAccount logs the client into the given account.
577
-func (client *Client) LoginToAccount(account string) {
679
+// loginToAccount logs the client into the given account.
680
+func (am *AccountManager) loginToAccount(client *Client, account string) {
578 681
 	changed := client.SetAccountName(account)
579 682
 	if changed {
580
-		client.nickTimer.Touch()
683
+		go client.nickTimer.Touch()
581 684
 	}
582 685
 }
583 686
 
584
-// LogoutOfAccount logs the client out of their current account.
585
-func (client *Client) LogoutOfAccount() {
687
+// logoutOfAccount logs the client out of their current account.
688
+func (am *AccountManager) logoutOfAccount(client *Client) {
586 689
 	if client.Account() == "" {
587 690
 		// already logged out
588 691
 		return
589 692
 	}
590 693
 
591 694
 	client.SetAccountName("")
592
-	client.nickTimer.Touch()
695
+	go client.nickTimer.Touch()
593 696
 
594 697
 	// dispatch account-notify
595 698
 	// TODO: doing the I/O here is kind of a kludge, let's move this somewhere else
596
-	for friend := range client.Friends(caps.AccountNotify) {
597
-		friend.Send(nil, client.nickMaskString, "ACCOUNT", "*")
598
-	}
699
+	go func() {
700
+		for friend := range client.Friends(caps.AccountNotify) {
701
+			friend.Send(nil, client.NickMaskString(), "ACCOUNT", "*")
702
+		}
703
+	}()
599 704
 }
600
-

+ 5
- 4
irc/config.go Parādīt failu

@@ -117,10 +117,11 @@ func (nr *NickReservationMethod) UnmarshalYAML(unmarshal func(interface{}) error
117 117
 }
118 118
 
119 119
 type NickReservationConfig struct {
120
-	Enabled       bool
121
-	Method        NickReservationMethod
122
-	RenameTimeout time.Duration `yaml:"rename-timeout"`
123
-	RenamePrefix  string        `yaml:"rename-prefix"`
120
+	Enabled             bool
121
+	AdditionalNickLimit int `yaml:"additional-nick-limit"`
122
+	Method              NickReservationMethod
123
+	RenameTimeout       time.Duration `yaml:"rename-timeout"`
124
+	RenamePrefix        string        `yaml:"rename-prefix"`
124 125
 }
125 126
 
126 127
 // ChannelRegistrationConfig controls channel registration.

+ 4
- 0
irc/errors.go Parādīt failu

@@ -12,11 +12,15 @@ var (
12 12
 	errAccountAlreadyRegistered       = errors.New("Account already exists")
13 13
 	errAccountCreation                = errors.New("Account could not be created")
14 14
 	errAccountDoesNotExist            = errors.New("Account does not exist")
15
+	errAccountNotLoggedIn             = errors.New("You're not logged into an account")
15 16
 	errAccountVerificationFailed      = errors.New("Account verification failed")
16 17
 	errAccountVerificationInvalidCode = errors.New("Invalid account verification code")
17 18
 	errAccountUnverified              = errors.New("Account is not yet verified")
18 19
 	errAccountAlreadyVerified         = errors.New("Account is already verified")
19 20
 	errAccountInvalidCredentials      = errors.New("Invalid account credentials")
21
+	errAccountTooManyNicks            = errors.New("Account has too many reserved nicks")
22
+	errAccountNickReservationFailed   = errors.New("Could not (un)reserve nick")
23
+	errAccountCantDropPrimaryNick     = errors.New("Can't unreserve primary nickname")
20 24
 	errCallbackFailed                 = errors.New("Account verification could not be sent")
21 25
 	errCertfpAlreadyExists            = errors.New("An account already exists with your certificate")
22 26
 	errChannelAlreadyRegistered       = errors.New("Channel is already registered")

+ 1
- 8
irc/handlers.go Parādīt failu

@@ -352,15 +352,8 @@ func authPlainHandler(server *Server, client *Client, mechanism string, value []
352 352
 		return false
353 353
 	}
354 354
 
355
-	// keep it the same as in the REG CREATE stage
356
-	accountKey, err := CasefoldName(accountKey)
357
-	if err != nil {
358
-		rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, client.t("SASL authentication failed: Bad account name"))
359
-		return false
360
-	}
361
-
362 355
 	password := string(splitValue[2])
363
-	err = server.accounts.AuthenticateByPassphrase(client, accountKey, password)
356
+	err := server.accounts.AuthenticateByPassphrase(client, accountKey, password)
364 357
 	if err != nil {
365 358
 		msg := authErrorToMessage(server, err)
366 359
 		rb.Add(nil, server.name, ERR_SASLFAIL, client.nick, fmt.Sprintf("%s: %s", client.t("SASL authentication failed"), client.t(msg)))

+ 121
- 2
irc/nickserv.go Parādīt failu

@@ -28,7 +28,18 @@ Leave out [username] if you're unregistering the user you're currently logged in
28 28
 To login to an account:
29 29
 	/NS IDENTIFY [username password]
30 30
 Leave out [username password] to use your client certificate fingerprint. Otherwise,
31
-the given username and password will be used.`
31
+the given username and password will be used.
32
+
33
+To see account information:
34
+	/NS INFO [username]
35
+Leave out [username] to see your own account information.
36
+
37
+To associate your current nick with the account you're logged into:
38
+	/NS GROUP
39
+
40
+To disassociate a nick with the account you're logged into:
41
+	/NS DROP [nickname]
42
+Leave out [nickname] to drop your association with your current nickname.`
32 43
 
33 44
 // extractParam extracts a parameter from the given string, returning the param and the rest of the string.
34 45
 func extractParam(line string) (string, string) {
@@ -69,6 +80,17 @@ func (server *Server) nickservPrivmsgHandler(client *Client, message string, rb
69 80
 	} else if command == "unregister" {
70 81
 		username, _ := extractParam(params)
71 82
 		server.nickservUnregisterHandler(client, username, rb)
83
+	} else if command == "ghost" {
84
+		nick, _ := extractParam(params)
85
+		server.nickservGhostHandler(client, nick, rb)
86
+	} else if command == "info" {
87
+		nick, _ := extractParam(params)
88
+		server.nickservInfoHandler(client, nick, rb)
89
+	} else if command == "group" {
90
+		server.nickservGroupHandler(client, rb)
91
+	} else if command == "drop" {
92
+		nick, _ := extractParam(params)
93
+		server.nickservDropHandler(client, nick, rb)
72 94
 	} else {
73 95
 		rb.Notice(client.t("Command not recognised. To see the available commands, run /NS HELP"))
74 96
 	}
@@ -158,7 +180,7 @@ func (server *Server) nickservRegisterHandler(client *Client, username, email, p
158 180
 	config := server.AccountConfig()
159 181
 	var callbackNamespace, callbackValue string
160 182
 	noneCallbackAllowed := false
161
-	for _, callback := range(config.Registration.EnabledCallbacks) {
183
+	for _, callback := range config.Registration.EnabledCallbacks {
162 184
 		if callback == "*" {
163 185
 			noneCallbackAllowed = true
164 186
 		}
@@ -233,3 +255,100 @@ func (server *Server) nickservIdentifyHandler(client *Client, username, passphra
233 255
 		rb.Notice(client.t("Could not login with your TLS certificate or supplied username/password"))
234 256
 	}
235 257
 }
258
+
259
+func (server *Server) nickservGhostHandler(client *Client, nick string, rb *ResponseBuffer) {
260
+	if !server.AccountConfig().NickReservation.Enabled {
261
+		rb.Notice(client.t("Nickname reservation is disabled"))
262
+		return
263
+	}
264
+
265
+	account := client.Account()
266
+	if account == "" || server.accounts.NickToAccount(nick) != account {
267
+		rb.Notice(client.t("You don't own that nick"))
268
+		return
269
+	}
270
+
271
+	ghost := server.clients.Get(nick)
272
+	if ghost == nil {
273
+		rb.Notice(client.t("No such nick"))
274
+		return
275
+	} else if ghost == client {
276
+		rb.Notice(client.t("You can't GHOST yourself (try /QUIT instead)"))
277
+		return
278
+	}
279
+
280
+	ghost.Quit(fmt.Sprintf(ghost.t("GHOSTed by %s"), client.Nick()))
281
+	ghost.destroy(false)
282
+}
283
+
284
+func (server *Server) nickservGroupHandler(client *Client, rb *ResponseBuffer) {
285
+	if !server.AccountConfig().NickReservation.Enabled {
286
+		rb.Notice(client.t("Nickname reservation is disabled"))
287
+		return
288
+	}
289
+
290
+	account := client.Account()
291
+	if account == "" {
292
+		rb.Notice(client.t("You're not logged into an account"))
293
+		return
294
+	}
295
+
296
+	nick := client.NickCasefolded()
297
+	err := server.accounts.SetNickReserved(client, nick, true)
298
+	if err == nil {
299
+		rb.Notice(fmt.Sprintf(client.t("Successfully grouped nick %s with your account"), nick))
300
+	} else if err == errAccountTooManyNicks {
301
+		rb.Notice(client.t("You have too many nicks reserved already (you can remove some with /NS DROP)"))
302
+	} else if err == errNicknameReserved {
303
+		rb.Notice(client.t("That nickname is already reserved"))
304
+	} else {
305
+		rb.Notice(client.t("Error reserving nickname"))
306
+	}
307
+}
308
+
309
+func (server *Server) nickservInfoHandler(client *Client, nick string, rb *ResponseBuffer) {
310
+	if nick == "" {
311
+		nick = client.Nick()
312
+	}
313
+
314
+	accountName := nick
315
+	if server.AccountConfig().NickReservation.Enabled {
316
+		accountName = server.accounts.NickToAccount(nick)
317
+		if accountName == "" {
318
+			rb.Notice(client.t("That nickname is not registered"))
319
+			return
320
+		}
321
+	}
322
+
323
+	account, err := server.accounts.LoadAccount(accountName)
324
+	if err != nil || !account.Verified {
325
+		rb.Notice(client.t("Account does not exist"))
326
+	}
327
+
328
+	rb.Notice(fmt.Sprintf(client.t("Account: %s"), account.Name))
329
+	registeredAt := account.RegisteredAt.Format("Jan 02, 2006 15:04:05Z")
330
+	rb.Notice(fmt.Sprintf(client.t("Registered at: %s"), registeredAt))
331
+	// TODO nicer formatting for this
332
+	for _, nick := range account.AdditionalNicks {
333
+		rb.Notice(fmt.Sprintf(client.t("Additional grouped nick: %s"), nick))
334
+	}
335
+}
336
+
337
+func (server *Server) nickservDropHandler(client *Client, nick string, rb *ResponseBuffer) {
338
+	account := client.Account()
339
+	if account == "" {
340
+		rb.Notice(client.t("You're not logged into an account"))
341
+		return
342
+	}
343
+
344
+	err := server.accounts.SetNickReserved(client, nick, false)
345
+	if err == nil {
346
+		rb.Notice(fmt.Sprintf(client.t("Successfully ungrouped nick %s with your account"), nick))
347
+	} else if err == errAccountCantDropPrimaryNick {
348
+		rb.Notice(fmt.Sprintf(client.t("You can't ungroup your primary nickname (try unregistering your account instead)")))
349
+	} else if err == errAccountNickReservationFailed {
350
+		rb.Notice(fmt.Sprintf(client.t("You don't own that nick")))
351
+	} else {
352
+		rb.Notice(client.t("Error ungrouping nick"))
353
+	}
354
+}

+ 3
- 0
oragono.yaml Parādīt failu

@@ -182,6 +182,9 @@ accounts:
182 182
         # is there any enforcement of reserved nicknames?
183 183
         enabled: false
184 184
 
185
+        # how many nicknames, in addition to the account name, can be reserved?
186
+        additional-nick-limit: 2
187
+
185 188
         # method describes how nickname reservation is handled
186 189
         #   timeout: let the user change to the registered nickname, give them X seconds
187 190
         #            to login and then rename them if they haven't done so

Notiek ielāde…
Atcelt
Saglabāt