diff --git a/irc/accounts.go b/irc/accounts.go index a8cf8d0e..0168281a 100644 --- a/irc/accounts.go +++ b/irc/accounts.go @@ -187,6 +187,10 @@ func (am *AccountManager) EnforcementStatus(nick string) (account string, method defer am.RUnlock() account = am.nickToAccount[cfnick] + if account == "" { + method = NickReservationNone + return + } method = am.accountToMethod[account] // if they don't have a custom setting, or customization is disabled, use the default if method == NickReservationOptional || !config.Accounts.NickReservation.AllowCustomEnforcement { diff --git a/irc/chanserv.go b/irc/chanserv.go index bdf779c0..9b502161 100644 --- a/irc/chanserv.go +++ b/irc/chanserv.go @@ -15,7 +15,6 @@ import ( "github.com/goshuirc/irc-go/ircfmt" "github.com/oragono/oragono/irc/modes" "github.com/oragono/oragono/irc/sno" - "github.com/oragono/oragono/irc/utils" ) const chanservHelp = `ChanServ lets you register and manage channels. @@ -26,8 +25,8 @@ To see in-depth help for a specific ChanServ command, try: Here are the commands you can use: %s` -func chanregEnabled(server *Server) bool { - return server.ChannelRegistrationEnabled() +func chanregEnabled(config *Config) bool { + return config.Channels.Registration.Enabled } var ( @@ -41,6 +40,7 @@ this command if you're the founder of the channel.`, helpShort: `$bOP$b makes the given user (or yourself) a channel admin.`, authRequired: true, enabled: chanregEnabled, + minParams: 1, }, "register": { handler: csRegisterHandler, @@ -52,6 +52,7 @@ remembered.`, helpShort: `$bREGISTER$b lets you own a given channel.`, authRequired: true, enabled: chanregEnabled, + minParams: 1, }, "unregister": { handler: csUnregisterHandler, @@ -62,6 +63,7 @@ To prevent accidental unregistrations, a verification code is required; invoking the command without a code will display the necessary code.`, helpShort: `$bUNREGISTER$b deletes a channel registration.`, enabled: chanregEnabled, + minParams: 1, }, "drop": { aliasOf: "unregister", @@ -77,6 +79,7 @@ accounts and modes, use $bAMODE #channel$b. Note that users are always referenced by their registered account names, not their nicknames.`, helpShort: `$bAMODE$b modifies persistent mode settings for channel members.`, enabled: chanregEnabled, + minParams: 1, }, } ) @@ -86,8 +89,8 @@ func csNotice(rb *ResponseBuffer, text string) { rb.Add(nil, "ChanServ", "NOTICE", rb.target.Nick(), text) } -func csAmodeHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) { - channelName, modeChange := utils.ExtractParam(params) +func csAmodeHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { + channelName := params[0] channel := server.channels.Get(channelName) if channel == nil { @@ -98,7 +101,7 @@ func csAmodeHandler(server *Server, client *Client, command, params string, rb * return } - modeChanges, unknown := modes.ParseChannelModeChanges(strings.Fields(modeChange)...) + modeChanges, unknown := modes.ParseChannelModeChanges(params[1:]...) var change modes.ModeChange if len(modeChanges) > 1 || len(unknown) > 0 { csNotice(rb, client.t("Invalid mode change")) @@ -159,27 +162,13 @@ func csAmodeHandler(server *Server, client *Client, command, params string, rb * } } -func csOpHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) { - channelName, clientToOp := utils.ExtractParam(params) - - if channelName == "" { - csNotice(rb, ircfmt.Unescape(client.t("Syntax: $bOP #channel [nickname]$b"))) - return - } - - clientToOp = strings.TrimSpace(clientToOp) - - channelKey, err := CasefoldChannel(channelName) - if err != nil { - csNotice(rb, client.t("Channel name is not valid")) - return - } - - channelInfo := server.channels.Get(channelKey) +func csOpHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { + channelInfo := server.channels.Get(params[0]) if channelInfo == nil { csNotice(rb, client.t("Channel does not exist")) return } + channelName := channelInfo.Name() clientAccount := client.Account() if clientAccount == "" || clientAccount != channelInfo.Founder() { @@ -188,10 +177,9 @@ func csOpHandler(server *Server, client *Client, command, params string, rb *Res } var target *Client - if clientToOp != "" { - casefoldedNickname, err := CasefoldName(clientToOp) - target = server.clients.Get(casefoldedNickname) - if err != nil || target == nil { + if len(params) > 1 { + target = server.clients.Get(params[1]) + if target == nil { csNotice(rb, client.t("Could not find given client")) return } @@ -216,16 +204,13 @@ func csOpHandler(server *Server, client *Client, command, params string, rb *Res csNotice(rb, fmt.Sprintf(client.t("Successfully op'd in channel %s"), channelName)) - server.logger.Info("chanserv", fmt.Sprintf("Client %s op'd [%s] in channel %s", client.nick, clientToOp, channelName)) - server.snomasks.Send(sno.LocalChannels, fmt.Sprintf(ircfmt.Unescape("Client $c[grey][$r%s$c[grey]] CS OP'd $c[grey][$r%s$c[grey]] in channel $c[grey][$r%s$c[grey]]"), client.nickMaskString, clientToOp, channelName)) + tnick := target.Nick() + server.logger.Info("services", fmt.Sprintf("Client %s op'd [%s] in channel %s", client.Nick(), tnick, channelName)) + server.snomasks.Send(sno.LocalChannels, fmt.Sprintf(ircfmt.Unescape("Client $c[grey][$r%s$c[grey]] CS OP'd $c[grey][$r%s$c[grey]] in channel $c[grey][$r%s$c[grey]]"), client.NickMaskString(), tnick, channelName)) } -func csRegisterHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) { - channelName := strings.TrimSpace(params) - if channelName == "" { - csNotice(rb, ircfmt.Unescape(client.t("Syntax: $bREGISTER #channel$b"))) - return - } +func csRegisterHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { + channelName := params[0] channelKey, err := CasefoldChannel(channelName) if err != nil { @@ -251,7 +236,7 @@ func csRegisterHandler(server *Server, client *Client, command, params string, r csNotice(rb, fmt.Sprintf(client.t("Channel %s successfully registered"), channelName)) - server.logger.Info("chanserv", fmt.Sprintf("Client %s registered channel %s", client.nick, channelName)) + server.logger.Info("services", fmt.Sprintf("Client %s registered channel %s", client.nick, channelName)) server.snomasks.Send(sno.LocalChannels, fmt.Sprintf(ircfmt.Unescape("Channel registered $c[grey][$r%s$c[grey]] by $c[grey][$r%s$c[grey]]"), channelName, client.nickMaskString)) // give them founder privs @@ -266,8 +251,13 @@ func csRegisterHandler(server *Server, client *Client, command, params string, r } } -func csUnregisterHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) { - channelName, verificationCode := utils.ExtractParam(params) +func csUnregisterHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { + channelName := params[0] + var verificationCode string + if len(params) > 1 { + verificationCode = params[1] + } + channelKey, err := CasefoldChannel(channelName) if channelKey == "" || err != nil { csNotice(rb, client.t("Channel name is not valid")) diff --git a/irc/client_lookup_set.go b/irc/client_lookup_set.go index 387a357a..74bfb318 100644 --- a/irc/client_lookup_set.go +++ b/irc/client_lookup_set.go @@ -129,7 +129,7 @@ func (clients *ClientManager) SetNick(client *Client, newNick string) error { if currentNewEntry != nil && currentNewEntry != client { return errNicknameInUse } - if method == NickReservationStrict && reservedAccount != client.Account() { + if method == NickReservationStrict && reservedAccount != "" && reservedAccount != client.Account() { return errNicknameReserved } clients.removeInternal(client) diff --git a/irc/hostserv.go b/irc/hostserv.go index 73faa55f..d66d9c23 100644 --- a/irc/hostserv.go +++ b/irc/hostserv.go @@ -7,10 +7,7 @@ import ( "errors" "fmt" "regexp" - "strings" "time" - - "github.com/oragono/oragono/irc/utils" ) const hostservHelp = `HostServ lets you manage your vhost (i.e., the string displayed @@ -29,13 +26,12 @@ var ( defaultValidVhostRegex = regexp.MustCompile(`^[0-9A-Za-z.\-_/]+$`) ) -func hostservEnabled(server *Server) bool { - return server.AccountConfig().VHosts.Enabled +func hostservEnabled(config *Config) bool { + return config.Accounts.VHosts.Enabled } -func hostservRequestsEnabled(server *Server) bool { - ac := server.AccountConfig() - return ac.VHosts.Enabled && ac.VHosts.UserRequests.Enabled +func hostservRequestsEnabled(config *Config) bool { + return config.Accounts.VHosts.Enabled && config.Accounts.VHosts.UserRequests.Enabled } var ( @@ -67,16 +63,16 @@ then be approved by a server operator.`, helpShort: `$bREQUEST$b requests a new vhost, pending operator approval.`, authRequired: true, enabled: hostservRequestsEnabled, + minParams: 1, }, "status": { handler: hsStatusHandler, - help: `Syntax: $bSTATUS$b + help: `Syntax: $bSTATUS [user]$b STATUS displays your current vhost, if any, and the status of your most recent -request for a new one.`, - helpShort: `$bSTATUS$b shows your vhost and request status.`, - authRequired: true, - enabled: hostservEnabled, +request for a new one. A server operator can view someone else's status.`, + helpShort: `$bSTATUS$b shows your vhost and request status.`, + enabled: hostservEnabled, }, "set": { handler: hsSetHandler, @@ -86,6 +82,7 @@ SET sets a user's vhost, bypassing the request system.`, helpShort: `$bSET$b sets a user's vhost.`, capabs: []string{"vhosts"}, enabled: hostservEnabled, + minParams: 2, }, "del": { handler: hsSetHandler, @@ -95,6 +92,7 @@ DEL deletes a user's vhost.`, helpShort: `$bDEL$b deletes a user's vhost.`, capabs: []string{"vhosts"}, enabled: hostservEnabled, + minParams: 1, }, "waiting": { handler: hsWaitingHandler, @@ -114,6 +112,7 @@ APPROVE approves a user's vhost request.`, helpShort: `$bAPPROVE$b approves a user's vhost request.`, capabs: []string{"vhosts"}, enabled: hostservEnabled, + minParams: 1, }, "reject": { handler: hsRejectHandler, @@ -124,6 +123,8 @@ for the rejection.`, helpShort: `$bREJECT$b rejects a user's vhost request.`, capabs: []string{"vhosts"}, enabled: hostservEnabled, + minParams: 1, + maxParams: 2, }, } ) @@ -146,7 +147,7 @@ func hsNotifyChannel(server *Server, message string) { } } -func hsOnOffHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) { +func hsOnOffHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { enable := false if command == "on" { enable = true @@ -162,8 +163,8 @@ func hsOnOffHandler(server *Server, client *Client, command, params string, rb * } } -func hsRequestHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) { - vhost, _ := utils.ExtractParam(params) +func hsRequestHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { + vhost := params[0] if validateVhost(server, vhost, false) != nil { hsNotice(rb, client.t("Invalid vhost")) return @@ -195,12 +196,24 @@ func hsRequestHandler(server *Server, client *Client, command, params string, rb } } -func hsStatusHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) { - accountName := client.Account() +func hsStatusHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { + var accountName string + if len(params) > 0 { + if !client.HasRoleCapabs("vhosts") { + hsNotice(rb, client.t("Command restricted")) + return + } + accountName = params[0] + } else { + accountName = client.Account() + } + account, err := server.accounts.LoadAccount(accountName) if err != nil { - server.logger.Warning("internal", "error loading account info", accountName, err.Error()) - hsNotice(rb, client.t("An error occurred")) + if err != errAccountDoesNotExist { + server.logger.Warning("internal", "error loading account info", accountName, err.Error()) + } + hsNotice(rb, client.t("No such account")) return } @@ -232,23 +245,18 @@ func validateVhost(server *Server, vhost string, oper bool) error { return nil } -func hsSetHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) { - var user, vhost string - user, params = utils.ExtractParam(params) - if user == "" { - hsNotice(rb, client.t("A user is required")) - return - } +func hsSetHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { + user := params[0] + var vhost string + if command == "set" { - vhost, _ = utils.ExtractParam(params) + vhost = params[1] if validateVhost(server, vhost, true) != nil { hsNotice(rb, client.t("Invalid vhost")) return } - } else if command != "del" { - server.logger.Warning("internal", "invalid hostserv set command", command) - return } + // else: command == "del", vhost == "" _, err := server.accounts.VHostSet(user, vhost) if err != nil { @@ -260,7 +268,7 @@ func hsSetHandler(server *Server, client *Client, command, params string, rb *Re } } -func hsWaitingHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) { +func hsWaitingHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { requests, total := server.accounts.VHostListRequests(10) hsNotice(rb, fmt.Sprintf(client.t("There are %d pending requests for vhosts (%d displayed)"), total, len(requests))) for i, request := range requests { @@ -268,12 +276,8 @@ func hsWaitingHandler(server *Server, client *Client, command, params string, rb } } -func hsApproveHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) { - user, _ := utils.ExtractParam(params) - if user == "" { - hsNotice(rb, client.t("A user is required")) - return - } +func hsApproveHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { + user := params[0] vhostInfo, err := server.accounts.VHostApprove(user) if err != nil { @@ -288,13 +292,12 @@ func hsApproveHandler(server *Server, client *Client, command, params string, rb } } -func hsRejectHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) { - user, params := utils.ExtractParam(params) - if user == "" { - hsNotice(rb, client.t("A user is required")) - return +func hsRejectHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { + var reason string + user := params[0] + if len(params) > 1 { + reason = params[1] } - reason := strings.TrimSpace(params) vhostInfo, err := server.accounts.VHostReject(user, reason) if err != nil { diff --git a/irc/nickname.go b/irc/nickname.go index d1df3153..370d3ec4 100644 --- a/irc/nickname.go +++ b/irc/nickname.go @@ -15,9 +15,10 @@ import ( ) var ( + // anything added here MUST be casefolded: restrictedNicknames = map[string]bool{ "=scene=": true, // used for rp commands - "HistServ": true, // TODO(slingamn) this should become a real service + "histserv": true, // TODO(slingamn) this should become a real service } ) diff --git a/irc/nickserv.go b/irc/nickserv.go index 936f0819..8acb1083 100644 --- a/irc/nickserv.go +++ b/irc/nickserv.go @@ -8,25 +8,22 @@ import ( "strings" "github.com/goshuirc/irc-go/ircfmt" - "github.com/oragono/oragono/irc/utils" ) // "enabled" callbacks for specific nickserv commands -func servCmdRequiresAccreg(server *Server) bool { - return server.AccountConfig().Registration.Enabled +func servCmdRequiresAccreg(config *Config) bool { + return config.Accounts.Registration.Enabled } -func servCmdRequiresAuthEnabled(server *Server) bool { - return server.AccountConfig().AuthenticationEnabled +func servCmdRequiresAuthEnabled(config *Config) bool { + return config.Accounts.AuthenticationEnabled } -func nsGroupEnabled(server *Server) bool { - conf := server.Config() - return conf.Accounts.AuthenticationEnabled && conf.Accounts.NickReservation.Enabled +func nsGroupEnabled(config *Config) bool { + return config.Accounts.AuthenticationEnabled && config.Accounts.NickReservation.Enabled } -func nsEnforceEnabled(server *Server) bool { - config := server.Config() +func nsEnforceEnabled(config *Config) bool { return config.Accounts.NickReservation.Enabled && config.Accounts.NickReservation.AllowCustomEnforcement } @@ -73,6 +70,7 @@ GHOST disconnects the given user from the network if they're logged in with the same user account, letting you reclaim your nickname.`, helpShort: `$bGHOST$b reclaims your nickname.`, authRequired: true, + minParams: 1, }, "group": { handler: nsGroupHandler, @@ -84,7 +82,6 @@ users from changing to it (or forcing them to rename).`, enabled: nsGroupEnabled, authRequired: true, }, - "identify": { handler: nsIdentifyHandler, help: `Syntax: $bIDENTIFY [password]$b @@ -92,6 +89,7 @@ users from changing to it (or forcing them to rename).`, IDENTIFY lets you login to the given username using either password auth, or certfp (your client certificate) if a password is not given.`, helpShort: `$bIDENTIFY$b lets you login to your account.`, + minParams: 1, }, "info": { handler: nsInfoHandler, @@ -113,6 +111,7 @@ If the password is left out, your account will be registered to your TLS client certificate (and you will need to use that certificate to login in future).`, helpShort: `$bREGISTER$b lets you register a user account.`, enabled: servCmdRequiresAccreg, + minParams: 2, }, "sadrop": { handler: nsDropHandler, @@ -122,6 +121,7 @@ SADROP forcibly de-links the given nickname from the attached user account.`, helpShort: `$bSADROP$b forcibly de-links the given nickname from its user account.`, capabs: []string{"accreg"}, enabled: servCmdRequiresAccreg, + minParams: 1, }, "unregister": { handler: nsUnregisterHandler, @@ -132,6 +132,8 @@ IRC operator with the correct permissions). To prevent accidental unregistrations, a verification code is required; invoking the command without a code will display the necessary code.`, helpShort: `$bUNREGISTER$b lets you delete your user account.`, + enabled: servCmdRequiresAccreg, + minParams: 1, }, "verify": { handler: nsVerifyHandler, @@ -141,6 +143,7 @@ VERIFY lets you complete an account registration, if the server requires email or other verification.`, helpShort: `$bVERIFY$b lets you complete account registration.`, enabled: servCmdRequiresAccreg, + minParams: 2, }, "passwd": { handler: nsPasswdHandler, @@ -153,6 +156,7 @@ with the correct permissions, you can use PASSWD to reset someone else's password by supplying their username and then the desired password.`, helpShort: `$bPASSWD$b lets you change your password.`, enabled: servCmdRequiresAuthEnabled, + minParams: 2, }, } ) @@ -162,9 +166,14 @@ func nsNotice(rb *ResponseBuffer, text string) { rb.Add(nil, "NickServ", "NOTICE", rb.target.Nick(), text) } -func nsDropHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) { +func nsDropHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { sadrop := command == "sadrop" - nick, _ := utils.ExtractParam(params) + var nick string + if len(params) > 0 { + nick = params[0] + } else { + nick = client.NickCasefolded() + } err := server.accounts.SetNickReserved(client, nick, sadrop, false) if err == nil { @@ -173,15 +182,13 @@ func nsDropHandler(server *Server, client *Client, command, params string, rb *R nsNotice(rb, client.t("You're not logged into an account")) } else if err == errAccountCantDropPrimaryNick { nsNotice(rb, client.t("You can't ungroup your primary nickname (try unregistering your account instead)")) - } else if err == errNicknameReserved { - nsNotice(rb, client.t("That nickname is already reserved by someone else")) } else { nsNotice(rb, client.t("Could not ungroup nick")) } } -func nsGhostHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) { - nick, _ := utils.ExtractParam(params) +func nsGhostHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { + nick := params[0] ghost := server.clients.Get(nick) if ghost == nil { @@ -207,7 +214,7 @@ func nsGhostHandler(server *Server, client *Client, command, params string, rb * ghost.destroy(false) } -func nsGroupHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) { +func nsGroupHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { nick := client.NickCasefolded() err := server.accounts.SetNickReserved(client, nick, false, true) if err == nil { @@ -230,13 +237,17 @@ func nsLoginThrottleCheck(client *Client, rb *ResponseBuffer) (success bool) { return true } -func nsIdentifyHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) { +func nsIdentifyHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { loginSuccessful := false - username, passphrase := utils.ExtractParam(params) + username := params[0] + var passphrase string + if len(params) > 1 { + passphrase = params[1] + } // try passphrase - if username != "" && passphrase != "" { + if passphrase != "" { if !nsLoginThrottleCheck(client, rb) { return } @@ -257,18 +268,23 @@ func nsIdentifyHandler(server *Server, client *Client, command, params string, r } } -func nsInfoHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) { - nick, _ := utils.ExtractParam(params) - - if nick == "" { - nick = client.Nick() - } - - accountName := nick - if server.AccountConfig().NickReservation.Enabled { - accountName = server.accounts.NickToAccount(nick) +func nsInfoHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { + var accountName string + if len(params) > 0 { + nick := params[0] + if server.AccountConfig().NickReservation.Enabled { + accountName = server.accounts.NickToAccount(nick) + if accountName == "" { + nsNotice(rb, client.t("That nickname is not registered")) + return + } + } else { + accountName = nick + } + } else { + accountName = client.Account() if accountName == "" { - nsNotice(rb, client.t("That nickname is not registered")) + nsNotice(rb, client.t("You're not logged into an account")) return } } @@ -276,6 +292,7 @@ func nsInfoHandler(server *Server, client *Client, command, params string, rb *R account, err := server.accounts.LoadAccount(accountName) if err != nil || !account.Verified { nsNotice(rb, client.t("Account does not exist")) + return } nsNotice(rb, fmt.Sprintf(client.t("Account: %s"), account.Name)) @@ -287,10 +304,13 @@ func nsInfoHandler(server *Server, client *Client, command, params string, rb *R } } -func nsRegisterHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) { +func nsRegisterHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { // get params - username, afterUsername := utils.ExtractParam(params) - email, passphrase := utils.ExtractParam(afterUsername) + username, email := params[0], params[1] + var passphrase string + if len(params) > 0 { + passphrase = params[2] + } if !server.AccountConfig().Registration.Enabled { nsNotice(rb, client.t("Account registration has been disabled")) @@ -366,12 +386,11 @@ func nsRegisterHandler(server *Server, client *Client, command, params string, r } } -func nsUnregisterHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) { - username, verificationCode := utils.ExtractParam(params) - - if !server.AccountConfig().Registration.Enabled { - nsNotice(rb, client.t("Account registration has been disabled")) - return +func nsUnregisterHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { + username := params[0] + var verificationCode string + if len(params) > 1 { + verificationCode = params[1] } if username == "" { @@ -415,9 +434,8 @@ func nsUnregisterHandler(server *Server, client *Client, command, params string, } } -func nsVerifyHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) { - username, code := utils.ExtractParam(params) - +func nsVerifyHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { + username, code := params[0], params[1] err := server.accounts.Verify(client, username, code) var errorMessage string @@ -435,7 +453,7 @@ func nsVerifyHandler(server *Server, client *Client, command, params string, rb sendSuccessfulRegResponse(client, rb, true) } -func nsPasswdHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) { +func nsPasswdHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { var target string var newPassword string var errorMessage string @@ -445,27 +463,26 @@ func nsPasswdHandler(server *Server, client *Client, command, params string, rb return } - fields := strings.Fields(params) - switch len(fields) { + switch len(params) { case 2: if !hasPrivs { errorMessage = "Insufficient privileges" } else { - target, newPassword = fields[0], fields[1] + target, newPassword = params[0], params[1] } case 3: target = client.Account() if target == "" { errorMessage = "You're not logged into an account" - } else if fields[1] != fields[2] { + } else if params[1] != params[2] { errorMessage = "Passwords do not match" } else { // check that they correctly supplied the preexisting password - _, err := server.accounts.checkPassphrase(target, fields[0]) + _, err := server.accounts.checkPassphrase(target, params[0]) if err != nil { errorMessage = "Password incorrect" } else { - newPassword = fields[1] + newPassword = params[1] } } default: @@ -486,14 +503,12 @@ func nsPasswdHandler(server *Server, client *Client, command, params string, rb } } -func nsEnforceHandler(server *Server, client *Client, command, params string, rb *ResponseBuffer) { - arg := strings.TrimSpace(params) - - if arg == "" { +func nsEnforceHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { + if len(params) == 0 { status := server.accounts.getStoredEnforcementStatus(client.Account()) nsNotice(rb, fmt.Sprintf(client.t("Your current nickname enforcement is: %s"), status)) } else { - method, err := nickReservationFromString(arg) + method, err := nickReservationFromString(params[0]) if err != nil { nsNotice(rb, client.t("Invalid parameters")) return diff --git a/irc/services.go b/irc/services.go index 674b84b1..5947458e 100644 --- a/irc/services.go +++ b/irc/services.go @@ -11,7 +11,6 @@ import ( "github.com/goshuirc/irc-go/ircfmt" "github.com/goshuirc/irc-go/ircmsg" - "github.com/oragono/oragono/irc/utils" ) @@ -28,11 +27,13 @@ type ircService struct { type serviceCommand struct { aliasOf string // marks this command as an alias of another capabs []string // oper capabs the given user has to have to access this command - handler func(server *Server, client *Client, command, params string, rb *ResponseBuffer) + handler func(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) help string helpShort string authRequired bool - enabled func(*Server) bool // is this command enabled in the server config? + enabled func(*Config) bool // is this command enabled in the server config? + minParams int + maxParams int // split into at most n params, with last param containing remaining unsplit text } // looks up a command in the table of command definitions for a service, resolving aliases @@ -90,7 +91,7 @@ HELP returns information on the given command.`, helpShort: `$bHELP$b shows in-depth information about commands.`, } -// this handles IRC commands like `/NICKSERV INFO`, translating into `/MSG NICKSERV INFO` +// generic handler for IRC commands like `/NICKSERV INFO` func serviceCmdHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { service, ok := oragonoServicesByCommandAlias[msg.Command] if !ok { @@ -98,28 +99,60 @@ func serviceCmdHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb return false } - fakePrivmsg := strings.Join(msg.Params, " ") - servicePrivmsgHandler(service, server, client, fakePrivmsg, rb) + if len(msg.Params) == 0 { + return false + } + commandName := strings.ToLower(msg.Params[0]) + params := msg.Params[1:] + cmd := lookupServiceCommand(service.Commands, commandName) + // for a maxParams command, join all final parameters together if necessary + if cmd != nil && cmd.maxParams != 0 && cmd.maxParams < len(params) { + newParams := make([]string, cmd.maxParams) + copy(newParams, params[:cmd.maxParams-1]) + newParams[cmd.maxParams-1] = strings.Join(params[cmd.maxParams-1:], " ") + params = newParams + } + serviceRunCommand(service, server, client, cmd, commandName, params, rb) return false } -// generic handler for service PRIVMSG +// generic handler for service PRIVMSG, like `/msg NickServ INFO` func servicePrivmsgHandler(service *ircService, server *Server, client *Client, message string, rb *ResponseBuffer) { - commandName, params := utils.ExtractParam(message) - commandName = strings.ToLower(commandName) + params := strings.Fields(message) + if len(params) == 0 { + return + } + // look up the service command to see how to parse it + commandName := strings.ToLower(params[0]) + cmd := lookupServiceCommand(service.Commands, commandName) + // reparse if needed + if cmd != nil && cmd.maxParams != 0 { + params = utils.FieldsN(message, cmd.maxParams+1)[1:] + } else { + params = params[1:] + } + serviceRunCommand(service, server, client, cmd, commandName, params, rb) +} + +// actually execute a service command +func serviceRunCommand(service *ircService, server *Server, client *Client, cmd *serviceCommand, commandName string, params []string, rb *ResponseBuffer) { nick := rb.target.Nick() sendNotice := func(notice string) { rb.Add(nil, service.Name, "NOTICE", nick, notice) } - cmd := lookupServiceCommand(service.Commands, commandName) if cmd == nil { sendNotice(fmt.Sprintf("%s /%s HELP", client.t("Unknown command. To see available commands, run"), service.ShortName)) return } - if cmd.enabled != nil && !cmd.enabled(server) { + if len(params) < cmd.minParams { + sendNotice(fmt.Sprintf(client.t("Invalid parameters. For usage, do /msg %s HELP %s"), service.Name, strings.ToUpper(commandName))) + return + } + + if cmd.enabled != nil && !cmd.enabled(server.Config()) { sendNotice(client.t("This command has been disabled by the server administrators")) return } @@ -143,15 +176,16 @@ func servicePrivmsgHandler(service *ircService, server *Server, client *Client, } // generic handler that displays help for service commands -func serviceHelpHandler(service *ircService, server *Server, client *Client, params string, rb *ResponseBuffer) { +func serviceHelpHandler(service *ircService, server *Server, client *Client, params []string, rb *ResponseBuffer) { nick := rb.target.Nick() + config := server.Config() sendNotice := func(notice string) { rb.Add(nil, service.Name, "NOTICE", nick, notice) } sendNotice(ircfmt.Unescape(fmt.Sprintf("*** $b%s HELP$b ***", service.Name))) - if params == "" { + if len(params) == 0 { // show general help var shownHelpLines sort.StringSlice var disabledCommands bool @@ -163,7 +197,7 @@ func serviceHelpHandler(service *ircService, server *Server, client *Client, par if commandInfo.aliasOf != "" { continue // don't show help lines for aliases } - if commandInfo.enabled != nil && !commandInfo.enabled(server) { + if commandInfo.enabled != nil && !commandInfo.enabled(config) { disabledCommands = true continue } @@ -187,7 +221,7 @@ func serviceHelpHandler(service *ircService, server *Server, client *Client, par sendNotice(line) } } else { - commandName := strings.ToLower(strings.TrimSpace(params)) + commandName := strings.ToLower(params[0]) commandInfo := lookupServiceCommand(service.Commands, commandName) if commandInfo == nil { sendNotice(client.t(fmt.Sprintf("Unknown command. To see available commands, run /%s HELP", service.ShortName))) diff --git a/irc/utils/args.go b/irc/utils/args.go index 966d7026..8f087423 100644 --- a/irc/utils/args.go +++ b/irc/utils/args.go @@ -3,19 +3,6 @@ package utils -import "strings" - -// ExtractParam extracts a parameter from the given string, returning the param and the rest of the string. -func ExtractParam(line string) (string, string) { - rawParams := strings.SplitN(strings.TrimSpace(line), " ", 2) - param0 := rawParams[0] - var param1 string - if 1 < len(rawParams) { - param1 = strings.TrimSpace(rawParams[1]) - } - return param0, param1 -} - // ArgsToStrings takes the arguments and splits them into a series of strings, // each argument separated by delim and each string bounded by maxLength. func ArgsToStrings(maxLength int, arguments []string, delim string) []string { diff --git a/irc/utils/fieldsn.go b/irc/utils/fieldsn.go new file mode 100644 index 00000000..67a24290 --- /dev/null +++ b/irc/utils/fieldsn.go @@ -0,0 +1,52 @@ +package utils + +// Copyright (c) 2014 Kevin Wallace +// Found here: https://github.com/kevinwallace/fieldsn +// Released under the MIT license +// XXX this implementation treats negative n as "return nil", +// unlike stdlib SplitN and friends, which treat it as "no limit" + +// Original source code below: + +// Package fieldsn implements FieldsN and FieldsFuncN, +// which are conspicuously missing from the strings package. + +import ( + "unicode" +) + +// FieldsN is like strings.Fields, but returns at most n fields, +// and the nth field includes any whitespace at the end of the string. +func FieldsN(s string, n int) []string { + return FieldsFuncN(s, unicode.IsSpace, n) +} + +// FieldsFuncN is like strings.FieldsFunc, but returns at most n fields, +// and the nth field includes any runes at the end of the string normally excluded by f. +func FieldsFuncN(s string, f func(rune) bool, n int) []string { + if n <= 0 { + return nil + } + + a := make([]string, 0, n) + na := 0 + fieldStart := -1 + for i, rune := range s { + if f(rune) { + if fieldStart >= 0 { + a = append(a, s[fieldStart:i]) + na++ + fieldStart = -1 + } + } else if fieldStart == -1 { + fieldStart = i + if na+1 == n { + break + } + } + } + if fieldStart >= 0 { + a = append(a, s[fieldStart:]) + } + return a +}