(最近更新:2022-11-23,勘誤)
序言 隨着馬斯克入主推特後對內激進整飭的作爲,許多人想到避難目標 Mastodon。但是也不一定要入駐 Mastodon,因為 Mastodon 是利用去中心化 ActivityPub 協定跨伺服器傳遞訊息的,所以只要支援 ActivityPub 的 SNS 平臺,比如 Pleroma、MissKey 都可以使用。
另外鑑於有些人反映 Mastodon 的效率太慢,其實也可以自己製作支援 ActivityPub 的微網誌服務軟體。我之前本來想要做一組SNS ,但是發現這個 side-project 會嚴重侵蝕自己的作息控制和身心健康,所以擱置了。
但是鑑於這個去中心化協定獲得的關注度很高,而網路上幾乎沒有華語的教學,從而沒有辦法百花齊放,推進這個技術的生態圈,洵為可惜,加上自己曾經參與一篇 Timmot 的教學的回饋,所以就用該教學來編譯成華語,希望能夠推動大家對這個協議的興趣。
如果這個教學有錯誤處請告訴我。另筆者不負責參考本教學執行的後果,還請注意。
教學簡介 教學的 Python 程式碼改自 timmot 的 ActivityPub 教學,將 原教學使用的 Flask 後端框架翻譯成 Django 這個 Python 網頁後端框架的版本,使用 MIT 授權 。
其中會先教到如何使用 Nginx 伺服器驅動一個新設置的 Django 專案,再用這個專案改成能夠傳輸 ActivityPub json 訊息之軟體。
先備條件 以下教學適用環境如下:
會 Python 和 Django 的基礎知識。
Ubuntu 20.04(我是在 Docker 裏面測試,但是實機應該也可以照樣用)
Python 3.10.6
Django 4.1.3
一組浮動 IP
No-IP Linux DUC Stable version 2.1.9
certbot 1.32.0
基礎軟體安裝 Nginx HTTP 伺服器 安裝 nginx:
1 2 $ sudo apt update $ sudo apt install nginx
確認一下 systemtctl 有沒有開啓 nginx,沒有則開啓:
1 $ sudo systemctl start nginx
這時候打開瀏覽器,進入 127.0.0.1 這個代表自己電腦的 IP,看到下列文字,代表成功了:
1 2 3 Welcome to nginx! If you see this page, the nginx web server is successfully installed and working. Further configuration is required. ...
Python 程式語言 因為 Django 是 Python 語言的後端框架,所以需要安裝 Python,和安裝 Python 套件的工具 pip:
1 $ sudo apt install python3-pip python-3
Django 安裝、建置專案 安裝 virtualenv、建立環境目錄 爲了隔離開發環境的依賴模組,我們把 Python 套件用 virtualenv 安裝在自定的目錄裏面,避免動到系統全局的套件安裝,所以先安裝 virtualenv:
1 $ pip3 install virtualenv #安裝 virtualenv
在家目錄或其他適當的地方建立新目錄 my_folder
,並在裏面建立自己的環境目錄my_env
,套用環境:
1 2 3 4 5 user@host:~$ mkdir my_folder user@host:~$ cd my_folder user@host:~/my_folder$ virtualenv my_env #建立自己的環境目錄 user@host:~/my_folder$ source my_env/bin/activate #進入自己的環境 (my_env) user@host:~/my_folder$
在環境目錄內安裝 Django 最後灌 Django 框架到環境目錄內:
1 (my_env) user@host:~/my_folder$ pip3 install django
建立自己的測試專案 用 django-admin 建立測試專案資料夾 activitypub_test
:
1 (my_env) user@host:~/my_folder$ django-admin startproject activitypub_test
安裝時區資料 tzdata 爲了正確處理時區資訊,需要安裝 tzdata:
Gunicorn 和 uWSGI 一樣,提供伺服器軟體和 Python Django 網頁後端框架溝通的橋樑:
1 (my_env) user@host:~/my_folder$ pip3 install gunicorn
進去自己的專案目錄,然後測試,出現類似下列的字樣,可以 Ctrl-z
離開:
1 2 3 4 5 6 (my_env) user@host:~/my_folder$ cd activitypub_test (my_env) user@host:~/my_folder/activitypub_test$ gunicorn --bind 0.0.0.0:8000 activitypub_test.wsgi [2022-11-19 09:14:29 +0000] [54] [INFO] Starting gunicorn 20.1.0 [2022-11-19 09:14:29 +0000] [54] [INFO] Listening at: http://0.0.0.0:8000 (54) [2022-11-19 09:14:29 +0000] [54] [INFO] Using worker: sync ...
爲了讓系統起動時能夠自動開啓 gunicorn,需要建立下列兩個 Systemd 的設定檔案:
/etc/systemd/system/gunicorn.socket
1 2 3 4 5 6 7 8 [Unit] Description=gunicorn socket [Socket] ListenStream=/run/gunicorn.sock [Install] WantedBy=sockets.target
/etc/systemd/system/gunicorn.service
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 [Unit] Description=gunicorn daemon Requires=gunicorn.socket After=network.target [Service] User=user Group=www-data WorkingDirectory=/home/my_folder/activitypub_test ExecStart=/home/my_folder/my_env/bin/gunicorn \ --access-logfile - \ --workers 3 \ --bind unix:/run/gunicorn.sock \ activitypub_test.wsgi:application [Install] WantedBy=multi-user.target
啓動 gunicorn:
1 sudo systemctl start gunicorn
Nginx 設定 建立該設定檔 /etc/nginx/sites-available/activitypub_test.conf:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 server { listen 80; server_name 127.0.0.1; # https://docs.djangoproject.com/en/dev/howto/static-files/#serving-static-files-in-production # location /static { # STATIC_URL # root /var/www/html/; # } location / { proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Scheme $scheme; proxy_pass http://127.0.0.1:8000; } }
建立設定檔連結給 nginx 使用,測試設定檔,然後重啓服務:
1 2 3 ln -s /etc/nginx/sites-available/activitypub_test.conf /etc/nginx/sites-enabled nginx -t # 測試設定檔有沒有問題 sudo systemctl restart nginx # 重啓 nginx 服務
嘗試連到 127.0.0.1,應該會出現這個結果,代表正常運作:
設定防火牆:
1 2 sudo ufw delete allow 8000 sudo ufw allow 'Nginx Full'
浮動 IP 轉址(DDNS)設定 因為我們使用浮動 IP,另外 ActivityPub 協定需要域名,所以需要動態轉址。以下採用 No-IP 的服務設定。
假設創立新賬戶,申請到 activitypub-test.ddns.net
這個網址後,到No-ip 的用戶端下載頁 下載。
解壓縮之後,編譯解壓縮:
1 2 3 4 tar xvf noip-duc-linux.tar.gz cd noip-2.1.9-1 make make install
在 make install
這個階段裏,依指示輸入之後。就設定動態轉址完畢。
啓用 noip2 客戶端程式:
修改 Nginx 設定檔的第 3 行server_name 127.0.0.1;
為server_name activitypub-test.ddns.net;
,如下:
1 2 3 4 server { listen 80; server_name activitypub-test.ddns.net; ...
然後將 Nginx 重啓:
到專案目錄裏面,開啓settings.py
:
1 2 cd ~/my_folder/activitypub_test/activitypub_test/ nano settings.py
將ALLOWED_HOSTS = []
這行添加我們新申請的域名,變成:ALLOWED_HOSTS = ['activitypub-test.ddns.net']
; 存檔然後離開。
最後重啓 gunicorn:
1 sudo systemctl restart gunicorn
連進去 activitypub-test.ddns.net
就應該可以看到 Django 測試頁面了。
設定 HTTPS 憑證等(用 Certbot) 安裝方法
1 2 3 sudo apt install snapd # 安裝 Snap 套件管理程式 sudo snap install --classic certbot # 安裝 certbot sudo ln -s /snap/bin/certbot /usr/bin/certbot # 建立執行檔連結到 /usr/bin/
執行 certbot,取得 HTTPS 證書:
會得到證書和私鑰的連結:
1 2 3 4 5 ... Successfully received certificate. Certificate is saved at: /etc/letsencrypt/live/activitypub-test.ddns.net/fullchain.pem Key is saved at: /etc/letsencrypt/live/activitypub-test.ddns.net/privkey.pem ...
/etc/nginx/sites-available/activitypub_test.conf 這個 nginx 也會被修改,增加 HTTPS 的功能。
產生公私鑰 因為 HTTP Signature 需要公私鑰,所以需要產生一組:
1 2 3 mkdir ~/my_folder/my_keys; cd ~/my_folder/my_keys # 建立新目錄並進入 openssl genrsa -out private.pem 2048 # 產生私鑰 openssl rsa -in private.pem -outform PEM -pubout -out public.pem # 產生公鑰
建構 ActivityPub 伺服器
先匯入必要的函式庫、函數等 先進入~/my_folder/activitypub_test/
專案頁面。 開啓 activitypub_test/views.py
,匯入必要的函式庫:
1 2 3 4 5 6 7 8 9 10 from django.http import HttpResponse # 回傳一般的 HTTP 的回應 (response) from django.http import HttpResponseNotFound # 回傳 404 回應 from django.views.decorators.csrf import csrf_exempt #不檢查 csrf token import json # 處理 json 用 import os.path # 處理路徑用 '''設定基本用戶 alice 的變數和域名''' default_user_name = "alice" # id 名稱 default_shown_name = "Alice" # 顯示名稱 domain = "activitypub-test.ddns.net" # 域名
製作可以回傳用戶基本資料的 json 開啓 activitypub_test/urls.py
,新增一個網頁規則:
1 2 3 4 5 6 7 8 9 from django.contrib import admin from django.urls import path from . import views urlpatterns = [ path('admin/', admin.site.urls), path('users/<username>', views.user_info), # 回傳用戶資料用 ]
開啓activitypub_test/views.py
,建立user_info
函數,決定回傳的內容。如果 username 是 alice,回傳其個人資訊:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 # 回傳使用者資料 def user_info(request, username): # 使用者公鑰的檔案路徑,這裏使用本站 https 的公鑰 home_folder = os.path.expanduser("~") public_key_path = os.path.join(home_folder, "my_folder/my_keys/public.pem") public_key = open(public_key_path, 'r').read() # 輸入公鑰 '''若是輸入的 user_name 不是 default_user_name,回傳找不到。''' if username != default_user_name: return HttpResponseNotFound("the user name doesn't exist.") else: # 個人資料的 dict user_info_dict = { "@context": [ "https://www.w3.org/ns/activitystreams", # 應該是協定的部分 "https://w3id.org/security/v1", ], "id": f"https://{domain}/users/{username}", # 指向使用者的頁面 "inbox": f"https://{domain}/users/{username}/inbox", # 收信匣 "outbox": f"https://{domain}/users/{username}/outbox", # 寄信匣 "type": "Person", "name": default_shown_name, # 使用者顯示名稱 "preferredUsername": username, # 使用者的 id 名稱 # 公鑰部分: "publicKey": { "id": f"https://{domain}/users/{username}#main-key", "id": f"https://{domain}/users/{username}", "publicKeyPem": public_key } } user_info_json = json.dumps(user_info_dict) # 轉成 json # 將 json 轉成 response,然後設定 content-type,最後回傳 user_info_json_response = HttpResponse(user_info_json, content_type = 'application/activity+json') return user_info_json_response
建立 Webfinger 資訊 Webfinger 是辨識使用者或其他實體的方法,採用一組獨特的 URI。位址形式有點像是 email。
activitypub_test/urls.py
的 urlpatterns
改成:
1 2 3 4 urlpatterns = [ path('admin/', admin.site.urls), path('users/<username>', views.user_info), path('.well-known/webfinger', views.webfingr), # webfinger 用
開啓activitypub_test/views.py
,建立webfinger
函數,決定回傳的 webfinger 資訊。如果 username 是 alice,回傳其個人資訊:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 # 傳遞 webfinger 資料 def webfinger(request): webfinger = request.GET.get('resource') # 得到用 GET 方法傳來的 resource(位址) '''將 'alice' 轉成 webfinger 位址 形式如 acct:alice@activitypub_test.ddns.org''' account = "acct:" + default_user_name + "@" + domain # 如果查詢的不是 alice 的位址,則回傳找不到 if webfinger != account: return HttpResponseNotFound("can't find the user in the site.") # 如果查詢的是 alice,回傳個人資料的 json webfinger_dict = { "subject": account, "links": [ { "rel": "self", "type": "application/activity+json", "href": f"https://{domain}/users/{default_user_name}" } ] } # 回傳 json return HttpResponse(json.dumps(webfinger_dict), content_type = "application/jrd+json")
建立 inbox(收信匣) 雖然也可以建立 outbox,但是這裏爲求方便,僅說明建立 inbox,以接受其他人傳來的 json 訊息。
ctivitypub_test/urls.py
的 urlpatterns
改成:
1 2 3 4 5 6 urlpatterns = [ path('admin/', admin.site.urls), path('users/<username>', views.user_info), path('.well-known/webfinger', views.webfinger), path('users/<username>/inbox', views.inbox), # 收信匣 ]
開啓activitypub_test/views.py
,建立ibox
函數,當做收信匣。另外我們把收到的信息存在 inbox_log.txt
裏面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @csrf_exempt # 不要進行 csrf token 檢查 def inbox(request, username): # 預設傳訊息的 inbox 使用 POST 方法 if request.method == 'POST': log = open('inbox_log.txt', 'a') # 打開 inbox_log.txt 這個 log # 如果不是 alice,回傳找不到 if username != default_user_name: HttpResponseNotFound("can't get the inbox") header = request.headers # request 的 header data = request.body # request 的內容 # 將 request 的內容寫在 log 裏面,然後關掉: text = f'message:\n{header} \n{data}\n' log.write(text) log.close() # 最後回傳 OK return HttpResponse("OK") #其他情況回傳找不到 else: return HttpResponseNotFound("can't get the inbox")
如何寄送跟隨請求? 可參後文
參考