tags: cofacts —

rumors-line-bot 過去傳過訊息 implementation

Past discussion: https://github.com/cofacts/rumors-line-bot/issues/133

User story

Task breakdown

  1. Chatbot server 需連結至新的資料庫。此資料庫:
    • 記錄每名 chatbot 使用者曾經查詢過哪些 article、哪些有新 reply
    • 記錄每名 chatbot 使用者是否點擊觀看新 reply(列表標上「新 reply 標記」,在使用者點入連結前持續顯示)
    • 紀錄每名 chatbot 使用者曾經接收過哪些 reply 的推播(避免重複推播)
  2. Chatbot 提供一個「查詢過的訊息列表」
    • 應使用 LIFF 實作、透過 rich menu 觸發
    • 使用者透過訊息列表點入訊息,需要標記成已讀、消除新 reply 標記

      「點入訊息」應該無法直接顯示文章頁面,因為這樣無法送出「有用」或「沒用」。 這可能要回到 chatbot 顯示或者直接在 LIFF 另外實作,才能正常讓使用者送出「有用」或者是「沒用」。

    • 應重構現有 LIFF 實作,套用 server-render 或 client UI library 以利開發
    • 應讓 postback button 往上捲回去的時候仍然可用
    • 需在 chatbot server 上建立 API server 支援上述列表與操作 server-sider render, 實作 CSRF protection 不想 proxy Cofacts API 也想降低 server load 而不走 server-side render,最後作成 GraphQL server + token-based authentication
  3. 需實作一 cron job 實作通知,包含:
    • 透過 push API 或 LINE Notify 告知 Chatbot 使用者有新回應。該 notification 的 call-to-action 就是請使用者打開「查詢過的訊息列表」介面。
    • 更新相對應的新 reply 標記(標成未讀)

資料庫: mongodb w/ mLab

496MB Free!

紀錄每個 bot user 與訊息的羈絆 (?)

[!NOTE] 大小估算: 2017~2020 年所有 user selects an article + confirm submit article = (270,018+18,369) ~= 300,000,每個都是一個 user-article link

粗估每個 document 500byte 300,000 * 0.5KB = 150MB

資料表:UserSettings

一個 LINE bot user 僅會有一筆 UserSetting。

[!NOTE] 大小估算: 200byte/record * 200K LINE bot users = 40MB

什麼時候會新增一筆 UserSettings

使用者加 chatbot 為好友時。

LINE bot user flow

  1. User 點擊 rich menu 選擇看過去訊息 / 收到 notification 並點按
  2. LINE bot 跳出全頁 LIFF,展示文章列表:
    • 時間
    • 文章內文節錄(即時從 Cofacts API 抓)
    • 未讀回應數(即時從 Cofacts API 抓回應,然後挑出比 lastViewedAt 晚的)+ 未讀標記
  3. 點擊文章列表項目,會傳訊息進 Cofacts chatbot,chatbot 列出未讀的回應列表,供使用者點按,進入 CHOOSING_REPLY state。若只有一則未讀回應,自動 skipUser
    • 此時要更新 corresponding UserArticleLink 的 lastViewedAt
    • 此設計讓使用者能下接「有用沒用」回應
    • 此設計亦方便使用者轉傳回應

Implementation detail (2020/5/10)

分成下面三個部分:

1. LIFF <> chatbot GraphQL API 的新認證方式

目前 LIFF 與 chatbot GraphQL 的 authentication,仰賴 chatbot 在 URL 上帶有的自產 JWT,讓 chatbot GraphQL 知道目前是哪個 userId 在使用 LIFF,以及是哪個 search session (by sessionId) 的按鈕。

可是,新功能「過去傳過訊息」LIFF 會放在 rich menu,URL 裡無法帶有會過期的 JWT;直接不放 exp 或設定過久的 exp 也有些危險,畢竟 JWT 被 chatbot graphql 視為等同密碼的存在,一但 leak 出去,只要 exp 沒到,攻擊者就可以一直用該 JWT 的身份存取 API。

因此,我們會新增一種新的 authentication:LINE ID token 法。

  1. LIFF 透過 liff.getIDToken() 拿到 LINE 發的 OpenID ID token
  2. LIFF 發 request 到 chatbot GraphQL 時帶有 Authorization: line ${ID token} (line 是為了與 JWT 使用的 bearer 做出區隔的自訂 authentication type)
  3. chatbot GraphQL 收到 line 開頭的 authorization header 之後,使用 verify ID token API 檢查是否正確,並且取用回傳的 user ID (sub)。

以上也是 LINE 官方文件中提到的作法。

由於 LINE ID token 必須呼叫 LINE API 才能取得,不會有洩漏的機會,所以比目前 chatbot 自發 JWT 的機制還更安全。不過,目前有些功能仍然需要保留「從 chatbot 傳資料給 LIFF 回到 GraphQL」的機制(如在按鈕上帶當時的 sessionId,LIFF 發 request 時也必須帶有該 sesionId 回到 GraphQL,且此 sessionId 不該在 LIFF 端被竄改) ,chatbot 自發 JWT 依然有不可取代的功能在,故會兩者並行。

不過,使用此種方式,會有下面的限制:

2. LIFF 顯示文章列表的機制

LIFF 從 chatbot GraphQL 取回的資料將不會帶有訊息文字之類的訊息。整個載入流程會是:

  1. LIFF 透過 myArticleLinks API 取得要顯示的 article links
  2. LIFF 顯示表格、可點選的超連結,文字部分則為 loading 樣態 (類似 https://github.com/zalog/placeholder-loading )

另外,顯示時發送 GA visit event,utm_source=myArticleLink, utm_medium 從 URL 拿,預設是 richmenu。

3. 點擊項目時觸發的東西

點擊表內訊息後,送出「📃 See new replies of https://cofacts.g0v.tw/article/<articleId>」 到聊天室,觸發下面動作:

[!NOTE] 未來推播實作時,由 push message 觸發的 LIFF URL 應帶有 utm_medium=push, 而發送到聊天室的內容也會稍微不同(如換 emoji 為 📌);chatbot 也使用此 emoji,判斷 utm_medium 之後要設定為何。

如此方可將「自己從 rich menu 點開 LIFF」的動作,與「看了 push message 之後點進來 LIFF」,跟其他一般查詢(完全不帶有 utm_xxx)分開。

2020/7/10 Update

根據 GA 文件 traffic source dimensions 與 collect campaign data,utm_source 應是「導流量到這個網站的來源」、utm_medium 則是媒介。

對於 LIFF 裡的網頁來說,source 就是顯示 URL / button 讓使用者進來的流量來源,對這裡來說就是特定的 LINE channel;而媒介則可能是 push messaage 或是 rich menu。因此:

Cron job 邏輯

  1. 從所有 UserArticleLinks 找出最大的 lastRepliedAt 作為 lastScannedAt

  2. 去 ListArticle API (sort by lastRepliedAt)列出所有在 lastScannedAt 以後才新建立、且未刪除的 ArticleReply
    • 刪除回應不會被偵測到
    • 編輯審閱時間:去掉最近 12 hr 的 articleReply
  3. 由 2 整理出 list of unique, updated article ids
  4. 從 user <> selected article 關聯表,找出要通知的對象 (list of unique users to update)
  5. 比照這些使用者的 UserSettings,使用 multicast API & LINE Notify 告知使用者「之前的訊息有回應囉」導引使用者點開 LIFF
  6. redis or DB 紀錄這次執行的 timestamp