diff --git a/ergonomadic.go b/ergonomadic.go index dbd45ddb..2c4cce76 100644 --- a/ergonomadic.go +++ b/ergonomadic.go @@ -45,5 +45,6 @@ func main() { irc.DEBUG_CHANNEL = config.Debug.Channel irc.DEBUG_SERVER = config.Debug.Server + log.Println(irc.SEM_VER, "running") irc.NewServer(config).Run() } diff --git a/irc/channel.go b/irc/channel.go index bab45747..d7992af8 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -8,7 +8,7 @@ import ( type Channel struct { flags ChannelModeSet - lists map[ChannelMode][]UserMask + lists map[ChannelMode]UserMaskSet key string members MemberSet name string @@ -26,10 +26,10 @@ func IsChannel(target string) bool { func NewChannel(s *Server, name string) *Channel { channel := &Channel{ flags: make(ChannelModeSet), - lists: map[ChannelMode][]UserMask{ - BanMask: []UserMask{}, - ExceptMask: []UserMask{}, - InviteMask: []UserMask{}, + lists: map[ChannelMode]UserMaskSet{ + BanMask: make(UserMaskSet), + ExceptMask: make(UserMaskSet), + InviteMask: make(UserMaskSet), }, members: make(MemberSet), name: strings.ToLower(name), @@ -307,7 +307,7 @@ func (channel *Channel) applyMode(client *Client, change *ChannelModeChange) boo case BanMask, ExceptMask, InviteMask: // TODO add/remove - for _, mask := range channel.lists[change.mode] { + for mask := range channel.lists[change.mode] { client.RplMaskList(change.mode, channel, mask) } client.RplEndOfMaskList(change.mode, channel) diff --git a/irc/commands.go b/irc/commands.go index 7f7a14bb..0fa347aa 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -656,7 +656,7 @@ func (msg *WhoisCommand) String() string { type WhoCommand struct { BaseCommand - mask Mask + mask string operatorOnly bool } @@ -665,7 +665,7 @@ func NewWhoCommand(args []string) (editableCommand, error) { cmd := &WhoCommand{} if len(args) > 0 { - cmd.mask = Mask(args[0]) + cmd.mask = args[0] } if (len(args) > 1) && (args[1] == "o") { diff --git a/irc/reply.go b/irc/reply.go index 3e69b746..27b39fa8 100644 --- a/irc/reply.go +++ b/irc/reply.go @@ -196,6 +196,16 @@ func (target *Client) RplYoureOper() { ":You are now an IRC operator") } +func (target *Client) RplWhois(client *Client) { + target.RplWhoisUser(client) + if client.flags[Operator] { + target.RplWhoisOperator(client) + } + target.RplWhoisIdle(client) + target.RplWhoisChannels(client) + target.RplEndOfWhois() +} + func (target *Client) RplWhoisUser(client *Client) { target.NumericReply(RPL_WHOISUSER, "%s %s %s * :%s", client.Nick(), client.username, client.hostname, @@ -258,7 +268,7 @@ func (target *Client) RplEndOfWho(name string) { "%s :End of WHO list", name) } -func (target *Client) RplMaskList(mode ChannelMode, channel *Channel, mask UserMask) { +func (target *Client) RplMaskList(mode ChannelMode, channel *Channel, mask string) { switch mode { case BanMask: target.RplBanList(channel, mask) @@ -284,7 +294,7 @@ func (target *Client) RplEndOfMaskList(mode ChannelMode, channel *Channel) { } } -func (target *Client) RplBanList(channel *Channel, mask UserMask) { +func (target *Client) RplBanList(channel *Channel, mask string) { target.NumericReply(RPL_BANLIST, "%s %s", channel, mask) } @@ -294,7 +304,7 @@ func (target *Client) RplEndOfBanList(channel *Channel) { "%s :End of channel ban list", channel) } -func (target *Client) RplExceptList(channel *Channel, mask UserMask) { +func (target *Client) RplExceptList(channel *Channel, mask string) { target.NumericReply(RPL_EXCEPTLIST, "%s %s", channel, mask) } @@ -304,7 +314,7 @@ func (target *Client) RplEndOfExceptList(channel *Channel) { "%s :End of channel exception list", channel) } -func (target *Client) RplInviteList(channel *Channel, mask UserMask) { +func (target *Client) RplInviteList(channel *Channel, mask string) { target.NumericReply(RPL_INVITELIST, "%s %s", channel, mask) } diff --git a/irc/server.go b/irc/server.go index ba2ed34c..fa186b0d 100644 --- a/irc/server.go +++ b/irc/server.go @@ -3,6 +3,7 @@ package irc import ( "bufio" "database/sql" + "errors" "fmt" "log" "net" @@ -18,7 +19,7 @@ import ( type Server struct { channels ChannelNameMap - clients ClientNameMap + clients *ClientLookupSet commands chan Command ctime time.Time db *sql.DB @@ -35,7 +36,7 @@ type Server struct { func NewServer(config *Config) *Server { server := &Server{ channels: make(ChannelNameMap), - clients: make(ClientNameMap), + clients: NewClientLookupSet(), commands: make(chan Command, 16), ctime: time.Now(), db: OpenDB(config.Server.Database), @@ -136,7 +137,7 @@ func (server *Server) processCommand(cmd Command) { func (server *Server) Shutdown() { server.db.Close() - for _, client := range server.clients { + for _, client := range server.clients.byNick { client.Reply(RplNotice(server, client, "shutting down")) } } @@ -556,19 +557,14 @@ func (m *WhoisCommand) HandleServer(server *Server) { // TODO implement target query for _, mask := range m.masks { - // TODO implement wildcard matching - mclient := server.clients.Get(mask) - if mclient == nil { + matches := server.clients.FindAll(mask) + if len(matches) == 0 { client.ErrNoSuchNick(mask) continue } - client.RplWhoisUser(mclient) - if mclient.flags[Operator] { - client.RplWhoisOperator(mclient) + for mclient := range matches { + client.RplWhois(mclient) } - client.RplWhoisIdle(mclient) - client.RplWhoisChannels(mclient) - client.RplEndOfWhois() } } @@ -583,9 +579,9 @@ func (msg *ChannelModeCommand) HandleServer(server *Server) { channel.Mode(client, msg.changes) } -func whoChannel(client *Client, channel *Channel) { +func whoChannel(client *Client, channel *Channel, friends ClientSet) { for member := range channel.members { - if !client.flags[Invisible] { + if !client.flags[Invisible] || friends[client] { client.RplWhoReply(channel, member) } } @@ -593,27 +589,21 @@ func whoChannel(client *Client, channel *Channel) { func (msg *WhoCommand) HandleServer(server *Server) { client := msg.Client() + friends := client.Friends() + mask := msg.mask - // TODO implement wildcard matching - mask := string(msg.mask) if mask == "" { for _, channel := range server.channels { - for member := range channel.members { - if !client.flags[Invisible] { - client.RplWhoReply(channel, member) - } - } + whoChannel(client, channel, friends) } } else if IsChannel(mask) { + // TODO implement wildcard matching channel := server.channels.Get(mask) if channel != nil { - for member := range channel.members { - client.RplWhoReply(channel, member) - } + whoChannel(client, channel, friends) } } else { - mclient := server.clients.Get(mask) - if mclient != nil { + for mclient := range server.clients.FindAll(mask) { client.RplWhoReply(nil, mclient) } } @@ -853,3 +843,163 @@ func (msg *KillCommand) HandleServer(server *Server) { quitMsg := fmt.Sprintf("KILLed by %s: %s", client.Nick(), msg.comment) target.Quit(quitMsg) } + +// +// keeping track of clients +// + +type ClientLookupSet struct { + byNick map[string]*Client + db *ClientDB +} + +func NewClientLookupSet() *ClientLookupSet { + return &ClientLookupSet{ + byNick: make(map[string]*Client), + db: NewClientDB(), + } +} + +var ( + ErrNickMissing = errors.New("nick missing") + ErrNicknameInUse = errors.New("nickname in use") + ErrNicknameMismatch = errors.New("nickname mismatch") +) + +func (clients *ClientLookupSet) Get(nick string) *Client { + return clients.byNick[strings.ToLower(nick)] +} + +func (clients *ClientLookupSet) Add(client *Client) error { + if !client.HasNick() { + return ErrNickMissing + } + if clients.Get(client.nick) != nil { + return ErrNicknameInUse + } + clients.byNick[strings.ToLower(client.nick)] = client + clients.db.Add(client) + return nil +} + +func (clients *ClientLookupSet) Remove(client *Client) error { + if !client.HasNick() { + return ErrNickMissing + } + if clients.Get(client.nick) != client { + return ErrNicknameMismatch + } + delete(clients.byNick, strings.ToLower(client.nick)) + clients.db.Remove(client) + return nil +} + +func ExpandUserHost(userhost string) (expanded string) { + expanded = userhost + // fill in missing wildcards for nicks + if !strings.Contains(expanded, "!") { + expanded += "!*" + } + if !strings.Contains(expanded, "@") { + expanded += "@*" + } + return +} + +func (clients *ClientLookupSet) FindAll(userhost string) (set ClientSet) { + userhost = ExpandUserHost(userhost) + set = make(ClientSet) + rows, err := clients.db.db.Query( + `SELECT nickname FROM client + WHERE userhost LIKE ? ESCAPE '\'`, + QuoteLike(userhost)) + if err != nil { + return + } + for rows.Next() { + var nickname string + err := rows.Scan(&nickname) + if err != nil { + return + } + client := clients.Get(nickname) + if client != nil { + set.Add(client) + } + } + return +} + +func (clients *ClientLookupSet) Find(userhost string) *Client { + userhost = ExpandUserHost(userhost) + row := clients.db.db.QueryRow( + `SELECT nickname FROM client + WHERE userhost LIKE ? ESCAPE \ + LIMIT 1`, + QuoteLike(userhost)) + var nickname string + err := row.Scan(&nickname) + if err != nil { + log.Println("ClientLookupSet.Find: ", err) + return nil + } + return clients.Get(nickname) +} + +// +// client db +// + +type ClientDB struct { + db *sql.DB +} + +func NewClientDB() *ClientDB { + db := &ClientDB{ + db: OpenDB(":memory:"), + } + _, err := db.db.Exec(` + CREATE TABLE client ( + nickname TEXT NOT NULL UNIQUE, + userhost TEXT NOT NULL)`) + if err != nil { + log.Fatal(err) + } + _, err = db.db.Exec(` + CREATE UNIQUE INDEX nickname_index ON client (nickname)`) + if err != nil { + log.Fatal(err) + } + return db +} + +func (db *ClientDB) Add(client *Client) { + _, err := db.db.Exec(`INSERT INTO client (nickname, userhost) VALUES (?, ?)`, + client.Nick(), client.UserHost()) + if err != nil { + log.Println(err) + } +} + +func (db *ClientDB) Remove(client *Client) { + _, err := db.db.Exec(`DELETE FROM client WHERE nickname = ?`, + client.Nick()) + if err != nil { + log.Println(err) + } +} + +func QuoteLike(userhost string) (like string) { + like = userhost + // escape escape char + like = strings.Replace(like, `\`, `\\`, -1) + // escape meta-many + like = strings.Replace(like, `%`, `\%`, -1) + // escape meta-one + like = strings.Replace(like, `_`, `\_`, -1) + // swap meta-many + like = strings.Replace(like, `*`, `%`, -1) + // swap meta-one + like = strings.Replace(like, `?`, `_`, -1) + return +} diff --git a/irc/types.go b/irc/types.go index 6cb49a60..9539c186 100644 --- a/irc/types.go +++ b/irc/types.go @@ -1,7 +1,6 @@ package irc import ( - "errors" "fmt" "strings" ) @@ -10,8 +9,7 @@ import ( // simple types // -// a string with wildcards -type Mask string +type UserMaskSet map[string]bool // add, remove, list modes type ModeOp rune @@ -74,40 +72,6 @@ func (channels ChannelNameMap) Remove(channel *Channel) error { return nil } -type ClientNameMap map[string]*Client - -var ( - ErrNickMissing = errors.New("nick missing") - ErrNicknameInUse = errors.New("nickname in use") - ErrNicknameMismatch = errors.New("nickname mismatch") -) - -func (clients ClientNameMap) Get(nick string) *Client { - return clients[strings.ToLower(nick)] -} - -func (clients ClientNameMap) Add(client *Client) error { - if !client.HasNick() { - return ErrNickMissing - } - if clients.Get(client.nick) != nil { - return ErrNicknameInUse - } - clients[strings.ToLower(client.nick)] = client - return nil -} - -func (clients ClientNameMap) Remove(client *Client) error { - if !client.HasNick() { - return ErrNickMissing - } - if clients.Get(client.nick) != client { - return ErrNicknameMismatch - } - delete(clients, strings.ToLower(client.nick)) - return nil -} - type ChannelModeSet map[ChannelMode]bool func (set ChannelModeSet) String() string { @@ -209,17 +173,3 @@ type RegServerCommand interface { Command HandleRegServer(*Server) } - -// -// structs -// - -type UserMask struct { - nickname Mask - username Mask - hostname Mask -} - -func (mask *UserMask) String() string { - return fmt.Sprintf("%s!%s@%s", mask.nickname, mask.username, mask.hostname) -}