Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Ary Borenszweig committed Nov 1, 2012
0 parents commit 1d4cc86
Show file tree
Hide file tree
Showing 12 changed files with 347 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.bundle
*.gem
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
source :rubygems

gemspec
20 changes: 20 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
PATH
remote: .
specs:
em-msn (0.1)
eventmachine
rest-client

GEM
remote: http://rubygems.org/
specs:
eventmachine (1.0.0)
mime-types (1.19)
rest-client (1.6.7)
mime-types (>= 1.16)

PLATFORMS
ruby

DEPENDENCIES
em-msn!
Empty file added README.md
Empty file.
31 changes: 31 additions & 0 deletions em-msn.gemspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# -*- encoding: utf-8 -*-
$:.push File.expand_path("../lib", __FILE__)
require "msn/version"

Gem::Specification.new do |s|
s.name = "em-msn"
s.version = Msn::VERSION
s.platform = Gem::Platform::RUBY
s.authors = ["Ary Borenszweig"]
s.email = %q{aborenszweig@manas.com.ar}
s.homepage = "http://github.com/manastech/em-msn"
s.summary = %q{MSN client (EventMachine + Ruby)}
s.description = %q{An MSN client for Ruby written on top of EventMachine}

s.files = [
"lib/em-msn.rb",
"lib/msn/message.rb",
"lib/msn/messenger.rb",
"lib/msn/notification_server.rb",
"lib/msn/protocol.rb",
"lib/msn/switchboard.rb",
"lib/msn/version.rb",
]

s.require_path = "lib"
s.has_rdoc = false
s.extra_rdoc_files = ["README.md"]

s.add_dependency "eventmachine"
s.add_dependency "rest-client"
end
14 changes: 14 additions & 0 deletions lib/em-msn.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module Msn
end

require 'eventmachine'
require 'rest-client'
require 'fiber'
require 'cgi'
require 'digest/md5'

require_relative 'msn/protocol'
require_relative 'msn/message'
require_relative 'msn/notification_server'
require_relative 'msn/switchboard'
require_relative 'msn/messenger'
11 changes: 11 additions & 0 deletions lib/msn/message.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class Msn::Message
attr_accessor :email
attr_accessor :display_name
attr_accessor :text

def initialize(email, display_name, text)
@email = email
@display_name = display_name
@text = text
end
end
59 changes: 59 additions & 0 deletions lib/msn/messenger.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
class Msn::Messenger
attr_reader :username
attr_reader :password

def initialize(username, password)
@username = username
@password = password
end

def connect
@notification_server = EM.connect 'messenger.hotmail.com', 1863, Msn::NotificationServer, self
end

def set_online_status(status)
case status
when :available, :online
@notification_server.chg "NLN", 0
when :busy
@notification_server.chg "BSY", 0
when :idle
@notification_server.chg "IDL", 0
when :brb, :be_right_back
@notification_server.chg "BRB", 0
when :away
@notification_server.chg "AWY", 0
when :phone, :on_the_phone
@notification_server.chg "PHN", 0
when :lunch, :out_to_lunch
@notification_server.chg "LUN", 0
else
raise "Wrong online status: #{status}"
end
end

def on_ready(&handler)
@on_ready_handler = handler
end

def on_message(&handler)
@on_message_handler = handler
end

def accept_message(message)
@on_message_handler.call(message) if @on_message_handler
end

def ready
@on_ready_handler.call if @on_ready_handler
end

def self.debug
@debug
end

def self.debug=(debug)
@debug = debug
end
end

99 changes: 99 additions & 0 deletions lib/msn/notification_server.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
class Msn::NotificationServer < EventMachine::Connection
include Msn::Protocol

attr_reader :messenger
attr_reader :display_name

def initialize(messenger)
@messenger = messenger
end

def username
messenger.username
end

def password
messenger.password
end

def post_init
super

login
end

def login
Fiber.new do
response = ver "MSNP8"
if response[2] != "MSNP8"
raise "Expected response to be 'VER 0 MSNP8' but it was '#{response}'"
end

response = cvr "0x0409", "winnt", "5.1", "i386", "MSNMSGR", "6.0.0602", "MSMSGS", username
if response[2] == "1.0.0000"
raise "The client version we are sending is not compatible anymore :-("
end

response = usr "TWN", "I", username
if response[0] == "XFR" && response[2] == "NS"
host, port = response[3].split ':'
@reconnect_host, @reconnect_port = response[3].split ':'
close_connection
else
login_with_challenge(response[4])
end
end.resume
end

