用於 Pump.io 的 facebook sync套件 -> Pump.io facebook sync plugin

大家好我是Davis,這次要介紹一下 Pump.io 以及今年暑假實習幫 Pump.io 做了什麼事情。 Pump.io 是一個基於 node.js ,且開放原始碼的「分散式社群網路」。

「分散式社群網路」這概念不同於目前市面上常看到的社群網路諸如FB, twitter 等等,在以往的社群網路中,都是有一個集中的伺服器,所有的人都在該伺服器上面註冊帳號等等。而「分散式社群網路」是一種比較新穎的概念,使用者可以自己建立實體 (instance) ,每個實體都可以有複數的使用者,在發文、follow等概念上與其他社群網路類似,但是在實體與實體之間,則依然可以讓使用者作互相follow的動作,卻沒有註冊該實體。

舉例來說,A實體上有一個「使用者甲」,而實體B上面有「使用者子」,「使用者甲」可以在獲得「使用者子」的ID連結之後,到該頁面對「使用者子」進 follow 的動作,實體B只會要求「使用者甲」進行認證的動作,取得其 token,這邊的 token 是 OAuth 標準下的一個令牌的概念,在不需要經過帳號密碼的情況下,授權一個網站在一段時間內可以使用該用戶的資源,在這邊就是將 follow 的人的文章 po 到自己的牆上,詳細的 OAuth 的介紹可以在 wiki 上找到。

因此,在不同實體底下的用戶可以透過 token,將訊息貼到對方的牆上,而不需要讓「使用者甲」在實體B上面重新註冊一個帳號。

一言以蔽之,用一張圖作為解釋,左半邊為正常的facebook結構,而右邊為pump.io這類的分散式社群網路為結構

圖1:

圖片來源:diasproa


facebook bridge

pump.io 在 github 有許多的 issues,而其中幾個 issues 是作者希望可以提供與其他社群網路同步的服務。

同步的好處多多,原因是目前pump.io是沒有搜尋的功能,也就是說,即使在一個實體中註冊了非常多的帳號,仍然沒有辦法搜尋到別的帳號作追蹤,更不用說在不同的實體。反正,沒有別人的ID,就沒有辦法作追蹤的服務。

因此透過同步的方式,除了可以通知目前的狀態,更可以將自己帳號的連結透過各式各樣的社群網路發佈出去,因此讓別人可以輕易找尋到你,也可以加入pump.io的家庭。

這次製作的是 facebook bridge,可以在pump.io 上發文、打卡,並且同步到 facebook (issues 連結)。

目前同步服務中較有多人在使用的是噗浪 (plurk) 的同步服務,噗浪提供的同步服務可以透過連結的方式將社群網路的帳號作連線,並在噗浪發的文章會同時發佈到各個地方,介面上如圖2:

在發文的部份,參考的是 facebook 網頁的打卡服務,如圖3:


pump.io 結構分析

pump.io 的結構,前端是由 backbone.js 所構成,而後端是 Node.js + express + mongoDB。

backbone.js 是一個類似 MVC 的前端 framework,也可以說根本沒有 C (controller),透過 model 向後端取得資料之後,顯示到V (view) 上。總之,詳細的使用方式可以到backbone.js 官網,其中也有提供不少的範例,這邊就只討論 pump.io 的發文結構,看圖4:

發文 function 會將文章打包成 activity 的物件,pump.io 同時也是一個 JSON activity stream 的 server,所有的訊息都是透過這樣的格式在做傳輸。因此文章也是一個 activity,裡面除了發佈的內容之外,也取用資料庫的api以及文章連結等資訊。

經過一連串的callback,最終得到的 activity 會被 ajax 放到 pump.io 的牆上面,這邊的概念可以想像成是 facebook 的塗鴉牆。在貼到塗鴉牆上的 activity 將會是資訊最完整的部份,所以我決定在這邊將資料同步給 facebook。


activity 的結構分析

activity 的結構如圖5:

我們可以從中取出 content 以及 url ,此兩項分別儲存該篇發文的網址以及內文。利用這兩個值,我們可以把 po 到 FB 的文章中放入除了內文之外,更外嵌需要的 url,可以讓其他人透過 FB 連到 pump.io。


