From 8de1538419e27bad0b8a5954774bee1415484120 Mon Sep 17 00:00:00 2001 From: Kevin Darbyshire-Bryant Date: Sat, 12 Nov 2016 20:35:39 +0000 Subject: [PATCH] znc: add advanced playback module The advanced playback module makes it possible for IRC clients to avoid undesired repetitive buffer playback. IRC clients may request the module to send a partial buffer playback starting from and ending to a certain point of time. Particularly useful with (supporting) mobile clients such as Mutter, Colloquy & others. Signed-off-by: Kevin Darbyshire-Bryant --- Unfortunately playback.cpp is not included as part of the standard ZNC modules and so has been added as a patch. --- net/znc/Makefile | 3 + net/znc/patches/110-add-playback-module.patch | 293 ++++++++++++++++++ 2 files changed, 296 insertions(+) create mode 100644 net/znc/patches/110-add-playback-module.patch diff --git a/net/znc/Makefile b/net/znc/Makefile index 4fa31c4687..d541e98679 100644 --- a/net/znc/Makefile +++ b/net/znc/Makefile @@ -236,6 +236,9 @@ $(eval $(call module,partyline,Allows ZNC users to join internal channels and \ $(eval $(call module,perform,Performs commands on connect.)) +$(eval $(call module,playback,Avoid repetitive playback buffers on re-connect \ + with supported clients (e.g. mutter, colluquy))) + $(eval $(call module,q,Auths you with Q (and a little more).)) $(eval $(call module,raw,View all of the raw traffic.)) diff --git a/net/znc/patches/110-add-playback-module.patch b/net/znc/patches/110-add-playback-module.patch new file mode 100644 index 0000000000..4da94cbf1f --- /dev/null +++ b/net/znc/patches/110-add-playback-module.patch @@ -0,0 +1,293 @@ +Index: znc-1.6.3/modules/playback.cpp +=================================================================== +--- /dev/null ++++ znc-1.6.3/modules/playback.cpp +@@ -0,0 +1,288 @@ ++/* ++ * Copyright (C) 2015 J-P Nurmi ++ * ++ * Licensed under the Apache License, Version 2.0 (the "License"); ++ * you may not use this file except in compliance with the License. ++ * You may obtain a copy of the License at ++ * ++ * http://www.apache.org/licenses/LICENSE-2.0 ++ * ++ * Unless required by applicable law or agreed to in writing, software ++ * distributed under the License is distributed on an "AS IS" BASIS, ++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++ * See the License for the specific language governing permissions and ++ * limitations under the License. ++ */ ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#if (VERSION_MAJOR < 1) || (VERSION_MAJOR == 1 && VERSION_MINOR < 6) ++#error The playback module requires ZNC version 1.6.0 or later. ++#endif ++ ++static const char* PlaybackCap = "znc.in/playback"; ++ ++class CPlaybackMod : public CModule ++{ ++public: ++ MODCONSTRUCTOR(CPlaybackMod) ++ { ++ m_play = false; ++ AddHelpCommand(); ++ AddCommand("Clear", static_cast(&CPlaybackMod::ClearCommand), "", "Clears playback for given buffers."); ++ AddCommand("Play", static_cast(&CPlaybackMod::PlayCommand), " [from] [to]", "Sends playback for given buffers."); ++ AddCommand("List", static_cast(&CPlaybackMod::ListCommand), "[buffer(s)]", "Lists available/matching buffers."); ++ AddCommand("Limit", static_cast(&CPlaybackMod::LimitCommand), " [limit]", "Get/set the buffer limit (<= 0 to clear) for the given client."); ++ } ++ ++ void OnClientCapLs(CClient* client, SCString& caps) override ++ { ++ caps.insert(PlaybackCap); ++ } ++ ++ bool IsClientCapSupported(CClient* client, const CString& cap, bool state) override ++ { ++ return cap.Equals(PlaybackCap); ++ } ++ ++ EModRet OnChanBufferStarting(CChan& chan, CClient& client) override ++ { ++ if (!m_play && client.IsCapEnabled(PlaybackCap)) ++ return HALTCORE; ++ return CONTINUE; ++ } ++ ++ EModRet OnChanBufferPlayLine(CChan& chan, CClient& client, CString& line) override ++ { ++ if (!m_play && client.IsCapEnabled(PlaybackCap)) ++ return HALTCORE; ++ return CONTINUE; ++ } ++ ++ EModRet OnChanBufferEnding(CChan& chan, CClient& client) override ++ { ++ if (!m_play && client.IsCapEnabled(PlaybackCap)) ++ return HALTCORE; ++ return CONTINUE; ++ } ++ ++ EModRet OnPrivBufferPlayLine(CClient& client, CString& line) override ++ { ++ if (!m_play && client.IsCapEnabled(PlaybackCap)) ++ return HALTCORE; ++ return CONTINUE; ++ } ++ ++ void ClearCommand(const CString& line) ++ { ++ // CLEAR ++ const CString arg = line.Token(1); ++ if (arg.empty() || !line.Token(2).empty()) ++ return; ++ std::vector chans = FindChans(arg); ++ for (CChan* chan : chans) ++ chan->ClearBuffer(); ++ std::vector queries = FindQueries(arg); ++ for (CQuery* query : queries) ++ query->ClearBuffer(); ++ } ++ ++ void PlayCommand(const CString& line) ++ { ++ // PLAY [from] [to] ++ const CString arg = line.Token(1); ++ if (arg.empty() || !line.Token(4).empty()) ++ return; ++ double from = line.Token(2).ToDouble(); ++ double to = DBL_MAX; ++ if (!line.Token(3).empty()) ++ to = line.Token(3).ToDouble(); ++ int limit = -1; ++ if (CClient* client = GetClient()) ++ limit = GetLimit(client->GetIdentifier()); ++ std::vector chans = FindChans(arg); ++ for (CChan* chan : chans) { ++ if (chan->IsOn() && !chan->IsDetached()) { ++ CBuffer lines = GetLines(chan->GetBuffer(), from, to, limit); ++ m_play = true; ++ chan->SendBuffer(GetClient(), lines); ++ m_play = false; ++ } ++ } ++ std::vector queries = FindQueries(arg); ++ for (CQuery* query : queries) { ++ CBuffer lines = GetLines(query->GetBuffer(), from, to, limit); ++ m_play = true; ++ query->SendBuffer(GetClient(), lines); ++ m_play = false; ++ } ++ } ++ ++ void ListCommand(const CString& line) ++ { ++ // LIST [buffer(s)] ++ CString arg = line.Token(1); ++ if (arg.empty()) ++ arg = "*"; ++ std::vector chans = FindChans(arg); ++ for (CChan* chan : chans) { ++ if (chan->IsOn() && !chan->IsDetached()) { ++ CBuffer buffer = chan->GetBuffer(); ++ if (!buffer.IsEmpty()) { ++ timeval from = UniversalTime(buffer.GetBufLine(0).GetTime()); ++ timeval to = UniversalTime(buffer.GetBufLine(buffer.Size() - 1).GetTime()); ++ PutModule(chan->GetName() + " " + CString(Timestamp(from)) + " " + CString(Timestamp(to))); ++ } ++ } ++ } ++ std::vector queries = FindQueries(arg); ++ for (CQuery* query : queries) { ++ CBuffer buffer = query->GetBuffer(); ++ if (!buffer.IsEmpty()) { ++ timeval from = UniversalTime(buffer.GetBufLine(0).GetTime()); ++ timeval to = UniversalTime(buffer.GetBufLine(buffer.Size() - 1).GetTime()); ++ PutModule(query->GetName() + " " + CString(Timestamp(from)) + " " + CString(Timestamp(to))); ++ } ++ } ++ } ++ ++ void LimitCommand(const CString& line) ++ { ++ // LIMIT [limit] ++ const CString client = line.Token(1); ++ if (client.empty()) { ++ PutModule("Usage: LIMIT [limit]"); ++ return; ++ } ++ const CString arg = line.Token(2); ++ int limit = GetLimit(client); ++ if (!arg.empty()) { ++ limit = arg.ToInt(); ++ SetLimit(client, limit); ++ } ++ if (limit <= 0) ++ PutModule(client + " buffer limit: -"); ++ else ++ PutModule(client + " buffer limit: " + CString(limit)); ++ } ++ ++ EModRet OnSendToClient(CString& line, CClient& client) override ++ { ++ if (client.IsAttached() && client.IsCapEnabled(PlaybackCap) && !line.Token(0).Equals("CAP")) { ++ MCString tags = CUtils::GetMessageTags(line); ++ if (tags.find("time") == tags.end()) { ++ // CUtils::FormatServerTime() converts to UTC ++ tags["time"] = CUtils::FormatServerTime(LocalTime()); ++ CUtils::SetMessageTags(line, tags); ++ } ++ } ++ return CONTINUE; ++ } ++ ++private: ++ static double Timestamp(timeval tv) ++ { ++ return tv.tv_sec + tv.tv_usec / 1000000.0; ++ } ++ ++ static timeval LocalTime() ++ { ++ timeval tv; ++ if (gettimeofday(&tv, NULL) == -1) { ++ tv.tv_sec = time(NULL); ++ tv.tv_usec = 0; ++ } ++ return tv; ++ } ++ ++ static timeval UniversalTime(timeval tv = LocalTime()) ++ { ++ tm stm; ++ memset(&stm, 0, sizeof(stm)); ++ const time_t secs = tv.tv_sec; // OpenBSD has tv_sec as int, so explicitly convert it to time_t to make gmtime_r() happy ++ gmtime_r(&secs, &stm); ++ const char* tz = getenv("TZ"); ++ setenv("TZ", "UTC", 1); ++ tzset(); ++ tv.tv_sec = mktime(&stm); ++ if (tz) ++ setenv("TZ", tz, 1); ++ else ++ unsetenv("TZ"); ++ tzset(); ++ return tv; ++ } ++ ++ std::vector FindChans(const CString& arg) const ++ { ++ std::vector chans; ++ CIRCNetwork* network = GetNetwork(); ++ if (network) { ++ VCString vargs; ++ arg.Split(",", vargs, false); ++ ++ for (const CString& name : vargs) { ++ std::vector found = network->FindChans(name); ++ chans.insert(chans.end(), found.begin(), found.end()); ++ } ++ } ++ return chans; ++ } ++ ++ std::vector FindQueries(const CString& arg) const ++ { ++ std::vector queries; ++ CIRCNetwork* network = GetNetwork(); ++ if (network) { ++ VCString vargs; ++ arg.Split(",", vargs, false); ++ ++ for (const CString& name : vargs) { ++ std::vector found = network->FindQueries(name); ++ queries.insert(queries.end(), found.begin(), found.end()); ++ } ++ } ++ return queries; ++ } ++ ++ int GetLimit(const CString& client) const ++ { ++ return GetNV(client).ToInt(); ++ } ++ ++ void SetLimit(const CString& client, int limit) ++ { ++ if (limit > 0) ++ SetNV(client, CString(limit)); ++ else ++ DelNV(client); ++ } ++ ++ static CBuffer GetLines(const CBuffer& buffer, double from, double to, int limit) ++ { ++ CBuffer lines(buffer.Size()); ++ for (size_t i = 0; i < buffer.Size(); ++i) { ++ const CBufLine& line = buffer.GetBufLine(i); ++ timeval tv = UniversalTime(line.GetTime()); ++ if (from < Timestamp(tv) && to >= Timestamp(tv)) ++ lines.AddLine(line.GetFormat(), line.GetText(), &tv); ++ } ++ if (limit > 0) ++ lines.SetLineCount(limit); ++ return lines; ++ } ++ ++ bool m_play; ++}; ++ ++GLOBALMODULEDEFS(CPlaybackMod, "An advanced playback module for ZNC") -- 2.30.2