3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-11-25 21:39:25 +01:00

Add MONITOR command

This commit is contained in:
Daniel Oaks 2016-10-16 20:14:56 +10:00
parent 859d0cd607
commit 1bab81091f
10 changed files with 266 additions and 21 deletions

View File

@ -14,7 +14,7 @@ Improved compatibility, more features, etc.
### Added ### Added
* Added integrated help (with the `/HELP` command). * Added integrated help (with the `/HELP` command).
* Added support for IRCv3.2 [capability negotiation](http://ircv3.net/specs/core/capability-negotiation-3.2.html) including CAP values. * Added support for IRCv3.2 [capability negotiation](http://ircv3.net/specs/core/capability-negotiation-3.2.html) including CAP values.
* Added support for IRCv3 capability [`account-notify`](http://ircv3.net/specs/extensions/account-notify-3.1.html), [`invite-notify`](http://ircv3.net/specs/extensions/invite-notify-3.2.html), [`sasl`](http://ircv3.net/specs/extensions/sasl-3.2.html), and draft capability [`message-tags`](http://ircv3.net/specs/core/message-tags-3.3.html) as `draft/message-tags`. * Added support for IRCv3 capability [`account-notify`](http://ircv3.net/specs/extensions/account-notify-3.1.html), [`invite-notify`](http://ircv3.net/specs/extensions/invite-notify-3.2.html), [`monitor`](http://ircv3.net/specs/core/monitor-3.2.html), [`sasl`](http://ircv3.net/specs/extensions/sasl-3.2.html), and draft capability [`message-tags`](http://ircv3.net/specs/core/message-tags-3.3.html) as `draft/message-tags`.
### Changed ### Changed
* Casemapping changed from custom unicode mapping to preliminary [rfc7700](https://github.com/ircv3/ircv3-specifications/pull/272) mapping. * Casemapping changed from custom unicode mapping to preliminary [rfc7700](https://github.com/ircv3/ircv3-specifications/pull/272) mapping.

View File

@ -44,6 +44,7 @@ type Client struct {
hops uint hops uint
hostname string hostname string
idleTimer *time.Timer idleTimer *time.Timer
monitoring map[string]bool
nick string nick string
nickCasefolded string nickCasefolded string
nickMaskString string // cache for nickmask string since it's used with lots of replies nickMaskString string // cache for nickmask string since it's used with lots of replies
@ -59,6 +60,7 @@ type Client struct {
username string username string
} }
// NewClient returns a client with all the appropriate info setup.
func NewClient(server *Server, conn net.Conn, isTLS bool) *Client { func NewClient(server *Server, conn net.Conn, isTLS bool) *Client {
now := time.Now() now := time.Now()
socket := NewSocket(conn) socket := NewSocket(conn)
@ -71,6 +73,7 @@ func NewClient(server *Server, conn net.Conn, isTLS bool) *Client {
channels: make(ChannelSet), channels: make(ChannelSet),
ctime: now, ctime: now,
flags: make(map[UserMode]bool), flags: make(map[UserMode]bool),
monitoring: make(map[string]bool),
server: server, server: server,
socket: &socket, socket: &socket,
account: &NoAccount, account: &NoAccount,
@ -214,6 +217,8 @@ func (client *Client) Register() {
} }
client.registered = true client.registered = true
client.Touch() client.Touch()
client.alertMonitors()
} }
func (client *Client) IdleTime() time.Duration { func (client *Client) IdleTime() time.Duration {
@ -306,7 +311,6 @@ func (client *Client) ChangeNickname(nickname string) {
client.nick = nickname client.nick = nickname
client.updateNickMask() client.updateNickMask()
client.server.clients.Add(client) client.server.clients.Add(client)
client.Send(nil, origNickMask, "NICK", nickname)
for friend := range client.Friends() { for friend := range client.Friends() {
friend.Send(nil, origNickMask, "NICK", nickname) friend.Send(nil, origNickMask, "NICK", nickname)
} }
@ -332,6 +336,14 @@ func (client *Client) destroy() {
friends := client.Friends() friends := client.Friends()
friends.Remove(client) friends.Remove(client)
// alert monitors
for _, mClient := range client.server.monitoring[client.nickCasefolded] {
mClient.Send(nil, client.server.name, RPL_MONOFFLINE, mClient.nick, client.nick)
}
// remove my monitors
client.clearMonitorList()
// clean up channels // clean up channels
for channel := range client.channels { for channel := range client.channels {
channel.Quit(client) channel.Quit(client)

View File

@ -43,6 +43,15 @@ func NewClientLookupSet() *ClientLookupSet {
} }
} }
func (clients *ClientLookupSet) Has(nick string) bool {
casefoldedName, err := CasefoldName(nick)
if err == nil {
return false
}
_, exists := clients.byNick[casefoldedName]
return exists
}
func (clients *ClientLookupSet) Get(nick string) *Client { func (clients *ClientLookupSet) Get(nick string) *Client {
casefoldedName, err := CasefoldName(nick) casefoldedName, err := CasefoldName(nick)
if err == nil { if err == nil {

View File

@ -100,6 +100,10 @@ var Commands = map[string]Command{
handler: modeHandler, handler: modeHandler,
minParams: 1, minParams: 1,
}, },
"MONITOR": {
handler: monitorHandler,
minParams: 1,
},
"MOTD": { "MOTD": {
handler: motdHandler, handler: motdHandler,
minParams: 0, minParams: 0,

View File

@ -93,12 +93,13 @@ type Config struct {
Operator map[string]*PassConfig Operator map[string]*PassConfig
Limits struct { Limits struct {
NickLen int `yaml:"nicklen"` NickLen uint `yaml:"nicklen"`
ChannelLen int `yaml:"channellen"` ChannelLen uint `yaml:"channellen"`
AwayLen int `yaml:"awaylen"` AwayLen uint `yaml:"awaylen"`
KickLen int `yaml:"kicklen"` KickLen uint `yaml:"kicklen"`
TopicLen int `yaml:"topiclen"` TopicLen uint `yaml:"topiclen"`
WhowasEntries uint `yaml:"whowas-entries"` WhowasEntries uint `yaml:"whowas-entries"`
MonitorEntries uint `yaml:"monitor-entries"`
} }
} }
@ -163,7 +164,7 @@ func LoadConfig(filename string) (config *Config, err error) {
if len(config.Server.Listen) == 0 { if len(config.Server.Listen) == 0 {
return nil, errors.New("Server listening addresses missing") return nil, errors.New("Server listening addresses missing")
} }
if config.Limits.NickLen < 1 || config.Limits.ChannelLen < 2 || config.Limits.AwayLen < 1 || config.Limits.TopicLen < 1 || config.Limits.TopicLen < 1 { if config.Limits.NickLen < 1 || config.Limits.ChannelLen < 2 || config.Limits.AwayLen < 1 || config.Limits.KickLen < 1 || config.Limits.TopicLen < 1 {
return nil, errors.New("Limits aren't setup properly, check them and make them sane") return nil, errors.New("Limits aren't setup properly, check them and make them sane")
} }
return config, nil return config, nil

205
irc/monitor.go Normal file
View File

@ -0,0 +1,205 @@
// Copyright (c) 2016- Daniel Oaks <daniel@danieloaks.net>
// released under the MIT license
package irc
import (
"strconv"
"strings"
"github.com/DanielOaks/girc-go/ircmsg"
)
// alertMonitors alerts everyone monitoring us that we're online.
func (client *Client) alertMonitors() {
// alert monitors
for _, mClient := range client.server.monitoring[client.nickCasefolded] {
// don't have to notify ourselves
if &mClient != client {
mClient.Send(nil, client.server.name, RPL_MONONLINE, mClient.nick, client.nickMaskString)
}
}
}
// clearMonitorList clears our MONITOR list.
func (client *Client) clearMonitorList() {
for name := range client.monitoring {
// just removes current client from the list
orig := client.server.monitoring[name]
var index int
for i, cli := range orig {
if &cli == client {
index = i
break
}
}
client.server.monitoring[name] = append(orig[:index], orig[index+1:]...)
}
client.monitoring = make(map[string]bool)
}
var (
metadataSubcommands = map[string]func(server *Server, client *Client, msg ircmsg.IrcMessage) bool{
"-": monitorRemoveHandler,
"+": monitorAddHandler,
"c": monitorClearHandler,
"l": monitorListHandler,
"s": monitorStatusHandler,
}
)
func monitorHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
handler, exists := metadataSubcommands[strings.ToLower(msg.Params[0])]
if !exists {
client.Send(nil, server.name, ERR_UNKNOWNERROR, client.nick, "MONITOR", msg.Params[0], "Unknown subcommand")
return false
}
return handler(server, client, msg)
}
func monitorRemoveHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
if len(msg.Params) < 2 {
client.Send(nil, server.name, ERR_NEEDMOREPARAMS, client.nick, msg.Command, "Not enough parameters")
return false
}
targets := strings.Split(msg.Params[1], ",")
for len(targets) > 0 {
// check name length
if len(targets[0]) < 1 {
targets = targets[1:]
continue
}
// remove target
casefoldedTarget, err := CasefoldName(targets[0])
if err != nil {
// skip silently I guess
targets = targets[1:]
continue
}
if client.monitoring[casefoldedTarget] {
// just removes current client from the list
orig := server.monitoring[casefoldedTarget]
var index int
for i, cli := range orig {
if &cli == client {
index = i
break
}
}
server.monitoring[casefoldedTarget] = append(orig[:index], orig[index+1:]...)
delete(client.monitoring, casefoldedTarget)
}
// remove first element of targets list
targets = targets[1:]
}
return false
}
func monitorAddHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
if len(msg.Params) < 2 {
client.Send(nil, server.name, ERR_NEEDMOREPARAMS, client.nick, msg.Command, "Not enough parameters")
return false
}
var online []string
var offline []string
targets := strings.Split(msg.Params[1], ",")
for len(targets) > 0 {
// check name length
if len(targets[0]) < 1 {
targets = targets[1:]
continue
}
// check the monitor list length
if len(client.monitoring) >= server.limits.MonitorEntries {
client.Send(nil, server.name, ERR_MONLISTFULL, client.nick, strconv.Itoa(server.limits.MonitorEntries), strings.Join(targets, ","))
break
}
// add target
casefoldedTarget, err := CasefoldName(targets[0])
if err != nil {
// skip silently I guess
targets = targets[1:]
continue
}
if !client.monitoring[casefoldedTarget] {
client.monitoring[casefoldedTarget] = true
orig := server.monitoring[casefoldedTarget]
server.monitoring[casefoldedTarget] = append(orig, *client)
}
// add to online / offline lists
target := server.clients.Get(casefoldedTarget)
if target == nil {
offline = append(offline, targets[0])
} else {
online = append(online, target.nickMaskString)
}
// remove first element of targets list
targets = targets[1:]
}
if len(online) > 0 {
client.Send(nil, server.name, RPL_MONONLINE, client.nick, strings.Join(online, ","))
}
if len(offline) > 0 {
client.Send(nil, server.name, RPL_MONOFFLINE, client.nick, strings.Join(offline, ","))
}
return false
}
func monitorClearHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
client.clearMonitorList()
return false
}
func monitorListHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
var monitorList []string
for name := range client.monitoring {
monitorList = append(monitorList, name)
}
client.Send(nil, server.name, RPL_MONLIST, client.nick, strings.Join(monitorList, ","))
return false
}
func monitorStatusHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
var online []string
var offline []string
for name := range client.monitoring {
target := server.clients.Get(name)
if target == nil {
offline = append(offline, name)
} else {
online = append(online, target.nickMaskString)
}
}
if len(online) > 0 {
client.Send(nil, server.name, RPL_MONONLINE, client.nick, strings.Join(online, ","))
}
if len(offline) > 0 {
client.Send(nil, server.name, RPL_MONOFFLINE, client.nick, strings.Join(offline, ","))
}
return false
}

View File

@ -43,6 +43,7 @@ func nickHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
if client.registered { if client.registered {
client.ChangeNickname(nicknameRaw) client.ChangeNickname(nicknameRaw)
client.alertMonitors()
} else { } else {
client.SetNickname(nicknameRaw) client.SetNickname(nicknameRaw)
} }

View File

@ -153,6 +153,11 @@ const (
RPL_HELPSTART = "704" RPL_HELPSTART = "704"
RPL_HELPTXT = "705" RPL_HELPTXT = "705"
RPL_ENDOFHELP = "706" RPL_ENDOFHELP = "706"
RPL_MONONLINE = "730"
RPL_MONOFFLINE = "731"
RPL_MONLIST = "732"
RPL_ENDOFMONLIST = "733"
ERR_MONLISTFULL = "734"
RPL_LOGGEDIN = "900" RPL_LOGGEDIN = "900"
RPL_LOGGEDOUT = "901" RPL_LOGGEDOUT = "901"
ERR_NICKLOCKED = "902" ERR_NICKLOCKED = "902"

View File

@ -26,11 +26,12 @@ import (
// Limits holds the maximum limits for various things such as topic lengths // Limits holds the maximum limits for various things such as topic lengths
type Limits struct { type Limits struct {
AwayLen int AwayLen int
ChannelLen int ChannelLen int
KickLen int KickLen int
NickLen int MonitorEntries int
TopicLen int NickLen int
TopicLen int
} }
type Server struct { type Server struct {
@ -42,6 +43,7 @@ type Server struct {
store buntdb.DB store buntdb.DB
idle chan *Client idle chan *Client
limits Limits limits Limits
monitoring map[string][]Client
motdLines []string motdLines []string
name string name string
nameCasefolded string nameCasefolded string
@ -85,12 +87,14 @@ func NewServer(config *Config) *Server {
ctime: time.Now(), ctime: time.Now(),
idle: make(chan *Client), idle: make(chan *Client),
limits: Limits{ limits: Limits{
AwayLen: config.Limits.AwayLen, AwayLen: int(config.Limits.AwayLen),
ChannelLen: config.Limits.ChannelLen, ChannelLen: int(config.Limits.ChannelLen),
KickLen: config.Limits.KickLen, KickLen: int(config.Limits.KickLen),
NickLen: config.Limits.NickLen, MonitorEntries: int(config.Limits.MonitorEntries),
TopicLen: config.Limits.TopicLen, NickLen: int(config.Limits.NickLen),
TopicLen: int(config.Limits.TopicLen),
}, },
monitoring: make(map[string][]Client),
name: config.Server.Name, name: config.Server.Name,
nameCasefolded: casefoldedName, nameCasefolded: casefoldedName,
newConns: make(chan clientConn), newConns: make(chan clientConn),
@ -172,15 +176,16 @@ func NewServer(config *Config) *Server {
server.isupport.Add("AWAYLEN", strconv.Itoa(server.limits.AwayLen)) server.isupport.Add("AWAYLEN", strconv.Itoa(server.limits.AwayLen))
server.isupport.Add("CASEMAPPING", "rfc7700") server.isupport.Add("CASEMAPPING", "rfc7700")
server.isupport.Add("CHANMODES", strings.Join([]string{ChannelModes{BanMask, ExceptMask, InviteMask}.String(), "", ChannelModes{UserLimit, Key}.String(), ChannelModes{InviteOnly, Moderated, NoOutside, OpOnlyTopic, Secret}.String()}, ",")) server.isupport.Add("CHANMODES", strings.Join([]string{ChannelModes{BanMask, ExceptMask, InviteMask}.String(), "", ChannelModes{UserLimit, Key}.String(), ChannelModes{InviteOnly, Moderated, NoOutside, OpOnlyTopic, Secret}.String()}, ","))
server.isupport.Add("CHANNELLEN", strconv.Itoa(config.Limits.ChannelLen)) server.isupport.Add("CHANNELLEN", strconv.Itoa(server.limits.ChannelLen))
server.isupport.Add("CHANTYPES", "#") server.isupport.Add("CHANTYPES", "#")
server.isupport.Add("EXCEPTS", "") server.isupport.Add("EXCEPTS", "")
server.isupport.Add("INVEX", "") server.isupport.Add("INVEX", "")
server.isupport.Add("KICKLEN", strconv.Itoa(server.limits.KickLen)) server.isupport.Add("KICKLEN", strconv.Itoa(server.limits.KickLen))
// server.isupport.Add("MAXLIST", "") //TODO(dan): Support max list length? // server.isupport.Add("MAXLIST", "") //TODO(dan): Support max list length?
// server.isupport.Add("MODES", "") //TODO(dan): Support max modes? // server.isupport.Add("MODES", "") //TODO(dan): Support max modes?
server.isupport.Add("MONITOR", strconv.Itoa(server.limits.MonitorEntries))
server.isupport.Add("NETWORK", config.Network.Name) server.isupport.Add("NETWORK", config.Network.Name)
server.isupport.Add("NICKLEN", strconv.Itoa(config.Limits.NickLen)) server.isupport.Add("NICKLEN", strconv.Itoa(server.limits.NickLen))
server.isupport.Add("PREFIX", "(qaohv)~&@%+") server.isupport.Add("PREFIX", "(qaohv)~&@%+")
// server.isupport.Add("STATUSMSG", "@+") //TODO(dan): Support STATUSMSG // server.isupport.Add("STATUSMSG", "@+") //TODO(dan): Support STATUSMSG
// server.isupport.Add("TARGMAX", "") //TODO(dan): Support this // server.isupport.Add("TARGMAX", "") //TODO(dan): Support this

View File

@ -86,5 +86,8 @@ limits:
# topiclen is the maximum length of a channel topic # topiclen is the maximum length of a channel topic
topiclen: 390 topiclen: 390
# maximum number of monitor entries a client can have
monitor-entries: 100
# whowas entries to store # whowas entries to store
whowas-entries: 100 whowas-entries: 100