em-msn 0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.md +0 -0
- data/lib/em-msn.rb +25 -0
- data/lib/msn/authentication_error.rb +2 -0
- data/lib/msn/challenge.rb +47 -0
- data/lib/msn/contact.rb +32 -0
- data/lib/msn/message.rb +11 -0
- data/lib/msn/messenger.rb +118 -0
- data/lib/msn/nexus.rb +101 -0
- data/lib/msn/notification_server.rb +160 -0
- data/lib/msn/protocol.rb +112 -0
- data/lib/msn/switchboard.rb +23 -0
- data/lib/msn/version.rb +3 -0
- metadata +137 -0
    
        data/README.md
    ADDED
    
    | 
            File without changes
         | 
    
        data/lib/em-msn.rb
    ADDED
    
    | @@ -0,0 +1,25 @@ | |
| 1 | 
            +
            module Msn
         | 
| 2 | 
            +
              ApplicationId = "CFE80F9D-180F-4399-82AB-413F33A1FA11"
         | 
| 3 | 
            +
            end
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            require 'eventmachine'
         | 
| 6 | 
            +
            require 'rest-client'
         | 
| 7 | 
            +
            require 'fiber'
         | 
| 8 | 
            +
            require 'cgi'
         | 
| 9 | 
            +
            require 'guid'
         | 
| 10 | 
            +
            require 'base64'
         | 
| 11 | 
            +
            require 'digest/md5'
         | 
| 12 | 
            +
            require 'digest/hmac'
         | 
| 13 | 
            +
            require 'erb'
         | 
| 14 | 
            +
            require "nokogiri"
         | 
| 15 | 
            +
            require 'rexml/text'
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            require_relative 'msn/authentication_error'
         | 
| 18 | 
            +
            require_relative 'msn/protocol'
         | 
| 19 | 
            +
            require_relative 'msn/message'
         | 
| 20 | 
            +
            require_relative 'msn/contact'
         | 
| 21 | 
            +
            require_relative 'msn/nexus'
         | 
| 22 | 
            +
            require_relative 'msn/challenge'
         | 
| 23 | 
            +
            require_relative 'msn/notification_server'
         | 
| 24 | 
            +
            require_relative 'msn/switchboard'
         | 
| 25 | 
            +
            require_relative 'msn/messenger'
         | 
| @@ -0,0 +1,47 @@ | |
| 1 | 
            +
            class Msn::Challenge
         | 
| 2 | 
            +
              ProductKey = "RG@XY*28Q5QHS%Q5"
         | 
| 3 | 
            +
              ProductId = "PROD0113H11T8$X_"
         | 
| 4 | 
            +
              F = "0x7FFFFFFF".to_i(16)
         | 
| 5 | 
            +
              E = "0x0E79A9C1".to_i(16)
         | 
| 6 | 
            +
             | 
| 7 | 
            +
              class << self
         | 
| 8 | 
            +
                def challenge(challenge, product_key = ProductKey, product_id = ProductId)
         | 
| 9 | 
            +
                  md5hash = Digest::MD5.hexdigest "#{challenge}#{product_key}"
         | 
| 10 | 
            +
                  md5array = md5hash.scan(/.{8}/)
         | 
| 11 | 
            +
                  new_hash_parts = md5array.map! { |s| s.scan(/.{2}/).reverse.join.to_i(16) }
         | 
| 12 | 
            +
                  md5array = new_hash_parts.map { |n| n & F }
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  chlstring = "#{challenge}#{product_id}"
         | 
| 15 | 
            +
                  chlstring = "#{chlstring}#{'0' * (8 - chlstring.length % 8)}"
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  chlstring_array = chlstring.scan(/.{4}/)
         | 
| 18 | 
            +
                  chlstring_array.map! { |str| str.bytes.map { |b| b.to_s(16) }.reverse.join.to_i(16) }
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  low = high = 0
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  i = 0
         | 
| 23 | 
            +
                  while i < chlstring_array.length
         | 
| 24 | 
            +
                    temp = (md5array[0] * (((E * chlstring_array[i]) % F) + high) + md5array[1]) % F
         | 
| 25 | 
            +
                    high = (md5array[2] * ((chlstring_array[i + 1] + temp) % F) + md5array[3]) % F
         | 
| 26 | 
            +
                    low = low + high + temp;
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                    i += 2
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  high = (high + md5array[1]) % F
         | 
| 32 | 
            +
                  low = (low + md5array[3]) % F
         | 
| 33 | 
            +
                  key = (high << 32) + low
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                  new_hash_parts[0] ^= high;
         | 
| 36 | 
            +
                  new_hash_parts[1] ^= low;
         | 
| 37 | 
            +
                  new_hash_parts[2] ^= high;
         | 
| 38 | 
            +
                  new_hash_parts[3] ^= low;
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                  new_hash_parts.map do |num| 
         | 
| 41 | 
            +
                    str = num.to_s(16)
         | 
| 42 | 
            +
                    str = "#{'0' * (8 - str.length)}#{str}" if str.length != 8
         | 
| 43 | 
            +
                    str.scan(/.{2}/).reverse.join
         | 
| 44 | 
            +
                  end.join
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
              end
         | 
