1 /*
2  * Copyright (C) 2004-2020 ZNC, see the NOTICE file for details.
3  * Author: imaginos <imaginos@imaginos.net>
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *     http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17 
18 /*
19  * Quiet Away and message logger
20  *
21  * I originally wrote this module for when I had multiple clients connected to
22  *ZNC. I would leave work and forget to close my client, arriving at home
23  * and re-attaching there someone may have messaged me in commute and I wouldn't
24  *know it until I would arrive back at work the next day. I wrote it such that
25  * my xchat client would monitor desktop activity and ping the module to let it
26  *know I was active. Within a few minutes of inactivity the pinging stops and
27  * the away module sets the user as away and logging commences.
28  */
29 
30 #define REQUIRESSL
31 
32 #include <znc/main.h>
33 #include <znc/User.h>
34 #include <znc/IRCNetwork.h>
35 #include <znc/FileUtils.h>
36 #include <znc/Modules.h>
37 #include <znc/Chan.h>
38 
39 using std::vector;
40 using std::map;
41 
42 #define CRYPT_VERIFICATION_TOKEN "::__:AWAY:__::"
43 
44 class CAway;
45 
46 class CAwayJob : public CTimer {
47   public:
CAwayJob(CModule * pModule,unsigned int uInterval,unsigned int uCycles,const CString & sLabel,const CString & sDescription)48     CAwayJob(CModule* pModule, unsigned int uInterval, unsigned int uCycles,
49              const CString& sLabel, const CString& sDescription)
50         : CTimer(pModule, uInterval, uCycles, sLabel, sDescription) {}
51 
~CAwayJob()52     ~CAwayJob() override {}
53 
54   protected:
55     void RunJob() override;
56 };
57 
58 class CAway : public CModule {
AwayCommand(const CString & sCommand)59     void AwayCommand(const CString& sCommand) {
60         CString sReason;
61         timeval curtime;
62         gettimeofday(&curtime, nullptr);
63 
64         if (sCommand.Token(1) != "-quiet") {
65             sReason = CUtils::FormatTime(curtime, sCommand.Token(1, true),
66                                          GetUser()->GetTimezone());
67             PutModNotice(t_s("You have been marked as away"));
68         } else {
69             sReason = CUtils::FormatTime(curtime, sCommand.Token(2, true),
70                                          GetUser()->GetTimezone());
71         }
72 
73         Away(false, sReason);
74     }
75 
BackCommand(const CString & sCommand)76     void BackCommand(const CString& sCommand) {
77         if ((m_vMessages.empty()) && (sCommand.Token(1) != "-quiet"))
78             PutModNotice(t_s("Welcome back!"));
79         Ping();
80         Back();
81     }
82 
MessagesCommand(const CString & sCommand)83     void MessagesCommand(const CString& sCommand) {
84         for (u_int a = 0; a < m_vMessages.size(); a++)
85             PutModule(m_vMessages[a]);
86     }
87 
ReplayCommand(const CString & sCommand)88     void ReplayCommand(const CString& sCommand) {
89         CString nick = GetClient()->GetNick();
90         for (u_int a = 0; a < m_vMessages.size(); a++) {
91             CString sWhom = m_vMessages[a].Token(1, false, ":");
92             CString sMessage = m_vMessages[a].Token(2, true, ":");
93             PutUser(":" + sWhom + " PRIVMSG " + nick + " :" + sMessage);
94         }
95     }
96 
DeleteCommand(const CString & sCommand)97     void DeleteCommand(const CString& sCommand) {
98         CString sWhich = sCommand.Token(1);
99         if (sWhich == "all") {
100             PutModNotice(t_f("Deleted {1} messages")(m_vMessages.size()));
101             for (u_int a = 0; a < m_vMessages.size(); a++)
102                 m_vMessages.erase(m_vMessages.begin() + a--);
103         } else if (sWhich.empty()) {
104             PutModNotice(t_s("USAGE: delete <num|all>"));
105             return;
106         } else {
107             u_int iNum = sWhich.ToUInt();
108             if (iNum >= m_vMessages.size()) {
109                 PutModNotice(t_s("Illegal message # requested"));
110                 return;
111             } else {
112                 m_vMessages.erase(m_vMessages.begin() + iNum);
113                 PutModNotice(t_s("Message erased"));
114             }
115             SaveBufferToDisk();
116         }
117     }
118 
SaveCommand(const CString & sCommand)119     void SaveCommand(const CString& sCommand) {
120         if (m_saveMessages) {
121             SaveBufferToDisk();
122             PutModNotice(t_s("Messages saved to disk"));
123         } else {
124             PutModNotice(t_s("There are no messages to save"));
125         }
126     }
127 
PingCommand(const CString & sCommand)128     void PingCommand(const CString& sCommand) {
129         Ping();
130         if (m_bIsAway) Back();
131     }
132 
PassCommand(const CString & sCommand)133     void PassCommand(const CString& sCommand) {
134         m_sPassword = sCommand.Token(1);
135         PutModNotice(t_f("Password updated to [{1}]")(m_sPassword));
136     }
137 
ShowCommand(const CString & sCommand)138     void ShowCommand(const CString& sCommand) {
139         map<CString, vector<CString>> msvOutput;
140         for (u_int a = 0; a < m_vMessages.size(); a++) {
141             CString sTime = m_vMessages[a].Token(0, false);
142             CString sWhom = m_vMessages[a].Token(1, false);
143             CString sMessage = m_vMessages[a].Token(2, true);
144 
145             if ((sTime.empty()) || (sWhom.empty()) || (sMessage.empty())) {
146                 // illegal format
147                 PutModule(t_f("Corrupt message! [{1}]")(m_vMessages[a]));
148                 m_vMessages.erase(m_vMessages.begin() + a--);
149                 continue;
150             }
151 
152             time_t iTime = sTime.ToULong();
153             char szFormat[64];
154             struct tm t;
155             localtime_r(&iTime, &t);
156             size_t iCount = strftime(szFormat, 64, "%F %T", &t);
157 
158             if (iCount <= 0) {
159                 PutModule(t_f("Corrupt time stamp! [{1}]")(m_vMessages[a]));
160                 m_vMessages.erase(m_vMessages.begin() + a--);
161                 continue;
162             }
163 
164             CString sTmp = "    " + CString(a) + ") [";
165             sTmp.append(szFormat, iCount);
166             sTmp += "] ";
167             sTmp += sMessage;
168             msvOutput[sWhom].push_back(sTmp);
169         }
170 
171         for (map<CString, vector<CString>>::iterator it = msvOutput.begin();
172              it != msvOutput.end(); ++it) {
173             PutModule(it->first);
174             for (u_int a = 0; a < it->second.size(); a++)
175                 PutModule(it->second[a]);
176         }
177 
178         PutModule(t_s("#--- End of messages"));
179     }
180 
EnableTimerCommand(const CString & sCommand)181     void EnableTimerCommand(const CString& sCommand) {
182         SetAwayTime(300);
183         PutModule(t_s("Timer set to 300 seconds"));
184     }
185 
DisableTimerCommand(const CString & sCommand)186     void DisableTimerCommand(const CString& sCommand) {
187         SetAwayTime(0);
188         PutModule(t_s("Timer disabled"));
189     }
190 
SetTimerCommand(const CString & sCommand)191     void SetTimerCommand(const CString& sCommand) {
192         int iSetting = sCommand.Token(1).ToInt();
193 
194         SetAwayTime(iSetting);
195 
196         if (iSetting == 0)
197             PutModule(t_s("Timer disabled"));
198         else
199             PutModule(t_f("Timer set to {1} seconds")(iSetting));
200     }
201 
TimerCommand(const CString & sCommand)202     void TimerCommand(const CString& sCommand) {
203         PutModule(t_f("Current timer setting: {1} seconds")(GetAwayTime()));
204     }
205 
206   public:
MODCONSTRUCTOR(CAway)207     MODCONSTRUCTOR(CAway) {
208         Ping();
209         m_bIsAway = false;
210         m_bBootError = false;
211         m_saveMessages = true;
212         m_chanMessages = false;
213         SetAwayTime(300);
214         AddTimer(
215             new CAwayJob(this, 60, 0, "AwayJob",
216                          "Checks for idle and saves messages every 1 minute"));
217 
218         AddHelpCommand();
219         AddCommand("Away",
220                    static_cast<CModCommand::ModCmdFunc>(&CAway::AwayCommand),
221                    "[-quiet]");
222         AddCommand("Back",
223                    static_cast<CModCommand::ModCmdFunc>(&CAway::BackCommand),
224                    "[-quiet]");
225         AddCommand("Messages",
226                    static_cast<CModCommand::ModCmdFunc>(&CAway::BackCommand));
227         AddCommand("Delete",
228                    static_cast<CModCommand::ModCmdFunc>(&CAway::DeleteCommand),
229                    "delete <num|all>");
230         AddCommand("Save",
231                    static_cast<CModCommand::ModCmdFunc>(&CAway::SaveCommand));
232         AddCommand("Ping",
233                    static_cast<CModCommand::ModCmdFunc>(&CAway::PingCommand));
234         AddCommand("Pass",
235                    static_cast<CModCommand::ModCmdFunc>(&CAway::PassCommand));
236         AddCommand("Show",
237                    static_cast<CModCommand::ModCmdFunc>(&CAway::ShowCommand));
238         AddCommand("Replay",
239                    static_cast<CModCommand::ModCmdFunc>(&CAway::ReplayCommand));
240         AddCommand("EnableTimer", static_cast<CModCommand::ModCmdFunc>(
241                                       &CAway::EnableTimerCommand));
242         AddCommand("DisableTimer", static_cast<CModCommand::ModCmdFunc>(
243                                        &CAway::DisableTimerCommand));
244         AddCommand("SetTimer", static_cast<CModCommand::ModCmdFunc>(
245                                    &CAway::SetTimerCommand),
246                    "<secs>");
247         AddCommand("Timer",
248                    static_cast<CModCommand::ModCmdFunc>(&CAway::TimerCommand));
249     }
250 
~CAway()251     ~CAway() override {
252         if (!m_bBootError) SaveBufferToDisk();
253     }
254 
OnLoad(const CString & sArgs,CString & sMessage)255     bool OnLoad(const CString& sArgs, CString& sMessage) override {
256         CString sMyArgs = sArgs;
257         size_t uIndex = 0;
258         if (sMyArgs.Token(0) == "-nostore") {
259             uIndex++;
260             m_saveMessages = false;
261         }
262         if (sMyArgs.Token(uIndex) == "-chans") {
263             uIndex++;
264             m_chanMessages = true;
265         }
266         if (sMyArgs.Token(uIndex) == "-notimer") {
267             SetAwayTime(0);
268             sMyArgs = sMyArgs.Token(uIndex + 1, true);
269         } else if (sMyArgs.Token(uIndex) == "-timer") {
270             SetAwayTime(sMyArgs.Token(uIndex + 1).ToInt());
271             sMyArgs = sMyArgs.Token(uIndex + 2, true);
272         }
273         if (m_saveMessages) {
274             if (!sMyArgs.empty()) {
275                 m_sPassword = CBlowfish::MD5(sMyArgs);
276             } else {
277                 sMessage =
278                     t_s("This module needs as an argument a keyphrase used for "
279                         "encryption");
280                 return false;
281             }
282 
283             if (!BootStrap()) {
284                 sMessage = t_s(
285                     "Failed to decrypt your saved messages - "
286                     "Did you give the right encryption key as an argument to "
287                     "this module?");
288                 m_bBootError = true;
289                 return false;
290             }
291         }
292 
293         return true;
294     }
295 
OnIRCConnected()296     void OnIRCConnected() override {
297         if (m_bIsAway) {
298             Away(true);  // reset away if we are reconnected
299         } else {
300             // ircd seems to remember your away if you killed the client and
301             // came back
302             Back();
303         }
304     }
305 
BootStrap()306     bool BootStrap() {
307         CString sFile;
308         if (DecryptMessages(sFile)) {
309             VCString vsLines;
310             VCString::iterator it;
311 
312             sFile.Split("\n", vsLines);
313 
314             for (it = vsLines.begin(); it != vsLines.end(); ++it) {
315                 CString sLine(*it);
316                 sLine.Trim();
317                 AddMessage(sLine);
318             }
319         } else {
320             m_sPassword = "";
321             CUtils::PrintError("[" + GetModName() +
322                                ".so] Failed to Decrypt Messages");
323             return (false);
324         }
325 
326         return (true);
327     }
328 
SaveBufferToDisk()329     void SaveBufferToDisk() {
330         if (!m_sPassword.empty()) {
331             CString sFile = CRYPT_VERIFICATION_TOKEN;
332 
333             for (u_int b = 0; b < m_vMessages.size(); b++)
334                 sFile += m_vMessages[b] + "\n";
335 
336             CBlowfish c(m_sPassword, BF_ENCRYPT);
337             sFile = c.Crypt(sFile);
338             CString sPath = GetPath();
339             if (!sPath.empty()) {
340                 CFile File(sPath);
341                 if (File.Open(O_WRONLY | O_CREAT | O_TRUNC, 0600)) {
342                     File.Chmod(0600);
343                     File.Write(sFile);
344                 }
345                 File.Close();
346             }
347         }
348     }
349 
OnClientLogin()350     void OnClientLogin() override { Back(true); }
OnClientDisconnect()351     void OnClientDisconnect() override { Away(); }
352 
GetPath()353     CString GetPath() {
354         CString sBuffer = GetUser()->GetUsername();
355         CString sRet = GetSavePath();
356         sRet += "/.znc-away-" + CBlowfish::MD5(sBuffer, true);
357         return (sRet);
358     }
359 
Away(bool bForce=false,const CString & sReason="")360     void Away(bool bForce = false, const CString& sReason = "") {
361         if ((!m_bIsAway) || (bForce)) {
362             if (!bForce)
363                 m_sReason = sReason;
364             else if (!sReason.empty())
365                 m_sReason = sReason;
366 
367             time_t iTime = time(nullptr);
368             char* pTime = ctime(&iTime);
369             CString sTime;
370             if (pTime) {
371                 sTime = pTime;
372                 sTime.Trim();
373             }
374             if (m_sReason.empty()) m_sReason = "Auto Away at " + sTime;
375             PutIRC("AWAY :" + m_sReason);
376             m_bIsAway = true;
377         }
378     }
379 
Back(bool bUsePrivMessage=false)380     void Back(bool bUsePrivMessage = false) {
381         PutIRC("away");
382         m_bIsAway = false;
383         if (!m_vMessages.empty()) {
384             if (bUsePrivMessage) {
385                 PutModule(t_s("Welcome back!"));
386                 PutModule(t_f("You have {1} messages!")(m_vMessages.size()));
387             } else {
388                 PutModNotice(t_s("Welcome back!"));
389                 PutModNotice(t_f("You have {1} messages!")(m_vMessages.size()));
390             }
391         }
392         m_sReason = "";
393     }
394 
OnPrivMsg(CNick & Nick,CString & sMessage)395     EModRet OnPrivMsg(CNick& Nick, CString& sMessage) override {
396         if (m_bIsAway) AddMessage(time(nullptr), Nick, sMessage);
397         return (CONTINUE);
398     }
399 
OnChanMsg(CNick & nick,CChan & channel,CString & sMessage)400     EModRet OnChanMsg(CNick& nick, CChan& channel, CString& sMessage) override {
401         if (m_bIsAway && m_chanMessages &&
402             sMessage.AsLower().find(m_pNetwork->GetCurNick().AsLower()) !=
403                 CString::npos) {
404             AddMessage(time(nullptr), nick, channel.GetName() + " " + sMessage);
405         }
406 
407         return (CONTINUE);
408     }
409 
OnPrivAction(CNick & Nick,CString & sMessage)410     EModRet OnPrivAction(CNick& Nick, CString& sMessage) override {
411         if (m_bIsAway) {
412             AddMessage(time(nullptr), Nick, "* " + sMessage);
413         }
414         return (CONTINUE);
415     }
416 
OnUserNotice(CString & sTarget,CString & sMessage)417     EModRet OnUserNotice(CString& sTarget, CString& sMessage) override {
418         Ping();
419         if (m_bIsAway) Back();
420 
421         return (CONTINUE);
422     }
423 
OnUserMsg(CString & sTarget,CString & sMessage)424     EModRet OnUserMsg(CString& sTarget, CString& sMessage) override {
425         Ping();
426         if (m_bIsAway) Back();
427 
428         return (CONTINUE);
429     }
430 
OnUserAction(CString & sTarget,CString & sMessage)431     EModRet OnUserAction(CString& sTarget, CString& sMessage) override {
432         Ping();
433         if (m_bIsAway) Back();
434 
435         return (CONTINUE);
436     }
437 
GetTimeStamp() const438     time_t GetTimeStamp() const { return (m_iLastSentData); }
Ping()439     void Ping() { m_iLastSentData = time(nullptr); }
GetAwayTime()440     time_t GetAwayTime() { return m_iAutoAway; }
SetAwayTime(time_t u)441     void SetAwayTime(time_t u) { m_iAutoAway = u; }
442 
IsAway()443     bool IsAway() { return (m_bIsAway); }
444 
445   private:
446     CString m_sPassword;
447     bool m_bBootError;
DecryptMessages(CString & sBuffer)448     bool DecryptMessages(CString& sBuffer) {
449         CString sMessages = GetPath();
450         CString sFile;
451         sBuffer = "";
452 
453         CFile File(sMessages);
454 
455         if (sMessages.empty() || !File.Open() || !File.ReadFile(sFile)) {
456             PutModule(t_s("Unable to find buffer"));
457             return (true);  // gonna be successful here
458         }
459 
460         File.Close();
461 
462         if (!sFile.empty()) {
463             CBlowfish c(m_sPassword, BF_DECRYPT);
464             sBuffer = c.Crypt(sFile);
465 
466             if (sBuffer.Left(strlen(CRYPT_VERIFICATION_TOKEN)) !=
467                 CRYPT_VERIFICATION_TOKEN) {
468                 // failed to decode :(
469                 PutModule(t_s("Unable to decode encrypted messages"));
470                 return (false);
471             }
472             sBuffer.erase(0, strlen(CRYPT_VERIFICATION_TOKEN));
473         }
474         return (true);
475     }
476 
AddMessage(time_t iTime,const CNick & Nick,const CString & sMessage)477     void AddMessage(time_t iTime, const CNick& Nick, const CString& sMessage) {
478         if (Nick.GetNick() == GetNetwork()->GetIRCNick().GetNick())
479             return;  // ignore messages from self
480         AddMessage(CString(iTime) + " " + Nick.GetNickMask() + " " + sMessage);
481     }
482 
AddMessage(const CString & sText)483     void AddMessage(const CString& sText) {
484         if (m_saveMessages) {
485             m_vMessages.push_back(sText);
486         }
487     }
488 
489     time_t m_iLastSentData;
490     bool m_bIsAway;
491     time_t m_iAutoAway;
492     vector<CString> m_vMessages;
493     CString m_sReason;
494     bool m_saveMessages;
495     bool m_chanMessages;
496 };
497 
RunJob()498 void CAwayJob::RunJob() {
499     CAway* p = (CAway*)GetModule();
500     p->SaveBufferToDisk();
501 
502     if (!p->IsAway()) {
503         time_t iNow = time(nullptr);
504 
505         if ((iNow - p->GetTimeStamp()) > p->GetAwayTime() &&
506             p->GetAwayTime() != 0)
507             p->Away();
508     }
509 }
510 
511 template <>
TModInfo(CModInfo & Info)512 void TModInfo<CAway>(CModInfo& Info) {
513     Info.SetWikiPage("awaystore");
514     Info.SetHasArgs(true);
515     Info.SetArgsHelpText(Info.t_s(
516         "[ -notimer | -timer N ] [-chans]  passw0rd . N is number of seconds, "
517         "600 by default."));
518 }
519 
520 NETWORKMODULEDEFS(
521     CAway, t_s("Adds auto-away with logging, useful when you use ZNC from "
522                "different locations"))
523