+109
-79
server/main.go
+109
-79
server/main.go
···
28
28
userSessions = map[string]Session{}
29
29
)
30
30
31
+
// Session represents a minimal legacy session persisted via cookie for
32
+
// non-OAuth flows and for compatibility with older endpoints.
33
+
type Session struct {
34
+
DID string `json:"did"`
35
+
Handle string `json:"handle"`
36
+
AccessJWT string `json:"accessJwt,omitempty"`
37
+
RefreshJWT string `json:"refreshJwt,omitempty"`
38
+
}
39
+
40
+
// handleATPSession manages a simple server-backed session store for Bluesky.
41
+
// Client obtains tokens via @atproto/api then POSTs here to persist server-side.
42
+
// Methods:
43
+
// - GET: return current session (handle, did) or 204 if none
44
+
// - POST: set/replace session from JSON body {did, handle, accessJwt, refreshJwt}
45
+
// - DELETE: clear session
46
+
func handleATPSession(w http.ResponseWriter, r *http.Request) {
47
+
w.Header().Set("Content-Type", "application/json; charset=utf-8")
48
+
sid := getOrCreateSessionID(w, r)
49
+
50
+
switch r.Method {
51
+
case http.MethodGet:
52
+
sessionsMu.Lock()
53
+
s, ok := userSessions[sid]
54
+
sessionsMu.Unlock()
55
+
if !ok || s.DID == "" {
56
+
w.WriteHeader(http.StatusNoContent)
57
+
return
58
+
}
59
+
_ = json.NewEncoder(w).Encode(s)
60
+
case http.MethodPost:
61
+
// Limit body size for session payload
62
+
r.Body = http.MaxBytesReader(w, r.Body, 16<<10) // 16 KiB
63
+
var s Session
64
+
if err := json.NewDecoder(r.Body).Decode(&s); err != nil {
65
+
http.Error(w, "invalid json", http.StatusBadRequest)
66
+
return
67
+
}
68
+
if s.Handle == "" || s.DID == "" {
69
+
http.Error(w, "missing did/handle", http.StatusBadRequest)
70
+
return
71
+
}
72
+
sessionsMu.Lock()
73
+
userSessions[sid] = s
74
+
sessionsMu.Unlock()
75
+
w.WriteHeader(http.StatusNoContent)
76
+
case http.MethodDelete:
77
+
sessionsMu.Lock()
78
+
delete(userSessions, sid)
79
+
sessionsMu.Unlock()
80
+
w.WriteHeader(http.StatusNoContent)
81
+
default:
82
+
w.WriteHeader(http.StatusMethodNotAllowed)
83
+
}
84
+
}
85
+
31
86
// resolveHandle resolves a Bluesky handle to a DID using the public AppView endpoint.
32
87
func resolveHandle(handle string) (string, error) {
33
88
u := "https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=" + url.QueryEscape(handle)
···
964
1019
if len(pb) > 0 { _, _ = w.Write(pb) }
965
1020
return
966
1021
}
967
-
w.WriteHeader(http.StatusNoContent)
1022
+
// Verify by re-reading the record and (best-effort) the blob
1023
+
verifyURL := pdsBaseFromUser(r) + "/xrpc/com.atproto.repo.getRecord?repo=" + s.DID + "&collection=lol.tapapp.tap.doc&rkey=" + id
1024
+
vRes, vErr := pdsRequest(w, r, http.MethodGet, verifyURL, "", nil)
1025
+
if vErr != nil {
1026
+
http.Error(w, "verify failed", http.StatusBadGateway)
1027
+
return
1028
+
}
1029
+
defer vRes.Body.Close()
1030
+
if vRes.StatusCode < 200 || vRes.StatusCode >= 300 {
1031
+
w.WriteHeader(http.StatusBadGateway)
1032
+
return
1033
+
}
1034
+
var vRec struct { Value map[string]any `json:"value"` }
1035
+
if e := json.NewDecoder(vRes.Body).Decode(&vRec); e != nil {
1036
+
http.Error(w, "verify decode failed", http.StatusBadGateway)
1037
+
return
1038
+
}
1039
+
var txt string
1040
+
if cb, ok := vRec.Value["contentBlob"].(map[string]any); ok {
1041
+
if ref, ok := cb["ref"].(map[string]any); ok {
1042
+
if l, ok := ref["$link"].(string); ok && l != "" {
1043
+
blobURL := pdsBaseFromUser(r) + "/xrpc/com.atproto.sync.getBlob?did=" + s.DID + "&cid=" + l
1044
+
br, be := pdsRequest(w, r, http.MethodGet, blobURL, "", nil)
1045
+
if be == nil && br.StatusCode >= 200 && br.StatusCode < 300 {
1046
+
defer br.Body.Close()
1047
+
bbuf := new(bytes.Buffer)
1048
+
_, _ = bbuf.ReadFrom(br.Body)
1049
+
txt = bbuf.String()
1050
+
}
1051
+
}
1052
+
}
1053
+
}
1054
+
w.Header().Set("Content-Type", "application/json; charset=utf-8")
1055
+
_ = json.NewEncoder(w).Encode(map[string]any{
1056
+
"id": id,
1057
+
"name": vRec.Value["name"],
1058
+
"text": txt,
1059
+
"updatedAt": vRec.Value["updatedAt"],
1060
+
})
1061
+
return
968
1062
case http.MethodDelete:
969
1063
// Delete the record for this rkey
970
1064
delPayload := map[string]any{
···
990
1084
}
991
1085
}
992
1086
993
-
// handleATPSession manages a simple server-backed session store for Bluesky.
994
-
// Client obtains tokens via @atproto/api then POSTs here to persist server-side.
995
-
// Methods:
996
-
// - GET: return current session (handle, did) or 204 if none
997
-
// - POST: set/replace session from JSON body {did, handle, accessJwt, refreshJwt}
998
-
// - DELETE: clear session
999
-
func handleATPSession(w http.ResponseWriter, r *http.Request) {
1000
-
w.Header().Set("Content-Type", "application/json; charset=utf-8")
1001
-
sid := getOrCreateSessionID(w, r)
1002
-
1003
-
switch r.Method {
1004
-
case http.MethodGet:
1005
-
sessionsMu.Lock()
1006
-
s, ok := userSessions[sid]
1007
-
sessionsMu.Unlock()
1008
-
if !ok || s.DID == "" {
1009
-
w.WriteHeader(http.StatusNoContent)
1010
-
return
1011
-
}
1012
-
_ = json.NewEncoder(w).Encode(s)
1013
-
case http.MethodPost:
1014
-
// Limit body size for session payload
1015
-
r.Body = http.MaxBytesReader(w, r.Body, 16<<10) // 16 KiB
1016
-
var s Session
1017
-
if err := json.NewDecoder(r.Body).Decode(&s); err != nil {
1018
-
http.Error(w, "invalid json", http.StatusBadRequest)
1019
-
return
1020
-
}
1021
-
if s.Handle == "" || s.DID == "" {
1022
-
http.Error(w, "missing did/handle", http.StatusBadRequest)
1023
-
return
1024
-
}
1025
-
sessionsMu.Lock()
1026
-
userSessions[sid] = s
1027
-
sessionsMu.Unlock()
1028
-
w.WriteHeader(http.StatusNoContent)
1029
-
case http.MethodDelete:
1030
-
sessionsMu.Lock()
1031
-
delete(userSessions, sid)
1032
-
sessionsMu.Unlock()
1033
-
w.WriteHeader(http.StatusNoContent)
1034
-
default:
1035
-
w.WriteHeader(http.StatusMethodNotAllowed)
1036
-
}
1037
-
}
1038
-
1039
1087
// handleATPPost posts a simple text note to Bluesky using the stored session.
1040
1088
// Body: { "text": "..." }
1041
1089
func handleATPPost(w http.ResponseWriter, r *http.Request) {
···
1075
1123
return
1076
1124
}
1077
1125
defer blobRes.Body.Close()
1078
-
var blobResp struct {
1079
-
Blob map[string]any `json:"blob"`
1080
-
}
1126
+
var blobResp struct { Blob map[string]any `json:"blob"` }
1081
1127
if err := json.NewDecoder(blobRes.Body).Decode(&blobResp); err != nil {
1082
1128
http.Error(w, "blob decode failed", http.StatusBadGateway)
1083
1129
return
1084
1130
}
1085
1131
1086
-
// 2) Build record referencing the blob and upsert at fixed rkey "current"
1132
+
// 2) Upsert at fixed rkey "current"
1087
1133
record := map[string]any{
1088
1134
"$type": "lol.tapapp.tap.doc",
1089
1135
"contentBlob": blobResp.Blob,
1090
1136
"updatedAt": time.Now().UTC().Format(time.RFC3339Nano),
1091
1137
}
1092
1138
putPayload := map[string]any{
1093
-
"repo": s.DID,
1094
-
"collection": "lol.tapapp.tap.doc",
1095
-
"rkey": "current",
1096
-
"record": record,
1139
+
"repo": s.DID, "collection": "lol.tapapp.tap.doc", "rkey": "current", "record": record,
1097
1140
}
1098
-
putBuf, _ := json.Marshal(putPayload)
1141
+
pbuf, _ := json.Marshal(putPayload)
1099
1142
putURL := pdsBaseFromUser(r) + "/xrpc/com.atproto.repo.putRecord"
1100
-
putRes, err := pdsRequest(w, r, http.MethodPost, putURL, "application/json", putBuf)
1101
-
if err == nil && putRes.StatusCode >= 200 && putRes.StatusCode < 300 {
1102
-
defer putRes.Body.Close()
1143
+
pRes, err := pdsRequest(w, r, http.MethodPost, putURL, "application/json", pbuf)
1144
+
if err == nil && pRes.StatusCode >= 200 && pRes.StatusCode < 300 {
1145
+
defer pRes.Body.Close()
1103
1146
w.WriteHeader(http.StatusNoContent)
1104
1147
return
1105
1148
}
1106
-
if putRes != nil {
1107
-
defer putRes.Body.Close()
1108
-
}
1149
+
if pRes != nil { defer pRes.Body.Close() }
1109
1150
1110
-
// 3) If put failed (e.g., not found), try create with fixed rkey
1151
+
// 3) Fallback to create
1111
1152
createPayload := map[string]any{
1112
-
"repo": s.DID,
1113
-
"collection": "lol.tapapp.tap.doc",
1114
-
"rkey": "current",
1115
-
"record": record,
1153
+
"repo": s.DID, "collection": "lol.tapapp.tap.doc", "rkey": "current", "record": record,
1116
1154
}
1117
-
createBuf, _ := json.Marshal(createPayload)
1155
+
cbuf, _ := json.Marshal(createPayload)
1118
1156
createURL := pdsBaseFromUser(r) + "/xrpc/com.atproto.repo.createRecord"
1119
-
createRes, err := pdsRequest(w, r, http.MethodPost, createURL, "application/json", createBuf)
1157
+
cRes, err := pdsRequest(w, r, http.MethodPost, createURL, "application/json", cbuf)
1120
1158
if err != nil {
1121
1159
http.Error(w, "create failed", http.StatusBadGateway)
1122
1160
return
1123
1161
}
1124
-
defer createRes.Body.Close()
1125
-
w.WriteHeader(createRes.StatusCode)
1126
-
_, _ = w.Write([]byte("{}"))
1127
-
}
1128
-
1129
-
type Session struct {
1130
-
DID string `json:"did"`
1131
-
Handle string `json:"handle"`
1132
-
AccessJWT string `json:"accessJwt,omitempty"`
1133
-
RefreshJWT string `json:"refreshJwt,omitempty"`
1162
+
defer cRes.Body.Close()
1163
+
w.WriteHeader(cRes.StatusCode)
1134
1164
}
1135
1165
1136
1166
func main() {
+8
-11
server/pdf.go
+8
-11
server/pdf.go
···
14
14
15
15
// getDocNameAndText fetches name and text for a document rkey from ATProto
16
16
func getDocNameAndText(w http.ResponseWriter, r *http.Request, ctx context.Context, s Session, id string) (name, text string, status int, err error) {
17
-
// getRecord for rkey id
18
-
url := "https://bsky.social/xrpc/com.atproto.repo.getRecord?repo=" + s.DID + "&collection=lol.tapapp.tap.doc&rkey=" + id
19
-
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
20
-
resp, err := authedDo(w, r, req)
17
+
// getRecord for rkey id via user's PDS
18
+
url := pdsBaseFromUser(r) + "/xrpc/com.atproto.repo.getRecord?repo=" + s.DID + "&collection=lol.tapapp.tap.doc&rkey=" + id
19
+
resp, err := pdsRequest(w, r, http.MethodGet, url, "", nil)
21
20
if err != nil {
22
21
return "", "", http.StatusBadGateway, err
23
22
}
···
50
49
}
51
50
}
52
51
if cid != "" {
53
-
blobURL := "https://bsky.social/xrpc/com.atproto.sync.getBlob?did=" + s.DID + "&cid=" + cid
54
-
bReq, _ := http.NewRequestWithContext(ctx, http.MethodGet, blobURL, nil)
55
-
bRes, err := authedDo(w, r, bReq)
52
+
blobURL := pdsBaseFromUser(r) + "/xrpc/com.atproto.sync.getBlob?did=" + s.DID + "&cid=" + cid
53
+
bRes, err := pdsRequest(w, r, http.MethodGet, blobURL, "", nil)
56
54
if err == nil && bRes.StatusCode >= 200 && bRes.StatusCode < 300 {
57
55
defer bRes.Body.Close()
58
56
buf := new(bytes.Buffer)
···
60
58
text = buf.String()
61
59
} else if bRes != nil {
62
60
// retry once for 5xx
63
-
status := bRes.StatusCode
61
+
st := bRes.StatusCode
64
62
bRes.Body.Close()
65
-
if status >= 500 {
66
-
bReq2, _ := http.NewRequestWithContext(ctx, http.MethodGet, blobURL, nil)
67
-
if bRes2, err2 := authedDo(w, r, bReq2); err2 == nil && bRes2.StatusCode >= 200 && bRes2.StatusCode < 300 {
63
+
if st >= 500 {
64
+
if bRes2, err2 := pdsRequest(w, r, http.MethodGet, blobURL, "", nil); err2 == nil && bRes2.StatusCode >= 200 && bRes2.StatusCode < 300 {
68
65
defer bRes2.Body.Close()
69
66
buf := new(bytes.Buffer)
70
67
_, _ = buf.ReadFrom(bRes2.Body)
server/server
server/server
This is a binary file and will not be displayed.