Total Pageviews

Saturday 25 April 2020

CSObot:一個幫助中國公民社會組織的機器人

A bot serving the civil society organisations in China.
推送平台:

#csobot:matrix.org (Matrix 平台的房间)

目前推送:

  • 使用 OONI probe 探测可能被墙网站(采用 Citizen Lab 的cn 列表),每日执行一次。OONI probe 报告发送到 OONI API, 报告编号和被墙网站数量推送到以上平台。
  • IPFS 转换器:把单一网页上传到 IPFS 并返回相应 IPFS gateway 网址(大部分可直接访问;约4~5个url)
  • 用户发送以下命令,稍等片刻即可得到 IPFS gateway 网址(如果遇到下载网页失败请再试一下):    ipfs-add
  • 检查 Signal (Android)无谷歌框架版的更新(官方发布网站),如有更新,推送下载链接和其校验码。(每日3次)
项目仓库:csobot

〔附〕Matrix平台使用方法

Matrix 房間
#csobot:matrix.org
加入姿势:
  1. 使用 Matrix 客戶端 (推荐 Riot.im),点击「+ 」搜索如 #csobot:matrix.org 即可进入。
  2. Riot.im 也有網頁版:  访问 https://riot.im/app/#/room/#csobot:matrix.org即可看到消息(但想互动则需登录)。riot.im 网页版,点击[ Room Directory] 可搜索房间。

另有 IRC 頻道与 Matrix 同步
#aqi-data-share at oftc.net
(加入姿势: IRC 客戶端(如Hexchat)中的「网络列表」里选择: freenode, 再在「服务器」菜单选择「加入一个频道」,加入上边的频道名即可 )
謹記:必須搭配 Tor 否則完全暴露自己 IP 。用 Tor 访问 Freenode 方法请参考这个博客
(注:本 bot 仍在开发中,今后会有变更,以项目仓库README为准:csobot。以后可能迁移至GitLab) --------------------------------

一个 IPFS 转换器-csobot

我能做的一點小事,就是在大數據和網絡實名制的時代,盡力地抵制国家机器的监控,捍卫言论自由、资讯自由。
之前听到网友有个需求:给出一个网页,把它传到IPFS,并吐出 IPFS gateway 地址,方便传播(chuan qiang)。
现在这个功能来了!
csobot,将帮助您完成这项工作 :joy:
这个机器人在Matrix平台提供服务(可能大家比较陌生,但为了实践去中心化精神,我开始减少使用中心化应用了)。
如果你下载 Riot.im 客户端(iOS,Google Play,F-Droid 和桌面版),注册登录之后,点击下方的 + 按钮进入房间:#csobot:matrix.org
在聊天框里输入:ipfs-add 
稍等片刻,即可得到5个该网页的 IPFS gateway 网址啦~
Riot.im 也提供web端(Riot.im 只是 Matrix 平台的一种客户端):https://riot.im/app
在网页选择 Room Directory 即进入这个搜索页面
(该房间免登录即可进入,若互动则需登录一下)
目前为初期功能,欢迎提需求和建议~(目前只会下载该网页本身,不会延伸下载第三方资源,所以有的图片若来源于别处则不会显示)
顺便强调一下 Matrix,如官网所述:是一种开源的、安全的(端对端加密)、去中心化实时通讯网络架构。它能让你像发邮件一样联络到对方,却不必要求使用统一的(客户端)应用;可以自主选择你信任的服务器来提供通讯服务(也叫「邦联式」);
端对端加密级的保密性;
基于web的 HTTP API,保持开放性。
-----------------------------------------

matrix 菜鳥使用心得

一直想架一個自由的即時通訊軟體, 之前 大概研究了幾個自由的 im , 最後因為有社團的資源可以蹭,所以架了聯邦制的 im matrix。 matrix 在架構上介於 discord(或 slack)與 irc 的中間, 但因為聯邦制和注重隱私,又多了一些不便。 加上很多功能還在開發中,會少一些功能,需要手動操作。 目前要直接從 discord 搬過來 matrix 會不太方便。 本文介紹了 matrix 的基本概念、使用經驗、與 discord 的比較, 最後是如何以 restful api 管理伺服器。

matrix 命名空間

matrix 是所謂聯邦制,在不同 server 上註冊的使用者可以互相溝通。 所有的資源都是掛在某一個 server 下, 包括使用者、頻道、頻道群組(像 discord 的 server 或說 guild), 但使用者仍可存取不同的 server 上的資源。

matrix 上的名稱建構方式,像是 irc 和 twitter 的綜合版, 再加上用冒號來後綴伺服器名來達成, 在同一個伺服器上則可以省去後綴的伺服器名。

伺服器名即是域名,例如 ccns 的伺服器名是 ccns.io , 那建立在 ccns.io 上的 general 頻道完整名稱即寫成 #general:ccns.io 。 而註冊在 ccns 上的使用者 gholk 即是 @gholk:ccns.io 。 和 xmpp 使用的 email 風格名稱 gholk@ccns.io 不太一樣。

頻道

頻道也就是群組聊天,是聊天的基本單位; 可以是一對一,也可以是多對一; 可以是公開,也可以是私密或端對端加密。 頻道也可以有地址,有地址的頻道方可以用井號加伺服器名的方式表示, 這類有地址的頻道就比較像 irc discord 或 telegram 上的頻道。 當然,伺服器管理者可以直接管理地址, 或控制是否允許使用者建立具有地址的頻道。 不然註冊像 #admin:ccns.io 之類的頻道會讓管理者很困擾。

在地址之外,也可以有單純用來顯示的頻道名稱。 其它像一對一的聊天,或是加了多個人的群聊, 預設是沒有地址也沒有名稱的,也是預設加密的。 名稱和地址可以事後再加上,但加密是不能解除。 這關係到所有加密的是端對端加密, 伺服器上保存的只是加密的資料, 所以沒辦法做到解除加密的操作。

頻道的公開程度可以有三種, 可被搜尋與加入、可用連結加入、需要被邀請才能加入。 和加密的選項是獨立的。

一個頻道也可以有多個地址, 雖然我想不到這功能有什麼應用。 目前我們的 discord bridge bot 會自動用 discord 的頻道 id 來當作頻道的地址, 如果我們想把 discord 上的 general 也當作 matrix 的 general, 就必須要在 id 之外再加上額外的地址 general。

頻道權限

頻道的權限是用分數決定的。 最高分者是是管理者,可以有絕對的權限,預設建立者是 100 分。 每個人可以調整較低分者的權限,最高到和自己同分為止。

每人預設是 0 分,moderator 是 50 分,admin 是 100 分, 頻道設定裡可以調不同的操作需要哪種身份, 像只有 admin 能 invite 或 moderator 可以刪文之類的。

admin 可以把人升到 admin,但 admin 之間不能互踢之類的。 此外 matrix 目前也沒有跨頻道的權限管理機制, 唯一比較像跨頻道的一個是 server admin 可以直接把人加入或踼出頻道, 且也可以把人直接升為頻道管理員。

頻道群組

有個東西叫 community,有點像 discord 上的 server, 可以把幾個頻道放到同一個群組裡。 群組是用加號開頭,可以有 avatar、html 的介紹。 使用者只要點選群組的圖示,就可以看到所有群組裡的頻道。 但也就這樣而已。

沒有權限管理,沒有頻道管理,沒有命名空間管理。 各頻道間還是幾乎獨立,沒辦法一次加入所有頻道, 沒辦法把一人指定為所有頻道的管理員, 也不能統一管理所有頻道的公開、加密程度。 而且沒有命名空間管理,所以例如 +free-rms:matrix.org 群組, 裡面有 press random general 等頻道,如果他們都要有地址, 只能叫 #free-rms-press:matrix.org #free-rms-random:matrix.org #free-rms:matrix.org , 至於為什麼沒有 #free-rms-general:matrix.org , 因為 general 即是通用的頻道,自然可以省去地址裡的 general。

不過這很大程度是因為 community 還是個開發中的功能。 在一開始,這個功能叫 group,現在叫 community。 未來應該會多出一個叫 space 的功能,取代現有的 community, 很有可能,因為現在已經在 beta 了。 只能希望未來這功能會好用一點,像把上面的幾點都改善一下。 而且這功能現在還缺少一些基本的 api,像不能改地址, 也沒有增加除了 creator 外的 admin 的功能。

space 似乎比較神奇,他是一種可以巢狀的頻道, 主要開發的原因是官方發現他們在實作 group 時, 幾乎要把現有的頻道功能重複實現一遍, 像權限管理、邀請之類的,他們覺得很沒意義。 所以他們新增了一種特殊的可以巢狀的頻道, 然後重用頻道現有的權限管理等 api。

discord 與 slack 的概觀

再來是與時下最強群聊軟體沒有之一的 discord 的類比。 以經營組織來說,discord 的確是很強的平台, 當初的 ccns 也是搬到 discord 上後慢慢累積起凝聚力的。

