├── .gitattributes ├── .gitignore ├── AirPacker.js ├── AirUnpacker.js ├── Client.js ├── CommConst.js ├── Log.js ├── README.md ├── SMServerAPI.js ├── SMServerWebsocket.js ├── conversionDatabase.js ├── conversionDatabase.json ├── encryption_test.js ├── index.js ├── message_packer.js ├── package-lock.json ├── package.json ├── settings.txt └── settingsManager.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | -------------------------------------------------------------------------------- /AirPacker.js: -------------------------------------------------------------------------------- 1 | const ByteBuffer = require('byte-buffer'); 2 | const EncryptionLib = require('./encryption_test.js'); 3 | const ConversionDatabase = require('./conversionDatabase.js'); 4 | const SMServerAPI = require("./SMServerAPI.js"); 5 | const CommConst = require('./CommConst.js'); 6 | const LogLib = require('./Log.js'); 7 | const crypto = require('crypto'); 8 | const fs = require('fs'); 9 | 10 | //TODO: Maybe convert this to use Uint8Array, because byte-buffer isn't installing on iOS 11 | 12 | function AirPacker(initial_buffer) { //Maybe later add bytes to pass into beginning? 13 | if (initial_buffer) { 14 | // this.bytebuffer = new ByteBuffer(Buffer.from(initial_buffer)); 15 | //TODO: This creates insanely long buffers (8000+ bytes) and I HAVE NO IDEA WHY 16 | this.bytebuffer = new ByteBuffer(); 17 | this.bytebuffer.append(initial_buffer.length); 18 | this.bytebuffer.write(initial_buffer); 19 | } else { 20 | this.bytebuffer = new ByteBuffer(); 21 | } 22 | 23 | 24 | 25 | this.writeInt = function(int_to_write) { 26 | this.bytebuffer.append(4); //4-byte int 27 | this.bytebuffer.writeUnsignedInt(int_to_write); //I think it's supposed to be unsigned? 28 | } 29 | 30 | this.packInt = this.writeInt; 31 | 32 | this.writeShort = function(short_to_write) { 33 | this.bytebuffer.append(2); //2-byte short 34 | this.bytebuffer.writeShort(short_to_write); //SHOULD THIS BE SIGNED?? 35 | } 36 | 37 | this.packShort = this.writeShort; 38 | 39 | this.writeLong = function(long_to_write) { //THE FOLLOWING IS UNTESTED 40 | // let b = new ArrayBuffer(8); //64bit Long ints are 8 bytes long 41 | // new DataView(b).setUint32(0, num); 42 | // return Array.from(new Uint8Array(b)); 43 | // var b = new Buffer(8); 44 | var b = Buffer.alloc(8); 45 | b.writeBigUInt64BE(BigInt(long_to_write), 0); 46 | this.bytebuffer.append(8); 47 | this.bytebuffer.write(b); 48 | } 49 | 50 | this.packLong = this.writeLong; 51 | 52 | this.writeArrayHeader = function(array_length) { 53 | this.writeInt(array_length); 54 | } 55 | 56 | this.packArrayHeader = this.writeArrayHeader; 57 | 58 | this.writeBoolean = function(bool_to_write) { 59 | this.bytebuffer.append(1); 60 | this.bytebuffer.writeByte(bool_to_write ? 1 : 0); 61 | } 62 | 63 | this.packBoolean = this.writeBoolean; 64 | 65 | this.writeVariableLengthBuffer = function(buffer_to_write) { 66 | this.writeInt(buffer_to_write.length); 67 | this.bytebuffer.append(buffer_to_write.length); 68 | this.bytebuffer.write(buffer_to_write); 69 | } 70 | 71 | this.packVariableLengthBuffer = this.writeVariableLengthBuffer; 72 | 73 | this.writeString = function(string_to_write) { 74 | var tmpbuf = Buffer.from(string_to_write, 'utf8'); 75 | this.writeInt(tmpbuf.length); 76 | this.bytebuffer.append(tmpbuf.length); 77 | this.bytebuffer.write(tmpbuf); 78 | } 79 | 80 | this.packString = this.writeString; 81 | 82 | this.writeNullableString = function(string_to_write) { 83 | if (string_to_write == null || string_to_write == "") { 84 | this.writeBoolean(false); 85 | } else { 86 | this.writeBoolean(true); 87 | this.writeString(string_to_write); 88 | } 89 | } 90 | this.packNullableString = this.writeNullableString; 91 | 92 | this.write = function(buffer_to_write) { 93 | this.bytebuffer.append(buffer_to_write.length); 94 | this.bytebuffer.write(buffer_to_write); 95 | } 96 | 97 | this.pack = this.write; 98 | 99 | this.writePayload = function(payload_to_write) { 100 | this.writeInt(payload_to_write.length); 101 | this.write(payload_to_write); 102 | }; 103 | 104 | this.packPayload = this.writePayload; 105 | 106 | this.writeNullablePayload = function(buffer_to_write) { 107 | if (buffer_to_write == null) { 108 | this.packBoolean(false); 109 | } else { 110 | this.packBoolean(true); 111 | this.writePayload(buffer_to_write); 112 | } 113 | } 114 | 115 | this.packNullablePayload = this.writeNullablePayload; 116 | 117 | this.getBuffer = function() { 118 | //return this.bytebuffer.buffer; 119 | return this.bytebuffer.raw; 120 | } 121 | 122 | this.writeHeader = function(encrypted) { 123 | //This assumes the rest of the data has already been encrypted if necessary 124 | var bytebufferLength = this.bytebuffer.length; 125 | 126 | this.bytebuffer.prepend(5); //Prepends 5 bytes 127 | this.bytebuffer.index = 0; //Sets the read point to the beginning 128 | this.bytebuffer.writeUnsignedInt(bytebufferLength); 129 | this.bytebuffer.writeByte(encrypted ? 1 : 0); 130 | } 131 | 132 | this.packHeader = this.writeHeader; 133 | 134 | this.encryptAndWriteHeader = function() { 135 | // console.log(JSON.stringify(this.bytebuffer.raw)); 136 | // console.log("Encrypting the following data and writing header:"); 137 | // ConversionDatabase.printUint8Array(this.bytebuffer.raw); 138 | return new Promise((resCb, rejCb) => { 139 | EncryptionLib.encrypt(Buffer.from(this.bytebuffer.raw)).then((data) => { 140 | var salt = data[0]; 141 | var iv = data[1]; 142 | var encrypted = data[2]; 143 | 144 | var tmppacker = new AirPacker(); 145 | 146 | tmppacker.write(salt); 147 | tmppacker.write(iv); 148 | tmppacker.write(encrypted); 149 | 150 | tmppacker.writeHeader(true); 151 | 152 | resCb(tmppacker.getBuffer()); 153 | }); 154 | }); 155 | } 156 | 157 | this.packMessageUpdateHeader = function(num_messages) { 158 | //packInt (header = nhtMessageUpdate = 200) 159 | this.packInt(CommConst.nhtMessageUpdate); //The message type is nhtMessageUpdate 160 | // console.log("Message type: 200 (nhtMessageUpdate)"); 161 | //packArrayHeader (length of the list of messages) 162 | this.packArrayHeader(num_messages); //Number of items in the list to return 163 | // console.log("Number of messages: "+num_messages); 164 | //Write the data for each object 165 | //Send the message 166 | } 167 | 168 | this.packConversationItem = function(itemType, serverID, guid, chatGuid, date) { 169 | //Item type is 0 for a message, 1 for a group action, and 2 for a chat renaming 170 | this.packInt(itemType); 171 | 172 | this.packLong(serverID); 173 | this.packString(guid); 174 | this.packString(chatGuid); 175 | } 176 | 177 | this.packAllMessagesFromSMServer = async function(messages) { 178 | // console.log("packing "+messages.length+" messages"); 179 | 180 | // console.log("Packing messages"); 181 | // console.log(messages); 182 | 183 | 184 | 185 | // var numMessages = 0; 186 | // for (var i = 0; i < messages.length; i++) { //This counts the messages. This is necessary since some messages returned by SMServer are tapbacks and aren't counted in the same way 187 | // let message = messages[i]; 188 | // let isEmpty = (message.text == "" && message.subject == "") || message.attachments; //Is only empty if attachments don't exist 189 | // isEmpty = false; 190 | // let isTapback = message.associated_message_guid !== ""; 191 | // let isRichLink = message.balloon_bundle_id == "com.apple.messages.URLBalloonProvider"; 192 | // //It looks like rich links still have text as the link, so we process them as usual (there's some extra data but we can ignore that) 193 | // let isDigitalTouch = message.balloon_bundle_id == "com.apple.messages.DigitalTouchBalloonProvider"; 194 | // if (!isEmpty && !isTapback && !isDigitalTouch) { 195 | // numMessages++; 196 | // } 197 | // } 198 | 199 | var numMessages = messages.length; //The messages are assumed to be tapback-stacked 200 | //TODO: What about messages that are empty? 201 | 202 | // console.log("Number of messages: "+numMessages); 203 | this.packMessageUpdateHeader(numMessages); 204 | 205 | 206 | for (var i = 0; i < messages.length; i++) { 207 | // let message = messages[i]; 208 | // // console.log(message.text); 209 | // let isEmpty = (message.text == "" && message.subject == "") && message.attachments; //Is only empty if attachments don't exist 210 | // isEmpty = false; 211 | // let isTapback = message.associated_message_guid !== ""; 212 | // let isRichLink = message.balloon_bundle_id == "com.apple.messages.URLBalloonProvider"; 213 | // //It looks like rich links still have text as the link, so we process them as usual (there's some extra data but we can ignore that) 214 | // let isDigitalTouch = message.balloon_bundle_id == "com.apple.messages.DigitalTouchBalloonProvider"; 215 | 216 | 217 | // if (!isEmpty && !isTapback && !isDigitalTouch) { 218 | // console.log("Actually packing data"); 219 | await this.packMessageDataFromSMServer(messages[i]); 220 | // } 221 | 222 | } 223 | } 224 | 225 | 226 | //TODO: Write methods that take in data from the SMServer and spit out AM-compatible data 227 | this.packMessageDataFromSMServer = async function(message) { //THE FOLLOWING IS UNTESTED 228 | let Log = new LogLib.Log("AirPacker.js","packMessageDataFromSMServer", 1); 229 | Log.p("Packing one message from SMServer"); 230 | //ConversationID will be converted to GUID later 231 | //TODO: Find out how to get all tapbacks for a message. Maybe get all messages after it and check which ones match? 232 | //TODO: GUIDs vs UUIDs can have conversion issues. If there are problems, CHECK THIS!! 233 | //TODO: Add a "Hey, X person sent a digital touch message but it can't be viewed from AirBridge" 234 | //TODO: Add logging here 235 | // console.log("Packing "+message.text); 236 | 237 | // message = { 238 | // subject: '', 239 | // is_from_me: true, 240 | // text: 'Sample group', 241 | // cache_has_attachments: false, 242 | // associated_message_type: 0, 243 | // date_read: 0, 244 | // service: 'iMessage', 245 | // associated_message_guid: '', 246 | // id: '', 247 | // item_type: 0, 248 | // group_action_type: 0, 249 | // date: 645656436981000000, 250 | // guid: '74BD893A-E4EA-4AF0-9701-94C6E5427F27', 251 | // conversation_id: 'chat16846957693768777', 252 | // ROWID: 13, 253 | // balloon_bundle_id: '', 254 | // tapbacks: [] 255 | // }; 256 | 257 | /* 258 | Message sent to me: 259 | { 260 | group_action_type: 0, 261 | balloon_bundle_id: '', 262 | date_read: 645402658616994000, 263 | associated_message_guid: '', 264 | item_type: 0, 265 | cache_has_attachments: false, 266 | associated_message_type: 0, 267 | text: 'Test test', 268 | id: 'name@example.com', 269 | guid: '1C2D60C6-379D-4EEB-9B0F-B1C75BC6ABDF', 270 | service: 'iMessage', 271 | is_from_me: false, 272 | subject: '', 273 | ROWID: 2, 274 | date: 645402655814933000 275 | } 276 | 277 | 278 | { 279 | guid: 'B3A3116C-407D-4A11-9105-DFCF9D1B9AEE', 280 | group_action_type: 0, 281 | balloon_bundle_id: '', 282 | text: 'Comment here!!', 283 | service: 'iMessage', 284 | associated_message_guid: '', 285 | item_type: 0, 286 | cache_has_attachments: true, 287 | date_read: 0, 288 | is_from_me: true, 289 | date: 645658188233000100, 290 | id: 'name@example.com', 291 | ROWID: 21, 292 | subject: '', 293 | associated_message_type: 0, 294 | attachments: [ 295 | { 296 | filename: 'Attachments/fc/12/F74A4A4B-5BF8-49BA-86B1-81FAAF71294C/tmp.gif', 297 | mime_type: 'image/gif' 298 | } 299 | ] 300 | } 301 | 302 | */ 303 | Log.p("Packing int (item type): 0 (message)"); 304 | this.packInt(0); //itemType is a message, so it's 0 305 | // console.log("itemType: 0"); 306 | // Long: Server ID (MAYBE, SEEMS STRANGE TO SEND THE SERVER ID SO MANY TIMES) 307 | Log.p("Packing int (Server ID/ROWID): "+message.ROWID); 308 | this.packLong(message.ROWID); //CHANGE: Server ID: WHAT IS THIS 309 | // console.log("Server ID: "+message.ROWID); 310 | //This is the number of the message, ascending order chronologically. 311 | //TODO: Figure out how to create a database of message IDs to server IDs. Maybe get all the messages from the conversation and create a message GUID-to-server-ID table? 312 | // This could also be a ROWID!! 313 | //Does this need to be unique across conversations? 314 | // String: GUID of message (I THINK) 315 | Log.p("Packing string (message GUID): "+message.guid.toUpperCase()); 316 | this.packString(message.guid.toUpperCase()); 317 | // console.log("Message GUID: "+message.guid.toLowerCase()); 318 | // String: GUID of conversation (PRETTY SURE) 319 | Log.p("Packing string (conversation ID): "+message.conversation_id); 320 | this.packString(message.conversation_id); 321 | // console.log("packed convo id"); 322 | 323 | // console.log("Conversation ID: "+message.conversation_id); 324 | //TODO: Change this to the regular chat ID, as it doesn't have to be in GUID format 325 | //On a Mac, each conversation has a GUID. On SMServer, each conversation has an ID but it isn't in 326 | //GUID format. Therefore, ConversionDatabase keeps track of which SMServer IDs match with which GUIDs, 327 | //and it (randomly) generates missing GUIDs as needed. 328 | //Update: Apparently AirMessage doesn't care if it's a GUID or not--it just has to be a unique string 329 | 330 | //Maybe we keep a conversion table for Conversation IDs --> generated GUIDs? (i.e. create GUIDs if they're not known) 331 | // Long: Date of message (I THINK IN UNIX TIME) 332 | var timestamp = ConversionDatabase.convertAppleDateToUnixTimestamp(message.date); 333 | //AirMessage expects the timestamp in milliseconds (I think) 334 | Log.p("Packing long (message timestamp): "+timestamp); 335 | this.packLong(timestamp); 336 | // console.log("Unix timestamp: "+timestamp+" ("+new Date(timestamp * 1000)+")") 337 | // THE FOLLOWING ASSUMES A MESSAGE INFO (SUBCLASS OF CONVERSATIONITEM) 338 | // Writes the information in writeObject for superclass ConversationItem() (i.e. it adds the above data under "THE FOLLOWING ASSUMES A CONVERSATION ITEM" to the beginning) 339 | // Nullable string: Message text 340 | 341 | Log.p("Packing nullable string (message text): "+message.text); 342 | this.writeNullableString(message.text); 343 | // console.log("Text: "+message.text); 344 | // Nullable string: Subject line text 345 | // console.log("text written."); 346 | // console.log(this.getBuffer()); 347 | Log.p("Packing nullable string (message subject): "+message.subject); 348 | this.writeNullableString(message.subject); 349 | // console.log("Subject: "+message.subject); 350 | // Nullable string: Sender (NOT SURE WHAT FORMAT THIS IS IN--IS IT A PHONE NUMBER OR EMAIL OR ID OR WHATEVER) 351 | // Does it even matter? Not sure if the client does any parsing or just displays it 352 | // console.log("packing sender"); 353 | // console.log("\nHERE IS THE DATA SO FAR"); 354 | // ConversionDatabase.printUint8Array(this.getBuffer()); 355 | if (message.is_from_me) { 356 | Log.p("Packing nullable string (sender): "+null); 357 | this.writeNullableString(null); 358 | // console.log("Sender: Me"); 359 | } else { 360 | 361 | Log.p("Packing nullable string (sender): "+message.id); 362 | this.writeNullableString(message.id); //Sender. WHAT FORMAT SHOULD THIS BE IN??? Any format is ok, just be consistent 363 | // console.log("Sender: "+message.id); 364 | } 365 | //WHAT ABOUT MESSAGES SENT FROM ME AS THOSE HAVE NO SENDER LISTED 366 | // Array header (just an int): Length of the list of attachments 367 | // this.writeArrayHeader(0); //0 attachments for now. FIX THIS BEFORE RELEASE 368 | // console.log("Number of attachments: 0 (hardwired for now)"); 369 | var attachments = message.attachments || []; 370 | 371 | attachments = this.filterAttachments(attachments); 372 | // var attachments = []; 373 | // attachments = []; 374 | // attachments = [{ 375 | // filename: 'Attachments/fc/12/F74A4A4B-5BF8-49BA-86B1-81FAAF71294C/tmp.gif', 376 | // mime_type: 'image/jpeg' 377 | // }]; 378 | 379 | //TODO: Why does message syncing with AttachmentInfo have issues? 380 | //Ohh, maybe it's because there are no attachments next?? 381 | 382 | //TODO: Maybe check for read receipts? idk 383 | Log.p("Packing array header (number of attachments): "+attachments.length); 384 | this.writeArrayHeader(attachments.length); 385 | // console.log("\n\n\n\n================================\n\n\n") 386 | for (var i = 0; i < attachments.length; i++) { 387 | await this.packAttachmentInfo(attachments[i]); 388 | 389 | // 390 | } 391 | // For each attachment, writeObject() for the AttachmentInfo item 392 | // TODO: FIND OUT WHAT THIS IS 393 | // Pack the attachment GUID as string 394 | // Pack the attachment name as string (filename I presume) 395 | // Pack the attachment type as nullable string (is this a MIME type?) 396 | // Pack the size as a long (in bytes I assume) 397 | // Pack the checksum as a nullable payload 398 | // Pack the sort (what is this?) as a long 399 | Log.p("Packing array header (number of stickers): "+0); 400 | this.writeArrayHeader(0); //0 stickers for now. FIX THIS BEFORE RELEASE 401 | // console.log("Number of stickers: 0 (hardwired for now)"); 402 | // var stickers = message.attachments || []; 403 | // this.writeArrayHeader(attachments.length); 404 | // for (var i = 0; i < attachments.length; i++) { 405 | // //TODO: IMPLEMENT WRITEOBJECT() FOR STICKERMODIFIERINFO 406 | // } 407 | // For each sticker, writeObject() for the StickerModifierInfo item 408 | // TODO: FIND OUT WHAT THIS IS 409 | // [Pack the sueprclass ModifierInfo] 410 | // Pack an int: Item type (for StickerModifierInfo this is 1) 411 | // Pack a string: message (Is this the GUID?) 412 | // Pack int: messageIndex (WHAT IS THIS? ROWID?) 413 | // Pack string: fileGUID 414 | // Pack nullable string: Sender (null if me) 415 | // Pack long: Date (unix millis) 416 | // Pack payload: Data (Sticker file data I presume?) 417 | // Pack string: Type (MIME?) 418 | // console.log("packing tapbacks: "+message.tapbacks.length); 419 | 420 | 421 | Log.p("Packing array header (number of tapbacks): "+message.tapbacks.length); 422 | this.writeArrayHeader(message.tapbacks.length); //0 tapbacks for now. FIX THIS BEFORE RELEASE 423 | for (var i = 0; i < message.tapbacks.length; i++) { 424 | this.packTapback(message.tapbacks[i]); 425 | // this.writeBoolean(true); //isAddition = true 426 | // this.writeInt(0); //Tapback type: heart (0) 427 | } 428 | // console.log("Number of tapbacks: 0 (hardwired for now)"); 429 | // var tapbacks = message.attachments || []; 430 | // this.writeArrayHeader(attachments.length); 431 | // for (var i = 0; i < attachments.length; i++) { 432 | // //TODO: IMPLEMENT WRITEOBJECT() FOR TAPBACKMODIFIERINFO 433 | // } 434 | // For each tapback, writeObject() for the TapbackModifierInfo item 435 | // TODO: FIND OUT WHAT THIS IS 436 | // [Pack the sueprclass ModifierInfo] 437 | // Pack an int: Item type (for TapbackModifierInfo this is 2) 438 | // Pack a string: message (Is this the GUID?) 439 | // Pack int: messageIndex 440 | // Pack mullable string: Sender (null if me) 441 | // Pack boolean: isAddition (if the tapback was added or removed) 442 | // Pack int: Tapback type (DOUBLE CHECK THE NUMBERS) 443 | // Nullable string: sendEffect WHAT IS THIS 444 | Log.p("Packing nullable string (message effect): "+null); 445 | this.writeNullableString(null); //Message effects aren't supported by SMServer 446 | // console.log("Message effects: null (it doesn't look like these are supported by SMServer)"); 447 | // Int: stateCode WHAT IS THIS 448 | //State codes: 449 | // Idle: 0 450 | // Sent: 1, 451 | // Delivered: 2, 452 | // Read: 3 453 | //TODO: FIGURE OUT IF SMSERVER TELLS US IF IT IS READ OR DELIVERED OR NOT 454 | console.log(message.date_read); 455 | if (message.date_read == 0) { 456 | Log.p("Packing int (message status): 1 (sent)"); 457 | this.writeInt(1); //Sent 458 | // console.log("Message status: \"delivered\""); 459 | } else { 460 | Log.p("Packing int (message status): 3 (read)"); 461 | this.writeInt(3); //Read 462 | // console.log("Message status: read"); 463 | } 464 | 465 | // Int: errorCode WHAT IS THIS 466 | 467 | //Error codes: 468 | // OK: 0 469 | // Unknown error code: 1, 470 | // Network error: 2, 471 | // Not registered with iMessage: 3 472 | Log.p("Packing int (error code): 0"); 473 | this.writeInt(0); 474 | // console.log("Error code: 0 (no errors, hardwired)"); 475 | 476 | 477 | // console.log(this.getBuffer()); 478 | // Long: dateRead: (unix I'm assuming) timestamp the message was read. Is it 0 if it isn't read?? 479 | var date_read = ConversionDatabase.convertAppleDateToUnixTimestamp(message.date_read); 480 | Log.p("Packing long (date read): "+date_read); 481 | this.writeLong(date_read); 482 | 483 | } 484 | 485 | this.packTapback = function(tapback_message) { 486 | var Log = new LogLib.Log("AirPacker.js","packTapback",2); 487 | //TODO: Add error handling and make this thing return a buffer so we 488 | //can call this.write() if it succeeded 489 | Log.p("Packing tapback"); 490 | // { 491 | // cache_has_attachments: false, 492 | // associated_message_guid: 'p:0/89A7BFE0-D485-41D9-9322-A988BF0CB837', 493 | // date_read: 0, 494 | // item_type: 0, 495 | // ROWID: 212, 496 | // is_from_me: true, 497 | // id: 'name@example.com', 498 | // date: 647399835374000100, 499 | // associated_message_type: 2000, 500 | // balloon_bundle_id: '', 501 | // guid: '005D9ACD-1B0A-4AE6-A7F7-C2BCBC210531', 502 | // service: 'iMessage', 503 | // text: 'Loved “Message here”', 504 | // subject: '', 505 | // group_action_type: 0, 506 | // conversation_id: 'name@example.com' 507 | // } 508 | 509 | 510 | //TODO: Implement try/catch for this block 511 | //TODO: Make sure data is in correct format--if it isn't, skip 512 | // console.log("Found a tapback: "+message.tapbacks[i].associated_message_type); 513 | Log.p("Packing int (item type): 2"); 514 | this.packInt(2); //Item type is TapbackModifierInfo 515 | // this.packString(message.tapbacks[i].text); //Message (WHAT IS THIS?) 516 | // this.packString(message.tapbacks[i].text); 517 | // this.packString("test"); 518 | Log.p("Packing string (associated message GUID): "+tapback_message.associated_message_guid.split("/")[1]); 519 | this.packString(tapback_message.associated_message_guid.split("/")[1]); //This is confirmed the GUID 520 | //TODO: Add AirMessage network protocol docs 521 | 522 | //associated_message_guid: 'p:0/688FB450-C715-4914-9D2F-A73F6FDB7BE7' 523 | // Log.p("Packing string (message index): "+tapback_message.associated_message_guid.match(/^p\:(\d+)/)[1]); 524 | var messageIndex = Number(tapback_message.associated_message_guid.match(/^p\:(\d+)/)[1]); //Usually 0, but 1, 2, etc for attachments 525 | //Slices out the message index from the associated message GUID 526 | //i.e. 'p:0/688FB450-C715-4914-9D2F-A73F6FDB7BE7' would become 0, and 527 | // 'p:1/688FB450-C715-4914-9D2F-A73F6FDB7BE7' would become 1 528 | 529 | // this.packInt(message.ROWID); //Is this the messageIndex?? 530 | Log.p("Packing string (message index): "+messageIndex); 531 | this.packInt(messageIndex); //messageIndex: What is this??? 532 | //0 for the message, a higher number for attachments 533 | //Does the Mac return every tapback ever? Is this why this is needed? 534 | if (tapback_message.is_from_me) { 535 | Log.p("Packing nullable string (sender): "+null); 536 | this.writeNullableString(null); 537 | } else { 538 | Log.p("Packing nullable string (sender): "+tapback_message.id); 539 | this.writeNullableString(tapback_message.id); 540 | } 541 | // this.writeNullableString(null); //Sender: Me! 542 | 543 | 544 | if (tapback_message.associated_message_type >= 3000 && tapback_message.associated_message_type <= 3005) { 545 | Log.p("Packing boolean (is tapback an addition): "+false); 546 | this.writeBoolean(false); //isAddition = false, because the tapback was removed. 547 | Log.p("Packing int (tapback type): "+(tapback_message.associated_message_type - 3000)); 548 | this.writeInt(tapback_message.associated_message_type - 3000); 549 | //The 200x/300x parts are not sent as the int--only the tapback type (0-5) 550 | } else { 551 | Log.p("Packing boolean (is tapback an addition): "+true); 552 | this.writeBoolean(true); 553 | Log.p("Packing int (tapback type): "+(tapback_message.associated_message_type - 2000)); 554 | this.writeInt(tapback_message.associated_message_type - 2000); 555 | } 556 | } 557 | this.packActivityStatus = function(message) { 558 | console.log(message); 559 | var Log = new LogLib.Log("AirPacker.js", "packActivityStatus", 2); 560 | //Write data for modifierinfo: 561 | //Int: item type (0) 562 | 563 | Log.p("Packing int (item type): 0 (activityStatusModifierInfo)") 564 | this.packInt(0); //Item type: activityStatusModifierInfo 565 | //String: Message ID 566 | Log.p("Packing string (message GUID): "+message.guid); 567 | this.packString(message.guid); 568 | //Pack int: State 569 | 570 | Log.p("Packing int (activity state): 3 (read)"); //TODO: Does this need to be more abstract--i.e. is packActivityStatus being called for more than just read receipts? 571 | this.packInt(3); 572 | //Pack long: Date read 573 | var date_read = ConversionDatabase.convertAppleDateToUnixTimestamp(message.date_read); 574 | Log.p("Packing long (date read): "+date_read); 575 | this.packLong(date_read); 576 | 577 | // Log.p("Packing long (date read): "+) 578 | } 579 | 580 | this.filterAttachments = function(attachments) { 581 | var filtered = []; 582 | for (var i = 0; i < attachments.length; i++) { 583 | if (attachments[i].filename.endsWith("unknown") || attachments[i].filename.endsWith(".pluginPayloadAttachment")) { 584 | //Don't add it to the filtered list 585 | } else { 586 | filtered.push(attachments[i]); 587 | } 588 | } 589 | return filtered; 590 | } 591 | 592 | this.packAttachmentInfo = async function(attachment_info) { 593 | //TODO: Don't pack pluginPayloadAttachment!!!!! 594 | let Log = new LogLib.Log("AirPacker.js","packAttachmentInfo", 2); 595 | // //TODO: IMPLEMENT WRITEOBJECT() FOR ATTACHMENTINFO 596 | // //String: GUID of attachment 597 | var localpath = await SMServerAPI.downloadAttachmentIfNecessary(attachment_info); 598 | Log.p("Packing attachment "+localpath); 599 | var promiseStats = function(filename) { 600 | return new Promise((resCb, rejCb) => { 601 | fs.stat(filename, (err, stats) => { 602 | if (err) { 603 | rejCb(err); 604 | } else { 605 | resCb(stats); 606 | } 607 | }); 608 | }); 609 | } 610 | Log.p("Attachment downloaded to "+localpath); 611 | 612 | var stats = await promiseStats(localpath); //TODO: Don't use sync, as it blocks the process! 613 | var fileLength = stats.size; 614 | 615 | // console.log(attachments[i].filename); 616 | // this.writeString(attachments[i].filename); //The filename, works as an ID 617 | // console.log("5a0fc282-d5f4-4de7-9179-0a268a6ad441"); 618 | // this.writeString("5a0fc282-d5f4-4de7-9179-0a268a6ad441"); 619 | Log.p("Packing string (attachment ID): "+attachment_info.filename); 620 | this.writeString(attachment_info.filename); 621 | 622 | //TODO: Should this be a GUID? 623 | // //String: Name of attachment 624 | Log.p("Packing string (attachment name): "+attachment_info.filename.match(/\/([^\/]+)$/)[1]); 625 | this.writeString(attachment_info.filename.match(/\/([^\/]+)$/)[1]); //Parses out the filename 626 | 627 | // //Nullable string: Type of attachment (WHAT IS THIS) 628 | Log.p("Packing nullable string (type): "+attachment_info.mime_type); 629 | this.writeNullableString(attachment_info.mime_type); //Assuming this is a mime type 630 | //When is type null? 631 | 632 | // //Long: Attachment size (In bytes I assume) 633 | Log.p("Packing long (file size): "+fileLength); 634 | this.writeLong(fileLength); 635 | // //Nullable payload: Checksum (I'm pretty sure this is hashAlgorithm in CommConst, which is MD5) 636 | var promiseMd5 = function(filepath) { 637 | return new Promise((resCb, rejCb) => { 638 | var md5sum = crypto.createHash('md5'); 639 | var s = fs.ReadStream(filepath); 640 | s.on('data', function(d) { 641 | md5sum.update(d); 642 | }); 643 | s.on('end', function() { 644 | var d = md5sum.digest(); 645 | resCb(d); 646 | }); 647 | }); 648 | } 649 | var md5 = await promiseMd5(localpath); 650 | Log.p("Packing nullable payload (hash): "+md5.toString('hex')); 651 | this.writeNullablePayload(md5); 652 | //Not a string, it's a payload 653 | 654 | // //Long: sort (WHAT IS THIS) 655 | //ASK ABOUT THIS 656 | // this.writeLong(message.ROWID); 657 | Log.p("Packing long (sort): "+1); 658 | this.writeLong(1); //sort (???) Is this the message index? 659 | } 660 | 661 | //TODO: Ask SMServer to deal with send errors 662 | 663 | this.packConversationUpdateHeader = function(num_conversations) { 664 | this.packInt(206); //Message type: nhtConversationUpdate 665 | this.packInt(num_conversations); //Number of conversations 666 | } 667 | 668 | this.packConversationDataFromSMServer = function(conversations) { 669 | this.packConversationUpdateHeader(conversations.length); 670 | for (var i = 0; i < conversations.length; i++) { 671 | this.packOneConversationFromSMServer(conversations[i]); 672 | } 673 | } 674 | 675 | this.packOneConversationFromSMServer = function(conversation) { //Packs a ConversationItem 676 | /* 677 | { 678 | chat_identifier: 'name@example.com', 679 | relative_time: '18:00', 680 | has_unread: false, 681 | display_name: 'Me Lastname', 682 | pinned: false, 683 | is_group: false, 684 | time_marker: 646275652160000100, 685 | addresses: 'name@example.com', 686 | latest_text: 'Reply', 687 | 688 | --members: ['firstlast@example.com'] //NOT IMPLEMENTED YET, POSSIBLY IN THE FUTURE BY SMSERVER 689 | } 690 | */ 691 | this.writeString(conversation.chat_identifier); //GUID, but can really be whatever ID you want as long as it's consistent 692 | this.writeBoolean(true); //Availability. WHEN SHOULD THIS BE FALSE??? 693 | // if (conversation.display_name == conversation.chat_identifier) { 694 | this.writeString("iMessage"); //Service. THIS VALUE IS ASSUMED 695 | if (conversation.chat_identifier.startsWith("chat")) { //Only group chats start with "chat" as in "chat12345678909876" 696 | this.writeNullableString(conversation.display_name); //Packs the group name 697 | } else { 698 | this.writeNullableString(conversation.display_name); //There is no group name if the title matches the chat ID. 699 | //TODO: CHANGE THIS BECAUSE THE DISPLAY NAME CAN EQUAL THE CONTACT NAME. 700 | //MAYBE DO SOME CONTACT CHECKING? 701 | } 702 | this.writeInt(1); //Number of members TODO: FIX THIS 703 | this.writeString(conversation.chat_identifier); //Member #1 704 | 705 | } //TODO: Skip pluginPayloadAttachment in packMessageDataFromSMServer 706 | 707 | this.packOneLiteConversationFromSMServer = async function(conversation) { 708 | var lastMessage = await SMServerAPI.getActualLastMessageFromConversation(conversation.chat_identifier); 709 | 710 | this.writeString(conversation.chat_identifier); //GUID, but can really be whatever ID you want as long as it's consistent 711 | this.writeString("iMessage"); //Service. THIS VALUE IS ASSUMED 712 | if (conversation.chat_identifier.startsWith("chat")) { //Only group chats start with "chat" as in "chat12345678909876" 713 | this.writeNullableString(conversation.display_name); //Packs the group name 714 | } else { 715 | this.writeNullableString(null); //There is no group name if the title matches the chat ID. This is because 716 | //single-person chat names use names from the user's contact list 717 | //TODO: CHANGE THIS BECAUSE THE DISPLAY NAME CAN EQUAL THE CONTACT NAME. 718 | //MAYBE DO SOME CONTACT CHECKING? 719 | } 720 | this.writeArrayHeader(1); //Number of members TODO: FIX THIS 721 | this.writeString(conversation.chat_identifier); //Member #1 722 | this.writeLong(ConversionDatabase.convertAppleDateToUnixTimestamp(conversation.time_market)) //Long: preview date 723 | //Nullable string: Preview sender 724 | if (lastMessage.is_from_me) { 725 | this.packNullableString(null); 726 | } else { 727 | this.packNullableString(conversation.chat_identifier); 728 | } 729 | //Nullable string: Preview text 730 | this.packNullableString(conversation.latest_text); 731 | //Nullable string: Preview send style (Is this a message effect??) 732 | this.packNullableString(null); //THIS VALUE IS ASSUMED 733 | this.packArrayHeader(0); //TODO: FIX THIS AND MAKE IT USE REAL ATTACHMENTS 734 | //If there are attachments, 735 | // Pack array header: num of attachments 736 | // Pack string: Attachment (Is this the GUID?) 737 | //Otherwise, 738 | // Pack array header: 0 739 | } 740 | 741 | this.packGroupActionInfo = function() { //NEEDS PARAMETERS 742 | //serverId, guid, chatGuid, date, agent, other, groupActionType 743 | this.packInt(0); //itemType is a message, so it's 0 744 | // Long: Server ID (MAYBE, SEEMS STRANGE TO SEND THE SERVER ID SO MANY TIMES) 745 | this.packLong(123456); //CHANGE: Server ID: WHAT IS THIS 746 | // String: GUID of message (I THINK) 747 | this.packString(message.guid.toLowerCase()); 748 | // String: GUID of conversation (PRETTY SURE) 749 | this.packString("7305c786-46c6-43c2-9496-721d70e838e2"); //CHANGE: Conversation GUID (I THINK) 750 | //Maybe we keep a conversion table for Conversation IDs --> generated GUIDs? (i.e. create GUIDs if they're not known) 751 | // Long: Date of message (I THINK IN UNIX TIME) 752 | this.packLong(date); 753 | //TODO: Merge the above into its own function? 754 | 755 | 756 | 757 | } 758 | 759 | //TODO: Pack attachmentInfo and pack attachment!! 760 | 761 | 762 | } 763 | //TODO: Handle group join/leave 764 | 765 | module.exports = AirPacker; 766 | -------------------------------------------------------------------------------- /AirUnpacker.js: -------------------------------------------------------------------------------- 1 | const EncryptionLib = require('./encryption_test.js'); 2 | function AirUnpacker(inbuffer) { 3 | //Turns a buffer into something like a readable stream. 4 | 5 | this.buffer = inbuffer; 6 | this.readerIndex = 0; 7 | //readVariableLengthData (reads length and then the data) 8 | this.getBuffer = function() { 9 | return this.buffer; 10 | } 11 | this.setReaderIndex = function(newIndex) { 12 | this.readerIndex = newIndex; 13 | } 14 | this.readInt = function() { 15 | var readedInt = this.buffer.readUInt32BE(this.readerIndex); 16 | this.readerIndex += 4; 17 | return readedInt; 18 | } 19 | this.unpackInt = this.readInt; 20 | this.readArrayHeader = this.readInt; 21 | this.unpackArrayHeader = this.readArrayHeader; 22 | this.readLong = function() { //THE FOLLOWING IS UNTESTED 23 | var readedLong = this.buffer.readBigUInt64BE(this.readerIndex); 24 | this.readerIndex += 8; 25 | return Number(readedLong); 26 | } 27 | this.unpackLong = this.readLong; 28 | this.readShort = function() { //THE FOLLOWING IS UNTESTED 29 | var readedShort = this.buffer.readUInt16BE(this.readerIndex); 30 | this.readerIndex += 2; 31 | return readedShort; 32 | } 33 | this.unpackShort = this.readShort; 34 | //TODO: ADD READ BOOLEAN 35 | this.readBoolean = function() { 36 | // var readedBool = this.buffer.slice(readerIndex, readerIndex + 1); 37 | // readerIndex += 1; 38 | var readedBoolAsInt = this.buffer.readInt8(this.readerIndex); 39 | //Booleans are stored in a whole byte. We pretend it's an integer and check if it's 1 or 0 40 | this.readerIndex += 1; 41 | return (readedBoolAsInt == 1) ? true : false; //Returns true if the int is 1, otherwise returns false. 42 | } 43 | this.unpackBoolean = this.readBoolean; 44 | this.readUTF8StringArray = function() { //THE FOLLOWING IS UNTESTED 45 | var arrayLength = this.readInt(); 46 | var finalarr = []; 47 | for (var i = 0; i < arrayLength; i++) { 48 | var item = this.readVariableLengthUTF8String(); 49 | finalarr.push(item); 50 | } 51 | return finalarr; 52 | } 53 | this.unpackUTF8StringArray = this.readUTF8StringArray; 54 | this.readUtf8String = function(length) { 55 | var outputstr = this.buffer.toString('utf8',this.readerIndex, this.readerIndex + length); 56 | this.readerIndex += length; 57 | return outputstr; 58 | } 59 | this.unpackUtf8String = this.readUtf8String; 60 | //TODO: Sort out the UTF8 string madness (Utf8 vs UTF8 vs just string) 61 | this.readVariableLengthUTF8String = function() { 62 | //Assumes there's an int length and the data immediately after 63 | var length = this.readInt(); 64 | return this.readUtf8String(length); 65 | } 66 | this.unpackVariableLengthUTF8String = this.readVariableLengthUTF8String; 67 | this.readString = this.readVariableLengthUTF8String; 68 | this.unpackString = this.readVariableLengthUTF8String; 69 | this.readBytes = function(length) { 70 | var outputBuffer = this.buffer.slice(this.readerIndex, this.readerIndex + length); 71 | this.readerIndex += length; 72 | return outputBuffer; 73 | }; 74 | this.unpackBytes = this.readBytes; 75 | this.readPayload = function() { 76 | var length = this.readInt(); 77 | return this.readBytes(length); 78 | }; 79 | this.unpackPayload = this.readPayload; 80 | this.readBytesToEnd = function() { //THE FOLLOWING IS UNTESTED 81 | var outputBuffer = this.buffer.slice(this.readerIndex, this.buffer.length); 82 | this.readerIndex = this.buffer.length; 83 | return outputBuffer; 84 | } 85 | this.unpackBytesToEnd = this.readBytesToEnd; 86 | this.readVariableLengthData = function() { //Change this to readVariableLengthBuffer? 87 | var length = this.readInt(); 88 | return this.readBytes(length); 89 | } 90 | this.unpackVariableLengthData = this.readVariableLengthData; 91 | this.decryptRestOfData = async function() { 92 | var salt = this.readBytes(EncryptionLib.SALT_LENGTH); 93 | var iv = this.readBytes(EncryptionLib.IV_LENGTH); 94 | var encrypted = this.readBytesToEnd(); 95 | var decryptedData = await EncryptionLib.decryptWithSaltIVAndData(salt, iv, encrypted); 96 | return Buffer.from(decryptedData); 97 | } 98 | //TODO: Function to read the rest of the data? 99 | //TODO: Maybe build decryption into AirUnpacker? 100 | } 101 | //TODO: When packing a conversation and including the latest_text, make sure to deal with it if it's null! 102 | // Maybe add a function that auto-puts something like "Unknown" if the value is null 103 | module.exports = AirUnpacker; 104 | -------------------------------------------------------------------------------- /CommConst.js: -------------------------------------------------------------------------------- 1 | exports.mmCommunicationsVersion = 5; 2 | exports.mmCommunicationsSubVersion = 3; 3 | 4 | //NHT - Net header type 5 | exports.nhtClose = 0; 6 | exports.nhtPing = 1; 7 | exports.nhtPong = 2; 8 | 9 | exports.nhtInformation = 100; 10 | exports.nhtAuthentication = 101; 11 | 12 | exports.nhtMessageUpdate = 200; 13 | exports.nhtTimeRetrieval = 201; 14 | exports.nhtIDRetrieval = 202; 15 | exports.nhtMassRetrieval = 203; 16 | exports.nhtMassRetrievalFile = 204; 17 | exports.nhtMassRetrievalFinish = 205; 18 | exports.nhtConversationUpdate = 206; 19 | exports.nhtModifierUpdate = 207; 20 | exports.nhtAttachmentReq = 208; 21 | exports.nhtAttachmentReqConfirm = 209; 22 | exports.nhtAttachmentReqFail = 210; 23 | exports.nhtIDUpdate = 211; 24 | 25 | exports.nhtLiteConversationRetrieval = 300; 26 | exports.nhtLiteThreadRetrieval = 301; 27 | 28 | exports.nhtSendResult = 400; 29 | exports.nhtSendTextExisting = 401; 30 | exports.nhtSendTextNew = 402; 31 | exports.nhtSendFileExisting = 403; 32 | exports.nhtSendFileNew = 404; 33 | exports.nhtCreateChat = 405; 34 | 35 | exports.hashAlgorithm = "MD5"; 36 | 37 | //NST - Net subtype 38 | exports.nstAuthenticationOK = 0; 39 | exports.nstAuthenticationUnauthorized = 1; 40 | exports.nstAuthenticationBadRequest = 2; 41 | 42 | exports.nstSendResultOK = 0; 43 | exports.nstSendResultScriptError = 1; //Some unknown AppleScript error 44 | exports.nstSendResultBadRequest = 2; //Invalid data received 45 | exports.nstSendResultUnauthorized = 3; //System rejected request to send message 46 | exports.nstSendResultNoConversation = 4; //A valid conversation wasn't found 47 | exports.nstSendResultRequestTimeout = 5; //File data blocks stopped being received 48 | 49 | exports.nstAttachmentReqNotFound = 1; //File GUID not found 50 | exports.nstAttachmentReqNotSaved = 2; //File (on disk) not found 51 | exports.nstAttachmentReqUnreadable = 3; //No access to file 52 | exports.nstAttachmentReqIO = 4; //IO error 53 | 54 | exports.nstCreateChatOK = 0; 55 | exports.nstCreateChatScriptError = 1; //Some unknown AppleScript error 56 | exports.nstCreateChatBadRequest = 2; //Invalid data received 57 | exports.nstCreateChatUnauthorized = 3; //System rejected request to send message 58 | 59 | //Timeouts 60 | exports.handshakeTimeout = 10 * 1000; //10 seconds 61 | exports.pingTimeout = 30 * 1000; //30 seconds 62 | exports.keepAliveMillis = 30 * 60 * 1000; //30 minutes 63 | 64 | exports.maxPacketAllocation = 50 * 1024 * 1024; //50 MB 65 | 66 | exports.transmissionCheckLength = 32; 67 | 68 | //TODO: Separate JS file for AirBridge bot? 69 | exports.introMessage = `Hey there! 70 | Thanks for using AirBridge. This isn't a "real" iMessage chat--instead, it's a bot that helps you manage your AirBridge server. You can talk to me using commands that start with hashtags--such as #status. You can get a list of available commands by sending #help. 71 | 72 | AirBridge will let you know right here if there are any server issues, but you can always disable this by sending #notifications.`; 73 | 74 | exports.helpMessage = `Here are the available commands you can use: 75 | #help: Displays this screen 76 | #notifications on/off: Enables/disables server notifications (errors/etc) 77 | #remote: Get a link to remote control your iPhone 78 | #settings: Adjust settings 79 | #tapback type message: Lets you send a tapback (this command is usable in any chat). Send #help tapbacks to learn more.`; 80 | 81 | exports.helpTapbackMessage = ` 82 | `; 83 | 84 | exports.settingsMessage = ` 85 | `; 86 | -------------------------------------------------------------------------------- /Log.js: -------------------------------------------------------------------------------- 1 | //This is for logging, and easy setup to output to a file. 2 | Reset = "\x1b[0m" 3 | Bright = "\x1b[1m" 4 | Dim = "\x1b[2m" 5 | Underscore = "\x1b[4m" 6 | Blink = "\x1b[5m" 7 | Reverse = "\x1b[7m" 8 | Hidden = "\x1b[8m" 9 | 10 | FgBlack = "\x1b[30m" 11 | FgRed = "\x1b[31m" 12 | FgGreen = "\x1b[32m" 13 | FgYellow = "\x1b[33m" 14 | FgBlue = "\x1b[34m" 15 | FgMagenta = "\x1b[35m" 16 | FgCyan = "\x1b[36m" 17 | FgWhite = "\x1b[37m" 18 | 19 | BgBlack = "\x1b[40m" 20 | BgRed = "\x1b[41m" 21 | BgGreen = "\x1b[42m" 22 | BgYellow = "\x1b[43m" 23 | BgBlue = "\x1b[44m" 24 | BgMagenta = "\x1b[45m" 25 | BgCyan = "\x1b[46m" 26 | BgWhite = "\x1b[47m" 27 | 28 | //TODO: Log.Good which only gives very high-level info (i.e. one message per client request) 29 | 30 | exports.Log = function(jsfile, sender, indentationLevel) { 31 | // var sender = "[Unknown]"; 32 | // exports.setSender = function(sender) { 33 | // sender = "["+sender+"]"; 34 | // } 35 | this.sender = "["+jsfile+"] ["+sender+"]"; 36 | this.indentation = ""; 37 | //TODO: Pad the sender string to make them all the same length? 38 | if (indentationLevel) { 39 | for (var i = 0; i < indentationLevel; i++) { 40 | this.indentation += " "; 41 | } 42 | } 43 | 44 | this.g = function(message) { 45 | console.log(FgGreen+this.indentation+"[ Ok ] "+this.sender+" "+message+Reset); 46 | } 47 | this.good = this.g; 48 | this.i = function(message) { 49 | console.log(FgCyan+this.indentation+"[Info] "+this.sender+" "+message+Reset); 50 | } 51 | this.info = this.i; 52 | this.w = function(message) { 53 | console.log(FgYellow+this.indentation+"[Warn] "+this.sender+" "+message+Reset); 54 | } 55 | this.warning = this.w; 56 | this.e = function(message) { 57 | console.log(FgRed+this.indentation+"[Err ] "+this.sender+" "+message+Reset); 58 | } 59 | this.error = this.e; 60 | this.v = function(message) { 61 | console.log(this.indentation+"[Verb] "+this.sender+" "+message+Reset); 62 | } 63 | this.verbose = this.v; 64 | this.vv = function(message) { 65 | //Super-verbose logging. Used to dump data returned from SMServer and raw bytes sent to the client 66 | //TODO: Maybe introduce a VVV and use that to dump data, use VV for packer logging 67 | // Maybe add a Log.packer(...) function? 68 | console.log(this.indentation+"[Verb] "+this.sender+" "+message+Reset); 69 | } 70 | this.verbosev = this.vv; 71 | this.blankLine = function() { 72 | console.log("\n"); 73 | } 74 | this.p = function(message) { 75 | console.log(FgMagenta+this.indentation+"[Pakr] "+this.sender+" "+message+Reset); 76 | } 77 | this.packer = this.p; 78 | } 79 | 80 | //TODO: Connection management logs? 81 | 82 | //TODO: Include log sender? Initialize at the top of each JS file! 83 | //TODO: Include function name? 84 | 85 | //TODO: Separate settings for what to output vs what to save to a file--i.e. output info messages but save verbose to a file? 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AirBridge 2 | AirMessage Bridge for SMServer! 3 | 4 | 5 | Hey everyone! I've been working on a script that allows you to use an iPhone as an AirMessage server! It's in alpha and might have weird glitches, so I'd recommend having a Mac to serve as a backup (i.e. enable NAT loopback/hairpinning and set the fallback address as the address on your Mac). It's open source, and you can find it on GitHub [here](https://github.com/SixDigitCode/AirBridge). 6 | 7 | **What works** 8 | 9 | - Sending messages 10 | 11 | - Sending attachments 12 | 13 | - Sending tapbacks! At the moment the process is a little clunky, but should work for most text messages. See the "How to send a tapback" section below. 14 | 15 | - Receiving messages (includes tapbacks, read receipts) 16 | 17 | - Receiving attachments 18 | 19 | - WebSockets and message pushing! There's no need to set a refresh interval as SMServer pushes messages to AirBridge, which means messages should arrive nearly instantly. (Messages typically arrive 0.8 seconds after being received on the iPhone) 20 | 21 | - Stickers kind of work, but they show up as attachments for now 22 | 23 | - Creating a chat should work, but I haven't done extensive testing. Group creation doesn't work at the moment. 24 | 25 | 26 | 27 | 28 | **What doesn't work (at the moment)** 29 | 30 | - I've gotten AirBridge to run on the iPhone itself but it still needs some optimizations. If anyone knows how to package a NodeJS script into a Cydia tweak please let me know! 31 | 32 | - The Electron client (for Windows/Mac) currently can't fetch messages or conversations (I have a planned fix for this) 33 | 34 | - Group chats have some issues: 35 | 36 | - SMServer doesn't currently support viewing the list of members in a group (though it will be in the next update--see [this](https://github.com/iandwelker/smserver/issues/130) for more info). 37 | 38 | - It isn't possible at the moment to create a group (see [this](https://github.com/iandwelker/smserver/issues/133) for more info) 39 | 40 | - It isn't possible at the moment to add people to a group 41 | 42 | - SMServer tends to crash when downloading large attachments 43 | 44 | - SMServer doesn't support "Sending", "Sent", or "Delivered" statuses (all messages report as "sent"). Read receipts and the "Read" status should work. 45 | 46 | - Sending or receiving message effects (SMServer doesn't support these yet, but it might be possible by directly accessing SMS.db) 47 | 48 | - AirMessage Cloud (the developer has kindly asked third-party developers to not use the official AM cloud servers) 49 | 50 | 51 | 52 | 53 | **What SMServer supports but AirMessage doesn't support (yet)** 54 | 55 | - Sending read receipts 56 | 57 | - "Officially" sending tapbacks via the AirMessage app isn't supported right now, so tapbacks are a little clunky at the moment and attachment reactions aren't possible. 58 | 59 | - Sending typing indicators 60 | 61 | - Receiving typing indicators 62 | 63 | - iMessage apps/GamePigeon. SMServer doesn't officially support this, but it might be possible to set up the iPhone as a VNC server and remote-control it from a webpage running on the Android phone. I'm not sure how that would work with a self-signed certificate though, as running it over HTTP isn't a great idea. 64 | 65 | 66 | 67 | 68 | **Installation instructions** (Please let me know if you have any questions or if any of this doesn't make sense--I'm working on bundling this into a Cydia tweak for easy installation) 69 | 70 | **You will need:** 71 | 72 | - A spare computer that will always be on and connected to your network. This doesn't need to be a Mac--just something that can run Node. Macs, PCs, Linux boxes, and Raspberry Pis should all work. I'm working on getting AirBridge running on an iPhone and making the installation much easier, so this requirement will (hopefully) not stick around for much longer. 73 | 74 | - A jailbroken iPhone with iOS 13 or 14 (though SMServer will likely have support for iOS 12 in the near future). I used [Checkra1n](https://checkra.in/) on my iPhone SE, but YMMV. 75 | 76 | - An Android phone (obviously). Support for AirMessage Electron clients is coming soon. 77 | 78 | 79 | **How to install (part 1):** 80 | 81 | 1. Jailbreak your iPhone if you haven't already 82 | 83 | 2. Open Cydia and go to the Sources tab. Choose "Edit" and add [https://repo.twickd.com/](https://repo.twickd.com/). 84 | 85 | 3. Search for and install the SMServer tweak. 86 | 87 | 4. Open SMServer and choose a password. Make sure the port is set to 8741. 88 | 89 | 5. In the SMServer settings, make sure "Automatically mark as read" is turned off. 90 | 91 | 6. Make sure to [create a DHCP reservation](https://lifehacker.com/how-to-set-up-dhcp-reservations-and-never-check-an-ip-5822605) in your router settings for your iPhone and computer. You can google "Create DHCP reservation [router brand]" for specific instructions on how to do this. 92 | 93 | 94 | **How to install (part 2):** 95 | 96 | 1. On your computer, download and install [Git](https://git-scm.com/downloads) and [NodeJS](https://nodejs.org/en/download/). 97 | 98 | 2. Open a command prompt (Windows) or terminal (Mac/Linux). 99 | 100 | 1. Enter git clone [https://github.com/SixDigitCode/AirBridge.git](https://github.com/SixDigitCode/AirBridge.git) and press enter. Then cd AirBridge and run npm install. 101 | 102 | 2. Open settings.txt in a text editor and enter your settings, replacing the example values. Change SMSERVER_IP to the IP address of your iPhone (you can find it in Settings > Wi-Fi > [Your network] > IP Address), set SMSERVER_PASSWORD to the password you chose earlier, and choose a password for AIRMESSAGE_PASSWORD. Save the file. 103 | 104 | 3. Once NPM is done installing, run node index.js in your command prompt/terminal. If you see a green message that says "SMServer WebSocket Client Connected", your computer has successfully connected to SMServer on the iPhone! 105 | 106 | 3. On your Android phone, open AirMessage and choose "Use manual configuration". Your server address should be the IP address of the computer running AirBridge (not the IP of the iPhone), and the password should be whatever you set for AIRMESSAGE_PASSWORD earlier. If all goes well, you should see a bunch of activity on your computer (where AirBridge is running) and your Android phone should connect! 107 | 108 | 4. If your Android phone doesn't connect and you're sure your password is right, please PM me with the AirBridge logs and I'd be happy to help you out. 109 | 110 | 111 | 112 | 113 | **How to send a tapback** 114 | 115 | Tapback sending isn't officially supported by AirMessage. That said, I've implemented a (slightly clunky) way of sending a tapback to a text message (attachments aren't supported at the moment). 116 | 117 | To use it, reply with a message that looks like this: /tapback [tapback type] Copy and paste message here 118 | 119 | The tapback type is pretty flexible. Any of the following should work: Here are some [examples](https://imgur.com/a/4uwy8j4) as well. 120 | 121 | - Heart: 💖, 💕, heart, love, loved 122 | 123 | - Thumbs up: 👍, thumbs_up, like, liked 124 | 125 | - Thumbs down: 👎, thumbs_down, dislike, disliked 126 | 127 | - Laugh: 🤣, 😂, 😆, laughed, laughed_at, haha, lol 128 | 129 | - Emphasis: ‼️, ❗, ❕, !, emphasis, emphasized, exclamation 130 | 131 | - Question: ❓, ❔, ?, question_mark, question, what 132 | -------------------------------------------------------------------------------- /SMServerAPI.js: -------------------------------------------------------------------------------- 1 | const https = require('https'); 2 | const fetch = require('node-fetch'); 3 | const fs = require('fs'); 4 | const { URLSearchParams } = require('url'); 5 | const ConversionDatabase = require('./conversionDatabase.js'); 6 | const LogLib = require("./Log.js"); 7 | const FormData = require('form-data'); 8 | const SettingsManager = require("./settingsManager.js"); 9 | // Log.setSender("SMServerAPI.js"); 10 | 11 | // const SERVER_IP = "192.168.1.33"; 12 | // var SERVER_IP = SettingsManager.readSetting("SMSERVER_IP"); 13 | // const SERVER_PORT = "8741"; 14 | // const SERVER_PASSWORD = "toor"; 15 | 16 | 17 | process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0; 18 | //TODO: WHY IS WEBSOCKET ATTACHMENTS NOT WORKING??? 19 | //TODO: Auto-authenticate if a request fails due to no authentication 20 | async function SMServerFetch(path, indata, emptyResponseIsOk) { 21 | var SERVER_IP = await SettingsManager.readSetting("SMSERVER_IP"); 22 | var SERVER_PORT = await SettingsManager.readSetting("SMSERVER_PORT"); 23 | var SERVER_PASSWORD = await SettingsManager.readSetting("SMSERVER_PASSWORD"); 24 | var Log = new LogLib.Log("SMServerAPI.js","SMServerFetch"); 25 | //TODO: DEAL WITH RESPONSE CODES AS OUTLINED IN SMSERVER API 26 | //TODO: HANDLE ECONNRESET: Error: socket hang up 27 | 28 | //TODO: Check if SMServer certificate stays the same across installs? 29 | //Not sure if the Node version on Cydia supports fetch, so we're writing our own fetch function! 30 | //TODO: If we get ECONNRESET, wait and try again 31 | 32 | path += "?"; 33 | for (property in indata) { 34 | if (indata.hasOwnProperty(property)) { 35 | path += property + "=" + encodeURIComponent(indata[property]) + "&"; 36 | // console.log(property); 37 | } 38 | } 39 | path = path.slice(0, -1); //Removes the "&" from the end 40 | 41 | var makeRequest = function(path) { 42 | return new Promise((resCb, rejCb) => { 43 | Log.v("Making request to "+path); 44 | var options = { 45 | host: SERVER_IP, 46 | port: SERVER_PORT, 47 | path: path 48 | }; 49 | 50 | callback = function(response) { 51 | var str = ''; 52 | 53 | //another chunk of data has been received, so append it to `str` 54 | response.on('data', function (chunk) { 55 | str += chunk; 56 | }); 57 | 58 | //the whole response has been received, so we just print it out here 59 | response.on('end', function () { 60 | Log.v("Request finished successfully, parsing..."); 61 | var responseIsEmpty = (str == "" || str == undefined); 62 | // Log.w("Response is empty: "+responseIsEmpty); 63 | if (emptyResponseIsOk && responseIsEmpty) { 64 | resCb({}); 65 | return; //Stops evaluating errors 66 | } else { 67 | try { 68 | var parsed = JSON.parse(str); 69 | resCb(parsed); 70 | } catch (err) { 71 | Log.e("Couldn't parse JSON: "+str); 72 | //TODO: Test what happens if the password is wrong and handle that error 73 | rejCb("Error: Couldn't parse JSON: "+parsed); 74 | } 75 | } 76 | }); 77 | } 78 | 79 | 80 | //There's no way SMServer has a signed certificate, so this is used to disable certificate checking. 81 | //SMServer doesn't work on HTTP for some reason, so we must deal with its self-signed cert. 82 | //This would be really dangerous, but seeing as we're only talking to localhost there's not much that can MITM this. 83 | 84 | var req = https.request(options, callback); 85 | req.end(); 86 | req.on('error', function(e) { 87 | if (emptyResponseIsOk) { 88 | //do nothing 89 | } else { 90 | rejCb(e); 91 | } 92 | }); 93 | }); 94 | } 95 | 96 | var response = null; 97 | var requestSuccessful = false; 98 | for (var i = 0; i < 5; i++) { 99 | try { 100 | var response_try = await makeRequest(path); 101 | response = response_try; 102 | requestSuccessful = true; 103 | break; 104 | } catch (err) { 105 | if (err.code == "ECONNREFUSED") { 106 | Log.w("Connection to "+SERVER_IP+" was refused. SMServer could be busy or offline. Retrying in 5s...") 107 | } else { 108 | Log.e(err+". Retrying in 5s..."); 109 | } 110 | } 111 | } 112 | if (!requestSuccessful) { 113 | Log.e("Request was unsuccessful after trying multiple times. See above warning for details"); 114 | throw "Request unsuccessful"; 115 | } 116 | // return await makeRequest(path); 117 | return response; 118 | } 119 | 120 | // async function SMServerFetchFile(attachment_data, indata) { 121 | // 122 | // } 123 | 124 | async function SMServerFetchPost(path, indata) { 125 | var SERVER_IP = await SettingsManager.readSetting("SMSERVER_IP"); 126 | var SERVER_PORT = await SettingsManager.readSetting("SMSERVER_PORT"); 127 | var SERVER_PASSWORD = await SettingsManager.readSetting("SMSERVER_PASSWORD"); 128 | 129 | var Log = new LogLib.Log("SMServerAPI.js","SMServerFetchPost"); 130 | //Data must be sent in as MULTIPART/FORM-DATA 131 | //Encode everything with encodeURIComponent, so spaces replace with %20 132 | return new Promise((resCb, rejCb) => { 133 | 134 | var options = { 135 | host: SERVER_IP, //TODO: Make this a constant? It might end up being localhost, though 136 | port: SERVER_PORT, 137 | path: path 138 | }; 139 | 140 | const params = new URLSearchParams(); 141 | 142 | for (property in indata) { 143 | if (indata.hasOwnProperty(property)) { 144 | // formData[property] = encodeURIComponent(indata[property]); 145 | params.append(property, encodeURIComponent(indata[property])); //DOES THIS NEED TO BE URI ENCODED??? 146 | } 147 | } 148 | Log.v("Sending a post request to "+path); 149 | fetch('https://'+SERVER_IP+':'+SERVER_PORT+path, {method: 'POST', body: params}).then(res => res.text()).then(text => console.log(text)); 150 | //TODO: DEAL WITH HTTP ERROR CODES 151 | }); 152 | } 153 | 154 | //TODO: Auto-configure SMServer to use a password that works with AirBridge by digging through the files? 155 | // What about people who want to use SMServer's web interface too? 156 | 157 | exports.fetch = fetch; 158 | exports.SMServerFetch = SMServerFetch; 159 | 160 | 161 | 162 | var authenticated = false; 163 | 164 | exports.authenticate = async function() { 165 | var Log = new LogLib.Log("SMServerAPI.js","authenticate"); 166 | Log.v("Authenticating to SMServer"); 167 | //TODO: Make a big scene if authentication doesn't work out 168 | //Such as if SMServer is unreachable 169 | var password = await SettingsManager.readSetting("SMSERVER_PASSWORD"); 170 | authenticated = await SMServerFetch("/requests", {password: password}); 171 | if (authenticated == false) { 172 | Log.e("SMServer authentication failed. Check your SMServer password."); 173 | } 174 | return authenticated; 175 | 176 | //TODO: Load PFX certificate if possible. 177 | // 178 | } 179 | 180 | var fsAccessPromise = function(path) { 181 | return new Promise(function(resolve, reject) { 182 | fs.access(path, resolve); 183 | }); 184 | } 185 | 186 | var fsCreateDirPromise = function(path) { 187 | return new Promise(function(resolve, reject) { 188 | fs.mkdir(path, resolve); 189 | }); 190 | } 191 | 192 | var ensureFolderExists = async function(path) { 193 | var folderAccess = await fsAccessPromise(path); 194 | if (folderAccess) { 195 | if (folderAccess.code == 'ENOENT') { 196 | //Create the file 197 | await fsCreateDirPromise(path); 198 | } else { 199 | Log.w("Couldn't access "+path+" folder: "+JSON.stringify(folderAccess)); 200 | } 201 | } 202 | return path; 203 | } 204 | 205 | exports.ensureAttachmentFoldersExist = async function() { 206 | var Log = LogLib.Log("SMServerAPI.js", "ensureAttachmentFoldersExist"); 207 | 208 | await ensureFolderExists("./attachment_cache"); 209 | await ensureFolderExists("./sent_attachment_cache"); 210 | } 211 | 212 | exports.downloadAttachmentIfNecessary = async function(attachment_info) { 213 | var Log = new LogLib.Log("SMServerAPI.js", "downloadAttachmentIfNecessary"); 214 | var SERVER_IP = await SettingsManager.readSetting("SMSERVER_IP"); 215 | var SERVER_PORT = await SettingsManager.readSetting("SMSERVER_PORT"); 216 | var SERVER_PASSWORD = await SettingsManager.readSetting("SMSERVER_PASSWORD"); 217 | await exports.ensureAttachmentFoldersExist(); 218 | //https://192.168.1.46:8741/data?path=Attachments/03/03/8398059F-C566-4721-A387-1A63546C0D2C/64642773570__2531BEFC-FD22-4C4C-B5E8-554CF87FC3F1.JPG 219 | //TODO: Check if SMServer certificate stays the same across installs? 220 | //Not sure if the Node version on Cydia supports fetch, so we're writing our own fetch function! 221 | //TODO: If we get ECONNRESET, wait and try again 222 | //TODO: Functionify this, instead of sharing code between SMServerFetch and SMServerFetchFile 223 | 224 | //TODO: Keep track of downloaded attachments in conversionDatabase 225 | 226 | //TODO: If the attachment path exists, don't redownload it 227 | 228 | // console.log("filename:"+attachment_info.filename); 229 | console.log("Looking up "+attachment_info.filename); 230 | 231 | 232 | //TODO: NEXT STEPS: Create the attachment_cache and sent_attachment_cache folders if they don't exist 233 | var savedFilePathIfExists = ConversionDatabase.checkIfAttachmentAlreadySaved(attachment_info.filename); 234 | Log.v("Saved file path (if the file exists): "+savedFilePathIfExists); 235 | 236 | if (savedFilePathIfExists) { //This is undefined if it doesn't exist 237 | //Check if the file exists. (If it doesn't, redownload it) 238 | Log.v("File exists, returning with path "+savedFilePathIfExists); 239 | if (fs.existsSync("./attachment_cache/"+savedFilePathIfExists)) { 240 | Log.v("Saved file exists! Using path of existing file."); 241 | return "./attachment_cache/"+savedFilePathIfExists; 242 | } else { 243 | Log.w("Looks like the saved attachment was deleted. Re-downloading..."); 244 | } 245 | } 246 | 247 | // const downloadedPath = await async function(url) { 248 | var savePath = "./attachment_cache/"+ConversionDatabase.getAttachmentSavePath(attachment_info.filename); 249 | Log.v("Will download file to "+savePath+". Fetching..."); 250 | 251 | // var fetchTimeout = setTimeout() 252 | 253 | const res = await fetch("https://"+SERVER_IP+":"+SERVER_PORT+"/data?path="+encodeURIComponent(attachment_info.filename)); 254 | Log.v("Data fetched, sending to fileStream"); 255 | //TODO: Add indata conversion and put that in the fetch function 256 | const fileStream = fs.createWriteStream(savePath); 257 | var downloadedPath = await new Promise((resolve, reject) => { //before it was just "await new Promise..." 258 | Log.v("Downloading file from SMServer:"+attachment_info.filename); 259 | //TODO: Error handling if SMServer goes down or whatever 260 | res.body.pipe(fileStream); //We need this! 261 | res.body.on("error", (info) => { 262 | Log.e("Error downloading file: "+info); 263 | rejCb(info); 264 | }); 265 | // fileStream.on("finish", resolve); 266 | fileStream.on("finish", () => { 267 | Log.v("File downloaded and saved to "+savePath); 268 | resolve(savePath); //this sets downloadedPath above 269 | }); 270 | }); 271 | // }; 272 | 273 | return downloadedPath; 274 | } 275 | //TODO: Whenever messages are downloaded, keep track of attachment paths!! 276 | //TODO: Only download attachments if the script is not running on the iPhone 277 | 278 | //TODO: When filtering messages, remove zero-width spaces 279 | //TODO: Create a function that makes each message unique by inserting zero-width spaces 280 | // Only really need to do this for messages sent from AM, as those are the only ones without GUIDs 281 | 282 | 283 | exports.sendTextMessage = async function(text, chatid) { //TODO: Figure out how to upload photos and send them. 284 | var Log = new LogLib.Log("SMServerAPI.js","sendTextMessage"); 285 | //Chatid can also be a phone number, but NEEDS to be in international format (ex. +11234567890) 286 | Log.v("Sending text message to "+chatid); 287 | if (!authenticated) { 288 | Log.e("Cannot send text messages due to not being authenticated with SMServer"); 289 | throw 'Error: Cannot send text message due to not being authenticated with SMServer' 290 | return; 291 | } 292 | //data should look something like this: 293 | // { 294 | // text: "This is the body of the text message", 295 | // subject: "Subject line", 296 | // chat: "1234567890", //Chat ID 297 | // photos: "/var/mobile/Media/whatever.png", //Photo path on the phone 298 | // attachments: 123 //Somehow files are sent here. I assume it's using the regular path? 299 | // } 300 | // text = '​😀​'; //Has the fancy unicode zero-width space 301 | Log.v("Sending "+text+" to "+chatid); 302 | SMServerFetchPost('/send', { 303 | text: text, 304 | subject: "", 305 | chat: chatid 306 | }); 307 | // console.log("\n\n\n\nMESSAGE SENTtTT"); 308 | //TODO: ADD A CRAP TON OF ERROR HANDLING 309 | } 310 | 311 | exports.sendTapbackGivenMessageText = async function(messageText, chatID, tapbackCode) { //TODO: Are chat IDs required? 312 | var Log = new LogLib.Log("SMServerAPI.js","sendTapbackWithMessageText"); 313 | 314 | //We need to get an associated message GUID 315 | // var searchResults = (await SMServerFetch("/requests", {search: messageText, search_case: false, search_gaps: false, search_group: "time"})).matches.texts; 316 | var i = 95; //TODO: Make a function to get a chunk of messages from SMServer 317 | var targetMessage = null; 318 | var searchResults = await exports.getMessagesForOneConversationWhileConditionIsTrue(chatID, (message) => { 319 | // console.log(message.text); 320 | i -= 1; 321 | if (i < 0) { 322 | // console.log("I is 0, returning"); 323 | return false; 324 | } 325 | var messageIsNotTapback = (message.associated_message_guid == "" || message.associated_message_guid == undefined); 326 | // if (message.text.indexOf(messageText) > -1 && messageIsNotTapback) { 327 | if (message.text.trim() === messageText.trim() && messageIsNotTapback) { 328 | //TODO: Get rid of newlines in the message text we're matching against. 329 | 330 | //TODO: Maybe 331 | targetMessage = message; 332 | return false; 333 | } 334 | return true; 335 | }); 336 | 337 | //TODO: If user wants to remove a tapback, stack tapbacks and find the one the user sent (is_from_me: true) and get the type. Remove it first. 338 | // Also do this if a tapback exists?? 339 | 340 | //TODO: (Test with iPhone 4--why is the tapback sent a bunch of times?) 341 | 342 | if (targetMessage == null) { 343 | return false; //TODO: Handle this error if the message wasn't found 344 | } 345 | 346 | // searchResults.filter((message) => { 347 | // return (message.) 348 | // }) 349 | 350 | console.log(targetMessage); 351 | await exports.sendTapback(tapbackCode, targetMessage.guid, false); 352 | // exports.sendTapback(mostRecentResult) 353 | //TODO: Wait, search doesn't return a GUID!! 354 | 355 | } 356 | 357 | exports.sendTapback = async function(tapbackType, associated_message_guid, remove_tap) { //TODO: Need a remove option? 358 | await SMServerFetch("/send", {tapback: tapbackType, tap_guid: associated_message_guid, remove_tap: remove_tap}, true); 359 | } 360 | 361 | //TODO: Message does not show up when sending a video from other device 362 | 363 | //TODO: What about message text??? 364 | exports.sendFile = async function(fileName, fileData, chatid, text) { 365 | var SERVER_IP = await SettingsManager.readSetting("SMSERVER_IP"); 366 | var SERVER_PORT = await SettingsManager.readSetting("SMSERVER_PORT"); 367 | var SERVER_PASSWORD = await SettingsManager.readSetting("SMSERVER_PASSWORD"); 368 | await exports.ensureAttachmentFoldersExist(); 369 | 370 | var Log = new LogLib.Log("SMServerAPI.js","sendFile"); 371 | //TODO: Large files fail!!!!!!!!!!!!!!!!! 372 | // Log.w("File size is "+fileData.length+" bytes"); 373 | 374 | //This is 1.96 MB 375 | 376 | //TODO: Implement message text 377 | 378 | // TODO: Figure out why file-receiving isn't working for pushing nhtMessageUpdate, but it works on message sync!! 379 | // fs.writeFileSync("test.png", fileData); 380 | 381 | // fileData = fs.readFileSync("C:/Users/aweso/Downloads/pcpartpicker.com_list_.png"); 382 | //TODO: What about multiple attachments?? 383 | var Log = new LogLib.Log("SMServerAPI.js","sendTextMessage"); 384 | //Chatid can also be a phone number, but NEEDS to be in international format (ex. +11234567890) 385 | Log.v("Sending file to "+chatid); 386 | if (!authenticated) { 387 | Log.e("Cannot send text messages due to not being authenticated with SMServer"); 388 | throw 'Error: Cannot send text message due to not being authenticated with SMServer' 389 | return; 390 | } 391 | 392 | var form = new FormData(); 393 | var buffer = fileData; 394 | 395 | form.append('attachments', buffer, { 396 | // contentType: 'image/png', 397 | // contentType: 'application/octet-stream', 398 | name: 'file', 399 | filename: fileName 400 | }); 401 | form.append('chat', chatid); 402 | // form.append('text', 'Heyyy this is a test yo'); 403 | //data should look something like this: 404 | // { 405 | // text: "This is the body of the text message", 406 | // subject: "Subject line", 407 | // chat: "1234567890", //Chat ID 408 | // photos: "/var/mobile/Media/whatever.png", //Photo path on the phone 409 | // attachments: 123 //Somehow files are sent here. I assume it's using the regular path? 410 | // } 411 | Log.v("Sending file to "+chatid); 412 | // SMServerFetchPost('/send', { 413 | // text: text, 414 | // subject: "", 415 | // chat: chatid 416 | // }); 417 | 418 | //TODO: Roll this into SMServerFetchPost (i.e. make SMServerFetchPost work with files too?) 419 | return new Promise((resCb, rejCb) => { 420 | 421 | // var form = new FormData(); 422 | // form.append('attachments', fileData, {knownLength: fileData.length}); 423 | path = "/send"; 424 | 425 | var options = { 426 | host: SERVER_IP, //TODO: Make this a constant? It might end up being localhost, though 427 | port: SERVER_PORT, 428 | path: path 429 | // attachments: [fileData] 430 | }; 431 | 432 | var file = [fileName, fileData, 'image/png']; //TODO: Find the MIME type 433 | 434 | console.log(options); 435 | 436 | const params = new URLSearchParams(); 437 | 438 | // indata = { 439 | // "chat": chatid, 440 | // "text": "Heyy, this a file test is", 441 | // "attachments": [fileData] 442 | // } 443 | // 444 | // for (property in indata) { 445 | // if (indata.hasOwnProperty(property)) { 446 | // // formData[property] = encodeURIComponent(indata[property]); 447 | // params.append(property, encodeURIComponent(indata[property])); //DOES THIS NEED TO BE URI ENCODED??? 448 | // } 449 | // } 450 | Log.v("Sending a post request to "+path); 451 | //TODO: Does this need a "Content-length" attr in the headers: {} part next to method: 'POST', ??? 452 | fetch('https://'+SERVER_IP+':'+SERVER_PORT+path, {method: 'POST', body: form}).then(res => res.text()).then(text => console.log(text)); 453 | //TODO: DEAL WITH HTTP ERROR CODES 454 | }); 455 | 456 | //TODO: Slice up the file data and send it in multiple passes so SMServer doesn't get confused? 457 | } 458 | 459 | exports.getListOfChats = async function(num_chats) { 460 | var Log = new LogLib.Log("SMServerAPI.js","getListOfChats"); 461 | Log.v("Getting list of chats"); 462 | if (num_chats == undefined) { 463 | var num_chats = 99999; //Number of chats to search through. Needs to be something ridiculously large. 464 | } 465 | var data = await SMServerFetch("/requests", { chats: num_chats }); 466 | Log.vv(JSON.stringify(data)); 467 | // console.log(data); 468 | return data; 469 | } 470 | 471 | //TODO: On first connect, assign GUIDs for every conversation? 472 | 473 | //TODO: Add a getAllMessagesWhileConditionIsTrue() method that takes a compare 474 | //function and keeps downloading older and older messages until it is satisfied or 475 | //runs out of messages? Useful for finding time_lower and tracing tapbacks to their 476 | //original message 477 | 478 | //TODO: Add a function that formats an SMServer message correctly--takes in an existing message from SMServer and adds "conversation_id" and "tapbacks":[] 479 | 480 | //TODO: Keep track of last client request upper bound and check for new messages and push them? 481 | 482 | exports.getMessagesForOneConversationWhileConditionIsTrue = async function(conversation, pre_filter_fn, chunk_callback) { 483 | var Log = new LogLib.Log("SMServerAPI.js","getMessagesForOneConversationWhileConditionIsTrue"); 484 | Log.v("Getting messages for one conversation while condition is true: "+conversation); 485 | //TODO: Add a callback argument that returns data as it is available, instead of waiting for it all to finish 486 | 487 | //This function continuously gets messages from SMServer until pre_filter_fn returns false. 488 | //Postfiltering is done by the parent function 489 | 490 | //TODO: Check for duplicates! 491 | 492 | 493 | //THE FOLLOWING IS UNTESTED 494 | //time_lower and time_upper are both in UNIX seconds 495 | var messages = []; 496 | var filtered_messages = []; 497 | var offset = 0; //How far back to start retrieving messages 498 | var chunkSize = 100; 499 | 500 | var continueLoop = true; 501 | //So if we don't get all 502 | while (continueLoop) { 503 | var results = await SMServerFetch("/requests", {messages: conversation, num_messages: 100, read_messages: false, messages_offset: offset}); 504 | Log.v("Got "+chunkSize+" results from SMServer, checking to see if they all meet the criteria"); 505 | // if (results.texts.length == 0) { //results.texts[results.texts.length - 1] was failing here 506 | // break; //If this request batch returns an empty list (i.e. exactly 100 messages in a chat), the loop ends as we're at the end. 507 | // } 508 | 509 | // messages = results.texts.concat(messages); //Adds the results 510 | offset += chunkSize; 511 | // console.log("\n"); 512 | // console.log(results); 513 | // var timeOfEarliestMessage = ConversionDatabase.convertAppleDateToUnixTimestamp(results.texts[results.texts.length - 1].date); 514 | 515 | //Filtering is now integrated into this loop 516 | //This loop performs pre_filter_fn on the results SMServer has returned (for this chunk only). 517 | //If pre_filter_fn returns false for any function in the chunk we just received, 518 | var chunk_messages = []; 519 | Log.v("Testing each message against the compare callback function"); 520 | for (var i = 0; i < results.texts.length; i++) { 521 | var current_message = results.texts[i]; 522 | if (pre_filter_fn(current_message)) { 523 | current_message.conversation_id = conversation; 524 | chunk_messages.push(current_message); 525 | } else { 526 | // console.log("Prefilter function returned false!"); 527 | Log.v("Prefilter function returned false, end of conditional search!"); 528 | continueLoop = false; 529 | break; //Stops counting messages after pre_filter_fn returns false 530 | } 531 | } 532 | messages = messages.concat(chunk_messages); 533 | 534 | if (chunk_callback) { //chunk_callback could be undefined 535 | chunk_callback(chunk_messages); 536 | } 537 | 538 | if (results.texts.length < chunkSize) { //This happens if we are at the very beginning of the conversation and have downloaded all messages. 539 | Log.v("Found end of conversation, stopping the loop"); 540 | // console.log("Results length is less than our chunk size"); 541 | continueLoop = false; 542 | } 543 | 544 | // console.log("Length of results is "+results.length+" vs "+chunkSize); 545 | 546 | } 547 | //TODO: What if we have an orphaned tapback at the beginning of the message query? 548 | //TODO: Maybe return the prefiltered list, as that can be useful to check if a tapback on an older message 549 | // was added at the very end. 550 | // console.log("Does htis work at all?????????????????????"); 551 | //Now do filtering so only the messages within the specified time frame exactly get returned 552 | // var filtered = messages.filter((item) => { 553 | // //return true if it should be kept 554 | // var unixstamp = ConversionDatabase.convertAppleDateToUnixTimestamp(item.date); 555 | // console.log("Filterring!"); 556 | // if (unixstamp >= time_lower && unixstamp <= time_upper) { 557 | // console.log(item.text+" matches the requirements!"); 558 | // } else { 559 | // console.log(item.text+": "+new Date(unixstamp * 1000)+" is not in the correct timeframe") 560 | // } 561 | // return unixstamp >= time_lower && unixstamp <= time_upper; 562 | // }); 563 | 564 | //TODO: Filter out tapbacks, digital touch, etc, and maybe associate with their parent messages in the future (instead of individual messages) 565 | //Anything with no text and no subject, or an associated_message_guid, or a balloon_bundle_id 566 | // console.log("messages: "+messages); 567 | messages = exports.fixFormattingForMultipleMessages(messages, conversation); 568 | 569 | return messages; 570 | 571 | 572 | } 573 | 574 | exports.getAllMessagesWhileConditionIsTrue = async function(pre_filter_fn, chunk_callback) { 575 | var Log = new LogLib.Log("SMServerAPI.js","getMessagesForAllConversationsWhileConditionIsTrue"); 576 | Log.v("Getting all messages while condition is true"); 577 | //TODO: Add a callback argument that returns data as it is available, instead of waiting for it all to finish 578 | 579 | //This function continuously gets messages from SMServer until pre_filter_fn returns false. 580 | //Postfiltering is done by the parent function 581 | 582 | //TODO: Check for duplicates! 583 | 584 | var conversations_json = await exports.getAllConversations(); 585 | var conversations = []; 586 | for (var i = 0; i < conversations_json.length; i++) { 587 | conversations.push(conversations_json[i].chat_identifier); 588 | } 589 | 590 | //THE FOLLOWING IS UNTESTED 591 | //time_lower and time_upper are both in UNIX seconds 592 | var messages = []; 593 | var filtered_messages = []; 594 | var offset = 0; //How far back to start retrieving messages 595 | var chunkSize = 100; 596 | 597 | var continueLoop = true; 598 | //So if we don't get all 599 | while (continueLoop) { 600 | var results = await SMServerFetch("/requests", {messages: conversations.join(","), num_messages: 100, read_messages: false, messages_offset: offset}); 601 | Log.v("Got "+chunkSize+" results from SMServer, checking to see if they all meet the criteria"); 602 | // if (results.texts.length == 0) { //results.texts[results.texts.length - 1] was failing here 603 | // break; //If this request batch returns an empty list (i.e. exactly 100 messages in a chat), the loop ends as we're at the end. 604 | // } 605 | 606 | // messages = results.texts.concat(messages); //Adds the results 607 | offset += chunkSize; 608 | // console.log("\n"); 609 | // console.log(results); 610 | // var timeOfEarliestMessage = ConversionDatabase.convertAppleDateToUnixTimestamp(results.texts[results.texts.length - 1].date); 611 | 612 | //Filtering is now integrated into this loop 613 | //This loop performs pre_filter_fn on the results SMServer has returned (for this chunk only). 614 | //If pre_filter_fn returns false for any function in the chunk we just received, 615 | var chunk_messages = []; 616 | Log.v("Testing each message against the compare callback function"); 617 | for (var i = 0; i < results.texts.length; i++) { 618 | var current_message = results.texts[i]; 619 | if (pre_filter_fn(current_message)) { 620 | current_message.conversation_id = "AirBridge"; //Not from AirBridge, but this is just to give it a conversation to attach to 621 | //This method shouldn't be used if you need the conversation ID (IDs are not added 622 | //from /requests?messages, so the best we can do is loop through each conversation and 623 | //add the data after the fact.) This function ignores conversation IDs but pretends that 624 | //these messages are from AirBridge so if they get inadvertently sent to the client the client 625 | //doesn't freak out if the conversation wasn't found. 626 | chunk_messages.push(current_message); 627 | } else { 628 | // console.log("Prefilter function returned false!"); 629 | Log.v("Prefilter function returned false, end of conditional search!"); 630 | continueLoop = false; 631 | break; //Stops counting messages after pre_filter_fn returns false 632 | } 633 | } 634 | messages = messages.concat(chunk_messages); 635 | 636 | if (chunk_callback) { //chunk_callback could be undefined 637 | chunk_callback(chunk_messages); 638 | } 639 | 640 | if (results.texts.length < chunkSize) { //This happens if we are at the very beginning of the conversation and have downloaded all messages. 641 | Log.v("Found end of conversation, stopping the loop"); 642 | // console.log("Results length is less than our chunk size"); 643 | continueLoop = false; 644 | } 645 | 646 | // console.log("Length of results is "+results.length+" vs "+chunkSize); 647 | 648 | } 649 | //TODO: What if we have an orphaned tapback at the beginning of the message query? 650 | //TODO: Maybe return the prefiltered list, as that can be useful to check if a tapback on an older message 651 | // was added at the very end. 652 | // console.log("Does htis work at all?????????????????????"); 653 | //Now do filtering so only the messages within the specified time frame exactly get returned 654 | // var filtered = messages.filter((item) => { 655 | // //return true if it should be kept 656 | // var unixstamp = ConversionDatabase.convertAppleDateToUnixTimestamp(item.date); 657 | // console.log("Filterring!"); 658 | // if (unixstamp >= time_lower && unixstamp <= time_upper) { 659 | // console.log(item.text+" matches the requirements!"); 660 | // } else { 661 | // console.log(item.text+": "+new Date(unixstamp * 1000)+" is not in the correct timeframe") 662 | // } 663 | // return unixstamp >= time_lower && unixstamp <= time_upper; 664 | // }); 665 | 666 | //TODO: Filter out tapbacks, digital touch, etc, and maybe associate with their parent messages in the future (instead of individual messages) 667 | //Anything with no text and no subject, or an associated_message_guid, or a balloon_bundle_id 668 | // console.log("messages: "+messages); 669 | 670 | // messages = exports.fixFormattingForMultipleMessages(messages, conversation); 671 | //TODO: Does this need to be broken down with extra data (i.e. conversationID) added? 672 | 673 | return messages; 674 | 675 | 676 | } 677 | 678 | exports.stackTapbacks = async function(messages, orphanedTapbackCallback) { 679 | 680 | var Log = new LogLib.Log("SMServerAPI.js","stackTapbacks"); 681 | Log.v("Stacking tapbacks"); 682 | Log.v("Original message count: "+messages.length); 683 | //TODO: Add a callback for orphaned tapbacks, so we can run nhtModifierUpdate later. 684 | 685 | //It is assumed the messages are ordered chronologically--i.e. newest message is at index 0 686 | //WE NEED DATA FOR ALL OF THE FOLLOWING 687 | // For each tapback, writeObject() for the TapbackModifierInfo item 688 | // [Pack the sueprclass ModifierInfo] 689 | // Pack a string: Item type (for StickerModifierInfo this is 1) 690 | // Pack a string: message (Is this the GUID? Or just the message text?) 691 | // Pack string: messageIndex (ROWID??? But it's a string) 692 | // Pack mullable string: Sender (null if me) 693 | // Pack boolean: isAddition (if the tapback was added or removed) 694 | // Pack int: Tapback type (DOUBLE CHECK THE NUMBERS) 695 | 696 | //We need to include messageIndex, Sender, isAddition, and tapback type. 697 | var textMessages = []; 698 | var orphanedTapbacks = []; 699 | for (var i = (messages.length - 1); i >= 0; i--) { //Loops from oldest to newest message 700 | // console.log(messages[i]); 701 | if (messages[i].associated_message_guid == "") { 702 | messages[i].tapbacks = []; 703 | textMessages.unshift(messages[i]); //Adds to the beginning of the textMessages array to keep the output chronological 704 | } else if (!messages[i].cache_has_attachments) { //If it has an associated message GUID and attachments, it must be a sticker. We only want tapbacks 705 | // console.log("Found a tapback! "+messages[i].text+" associated with "+messages[i].associated_message_guid); 706 | 707 | // if (messages[i].associated_message_type >= 3000 && messages[i].associated_message_type <= 4000) { //If the tapback has been removed 708 | // //Okay, this will take some explanation. SMServer only shows you the tapbacks that currently 709 | // //apply to messages. So if I like your text and then change it to a dislike, SMServer will only 710 | // //keep track of the dislike and the like will vanish from the database. If you remove a tapback 711 | // //(removed tapbacks have an associated_message_type of 3000 to 3005) then that will be the only 712 | // //tapback saved from that person as it's the only one that applies right now. Therefore, if the 713 | // //tapback has an associated_message_type of 300x, that means there are no active tapbacks from 714 | // //this person. So we skip adding it to the database. 715 | // continue; 716 | // } //AirMessage asks for isAddition, so I guess I should pass along 300x values? 717 | 718 | var parts = messages[i].associated_message_guid.split("/"); 719 | var targetedMessageGUID = parts[1]; 720 | //TODO: How to find tapback type? p:0/ is always at the beginning 721 | //p:0 indicates the part number (i.e. if message is sent with attachment or whatever I think) 722 | 723 | 724 | /* 725 | { 726 | text: 'Emphasized “Subjectline This is the body of the text message”', 727 | date: 645406250523999900, 728 | balloon_bundle_id: '', 729 | ROWID: 10, 730 | group_action_type: 0, 731 | associated_message_guid: 'p:0/688FB450-C715-4914-9D2F-A73F6FDB7BE7', 732 | id: 'name@example.com', 733 | cache_has_attachments: false, 734 | guid: 'C5552DE4-3A88-4D63-9AD2-A11A23202C58', 735 | service: 'iMessage', 736 | is_from_me: true, 737 | subject: '', 738 | associated_message_type: 2004, 739 | item_type: 0, 740 | date_read: 0, 741 | conversation_id: 'name@example.com' 742 | } 743 | */ 744 | 745 | var tapbackIsOrphaned = true; 746 | for (var j = 0; j < textMessages.length; j++) { 747 | //Loops through the text messages we have so far, looking for one that matches 748 | //We are looping through the messages array backwards, so every message older 749 | //than the current one is already in the textMessages array 750 | if (targetedMessageGUID == textMessages[j].guid) { 751 | 752 | 753 | //associated_message_type: 754 | // 2000: Loved 755 | // 2001: Liked 756 | // 2002: Disliked 757 | // 2003: Laughed 758 | // 2004: Emphasized 759 | // 2005: Questioned 760 | // 300x: Removed tapback 761 | textMessages[j].tapbacks.push(messages[i]); 762 | 763 | tapbackIsOrphaned = false; 764 | break; 765 | } 766 | //break in here 767 | } 768 | 769 | if (tapbackIsOrphaned) { 770 | // console.log("[WARN] Orphaned tapback associated with: "+targetedMessageGUID+": "+messages[i].text); 771 | // if (orphanedTapbackCallback) { //This could be undefined 772 | // orphanedTapbackCallback(messages[i]); 773 | // } 774 | orphanedTapbacks.push(messages[i]); 775 | } 776 | //Yell that there's an orphaned tapback 777 | } 778 | } 779 | Log.v("Tapback stacking completed with "+textMessages.length+" messages left and "+orphanedTapbacks.length+" orphaned tapbacks"); 780 | //TODO: Check if removing tapbacks works as expected 781 | if (orphanedTapbackCallback) {//This could be Undefined 782 | orphanedTapbackCallback(orphanedTapbacks); 783 | } 784 | 785 | return textMessages; 786 | } 787 | 788 | exports.fixMessageFormat = function(message, conversation_id) { //Adds some useful data to messages as they're returned from SMServer (note: tapbacks aren't included) 789 | message.unixdate = ConversionDatabase.convertAppleDateToUnixTimestamp(message.date); 790 | message.conversation_id = conversation_id; 791 | return message; 792 | } 793 | 794 | exports.fixFormattingForMultipleMessages = function(messages, conversation_id) { 795 | var fixed = []; 796 | for (var i = 0; i < messages.length; i++) { 797 | fixed.push(exports.fixMessageFormat(messages[i], conversation_id)); 798 | } 799 | // console.log(fixed); 800 | return fixed; 801 | } 802 | 803 | //TOOD: Add a function for getting all attachment info from messages (maybe auto-) 804 | exports.extractAttachmentInfoFromMessages = function(messages) { 805 | var attachments = []; 806 | for (var i = 0; i < messages.length; i++) { 807 | if (messages[i].attachments) { //If the attachments exist 808 | attachments = attachments.concat(messages[i].attachments); 809 | } 810 | } 811 | return attachments; 812 | } 813 | 814 | exports.filterAttachments = function(attachments) { 815 | //TODO: Filter out attachments with no extension?? 816 | var filtered = []; 817 | for (var i = 0; i < attachments.length; i++) { 818 | if (attachments[i].filename.endsWith("unknown") || attachments[i].filename.endsWith(".pluginPayloadAttachment")) { 819 | //Don't add it to the filtered list 820 | } else { 821 | filtered.push(attachments[i]); 822 | } 823 | } 824 | return filtered; 825 | } 826 | 827 | 828 | 829 | //TODO: Auto-add necessary data (ex. conversation_id, unix_date, etc but not tapbacks) when data is returned from SMServer? or for getAllMessagesWhileConditionIsTrue? 830 | // I'm thinking about doing this 831 | 832 | exports.getAllMessagesFromSpecifiedTimeInterval = async function(time_lower, time_upper) { //I'm assuming these are in unix timestamps? Or Apple's format maybe? 833 | //TODO: Use websockets for new messages! 834 | //TODO: Send client a message when the iphone runs low on battery? 835 | var Log = new LogLib.Log("SMServerAPI.js","getAllMessagesFromSpecifiedTimeInterval"); 836 | Log.v("Getting all messages from time "+time_lower+" to "+time_upper); 837 | //time_lower and time_upper are both in UNIX milliseconds 838 | 839 | //Looks like there isn't an easy way to find messages from X time to Y time 840 | //We'll probably end up having to get the most recent 200 messages or so and see if we need to go back. 841 | // var results = await SMServerFetch("/requests"); 842 | Log.v("Getting list of conversations after "+time_lower); 843 | var conversations = await exports.getConversationsAfterTime(time_lower); 844 | // console.log(conversations); 845 | // var conversation_ids = []; 846 | // for (var i = 0; i < conversations.length; i++) { 847 | // conversation_ids.push(conversations[i].chat_identifier); 848 | // } 849 | // console.log("Got conversation IDs: "+conversation_ids); 850 | //SMServer lets us query multiple converations at once, as long as each ID is separated with commas 851 | 852 | //TODO: Sort these into their own conversations and label with the ID, as we'll need that later! 853 | // var messages = []; 854 | // for (var i = 0; i < conversations.length; i++) { 855 | // var messagesFromConversation = await exports.getMessagesFromOneConversationFromTimeInterval(conversations[i].chat_identifier, time_lower, time_upper); 856 | // //This adds a conversation_id to each message so AirPacker doesn't have to scramble to find it. 857 | // //For some reason the conversation ID isn't included in the results, so we have to query each conversation individually 858 | // 859 | // for (var j = 0; j < messagesFromConversation.length; j++) { 860 | // var message = messagesFromConversation[j]; 861 | // // console.log(message); 862 | // // console.log(i+" / "+conversations.length) 863 | // // console.log(conversations[i]); 864 | // message.conversation_id = conversations[i].chat_identifier; 865 | // messages.push(message); 866 | // } 867 | // } 868 | //THIS WAS CHANGED AND IS UNTESTED 869 | Log.v("Getting messages for each conversation"); 870 | var messages = []; 871 | for (var i = 0; i < conversations.length; i++) { 872 | Log.v("Getting messages after "+time_lower+" for conversation "+conversations[i]); 873 | var conv_messages = await exports.getMessagesForOneConversationWhileConditionIsTrue(conversations[i].chat_identifier, (message) => { 874 | //This is our compare function. Nifty! 875 | var unixstamp = ConversionDatabase.convertAppleDateToUnixTimestamp(message.date); 876 | // return unixstamp >= time_lower && unixstamp <= time_upper; 877 | return unixstamp >= time_lower; 878 | //TODO: This was changed as any conversations with messages both ahead of and behind time_upper 879 | // would cause messages to be missed, because as as soon as the first (newest) message was seen, 880 | // the function would stop 881 | }); 882 | Log.v("Found "+conv_messages.length+" messages from conversation "+conversations[i]); 883 | // console.log(conv_messages); 884 | for (var j = 0; j < conv_messages.length; j++) { 885 | var message = conv_messages[j]; 886 | message.conversation_id = conversations[i].chat_identifier; //This makes it easier to pass messages to the client later, as each message says which conversation it came from. 887 | messages.push(message); 888 | } 889 | } 890 | // console.log("all messages: "+messages); 891 | 892 | Log.v("Filtering out messages after "+time_upper+". Current message count is "+messages.length); 893 | var filtered = messages.filter((item) => { 894 | //return true if it should be kept 895 | var unixstamp = ConversionDatabase.convertAppleDateToUnixTimestamp(item.date); 896 | // console.log("Filterring!"); 897 | // if (unixstamp >= time_lower && unixstamp <= time_upper) { 898 | // console.log(item.text+" matches the requirements!"); 899 | // } else { 900 | // console.log(item.text+": "+new Date(unixstamp)+" is not in the correct timeframe") 901 | // } 902 | return unixstamp >= time_lower && unixstamp <= time_upper; 903 | }); 904 | //>>> FUTURE TODO: Put this into Promise.all() to be asynchronous and better! 905 | Log.v("Messages filtered. Message count is now "+messages.length); 906 | 907 | // console.log(allMessagesFromTime); 908 | Log.v("Message time retrieval finished"); 909 | // messages = exports.fixFormattingForMultipleMessages(messages); 910 | 911 | return messages; 912 | //join with commas 913 | } 914 | 915 | //NOTE: When installing on an iPhone, Python is required for byte-buffer to work 916 | 917 | //TODO: requests?search seems to include a chat_identifier field--use that instead of looping through each conversation? 918 | 919 | 920 | //TODO: Is getting from time interval used anymore (newer method = while condition is true)? 921 | 922 | // exports.getMessagesFromOneConversationFromTimeInterval = async function (conversation_id, time_lower, time_upper) { 923 | // //TODO: Is this ever used? 924 | // var Log = new LogLib.Log("SMServerAPI.js","getMessagesFromOneConversationFromTimeInterval"); 925 | // Log.v("Getting messages from conversation "+conversation_id+" from "+time_lower+" to "+time_upper); 926 | // //TODO: Check for duplicates! 927 | // 928 | // 929 | // //THE FOLLOWING IS UNTESTED 930 | // //time_lower and time_upper are both in UNIX seconds 931 | // // console.log("function ran"); 932 | // var timeOfEarliestMessage = 9999999999999999999; 933 | // var messages = []; 934 | // var offset = 0; //How far back to start retrieving messages 935 | // var chunkSize = 100; 936 | // //So if we don't get all 937 | // 938 | // // //TODO: Rewrite this using getMessagesForOneConversationWhileConditionIsTrue? 939 | // // while (timeOfEarliestMessage >= time_lower) { 940 | // // var results = await SMServerFetch("/requests", {messages: conversation_id, num_messages: 100, read_messages: false, messages_offset: offset}); 941 | // // if (results.texts.length == 0) { //results.texts[results.texts.length - 1] was failing here 942 | // // break; 943 | // // } 944 | // // messages = results.texts.concat(messages); //Adds the results 945 | // // offset += chunkSize; 946 | // // // console.log("\n"); 947 | // // // console.log(results); 948 | // // var timeOfEarliestMessage = ConversionDatabase.convertAppleDateToUnixTimestamp(results.texts[results.texts.length - 1].date); 949 | // // 950 | // // // console.log("Length of results is "+results.length+" vs "+chunkSize); 951 | // // if (results.texts.length < chunkSize) { 952 | // // // console.log("Results length is less than our chunk size"); 953 | // // break; //This happens if we are at the very beginning of the conversation. 954 | // // } 955 | // // } 956 | // var messages = await exports.getMessagesForOneConversationWhileConditionIsTrue(conversation_id, (item) => { 957 | // var unixstamp = ConversionDatabase.convertAppleDateToUnixTimestamp(item.date); 958 | // return unixstamp >= time_lower; 959 | // }); //TODO: Automatically stack tapbacks inside getMessagesForOneConversationWhileConditionIsTrue()? 960 | // 961 | // Log.v("Got "+messages.length+" messages from conversation during time interval"); 962 | // 963 | // // console.log("Does htis work at all?????????????????????"); 964 | // //Now do filtering so only the messages within the specified time frame exactly get returned 965 | // var filtered = messages.filter((item) => { 966 | // //return true if it should be kept 967 | // var unixstamp = ConversionDatabase.convertAppleDateToUnixTimestamp(item.date); 968 | // console.log("Filterring!"); 969 | // if (unixstamp >= time_lower && unixstamp <= time_upper) { 970 | // console.log(item.text+" matches the requirements!"); 971 | // } else { 972 | // console.log(item.text+": "+new Date(unixstamp)+" is not in the correct timeframe") 973 | // } 974 | // return unixstamp >= time_lower && unixstamp <= time_upper; 975 | // }); 976 | // 977 | // //TODO: Filter out tapbacks, digital touch, etc, and maybe associate with their parent messages in the future (instead of individual messages) 978 | // //Anything with no text and no subject, or an associated_message_guid, or a balloon_bundle_id 979 | // 980 | // return filtered; 981 | // } 982 | 983 | exports.getLastMessageFromConversation = async function (conversation_id, start_time) { 984 | var Log = new LogLib.Log("SMServerAPI.js","getLastMessageFromConversation"); 985 | Log.v("Getting last messages from conversation "+conversation_id+" (after "+start_time+")") 986 | // console.log("convo id: "); 987 | // console.log(conversation_id); 988 | 989 | //TODO: Maybe log all messages from after the message is sent, to avoid them getting lost? 990 | Log.v("Fetching results"); 991 | var results = (await SMServerFetch("/requests", {messages: conversation_id, num_messages: 4, read_messages: false, messages_offset: 0})).texts; //TODO: Should this use getAllMessagesWhileConditionIsTrue? 992 | Log.vv(JSON.stringify(results)); 993 | var results_filtered = []; 994 | Log.v("Got "+results.length+" results. Filtering..."); 995 | for (var i = 0; i < results.length; i++) { 996 | var unixsenddate = ConversionDatabase.convertAppleDateToUnixTimestamp(results[i].date); 997 | if (unixsenddate > start_time) { 998 | var result = results[i]; //TODO: Integrate conversation IDing into fetch (or a fetch wrapper) instead of having to deal with it each time 999 | result.conversation_id = conversation_id; 1000 | results_filtered.push(result); 1001 | } 1002 | } 1003 | Log.v("Finished getting last messages (filtered length is "+results_filtered.length+") from conversation"); 1004 | results_filtered = exports.fixFormattingForMultipleMessages(results_filtered, conversation_id); 1005 | 1006 | return results_filtered; 1007 | } 1008 | 1009 | exports.getActualLastMessageFromConversation = async function(conversation_id) { 1010 | var Log = new LogLib.Log("SMServerAPI.js","getActualLastMessageFromConversation"); 1011 | Log.v("Getting actual last message from conversation "+conversation_id+" (after "+start_time+")") 1012 | // console.log("convo id: "); 1013 | // console.log(conversation_id); 1014 | 1015 | //TODO: Maybe log all messages from after the message is sent, to avoid them getting lost? 1016 | Log.v("Fetching results"); 1017 | var results = (await SMServerFetch("/requests", {messages: conversation_id, num_messages: 1, read_messages: false, messages_offset: 0})).texts; //TODO: Should this use getAllMessagesWhileConditionIsTrue? 1018 | 1019 | //TODO: Add conversion step that: 1020 | // Converts date into UNIX milliseconds timestamp 1021 | // Adds the conversation_id field (double-check the actual name (is it conversation_id?) as I'm not sure I remember correctly) 1022 | // (possibly) stacks tapbacks 1023 | 1024 | Log.vv(JSON.stringify(results)); 1025 | Log.v("Finished getting last message from "+conversation_id); 1026 | results = exports.fixFormattingForMultipleMessages(results, conversation_id); 1027 | 1028 | return results[0]; 1029 | } 1030 | 1031 | //TODO: Figure out which sent messages are which (i.e. duplicates) by forcing each message to 1032 | //be unique. If a message is sent that exactly matches a message from before, add an invisible 1033 | //unicode character to tell it apart. 1034 | 1035 | exports.getAllConversations = async function() { 1036 | var Log = new LogLib.Log("SMServerAPI.js","getAllConversations"); 1037 | Log.v("Fetching all conversations"); 1038 | //TODO: What to do about SMServer returning 'ECONNRESET'? 1039 | var loopFinished = false; 1040 | var offset = 0; 1041 | var chunkSize = 100; 1042 | var conversations = []; 1043 | 1044 | while (!loopFinished) { 1045 | Log.v("Fetching "+chunkSize+" conversations"); 1046 | var results = await SMServerFetch("/requests", {chats: chunkSize, chats_offset: offset}) 1047 | // console.log(results.chats.length); 1048 | if (results.chats.length < chunkSize) { 1049 | loopFinished = true; 1050 | } 1051 | // for (var i = 0; i < results.chats.length; i++) { 1052 | // //AirMessage needs UUIDs for each conversation, but SMServer only gives us GUIDs for 1053 | // //individual messages. ConversionDatabase is used to randomly generate UUIDS and match 1054 | // //them with the chat ID format from SMServer, for easy conversion. 1055 | // // ConversionDatabase.saveGUIDAssociation(results.chats[i].chat_identifier); 1056 | // ConversionDatabase.ensureUUIDExists(results.chats[i].chat_identifier); 1057 | // } 1058 | conversations = results.chats.concat(conversations); //Adds the results to the end of the conversations array 1059 | offset += chunkSize; 1060 | } 1061 | Log.v("Fetching all conversations finished, "+conversations.length+" conversations found"); 1062 | return conversations; 1063 | } 1064 | 1065 | exports.getConversationsAfterTime = async function(time_lower) { //time lower is in UNIX seconds 1066 | var Log = new LogLib.Log("SMServerAPI.js","getConversationsAfterTime"); 1067 | Log.v("Getting all conversations after time "+time_lower); 1068 | //We only care if the newest message is newer than time_lower 1069 | //Accesses list of all conversations 1070 | // console.log("time_lower: "+time_lower); 1071 | var conversations = await exports.getAllConversations(); 1072 | Log.v("Got all conversations. Filtering by time of last message..."); 1073 | 1074 | //TODO: Convert all of this to use the message.unixstamp property instead of using the ConversionDatabase every time 1075 | 1076 | //TODO: Continuously fetch until we don't need to fetch anymore instead of fetching ALL the conversations, which might be unnecessary 1077 | var filtered = conversations.filter((item) => { 1078 | // console.log(item.time_marker); 1079 | var unixstamp = ConversionDatabase.convertAppleDateToUnixTimestamp(item.time_marker); 1080 | // console.log(unixstamp); 1081 | //time_marker is the date the last text was sent, in Apple's time format. 1082 | //If the last message was sent before the time_lower date, we throw it out. 1083 | //If the last message was sent after the time_upper date, we don't know if 1084 | // there are other messages in there during the window so we add the 1085 | // conversation just to be safe. 1086 | // console.log(unixstamp+" vs "+time_lower); 1087 | // console.log(unixstamp >= time_lower); 1088 | // console.log("Cutoff: "+new Date(time_lower)); 1089 | // console.log(item.display_name+": "+new Date(unixstamp)+": Is above cutoff? "+(unixstamp >= time_lower)); 1090 | return unixstamp >= time_lower; 1091 | }); 1092 | Log.v("Conversation fetch finished"); 1093 | return filtered; 1094 | } 1095 | 1096 | exports.findMessageByGUID = async function(message_guid) { 1097 | var Log = new LogLib.Log("SMServerAPI.js", "findMessageByGUID"); 1098 | var associatedMessage = null; 1099 | await exports.getAllMessagesWhileConditionIsTrue((message) => { 1100 | if (message.guid == message_guid) { 1101 | // console.log("Found message!"); 1102 | // console.log(message); 1103 | Log.v("Found message that matches GUID: "+message.text); 1104 | associatedMessage = message; 1105 | return false; 1106 | } else { 1107 | return true; 1108 | } 1109 | }); 1110 | return associatedMessage; 1111 | } 1112 | 1113 | //TODO: Find out if group chat name changed or not 1114 | 1115 | //How to find the users in a group? Worst case scenario = guess based on who sent what 1116 | // exports.getMessagesFromSpecifiedChats = async function(chat_identifiers) { 1117 | // var Log = new LogLib.Log("SMServerAPI.js","getMessagesFromSpecifiedChats"); 1118 | // //TODO: Add support for tapbacks and other messages where the text is '' 1119 | // //associated_message_guid is not '' when it is a tapback 1120 | // 1121 | // //TODO: Add array support for chat_identifiers 1122 | // //WHY DOES THIS FAIL SOMETIMES? 1123 | // 1124 | // var results = await SMServerFetch("/requests", {messages: chat_identifiers, num_message: 100, read_messages: false}); 1125 | // //TODO: Looks like tapbacks are included. How do we filter them out? 1126 | // return results; 1127 | // } 1128 | 1129 | //TODO: ID retrieval? 1130 | 1131 | //Sending a first-time text message requires something to send! 1132 | //TODO: Add contact sending with /requests?name=name@example.com ? 1133 | 1134 | // exports.searchForOneConversation = async function(conversation_id) { 1135 | // var results = await SMServerFetch("/requests", {search: conversation_id}); 1136 | // return results; 1137 | // } 1138 | // exports.searchForOneConversation = async function(chat_identifier) { 1139 | // 1140 | // 1141 | // 1142 | // //chat_identifiers should be an array 1143 | // 1144 | // // var conversations = exports.getAllConversations(); 1145 | // // 1146 | // // var filtered_conversations = []; 1147 | // // 1148 | // // for (var i = 0; i < conversations.length; i++) { 1149 | // // if (conersations[i].chat_identifier in chat_identifiers) { 1150 | // // filtered_conversations.push(conversations[i]); 1151 | // // } 1152 | // // } 1153 | // 1154 | // var results = await SMServerFetch("/requests", {match: chat_identifier, match_type: "chat"}); 1155 | // var finalresults = []; 1156 | // for (var i = 0; i < results.matches.length; i++) { 1157 | // if (results.matches[i].chat_id == chat_identifier) { 1158 | // finalresults.push(results.matches[i]); 1159 | // } 1160 | // } 1161 | // if (finalresults.length > 1) { 1162 | // console.log("Got too many conversation items back from SMServer! "+JSON.stringify(finalresults)); 1163 | // } 1164 | // return finalresults[0]; 1165 | // /* 1166 | // Returns something like: 1167 | // [ 1168 | // { 1169 | // "display_name": "Firstame ", 1170 | // "chat_id": "+11234567890" 1171 | // } 1172 | // ] 1173 | // 1174 | // */ 1175 | // } 1176 | 1177 | exports.searchForMultipleConversations = async function(chat_identifiers) { 1178 | var Log = new LogLib.Log("SMServerAPI.js","searchForMultipleConversations"); 1179 | var conversations = await exports.getAllConversations(); 1180 | //TODO: Promise.all() this! Maybe write a custom function that retries errors too, like the dreaded ECONNRESET? 1181 | var filtered_conversations = []; 1182 | 1183 | for (var i = 0; i < conversations.length; i++) { 1184 | // console.log(conversations[i]); 1185 | if (chat_identifiers.indexOf(conversations[i].chat_identifier) > -1) { 1186 | filtered_conversations.push(conversations[i]); 1187 | } 1188 | } 1189 | 1190 | //TODO: Set availability as true or false depending on if there is a chat Fidentifier missing in the returned conversations list 1191 | return filtered_conversations; 1192 | } 1193 | 1194 | 1195 | //TODO: Set up a function that the client runs (with the last known time_lower) that constantly checks and waits for new messages. 1196 | // Then resCb the promise as soon as it's done. It's up to Client.js to await this and put it in a forever loop. 1197 | // What about client-originated requests? If everything is all good, they shouldn't matter (bc the client would already be up to date). 1198 | // If the client disconnects, THE LOOP SHOULD STOP RUNNING. 1199 | 1200 | 1201 | 1202 | 1203 | //What do we need? 1204 | // Chat identifier 1205 | // Conversation display name (if group) 1206 | // Members 1207 | 1208 | async function dostuff() { 1209 | console.log("\n\n\n"); 1210 | await exports.authenticate(); 1211 | 1212 | setInterval(exports.authenticate, 500 * 1000); //Every 500 seconds (8.3ish minutes) it re-authenticates with SMServer, just to be safe 1213 | //TODO: Continuously try to authenticate if it fails, send the client a message that SMServer isn't connecting 1214 | 1215 | 1216 | // exports.sendTextMessage(1,2,3); 1217 | // exports.getAllMessagesFromSpecifiedTime(1,2); 1218 | // console.log(await exports.getListOfChats()); 1219 | //chat16846957693768777 1220 | // console.log(await exports.getMessagesFromSpecifiedChats("chat16846957693768777")); 1221 | // console.log(JSON.stringify(await exports.getMessagesFromSpecifiedChats("name@example.com"))); 1222 | // console.log(await exports.getMessagesFromSpecifiedChats("name@example.com")); 1223 | // var results = await exports.getMessagesFromSpecifiedChats("name@example.com"); 1224 | // console.log(JSON.stringify(results, null, 4)); 1225 | // console.log(await exports.getConversationsAfterTime(1623964100.4529998)); 1226 | 1227 | // console.log(await exports.getAllConversations()); 1228 | // console.log(await exports.getAllMessagesFromSpecifiedTimeInterval(1623963720, 1623964920)); 1229 | // console.log(await exports.getAllMessagesFromSpecifiedTimeInterval(0, 999999999999999999)); 1230 | // var messages = await exports.getAllMessagesFromSpecifiedTimeInterval(0, 9999999999999999); 1231 | // console.log(messages); 1232 | // console.log(JSON.stringify(await exports.stackTapbacks(messages), null, 4)); 1233 | 1234 | // 645656520000000000 in apple time 1235 | // console.log(JSON.stringify(await exports.searchForMultipleConversations(['+12068514105', 'name@example.com']), null, 4)); 1236 | // console.log(JSON.stringify(await exports.sendTextMessage('This is another automated message test', 'name@example.com'))); 1237 | // console.log(await exports.downloadAttachmentIfNecessary({ 1238 | // "mime_type": "image'jpeg", 1239 | // "filename": "Attachments/33/03/6DA2243B-E98C-4B97-8104-E02882B9F2F5/64707007403__6AF2C454-0578-4355-ADD1-CE6E25FEBA27.JPG" 1240 | // })); 1241 | // console.log(await exports.getLastMessageFromConversation("name@example.com",0)); 1242 | } 1243 | 1244 | //TODO: How to send delivery updates? 1245 | dostuff(); 1246 | -------------------------------------------------------------------------------- /SMServerWebsocket.js: -------------------------------------------------------------------------------- 1 | var WebSocketClient = require('websocket').client; 2 | const LogLib = require("./Log.js"); 3 | var Log = new LogLib.Log("SMServerWebsocket.js", "SMServerWebsocket.js"); 4 | 5 | const SMServerAPI = require("./SMServerAPI.js"); 6 | const SettingsManager = require("./settingsManager.js"); 7 | 8 | process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0; 9 | 10 | //TODO: Maybe auto-convert 11 | 12 | var eventListeners = { 13 | "message": [], 14 | "read": [], 15 | "battery_level": [], 16 | "battery_charging": [] 17 | } 18 | 19 | var client = new WebSocketClient(); 20 | 21 | var websocketIsOpen = false; 22 | 23 | client.on('connectFailed', function(error) { 24 | console.log('Connect Error: ' + error.toString()); 25 | }); 26 | 27 | client.on('connect', function(connection) { 28 | websocketIsOpen = true; 29 | Log.g('SMServer WebSocket Client Connected'); 30 | connection.on('error', function(error) { 31 | // console.log("Connection Error: " + error.toString()); 32 | Log.e("Websocket connection error: "+error.toString()); 33 | //Should we set websocketIsOpen to false here?? 34 | }); 35 | connection.on('close', function() { 36 | //TODO: Detect if the connection is closed due to no password and run the SMServerAPI.authenticate() 37 | 38 | SMServerAPI.authenticate(); //TODO: Is this too spammy? 39 | // console.log('Connection Closed'); 40 | 41 | Log.w("Websocket connection closed"); 42 | websocketIsOpen = false; 43 | }); 44 | connection.on('message', function(message) { 45 | // console.log("Got a message"); 46 | if (message.type === 'utf8') { 47 | // Log.i("New message via websocket "+message.utf8Data); 48 | //TODO: What about many battery messages? 49 | //TODO: Yell at client if the battery is low 50 | // console.log("Received: '" + message.utf8Data + "'"); 51 | var message_parts = message.utf8Data.match(/^(\w+)\:(.*)/s); //Regex to separate the message type (before and after the colon :) 52 | // console.log(message_parts); 53 | var message_type = message_parts[1]; 54 | var message_data = message_parts[2]; 55 | // Log.v("Message type is "+message_type); 56 | if (message_type == "text") { 57 | Log.i("New text message via websocket!"); 58 | // console.log(message_data); 59 | var jsondata = JSON.parse(message_data).text; 60 | // console.log(jsondata); 61 | // console.log(jsondata.guid+": "+jsondata.text); 62 | 63 | //TODO: If it's a link, refresh all recent messages as I'm not sure if the link gets returned with the websocket 64 | jsondata.conversation_id = jsondata.chat_identifier; 65 | jsondata.tapbacks = []; 66 | var messages = [jsondata]; 67 | SMServerAPI.fixFormattingForMultipleMessages(messages, jsondata.chat_identifier); //Maybe fix it for just the one? 68 | // Log.v("Sending event 'message' with text contents"); 69 | 70 | // console.log(messages); 71 | sendEvent("message",messages); 72 | 73 | //>>>TODO: Stack tapbacks?? By default everywhere?? 74 | //TODO: Also add conversation_id: it's chat_identifier 75 | } else if (message_type == "read") { 76 | // Log.i(">>>>>>>>>>>>>>>>>>>>New read update via websocket!"); 77 | //read:{"date":"17:19","guid":"EEDA85EA-0E74-4C93-BF55-204124F97F80"} 78 | var jsondata = JSON.parse(message_data); 79 | sendEvent("read", jsondata); 80 | } else if (message_type == "battery") { 81 | if (Number(message_data) !== NaN) { 82 | sendEvent("battery_level", Number(message_data)); 83 | } else { 84 | sendEvent("battery_charging", message_data == "charging") 85 | } 86 | } 87 | 88 | //i.e. also include 'battery:unplugged' and 'battery:12.3456789' 89 | } 90 | return false; 91 | }); 92 | 93 | //TODO: Wait, it sends messages while you're typing? 94 | //It includes text but gives it to you before you finish typing 95 | 96 | 97 | // function sendNumber() { 98 | // if (connection.connected) { 99 | // var number = Math.round(Math.random() * 0xFFFFFF); 100 | // connection.sendUTF(number.toString()); 101 | // setTimeout(sendNumber, 1000); 102 | // } 103 | // } 104 | // sendNumber(); 105 | }); 106 | 107 | function sendEvent(eventType, data) { 108 | for (var i = 0; i < eventListeners[eventType].length; i++) { 109 | console.log("Sending to listener: "+data); 110 | eventListeners[eventType][i].callback(data); 111 | } 112 | } 113 | 114 | exports.addEventListener = function(eventType, callback) { 115 | if (eventListeners.hasOwnProperty(eventType)) { 116 | //Add the event 117 | var eventID = Math.random(); 118 | eventListeners[eventType].push({ 119 | "id": eventID, 120 | "callback": callback 121 | }); 122 | return eventID; 123 | } else { 124 | Log.w("Event "+eventType+" does not exist!"); 125 | } 126 | } 127 | 128 | exports.removeEventListener = function(eventID) { 129 | for (eventType in eventListeners) { 130 | if (eventListeners.hasOwnProperty(eventType)) { 131 | //Loops through each event handler in each event 132 | for (var i = 0; i < eventListeners[eventType].length; i++) { 133 | if (eventListeners[eventType][i].id == eventID) { 134 | eventListeners[eventType].splice(i, 1); 135 | //Searches for the event and removes it 136 | return; 137 | } 138 | } 139 | } 140 | } 141 | Log.w("Couldn't remove event "+eventID+" as it wasn't found in the event handlers list."); 142 | } 143 | 144 | // client.connect('ws://192.168.1.38:8740/', 'echo-protocol'); //TODO: Set this IP address according to user settings 145 | //TODO: Set this IP address according to user settings 146 | //TODO: Continuously try to reconnect if the connection drops 147 | setInterval(async() => { 148 | if (!websocketIsOpen) { 149 | var SERVER_IP = await SettingsManager.readSetting("SMSERVER_IP"); 150 | var SERVER_WS_PORT = await SettingsManager.readSetting("SMSERVER_WEBSOCKET_PORT") 151 | Log.i("Trying to reconnect to websocket..."); 152 | // client.connect('wss://192.168.1.38:8740/'); 153 | client.connect('wss://'+SERVER_IP+":"+SERVER_WS_PORT+"/"); 154 | } 155 | }, 1000); 156 | 157 | //TODO: Keep the connection alive 158 | Log.i("Started listening to websocket"); 159 | 160 | //TODO: Maybe authenticate via SMServerAPI? 161 | -------------------------------------------------------------------------------- /conversionDatabase.js: -------------------------------------------------------------------------------- 1 | const {v4: uuidv4} = require('uuid'); 2 | const LogLib = require("./Log.js"); 3 | const fs = require('fs'); 4 | 5 | function getKeyFromValue(object, value) { 6 | return Object.keys(object).find(key => object[key] === value); 7 | } 8 | 9 | //TODO: Create the file if it doesn't exist!!! Add a getFileDataOrCreateIfItDoesNotExist() function? 10 | 11 | function getDatabase() { 12 | try { 13 | var data = fs.readFileSync("./conversionDatabase.json"); 14 | var jsondata = JSON.parse(data); 15 | return jsondata; 16 | } catch (err) { 17 | Log.e("CRITICAL ERROR: conversionDatabase has been corrupted and isn't valid JSON!") 18 | //TODO: Send to client (as text) here? 19 | } 20 | } 21 | 22 | function updateDatabase(newDatabase) { 23 | try { 24 | fs.writeFileSync("./conversionDatabase.json", JSON.stringify(newDatabase)); 25 | } catch (err) { 26 | Log.e("ERROR: Could not write to conversionDatabase: "+err); 27 | } 28 | } 29 | 30 | function resetDatabase() { 31 | var emptyDb = { 32 | "installationId": "", 33 | "conversationIdToUUIDConversionTable": {}, 34 | "savedAttachmentPaths": {}, 35 | "infoMessageROWID": 1000000 36 | } 37 | updateDatabase(emptyDb); 38 | } 39 | 40 | // exports.chatIDToGUID = function(chatID) { 41 | // var db = getDatabase(); 42 | // 43 | // if (chatID in db.conversationIdToUUIDConversionTable) { 44 | // return db.conversationIdToUUIDConversionTable[chatID]; 45 | // } else { 46 | // var newUUID = uuidv4(); 47 | // db.conversationIdToUUIDConversionTable[chatID] = newUUID; 48 | // updateDatabase(db); 49 | // return newUUID; 50 | // // return ""; 51 | // } 52 | // } 53 | // 54 | // exports.saveGUIDAssociation = function(chatID, chatUUID) { 55 | // var db = getDatabase(); 56 | // if (chatID in db.conversationIdToUUIDConversionTable && dv.conversationIdToUUIDConversionTable[chatID] == chatUUID) { 57 | // return; //Do nothing, as the UUID already exists in the database and matches what we're trying to set 58 | // } else { 59 | // // var newUUID = uuidv4(); 60 | // db.conversationIdToUUIDConversionTable[chatID] = chatUUID; 61 | // updateDatabase(db); 62 | // // return newUUID; 63 | // } 64 | // } 65 | // 66 | // exports.ensureUUIDExists = function(chatID) { 67 | // var db = getDatabase(); 68 | // if (chatID in db.conversationIdToUUIDConversionTable) { 69 | // return; //Do nothing, as the UUID already exists in the database 70 | // } else { 71 | // var newUUID = uuidv4(); 72 | // db.conversationIdToUUIDConversionTable[chatID] = newUUID; 73 | // updateDatabase(db); 74 | // // return newUUID; 75 | // } 76 | // } 77 | // 78 | // exports.UUIDToChatID = function(chatUUID) { 79 | // //If it doesn't exist, complain because have nothing! 80 | // var db = getDatabase(); 81 | // var chatID = getKeyFromValue(db.conversationIdToUUIDConversionTable, chatUUID); 82 | // if (chatID == undefined) { 83 | // Log.e("ERROR: Couldn't reverse GUID to Chat ID as the chat ID doesn't exist"); 84 | // } 85 | // return chatID; //Undefined if it doesn't exist 86 | // } 87 | 88 | exports.getInstallationID = function() { 89 | var db = getDatabase(); 90 | if (db.installationId == "") { 91 | db.installationId = uuidv4(); 92 | updateDatabase(db); 93 | } 94 | return db.installationId; 95 | } 96 | 97 | exports.convertAppleDateToUnixTimestamp = function(date) { 98 | return Math.floor(((date / 1000000000) + 978307200) * 1000); //Returns a timestamp in UNIX milliseconds. 99 | //TODO: Convert this to milliseconds and make EVERYTHING be done in milliseconds 100 | } 101 | 102 | exports.convertUnixTimestampToAppleDate = function(date) { 103 | return Math.floor(((date / 1000) - 978307200) * 1000000000); //Takes a timestamp in UNIX milliseconds 104 | } 105 | 106 | exports.getUint8ArrayAsPrettyString = function(inarr) { 107 | var data = JSON.parse(JSON.stringify(inarr)); 108 | var maxindex = 0; 109 | for (property in data) { 110 | if (data.hasOwnProperty(property)) { 111 | if (Number(property) > maxindex) { 112 | maxindex = Number(property); 113 | } 114 | } 115 | } 116 | 117 | var finalarr = []; 118 | 119 | for (var i = 0; i <= maxindex; i++) { 120 | finalarr.push(data[i]); 121 | } 122 | // > XXX, 123 | finalstr = ""; 124 | for (var i = 0; i < finalarr.length; i++) { 125 | if (i % 4 == 0) { 126 | finalstr += "\n."; 127 | } 128 | 129 | var numSpaces = 1; 130 | var stringified = finalarr[i].toString(); 131 | numSpaces += (3 - stringified.length); //Adds appropriate padding 132 | for (var j = 0; j < numSpaces; j++) { 133 | finalstr += " "; 134 | } 135 | 136 | finalstr += stringified + ","; 137 | } 138 | 139 | return finalstr; 140 | } 141 | 142 | exports.printUint8Array = function(inarr) { 143 | console.log(exports.getUint8ArrayAsPrettyString(inarr)); 144 | } 145 | 146 | //TODO: Keep track of attachment paths when conversations are dumped 147 | //TODO: Keep track of downloaded attachments 148 | exports.getAttachmentSavePath = function(smserver_filename) { 149 | var Log = new LogLib.Log("conversionDatabase.js", "getAttachmentSavePath"); 150 | //Here is where the server should keep track of which attachments live where 151 | //Let's keep the filename the same to make things easy. 152 | //Filename example: "Attachments/32/02/26ADB495-9E1C-44F2-B0E6-176A737D7F45/jpeg-image-JgOYM9.jpeg" 153 | //Filename example: "Attachments/33/03/6DA2243B-E98C-4B97-8104-E02882B9F2F5/64707007403__6AF2C454-0578-4355-ADD1-CE6E25FEBA27.JPG" 154 | //Mapping example: "Attachments/32/02/26ADB495-9E1C-44F2-B0E6-176A737D7F45/jpeg-image-JgOYM9.jpeg": "/e4b3df3f-de31-4bf5-adfd-93bee41ff0e8.JPG" 155 | 156 | //savedAttachmentPaths 157 | Log.v("Getting attachment save path for "+smserver_filename); 158 | var db = getDatabase(); 159 | //uuidv4() 160 | var attachmentPath = exports.checkIfAttachmentAlreadySaved(smserver_filename); 161 | if (attachmentPath) { //attachmentPath is undefined if it doesn't exist 162 | Log.v("Attachment path exists: "+attachmentPath); 163 | return attachmentPath; 164 | } 165 | var fileExtension = smserver_filename.match(/(\.[^\/]+)$/)[1]; //Slices out the file extension (ex. ".jpeg" or ".JPG" or ".mp4" or whatever) 166 | //For security reasons (mainly directory traversal), no slashes are allowed in the extension (so ../index as the filename would would return nothing for the extension. ../index.js would return ".js") 167 | var filenameToSave = uuidv4() + fileExtension; 168 | db.savedAttachmentPaths[smserver_filename] = filenameToSave; 169 | updateDatabase(db); 170 | return filenameToSave; 171 | } 172 | //TODO: Do attachment GUIDs matter? 173 | exports.checkIfAttachmentAlreadySaved = function(smserver_filename) { 174 | var db = getDatabase(); 175 | return db.savedAttachmentPaths[smserver_filename]; //Undefined if it doesn't exist, otherwise it's the filename 176 | } 177 | 178 | exports.isPhoneNumber = function(identifier) { 179 | //Code to check if an identifier is a phone number or email 180 | return (identifier.indexOf("@") == -1); //If an email @ is nowhere to be found, it's assumed to be a phone number 181 | } 182 | 183 | exports.getInternationalPhoneNumberFormat = function(phone_number) { 184 | //TODO: Set the "default" country code in the settings? 185 | //Samples: 186 | //+44 7911 123456 187 | //(123) 456-7890 188 | //1234567890 189 | //123-456-7890 190 | //11234567890 (Includes country code without the plus. I'm disregarding this one as that makes it way more complex) 191 | 192 | //How this works: 193 | //This basically disregards any punctuation and sticks the numbers 194 | //all together in a line. Country code is added if there's no plus (+) present. 195 | 196 | var intlFormat = ""; 197 | if (phone_number.indexOf("+") > -1) { 198 | intlFormat += "+"; //Country number is taken care of later. 199 | } else { 200 | intlFormat += "+1"; //TODO: Replace this with the country code in settings 201 | } 202 | 203 | for (var i = 0; i < phone_number.length; i++) { 204 | if (/^\d$/.test(phone_number.charAt(i))) { //If the current index is a number 205 | //Add it to the international format string 206 | intlFormat += phone_number.charAt(i); 207 | } 208 | } 209 | return intlFormat; 210 | } 211 | 212 | exports.getInfoMessageROWID = function() { 213 | var db = getDatabase(); 214 | if (db.infoMessageROWID == undefined || Number(db.infoMessageROWID) == NaN) { 215 | db.infoMessageROWID = 999999; //I hope this fits in a long 216 | } else { 217 | db.infoMessageROWID = db.infoMessageROWID + 1; 218 | } 219 | updateDatabase(db); 220 | return db.infoMessageROWID; 221 | } 222 | 223 | exports.promiseStats = function(filename) { 224 | return new Promise((resCb, rejCb) => { 225 | fs.stat(filename, (err, stats) => { 226 | if (err) { 227 | rejCb(err); 228 | } else { 229 | resCb(stats); 230 | } 231 | }); 232 | }); 233 | } 234 | -------------------------------------------------------------------------------- /conversionDatabase.json: -------------------------------------------------------------------------------- 1 | { 2 | "installationId": "", 3 | "conversationIdToUUIDConversionTable": {}, 4 | "savedAttachmentPaths": {}, 5 | "infoMessageROWID": 1000000 6 | } -------------------------------------------------------------------------------- /encryption_test.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | var ByteBuffer = require('byte-buffer'); 3 | const SettingsManager = require("./settingsManager.js"); 4 | const LogLib = require("./Log.js"); 5 | 6 | const SALT_LENGTH = 8; //8-byte salt 7 | const IV_LENGTH = 12; //12-byte IV 8 | const KEY_LENGTH = 16; //The encryption uses a 16-byte key 9 | const AUTHTAG_LENGTH = 16; 10 | const KEY_ITERATIONS = 10000; 11 | 12 | const KEY_DIGEST = 'sha256'; 13 | const ENCRYPTION_ALGORITHM = 'aes-128-gcm'; 14 | 15 | exports.SALT_LENGTH = SALT_LENGTH; 16 | exports.IV_LENGTH = IV_LENGTH; 17 | exports.SKEY_LENGTH = KEY_LENGTH; 18 | 19 | //TODO: What about crashes when the password is wrong? 20 | 21 | 22 | //TODO: Add error handling and retrying for "Unsupported state or unable to authenticate data" 23 | 24 | function decryptThisAlreadyDangit(message) { 25 | //The incoming message should have the salt, iv, and encrypted message (encrypted message = ciphertext stuck with authTag) all stuck together. 26 | //The following variables slice apart the message into its respective components 27 | var salt = message.slice(0, SALT_LENGTH); 28 | var iv = message.slice(SALT_LENGTH, SALT_LENGTH + IV_LENGTH); 29 | var encrypted = message.slice(SALT_LENGTH + IV_LENGTH, message.length); 30 | //What message is sent back if the password is wrong? 31 | return decryptWithSaltIVAndData(salt, iv, encrypted); //This returns a Promise 32 | } 33 | 34 | 35 | function decryptWithSaltIVAndData(salt, iv, encryptedWithAuthTag) { 36 | var Log = new LogLib.Log("encryption_test.js", "decryptWithSaltIVAndData"); 37 | return new Promise(async (resCb, rejCb) => { 38 | var password = await SettingsManager.readSetting("AIRMESSAGE_PASSWORD"); 39 | 40 | var encrypted = encryptedWithAuthTag.slice(0, encryptedWithAuthTag.length - AUTHTAG_LENGTH); 41 | var authTag = encryptedWithAuthTag.slice(encryptedWithAuthTag.length - AUTHTAG_LENGTH, encryptedWithAuthTag.length); 42 | //For more info on the authTag business, check out the encryption function for more details. 43 | //The authTag is the last 16 byes of the encrypted message 44 | 45 | crypto.pbkdf2(password, salt, KEY_ITERATIONS, KEY_LENGTH, KEY_DIGEST, (err, derivedKey) => { 46 | // This gets us the key. It's a symmetric key, so used for both encryption and decryption 47 | // This converts the user-supplied AirMessage password into the key that was used to encrypt the message 48 | 49 | var decipher = crypto.createDecipheriv(ENCRYPTION_ALGORITHM, derivedKey, iv); //Create a cipher with the key and iv 50 | decipher.setAuthTag(authTag); //The infamous authTag. See the encrypt function for more info 51 | 52 | 53 | var decrypted = new ByteBuffer(); //Creates a byte buffer to add the encrypted data to. 54 | 55 | var decryptedchunk = decipher.update(encrypted); //This pipes our encrypted data into the cipher and gets the decrypted data. 56 | decrypted.append(decryptedchunk.length); //Allocates space for the decrypted chunk in the byte buffer 57 | decrypted.write(decryptedchunk); //Writes the decrypted data to the byte buffer 58 | try { 59 | decipher.final(); //Finishes the encryption. Some encryption methods put some extra stuff at the end, but 60 | //GCM doesn't, so cipher.final() just returns an empty buffer every time. 61 | //TODO: Add error handling to this where it tries to decrypt again 62 | } catch (err) { 63 | Log.w(err); 64 | } 65 | 66 | resCb(decrypted.raw); //Returns the decrypted data 67 | }); 68 | }); 69 | } 70 | 71 | function encrypt(data) { 72 | // This drove me crazy for a solid week, so I'm letting you know what the heck this is doing 73 | // So you hopefully don't have to deal with the same crap I did: 74 | // 75 | // So GCM encryption is weird. In addition to requiring the salt, IV, and encrypted data, it 76 | // also likes to have something called an authTag. This is basically a checksum that's signed 77 | // by the encryption key (I think). It's not strictly necessary to decrypt the data, but many 78 | // programming languages (ex. Java) expect it in order to decrypt anything at all. 79 | // 80 | // So, the decryption should work fine if you only care about the encrypted data and pay no 81 | // attention to the authTag. HOWEVER, if you try to encrypt data without including the authTag 82 | // at the end (it's usually 16 bytes), many other programming languages (cough cough, Java) that 83 | // expect the authTag will get real mad and crash. Node for some reason doesn't include it by 84 | // default at the end of the encrypted data, so you need to call cipher.getAuthTag() and stick 85 | // the buffer to the end manually. If you don't do this, you'll spend a week wondering why other 86 | // languages output encrypted data that's 16 bytes longer. 87 | 88 | 89 | return new Promise(async(resCb, rejCb) => { 90 | var password = await SettingsManager.readSetting("AIRMESSAGE_PASSWORD"); 91 | 92 | var salt = crypto.randomBytes(SALT_LENGTH); //Generates a random 8-byte salt used to derive the key from the password 93 | 94 | crypto.pbkdf2(password, salt, KEY_ITERATIONS, KEY_LENGTH, KEY_DIGEST, (err, derivedKey) => { 95 | // This gets us the key. It's a symmetric key, so used for both encryption and decryption 96 | // This converts the user-supplied AirMessage password into the key that was used to encrypt the message 97 | 98 | var iv = crypto.randomBytes(IV_LENGTH); //Generate 12 bytes of secure random noise for the initialization vector 99 | 100 | var cipher = crypto.createCipheriv(ENCRYPTION_ALGORITHM, derivedKey, iv); //Create a cipher with the key and iv 101 | 102 | // cipher.setAutoPadding(true); 103 | 104 | var encrypted = new ByteBuffer(); //Creates a byte buffer to add the encrypted data to. 105 | 106 | var encryptedchunk = cipher.update(data); //This pipes our unencrypted data into the cipher and gets the encrypted data. 107 | encrypted.append(encryptedchunk.length); //Allocates space for the encrypted chunk in the byte buffer 108 | encrypted.write(encryptedchunk); //Writes the encrypted data to the byte buffer 109 | 110 | cipher.final(); //Finishes the encryption. Some encryption methods have some extra stuff at the end, but 111 | //GCM doesn't, so cipher.final() just returns an empty buffer every time. 112 | 113 | var authTag = cipher.getAuthTag(); //Gets the infamous authTag. Should be 16 bytes every time. 114 | 115 | encrypted.append(authTag.length); //Allocates space for the AuthTag. Should always be 16 bytes 116 | encrypted.write(authTag); //Writes the authTag right up next to the encrypted data. 117 | 118 | resCb([salt, iv, encrypted.raw]); 119 | }); 120 | }); 121 | } 122 | 123 | // decryptThisAlreadyDangit(message).then((data) => { 124 | // testbuf = new Buffer(data); 125 | // console.log(testbuf.toString('hex').match(/../g).join(' ')); 126 | // }); 127 | 128 | exports.decryptThisAlreadyDangit = decryptThisAlreadyDangit; 129 | exports.decryptWithSaltIVAndData = decryptWithSaltIVAndData; 130 | exports.encrypt = encrypt; 131 | 132 | // (async function() { 133 | // 134 | // console.log("encrypting"); 135 | // var encrypted = await encrypt(dataToEncrypt); 136 | // console.log("going to print encrypted"); 137 | // console.log(encrypted); 138 | // console.log("decrypting"); 139 | // var decrypted = await decryptWithSaltIVAndData(encrypted[0], encrypted[1], encrypted[2]); 140 | // //You can also run decryptThisAlreadyDangit(message) if it contains the salt and IV 141 | // console.log(decrypted); 142 | // 143 | // 144 | // })(); 145 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const net = require('net'); 2 | const Client = require('./Client.js'); 3 | const LogLib = require("./Log.js"); 4 | var Log = new LogLib.Log("index.js", "index.js"); 5 | const SettingsManager = require("./settingsManager.js"); 6 | const SMServerAPI = require("./SMServerAPI.js"); 7 | 8 | // var AIRMESSAGE_PORT = SettingsManager.readSetting("AIRMESSAGE_PORT"); 9 | 10 | //FUTURE TODO: Integrate VNC for GamePigeon, etc 11 | //FUTURE TODO: When texting the server, send "Remote" to get a remote control link 12 | SMServerAPI.ensureAttachmentFoldersExist(); //A little race condition but it probably doesn't matter 13 | SettingsManager.readSetting("AIRMESSAGE_PORT").then((port) => { 14 | var server = net.createServer(); 15 | server.on('connection', handleConnection); 16 | server.listen(port, function() { 17 | Log.i('server listening to '+ JSON.stringify(server.address())); 18 | }); 19 | var clients = []; 20 | function handleConnection(conn) { 21 | clients.push(new Client(conn)); 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /message_packer.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SixDigitCode/AirBridge/342da6d036e2d71e4b0fd8171d0ea9f682a07f72/message_packer.js -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "airbridge", 3 | "version": "0.1.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "airbridge", 9 | "version": "0.1.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "byte-buffer": "^2.0.0", 13 | "form-data": "^4.0.0", 14 | "node-fetch": "^2.6.1", 15 | "robotjs": "^0.6.0", 16 | "uuid": "^8.3.2", 17 | "websocket": "^1.0.34" 18 | } 19 | }, 20 | "node_modules/ansi-regex": { 21 | "version": "2.1.1", 22 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", 23 | "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", 24 | "engines": { 25 | "node": ">=0.10.0" 26 | } 27 | }, 28 | "node_modules/aproba": { 29 | "version": "1.2.0", 30 | "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", 31 | "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" 32 | }, 33 | "node_modules/are-we-there-yet": { 34 | "version": "1.1.5", 35 | "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", 36 | "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", 37 | "dependencies": { 38 | "delegates": "^1.0.0", 39 | "readable-stream": "^2.0.6" 40 | } 41 | }, 42 | "node_modules/asynckit": { 43 | "version": "0.4.0", 44 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 45 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 46 | }, 47 | "node_modules/base64-js": { 48 | "version": "1.5.1", 49 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 50 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", 51 | "funding": [ 52 | { 53 | "type": "github", 54 | "url": "https://github.com/sponsors/feross" 55 | }, 56 | { 57 | "type": "patreon", 58 | "url": "https://www.patreon.com/feross" 59 | }, 60 | { 61 | "type": "consulting", 62 | "url": "https://feross.org/support" 63 | } 64 | ] 65 | }, 66 | "node_modules/bl": { 67 | "version": "4.1.0", 68 | "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", 69 | "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", 70 | "dependencies": { 71 | "buffer": "^5.5.0", 72 | "inherits": "^2.0.4", 73 | "readable-stream": "^3.4.0" 74 | } 75 | }, 76 | "node_modules/bl/node_modules/readable-stream": { 77 | "version": "3.6.0", 78 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", 79 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", 80 | "dependencies": { 81 | "inherits": "^2.0.3", 82 | "string_decoder": "^1.1.1", 83 | "util-deprecate": "^1.0.1" 84 | }, 85 | "engines": { 86 | "node": ">= 6" 87 | } 88 | }, 89 | "node_modules/buffer": { 90 | "version": "5.7.1", 91 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", 92 | "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", 93 | "funding": [ 94 | { 95 | "type": "github", 96 | "url": "https://github.com/sponsors/feross" 97 | }, 98 | { 99 | "type": "patreon", 100 | "url": "https://www.patreon.com/feross" 101 | }, 102 | { 103 | "type": "consulting", 104 | "url": "https://feross.org/support" 105 | } 106 | ], 107 | "dependencies": { 108 | "base64-js": "^1.3.1", 109 | "ieee754": "^1.1.13" 110 | } 111 | }, 112 | "node_modules/bufferutil": { 113 | "version": "4.0.3", 114 | "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.3.tgz", 115 | "integrity": "sha512-yEYTwGndELGvfXsImMBLop58eaGW+YdONi1fNjTINSY98tmMmFijBG6WXgdkfuLNt4imzQNtIE+eBp1PVpMCSw==", 116 | "hasInstallScript": true, 117 | "dependencies": { 118 | "node-gyp-build": "^4.2.0" 119 | } 120 | }, 121 | "node_modules/byte-buffer": { 122 | "version": "2.0.0", 123 | "resolved": "https://registry.npmjs.org/byte-buffer/-/byte-buffer-2.0.0.tgz", 124 | "integrity": "sha512-D5HoyMxmH3fudAYBZmfn1MF4mf6IgvSGMbNLHf/SJ1UVtj1g+9D3+jlMssNRxdDgJeh1M6gZY5lFHUIsYW44lw==" 125 | }, 126 | "node_modules/chownr": { 127 | "version": "1.1.4", 128 | "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", 129 | "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" 130 | }, 131 | "node_modules/code-point-at": { 132 | "version": "1.1.0", 133 | "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", 134 | "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", 135 | "engines": { 136 | "node": ">=0.10.0" 137 | } 138 | }, 139 | "node_modules/combined-stream": { 140 | "version": "1.0.8", 141 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 142 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 143 | "dependencies": { 144 | "delayed-stream": "~1.0.0" 145 | }, 146 | "engines": { 147 | "node": ">= 0.8" 148 | } 149 | }, 150 | "node_modules/console-control-strings": { 151 | "version": "1.1.0", 152 | "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", 153 | "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" 154 | }, 155 | "node_modules/core-util-is": { 156 | "version": "1.0.2", 157 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 158 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 159 | }, 160 | "node_modules/d": { 161 | "version": "1.0.1", 162 | "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", 163 | "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", 164 | "dependencies": { 165 | "es5-ext": "^0.10.50", 166 | "type": "^1.0.1" 167 | } 168 | }, 169 | "node_modules/debug": { 170 | "version": "2.6.9", 171 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 172 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 173 | "dependencies": { 174 | "ms": "2.0.0" 175 | } 176 | }, 177 | "node_modules/decompress-response": { 178 | "version": "4.2.1", 179 | "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", 180 | "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", 181 | "dependencies": { 182 | "mimic-response": "^2.0.0" 183 | }, 184 | "engines": { 185 | "node": ">=8" 186 | } 187 | }, 188 | "node_modules/deep-extend": { 189 | "version": "0.6.0", 190 | "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", 191 | "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", 192 | "engines": { 193 | "node": ">=4.0.0" 194 | } 195 | }, 196 | "node_modules/delayed-stream": { 197 | "version": "1.0.0", 198 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 199 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", 200 | "engines": { 201 | "node": ">=0.4.0" 202 | } 203 | }, 204 | "node_modules/delegates": { 205 | "version": "1.0.0", 206 | "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", 207 | "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" 208 | }, 209 | "node_modules/detect-libc": { 210 | "version": "1.0.3", 211 | "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", 212 | "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", 213 | "bin": { 214 | "detect-libc": "bin/detect-libc.js" 215 | }, 216 | "engines": { 217 | "node": ">=0.10" 218 | } 219 | }, 220 | "node_modules/end-of-stream": { 221 | "version": "1.4.4", 222 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", 223 | "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", 224 | "dependencies": { 225 | "once": "^1.4.0" 226 | } 227 | }, 228 | "node_modules/es5-ext": { 229 | "version": "0.10.53", 230 | "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", 231 | "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", 232 | "dependencies": { 233 | "es6-iterator": "~2.0.3", 234 | "es6-symbol": "~3.1.3", 235 | "next-tick": "~1.0.0" 236 | } 237 | }, 238 | "node_modules/es6-iterator": { 239 | "version": "2.0.3", 240 | "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", 241 | "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", 242 | "dependencies": { 243 | "d": "1", 244 | "es5-ext": "^0.10.35", 245 | "es6-symbol": "^3.1.1" 246 | } 247 | }, 248 | "node_modules/es6-symbol": { 249 | "version": "3.1.3", 250 | "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", 251 | "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", 252 | "dependencies": { 253 | "d": "^1.0.1", 254 | "ext": "^1.1.2" 255 | } 256 | }, 257 | "node_modules/expand-template": { 258 | "version": "2.0.3", 259 | "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", 260 | "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", 261 | "engines": { 262 | "node": ">=6" 263 | } 264 | }, 265 | "node_modules/ext": { 266 | "version": "1.4.0", 267 | "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", 268 | "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==", 269 | "dependencies": { 270 | "type": "^2.0.0" 271 | } 272 | }, 273 | "node_modules/ext/node_modules/type": { 274 | "version": "2.5.0", 275 | "resolved": "https://registry.npmjs.org/type/-/type-2.5.0.tgz", 276 | "integrity": "sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw==" 277 | }, 278 | "node_modules/form-data": { 279 | "version": "4.0.0", 280 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", 281 | "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", 282 | "dependencies": { 283 | "asynckit": "^0.4.0", 284 | "combined-stream": "^1.0.8", 285 | "mime-types": "^2.1.12" 286 | }, 287 | "engines": { 288 | "node": ">= 6" 289 | } 290 | }, 291 | "node_modules/fs-constants": { 292 | "version": "1.0.0", 293 | "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", 294 | "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" 295 | }, 296 | "node_modules/gauge": { 297 | "version": "2.7.4", 298 | "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", 299 | "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", 300 | "dependencies": { 301 | "aproba": "^1.0.3", 302 | "console-control-strings": "^1.0.0", 303 | "has-unicode": "^2.0.0", 304 | "object-assign": "^4.1.0", 305 | "signal-exit": "^3.0.0", 306 | "string-width": "^1.0.1", 307 | "strip-ansi": "^3.0.1", 308 | "wide-align": "^1.1.0" 309 | } 310 | }, 311 | "node_modules/github-from-package": { 312 | "version": "0.0.0", 313 | "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", 314 | "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=" 315 | }, 316 | "node_modules/has-unicode": { 317 | "version": "2.0.1", 318 | "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", 319 | "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" 320 | }, 321 | "node_modules/ieee754": { 322 | "version": "1.2.1", 323 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 324 | "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", 325 | "funding": [ 326 | { 327 | "type": "github", 328 | "url": "https://github.com/sponsors/feross" 329 | }, 330 | { 331 | "type": "patreon", 332 | "url": "https://www.patreon.com/feross" 333 | }, 334 | { 335 | "type": "consulting", 336 | "url": "https://feross.org/support" 337 | } 338 | ] 339 | }, 340 | "node_modules/inherits": { 341 | "version": "2.0.4", 342 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 343 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 344 | }, 345 | "node_modules/ini": { 346 | "version": "1.3.8", 347 | "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", 348 | "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" 349 | }, 350 | "node_modules/is-fullwidth-code-point": { 351 | "version": "1.0.0", 352 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", 353 | "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", 354 | "dependencies": { 355 | "number-is-nan": "^1.0.0" 356 | }, 357 | "engines": { 358 | "node": ">=0.10.0" 359 | } 360 | }, 361 | "node_modules/is-typedarray": { 362 | "version": "1.0.0", 363 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 364 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" 365 | }, 366 | "node_modules/isarray": { 367 | "version": "1.0.0", 368 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 369 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 370 | }, 371 | "node_modules/mime-db": { 372 | "version": "1.48.0", 373 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz", 374 | "integrity": "sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==", 375 | "engines": { 376 | "node": ">= 0.6" 377 | } 378 | }, 379 | "node_modules/mime-types": { 380 | "version": "2.1.31", 381 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.31.tgz", 382 | "integrity": "sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==", 383 | "dependencies": { 384 | "mime-db": "1.48.0" 385 | }, 386 | "engines": { 387 | "node": ">= 0.6" 388 | } 389 | }, 390 | "node_modules/mimic-response": { 391 | "version": "2.1.0", 392 | "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", 393 | "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", 394 | "engines": { 395 | "node": ">=8" 396 | }, 397 | "funding": { 398 | "url": "https://github.com/sponsors/sindresorhus" 399 | } 400 | }, 401 | "node_modules/minimist": { 402 | "version": "1.2.5", 403 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 404 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" 405 | }, 406 | "node_modules/mkdirp-classic": { 407 | "version": "0.5.3", 408 | "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", 409 | "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" 410 | }, 411 | "node_modules/ms": { 412 | "version": "2.0.0", 413 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 414 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 415 | }, 416 | "node_modules/nan": { 417 | "version": "2.14.2", 418 | "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", 419 | "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==" 420 | }, 421 | "node_modules/napi-build-utils": { 422 | "version": "1.0.2", 423 | "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", 424 | "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" 425 | }, 426 | "node_modules/next-tick": { 427 | "version": "1.0.0", 428 | "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", 429 | "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" 430 | }, 431 | "node_modules/node-abi": { 432 | "version": "2.30.0", 433 | "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.0.tgz", 434 | "integrity": "sha512-g6bZh3YCKQRdwuO/tSZZYJAw622SjsRfJ2X0Iy4sSOHZ34/sPPdVBn8fev2tj7njzLwuqPw9uMtGsGkO5kIQvg==", 435 | "dependencies": { 436 | "semver": "^5.4.1" 437 | } 438 | }, 439 | "node_modules/node-fetch": { 440 | "version": "2.6.1", 441 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", 442 | "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", 443 | "engines": { 444 | "node": "4.x || >=6.0.0" 445 | } 446 | }, 447 | "node_modules/node-gyp-build": { 448 | "version": "4.2.3", 449 | "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz", 450 | "integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg==", 451 | "bin": { 452 | "node-gyp-build": "bin.js", 453 | "node-gyp-build-optional": "optional.js", 454 | "node-gyp-build-test": "build-test.js" 455 | } 456 | }, 457 | "node_modules/noop-logger": { 458 | "version": "0.1.1", 459 | "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", 460 | "integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=" 461 | }, 462 | "node_modules/npmlog": { 463 | "version": "4.1.2", 464 | "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", 465 | "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", 466 | "dependencies": { 467 | "are-we-there-yet": "~1.1.2", 468 | "console-control-strings": "~1.1.0", 469 | "gauge": "~2.7.3", 470 | "set-blocking": "~2.0.0" 471 | } 472 | }, 473 | "node_modules/number-is-nan": { 474 | "version": "1.0.1", 475 | "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", 476 | "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", 477 | "engines": { 478 | "node": ">=0.10.0" 479 | } 480 | }, 481 | "node_modules/object-assign": { 482 | "version": "4.1.1", 483 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 484 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", 485 | "engines": { 486 | "node": ">=0.10.0" 487 | } 488 | }, 489 | "node_modules/once": { 490 | "version": "1.4.0", 491 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 492 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 493 | "dependencies": { 494 | "wrappy": "1" 495 | } 496 | }, 497 | "node_modules/prebuild-install": { 498 | "version": "5.3.6", 499 | "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.3.6.tgz", 500 | "integrity": "sha512-s8Aai8++QQGi4sSbs/M1Qku62PFK49Jm1CbgXklGz4nmHveDq0wzJkg7Na5QbnO1uNH8K7iqx2EQ/mV0MZEmOg==", 501 | "dependencies": { 502 | "detect-libc": "^1.0.3", 503 | "expand-template": "^2.0.3", 504 | "github-from-package": "0.0.0", 505 | "minimist": "^1.2.3", 506 | "mkdirp-classic": "^0.5.3", 507 | "napi-build-utils": "^1.0.1", 508 | "node-abi": "^2.7.0", 509 | "noop-logger": "^0.1.1", 510 | "npmlog": "^4.0.1", 511 | "pump": "^3.0.0", 512 | "rc": "^1.2.7", 513 | "simple-get": "^3.0.3", 514 | "tar-fs": "^2.0.0", 515 | "tunnel-agent": "^0.6.0", 516 | "which-pm-runs": "^1.0.0" 517 | }, 518 | "bin": { 519 | "prebuild-install": "bin.js" 520 | }, 521 | "engines": { 522 | "node": ">=6" 523 | } 524 | }, 525 | "node_modules/process-nextick-args": { 526 | "version": "2.0.1", 527 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", 528 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" 529 | }, 530 | "node_modules/pump": { 531 | "version": "3.0.0", 532 | "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", 533 | "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", 534 | "dependencies": { 535 | "end-of-stream": "^1.1.0", 536 | "once": "^1.3.1" 537 | } 538 | }, 539 | "node_modules/rc": { 540 | "version": "1.2.8", 541 | "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", 542 | "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", 543 | "dependencies": { 544 | "deep-extend": "^0.6.0", 545 | "ini": "~1.3.0", 546 | "minimist": "^1.2.0", 547 | "strip-json-comments": "~2.0.1" 548 | }, 549 | "bin": { 550 | "rc": "cli.js" 551 | } 552 | }, 553 | "node_modules/readable-stream": { 554 | "version": "2.3.7", 555 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", 556 | "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", 557 | "dependencies": { 558 | "core-util-is": "~1.0.0", 559 | "inherits": "~2.0.3", 560 | "isarray": "~1.0.0", 561 | "process-nextick-args": "~2.0.0", 562 | "safe-buffer": "~5.1.1", 563 | "string_decoder": "~1.1.1", 564 | "util-deprecate": "~1.0.1" 565 | } 566 | }, 567 | "node_modules/robotjs": { 568 | "version": "0.6.0", 569 | "resolved": "https://registry.npmjs.org/robotjs/-/robotjs-0.6.0.tgz", 570 | "integrity": "sha512-6pRWI3d+CBZqCXT/rsJfabbZoELua+jTeXilG27F8Jvix/J2BYZ0O7Tly2WCmXyqw5xYdCvOwvCeLRHEtXkt4w==", 571 | "hasInstallScript": true, 572 | "dependencies": { 573 | "nan": "^2.14.0", 574 | "node-abi": "^2.13.0", 575 | "prebuild-install": "^5.3.3" 576 | } 577 | }, 578 | "node_modules/safe-buffer": { 579 | "version": "5.1.2", 580 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 581 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 582 | }, 583 | "node_modules/semver": { 584 | "version": "5.7.1", 585 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", 586 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", 587 | "bin": { 588 | "semver": "bin/semver" 589 | } 590 | }, 591 | "node_modules/set-blocking": { 592 | "version": "2.0.0", 593 | "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", 594 | "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" 595 | }, 596 | "node_modules/signal-exit": { 597 | "version": "3.0.3", 598 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", 599 | "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" 600 | }, 601 | "node_modules/simple-concat": { 602 | "version": "1.0.1", 603 | "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", 604 | "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", 605 | "funding": [ 606 | { 607 | "type": "github", 608 | "url": "https://github.com/sponsors/feross" 609 | }, 610 | { 611 | "type": "patreon", 612 | "url": "https://www.patreon.com/feross" 613 | }, 614 | { 615 | "type": "consulting", 616 | "url": "https://feross.org/support" 617 | } 618 | ] 619 | }, 620 | "node_modules/simple-get": { 621 | "version": "3.1.0", 622 | "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz", 623 | "integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==", 624 | "dependencies": { 625 | "decompress-response": "^4.2.0", 626 | "once": "^1.3.1", 627 | "simple-concat": "^1.0.0" 628 | } 629 | }, 630 | "node_modules/string_decoder": { 631 | "version": "1.1.1", 632 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 633 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 634 | "dependencies": { 635 | "safe-buffer": "~5.1.0" 636 | } 637 | }, 638 | "node_modules/string-width": { 639 | "version": "1.0.2", 640 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", 641 | "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", 642 | "dependencies": { 643 | "code-point-at": "^1.0.0", 644 | "is-fullwidth-code-point": "^1.0.0", 645 | "strip-ansi": "^3.0.0" 646 | }, 647 | "engines": { 648 | "node": ">=0.10.0" 649 | } 650 | }, 651 | "node_modules/strip-ansi": { 652 | "version": "3.0.1", 653 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", 654 | "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", 655 | "dependencies": { 656 | "ansi-regex": "^2.0.0" 657 | }, 658 | "engines": { 659 | "node": ">=0.10.0" 660 | } 661 | }, 662 | "node_modules/strip-json-comments": { 663 | "version": "2.0.1", 664 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", 665 | "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", 666 | "engines": { 667 | "node": ">=0.10.0" 668 | } 669 | }, 670 | "node_modules/tar-fs": { 671 | "version": "2.1.1", 672 | "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", 673 | "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", 674 | "dependencies": { 675 | "chownr": "^1.1.1", 676 | "mkdirp-classic": "^0.5.2", 677 | "pump": "^3.0.0", 678 | "tar-stream": "^2.1.4" 679 | } 680 | }, 681 | "node_modules/tar-stream": { 682 | "version": "2.2.0", 683 | "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", 684 | "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", 685 | "dependencies": { 686 | "bl": "^4.0.3", 687 | "end-of-stream": "^1.4.1", 688 | "fs-constants": "^1.0.0", 689 | "inherits": "^2.0.3", 690 | "readable-stream": "^3.1.1" 691 | }, 692 | "engines": { 693 | "node": ">=6" 694 | } 695 | }, 696 | "node_modules/tar-stream/node_modules/readable-stream": { 697 | "version": "3.6.0", 698 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", 699 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", 700 | "dependencies": { 701 | "inherits": "^2.0.3", 702 | "string_decoder": "^1.1.1", 703 | "util-deprecate": "^1.0.1" 704 | }, 705 | "engines": { 706 | "node": ">= 6" 707 | } 708 | }, 709 | "node_modules/tunnel-agent": { 710 | "version": "0.6.0", 711 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 712 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 713 | "dependencies": { 714 | "safe-buffer": "^5.0.1" 715 | }, 716 | "engines": { 717 | "node": "*" 718 | } 719 | }, 720 | "node_modules/type": { 721 | "version": "1.2.0", 722 | "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", 723 | "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" 724 | }, 725 | "node_modules/typedarray-to-buffer": { 726 | "version": "3.1.5", 727 | "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", 728 | "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", 729 | "dependencies": { 730 | "is-typedarray": "^1.0.0" 731 | } 732 | }, 733 | "node_modules/utf-8-validate": { 734 | "version": "5.0.5", 735 | "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.5.tgz", 736 | "integrity": "sha512-+pnxRYsS/axEpkrrEpzYfNZGXp0IjC/9RIxwM5gntY4Koi8SHmUGSfxfWqxZdRxrtaoVstuOzUp/rbs3JSPELQ==", 737 | "hasInstallScript": true, 738 | "dependencies": { 739 | "node-gyp-build": "^4.2.0" 740 | } 741 | }, 742 | "node_modules/util-deprecate": { 743 | "version": "1.0.2", 744 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 745 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 746 | }, 747 | "node_modules/uuid": { 748 | "version": "8.3.2", 749 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", 750 | "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", 751 | "bin": { 752 | "uuid": "dist/bin/uuid" 753 | } 754 | }, 755 | "node_modules/websocket": { 756 | "version": "1.0.34", 757 | "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.34.tgz", 758 | "integrity": "sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ==", 759 | "dependencies": { 760 | "bufferutil": "^4.0.1", 761 | "debug": "^2.2.0", 762 | "es5-ext": "^0.10.50", 763 | "typedarray-to-buffer": "^3.1.5", 764 | "utf-8-validate": "^5.0.2", 765 | "yaeti": "^0.0.6" 766 | }, 767 | "engines": { 768 | "node": ">=4.0.0" 769 | } 770 | }, 771 | "node_modules/which-pm-runs": { 772 | "version": "1.0.0", 773 | "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz", 774 | "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=" 775 | }, 776 | "node_modules/wide-align": { 777 | "version": "1.1.3", 778 | "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", 779 | "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", 780 | "dependencies": { 781 | "string-width": "^1.0.2 || 2" 782 | } 783 | }, 784 | "node_modules/wrappy": { 785 | "version": "1.0.2", 786 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 787 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 788 | }, 789 | "node_modules/yaeti": { 790 | "version": "0.0.6", 791 | "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", 792 | "integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=", 793 | "engines": { 794 | "node": ">=0.10.32" 795 | } 796 | } 797 | }, 798 | "dependencies": { 799 | "ansi-regex": { 800 | "version": "2.1.1", 801 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", 802 | "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" 803 | }, 804 | "aproba": { 805 | "version": "1.2.0", 806 | "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", 807 | "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" 808 | }, 809 | "are-we-there-yet": { 810 | "version": "1.1.5", 811 | "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", 812 | "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", 813 | "requires": { 814 | "delegates": "^1.0.0", 815 | "readable-stream": "^2.0.6" 816 | } 817 | }, 818 | "asynckit": { 819 | "version": "0.4.0", 820 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 821 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 822 | }, 823 | "base64-js": { 824 | "version": "1.5.1", 825 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 826 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" 827 | }, 828 | "bl": { 829 | "version": "4.1.0", 830 | "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", 831 | "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", 832 | "requires": { 833 | "buffer": "^5.5.0", 834 | "inherits": "^2.0.4", 835 | "readable-stream": "^3.4.0" 836 | }, 837 | "dependencies": { 838 | "readable-stream": { 839 | "version": "3.6.0", 840 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", 841 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", 842 | "requires": { 843 | "inherits": "^2.0.3", 844 | "string_decoder": "^1.1.1", 845 | "util-deprecate": "^1.0.1" 846 | } 847 | } 848 | } 849 | }, 850 | "buffer": { 851 | "version": "5.7.1", 852 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", 853 | "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", 854 | "requires": { 855 | "base64-js": "^1.3.1", 856 | "ieee754": "^1.1.13" 857 | } 858 | }, 859 | "bufferutil": { 860 | "version": "4.0.3", 861 | "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.3.tgz", 862 | "integrity": "sha512-yEYTwGndELGvfXsImMBLop58eaGW+YdONi1fNjTINSY98tmMmFijBG6WXgdkfuLNt4imzQNtIE+eBp1PVpMCSw==", 863 | "requires": { 864 | "node-gyp-build": "^4.2.0" 865 | } 866 | }, 867 | "byte-buffer": { 868 | "version": "2.0.0", 869 | "resolved": "https://registry.npmjs.org/byte-buffer/-/byte-buffer-2.0.0.tgz", 870 | "integrity": "sha512-D5HoyMxmH3fudAYBZmfn1MF4mf6IgvSGMbNLHf/SJ1UVtj1g+9D3+jlMssNRxdDgJeh1M6gZY5lFHUIsYW44lw==" 871 | }, 872 | "chownr": { 873 | "version": "1.1.4", 874 | "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", 875 | "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" 876 | }, 877 | "code-point-at": { 878 | "version": "1.1.0", 879 | "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", 880 | "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" 881 | }, 882 | "combined-stream": { 883 | "version": "1.0.8", 884 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 885 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 886 | "requires": { 887 | "delayed-stream": "~1.0.0" 888 | } 889 | }, 890 | "console-control-strings": { 891 | "version": "1.1.0", 892 | "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", 893 | "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" 894 | }, 895 | "core-util-is": { 896 | "version": "1.0.2", 897 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 898 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 899 | }, 900 | "d": { 901 | "version": "1.0.1", 902 | "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", 903 | "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", 904 | "requires": { 905 | "es5-ext": "^0.10.50", 906 | "type": "^1.0.1" 907 | } 908 | }, 909 | "debug": { 910 | "version": "2.6.9", 911 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 912 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 913 | "requires": { 914 | "ms": "2.0.0" 915 | } 916 | }, 917 | "decompress-response": { 918 | "version": "4.2.1", 919 | "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", 920 | "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", 921 | "requires": { 922 | "mimic-response": "^2.0.0" 923 | } 924 | }, 925 | "deep-extend": { 926 | "version": "0.6.0", 927 | "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", 928 | "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" 929 | }, 930 | "delayed-stream": { 931 | "version": "1.0.0", 932 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 933 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 934 | }, 935 | "delegates": { 936 | "version": "1.0.0", 937 | "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", 938 | "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" 939 | }, 940 | "detect-libc": { 941 | "version": "1.0.3", 942 | "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", 943 | "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" 944 | }, 945 | "end-of-stream": { 946 | "version": "1.4.4", 947 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", 948 | "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", 949 | "requires": { 950 | "once": "^1.4.0" 951 | } 952 | }, 953 | "es5-ext": { 954 | "version": "0.10.53", 955 | "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", 956 | "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", 957 | "requires": { 958 | "es6-iterator": "~2.0.3", 959 | "es6-symbol": "~3.1.3", 960 | "next-tick": "~1.0.0" 961 | } 962 | }, 963 | "es6-iterator": { 964 | "version": "2.0.3", 965 | "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", 966 | "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", 967 | "requires": { 968 | "d": "1", 969 | "es5-ext": "^0.10.35", 970 | "es6-symbol": "^3.1.1" 971 | } 972 | }, 973 | "es6-symbol": { 974 | "version": "3.1.3", 975 | "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", 976 | "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", 977 | "requires": { 978 | "d": "^1.0.1", 979 | "ext": "^1.1.2" 980 | } 981 | }, 982 | "expand-template": { 983 | "version": "2.0.3", 984 | "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", 985 | "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" 986 | }, 987 | "ext": { 988 | "version": "1.4.0", 989 | "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", 990 | "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==", 991 | "requires": { 992 | "type": "^2.0.0" 993 | }, 994 | "dependencies": { 995 | "type": { 996 | "version": "2.5.0", 997 | "resolved": "https://registry.npmjs.org/type/-/type-2.5.0.tgz", 998 | "integrity": "sha512-180WMDQaIMm3+7hGXWf12GtdniDEy7nYcyFMKJn/eZz/6tSLXrUN9V0wKSbMjej0I1WHWbpREDEKHtqPQa9NNw==" 999 | } 1000 | } 1001 | }, 1002 | "form-data": { 1003 | "version": "4.0.0", 1004 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", 1005 | "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", 1006 | "requires": { 1007 | "asynckit": "^0.4.0", 1008 | "combined-stream": "^1.0.8", 1009 | "mime-types": "^2.1.12" 1010 | } 1011 | }, 1012 | "fs-constants": { 1013 | "version": "1.0.0", 1014 | "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", 1015 | "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" 1016 | }, 1017 | "gauge": { 1018 | "version": "2.7.4", 1019 | "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", 1020 | "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", 1021 | "requires": { 1022 | "aproba": "^1.0.3", 1023 | "console-control-strings": "^1.0.0", 1024 | "has-unicode": "^2.0.0", 1025 | "object-assign": "^4.1.0", 1026 | "signal-exit": "^3.0.0", 1027 | "string-width": "^1.0.1", 1028 | "strip-ansi": "^3.0.1", 1029 | "wide-align": "^1.1.0" 1030 | } 1031 | }, 1032 | "github-from-package": { 1033 | "version": "0.0.0", 1034 | "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", 1035 | "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=" 1036 | }, 1037 | "has-unicode": { 1038 | "version": "2.0.1", 1039 | "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", 1040 | "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" 1041 | }, 1042 | "ieee754": { 1043 | "version": "1.2.1", 1044 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 1045 | "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" 1046 | }, 1047 | "inherits": { 1048 | "version": "2.0.4", 1049 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 1050 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 1051 | }, 1052 | "ini": { 1053 | "version": "1.3.8", 1054 | "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", 1055 | "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" 1056 | }, 1057 | "is-fullwidth-code-point": { 1058 | "version": "1.0.0", 1059 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", 1060 | "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", 1061 | "requires": { 1062 | "number-is-nan": "^1.0.0" 1063 | } 1064 | }, 1065 | "is-typedarray": { 1066 | "version": "1.0.0", 1067 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 1068 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" 1069 | }, 1070 | "isarray": { 1071 | "version": "1.0.0", 1072 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 1073 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" 1074 | }, 1075 | "mime-db": { 1076 | "version": "1.48.0", 1077 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz", 1078 | "integrity": "sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==" 1079 | }, 1080 | "mime-types": { 1081 | "version": "2.1.31", 1082 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.31.tgz", 1083 | "integrity": "sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==", 1084 | "requires": { 1085 | "mime-db": "1.48.0" 1086 | } 1087 | }, 1088 | "mimic-response": { 1089 | "version": "2.1.0", 1090 | "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", 1091 | "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==" 1092 | }, 1093 | "minimist": { 1094 | "version": "1.2.5", 1095 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 1096 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" 1097 | }, 1098 | "mkdirp-classic": { 1099 | "version": "0.5.3", 1100 | "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", 1101 | "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" 1102 | }, 1103 | "ms": { 1104 | "version": "2.0.0", 1105 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 1106 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 1107 | }, 1108 | "nan": { 1109 | "version": "2.14.2", 1110 | "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", 1111 | "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==" 1112 | }, 1113 | "napi-build-utils": { 1114 | "version": "1.0.2", 1115 | "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", 1116 | "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" 1117 | }, 1118 | "next-tick": { 1119 | "version": "1.0.0", 1120 | "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", 1121 | "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" 1122 | }, 1123 | "node-abi": { 1124 | "version": "2.30.0", 1125 | "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.0.tgz", 1126 | "integrity": "sha512-g6bZh3YCKQRdwuO/tSZZYJAw622SjsRfJ2X0Iy4sSOHZ34/sPPdVBn8fev2tj7njzLwuqPw9uMtGsGkO5kIQvg==", 1127 | "requires": { 1128 | "semver": "^5.4.1" 1129 | } 1130 | }, 1131 | "node-fetch": { 1132 | "version": "2.6.1", 1133 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", 1134 | "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" 1135 | }, 1136 | "node-gyp-build": { 1137 | "version": "4.2.3", 1138 | "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz", 1139 | "integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg==" 1140 | }, 1141 | "noop-logger": { 1142 | "version": "0.1.1", 1143 | "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", 1144 | "integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=" 1145 | }, 1146 | "npmlog": { 1147 | "version": "4.1.2", 1148 | "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", 1149 | "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", 1150 | "requires": { 1151 | "are-we-there-yet": "~1.1.2", 1152 | "console-control-strings": "~1.1.0", 1153 | "gauge": "~2.7.3", 1154 | "set-blocking": "~2.0.0" 1155 | } 1156 | }, 1157 | "number-is-nan": { 1158 | "version": "1.0.1", 1159 | "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", 1160 | "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" 1161 | }, 1162 | "object-assign": { 1163 | "version": "4.1.1", 1164 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 1165 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 1166 | }, 1167 | "once": { 1168 | "version": "1.4.0", 1169 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 1170 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 1171 | "requires": { 1172 | "wrappy": "1" 1173 | } 1174 | }, 1175 | "prebuild-install": { 1176 | "version": "5.3.6", 1177 | "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.3.6.tgz", 1178 | "integrity": "sha512-s8Aai8++QQGi4sSbs/M1Qku62PFK49Jm1CbgXklGz4nmHveDq0wzJkg7Na5QbnO1uNH8K7iqx2EQ/mV0MZEmOg==", 1179 | "requires": { 1180 | "detect-libc": "^1.0.3", 1181 | "expand-template": "^2.0.3", 1182 | "github-from-package": "0.0.0", 1183 | "minimist": "^1.2.3", 1184 | "mkdirp-classic": "^0.5.3", 1185 | "napi-build-utils": "^1.0.1", 1186 | "node-abi": "^2.7.0", 1187 | "noop-logger": "^0.1.1", 1188 | "npmlog": "^4.0.1", 1189 | "pump": "^3.0.0", 1190 | "rc": "^1.2.7", 1191 | "simple-get": "^3.0.3", 1192 | "tar-fs": "^2.0.0", 1193 | "tunnel-agent": "^0.6.0", 1194 | "which-pm-runs": "^1.0.0" 1195 | } 1196 | }, 1197 | "process-nextick-args": { 1198 | "version": "2.0.1", 1199 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", 1200 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" 1201 | }, 1202 | "pump": { 1203 | "version": "3.0.0", 1204 | "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", 1205 | "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", 1206 | "requires": { 1207 | "end-of-stream": "^1.1.0", 1208 | "once": "^1.3.1" 1209 | } 1210 | }, 1211 | "rc": { 1212 | "version": "1.2.8", 1213 | "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", 1214 | "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", 1215 | "requires": { 1216 | "deep-extend": "^0.6.0", 1217 | "ini": "~1.3.0", 1218 | "minimist": "^1.2.0", 1219 | "strip-json-comments": "~2.0.1" 1220 | } 1221 | }, 1222 | "readable-stream": { 1223 | "version": "2.3.7", 1224 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", 1225 | "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", 1226 | "requires": { 1227 | "core-util-is": "~1.0.0", 1228 | "inherits": "~2.0.3", 1229 | "isarray": "~1.0.0", 1230 | "process-nextick-args": "~2.0.0", 1231 | "safe-buffer": "~5.1.1", 1232 | "string_decoder": "~1.1.1", 1233 | "util-deprecate": "~1.0.1" 1234 | } 1235 | }, 1236 | "robotjs": { 1237 | "version": "0.6.0", 1238 | "resolved": "https://registry.npmjs.org/robotjs/-/robotjs-0.6.0.tgz", 1239 | "integrity": "sha512-6pRWI3d+CBZqCXT/rsJfabbZoELua+jTeXilG27F8Jvix/J2BYZ0O7Tly2WCmXyqw5xYdCvOwvCeLRHEtXkt4w==", 1240 | "requires": { 1241 | "nan": "^2.14.0", 1242 | "node-abi": "^2.13.0", 1243 | "prebuild-install": "^5.3.3" 1244 | } 1245 | }, 1246 | "safe-buffer": { 1247 | "version": "5.1.2", 1248 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 1249 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 1250 | }, 1251 | "semver": { 1252 | "version": "5.7.1", 1253 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", 1254 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" 1255 | }, 1256 | "set-blocking": { 1257 | "version": "2.0.0", 1258 | "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", 1259 | "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" 1260 | }, 1261 | "signal-exit": { 1262 | "version": "3.0.3", 1263 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", 1264 | "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" 1265 | }, 1266 | "simple-concat": { 1267 | "version": "1.0.1", 1268 | "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", 1269 | "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" 1270 | }, 1271 | "simple-get": { 1272 | "version": "3.1.0", 1273 | "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz", 1274 | "integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==", 1275 | "requires": { 1276 | "decompress-response": "^4.2.0", 1277 | "once": "^1.3.1", 1278 | "simple-concat": "^1.0.0" 1279 | } 1280 | }, 1281 | "string_decoder": { 1282 | "version": "1.1.1", 1283 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 1284 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 1285 | "requires": { 1286 | "safe-buffer": "~5.1.0" 1287 | } 1288 | }, 1289 | "string-width": { 1290 | "version": "1.0.2", 1291 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", 1292 | "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", 1293 | "requires": { 1294 | "code-point-at": "^1.0.0", 1295 | "is-fullwidth-code-point": "^1.0.0", 1296 | "strip-ansi": "^3.0.0" 1297 | } 1298 | }, 1299 | "strip-ansi": { 1300 | "version": "3.0.1", 1301 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", 1302 | "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", 1303 | "requires": { 1304 | "ansi-regex": "^2.0.0" 1305 | } 1306 | }, 1307 | "strip-json-comments": { 1308 | "version": "2.0.1", 1309 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", 1310 | "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" 1311 | }, 1312 | "tar-fs": { 1313 | "version": "2.1.1", 1314 | "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", 1315 | "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", 1316 | "requires": { 1317 | "chownr": "^1.1.1", 1318 | "mkdirp-classic": "^0.5.2", 1319 | "pump": "^3.0.0", 1320 | "tar-stream": "^2.1.4" 1321 | } 1322 | }, 1323 | "tar-stream": { 1324 | "version": "2.2.0", 1325 | "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", 1326 | "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", 1327 | "requires": { 1328 | "bl": "^4.0.3", 1329 | "end-of-stream": "^1.4.1", 1330 | "fs-constants": "^1.0.0", 1331 | "inherits": "^2.0.3", 1332 | "readable-stream": "^3.1.1" 1333 | }, 1334 | "dependencies": { 1335 | "readable-stream": { 1336 | "version": "3.6.0", 1337 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", 1338 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", 1339 | "requires": { 1340 | "inherits": "^2.0.3", 1341 | "string_decoder": "^1.1.1", 1342 | "util-deprecate": "^1.0.1" 1343 | } 1344 | } 1345 | } 1346 | }, 1347 | "tunnel-agent": { 1348 | "version": "0.6.0", 1349 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 1350 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 1351 | "requires": { 1352 | "safe-buffer": "^5.0.1" 1353 | } 1354 | }, 1355 | "type": { 1356 | "version": "1.2.0", 1357 | "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", 1358 | "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" 1359 | }, 1360 | "typedarray-to-buffer": { 1361 | "version": "3.1.5", 1362 | "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", 1363 | "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", 1364 | "requires": { 1365 | "is-typedarray": "^1.0.0" 1366 | } 1367 | }, 1368 | "utf-8-validate": { 1369 | "version": "5.0.5", 1370 | "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.5.tgz", 1371 | "integrity": "sha512-+pnxRYsS/axEpkrrEpzYfNZGXp0IjC/9RIxwM5gntY4Koi8SHmUGSfxfWqxZdRxrtaoVstuOzUp/rbs3JSPELQ==", 1372 | "requires": { 1373 | "node-gyp-build": "^4.2.0" 1374 | } 1375 | }, 1376 | "util-deprecate": { 1377 | "version": "1.0.2", 1378 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 1379 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" 1380 | }, 1381 | "uuid": { 1382 | "version": "8.3.2", 1383 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", 1384 | "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" 1385 | }, 1386 | "websocket": { 1387 | "version": "1.0.34", 1388 | "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.34.tgz", 1389 | "integrity": "sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ==", 1390 | "requires": { 1391 | "bufferutil": "^4.0.1", 1392 | "debug": "^2.2.0", 1393 | "es5-ext": "^0.10.50", 1394 | "typedarray-to-buffer": "^3.1.5", 1395 | "utf-8-validate": "^5.0.2", 1396 | "yaeti": "^0.0.6" 1397 | } 1398 | }, 1399 | "which-pm-runs": { 1400 | "version": "1.0.0", 1401 | "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz", 1402 | "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=" 1403 | }, 1404 | "wide-align": { 1405 | "version": "1.1.3", 1406 | "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", 1407 | "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", 1408 | "requires": { 1409 | "string-width": "^1.0.2 || 2" 1410 | } 1411 | }, 1412 | "wrappy": { 1413 | "version": "1.0.2", 1414 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1415 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 1416 | }, 1417 | "yaeti": { 1418 | "version": "0.0.6", 1419 | "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz", 1420 | "integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=" 1421 | } 1422 | } 1423 | } 1424 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "airbridge", 3 | "version": "0.1.0", 4 | "description": "A script that allows AirMessage clients to talk to an SMServer", 5 | "main": "index.js", 6 | "dependencies": { 7 | "byte-buffer": "^2.0.0", 8 | "form-data": "^4.0.0", 9 | "node-fetch": "^2.6.1", 10 | "uuid": "^8.3.2", 11 | "websocket": "^1.0.34" 12 | }, 13 | "scripts": { 14 | "test": "echo \"Error: no test specified\" && exit 1" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/AwesomeIndustry/AirBridge.git" 19 | }, 20 | "author": "", 21 | "license": "ISC", 22 | "bugs": { 23 | "url": "https://github.com/AwesomeIndustry/AirBridge/issues" 24 | }, 25 | "homepage": "https://github.com/AwesomeIndustry/AirBridge#readme" 26 | } -------------------------------------------------------------------------------- /settings.txt: -------------------------------------------------------------------------------- 1 | SMSERVER_IP=192.168.1.2 2 | SMSERVER_PASSWORD=YourSMServerPasswordGoesHere 3 | SMSERVER_PORT=8741 4 | SMSERVER_WEBSOCKET_PORT=8740 5 | AIRMESSAGE_PASSWORD=SetYourAirMessagePasswordHere 6 | AIRMESSAGE_PORT=1359 7 | -------------------------------------------------------------------------------- /settingsManager.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const LogLib = require("./Log.js"); 3 | exports.readSettingsFile = function() { 4 | return new Promise(function(resolve, reject) { 5 | fs.readFile('./settings.txt', 'utf8', (err, data) => { //Should this be UTF8? 6 | // console.log(data); 7 | resolve(data); 8 | }) 9 | }); 10 | } 11 | 12 | exports.readSetting = async function(setting_name) { 13 | var Log = new LogLib.Log("settingsManager.js", "readSetting"); 14 | var fileData = await exports.readSettingsFile(); 15 | var fileLines = fileData.split("\n"); 16 | for (var i = 0; i < fileLines.length; i++) { 17 | var parts = fileLines[i].match(/^([^=]+)=(.*)/); 18 | if (parts[1] == setting_name) { 19 | return parts[2]; 20 | } 21 | } 22 | 23 | Log.e("Setting doesn't exist in settings.txt: "+setting_name); 24 | // return false; 25 | } 26 | 27 | //TODO: Maybe reset settings if they don't exist or are in the wrong format? 28 | --------------------------------------------------------------------------------