def login_with_challenge(challenge)
nexus_response = RestClient.get "https://nexus.passport.com/rdr/pprdr.asp"
passport_urls = nexus_response.headers[:passporturls]
passport_urls = Hash[passport_urls.split(',').map { |key_value| key_value.split('=', 2) }]
passport_url = "https://#{passport_urls['DALogin']}"

authorization = "Passport1.4 OrgVerb=GET,OrgURL=http%3A%2F%2Fmessenger%2Emsn%2Ecom,sign-in=#{CGI.escape username},pwd=#{CGI.escape password},#{challenge}"
da_login_response = RestClient.get passport_url, 'Authorization' => authorization
if da_login_response.net_http_res.code != "200"
raise "Login failed (1)"
end

authentication_info = da_login_response.headers[:authentication_info]
authentication_info = authentication_info["Passport1.4 ".length .. -1]
authentication_info = Hash[authentication_info.split(',').map { |key_value| key_value.split('=', 2) }]
if authentication_info['da-status'] != "success"
raise "Login failed (2)"
end

from_pp = authentication_info['from-PP']
token = from_pp[1 .. -2] # remove single quotes

first_msg = true
on_event('MSG') do
if first_msg
first_msg = false
messenger.ready
end
end

on_event('RNG') do |header|
host, port = header[2].split(':')
rng_conn = EM.connect host, port, Msn::Switchboard, messenger
rng_conn.ans username, header[4], header[1]
end

response = usr "TWN", "S", token
if response[2] != "OK"
raise "Login failed (3)"
end

@display_name = CGI.unescape response[4]
end

def unbind
if @reconnect_host
reconnect @reconnect_host, @reconnect_port.to_i
@reconnect_host = @reconnect_port = nil
login
end
end
end
86 changes: 86 additions & 0 deletions lib/msn/protocol.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
module Msn::Protocol
include EventMachine::Protocols::LineText2

def post_init
@trid = 0
@command_fibers = {}
end

def receive_line(line)
puts "<< #{line}" if Msn::Messenger.debug
pieces = line.split(' ')

case pieces[0]
when 'CHL'
answer_challenge pieces[2]
when 'RNG'
handle_event pieces
when 'MSG'
@header = pieces

size = pieces.last.to_i
set_binary_mode size
when 'CHG', 'QRY'
# ignore
else
if fiber = @command_fibers.delete(pieces[1].to_i)
fiber.resume pieces
else
handle_event pieces
end
end
end

def receive_binary_data(data)
puts "<<* #{data}" if Msn::Messenger.debug

handle_event @header, data
end

def handle_event(header, data = nil)
return unless @event_handlers

handler = @event_handlers[header[0]]
if handler
Fiber.new do
handler.call header, data
end.resume
end
end

def answer_challenge(challenge_string)
payload = Digest::MD5.hexdigest "#{challenge_string}Q1P7W2E4J9R8U3S5"

data = "QRY #{@trid} msmsgs@msnmsgr.com 32\r\n#{payload}"
puts ">>* #{data}" if Msn::Messenger.debug

send_data data

@trid += 1
end

def send_command(command, *args)
@command_fibers[@trid] = Fiber.current

text = "#{command} #{@trid} #{args.join ' '}\r\n"
puts ">> #{text}" if Msn::Messenger.debug
send_data text
@trid += 1

Fiber.yield
end

def on_event(kind, &block)
@event_handlers ||= {}
@event_handlers[kind] = block
end

def method_missing(name, *args)
send_command name.upcase, *args
end

def unbind
puts "Chau :-("
end
end

19 changes: 19 additions & 0 deletions lib/msn/switchboard.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
class Msn::Switchboard < EventMachine::Connection
include Msn::Protocol

def initialize(messenger)
@messenger = messenger

on_event 'MSG' do |header, data|
email = header[1]
display_name = header[2]
head, body = data.split "\r\n\r\n", 2
headers = Hash[head.split("\r\n").map { |line| line.split ':', 2 }]

if headers['Content-Type'] =~ %r(text/plain)
@messenger.accept_message Msn::Message.new(email, display_name, body)
end
end
end
end

3 changes: 3 additions & 0 deletions lib/msn/version.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module Msn
VERSION = "0.1"
end

0 comments on commit 1d4cc86

Please sign in to comment.