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