parser.js (31288B)
1 // http://weechat.org/files/doc/devel/weechat_dev.en.html#color_codes_in_strings 2 'use strict'; 3 4 /** 5 * WeeChat protocol handling. 6 * 7 * This object parses messages and formats commands for the WeeChat 8 * protocol. It's independent from the communication layer and thus 9 * may be used with any network mechanism. 10 */ 11 export const WeeChatProtocol = function () { 12 // specific parsing for each object type 13 this._types = { 14 chr: this._getChar, 15 int: this._getInt, 16 str: this._getString, 17 inf: this._getInfo, 18 hda: this._getHdata, 19 ptr: this._getPointer, 20 lon: this._getStrNumber, 21 tim: this._getTime, 22 buf: this._getString, 23 arr: this._getArray, 24 htb: this._getHashTable, 25 inl: this._getInfolist 26 }; 27 28 // string value for some object types 29 this._typesStr = { 30 chr: this._strDirect, 31 str: this._strDirect, 32 int: this._strToString, 33 tim: this._strToString, 34 ptr: this._strDirect 35 }; 36 }; 37 38 /** 39 * WeeChat colors names. 40 */ 41 WeeChatProtocol._weeChatColorsNames = [ 42 'default', 43 'black', 44 'darkgray', 45 'red', 46 'lightred', 47 'green', 48 'lightgreen', 49 'brown', 50 'yellow', 51 'blue', 52 'lightblue', 53 'magenta', 54 'lightmagenta', 55 'cyan', 56 'lightcyan', 57 'gray', 58 'white' 59 ]; 60 61 /** 62 * Style options names. 63 */ 64 WeeChatProtocol._colorsOptionsNames = [ 65 'separator', 66 'chat', 67 'chat_time', 68 'chat_time_delimiters', 69 'chat_prefix_error', 70 'chat_prefix_network', 71 'chat_prefix_action', 72 'chat_prefix_join', 73 'chat_prefix_quit', 74 'chat_prefix_more', 75 'chat_prefix_suffix', 76 'chat_buffer', 77 'chat_server', 78 'chat_channel', 79 'chat_nick', 80 'chat_nick_self', 81 'chat_nick_other', 82 'invalid', 83 'invalid', 84 'invalid', 85 'invalid', 86 'invalid', 87 'invalid', 88 'invalid', 89 'invalid', 90 'invalid', 91 'invalid', 92 'chat_host', 93 'chat_delimiters', 94 'chat_highlight', 95 'chat_read_marker', 96 'chat_text_found', 97 'chat_value', 98 'chat_prefix_buffer', 99 'chat_tags', 100 'chat_inactive_window', 101 'chat_inactive_buffer', 102 'chat_prefix_buffer_inactive_buffer', 103 'chat_nick_offline', 104 'chat_nick_offline_highlight', 105 'chat_nick_prefix', 106 'chat_nick_suffix', 107 'emphasis', 108 'chat_day_change' 109 ]; 110 111 /** 112 * Gets the default color. 113 * 114 * @return Default color 115 */ 116 WeeChatProtocol._getDefaultColor = function () { 117 return { 118 type: 'weechat', 119 name: 'default' 120 }; 121 }; 122 123 /** 124 * Gets the default attributes. 125 * 126 * @return Default attributes 127 */ 128 WeeChatProtocol._getDefaultAttributes = function () { 129 return { 130 name: null, 131 override: { 132 bold: false, 133 reverse: false, 134 italic: false, 135 underline: false 136 } 137 }; 138 }; 139 140 /** 141 * Gets the default style (default colors and attributes). 142 * 143 * @return Default style 144 */ 145 WeeChatProtocol._getDefaultStyle = function () { 146 return { 147 fgColor: WeeChatProtocol._getDefaultColor(), 148 bgColor: WeeChatProtocol._getDefaultColor(), 149 attrs: WeeChatProtocol._getDefaultAttributes() 150 }; 151 }; 152 153 /** 154 * Clones a color object. 155 * 156 * @param color Color object to clone 157 * @return Cloned color object 158 */ 159 WeeChatProtocol._cloneColor = function (color) { 160 var clone = {}; 161 162 for (var key in color) { 163 clone[key] = color[key]; 164 } 165 166 return clone; 167 }; 168 169 /** 170 * Clones an attributes object. 171 * 172 * @param attrs Attributes object to clone 173 * @return Cloned attributes object 174 */ 175 WeeChatProtocol._cloneAttrs = function (attrs) { 176 var clone = {}; 177 178 clone.name = attrs.name; 179 clone.override = {}; 180 for (var attr in attrs.override) { 181 clone.override[attr] = attrs.override[attr]; 182 } 183 184 return clone; 185 }; 186 187 /** 188 * Gets the name of an attribute from its character. 189 * 190 * @param ch Character of attribute 191 * @return Name of attribute 192 */ 193 WeeChatProtocol._attrNameFromChar = function (ch) { 194 var chars = { 195 // WeeChat protocol 196 '*': 'b', 197 '!': 'r', 198 '/': 'i', 199 _: 'u', 200 201 // some extension often used (IRC?) 202 '\x01': 'b', 203 '\x02': 'r', 204 '\x03': 'i', 205 '\x04': 'u' 206 }; 207 208 if (ch in chars) { 209 return chars[ch]; 210 } 211 212 return null; 213 }; 214 215 /** 216 * Gets an attributes object from a string of attribute characters. 217 * 218 * @param str String of attribute characters 219 * @return Attributes object (null if unchanged) 220 */ 221 WeeChatProtocol._attrsFromStr = function (str) { 222 var attrs = WeeChatProtocol._getDefaultAttributes(); 223 224 for (var i = 0; i < str.length; ++i) { 225 var ch = str.charAt(i); 226 if (ch === '|') { 227 // means keep attributes, so unchanged 228 return null; 229 } 230 var attrName = WeeChatProtocol._attrNameFromChar(ch); 231 if (attrName !== null) { 232 attrs.override[attrName] = true; 233 } 234 } 235 236 return attrs; 237 }; 238 239 /** 240 * Gets a single color from a string representing its index (WeeChat and 241 * extended colors only, NOT colors options). 242 * 243 * @param str Color string (e.g., "05" or "00134") 244 * @return Color object 245 */ 246 WeeChatProtocol._getColorObj = function (str) { 247 if (str.length === 2) { 248 var code = parseInt(str); 249 if (code > 16) { 250 // should never happen 251 return WeeChatProtocol._getDefaultColor(); 252 } else { 253 return { 254 type: 'weechat', 255 name: WeeChatProtocol._weeChatColorsNames[code] 256 }; 257 } 258 } else { 259 var codeStr = str.substring(1); 260 return { 261 type: 'ext', 262 name: parseInt(codeStr).toString() 263 }; 264 } 265 }; 266 267 /** 268 * Gets colors and attributes of text element. 269 * 270 * See <http://www.weechat.org/files/doc/devel/weechat_dev.en.html#color_codes_in_strings>. 271 * 272 * @param txt Text element 273 * @return Colors, attributes and plain text of this text element: 274 * fgColor: Foreground color (null if unchanged) 275 * bgColor: Background color (null if unchanged) 276 * attrs: Attributes (null if unchanged) 277 * text: Plain text element 278 */ 279 WeeChatProtocol._getStyle = function (txt) { 280 var matchers = [ 281 { 282 // color option 283 // STD 284 regex: /^(\d{2})/, 285 fn: function (m) { 286 var ret = {}; 287 var optionCode = parseInt(m[1]); 288 289 if (optionCode >= WeeChatProtocol._colorsOptionsNames.length) { 290 // should never happen 291 return { 292 fgColor: null, 293 bgColor: null, 294 attrs: null 295 }; 296 } 297 var optionName = WeeChatProtocol._colorsOptionsNames[optionCode]; 298 ret.fgColor = { 299 type: 'option', 300 name: optionName 301 }; 302 ret.bgColor = WeeChatProtocol._cloneColor(ret.fgColor); 303 ret.attrs = { 304 name: optionName, 305 override: {} 306 }; 307 308 return ret; 309 } 310 }, 311 { 312 // ncurses pair 313 // EXT 314 regex: /^@(\d{5})/, 315 fn: function (m) { 316 // unimplemented case 317 return { 318 fgColor: null, 319 bgColor: null, 320 attrs: null 321 }; 322 } 323 }, 324 { 325 // foreground color with F 326 // "F" + (A)STD 327 // "F" + (A)EXT 328 regex: /^F(?:([*!\/_|]*)(\d{2})|@([\x01\x02\x03\x04*!\/_|]*)(\d{5}))/, 329 fn: function (m) { 330 var ret = { 331 bgColor: null 332 }; 333 334 if (m[2]) { 335 ret.attrs = WeeChatProtocol._attrsFromStr(m[1]); 336 ret.fgColor = WeeChatProtocol._getColorObj(m[2]); 337 } else { 338 ret.attrs = WeeChatProtocol._attrsFromStr(m[3]); 339 ret.fgColor = WeeChatProtocol._getColorObj(m[4]); 340 } 341 342 return ret; 343 } 344 }, 345 { 346 // background color (no attributes) 347 // "B" + STD 348 // "B" + EXT 349 regex: /^B(\d{2}|@\d{5})/, 350 fn: function (m) { 351 return { 352 fgColor: null, 353 bgColor: WeeChatProtocol._getColorObj(m[1]), 354 attrs: null 355 }; 356 } 357 }, 358 { 359 // foreground, background (+ attributes) 360 // "*" + (A)STD + "," + STD 361 // "*" + (A)STD + "," + EXT 362 // "*" + (A)EXT + "," + STD 363 // "*" + (A)EXT + "," + EXT 364 // WeeChat 2.6+ use a tilde (~) instead of a comma (,) so recognise both 365 regex: /^\*(?:([\x01\x02\x03\x04*!\/_|]*)(\d{2})|@([\x01\x02\x03\x04*!\/_|]*)(\d{5}))[,~](\d{2}|@\d{5})/, 366 fn: function (m) { 367 var ret = {}; 368 369 if (m[2]) { 370 ret.attrs = WeeChatProtocol._attrsFromStr(m[1]); 371 ret.fgColor = WeeChatProtocol._getColorObj(m[2]); 372 } else { 373 ret.attrs = WeeChatProtocol._attrsFromStr(m[3]); 374 ret.fgColor = WeeChatProtocol._getColorObj(m[4]); 375 } 376 ret.bgColor = WeeChatProtocol._getColorObj(m[5]); 377 378 return ret; 379 } 380 }, 381 { 382 // foreground color with * (+ attributes) (fall back, must be checked before previous case) 383 // "*" + (A)STD 384 // "*" + (A)EXT 385 regex: /^\*([\x01\x02\x03\x04*!\/_|]*)(\d{2}|@\d{5})/, 386 fn: function (m) { 387 return { 388 fgColor: WeeChatProtocol._getColorObj(m[2]), 389 bgColor: null, 390 attrs: WeeChatProtocol._attrsFromStr(m[1]) 391 }; 392 } 393 }, 394 { 395 // emphasis 396 // "E" 397 regex: /^E/, 398 fn: function (m) { 399 var ret = {}; 400 401 ret.fgColor = { 402 type: 'option', 403 name: 'emphasis' 404 }; 405 ret.bgColor = WeeChatProtocol._cloneColor(ret.fgColor); 406 ret.attrs = { 407 name: 'emphasis', 408 override: {} 409 }; 410 411 return ret; 412 } 413 } 414 ]; 415 416 // parse 417 var ret = { 418 fgColor: null, 419 bgColor: null, 420 attrs: null, 421 text: txt 422 }; 423 matchers.some(function (matcher) { 424 var m = txt.match(matcher.regex); 425 if (m) { 426 ret = matcher.fn(m); 427 ret.text = txt.substring(m[0].length); 428 return true; 429 } 430 431 return false; 432 }); 433 434 return ret; 435 }; 436 437 /** 438 * Transforms a raw text into an array of text elements with integrated 439 * colors and attributes. 440 * 441 * @param rawText Raw text to transform 442 * @return Array of text elements 443 */ 444 WeeChatProtocol.rawText2Rich = function (rawText) { 445 /* This is subtle, but JavaScript adds the token to the output list 446 * when it's surrounded by capturing parentheses. 447 */ 448 var parts = rawText.split(/(\x19|\x1a|\x1b|\x1c)/); 449 450 // no colors/attributes 451 if (parts.length === 1) { 452 return [ 453 { 454 attrs: WeeChatProtocol._getDefaultAttributes(), 455 fgColor: WeeChatProtocol._getDefaultColor(), 456 bgColor: WeeChatProtocol._getDefaultColor(), 457 text: parts[0] 458 } 459 ]; 460 } 461 462 // find the style of every part 463 var curFgColor = WeeChatProtocol._getDefaultColor(); 464 var curBgColor = WeeChatProtocol._getDefaultColor(); 465 var curAttrs = WeeChatProtocol._getDefaultAttributes(); 466 var curSpecialToken = null; 467 var curAttrsOnlyFalseOverrides = true; 468 469 return parts 470 .map(function (p) { 471 if (p.length === 0) { 472 return null; 473 } 474 var firstCharCode = p.charCodeAt(0); 475 var firstChar = p.charAt(0); 476 477 if (firstCharCode >= 0x19 && firstCharCode <= 0x1c) { 478 // special token 479 if (firstCharCode === 0x1c) { 480 // always reset colors 481 curFgColor = WeeChatProtocol._getDefaultColor(); 482 curBgColor = WeeChatProtocol._getDefaultColor(); 483 if (curSpecialToken !== 0x19) { 484 // also reset attributes 485 curAttrs = WeeChatProtocol._getDefaultAttributes(); 486 } 487 } 488 curSpecialToken = firstCharCode; 489 return null; 490 } 491 492 var text = p; 493 if (curSpecialToken === 0x19) { 494 // get new style 495 var style = WeeChatProtocol._getStyle(p); 496 497 // set foreground color if changed 498 if (style.fgColor !== null) { 499 curFgColor = style.fgColor; 500 } 501 502 // set background color if changed 503 if (style.bgColor !== null) { 504 curBgColor = style.bgColor; 505 } 506 507 // set attibutes if changed 508 if (style.attrs !== null) { 509 curAttrs = style.attrs; 510 } 511 512 // set plain text 513 text = style.text; 514 } else if (curSpecialToken === 0x1a || curSpecialToken === 0x1b) { 515 // set/reset attribute 516 var orideVal = curSpecialToken === 0x1a; 517 518 // set attribute override if we don't have to keep all of them 519 if (firstChar !== '|') { 520 var orideName = WeeChatProtocol._attrNameFromChar(firstChar); 521 if (orideName) { 522 // known attribute 523 curAttrs.override[orideName] = orideVal; 524 text = p.substring(1); 525 } 526 } 527 } 528 529 // reset current special token 530 curSpecialToken = null; 531 532 // if text is empty, don't bother returning it 533 if (text.length === 0) { 534 return null; 535 } 536 537 /* As long as attributes are only false overrides, without any option 538 * name, it's safe to remove them. 539 */ 540 if (curAttrsOnlyFalseOverrides && curAttrs.name === null) { 541 var allReset = true; 542 for (var attr in curAttrs.override) { 543 if (curAttrs.override[attr]) { 544 allReset = false; 545 break; 546 } 547 } 548 if (allReset) { 549 curAttrs.override = {}; 550 } else { 551 curAttrsOnlyFalseOverrides = false; 552 } 553 } 554 555 // parsed text element 556 return { 557 fgColor: WeeChatProtocol._cloneColor(curFgColor), 558 bgColor: WeeChatProtocol._cloneColor(curBgColor), 559 attrs: WeeChatProtocol._cloneAttrs(curAttrs), 560 text: text 561 }; 562 }) 563 .filter(function (p) { 564 return p !== null; 565 }); 566 }; 567 568 /** 569 * Unsigned integer array to string. 570 * 571 * @param uia Unsigned integer array 572 * @return Decoded string 573 */ 574 WeeChatProtocol._uia2s = function (uia) { 575 if (!uia.length || uia[0] === 0) return ''; 576 577 try { 578 var encodedString = String.fromCharCode.apply(null, uia), 579 decodedString = decodeURIComponent(escape(encodedString)); 580 return decodedString; 581 } catch (exception) { 582 // Replace all non-ASCII bytes with "?" if the string couldn't be 583 // decoded as UTF-8. 584 var s = ''; 585 for (var i = 0, n = uia.length; i < n; i++) { 586 s += uia[i] < 0x80 ? String.fromCharCode(uia[i]) : '?'; 587 } 588 return s; 589 } 590 }; 591 592 /** 593 * Merges default parameters with overriding parameters. 594 * 595 * @param defaults Default parameters 596 * @param override Overriding parameters 597 * @return Merged parameters 598 */ 599 WeeChatProtocol._mergeParams = function (defaults, override) { 600 for (var v in override) { 601 defaults[v] = override[v]; 602 } 603 604 return defaults; 605 }; 606 607 /** 608 * Formats a command. 609 * 610 * @param id Command ID (null for no ID) 611 * @param name Command name 612 * @param parts Command parts 613 * @return Formatted command string 614 */ 615 WeeChatProtocol._formatCmd = function (id, name, parts) { 616 var cmdIdName; 617 var cmd; 618 619 cmdIdName = id !== null ? '(' + id + ') ' : ''; 620 cmdIdName += name; 621 parts.unshift(cmdIdName); 622 cmd = parts.join(' '); 623 cmd += '\n'; 624 625 cmd.replace(/[\r\n]+$/g, '').split('\n'); 626 627 return cmd; 628 }; 629 630 /** 631 * Formats a handshake command. 632 * 633 * @param params Parameters: 634 * password: list of supported hash algorithms, colon separated (optional) 635 * compression: compression ('off' or 'zlib') (optional) 636 * @return Formatted handshake command string 637 */ 638 //https://weechat.org/files/doc/stable/weechat_relay_protocol.en.html#command_handshake 639 WeeChatProtocol.formatHandshake = function (params) { 640 var defaultParams = { 641 password_hash_algo: 'pbkdf2+sha512', 642 compression: 'zlib' 643 }; 644 var keys = []; 645 var parts = []; 646 647 params = WeeChatProtocol._mergeParams(defaultParams, params); 648 649 if (params.compression !== null) { 650 keys.push('compression=' + params.compression); 651 } 652 653 if (params.password_hash_algo !== null) { 654 keys.push('password_hash_algo=' + params.password_hash_algo); 655 } 656 657 parts.push(keys.join(',')); 658 659 return WeeChatProtocol._formatCmd(null, 'handshake', parts); 660 }; 661 662 /** 663 * Formats an init command for weechat versions < 2.9 664 * 665 * @param params Parameters: 666 * password: password (optional) 667 * compression: compression ('off' or 'zlib') (optional) 668 * totp: One Time Password (optional) 669 * @return Formatted init command string 670 */ 671 WeeChatProtocol.formatInitPre29 = function (params) { 672 var defaultParams = { 673 password: null, 674 compression: 'zlib', 675 totp: null 676 }; 677 var keys = []; 678 var parts = []; 679 680 params = WeeChatProtocol._mergeParams(defaultParams, params); 681 keys.push('compression=' + params.compression); 682 if (params.password !== null) { 683 keys.push('password=' + params.password); 684 } 685 if (params.totp !== null) { 686 keys.push('totp=' + params.totp); 687 } 688 parts.push(keys.join(',')); 689 690 return WeeChatProtocol._formatCmd(null, 'init', parts); 691 }; 692 693 /** 694 * Formats an init command for weechat versions >= 2.9 695 * 696 * @param params Parameters: 697 * password_hash: hash of password with method and salt 698 * totp: One Time Password (can be null) 699 * @return Formatted init command string 700 */ 701 WeeChatProtocol.formatInit29 = function (password_hash, totp) { 702 var keys = []; 703 var parts = []; 704 705 if (totp != null) { 706 keys.push('totp=' + totp); 707 } 708 if (password_hash !== null) { 709 keys.push('password_hash=' + password_hash); 710 } 711 parts.push(keys.join(',')); 712 713 return WeeChatProtocol._formatCmd(null, 'init', parts); 714 }; 715 716 /** 717 * Formats an hdata command. 718 * 719 * @param params Parameters: 720 * id: command ID (optional) 721 * path: hdata path (mandatory) 722 * keys: array of keys (optional) 723 * @return Formatted hdata command string 724 */ 725 WeeChatProtocol.formatHdata = function (params) { 726 var defaultParams = { 727 id: null, 728 keys: null 729 }; 730 var parts = []; 731 732 params = WeeChatProtocol._mergeParams(defaultParams, params); 733 parts.push(params.path); 734 if (params.keys !== null) { 735 parts.push(params.keys.join(',')); 736 } 737 738 return WeeChatProtocol._formatCmd(params.id, 'hdata', parts); 739 }; 740 741 /** 742 * Formats an info command. 743 * 744 * @param params Parameters: 745 * id: command ID (optional) 746 * name: info name (mandatory) 747 * @return Formatted info command string 748 */ 749 WeeChatProtocol.formatInfo = function (params) { 750 var defaultParams = { 751 id: null 752 }; 753 var parts = []; 754 755 params = WeeChatProtocol._mergeParams(defaultParams, params); 756 parts.push(params.name); 757 758 return WeeChatProtocol._formatCmd(params.id, 'info', parts); 759 }; 760 761 /** 762 * Formats an infolist command. 763 * 764 * @param params Parameters: 765 * id: command ID (optional) 766 * name: infolist name (mandatory) 767 * pointer: optional 768 * arguments: optional 769 * @return Formatted infolist command string 770 */ 771 WeeChatProtocol.formatInfolist = function (params) { 772 var defaultParams = { 773 id: null, 774 pointer: null, 775 args: null 776 }; 777 var parts = []; 778 779 params = WeeChatProtocol._mergeParams(defaultParams, params); 780 parts.push(params.name); 781 if (params.pointer !== null) { 782 parts.push(params.pointer); 783 } 784 if (params.pointer !== null) { 785 parts.push(params.args); 786 } 787 788 return WeeChatProtocol._formatCmd(params.id, 'infolist', parts); 789 }; 790 791 /** 792 * Formats a nicklist command. 793 * 794 * @param params Parameters: 795 * id: command ID (optional) 796 * buffer: buffer name (optional) 797 * @return Formatted nicklist command string 798 */ 799 WeeChatProtocol.formatNicklist = function (params) { 800 var defaultParams = { 801 id: null, 802 buffer: null 803 }; 804 var parts = []; 805 806 params = WeeChatProtocol._mergeParams(defaultParams, params); 807 if (params.buffer !== null) { 808 parts.push(params.buffer); 809 } 810 811 return WeeChatProtocol._formatCmd(params.id, 'nicklist', parts); 812 }; 813 814 /** 815 * Formats an input command. 816 * 817 * @param params Parameters: 818 * id: command ID (optional) 819 * buffer: target buffer (mandatory) 820 * data: input data (mandatory) 821 * @return Formatted input command string 822 */ 823 WeeChatProtocol.formatInput = function (params) { 824 var defaultParams = { 825 id: null 826 }; 827 var parts = []; 828 829 params = WeeChatProtocol._mergeParams(defaultParams, params); 830 parts.push(params.buffer); 831 parts.push(params.data); 832 833 return WeeChatProtocol._formatCmd(params.id, 'input', parts); 834 }; 835 836 /** 837 * Formats a completion command. 838 * https://weechat.org/files/doc/stable/weechat_relay_protocol.en.html#command_completion 839 * @param params Parameters: 840 * id: command ID (optional) 841 * buffer: target buffer (mandatory) 842 * position: position for completion in string (optional) 843 * data: input data (optional) 844 * @return Formatted input command string 845 */ 846 WeeChatProtocol.formatCompletion = function (params) { 847 var defaultParams = { 848 id: null, 849 position: -1 850 }; 851 var parts = []; 852 853 params = WeeChatProtocol._mergeParams(defaultParams, params); 854 parts.push(params.buffer); 855 parts.push(params.position); 856 if (params.data) { 857 parts.push(params.data); 858 } 859 860 return WeeChatProtocol._formatCmd(params.id, 'completion', parts); 861 }; 862 863 /** 864 * Formats a sync or a desync command. 865 * 866 * @param params Parameters (see _formatSync and _formatDesync) 867 * @return Formatted sync/desync command string 868 */ 869 WeeChatProtocol._formatSyncDesync = function (cmdName, params) { 870 var defaultParams = { 871 id: null, 872 buffers: null, 873 options: null 874 }; 875 var parts = []; 876 877 params = WeeChatProtocol._mergeParams(defaultParams, params); 878 if (params.buffers !== null) { 879 parts.push(params.buffers.join(',')); 880 if (params.options !== null) { 881 parts.push(params.options.join(',')); 882 } 883 } 884 885 return WeeChatProtocol._formatCmd(params.id, cmdName, parts); 886 }; 887 888 /** 889 * Formats a sync command. 890 * 891 * @param params Parameters: 892 * id: command ID (optional) 893 * buffers: array of buffers to sync (optional) 894 * options: array of options (optional) 895 * @return Formatted sync command string 896 */ 897 WeeChatProtocol.formatSync = function (params) { 898 return WeeChatProtocol._formatSyncDesync('sync', params); 899 }; 900 901 /** 902 * Formats a desync command. 903 * 904 * @param params Parameters: 905 * id: command ID (optional) 906 * buffers: array of buffers to desync (optional) 907 * options: array of options (optional) 908 * @return Formatted desync command string 909 */ 910 WeeChatProtocol.formatDesync = function (params) { 911 return WeeChatProtocol._formatSyncDesync('desync', params); 912 }; 913 914 /** 915 * Formats a test command. 916 * 917 * @param params Parameters: 918 * id: command ID (optional) 919 * @return Formatted test command string 920 */ 921 WeeChatProtocol.formatTest = function (params) { 922 var defaultParams = { 923 id: null 924 }; 925 var parts = []; 926 927 params = WeeChatProtocol._mergeParams(defaultParams, params); 928 929 return WeeChatProtocol._formatCmd(params.id, 'test', parts); 930 }; 931 932 /** 933 * Formats a quit command. 934 * 935 * @return Formatted quit command string 936 */ 937 WeeChatProtocol.formatQuit = function () { 938 return WeeChatProtocol._formatCmd(null, 'quit', []); 939 }; 940 941 /** 942 * Formats a ping command. 943 * 944 * @param params Parameters: 945 * id: command ID (optional) 946 * args: array of custom arguments (optional) 947 * @return Formatted ping command string 948 */ 949 WeeChatProtocol.formatPing = function (params) { 950 var defaultParams = { 951 id: null, 952 args: null 953 }; 954 var parts = []; 955 956 params = WeeChatProtocol._mergeParams(defaultParams, params); 957 if (params.args !== null) { 958 parts.push(params.args.join(' ')); 959 } 960 961 return WeeChatProtocol._formatCmd(params.id, 'ping', parts); 962 }; 963 964 WeeChatProtocol.prototype = { 965 /** 966 * Warns that message parsing is not implemented for a 967 * specific type. 968 * 969 * @param type Message type to display 970 */ 971 _warnUnimplemented: function (type) { 972 console.log('Warning: ' + type + ' message parsing is not implemented'); 973 }, 974 975 /** 976 * Reads a 3-character message type token value from current 977 * set data. 978 * 979 * @return Type 980 */ 981 _getType: function () { 982 var t = this._getSlice(3); 983 984 if (!t) { 985 return null; 986 } 987 988 return WeeChatProtocol._uia2s(new Uint8Array(t)); 989 }, 990 991 /** 992 * Runs the appropriate read routine for the specified message type. 993 * 994 * @param type Message type 995 * @return Data value 996 */ 997 _runType: function (type) { 998 var cb = this._types[type]; 999 var boundCb = cb.bind(this); 1000 1001 return boundCb(); 1002 }, 1003 1004 /** 1005 * Reads a "number as a string" token value from current set data. 1006 * 1007 * @return Number as a string 1008 */ 1009 _getStrNumber: function () { 1010 var len = this._getByte(); 1011 var str = this._getSlice(len); 1012 1013 return WeeChatProtocol._uia2s(new Uint8Array(str)); 1014 }, 1015 1016 /** 1017 * Returns the passed object. 1018 * 1019 * @param obj Object 1020 * @return Passed object 1021 */ 1022 _strDirect: function (obj) { 1023 return obj; 1024 }, 1025 1026 /** 1027 * Calls toString() on the passed object and returns the value. 1028 * 1029 * @param obj Object to call toString() on 1030 * @return String value of object 1031 */ 1032 _strToString: function (obj) { 1033 return obj.toString(); 1034 }, 1035 1036 /** 1037 * Gets the string value of an object representing the message 1038 * value for a specified type. 1039 * 1040 * @param obj Object for which to get the string value 1041 * @param type Message type 1042 * @return String value of object 1043 */ 1044 _objToString: function (obj, type) { 1045 var cb = this._typesStr[type]; 1046 var boundCb = cb.bind(this); 1047 1048 return boundCb(obj); 1049 }, 1050 1051 /** 1052 * Reads an info token value from current set data. 1053 * 1054 * @return Info object 1055 */ 1056 _getInfo: function () { 1057 var info = {}; 1058 info.key = this._getString(); 1059 info.value = this._getString(); 1060 1061 return info; 1062 }, 1063 1064 /** 1065 * Reads an hdata token value from current set data. 1066 * 1067 * @return Hdata object 1068 */ 1069 _getHdata: function () { 1070 var self = this; 1071 var paths; 1072 var count; 1073 var objs = []; 1074 var hpath = this._getString(); 1075 1076 var keys = this._getString().split(','); 1077 paths = hpath.split('/'); 1078 count = this._getInt(); 1079 1080 keys = keys.map(function (key) { 1081 return key.split(':'); 1082 }); 1083 1084 function runType() { 1085 var tmp = {}; 1086 1087 tmp.pointers = paths.map(function (path) { 1088 return self._getPointer(); 1089 }); 1090 keys.forEach(function (key) { 1091 tmp[key[0]] = self._runType(key[1]); 1092 }); 1093 objs.push(tmp); 1094 } 1095 1096 for (var i = 0; i < count; i++) { 1097 runType(); 1098 } 1099 1100 return objs; 1101 }, 1102 1103 /** 1104 * Reads a pointer token value from current set data. 1105 * 1106 * @return Pointer value 1107 */ 1108 _getPointer: function () { 1109 return this._getStrNumber(); 1110 }, 1111 1112 /** 1113 * Reads a time token value from current set data. 1114 * 1115 * @return Time value (Date) 1116 */ 1117 _getTime: function () { 1118 var str = this._getStrNumber(); 1119 1120 return new Date(parseInt(str, 10) * 1000); 1121 }, 1122 1123 /** 1124 * Reads an integer token value from current set data. 1125 * 1126 * @return Integer value 1127 */ 1128 _getInt: function () { 1129 var parsedData = new Uint8Array(this._getSlice(4)); 1130 1131 return ( 1132 ((parsedData[0] & 0xff) << 24) | 1133 ((parsedData[1] & 0xff) << 16) | 1134 ((parsedData[2] & 0xff) << 8) | 1135 (parsedData[3] & 0xff) 1136 ); 1137 }, 1138 1139 /** 1140 * Reads a byte from current set data. 1141 * 1142 * @return Byte value (integer) 1143 */ 1144 _getByte: function () { 1145 var parsedData = new Uint8Array(this._getSlice(1)); 1146 1147 return parsedData[0]; 1148 }, 1149 1150 /** 1151 * Reads a character token value from current set data. 1152 * 1153 * @return Character (string) 1154 */ 1155 _getChar: function () { 1156 return this._getByte(); 1157 }, 1158 1159 /** 1160 * Reads a string token value from current set data. 1161 * 1162 * @return String value 1163 */ 1164 _getString: function () { 1165 var l = this._getInt(); 1166 1167 if (l > 0) { 1168 var s = this._getSlice(l); 1169 var parsedData = new Uint8Array(s); 1170 1171 return WeeChatProtocol._uia2s(parsedData); 1172 } 1173 1174 return ''; 1175 }, 1176 1177 /** 1178 * Reads a message header from current set data. 1179 * 1180 * @return Header object 1181 */ 1182 _getHeader: function () { 1183 var len = this._getInt(); 1184 var comp = this._getByte(); 1185 1186 return { 1187 length: len, 1188 compression: comp 1189 }; 1190 }, 1191 1192 /** 1193 * Reads a message header ID from current set data. 1194 * 1195 * @return Message ID (string) 1196 */ 1197 _getId: function () { 1198 return this._getString(); 1199 }, 1200 1201 /** 1202 * Reads an arbitrary object token from current set data. 1203 * 1204 * @return Object value 1205 */ 1206 _getObject: function () { 1207 var self = this; 1208 var type = this._getType(); 1209 1210 if (type) { 1211 return { 1212 type: type, 1213 content: self._runType(type) 1214 }; 1215 } 1216 }, 1217 1218 /** 1219 * Reads an hash table token from current set data. 1220 * 1221 * @return Hash table 1222 */ 1223 _getHashTable: function () { 1224 var self = this; 1225 var typeKeys, typeValues, count; 1226 var dict = {}; 1227 1228 typeKeys = this._getType(); 1229 typeValues = this._getType(); 1230 count = this._getInt(); 1231 1232 for (var i = 0; i < count; ++i) { 1233 var key = self._runType(typeKeys); 1234 var keyStr = self._objToString(key, typeKeys); 1235 var value = self._runType(typeValues); 1236 dict[keyStr] = value; 1237 } 1238 1239 return dict; 1240 }, 1241 1242 /** 1243 * Reads an array token from current set data. 1244 * 1245 * @return Array 1246 */ 1247 _getArray: function () { 1248 var self = this; 1249 var type; 1250 var count; 1251 var values; 1252 1253 type = this._getType(); 1254 count = this._getInt(); 1255 values = []; 1256 1257 for (var i = 0; i < count; i++) { 1258 values.push(self._runType(type)); 1259 } 1260 1261 return values; 1262 }, 1263 1264 /** 1265 * Reads an infolist object from the current set of data 1266 * 1267 * @return Array 1268 */ 1269 _getInfolist: function () { 1270 var self = this; 1271 var name; 1272 var count; 1273 var values; 1274 1275 name = this._getString(); 1276 count = this._getInt(); 1277 values = []; 1278 1279 for (var i = 0; i < count; i++) { 1280 var itemcount = self._getInt(); 1281 var litem = []; 1282 for (var j = 0; j < itemcount; j++) { 1283 var item = {}; 1284 item[self._getString()] = self._runType(self._getType()); 1285 litem.push(item); 1286 } 1287 values.push(litem); 1288 } 1289 1290 return values; 1291 }, 1292 1293 /** 1294 * Reads a specified number of bytes from current set data. 1295 * 1296 * @param length Number of bytes to read 1297 * @return Sliced array 1298 */ 1299 _getSlice: function (length) { 1300 if (this.dataAt + length > this._data.byteLength) { 1301 return null; 1302 } 1303 1304 var slice = this._data.slice(this._dataAt, this._dataAt + length); 1305 1306 this._dataAt += length; 1307 1308 return slice; 1309 }, 1310 1311 /** 1312 * Sets the current data. 1313 * 1314 * @param data Current data 1315 */ 1316 _setData: function (data) { 1317 this._data = data; 1318 }, 1319 1320 /** 1321 * Add the ID to the previously formatted command 1322 * 1323 * @param id Command ID 1324 * @param command previously formatted command 1325 */ 1326 setId: function (id, command) { 1327 return '(' + id + ') ' + command; 1328 }, 1329 1330 /** 1331 * Parses a WeeChat message. 1332 * 1333 * @param data Message data (ArrayBuffer) 1334 * @return Message value 1335 */ 1336 parse: function (data, optionsValues) { 1337 var self = this; 1338 1339 this._setData(data); 1340 this._dataAt = 0; 1341 1342 var header = this._getHeader(); 1343 1344 if (header.compression) { 1345 var raw = new Uint8Array(data, 5); // skip first five bytes (header, 4B size, 1B compression flag) 1346 var inflate = new Zlib.Inflate(raw); 1347 var plain = inflate.decompress(); 1348 this._setData(plain.buffer); 1349 this._dataAt = 0; // reset position in data, as the header is not part of the decompressed data 1350 } 1351 1352 var id = this._getId(); 1353 var objects = []; 1354 var object = this._getObject(); 1355 1356 while (object) { 1357 objects.push(object); 1358 object = self._getObject(); 1359 } 1360 var msg = { 1361 header: header, 1362 id: id, 1363 objects: objects 1364 }; 1365 1366 return msg; 1367 } 1368 };