| 47 | 
            +
            end
         | 
    
        data/lib/msn/contact.rb
    ADDED
    
    | @@ -0,0 +1,32 @@ | |
| 1 | 
            +
            class Msn::Contact
         | 
| 2 | 
            +
              attr_accessor :email
         | 
| 3 | 
            +
              attr_accessor :display_name
         | 
| 4 | 
            +
              attr_accessor :allow
         | 
| 5 | 
            +
              attr_accessor :block
         | 
| 6 | 
            +
              attr_accessor :reverse
         | 
| 7 | 
            +
              attr_accessor :pending
         | 
| 8 | 
            +
             | 
| 9 | 
            +
              def initialize(email, display_name = nil)
         | 
| 10 | 
            +
                @email = email
         | 
| 11 | 
            +
                @display_name = display_name
         | 
| 12 | 
            +
              end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
              def to_s
         | 
| 15 | 
            +
                if display_name && display_name.length > 0
         | 
| 16 | 
            +
                  str = "#{display_name} <#{email}>"
         | 
| 17 | 
            +
                else
         | 
| 18 | 
            +
                  str = "#{email}"
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
                if allow || reverse || pending
         | 
| 21 | 
            +
                  str << ' ('
         | 
| 22 | 
            +
                  pieces = []
         | 
| 23 | 
            +
                  pieces << 'allow' if allow
         | 
| 24 | 
            +
                  pieces << 'block' if block
         | 
| 25 | 
            +
                  pieces << 'reverse' if reverse
         | 
| 26 | 
            +
                  pieces << 'pending' if pending
         | 
| 27 | 
            +
                  str << pieces.join(', ')
         | 
| 28 | 
            +
                  str << ')'
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
                str
         | 
| 31 | 
            +
              end
         | 
| 32 | 
            +
            end
         | 
    
        data/lib/msn/message.rb
    ADDED
    
    
| @@ -0,0 +1,118 @@ | |
| 1 | 
            +
            class Msn::Messenger
         | 
| 2 | 
            +
              attr_reader :username
         | 
| 3 | 
            +
              attr_reader :password
         | 
| 4 | 
            +
             | 
| 5 | 
            +
              def initialize(username, password)
         | 
| 6 | 
            +
                @username = username
         | 
| 7 | 
            +
                @password = password
         | 
| 8 | 
            +
              end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
              def connect
         | 
| 11 | 
            +
                @notification_server = EM.connect 'messenger.hotmail.com', 1863, Msn::NotificationServer, self
         | 
| 12 | 
            +
              end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
              def set_online_status(status)
         | 
| 15 | 
            +
                case status
         | 
| 16 | 
            +
                when :available, :online
         | 
| 17 | 
            +
                  @notification_server.chg "NLN", 0
         | 
| 18 | 
            +
                when :busy
         | 
| 19 | 
            +
                  @notification_server.chg "BSY", 0
         | 
| 20 | 
            +
                when :idle
         | 
| 21 | 
            +
                  @notification_server.chg "IDL", 0
         | 
| 22 | 
            +
                when :brb, :be_right_back
         | 
| 23 | 
            +
                  @notification_server.chg "BRB", 0
         | 
| 24 | 
            +
                when :away
         | 
| 25 | 
            +
                  @notification_server.chg "AWY", 0
         | 
| 26 | 
            +
                when :phone, :on_the_phone
         | 
| 27 | 
            +
                  @notification_server.chg "PHN", 0
         | 
| 28 | 
            +
                when :lunch, :out_to_lunch
         | 
| 29 | 
            +
                  @notification_server.chg "LUN", 0
         | 
| 30 | 
            +
                else
         | 
| 31 | 
            +
                  raise "Wrong online status: #{status}"
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
              end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
              def add_contact(email)
         | 
| 36 | 
            +
                send_contact_command email, 'ADL', '1'
         | 
| 37 | 
            +
              end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
              def remove_contact(email)
         | 
| 40 | 
            +
                send_contact_command email, 'RML', '1'
         | 
| 41 | 
            +
              end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
              def block_contact(email)
         | 
| 44 | 
            +
                send_contact_command email, 'RML', '2'
         | 
| 45 | 
            +
              end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
              def unblock_contact(email)
         | 
| 48 | 
            +
                send_contact_command email, 'ADL', '2'
         | 
| 49 | 
            +
              end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
              def get_contacts
         | 
| 52 | 
            +
                @notification_server.get_contacts
         | 
| 53 | 
            +
              end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
              def send_contact_command(email, command, list)
         | 
| 56 | 
            +
                username, domain = email.split '@', 2
         | 
| 57 | 
            +
                @notification_server.send_payload_command_and_wait command, %Q(<ml><d n="#{domain}"><c n="#{username}" t="1" l="#{list}" /></d></ml>)
         | 
| 58 | 
            +
              end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
              def on_ready(&handler)
         | 
| 61 | 
            +
                @on_ready_handler = handler
         | 
| 62 | 
            +
              end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
              def on_login_failed(&handler)
         | 
| 65 | 
            +
                @on_login_failed = handler
         | 
| 66 | 
            +
              end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
              def on_disconnect(&handler)
         | 