facebook javascript 使用設定

facebook 提供了相當多的SDK套件,其中我們使用 javascript api。 javascript 的 api 會先執行 FB.init 的動作,這是為了載入資料庫,以及填寫需要的 facebook app ID 。

此動作會在 window.fbAsyncInit 的時候執行,這可能會在頁面出來前或出來之後被執行到,而通常這時候 UI 其實已經建立完成,因此,稍後會使用 jQuery 的 trigger 的方式做介面上面的更新。 如程式碼1:

<div id="fb-root"></div>
<script>
  window.fbAsyncInit = function() {
    // init the FB JS SDK
    FB.init({
      appId      : 'YOUR_APP_ID',                        // App ID from the app dashboard
      channelUrl : '//WWW.YOUR_DOMAIN.COM/channel.html', // Channel file for x-domain comms
      status     : true,                                 // Check Facebook Login status
      xfbml      : true                                  // Look for social plugins on the page
    });

    // Additional initialization code such as adding Event Listeners goes here
  };

  // Load the SDK asynchronously
  (function(d, s, id){
     var js, fjs = d.getElementsByTagName(s)[0];
     if (d.getElementById(id)) {return;}
     js = d.createElement(s); js.id = id;
     js.src = "//connect.facebook.net/en_US/all.js";
     fjs.parentNode.insertBefore(js, fjs);
   }(document, 'script', 'facebook-jssdk'));
</script>

facebook graph api 使用方法

graph api 在使用上必須要先Login,如程式碼2:

FB.login(function (response) {
            FB.getLoginStatus(function (response) {
                if (response.status === 'connected') {
                    //Do something when you have allready log in, like update UI
                    callback(true);
                } else {
                    //User cancelled login or did not fully authorize. You should show on              
                    the UI
                }
            });
        }, {
            scope: 'email, user_likes, offline_access, publish_stream, read_stream'
        });

而特別注意到在scope的部份,是詢問取用的權限,也就是說,必須要有這些權限,才可以做po文,讀取文章甚至是接下來要取得打卡地點。


FB login, po文設計

  • Login

    • FB.Event.subscribe()

      在window.fbAsyncInit 的 function,確認目前登入的情況,若為登入且認證時,則做更新頁面的動作。

    • getStatusFB

      在頁面重新整理或是登出再重新登入之後,因為fbAsyncInit不會再次運行,所以透過此function再次取得目前的狀態。

    • loginFB

      在沒有做認證或是瀏覽器沒有登入FB的情況下,透過此方法做登入並取得權限。

  • postFB

    將activity的內容取出,並且發佈到FB,並可以透過發佈的連結連結到pump.io頁面。


backbone.js 結構設計

backbone.js 是一個前端的類MVC架構的framework,可以很結構化的在modal地方與node.js存取物件,而Controller與View幾乎是合併的狀態下生成頁面,詳細請到官網或是這個基礎學習。 而在這邊只針對需要的頁面做增加程式碼。

如程式碼3:

events: {
    "click #logout": "logout",
    "click #post-note-button": "postNoteModal",
    "click #post-picture-button": "postPictureModal",
    "click #post-FB-button":"getFBLogin",
    "click #post-place-button": "postPlaceModal"
},

其中events的array中表示監聽「事件」、「物件」、「function」,而頁面設計上,「#post-FB-button」是我的按鍵的id, 當「click」事件發生的時候,呼叫「getFBLogin」方法。而該方法是使用jQuery對頁面上的按鈕做隱藏或是顯示。

如程式碼4:

getFBLogin: function() {
            facebookconnect.loginFB(function(res){
                if (res) {
                    $('#post-FB-button .icon-thumbs-up').show();
                    $('#post-FB-button .icon-thumbs-down').hide();
                    $('#post-note #FBcheckbox').show();
                } else {
                    $('#post-FB-button .icon-thumbs-up').hide();
                    $('#post-FB-button .icon-thumbs-down').show();
                    $('#post-note #FBcheckbox').hide();
                }
            });
            return false;
        },

facebook po文 function 實作

圖6:

