Share

Facebook Place Editor Helper

Thời gian gần đây mình không có nhiều cảm hứng viết bài, vì vậy bài viết càng ngày càng ít đi. Lâu lâu cũng tìm được cảm hướng để code cho một vài project nho nhỏ, nhưng cũng không có cảm hứng để viết, viết sợ chất lượng kém, các bạn trách.

Hôm nay cảm thấy tâm trạng khá hơn một chút, thử viết về một project nhỏ mới làm xong. Trong mấy ngày chán nản, mình có tham gia sửa địa điểm trên Facebook Place Editor. Nhưng mục đích của mình không phải là đem lại lợi ích cho ai cả, mà chỉ là cố lấy cái áo của facebook, Khi bạn đạt level 30, bạn sẽ được nhận một cái áo từ facebook.

 

Super Editor
Super Editor

Mình khá là thích cái áo này, chỉ vì nó có chữ facebook trên đó. Tất nhiên là chẳng có ý định đóng góp gì cho cộng đồng nên mình không muốn mất nhiều thời gian cho nó để làm gì, nhưng mình không muốn phá hoại, vì vậy mình xây dựng một tool dùng để tự động lựa chọn,submit các giá trị “hiển nhiên” trong một place. Và hôm nay mình sẽ hướng dẫn các bạn cách viết tool như vậy.

Bài viết này mình không viết kỹ cho lắm, nếu bạn nóng lòng thì hãy tải luôn extension tại đây nhé :

Chrome Extension

Mình viết tool dạng extension cho trình duyệt chrome. Nên trước tiên chúng ta cần file manifest.json như sau :

{
  "manifest_version": 2,
  "name": "Facebook Place Editor Bot",
  "description": "Just for T-Shirt",
  "version": "1.0",
  "content_scripts": [
    {
      "matches": ["https://www.facebook.com/editor/*"],
      "css": ["css/content.css"],
      "js": ["js/lib/jquery-2.1.4.min.js", "js/lib/underscore.js","js/content.js"],
      "run_at":"document_idle"
    }
  ],
  "background": {
    "scripts": ["js/lib/jquery-2.1.4.min.js", "js/background.js"]
  },
  "permissions": [
    "background",
    "activeTab",
    "webRequest",
    "webRequestBlocking",
    "*://*.facebook.com/*",
    "management"
  ]
}

Chúng ta đã đăng ký content script là content.js và background script là background.js, Trước tiên chúng ta sẽ code background.js. Đồng thời request một số quyền, trong đó quan trọng nhất là webRequest, activeTab và quyền truy cập vào các url của facebook.com

Background.js

File này luôn chạy khi chrome mở lên và extension của chúng ta được enable. Trước khi bắt đầu, bạn hãy vào Facebook Place Editor để thử submit một vài place và nhớ mở console, tab network, và hãy tìm giải pháp nhé.

Các bạn sẽ thấy rằng, để có nội dung hiển thị ở panel, facebook sẽ tạo một ajax request về address https://www.facebook.com/editor/fetch_more. sau đó nó sẽ hiển thị content lên panel, sau khi chúng ta submit, nó tiếp tục request vào url này và chúng ta có một loop.

Content script không thể handle được các request, nhưng background script thì có thể, đó là lý do chúng ta cần background.js. chrome.webRequest là api cho phép chúng ta can thiệp vào quá trình request và hồi đáp giữa trình duyệt và server. Các bạn đọc thêm tại chrome.webRequest.

Chúng ta sẽ handle sự kiện khi https://www.facebook.com/editor/fetch_more trả về kết quả sau khi được request thành công. Và sẽ send một message dến content script để nó biết là request đã xong và có thể bắt đầu xử lý data trên UI. code sẽ như sau :

(function(){

    var Application = function(){
        "use strict";
        this.filter = {
            urls: ["https://www.facebook.com/editor/fetch_more*"]
        };
        this.opt_extraInfoSpec = ["responseHeaders"];
    };
    Application.prototype.onCompleted = function(details){
        "use strict";
        console.log(details);
        if(details.statusCode == 200) {
            chrome.tabs.sendMessage(details.tabId, {event: "fetch_more_success"}, function (response) {
                console.log(response);
            });
        }
        else{
            console.log('Error', details);
        }
    };
    /**
     * Create new instance of application
     * @type {Application}
     */
    var app = new Application();
    /**
     * https://developer.chrome.com/extensions/webRequest#event-onBeforeRequest
     */
    chrome.webRequest.onCompleted.addListener(app.onCompleted, app.filter, app.opt_extraInfoSpec);
})();

