-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
9 changed files
with
408 additions
and
40 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
serverlet/serverlet | ||
*.swp | ||
serverlet/actor.json | ||
serverlet/config.json | ||
serverlet/config.json | ||
serverlet/object.json |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
package activitypub | ||
|
||
import ( | ||
"encoding/json" | ||
"log/slog" | ||
"net/http" | ||
|
||
"github.com/pichuchen/hatsuaki/datastore/actor" | ||
"github.com/pichuchen/hatsuaki/datastore/config" | ||
"github.com/pichuchen/hatsuaki/datastore/object" | ||
) | ||
|
||
// 這邊會接收所有 /.activitypub/actor/{actor}/outbox 開頭的請求 | ||
// 舉例來說會像是 GET /.activitypub/actor/alice/outbox | ||
// Outbox 的用途是讓沒有收到先前消息的人可以查詢某個 Actor 的所有發送過的消息。 | ||
func RouteActorOutbox(w http.ResponseWriter, r *http.Request) { | ||
|
||
// 在 Get 的部分標準中並沒有要求一定要驗證簽章 | ||
// 然而在 Mastdon 的實作當中因為支援封鎖伺服器功能, | ||
// 因此需要鑑別 (Authentication) 來判斷是否有權限查看。 | ||
|
||
username := r.PathValue("actor") | ||
a, err := actor.FindActorByUsername(username) | ||
if err != nil { | ||
w.WriteHeader(http.StatusNotFound) | ||
json.NewEncoder(w).Encode(map[string]string{"error": "actor not found"}) | ||
return | ||
} | ||
|
||
page := r.URL.Query().Get("page") | ||
if page == "true" { | ||
// 如果有 page=true 的參數,則回傳一個 OrderedCollection | ||
// 這個 OrderedCollection 會包含所有的 Object | ||
RouteActorOutboxPage(w, r, a) | ||
return | ||
} | ||
|
||
w.Header().Set("Content-Type", "application/activity+json") | ||
m := map[string]interface{}{} | ||
|
||
c := []interface{}{} | ||
c = append(c, "https://www.w3.org/ns/activitystreams") | ||
c = append(c, "https://w3id.org/security/v1") | ||
m["@context"] = c | ||
|
||
id := "https://" + config.GetDomain() + "/.activitypub/actor/" + a.GetUsername() + "/outbox" | ||
|
||
// 這邊是在 ActivityPub 中的必要 (MUST) 欄位 | ||
m["id"] = id | ||
m["type"] = "OrderedCollection" | ||
m["totalItems"] = a.GetObjectsCount() | ||
m["first"] = id + "?page=true" | ||
m["last"] = id + "?page=true" | ||
|
||
json.NewEncoder(w).Encode(m) | ||
|
||
} | ||
|
||
// RouteActorOutboxPage 會回傳一個 OrderedCollection | ||
// 這個 OrderedCollection 會包含所有的 Object | ||
func RouteActorOutboxPage(w http.ResponseWriter, r *http.Request, a *actor.Actor) { | ||
w.Header().Set("Content-Type", "application/activity+json") | ||
m := map[string]interface{}{} | ||
|
||
c := []interface{}{} | ||
c = append(c, "https://www.w3.org/ns/activitystreams") | ||
c = append(c, "https://w3id.org/security/v1") | ||
m["@context"] = c | ||
|
||
id := "https://" + config.GetDomain() + "/.activitypub/actor/" + a.GetUsername() + "/outbox" | ||
|
||
// 這邊是在 ActivityPub 中的必要 (MUST) 欄位 | ||
m["id"] = id | ||
m["type"] = "OrderedCollection" | ||
m["totalItems"] = a.GetObjectsCount() | ||
m["first"] = id + "?page=true" | ||
m["last"] = id + "?page=true" | ||
|
||
objectIDs, err := a.GetObjects() | ||
if err != nil { | ||
w.WriteHeader(http.StatusInternalServerError) | ||
json.NewEncoder(w).Encode(map[string]string{"error": "internal server error"}) | ||
return | ||
} | ||
orderedItems := []interface{}{} | ||
|
||
for _, oid := range objectIDs { | ||
o, err := object.FindObjectByID(oid) | ||
if err != nil { | ||
slog.Warn("activitypub.RouteActorOutboxPage", "error", err.Error()) | ||
continue | ||
} | ||
activityMap := map[string]interface{}{} | ||
actor := "https://" + config.GetDomain() + "/.activitypub/actor/" + a.GetUsername() | ||
activityMap["id"] = o.GetFullID() + "/activity" | ||
activityMap["type"] = "Create" | ||
activityMap["published"] = o.GetPublished() | ||
activityMap["actor"] = actor | ||
|
||
objectMap := map[string]interface{}{} | ||
|
||
objectMap["id"] = o.GetFullID() | ||
objectMap["type"] = "Note" | ||
objectMap["published"] = o.GetPublished() | ||
objectMap["attributedTo"] = o.GetAttributedTo() | ||
objectMap["content"] = o.GetContent() | ||
|
||
activityMap["object"] = objectMap | ||
|
||
orderedItems = append(orderedItems, activityMap) | ||
} | ||
|
||
m["orderedItems"] = orderedItems | ||
|
||
json.NewEncoder(w).Encode(m) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
package actor | ||
|
||
func (a *Actor) GetObjects() ([]string, error) { | ||
n, ok := (*a)["objects"] | ||
if !ok { | ||
return []string{}, nil | ||
} | ||
|
||
is := n.([]interface{}) | ||
objects := make([]string, len(is)) | ||
for i, v := range is { | ||
objects[i] = v.(string) | ||
} | ||
return objects, nil | ||
} | ||
|
||
func (a *Actor) GetObjectsCount() int { | ||
objects, err := a.GetObjects() | ||
if err != nil { | ||
return 0 | ||
} | ||
|
||
return len(objects) | ||
} | ||
|
||
func (a *Actor) AppendObject(objectID string) { | ||
objects, err := a.GetObjects() | ||
if err != nil { | ||
objects = []string{} | ||
} | ||
|
||
objects = append(objects, objectID) | ||
(*a)["objects"] = objects | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
package object | ||
|
||
import ( | ||
"crypto/rand" | ||
"encoding/json" | ||
"fmt" | ||
"log/slog" | ||
"math/big" | ||
"os" | ||
"sync" | ||
"time" | ||
) | ||
|
||
type Object map[string]interface{} | ||
|
||
var datastore = &sync.Map{} | ||
|
||
func LoadObject(filepath string) error { | ||
slog.Debug("object.Load", "info", "load objects") | ||
|
||
f, err := os.ReadFile(filepath) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
tmpMap := map[string]interface{}{} | ||
tmpDatastore := sync.Map{} | ||
|
||
err = json.Unmarshal(f, &tmpMap) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
for k, v := range tmpMap { | ||
m := v.(map[string]interface{}) | ||
a := Object(m) | ||
tmpDatastore.Store(k, &a) | ||
} | ||
|
||
// old datastore should be garbage collected | ||
datastore = &tmpDatastore | ||
slog.Info("object.Load", "info", "objects loaded") | ||
return nil | ||
} | ||
|
||
func SaveObject(filepath string) error { | ||
slog.Debug("object.Save", "info", "save objects", "filepath", filepath) | ||
|
||
tmpMap := map[string]interface{}{} | ||
datastore.Range(func(k, v interface{}) bool { | ||
tmpMap[k.(string)] = v | ||
return true | ||
}) | ||
|
||
f, err := json.MarshalIndent(tmpMap, "", " ") | ||
if err != nil { | ||
return err | ||
} | ||
|
||
err = os.WriteFile(filepath, f, 0644) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
slog.Info("object.Save", "info", "objects saved") | ||
return nil | ||
} | ||
|
||
func FindObjectByID(id string) (*Object, error) { | ||
if v, ok := datastore.Load(id); ok { | ||
return v.(*Object), nil | ||
} | ||
return nil, fmt.Errorf("object not found") | ||
} | ||
|
||
func GenerateUUIDv7() string { | ||
// UUIDv7 | ||
var buf [16]byte | ||
rand.Read(buf[:]) | ||
t := big.NewInt(time.Now().UnixMilli()) | ||
t.FillBytes(buf[:6]) | ||
buf[6] = 0x70 | (buf[6] & 0x0f) | ||
buf[8] = 0x80 | (buf[8] & 0x3f) | ||
return fmt.Sprintf("%x-%x-%x-%x-%x", buf[:4], buf[4:6], buf[6:8], buf[8:10], buf[10:]) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
package object | ||
|
||
import ( | ||
"strings" | ||
"time" | ||
|
||
"github.com/pichuchen/hatsuaki/datastore/config" | ||
) | ||
|
||
// https://www.w3.org/TR/activitystreams-vocabulary/#dfn-note | ||
func NewNote() *Object { | ||
id := GenerateUUIDv7() | ||
note := Object{ | ||
"id": id, | ||
"type": "Note", | ||
"published": time.Now().Format(time.RFC3339), | ||
} | ||
datastore.Store(id, ¬e) | ||
return ¬e | ||
} | ||
|
||
func (o *Object) GetFullID() string { | ||
id := o.GetID() | ||
if strings.HasPrefix(id, "https://") { | ||
return id | ||
} | ||
return "https://" + config.GetDomain() + "/.activitypub/object/" + id | ||
} | ||
|
||
func (o *Object) GetID() string { | ||
return (*o)["id"].(string) | ||
} | ||
|
||
func (o *Object) GetPublished() string { | ||
return (*o)["published"].(string) | ||
} | ||
|
||
func (o *Object) GetContent() string { | ||
return (*o)["content"].(string) | ||
} | ||
|
||
func (o *Object) SetContent(content string) { | ||
(*o)["content"] = content | ||
} | ||
|
||
func (o *Object) GetAttributedTo() string { | ||
return (*o)["attributedTo"].(string) | ||
} | ||
|
||
func (o *Object) SetAttributedTo(actor string) { | ||
(*o)["attributedTo"] = actor | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.