mirror of
https://github.com/ergochat/ergo.git
synced 2024-12-22 10:42:52 +01:00
Add very initial ChanServ and NickServ virtual clients
As well, add channel registration and re-applying founder privs on the first client joining the channel. I'm going to re-architect our modes system to better acocunt for this sort of change.
This commit is contained in:
parent
439331cfb8
commit
b33b217fab
@ -16,15 +16,6 @@ import (
|
||||
"github.com/tidwall/buntdb"
|
||||
)
|
||||
|
||||
const (
|
||||
keyAccountExists = "account.exists %s"
|
||||
keyAccountVerified = "account.verified %s"
|
||||
keyAccountName = "account.name %s" // stores the 'preferred name' of the account, not casemapped
|
||||
keyAccountRegTime = "account.registered.time %s"
|
||||
keyAccountCredentials = "account.credentials %s"
|
||||
keyCertToAccount = "account.creds.certfp %s"
|
||||
)
|
||||
|
||||
var (
|
||||
errAccountCreation = errors.New("Account could not be created")
|
||||
errCertfpAlreadyExists = errors.New("An account already exists with your certificate")
|
@ -17,6 +17,15 @@ import (
|
||||
"github.com/tidwall/buntdb"
|
||||
)
|
||||
|
||||
const (
|
||||
keyAccountExists = "account.exists %s"
|
||||
keyAccountVerified = "account.verified %s"
|
||||
keyAccountName = "account.name %s" // stores the 'preferred name' of the account, not casemapped
|
||||
keyAccountRegTime = "account.registered.time %s"
|
||||
keyAccountCredentials = "account.credentials %s"
|
||||
keyCertToAccount = "account.creds.certfp %s"
|
||||
)
|
||||
|
||||
var (
|
||||
// EnabledSaslMechanisms contains the SASL mechanisms that exist and that we support.
|
||||
// This can be moved to some other data structure/place if we need to load/unload mechs later.
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/DanielOaks/girc-go/ircmsg"
|
||||
"github.com/tidwall/buntdb"
|
||||
)
|
||||
|
||||
type Channel struct {
|
||||
@ -277,10 +278,27 @@ func (channel *Channel) Join(client *Client, key string) {
|
||||
client.channels.Add(channel)
|
||||
channel.members.Add(client)
|
||||
if len(channel.members) == 1 {
|
||||
client.server.registeredChannelsMutex.Lock()
|
||||
defer client.server.registeredChannelsMutex.Unlock()
|
||||
client.server.store.Update(func(tx *buntdb.Tx) error {
|
||||
chanReg := client.server.loadChannelNoMutex(tx, channel.nameCasefolded)
|
||||
|
||||
if chanReg == nil {
|
||||
channel.createdTime = time.Now()
|
||||
// // we should only do this on registered channels
|
||||
// channel.members[client][ChannelFounder] = true
|
||||
channel.members[client][ChannelOperator] = true
|
||||
} else {
|
||||
// we should only do this on registered channels
|
||||
if client.account != nil && client.account.Name == chanReg.Founder {
|
||||
channel.members[client][ChannelFounder] = true
|
||||
}
|
||||
channel.topic = chanReg.Topic
|
||||
channel.topicSetBy = chanReg.TopicSetBy
|
||||
channel.topicSetTime = chanReg.TopicSetTime
|
||||
channel.name = chanReg.Name
|
||||
channel.createdTime = chanReg.RegisteredAt
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if client.capabilities[ExtendedJoin] {
|
||||
|
90
irc/channelreg.go
Normal file
90
irc/channelreg.go
Normal file
@ -0,0 +1,90 @@
|
||||
// Copyright (c) 2016- Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the MIT license
|
||||
|
||||
package irc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/tidwall/buntdb"
|
||||
)
|
||||
|
||||
const (
|
||||
keyChannelExists = "channel.exists %s"
|
||||
keyChannelName = "channel.name %s" // stores the 'preferred name' of the channel, not casemapped
|
||||
keyChannelRegTime = "channel.registered.time %s"
|
||||
keyChannelFounder = "channel.founder %s"
|
||||
keyChannelTopic = "channel.topic %s"
|
||||
keyChannelTopicSetBy = "channel.topic.setby %s"
|
||||
keyChannelTopicSetTime = "channel.topic.settime %s"
|
||||
)
|
||||
|
||||
var (
|
||||
errChanExists = errors.New("Channel already exists")
|
||||
)
|
||||
|
||||
// RegisteredChannel holds details about a given registered channel.
|
||||
type RegisteredChannel struct {
|
||||
// Name of the channel.
|
||||
Name string
|
||||
// RegisteredAt represents the time that the channel was registered.
|
||||
RegisteredAt time.Time
|
||||
// Founder indicates the founder of the channel.
|
||||
Founder string
|
||||
// Topic represents the channel topic.
|
||||
Topic string
|
||||
// TopicSetBy represents the host that set the topic.
|
||||
TopicSetBy string
|
||||
// TopicSetTime represents the time the topic was set.
|
||||
TopicSetTime time.Time
|
||||
}
|
||||
|
||||
// loadChannelNoMutex loads a channel from the store.
|
||||
func (server *Server) loadChannelNoMutex(tx *buntdb.Tx, channelKey string) *RegisteredChannel {
|
||||
// return loaded chan if it already exists
|
||||
if server.registeredChannels[channelKey] != nil {
|
||||
return server.registeredChannels[channelKey]
|
||||
}
|
||||
_, err := tx.Get(fmt.Sprintf(keyChannelExists, channelKey))
|
||||
if err == buntdb.ErrNotFound {
|
||||
// chan does not already exist, return
|
||||
return nil
|
||||
}
|
||||
|
||||
// channel exists, load it
|
||||
name, _ := tx.Get(fmt.Sprintf(keyChannelName, channelKey))
|
||||
regTime, _ := tx.Get(fmt.Sprintf(keyChannelRegTime, channelKey))
|
||||
regTimeInt, _ := strconv.ParseInt(regTime, 10, 64)
|
||||
founder, _ := tx.Get(fmt.Sprintf(keyChannelFounder, channelKey))
|
||||
topic, _ := tx.Get(fmt.Sprintf(keyChannelTopic, channelKey))
|
||||
topicSetBy, _ := tx.Get(fmt.Sprintf(keyChannelTopicSetBy, channelKey))
|
||||
topicSetTime, _ := tx.Get(fmt.Sprintf(keyChannelTopicSetTime, channelKey))
|
||||
topicSetTimeInt, _ := strconv.ParseInt(topicSetTime, 10, 64)
|
||||
|
||||
chanInfo := RegisteredChannel{
|
||||
Name: name,
|
||||
RegisteredAt: time.Unix(regTimeInt, 0),
|
||||
Founder: founder,
|
||||
Topic: topic,
|
||||
TopicSetBy: topicSetBy,
|
||||
TopicSetTime: time.Unix(topicSetTimeInt, 0),
|
||||
}
|
||||
server.registeredChannels[channelKey] = &chanInfo
|
||||
|
||||
return &chanInfo
|
||||
}
|
||||
|
||||
// saveChannelNoMutex saves a channel to the store.
|
||||
func (server *Server) saveChannelNoMutex(tx *buntdb.Tx, channelKey string, channelInfo RegisteredChannel) {
|
||||
tx.Set(fmt.Sprintf(keyChannelExists, channelKey), "1", nil)
|
||||
tx.Set(fmt.Sprintf(keyChannelName, channelKey), channelInfo.Name, nil)
|
||||
tx.Set(fmt.Sprintf(keyChannelRegTime, channelKey), strconv.FormatInt(channelInfo.RegisteredAt.Unix(), 10), nil)
|
||||
tx.Set(fmt.Sprintf(keyChannelFounder, channelKey), channelInfo.Founder, nil)
|
||||
tx.Set(fmt.Sprintf(keyChannelTopic, channelKey), channelInfo.Topic, nil)
|
||||
tx.Set(fmt.Sprintf(keyChannelTopicSetBy, channelKey), channelInfo.TopicSetBy, nil)
|
||||
tx.Set(fmt.Sprintf(keyChannelTopicSetTime, channelKey), strconv.FormatInt(channelInfo.TopicSetTime.Unix(), 10), nil)
|
||||
server.registeredChannels[channelKey] = &channelInfo
|
||||
}
|
119
irc/chanserv.go
Normal file
119
irc/chanserv.go
Normal file
@ -0,0 +1,119 @@
|
||||
// Copyright (c) 2017 Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the MIT license
|
||||
|
||||
package irc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/DanielOaks/girc-go/ircmsg"
|
||||
"github.com/tidwall/buntdb"
|
||||
)
|
||||
|
||||
// csHandler handles the /CS and /CHANSERV commands
|
||||
func csHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
|
||||
server.chanservReceivePrivmsg(client, strings.Join(msg.Params, " "))
|
||||
return false
|
||||
}
|
||||
|
||||
func (server *Server) chanservReceiveNotice(client *Client, message string) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
// ChanServNotice sends the client a notice from ChanServ.
|
||||
func (client *Client) ChanServNotice(text string) {
|
||||
client.Send(nil, fmt.Sprintf("ChanServ!services@%s", client.server.name), "NOTICE", client.nick, text)
|
||||
}
|
||||
|
||||
func (server *Server) chanservReceivePrivmsg(client *Client, message string) {
|
||||
var params []string
|
||||
for _, p := range strings.Split(message, " ") {
|
||||
if len(p) > 0 {
|
||||
params = append(params, p)
|
||||
}
|
||||
}
|
||||
if len(params) < 1 {
|
||||
client.ChanServNotice("You need to run a command")
|
||||
//TODO(dan): dump CS help here
|
||||
return
|
||||
}
|
||||
|
||||
command := strings.ToLower(params[0])
|
||||
server.logger.Debug("chanserv", fmt.Sprintf("Client %s ran command %s", client.nick, command))
|
||||
|
||||
if command == "register" {
|
||||
if len(params) < 2 {
|
||||
client.ChanServNotice("Syntax: REGISTER <channel>")
|
||||
return
|
||||
}
|
||||
|
||||
server.registeredChannelsMutex.Lock()
|
||||
defer server.registeredChannelsMutex.Unlock()
|
||||
|
||||
channelName := params[1]
|
||||
channelKey, err := CasefoldChannel(channelName)
|
||||
if err != nil {
|
||||
client.ChanServNotice("Channel name is not valid")
|
||||
return
|
||||
}
|
||||
|
||||
channelInfo := server.channels.Get(channelKey)
|
||||
if channelInfo == nil {
|
||||
client.ChanServNotice("You must be an oper on the channel to register it")
|
||||
return
|
||||
}
|
||||
|
||||
if !channelInfo.ClientIsAtLeast(client, ChannelOperator) {
|
||||
client.ChanServNotice("You must be an oper on the channel to register it")
|
||||
return
|
||||
}
|
||||
|
||||
server.store.Update(func(tx *buntdb.Tx) error {
|
||||
currentChan := server.loadChannelNoMutex(tx, channelKey)
|
||||
if currentChan != nil {
|
||||
client.ChanServNotice("Channel is already registered")
|
||||
return nil
|
||||
}
|
||||
|
||||
account := client.account
|
||||
if account == nil {
|
||||
client.ChanServNotice("You must be logged in to register a channel")
|
||||
return nil
|
||||
}
|
||||
|
||||
chanRegInfo := RegisteredChannel{
|
||||
Name: channelName,
|
||||
RegisteredAt: time.Now(),
|
||||
Founder: account.Name,
|
||||
Topic: channelInfo.topic,
|
||||
TopicSetBy: channelInfo.topicSetBy,
|
||||
TopicSetTime: channelInfo.topicSetTime,
|
||||
}
|
||||
server.saveChannelNoMutex(tx, channelKey, chanRegInfo)
|
||||
|
||||
client.ChanServNotice(fmt.Sprintf("Channel %s successfully registered", channelName))
|
||||
|
||||
server.logger.Info("chanserv", fmt.Sprintf("Client %s registered channel %s", client.nick, channelName))
|
||||
|
||||
channelInfo.membersMutex.Lock()
|
||||
defer channelInfo.membersMutex.Unlock()
|
||||
|
||||
// give them founder privs
|
||||
change := channelInfo.applyModeMemberNoMutex(client, ChannelFounder, Add, client.nickCasefolded)
|
||||
if change != nil {
|
||||
//TODO(dan): we should change the name of String and make it return a slice here
|
||||
//TODO(dan): unify this code with code in modes.go
|
||||
args := append([]string{channelName}, strings.Split(change.String(), " ")...)
|
||||
for member := range channelInfo.members {
|
||||
member.Send(nil, fmt.Sprintf("ChanServ!services@%s", client.server.name), "MODE", args...)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
} else {
|
||||
client.ChanServNotice("Sorry, I don't know that command")
|
||||
}
|
||||
}
|
@ -74,6 +74,14 @@ var Commands = map[string]Command{
|
||||
usablePreReg: true,
|
||||
minParams: 1,
|
||||
},
|
||||
"CHANSERV": {
|
||||
handler: csHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"CS": {
|
||||
handler: csHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"DEBUG": {
|
||||
handler: debugHandler,
|
||||
minParams: 1,
|
||||
@ -143,6 +151,10 @@ var Commands = map[string]Command{
|
||||
usablePreReg: true,
|
||||
minParams: 1,
|
||||
},
|
||||
"NICKSERV": {
|
||||
handler: nsHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"NOTICE": {
|
||||
handler: noticeHandler,
|
||||
minParams: 2,
|
||||
@ -155,6 +167,10 @@ var Commands = map[string]Command{
|
||||
handler: npcaHandler,
|
||||
minParams: 3,
|
||||
},
|
||||
"NS": {
|
||||
handler: nsHandler,
|
||||
minParams: 1,
|
||||
},
|
||||
"OPER": {
|
||||
handler: operHandler,
|
||||
minParams: 2,
|
||||
|
20
irc/help.go
20
irc/help.go
@ -84,6 +84,16 @@ longer away.`,
|
||||
Used in capability negotiation. See the IRCv3 specs for more info:
|
||||
http://ircv3.net/specs/core/capability-negotiation-3.1.html
|
||||
http://ircv3.net/specs/core/capability-negotiation-3.2.html`,
|
||||
},
|
||||
"chanserv": {
|
||||
text: `CHANSERV <subcommand> [params]
|
||||
|
||||
ChanServ controls channel registrations.`,
|
||||
},
|
||||
"cs": {
|
||||
text: `CS <subcommand> [params]
|
||||
|
||||
ChanServ controls channel registrations.`,
|
||||
},
|
||||
"debug": {
|
||||
oper: true,
|
||||
@ -239,6 +249,11 @@ view the channel membership prefixes supported by this server, see the help for
|
||||
text: `NICK <newnick>
|
||||
|
||||
Sets your nickname to the new given one.`,
|
||||
},
|
||||
"nickserv": {
|
||||
text: `NICKSERV <subcommand> [params]
|
||||
|
||||
NickServ controls accounts and user registrations.`,
|
||||
},
|
||||
"notice": {
|
||||
text: `NOTICE <target>{,<target>} <text to be sent>
|
||||
@ -258,6 +273,11 @@ Requires the roleplay mode (+E) to be set on the target.`,
|
||||
The NPC command is used to send an action to the target as the source.
|
||||
|
||||
Requires the roleplay mode (+E) to be set on the target.`,
|
||||
},
|
||||
"ns": {
|
||||
text: `NS <subcommand> [params]
|
||||
|
||||
NickServ controls accounts and user registrations.`,
|
||||
},
|
||||
"oper": {
|
||||
text: `OPER <name> <password>
|
||||
|
@ -11,6 +11,14 @@ import (
|
||||
"github.com/DanielOaks/girc-go/ircmsg"
|
||||
)
|
||||
|
||||
var (
|
||||
restrictedNicknames = map[string]bool{
|
||||
"=scene=": true, // used for rp commands
|
||||
"chanserv": true,
|
||||
"nickserv": true,
|
||||
}
|
||||
)
|
||||
|
||||
// NICK <nickname>
|
||||
func nickHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
|
||||
if !client.authorized {
|
||||
@ -26,7 +34,7 @@ func nickHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
if err != nil || len(nicknameRaw) > server.limits.NickLen || nickname == "=scene=" {
|
||||
if err != nil || len(nicknameRaw) > server.limits.NickLen || restrictedNicknames[nickname] {
|
||||
client.Send(nil, server.name, ERR_ERRONEUSNICKNAME, client.nick, nicknameRaw, "Erroneous nickname")
|
||||
return false
|
||||
}
|
||||
@ -70,7 +78,7 @@ func sanickHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
if oerr != nil || err != nil || len(strings.TrimSpace(msg.Params[1])) > server.limits.NickLen || nickname == "=scene=" {
|
||||
if oerr != nil || err != nil || len(strings.TrimSpace(msg.Params[1])) > server.limits.NickLen || restrictedNicknames[nickname] {
|
||||
client.Send(nil, server.name, ERR_ERRONEUSNICKNAME, client.nick, msg.Params[0], "Erroneous nickname")
|
||||
return false
|
||||
}
|
||||
|
24
irc/nickserv.go
Normal file
24
irc/nickserv.go
Normal file
@ -0,0 +1,24 @@
|
||||
// Copyright (c) 2017 Daniel Oaks <daniel@danieloaks.net>
|
||||
// released under the MIT license
|
||||
|
||||
package irc
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/DanielOaks/girc-go/ircmsg"
|
||||
)
|
||||
|
||||
// nsHandler handles the /NS and /NICKSERV commands
|
||||
func nsHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
|
||||
server.nickservReceivePrivmsg(client, strings.Join(msg.Params, " "))
|
||||
return false
|
||||
}
|
||||
|
||||
func (server *Server) nickservReceiveNotice(client *Client, message string) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
func (server *Server) nickservReceivePrivmsg(client *Client, message string) {
|
||||
client.Notice("NickServ is not yet implemented, sorry!")
|
||||
}
|
@ -114,6 +114,8 @@ type Server struct {
|
||||
operclasses map[string]OperClass
|
||||
password []byte
|
||||
passwords *PasswordManager
|
||||
registeredChannels map[string]*RegisteredChannel
|
||||
registeredChannelsMutex sync.RWMutex
|
||||
rehashMutex sync.Mutex
|
||||
rehashSignal chan os.Signal
|
||||
restAPI *RestAPIConfig
|
||||
@ -185,9 +187,10 @@ func NewServer(configFilename string, config *Config, logger *logger.Manager) (*
|
||||
}
|
||||
|
||||
server := &Server{
|
||||
accounts: make(map[string]*ClientAccount),
|
||||
accountAuthenticationEnabled: config.Accounts.AuthenticationEnabled,
|
||||
accounts: make(map[string]*ClientAccount),
|
||||
channels: make(ChannelNameMap),
|
||||
checkIdent: config.Server.CheckIdent,
|
||||
clients: NewClientLookupSet(),
|
||||
commands: make(chan Command),
|
||||
configFilename: configFilename,
|
||||
@ -216,14 +219,14 @@ func NewServer(configFilename string, config *Config, logger *logger.Manager) (*
|
||||
nameCasefolded: casefoldedName,
|
||||
networkName: config.Network.Name,
|
||||
newConns: make(chan clientConn),
|
||||
operclasses: *operClasses,
|
||||
operators: opers,
|
||||
signals: make(chan os.Signal, len(ServerExitSignals)),
|
||||
stsEnabled: config.Server.STS.Enabled,
|
||||
operclasses: *operClasses,
|
||||
registeredChannels: make(map[string]*RegisteredChannel),
|
||||
rehashSignal: make(chan os.Signal, 1),
|
||||
restAPI: &config.Server.RestAPI,
|
||||
signals: make(chan os.Signal, len(ServerExitSignals)),
|
||||
stsEnabled: config.Server.STS.Enabled,
|
||||
whoWas: NewWhoWasList(config.Limits.WhowasEntries),
|
||||
checkIdent: config.Server.CheckIdent,
|
||||
}
|
||||
|
||||
// open data store
|
||||
@ -949,6 +952,13 @@ func privmsgHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool
|
||||
channel.SplitPrivMsg(msgid, lowestPrefix, clientOnlyTags, client, splitMsg)
|
||||
} else {
|
||||
target, err = CasefoldName(targetString)
|
||||
if target == "chanserv" {
|
||||
server.chanservReceivePrivmsg(client, message)
|
||||
continue
|
||||
} else if target == "nickserv" {
|
||||
server.nickservReceivePrivmsg(client, message)
|
||||
continue
|
||||
}
|
||||
user := server.clients.Get(target)
|
||||
if err != nil || user == nil {
|
||||
if len(target) > 0 {
|
||||
@ -1593,6 +1603,13 @@ func noticeHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if target == "chanserv" {
|
||||
server.chanservReceiveNotice(client, message)
|
||||
continue
|
||||
} else if target == "nickserv" {
|
||||
server.nickservReceiveNotice(client, message)
|
||||
continue
|
||||
}
|
||||
|
||||
user := server.clients.Get(target)
|
||||
if user == nil {
|
||||
|
Loading…
Reference in New Issue
Block a user