| 69 | 
            +
                @on_disconnect = handler
         | 
| 70 | 
            +
              end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
              def on_message(&handler)
         | 
| 73 | 
            +
                @on_message_handler = handler
         | 
| 74 | 
            +
              end
         | 
| 75 | 
            +
             | 
| 76 | 
            +
              def on_contact_request(&handler)
         | 
| 77 | 
            +
                @on_contact_request = handler
         | 
| 78 | 
            +
              end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
              def send_message(email, text)
         | 
| 81 | 
            +
                @notification_server.send_message email, text
         | 
| 82 | 
            +
              end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
              def accept_message(message)
         | 
| 85 | 
            +
                call_handler @on_message_handler, message
         | 
| 86 | 
            +
              end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
              def contact_request(email, display_name)
         | 
| 89 | 
            +
                call_handler @on_contact_request, email, display_name
         | 
| 90 | 
            +
              end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
              def ready
         | 
| 93 | 
            +
                call_handler @on_ready_handler
         | 
| 94 | 
            +
              end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
              def login_failed(message)
         | 
| 97 | 
            +
                call_handler @on_login_failed, message
         | 
| 98 | 
            +
              end
         | 
| 99 | 
            +
             | 
| 100 | 
            +
              def disconnected
         | 
| 101 | 
            +
                call_handler @on_disconnect
         | 
| 102 | 
            +
              end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
              def call_handler(handler, *args)
         | 
| 105 | 
            +
                if handler
         | 
| 106 | 
            +
                  Fiber.new { handler.call(*args) }.resume
         | 
| 107 | 
            +
                end
         | 
| 108 | 
            +
              end
         | 
| 109 | 
            +
             | 
| 110 | 
            +
              def self.debug
         | 
| 111 | 
            +
                @debug
         | 
| 112 | 
            +
              end
         | 
| 113 | 
            +
             | 
| 114 | 
            +
              def self.debug=(debug)
         | 
| 115 | 
            +
                @debug = debug
         | 
| 116 | 
            +
              end
         | 
| 117 | 
            +
            end
         | 
| 118 | 
            +
             | 
    
        data/lib/msn/nexus.rb
    ADDED
    
    | @@ -0,0 +1,101 @@ | |
| 1 | 
            +
            class Msn::Nexus
         | 
| 2 | 
            +
              attr_reader :messenger
         | 
| 3 | 
            +
              attr_reader :policy
         | 
| 4 | 
            +
              attr_reader :nonce
         | 
| 5 | 
            +
             | 
| 6 | 
            +
              Namespaces = {
         | 
| 7 | 
            +
                "wsse" => "http://schemas.xmlsoap.org/ws/2003/06/secext",
         | 
| 8 | 
            +
                "wst" => "http://schemas.xmlsoap.org/ws/2004/04/trust",
         | 
| 9 | 
            +
                "wsp" => "http://schemas.xmlsoap.org/ws/2002/12/policy",
         | 
| 10 | 
            +
                "wsa" => "http://schemas.xmlsoap.org/ws/2004/03/addressing",
         | 
| 11 | 
            +
                "S" => "http://schemas.xmlsoap.org/soap/envelope/",
         | 
| 12 | 
            +
              }
         | 
| 13 | 
            +
             | 
| 14 | 
            +
              def initialize(messenger, policy, nonce)
         | 
| 15 | 
            +
                @messenger = messenger
         | 
| 16 | 
            +
                @policy = policy
         | 
| 17 | 
            +
                @nonce = nonce
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
              def sso_token
         | 
| 21 | 
            +
                fetch_data
         | 
| 22 | 
            +
                @sso_token
         | 
| 23 | 
            +
              end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
              def ticket_token
         | 
| 26 | 
            +
                fetch_data
         | 
| 27 | 
            +
                @ticket_token
         | 
| 28 | 
            +
              end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
              def secret
         | 
| 31 | 
            +
                fetch_data
         | 
| 32 | 
            +
                @secret
         | 
| 33 | 
            +
              end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
              def fetch_data
         | 
| 36 | 
            +
                return if @fetched_data
         | 
| 37 | 
            +
                @fetched_data = true
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                @sso_token, secret, @ticket_token = get_binary_secret messenger.username, messenger.password
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                @secret = compute_return_value secret
         | 
| 42 | 
            +
              end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
              def get_binary_secret(username, password)
         | 
| 45 | 
            +
                msn_sso_template_file = File.expand_path('../soap/msn_sso_template.xml', __FILE__)
         | 
| 46 | 
            +
                msn_sso_template = ERB.new File.read(msn_sso_template_file)
         | 
| 47 | 
            +
                soap = msn_sso_template.result(binding)
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                response = RestClient.post "https://login.live.com/RST.srf", soap
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                xml = Nokogiri::XML(response)
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                # Check invalid login
         | 
| 54 | 
            +
                fault = xml.xpath("//S:Fault/faultstring")
         | 
| 55 | 
            +
                if fault
         | 
| 56 | 
            +
                  raise Msn::AuthenticationError.new(fault.text)
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                rstr = xml.xpath "//wst:RequestSecurityTokenResponse[wsp:AppliesTo/wsa:EndpointReference/wsa:Address='messengerclear.live.com']", Namespaces
         | 
