Margin is an open annotation layer for the internet. Powered by the AT Protocol.
margin.at
extension
web
atproto
comments
1package api
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "net/http"
8 "time"
9
10 "margin.at/internal/config"
11 "margin.at/internal/db"
12 "margin.at/internal/xrpc"
13)
14
15type LabelerSubscription struct {
16 DID string `json:"did"`
17}
18
19type LabelPreference struct {
20 LabelerDID string `json:"labelerDid"`
21 Label string `json:"label"`
22 Visibility string `json:"visibility"`
23}
24
25type PreferencesResponse struct {
26 ExternalLinkSkippedHostnames []string `json:"externalLinkSkippedHostnames"`
27 SubscribedLabelers []LabelerSubscription `json:"subscribedLabelers"`
28 LabelPreferences []LabelPreference `json:"labelPreferences"`
29 DisableExternalLinkWarning bool `json:"disableExternalLinkWarning"`
30}
31
32func (h *Handler) GetPreferences(w http.ResponseWriter, r *http.Request) {
33 session, err := h.refresher.GetSessionWithAutoRefresh(r)
34 if err != nil {
35 http.Error(w, "Unauthorized", http.StatusUnauthorized)
36 return
37 }
38
39 prefs, err := h.db.GetPreferences(session.DID)
40 if err != nil {
41 http.Error(w, "Failed to fetch preferences", http.StatusInternalServerError)
42 return
43 }
44
45 hostnames := []string{}
46 if prefs != nil && prefs.ExternalLinkSkippedHostnames != nil {
47 json.Unmarshal([]byte(*prefs.ExternalLinkSkippedHostnames), &hostnames)
48 }
49
50 var labelers []LabelerSubscription
51 if prefs != nil && prefs.SubscribedLabelers != nil {
52 json.Unmarshal([]byte(*prefs.SubscribedLabelers), &labelers)
53 }
54 if labelers == nil {
55 labelers = []LabelerSubscription{}
56 serviceDID := config.Get().ServiceDID
57 if serviceDID != "" {
58 labelers = append(labelers, LabelerSubscription{DID: serviceDID})
59 }
60 }
61
62 var labelPrefs []LabelPreference
63 if prefs != nil && prefs.LabelPreferences != nil {
64 json.Unmarshal([]byte(*prefs.LabelPreferences), &labelPrefs)
65 }
66 if labelPrefs == nil {
67 labelPrefs = []LabelPreference{}
68 }
69
70 disableWarning := false
71 if prefs != nil && prefs.DisableExternalLinkWarning != nil {
72 disableWarning = *prefs.DisableExternalLinkWarning
73 }
74
75 w.Header().Set("Content-Type", "application/json")
76 json.NewEncoder(w).Encode(PreferencesResponse{
77 ExternalLinkSkippedHostnames: hostnames,
78 SubscribedLabelers: labelers,
79 LabelPreferences: labelPrefs,
80 DisableExternalLinkWarning: disableWarning,
81 })
82}
83
84func (h *Handler) UpdatePreferences(w http.ResponseWriter, r *http.Request) {
85 session, err := h.refresher.GetSessionWithAutoRefresh(r)
86 if err != nil {
87 http.Error(w, "Unauthorized", http.StatusUnauthorized)
88 return
89 }
90
91 var input PreferencesResponse
92 if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
93 http.Error(w, "Invalid input", http.StatusBadRequest)
94 return
95 }
96
97 var xrpcLabelers []xrpc.LabelerSubscription
98 for _, l := range input.SubscribedLabelers {
99 xrpcLabelers = append(xrpcLabelers, xrpc.LabelerSubscription{
100 Type: "at.margin.preferences#labelerSubscription",
101 DID: l.DID,
102 })
103 }
104 var xrpcLabelPrefs []xrpc.LabelPreference
105 for _, lp := range input.LabelPreferences {
106 xrpcLabelPrefs = append(xrpcLabelPrefs, xrpc.LabelPreference{
107 Type: "at.margin.preferences#labelPreference",
108 LabelerDID: lp.LabelerDID,
109 Label: lp.Label,
110 Visibility: lp.Visibility,
111 })
112 }
113
114 record := xrpc.NewPreferencesRecord(input.ExternalLinkSkippedHostnames, xrpcLabelers, xrpcLabelPrefs, &input.DisableExternalLinkWarning)
115 if err := record.Validate(); err != nil {
116 http.Error(w, fmt.Sprintf("Invalid record: %v", err), http.StatusBadRequest)
117 return
118 }
119
120 err = h.refresher.ExecuteWithAutoRefresh(r, session, func(client *xrpc.Client, did string) error {
121 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
122 defer cancel()
123 _, err := client.PutRecord(ctx, did, xrpc.CollectionPreferences, "self", record)
124 return err
125 })
126
127 if err != nil {
128 fmt.Printf("[UpdatePreferences] PDS write failed: %v\n", err)
129 }
130
131 createdAt, _ := time.Parse(time.RFC3339, record.CreatedAt)
132 hostnamesJSON, _ := json.Marshal(input.ExternalLinkSkippedHostnames)
133 hostnamesStr := string(hostnamesJSON)
134
135 var subscribedLabelersPtr, labelPrefsPtr *string
136 if len(input.SubscribedLabelers) > 0 {
137 labelersJSON, _ := json.Marshal(input.SubscribedLabelers)
138 s := string(labelersJSON)
139 subscribedLabelersPtr = &s
140 }
141 if len(input.LabelPreferences) > 0 {
142 prefsJSON, _ := json.Marshal(input.LabelPreferences)
143 s := string(prefsJSON)
144 labelPrefsPtr = &s
145 }
146
147 uri := fmt.Sprintf("at://%s/%s/self", session.DID, xrpc.CollectionPreferences)
148
149 err = h.db.UpsertPreferences(&db.Preferences{
150 URI: uri,
151 AuthorDID: session.DID,
152 ExternalLinkSkippedHostnames: &hostnamesStr,
153 SubscribedLabelers: subscribedLabelersPtr,
154 LabelPreferences: labelPrefsPtr,
155 DisableExternalLinkWarning: &input.DisableExternalLinkWarning,
156 CreatedAt: createdAt,
157 IndexedAt: time.Now(),
158 })
159
160 if err != nil {
161 fmt.Printf("Failed to update local db preferences: %v\n", err)
162 }
163
164 w.WriteHeader(http.StatusOK)
165}