hid: port PRIVMSG, NOTICE, NAMES, WHO, WHOIS/WAS, TOPIC, SUMMON, USERS

This commit is contained in:
Přemysl Eric Janouch 2018-07-29 15:57:39 +02:00
parent 208a8fcc7e
commit 3322fe2851
1 changed files with 486 additions and 34 deletions

View File

@ -201,7 +201,7 @@ func readConfigFile(name string, output interface{}) error {
// --- Rate limiter ------------------------------------------------------------
type floodDetector struct {
interval uint // interval for the limit
interval uint // interval for the limit in seconds
limit uint // maximum number of events allowed
timestamps []int64 // timestamps of last events
pos uint // index of the oldest event
@ -217,7 +217,7 @@ func newFloodDetector(interval, limit uint) *floodDetector {
}
func (fd *floodDetector) check() bool {
now := time.Now().Unix()
now := time.Now().UnixNano()
fd.timestamps[fd.pos] = now
fd.pos++
@ -226,7 +226,7 @@ func (fd *floodDetector) check() bool {
}
var count uint
begin := now - int64(fd.interval)
begin := now - int64(time.Second)*int64(fd.interval)
for _, ts := range fd.timestamps {
if ts >= begin {
count++
@ -471,12 +471,11 @@ func newWhowasInfo(c *client) *whowasInfo {
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
type ircCommand struct {
name string
requiresRegistration bool
handler func(*message, *client)
nReceived uint // number of commands received
bytesReceived uint // number of bytes received total
bytesReceived int // number of bytes received total
}
type preparedEvent struct {
@ -563,39 +562,29 @@ func ircChannelDestroyIfEmpty(ch *channel) {
// TODO
}
// TODO: ircSendToRoommates
// Broadcast to all /other/ clients (telnet-friendly, also in accordance to
// the plan of extending this to an IRCd).
func broadcast(line string, except *client) {
for c := range clients {
if c != except {
c.send(line)
}
}
}
func ircSendToRoommates(c *client, message string) {
// TODO
}
// --- Clients (continued) -----------------------------------------------------
func clientModeToString(m uint, mode *[]byte) {
func ircAppendClientModes(m uint, mode []byte) []byte {
if 0 != m&ircUserModeInvisible {
*mode = append(*mode, 'i')
mode = append(mode, 'i')
}
if 0 != m&ircUserModeRxWallops {
*mode = append(*mode, 'w')
mode = append(mode, 'w')
}
if 0 != m&ircUserModeRestricted {
*mode = append(*mode, 'r')
mode = append(mode, 'r')
}
if 0 != m&ircUserModeOperator {
*mode = append(*mode, 'o')
mode = append(mode, 'o')
}
if 0 != m&ircUserModeRxServerNotices {
*mode = append(*mode, 's')
mode = append(mode, 's')
}
return mode
}
func (c *client) getMode() string {
@ -603,8 +592,7 @@ func (c *client) getMode() string {
if c.awayMessage != "" {
mode = append(mode, 'a')
}
clientModeToString(c.mode, &mode)
return string(mode)
return string(ircAppendClientModes(c.mode, mode))
}
func (c *client) send(line string) {
@ -1215,15 +1203,15 @@ func ircHandleVERSION(msg *message, c *client) {
c.sendISUPPORT()
}
/*
func ircChannelMulticast(ch *channel, msg string, except *client) {
for c, m := range ch.userModes {
for c := range ch.userModes {
if c != except {
c.send(msg)
}
}
}
/*
func ircModifyMode(mask *uint, mode uint, add bool) bool {
orig := *mask
if add {
@ -1271,21 +1259,484 @@ func ircHandleUserMessage(msg *message, c *client,
}
target, text := msg.params[0], msg.params[1]
if client, ok := users[ircToCanon(target)]; ok {
// TODO
_ = client
_ = text
} else if ch, ok := channels[ircToCanon(target)]; ok {
// TODO
_ = ch
message := fmt.Sprintf(":%s!%s@%s %s %s :%s",
c.nickname, c.username, c.hostname, command, target, text)
if client := users[ircToCanon(target)]; client != nil {
client.send(message)
if allowAwayReply && client.awayMessage != "" {
c.sendReply(RPL_AWAY, target, client.awayMessage)
}
// Acknowledging a message from the client to itself would be silly.
if client != c && (0 != c.capsEnabled&ircCapEchoMessage) {
c.send(message)
}
} else if ch := channels[ircToCanon(target)]; ch != nil {
modes, present := ch.userModes[c]
outsider := !present && 0 != ch.modes&ircChanModeNoOutsideMsgs
moderated := 0 != ch.modes&ircChanModeModerated &&
0 == modes&(ircChanModeVoice|ircChanModeOperator)
banned := c.inMaskList(ch.banList) && !c.inMaskList(ch.exceptionList)
if outsider || moderated || banned {
c.sendReply(ERR_CANNOTSENDTOCHAN, target)
return
}
except := c
if 0 != c.capsEnabled&ircCapEchoMessage {
except = nil
}
ircChannelMulticast(ch, message, except)
} else {
c.sendReply(ERR_NOSUCHNICK, target)
}
}
func ircHandlePRIVMSG(msg *message, c *client) {
ircHandleUserMessage(msg, c, "PRIVMSG", true /* allowAwayReply */)
// Let's not care too much about success or failure.
c.lastActive = time.Now().UnixNano()
}
func ircHandleNOTICE(msg *message, c *client) {
ircHandleUserMessage(msg, c, "NOTICE", false /* allowAwayReply */)
}
func ircHandleLIST(msg *message, c *client) {
if len(msg.params) > 1 && !isThisMe(msg.params[1]) {
c.sendReply(ERR_NOSUCHSERVER, msg.params[1])
return
}
// XXX: Maybe we should skip ircUserModeInvisible from user counts.
if len(msg.params) == 0 {
for _, ch := range channels {
if _, present := ch.userModes[c]; present ||
0 == ch.modes&(ircChanModePrivate|ircChanModeSecret) {
c.sendReply(RPL_LIST, ch.name, len(ch.userModes), ch.topic)
}
}
} else {
for _, target := range splitString(msg.params[0], ",", true) {
if ch := channels[ircToCanon(target)]; ch != nil &&
0 == ch.modes&ircChanModeSecret {
c.sendReply(RPL_LIST, ch.name, len(ch.userModes), ch.topic)
}
}
}
c.sendReply(RPL_LISTEND)
}
func ircAppendPrefixes(c *client, modes uint, buf []byte) []byte {
var all []byte
if 0 != modes&ircChanModeOperator {
all = append(all, '@')
}
if 0 != modes&ircChanModeVoice {
all = append(all, '+')
}
if len(all) > 0 {
if 0 != c.capsEnabled&ircCapMultiPrefix {
buf = append(buf, all...)
} else {
buf = append(buf, all[0])
}
}
return buf
}
func ircMakeRPLNAMREPLYItem(c, target *client, modes uint) string {
result := string(ircAppendPrefixes(c, modes, nil)) + target.nickname
if 0 != c.capsEnabled&ircCapUserhostInNames {
result += fmt.Sprintf("!%s@%s", target.username, target.hostname)
}
return result
}
// TODO: Consider using *client instead of string as the map key.
func ircSendRPLNAMREPLY(c *client, ch *channel, usedNicks map[string]bool) {
kind := '='
if 0 != ch.modes&ircChanModeSecret {
kind = '@'
} else if 0 != ch.modes&ircChanModePrivate {
kind = '*'
}
_, present := ch.userModes[c]
var nicks []string
for client, modes := range ch.userModes {
if !present && 0 != client.mode&ircUserModeInvisible {
continue
}
if usedNicks != nil {
usedNicks[ircToCanon(client.nickname)] = true
}
nicks = append(nicks, ircMakeRPLNAMREPLYItem(c, client, modes))
}
c.sendReplyVector(RPL_NAMREPLY, nicks, kind, ch.name, "")
}
func ircSendDisassociatedNames(c *client, usedNicks map[string]bool) {
var nicks []string
for canonNickname, client := range users {
if 0 == client.mode&ircUserModeInvisible && !usedNicks[canonNickname] {
nicks = append(nicks, ircMakeRPLNAMREPLYItem(c, client, 0))
}
}
if len(nicks) > 0 {
c.sendReplyVector(RPL_NAMREPLY, nicks, '*', "*", "")
}
}
func ircHandleNAMES(msg *message, c *client) {
if len(msg.params) > 1 && !isThisMe(msg.params[1]) {
c.sendReply(ERR_NOSUCHSERVER, msg.params[1])
return
}
if len(msg.params) == 0 {
usedNicks := make(map[string]bool)
for _, ch := range channels {
if _, present := ch.userModes[c]; present ||
0 == ch.modes&(ircChanModePrivate|ircChanModeSecret) {
ircSendRPLNAMREPLY(c, ch, usedNicks)
}
}
// Also send all visible users we haven't listed yet.
ircSendDisassociatedNames(c, usedNicks)
c.sendReply(RPL_ENDOFNAMES, "*")
} else {
for _, target := range splitString(msg.params[0], ",", true) {
if ch := channels[ircToCanon(target)]; ch == nil {
} else if _, present := ch.userModes[c]; present ||
0 == ch.modes&ircChanModeSecret {
ircSendRPLNAMREPLY(c, ch, nil)
c.sendReply(RPL_ENDOFNAMES, target)
}
}
}
}
func ircSendRPLWHOREPLY(c *client, ch *channel, target *client) {
var chars []byte
if target.awayMessage != "" {
chars = append(chars, 'G')
} else {
chars = append(chars, 'H')
}
if 0 != target.mode&ircUserModeOperator {
chars = append(chars, '*')
}
channelName := "*"
if ch != nil {
channelName = ch.name
if modes, present := ch.userModes[target]; present {
chars = ircAppendPrefixes(c, modes, chars)
}
}
c.sendReply(RPL_WHOREPLY, channelName,
target.username, target.hostname, serverName,
target.nickname, string(chars), 0 /* hop count */, target.realname)
}
func ircMatchSendRPLWHOREPLY(c, target *client, mask string) {
isRoommate := false
for _, ch := range channels {
_, presentClient := ch.userModes[c]
_, presentTarget := ch.userModes[target]
if presentClient && presentTarget {
isRoommate = true
break
}
}
if !isRoommate && 0 != target.mode&ircUserModeInvisible {
return
}
if !ircFnmatch(mask, target.hostname) &&
!ircFnmatch(mask, target.nickname) &&
!ircFnmatch(mask, target.realname) &&
!ircFnmatch(mask, serverName) {
return
}
// Try to find a channel they're on that's visible to us.
var userCh *channel
for _, ch := range channels {
_, presentClient := ch.userModes[c]
_, presentTarget := ch.userModes[target]
if presentTarget && (presentClient ||
0 == ch.modes&(ircChanModePrivate|ircChanModeSecret)) {
userCh = ch
break
}
}
ircSendRPLWHOREPLY(c, userCh, target)
}
func ircHandleWHO(msg *message, c *client) {
onlyOps := len(msg.params) > 1 && msg.params[1] == "o"
shownMask, usedMask := "*", "*"
if len(msg.params) > 0 {
shownMask = msg.params[0]
if shownMask != "0" {
usedMask = shownMask
}
}
if ch := channels[ircToCanon(usedMask)]; ch != nil {
_, present := ch.userModes[c]
if present || 0 == ch.modes&ircChanModeSecret {
for client := range ch.userModes {
if (present || 0 == client.mode&ircUserModeInvisible) &&
(!onlyOps || 0 != client.mode&ircUserModeOperator) {
ircSendRPLWHOREPLY(c, ch, client)
}
}
}
} else {
for _, client := range users {
if !onlyOps || 0 != client.mode&ircUserModeOperator {
ircMatchSendRPLWHOREPLY(c, client, usedMask)
}
}
}
c.sendReply(RPL_ENDOFWHO, shownMask)
}
func ircSendWHOISReply(c, target *client) {
nick := target.nickname
c.sendReply(RPL_WHOISUSER, nick,
target.username, target.hostname, target.realname)
c.sendReply(RPL_WHOISSERVER, nick,
serverName, "TODO server_info from configuration")
if 0 != target.mode&ircUserModeOperator {
c.sendReply(RPL_WHOISOPERATOR, nick)
}
c.sendReply(RPL_WHOISIDLE, nick,
(time.Now().UnixNano()-target.lastActive)/int64(time.Second))
if target.awayMessage != "" {
c.sendReply(RPL_AWAY, nick, target.awayMessage)
}
var chans []string
for _, ch := range channels {
_, presentClient := ch.userModes[c]
modes, presentTarget := ch.userModes[target]
if presentTarget && (presentClient ||
0 == ch.modes&(ircChanModePrivate|ircChanModeSecret)) {
// TODO: Deduplicate, ircAppendPrefixes just also cuts prefixes.
var all []byte
if 0 != modes&ircChanModeOperator {
all = append(all, '@')
}
if 0 != modes&ircChanModeVoice {
all = append(all, '+')
}
chans = append(chans, string(all)+ch.name)
}
}
c.sendReplyVector(RPL_WHOISCHANNELS, chans, nick, "")
c.sendReply(RPL_ENDOFWHOIS, nick)
}
func ircHandleWHOIS(msg *message, c *client) {
if len(msg.params) < 1 {
c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
return
}
if len(msg.params) > 1 && !isThisMe(msg.params[0]) {
c.sendReply(ERR_NOSUCHSERVER, msg.params[0])
return
}
masksStr := msg.params[0]
if len(msg.params) > 1 {
masksStr = msg.params[1]
}
for _, mask := range splitString(masksStr, ",", true /* ignoreEmpty */) {
if strings.IndexAny(mask, "*?") < 0 {
if target := users[ircToCanon(mask)]; target == nil {
c.sendReply(ERR_NOSUCHNICK, mask)
} else {
ircSendWHOISReply(c, target)
}
} else {
found := false
for _, target := range users {
if ircFnmatch(mask, target.nickname) {
ircSendWHOISReply(c, target)
found = true
}
}
if !found {
c.sendReply(ERR_NOSUCHNICK, mask)
}
}
}
}
func ircHandleWHOWAS(msg *message, c *client) {
if len(msg.params) < 1 {
c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
return
}
if len(msg.params) > 2 && !isThisMe(msg.params[2]) {
c.sendReply(ERR_NOSUCHSERVER, msg.params[2])
return
}
// The "count" parameter is ignored, we only store one entry for a nick.
for _, nick := range splitString(msg.params[0], ",", true) {
if info := whowas[ircToCanon(nick)]; info == nil {
c.sendReply(ERR_WASNOSUCHNICK, nick)
} else {
c.sendReply(RPL_WHOWASUSER, nick,
info.username, info.hostname, info.realname)
c.sendReply(RPL_WHOISSERVER, nick,
serverName, "TODO server_info from configuration")
}
c.sendReply(RPL_ENDOFWHOWAS, nick)
}
}
func ircSendRPLTOPIC(c *client, ch *channel) {
if ch.topic == "" {
c.sendReply(RPL_NOTOPIC, ch.name)
} else {
c.sendReply(RPL_TOPIC, ch.name, ch.topic)
c.sendReply(RPL_TOPICWHOTIME,
ch.name, ch.topicWho, ch.topicTime/int64(time.Second))
}
}
func ircHandleTOPIC(msg *message, c *client) {
if len(msg.params) < 1 {
c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
return
}
target := msg.params[0]
ch := channels[ircToCanon(target)]
if ch == nil {
c.sendReply(ERR_NOSUCHCHANNEL, target)
return
}
if len(msg.params) < 2 {
ircSendRPLTOPIC(c, ch)
return
}
modes, present := ch.userModes[c]
if !present {
c.sendReply(ERR_NOTONCHANNEL, target)
return
}
if 0 != ch.modes&ircChanModeProtectedTopic &&
0 == modes&ircChanModeOperator {
c.sendReply(ERR_CHANOPRIVSNEEDED, target)
return
}
ch.topic = msg.params[1]
ch.topicWho = fmt.Sprintf("%s@%s@%s", c.nickname, c.username, c.hostname)
ch.topicTime = time.Now().UnixNano()
message := fmt.Sprintf(":%s!%s@%s TOPIC %s :%s",
c.nickname, c.username, c.hostname, target, ch.topic)
ircChannelMulticast(ch, message, nil)
}
// TODO: All the various real command handlers.
func ircHandleX(msg *message, c *client) {
if len(msg.params) < 1 {
c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
return
}
}
func ircHandleSUMMON(msg *message, c *client) {
c.sendReply(ERR_SUMMONDISABLED)
}
func ircHandleUSERS(msg *message, c *client) {
c.sendReply(ERR_USERSDISABLED)
}
// -----------------------------------------------------------------------------
// TODO: Add an index for IRC_ERR_NOSUCHSERVER validation?
// TODO: Add a minimal parameter count?
// TODO: Add a field for oper-only commands?
var ircHandlers = map[string]*ircCommand{
"CAP": {false, ircHandleCAP, 0, 0},
"PASS": {false, ircHandlePASS, 0, 0},
"NICK": {false, ircHandleNICK, 0, 0},
"USER": {false, ircHandleUSER, 0, 0},
"USERHOST": {true, ircHandleUSERHOST, 0, 0},
"LUSERS": {true, ircHandleLUSERS, 0, 0},
"MOTD": {true, ircHandleMOTD, 0, 0},
"PING": {true, ircHandlePING, 0, 0},
"PONG": {false, ircHandlePONG, 0, 0},
"QUIT": {false, ircHandleQUIT, 0, 0},
"TIME": {true, ircHandleTIME, 0, 0},
"VERSION": {true, ircHandleVERSION, 0, 0},
"USERS": {true, ircHandleUSERS, 0, 0},
"SUMMON": {true, ircHandleSUMMON, 0, 0},
"MODE": {true, ircHandleMODE, 0, 0},
"PRIVMSG": {true, ircHandlePRIVMSG, 0, 0},
"NOTICE": {true, ircHandleNOTICE, 0, 0},
"TOPIC": {true, ircHandleTOPIC, 0, 0},
"LIST": {true, ircHandleLIST, 0, 0},
"NAMES": {true, ircHandleNAMES, 0, 0},
"WHO": {true, ircHandleWHO, 0, 0},
"WHOIS": {true, ircHandleWHOIS, 0, 0},
"WHOWAS": {true, ircHandleWHOWAS, 0, 0},
}
func ircProcessMessage(c *client, msg *message, raw string) {
if c.closing {
return
}
c.nReceivedMessages++
c.receivedBytes += len(raw) + 2
if !c.antiflood.check() {
c.closeLink("Excess flood")
return
}
if cmd, ok := ircHandlers[ircToCanon(msg.command)]; !ok {
c.sendReply(ERR_UNKNOWNCOMMAND, msg.command)
} else {
cmd.nReceived++
cmd.bytesReceived += len(raw) + 2
if cmd.requiresRegistration && !c.registered {
c.sendReply(ERR_NOTREGISTERED)
} else {
cmd.handler(msg, c)
}
}
}
// --- ? -----------------------------------------------------------------------
@ -1338,7 +1789,8 @@ func (c *client) onRead(data []byte, readErr error) {
msg.params = append(msg.params, x[1:])
}
broadcast(line, c)
// XXX: And since it accepts LF, we miscalculate receivedBytes within.
ircProcessMessage(c, &msg, line)
}
if readErr != nil {