Code đơn giản và tài liệu thì có hết rồi, nên mình nghĩ là không cần giản thích dài dòng, nếu có chỗ nào bạn nghĩ là rất khó thì hãy comment phía dưới, mình sẽ giải thích cho bạn và cập nhật vào bài viết.

content.js

Content.js là file sẽ được inject vào page https://www.facebook.com/editor/ mỗi khi nó được render. Trong file này chúng ta sẽ thực hiện các tác vụ chính. Mỗi khi nhận được message từ background.js, nó sẽ bắt đầu xử lý UI, điền thông tin và submit form. Code handel message như sau :

chrome.runtime.onMessage.addListener(
    function (request, sender, sendResponse) {
       
    }
);

Thôi mà tự nhiên thấy rất buồn, các bạn ráng đọc fulll code hộ mình, xin lỗi các bạn nhé :

(function ($) {

    jQuery.expr[':'].regex = function (elem, index, match) {
        var matchParams = match[3].split(','),
            validLabels = /^(data|css):/,
            attr = {
                method: matchParams[0].match(validLabels) ?
                    matchParams[0].split(':')[0] : 'attr',
                property: matchParams.shift().replace(validLabels, '')
            },
            regexFlags = 'ig',
            regex = new RegExp(matchParams.join('').replace(/^s+|s+$/g, ''), regexFlags);
        return regex.test(jQuery(elem)[attr.method](attr.property));
    };

    var Controler = function (options) {
        "use strict";
        var self = this;
        this.$module_editor = $('#module_editor');
        this.categoryNameMap = {
            "nhiếp ảnh": ["Nhiếp ảnh gia", "chụp ảnh"],
            "cà phê": ["Cà phê", "Quán cà phê", "Cửa hàng cà phê"],
            "Trường học": ["Cao đẳng Đại học"],
            "Học vấn": ["Giáo dục", "Trường học"],
            "Dịch vụ sửa chữa": ["Dịch vụ sửa chữa"],
            "Nhà Hàng": ["Tiệc nướng"]

        };
        this.detectedCategories = null;
        this.waitTime = Date.now();
        this.delayTime = 7000;
        this.isWaitToSubmit = false;
        setInterval(function () {
                self.updateTimer();
            }, 1000
        );
    };
    Controler.prototype.updateTimer = function () {
        if (this.isWaitToSubmit) {
            this.$module_editor.find('button[name="submit_form"]').text("Next in " + (((this.delayTime + 5000) / 1000 - 1) - Math.floor((Date.now() - this.waitTime) / 1000)) + "s");
        }
    };
    Controler.prototype.getDetectedCategories = function () {
        "use strict";
        if (this.detectedCategories == null) {
            this.detectedCategories = [];
            var self = this;
            var $categories = this.$module_editor.find('.pageSuggestTitle .fsm.fwb.fcw');
            var rawText = $categories.text();
            var rawArray = rawText.split(" · ");
            _.each(rawArray, function (name) {
                name = name.trim();
                name = self.assimilationCateogryName(name);
                self.detectedCategories.push(name);
            });
        }
        return this.detectedCategories;
    };
    Controler.prototype.autoCheckCity = function () {
        var $cityContainer = this.$module_editor.find('._51mx');
    };
    Controler.prototype.autoCheckCategories = function () {
        "use strict";
        var $cateogoriesContainer = this.$module_editor.find('._3jvu._4c10');
        var $categoryRows = $cateogoriesContainer.find('._226w');
        var self = this;
        if ($categoryRows.length > 0) {
            $categoryRows.each(function () {
                var row = $(this);
                var $name = row.find('._6a');
                var name = $name.text().trim();

                if (self.mayCheck(name) >= 0) {
                    var $button = row.find('button[value="agree"]');
                    $button.click();
                }
            });
        }
    };
    Controler.prototype.assimilationCateogryName = function (name) {
        "use strict";
        var found = null;
        _.each(this.categoryNameMap, function (names, key) {
            //console.log(names, key);
            _.each(names, function (longName) {
                if (longName == name) {
                    found = key;
                    return true;
                }
            });
            if (found != null) {
                return true;
            }
        });
        //console.log(found);
        return found;
    };
    Controler.prototype.mayCheck = function (name) {
        "use strict";
        var self = this;
        var definedCategories = this.getDetectedCategories();
        _.each(definedCategories, function (categoryName) {
            return categoryName == self.assimilationCateogryName(name);
        });
    };
    Controler.prototype.fillCity = function () {
        var $cityInput = this.$module_editor.find('input[name="place_city_id"]');
        $cityInput.val("108458769184495");
    };
    Controler.prototype.vote = function () {
        //Vote city
        var $cityVote = this.$module_editor.find("input:regex(name,place_address_vote\\[.*\"city_id\":108458769184495.*)");
        if ($cityVote.length > 0) {
            $cityVote.siblings().find('button[value="agree"]').click();
        }
        //Vote website
        var $currentWebsite = this.$module_editor.find('.pageSuggestTitle .fsm.fwn.fcw a');
        if ($currentWebsite.length > 0) {
            var href = $currentWebsite.text();
            var domain = this.getDomainFromURL(href);
            var $suggestWebsite = this.$module_editor.find("input:regex(name,page_website_vote\\[.*\"website\":.*" + domain + ".*)");
            if ($suggestWebsite.length > 0) {
                if (domain.indexOf("clj.vn") >= 0 || domain.indexOf("facebook.com") >= 0 || domain.indexOf("5giay.vn") >= 0 || domain.indexOf("webmienphi.in") >= 0) {
                    $suggestWebsite.siblings().find('button[value="disagree"]').click();
                } else {
                    $suggestWebsite.siblings().find('button[value="agree"]').click();
                }
            }

        }

    };
    Controler.prototype.getDomainFromURL = function (url) {
        var a = document.createElement('a');
        a.href = url;
        return a.hostname;
    };
    Controler.prototype.submit = function () {
        var $places_editor_save = this.$module_editor.find('#place_editor_next_area #places_editor_save');
        $places_editor_save.click();
    };
    /**
     *
     * @type {Controler}
     */
    var controller = new Controler();
    chrome.runtime.onMessage.addListener(
        function (request, sender, sendResponse) {
            if (!controller.isWaitToSubmit) {
                //console.log(sender.tab ? "from a content script:" + sender.tab.url : "from the extension");
                if (request.event == "fetch_more_success") {
                    controller.isWaitToSubmit = true;
                    controller.waitTime = Date.now();
                    setTimeout(function () {
                        controller.autoCheckCategories();
                        controller.fillCity();
                        controller.vote();
                        setTimeout(function () {
                            controller.isWaitToSubmit = false;
                            controller.submit();
                            sendResponse({success: true});
                        }, 5000);
                    }, controller.delayTime);
                }
                else {
                    sendResponse({success: false, request: request});
                }
            } else {
                sendResponse({success: false, request: request, reason: 'busy'});
            }
        }
    );
})(jQuery);

Nếu các bạn nhận ra, thì trong đây có một đoạn code cứu vớt cả cuộc đời của extension này đó là thêm regex selector cho jQuery, nếu không có nó thì code vô cùng khó. Một điểm nữa đó là có thời gian delay giữa những lần submit, nếu không thì facebook sẽ hiển thị captcha và có thể block luôn không cho sử dụng nữa. Đồng thời mình cũng đưa thao tác xử lý data và thao tác submit cách nhau 5 giây, để nếu bạn có muốn thay đổi data thì còn kịp.

Lời kết

Xin lỗi các bạn vì bài viết quá ẩu này. Mọi thắc mắc đều sẽ được mình trả lời và cập nhật vào bài viết. Mình sẽ sắp xếp thời gian cập nhật lại bài viết cho các bạn. Và hãy nhớ một điều, là đừng bao giờ cố gắng làm điều xấu, nếu không thể làm cho mọi thứ tốt đẹp hơn, thì chỉ đơn giản là làm những gì có lợi cho bản thân mà không ảnh hưởng xấu tới bất cứ cái gì khác. Trong trường hợp này, các bạn đừng cố làm sai lệch thông tin của cộng đồng, nó là công sức và thời gian của rất nhiều người. Hãy lấy áo thôi nhé.