mirror of
https://github.com/ergochat/ergo.git
synced 2024-12-22 10:42:52 +01:00
Merge remote-tracking branch 'origin/master' into vhosts.4
This commit is contained in:
commit
74fa58dda0
10
.travis.gofmt.sh
Normal file
10
.travis.gofmt.sh
Normal file
@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
# exclude vendor/
|
||||
SOURCES="./oragono.go ./irc"
|
||||
|
||||
if [ -n "$(gofmt -s -l $SOURCES)" ]; then
|
||||
echo "Go code is not formatted correctly with \`gofmt -s\`:"
|
||||
gofmt -s -d $SOURCES
|
||||
exit 1
|
||||
fi
|
@ -7,3 +7,4 @@ script:
|
||||
- tar -xzf goreleaser_Linux_x86_64.tar.gz -C $GOPATH/bin
|
||||
- make
|
||||
- make test
|
||||
- bash ./.travis.gofmt.sh
|
||||
|
@ -79,9 +79,19 @@ As well, there's a decent set of 'tests' here, which I like to run Oragono throu
|
||||
https://github.com/DanielOaks/irctest
|
||||
|
||||
|
||||
## Debugging Hangs
|
||||
## Debugging
|
||||
|
||||
To debug a hang, the best thing to do is to get a stack trace. Go's nice, and you can do so by running this:
|
||||
It's helpful to enable all loglines while developing. Here's how to configure this:
|
||||
|
||||
```yaml
|
||||
logging:
|
||||
-
|
||||
method: stderr
|
||||
type: "*"
|
||||
level: debug
|
||||
```
|
||||
|
||||
To debug a hang, the best thing to do is to get a stack trace. The easiest way to get stack traces is with the [pprof listener](https://golang.org/pkg/net/http/pprof/), which can be enabled in the `debug` section of the config. Once it's enabled, you can navigate to `http://localhost:6060/debug/pprof/` in your browser and go from there. If that doesn't work, try:
|
||||
|
||||
$ kill -ABRT <procid>
|
||||
|
||||
|
6
Makefile
6
Makefile
@ -12,5 +12,7 @@ deps:
|
||||
git submodule update --init
|
||||
|
||||
test:
|
||||
cd irc && go test .
|
||||
cd irc && go vet .
|
||||
cd irc && go test . && go vet .
|
||||
cd irc/isupport && go test . && go vet .
|
||||
cd irc/modes && go test . && go vet .
|
||||
cd irc/utils && go test . && go vet .
|
||||
|
@ -20,7 +20,7 @@ import (
|
||||
|
||||
// Channel represents a channel that clients can join.
|
||||
type Channel struct {
|
||||
flags modes.ModeSet
|
||||
flags *modes.ModeSet
|
||||
lists map[modes.Mode]*UserMaskSet
|
||||
key string
|
||||
members MemberSet
|
||||
@ -51,7 +51,7 @@ func NewChannel(s *Server, name string, regInfo *RegisteredChannel) *Channel {
|
||||
|
||||
channel := &Channel{
|
||||
createdTime: time.Now(), // may be overwritten by applyRegInfo
|
||||
flags: make(modes.ModeSet),
|
||||
flags: modes.NewModeSet(),
|
||||
lists: map[modes.Mode]*UserMaskSet{
|
||||
modes.BanMask: NewUserMaskSet(),
|
||||
modes.ExceptMask: NewUserMaskSet(),
|
||||
@ -68,7 +68,7 @@ func NewChannel(s *Server, name string, regInfo *RegisteredChannel) *Channel {
|
||||
channel.applyRegInfo(regInfo)
|
||||
} else {
|
||||
for _, mode := range s.DefaultChannelModes() {
|
||||
channel.flags[mode] = true
|
||||
channel.flags.SetMode(mode, true)
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,7 +87,7 @@ func (channel *Channel) applyRegInfo(chanReg *RegisteredChannel) {
|
||||
channel.key = chanReg.Key
|
||||
|
||||
for _, mode := range chanReg.Modes {
|
||||
channel.flags[mode] = true
|
||||
channel.flags.SetMode(mode, true)
|
||||
}
|
||||
for _, mask := range chanReg.Banlist {
|
||||
channel.lists[modes.BanMask].Add(mask)
|
||||
@ -120,9 +120,7 @@ func (channel *Channel) ExportRegistration(includeFlags uint) (info RegisteredCh
|
||||
|
||||
if includeFlags&IncludeModes != 0 {
|
||||
info.Key = channel.key
|
||||
for mode := range channel.flags {
|
||||
info.Modes = append(info.Modes, mode)
|
||||
}
|
||||
info.Modes = channel.flags.AllModes()
|
||||
}
|
||||
|
||||
if includeFlags&IncludeLists != 0 {
|
||||
@ -225,14 +223,16 @@ func (channel *Channel) ClientIsAtLeast(client *Client, permission modes.Mode) b
|
||||
channel.stateMutex.RLock()
|
||||
defer channel.stateMutex.RUnlock()
|
||||
|
||||
clientModes := channel.members[client]
|
||||
|
||||
// get voice, since it's not a part of ChannelPrivModes
|
||||
if channel.members.HasMode(client, permission) {
|
||||
if clientModes.HasMode(permission) {
|
||||
return true
|
||||
}
|
||||
|
||||
// check regular modes
|
||||
for _, mode := range modes.ChannelPrivModes {
|
||||
if channel.members.HasMode(client, mode) {
|
||||
if clientModes.HasMode(mode) {
|
||||
return true
|
||||
}
|
||||
|
||||
@ -263,14 +263,14 @@ func (channel *Channel) ClientHasPrivsOver(client *Client, target *Client) bool
|
||||
targetModes := channel.members[target]
|
||||
result := false
|
||||
for _, mode := range modes.ChannelPrivModes {
|
||||
if clientModes[mode] {
|
||||
if clientModes.HasMode(mode) {
|
||||
result = true
|
||||
// admins cannot kick other admins
|
||||
if mode == modes.ChannelAdmin && targetModes[modes.ChannelAdmin] {
|
||||
if mode == modes.ChannelAdmin && targetModes.HasMode(modes.ChannelAdmin) {
|
||||
result = false
|
||||
}
|
||||
break
|
||||
} else if channel.members[target][mode] {
|
||||
} else if targetModes.HasMode(mode) {
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -331,14 +331,11 @@ func (channel *Channel) modeStrings(client *Client) (result []string) {
|
||||
mods += modes.UserLimit.String()
|
||||
}
|
||||
|
||||
mods += channel.flags.String()
|
||||
|
||||
channel.stateMutex.RLock()
|
||||
defer channel.stateMutex.RUnlock()
|
||||
|
||||
// flags
|
||||
for mode := range channel.flags {
|
||||
mods += mode.String()
|
||||
}
|
||||
|
||||
result = []string{mods}
|
||||
|
||||
// args for flags with args: The order must match above to keep
|
||||
@ -395,7 +392,7 @@ func (channel *Channel) Join(client *Client, key string, rb *ResponseBuffer) {
|
||||
}
|
||||
|
||||
isInvited := channel.lists[modes.InviteMask].Match(client.nickMaskCasefolded)
|
||||
if channel.flags[modes.InviteOnly] && !isInvited {
|
||||
if channel.flags.HasMode(modes.InviteOnly) && !isInvited {
|
||||
rb.Add(nil, client.server.name, ERR_INVITEONLYCHAN, channel.name, fmt.Sprintf(client.t("Cannot join channel (+%s)"), "i"))
|
||||
return
|
||||
}
|
||||
@ -446,7 +443,7 @@ func (channel *Channel) Join(client *Client, key string, rb *ResponseBuffer) {
|
||||
givenMode = &modes.ChannelOperator
|
||||
}
|
||||
if givenMode != nil {
|
||||
channel.members[client][*givenMode] = true
|
||||
channel.members[client].SetMode(*givenMode, true)
|
||||
}
|
||||
channel.stateMutex.Unlock()
|
||||
|
||||
@ -515,12 +512,12 @@ func (channel *Channel) SendTopic(client *Client, rb *ResponseBuffer) {
|
||||
|
||||
// SetTopic sets the topic of this channel, if the client is allowed to do so.
|
||||
func (channel *Channel) SetTopic(client *Client, topic string, rb *ResponseBuffer) {
|
||||
if !(client.flags[modes.Operator] || channel.hasClient(client)) {
|
||||
if !(client.HasMode(modes.Operator) || channel.hasClient(client)) {
|
||||
rb.Add(nil, client.server.name, ERR_NOTONCHANNEL, channel.name, client.t("You're not on that channel"))
|
||||
return
|
||||
}
|
||||
|
||||
if channel.HasMode(modes.OpOnlyTopic) && !channel.ClientIsAtLeast(client, modes.ChannelOperator) {
|
||||
if channel.flags.HasMode(modes.OpOnlyTopic) && !channel.ClientIsAtLeast(client, modes.ChannelOperator) {
|
||||
rb.Add(nil, client.server.name, ERR_CHANOPRIVSNEEDED, channel.name, client.t("You're not a channel operator"))
|
||||
return
|
||||
}
|
||||
@ -552,13 +549,13 @@ func (channel *Channel) CanSpeak(client *Client) bool {
|
||||
defer channel.stateMutex.RUnlock()
|
||||
|
||||
_, hasClient := channel.members[client]
|
||||
if channel.flags[modes.NoOutside] && !hasClient {
|
||||
if channel.flags.HasMode(modes.NoOutside) && !hasClient {
|
||||
return false
|
||||
}
|
||||
if channel.flags[modes.Moderated] && !channel.ClientIsAtLeast(client, modes.Voice) {
|
||||
if channel.flags.HasMode(modes.Moderated) && !channel.ClientIsAtLeast(client, modes.Voice) {
|
||||
return false
|
||||
}
|
||||
if channel.flags[modes.RegisteredOnly] && client.Account() == "" {
|
||||
if channel.flags.HasMode(modes.RegisteredOnly) && client.Account() == "" {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@ -682,13 +679,7 @@ func (channel *Channel) sendSplitMessage(msgid, cmd string, minPrefix *modes.Mod
|
||||
}
|
||||
}
|
||||
|
||||
func (channel *Channel) applyModeMemberNoMutex(client *Client, mode modes.Mode, op modes.ModeOp, nick string, rb *ResponseBuffer) *modes.ModeChange {
|
||||
if nick == "" {
|
||||
//TODO(dan): shouldn't this be handled before it reaches this function?
|
||||
rb.Add(nil, client.server.name, ERR_NEEDMOREPARAMS, "MODE", client.t("Not enough parameters"))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (channel *Channel) applyModeToMember(client *Client, mode modes.Mode, op modes.ModeOp, nick string, rb *ResponseBuffer) (result *modes.ModeChange) {
|
||||
casefoldedName, err := CasefoldName(nick)
|
||||
target := channel.server.clients.Get(casefoldedName)
|
||||
if err != nil || target == nil {
|
||||
@ -698,26 +689,21 @@ func (channel *Channel) applyModeMemberNoMutex(client *Client, mode modes.Mode,
|
||||
|
||||
channel.stateMutex.Lock()
|
||||
modeset, exists := channel.members[target]
|
||||
var already bool
|
||||
if exists {
|
||||
enable := op == modes.Add
|
||||
already = modeset[mode] == enable
|
||||
modeset[mode] = enable
|
||||
if modeset.SetMode(mode, op == modes.Add) {
|
||||
result = &modes.ModeChange{
|
||||
Op: op,
|
||||
Mode: mode,
|
||||
Arg: nick,
|
||||
}
|
||||
}
|
||||
}
|
||||
channel.stateMutex.Unlock()
|
||||
|
||||
if !exists {
|
||||
rb.Add(nil, client.server.name, ERR_USERNOTINCHANNEL, client.nick, channel.name, client.t("They aren't on that channel"))
|
||||
return nil
|
||||
} else if already {
|
||||
return nil
|
||||
} else {
|
||||
return &modes.ModeChange{
|
||||
Op: op,
|
||||
Mode: mode,
|
||||
Arg: nick,
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ShowMaskList shows the given list to the client.
|
||||
@ -790,7 +776,7 @@ func (channel *Channel) Quit(client *Client) {
|
||||
}
|
||||
|
||||
func (channel *Channel) Kick(client *Client, target *Client, comment string, rb *ResponseBuffer) {
|
||||
if !(client.flags[modes.Operator] || channel.hasClient(client)) {
|
||||
if !(client.HasMode(modes.Operator) || channel.hasClient(client)) {
|
||||
rb.Add(nil, client.server.name, ERR_NOTONCHANNEL, channel.name, client.t("You're not on that channel"))
|
||||
return
|
||||
}
|
||||
@ -823,7 +809,7 @@ func (channel *Channel) Kick(client *Client, target *Client, comment string, rb
|
||||
|
||||
// Invite invites the given client to the channel, if the inviter can do so.
|
||||
func (channel *Channel) Invite(invitee *Client, inviter *Client, rb *ResponseBuffer) {
|
||||
if channel.flags[modes.InviteOnly] && !channel.ClientIsAtLeast(inviter, modes.ChannelOperator) {
|
||||
if channel.flags.HasMode(modes.InviteOnly) && !channel.ClientIsAtLeast(inviter, modes.ChannelOperator) {
|
||||
rb.Add(nil, inviter.server.name, ERR_CHANOPRIVSNEEDED, channel.name, inviter.t("You're not a channel operator"))
|
||||
return
|
||||
}
|
||||
@ -834,7 +820,7 @@ func (channel *Channel) Invite(invitee *Client, inviter *Client, rb *ResponseBuf
|
||||
}
|
||||
|
||||
//TODO(dan): handle this more nicely, keep a list of last X invited channels on invitee rather than explicitly modifying the invite list?
|
||||
if channel.flags[modes.InviteOnly] {
|
||||
if channel.flags.HasMode(modes.InviteOnly) {
|
||||
nmc := invitee.NickCasefolded()
|
||||
channel.stateMutex.Lock()
|
||||
channel.lists[modes.InviteMask].Add(nmc)
|
||||
@ -850,7 +836,7 @@ func (channel *Channel) Invite(invitee *Client, inviter *Client, rb *ResponseBuf
|
||||
//TODO(dan): should inviter.server.name here be inviter.nickMaskString ?
|
||||
rb.Add(nil, inviter.server.name, RPL_INVITING, invitee.nick, channel.name)
|
||||
invitee.Send(nil, inviter.nickMaskString, "INVITE", invitee.nick, channel.name)
|
||||
if invitee.flags[modes.Away] {
|
||||
if invitee.HasMode(modes.Away) {
|
||||
rb.Add(nil, inviter.server.name, RPL_AWAY, invitee.nick, invitee.awayMessage)
|
||||
}
|
||||
}
|
||||
|
@ -95,7 +95,7 @@ func csOpHandler(server *Server, client *Client, command, params string, rb *Res
|
||||
if client == target {
|
||||
givenMode = modes.ChannelFounder
|
||||
}
|
||||
change := channelInfo.applyModeMemberNoMutex(target, givenMode, modes.Add, client.NickCasefolded(), rb)
|
||||
change := channelInfo.applyModeToMember(target, givenMode, modes.Add, client.NickCasefolded(), rb)
|
||||
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
|
||||
@ -151,7 +151,7 @@ func csRegisterHandler(server *Server, client *Client, command, params string, r
|
||||
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
|
||||
change := channelInfo.applyModeMemberNoMutex(client, modes.ChannelFounder, modes.Add, client.NickCasefolded(), rb)
|
||||
change := channelInfo.applyModeToMember(client, modes.ChannelFounder, modes.Add, client.NickCasefolded(), rb)
|
||||
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
|
||||
|
@ -49,7 +49,7 @@ type Client struct {
|
||||
ctime time.Time
|
||||
exitedSnomaskSent bool
|
||||
fakelag *Fakelag
|
||||
flags map[modes.Mode]bool
|
||||
flags *modes.ModeSet
|
||||
hasQuit bool
|
||||
hops int
|
||||
hostname string
|
||||
@ -88,7 +88,6 @@ func NewClient(server *Server, conn net.Conn, isTLS bool) *Client {
|
||||
limits := server.Limits()
|
||||
fullLineLenLimit := limits.LineLen.Tags + limits.LineLen.Rest
|
||||
socket := NewSocket(conn, fullLineLenLimit*2, server.MaxSendQBytes())
|
||||
go socket.RunSocketWriter()
|
||||
client := &Client{
|
||||
atime: now,
|
||||
authorized: server.Password() == nil,
|
||||
@ -97,9 +96,9 @@ func NewClient(server *Server, conn net.Conn, isTLS bool) *Client {
|
||||
capVersion: caps.Cap301,
|
||||
channels: make(ChannelSet),
|
||||
ctime: now,
|
||||
flags: make(map[modes.Mode]bool),
|
||||
flags: modes.NewModeSet(),
|
||||
server: server,
|
||||
socket: &socket,
|
||||
socket: socket,
|
||||
nick: "*", // * is used until actual nick is given
|
||||
nickCasefolded: "*",
|
||||
nickMaskString: "*", // * is used until actual nick is given
|
||||
@ -108,7 +107,7 @@ func NewClient(server *Server, conn net.Conn, isTLS bool) *Client {
|
||||
|
||||
client.recomputeMaxlens()
|
||||
if isTLS {
|
||||
client.flags[modes.TLS] = true
|
||||
client.SetMode(modes.TLS, true)
|
||||
|
||||
// error is not useful to us here anyways so we can ignore it
|
||||
client.certfp, _ = client.socket.CertFP()
|
||||
@ -500,13 +499,7 @@ func (client *Client) HasRoleCapabs(capabs ...string) bool {
|
||||
|
||||
// ModeString returns the mode string for this client.
|
||||
func (client *Client) ModeString() (str string) {
|
||||
str = "+"
|
||||
|
||||
for flag := range client.flags {
|
||||
str += flag.String()
|
||||
}
|
||||
|
||||
return
|
||||
return "+" + client.flags.String()
|
||||
}
|
||||
|
||||
// Friends refers to clients that share a channel with this client.
|
||||
@ -661,10 +654,14 @@ func (client *Client) LoggedIntoAccount() bool {
|
||||
// RplISupport outputs our ISUPPORT lines to the client. This is used on connection and in VERSION responses.
|
||||
func (client *Client) RplISupport(rb *ResponseBuffer) {
|
||||
translatedISupport := client.t("are supported by this server")
|
||||
for _, tokenline := range client.server.ISupport().CachedReply {
|
||||
// ugly trickery ahead
|
||||
tokenline = append(tokenline, translatedISupport)
|
||||
rb.Add(nil, client.server.name, RPL_ISUPPORT, append([]string{client.nick}, tokenline...)...)
|
||||
nick := client.Nick()
|
||||
for _, cachedTokenLine := range client.server.ISupport().CachedReply {
|
||||
length := len(cachedTokenLine) + 2
|
||||
tokenline := make([]string, length)
|
||||
tokenline[0] = nick
|
||||
copy(tokenline[1:], cachedTokenLine)
|
||||
tokenline[length-1] = translatedISupport
|
||||
rb.Add(nil, client.server.name, RPL_ISUPPORT, tokenline...)
|
||||
}
|
||||
}
|
||||
|
||||
@ -758,6 +755,15 @@ func (client *Client) destroy(beingResumed bool) {
|
||||
|
||||
// send quit messages to friends
|
||||
if !beingResumed {
|
||||
client.server.stats.ChangeTotal(-1)
|
||||
if client.HasMode(modes.Invisible) {
|
||||
client.server.stats.ChangeInvisible(-1)
|
||||
}
|
||||
|
||||
if client.HasMode(modes.Operator) || client.HasMode(modes.LocalOperator) {
|
||||
client.server.stats.ChangeOperators(-1)
|
||||
}
|
||||
|
||||
for friend := range friends {
|
||||
if client.quitMessage == "" {
|
||||
client.quitMessage = "Exited"
|
||||
|
@ -22,6 +22,7 @@ import (
|
||||
"github.com/oragono/oragono/irc/custime"
|
||||
"github.com/oragono/oragono/irc/languages"
|
||||
"github.com/oragono/oragono/irc/logger"
|
||||
"github.com/oragono/oragono/irc/modes"
|
||||
"github.com/oragono/oragono/irc/passwd"
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
"gopkg.in/yaml.v2"
|
||||
@ -243,7 +244,8 @@ type Config struct {
|
||||
}
|
||||
|
||||
Datastore struct {
|
||||
Path string
|
||||
Path string
|
||||
AutoUpgrade bool
|
||||
}
|
||||
|
||||
Accounts AccountConfig
|
||||
@ -366,7 +368,7 @@ type Oper struct {
|
||||
WhoisLine string
|
||||
Vhost string
|
||||
Pass []byte
|
||||
Modes string
|
||||
Modes []modes.ModeChange
|
||||
}
|
||||
|
||||
// Operators returns a map of operator configs from the given OperClass and config.
|
||||
@ -394,7 +396,12 @@ func (conf *Config) Operators(oc map[string]*OperClass) (map[string]*Oper, error
|
||||
} else {
|
||||
oper.WhoisLine = class.WhoisLine
|
||||
}
|
||||
oper.Modes = strings.TrimSpace(opConf.Modes)
|
||||
modeStr := strings.TrimSpace(opConf.Modes)
|
||||
modeChanges, unknownChanges := modes.ParseUserModeChanges(strings.Split(modeStr, " ")...)
|
||||
if len(unknownChanges) > 0 {
|
||||
return nil, fmt.Errorf("Could not load operator [%s] due to unknown modes %v", name, unknownChanges)
|
||||
}
|
||||
oper.Modes = modeChanges
|
||||
|
||||
// successful, attach to list of opers
|
||||
operators[name] = &oper
|
||||
|
116
irc/database.go
116
irc/database.go
@ -11,9 +11,11 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/oragono/oragono/irc/modes"
|
||||
"github.com/oragono/oragono/irc/passwd"
|
||||
"github.com/oragono/oragono/irc/utils"
|
||||
|
||||
"github.com/tidwall/buntdb"
|
||||
)
|
||||
@ -38,6 +40,22 @@ type SchemaChange struct {
|
||||
// maps an initial version to a schema change capable of upgrading it
|
||||
var schemaChanges map[string]SchemaChange
|
||||
|
||||
type incompatibleSchemaError struct {
|
||||
currentVersion string
|
||||
requiredVersion string
|
||||
}
|
||||
|
||||
func IncompatibleSchemaError(currentVersion string) (result *incompatibleSchemaError) {
|
||||
return &incompatibleSchemaError{
|
||||
currentVersion: currentVersion,
|
||||
requiredVersion: latestDbSchema,
|
||||
}
|
||||
}
|
||||
|
||||
func (err *incompatibleSchemaError) Error() string {
|
||||
return fmt.Sprintf("Database requires update. Expected schema v%s, got v%s", err.requiredVersion, err.currentVersion)
|
||||
}
|
||||
|
||||
// InitDB creates the database.
|
||||
func InitDB(path string) {
|
||||
// prepare kvstore db
|
||||
@ -69,36 +87,80 @@ func InitDB(path string) {
|
||||
}
|
||||
|
||||
// OpenDatabase returns an existing database, performing a schema version check.
|
||||
func OpenDatabase(path string) (*buntdb.DB, error) {
|
||||
// open data store
|
||||
db, err := buntdb.Open(path)
|
||||
func OpenDatabase(config *Config) (*buntdb.DB, error) {
|
||||
return openDatabaseInternal(config, config.Datastore.AutoUpgrade)
|
||||
}
|
||||
|
||||
// open the database, giving it at most one chance to auto-upgrade the schema
|
||||
func openDatabaseInternal(config *Config, allowAutoupgrade bool) (db *buntdb.DB, err error) {
|
||||
db, err = buntdb.Open(config.Datastore.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return
|
||||
}
|
||||
|
||||
// check db version
|
||||
err = db.View(func(tx *buntdb.Tx) error {
|
||||
version, _ := tx.Get(keySchemaVersion)
|
||||
if version != latestDbSchema {
|
||||
return fmt.Errorf("Database must be updated. Expected schema v%s, got v%s", latestDbSchema, version)
|
||||
defer func() {
|
||||
if err != nil && db != nil {
|
||||
db.Close()
|
||||
db = nil
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}()
|
||||
|
||||
// read the current version string
|
||||
var version string
|
||||
err = db.View(func(tx *buntdb.Tx) error {
|
||||
version, err = tx.Get(keySchemaVersion)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
// close the db
|
||||
db.Close()
|
||||
return nil, err
|
||||
return
|
||||
}
|
||||
|
||||
return db, nil
|
||||
if version == latestDbSchema {
|
||||
// success
|
||||
return
|
||||
}
|
||||
|
||||
// XXX quiesce the DB so we can be sure it's safe to make a backup copy
|
||||
db.Close()
|
||||
db = nil
|
||||
if allowAutoupgrade {
|
||||
err = performAutoUpgrade(version, config)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// successful autoupgrade, let's try this again:
|
||||
return openDatabaseInternal(config, false)
|
||||
} else {
|
||||
err = IncompatibleSchemaError(version)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func performAutoUpgrade(currentVersion string, config *Config) (err error) {
|
||||
path := config.Datastore.Path
|
||||
log.Printf("attempting to auto-upgrade schema from version %s to %s\n", currentVersion, latestDbSchema)
|
||||
timestamp := time.Now().UTC().Format("2006-01-02-15:04:05.000Z")
|
||||
backupPath := fmt.Sprintf("%s.v%s.%s.bak", path, currentVersion, timestamp)
|
||||
log.Printf("making a backup of current database at %s\n", backupPath)
|
||||
err = utils.CopyFile(path, backupPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = UpgradeDB(config)
|
||||
if err != nil {
|
||||
// database upgrade is a single transaction, so we don't need to restore the backup;
|
||||
// we can just delete it
|
||||
os.Remove(backupPath)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// UpgradeDB upgrades the datastore to the latest schema.
|
||||
func UpgradeDB(config *Config) {
|
||||
func UpgradeDB(config *Config) (err error) {
|
||||
store, err := buntdb.Open(config.Datastore.Path)
|
||||
if err != nil {
|
||||
log.Fatal(fmt.Sprintf("Failed to open datastore: %s", err.Error()))
|
||||
return err
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
@ -108,9 +170,14 @@ func UpgradeDB(config *Config) {
|
||||
version, _ = tx.Get(keySchemaVersion)
|
||||
change, schemaNeedsChange := schemaChanges[version]
|
||||
if !schemaNeedsChange {
|
||||
break
|
||||
if version == latestDbSchema {
|
||||
// success!
|
||||
break
|
||||
}
|
||||
// unable to upgrade to the desired version, roll back
|
||||
return IncompatibleSchemaError(version)
|
||||
}
|
||||
log.Println("attempting to update store from version " + version)
|
||||
log.Println("attempting to update schema from version " + version)
|
||||
err := change.Changer(config, tx)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -119,16 +186,15 @@ func UpgradeDB(config *Config) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Println("successfully updated store to version " + change.TargetVersion)
|
||||
log.Println("successfully updated schema to version " + change.TargetVersion)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("Could not update datastore:", err.Error())
|
||||
log.Println("database upgrade failed and was rolled back")
|
||||
}
|
||||
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
func schemaChangeV1toV2(config *Config, tx *buntdb.Tx) error {
|
||||
@ -216,12 +282,12 @@ func schemaChangeV2ToV3(config *Config, tx *buntdb.Tx) error {
|
||||
|
||||
func init() {
|
||||
allChanges := []SchemaChange{
|
||||
SchemaChange{
|
||||
{
|
||||
InitialVersion: "1",
|
||||
TargetVersion: "2",
|
||||
Changer: schemaChangeV1toV2,
|
||||
},
|
||||
SchemaChange{
|
||||
{
|
||||
InitialVersion: "2",
|
||||
TargetVersion: "3",
|
||||
Changer: schemaChangeV2ToV3,
|
||||
|
@ -84,11 +84,7 @@ func (client *Client) ApplyProxiedIP(proxiedIP string, tls bool) (exiting bool)
|
||||
|
||||
// set tls info
|
||||
client.certfp = ""
|
||||
if tls {
|
||||
client.flags[modes.TLS] = true
|
||||
} else {
|
||||
delete(client.flags, modes.TLS)
|
||||
}
|
||||
client.SetMode(modes.TLS, tls)
|
||||
|
||||
return false
|
||||
}
|
||||
|
@ -210,9 +210,12 @@ func (client *Client) SetPreregNick(preregNick string) {
|
||||
}
|
||||
|
||||
func (client *Client) HasMode(mode modes.Mode) bool {
|
||||
client.stateMutex.RLock()
|
||||
defer client.stateMutex.RUnlock()
|
||||
return client.flags[mode]
|
||||
// client.flags has its own synch
|
||||
return client.flags.HasMode(mode)
|
||||
}
|
||||
|
||||
func (client *Client) SetMode(mode modes.Mode, on bool) bool {
|
||||
return client.flags.SetMode(mode, on)
|
||||
}
|
||||
|
||||
func (client *Client) Channels() (result []*Channel) {
|
||||
@ -282,29 +285,8 @@ func (channel *Channel) setKey(key string) {
|
||||
channel.key = key
|
||||
}
|
||||
|
||||
func (channel *Channel) HasMode(mode modes.Mode) bool {
|
||||
channel.stateMutex.RLock()
|
||||
defer channel.stateMutex.RUnlock()
|
||||
return channel.flags[mode]
|
||||
}
|
||||
|
||||
func (channel *Channel) Founder() string {
|
||||
channel.stateMutex.RLock()
|
||||
defer channel.stateMutex.RUnlock()
|
||||
return channel.registeredFounder
|
||||
}
|
||||
|
||||
// set a channel mode, return whether it was already set
|
||||
func (channel *Channel) setMode(mode modes.Mode, enable bool) (already bool) {
|
||||
channel.stateMutex.Lock()
|
||||
already = (channel.flags[mode] == enable)
|
||||
if !already {
|
||||
if enable {
|
||||
channel.flags[mode] = true
|
||||
} else {
|
||||
delete(channel.flags, mode)
|
||||
}
|
||||
}
|
||||
channel.stateMutex.Unlock()
|
||||
return
|
||||
}
|
||||
|
@ -406,15 +406,11 @@ func awayHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
||||
}
|
||||
}
|
||||
|
||||
if isAway {
|
||||
client.flags[modes.Away] = true
|
||||
} else {
|
||||
delete(client.flags, modes.Away)
|
||||
}
|
||||
client.SetMode(modes.Away, isAway)
|
||||
client.awayMessage = text
|
||||
|
||||
var op modes.ModeOp
|
||||
if client.flags[modes.Away] {
|
||||
if isAway {
|
||||
op = modes.Add
|
||||
rb.Add(nil, server.name, RPL_NOWAWAY, client.nick, client.t("You have been marked as being away"))
|
||||
} else {
|
||||
@ -430,7 +426,7 @@ func awayHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
||||
|
||||
// dispatch away-notify
|
||||
for friend := range client.Friends(caps.AwayNotify) {
|
||||
if client.flags[modes.Away] {
|
||||
if isAway {
|
||||
friend.SendFromClient("", client, nil, "AWAY", client.awayMessage)
|
||||
} else {
|
||||
friend.SendFromClient("", client, nil, "AWAY")
|
||||
@ -773,7 +769,7 @@ Get an explanation of <argument>, or "index" for a list of help topics.`), rb)
|
||||
|
||||
// handle index
|
||||
if argument == "index" {
|
||||
if client.flags[modes.Operator] {
|
||||
if client.HasMode(modes.Operator) {
|
||||
client.sendHelp("HELP", GetHelpIndex(client.languages, HelpIndexOpers), rb)
|
||||
} else {
|
||||
client.sendHelp("HELP", GetHelpIndex(client.languages, HelpIndex), rb)
|
||||
@ -783,7 +779,7 @@ Get an explanation of <argument>, or "index" for a list of help topics.`), rb)
|
||||
|
||||
helpHandler, exists := Help[argument]
|
||||
|
||||
if exists && (!helpHandler.oper || (helpHandler.oper && client.flags[modes.Operator])) {
|
||||
if exists && (!helpHandler.oper || (helpHandler.oper && client.HasMode(modes.Operator))) {
|
||||
if helpHandler.textGenerator != nil {
|
||||
client.sendHelp(strings.ToUpper(argument), client.t(helpHandler.textGenerator(client)), rb)
|
||||
} else {
|
||||
@ -1254,9 +1250,10 @@ func listHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
||||
}
|
||||
}
|
||||
|
||||
clientIsOp := client.HasMode(modes.Operator)
|
||||
if len(channels) == 0 {
|
||||
for _, channel := range server.channels.Channels() {
|
||||
if !client.flags[modes.Operator] && channel.flags[modes.Secret] {
|
||||
if !clientIsOp && channel.flags.HasMode(modes.Secret) {
|
||||
continue
|
||||
}
|
||||
if matcher.Matches(channel) {
|
||||
@ -1265,14 +1262,14 @@ func listHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
||||
}
|
||||
} else {
|
||||
// limit regular users to only listing one channel
|
||||
if !client.flags[modes.Operator] {
|
||||
if !clientIsOp {
|
||||
channels = channels[:1]
|
||||
}
|
||||
|
||||
for _, chname := range channels {
|
||||
casefoldedChname, err := CasefoldChannel(chname)
|
||||
channel := server.channels.Get(casefoldedChname)
|
||||
if err != nil || channel == nil || (!client.flags[modes.Operator] && channel.flags[modes.Secret]) {
|
||||
if err != nil || channel == nil || (!clientIsOp && channel.flags.HasMode(modes.Secret)) {
|
||||
if len(chname) > 0 {
|
||||
rb.Add(nil, server.name, ERR_NOSUCHCHANNEL, client.nick, chname, client.t("No such channel"))
|
||||
}
|
||||
@ -1290,21 +1287,13 @@ func listHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
||||
// LUSERS [<mask> [<server>]]
|
||||
func lusersHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||
//TODO(vegax87) Fix network statistics and additional parameters
|
||||
var totalcount, invisiblecount, opercount int
|
||||
totalCount, invisibleCount, operCount := server.stats.GetStats()
|
||||
|
||||
rb.Add(nil, server.name, RPL_LUSERCLIENT, client.nick, fmt.Sprintf(client.t("There are %[1]d users and %[2]d invisible on %[3]d server(s)"), totalCount-invisibleCount, invisibleCount, 1))
|
||||
rb.Add(nil, server.name, RPL_LUSEROP, client.nick, strconv.Itoa(operCount), client.t("IRC Operators online"))
|
||||
rb.Add(nil, server.name, RPL_LUSERCHANNELS, client.nick, strconv.Itoa(server.channels.Len()), client.t("channels formed"))
|
||||
rb.Add(nil, server.name, RPL_LUSERME, client.nick, fmt.Sprintf(client.t("I have %[1]d clients and %[2]d servers"), totalCount, 1))
|
||||
|
||||
for _, onlineusers := range server.clients.AllClients() {
|
||||
totalcount++
|
||||
if onlineusers.flags[modes.Invisible] {
|
||||
invisiblecount++
|
||||
}
|
||||
if onlineusers.flags[modes.Operator] {
|
||||
opercount++
|
||||
}
|
||||
}
|
||||
rb.Add(nil, server.name, RPL_LUSERCLIENT, client.nick, fmt.Sprintf(client.t("There are %[1]d users and %[2]d invisible on %[3]d server(s)"), totalcount, invisiblecount, 1))
|
||||
rb.Add(nil, server.name, RPL_LUSEROP, client.nick, fmt.Sprintf(client.t("%d IRC Operators online"), opercount))
|
||||
rb.Add(nil, server.name, RPL_LUSERCHANNELS, client.nick, fmt.Sprintf(client.t("%d channels formed"), server.channels.Len()))
|
||||
rb.Add(nil, server.name, RPL_LUSERME, client.nick, fmt.Sprintf(client.t("I have %[1]d clients and %[2]d servers"), totalcount, 1))
|
||||
return false
|
||||
}
|
||||
|
||||
@ -1334,7 +1323,7 @@ func cmodeHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
|
||||
if 1 < len(msg.Params) {
|
||||
// parse out real mode changes
|
||||
params := msg.Params[1:]
|
||||
changes, unknown := ParseChannelModeChanges(params...)
|
||||
changes, unknown := modes.ParseChannelModeChanges(params...)
|
||||
|
||||
// alert for unknown mode changes
|
||||
for char := range unknown {
|
||||
@ -1420,14 +1409,14 @@ func umodeHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
|
||||
}
|
||||
|
||||
// apply mode changes
|
||||
applied = target.applyUserModeChanges(msg.Command == "SAMODE", changes)
|
||||
applied = ApplyUserModeChanges(client, changes, msg.Command == "SAMODE")
|
||||
}
|
||||
|
||||
if len(applied) > 0 {
|
||||
rb.Add(nil, client.nickMaskString, "MODE", targetNick, applied.String())
|
||||
} else if hasPrivs {
|
||||
rb.Add(nil, target.nickMaskString, RPL_UMODEIS, targetNick, target.ModeString())
|
||||
if client.flags[modes.LocalOperator] || client.flags[modes.Operator] {
|
||||
if client.HasMode(modes.LocalOperator) || client.HasMode(modes.Operator) {
|
||||
masks := server.snomasks.String(client)
|
||||
if 0 < len(masks) {
|
||||
rb.Add(nil, target.nickMaskString, RPL_SNOMASKIS, targetNick, masks, client.t("Server notice masks"))
|
||||
@ -1673,7 +1662,7 @@ func noticeHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
|
||||
msgid := server.generateMessageID()
|
||||
// restrict messages appropriately when +R is set
|
||||
// intentionally make the sending user think the message went through fine
|
||||
if !user.flags[modes.RegisteredOnly] || client.registered {
|
||||
if !user.HasMode(modes.RegisteredOnly) || client.LoggedIntoAccount() {
|
||||
user.SendSplitMsgFromClient(msgid, client, clientOnlyTags, "NOTICE", user.nick, splitMsg)
|
||||
}
|
||||
if client.capabilities.Has(caps.EchoMessage) {
|
||||
@ -1723,7 +1712,7 @@ func npcaHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
||||
|
||||
// OPER <name> <password>
|
||||
func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
|
||||
if client.flags[modes.Operator] == true {
|
||||
if client.HasMode(modes.Operator) == true {
|
||||
rb.Add(nil, server.name, ERR_UNKNOWNERROR, "OPER", client.t("You're already opered-up!"))
|
||||
return false
|
||||
}
|
||||
@ -1746,26 +1735,16 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
||||
client.sendChghost(oldNickmask, oper.Vhost)
|
||||
}
|
||||
|
||||
// set new modes
|
||||
var applied modes.ModeChanges
|
||||
if 0 < len(oper.Modes) {
|
||||
modeChanges, unknownChanges := modes.ParseUserModeChanges(strings.Split(oper.Modes, " ")...)
|
||||
applied = client.applyUserModeChanges(true, modeChanges)
|
||||
if 0 < len(unknownChanges) {
|
||||
var runes string
|
||||
for r := range unknownChanges {
|
||||
runes += string(r)
|
||||
}
|
||||
rb.Notice(fmt.Sprintf(client.t("Could not apply mode changes: +%s"), runes))
|
||||
}
|
||||
}
|
||||
|
||||
rb.Add(nil, server.name, RPL_YOUREOPER, client.nick, client.t("You are now an IRC operator"))
|
||||
|
||||
applied = append(applied, modes.ModeChange{
|
||||
// set new modes: modes.Operator, plus anything specified in the config
|
||||
modeChanges := make([]modes.ModeChange, len(oper.Modes)+1)
|
||||
modeChanges[0] = modes.ModeChange{
|
||||
Mode: modes.Operator,
|
||||
Op: modes.Add,
|
||||
})
|
||||
}
|
||||
copy(modeChanges[1:], oper.Modes)
|
||||
applied := ApplyUserModeChanges(client, modeChanges, true)
|
||||
|
||||
rb.Add(nil, server.name, RPL_YOUREOPER, client.nick, client.t("You are now an IRC operator"))
|
||||
rb.Add(nil, server.name, "MODE", client.nick, applied.String())
|
||||
|
||||
server.snomasks.Send(sno.LocalOpers, fmt.Sprintf(ircfmt.Unescape("Client opered up $c[grey][$r%s$c[grey], $r%s$c[grey]]"), client.nickMaskString, oper.Name))
|
||||
@ -1773,7 +1752,6 @@ func operHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
|
||||
// client may now be unthrottled by the fakelag system
|
||||
client.resetFakelag()
|
||||
|
||||
client.flags[modes.Operator] = true
|
||||
return false
|
||||
}
|
||||
|
||||
@ -1885,13 +1863,13 @@ func privmsgHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
|
||||
msgid := server.generateMessageID()
|
||||
// restrict messages appropriately when +R is set
|
||||
// intentionally make the sending user think the message went through fine
|
||||
if !user.flags[modes.RegisteredOnly] || client.registered {
|
||||
if !user.HasMode(modes.RegisteredOnly) || client.LoggedIntoAccount() {
|
||||
user.SendSplitMsgFromClient(msgid, client, clientOnlyTags, "PRIVMSG", user.nick, splitMsg)
|
||||
}
|
||||
if client.capabilities.Has(caps.EchoMessage) {
|
||||
rb.AddSplitMessageFromClient(msgid, client, clientOnlyTags, "PRIVMSG", user.nick, splitMsg)
|
||||
}
|
||||
if user.flags[modes.Away] {
|
||||
if user.HasMode(modes.Away) {
|
||||
//TODO(dan): possibly implement cooldown of away notifications to users
|
||||
rb.Add(nil, server.name, RPL_AWAY, user.nick, user.awayMessage)
|
||||
}
|
||||
@ -2139,7 +2117,7 @@ func tagmsgHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
|
||||
if client.capabilities.Has(caps.EchoMessage) {
|
||||
rb.AddFromClient(msgid, client, clientOnlyTags, "TAGMSG", user.nick)
|
||||
}
|
||||
if user.flags[modes.Away] {
|
||||
if user.HasMode(modes.Away) {
|
||||
//TODO(dan): possibly implement cooldown of away notifications to users
|
||||
rb.Add(nil, server.name, RPL_AWAY, user.nick, user.awayMessage)
|
||||
}
|
||||
@ -2337,10 +2315,10 @@ func userhostHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *
|
||||
|
||||
var isOper, isAway string
|
||||
|
||||
if target.flags[modes.Operator] {
|
||||
if target.HasMode(modes.Operator) {
|
||||
isOper = "*"
|
||||
}
|
||||
if target.flags[modes.Away] {
|
||||
if target.HasMode(modes.Away) {
|
||||
isAway = "-"
|
||||
} else {
|
||||
isAway = "+"
|
||||
@ -2381,7 +2359,7 @@ func webircHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
|
||||
lkey := strings.ToLower(key)
|
||||
if lkey == "tls" || lkey == "secure" {
|
||||
// only accept "tls" flag if the gateway's connection to us is secure as well
|
||||
if client.flags[modes.TLS] || utils.AddrIsLocal(client.socket.conn.RemoteAddr()) {
|
||||
if client.HasMode(modes.TLS) || utils.AddrIsLocal(client.socket.conn.RemoteAddr()) {
|
||||
secure = true
|
||||
}
|
||||
}
|
||||
@ -2470,7 +2448,7 @@ func whoisHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
|
||||
return false
|
||||
}
|
||||
|
||||
if client.flags[modes.Operator] {
|
||||
if client.HasMode(modes.Operator) {
|
||||
masks := strings.Split(masksString, ",")
|
||||
for _, mask := range masks {
|
||||
casefoldedMask, err := Casefold(mask)
|
||||
|
130
irc/modes.go
130
irc/modes.go
@ -21,8 +21,8 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
// applyUserModeChanges applies the given changes, and returns the applied changes.
|
||||
func (client *Client) applyUserModeChanges(force bool, changes modes.ModeChanges) modes.ModeChanges {
|
||||
// ApplyUserModeChanges applies the given changes, and returns the applied changes.
|
||||
func ApplyUserModeChanges(client *Client, changes modes.ModeChanges, force bool) modes.ModeChanges {
|
||||
applied := make(modes.ModeChanges, 0)
|
||||
|
||||
for _, change := range changes {
|
||||
@ -34,22 +34,28 @@ func (client *Client) applyUserModeChanges(force bool, changes modes.ModeChanges
|
||||
continue
|
||||
}
|
||||
|
||||
if client.flags[change.Mode] {
|
||||
continue
|
||||
if client.SetMode(change.Mode, true) {
|
||||
if change.Mode == modes.Invisible {
|
||||
client.server.stats.ChangeInvisible(1)
|
||||
} else if change.Mode == modes.Operator || change.Mode == modes.LocalOperator {
|
||||
client.server.stats.ChangeOperators(1)
|
||||
}
|
||||
applied = append(applied, change)
|
||||
}
|
||||
client.flags[change.Mode] = true
|
||||
applied = append(applied, change)
|
||||
|
||||
case modes.Remove:
|
||||
if !client.flags[change.Mode] {
|
||||
continue
|
||||
if client.SetMode(change.Mode, false) {
|
||||
if change.Mode == modes.Invisible {
|
||||
client.server.stats.ChangeInvisible(-1)
|
||||
} else if change.Mode == modes.Operator || change.Mode == modes.LocalOperator {
|
||||
client.server.stats.ChangeOperators(-1)
|
||||
}
|
||||
applied = append(applied, change)
|
||||
}
|
||||
delete(client.flags, change.Mode)
|
||||
applied = append(applied, change)
|
||||
}
|
||||
|
||||
case modes.ServerNotice:
|
||||
if !client.flags[modes.Operator] {
|
||||
if !client.HasMode(modes.Operator) {
|
||||
continue
|
||||
}
|
||||
var masks []sno.Mask
|
||||
@ -87,7 +93,7 @@ func ParseDefaultChannelModes(config *Config) modes.Modes {
|
||||
return DefaultChannelModes
|
||||
}
|
||||
modeChangeStrings := strings.Split(strings.TrimSpace(*config.Channels.DefaultModes), " ")
|
||||
modeChanges, _ := ParseChannelModeChanges(modeChangeStrings...)
|
||||
modeChanges, _ := modes.ParseChannelModeChanges(modeChangeStrings...)
|
||||
defaultChannelModes := make(modes.Modes, 0)
|
||||
for _, modeChange := range modeChanges {
|
||||
if modeChange.Op == modes.Add {
|
||||
@ -97,83 +103,6 @@ func ParseDefaultChannelModes(config *Config) modes.Modes {
|
||||
return defaultChannelModes
|
||||
}
|
||||
|
||||
// ParseChannelModeChanges returns the valid changes, and the list of unknown chars.
|
||||
func ParseChannelModeChanges(params ...string) (modes.ModeChanges, map[rune]bool) {
|
||||
changes := make(modes.ModeChanges, 0)
|
||||
unknown := make(map[rune]bool)
|
||||
|
||||
op := modes.List
|
||||
|
||||
if 0 < len(params) {
|
||||
modeArg := params[0]
|
||||
skipArgs := 1
|
||||
|
||||
for _, mode := range modeArg {
|
||||
if mode == '-' || mode == '+' {
|
||||
op = modes.ModeOp(mode)
|
||||
continue
|
||||
}
|
||||
change := modes.ModeChange{
|
||||
Mode: modes.Mode(mode),
|
||||
Op: op,
|
||||
}
|
||||
|
||||
// put arg into modechange if needed
|
||||
switch modes.Mode(mode) {
|
||||
case modes.BanMask, modes.ExceptMask, modes.InviteMask:
|
||||
if len(params) > skipArgs {
|
||||
change.Arg = params[skipArgs]
|
||||
skipArgs++
|
||||
} else {
|
||||
change.Op = modes.List
|
||||
}
|
||||
case modes.ChannelFounder, modes.ChannelAdmin, modes.ChannelOperator, modes.Halfop, modes.Voice:
|
||||
if len(params) > skipArgs {
|
||||
change.Arg = params[skipArgs]
|
||||
skipArgs++
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
case modes.Key, modes.UserLimit:
|
||||
// don't require value when removing
|
||||
if change.Op == modes.Add {
|
||||
if len(params) > skipArgs {
|
||||
change.Arg = params[skipArgs]
|
||||
skipArgs++
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var isKnown bool
|
||||
for _, supportedMode := range modes.SupportedChannelModes {
|
||||
if rune(supportedMode) == mode {
|
||||
isKnown = true
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, supportedMode := range modes.ChannelPrivModes {
|
||||
if rune(supportedMode) == mode {
|
||||
isKnown = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if mode == rune(modes.Voice) {
|
||||
isKnown = true
|
||||
}
|
||||
if !isKnown {
|
||||
unknown[mode] = true
|
||||
continue
|
||||
}
|
||||
|
||||
changes = append(changes, change)
|
||||
}
|
||||
}
|
||||
|
||||
return changes, unknown
|
||||
}
|
||||
|
||||
// ApplyChannelModeChanges applies a given set of mode changes.
|
||||
func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, changes modes.ModeChanges, rb *ResponseBuffer) modes.ModeChanges {
|
||||
// so we only output one warning for each list type when full
|
||||
@ -194,15 +123,17 @@ func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, c
|
||||
}
|
||||
switch change.Mode {
|
||||
case modes.ChannelFounder, modes.ChannelAdmin, modes.ChannelOperator, modes.Halfop, modes.Voice:
|
||||
// Admins can't give other people Admin or remove it from others
|
||||
if change.Mode == modes.ChannelAdmin {
|
||||
return false
|
||||
}
|
||||
// List on these modes is a no-op anyway
|
||||
if change.Op == modes.List {
|
||||
return true
|
||||
}
|
||||
cfarg, _ := CasefoldName(change.Arg)
|
||||
if change.Op == modes.Remove && cfarg == client.nickCasefolded {
|
||||
isSelfChange := cfarg == client.NickCasefolded()
|
||||
// Admins can't give other people Admin or remove it from others
|
||||
if change.Mode == modes.ChannelAdmin && !isSelfChange {
|
||||
return false
|
||||
}
|
||||
if change.Op == modes.Remove && isSelfChange {
|
||||
// "There is no restriction, however, on anyone `deopping' themselves"
|
||||
// <https://tools.ietf.org/html/rfc2812#section-3.1.5>
|
||||
return true
|
||||
@ -285,8 +216,7 @@ func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, c
|
||||
continue
|
||||
}
|
||||
|
||||
already := channel.setMode(change.Mode, change.Op == modes.Add)
|
||||
if !already {
|
||||
if channel.flags.SetMode(change.Mode, change.Op == modes.Add) {
|
||||
applied = append(applied, change)
|
||||
}
|
||||
|
||||
@ -295,7 +225,13 @@ func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, c
|
||||
continue
|
||||
}
|
||||
|
||||
change := channel.applyModeMemberNoMutex(client, change.Mode, change.Op, change.Arg, rb)
|
||||
nick := change.Arg
|
||||
if nick == "" {
|
||||
rb.Add(nil, client.server.name, ERR_NEEDMOREPARAMS, "MODE", client.t("Not enough parameters"))
|
||||
return nil
|
||||
}
|
||||
|
||||
change := channel.applyModeToMember(client, change.Mode, change.Op, nick, rb)
|
||||
if change != nil {
|
||||
applied = append(applied, *change)
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ package modes
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -247,34 +248,156 @@ func ParseUserModeChanges(params ...string) (ModeChanges, map[rune]bool) {
|
||||
return changes, unknown
|
||||
}
|
||||
|
||||
// ParseChannelModeChanges returns the valid changes, and the list of unknown chars.
|
||||
func ParseChannelModeChanges(params ...string) (ModeChanges, map[rune]bool) {
|
||||
changes := make(ModeChanges, 0)
|
||||
unknown := make(map[rune]bool)
|
||||
|
||||
op := List
|
||||
|
||||
if 0 < len(params) {
|
||||
modeArg := params[0]
|
||||
skipArgs := 1
|
||||
|
||||
for _, mode := range modeArg {
|
||||
if mode == '-' || mode == '+' {
|
||||
op = ModeOp(mode)
|
||||
continue
|
||||
}
|
||||
change := ModeChange{
|
||||
Mode: Mode(mode),
|
||||
Op: op,
|
||||
}
|
||||
|
||||
// put arg into modechange if needed
|
||||
switch Mode(mode) {
|
||||
case BanMask, ExceptMask, InviteMask:
|
||||
if len(params) > skipArgs {
|
||||
change.Arg = params[skipArgs]
|
||||
skipArgs++
|
||||
} else {
|
||||
change.Op = List
|
||||
}
|
||||
case ChannelFounder, ChannelAdmin, ChannelOperator, Halfop, Voice:
|
||||
if len(params) > skipArgs {
|
||||
change.Arg = params[skipArgs]
|
||||
skipArgs++
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
case Key, UserLimit:
|
||||
// don't require value when removing
|
||||
if change.Op == Add {
|
||||
if len(params) > skipArgs {
|
||||
change.Arg = params[skipArgs]
|
||||
skipArgs++
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var isKnown bool
|
||||
for _, supportedMode := range SupportedChannelModes {
|
||||
if rune(supportedMode) == mode {
|
||||
isKnown = true
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, supportedMode := range ChannelPrivModes {
|
||||
if rune(supportedMode) == mode {
|
||||
isKnown = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if mode == rune(Voice) {
|
||||
isKnown = true
|
||||
}
|
||||
if !isKnown {
|
||||
unknown[mode] = true
|
||||
continue
|
||||
}
|
||||
|
||||
changes = append(changes, change)
|
||||
}
|
||||
}
|
||||
|
||||
return changes, unknown
|
||||
}
|
||||
|
||||
// ModeSet holds a set of modes.
|
||||
type ModeSet map[Mode]bool
|
||||
type ModeSet struct {
|
||||
sync.RWMutex // tier 0
|
||||
modes map[Mode]bool
|
||||
}
|
||||
|
||||
// returns a pointer to a new ModeSet
|
||||
func NewModeSet() *ModeSet {
|
||||
return &ModeSet{
|
||||
modes: make(map[Mode]bool),
|
||||
}
|
||||
}
|
||||
|
||||
// test whether `mode` is set
|
||||
func (set *ModeSet) HasMode(mode Mode) bool {
|
||||
set.RLock()
|
||||
defer set.RUnlock()
|
||||
return set.modes[mode]
|
||||
}
|
||||
|
||||
// set `mode` to be on or off, return whether the value actually changed
|
||||
func (set *ModeSet) SetMode(mode Mode, on bool) (applied bool) {
|
||||
set.Lock()
|
||||
defer set.Unlock()
|
||||
|
||||
previouslyOn := set.modes[mode]
|
||||
needsApply := (on != previouslyOn)
|
||||
if on && needsApply {
|
||||
set.modes[mode] = true
|
||||
} else if !on && needsApply {
|
||||
delete(set.modes, mode)
|
||||
}
|
||||
return needsApply
|
||||
}
|
||||
|
||||
// return the modes in the set as a slice
|
||||
func (set *ModeSet) AllModes() (result []Mode) {
|
||||
set.RLock()
|
||||
defer set.RUnlock()
|
||||
|
||||
for mode := range set.modes {
|
||||
result = append(result, mode)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// String returns the modes in this set.
|
||||
func (set ModeSet) String() string {
|
||||
if len(set) == 0 {
|
||||
func (set *ModeSet) String() string {
|
||||
set.RLock()
|
||||
defer set.RUnlock()
|
||||
|
||||
if len(set.modes) == 0 {
|
||||
return ""
|
||||
}
|
||||
strs := make([]string, len(set))
|
||||
index := 0
|
||||
for mode := range set {
|
||||
strs[index] = mode.String()
|
||||
index++
|
||||
var result []byte
|
||||
for mode := range set.modes {
|
||||
result = append(result, mode.String()...)
|
||||
}
|
||||
return strings.Join(strs, "")
|
||||
return string(result)
|
||||
}
|
||||
|
||||
// Prefixes returns a list of prefixes for the given set of channel modes.
|
||||
func (set ModeSet) Prefixes(isMultiPrefix bool) string {
|
||||
var prefixes string
|
||||
func (set *ModeSet) Prefixes(isMultiPrefix bool) (prefixes string) {
|
||||
set.RLock()
|
||||
defer set.RUnlock()
|
||||
|
||||
// add prefixes in order from highest to lowest privs
|
||||
for _, mode := range ChannelPrivModes {
|
||||
if set[mode] {
|
||||
if set.modes[mode] {
|
||||
prefixes += ChannelModePrefixes[mode]
|
||||
}
|
||||
}
|
||||
if set[Voice] {
|
||||
if set.modes[Voice] {
|
||||
prefixes += ChannelModePrefixes[Voice]
|
||||
}
|
||||
|
||||
|
37
irc/modes/modes_test.go
Normal file
37
irc/modes/modes_test.go
Normal file
@ -0,0 +1,37 @@
|
||||
// Copyright (c) 2018 Shivaram Lingamneni
|
||||
// released under the MIT license
|
||||
|
||||
package modes
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSetMode(t *testing.T) {
|
||||
set := NewModeSet()
|
||||
|
||||
if applied := set.SetMode(Invisible, false); applied != false {
|
||||
t.Errorf("all modes should be false by default")
|
||||
}
|
||||
|
||||
if applied := set.SetMode(Invisible, true); applied != true {
|
||||
t.Errorf("initial SetMode call should return true")
|
||||
}
|
||||
|
||||
set.SetMode(Operator, true)
|
||||
|
||||
if applied := set.SetMode(Invisible, true); applied != false {
|
||||
t.Errorf("redundant SetMode call should return false")
|
||||
}
|
||||
|
||||
expected1 := []Mode{Invisible, Operator}
|
||||
expected2 := []Mode{Operator, Invisible}
|
||||
if allModes := set.AllModes(); !(reflect.DeepEqual(allModes, expected1) || reflect.DeepEqual(allModes, expected2)) {
|
||||
t.Errorf("unexpected AllModes value: %v", allModes)
|
||||
}
|
||||
|
||||
if modeString := set.String(); !(modeString == "io" || modeString == "oi") {
|
||||
t.Errorf("unexpected modestring: %s", modeString)
|
||||
}
|
||||
}
|
@ -35,7 +35,7 @@ func sendRoleplayMessage(server *Server, client *Client, source string, targetSt
|
||||
return
|
||||
}
|
||||
|
||||
if !channel.flags[modes.ChanRoleplaying] {
|
||||
if !channel.flags.HasMode(modes.ChanRoleplaying) {
|
||||
rb.Add(nil, client.server.name, ERR_CANNOTSENDRP, channel.name, client.t("Channel doesn't have roleplaying mode available"))
|
||||
return
|
||||
}
|
||||
@ -58,7 +58,7 @@ func sendRoleplayMessage(server *Server, client *Client, source string, targetSt
|
||||
return
|
||||
}
|
||||
|
||||
if !user.flags[modes.UserRoleplaying] {
|
||||
if !user.HasMode(modes.UserRoleplaying) {
|
||||
rb.Add(nil, client.server.name, ERR_CANNOTSENDRP, user.nick, client.t("User doesn't have roleplaying mode enabled"))
|
||||
return
|
||||
}
|
||||
@ -67,7 +67,7 @@ func sendRoleplayMessage(server *Server, client *Client, source string, targetSt
|
||||
if client.capabilities.Has(caps.EchoMessage) {
|
||||
rb.Add(nil, source, "PRIVMSG", user.nick, message)
|
||||
}
|
||||
if user.flags[modes.Away] {
|
||||
if user.HasMode(modes.Away) {
|
||||
//TODO(dan): possibly implement cooldown of away notifications to users
|
||||
rb.Add(nil, server.name, RPL_AWAY, user.nick, user.awayMessage)
|
||||
}
|
||||
|
@ -127,10 +127,10 @@ type Server struct {
|
||||
signals chan os.Signal
|
||||
snomasks *SnoManager
|
||||
store *buntdb.DB
|
||||
storeFilename string
|
||||
stsEnabled bool
|
||||
webirc []webircConfig
|
||||
whoWas *WhoWasList
|
||||
stats *Stats
|
||||
}
|
||||
|
||||
var (
|
||||
@ -164,6 +164,7 @@ func NewServer(config *Config, logger *logger.Manager) (*Server, error) {
|
||||
signals: make(chan os.Signal, len(ServerExitSignals)),
|
||||
snomasks: NewSnoManager(),
|
||||
whoWas: NewWhoWasList(config.Limits.WhowasEntries),
|
||||
stats: NewStats(),
|
||||
}
|
||||
|
||||
if err := server.applyConfig(config, true); err != nil {
|
||||
@ -472,6 +473,9 @@ func (server *Server) tryRegister(c *Client) {
|
||||
return
|
||||
}
|
||||
|
||||
// count new user in statistics
|
||||
server.stats.ChangeTotal(1)
|
||||
|
||||
// continue registration
|
||||
server.logger.Debug("localconnect", fmt.Sprintf("Client registered [%s] [u:%s] [r:%s]", c.nick, c.username, c.realname))
|
||||
server.snomasks.Send(sno.LocalConnects, fmt.Sprintf(ircfmt.Unescape("Client registered $c[grey][$r%s$c[grey]] [u:$r%s$c[grey]] [h:$r%s$c[grey]] [r:$r%s$c[grey]]"), c.nick, c.username, c.rawHostname, c.realname))
|
||||
@ -633,8 +637,8 @@ func (client *Client) WhoisChannelsNames(target *Client) []string {
|
||||
var chstrs []string
|
||||
for _, channel := range target.Channels() {
|
||||
// channel is secret and the target can't see it
|
||||
if !client.flags[modes.Operator] {
|
||||
if (target.HasMode(modes.Invisible) || channel.HasMode(modes.Secret)) && !channel.hasClient(client) {
|
||||
if !client.HasMode(modes.Operator) {
|
||||
if (target.HasMode(modes.Invisible) || channel.flags.HasMode(modes.Secret)) && !channel.hasClient(client) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
@ -657,16 +661,16 @@ func (client *Client) getWhoisOf(target *Client, rb *ResponseBuffer) {
|
||||
if tOper != nil {
|
||||
rb.Add(nil, client.server.name, RPL_WHOISOPERATOR, client.nick, target.nick, tOper.WhoisLine)
|
||||
}
|
||||
if client.flags[modes.Operator] || client == target {
|
||||
if client.HasMode(modes.Operator) || client == target {
|
||||
rb.Add(nil, client.server.name, RPL_WHOISACTUALLY, client.nick, target.nick, fmt.Sprintf("%s@%s", target.username, utils.LookupHostname(target.IPString())), target.IPString(), client.t("Actual user@host, Actual IP"))
|
||||
}
|
||||
if target.flags[modes.TLS] {
|
||||
if target.HasMode(modes.TLS) {
|
||||
rb.Add(nil, client.server.name, RPL_WHOISSECURE, client.nick, target.nick, client.t("is using a secure connection"))
|
||||
}
|
||||
if target.LoggedIntoAccount() {
|
||||
rb.Add(nil, client.server.name, RPL_WHOISACCOUNT, client.nick, target.AccountName(), client.t("is logged in as"))
|
||||
}
|
||||
if target.flags[modes.Bot] {
|
||||
if target.HasMode(modes.Bot) {
|
||||
rb.Add(nil, client.server.name, RPL_WHOISBOT, client.nick, target.nick, ircfmt.Unescape(fmt.Sprintf(client.t("is a $bBot$b on %s"), client.server.networkName)))
|
||||
}
|
||||
|
||||
@ -679,7 +683,7 @@ func (client *Client) getWhoisOf(target *Client, rb *ResponseBuffer) {
|
||||
rb.Add(nil, client.server.name, RPL_WHOISLANGUAGE, params...)
|
||||
}
|
||||
|
||||
if target.certfp != "" && (client.flags[modes.Operator] || client == target) {
|
||||
if target.certfp != "" && (client.HasMode(modes.Operator) || client == target) {
|
||||
rb.Add(nil, client.server.name, RPL_WHOISCERTFP, client.nick, target.nick, fmt.Sprintf(client.t("has client certificate fingerprint %s"), target.certfp))
|
||||
}
|
||||
rb.Add(nil, client.server.name, RPL_WHOISIDLE, client.nick, target.nick, strconv.FormatUint(target.IdleSeconds(), 10), strconv.FormatInt(target.SignonTime(), 10), client.t("seconds idle, signon time"))
|
||||
@ -710,7 +714,7 @@ func (target *Client) rplWhoReply(channel *Channel, client *Client, rb *Response
|
||||
|
||||
func whoChannel(client *Client, channel *Channel, friends ClientSet, rb *ResponseBuffer) {
|
||||
for _, member := range channel.Members() {
|
||||
if !client.flags[modes.Invisible] || friends[client] {
|
||||
if !client.HasMode(modes.Invisible) || friends[client] {
|
||||
client.rplWhoReply(channel, member, rb)
|
||||
}
|
||||
}
|
||||
@ -749,7 +753,7 @@ func (server *Server) applyConfig(config *Config, initial bool) error {
|
||||
return fmt.Errorf("Maximum line length (linelen) cannot be changed after launching the server, rehash aborted")
|
||||
} else if server.name != config.Server.Name {
|
||||
return fmt.Errorf("Server name cannot be changed after launching the server, rehash aborted")
|
||||
} else if server.storeFilename != config.Datastore.Path {
|
||||
} else if server.config.Datastore.Path != config.Datastore.Path {
|
||||
return fmt.Errorf("Datastore path cannot be changed after launching the server, rehash aborted")
|
||||
}
|
||||
}
|
||||
@ -975,10 +979,9 @@ func (server *Server) applyConfig(config *Config, initial bool) error {
|
||||
server.config = config
|
||||
server.configurableStateMutex.Unlock()
|
||||
|
||||
server.storeFilename = config.Datastore.Path
|
||||
server.logger.Info("rehash", "Using datastore", server.storeFilename)
|
||||
server.logger.Info("rehash", "Using datastore", config.Datastore.Path)
|
||||
if initial {
|
||||
if err := server.loadDatastore(server.storeFilename); err != nil {
|
||||
if err := server.loadDatastore(config); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@ -1075,11 +1078,11 @@ func (server *Server) loadMOTD(motdPath string, useFormatting bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (server *Server) loadDatastore(datastorePath string) error {
|
||||
func (server *Server) loadDatastore(config *Config) error {
|
||||
// open the datastore and load server state for which it (rather than config)
|
||||
// is the source of truth
|
||||
|
||||
db, err := OpenDatabase(datastorePath)
|
||||
db, err := OpenDatabase(config)
|
||||
if err == nil {
|
||||
server.store = db
|
||||
} else {
|
||||
@ -1213,7 +1216,7 @@ func (matcher *elistMatcher) Matches(channel *Channel) bool {
|
||||
func (target *Client) RplList(channel *Channel, rb *ResponseBuffer) {
|
||||
// get the correct number of channel members
|
||||
var memberCount int
|
||||
if target.flags[modes.Operator] || channel.hasClient(target) {
|
||||
if target.HasMode(modes.Operator) || channel.hasClient(target) {
|
||||
memberCount = len(channel.Members())
|
||||
} else {
|
||||
for _, member := range channel.Members() {
|
||||
|
101
irc/socket.go
101
irc/socket.go
@ -31,23 +31,26 @@ type Socket struct {
|
||||
|
||||
maxSendQBytes int
|
||||
|
||||
// coordination system for asynchronous writes
|
||||
buffer []byte
|
||||
lineToSendExists chan bool
|
||||
// this is a trylock enforcing that only one goroutine can write to `conn` at a time
|
||||
writerSlotOpen chan bool
|
||||
|
||||
buffer []byte
|
||||
closed bool
|
||||
sendQExceeded bool
|
||||
finalData string // what to send when we die
|
||||
finalized bool
|
||||
}
|
||||
|
||||
// NewSocket returns a new Socket.
|
||||
func NewSocket(conn net.Conn, maxReadQBytes int, maxSendQBytes int) Socket {
|
||||
return Socket{
|
||||
conn: conn,
|
||||
reader: bufio.NewReaderSize(conn, maxReadQBytes),
|
||||
maxSendQBytes: maxSendQBytes,
|
||||
lineToSendExists: make(chan bool, 1),
|
||||
func NewSocket(conn net.Conn, maxReadQBytes int, maxSendQBytes int) *Socket {
|
||||
result := Socket{
|
||||
conn: conn,
|
||||
reader: bufio.NewReaderSize(conn, maxReadQBytes),
|
||||
maxSendQBytes: maxSendQBytes,
|
||||
writerSlotOpen: make(chan bool, 1),
|
||||
}
|
||||
result.writerSlotOpen <- true
|
||||
return &result
|
||||
}
|
||||
|
||||
// Close stops a Socket from being able to send/receive any more data.
|
||||
@ -114,7 +117,11 @@ func (socket *Socket) Read() (string, error) {
|
||||
return line, nil
|
||||
}
|
||||
|
||||
// Write sends the given string out of Socket.
|
||||
// Write sends the given string out of Socket. Requirements:
|
||||
// 1. MUST NOT block for macroscopic amounts of time
|
||||
// 2. MUST NOT reorder messages
|
||||
// 3. MUST provide mutual exclusion for socket.conn.Write
|
||||
// 4. SHOULD NOT tie up additional goroutines, beyond the one blocked on socket.conn.Write
|
||||
func (socket *Socket) Write(data string) (err error) {
|
||||
socket.Lock()
|
||||
if socket.closed {
|
||||
@ -131,12 +138,15 @@ func (socket *Socket) Write(data string) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// wakeWriter wakes up the goroutine that actually performs the write, without blocking
|
||||
// wakeWriter starts the goroutine that actually performs the write, without blocking
|
||||
func (socket *Socket) wakeWriter() {
|
||||
// nonblocking send to the channel, no-op if it's full
|
||||
// attempt to acquire the trylock
|
||||
select {
|
||||
case socket.lineToSendExists <- true:
|
||||
case <-socket.writerSlotOpen:
|
||||
// acquired the trylock; send() will release it
|
||||
go socket.send()
|
||||
default:
|
||||
// failed to acquire; the holder will check for more data after releasing it
|
||||
}
|
||||
}
|
||||
|
||||
@ -154,32 +164,59 @@ func (socket *Socket) IsClosed() bool {
|
||||
return socket.closed
|
||||
}
|
||||
|
||||
// RunSocketWriter starts writing messages to the outgoing socket.
|
||||
func (socket *Socket) RunSocketWriter() {
|
||||
localBuffer := make([]byte, 0)
|
||||
shouldStop := false
|
||||
for !shouldStop {
|
||||
// wait for new lines
|
||||
select {
|
||||
case <-socket.lineToSendExists:
|
||||
// retrieve the buffered data, clear the buffer
|
||||
socket.Lock()
|
||||
localBuffer = append(localBuffer, socket.buffer...)
|
||||
socket.buffer = socket.buffer[:0]
|
||||
socket.Unlock()
|
||||
// is there data to write?
|
||||
func (socket *Socket) readyToWrite() bool {
|
||||
socket.Lock()
|
||||
defer socket.Unlock()
|
||||
// on the first time observing socket.closed, we still have to write socket.finalData
|
||||
return !socket.finalized && (len(socket.buffer) > 0 || socket.closed || socket.sendQExceeded)
|
||||
}
|
||||
|
||||
_, err := socket.conn.Write(localBuffer)
|
||||
localBuffer = localBuffer[:0]
|
||||
|
||||
socket.Lock()
|
||||
shouldStop = (err != nil) || socket.closed || socket.sendQExceeded
|
||||
socket.Unlock()
|
||||
// send actually writes messages to socket.Conn; it may block
|
||||
func (socket *Socket) send() {
|
||||
for {
|
||||
// we are holding the trylock: actually do the write
|
||||
socket.performWrite()
|
||||
// surrender the trylock, avoiding a race where a write comes in after we've
|
||||
// checked readyToWrite() and it returned false, but while we still hold the trylock:
|
||||
socket.writerSlotOpen <- true
|
||||
// check if more data came in while we held the trylock:
|
||||
if !socket.readyToWrite() {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-socket.writerSlotOpen:
|
||||
// got the trylock, loop back around and write
|
||||
default:
|
||||
// failed to acquire; exit and wait for the holder to observe readyToWrite()
|
||||
// after releasing it
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// write the contents of the buffer, then see if we need to close
|
||||
func (socket *Socket) performWrite() {
|
||||
// retrieve the buffered data, clear the buffer
|
||||
socket.Lock()
|
||||
buffer := socket.buffer
|
||||
socket.buffer = nil
|
||||
socket.Unlock()
|
||||
|
||||
_, err := socket.conn.Write(buffer)
|
||||
|
||||
socket.Lock()
|
||||
shouldClose := (err != nil) || socket.closed || socket.sendQExceeded
|
||||
socket.Unlock()
|
||||
|
||||
if !shouldClose {
|
||||
return
|
||||
}
|
||||
|
||||
// mark the socket closed (if someone hasn't already), then write error lines
|
||||
socket.Lock()
|
||||
socket.closed = true
|
||||
socket.finalized = true
|
||||
finalData := socket.finalData
|
||||
if socket.sendQExceeded {
|
||||
finalData = "\r\nERROR :SendQ Exceeded\r\n"
|
||||
|
57
irc/stats.go
Normal file
57
irc/stats.go
Normal file
@ -0,0 +1,57 @@
|
||||
package irc
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Stats contains the numbers of total, invisible and operators on the server
|
||||
type Stats struct {
|
||||
sync.RWMutex
|
||||
|
||||
Total int
|
||||
Invisible int
|
||||
Operators int
|
||||
}
|
||||
|
||||
// NewStats creates a new instance of Stats
|
||||
func NewStats() *Stats {
|
||||
serverStats := &Stats{
|
||||
Total: 0,
|
||||
Invisible: 0,
|
||||
Operators: 0,
|
||||
}
|
||||
|
||||
return serverStats
|
||||
}
|
||||
|
||||
// ChangeTotal increments the total user count on server
|
||||
func (s *Stats) ChangeTotal(i int) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
s.Total += i
|
||||
}
|
||||
|
||||
// ChangeInvisible increments the invisible count
|
||||
func (s *Stats) ChangeInvisible(i int) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
s.Invisible += i
|
||||
}
|
||||
|
||||
// ChangeOperators increases the operator count
|
||||
func (s *Stats) ChangeOperators(i int) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
s.Operators += i
|
||||
}
|
||||
|
||||
// GetStats retrives total, invisible and oper count
|
||||
func (s *Stats) GetStats() (int, int, int) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
return s.Total, s.Invisible, s.Operators
|
||||
}
|
15
irc/types.go
15
irc/types.go
@ -26,11 +26,11 @@ func (clients ClientSet) Has(client *Client) bool {
|
||||
}
|
||||
|
||||
// MemberSet is a set of members with modes.
|
||||
type MemberSet map[*Client]modes.ModeSet
|
||||
type MemberSet map[*Client]*modes.ModeSet
|
||||
|
||||
// Add adds the given client to this set.
|
||||
func (members MemberSet) Add(member *Client) {
|
||||
members[member] = make(modes.ModeSet)
|
||||
members[member] = modes.NewModeSet()
|
||||
}
|
||||
|
||||
// Remove removes the given client from this set.
|
||||
@ -44,19 +44,10 @@ func (members MemberSet) Has(member *Client) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
// HasMode returns true if the given client is in this set with the given mode.
|
||||
func (members MemberSet) HasMode(member *Client, mode modes.Mode) bool {
|
||||
modes, ok := members[member]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return modes[mode]
|
||||
}
|
||||
|
||||
// AnyHasMode returns true if any of our clients has the given mode.
|
||||
func (members MemberSet) AnyHasMode(mode modes.Mode) bool {
|
||||
for _, modes := range members {
|
||||
if modes[mode] {
|
||||
if modes.HasMode(mode) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
31
irc/utils/os.go
Normal file
31
irc/utils/os.go
Normal file
@ -0,0 +1,31 @@
|
||||
// Copyright (c) 2018 Shivaram Lingamneni
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// implementation of `cp` (go should really provide this...)
|
||||
func CopyFile(src string, dst string) (err error) {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer in.Close()
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
closeError := out.Close()
|
||||
if err == nil {
|
||||
err = closeError
|
||||
}
|
||||
}()
|
||||
if _, err = io.Copy(out, in); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
@ -84,7 +84,10 @@ Options:
|
||||
log.Println("database initialized: ", config.Datastore.Path)
|
||||
}
|
||||
} else if arguments["upgradedb"].(bool) {
|
||||
irc.UpgradeDB(config)
|
||||
err = irc.UpgradeDB(config)
|
||||
if err != nil {
|
||||
log.Fatal("Error while upgrading db:", err.Error())
|
||||
}
|
||||
if !arguments["--quiet"].(bool) {
|
||||
log.Println("database upgraded: ", config.Datastore.Path)
|
||||
}
|
||||
|
@ -373,6 +373,11 @@ datastore:
|
||||
# path to the datastore
|
||||
path: ircd.db
|
||||
|
||||
# if the database schema requires an upgrade, `autoupgrade` will attempt to
|
||||
# perform it automatically on startup. the database will be backed
|
||||
# up, and if the upgrade fails, the original database will be restored.
|
||||
autoupgrade: true
|
||||
|
||||
# languages config
|
||||
languages:
|
||||
# whether to load languages
|
||||
|
Loading…
Reference in New Issue
Block a user