Facebook Chat API
Facebook uses a Comet and JSON approach to receive incoming messages. Comet is a technique where you open a long running HTTP connection to a server (usually AJAX). The connection stays open until the server has something to tell you at which point it sends the data through the connection and the connection can be closed (or in some cases remain open). JSON is just a way of representing data in Javascript. Let’s look at how it works.
A connection is opened to: http://[ANYTHING].channel[NUM].facebook.com/x/[ANYTHING]/false/p_[UID]=[SEQ]. First let’s look at the url and what the different placeholders represent.
ANYTHING can be anything, Facebook has setup *.channel[NUM].facebook.com as a catch-all subdomain. So no matter what you put there it will resolve to the same IP. The ANYTHING in the path can also be anything. I assume both the ANYTHING values are used to prevent the browser from caching these requests.
NUM represents the channel number you are using. It appears there are 20 channels (01-20), as that range of subdomains resolve and the rest don’t. I am assuming that a “channel” is just one of Facebook’s comet interface servers. Every time you log in you are assigned to a specific channel and it doesn’t change throughout your session. It may also be fixed per user, I’m not sure about this though.
UID is just your user id. SEQ is the sequence number you are requesting from the server. If you give an out of range seq number the server will tell you what the latest seq number is so that you can request it. If you give an old seq number the server will show you the (old) messages that correspond with that number. Requesting http://0.channel06.facebook.com/x/0/false/p_MYID=-1 will immediately respond telling me I should re-request with a seq number of 0 (or whatever my current seq number is) since the seq number -1 is never valid.
view plaincopy to clipboardprint
1. // request http://0.channel06.facebook.com/x/0/false/p_MYID=-1
2. for (;;);{"t":"refresh", "seq":0}
// request http://0.channel06.facebook.com/x/0/false/p_MYID=-1
for (;;);{"t":"refresh", "seq":0}
After we build the correct URL and make the connection, it will just “sit there”. This is how Comet connections work. The connection will remain inactive in a “requesting” state until someone sends us a message. If a message is not received in less than a minute (approx 55 seconds) Facebook will close the connection and instruct you to reopen it. I assume they do this to get around browser timeout issues. If a message is received you will get the message data through the connection which is then closed. After a message is received the seq number should be incremented for the next request. After either a message is received or the connection times out a new connection will immediately be established.
view plaincopy to clipboardprint
1. // request http://0.channel06.facebook.com/x/0/false/p_MYID=0 times out
2. for (;;);{"t":"continue"}
3. // request http://0.channel06.facebook.com/x/0/false/p_MYID=0 receives a message!
4. for (;;);{"t":"msg","c":"p_MYID","ms":[{"type":"msg","msg":{"text":"yo","time":1209557234412,"clientTime":1209557232415,"msgID":"4177168544"},"from":FRIENDSID,"to":MYID,"from_name":"Myfriends Name","to_name":"My name","from_first_name":"Myfriends","to_first_name":"My"}]}
5. // request http://0.channel06.facebook.com/x/0/false/p_MYID=1 for the next message
// request http://0.channel06.facebook.com/x/0/false/p_MYID=0 times out
for (;;);{"t":"continue"}
// request http://0.channel06.facebook.com/x/0/false/p_MYID=0 receives a message!
for (;;);{"t":"msg","c":"p_MYID","ms":[{"type":"msg","msg":{"text":"yo","time":1209557234412,"clientTime":1209557232415,"msgID":"4177168544"},"from":FRIENDSID,"to":MYID,"from_name":"Myfriends Name","to_name":"My name","from_first_name":"Myfriends","to_first_name":"My"}]}
// request http://0.channel06.facebook.com/x/0/false/p_MYID=1 for the next message
Sending Messages
Sending messages is a lot simpler. All you need to do is make a POST request to http://www.facebook.com/ajax/chat/send.php with a few simple values: msg_text with the text of the message, to with the uid of the recipient, msg_id with a random unique number id for the message, client_time with the current time in microseconds, and post_form_id which can be found as a hidden form value on any facebook page. Facebook will respond with some JSON indicating there was no error and the message will be sent!
view plaincopy to clipboardprint
1. // POST request to: http://www.facebook.com/ajax/chat/send.php?msg_text=hey&msg_id34567890&to=FRIENDSID&client_time09558664256&post_form_id)7fdbad61d3b1d88f0c311cda25bbbc
2. for (;;);{"error":0,"errorSummary":"","errorDescription":"No error.","payload":[]}
// POST request to: http://www.facebook.com/ajax/chat/send.php?msg_text=hey&msg_id34567890&to=FRIENDSID&client_time09558664256&post_form_id)7fdbad61d3b1d88f0c311cda25bbbc
for (;;);{"error":0,"errorSummary":"","errorDescription":"No error.","payload":[]}
Buddy List
The list of your friends who are online is polled about every 3 minutes with a POST request to http://www.facebook.com/ajax/presence/update.php. The ‘userInfos’ hash contains the actual buddy list. Some extra info is provided to let you know how the buddy list has changed since the last request.
view plaincopy to clipboardprint
1. // POST request to http://www.facebook.com/ajax/presence/update.php?buddy_list=1
2. for (;;);{"error":0,"errorSummary":"","errorDescription":"No error.","payload":{"buddy_list":{"listChanged":true,"availableCount":1,"nowAvailableList":{"UID1":{"i":false}},"wasAvailableIDs":[],"userInfos":{"UID1":{"name":"Buddy 1","firstName":"Buddy","thumbSrc":"http:\/\/static.ak.fbcdn.net\/pics\/q_default.gif","status":null,"statusTime":0,"statusTimeRel":""},"UID2":{"name":"Buddi 2","firstName":"Buddi","thumbSrc":"http:\/\/static.ak.fbcdn.net\/pics\/q_default.gif","status":null,"statusTime":0,"statusTimeRel":""}},"forcedRender":true},"time":1209560380000}}
// POST request to http://www.facebook.com/ajax/presence/update.php?buddy_list=1
for (;;);{"error":0,"errorSummary":"","errorDescription":"No error.","payload":{"buddy_list":{"listChanged":true,"availableCount":1,"nowAvailableList":{"UID1":{"i":false}},"wasAvailableIDs":[],"userInfos":{"UID1":{"name":"Buddy 1","firstName":"Buddy","thumbSrc":"http:\/\/static.ak.fbcdn.net\/pics\/q_default.gif","status":null,"statusTime":0,"statusTimeRel":""},"UID2":{"name":"Buddi 2","firstName":"Buddi","thumbSrc":"http:\/\/static.ak.fbcdn.net\/pics\/q_default.gif","status":null,"statusTime":0,"statusTimeRel":""}},"forcedRender":true},"time":1209560380000}}
Notifications
Notifications are polled through the same mechanism as the buddy list. You need to pass ¬ifications=1 to update.php to receive info about them. Other than that I won’t be covering them here.
The Fun Part
So here’s a very simple implementation of a Facebook Chat client written in Ruby. It will require you have the json and mechanize gems installed.
view plaincopy to clipboardprint
1. require 'mechanize'
2. require 'json'
3. require 'ostruct'
4. require 'pp'
5.
6. class FacebookChat
7. def initialize(email, pass); @email, @pass = email, pass; end
8.
9. def login
10. @agent = WWW::Mechanize.new
11. @agent.user_agent_alias = 'Windows IE 7'
12. f = @agent.get("http://facebook.com/login.php").forms.first
13. f.fields.name("email").value = @email
14. f.fields.name("pass").value = @pass
15. f.submit
16. body = @agent.get("http://www.facebook.com/home.php").body
17.
18. # parse info out of facebook home page
19. @uid = %r{<a href=".+?/profile.php\?id=(\d+)" class="profile_nav_link">Profile</a>}.match(body)[1].to_i
20. @channel = %r{"channel(\d+)"}.match(body)[1]
21. @post_form_id = %r{<input type="hidden" id="post_form_id" name="post_form_id" value="([^"]+)}.match(body)[1]
22. end
23.
24. def wait_for_messages
25. determine_initial_seq_number unless @seq
26.
27. begin
28. json = parse_json @agent.get(get_message_url(@seq)).body
29. end while json["t"] == "continue" # no messages yet, keep waiting
30. @seq += 1
31.
32. json["ms"].select{|m| m['type'] == 'msg'}.map do |msg|
33. info = msg.delete 'msg'
34. msg['text'] = info['text']
35. msg['time'] = Time.at(info['time']/1000)
36. OpenStruct.new msg
37. end.reject {|msg| msg.from == @uid } # get rid of messages from us
38. end
39.
40. def send_message(uid, text)
41. r = @agent.post "http://www.facebook.com/ajax/chat/send.php",
42. 'msg_text' => text,
43. 'msg_id' => rand(999999999),
44. 'client_time' => (Time.now.to_f*1000).to_i,
45. 'to' => uid,
46. 'post_form_id' => @post_form_id
47. end
48.
49. def buddy_list
50. json = parse_json(@agent.post("http://www.facebook.com/ajax/presence/update.php",
51. 'buddy_list' => 1, 'post_form_id' => @post_form_id, 'user' => @uid).body)
52. json['payload']['buddy_list']['userInfos'].inject({}) do |hash, (uid, info)|
53. hash.merge uid => info['name']
54. end
55. end
56.
57. private
58.
59. def determine_initial_seq_number
60. # -1 will always be a bad seq number so fb will tell us what the correct one is
61. json = parse_json @agent.get(get_message_url(-1)).body
62. @seq = json["seq"].to_i
63. end
64.
65. def get_message_url(seq)
66. "http://0.channel#{@channel}.facebook.com/x/0/false/p_#{@uid}=#{seq}"
67. end
68.
69. # get rid of initial js junk, like 'for(;;);'
70. def parse_json(s)
71. JSON.parse s.sub(/^[^{]+/, '')
72. end
73. end
74.
75. if __FILE__ == $0
76. fb = FacebookChat.new(ARGV.shift, ARGV.shift)
77. fb.login
78.
79. puts "Buddy List:"
80. pp fb.buddy_list
81.
82. Thread.abort_on_exception = true
83. Thread.new do
84. puts usage = "Enter message as <facebook_id> <message> (eg: 124423 hey man wassup?) or type 'buddy' for buddy list"
85. loop do
86. case gets.strip
87. when 'buddy' then pp fb.buddy_list
88. when /^(\d+) (.+)$/
89. uid, text = $1.to_i, $2
90. fb.send_message(uid, text)
91. else
92. puts usage
93. end
94. end
95. end
96.
97. # message receiving loop
98. loop do
99. fb.wait_for_messages.each do |msg|
100. puts "[#{msg.time.strftime('%H:%M')}] #{msg.from_name} (#{msg.from}): #{msg.text}"
101. end
102. end
103. end
source: coderrr