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: []
|