| 60 | 
            +
                token = rstr.xpath("wst:RequestedSecurityToken/wsse:BinarySecurityToken[@Id='Compact1']", Namespaces).first.text
         | 
| 61 | 
            +
                secret = rstr.xpath("wst:RequestedProofToken/wst:BinarySecret", Namespaces).first.text
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                rstr2 = xml.xpath "//wst:RequestSecurityTokenResponse[wsp:AppliesTo/wsa:EndpointReference/wsa:Address='contacts.msn.com']", Namespaces
         | 
| 64 | 
            +
                ticket_token = rstr2.xpath("wst:RequestedSecurityToken/wsse:BinarySecurityToken[@Id='Compact2']", Namespaces).first.text
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                [token, secret, ticket_token]
         | 
| 67 | 
            +
              end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
              def compute_return_value(binary_secret, iv = Random.new.bytes(8))
         | 
| 70 | 
            +
                key1 = Base64.decode64 binary_secret
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                key2 = compute_key key1, "WS-SecureConversationSESSION KEY HASH"
         | 
| 73 | 
            +
                key3 = compute_key key1, "WS-SecureConversationSESSION KEY ENCRYPTION"
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                hash = sha1_hmac key2, @nonce
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                nonce = "#{@nonce}#{8.chr * 8}"
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                des = OpenSSL::Cipher::Cipher.new("des-ede3-cbc")
         | 
| 80 | 
            +
                des.encrypt
         | 
| 81 | 
            +
                des.iv = iv
         | 
| 82 | 
            +
                des.key = key3
         | 
| 83 | 
            +
                encrypted_data = des.update(nonce) + des.final
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                final = [28, 1, 0x6603, 0x8004, 8, 20, 72, iv, hash, encrypted_data].pack "L<L<L<L<L<L<L<A8A20A72"
         | 
| 86 | 
            +
                Base64.strict_encode64 final
         | 
| 87 | 
            +
              end
         | 
| 88 | 
            +
             | 
| 89 | 
            +
              def compute_key(key, hash)
         | 
| 90 | 
            +
                hash1 = sha1_hmac(key, hash)
         | 
| 91 | 
            +
                hash2 = sha1_hmac(key, "#{hash1}#{hash}")
         | 
| 92 | 
            +
                hash3 = sha1_hmac(key, hash1)
         | 
| 93 | 
            +
                hash4 = sha1_hmac(key, "#{hash3}#{hash}")
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                "#{hash2}#{hash4[0 ... 4]}"
         | 
| 96 | 
            +
              end
         | 
| 97 | 
            +
             | 
| 98 | 
            +
              def sha1_hmac(data, key)
         | 
| 99 | 
            +
                Digest::HMAC.digest(key, data, Digest::SHA1)
         | 
| 100 | 
            +
              end
         | 
| 101 | 
            +
            end
         | 
| @@ -0,0 +1,160 @@ | |
| 1 | 
            +
            class Msn::NotificationServer < EventMachine::Connection
         | 
| 2 | 
            +
              include Msn::Protocol
         | 
| 3 | 
            +
             | 
| 4 | 
            +
              ContactsNamespace = {'ns' => 'http://www.msn.com/webservices/AddressBook'}
         | 
| 5 | 
            +
             | 
| 6 | 
            +
              attr_reader :messenger
         | 
| 7 | 
            +
              attr_reader :guid
         | 
| 8 | 
            +
             | 
| 9 | 
            +
              def initialize(messenger)
         | 
| 10 | 
            +
                @messenger = messenger
         | 
| 11 | 
            +
                @guid = Guid.new.to_s
         | 
| 12 | 
            +
                @switchboards = {}
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                on_event 'ADL' do |header, data|
         | 
| 15 | 
            +
                  data = Nokogiri::XML(data)
         | 
| 16 | 
            +
                  domain = data.xpath('//ml/d').first['n']
         | 
| 17 | 
            +
                  c = data.xpath('//ml/d/c').first
         | 
| 18 | 
            +
                  username = c['n']
         | 
| 19 | 
            +
                  display_name = c['f']
         | 
| 20 | 
            +
                  messenger.contact_request "#{username}@#{domain}", display_name
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
              end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
              def username_guid
         | 
| 25 | 
            +
                @username_guid ||= "#{messenger.username};{#{guid}}"
         | 
| 26 | 
            +
              end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
              def send_message(email, text)
         | 
| 29 | 
            +
                switchboard = @switchboards[email]
         | 
| 30 | 
            +
                if switchboard
         | 
| 31 | 
            +
                  switchboard.send_message text
         | 
| 32 | 
            +
                else
         | 
| 33 | 
            +
                  Fiber.new do
         | 
| 34 | 
            +
                    response = xfr "SB"
         | 
| 35 | 
            +
                    switchboard = create_switchboard email, response[3]
         | 
| 36 | 
            +
                    switchboard.on_event 'JOI' do
         | 
| 37 | 
            +
                      switchboard.clear_event 'JOI'
         | 
| 38 | 
            +
                      switchboard.send_message text
         | 
| 39 | 
            +
                    end
         | 
| 40 | 
            +
                    switchboard.usr username_guid, response[5]
         | 
