├── README └── znc ├── AUTHORS ├── README └── colloquy.cpp /README: -------------------------------------------------------------------------------- 1 | This repository was created to keep track of the various modules that provide push support for Colloquy Mobile. 2 | 3 | Instructions for each module should be available in [module_folder]/README 4 | 5 | Contact me for access, patches or anything else :) 6 | -------------------------------------------------------------------------------- /znc/AUTHORS: -------------------------------------------------------------------------------- 1 | prozac 2 | -- main development 3 | 4 | Alex Alexander (wired) 5 | -- badge patch, attachedpush argument option 6 | 7 | Julian Jares 8 | -- Idle + Nighthours settings patch 9 | 10 | Loria 11 | -- controlnick handling fixes 12 | 13 | adamcandy (github) 14 | -- ignore network services 15 | -------------------------------------------------------------------------------- /znc/README: -------------------------------------------------------------------------------- 1 | Thanks to prozac we have a working znc module that pushes priv messages and hilights to Colloquy Mobile. 2 | 3 | * arguments accepted on module load: 4 | -attachedpush 0|1 - when set to 0, znc won't push messages if 5 | another client is attached 6 | 7 | -skipmessagecontent 0|1 - when set to 1, znc won't push the content of 8 | the message, only an indication that a 9 | message has been recieved, which channel and 10 | the sender. 11 | 12 | -awayonlypush 0|1 - when set to 1, znc won't push any messages or 13 | hilights, if the user is not marked as "away" 14 | in IRC. This can be useful if used with 15 | irssi (irssi_proxy) and screen_away plugin. 16 | 17 | -ignorenetworkservices 0|1 - when set to 1, znc won't push messages from network services 18 | 19 | * all the arguments above can be set from with-in your IRC client by messaging *colloquy now 20 | try /msg *colloquy help 21 | 22 | To install make sure you have znc installed and configured, then run the following: 23 | 24 | $ wget http://github.com/wired/colloquypush/raw/master/znc/colloquy.cpp 25 | $ znc-buildmod colloquy.cpp 26 | $ mv colloquy.so ~/.znc/modules/ 27 | 28 | you should now load the "colloquy" module either by enabling it in the web admin module, 29 | or by typing "/msg *status loadmod colloquy" 30 | 31 | * if you don't want to receive push notifications when another client is attached, 32 | you have to tell the module. either add 33 | -attachedpush 0 34 | in znc colloquy module's arguments box or send a message to *colloquy 35 | /msg *colloquy attachedpush 0 36 | 37 | * if you don't want to get the content (body) of the message sent via push, 38 | you have to add 39 | -skipmessagecontent 1 40 | in znc colloquy module's argument box or send a mesage to *colloquy 41 | /msg *colloquy skipmessagecontent 1 42 | 43 | to configure your Colloquy Mobile to use Push, follow the following instructions, quoted from: 44 | http://colloquy.info/project/wiki/PushNotifications 45 | 46 | 1. Hit the + Button in the Connections tab and select "IRC Connection" 47 | 2. Enter the server address as the Address. 48 | 3. Select "Push Notifications" and then turn them On. 49 | 4. Go to "Advanced". 50 | 5. Enter the Server Port and toggle SSL status as necessary. 51 | 6. Enter your ZNC account name as your Username. 52 | 7. Enter your ZNC account password as the Password. (Note: not Nick Pass). 53 | 8. Go back and hit "Connect". 54 | 55 | Result: If all went well, you should get a message from *colloquy saying 56 | that push is now loaded on ZNC for your device. You can also type 57 | "/msg *colloquy list" to see all of the devices that receive push 58 | notifications from your ZNC account. 59 | 60 | Manually kill and restart Colloquy if your device is not automatically 61 | registered. Ensure Colloquy has been granted push notification privileges. 62 | 63 | 2011/02/22 64 | --- 65 | all options are manageable by messaging *colloquy now. 66 | note that module parameters will override any other settings. 67 | 68 | 2009/12/03 69 | --- 70 | you can now set 'night' hours during which znc won't send you notifications, 71 | as well as an 'idle' duration that will have to pass before you receive 72 | any notifications. 73 | send a 'help' message to *colloquy for more instructions 74 | "/msg *colloquy help" 75 | 76 | Note: badge clearing works now :) 77 | -------------------------------------------------------------------------------- /znc/colloquy.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2004-2009 See the AUTHORS file for details. 3 | * 4 | * This program is free software; you can redistribute it and/or modify it 5 | * under the terms of the GNU General Public License version 2 as published 6 | * by the Free Software Foundation. 7 | */ 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #define REQUIRESSL 1 17 | 18 | using std::set; 19 | using std::map; 20 | using std::vector; 21 | 22 | class CDevice { 23 | public: 24 | CDevice(const CString& sToken, CModule& Parent) 25 | : m_Parent(Parent), m_sToken(sToken) { 26 | m_uPort = 0; 27 | m_bNew = false; 28 | m_uFlags = 0; 29 | } 30 | 31 | virtual ~CDevice() {} 32 | 33 | void RemoveClient(CClient* pClient) { 34 | m_spClients.erase(pClient); 35 | } 36 | 37 | bool RemKeyword(const CString& sWord) { 38 | return (m_ssKeywords.erase(sWord) && Save()); 39 | } 40 | 41 | bool AddKeyword(const CString& sWord) { 42 | if (!sWord.empty()) { 43 | m_ssKeywords.insert(sWord); 44 | 45 | return Save(); 46 | } 47 | 48 | return false; 49 | } 50 | 51 | bool Save() { 52 | CString sStr(Serialize()); 53 | 54 | if (!m_Parent.SetNV("device::" + GetToken(), sStr)) { 55 | DEBUG("ERROR while saving colloquy info!"); 56 | return false; 57 | } 58 | 59 | DEBUG("SAVED [" + GetToken() + "]"); 60 | return true; 61 | } 62 | 63 | bool Push(const CString& sNick, const CString& sMessage, const CString& sChannel, bool bHilite, int iBadge) { 64 | if (m_sToken.empty()) { 65 | DEBUG("---- Push(\"" + sNick + "\", \"" + sMessage + "\", \"" + sChannel + "\", " + CString(bHilite) + ", " + CString(iBadge) + ")"); 66 | return false; 67 | } 68 | 69 | if (!m_uPort || m_sHost.empty()) { 70 | DEBUG("---- Push() undefined host or port!"); 71 | } 72 | 73 | CString sPayload; 74 | 75 | sPayload = "{"; 76 | 77 | sPayload += "\"device-token\":\"" + CollEscape(m_sToken) + "\""; 78 | 79 | if (!sMessage.empty()) { 80 | sPayload += ",\"message\":\"" + CollEscape(sMessage) + "\""; 81 | } 82 | // action 83 | if (!sNick.empty()) { 84 | sPayload += ",\"sender\":\"" + CollEscape(sNick) + "\""; 85 | } 86 | 87 | // this handles the badge 88 | // 0 resets, other values add/subtract 89 | if ( iBadge == 0 ) 90 | { 91 | sPayload += ",\"badge\": \"reset\""; 92 | } 93 | else 94 | { 95 | sPayload += ",\"badge\": " + CString(iBadge); 96 | } 97 | 98 | if (!sChannel.empty()) { 99 | sPayload += ",\"room\":\"" + CollEscape(sChannel) + "\""; 100 | } 101 | 102 | if (!m_sConnectionToken.empty()) { 103 | sPayload += ",\"connection\":\"" + CollEscape(m_sConnectionToken) + "\""; 104 | } 105 | 106 | if (!m_sConnectionName.empty()) { 107 | sPayload += ",\"server\":\"" + CollEscape(m_sConnectionName) + "\""; 108 | } 109 | 110 | if (bHilite && !m_sHiliteSound.empty()) { 111 | sPayload += ",\"sound\":\"" + CollEscape(m_sHiliteSound) + "\""; 112 | } else if (!bHilite && !m_sMessageSound.empty() && iBadge != 0) { 113 | sPayload += ",\"sound\":\"" + CollEscape(m_sMessageSound) + "\""; 114 | } 115 | 116 | sPayload += "}"; 117 | 118 | DEBUG("Connecting to [" << m_sHost << ":" << m_uPort << "] to send..."); 119 | DEBUG("----------------------------------------------------------------------------"); 120 | DEBUG(sPayload); 121 | DEBUG("----------------------------------------------------------------------------"); 122 | 123 | CSocket *pSock = new CSocket(&m_Parent); 124 | pSock->Connect(m_sHost, m_uPort, true); 125 | pSock->Write(sPayload); 126 | pSock->Close(Csock::CLT_AFTERWRITE); 127 | m_Parent.AddSocket(pSock); 128 | 129 | return true; 130 | } 131 | 132 | CString CollEscape(const CString& sStr) const { 133 | CString sRet(sStr); 134 | // @todo improve this and eventually support unicode 135 | 136 | sRet.Replace("\\", "\\\\"); 137 | sRet.Replace("\r", "\\r"); 138 | sRet.Replace("\n", "\\n"); 139 | sRet.Replace("\t", "\\t"); 140 | sRet.Replace("\a", "\\a"); 141 | sRet.Replace("\b", "\\b"); 142 | sRet.Replace("\e", "\\e"); 143 | sRet.Replace("\f", "\\f"); 144 | sRet.Replace("\"", "\\\""); 145 | 146 | set ssBadChars; // hack so we don't have to mess around with modifying the string while iterating through it 147 | 148 | for (CString::iterator it = sRet.begin(); it != sRet.end(); it++) { 149 | if (!isprint(*it)) { 150 | ssBadChars.insert(*it); 151 | } 152 | } 153 | 154 | for (set::iterator b = ssBadChars.begin(); b != ssBadChars.end(); b++) { 155 | sRet.Replace(CString(*b), ToHex(*b)); 156 | } 157 | 158 | return sRet; 159 | } 160 | 161 | CString ToHex(const char c) const { 162 | return "\\u00" + CString(c).Escape_n(CString::EURL).TrimPrefix_n("%"); 163 | } 164 | 165 | bool Parse(const CString& sStr) { 166 | VCString vsLines; 167 | sStr.Split("\n", vsLines); 168 | 169 | if (vsLines.size() != 9) { 170 | DEBUG("Wrong number of lines [" << vsLines.size() << "] [" + sStr + "]"); 171 | for (unsigned int a = 0; a < vsLines.size(); a++) { 172 | DEBUG("=============== [" + vsLines[a] + "]"); 173 | } 174 | 175 | return false; 176 | } 177 | 178 | m_sToken = vsLines[0]; 179 | m_sName = vsLines[1]; 180 | m_sHost = vsLines[2].Token(0, false, ":"); 181 | m_uPort = vsLines[2].Token(1, false, ":").ToUInt(); 182 | m_uFlags = vsLines[3].ToUInt(); 183 | m_sConnectionToken = vsLines[4]; 184 | m_sConnectionName = vsLines[5]; 185 | m_sMessageSound = vsLines[6]; 186 | m_sHiliteSound = vsLines[7]; 187 | vsLines[8].Split("\t", m_ssKeywords, false); 188 | 189 | return true; 190 | } 191 | 192 | CString Serialize() const { 193 | CString sRet(m_sToken.FirstLine() + "\n" 194 | + m_sName.FirstLine() + "\n" 195 | + m_sHost.FirstLine() + ":" + CString(m_uPort) + "\n" 196 | + CString(m_uFlags) + "\n" 197 | + m_sConnectionToken.FirstLine() + "\n" 198 | + m_sConnectionName.FirstLine() + "\n" 199 | + m_sMessageSound.FirstLine() + "\n" 200 | + m_sHiliteSound.FirstLine() + "\n"); 201 | 202 | for (SCString::iterator it = m_ssKeywords.begin(); it != m_ssKeywords.end(); it++) { 203 | if (it != m_ssKeywords.begin()) { 204 | sRet += "\t"; 205 | } 206 | 207 | sRet += (*it).FirstLine(); 208 | } 209 | 210 | sRet += "\t"; // Hack to work around a bug, @todo remove once fixed 211 | 212 | return sRet; 213 | } 214 | 215 | // Getters 216 | CString GetToken() const { return m_sToken; } 217 | CString GetName() const { return m_sName; } 218 | CString GetConnectionToken() const { return m_sConnectionToken; } 219 | CString GetConnectionName() const { return m_sConnectionName; } 220 | CString GetMessageSound() const { return m_sMessageSound; } 221 | CString GetHiliteSound() const { return m_sHiliteSound; } 222 | const SCString& GetKeywords() const { return m_ssKeywords; } 223 | bool IsConnected() const { return !m_spClients.empty(); } 224 | bool HasClient(CClient* p) const { return m_spClients.find(p) != m_spClients.end(); } 225 | CString GetHost() const { return m_sHost; } 226 | unsigned short GetPort() const { return m_uPort; } 227 | bool IsNew() const { return m_bNew; } 228 | 229 | // Setters 230 | void SetToken(const CString& s) { m_sToken = s; } 231 | void SetName(const CString& s) { m_sName = s; } 232 | void SetConnectionToken(const CString& s) { m_sConnectionToken = s; } 233 | void SetConnectionName(const CString& s) { m_sConnectionName = s; } 234 | void SetMessageSound(const CString& s) { m_sMessageSound = s; } 235 | void SetHiliteSound(const CString& s) { m_sHiliteSound = s; } 236 | void AddClient(CClient* p) { m_spClients.insert(p); } 237 | void SetHost(const CString& s) { m_sHost = s; } 238 | void SetPort(unsigned short u) { m_uPort = u; } 239 | void SetNew(bool b = true) { m_bNew = b; } 240 | 241 | // Flags 242 | void SetFlag(unsigned int u) { m_uFlags |= u; } 243 | void UnsetFlag(unsigned int u) { m_uFlags &= ~u; } 244 | bool HasFlag(unsigned int u) const { return m_uFlags & u; } 245 | 246 | enum EFlags { 247 | Disabled = 1 << 0, 248 | IncludeMsg = 1 << 1, 249 | IncludeNick = 1 << 2, 250 | IncludeChan = 1 << 3 251 | }; 252 | // !Flags 253 | 254 | void Reset() { 255 | m_sToken.clear(); 256 | m_sName.clear(); 257 | m_sConnectionToken.clear(); 258 | m_sConnectionName.clear(); 259 | m_sMessageSound.clear(); 260 | m_sHiliteSound.clear(); 261 | m_sHost.clear(); 262 | m_uPort = 0; 263 | m_uFlags = 0; 264 | m_ssKeywords.clear(); 265 | } 266 | 267 | private: 268 | set m_spClients; 269 | CModule& m_Parent; 270 | bool m_bNew; 271 | CString m_sToken; 272 | CString m_sName; 273 | CString m_sConnectionToken; 274 | CString m_sConnectionName; 275 | CString m_sMessageSound; 276 | CString m_sHiliteSound; 277 | SCString m_ssKeywords; 278 | CString m_sHost; 279 | unsigned short m_uPort; 280 | unsigned int m_uFlags; 281 | }; 282 | 283 | 284 | class CColloquyMod : public CModule { 285 | protected: 286 | int m_idleAfterMinutes; 287 | int m_lastActivity; 288 | int m_debug; 289 | int m_nightHoursStart; 290 | int m_nightHoursEnd; 291 | bool m_bAttachedPush; 292 | bool m_bSkipMessageContent; 293 | bool m_bAwayOnlyPush; 294 | bool m_bIgnoreNetworkServices; 295 | public: 296 | MODCONSTRUCTOR(CColloquyMod) { 297 | // init vars 298 | m_bAttachedPush = true; 299 | m_bSkipMessageContent = false; 300 | m_bAwayOnlyPush = false; 301 | m_bIgnoreNetworkServices = false; 302 | m_idleAfterMinutes=0; 303 | m_nightHoursStart=-1; 304 | m_nightHoursEnd=-1; 305 | m_debug=0; 306 | 307 | LoadRegistry(); 308 | 309 | for (MCString::iterator it = BeginNV(); it != EndNV(); it++) { 310 | CString sKey(it->first); 311 | 312 | if (sKey.TrimPrefix("device::")) { 313 | CDevice* pDevice = new CDevice(sKey, *this); 314 | 315 | if (!pDevice->Parse(it->second)) { 316 | DEBUG(" --- Error while parsing device [" + sKey + "]"); 317 | delete pDevice; 318 | continue; 319 | } 320 | 321 | m_mspDevices[pDevice->GetToken()] = pDevice; 322 | } else { 323 | DEBUG(" --- Unknown registry entry: [" << it->first << "]"); 324 | } 325 | } 326 | } 327 | 328 | virtual bool OnLoad(const CString& sArgs, CString& sErrorMsg) { 329 | SCString sArgSet; 330 | 331 | //Loading stored stuff 332 | for(MCString::iterator it = BeginNV(); it != EndNV(); it++) 333 | { 334 | if(it->first == "u:idle") { 335 | m_idleAfterMinutes = it->second.ToInt(); 336 | } else if (it->first == "u:attachedpush") { 337 | m_bAttachedPush = it->second.ToBool(); 338 | } else if (it->first == "u:skipmessagecontent") { 339 | m_bSkipMessageContent = it->second.ToBool(); 340 | } else if (it->first == "u:awayonlypush") { 341 | m_bAwayOnlyPush = it->second.ToBool(); 342 | } else if (it->first == "u:ignorenetworkservices") { 343 | m_bIgnoreNetworkServices = it->second.ToBool(); 344 | } else if (it->first == "u:nighthoursstart") { 345 | m_nightHoursStart = it->second.ToInt(); 346 | } else if (it->first == "u:nighthoursend") { 347 | m_nightHoursEnd = it->second.ToInt(); 348 | } else if (it->first == "u:debug") { 349 | m_debug = it->second.ToInt(); 350 | } 351 | } 352 | 353 | sArgs.Split("-",sArgSet); 354 | for ( SCString::iterator it = sArgSet.begin(); it != sArgSet.end(); it++ ) { 355 | CString sArg(*it); 356 | sArg.Trim(); 357 | if ( sArg.TrimPrefix("attachedpush") ) { 358 | m_bAttachedPush = sArg.ToBool(); 359 | } else if ( sArg.TrimPrefix("skipmessagecontent") ) { 360 | m_bSkipMessageContent = sArg.ToBool(); 361 | } else if ( sArg.TrimPrefix("awayonlypush") ) { 362 | m_bAwayOnlyPush = sArg.ToBool(); 363 | } else if ( sArg.TrimPrefix("ignorenetworkservices") ) { 364 | m_bIgnoreNetworkServices = sArg.ToBool(); 365 | } 366 | } 367 | 368 | return true; 369 | } 370 | 371 | virtual ~CColloquyMod() { 372 | for (map::iterator it = m_mspDevices.begin(); it != m_mspDevices.end(); it++) { 373 | it->second->Save(); 374 | delete it->second; 375 | } 376 | } 377 | 378 | virtual EModRet OnUserRaw(CString& sLine) { 379 | // Trap "ISON *modname" and fake a reply or else colloquy won't let you communicate with *status or *module 380 | // This is a hack in that it doesn't support multiple users 381 | const CString& sStatusPrefix = m_pUser->GetStatusPrefix(); 382 | 383 | // Assume if we encounter sStatusPrefix on first nick, only controlnicks follow if there are more then one 384 | if (sLine.Equals("ISON " + sStatusPrefix, false, 5 + sStatusPrefix.size()) || 385 | sLine.Equals("ISON :" + sStatusPrefix, false, 6 + sStatusPrefix.size())) { 386 | CString sNicks = sLine.Token(1, true); 387 | if(sNicks[0] == ':') 388 | sNicks.LeftChomp(); 389 | PutUser(":" + GetNetwork()->GetIRCServer() + " 303 " + GetNetwork()->GetIRCNick().GetNick() + " :" + sNicks); 390 | 391 | return HALTCORE; 392 | } 393 | 394 | // Trap the PUSH messages that colloquy sends to give us info about the client 395 | if (sLine.TrimPrefix("PUSH ")) { 396 | if (sLine.TrimPrefix("add-device ")) { 397 | CString sToken(sLine.Token(0)); 398 | CDevice* pDevice = FindDevice(sToken); 399 | 400 | if (!pDevice) { 401 | pDevice = new CDevice(sToken, *this); 402 | pDevice->SetNew(); 403 | m_mspDevices[pDevice->GetToken()] = pDevice; 404 | } 405 | 406 | pDevice->Reset(); 407 | pDevice->SetToken(sToken); 408 | pDevice->SetName(sLine.Token(1, true).TrimPrefix_n(":")); 409 | pDevice->AddClient(m_pClient); 410 | } else if (sLine.TrimPrefix("remove-device :")) { 411 | CDevice* pDevice = FindDevice(sLine); 412 | 413 | if (pDevice) { 414 | pDevice->SetFlag(CDevice::Disabled); 415 | //PutModule("Disabled phone [" + pDevice->GetName() + "]"); 416 | m_pUser->PutModule(GetModName(), "Disabled phone [" + pDevice->GetName() + "]", NULL, m_pClient); 417 | } 418 | } else { 419 | CDevice* pDevice = FindDevice(m_pClient); 420 | 421 | if (pDevice) { 422 | if (sLine.TrimPrefix("connection ")) { 423 | pDevice->SetConnectionToken(sLine.Token(0)); 424 | pDevice->SetConnectionName(sLine.Token(1, true).TrimPrefix_n(":")); 425 | } else if (sLine.TrimPrefix("service ")) { 426 | pDevice->SetHost(sLine.Token(0)); 427 | pDevice->SetPort(sLine.Token(1).ToUInt()); 428 | } else if (sLine.TrimPrefix("highlight-word :")) { 429 | pDevice->AddKeyword(sLine); 430 | } else if (sLine.TrimPrefix("highlight-sound :")) { 431 | pDevice->SetHiliteSound(sLine); 432 | } else if (sLine.TrimPrefix("message-sound :")) { 433 | pDevice->SetMessageSound(sLine); 434 | } else if (sLine.Equals("end-device")) { 435 | if (!pDevice->Save()) { 436 | PutModule("Unable to save phone [" + pDevice->GetName() + "]"); 437 | } else { 438 | if (pDevice->IsNew()) { 439 | pDevice->SetNew(false); 440 | m_pUser->PutModule(GetModName(), "Added new phone [" + pDevice->GetName() + "] to the system", NULL, m_pClient); 441 | } 442 | } 443 | } else { 444 | DEBUG("---------------------------------------------------------------------- PUSH ERROR [" + sLine + "]"); 445 | } 446 | } else { 447 | DEBUG("No pDevice defined for this client!"); 448 | } 449 | } 450 | 451 | return HALT; 452 | } 453 | 454 | return CONTINUE; 455 | } 456 | 457 | CDevice* FindDevice(const CString& sToken) { 458 | map::iterator it = m_mspDevices.find(sToken); 459 | 460 | if (it != m_mspDevices.end()) { 461 | return it->second; 462 | } 463 | 464 | return NULL; 465 | } 466 | 467 | CDevice* FindDevice(CClient* pClient) { 468 | for (map::iterator it = m_mspDevices.begin(); it != m_mspDevices.end(); it++) { 469 | if (it->second->HasClient(pClient)) { 470 | return it->second; 471 | } 472 | } 473 | 474 | return NULL; 475 | } 476 | 477 | virtual EModRet OnPrivNotice(CNick& Nick, CString& sMessage) { 478 | Push(Nick.GetNick(), sMessage, "", false, 1); 479 | return CONTINUE; 480 | } 481 | 482 | virtual EModRet OnChanNotice(CNick& Nick, CChan& Channel, CString& sMessage) { 483 | Push(Nick.GetNick(), sMessage, Channel.GetName(), true, 1); 484 | return CONTINUE; 485 | } 486 | 487 | virtual EModRet OnPrivMsg(CNick& Nick, CString& sMessage) { 488 | Push(Nick.GetNick(), sMessage, "", false, 1); 489 | return CONTINUE; 490 | } 491 | 492 | virtual EModRet OnChanMsg(CNick& Nick, CChan& Channel, CString& sMessage) { 493 | Push(Nick.GetNick(), sMessage, Channel.GetName(), true, 1); 494 | return CONTINUE; 495 | } 496 | 497 | virtual CString intToHours(int ihour) { 498 | if (ihour<0) { 499 | return "---"; 500 | } 501 | int minutes = (ihour%60); 502 | CString cMinutes; 503 | if (minutes<10) { 504 | cMinutes="0"+CString(minutes); 505 | } else { 506 | cMinutes=CString(minutes); 507 | } 508 | int hours= (ihour-minutes)/60; 509 | CString cHour=CString(hours); 510 | return cHour+":"+cMinutes; 511 | } 512 | 513 | virtual int hoursToInt(CString chour) { 514 | int len=chour.length(); 515 | int colon=chour.find(":"); 516 | if (((colon==1) && (len==4)) || ((colon==2) && (len==5))) {//only valid hours 517 | int hour; 518 | int minutes; 519 | if (colon==1) { 520 | hour=atoi(chour.substr(0,1).c_str()); 521 | minutes=atoi(chour.substr(2,2).c_str()); 522 | } else { 523 | hour=atoi(chour.substr(0,2).c_str()); 524 | minutes=atoi(chour.substr(3,2).c_str()); 525 | } 526 | return hour*60+minutes; 527 | } else { 528 | return -1; 529 | } 530 | 531 | } 532 | 533 | virtual void OnModCommand(const CString& sCommand) { 534 | if (sCommand.Equals("HELP")) { 535 | PutModule("Commands: HELP, LIST, SET