em-msn 0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|