| 41 | 
            +
                    switchboard.cal email
         | 
| 42 | 
            +
                  end.resume
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
              end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
              def username
         | 
| 47 | 
            +
                messenger.username
         | 
| 48 | 
            +
              end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
              def password
         | 
| 51 | 
            +
                messenger.password
         | 
| 52 | 
            +
              end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
              def get_contacts
         | 
| 55 | 
            +
                contacts = {}
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                xml = Nokogiri::XML(get_soap_contacts.body)
         | 
| 58 | 
            +
                xml.xpath('//ns:Membership', ContactsNamespace).each do |membership|
         | 
| 59 | 
            +
                  role = membership.xpath('ns:MemberRole', ContactsNamespace).text
         | 
| 60 | 
            +
                  membership.xpath('ns:Members/ns:Member', ContactsNamespace).each do |member|
         | 
| 61 | 
            +
                    cid = member.xpath('ns:CID', ContactsNamespace).text
         | 
| 62 | 
            +
                    email = member.xpath('ns:PassportName', ContactsNamespace).text
         | 
| 63 | 
            +
                    display_name = member.xpath('ns:DisplayName', ContactsNamespace)
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                    contact = contacts[cid] ||= Msn::Contact.new(email, display_name ? display_name.text : nil)
         | 
| 66 | 
            +
                    case role
         | 
| 67 | 
            +
                    when 'Allow' then contact.allow = true
         | 
| 68 | 
            +
                    when 'Block' then contact.block = true
         | 
| 69 | 
            +
                    when 'Reverse' then contact.reverse = true
         | 
| 70 | 
            +
                    when 'Pending' then contact.pending = true
         | 
| 71 | 
            +
                    end
         | 
| 72 | 
            +
                  end
         | 
| 73 | 
            +
                end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                contacts.values
         | 
| 76 | 
            +
              end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
              def get_soap_contacts
         | 
| 79 | 
            +
                msn_get_contacts_template_file = File.expand_path('../soap/msn_get_contacts_template.xml', __FILE__)
         | 
| 80 | 
            +
                msn_get_contacts_template = ERB.new File.read(msn_get_contacts_template_file)
         | 
| 81 | 
            +
                soap = msn_get_contacts_template.result(binding)
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                RestClient.post "https://local-bay.contacts.msn.com/abservice/SharingService.asmx", soap, {
         | 
| 84 | 
            +
                  'SOAPAction' => 'http://www.msn.com/webservices/AddressBook/FindMembership',
         | 
| 85 | 
            +
                  'Content-Type' => 'text/xml',
         | 
| 86 | 
            +
                }
         | 
| 87 | 
            +
              end
         | 
| 88 | 
            +
             | 
| 89 | 
            +
              def post_init
         | 
| 90 | 
            +
                super
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                login
         | 
| 93 | 
            +
              end
         | 
| 94 | 
            +
             | 
| 95 | 
            +
              def login
         | 
| 96 | 
            +
                Fiber.new do
         | 
| 97 | 
            +
                  begin
         | 
| 98 | 
            +
                    ver "MSNP18", "CVR0"
         | 
| 99 | 
            +
                    cvr "0x0409", "winnt", "5.1", "i386", "MSNMSGR", "8.5.1302", "BC01", username
         | 
| 100 | 
            +
                    response = usr "SSO", "I", username
         | 
| 101 | 
            +
                    if response[0] == "XFR" && response[2] == "NS"
         | 
| 102 | 
            +
                      host, port = response[3].split ':'
         | 
| 103 | 
            +
                      @reconnect_host, @reconnect_port = response[3].split ':'
         | 
| 104 | 
            +
                      close_connection
         | 
| 105 | 
            +
                    else
         | 
| 106 | 
            +
                      login_to_nexus(response[4], response[5])
         | 
| 107 | 
            +
                    end
         | 
| 108 | 
            +
                  rescue Msn::AuthenticationError => ex
         | 
| 109 | 
            +
                    messenger.login_failed(ex.message)
         | 
| 110 | 
            +
                    close_connection
         | 
| 111 | 
            +
                  end
         | 
| 112 | 
            +
                end.resume
         | 
| 113 | 
            +
              end
         | 
| 114 | 
            +
             | 
| 115 | 
            +
              def login_to_nexus(policy, nonce)
         | 
| 116 | 
            +
                @nexus = Msn::Nexus.new self, policy, nonce
         | 
| 117 | 
            +
             | 
| 118 | 
            +
                first_msg = true
         | 
| 119 | 
            +
                on_event('MSG') do
         | 
| 120 | 
            +
                  if first_msg
         | 
| 121 | 
            +
                    first_msg = false
         | 
| 122 | 
            +
                    messenger.ready
         | 
| 123 | 
            +
                  end
         | 
| 124 | 
            +
                end
         | 
| 125 | 
            +
             | 
| 126 | 
            +
                on_event('RNG') do |header|
         | 
| 127 | 
            +
                  switchboard = create_switchboard header[5], header[2]
         | 
| 128 | 
            +
                  switchboard.ans username_guid, header[4], header[1]
         | 
