Skip to content

Commit 2acf69c

Browse files
committed
feat: implement PDS & User ID lookup
1 parent 609a671 commit 2acf69c

5 files changed

Lines changed: 220 additions & 155 deletions

File tree

bluesky/auth.go

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
package blueskyapi
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"io"
9+
"net"
10+
"net/http"
11+
"regexp"
12+
"strings"
13+
)
14+
15+
type DIDDoc struct {
16+
// yes i know this isn't all of the fields, but it's good enuff
17+
Service []struct {
18+
ID string `json:"id"`
19+
Type string `json:"type"`
20+
ServiceEndpoint string `json:"serviceEndpoint"`
21+
} `json:"service"`
22+
}
23+
24+
func Authenticate(username, password string) (*AuthResponse, *string, error) {
25+
_, userPDS, err := GetUserAuthData(username)
26+
if err != nil {
27+
return nil, nil, err
28+
}
29+
30+
url := *userPDS + "/xrpc/com.atproto.server.createSession"
31+
32+
authReq := AuthRequest{
33+
Identifier: username,
34+
Password: password,
35+
}
36+
37+
reqBody, err := json.Marshal(authReq)
38+
if err != nil {
39+
return nil, nil, err
40+
}
41+
42+
resp, err := SendRequest(nil, http.MethodPost, url, bytes.NewBuffer(reqBody))
43+
if err != nil {
44+
return nil, nil, err
45+
}
46+
defer resp.Body.Close()
47+
48+
if resp.StatusCode != http.StatusOK {
49+
bodyBytes, _ := io.ReadAll(resp.Body)
50+
bodyString := string(bodyBytes)
51+
fmt.Println("Response Status:", resp.StatusCode)
52+
fmt.Println("Response Body:", bodyString)
53+
return nil, nil, errors.New("authentication failed")
54+
}
55+
56+
var authResp AuthResponse
57+
if err := json.NewDecoder(resp.Body).Decode(&authResp); err != nil {
58+
return nil, nil, err
59+
}
60+
61+
return &authResp, userPDS, nil
62+
}
63+
64+
// TODO: This looks like it's a bsky.social specific endpoint, can we get the user's server?
65+
func RefreshToken(pds string, refreshToken string) (*AuthResponse, error) {
66+
url := "https://bsky.social/xrpc/com.atproto.server.refreshSession"
67+
68+
resp, err := SendRequest(&refreshToken, http.MethodPost, url, nil)
69+
if err != nil {
70+
return nil, err
71+
}
72+
defer resp.Body.Close()
73+
74+
if resp.StatusCode != http.StatusOK {
75+
bodyBytes, _ := io.ReadAll(resp.Body)
76+
bodyString := string(bodyBytes)
77+
fmt.Println("Response Status:", resp.StatusCode)
78+
fmt.Println("Response Body:", bodyString)
79+
return nil, errors.New("reauth failed")
80+
}
81+
82+
var authResp AuthResponse
83+
if err := json.NewDecoder(resp.Body).Decode(&authResp); err != nil {
84+
return nil, err
85+
}
86+
87+
return &authResp, nil
88+
}
89+
90+
// This function is to get: the user DID and the user's PDS this should **ONLY** be used during authentication.
91+
//
92+
// @results: userDID, userPDS, error
93+
func GetUserAuthData(handle string) (*string, *string, error) {
94+
// thank you https://discord.com/channels/1097580399187738645/1097580399187738648/1318477650485973004 (ducky.ws) on https://discord.gg/zYvmrHAr8M for explaining this to me
95+
96+
// Validate our handle
97+
if !regexp.MustCompile(`^([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$`).MatchString(handle) {
98+
return nil, nil, errors.New("invalid handle")
99+
}
100+
userDID := ""
101+
102+
// Get the handle's DID
103+
104+
// Get DID thru .well-known, since this is what the most common handle PDS, bsky.social uses.
105+
wellKnownDIDResp, err := http.Get(fmt.Sprintf("https://%s/.well-known/atproto-did", handle))
106+
if err == nil {
107+
bodyBytes, _ := io.ReadAll(wellKnownDIDResp.Body)
108+
bodyString := string(bodyBytes)
109+
110+
if regexp.MustCompile(`^did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$`).MatchString(bodyString) {
111+
userDID = bodyString
112+
}
113+
wellKnownDIDResp.Body.Close()
114+
}
115+
if userDID == "" {
116+
// Get DID through _atproto DNS records
117+
txts, err := net.LookupTXT(fmt.Sprintf("_atproto.%s", handle))
118+
if err == nil {
119+
for _, txt := range txts {
120+
if regexp.MustCompile(`^did=did:[a-z]+:[a-zA-Z0-9._:%-]*[a-zA-Z0-9._-]$`).MatchString(txt) {
121+
userDID = txt[4:] // Extract the DID without the 'did=' prefix
122+
break
123+
}
124+
}
125+
}
126+
}
127+
128+
if userDID == "" {
129+
return nil, nil, errors.New("user does not exist")
130+
}
131+
132+
// Get the user's PDS
133+
134+
// we must do different things depending on the DID type.
135+
didDocReqUrl := ""
136+
switch strings.Split(userDID, ":")[1] {
137+
case "plc":
138+
// https://plc.directory/did:plc:<id>
139+
didDocReqUrl = fmt.Sprintf("https://plc.directory/%s", userDID)
140+
case "web":
141+
didDocReqUrl = fmt.Sprintf("https://%s/.well-known/did.json", strings.Split(userDID, ":")[2])
142+
}
143+
144+
// get the DID doc
145+
didDocReq, err := http.Get(didDocReqUrl)
146+
if err != nil {
147+
return nil, nil, errors.New("could not find PDS")
148+
}
149+
bodyBytes, err := io.ReadAll(didDocReq.Body)
150+
if err != nil {
151+
return nil, nil, err
152+
}
153+
var userDIDDoc DIDDoc
154+
err = json.Unmarshal(bodyBytes, &userDIDDoc)
155+
didDocReq.Body.Close()
156+
if err != nil {
157+
return nil, nil, errors.New("could not find PDS")
158+
}
159+
160+
// get the user's PDS
161+
userPDS := ""
162+
for _, service := range userDIDDoc.Service {
163+
if service.ID == "#atproto_pds" {
164+
userPDS = service.ServiceEndpoint
165+
break
166+
}
167+
}
168+
if userPDS == "" {
169+
return nil, nil, errors.New("could not find PDS")
170+
}
171+
172+
// and finally, return our data
173+
return &userDID, &userPDS, nil
174+
}

