Skip to content

Commit

Permalink
實作 outbox 取得 Note 流程
Browse files Browse the repository at this point in the history
  • Loading branch information
PichuChen committed Jul 21, 2024
1 parent 90a02f4 commit 8efbff6
Show file tree
Hide file tree
Showing 9 changed files with 408 additions and 40 deletions.
3 changes: 2 additions & 1 deletion .gitignore
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
2 changes: 1 addition & 1 deletion activitypub/inbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"github.com/pichuchen/hatsuaki/datastore/config"
)

// 這邊會接收所有 /.activitypub/actor 開頭的請求
// 這邊會接收所有 /.activitypub/actor/inbox 開頭的請求
// 舉例來說會像是 GET /.activitypub/actor/alice
func RouteActorInbox(w http.ResponseWriter, r *http.Request) {
slog.Debug("activitypub.RouteActorInbox", "request", r.URL.String())
Expand Down
116 changes: 116 additions & 0 deletions activitypub/outbox.go
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)
}
1 change: 1 addition & 0 deletions activitypub/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ func Route(w http.ResponseWriter, r *http.Request) {
mux := http.NewServeMux()
mux.HandleFunc("GET /.activitypub/actor/{actor}", RouteActor)
mux.HandleFunc("GET /.activitypub/actor/{actor}/inbox", RouteActorInbox)
mux.HandleFunc("GET /.activitypub/actor/{actor}/outbox", RouteActorOutbox)

mux.ServeHTTP(w, r)
}
Expand Down
34 changes: 34 additions & 0 deletions datastore/actor/object.go
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
}
85 changes: 85 additions & 0 deletions datastore/object/main.go
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:])
}
52 changes: 52 additions & 0 deletions datastore/object/note.go
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, &note)
return &note
}

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
}
26 changes: 26 additions & 0 deletions serverlet/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/pichuchen/hatsuaki/datastore/actor"
"github.com/pichuchen/hatsuaki/datastore/config"
"github.com/pichuchen/hatsuaki/datastore/object"
)

// 這個檔案的用途是整個系統的最初進入點
Expand Down Expand Up @@ -58,6 +59,31 @@ func main() {
slog.Error("main", "error", err)
}

err = object.LoadObject("./object.json")
if errors.Is(err, os.ErrNotExist) {
slog.Info("main", "object", "object.json not found, creating a new one")
n := object.NewNote()
a, err := actor.FindActorByUsername("instance.actor")
if err != nil {
slog.Error("main", "error", err)
}

n.SetContent("Hello, World!")
n.SetAttributedTo("https://" + config.GetDomain() + "/.activitypub/actor/instance.actor")
a.AppendObject(n.GetID())

err = object.SaveObject("./object.json")
if err != nil {
slog.Error("main", "error", err)
}
err = actor.SaveActor("./actor.json")
if err != nil {
slog.Error("main", "error", err)
}
} else if err != nil {
slog.Error("main", "error", err)
}

mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
XForwardedFor := r.Header.Get("X-Forwarded-For")
Expand Down
Loading

0 comments on commit 8efbff6

Please sign in to comment.