| 129 | 
            +
                end
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                response = usr "SSO", "S", @nexus.sso_token, @nexus.secret, guid
         | 
| 132 | 
            +
                if response[2] != "OK"
         | 
| 133 | 
            +
                  raise Msn::AuthenticationError.new("Didn't receive OK from SSO")
         | 
| 134 | 
            +
                end
         | 
| 135 | 
            +
              end
         | 
| 136 | 
            +
             | 
| 137 | 
            +
              def create_switchboard(email, host_and_port)
         | 
| 138 | 
            +
                host, port = host_and_port.split(':')
         | 
| 139 | 
            +
                switchboard = EM.connect host, port, Msn::Switchboard, messenger
         | 
| 140 | 
            +
                switchboard.on_event 'BYE' do |header|
         | 
| 141 | 
            +
                  destroy_switchboard email if header[1] =~ /#{email}/
         | 
| 142 | 
            +
                end
         | 
| 143 | 
            +
                @switchboards[email] = switchboard
         | 
| 144 | 
            +
              end
         | 
| 145 | 
            +
             | 
| 146 | 
            +
              def destroy_switchboard(email)
         | 
| 147 | 
            +
                switchboard = @switchboards.delete email
         | 
| 148 | 
            +
                switchboard.close_connection if switchboard
         | 
| 149 | 
            +
              end
         | 
| 150 | 
            +
             | 
| 151 | 
            +
              def unbind
         | 
| 152 | 
            +
                if @reconnect_host
         | 
| 153 | 
            +
                  reconnect @reconnect_host, @reconnect_port.to_i
         | 
| 154 | 
            +
                  @reconnect_host = @reconnect_port = nil
         | 
| 155 | 
            +
                  login
         | 
| 156 | 
            +
                else
         | 
| 157 | 
            +
                  messenger.disconnected
         | 
| 158 | 
            +
                end
         | 
| 159 | 
            +
              end
         | 
| 160 | 
            +
            end
         | 
    
        data/lib/msn/protocol.rb
    ADDED
    
    | @@ -0,0 +1,112 @@ | |
| 1 | 
            +
            module Msn::Protocol
         | 
| 2 | 
            +
              include EventMachine::Protocols::LineText2
         | 
| 3 | 
            +
             | 
| 4 | 
            +
              def post_init
         | 
| 5 | 
            +
                @trid = 1
         | 
| 6 | 
            +
                @command_fibers = {}
         | 
| 7 | 
            +
              end
         | 
| 8 | 
            +
             | 
| 9 | 
            +
              def receive_line(line)
         | 
| 10 | 
            +
                puts "<< #{line}" if Msn::Messenger.debug
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                pieces = line.split(' ')
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                case pieces[0]
         | 
| 15 | 
            +
                when 'CHL'
         | 
| 16 | 
            +
                  answer_challenge pieces[2]
         | 
| 17 | 
            +
                when 'RNG'
         | 
| 18 | 
            +
                  handle_event pieces
         | 
| 19 | 
            +
                when 'MSG', 'NOT', 'GCF', 'UBX'
         | 
| 20 | 
            +
                  handle_payload_command pieces
         | 
| 21 | 
            +
                when 'QRY'
         | 
| 22 | 
            +
                  # ignore
         | 
| 23 | 
            +
                when 'ADL', 'RML'
         | 
| 24 | 
            +
                  if pieces[2] == 'OK'
         | 
| 25 | 
            +
                    handle_normal_command pieces
         | 
| 26 | 
            +
                  else
         | 
| 27 | 
            +
                    handle_payload_command pieces
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
                else
         | 
| 30 | 
            +
                  handle_normal_command pieces
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
              end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
              def handle_normal_command(pieces)
         | 
| 35 | 
            +
                if fiber = @command_fibers.delete(pieces[1].to_i)
         | 
| 36 | 
            +
                  fiber.resume pieces
         | 
| 37 | 
            +
                else
         | 
| 38 | 
            +
                  handle_event pieces
         | 
| 39 | 
            +
                end
         | 
| 40 | 
            +
              end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
              def handle_payload_command(pieces)
         | 
| 43 | 
            +
                @header = pieces
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                size = pieces.last.to_i
         | 
| 46 | 
            +
                set_binary_mode size
         | 
| 47 | 
            +
              end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
              def receive_binary_data(data)
         | 
| 50 | 
            +
                puts "<<* #{data}" if Msn::Messenger.debug
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                handle_event @header, data
         | 
| 53 | 
            +
              end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
              def handle_event(header, data = nil)
         | 
| 56 | 
            +
                return unless @event_handlers
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                handler = @event_handlers[header[0]]
         | 
| 59 | 
            +
                if handler
         | 
| 60 | 
            +
                  Fiber.new do
         | 
| 61 | 
            +
                    handler.call header, data
         | 
| 62 | 
            +
                  end.resume
         | 
| 63 | 
            +
                end
         | 
| 64 | 
            +
              end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
              def answer_challenge(challenge_string)
         | 
| 67 | 
            +
                send_payload_command "QRY", Msn::Challenge::ProductId, Msn::Challenge.challenge(challenge_string)
         | 
| 68 | 
            +
              end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
              def send_command(command, *args)
         | 
