Browse Source

implement user preferences system

tags/v1.1.0-rc1
Shivaram Lingamneni 5 years ago
parent
commit
8fc588375b
16 changed files with 515 additions and 167 deletions
  1. 131
    82
      irc/accounts.go
  2. 0
    6
      irc/caps/set.go
  3. 17
    6
      irc/channel.go
  4. 1
    1
      irc/chanserv.go
  5. 1
    0
      irc/client.go
  6. 15
    2
      irc/client_lookup_set.go
  7. 27
    27
      irc/config.go
  8. 37
    1
      irc/database.go
  9. 2
    2
      irc/errors.go
  10. 13
    0
      irc/getters.go
  11. 2
    2
      irc/handlers.go
  12. 1
    1
      irc/hostserv.go
  13. 3
    3
      irc/idletimer.go
  14. 256
    29
      irc/nickserv.go
  15. 8
    4
      irc/services.go
  16. 1
    1
      oragono.yaml

+ 131
- 82
irc/accounts.go View File

@@ -29,7 +29,7 @@ const (
29 29
 	keyAccountRegTime          = "account.registered.time %s"
30 30
 	keyAccountCredentials      = "account.credentials %s"
31 31
 	keyAccountAdditionalNicks  = "account.additionalnicks %s"
32
-	keyAccountEnforcement      = "account.customenforcement %s"
32
+	keyAccountSettings         = "account.settings %s"
33 33
 	keyAccountVHost            = "account.vhost %s"
34 34
 	keyCertToAccount           = "account.creds.certfp %s"
35 35
 	keyAccountChannels         = "account.channels %s"
@@ -55,14 +55,14 @@ type AccountManager struct {
55 55
 	accountToClients  map[string][]*Client
56 56
 	nickToAccount     map[string]string
57 57
 	skeletonToAccount map[string]string
58
-	accountToMethod   map[string]NickReservationMethod
58
+	accountToMethod   map[string]NickEnforcementMethod
59 59
 }
60 60
 
61 61
 func (am *AccountManager) Initialize(server *Server) {
62 62
 	am.accountToClients = make(map[string][]*Client)
63 63
 	am.nickToAccount = make(map[string]string)
64 64
 	am.skeletonToAccount = make(map[string]string)
65
-	am.accountToMethod = make(map[string]NickReservationMethod)
65
+	am.accountToMethod = make(map[string]NickEnforcementMethod)
66 66
 	am.server = server
67 67
 
68 68
 	am.buildNickToAccountIndex()
@@ -76,7 +76,7 @@ func (am *AccountManager) buildNickToAccountIndex() {
76 76
 
77 77
 	nickToAccount := make(map[string]string)
78 78
 	skeletonToAccount := make(map[string]string)
79
-	accountToMethod := make(map[string]NickReservationMethod)
79
+	accountToMethod := make(map[string]NickEnforcementMethod)
80 80
 	existsPrefix := fmt.Sprintf(keyAccountExists, "")
81 81
 
82 82
 	am.serialCacheUpdateMutex.Lock()
@@ -109,12 +109,16 @@ func (am *AccountManager) buildNickToAccountIndex() {
109 109
 				}
110 110
 			}
111 111
 
112
-			if methodStr, err := tx.Get(fmt.Sprintf(keyAccountEnforcement, account)); err == nil {
113
-				method, err := nickReservationFromString(methodStr)
114
-				if err == nil {
115
-					accountToMethod[account] = method
112
+			if rawPrefs, err := tx.Get(fmt.Sprintf(keyAccountSettings, account)); err == nil {
113
+				var prefs AccountSettings
114
+				err := json.Unmarshal([]byte(rawPrefs), &prefs)
115
+				if err == nil && prefs.NickEnforcement != NickEnforcementOptional {
116
+					accountToMethod[account] = prefs.NickEnforcement
117
+				} else {
118
+					am.server.logger.Error("internal", "corrupt account creds", account)
116 119
 				}
117 120
 			}
121
+
118 122
 			return true
119 123
 		})
120 124
 		return err
@@ -180,36 +184,44 @@ func (am *AccountManager) NickToAccount(nick string) string {
180 184
 	return am.nickToAccount[cfnick]
181 185
 }
182 186
 
187
+// given an account, combine stored enforcement method with the config settings
188
+// to compute the actual enforcement method
189
+func configuredEnforcementMethod(config *Config, storedMethod NickEnforcementMethod) (result NickEnforcementMethod) {
190
+	if !config.Accounts.NickReservation.Enabled {
191
+		return NickEnforcementNone
192
+	}
193
+	result = storedMethod
194
+	// if they don't have a custom setting, or customization is disabled, use the default
195
+	if result == NickEnforcementOptional || !config.Accounts.NickReservation.AllowCustomEnforcement {
196
+		result = config.Accounts.NickReservation.Method
197
+	}
198
+	if result == NickEnforcementOptional {
199
+		// enforcement was explicitly enabled neither in the config or by the user
200
+		result = NickEnforcementNone
201
+	}
202
+	return
203
+}
204
+
183 205
 // Given a nick, looks up the account that owns it and the method (none/timeout/strict)
184 206
 // used to enforce ownership.
185
-func (am *AccountManager) EnforcementStatus(cfnick, skeleton string) (account string, method NickReservationMethod) {
207
+func (am *AccountManager) EnforcementStatus(cfnick, skeleton string) (account string, method NickEnforcementMethod) {
186 208
 	config := am.server.Config()
187 209
 	if !config.Accounts.NickReservation.Enabled {
188
-		return "", NickReservationNone
210
+		return "", NickEnforcementNone
189 211
 	}
190 212
 
191 213
 	am.RLock()
192 214
 	defer am.RUnlock()
193 215
 
194
-	// given an account, combine stored enforcement method with the config settings
195
-	// to compute the actual enforcement method
196
-	finalEnforcementMethod := func(account_ string) (result NickReservationMethod) {
197
-		result = am.accountToMethod[account_]
198
-		// if they don't have a custom setting, or customization is disabled, use the default
199
-		if result == NickReservationOptional || !config.Accounts.NickReservation.AllowCustomEnforcement {
200
-			result = config.Accounts.NickReservation.Method
201
-		}
202
-		if result == NickReservationOptional {
203
-			// enforcement was explicitly enabled neither in the config or by the user
204
-			result = NickReservationNone
205
-		}
206
-		return
216
+	finalEnforcementMethod := func(account_ string) (result NickEnforcementMethod) {
217
+		storedMethod := am.accountToMethod[account_]
218
+		return configuredEnforcementMethod(config, storedMethod)
207 219
 	}
208 220
 
209 221
 	nickAccount := am.nickToAccount[cfnick]
210 222
 	skelAccount := am.skeletonToAccount[skeleton]
211 223
 	if nickAccount == "" && skelAccount == "" {
212
-		return "", NickReservationNone
224
+		return "", NickEnforcementNone
213 225
 	} else if nickAccount != "" && (skelAccount == nickAccount || skelAccount == "") {
214 226
 		return nickAccount, finalEnforcementMethod(nickAccount)
215 227
 	} else if skelAccount != "" && nickAccount == "" {
@@ -220,75 +232,47 @@ func (am *AccountManager) EnforcementStatus(cfnick, skeleton string) (account st
220 232
 		nickMethod := finalEnforcementMethod(nickAccount)
221 233
 		skelMethod := finalEnforcementMethod(skelAccount)
222 234
 		switch {
223
-		case skelMethod == NickReservationNone:
235
+		case skelMethod == NickEnforcementNone:
224 236
 			return nickAccount, nickMethod
225
-		case nickMethod == NickReservationNone:
237
+		case nickMethod == NickEnforcementNone:
226 238
 			return skelAccount, skelMethod
227 239
 		default:
228 240
 			// nobody can use this nick
229
-			return "!", NickReservationStrict
241
+			return "!", NickEnforcementStrict
230 242
 		}
231 243
 	}
232 244
 }
233 245
 
234
-func (am *AccountManager) BouncerAllowed(account string, session *Session) bool {
235
-	// TODO stub
236
-	config := am.server.Config()
237
-	if !config.Accounts.Bouncer.Enabled {
238
-		return false
239
-	}
240
-	if config.Accounts.Bouncer.AllowedByDefault {
241
-		return true
242
-	}
243
-	return session != nil && session.capabilities.Has(caps.Bouncer)
244
-}
245
-
246
-// Looks up the enforcement method stored in the database for an account
247
-// (typically you want EnforcementStatus instead, which respects the config)
248
-func (am *AccountManager) getStoredEnforcementStatus(account string) string {
249
-	am.RLock()
250
-	defer am.RUnlock()
251
-	return nickReservationToString(am.accountToMethod[account])
252
-}
253
-
254 246
 // Sets a custom enforcement method for an account and stores it in the database.
255
-func (am *AccountManager) SetEnforcementStatus(account string, method NickReservationMethod) (err error) {
247
+func (am *AccountManager) SetEnforcementStatus(account string, method NickEnforcementMethod) (finalSettings AccountSettings, err error) {
256 248
 	config := am.server.Config()
257 249
 	if !(config.Accounts.NickReservation.Enabled && config.Accounts.NickReservation.AllowCustomEnforcement) {
258
-		return errFeatureDisabled
250
+		err = errFeatureDisabled
251
+		return
259 252
 	}
260 253
 
261
-	var serialized string
262
-	if method == NickReservationOptional {
263
-		serialized = "" // normally this is "default", but we're going to delete the key
264
-	} else {
265
-		serialized = nickReservationToString(method)
254
+	setter := func(in AccountSettings) (out AccountSettings, err error) {
255
+		out = in
256
+		out.NickEnforcement = method
257
+		return out, nil
266 258
 	}
267 259
 
268
-	key := fmt.Sprintf(keyAccountEnforcement, account)
260
+	_, err = am.ModifyAccountSettings(account, setter)
261
+	if err != nil {
262
+		return
263
+	}
269 264
 
265
+	// this update of the data plane is racey, but it's probably fine
270 266
 	am.Lock()
271 267
 	defer am.Unlock()
272 268
 
273
-	currentMethod := am.accountToMethod[account]
274
-	if method != currentMethod {
275
-		if method == NickReservationOptional {
276
-			delete(am.accountToMethod, account)
277
-		} else {
278
-			am.accountToMethod[account] = method
279
-		}
280
-
281
-		return am.server.store.Update(func(tx *buntdb.Tx) (err error) {
282
-			if serialized != "" {
283
-				_, _, err = tx.Set(key, nickReservationToString(method), nil)
284
-			} else {
285
-				_, err = tx.Delete(key)
286
-			}
287
-			return
288
-		})
269
+	if method == NickEnforcementOptional {
270
+		delete(am.accountToMethod, account)
271
+	} else {
272
+		am.accountToMethod[account] = method
289 273
 	}
290 274
 
291
-	return nil
275
+	return
292 276
 }
293 277
 
294 278
 func (am *AccountManager) AccountToClients(account string) (result []*Client) {
@@ -813,6 +797,12 @@ func (am *AccountManager) deserializeRawAccount(raw rawClientAccount) (result Cl
813 797
 			// pretend they have no vhost and move on
814 798
 		}
815 799
 	}
800
+	if raw.Settings != "" {
801
+		e := json.Unmarshal([]byte(raw.Settings), &result.Settings)
802
+		if e != nil {
803
+			am.server.logger.Warning("internal", "could not unmarshal settings for account", result.Name, e.Error())
804
+		}
805
+	}
816 806
 	return
817 807
 }
818 808
 
@@ -825,6 +815,7 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string
825 815
 	callbackKey := fmt.Sprintf(keyAccountCallback, casefoldedAccount)
826 816
 	nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
827 817
 	vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
818
+	settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
828 819
 
829 820
 	_, e := tx.Get(accountKey)
830 821
 	if e == buntdb.ErrNotFound {
@@ -838,6 +829,7 @@ func (am *AccountManager) loadRawAccount(tx *buntdb.Tx, casefoldedAccount string
838 829
 	result.Callback, _ = tx.Get(callbackKey)
839 830
 	result.AdditionalNicks, _ = tx.Get(nicksKey)
840 831
 	result.VHost, _ = tx.Get(vhostKey)
832
+	result.Settings, _ = tx.Get(settingsKey)
841 833
 
842 834
 	if _, e = tx.Get(verifiedKey); e == nil {
843 835
 		result.Verified = true
@@ -861,7 +853,7 @@ func (am *AccountManager) Unregister(account string) error {
861 853
 	verificationCodeKey := fmt.Sprintf(keyAccountVerificationCode, casefoldedAccount)
862 854
 	verifiedKey := fmt.Sprintf(keyAccountVerified, casefoldedAccount)
863 855
 	nicksKey := fmt.Sprintf(keyAccountAdditionalNicks, casefoldedAccount)
864
-	enforcementKey := fmt.Sprintf(keyAccountEnforcement, casefoldedAccount)
856
+	settingsKey := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
865 857
 	vhostKey := fmt.Sprintf(keyAccountVHost, casefoldedAccount)
866 858
 	vhostQueueKey := fmt.Sprintf(keyVHostQueueAcctToId, casefoldedAccount)
867 859
 	channelsKey := fmt.Sprintf(keyAccountChannels, casefoldedAccount)
@@ -892,7 +884,7 @@ func (am *AccountManager) Unregister(account string) error {
892 884
 		tx.Delete(registeredTimeKey)
893 885
 		tx.Delete(callbackKey)
894 886
 		tx.Delete(verificationCodeKey)
895
-		tx.Delete(enforcementKey)
887
+		tx.Delete(settingsKey)
896 888
 		rawNicks, _ = tx.Get(nicksKey)
897 889
 		tx.Delete(nicksKey)
898 890
 		credText, err = tx.Get(credentialsKey)
@@ -980,19 +972,13 @@ func (am *AccountManager) AuthenticateByCertFP(client *Client) error {
980 972
 	}
981 973
 
982 974
 	var account string
983
-	var rawAccount rawClientAccount
984 975
 	certFPKey := fmt.Sprintf(keyCertToAccount, client.certfp)
985 976
 
986
-	err := am.server.store.Update(func(tx *buntdb.Tx) error {
987
-		var err error
977
+	err := am.server.store.View(func(tx *buntdb.Tx) error {
988 978
 		account, _ = tx.Get(certFPKey)
989 979
 		if account == "" {
990 980
 			return errAccountInvalidCredentials
991 981
 		}
992
-		rawAccount, err = am.loadRawAccount(tx, account)
993
-		if err != nil || !rawAccount.Verified {
994
-			return errAccountUnverified
995
-		}
996 982
 		return nil
997 983
 	})
998 984
 
@@ -1001,14 +987,57 @@ func (am *AccountManager) AuthenticateByCertFP(client *Client) error {
1001 987
 	}
1002 988
 
1003 989
 	// ok, we found an account corresponding to their certificate
1004
-	clientAccount, err := am.deserializeRawAccount(rawAccount)
990
+	clientAccount, err := am.LoadAccount(account)
1005 991
 	if err != nil {
1006 992
 		return err
993
+	} else if !clientAccount.Verified {
994
+		return errAccountUnverified
1007 995
 	}
1008 996
 	am.Login(client, clientAccount)
1009 997
 	return nil
1010 998
 }
1011 999
 
1000
+type settingsMunger func(input AccountSettings) (output AccountSettings, err error)
1001
+
1002
+func (am *AccountManager) ModifyAccountSettings(account string, munger settingsMunger) (newSettings AccountSettings, err error) {
1003
+	casefoldedAccount, err := CasefoldName(account)
1004
+	if err != nil {
1005
+		return newSettings, errAccountDoesNotExist
1006
+	}
1007
+	// TODO implement this in general via a compare-and-swap API
1008
+	accountData, err := am.LoadAccount(casefoldedAccount)
1009
+	if err != nil {
1010
+		return
1011
+	} else if !accountData.Verified {
1012
+		return newSettings, errAccountUnverified
1013
+	}
1014
+	newSettings, err = munger(accountData.Settings)
1015
+	if err != nil {
1016
+		return
1017
+	}
1018
+	text, err := json.Marshal(newSettings)
1019
+	if err != nil {
1020
+		return
1021
+	}
1022
+	key := fmt.Sprintf(keyAccountSettings, casefoldedAccount)
1023
+	serializedValue := string(text)
1024
+	err = am.server.store.Update(func(tx *buntdb.Tx) (err error) {
1025
+		_, _, err = tx.Set(key, serializedValue, nil)
1026
+		return
1027
+	})
1028
+	if err != nil {
1029
+		err = errAccountUpdateFailed
1030
+		return
1031
+	}
1032
+	// success, push new settings into the client objects
1033
+	am.Lock()
1034
+	defer am.Unlock()
1035
+	for _, client := range am.accountToClients[casefoldedAccount] {
1036
+		client.SetAccountSettings(newSettings)
1037
+	}
1038
+	return
1039
+}
1040
+
1012 1041
 // represents someone's status in hostserv
1013 1042
 type VHostInfo struct {
1014 1043
 	ApprovedVHost   string
@@ -1237,6 +1266,9 @@ func (am *AccountManager) Login(client *Client, account ClientAccount) {
1237 1266
 	am.Lock()
1238 1267
 	defer am.Unlock()
1239 1268
 	am.accountToClients[casefoldedAccount] = append(am.accountToClients[casefoldedAccount], client)
1269
+	for _, client := range am.accountToClients[casefoldedAccount] {
1270
+		client.SetAccountSettings(account.Settings)
1271
+	}
1240 1272
 }
1241 1273
 
1242 1274
 func (am *AccountManager) Logout(client *Client) {
@@ -1283,6 +1315,21 @@ type AccountCredentials struct {
1283 1315
 	Certificate    string // fingerprint
1284 1316
 }
1285 1317
 
1318
+type BouncerAllowedSetting int
1319
+
1320
+const (
1321
+	BouncerAllowedServerDefault BouncerAllowedSetting = iota
1322
+	BouncerDisallowedByUser
1323
+	BouncerAllowedByUser
1324
+)
1325
+
1326
+type AccountSettings struct {
1327
+	AutoreplayLines *int
1328
+	NickEnforcement NickEnforcementMethod
1329
+	AllowBouncer    BouncerAllowedSetting
1330
+	AutoreplayJoins bool
1331
+}
1332
+
1286 1333
 // ClientAccount represents a user account.
1287 1334
 type ClientAccount struct {
1288 1335
 	// Name of the account.
@@ -1293,6 +1340,7 @@ type ClientAccount struct {
1293 1340
 	Verified        bool
1294 1341
 	AdditionalNicks []string
1295 1342
 	VHost           VHostInfo
1343
+	Settings        AccountSettings
1296 1344
 }
1297 1345
 
1298 1346
 // convenience for passing around raw serialized account data
@@ -1304,6 +1352,7 @@ type rawClientAccount struct {
1304 1352
 	Verified        bool
1305 1353
 	AdditionalNicks string
1306 1354
 	VHost           string
1355
+	Settings        string
1307 1356
 }
1308 1357
 
1309 1358
 // logoutOfAccount logs the client out of their current account.

+ 0
- 6
irc/caps/set.go View File

@@ -114,9 +114,3 @@ func (s *Set) String(version Version, values *Values) string {
114 114
 
115 115
 	return strings.Join(strs, " ")
116 116
 }
117
-
118
-// returns whether we should send `znc.in/self-message`-style echo messages
119
-// to sessions other than that which originated the message
120
-func (capabs *Set) SelfMessagesEnabled() bool {
121
-	return capabs.Has(EchoMessage) || capabs.Has(ZNCSelfMessage)
122
-}

+ 17
- 6
irc/channel.go View File

@@ -620,7 +620,17 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
620 620
 	// TODO #259 can be implemented as Flush(false) (i.e., nonblocking) while holding joinPartMutex
621 621
 	rb.Flush(true)
622 622
 
623
-	replayLimit := channel.server.Config().History.AutoreplayOnJoin
623
+	var replayLimit int
624
+	customReplayLimit := client.AccountSettings().AutoreplayLines
625
+	if customReplayLimit != nil {
626
+		replayLimit = *customReplayLimit
627
+		maxLimit := channel.server.Config().History.ChathistoryMax
628
+		if maxLimit < replayLimit {
629
+			replayLimit = maxLimit
630
+		}
631
+	} else {
632
+		replayLimit = channel.server.Config().History.AutoreplayOnJoin
633
+	}
624 634
 	if 0 < replayLimit {
625 635
 		// TODO don't replay the client's own JOIN line?
626 636
 		items := channel.history.Latest(replayLimit)
@@ -782,6 +792,7 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
782 792
 	client := rb.target
783 793
 	eventPlayback := rb.session.capabilities.Has(caps.EventPlayback)
784 794
 	extendedJoin := rb.session.capabilities.Has(caps.ExtendedJoin)
795
+	playJoinsAsPrivmsg := (!autoreplay || client.AccountSettings().AutoreplayJoins)
785 796
 
786 797
 	if len(items) == 0 {
787 798
 		return
@@ -808,7 +819,7 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
808 819
 					rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "JOIN", chname)
809 820
 				}
810 821
 			} else {
811
-				if autoreplay {
822
+				if !playJoinsAsPrivmsg {
812 823
 					continue // #474
813 824
 				}
814 825
 				var message string
@@ -823,7 +834,7 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
823 834
 			if eventPlayback {
824 835
 				rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "PART", chname, item.Message.Message)
825 836
 			} else {
826
-				if autoreplay {
837
+				if !playJoinsAsPrivmsg {
827 838
 					continue // #474
828 839
 				}
829 840
 				message := fmt.Sprintf(client.t("%[1]s left the channel (%[2]s)"), nick, item.Message.Message)
@@ -840,7 +851,7 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
840 851
 			if eventPlayback {
841 852
 				rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "QUIT", item.Message.Message)
842 853
 			} else {
843
-				if autoreplay {
854
+				if !playJoinsAsPrivmsg {
844 855
 					continue // #474
845 856
 				}
846 857
 				message := fmt.Sprintf(client.t("%[1]s quit (%[2]s)"), nick, item.Message.Message)
@@ -989,7 +1000,7 @@ func (channel *Channel) SendSplitMessage(command string, minPrefixMode modes.Mod
989 1000
 	}
990 1001
 	// send echo-message to other connected sessions
991 1002
 	for _, session := range client.Sessions() {
992
-		if session == rb.session || !session.capabilities.SelfMessagesEnabled() {
1003
+		if session == rb.session {
993 1004
 			continue
994 1005
 		}
995 1006
 		var tagsToUse map[string]string
@@ -998,7 +1009,7 @@ func (channel *Channel) SendSplitMessage(command string, minPrefixMode modes.Mod
998 1009
 		}
999 1010
 		if histType == history.Tagmsg && session.capabilities.Has(caps.MessageTags) {
1000 1011
 			session.sendFromClientInternal(false, message.Time, message.Msgid, nickmask, account, tagsToUse, command, chname)
1001
-		} else {
1012
+		} else if histType != history.Tagmsg {
1002 1013
 			session.sendSplitMsgFromClientInternal(false, nickmask, account, tagsToUse, command, chname, message)
1003 1014
 		}
1004 1015
 	}

+ 1
- 1
irc/chanserv.go View File

@@ -86,7 +86,7 @@ referenced by their registered account names, not their nicknames.`,
86 86
 
87 87
 // csNotice sends the client a notice from ChanServ
88 88
 func csNotice(rb *ResponseBuffer, text string) {
89
-	rb.Add(nil, "ChanServ", "NOTICE", rb.target.Nick(), text)
89
+	rb.Add(nil, "ChanServ!ChanServ@localhost", "NOTICE", rb.target.Nick(), text)
90 90
 }
91 91
 
92 92
 func csAmodeHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {

+ 1
- 0
irc/client.go View File

@@ -48,6 +48,7 @@ type ResumeDetails struct {
48 48
 type Client struct {
49 49
 	account            string
50 50
 	accountName        string // display name of the account: uncasefolded, '*' if not logged in
51
+	accountSettings    AccountSettings
51 52
 	atime              time.Time
52 53
 	away               bool
53 54
 	awayMessage        string

+ 15
- 2
irc/client_lookup_set.go View File

@@ -145,7 +145,20 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
145 145
 
146 146
 	reservedAccount, method := client.server.accounts.EnforcementStatus(newcfnick, newSkeleton)
147 147
 	account := client.Account()
148
-	bouncerAllowed := client.server.accounts.BouncerAllowed(account, session)
148
+	config := client.server.Config()
149
+	var bouncerAllowed bool
150
+	if config.Accounts.Bouncer.Enabled {
151
+		if session != nil && session.capabilities.Has(caps.Bouncer) {
152
+			bouncerAllowed = true
153
+		} else {
154
+			settings := client.AccountSettings()
155
+			if config.Accounts.Bouncer.AllowedByDefault && settings.AllowBouncer != BouncerDisallowedByUser {
156
+				bouncerAllowed = true
157
+			} else if settings.AllowBouncer == BouncerAllowedByUser {
158
+				bouncerAllowed = true
159
+			}
160
+		}
161
+	}
149 162
 
150 163
 	clients.Lock()
151 164
 	defer clients.Unlock()
@@ -168,7 +181,7 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick
168 181
 	if skeletonHolder != nil && skeletonHolder != client {
169 182
 		return errNicknameInUse
170 183
 	}
171
-	if method == NickReservationStrict && reservedAccount != "" && reservedAccount != account {
184
+	if method == NickEnforcementStrict && reservedAccount != "" && reservedAccount != account {
172 185
 		return errNicknameReserved
173 186
 	}
174 187
 	clients.removeInternal(client)

+ 27
- 27
irc/config.go View File

@@ -112,63 +112,63 @@ type VHostConfig struct {
112 112
 	} `yaml:"user-requests"`
113 113
 }
114 114
 
115
-type NickReservationMethod int
115
+type NickEnforcementMethod int
116 116
 
117 117
 const (
118
-	// NickReservationOptional is the zero value; it serializes to
118
+	// NickEnforcementOptional is the zero value; it serializes to
119 119
 	// "optional" in the yaml config, and "default" as an arg to `NS ENFORCE`.
120 120
 	// in both cases, it means "defer to the other source of truth", i.e.,
121 121
 	// in the config, defer to the user's custom setting, and as a custom setting,
122
-	// defer to the default in the config. if both are NickReservationOptional then
122
+	// defer to the default in the config. if both are NickEnforcementOptional then
123 123
 	// there is no enforcement.
124
-	NickReservationOptional NickReservationMethod = iota
125
-	NickReservationNone
126
-	NickReservationWithTimeout
127
-	NickReservationStrict
124
+	// XXX: these are serialized as numbers in the database, so beware of collisions
125
+	// when refactoring (any numbers currently in use must keep their meanings, or
126
+	// else be fixed up by a schema change)
127
+	NickEnforcementOptional NickEnforcementMethod = iota
128
+	NickEnforcementNone
129
+	NickEnforcementWithTimeout
130
+	NickEnforcementStrict
128 131
 )
129 132
 
130
-func nickReservationToString(method NickReservationMethod) string {
133
+func nickReservationToString(method NickEnforcementMethod) string {
131 134
 	switch method {
132
-	case NickReservationOptional:
135
+	case NickEnforcementOptional:
133 136
 		return "default"
134
-	case NickReservationNone:
137
+	case NickEnforcementNone:
135 138
 		return "none"
136
-	case NickReservationWithTimeout:
139
+	case NickEnforcementWithTimeout:
137 140
 		return "timeout"
138
-	case NickReservationStrict:
141
+	case NickEnforcementStrict:
139 142
 		return "strict"
140 143
 	default:
141 144
 		return ""
142 145
 	}
143 146
 }
144 147
 
145
-func nickReservationFromString(method string) (NickReservationMethod, error) {
146
-	switch method {
148
+func nickReservationFromString(method string) (NickEnforcementMethod, error) {
149
+	switch strings.ToLower(method) {
147 150
 	case "default":
148
-		return NickReservationOptional, nil
151
+		return NickEnforcementOptional, nil
149 152
 	case "optional":
150
-		return NickReservationOptional, nil
153
+		return NickEnforcementOptional, nil
151 154
 	case "none":
152
-		return NickReservationNone, nil
155
+		return NickEnforcementNone, nil
153 156
 	case "timeout":
154
-		return NickReservationWithTimeout, nil
157
+		return NickEnforcementWithTimeout, nil
155 158
 	case "strict":
156
-		return NickReservationStrict, nil
159
+		return NickEnforcementStrict, nil
157 160
 	default:
158
-		return NickReservationOptional, fmt.Errorf("invalid nick-reservation.method value: %s", method)
161
+		return NickEnforcementOptional, fmt.Errorf("invalid nick-reservation.method value: %s", method)
159 162
 	}
160 163
 }
161 164
 
162
-func (nr *NickReservationMethod) UnmarshalYAML(unmarshal func(interface{}) error) error {
163
-	var orig, raw string
165
+func (nr *NickEnforcementMethod) UnmarshalYAML(unmarshal func(interface{}) error) error {
166
+	var orig string
164 167
 	var err error
165 168
 	if err = unmarshal(&orig); err != nil {
166 169
 		return err
167 170
 	}
168
-	if raw, err = Casefold(orig); err != nil {
169
-		return err
170
-	}
171
-	method, err := nickReservationFromString(raw)
171
+	method, err := nickReservationFromString(orig)
172 172
 	if err == nil {
173 173
 		*nr = method
174 174
 	}
@@ -178,7 +178,7 @@ func (nr *NickReservationMethod) UnmarshalYAML(unmarshal func(interface{}) error
178 178
 type NickReservationConfig struct {
179 179
 	Enabled                bool
180 180
 	AdditionalNickLimit    int `yaml:"additional-nick-limit"`
181
-	Method                 NickReservationMethod
181
+	Method                 NickEnforcementMethod
182 182
 	AllowCustomEnforcement bool          `yaml:"allow-custom-enforcement"`
183 183
 	RenameTimeout          time.Duration `yaml:"rename-timeout"`
184 184
 	RenamePrefix           string        `yaml:"rename-prefix"`

+ 37
- 1
irc/database.go View File

@@ -22,7 +22,7 @@ const (
22 22
 	// 'version' of the database schema
23 23
 	keySchemaVersion = "db.version"
24 24
 	// latest schema of the db
25
-	latestDbSchema = "5"
25
+	latestDbSchema = "6"
26 26
 )
27 27
 
28 28
 type SchemaChanger func(*Config, *buntdb.Tx) error
@@ -409,6 +409,37 @@ func schemaChangeV4ToV5(config *Config, tx *buntdb.Tx) error {
409 409
 	return nil
410 410
 }
411 411
 
412
+// custom nick enforcement was a separate db key, now it's part of settings
413
+func schemaChangeV5ToV6(config *Config, tx *buntdb.Tx) error {
414
+	accountToEnforcement := make(map[string]NickEnforcementMethod)
415
+	prefix := "account.customenforcement "
416
+	tx.AscendGreaterOrEqual("", prefix, func(key, value string) bool {
417
+		if !strings.HasPrefix(key, prefix) {
418
+			return false
419
+		}
420
+		account := strings.TrimPrefix(key, prefix)
421
+		method, err := nickReservationFromString(value)
422
+		if err == nil {
423
+			accountToEnforcement[account] = method
424
+		} else {
425
+			log.Printf("skipping corrupt custom enforcement value for %s\n", account)
426
+		}
427
+		return true
428
+	})
429
+
430
+	for account, method := range accountToEnforcement {
431
+		var settings AccountSettings
432
+		settings.NickEnforcement = method
433
+		text, err := json.Marshal(settings)
434
+		if err != nil {
435
+			return err
436
+		}
437
+		tx.Delete(prefix + account)
438
+		tx.Set(fmt.Sprintf("account.settings %s", account), string(text), nil)
439
+	}
440
+	return nil
441
+}
442
+
412 443
 func init() {
413 444
 	allChanges := []SchemaChange{
414 445
 		{
@@ -431,6 +462,11 @@ func init() {
431 462
 			TargetVersion:  "5",
432 463
 			Changer:        schemaChangeV4ToV5,
433 464
 		},
465
+		{
466
+			InitialVersion: "5",
467
+			TargetVersion:  "6",
468
+			Changer:        schemaChangeV5ToV6,
469
+		},
434 470
 	}
435 471
 
436 472
 	// build the index

+ 2
- 2
irc/errors.go View File

@@ -19,10 +19,10 @@ var (
19 19
 	errAccountNickReservationFailed   = errors.New("Could not (un)reserve nick")
20 20
 	errAccountNotLoggedIn             = errors.New("You're not logged into an account")
21 21
 	errAccountTooManyNicks            = errors.New("Account has too many reserved nicks")
22
-	errAccountUnverified              = errors.New("Account is not yet verified")
22
+	errAccountUnverified              = errors.New(`Account is not yet verified`)
23 23
 	errAccountVerificationFailed      = errors.New("Account verification failed")
24 24
 	errAccountVerificationInvalidCode = errors.New("Invalid account verification code")
25
-	errAccountUpdateFailed            = errors.New("Error while updating your account information")
25
+	errAccountUpdateFailed            = errors.New(`Error while updating your account information`)
26 26
 	errAccountMustHoldNick            = errors.New(`You must hold that nickname in order to register it`)
27 27
 	errCallbackFailed                 = errors.New("Account verification could not be sent")
28 28
 	errCertfpAlreadyExists            = errors.New(`An account already exists for your certificate fingerprint`)

+ 13
- 0
irc/getters.go View File

@@ -275,6 +275,19 @@ func (client *Client) SetAccountName(account string) (changed bool) {
275 275
 	return
276 276
 }
277 277
 
278
+func (client *Client) AccountSettings() (result AccountSettings) {
279
+	client.stateMutex.RLock()
280
+	result = client.accountSettings
281
+	client.stateMutex.RUnlock()
282
+	return
283
+}
284
+
285
+func (client *Client) SetAccountSettings(settings AccountSettings) {
286
+	client.stateMutex.Lock()
287
+	client.accountSettings = settings
288
+	client.stateMutex.Unlock()
289
+}
290
+
278 291
 func (client *Client) Languages() (languages []string) {
279 292
 	client.stateMutex.RLock()
280 293
 	languages = client.languages

+ 2
- 2
irc/handlers.go View File

@@ -2046,12 +2046,12 @@ func messageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
2046 2046
 			}
2047 2047
 			// an echo-message may need to go out to other client sessions:
2048 2048
 			for _, session := range client.Sessions() {
2049
-				if session == rb.session || !rb.session.capabilities.SelfMessagesEnabled() {
2049
+				if session == rb.session {
2050 2050
 					continue
2051 2051
 				}
2052 2052
 				if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) {
2053 2053
 					session.sendFromClientInternal(false, splitMsg.Time, splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick)
2054
-				} else {
2054
+				} else if histType != history.Tagmsg {
2055 2055
 					session.sendSplitMsgFromClientInternal(false, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick, splitMsg)
2056 2056
 				}
2057 2057
 			}

+ 1
- 1
irc/hostserv.go View File

@@ -131,7 +131,7 @@ for the rejection.`,
131 131
 
132 132
 // hsNotice sends the client a notice from HostServ
133 133
 func hsNotice(rb *ResponseBuffer, text string) {
134
-	rb.Add(nil, "HostServ", "NOTICE", rb.target.Nick(), text)
134
+	rb.Add(nil, "HostServ!HostServ@localhost", "NOTICE", rb.target.Nick(), text)
135 135
 }
136 136
 
137 137
 // hsNotifyChannel notifies the designated channel of new vhost activity

+ 3
- 3
irc/idletimer.go View File

@@ -198,7 +198,7 @@ func (nt *NickTimer) Initialize(client *Client) {
198 198
 	}
199 199
 
200 200
 	config := &client.server.Config().Accounts.NickReservation
201
-	enabled := config.Enabled && (config.Method == NickReservationWithTimeout || config.AllowCustomEnforcement)
201
+	enabled := config.Enabled && (config.Method == NickEnforcementWithTimeout || config.AllowCustomEnforcement)
202 202
 
203 203
 	nt.Lock()
204 204
 	defer nt.Unlock()
@@ -235,7 +235,7 @@ func (nt *NickTimer) Touch(rb *ResponseBuffer) {
235 235
 	cfnick, skeleton := nt.client.uniqueIdentifiers()
236 236
 	account := nt.client.Account()
237 237
 	accountForNick, method := nt.client.server.accounts.EnforcementStatus(cfnick, skeleton)
238
-	enforceTimeout := method == NickReservationWithTimeout
238
+	enforceTimeout := method == NickEnforcementWithTimeout
239 239
 
240 240
 	var shouldWarn, shouldRename bool
241 241
 
@@ -258,7 +258,7 @@ func (nt *NickTimer) Touch(rb *ResponseBuffer) {
258 258
 		if enforceTimeout && delinquent && (accountChanged || nt.timer == nil) {
259 259
 			nt.timer = time.AfterFunc(nt.timeout, nt.processTimeout)
260 260
 			shouldWarn = true
261
-		} else if method == NickReservationStrict && delinquent {
261
+		} else if method == NickEnforcementStrict && delinquent {
262 262
 			shouldRename = true // this can happen if reservation was enabled by rehash
263 263
 		}
264 264
 	}()

+ 256
- 29
irc/nickserv.go View File

@@ -5,6 +5,8 @@ package irc
5 5
 
6 6
 import (
7 7
 	"fmt"
8
+	"strconv"
9
+	"strings"
8 10
 	"time"
9 11
 
10 12
 	"github.com/goshuirc/irc-go/ircfmt"
@@ -25,10 +27,6 @@ func servCmdRequiresNickRes(config *Config) bool {
25 27
 	return config.Accounts.AuthenticationEnabled && config.Accounts.NickReservation.Enabled
26 28
 }
27 29
 
28
-func nsEnforceEnabled(config *Config) bool {
29
-	return servCmdRequiresNickRes(config) && config.Accounts.NickReservation.AllowCustomEnforcement
30
-}
31
-
32 30
 func servCmdRequiresBouncerEnabled(config *Config) bool {
33 31
 	return config.Accounts.Bouncer.Enabled
34 32
 }
@@ -61,20 +59,14 @@ DROP de-links the given (or your current) nickname from your user account.`,
61 59
 			authRequired: true,
62 60
 		},
63 61
 		"enforce": {
62
+			hidden:  true,
64 63
 			handler: nsEnforceHandler,
65 64
 			help: `Syntax: $bENFORCE [method]$b
66 65
 
67
-ENFORCE lets you specify a custom enforcement mechanism for your registered
68
-nicknames. Your options are:
69
-1. 'none'    [no enforcement, overriding the server default]
70
-2. 'timeout' [anyone using the nick must authenticate before a deadline,
71
-              or else they will be renamed]
72
-3. 'strict'  [you must already be authenticated to use the nick]
73
-4. 'default' [use the server default]
74
-With no arguments, queries your current enforcement status.`,
75
-			helpShort:    `$bENFORCE$b lets you change how your nicknames are reserved.`,
66
+ENFORCE is an alias for $bGET enforce$b and $bSET enforce$b. See the help
67
+entry for $bSET$b for more information.`,
76 68
 			authRequired: true,
77
-			enabled:      nsEnforceEnabled,
69
+			enabled:      servCmdRequiresAccreg,
78 70
 		},
79 71
 		"ghost": {
80 72
 			handler: nsGhostHandler,
@@ -194,12 +186,257 @@ password by supplying their username and then the desired password.`,
194 186
 			enabled:   servCmdRequiresAuthEnabled,
195 187
 			minParams: 2,
196 188
 		},
189
+		"get": {
190
+			handler: nsGetHandler,
191
+			help: `Syntax: $bGET <setting>$b
192
+
193
+GET queries the current values of your account settings. For more information
194
+on the settings and their possible values, see HELP SET.`,
195
+			helpShort:    `$bGET$b queries the current values of your account settings`,
196
+			authRequired: true,
197
+			enabled:      servCmdRequiresAccreg,
198
+			minParams:    1,
199
+		},
200
+		"saget": {
201
+			handler: nsGetHandler,
202
+			help: `Syntax: $bSAGET <account> <setting>$b
203
+
204
+SAGET queries the values of someone else's account settings. For more
205
+information on the settings and their possible values, see HELP SET.`,
206
+			helpShort: `$bSAGET$b queries the current values of another user's account settings`,
207
+			enabled:   servCmdRequiresAccreg,
208
+			minParams: 2,
209
+			capabs:    []string{"accreg"},
210
+		},
211
+		"set": {
212
+			handler: nsSetHandler,
213
+			help: `Syntax $bSET <setting> <value>$b
214
+
215
+Set modifies your account settings. The following settings ara available:
216
+
217
+$bENFORCE$b
218
+'enforce' lets you specify a custom enforcement mechanism for your registered
219
+nicknames. Your options are:
220
+1. 'none'    [no enforcement, overriding the server default]
221
+2. 'timeout' [anyone using the nick must authenticate before a deadline,
222
+              or else they will be renamed]
223
+3. 'strict'  [you must already be authenticated to use the nick]
224
+4. 'default' [use the server default]
225
+
226
+$bBOUNCER$b
227
+If 'bouncer' is enabled and you are already logged in and using a nick, a
228
+second client of yours that authenticates with SASL and requests the same nick
229
+is allowed to attach to the nick as well (this is comparable to the behavior
230
+of IRC "bouncers" like ZNC). Your options are 'on' (allow this behavior),
231
+'off' (disallow it), and 'default' (use the server default value).
232
+
233
+$bAUTOREPLAY-LINES$b
234
+'autoreplay-lines' controls the number of lines of channel history that will
235
+be replayed to you automatically when joining a channel. Your options are any
236
+positive number, 0 to disable the feature, and 'default' to use the server
237
+default.
238
+
239
+$bAUTOREPLAY-JOINS$b
240
+'autoreplay-joins' controls whether autoreplayed channel history will include
241
+lines for join and part. This provides more information about the context of
242
+messages, but may be spammy. Your options are 'on' and 'off'.
243
+`,
244
+			helpShort:    `$bSET$b modifies your account settings`,
245
+			authRequired: true,
246
+			enabled:      servCmdRequiresAccreg,
247
+			minParams:    2,
248
+		},
249
+		"saset": {
250
+			handler:   nsSetHandler,
251
+			help:      `Syntax: $bSASET <account> <setting> <value>$b`,
252
+			helpShort: `$bSASET$b modifies another user's account settings`,
253
+			enabled:   servCmdRequiresAccreg,
254
+			minParams: 3,
255
+			capabs:    []string{"accreg"},
256
+		},
197 257
 	}
198 258
 )
199 259
 
200 260
 // nsNotice sends the client a notice from NickServ
201 261
 func nsNotice(rb *ResponseBuffer, text string) {
202
-	rb.Add(nil, "NickServ", "NOTICE", rb.target.Nick(), text)
262
+	// XXX i can't figure out how to use OragonoServices[servicename].prefix here
263
+	// without creating a compile-time initialization loop
264
+	rb.Add(nil, "NickServ!NickServ@localhost", "NOTICE", rb.target.Nick(), text)
265
+}
266
+
267
+func nsGetHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
268
+	var account string
269
+	if command == "saget" {
270
+		account = params[0]
271
+		params = params[1:]
272
+	} else {
273
+		account = client.Account()
274
+	}
275
+
276
+	accountData, err := server.accounts.LoadAccount(account)
277
+	if err == errAccountDoesNotExist {
278
+		nsNotice(rb, client.t("No such account"))
279
+		return
280
+	} else if err != nil {
281
+		nsNotice(rb, client.t("Error loading account data"))
282
+		return
283
+	}
284
+
285
+	displaySetting(params[0], accountData.Settings, client, rb)
286
+}
287
+
288
+func displaySetting(settingName string, settings AccountSettings, client *Client, rb *ResponseBuffer) {
289
+	config := client.server.Config()
290
+	switch strings.ToLower(settingName) {
291
+	case "enforce":
292
+		storedValue := settings.NickEnforcement
293
+		serializedStoredValue := nickReservationToString(storedValue)
294
+		nsNotice(rb, fmt.Sprintf(client.t("Your stored nickname enforcement setting is: %s"), serializedStoredValue))
295
+		serializedActualValue := nickReservationToString(configuredEnforcementMethod(config, storedValue))
296
+		nsNotice(rb, fmt.Sprintf(client.t("Given current server settings, your nickname is enforced with: %s"), serializedActualValue))
297
+	case "autoreplay-lines":
298
+		if settings.AutoreplayLines == nil {
299
+			nsNotice(rb, fmt.Sprintf(client.t("You will receive the server default of %d lines of autoreplayed history"), config.History.AutoreplayOnJoin))
300
+		} else {
301
+			nsNotice(rb, fmt.Sprintf(client.t("You will receive %d lines of autoreplayed history"), *settings.AutoreplayLines))
302
+		}
303
+	case "autoreplay-joins":
304
+		if settings.AutoreplayJoins {
305
+			nsNotice(rb, client.t("You will see JOINs and PARTs in autoreplayed history lines"))
306
+		} else {
307
+			nsNotice(rb, client.t("You will not see JOINs and PARTs in autoreplayed history lines"))
308
+		}
309
+	case "bouncer":
310
+		if !config.Accounts.Bouncer.Enabled {
311
+			nsNotice(rb, fmt.Sprintf(client.t("This feature has been disabled by the server administrators")))
312
+		} else {
313
+			switch settings.AllowBouncer {
314
+			case BouncerAllowedServerDefault:
315
+				if config.Accounts.Bouncer.AllowedByDefault {
316
+					nsNotice(rb, fmt.Sprintf(client.t("Bouncer functionality is currently enabled for your account, but you can opt out")))
317
+				} else {
318
+					nsNotice(rb, fmt.Sprintf(client.t("Bouncer functionality is currently disabled for your account, but you can opt in")))
319
+				}
320
+			case BouncerDisallowedByUser:
321
+				nsNotice(rb, fmt.Sprintf(client.t("Bouncer functionality is currently disabled for your account")))
322
+			case BouncerAllowedByUser:
323
+				nsNotice(rb, fmt.Sprintf(client.t("Bouncer functionality is currently enabled for your account")))
324
+			}
325
+		}
326
+	default:
327
+		nsNotice(rb, client.t("No such setting"))
328
+	}
329
+}
330
+
331
+func stringToBool(str string) (result bool, err error) {
332
+	switch strings.ToLower(str) {
333
+	case "on":
334
+		result = true
335
+	case "off":
336
+		result = false
337
+	case "true":
338
+		result = true
339
+	case "false":
340
+		result = false
341
+	default:
342
+		err = errInvalidParams
343
+	}
344
+	return
345
+}
346
+
347
+func nsSetHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
348
+	var account string
349
+	if command == "saset" {
350
+		account = params[0]
351
+		params = params[1:]
352
+	} else {
353
+		account = client.Account()
354
+	}
355
+
356
+	var munger settingsMunger
357
+	var finalSettings AccountSettings
358
+	var err error
359
+	switch strings.ToLower(params[0]) {
360
+	case "pass":
361
+		nsNotice(rb, client.t("To change a password, use the PASSWD command. For details, /msg NickServ HELP PASSWD"))
362
+		return
363
+	case "enforce":
364
+		var method NickEnforcementMethod
365
+		method, err = nickReservationFromString(params[1])
366
+		if err != nil {
367
+			err = errInvalidParams
368
+			break
369
+		}
370
+		// updating enforcement settings is special-cased, because it requires
371
+		// an update to server.accounts.accountToMethod
372
+		finalSettings, err = server.accounts.SetEnforcementStatus(account, method)
373
+		if err == nil {
374
+			finalSettings.NickEnforcement = method // success
375
+		}
376
+	case "autoreplay-lines":
377
+		var newValue *int
378
+		if strings.ToLower(params[1]) != "default" {
379
+			val, err_ := strconv.Atoi(params[1])
380
+			if err_ != nil || val < 0 {
381
+				err = errInvalidParams
382
+				break
383
+			}
384
+			newValue = new(int)
385
+			*newValue = val
386
+		}
387
+		munger = func(in AccountSettings) (out AccountSettings, err error) {
388
+			out = in
389
+			out.AutoreplayLines = newValue
390
+			return
391
+		}
392
+	case "bouncer":
393
+		var newValue BouncerAllowedSetting
394
+		if strings.ToLower(params[1]) == "default" {
395
+			newValue = BouncerAllowedServerDefault
396
+		} else {
397
+			var enabled bool
398
+			enabled, err = stringToBool(params[1])
399
+			if enabled {
400
+				newValue = BouncerAllowedByUser
401
+			} else {
402
+				newValue = BouncerDisallowedByUser
403
+			}
404
+		}
405
+		if err == nil {
406
+			munger = func(in AccountSettings) (out AccountSettings, err error) {
407
+				out = in
408
+				out.AllowBouncer = newValue
409
+				return
410
+			}
411
+		}
412
+	case "autoreplay-joins":
413
+		var newValue bool
414
+		newValue, err = stringToBool(params[1])
415
+		if err == nil {
416
+			munger = func(in AccountSettings) (out AccountSettings, err error) {
417
+				out = in
418
+				out.AutoreplayJoins = newValue
419
+				return
420
+			}
421
+		}
422
+	default:
423
+		err = errInvalidParams
424
+	}
425
+
426
+	if munger != nil {
427
+		finalSettings, err = server.accounts.ModifyAccountSettings(account, munger)
428
+	}
429
+
430
+	switch err {
431
+	case nil:
432
+		nsNotice(rb, client.t("Successfully changed your account settings"))
433
+		displaySetting(params[0], finalSettings, client, rb)
434
+	case errInvalidParams, errAccountDoesNotExist, errFeatureDisabled, errAccountUnverified, errAccountUpdateFailed:
435
+		nsNotice(rb, client.t(err.Error()))
436
+	default:
437
+		// unknown error
438
+		nsNotice(rb, client.t("An error occurred"))
439
+	}
203 440
 }
204 441
 
205 442
 func nsDropHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
@@ -568,22 +805,12 @@ func nsPasswdHandler(server *Server, client *Client, command string, params []st
568 805
 }
569 806
 
570 807
 func nsEnforceHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {
808
+	newParams := []string{"enforce"}
571 809
 	if len(params) == 0 {
572
-		status := server.accounts.getStoredEnforcementStatus(client.Account())
573
-		nsNotice(rb, fmt.Sprintf(client.t("Your current nickname enforcement is: %s"), status))
810
+		nsGetHandler(server, client, "get", newParams, rb)
574 811
 	} else {
575
-		method, err := nickReservationFromString(params[0])
576
-		if err != nil {
577
-			nsNotice(rb, client.t("Invalid parameters"))
578
-			return
579
-		}
580
-		err = server.accounts.SetEnforcementStatus(client.Account(), method)
581
-		if err == nil {
582
-			nsNotice(rb, client.t("Enforcement method set"))
583
-		} else {
584
-			server.logger.Error("internal", "couldn't store NS ENFORCE data", err.Error())
585
-			nsNotice(rb, client.t("An error occurred"))
586
-		}
812
+		newParams = append(newParams, params[0])
813
+		nsSetHandler(server, client, "set", newParams, rb)
587 814
 	}
588 815
 }
589 816
 

+ 8
- 4
irc/services.go View File

@@ -18,6 +18,7 @@ import (
18 18
 type ircService struct {
19 19
 	Name           string
20 20
 	ShortName      string
21
+	prefix         string
21 22
 	CommandAliases []string
22 23
 	Commands       map[string]*serviceCommand
23 24
 	HelpBanner     string
@@ -31,6 +32,7 @@ type serviceCommand struct {
31 32
 	help         string
32 33
 	helpShort    string
33 34
 	authRequired bool
35
+	hidden       bool
34 36
 	enabled      func(*Config) bool // is this command enabled in the server config?
35 37
 	minParams    int
36 38
 	maxParams    int // split into at most n params, with last param containing remaining unsplit text
@@ -139,7 +141,7 @@ func servicePrivmsgHandler(service *ircService, server *Server, client *Client,
139 141
 func serviceRunCommand(service *ircService, server *Server, client *Client, cmd *serviceCommand, commandName string, params []string, rb *ResponseBuffer) {
140 142
 	nick := rb.target.Nick()
141 143
 	sendNotice := func(notice string) {
142
-		rb.Add(nil, service.Name, "NOTICE", nick, notice)
144
+		rb.Add(nil, service.prefix, "NOTICE", nick, notice)
143 145
 	}
144 146
 
145 147
 	if cmd == nil {
@@ -180,7 +182,7 @@ func serviceHelpHandler(service *ircService, server *Server, client *Client, par
180 182
 	nick := rb.target.Nick()
181 183
 	config := server.Config()
182 184
 	sendNotice := func(notice string) {
183
-		rb.Add(nil, service.Name, "NOTICE", nick, notice)
185
+		rb.Add(nil, service.prefix, "NOTICE", nick, notice)
184 186
 	}
185 187
 
186 188
 	sendNotice(ircfmt.Unescape(fmt.Sprintf("*** $b%s HELP$b ***", service.Name)))
@@ -194,7 +196,7 @@ func serviceHelpHandler(service *ircService, server *Server, client *Client, par
194 196
 			if 0 < len(commandInfo.capabs) && !client.HasRoleCapabs(commandInfo.capabs...) {
195 197
 				continue
196 198
 			}
197
-			if commandInfo.aliasOf != "" {
199
+			if commandInfo.aliasOf != "" || commandInfo.hidden {
198 200
 				continue // don't show help lines for aliases
199 201
 			}
200 202
 			if commandInfo.enabled != nil && !commandInfo.enabled(config) {
@@ -241,6 +243,8 @@ func initializeServices() {
241 243
 	oragonoServicesByCommandAlias = make(map[string]*ircService)
242 244
 
243 245
 	for serviceName, service := range OragonoServices {
246
+		service.prefix = fmt.Sprintf("%s!%s@localhost", service.Name, service.Name)
247
+
244 248
 		// make `/MSG ServiceName HELP` work correctly
245 249
 		service.Commands["help"] = &servHelpCmd
246 250
 
@@ -257,7 +261,7 @@ func initializeServices() {
257 261
 
258 262
 		// force devs to write a help entry for every command
259 263
 		for commandName, commandInfo := range service.Commands {
260
-			if commandInfo.aliasOf == "" && (commandInfo.help == "" || commandInfo.helpShort == "") {
264
+			if commandInfo.aliasOf == "" && !commandInfo.hidden && (commandInfo.help == "" || commandInfo.helpShort == "") {
261 265
 				log.Fatal(fmt.Sprintf("help entry missing for %s command %s", serviceName, commandName))
262 266
 			}
263 267
 		}

+ 1
- 1
oragono.yaml View File

@@ -134,7 +134,7 @@ server:
134 134
         # defaults to true when unset for that reason.
135 135
         force-trailing: true
136 136
 
137
-        # some clients (ZNC 1.6.x and lower, Pidgin 2.12 and lower, Adium) do not
137
+        # some clients (ZNC 1.6.x and lower, Pidgin 2.12 and lower) do not
138 138
         # respond correctly to SASL messages with the server name as a prefix:
139 139
         # https://github.com/znc/znc/issues/1212
140 140
         # this works around that bug, allowing them to use SASL.

Loading…
Cancel
Save