程式碼5:

text = view.$('#post-note #note-content').val(),
                to = view.$('#post-note #note-to').val(),
                cc = view.$('#post-note #note-cc').val(),
                checkbox = view.$('#post-note #FBcheckbox').attr("checked"),
                act = new Pump.Activity({
                    verb: "post",
                    object: {
                        objectType: "note",
                        content: text
                    }
                }),

                Pump.newMajorActivity(act, function(err, act) {
                if (err) {
                    view.showError(err);
                    view.stopSpin();
                } else {
                    //FB post
                    if(checkbox){
                        facebookconnect.postFB(act);
                    }else{
                        console.log('you dont want to post to FB');
                    }
                    view.stopSpin();
                    view.$el.modal('hide');
                    Pump.resetWysihtml5(view.$('#note-content'));
                    // Reload the current page
                    Pump.addMajorActivity(act);
                    view.remove();
                }

為實作在FB同步文章。在發佈文章的方法中,會先去抓取其中的物件,而程式碼5為判斷是否將選取框打溝,接著依照pump.io的設計,將文章到後端做儲存,儲存之後會回傳到前端用ajax的方式貼文到牆上。

在貼到牆上之前,判斷checkbox是否為勾起的狀態,若確定同步則呼叫 postFB,如程式碼6:

postFB: function (act) {
        var link = "\n see the pump -> " + act.attributes.object.url;
        var str = act.attributes.object.content + link;
        var regex = /<br\s*[\/]?>/gi;
        str = str.replace(regex, "\n");
        var body = str;
        FB.api('/me/feed', 'post', {
            message: body
        }, function(response) {
            if (!response || response.error) {
                console.log(response.error);
                console.log('Error occured');
            } else {
            }
        });
    },

首先先取得本篇文章的url,之後可以透過此連結來到pump.io的頁面。 再使用FB.api的post方法,並將要發佈的文章帶入message中,並發佈到FB牆上。


成果

圖7,在pump.io上的po文

圖8,在FB上的同步po文

透過FB上面的連結,可以連結到pump.io上的該篇文章。


facebook 打卡需求及 function 設計

再來討論打卡的部份。

在打卡之前,可以透過 UI 看到這附近有什麼點可以打,必須有個 table 還有地圖 打卡的地點會是從FB api中取得,所以必須要有 read_stream 的權限打卡的地圖透過 google map api 取得為了取得目前的位置,使用 HTML5的navigator.geolocation.getCurrentPostion

function 設計:

  • getLocation
    • 使用 HTML 5 的 geo api 取得目前的經緯度,必須在wifi開著的情況下
  • getPlace
    • 針對目前的經緯度,取得這附近的打卡地點,回傳是25個
  • mapImgUrl
    • 針對取回的25個地點的經緯度,透過google map api 取得每個地點的地圖照片,並一起傳到UI 事件。
  • getPlaceLink
    • 打卡前,針對選取的地點取得其FB的網址
  • postPlaceFB
    • 組合連結並且打卡,api 的部份幾個月前已經與 post 整合了,因此不再使用 checkin api

HTML 5 geoLocation 使用方法

程式碼7:

if (navigator.geolocation) {
            navigator.geolocation.getCurrentPosition(function(position) {
                callback(position);
            }, function(error) {
                console.log(error);
                alert('Error occurred. Error code: ' + error.code);   
            });
        }else{
            alert('no geolocation support');
        }

圖9:

使用此 function 要特別注意必須開起 wifi,若有有線網路則無法取得目前位置。 而在取用之前,一定會有使用條款,因此應該等到確定使用者同意存取地點之後再進行打卡的動作,才不會發生錯誤。

回傳值為經緯度。


google map api 使用方法

程式碼8:

var mapImgUrl = function (latlon) {
                    return "http://maps.googleapis.com/maps/api/staticmap?center=" + latlon + "&zoom=16&size=500x200&sensor=false&markers=size:mid%7Ccolor:red%7C" + latlon;
                },

圖10:

給予經緯度,就可以透過此方法可以取得以給予的經緯度為中心的點 google map 的靜態圖片。


facebook get location

圖11:

透過經緯度以及設定距離,可以得到這範圍內所有可以打卡的地點,而每個地點都會有其 Facebook 頁面的 ID 以及經緯度。

一次回傳的值為25個,透過其給予每一個經緯度,我們可以取得每個地點的靜態圖片。


backbone.js 設計

程式碼9:

data: {
   user: Pump.principalUser,
   lists: lists,
   following: following,
   places: places
}

events: {
            "click #send-place": "postPlace",
            "hover .place-select li": "hoverPlace",
            "click .place-select li": "selectPlace"
},

hoverPlace: function (ev) {
            $('.place-select img').attr('src', $(ev.currentTarget).attr('imgurl'));
        },

selectPlace: function (ev) {
            $('.place-select li').removeClass('active');
            $(ev.currentTarget).addClass('active');
            return false;
        }

透過data的陣列可以將資料傳至前端的utml(pump.io作者由ejs改寫),其中places帶的值為前面得到的25個地理資訊、名稱以及由google map得到的地理圖片。

event中間聽hover與click事件,當hover發生的時候,就將imgurl顯示出來,而click事件發生時變先移除先前選好的地點,並把新的地點重新覆蓋上顏色。


place-selector.utml

程式碼10:

<div class="place-select">
  <ul>
    <% if (places && places.length > 0) { %>
      <% _.each(places, function (place) { %>
      <li fbid="<%- place.id %>" imgurl="<%- place.imgUrl %>">
        <a href="javascript:void(0)"><%- place.name %></a>
      </li>
      <% }); %>
    <% } %>
  </ul>
  <img width="100%" />
</div>

此段程式碼為前端的ejs,由前面的data傳入資料之後,變可以在utml中,利用較為簡單,類似javascript的code將需要的資料方式呈現出來。


facebook 打卡

圖12:

程式碼11:

selectPlace = $('.place-select .active').attr('fbid');
facebookconnect.getPlaceLink(selectPlace, function(respone){
var addText = '--@ <a href="'+respone.link + '">' + respone.name + '</a>';

if(checkbox && selectPlace){
    facebookconnect.postPlaceFB(act, selectPlace, text);
}else{
    console.log('you dont want to post to FB');
}

當確定選好地點要打卡之後,先將選取地點中的 FB ID 取出,並透過 facebook 的 search api 取得該地點的 Facebook 頁面(一串 url ),並放入貼文之中,傳入後端儲存。

儲存成功之後,傳到前端的同時也透過先前設計postPlaceFB api將貼文同步到facebook。

posPlaceFB 程式碼12:

postPlaceFB: function (act, id, text) {
        var link = "\n see the pump -> " + act.attributes.object.url;
        var str = text + link;
        var regex = /<br\s*[\/]?>/gi;
        str = str.replace(regex, "\n");
        FB.api('/me/feed', 'post', {
            message: str,
            place: id
        }, function(response) {
            if (!response || response.error) {
                console.log(response.error);
                console.log('Error occured');
            } else {
                console.log(response);
            }
        });
    },

facebook的post api中,若特別帶上place的id時,就從po文變成了打卡的形式,在文章上面就會出現該地點的圖案,以及在貼文的最後附上打卡的地點。


成果

圖13:

圖14:


總結

pump.io 提供了一個全新的社群網路模式,這個模式有點像是 email 的社群版,除了有更多的隱私之外,每個 pump.io instance 也可以有自己客製化的功能,若不希望自己的資料會外流到別人的伺服器,建立一個屬於自己的社群網路也會是一個好的選項。 目前 pump.io 在推廣上面是稍微困難一些,因為即使是在同一個 instace 上面,也沒有辦法簡單的找到其他的用戶。

facebook bridget 希望做到的是提供一個簡單的推廣方案,可以透過發文上的連結,讓親朋好友連 結到 pump.io 的系統,進而推廣 pump.io,因為這樣的方式可以讓朋友很方便的知道你的 pump.io ID,也才有辦法做follow 的動作。

謝謝大家看完這篇文章,也希望透過此文章,不管是在分散式社群網路、facebook graph api 以及 backbone.js 的使用上有更進一步的認知,謝謝!