| 71 | 
            +
                @command_fibers[@trid] = Fiber.current
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                text = "#{command} #{@trid} #{args.join ' '}\r\n"
         | 
| 74 | 
            +
                send_command_internal text
         | 
| 75 | 
            +
             | 
| 76 | 
            +
                Fiber.yield
         | 
| 77 | 
            +
              end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
              def send_payload_command(command, *args)
         | 
| 80 | 
            +
                payload = args.pop
         | 
| 81 | 
            +
                args.push payload.length
         | 
| 82 | 
            +
                send_command_internal "#{command} #{@trid} #{args.join ' '}\r\n#{payload}"
         | 
| 83 | 
            +
              end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
              def send_payload_command_and_wait(command, *args)
         | 
| 86 | 
            +
                @command_fibers[@trid] = Fiber.current
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                send_payload_command command, *args
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                Fiber.yield
         | 
| 91 | 
            +
              end
         | 
| 92 | 
            +
             | 
| 93 | 
            +
              def send_command_internal(text)
         | 
| 94 | 
            +
                puts ">> #{text}" if Msn::Messenger.debug
         | 
| 95 | 
            +
                send_data text
         | 
| 96 | 
            +
                @trid += 1
         | 
| 97 | 
            +
              end
         | 
| 98 | 
            +
             | 
| 99 | 
            +
              def on_event(kind, &block)
         | 
| 100 | 
            +
                @event_handlers ||= {}
         | 
| 101 | 
            +
                @event_handlers[kind] = block
         | 
| 102 | 
            +
              end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
              def clear_event(kind)
         | 
| 105 | 
            +
                @event_handlers.delete kind
         | 
| 106 | 
            +
              end
         | 
| 107 | 
            +
             | 
| 108 | 
            +
              def method_missing(name, *args)
         | 
| 109 | 
            +
                send_command name.upcase, *args
         | 
| 110 | 
            +
              end
         | 
| 111 | 
            +
            end
         | 
| 112 | 
            +
             | 
| @@ -0,0 +1,23 @@ | |
| 1 | 
            +
            class Msn::Switchboard < EventMachine::Connection
         | 
| 2 | 
            +
              include Msn::Protocol
         | 
| 3 | 
            +
             | 
| 4 | 
            +
              def initialize(messenger)
         | 
| 5 | 
            +
                @messenger = messenger
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                on_event 'MSG' do |header, data|
         | 
| 8 | 
            +
                  email = header[1]
         | 
| 9 | 
            +
                  display_name = header[2]
         | 
| 10 | 
            +
                  head, body = data.split "\r\n\r\n", 2
         | 
| 11 | 
            +
                  headers = Hash[head.split("\r\n").map { |line| line.split ':', 2 }]
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  if headers['Content-Type'] =~ %r(text/plain)
         | 
| 14 | 
            +
                    @messenger.accept_message Msn::Message.new(email, display_name, body)
         | 
| 15 | 
            +
                  end
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
              end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
              def send_message(text)
         | 
| 20 | 
            +
                send_payload_command "MSG", "N", "MIME-Version: 1.0\r\nContent-Type: text/plain; charset=UTF-8\r\nUser-Agent: pidgin/2.10.5devel\r\nX-MMS-IM-Format: FN=Segoe%20UI; EF=; CO=0; PF=0; RL=0\r\n\r\n#{text}"
         | 
| 21 | 
            +
              end
         | 
| 22 | 
            +
            end
         | 
| 23 | 
            +
             | 
    
        data/lib/msn/version.rb
    ADDED
    
    
    
        metadata
    ADDED
    
    | @@ -0,0 +1,137 @@ | |
| 1 | 
            +
            --- !ruby/object:Gem::Specification
         | 
| 2 | 
            +
            name: em-msn
         | 
| 3 | 
            +
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            +
              version: '0.1'
         | 
| 5 | 
            +
              prerelease: 
         | 
| 6 | 
            +
            platform: ruby
         | 
| 7 | 
            +
            authors:
         | 
| 8 | 
            +
            - Ary Borenszweig
         | 
| 9 | 
            +
            autorequire: 
         | 
| 10 | 
            +
            bindir: bin
         | 
| 11 | 
            +
            cert_chain: []
         | 
| 12 | 
            +
            date: 2012-11-07 00:00:00.000000000 Z
         | 
| 13 | 
            +
            dependencies:
         | 
| 14 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 15 | 
            +
              name: eventmachine
         | 
| 16 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 17 | 
            +
                none: false
         | 
| 18 | 
            +
                requirements:
         | 
| 19 | 
            +
                - - ! '>='
         | 
| 20 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 21 | 
            +
                    version: '0'
         | 
| 22 | 
            +
              type: :runtime
         | 
| 23 | 
            +
              prerelease: false
         | 
| 24 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 25 | 
            +
                none: false
         | 
| 26 | 
            +
                requirements:
         | 
| 27 | 
            +
                - - ! '>='
         | 
| 28 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 29 | 
            +
                    version: '0'
         | 
| 30 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 31 | 
            +
              name: rest-client
         | 
| 32 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 33 | 
            +
                none: false
         | 
| 34 | 
            +
                requirements:
         | 
| 35 | 
            +
                - - ! '>='
         | 
