Unsend Recall for Messenger — Recalling removed messages in Facebook Messenger
Tầm hơn 2 năm trước tình cờ mình đọc được bài blog này nói về quá trình nghiên cứu và tạo ra một extension giúp lưu và xem được các tin nhắn đã bị xoá trên Facebook bản web.
Bài blog này thú vị ở chỗ nó dùng một kiểu kĩ thuật khá lạ, thay vì bắt các event từ DOM hay window thì tác giả sử dụng module được define từ code Js của FB. Nói đơn giản thì kĩ thuật này sử dụng luôn cơ chế mà Facebook định nghĩa ra để xử lý các sự kiện về tin nhắn chứ không cần nghiên cứu thêm một cơ chế đặc biệt nào hết. Điều đó khiến cho giải pháp này vừa ngắn gọn lại vừa có khả năng tương thích tốt.
Ngoài bài blog nói trên ra thì còn 1 bài blog nữa thậm chí còn giải thích chi tiết về cơ chế define/require module của Facebook vốn dựa trên Async Module Definition - AMD:
A Facebook Sixth Sense · Alexandre Kirszenberg
Alexandre KirszenbergAlexandre Kirszenberg
Một đặc điểm chung của các sản phẩm được đề cập trong 2 bài blog này đó chính là chúng đều bị Facebook yêu cầu xoá bỏ không rõ lý do. Mình đoán việc hiểu cách sử dụng các module này cũng giống như có trong tay một con dao sắc đe doạ tới "quyền riêng tư" trên Facebook.Vì từng dành nhiều thời gian debug các web API cũng như code Js trên web của Facebook nên mình cực thích ý tưởng này và đã thử sử dụng nó để mod một số tính năng trên Facebook mà mình nghĩ là thú vị. Tuy nhiên rào cản lớn nhất đó là không phải lúc nào cũng có thể tìm được ngay một event module như thế này để sử dụng, một số module React component nếu inject được vào thì có thể chỉnh sửa ngay thông tin được hiển thị ra tuy nhiên lại rất khó để override được (immutable).
Giải pháp hoàn hảo nhất trong đầu mình lúc này là override ngay từ hàm define từ khi trang web được load lên, nhưng với hiểu biết và khả năng sử dụng Js của mình lúc ấy thì việc này là vô cùng khó. Mãi cho tới tận bây giờ sau nhiều lần, thử nhiều cách và thất bại sml, mình mới hoàn thiện được giải pháp ấy, theo mình giải pháp này có thể dùng để mod được bất kì module nào trên web Facebook tuỳ ý, giúp mình tạo ra các tính năng tiện lợi hơn theo nhu cầu (lọc ads, lọc comment, tự động thông tin,...).
Sản phẩm nho nhỏ đầu tiên mình làm ra để test giả thuyết này là đoạn userscript để gửi tin nhắn tàng hình, được giới thiệu ở bài viết này:
https://www.facebook.com/groups/j2team.community/posts/1607769529555161
Vì vậy mình viết bài blog này nhằm chia sẻ về quá trình nghiên cứu và phát triển giải pháp này, kì vọng duy nhất của mình đấy là nhỡ sau này có cần dùng lại thì mong là không quên hết vì nó khó sml :)).
Khởi đầu
Mình sẽ bắt đầu bằng việc giải thích các kiến thức học được từ 2 bài blog trên.
Facebook Sixth Sense
Ở bài blog này tác giả có 2 mục đích chính cần đạt được khi nghiên cứu mã nguồn của Facebook web:
Bắt sự kiện "typing" tin nhắn (chính là khi Facebook hiển thị ba dấu chấm trong inbox của một người khi người đó đang soạn tin gửi cho bạn) và lưu lại
Hiển thị các sự kiện typing đã được lưu
Một bức ảnh demo giao diện của extension "Facebook Sixth Sense"
Phần hiển thị của extension này khá dễ, chỉ là một popup lấy các thông tin từ localstorage được lưu lại từ trước và hiển thị ra, chỉ liên quan tới các API của chrome extension chứ không liên quan gì tới Facebook web, nên mình sẽ bỏ qua phần này.
Phần quan trọng nhất của vấn đề ở đây đó là làm sao bắt được sự kiện "typing" tin nhắn của một người dùng bất kì.
Nghiên cứu bắt đầu bằng việc tác giả nhận thấy có một response gửi về khi Facebook web polling (ở thời điểm bài blog này được viết năm 2016, Facebook vẫn còn dùng kĩ thuật polling để nhận các update mới từ server, hiện tại họ đã chuyển sang MQTT nên hầu hết các data bạn nhìn thấy khi capture request đã được encode, không có nhiều raw data để đọc như xưa và một số kĩ thuật sẽ khó để áp dụng hơn). Đây là một nhận định hay để khởi đầu, vì một khi đã tìm được response, chắc chắn sẽ có một đoạn code nào đấy chịu trách nhiệm cho việc request những thông tin này.
Phần tiếp theo có thể coi như phần giải thích cho code base của Facebook. Đây là những kiến thức đầu tiên mà mình học được khi mình bắt đầu nghiên cứu tới mảng này, nó giải thích cho việc tại sao ta có thể override một module bất kì trên trang Facebook web.
Facebook sử dụng framework React. Không ngạc nhiên lắm vì React chính là con đẻ của họ mà. Dòng này chỉ mang tính chất thông báo tránh cho bạn khỏi bỡ ngỡ khi thấy mình require React ra để demo ở dưới thôi.
Khi inspect một file code Js của Facebook (bạn nên search file nào có từ khoá "__d" vì những file này chứa definition của các module, tránh nghiên cứu những đoạn code không liên quan tốn thời gian), chúng ta sẽ có một mớ bòng bong code đại loại như thế này.
Đoạn code đã được minify của Facebook
Hầu hết các trang web lớn đều minify code Javascript của họ để giảm dung lượng, giảm chi phí băng thông. Những file code này hoàn toàn không phải dành cho con người đọc, nên đừng hi vọng chúng ta sẽ có những dòng code ngay ngắn đẹp đẽ :v.
Sau khi beautify code bằng nút có biểu tượng {} như phần cuối bức ảnh, ta được một đoạn code như thế này:
Đoạn code sau khi chúng ta beautify
Bạn nhìn thấy hàm "__d" không? Đây chính là cú pháp để khai báo một module trên web Facebook. Một hàm khai báo module sẽ có dạng đại loại như thế này:
// a, b, c, d, e, f: Some factory variables for requiring & exporing modules, I don't clearly understand yet
__d("ModuleName", ["Dependency1", "Dependency2"], function(a, b, c, d, e, f) {
//Do something
});
Tác giả giải thích rằng đây là đoạn code của Async Module Definition - Gọi tắt là AMD (không phải AMD CPU đâu nhé :v). Đây là một giải pháp định nghĩa module theo cách bất đồng bộ. Hiểu đơn giản là khi ta mở web Facebook, nó sẽ nạp vào rất nhiều module chịu trách nhiệm cho từng công việc riêng, tuy nhiên các module này có thể phụ thuộc lẫn nhau. Theo cách tư duy thông thường ta sẽ xử lý module nào không bị phụ thuộc trước (giống như khi chạy tiếp sức, mỗi người phải phụ thuộc vào người chạy trước, tuy nhiên quan hệ trong này có thể nhiều hơn một-một). Nhưng vì Javascript hỗ trợ xử lý bất đồng bộ nên cách để xử lý ít tốn thời gian nhất đó là xử lý nhiều module không phụ thuộc cùng một lúc, đây là việc mà AMD được sinh ra để giải quyết. Mình không quá hiểu về phần AMD này nên chỉ cố giải thích được theo khả năng.
Trong tư duy của mình thì đơn giản là mã nguồn của Facebook web được chia ra làm nhiều module và ta có thể chỉnh sửa độc lập trong phạm vi module mà không sợ phá vỡ tính tương thích.
Chúng ta có thể sử dụng các module trên web Facebook bằng 2 hàm là "require" và "requireLazy". Ví dụ:
require('react');
Kết quả:
Tuy nhiên hàm "require" này không đảm bảo module đã được khai báo thành công ở thời điểm bạn sử dụng nó. "requireLazy" sẽ an toàn hơn trong trường hợp này:
// Callback style
requireLazy(['react'], function(react) {
console.log(react)
});
// Promise style
function requireAsync(modules) {
return new Promise(cb => requireLazy([modules], cb));
}
requireAsync('react').then(console.log)
Kết quả:
Để biết trên trang có những module gì, ta có thể search bằng từ khoá "__d(". Mình vẫn hay search __d("ModuleName để xem module được khai báo ở đâu :v
Demo kết quả search của mình. 11739 modules!?
Hãy nhớ, vì mục đích chính của chúng ta là bắt sự kiện "typing" tin nhắn, nên nơi đầu tiên chúng ta nên chọn để bắt đầu đó là các module liên quan tới chat.
Vì vậy ở đây tác giả dùng React Dev Tools để inspect vào component trên trang
Cụ thể hơn nữa đó là bong bóng "typing". Ở đây ta có thể thấy component với cái tên rất rõ ràng "ChatTypingIndicator"
Tiếp theo tác giả search dòng chữ __d('ChatTyping để tìm kiếm module liên quan. Ở đây ta được 2 kết quả, ChatTypingIndicator.react.js và ChatTypingIndicators.react.js, chính là thứ chúng ta tìm kiếm. Hãy chú ý rằng nếu trong tên module có phần mở rộng .react, nghĩa là module này export ra một React component.
Thực ra React Dev Tools ở hiện tại cho phép nhảy ngay đến phần code tương ứng với component, chúng ta không còn cần phải search code giống như bài viết nữa, tuy nhiên mình sẽ dịch lại nguyên bản theo cách của tác giả. Một số kĩ thuật debugging mình sẽ mô tả thêm ở phần của bài blog thứ 2.
Nếu bạn đã có kinh nghiệm với React, bạn có thể thấy trong đây tên các hàm rất quen thuộc. "componentDidMount" là một trong số đó. Hàm này sẽ được thực thi sau khi component này được nạp vào DOM.
Ta thấy hàm này chứa các đoạn code subscribe vào các sự kiện, nên ta sẽ phân tích hàm này trước.
Đây là đoạn code chúng ta đang quan tâm:
function() {
var k = c("MercuryThreadInformer").getForFBID(this.props.viewer),
l = c("MercuryTypingReceiver").getForFBID(this.props.viewer);
this._subscriptions = new (c("SubscriptionsHandler"))();
this._subscriptions.addSubscriptions(
l.addRetroactiveListener("state-changed", this.typingStateChanged),
k.subscribe("messages-received", this.messagesReceived)
);
};
Lời gọi hàm c('MercuryTypingReceiver') thực chất tương tự với require('MercuryTypingReceiver'), và đưa cho nó hàm this.typingStateChanged để gọi mỗi khi có thay đổi. Dù chúng ta không rõ bản chất nó hoạt động ra sao (chắc tác giả lười phân tích :v), tuy nhiên có vẻ đây chính là hàm chúng ta cần.
Nhìn qua hàm typingStateChanged, ta có ý tưởng về cấu trúc lưu trữ của MercuryTypingReceiver. Có vẻ như nó là một dictionary dùng để map các thread id với một mảng các typing user id. Hiểu đơn giản là nó lưu trữ xem những user nào đang typing trong một thread tin nhắn.
Ok, giờ ta sẽ thử sử dụng MercuryTypingReceiver
Bằng cách sử dụng hàm "require" như mô tả ở trên, ta có thể test MercuryTypingReceiver ngay trong console (tuy nhiên với phiên bản Facebook web hiện tại thì không nhé :'( ). Kết quả được như sau:
> const MercuryTypingReceiver = require('MercuryTypingReceiver');
// undefined
> MercuryTypingReceiver
// function j(k){/* bunch of gibberish /}
Trước đó ta thấy MercuryTypingReceiver có hàm static getForFBID*. Tuy nhiên Chrome Dev Tools không cho ta inspect trực tiếp thuộc tính của hàm. Vì vậy ta sẽ wrap nó trong object (đây là một trick hay bạn có thể note lại để sử dụng).*
Ổn đấy
Giờ chúng ta xem thử các hàm static, get và getForFBID*:*
> MercuryTypingReceiver.getForFBID
// function (i){var j=this._getInstances();if(!j[i])j[i]=new this(i);return j[i];}
> MercuryTypingReceiver.get
// function (){return this.getForFBID(c('CurrentUser').getID());}
Từ đây, ta nhận ra rằng hàm MercuryTypingReceiver.getForFBID(fbid) tạo ra một instance của MercuryTypingReceiver từ fbid (id của người dùng FB), trong khi MercuryTypingReceiver.get() là một hàm tiện ích dùng để lấy instance của MercuryTypingReceiver của user hiện tại. Chúng ta sẽ bỏ qua getForFBID() và dùng hàm get() cho tiện.
Như chúng ta đã biết hàm addRetroactiveListener có params là (eventName, listener) gọi từ MercuryTypingReceiver instance. Giờ việc sử dụng của chúng ta rất đơn giản:
const inst = MercuryTypingReceiver.get();
inst.addRetroactiveListener('state-changed', state => {
console.log(state);
});
Sau khi chạy dòng code này trong console, có thể bạn đã thấy một số object bắt đầu được in ra (tuy nhiên hiện tại phần này không còn hoạt động được nữa nhé :v). Ngoài ra bạn cũng có thể tự gửi tin nhắn cho mình để xem các sự kiện này:
Điều này cũng khiến ta khẳng định thêm rằng MercuryTypingReceiver lưu trữ các trạng thái trong một dictionary và map các thread id với id của các user đang typing trong thread đó.
Đây là những thông tin mà chúng ta đang tìm kiếm, tuy nhiên nếu như chúng ta không lấy được tên của người dùng từ user id, có nghĩa là chúng ta không thực sự có được những thông tin như mong muốn.
Sau một hồi nghiên cứu mã nguồn, tác giả tìm được 2 module hữu ích khác:
MercuryThreads cho phép chúng ta lấy mọi thông tin về một thread trong Messenger từ id của nó
ShortProfiles cũng tương tự nhưng với user profiles
Cuối cùng đó chính là đoạn code hooking đưa mọi thứ vào hoạt động:
function getUserId(fbid) {
return fbid.split(":")[1];
}
requireLazy(
["MercuryTypingReceiver", "MercuryThreads", "ShortProfiles"],
(MercuryTypingReceiver, MercuryThreads, ShortProfiles) => {
MercuryTypingReceiver.get().addRetroactiveListener(
"state-changed",
onStateChanged
);
// Called every time a user starts or stops typing in a thread
function onStateChanged(state) {
// State is a dictionary that maps thread ids to the list of the
// currently typing users ids'
const threadIds = Object.keys(state);
// Walk through all threads in order to retrieve a list of all
// user ids
const userIds = threadIds.reduce(
(res, threadId) => res.concat(state[threadId].map(getUserId)),
[]
);
MercuryThreads.get().getMultiThreadMeta(threadIds, (threads) => {
ShortProfiles.getMulti(userIds, (users) => {
// Now that we've retrieved all the information we need
// about the threads and the users, we send it to the
// Chrome application to process and display it to the user.
window.postMessage(
{
type: "update",
threads,
users,
state,
},
""
);
});
});
}
}
);
Unsend Recall
Ở bài blog này, tác giả có 2 mục đích chính cần đạt được khi nghiên cứu mã nguồn của Facebook web:
Bắt sự kiện tin nhắn bị xoá, lấy thông tin về tin nhắn và lưu lại
Hiển thị lại các tin nhắn bị xoá này
Với mục đích hiển thị tin nhắn, tác giả đã sử dụng React Dev Tools để inspect vào component trên trang
Bức ảnh của tác giả thể hiện quá trình inspect element
Vì bức ảnh của tác giả hơi thiếu thông tin nên mình sẽ show thêm một bức ảnh khác thể hiện quá trình inspect React component của mình. Sau khi cài React Dev Tools thì web console của bạn sẽ hiển thị thêm 1 tab "Components" trên các trang React và 1 nút inspect riêng như mình tô đỏ ở phía bên trái. Bạn nhấn nút inspect này sau đó nhấn vào component cần inspect sẽ xem được một số thông tin hữu ích như tên của component, props, state của component, source code,...
Cho tới hiện tại đây vẫn là một cách mình thường sử dụng để xem code flow của các components. Tuy nhiên việc trace ngược từ dưới lên khó ở chỗ bạn phải trace ngược call stack lên để tìm các params mà bạn quan tâm. Các component nằm ở dưới sâu trong DOM tree thường được phân chia quá lặt vặt và không chứa các thông tin bạn mong muốn, ngoài ra trong quá trình trace có thể chúng sẽ bị wrap mất. Ví dụ dưới đây function call tới hàm render được wrap bằng hàm applyWithGuard. Nếu trace tiếp lên trên thì chỉ toàn gặp các function call lủng củng không liên quan lắm.
Vì vậy kinh nghiệm của mình khi trace các component này đó là nhìn xem tên component trông có liên quan đến nhiệm vụ của nó hay không. Ví dụ component được tác giả lựa chọn làm điểm bắt đầu của nghiên cứu đó là "MessengerRemovedMessageTombstone.react", một cái tên rất là liên quan luôn.
Ngoài ra bạn cũng có thể xem props và state của chúng, thậm chí sửa/xoá các DOM element này xem chúng ta mối liên quan gì.
Ví dụ ở đây vì không tìm được cái tên nào liên quan nên mình đã tìm loanh quanh và thấy 1 component có props chứa dòng chữ "You unsent a message", chính là dòng chữ hiển thị ở tin nhắn đã xoá.
Sau khi tìm được component liên quan rồi thì ta inspect code của nó bằng cái nút mình khoanh đỏ trên ảnh.
Sau khi nhận được đoạn code, ở đây mình lấy đoạn code của tác giả bài blog cho dễ hiểu nhé, các bạn hãy coi mấy phần ảnh ở trên mình chụp chỉ là ví dụ để làm rõ cho kiến thức thôi, chúng ta vẫn sẽ đi theo sườn nội dung của bài viết.
Ở đây ta thấy rằng phần code của hàm "getTombstoneContent" được bôi đậm trong ảnh có vẻ như là hàm chịu trách nhiệm trả về nội dung của tin nhắn bị xoá này. Để chắc chắn hơn, chúng ta có thể đặt debug để xem hàm này chịu trách nhiệm cho việc gì.
Để demo cho phần này chúng ta sẽ quay trở lại với ví dụ của component có chứa dòng chữ "You unsent a message" của mình.
Để đặt breakpoint tại một dòng, chúng ta chỉ cần nhấn vào số dòng đó. Một dòng tag màu xanh như sau sẽ hiện lên. Sau đó bất cứ khi nào đoạn code được execute tới chỗ này, nó sẽ dừng lại để cho bạn xem và sửa thông tin.
Ta cũng có thể xem callstack và đặt watch bằng thanh công cụ debug ở bên phải. Việc xem callstack giúp cho chúng ta hiểu được lời gọi hàm này xuất phát từ đâu, đặt watch giúp ta theo dõi các biến ta quan tâm có giá trị gì. Ngoài ra ta có thể di chuột vào các biến trong hàm để xem giá trị và dùng console để chỉnh sửa các giá trị trong scope theo nhu cầu.
Tóm lại ta hiểu đơn giản ở bước này ta cần tìm được hàm chịu trách nhiệm cho việc hiển thị một dữ liệu bất kì. Nếu ghi đè được hàm này ta sẽ có quyền kiểm soát nó hiển thị ra bất cứ thông tin gì mình muốn.
Tuy nhiên lại có một vấn đề khác ta cần lưu ý. Ở đây với component trong hình này, việc ghi đè hàm "getTombstoneContent" là rất dễ, do nó nằm trong 1 object (a = {}). Trong javascript object là mutable, khi ta sửa object thì object gốc cũng sẽ bị thay đổi luôn.
Vì vậy công việc của tác giả bài viết này ở đây chỉ đơn giản là gọi object này ra sau đó ghi đè lại hàm cần thiết là xong (việc gọi nó ra bằng cách nào mình sẽ giải thích sau).
Trong thực tế hầu hết các component React hiện tại mà mình gặp đều trả về 1 function, khiến cho việc ghi đè không còn dễ như vậy nữa. Hầu hết thời gian mình dành để nghiên cứu cũng là dành để giải quyết vấn đề này. Về chi tiết làm thế nào mình sẽ mô tả sau.
Sau khi xử lý được dữ liệu rồi, ta cần quan tâm tiếp tới việc làm sao để bắt được sự kiện mình mong muốn. Ở đây ta cần bắt sự kiện khi người dùng xoá tin nhắn.
Theo như mô tả của tác giả thì sau khi tìm kiếm trong mã nguồn, tác giả đã tìm thấy module tên là "MercuryThreadInformer" chứa hàm "informNewMessage". Hàm này có nhiệm vụ thông báo sự kiện mỗi khi tin nhắn được gửi tới.
Tuy các biến ở đây (và hầu hết tất cả các biến trong đống mã nguồn bạn sẽ thử đọc) đều đã được rút gọn, chúng ta có thể thấy trong object, biến a được map vào key "threadID", còn biến b được map vào key "message". Việc của chúng ta lúc này chỉ đơn giản là ghi đè hàm nói trên bằng phần xử lý của chúng ta.
Để mô tả thêm cho phương pháp ghi đè hàm trong object, mình xin đưa ra một ví dụ như sau:
// Object chứa function ban đầu
var obj = {
f: function (a, b) {
// Do something
}
}
// Function xử lý của chúng ta dùng để ghi đè
function preOverride(a, b) {
// Do some thing else
}
function postOverride(c) {
// Do something from execution result
}
// Lưu lại function ban đầu
const origF = obj.f
obj.f = function(a, b) {
preOverride(a, b);
let res = origF(a, b);
return postOverride(res);
}
Ở đây mình demo 2 kiểu override đó là override trước hàm gốc (preOverride) và override sau hàm gốc, sử dụng kết quả của hàm gốc (postOverride). Ý tưởng chính của việc override này đó là lưu lại tham chiếu tới hàm gốc ban đầu sau đó ghi đè hàm này và thêm hàm xử lý của chúng ta vào tuỳ theo nhu cầu.
Nếu muốn xem phần code hook của tác giả, bạn có thể tham khảo tại đây. Đây là phần code đã bị xoá nhưng mình may mắn fork lại được.
Tổng kết kiến thức
Sau khi vừa dịch vừa viết kiến thức của phần vừa rồi, chợt mình cảm thấy lo lắng vì 2 bài blog nêu trên vừa dài lê thê, toàn những câu từ kĩ thuật mà lại còn toàn những kiến thức bị outdate, khó thực hành được. Mình tự hỏi liệu đọc xong đống kiến thức ở trên liệu người ta hiểu vào đầu được bao nhiêu?
Bản thân mình đã nhiều lần mày mò thử nghiệm sử dụng những kĩ thuật trên hàng tháng trời trước khi bắt đầu tạo ra được một thứ gì đó, cho nên mình sẽ tóm tắt ngắn gọn những kiến thức mà mình nghĩ người đọc sẽ cần đạt được trước khi tiến xa hơn:
Hiểu biết về cách định nghĩa một module trên web Facebook
Hiểu biết cơ bản về cấu trúc của một React component, cách debug React component
Hiểu về cách sử dụng React Dev Tools, về cách debug/trace code Javascript
Biết cách chọn starting point cho nghiên cứu, bắt đầu từ những từ khoá liên quan nhất, biết cách search một số pattern keyword để tìm manh mối
Lời kết phần 1
Phần này dù dài dòng và nhiều lý thuyết, thậm chí những kiến thức có thể thực hành được thì đã outdate rất nhiều nên những gì bạn có thể thực sự đem ra áp dụng ngay sau phần này có lẽ không nhiều. Nhưng việc hiểu những kiến thức ở phần này là điều kiện rất quan trọng để bạn có thể hiểu được cách mọi thứ hoạt động và tự tạo ra những tính năng mà mình muốn, chứ không phải phụ thuộc vào một vài dòng code được chuẩn bị sẵn.
Ở phần 2 mình sẽ tập trung vào trải nghiệm thực tế của bản thân, và chia sẻ những dòng code tự mình viết ra, những dòng code mà bạn có thể paste ngay vào console để tự mình thấy kết quả. Hãy đón xem nhé!