From b96fdb229339137330126775636e73264a86bcd6 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Mon, 20 May 2019 19:08:57 -0400 Subject: [PATCH 1/4] support znc.in/playback --- gencapdefs.py | 6 +++ irc/caps/defs.go | 7 +++- irc/channel.go | 48 ++++++++++++++++-------- irc/client.go | 2 + irc/commands.go | 4 ++ irc/handlers.go | 18 +++++++-- irc/help.go | 7 ++++ irc/misc_test.go | 17 +++++++++ irc/znc.go | 96 ++++++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 184 insertions(+), 21 deletions(-) create mode 100644 irc/misc_test.go create mode 100644 irc/znc.go diff --git a/gencapdefs.py b/gencapdefs.py index 3cacebda..31045839 100644 --- a/gencapdefs.py +++ b/gencapdefs.py @@ -165,6 +165,12 @@ CAPDEFS = [ url="https://github.com/ircv3/ircv3-specifications/pull/362", standard="Proposed IRCv3", ), + CapDef( + identifier="ZNCPlayback", + name="znc.in/playback", + url="https://wiki.znc.in/Playback", + standard="ZNC vendor", + ), ] def validate_defs(): diff --git a/irc/caps/defs.go b/irc/caps/defs.go index c261a6e4..2d086227 100644 --- a/irc/caps/defs.go +++ b/irc/caps/defs.go @@ -7,7 +7,7 @@ package caps const ( // number of recognized capabilities: - numCapabs = 25 + numCapabs = 26 // length of the uint64 array that represents the bitset: bitsetLen = 1 ) @@ -112,6 +112,10 @@ const ( // EventPlayback is the Proposed IRCv3 capability named "draft/event-playback": // https://github.com/ircv3/ircv3-specifications/pull/362 EventPlayback Capability = iota + + // ZNCPlayback is the ZNC vendor capability named "znc.in/playback": + // https://wiki.znc.in/Playback + ZNCPlayback Capability = iota ) // `capabilityNames[capab]` is the string name of the capability `capab` @@ -142,5 +146,6 @@ var ( "oragono.io/bnc", "znc.in/self-message", "draft/event-playback", + "znc.in/playback", } ) diff --git a/irc/channel.go b/irc/channel.go index 48e51cef..8d814838 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -620,24 +620,40 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp // TODO #259 can be implemented as Flush(false) (i.e., nonblocking) while holding joinPartMutex rb.Flush(true) - var replayLimit int - customReplayLimit := client.AccountSettings().AutoreplayLines - if customReplayLimit != nil { - replayLimit = *customReplayLimit - maxLimit := channel.server.Config().History.ChathistoryMax - if maxLimit < replayLimit { - replayLimit = maxLimit - } + // autoreplay any messages as necessary + config := channel.server.Config() + var items []history.Item + if rb.session.zncPlaybackTimes != nil && (rb.session.zncPlaybackTimes.targets == nil || rb.session.zncPlaybackTimes.targets[chcfname]) { + items, _ = channel.history.Between(rb.session.zncPlaybackTimes.after, rb.session.zncPlaybackTimes.before, false, config.History.ChathistoryMax) } else { - replayLimit = channel.server.Config().History.AutoreplayOnJoin - } - if 0 < replayLimit { - // TODO don't replay the client's own JOIN line? - items := channel.history.Latest(replayLimit) - if 0 < len(items) { - channel.replayHistoryItems(rb, items, true) - rb.Flush(true) + var replayLimit int + customReplayLimit := client.AccountSettings().AutoreplayLines + if customReplayLimit != nil { + replayLimit = *customReplayLimit + maxLimit := channel.server.Config().History.ChathistoryMax + if maxLimit < replayLimit { + replayLimit = maxLimit + } + } else { + replayLimit = channel.server.Config().History.AutoreplayOnJoin } + if 0 < replayLimit { + items = channel.history.Latest(replayLimit) + } + } + // remove the client's own JOIN line from the replay + numItems := len(items) + for i := len(items) - 1; 0 <= i; i-- { + if items[i].Message.Msgid == message.Msgid { + // zero'ed items will not be replayed because their `Type` field is not recognized + items[i] = history.Item{} + numItems-- + break + } + } + if 0 < numItems { + channel.replayHistoryItems(rb, items, true) + rb.Flush(true) } } diff --git a/irc/client.go b/irc/client.go index 3beb80d6..527cdd0b 100644 --- a/irc/client.go +++ b/irc/client.go @@ -113,6 +113,8 @@ type Session struct { maxlenRest uint32 capState caps.State capVersion caps.Version + + zncPlaybackTimes *zncPlaybackTimes } // sets the session quit message, if there isn't one already diff --git a/irc/commands.go b/irc/commands.go index c505721b..84b24ecb 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -321,6 +321,10 @@ func init() { handler: whowasHandler, minParams: 1, }, + "ZNC": { + handler: zncHandler, + minParams: 1, + }, } initializeServices() diff --git a/irc/handlers.go b/irc/handlers.go index 038d296a..801cd8c6 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -2001,12 +2001,16 @@ func messageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R } channel.SendSplitMessage(msg.Command, lowestPrefix, clientOnlyTags, client, splitMsg, rb) } else { - if service, isService := OragonoServices[strings.ToLower(targetString)]; isService { - // NOTICE and TAGMSG to services are ignored - if histType == history.Privmsg { + // NOTICE and TAGMSG to services are ignored + if histType == history.Privmsg { + lowercaseTarget := strings.ToLower(targetString) + if service, isService := OragonoServices[lowercaseTarget]; isService { servicePrivmsgHandler(service, server, client, message, rb) + continue + } else if _, isZNC := zncHandlers[lowercaseTarget]; isZNC { + zncPrivmsgHandler(client, lowercaseTarget, message, rb) + continue } - continue } user := server.clients.Get(targetString) @@ -2746,3 +2750,9 @@ func whowasHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re } return false } + +// ZNC [params] +func zncHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { + zncModuleHandler(client, msg.Params[0], msg.Params[1:], rb) + return false +} diff --git a/irc/help.go b/irc/help.go index 38bb62d5..07e48bc4 100644 --- a/irc/help.go +++ b/irc/help.go @@ -540,6 +540,13 @@ Returns information for the given user(s).`, Returns historical information on the last user with the given nickname.`, }, + "znc": { + text: `ZNC [params] + +Used to emulate features of the ZNC bouncer. This command is not intended +for direct use by end users.`, + duplicate: true, + }, // Informational "modes": { diff --git a/irc/misc_test.go b/irc/misc_test.go new file mode 100644 index 00000000..73806ff6 --- /dev/null +++ b/irc/misc_test.go @@ -0,0 +1,17 @@ +// Copyright (c) 2019 Shivaram Lingamneni +// released under the MIT license + +package irc + +import ( + "testing" + "time" +) + +func TestZncTimestampParser(t *testing.T) { + assertEqual(zncWireTimeToTime("1558338348.988"), time.Unix(1558338348, 988000000), t) + assertEqual(zncWireTimeToTime("1558338348.9"), time.Unix(1558338348, 900000000), t) + assertEqual(zncWireTimeToTime("1558338348"), time.Unix(1558338348, 0), t) + assertEqual(zncWireTimeToTime(".988"), time.Unix(0, 988000000), t) + assertEqual(zncWireTimeToTime("garbage"), time.Unix(0, 0), t) +} diff --git a/irc/znc.go b/irc/znc.go new file mode 100644 index 00000000..fecc7555 --- /dev/null +++ b/irc/znc.go @@ -0,0 +1,96 @@ +// Copyright (c) 2019 Shivaram Lingamneni +// released under the MIT license + +package irc + +import ( + "fmt" + "strconv" + "strings" + "time" +) + +type zncCommandHandler func(client *Client, command string, params []string, rb *ResponseBuffer) + +var zncHandlers = map[string]zncCommandHandler{ + "*playback": zncPlaybackHandler, +} + +func zncPrivmsgHandler(client *Client, command string, privmsg string, rb *ResponseBuffer) { + zncModuleHandler(client, command, strings.Fields(privmsg), rb) +} + +func zncModuleHandler(client *Client, command string, params []string, rb *ResponseBuffer) { + command = strings.ToLower(command) + if subHandler, ok := zncHandlers[command]; ok { + subHandler(client, command, params, rb) + } else { + rb.Add(nil, "*status!znc@znc.in", "NOTICE", rb.target.Nick(), fmt.Sprintf(client.t("No such module [%s]"), command)) + } +} + +// "number of seconds (floating point for millisecond precision) elapsed since January 1, 1970" +func zncWireTimeToTime(str string) (result time.Time) { + var secondsPortion, fracPortion string + dot := strings.IndexByte(str, '.') + if dot == -1 { + secondsPortion = str + } else { + secondsPortion = str[:dot] + fracPortion = str[dot:] + } + seconds, _ := strconv.ParseInt(secondsPortion, 10, 64) + fraction, _ := strconv.ParseFloat(fracPortion, 64) + return time.Unix(seconds, int64(fraction*1000000000)) +} + +type zncPlaybackTimes struct { + after time.Time + before time.Time + targets map[string]bool // nil for "*" (everything), otherwise the channel names +} + +// https://wiki.znc.in/Playback +// PRIVMSG *playback :play [lower_bound] [upper_bound] +// e.g., PRIVMSG *playback :play * 1558374442 +func zncPlaybackHandler(client *Client, command string, params []string, rb *ResponseBuffer) { + if len(params) < 2 { + return + } else if strings.ToLower(params[0]) != "play" { + return + } + targetString := params[1] + + var after, before time.Time + if 2 < len(params) { + after = zncWireTimeToTime(params[2]) + } + if 3 < len(params) { + before = zncWireTimeToTime(params[3]) + } + + var targets map[string]bool + + // OK: the user's PMs get played back immediately on receiving this, + // then we save the timestamps in the session to handle replay on future channel joins + config := client.server.Config() + if params[1] == "*" { + items, _ := client.history.Between(after, before, false, config.History.ChathistoryMax) + client.replayPrivmsgHistory(rb, items, true) + } else { + for _, targetName := range strings.Split(targetString, ",") { + if cfTarget, err := CasefoldChannel(targetName); err == nil { + if targets == nil { + targets = make(map[string]bool) + } + targets[cfTarget] = true + } + } + } + + rb.session.zncPlaybackTimes = &zncPlaybackTimes{ + after: after, + before: before, + targets: targets, + } +} From f996e6bb87604332dac61f8b0978de4c6c9dce21 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Mon, 20 May 2019 20:08:06 -0400 Subject: [PATCH 2/4] review fix --- irc/znc.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/irc/znc.go b/irc/znc.go index fecc7555..ab5b0b3d 100644 --- a/irc/znc.go +++ b/irc/znc.go @@ -25,7 +25,9 @@ func zncModuleHandler(client *Client, command string, params []string, rb *Respo if subHandler, ok := zncHandlers[command]; ok { subHandler(client, command, params, rb) } else { - rb.Add(nil, "*status!znc@znc.in", "NOTICE", rb.target.Nick(), fmt.Sprintf(client.t("No such module [%s]"), command)) + nick := rb.target.Nick() + rb.Add(nil, client.server.name, "NOTICE", nick, fmt.Sprintf(client.t("Oragono does not emulate the ZNC module %s"), command)) + rb.Add(nil, "*status!znc@znc.in", "NOTICE", nick, fmt.Sprintf(client.t("No such module [%s]"), command)) } } From cc53caae9b05c6b76be1cfab84a4d50f9dbbaef0 Mon Sep 17 00:00:00 2001 From: Daniel Oaks Date: Wed, 22 May 2019 00:33:53 +1000 Subject: [PATCH 3/4] New translations help.lang.json (Romanian) --- languages/ro-RO-help.lang.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/languages/ro-RO-help.lang.json b/languages/ro-RO-help.lang.json index 84f3bd2b..9897ea13 100644 --- a/languages/ro-RO-help.lang.json +++ b/languages/ro-RO-help.lang.json @@ -3,7 +3,7 @@ "== Channel Modes ==\n\nOragono supports the following channel modes:\n\n +b | Client masks that are banned from the channel (e.g. *!*@127.0.0.1)\n +e | Client masks that are exempted from bans.\n +I | Client masks that are exempted from the invite-only flag.\n +i | Invite-only mode, only invited clients can join the channel.\n +k | Key required when joining the channel.\n +l | Client join limit for the channel.\n +m | Moderated mode, only privileged clients can talk on the channel.\n +n | No-outside-messages mode, only users that are on the channel can send\n | messages to it.\n +R | Only registered users can talk in the channel.\n +s | Secret mode, channel won't show up in /LIST or whois replies.\n +t | Only channel opers can modify the topic.\n\n= Prefixes =\n\n +q (~) | Founder channel mode.\n +a (&) | Admin channel mode.\n +o (@) | Operator channel mode.\n +h (%) | Halfop channel mode.\n +v (+) | Voice channel mode.": "== Moduri de canal ==\n\nOragono suportă următoarele moduri de canal:\n\n +b | Măștile unui client, interzise pe canal (ex. *!*@127.0.0.1)\n +e | Măștile unui client, ce sunt exceptate de la ban.\n +I | Măștile clienților ce fac excepție de la fanionul numai-invitație.\n +i | Fanion doar-invitație, doar clienții invitați pot intra pe canal.\n +k | Este necesară o cheie, pentru a putea intra pe canal.\n +l | Limită de clienți ce se pot alătura canalului.\n +m | Modul moderat, doar clienții privilegiați pot trimite text pe canal.\n +n | Fanion fără-mesaje-din-afară, doar utilizatorii care se află pe canal\n | pot trimite mesaje.\n +R | Doar utilizatorii înregistrați la servicii pot trimite text pe canal.\n +s | Mod secret, canalul nu va fi afișat în /LIST sau mesaje whois.\n +t | Doar operatorii pot modifica topicul.\n\n= Prefixe =\n\n +q (~) | Mod fondator de canal.\n +a (&) | Mod administrator de canal.\n +o (@) | Mod operator de canal.\n +h (%) | Mod semi-op pe canal.\n +v (+) | Mod voce pe canal.", "== Server Notice Masks ==\n\nOragono supports the following server notice masks for operators:\n\n a | Local announcements.\n c | Local client connections.\n j | Local channel actions.\n k | Local kills.\n n | Local nick changes.\n o | Local oper actions.\n q | Local quits.\n t | Local /STATS usage.\n u | Local client account actions.\n x | Local X-lines (DLINE/KLINE/etc).\n\nTo set a snomask, do this with your nickname:\n\n /MODE +s \n\nFor instance, this would set the kill, oper, account and xline snomasks on dan:\n\n /MODE dan +s koux": "== Măști de atenționare de server ==\n\nOragono suportă următoarele măști de atenționare valabile pentru operatori:\n\n a | Anunțuri locale.\n c | Conexiuni clienți locali.\n j | Acțiuni canale locale.\n k | Kill locale.\n n | Schimbări locale de pseudonime.\n o | Acțiuni operatori locali.\n q | Renunțări locale.\n t | Utilizare /STATS local.\n u | Local client account actions.\n x | X-lines (DLINE/KLINE/etc) locale.\n\nPentru a seta o mască de atenționare, setează-ți pe pseudonim:\n\n /MODE +s \n\nSpre exemplu, următoarea comandă ar seta modurile kill, oper, cont și xline pentru utilizatorul dan:\n\n /MODE dan +s koux", "== User Modes ==\n\nOragono supports the following user modes:\n\n +a | User is marked as being away. This mode is set with the /AWAY command.\n +i | User is marked as invisible (their channels are hidden from whois replies).\n +o | User is an IRC operator.\n +R | User only accepts messages from other registered users. \n +s | Server Notice Masks (see help with /HELPOP snomasks).\n +Z | User is connected via TLS.": "== Moduri utilizator ==\n\nOragono suportă următoarele moduri(fanioane) de utilizator:\n\n +a | Utilizatorul a fost marcat ca fiind fără activitate. Acest fanion poate fi setat cu comanda /AWAY.\n +i | Utilizatorul a fost marcat ca invizibil (canalele pe care se află vor fi ascunse în conținutul /whois).\n +o | Utilizatorul este un Operator IRC.\n +R | Utilizatorul acceptă mesaje doar de la alți utilizatori înregistrați. \n +s | Măști de Anunț de Server (vezi informații de ajutor cu comanda /HELPOP snomasks).\n +Z | Utilizatorul se conectează via TLS.", - "@+client-only-tags TAGMSG {,}\n\nSends the given client-only tags to the given targets as a TAGMSG. See the IRCv3\nspecs for more info: http://ircv3.net/specs/core/message-tags-3.3.html": "", + "@+client-only-tags TAGMSG {,}\n\nSends the given client-only tags to the given targets as a TAGMSG. See the IRCv3\nspecs for more info: http://ircv3.net/specs/core/message-tags-3.3.html": "@+client-only-tags TAGMSG <țintă>{,<țintă>}\n\nExpediază marcajele client-doar către țintele precizate ca și TAGMSG.\nVezi specificațiile IRCv3, iar pentru mai multe informații, accesează acest link:\nhttp://ircv3.net/specs/core/message-tags-3.3.html", "ACC LS\nACC REGISTER [callback_namespace:] [cred_type] :\nACC VERIFY \n\nUsed in account registration. See the relevant specs for more info:\nhttps://oragono.io/specs.html": "", "AMBIANCE \n\nThe AMBIANCE command is used to send a scene notification to the given target.": "", "AUTHENTICATE\n\nUsed during SASL authentication. See the IRCv3 specs for more info:\nhttp://ircv3.net/specs/extensions/sasl-3.1.html": "", @@ -66,5 +66,5 @@ "WEBIRC [:]\n\nUsed by web<->IRC gateways and bouncers, the WEBIRC command allows gateways to\npass-through the real IP addresses of clients:\nircv3.net/specs/extensions/webirc.html\n\n is a list of space-separated strings indicating various details about\nthe connection from the client to the gateway, such as:\n\n- tls: this flag indicates that the client->gateway connection is secure": "", "WHO [o]\n\nReturns information for the given user.": "", "WHOIS {,}\n\nReturns information for the given user(s).": "", - "WHOWAS \n\nReturns historical information on the last user with the given nickname.": "" + "WHOWAS \n\nReturns historical information on the last user with the given nickname.": "WHOWAS \n\nAfișează informații despre istoricul ultimului utilizator cu pseudonimul cerut." } From 1121e3d320e8e749352d5d642fdde8687e1f8822 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Tue, 21 May 2019 23:55:04 -0400 Subject: [PATCH 4/4] fix #404 --- irc/accounts.go | 4 ++++ irc/errors.go | 1 + irc/hostserv.go | 4 +++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/irc/accounts.go b/irc/accounts.go index 81dfecb4..8815d886 100644 --- a/irc/accounts.go +++ b/irc/accounts.go @@ -1108,6 +1108,10 @@ func (am *AccountManager) VHostReject(account string, reason string) (result VHo func (am *AccountManager) VHostSetEnabled(client *Client, enabled bool) (result VHostInfo, err error) { munger := func(input VHostInfo) (output VHostInfo, err error) { + if input.ApprovedVHost == "" { + err = errNoVhost + return + } output = input output.Enabled = enabled return diff --git a/irc/errors.go b/irc/errors.go index df47f8d2..6cbe55bf 100644 --- a/irc/errors.go +++ b/irc/errors.go @@ -44,6 +44,7 @@ var ( errFeatureDisabled = errors.New(`That feature is disabled`) errBanned = errors.New("IP or nickmask banned") errInvalidParams = utils.ErrInvalidParams + errNoVhost = errors.New(`You do not have an approved vhost`) ) // Socket Errors diff --git a/irc/hostserv.go b/irc/hostserv.go index cc81fbb6..698830e3 100644 --- a/irc/hostserv.go +++ b/irc/hostserv.go @@ -154,7 +154,9 @@ func hsOnOffHandler(server *Server, client *Client, command string, params []str } _, err := server.accounts.VHostSetEnabled(client, enable) - if err != nil { + if err == errNoVhost { + hsNotice(rb, client.t(err.Error())) + } else if err != nil { hsNotice(rb, client.t("An error occurred")) } else if enable { hsNotice(rb, client.t("Successfully enabled your vhost"))