| 36 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 37 | 
            +
                    version: '0'
         | 
| 38 | 
            +
              type: :runtime
         | 
| 39 | 
            +
              prerelease: false
         | 
| 40 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 41 | 
            +
                none: false
         | 
| 42 | 
            +
                requirements:
         | 
| 43 | 
            +
                - - ! '>='
         | 
| 44 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 45 | 
            +
                    version: '0'
         | 
| 46 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 47 | 
            +
              name: nokogiri
         | 
| 48 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 49 | 
            +
                none: false
         | 
| 50 | 
            +
                requirements:
         | 
| 51 | 
            +
                - - ! '>='
         | 
| 52 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 53 | 
            +
                    version: '0'
         | 
| 54 | 
            +
              type: :runtime
         | 
| 55 | 
            +
              prerelease: false
         | 
| 56 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 57 | 
            +
                none: false
         | 
| 58 | 
            +
                requirements:
         | 
| 59 | 
            +
                - - ! '>='
         | 
| 60 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 61 | 
            +
                    version: '0'
         | 
| 62 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 63 | 
            +
              name: guid
         | 
| 64 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 65 | 
            +
                none: false
         | 
| 66 | 
            +
                requirements:
         | 
| 67 | 
            +
                - - ! '>='
         | 
| 68 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 69 | 
            +
                    version: '0'
         | 
| 70 | 
            +
              type: :runtime
         | 
| 71 | 
            +
              prerelease: false
         | 
| 72 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 73 | 
            +
                none: false
         | 
| 74 | 
            +
                requirements:
         | 
| 75 | 
            +
                - - ! '>='
         | 
| 76 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 77 | 
            +
                    version: '0'
         | 
| 78 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 79 | 
            +
              name: rspec
         | 
| 80 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 81 | 
            +
                none: false
         | 
| 82 | 
            +
                requirements:
         | 
| 83 | 
            +
                - - ~>
         | 
| 84 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 85 | 
            +
                    version: '2.7'
         | 
| 86 | 
            +
              type: :development
         | 
| 87 | 
            +
              prerelease: false
         | 
| 88 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 89 | 
            +
                none: false
         | 
| 90 | 
            +
                requirements:
         | 
| 91 | 
            +
                - - ~>
         | 
| 92 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 93 | 
            +
                    version: '2.7'
         | 
| 94 | 
            +
            description: An MSN client for Ruby written on top of EventMachine
         | 
| 95 | 
            +
            email: aborenszweig@manas.com.ar
         | 
| 96 | 
            +
            executables: []
         | 
| 97 | 
            +
            extensions: []
         | 
| 98 | 
            +
            extra_rdoc_files:
         | 
| 99 | 
            +
            - README.md
         | 
| 100 | 
            +
            files:
         | 
| 101 | 
            +
            - lib/em-msn.rb
         | 
| 102 | 
            +
            - lib/msn/authentication_error.rb
         | 
| 103 | 
            +
            - lib/msn/message.rb
         | 
| 104 | 
            +
            - lib/msn/contact.rb
         | 
| 105 | 
            +
            - lib/msn/nexus.rb
         | 
| 106 | 
            +
            - lib/msn/messenger.rb
         | 
| 107 | 
            +
            - lib/msn/challenge.rb
         | 
| 108 | 
            +
            - lib/msn/notification_server.rb
         | 
| 109 | 
            +
            - lib/msn/protocol.rb
         | 
| 110 | 
            +
            - lib/msn/switchboard.rb
         | 
| 111 | 
            +
            - lib/msn/version.rb
         | 
| 112 | 
            +
            - README.md
         | 
| 113 | 
            +
            homepage: http://github.com/manastech/em-msn
         | 
| 114 | 
            +
            licenses: []
         | 
| 115 | 
            +
            post_install_message: 
         | 
| 116 | 
            +
            rdoc_options: []
         | 
| 117 | 
            +
            require_paths:
         | 
| 118 | 
            +
            - lib
         | 
| 119 | 
            +
            required_ruby_version: !ruby/object:Gem::Requirement
         | 
| 120 | 
            +
              none: false
         | 
| 121 | 
            +
              requirements:
         | 
| 122 | 
            +
              - - ! '>='
         | 
| 123 | 
            +
                - !ruby/object:Gem::Version
         | 
| 124 | 
            +
                  version: '0'
         | 
| 125 | 
            +
            required_rubygems_version: !ruby/object:Gem::Requirement
         | 
| 126 | 
            +
              none: false
         | 
| 127 | 
            +
              requirements:
         | 
| 128 | 
            +
              - - ! '>='
         | 
| 129 | 
            +
                - !ruby/object:Gem::Version
         | 
| 130 | 
            +
                  version: '0'
         | 
| 131 | 
            +
            requirements: []
         | 
| 132 | 
            +
            rubyforge_project: 
         | 
| 133 | 
            +
            rubygems_version: 1.8.23
         | 
| 134 | 
            +
            signing_key: 
         | 
| 135 | 
            +
            specification_version: 3
         | 
| 136 | 
            +
            summary: MSN client (EventMachine + Ruby)
         | 
| 137 | 
            +
            test_files: []
         |