Federated via graft
3.7 kB
125 lines
1package activitypub
2
3import (
4 "strconv"
5 "strings"
6 "time"
7
8 "graft/internal/state"
9)
10
11// OrderedCollection wraps a series' outbox: every mirrored event as a
12// Create{Note} activity, newest first.
13type OrderedCollection struct {
14 Context string `json:"@context"`
15 ID string `json:"id"`
16 Type string `json:"type"`
17 TotalItems int `json:"totalItems"`
18 OrderedItems []Create `json:"orderedItems"`
19}
20
21type Create struct {
22 Context string `json:"@context,omitempty"`
23 ID string `json:"id"`
24 Type string `json:"type"`
25 Actor string `json:"actor"`
26 Published string `json:"published"`
27 To []string `json:"to,omitempty"`
28 Object Note `json:"object"`
29}
30
31// Note is one mirrored event, addressable on its own
32// (/actors/{series}/notes/{id}) so a reply's inReplyTo can point back at
33// the specific commit/issue/patch it's about.
34type Note struct {
35 Context string `json:"@context,omitempty"`
36 ID string `json:"id"`
37 Type string `json:"type"`
38 AttributedTo string `json:"attributedTo"`
39 Content string `json:"content"`
40 URL string `json:"url,omitempty"`
41 Published string `json:"published"`
42 To []string `json:"to,omitempty"`
43}
44
45const publicAudience = "https://www.w3.org/ns/activitystreams#Public"
46
47// NoteURI returns the stable object URI for one activity_log entry's Note.
48func NoteURI(host, series string, entryID int64) string {
49 return ActorURI(host, series) + "/notes/" + strconv.FormatInt(entryID, 10)
50}
51
52// ParseNoteURI is NoteURI's inverse: given a URI a reply's inReplyTo
53// claims to point at, extract the series and activity_log id it names —
54// used by the inbox to resolve a reply back to the item it's about.
55func ParseNoteURI(host, uri string) (series string, entryID int64, ok bool) {
56 prefix := "https://" + host + "/actors/"
57 rest := strings.TrimPrefix(uri, prefix)
58 if rest == uri {
59 return "", 0, false
60 }
61 parts := strings.Split(rest, "/")
62 if len(parts) != 3 || parts[1] != "notes" {
63 return "", 0, false
64 }
65 id, err := strconv.ParseInt(parts[2], 10, 64)
66 if err != nil {
67 return "", 0, false
68 }
69 return parts[0], id, true
70}
71
72// BuildNote turns one mirrored event into its Note representation.
73func BuildNote(host, series string, e state.ActivityEntry) Note {
74 actor := ActorURI(host, series)
75 content := kindLabel(e.Kind) + ": " + e.Summary
76 return Note{
77 ID: NoteURI(host, series, e.ID),
78 Type: "Note",
79 AttributedTo: actor,
80 Content: content,
81 URL: e.URL,
82 Published: e.OccurredAt.Format(time.RFC3339),
83 To: []string{publicAudience},
84 }
85}
86
87// BuildOutbox wraps a series' recent activity entries (newest first, as
88// returned by state.Store.ActivityForSeries) into an OrderedCollection of
89// Create activities.
90func BuildOutbox(host, series string, entries []state.ActivityEntry) OrderedCollection {
91 base := ActorURI(host, series)
92 actor := base
93 items := make([]Create, 0, len(entries))
94 for _, e := range entries {
95 note := BuildNote(host, series, e)
96 items = append(items, Create{
97 ID: note.ID + "/activity",
98 Type: "Create",
99 Actor: actor,
100 Published: note.Published,
101 To: []string{publicAudience},
102 Object: note,
103 })
104 }
105 return OrderedCollection{
106 Context: "https://www.w3.org/ns/activitystreams",
107 ID: base + "/outbox",
108 Type: "OrderedCollection",
109 TotalItems: len(items),
110 OrderedItems: items,
111 }
112}
113
114func kindLabel(kind string) string {
115 switch kind {
116 case "git":
117 return "Commit"
118 case "issue":
119 return "Issue"
120 case "patch":
121 return "Patch"
122 default:
123 return kind
124 }
125}