bluesky/blueskyapi.go

Lines changed: 0 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -280,67 +280,6 @@ func SendRequest(token *string, method string, url string, body io.Reader) (*htt
280280
return resp, nil
281281
}
282282

283-
func Authenticate(username, password string) (*AuthResponse, error) {
284-
url := "https://bsky.social/xrpc/com.atproto.server.createSession"
285-
286-
authReq := AuthRequest{
287-
Identifier: username,
288-
Password: password,
289-
}
290-
291-
reqBody, err := json.Marshal(authReq)
292-
if err != nil {
293-
return nil, err
294-
}
295-
296-
resp, err := SendRequest(nil, http.MethodPost, url, bytes.NewBuffer(reqBody))
297-
if err != nil {
298-
return nil, err
299-
}
300-
defer resp.Body.Close()
301-
302-
if resp.StatusCode != http.StatusOK {
303-
bodyBytes, _ := io.ReadAll(resp.Body)
304-
bodyString := string(bodyBytes)
305-
fmt.Println("Response Status:", resp.StatusCode)
306-
fmt.Println("Response Body:", bodyString)
307-
return nil, errors.New("authentication failed")
308-
}
309-
310-
var authResp AuthResponse
311-
if err := json.NewDecoder(resp.Body).Decode(&authResp); err != nil {
312-
return nil, err
313-
}
314-
315-
return &authResp, nil
316-
}
317-
318-
// TODO: This looks like it's a bsky.social specific endpoint, can we get the user's server?
319-
func RefreshToken(refreshToken string) (*AuthResponse, error) {
320-
url := "https://bsky.social/xrpc/com.atproto.server.refreshSession"
321-
322-
resp, err := SendRequest(&refreshToken, http.MethodPost, url, nil)
323-
if err != nil {
324-
return nil, err
325-
}
326-
defer resp.Body.Close()
327-
328-
if resp.StatusCode != http.StatusOK {
329-
bodyBytes, _ := io.ReadAll(resp.Body)
330-
bodyString := string(bodyBytes)
331-
fmt.Println("Response Status:", resp.StatusCode)
332-
fmt.Println("Response Body:", bodyString)
333-
return nil, errors.New("reauth failed")
334-
}
335-
336-
var authResp AuthResponse
337-
if err := json.NewDecoder(resp.Body).Decode(&authResp); err != nil {
338-
return nil, err
339-
}
340-
341-
return &authResp, nil
342-
}
343-
344283
func GetUserInfo(token string, screen_name string) (*bridge.TwitterUser, error) {
345284
if user, found := userCache.Get(screen_name); found {
346285
return &user, nil

db_controller/db_controller.go

Lines changed: 16 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package db_controller
22

33
import (
44
"fmt"
5-
"math/big"
65
"os"
76
"path/filepath"
87

@@ -44,6 +43,7 @@ import (
4443
// Token represents the schema for the tokens table
4544
type Token struct {
4645
UserDid string `gorm:"type:string;primaryKey;not null"`
46+
UserPDS string `gorm:"type:string;not null"`
4747
TokenUUID string `gorm:"type:string;primaryKey;not null"`
4848
EncryptedAccessToken string `gorm:"type:string;not null"`
4949
EncryptedRefreshToken string `gorm:"type:string;not null"`
@@ -94,26 +94,16 @@ func InitDB() {
9494
// Returns:
9595
// - The UUID of the stored token.
9696
// - An error if the operation fails.
97-
func StoreToken(did string, accessToken string, refreshToken string, encryptionKey string, accessExpiry float64, refreshExpiry float64) (*string, error) {
98-
// Check if token exists for this DID
99-
var existingToken Token
100-
result := db.Where("user_did = ?", did).First(&existingToken)
101-
102-
var tokenUUID string
103-
if result.Error == nil {
104-
// Token exists, use existing UUID
105-
tokenUUID = existingToken.TokenUUID
106-
} else {
107-
// Generate new UUID for new token
108-
uuid, err := uuid.NewRandom()
109-
if err != nil {
110-
return nil, err
111-
}
112-
tokenUUID = uuid.String()
97+
func StoreToken(did string, pds string, accessToken string, refreshToken string, encryptionKey string, accessExpiry float64, refreshExpiry float64) (*string, error) {
98+
// Check if token exists for this DID.
99+
// Generate new UUID for new token
100+
uuid, err := uuid.NewRandom()
101+
if err != nil {
102+
return nil, err
113103
}
114104

115105
// Update or create token
116-
finalUUID, err := UpdateToken(tokenUUID, did, accessToken, refreshToken, encryptionKey, accessExpiry, refreshExpiry)
106+
finalUUID, err := UpdateToken(uuid.String(), did, accessToken, refreshToken, encryptionKey, accessExpiry, refreshExpiry)
117107
if err != nil {
118108
return nil, err
119109
}
@@ -156,83 +146,24 @@ func UpdateToken(uuid string, did string, accessToken string, refreshToken strin
156146
return &token.TokenUUID, nil
157147
}
158148

159-
func GetToken(did string, tokenUUID string, encryptionKey string) (*string, *string, *float64, *float64, error) {
149+
// GetToken retrieves account data from the database
150+
// @results: accessToken, refreshToken, accessExpiry, refreshExpiry, pds, error
151+
152+
func GetToken(did string, tokenUUID string, encryptionKey string) (*string, *string, *float64, *float64, *string, error) {
160153
var token Token
161154
if err := db.Where("user_did = ? AND token_uuid = ?", did, tokenUUID).First(&token).Error; err != nil {
162-
return nil, nil, nil, nil, err
155+
return nil, nil, nil, nil, nil, err
163156
}
164157

165158
accessToken, err := bridge.Decrypt(token.EncryptedAccessToken, encryptionKey)
166159
if err != nil {
167-
return nil, nil, nil, nil, err
160+
return nil, nil, nil, nil, nil, err
168161
}
169162

170163
refreshToken, err := bridge.Decrypt(token.EncryptedRefreshToken, encryptionKey)
171164
if err != nil {
172-
return nil, nil, nil, nil, err
173-
}
174-
175-
return &accessToken, &refreshToken, &token.AccessExpiry, &token.RefreshExpiry, nil
176-
}
177-
178-
// SetTimelineContext stores or updates the message context in the database.
179-
// Parameters:
180-
// - did: The decentralized identifier of the user.
181-
// - tokenUUID: The UUID of the token.
182-
// - lastMessageId: The ID of the last message.
183-
// - timelineContext: The context of the timeline.
184-
// - encryptionKey: The key used to encrypt the context.
185-
func SetTimelineContext(did string, tokenUUID string, lastMessageId big.Int, timelineContext string, encryptionKey string) error {
186-
fmt.Println("Last Message ID: " + lastMessageId.String())
187-
fmt.Println("Decoded ID: " + bridge.TwitterIDToBlueSky(lastMessageId))
188-
encryptedLastMessageId, err := bridge.Encrypt(lastMessageId.String(), encryptionKey)
189-
if err != nil {
190-
return err
191-
}
192-
193-
encryptedTimelineContext, err := bridge.Encrypt(timelineContext, encryptionKey)
194-
if err != nil {
195-
return err
196-
}
197-
198-
messageContext := MessageContext{
199-
UserDid: did,
200-
TokenUUID: tokenUUID,
201-
LastMessageId: encryptedLastMessageId,
202-
TimelineContext: encryptedTimelineContext,
203-
}
204-
205-
if err := db.Where("user_did = ? AND token_uuid = ?", did, tokenUUID).Assign(&messageContext).FirstOrCreate(&messageContext).Error; err != nil {
206-
return err
207-
}
208-
209-
return nil
210-
}
211-
212-
// GetMessageContext retrieves the message context from the database.
213-
// Parameters:
214-
// - did: The decentralized identifier of the user.
215-
// - tokenUUID: The UUID of the token.
216-
// - message_id: The ID of the last message.
217-
// - encryptionKey: The key used to decrypt the context.
218-
// Returns:
219-
// - The timeline context.
220-
// - An error if the operation fails.
221-
func GetTimelineContext(did string, tokenUUID string, message_id big.Int, encryptionKey string) (*string, error) {
222-
encryptedLastMessageId, err := bridge.Encrypt(message_id.String(), encryptionKey)
223-
if err != nil {
224-
return nil, err
225-
}
226-
227-
var messageContext MessageContext
228-
if err := db.Where("user_did = ? AND token_uuid = ? AND message_id = ?", did, tokenUUID, encryptedLastMessageId).First(&messageContext).Error; err != nil {
229-
return nil, err
230-
}
231-
232-
timelineContext, err := bridge.Decrypt(messageContext.TimelineContext, encryptionKey)
233-
if err != nil {
234-
return nil, err
165+
return nil, nil, nil, nil, nil, err
235166
}
236167

237-
return &timelineContext, nil
168+
return &accessToken, &refreshToken, &token.AccessExpiry, &token.RefreshExpiry, &token.UserPDS, nil
238169
}

0 commit comments

Comments
 (0)