discord 或 slack 都繼承自 irc 的伺服器概念, 一個組織會有一個伺服器,一個伺服器下可以有多個頻道, 同一個伺服器內的管理者是同一群人。 但又和 irc 那種限於頻道內的自治不同, 在不同的 irc 伺服器間,同一個頻道的訊息是會互相轉發的, 而且 irc 管理者也不太會管理到頻道內的事。

在此之外,discord 和 slack 又有一些差別, slack 不同的伺服器間幾乎是完全獨立的, 在每個伺服器都需要重新註冊一遍, 從不同伺服器使用不同的域名就可以大概看出來。 (如 https://g0v.slack.com 或 https://ccca.slack.com 。) 且不同的伺服器需要在獨立的分頁運作, 不同伺服器的使用者當然也不可能互相溝通。 slack 唯一統一的就是應用程式,或說擴充功能的介面和商店。

而 discord 則較開放。 所謂的伺服器,比較像一個組織, 不知道程式中還是不是對應到一個伺服器。 使用者加入的所有的伺服器,都一樣掛在 discord.com 這個域名下。 (以前是 discordapp.com,後來可能有錢了,把 discord.com 買回來了。) 不同伺服器都有同一個 web 介面中展示, 不同伺服器中的帳號也是統一的,只需要在 discord 註冊一次, 使用者間也可以互相溝通。

slack 可能想製造一種感覺,聊天伺服器的確是由該組織建立的, 是一個內部架設供內部使用的工具,而 slack 只是提供托管伺服器平台。 所以使用者當然需要獨立註冊。 而我猜 discord 則單純認為統一註冊比較簡單而已, 當然可能也考慮了使用者情境。 discord 最早是供遊戲使用的聊天平台, 所以很多人可能只是從某個遊戲得知 discord 這個平台, 上面有一些資訊,所以來到這裡。 然後如果還需要註冊才能進入,那可能就會有一大半放棄了, (相比多數不需登入的 web 上的攻略。) 所以開放了允許訪客的功能。 整體比 slack 要 開放 很多。

discord 與 matrix 的比較

matrix 中有二種功能可以和 discord 的 server 中類比, 一是用實際一部 matrix server,二是用 matrix 中的 community 功能; 但其實二者都不太像。

先前說過,matrix 上允許頻道的自治, 雖然 server admin 能直接把人新增為頻道的 admin, 但頻道內的 admin 還是和 server 的 admin 獨立的。 每個頻道有自己的管理者,最多只能做到同一群人建立的頻道中, 都由同一群人來擔任管理者,但還是需要一一指派。 不像 discord 內,可以直接在 server 中把人劃為不同的階級, 依階級對不同頻道有不同的存取權。

matrix 中的 community 可以用來組織頻道, 但尚不包含統一管理權限的功能。 而且組織事實上是與頻道的管理無關的, 任何人都可以建立組織,將任意的頻道加入自己有管理權限的組織中, 無論在該頻道中是為管理員。 (但如果要建立具有地址的組織, 就得看 server admin 是否開放這項功能,和建立具名頻道一樣。) 所以組織目前比較像是建立了個人用的對該頻道的 捷徑 , 而不是像 discord 中頻道必定隸屬 server 的階層式關係。

而且詳細來看,在 server 下 discord 還有一層 頻道群組 , 其實那層也是關鍵,因為可以把幾個內部頻道放一起設定權限, 而不用一個個單獨設。 其實如果用 matrix server 對比 disocrd server, 那 category 就是類比到 community, 但偏偏 community 又做的比較像 server。 (都是在左側側邊欄的小圈圈。)

如果用命名空間來看,要類比 discord 上的 server, 還是用一座 matrix server 比較適合。 以命名空間來說,某個 server 下的某個頻道, 套在 discord 和 matrix 上都是正確的。 category 和 community 就暫時忽略吧。

matrix 也有把 discord 的 reaction 功能抄過來。 但 bridge 時有二個問題, 一是 bot 沒辦法把 matrix 的 reaction 傳到 discord 上, 因為 discord bot 只有一個,沒辦法傳遞多個 emoji。 而 bridge 則會用建立人頭帳號的方把 discord 上的帳號 bridge 到 matrix, 所以可以用同樣的人頭做出和 discord 上同樣的反應。

另一個比較麻煩的是我們的 discord server 有大量的 costum reaction, matrix 規格現在還不支援。 bridge 會把 costum reaction 的圖片存到 matrix 上, 然後用 mxc 的連結網址當作 reaction name 來對訊息做 react。 但目前 element 還不會把 mxc reaction 顯示成圖片, 就只會看到一串網址。

顯示 custom reaction 的油猴腳本

如上所述,因為我們 discord 伺服器的自訂 reaction 有點多, 加上很多人會用按 reaction 取代傳訊息, 所以有時在 matrix 上看到訊息下有 5、6 個人反應, 但只看 mxc 的 hash 看不出來是在按什麼意思的,就很不方便。

於是就簡單寫了一個 油猴腳本來顯示自訂的反應 , 只要每次 dom 改變時, 就檢查有沒有 reaction 的文字內容是 mxc url, 有的話就把 mxc 換成對應的圖片。

用 mxc 取得圖片的 api 可以在 server-client 規範裡找到。 監聽 dom 改變的 api, 我是參考另一支 user script google open in new tab。 當初是想改掉 google 搜尋結果中,按右鍵複製網址時, 只會複製到 google 的重導向的網址的問題。 但原本那支太笨重了,我也不需要在新分頁開啟的功能, 就自己改寫了比較 簡短的版本

server client 與 federation 溝通

matrix 是聯邦式的架構,而不是 p2p, 所以和 xmpp 類似,所有資訊都是透過自己的 server 進行, 而不會發生客戶端直接連到對方伺服器的情況。 例如我 @gholk:ccns.io 想傳訊息到 #free-rms:matrix.org 這個頻道, 那我在客戶端操作後,客戶端會把要求傳給我的伺服器, 我的伺服器 ccns.io 會去向 matrix.org 發起連線傳送訊息。

server 端的代理機制

但因為 matrix 有設計 delegation 機制, 可以讓 server 可以用比較好看的網域名稱, 前一篇安裝教學也有稍微提到。 所以要傳訊息給 matrix.org 時, 會先查 matrix.org 這個名字的 matrix 通訊歸誰管:

  1. 查 _matrix._tcp.matrix.org 這個域名的 dns srv 記錄, 代表要和 matrix.org 以 matrix 協議通訊時,實際負責的是哪個伺服器: host -t SRV _matrix._tcp.matrix.org 。

  2. 如果沒有查到,那接著就會嘗試用連到 http://matrix.org/.well-known/matrix/server , 看該 json 檔案有沒有指定 matrix 由哪個伺服器負責。 如果該位址是重導向,則會跟隨重導向。

  3. 如果也沒查到,最後就會直接把該網址當作 matrix server 連線。

所以當你發訊息到 *:matrix.org 時, 依 dns srv 記錄可以看到, 實際上負責 matrix.org 域名的 matrix server 是 matrix-federation.matrix.org.cdn.cloudflare.net 。 而 ccns.io 時,則是 https://ccns.io/.well-known/matrix/server 會轉址到 http://www.ccns.io/.well-known/matrix/server , json 內容即是指定到 matrix.ccns.io:443 {"m.server":"matrix.ccns.io:443"} 。

client 端代理機制

上述的傳訊息,是由客戶端發請求給自己的伺服器端, 自己的伺服器端再去找訊息的收件人是在哪個伺服器上。 但客戶端要如何傳送此一請求給自己的伺服器呢? 同樣有類似的代理機制,但又不太一樣。

例如以 @gholk:ccns.io 的身份登入時, 客戶端一樣會去找 ccns.io 這個名稱的伺服器在哪。 但依規範不會檢查 dns,只會檢查 https://ccns.io/.well-known/matrix/client , 因為 matrix 在定規範時考量到想設計出 純前端 client, 所以不會定瀏覽器前端用 javascript 做不到的事。 前端 javascript 不能查 dns,甚至要發 http request 到其它域名, 都需要 cors 標頭,所以 client 代理機制還要求 well known 網址需要有 cors header, 而且轉址的 30X 回覆本身也需要有 cors header。

但因為我們社團 ccns.io 是架在 github page 上, 而 github page 會把 ccns.io 所有網址都用 301 轉址到 www.ccns.io, 且該轉址到目前為止是沒有 cors 的。 所以很明顯,要用客戶端登入 ccns.io 時, 伺服器不能只打 ccns.io,不然客戶端看不到 https://ccns.io/.well-known/matrix/client , 就不會去找 matrix.ccns.io,而會直接把請求發到 ccns.io, 然後就無法登入。 因此要在客戶端登入時,只能直接打 matrix.ccns.io。

element 中取消隱藏 community

這是比較蠢的小事,有關 element ui 設計的問題。 一次點了 ccns community 的三個點的更多符號, 按了 hide 把 community 隱藏了, 然後就不知道怎麼讓該 community 顯示回來。 後來找了很久才找到,要點 community 最下面的 + 新增 community, 然後中間欄的下方會顯示 Your Communities , 把要加回去的 community icon 拖拉到最左側的 community 欄即可。

matrix 金鑰與訊息的儲存

matrix 使用金鑰來驗證身份,client 第一次登入時會產生金鑰, 使用者可以選擇將金鑰備份到 matrix server(在以密碼加密的情況下), 或只存在本機。金鑰以 48 個大小寫英數字混合組成, 如果金鑰沒有備份到伺服器端,則遺失金鑰沒有人能救的了你; 當然遺失密碼也是一樣。

matrix 中所有訊息都存在伺服器端, 加密頻道中訊息是以 e2e 加密的型式儲存的, 所以伺服器存的是加密過的訊息, 伺服器自己也無法解讀,故在搜尋訊息上會不太方便。

另外還有在使用另一台裝置登入時,會需要驗證該登入; 可以用金鑰驗證或用已登入的裝置驗證, 主要就是把金鑰傳到另一台裝置上,讓另一台裝置也能解讀 e2e 訊息。 此步驟可以確保帳號後的使用者是持有同一份金鑰的使用者, 就算伺服器的管理者接管了你的帳號,但他也不會有你的金鑰, 所以其它人也會發現這個人不一樣了。

一些細節可以參考 element 對密碼和金鑰的說明 中 End-to-end encryption 的章節。

mx-puppet-discord 的權限管理

預設 mx-puppet-discord 預設橋接過來後, 是用 discord 的 server id 和 guild id 組成一個很醜的頻道地址, 像原本 discord 上的 general 會變成 #_discordpuppet__330361736932884482:ccns.io ; 然後權限會是只有被邀請者才能加入。 至少要調這二個部份,讓原本在 discord 公開的頻道一樣是公開, 然後有一個好看一點的地址,像原本的 general 就一樣叫 general 就好。

mx-puppet-discord 橋接過來後, 預設只有 mx-puppet-discord bot 自己是頻道管理者, 但有提供一個叫 adminme #channel_id:server_name 的指令, 讓 bot 把使用者在某個頻道調整為管理者; 但這個指令當然設定橋接的那個使用者能用。 要所以要調整的方式:

  1. 先傳送訊息給 bot adminme #_discordpuppet__330361736932884482:ccns.io , 讓他把使用者調成 admin。

  2. 之後去該頻道的設定頁,新增 local address, 除了原本的一長串 discord id 地址外, 再增加和頻道同名的地址。

  3. 修改設定讓所有人都可以加入。

因為 matrix 不像 discord 可以一次設定整個分類或 server 中所有頻道的權限, 所以用 mx-puppet-discord 橋接,自動建立了一堆頻道後, 要一一調整設定其實蠻麻煩的。

由於我們 discord 上有十幾個頻道, 要一個個點顯然不切實際,所以只好自己看 matrix restful api, 用 wget 和 shell 腳本來一次處理完。

使用 shell 腳本批次處理頻道權限

matrix 幾乎所有 api 都是 restful, 可以發 http request 來達成那類修改權限和名稱的操作。 但首先需要是該頻道的 admin, 不知道把頻道 id 一一複製下來再一次貼給 bot 可不可以。 (後來試了似乎是不行。)

取得登入後的 token

這類 api 最簡單的驗證方式是用 token, 也就是用 client 登入後,會取得一個 token, 把 client 端已經取得的 token,直接放在 http header 裡, matrix 就會認得了。 matrix 官方有建議 client 應該要設計一個 ui, 能使用者看到目前使用的 token, 也就能配合其它工具 debug 或 做壞事 管理 server。

以 element 來說,可以點選左上角的使用者帳號選單, 開啟個人設定,在 help 分頁裡有一個展開符號可以顯示 access token, token 應該是英數字和底線組成。 取得之後,把 token 放在要發的 http request Authorization header 裡, 加上一個 bearer 前綴:

wget --header "Authorization: Bearer $token" $request
curl --header "Authorization: Bearer $token" $request

如果在 production 環境上,預設可能只有 wget, wget 有用的選項是 wget -q --server-response , 可以把 http 回應的標頭一起顯示。

matrix api

首先需要取得所有頻道的列表, 當然要先加入所有 bridge 過來的頻道, 以 mx-puppet-bridge 來說可以用 joinentireguild 指令 來讓 bot 對自己發出邀請加入所有 bridge 的頻道, 但這個指令只有當初設定 bridge 的人可以用。 之後可以在個人設定裡找到一個選項,是一次同意所有邀請。 這部份可以參見 matrix 安裝中的 bot 連結頻道章節 。

之後就能用 matrix api 取得所有頻道列表了,指令如下。 其中 token 是上面取得的登入取得的 token, 返回內容是一個 json。 所以可以先準備個 jq,或直接用 python 或 node js 來解析也可以。

curl --header  "Authorization: Bearer $token"  \
    https://matrix.ccns.io/_matrix/client/r0/joined_rooms

返回 json 的 joined_rooms 屬性是加入的頻道 id 列表, id 是 matrix 在內部用於唯一識別頻道的, 我們還需要取得各頻道的名稱,然後為該頻道加上同名的地址, 與調整權限讓所有使用者可以加入。 更多 api 可以參考 matrix 官方的規格文件 。

取得頻道屬性

取得頻道所有屬性的 api 是 state, 但可能會返回太多內容,像每個成員的名稱、icon。

curl -H "Authorization: Bearer $token" \
    https://matrix.ccns.io/_matrix/client/r0/rooms/$id/state

如果不想取回太多東西, 可以在 state 後面加上想取回的事件的類型: https://matrix.ccns.io/_matrix/client/r0/rooms/$id/state/m.room.name , 本文會用到的大概有 m.room.name m.room.guest_access m.room.join_rules m.room.canonical_alias 。

增加頻道的地址

我們要先把 room name 取回,然後加為地址,也就是 alias:

json=$(curl -H "Authorization: Bearer $token" \
    https://matrix.ccns.io/_matrix/client/r0/rooms/$id/state/m.room.name)
name=$(node -p "($json).name")
curl -X put -H "Authorization: Bearer $token" \
        https://matrix.ccns.io/_matrix/client/r0/rooms/$id/aliases/$name

把頻道改為任何人可加入

因為用 wget 每次都打一堆選項很麻煩, 所以這裡用一個 alias 把選項塞在一起。 還有因為在 bash 裡打 json 很麻煩, 所以用 node 包裝了一個小工具,可以簡單生成 json; 只要寫單純的 javascript object,就會轉成有雙引號的 json。 如果要比較正式的工具,可以用 jo , 或是可以到 github 用我自己寫的 jc 完整版 。

alias www='wget -q --server-response -O -'
jc() {
    node --print "JSON.stringify({$1}, null, '  ')"
}

www --method put --body-data "$(jc "join_rule:'public'")" \
    --header  "Authorization: Bearer $token" \
    https://matrix.ccns.io/_matrix/client/r0/rooms/$id/state/m.room.join_rules/
www --method put --body-data "$(jc 'guest_access:"can_join"')" \
    --header  "Authorization: Bearer $token" \
    http://localhost:8008/_matrix/client/r0/rooms/$id/state/m.room.guest_access/

邀請使用者加入頻道

user 是使用者的完整地址。

# example:
# user=@gholk:ccns.io
curl -H "Authorization: Bearer $token" \
    -H 'content-type: application/json' \
    --data "$(jc "user_id:'$user'")" \
    https://matrix.ccns.io/_matrix/client/r0/rooms/$id/invite

使用者自行要求加入

這 api 只能用來加入公開頻道, 私人頻道一定要有人邀請。

curl -H "Authorization: Bearer $token" \
    -H 'content-type: application/json' \
    --data '{}' \
    https://matrix.ccns.io/_matrix/client/r0/join/$room

有 server admin 權限的情況下

如果有 server 的 admin 權限, 也就在安裝 synapse 時建立的特殊 admin user, 就能用 synapse 的 admin api 直接把人升為 admin。 如果沒有,在有 server 的 root 權限的話也能直接建立一個:

sudo -su matrix-synapse <<MATRIX
cd /opt/venvs/matrix-synapse
. bin/activate
register_new_matrix_user --admin \
    --config /etc/matrix-synapse/homeserver.yaml \
    --user root --password my-password
    https://matrix.ccns.io

MATRIX

之後使用 admin 的流程也是相同, 用 client 登入後,把 token 複製出來用 curl 或 wget 發。 admin 權限能直接把任意使用者升為頻道的 admin。 但這類使用 server admin 的 api, 還沒標準化為 matrix 的一部份,是 synapse 限定的:

wget -O - -q --server-response \
    --post-data "$(jc "user_id:'$user'")" \
    --header  "Authorization: Bearer $token_admin"  \
    https://matrix.ccns.io/_synapse/admin/v1/rooms/$id/make_room_admin

直接取得所有頻道資料

此外,server admin 能直接取得所有頻道的資料, 但 matrix.ccns 上的 _synapse 路徑我是鎖起來的, 不能從外網存取,所以得直接登入 matrix.ccns 上, 然後找找 localhost。

wget -O - -q --server-response \
    --header  "Authorization: Bearer $token_admin"  \
    http://localhost:8008/_synapse/admin/v1/rooms

新增頻道管理員

該使用者需要在頻道內:

www --post-data "$(jc "user_id:'$user'")" \
    --header "Authorization: Bearer $token_root" \
    "http://localhost:8008/_synapse/admin/v1/rooms/$id/make_room_admin"

讓使用者在註冊後自動加入頻道

修改 /etc/matrix-synapse/homeserver.yaml , 我是加在 /etc/matrix-synapse/auto_join_rooms.yaml , 以 ccns 來說預設有以下頻道, 這樣註冊後就會自動加入這些頻道了。

auto_join_rooms:
  - "#acgn:ccns.io"
  - "#ani-platform:ccns.io"
  - "#bbs-dev:ccns.io"
  - "#bbs-dev-notify:ccns.io"
  - "#blockchain:ccns.io"
  - "#book:ccns.io"
  - "#bot-commands:ccns.io"
  - "#bullshit:ccns.io"
  - "#channel-request:ccns.io"
  - "#computer-system:ccns.io"
  - "#cryptocurrency:ccns.io"
  - "#emoji-request:ccns.io"
  - "#food:ccns.io"
  - "#game-design:ccns.io"
  - "#general:ccns.io"
  - "#general-english:ccns.io"
  - "#git:ccns.io"
  - "#hydra-command:ccns.io"
  - "#hydra-song-requests:ccns.io"
  - "#machine-learning:ccns.io"
  - "#math:ccns.io"
  - "#minecraft-chat:ccns.io"
  - "#music:ccns.io"
  - "#programming-contest:ccns.io"
  - "#radio:ccns.io"
  - "#robotics:ccns.io"
  - "#security:ccns.io"
  - "#service-status:ccns.io"
  - "#software-develop:ccns.io"
  - "#stonk:ccns.io"
  - "#study-group:ccns.io"
  - "#sysadmin:ccns.io"
  - "#vim:ccns.io"
  - "#vscode:ccns.io"
  - "#vtuber:ccns.io"
  - "#vtuber-notify:ccns.io"
  - "#web-develop:ccns.io"
  - "#welcome:ccns.io"
  - "#announcement:ccns.io"
from http://gholk.github.io/ccns-matrix-luser-review.html
-----------------------------------------

matrix 安裝筆記

matrix 是通訊協定與基金會名稱, matrix 基金會實作的伺服器端軟體為 synapse , 客戶端軟體舊稱為 riot,後改名為 element 。

使用說明

  • 最基本可以使用 element 的 web 版 登入, 要裝桌面版或 android 版也可以。
  • 登入時 homeserver 輸入 matrix.ccns.io,帳號密碼同 ccns ldap 服務 。
  • server 名稱為 ccns.io,使用者全名為 @your-name:ccns.io , 頻道全名為 #channel-name:ccns.io , matrix 通用短連結為 https://matrix.to/#/@gholk:ccns.io 。

synapse 安裝

debian/ubuntu 都可以用 apt 裝,建議用 matrix 的 repository。

相依軟體

  • postgreSQL: 可以用 ubuntu 官方的套件,如果只是測試用可以用 sqlite。
  • python: 用 matrix.org 提供的第三方 repository, 直接裝好對應的 python 版本。

安裝

wget https://packages.matrix.org/debian/matrix-org-archive-keyring.gpg
cp matrix-org-archive-keyring.gpg /usr/share/keyrings/
echo deb [signed-by=/usr/share/keyrings/matrix-org-archive-keyring.gpg] https://packages.matrix.org/debian/ $(lsb_release --codename --short) main > /etc/apt/source.list.d/matrix-org.list
apt update
apt install matrix-synapse-py3

安裝時會問 server name,填 ccns.io。 裝好後會自動建立 systemd service,如果有開起來先關掉, 因為預設是用 sqlite 當資料庫, 我們要換成 postgresql。

postgresql

參考 synapse 官方的 postgresql 設定文件 。

apt install postgresql # 用 ubuntu 裝 synapse 時會自己裝 libpq5,不用像文件自己裝
sudo -iu postgres <<POSTGRES

# 以下指令在 postgresql 使用者下執行,
# 不確定在 root 下執行可不可以。
createuser --pwprompt synapse_user # 密碼之後要寫到 matrix 的 config 裡

# 進入 sql 介面
# 輸入以下指令建立表格
psql <<PSQL
CREATE DATABASE synapse
 ENCODING 'UTF8'
 LC_COLLATE='C'
 LC_CTYPE='C'
 template=template0
 OWNER synapse_user;

PSQL

POSTGRES

最後可能需要修改 /etc/postgresql/12/main/pg_hba.conf 讓 synapse 能連線到 postgresql, ubuntu 預設允許 localhost 的連線。

同時也要修改 /etc/matrix-synapse/homeserver.yaml 的 database 設定, 預設是 database.name: sqlite3 ,改成 database.name: psycopg2 , 再填入剛才建立的 synapse_user 的密碼來登入。

database:
  name: psycopg2
  args:
    user: synapse_user
    password: "**you can't see me**"
    database: synapse
    host: localhost
    cp_min: 5
    cp_max: 10

synapse config

現在要來改設定檔了,主要有 /etc/matrix-synapse/homeserver.yaml 和 /etc/matrix-synapse/conf.d/*.yaml 二個地方, 格式同 yaml, # 開頭是註解。

well known uri

一開始會看到 homeserver.yaml 的 server_name 被註解掉, 因為照 debian 的習慣在你安裝時問 server_name 了, 已經存在 conf.d/server_name.yaml。

這裡的 server_name 是供其它 matrix server 辨識的 server_name, 也就是如果在其它 server 上的使用者如在 matrix.org 上註冊的 r2, 要和本機器上註冊的 gholk 聯絡,就是要本機的 server_name ccns.io, 輸入 @gholk:ccns.io 來和 gholk 聯絡, 而不管這台 server 實際上域名是 matrix.ccns.io。

可以這樣設是因為 delegation 的機制, 好處有可以用短一點的伺服器名,和可以分散流量。 有點像 email 上 gmail 都是 username@gmail.com , 但實際上 google 的伺服器不是擺在 gmail.com 這個域名, 而是用 mx dns 記錄來指向實際的域名。

matrix 有 well known uri 和 dns srv record 二種作法, 而 ccns 則是用 well-known uri, 也就是當你傳訊息到 *:ccns.io 時, 你的 matrix server 要負責把訊息傳到對方的伺服器, 他會先檢查有沒有 srv 記錄,沒有會再向 ccns.io 這個域名請求 https://ccns.io/.well-known/matrix/server , 如果這個 json 存在,就會以該 json 中的 ["m.homeserver"]["base_url"] 這個欄位的 url, 作為 ccns.io 這個伺服器名的實際網址。 如果該網址是 30X 的重導向,server 應該要跟隨此網址。 最後如果該檔案不存在,就會直接將 ccns.io 視為實際網址。

所以再來要新增 https://ccns.io/.well-known/matrix/server 這個檔案, ccns.io 是轉址到 www.ccns.io 的 github-page ccns.github.io 的 cname, 因此在 ccns/ccns.github.io 這個 github repo 新增 .well-known/matrix/server 檔案即可 。 預設是走 8448 port,但因為我們設 reverse proxy 的機器有防火牆, 為了方便我就改 443。

{"m.server":"matrix.ccns.io:443"}

public url 則設成 https://matrix.ccns.io , 這條 url 是用來在 client 登入時使用的。 依 matrix 規格實作的 client,輸入所在的 server 的 url 時要輸入這條。 這條和上面的 server name 一樣可以 delegation, 網址是 /.well-known/matrix/client , 但規範此網址需要有 cors header 好讓 web 前端的 client 能連線, 同時因為考慮瀏覽器前端的限制較多,不能有重導向; 最後前端當然沒有發 dns 的能力, 所以只能用 well-known 而不支援 dns srv 紀錄來重導向。

雖然所有 github-page 都有 cors header access-control-allow-origin: * , (github 真的心臟很大顆。) 但因為 github-page 的機制, ccns.io 所有 http 路徑都 301 轉址到 www.ccns.io 的域名, 且該轉址的 301 response 沒有 cors header, 所以前端 client 會死在這裡。 因此我們不能用 ccns.io 當作前端登入時的名稱, 只好直接用 matrix.ccns.io。

matrix 在 server 與 client 各規範了類似卻不同的 delegation 方式實在不是很優雅, 但也是為了幫純前端的 client 考慮。

總結 ccns 的 matrix 有二個 server name, matrix.ccns.io 與 ccns.io,使用上也很好區分, 只有要登入時要填 matrix.ccns.io, 其它在提及使用者 @*:ccns.io 、頻道 #*:ccns.io 都是用短的 ccns.io 即可。 我考慮在提及時用 @*:matrix.ccns.io 會太長, 很不方便,就還是設了短的 ccns.io,雖然不太統一,也就這樣了。

synapse 開放的埠

再來可以直接跳到 config 的 listen 部份, 因為 ccns 有 reverse proxy server, 所以 synapse 端只要設好 http port,也不需設定 tls, 讓 reverse proxy 處理即可。

listen:
  - port: 8008
    tls: false
    type: http
    x_forwarded: true
    # default bind to all
    #bind_addresses: ['::1', '127.0.0.1']                                      

    resources:
      - names: [client, federation]                        
        compress: false

預設是 8008 http 和 8448 https, 但因為 reverse proxy 會處理 https, 所以只開 https 即可。 因為都是開在 1000 以上的埠,不需要用 root 執行, 所以 systemd 的 service 裡是用一個 matrix-synapse 的帳號來執行。

如果要用內建的 tls 功能的話,要注意一般 tls 憑證需要有 root 權限才能讀取。 之前我是用 setfacl --modify matrix-synapse:r /etc/letsencrypt/live/$domain_name/fullchain.pem , 再把對目錄的進入權限打開 setfacl --recursive --modify matrix-synapse:rx /etc/letsencrypt/{live,archive} 。 不過 pve 的 lxc 似乎不一定有 acl。

nginx reverse proxy

reverse proxy 的 config synapse 的文件都給的很詳細, 去找 nginx 的範例 抄一抄就好了。 但我看 reverse-proxy.ccns.internal 裡的其它 reverse proxy config, 像 /etc/nginx/conf.d/md.ccns.moe.conf , 有點不太一樣,像會多設一個 X-Real-IP 的 http header。

ldap 登入

讓社員可以用 ccns 的 ldap 功能登入, synapse 裝好就就有附帶 ldap 模組了。 我不懂 ldap,所以以下設定我也不太懂, 本社的 ldap 是不對外開放的,要用來驗證登入要先要開一個可以登入的帳號, 之後把該帳號密碼寫在 bind_dn bind_password 裡。

ldap 的路徑順序是有意義的, 階層和 domain name 一樣是反著寫,之間用逗號分隔。 最底層都是用 dc 代表網域名,cn 是 common name。 base 那欄大概就是登入時在 ldap.ccns.io 下的頂級 common name users 中驗證該帳號密碼。

有人說 cn 不是唯一的,不建議用來當 uid, 建議改用 samaccountname, 但我看社團其它人也都用 cn,就這樣吧。

password_providers:
    # Example config for an LDAP auth provider
    - module: "ldap_auth_provider.LdapAuthProvider"
      config:
        enabled: true
        uri: "ldaps://your.url"
        start_tls: true
        base: "cn=club,dc=xxxxx,dc=xxx"
        attributes:
           uid: "cn"
           mail: "email"
           name: "givenName"
        bind_dn: "uid=matrix-app,cn=club,dc=xxxxx,dc=xxx"
        bind_password: "**you can't see me**"
        filter: "(objectClass=posixAccount)"
systemctl start matrix-synapse

bot

前面作完 matrix 就可以動了, 也可以用 ccns 的 ldap 帳號登入了。 再來要做的事是把 ccns 的 discord 上的訊息, 用機器人轉送到 matrix 上的同樣頻道。

據 matrix 官方的 bridge 介紹頁 , discord 有二款 bridge 分別是 mx-puppet-discord 和 matrix-appservice-discord。 matrix-appservice-discord 似乎不能 bridge 所有頻道, 所以我選擇 mx-puppet-discord 來橋接 discord 和 matrix。 因為我的目的是想把 discord 整個換成 matrix, 而不是像 telegram 的 bridge 那樣,只把 general 搬過去。 mx-puppet-discord 還有個功能是,可以在建立、刪除 discord 頻道時, 在 matrix 也進行相同的操作。

其實還有另一個選擇是 matterbridge 這個八爪章魚,支援最多平台的 bridge。 用 go 語言寫的,直接下載一個靜態連結執行檔就可以動了, matterbridge 是寫設定檔控制要橋接哪些頻道, 而 mx-puppet-discord 則是用對聊天機器下指令的方式控制。

mx-puppet-bridge 安裝

安裝和設定我是參考 mx-puppet-discord 的 README.md 。

相依套件

這是用 node js 寫的,所以請先裝好 node.js 和 npm 全家桶, 同時因為 npm install 時某些套件會用到 git url, 所以也要裝 git,最後因為某些 npm 套件用到 sqlite, 需要用 node-gyp,所以 gcc 和 make 也要裝起來。

apt install build-essential git

安裝

git clone https://github.com/matrix-discord/mx-puppet-discord
cd mx-puppet-discord
npm install

mx-puppet-discord 設定

mx-puppet-discord 設定檔

直接貼 diff -u sample.config.yaml config.yaml 的結果:

--- sample.config.yaml    2021-05-17 07:50:26.635416325 +0000
+++ config.yaml    2021-05-18 15:42:43.195383396 +0000
@@ -7,10 +7,10 @@
   bindAddress: localhost

   # Public domain of the homeserver
-  domain: matrix.org
+  domain: ccns.io

   # Reachable URL of the Matrix homeserver
-  homeserverUrl: https://matrix.org
+  homeserverUrl: https://matrix.ccns.io

   # Optionally specify a different media URL used for the media store
   #
@@ -36,7 +36,7 @@
   displayname: Discord Puppet Bridge

   # Avatar URL of the bridge bot
-  #avatarUrl: mxc://example.com/abcdef12345
+  avatarUrl: mxc://ccns.io/ztBApnmUJETzBSYuRkOmqNgo

   # Whether to create groups for each Discord Server
   #
@@ -46,7 +46,7 @@

 presence:
   # Bridge Discord online/offline status
-  enabled: true
+  enabled: false

   # How often to send status to the homeserver in milliseconds
   interval: 500
@@ -58,7 +58,7 @@
     #- "@user:server\\.com"

     # Allow users on a specific homeserver
-    - "@.*:server\\.com"
+    - "@.*:ccns\\.io"

     # Allow anyone
     #- ".*"
@@ -78,7 +78,7 @@
   #
   # Same format as in provisioning
   whitelist:
-    - "@.*:yourserver\\.com"
+    - ".*"

   #blacklist:
     #- "@user:yourserver\\.com"
@@ -88,7 +88,7 @@
   #
   # Same format as in provisioning
   whitelist:
-    - "@.*:server\\.com"
+    - "@.*:ccns\\.io"

   #blacklist:
     #- "@user:server\\.com"
@@ -145,12 +145,12 @@
   # with username "user", password "pass", host "localhost" and database name "dbname".
   #
   # Modify each value as necessary
-  #connString: "postgres://user:pass@localhost/dbname?sslmode=disable"
+  connString: "postgres://bridge-discord-matrix:password@localhost/bridge_discord_matrix?sslmode=disable"

   # Use SQLite3 as a database backend
   #
   # The name of the database file
-  filename: database.db
+  #filename: database.db

 limits:
   # Up to how many users should be auto-joined on room creation? -1 to disable
@@ -169,7 +169,7 @@
   #
   # Allowed values starting with most verbose:
   # silly, verbose, info, warn, error
-  console: info
+  console: error

   # Date and time formatting
   lineDateFormat: MMM-D HH:mm:ss.SSS
  • domain 是 well-known url,homeserverurl 是登入用的,要加 matrix。
  • avartarUrl 可設可不設,我是把圖片在和 bot 的對話,就可以用 dev tool 看到 mxc url。
  • 上下線狀態為了減少伺服器負擔我就關掉了,因為我們的 discord server 人有點多。
  • provisioning, relay, selfService 三個 whitelist:
    • relay 是指誰可以加入 bridge 的頻道發言,並被 bridge 到 discord 上。
    • 另外二個可能是誰能對 bot 下指令

database 那裡,要新建一個資料庫,然後把對應有存取權限的帳號密碼打上去。 文件裡也沒寫要怎麼建,我就照著 synapse 的建法, 建立一個 bridge 用的帳號,和對應的資料庫。 connString 的內容中 user 和 dbname, 要換成對應的使用者帳號和資料庫名稱。

# 先用 sudo -su postgres 切換為 postgres 的 uid
createuser --pwprompt bridge-discord-matrix
# 進入 sql 介面後輸入以下指令
psql <<PSQL
CREATE DATABASE bridge_discord_matrix
 ENCODING 'UTF8'
 LC_COLLATE='C'
 LC_CTYPE='C'
 template=template0
 OWNER "bridge-discord-matrix";

PSQL

另外,synapse 的設定檔要允許建立頻道: enable_group_creation: true 。

產生 appservice 註冊檔

appservice 有點像是擴充元件,是 synapse 和其它程式互動時, 其它程式向 synapse 註冊的方式。 下面這個指令會產生一個 appservice 註冊檔案 discord-registration.yaml , 在 synapse 設定檔中載入這個檔案後, 此 mx-puppet-discord 即可和該 synapse 連接互動。

npm run start -- -r -c config.yaml -f discord-registration-puppet.yaml
sudo cp discord-registration-puppet.yaml /etc/matrix-synapse

之後在 /etc/matrix-synapse/homeserver.yaml 或其它 synapse 設定檔中,指定要載入此 appservice 檔即可, 我是建議放到 /etc/matrix-synapse 下, 把 owner 改成 root,比較安全,比較不容易被亂改。

# A list of application service config files to use
#
app_service_config_files:
  - /etc/matrix-synapse/discord-registration-puppet.yaml

要寫絕對路徑是因為 config file 的路徑似乎是相對於 cwd, 以 debian 裝的 synapse 來說,cwd 是 /var/lib/matrix-synapse , 寫在 systemd.service 裡的。

啟動

對,就可以啟動了 npm start 。 加入 discord bot token 是用 bot 的指令做, 但加入 matrix 的 app service 是要寫設定檔,我不知道為什麼。

可以用 nohup npm start >/dev/null 2>&1 & 來在背景執行, 預設 log 除了 stdout,也會寫入在 cwd 下。

傳私人訊息給 @_discordpuppet_bot:ccns.io 就可以下指令了, 可以先用 help 看有哪些哪些指令, help xx-command 可以看指令格式。

discord bot

在 discord developer 建立一個 bridge 用應用程式,再在應用程式下建立一個 bot, 把 bot 邀請到 discord 伺服器裡,要是 server owner。 邀請時用這個連結: https://discord.com/oauth2/authorize?client_id=123456789012345678&scope=bot&permissions=607250432 , 把 client id 改成應用程式的 id, permissions 是我從另一個專案抄過來的 。

然後在 discord developer 網站裡把 bot token 複製下來, 在 matrix 上傳訊息給 bot: link bot your.token-here , 就連結成功了。

連結頻道

用 help 可以看 bot 的指令, 先把 discord bot 的 token 傳過去:

link bot your.token

puppet id 似乎是對應一個 discord bot。 先設定 bridge 時的模式,讓 bridge 的頻道為公開,不然預設是加密; matrix 預設保護隱私的作法有時也很傷腦筋。

# see puppet id
list
# if puppet id is 1
# relay mode mean not only message but also author are bridge
settype 1 relay
# first 1 is puppet id, and second 1 is true
bridgeall 1 1
setispublic 1 1
setisglobalnamespace 1 1

而一個 bot 可以加入多個 discord server (在 api 中稱 server 為 guild), 用 listguilds 看該 bot 加入了哪些 guild, 之後就 bridge 了。

# list server(guild) which discord bot join
listguilds 1
# if discord guild id is 330361502643257345
bridgeguild 1 123455555555555555

bridge 後頻道的地址會很長一串很醜, 可以進頻道管理選單裡新增其它地址,一個頻道可以有多個地址, 但要是該頻道的管理員才行。 預設 bridge 過來的頻道只有該 bot 是管理員, 要用 adminme 指令把自己加為管理員。

adminme #_discordpuppet__330361736932884482:ccns.io

bridge 來一堆頻道後,要一一加入很麻煩, 可以向 bot 傳 joinentireguild 指令, 讓 bot 自動向你發邀請。 如果覺得要一一確認邀請很麻煩, 可以在個人設定裡 security 頁面好像可以一次同意所有邀請。

joinentireguild 1 330361502643257345

如果想知道怎麼用指令打 http request 用 restfull api 一次全部改名和修正頻道權限, 請參考下一篇 matrix review

------------------

對下一代即時通訊系統的想像

現代智慧型手機上的即時通訊軟體都有許多體制上的缺陷, 包含不自由、不安全、侵犯隱私、依賴中心伺服器等。 自由軟體界蠻重視這些議題,也都有提出對應的解決方法, 本文介紹我自己的理解與對應的軟體。

不只要自由軟體

要實現自由的即時通訊軟體 im, 其實不只是開放原始碼那麼簡單。 自由軟體不只是開放原始碼, 而是要可以被修改、重新發布、任意使用。 對 im 來說不是單純在不同電腦上安裝就好, 還要考慮多數 im 其實還需要有一個伺服器。

現代的網路環境多數已經不是固定 ip, 甚至不是公網 ip 在防火牆後。 所以一般 im 各 client 都還需要藉助中心伺服器彼此溝通, 否則無從得知對方的 ip 為何,相較上一次連線是否有更換。 加上許多功能,例如顯示對方是否上線, 和暫存未被成功接收的訊息,由中心伺服器實現較方便, 所以多數 im 都會有中心伺服器。

聯邦制

所以中心伺服器的原始碼也同樣要公開, 重發布過的 client 也要能使用自行架設的伺服器, 確保使用者有原始碼即可重新打造整個系統, 無需依賴原本的中心伺服器。 但這裡出現了有趣的問題, 使用不同中心伺服器的使用者,是否能互相溝通?

直覺來看是不行,就像 line 和 whatsapp 之間, 是使用不同的伺服器,那也當然不能互聯。 但這樣每重發布一次,都只能在同一伺服器間通話, 感覺很沒意義;因此出現所謂聯邦制的架構。

聯邦制的意義在於,每個人可以架自己的伺服器, 同時不同伺服器上的使用者仍可以互連。 通常這類會由社群定義出統一的協議, 之後伺服器與客戶端就可以自由發展。 這類架構知名的案例有 email、irc、新聞群組, 和 mastodon 的 open social。

聯邦制起源

早期的 email 只能在單一主機上的不同使用者互相寄信, 因為以前是大型主機,可能會連不同的終端機, 終端機可能會拉線到不同的房間,或走電話線到另一市區。 所以同一台主機的使用者不一定會很接近, email 就是不同終端機間使用者溝通的方式。

之後網路發展時,有一段時間 email 是直接遠端登入, 去修改對方家目錄下的信箱檔 mbox。 後來統一出 smtp 協議,讓不同伺服器的使用者可以互相寄信。 因此,任何使用者,可以任意選擇自己想使用的 email 平台, 不同平台上的使用者也可以互相寄信。

聯邦制相對現代平台的反思

早期網路發展時,很重視這類分散式的系統, 其它案例有 news server 新聞群組、irc , server 間會互相交換訊息。 較現代的案例 gnu social 提出的 open social 協議, 可以說是對抗現代社群平台壟斷的旗誌。

對現代人來說,fb 上只能追縱其它人的 fb, 如果你想關注的人在 twitter, 那你只能註冊一個 twitter 來追蹤他。 更不用講其它 vocus、medium、patron 平台, 每個平台都有自己的追蹤方式, 最基本就登入 web 介面會有通知,好的會寄 email。

你能想像你可以在 fb 上追縱 vocus 的作者發布的文章嗎? 那你能想像為什麼許多平台,都可以用 email 訂閱嗎? 因為 email 定義了一個標準的格式。 如果為傳送通知定義一個統一的協定, 讓 fb 上可以追縱 twitter,甚至追蹤 vocus medium? 這個協定就是 open social。

同理也可以套用在現代的 im 上, 我們希望,在不同的平台上,仍可以互相傳送訊息; 就像不同 email 平台上仍可以互相寄信一樣。

但這類聯邦制的協議,相對於中心式的 im, 從 client 到註冊到 server 一手包, 要考慮到由誰來架設管理 server,以及常見的 server 的開放程度。 許多自由軟體會用開放原始碼,但提供付費的 server 托管, 或付費才能在他們的 server 上註冊使用。 因此,我理想中的聯邦制,是由各小型的社團會各自維護自己的伺服器, 有限度的開放使用。

email

聯邦制的訊息,最古老的就是 email; 但 email 比較習慣用作非即時的溝通。 比較有趣是 日本的 email 曾被用來代替手機簡訊 , 在功能型手機時代,每個手機門號都會對應一個 email 地址, 手機內會由代理商提供可以收發 email 的程式。

由於 email 比用字計價的簡訊便宜,且可以內嵌圖片等多媒體內容, 所以日本多數都是用 email 而少用簡訊。 以前日系動畫常看到角色們的手機收到簡訊, 有一個 無題 的欄位, 即是發送 email 時不輸入標題導致顯示為沒有題目, 但不知道和 komica、2ch 的那個無題有沒有關係就是了。

email 的註冊就不用說,大家應該都很熟悉, 大的網路公司會提供免費的 email 註冊服務。 以前還會有很緊的空間限制,現在都放到很鬆, 因為巨頭發現可以從信箱裡挖個人資料出來賣錢。 以前空間小時,還會有人習慣把信收到自己的電腦來, 後來大部份都是用 web 介面在收信,就信全部丟雲端了。

irc

後來的例子 irc 就是可以用來聊天的即時訊息了。 irc 比較像是聊天室,而不是簡訊, 架構類似以前的即時通,但因為 irc 協議公開本身也夠流行, 會有許多不同的 server client 實作。 明確區分會有上下線狀態,離線後就不會收到通知。

但 傳統的 irc 不適合在手機的網路條件下使用 , 主要因為 irc 是保持長時間的 tcp 連線來收發訊息, 在手機上網路可能會在基地台、wifi 間不斷切換, 難以如同桌上型電腦維持長時間穩定的連線, 造成伺服器頻繁處理超時斷線負的擔與不良的使用體驗。 因此讓手機上的 im 出現一段真空期, 使 whatsapp line 等軟體有機會掘起佔領市場。

irc 歷經這段挑戰,也有提出新版的協議,但如同前文中所述, 這類開放社群維護的協議改版效率一直很差。 但我一直不是 irc 使用者,目前也不知道成果如何。

irc 的性質比較開放,相比 email 和其它系統, 甚至沒有 註冊 這件事,只要連進去就可以直接聊天。 現在有一個像註冊的手續是可以固定自己的名字, 因為 irc 中每個人可以自己設定要什麼名字, 欸名字可以自己取欸!後來才出現用有管理權限的 bot 管理哪些名字不能被隨意設定避免冒用。

現代的聯邦即時通訊系統

既 irc 後我知道有二款這類聯邦制的協議, 分別是 2010 年左右的 xmpp 和智慧型手機時代的 matrix 。 另外還有一類用來傳送語音的網路電話協議 sip。

xmpp

xmpp 是基於 xml 的訊息傳送, 類似 email 每個人會在一個伺服器上註冊, 用 username@server.ip 的格式, 要通訊時也是需要知道對方的完整地址。 這協議蠻老了,很早期有流行過一段時間, google talk 即是基於這款協議的產品。

xmpp 是基於 xml 的協議, 對大檔案的傳輸只能編碼為 base64 不太便利, 且不支援音訊的傳輸。 後來 google 提出的 xmpp 擴充版 jingle 協議, 使用 rtp 協議來傳送音訊來實現。

現在雖然 google talk 收掉了, 仍有不少 xmpp server client 運作中。 現代比較流行的是用 web rtc 來傳送 p2p 音訊, web rtc 和 rtp 都有一定的 nat 穿透能力。

xmpp 的問題是很多功能是以擴充的方式實現的。 xmpp 基金會發布的一系列 XEP 文件, 定義了 xmpp 的許多擴充功能,例如群組對話如何實現, 加密、語音通話、傳送訊息時對方不在線如何處理。 各 server client 間會有各自支援的擴充, 因此不同 client 間可能不相容而有些功能不能運作。 對架設 server 的人也很傷腦筋,要考慮各功能的相容性。 總之,這是一個雜亂、帶有歷史包袱的系统, 詳細的入門可以參考 貴哥幾年前的 xmpp 介紹

xmpp 的服務提供者比較多元,大部份都需要制式的註冊, 有些則採收費制來維持運作。 如 android 上著名的 app conversation, app 作者自己就有一個收費制的伺服器,(前二個月可以免費使用,) 直接幫自己的伺服器打廣告。

matrix

matrix 比較簡單,他宣稱就是要開發一個 足夠 現代 的聯邦制即時通訊系統, 從現代的 im 中吸取經驗與優勢, 並且沒有 xmpp 那一堆歷史包袱。

matrix 借鑑的對象主要是 slack 和 irc, 相較 xmpp,matrix 標榜 現代 , 像歷史記錄儲存、跨裝置同步訊息等都有考慮。 規格書是用 json 和 http 作為通訊手段, 並且 matrix 十分考慮瀏覽器的相容性, 所以 matrix 可以有所謂的 純前端 client。

目前我正在嘗試架一個社團的 matrix server, 之後再來寫一篇文章分享。

sip

sip 我沒碰過,但似乎是個用來語音通話的協定, 貴哥有幾篇文章在介紹 sip,但他後來似乎轉投 xmpp? 和 xmpp 類似,同為有一定年代的協議, 也都需要找一個伺服器來註冊使用。 但 sip 主要是用來語音通話,而 xmpp 則是傳送文字訊息, 但後來二者都有互相發展對方的功能,sip 現在應該也可以傳送文字訊息了。

點對點加密

e2ee 指的是訊息在傳出自己手機前就加密,到收件者裝置上才解密, 代表伺服器端及中間的所有網路服務商,都無法窺探訊息內容。 從最早的代表性加密軟體 pgp,常配合 email 使用; 到現代一些支援 e2e 加密的 im 出現後, 各大既有的商業 im 也漸漸支援此功能了。 目前 telegram 需要自行開啟,line 和 whats app 似乎也可以開啟。 irc 絕對是沒有的,irc 的本質是聊天室類似論壇, 而不是一對一聊天;某些 irc 還會每月把訊息打包存檔供下載。

由此可以發現點對點加密和群組聊天的歷史記錄有本質上的互斥。 e2e encrypt 中應該只有彼此能解密訊息, 所以伺服器是不會幫你存歷史訊息的,因為存了也看不懂。 就算可以存加密的訊息,但若未來金鑰流出還是會有危險。 因此 e2e encrypt 中,訊息一般只會留在彼此的手機中。 在群組聊天中啟用 e2e,則會用每個成員的金鑰都加密過一次再傳出, 而過去的歷史訊息在加密時,沒有用後加入者的金鑰,所以無法看到歷史訊息。

xmpp 的 e2e 加密是以擴充的方式實現的,但也因此有些實作不同的問題。 matrix 則是在公開頻道無法啟用 e2e 加密功能, 新進者也因此能看到歷史訊息。 而加密頻道的歷史訊息,matrix 則是會在伺服器端存加密的訊息, 而金鑰仍存在使用者端,只有新加入者無法看到歷史訊息的問題。

去中心化的點對點溝通

點對點加密是要讓中心伺服器無法窺看訊息內容, 主要是現今多數架構都需要有一個中心伺服器, 無論是聯邦制或一般的唯一中心制。 如果在通訊架構上能免去中心伺服器, 讓所有通訊都是直接點對點,就能直接省去這一大堆問題。

現今要直接點對點,主要的問題是大量的防火牆, 與 ip 地址不足而使用 nat 及虛擬 ip。 例如我的行動網路所拿到的就是虛擬 ip。 因此要實現直接的點對點通訊, 大部份需要另外實現一套 p2p 的通訊架構, 例如 bit torrent 類的 p2p 網路。

現今我所知道 p2p 的 im,有 briar 與 session 二種。 briar 是走 tor 洋蒽網路 的 im, 因為 tor 的特性讓所有節點可以直接點對點確認。 session 則是去掉中心伺服器版本的 signal, 據 reddit 上的說明 ,就是用區塊鏈取代掉 signal 的中心伺服器, 也無需再綁定電話號碼註冊。

tor 是用來匿蹤的 p2p 網路,也就是暗網所用的網路, 除了匿蹤外,還有個特性是每一個洋蒽地址都是可以被直接連線的, 不管原本是不是在 nat 內。 因此 tor 有一個功能就是用來讓個人能架不需要申請固定 ip 的伺服器, 像 gslin 在太陽花學運時就有提過用來放會被 youtube 下架的警察施暴影片 。 應用在 im 上只要打開連接埠, 就是可以讓二個節點直接溝通無論有沒有 nat。

briar

briar 即是直接在 app 裡開 tor 節點,因此開著時會有點耗電, 個人的邀請連結是長的像 tor 網址的東西。 另外 briar 還提供了群組聊天、開放式的群聊(稱為論壇)、 開放式的個人公告(稱為 blog)等功能。 所有訊息是用金鑰加密後存在本機, 在開機後需要輸入密碼解鎖 briar,才能瀏覽與發送訊息。 如果金鑰丟了,訊息就全都沒了, 同時也沒有跨裝置的同步功能,每個裝置是獨一無二的 id。 (就像 ssh-key 不會傳出本機一樣。)

這類直接 p2p 的軟體會有一個問題, 他需要雙方都上線才能傳送訊息, 因為訊息是連到對方機器直接送過去,如果連不到就沒得談。 根據官網的說明,briar 傳送訊息時如果對方不在線, 會在自己的 briar 裡暫存起來,隔一段時間就嘗試重傳, 一直到超過期限刪除。 如果需要一直開著 briar, 尤其 briar 又會開著 tor 網路,會比較耗電。

briar 還有個問題,目前 pc 端軟體還沒正式釋出, 但我有看到消息了,一個叫 Briar GTK 的 linux 軟體, 似乎有人包了 flatpak 版本的 briar gtk 。 這東西可以期待,如果未來沒辦法維持 matrix.ccns.io, 比起 session 我會用 briar。 因為 session 會用到加密貨幣,所消耗的電力太大, 對全球暖化是個負擔。

session

關於 session,原名好像叫 loki messenger, reddit 上那篇文章總結成 signal + 門羅幣 + tor。 signal 是安全性很高的 e2ee,但使用中心伺服器; 門羅幣是標榜不歡迎礦機,設計上需要用 cpu 採的加密貨幣, 因為不歡迎礦機,社區的自治很活躍,難以被礦廠壟斷; tor 上面介紹過了。 但除此之外我也不是很清楚。

signal 是早期著名的 e2ee app, 但和 telegram 一樣都因為要綁電話號碼為人垢病。 此外也需要中心伺服器,但只做基本的建立通訊用, 不會存對話資料。

session 就是把 signal 的中心伺服器用 oxen 區塊鏈取代, tor 的部份就不清楚了。 缺點應該就是用區塊鏈支持,所以會耗一堆電在算沒有意義的資料, 所以我不喜歡。

歷史訊息

回到伺服器該不該保存歷史訊息的問題, xmpp 和早期的 email 很明顯是不保存的那派, 因為他們是負責傳遞,就像簡訊和電信商的關係。 p2p 的 im 就不用說,根本沒有中心伺服器可以幫你存。 matrix 則是另一派,matrix 可能認知到現代人有多看重聊天記錄, 因此身會一個現代的聊天軟體,幫使用者保存聊天記錄是必要的, 例如 slack discord。(line whatsapp:耳朵怎麼癢癢的?)

某些 irc 頻道則是繼承到郵件群組的傳統, 會用機器人存檔歷史聊天記錄,例如 wikipedia 的 irc, 就像一些學術組織會存檔郵件群組的歷史訊息一樣。 其它大部份 irc 還是屬於單純的即時聊天, 你在線就看的到,不在線就看不到, server 只負責把訊息轉給 client,想記錄就自己寫 bot。

離線與訊息處理

現在能稱霸市場的主流 im,在訊息暫存上都做的不錯; 意思是如果收件人斷線,line 的訊息還是會暫存在伺服器上, 對方再上線時就能收到,不會因為下線就掉訊息。 當然 line 的訊息是有保存期限的,等太久就會刪掉, 就像 line 傳的檔案也是有一個下載期限。

irc 就完全沒有所謂離線的通知。 離線就離線了,別人沒辦法傳訊息給你, 這段期間的群聊你也看不到,再上線後也不會收到通知。 因此 irc 出現一類雲端輔助的 client, 會實際在另一台 server 上持續接收 irc 的訊息, 在手機端開啟 app 實際上只是連去 server 上 把接到的訊息拿來讀而已,也就不會漏訊息。

xmpp 應該和 line 類似,有固定容量和期限的離線暫存; matrix 應該都是會存的伺服器上的,但私人的會有 e2e 加密。 p2p 系列的就都幾乎沒有。 briar 是前面講的土砲本機重傳, session 我看官網上 faq 寫區塊鏈伺服器端會有一個暫存機制, 但也是一定期間而已。

而這類非傳統中心式的 im,還都會碰到一個需要維護連線的問題。 如 霍炬這篇文章中所說 , 早期 iphone 甚至不允許背景 tcp 連線, 就算 android 可以,但也少有 app 用,因為會很耗電。 如 matrix 的半官方 client element, 就發布了使用純 tcp 連線及 google cloud messaging framework 的版本, 使用 tcp 的就會比較耗電

其它議題

最後再來講幾樣比較有趣的議題,像是斷網下仍可運作的藍牙 p2p 網路, 橋接不同 im 的 bot,與政府監聽的權力及祕密通訊自由。

藍牙 p2p 網路

如何抵抗政府管制造成的斷網。 有個 app 叫 bridgefy,可以在沒有行動網路或 wifi 的情況下, 用人群的 android 的藍牙來組成網路傳遞訊息。 類似的概念還有 apple 幾年前推出的 find my device, 和最近的進化版吵很兇的 air tag, 是用 apple 的裝置組成藍牙網路,就算離線也能使用。

bridgefy 的設計是每個人都有 android, 如果人群不中斷的話,可以一個連一個組成不小的網路了, 至少傳個幾條街沒問題,那這些人就可以互為中繼協助傳訊息; 當然要建立在每個人都有裝 bridgefy 的情況下。 但其實 bridgefy 還有內建一些限制, 像訊息只能被 relay 一次,所以傳遞距離應該只有 100 公尺左右, 可能是怕被用來 ddos 或大量浪費頻寬。

apple 的 find my device 是利用 iphone 內建的藍牙 beacon, 當其它 apple 裝置偵測到時,如果其它裝置有網路連線及 gps, 就會把自己的座標回傳給 apple; 注意其它有連網的裝置會回傳,所以遺失裝置離線也沒關係。 現在則更進化出 air tag,可以直接追蹤一顆標籤。 apple 的好處是 apple 裝置都是 apple 說了算, 裡面的功能使用者自己幾乎是關不掉的,除非你 JB 越獄。 所以 apple 可以很強硬的推這些功能,只要使用 apple 的裝置夠多, 利用這些運作中的裝置,彼此就能直接組成網路,互相轉發訊息。

橋接不同的通訊軟體

也是在近年,因為不同的 im 大量增加, 出現許多來將訊息從一平台轉發到另一平台的程式。 早期很知名的轉發程式就是,微軟 MSN 的殺手鐧, 讓 msn 的使用者能和 AOL 的使用者對話, 也就是注名的 即時通大戰 ( 未完成的譯文 )。

後來許多 im 在發展時,都意識到要能和其它既有軟體的使用者對話, 所以多數開源的 im 系統發展時, 都會伴隨一些聊天機器人程式來轉接來自其它平台的對話。 功能最全面的大概是 matterbridge , 來自一間專門抄其它成功產品成開源專案的公司 mattermost, 支援的平台超多,主要是供其核心產品 mattermost, 一個開源版的 slack 使用。 之前我是用來橋接 discord 和 telegram。

matrix 現在也有不少橋接用的程式, 但多是橋接二種不同軟體,不像 matterbridge 是八爪章魚, 一支程式就能轉發一堆協定。

mattermost 的缺點大概就他不是聯邦制的系統, 而是單純一個個可以自己架設的孤島, 就像不且的 slack server 間也不能互相通訊, 遠不如 discord 方便。

監聽權

最後談了那麼多安全,談到點對點加密, 回顧密碼學發展時的 密碼戰爭 , 其實點對點的強加密技術很長一段時間是存在爭議的。 你的祕密通訊自由不是絕對的, 法律上可以允許司法機關依法申請監聽電話、檢查信件。 那為什麼 im 可以使用無法被監聽的 e2ee?

近年的 im 漸漸取代的電話的功能,在點對點加密技術下, 甚至 im 服務商本身都無法監看訊息記錄, 為執法機關帶來新的挑戰, 因此一些國家開始重新審視這類點對點加密技術。 例如 911 事件,就為催生了美國可以以安全為由, 進入私人公司的伺服器; 歐洲近年逐漸增加的恐怖攻擊,也讓歐盟面臨點對點加密的問題, 另外也有被用作 犯罪工具的加密通訊解決方案公司,被警察機關的駭客攻擊的新聞

對照下,密碼戰爭時像 tls 這類技術,就對執法機關是比較友好的, 因為有一個根證書可以用來管理。 政府在必要時,可以透過根證書來影響加密系統的運作。 當然,平時根證書也必須管理好自己的證書。

總結

一個現代的 im,我認為需要以下幾點,其中幾點可能會互斥。 各位可以自行檢驗哪些軟體有具備以下功能,或缺少的。

  • 跨平台,如果這點也要列的話;一些需要電話號碼的 im 可能會因此出問題。
  • 最基本的 client 和 server 間通訊必須加密
  • e2ee 功能也是必要的,可能不需要全面 e2ee。
  • 不依賴電話號碼,奇怪的是很多系統都仍以電話號碼作為識別碼。
  • 自由軟體,這很大程度必須和聯邦制或是 p2p 結合, 當然也有例外如 signal 或 wire 這類,伺服器開源但仍中心。
  • p2p 非必要,但可以免去中心伺服器的依賴,減少風險。
  • 藍牙構成的分散式網路是一個有趣的想法,但有一些困難需要客服。

在訊息上的介面及功能,可能就和 slack 或 discord 借鑑, 我認為這二者都做的很不錯了,至少比 messenger line 要好的多。 briar 也很有趣,不只作為 im,也利用 p2p 的特性,把手伸向 blog 市場。 line 的那個記事本真的是,明顯把手伸出聊天領域了, 也讓 line 更肥大了。

目前我認為 signal、telegram、wire 等系統明顯出局, 因為他們是中心化的; 比較合適的系統至少要是聯邦制或 p2p,如下:

  • 聯邦制:matrix、xmpp 都很 ok,但 matrix 比較現代,適合組織使用。
  • briar,如果桌面版能順利釋出的話。 但很明顯這東西不能在 iphone 上用,因為 ios 的限制 tor 應該沒辦法動。
  • session,如果不在意挖礦的問題的話,我想是不錯的系統。
from http://gholk.github.io/instant-message-app-future-work.html

No comments:

Post a Comment