+2
.env.template
+2
.env.template
-1
Dockerfile
-1
Dockerfile
···
29
29
#Overwrite the main.css with the one from the builder
30
30
COPY --from=node_builder /app/static/main.css /app/pages/static/main.css
31
31
#generate the jwks
32
-
RUN go run github.com/haileyok/atproto-oauth-golang/cmd/helper generate-jwks
33
32
RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags='-w -s -extldflags "-static"' -o main ./cmd
34
33
ARG TARGETOS=${TARGETPLATFORM%%/*}
35
34
ARG TARGETARCH=${TARGETPLATFORM##*/}
-4
Makefile
-4
Makefile
+8
-1
README.md
+8
-1
README.md
···
23
23
24
24
This is a break down of what each env variable is and what it may look like
25
25
26
+
**_breaking piper/v0.0.2 changes env_**
27
+
28
+
You now have to bring your own private key to run piper. Can do this via goat `goat key generate -t P-256`. You want the one that is labeled under "Secret Key (Multibase Syntax): save this securely (eg, add to password manager)"
29
+
30
+
- `ATPROTO_CLIENT_SECRET_KEY` - Private key for oauth confidential client. This can be generated via goat `goat key generate -t P-256`
31
+
- `ATPROTO_CLIENT_SECRET_KEY_ID` - Key ID for oauth confidential client. This needs to be persistent and unique, can use a timestamp. Here's one for you: `1758199756`
32
+
33
+
26
34
- `SERVER_PORT` - The port piper is hosted on
27
35
- `SERVER_HOST` - The server host. `localhost` is fine here, or `0.0.0.0` for docker
28
36
- `SERVER_ROOT_URL` - This needs to be the pubically accessible url created in [Setup](#setup). Like `https://piper.teal.fm`
···
53
61
run some make scripts:
54
62
55
63
```
56
-
make jwtgen
57
64
58
65
make dev-setup
59
66
```
+1
-1
cmd/handlers.go
+1
-1
cmd/handlers.go
···
425
425
426
426
// Submit to PDS as feed.play record
427
427
if user.ATProtoDID != nil && atprotoService != nil {
428
-
if err := atprotoservice.SubmitPlayToPDS(r.Context(), *user.ATProtoDID, &track, atprotoService); err != nil {
428
+
if err := atprotoservice.SubmitPlayToPDS(r.Context(), *user.ATProtoDID, *user.MostRecentAtProtoSessionID, &track, atprotoService); err != nil {
429
429
log.Printf("apiSubmitListensHandler: Error submitting play to PDS for user %d: %v", userID, err)
430
430
// Don't fail the request, just log the error
431
431
}
+16
-11
cmd/main.go
+16
-11
cmd/main.go
···
5
5
"fmt"
6
6
"log"
7
7
"net/http"
8
-
"os"
9
8
"time"
10
9
11
10
"github.com/teal-fm/piper/service/lastfm"
···
57
56
log.Fatalf("Error initializing database: %v", err)
58
57
}
59
58
59
+
sessionManager := session.NewSessionManager(database)
60
+
60
61
// --- Service Initializations ---
61
-
jwksBytes, err := os.ReadFile("./jwks.json")
62
-
if err != nil {
63
-
// run `make jwtgen`
64
-
log.Fatalf("Error reading JWK file: %v", err)
62
+
63
+
var newJwkPrivateKey = viper.GetString("atproto.client_secret_key")
64
+
if newJwkPrivateKey == "" {
65
+
fmt.Printf("You now have to set the ATPROTO_CLIENT_SECRET_KEY env var to a private key. This can be done via goat key generate -t P-256")
66
+
return
65
67
}
66
-
jwks, err := atproto.LoadJwks(jwksBytes)
67
-
if err != nil {
68
-
log.Fatalf("Error loading JWK: %v", err)
68
+
var clientSecretKeyId = viper.GetString("atproto.client_secret_key_id")
69
+
if clientSecretKeyId == "" {
70
+
fmt.Printf("You also now have to set the ATPROTO_CLIENT_SECRET_KEY_ID env var to a key ID. This needs to be persistent and unique. Here's one for you: %d", time.Now().Unix())
71
+
return
69
72
}
73
+
70
74
atprotoService, err := atproto.NewATprotoAuthService(
71
75
database,
72
-
jwks,
76
+
sessionManager,
77
+
newJwkPrivateKey,
73
78
viper.GetString("atproto.client_id"),
74
79
viper.GetString("atproto.callback_url"),
80
+
clientSecretKeyId,
75
81
)
76
82
if err != nil {
77
83
log.Fatalf("Error creating ATproto auth service: %v", err)
···
82
88
spotifyService := spotify.NewSpotifyService(database, atprotoService, mbService, playingNowService)
83
89
lastfmService := lastfm.NewLastFMService(database, viper.GetString("lastfm.api_key"), mbService, atprotoService, playingNowService)
84
90
85
-
sessionManager := session.NewSessionManager(database)
86
-
oauthManager := oauth.NewOAuthServiceManager(sessionManager)
91
+
oauthManager := oauth.NewOAuthServiceManager()
87
92
88
93
spotifyOAuth := oauth.NewOAuth2Service(
89
94
viper.GetString("spotify.client_id"),
+2
-2
cmd/routes.go
+2
-2
cmd/routes.go
···
28
28
mux.HandleFunc("/api-keys", session.WithAuth(app.apiKeyService.HandleAPIKeyManagement(app.database, app.pages), app.sessionManager))
29
29
mux.HandleFunc("/link-lastfm", session.WithAuth(handleLinkLastfmForm(app.database, app.pages), app.sessionManager)) // GET form
30
30
mux.HandleFunc("/link-lastfm/submit", session.WithAuth(handleLinkLastfmSubmit(app.database), app.sessionManager)) // POST submit - Changed route slightly
31
-
mux.HandleFunc("/logout", app.sessionManager.HandleLogout)
31
+
mux.HandleFunc("/logout", app.oauthManager.HandleLogout("atproto"))
32
32
mux.HandleFunc("/debug/", session.WithAuth(app.sessionManager.HandleDebug, app.sessionManager))
33
33
34
34
mux.HandleFunc("/api/v1/me", session.WithAPIAuth(apiMeHandler(app.database), app.sessionManager))
···
45
45
serverUrlRoot := viper.GetString("server.root_url")
46
46
atpClientId := viper.GetString("atproto.client_id")
47
47
atpCallbackUrl := viper.GetString("atproto.callback_url")
48
-
mux.HandleFunc("/.well-known/client-metadata.json", func(w http.ResponseWriter, r *http.Request) {
48
+
mux.HandleFunc("/oauth-client-metadata.json", func(w http.ResponseWriter, r *http.Request) {
49
49
app.atprotoService.HandleClientMetadata(w, r, serverUrlRoot, atpClientId, atpCallbackUrl)
50
50
})
51
51
mux.HandleFunc("/oauth/jwks.json", app.atprotoService.HandleJwks)
+261
-170
db/atproto.go
+261
-170
db/atproto.go
···
3
3
import (
4
4
"context"
5
5
"database/sql"
6
-
"encoding/json"
7
6
"fmt"
7
+
"strings"
8
8
"time"
9
9
10
-
oauth "github.com/haileyok/atproto-oauth-golang"
11
-
"github.com/haileyok/atproto-oauth-golang/helpers"
12
-
"github.com/lestrrat-go/jwx/v2/jwk"
10
+
"github.com/bluesky-social/indigo/atproto/auth/oauth"
11
+
"github.com/bluesky-social/indigo/atproto/syntax"
13
12
"github.com/teal-fm/piper/models"
14
13
)
15
14
16
-
type ATprotoAuthData struct {
17
-
State string `json:"state"`
18
-
DID string `json:"did"`
19
-
PDSUrl string `json:"pds_url"`
20
-
AuthServerIssuer string `json:"authserver_issuer"`
21
-
PKCEVerifier string `json:"pkce_verifier"`
22
-
DPoPAuthServerNonce string `json:"dpop_authserver_nonce"`
23
-
DPoPPrivateJWK jwk.Key `json:"dpop_private_jwk"`
24
-
CreatedAt time.Time `json:"created_at"`
25
-
}
26
-
27
-
func (db *DB) SaveATprotoAuthData(data *models.ATprotoAuthData) error {
28
-
dpopPrivateJWKBytes, err := json.Marshal(data.DPoPPrivateJWK)
29
-
if err != nil {
30
-
return err
31
-
}
32
-
33
-
_, err = db.Exec(`
34
-
INSERT INTO atproto_auth_data (state, did, pds_url, authserver_issuer, pkce_verifier, dpop_authserver_nonce, dpop_private_jwk)
35
-
VALUES (?, ?, ?, ?, ?, ?, ?)`,
36
-
data.State, data.DID, data.PDSUrl, data.AuthServerIssuer, data.PKCEVerifier, data.DPoPAuthServerNonce, string(dpopPrivateJWKBytes))
37
-
38
-
return err
39
-
}
40
-
41
-
func (db *DB) GetATprotoAuthData(state string) (*models.ATprotoAuthData, error) {
42
-
var data models.ATprotoAuthData
43
-
var dpopPrivateJWKString string
44
-
45
-
err := db.QueryRow(`
46
-
SELECT state, did, pds_url, authserver_issuer, pkce_verifier, dpop_authserver_nonce, dpop_private_jwk
47
-
FROM atproto_auth_data
48
-
WHERE state = ?`,
49
-
state).Scan(
50
-
&data.State,
51
-
&data.DID,
52
-
&data.PDSUrl,
53
-
&data.AuthServerIssuer,
54
-
&data.PKCEVerifier,
55
-
&data.DPoPAuthServerNonce,
56
-
&dpopPrivateJWKString,
57
-
)
58
-
if err != nil {
59
-
if err == sql.ErrNoRows {
60
-
return nil, fmt.Errorf("no auth data found for state %s: %w", state, err)
61
-
}
62
-
return nil, fmt.Errorf("failed to scan auth data for state %s: %w", state, err)
63
-
}
64
-
65
-
key, err := helpers.ParseJWKFromBytes([]byte(dpopPrivateJWKString))
66
-
if err != nil {
67
-
return nil, fmt.Errorf("failed to parse DPoPPrivateJWK for state %s: %w", state, err)
68
-
}
69
-
data.DPoPPrivateJWK = key
70
-
71
-
return &data, nil
72
-
}
73
-
74
15
func (db *DB) FindOrCreateUserByDID(did string) (*models.User, error) {
75
16
var user models.User
76
17
err := db.QueryRow(`
···
108
49
return &user, err
109
50
}
110
51
111
-
// create or update the current user's ATproto session data.
112
-
func (db *DB) SaveATprotoSession(tokenResp *oauth.TokenResponse, authserverIss string, dpopPrivateJWK jwk.Key, pdsUrl string) error {
113
-
db.logger.Printf("Saving session with PDS url %s", pdsUrl)
114
-
expiryTime := time.Now().UTC().Add(time.Second * time.Duration(tokenResp.ExpiresIn))
52
+
func (db *DB) SetLatestATProtoSessionId(did string, atProtoSessionID string) error {
53
+
db.logger.Printf("Setting latest atproto session id for did %s to %s", did, atProtoSessionID)
115
54
now := time.Now().UTC()
116
55
117
-
dpopPrivateJWKBytes, err := json.Marshal(dpopPrivateJWK)
118
-
if err != nil {
119
-
return err
120
-
}
121
-
122
56
result, err := db.Exec(`
123
57
UPDATE users
124
-
SET atproto_access_token = ?,
125
-
atproto_refresh_token = ?,
126
-
atproto_token_expiry = ?,
127
-
atproto_scope = ?,
128
-
atproto_sub = ?,
129
-
atproto_authserver_issuer = ?,
130
-
atproto_token_type = ?,
131
-
atproto_authserver_nonce = ?,
132
-
atproto_dpop_private_jwk = ?,
133
-
atproto_pds_url = ?,
134
-
atproto_pds_nonce = ?,
58
+
SET
59
+
most_recent_at_session_id = ?,
135
60
updated_at = ?
136
61
WHERE atproto_did = ?`,
137
-
tokenResp.AccessToken,
138
-
tokenResp.RefreshToken,
139
-
expiryTime,
140
-
tokenResp.Scope,
141
-
tokenResp.Sub,
142
-
authserverIss,
143
-
tokenResp.TokenType,
144
-
tokenResp.DpopAuthserverNonce,
145
-
string(dpopPrivateJWKBytes),
146
-
pdsUrl,
147
-
// will get set later
148
-
"",
62
+
atProtoSessionID,
149
63
now,
150
-
tokenResp.Sub,
64
+
did,
151
65
)
152
-
153
66
if err != nil {
154
-
return fmt.Errorf("failed to update atproto session for did %s: %w", tokenResp.Sub, err)
67
+
db.logger.Printf("%v", err)
68
+
return fmt.Errorf("failed to update atproto session for did %s: %w", did, atProtoSessionID)
155
69
}
156
70
157
71
rowsAffected, err := result.RowsAffected()
158
72
if err != nil {
159
73
// it's possible the update succeeded here?
160
-
return fmt.Errorf("failed to check rows affected after updating atproto session for did %s: %w", tokenResp.Sub, err)
74
+
return fmt.Errorf("failed to check rows affected after updating atproto session for did %s: %w", did, atProtoSessionID)
161
75
}
162
76
163
77
if rowsAffected == 0 {
164
-
return fmt.Errorf("no user found with did %s to update session, creating new session", tokenResp.Sub)
78
+
return fmt.Errorf("no user found with did %s to update session, creating new session", did)
165
79
}
166
80
167
81
return nil
168
82
}
169
83
170
-
func (db *DB) GetAtprotoSession(did string, ctx context.Context, oauthClient oauth.Client) (*models.ATprotoAuthSession, error) {
171
-
var oauthSession models.ATprotoAuthSession
172
-
var authserverIss string
173
-
var jwkBytes string
84
+
type SqliteATProtoStore struct {
85
+
db *sql.DB
86
+
}
174
87
175
-
err := db.QueryRow(
176
-
`
177
-
SELECT id,
178
-
atproto_did,
179
-
atproto_pds_url,
180
-
atproto_authserver_issuer,
181
-
atproto_access_token,
182
-
atproto_refresh_token,
183
-
atproto_pds_nonce,
184
-
atproto_authserver_nonce,
185
-
atproto_dpop_private_jwk,
186
-
atproto_token_expiry
187
-
FROM users
188
-
WHERE atproto_did = ?`,
189
-
did,
190
-
).Scan(
191
-
&oauthSession.ID,
192
-
&oauthSession.DID,
193
-
&oauthSession.PDSUrl,
194
-
&authserverIss,
195
-
&oauthSession.AccessToken,
196
-
&oauthSession.RefreshToken,
197
-
&oauthSession.DpopPdsNonce,
198
-
&oauthSession.DpopAuthServerNonce,
199
-
&jwkBytes,
200
-
&oauthSession.TokenExpiry,
88
+
var _ oauth.ClientAuthStore = (*SqliteATProtoStore)(nil)
89
+
90
+
func NewSqliteATProtoStore(db *sql.DB) *SqliteATProtoStore {
91
+
return &SqliteATProtoStore{
92
+
db: db,
93
+
}
94
+
}
95
+
96
+
func sessionKey(did syntax.DID, sessionID string) string {
97
+
return fmt.Sprintf("%s/%s", did, sessionID)
98
+
}
99
+
100
+
func splitScopes(s string) []string {
101
+
if s == "" {
102
+
return nil
103
+
}
104
+
return strings.Fields(s)
105
+
}
106
+
107
+
func joinScopes(scopes []string) string {
108
+
if len(scopes) == 0 {
109
+
return ""
110
+
}
111
+
return strings.Join(scopes, " ")
112
+
}
113
+
114
+
func (s *SqliteATProtoStore) GetSession(ctx context.Context, did syntax.DID, sessionID string) (*oauth.ClientSessionData, error) {
115
+
lookUpKey := sessionKey(did, sessionID)
116
+
117
+
var (
118
+
accountDIDStr string
119
+
lookUpKeyStr string
120
+
sessionIDStr string
121
+
hostURL string
122
+
authServerURL string
123
+
authServerTokenEndpoint string
124
+
authServerRevocationEndpoint string
125
+
scopesStr string
126
+
accessToken string
127
+
refreshToken string
128
+
dpopAuthServerNonce string
129
+
dpopHostNonce string
130
+
dpopPrivateKeyMultibase string
131
+
)
132
+
133
+
err := s.db.QueryRow(`
134
+
SELECT account_did,
135
+
look_up_key,
136
+
session_id,
137
+
host_url,
138
+
authserver_url,
139
+
authserver_token_endpoint,
140
+
authserver_revocation_endpoint,
141
+
scopes,
142
+
access_token,
143
+
refresh_token,
144
+
dpop_authserver_nonce,
145
+
dpop_host_nonce,
146
+
dpop_privatekey_multibase
147
+
FROM atproto_sessions
148
+
WHERE look_up_key = ?
149
+
`, lookUpKey).Scan(
150
+
&accountDIDStr,
151
+
&lookUpKeyStr,
152
+
&sessionIDStr,
153
+
&hostURL,
154
+
&authServerURL,
155
+
&authServerTokenEndpoint,
156
+
&authServerRevocationEndpoint,
157
+
&scopesStr,
158
+
&accessToken,
159
+
&refreshToken,
160
+
&dpopAuthServerNonce,
161
+
&dpopHostNonce,
162
+
&dpopPrivateKeyMultibase,
201
163
)
202
164
165
+
if err == sql.ErrNoRows {
166
+
return nil, fmt.Errorf("session not found: %s", lookUpKey)
167
+
}
203
168
if err != nil {
204
-
return nil, fmt.Errorf("failed to get atproto session for did %s: %w", did, err)
169
+
return nil, err
205
170
}
206
171
207
-
privateJwk, err := helpers.ParseJWKFromBytes([]byte(jwkBytes))
172
+
accDID, err := syntax.ParseDID(accountDIDStr)
208
173
if err != nil {
209
-
return nil, fmt.Errorf("failed to parse DPoPPrivateJWK: %w", err)
210
-
} else {
211
-
// add jwk to the struct
212
-
oauthSession.DpopPrivateJWK = privateJwk
174
+
return nil, fmt.Errorf("invalid account DID in session: %w", err)
213
175
}
214
176
215
-
// printout the session details
216
-
db.logger.Printf("Getting session details for the did: %+v\n", oauthSession.DID)
177
+
sess := oauth.ClientSessionData{
178
+
AccountDID: accDID,
179
+
SessionID: sessionIDStr,
180
+
HostURL: hostURL,
181
+
AuthServerURL: authServerURL,
182
+
AuthServerTokenEndpoint: authServerTokenEndpoint,
183
+
AuthServerRevocationEndpoint: authServerRevocationEndpoint,
184
+
Scopes: splitScopes(scopesStr),
185
+
AccessToken: accessToken,
186
+
RefreshToken: refreshToken,
187
+
DPoPAuthServerNonce: dpopAuthServerNonce,
188
+
DPoPHostNonce: dpopHostNonce,
189
+
DPoPPrivateKeyMultibase: dpopPrivateKeyMultibase,
190
+
}
217
191
218
-
// if token is expired, refresh it
219
-
if time.Now().UTC().After(oauthSession.TokenExpiry) {
192
+
return &sess, nil
193
+
}
220
194
221
-
resp, err := oauthClient.RefreshTokenRequest(ctx, oauthSession.RefreshToken, authserverIss, oauthSession.DpopAuthServerNonce, privateJwk)
222
-
if err != nil {
223
-
return nil, err
224
-
}
195
+
func (s *SqliteATProtoStore) SaveSession(ctx context.Context, sess oauth.ClientSessionData) error {
196
+
lookUpKey := sessionKey(sess.AccountDID, sess.SessionID)
197
+
// simple upsert: delete then insert
198
+
_, _ = s.db.Exec(`DELETE FROM atproto_sessions WHERE look_up_key = ?`, lookUpKey)
199
+
_, err := s.db.Exec(`
200
+
INSERT INTO atproto_sessions (
201
+
look_up_key,
202
+
account_did,
203
+
session_id,
204
+
host_url,
205
+
authserver_url,
206
+
authserver_token_endpoint,
207
+
authserver_revocation_endpoint,
208
+
scopes,
209
+
access_token,
210
+
refresh_token,
211
+
dpop_authserver_nonce,
212
+
dpop_host_nonce,
213
+
dpop_privatekey_multibase
214
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
215
+
`,
216
+
lookUpKey,
217
+
sess.AccountDID.String(),
218
+
sess.SessionID,
219
+
sess.HostURL,
220
+
sess.AuthServerURL,
221
+
sess.AuthServerTokenEndpoint,
222
+
sess.AuthServerRevocationEndpoint,
223
+
joinScopes(sess.Scopes),
224
+
sess.AccessToken,
225
+
sess.RefreshToken,
226
+
sess.DPoPAuthServerNonce,
227
+
sess.DPoPHostNonce,
228
+
sess.DPoPPrivateKeyMultibase,
229
+
)
230
+
return err
231
+
}
225
232
226
-
if err := db.SaveATprotoSession(resp, authserverIss, privateJwk, oauthSession.PDSUrl); err != nil {
227
-
return nil, fmt.Errorf("failed to save refreshed token: %w", err)
228
-
}
233
+
func (s *SqliteATProtoStore) DeleteSession(ctx context.Context, did syntax.DID, sessionID string) error {
234
+
lookUpKey := sessionKey(did, sessionID)
235
+
_, err := s.db.Exec(`DELETE FROM atproto_sessions WHERE look_up_key = ?`, lookUpKey)
236
+
return err
237
+
}
229
238
230
-
oauthSession = models.ATprotoAuthSession{
231
-
ID: oauthSession.ID,
232
-
DID: oauthSession.DID,
233
-
PDSUrl: oauthSession.PDSUrl,
234
-
AuthServerIssuer: authserverIss,
235
-
AccessToken: resp.AccessToken,
236
-
RefreshToken: resp.RefreshToken,
237
-
DpopPdsNonce: oauthSession.DpopPdsNonce,
238
-
DpopAuthServerNonce: resp.DpopAuthserverNonce,
239
-
DpopPrivateJWK: privateJwk,
240
-
TokenExpiry: time.Now().UTC().Add(time.Duration(resp.ExpiresIn) * time.Second),
239
+
func (s *SqliteATProtoStore) GetAuthRequestInfo(ctx context.Context, state string) (*oauth.AuthRequestData, error) {
240
+
var (
241
+
authServerURL string
242
+
accountDIDStr sql.NullString
243
+
scopesStr string
244
+
requestURI string
245
+
authServerTokenEndpoint string
246
+
authServerRevocationEndpoint string
247
+
pkceVerifier string
248
+
dpopAuthServerNonce string
249
+
dpopPrivateKeyMultibase string
250
+
)
251
+
err := s.db.QueryRow(`
252
+
SELECT authserver_url,
253
+
account_did,
254
+
scopes,
255
+
request_uri,
256
+
authserver_token_endpoint,
257
+
authserver_revocation_endpoint,
258
+
pkce_verifier,
259
+
dpop_authserver_nonce,
260
+
dpop_privatekey_multibase
261
+
FROM atproto_state
262
+
WHERE state = ?
263
+
`, state).Scan(
264
+
&authServerURL,
265
+
&accountDIDStr,
266
+
&scopesStr,
267
+
&requestURI,
268
+
&authServerTokenEndpoint,
269
+
&authServerRevocationEndpoint,
270
+
&pkceVerifier,
271
+
&dpopAuthServerNonce,
272
+
&dpopPrivateKeyMultibase,
273
+
)
274
+
if err == sql.ErrNoRows {
275
+
return nil, fmt.Errorf("request info not found: %s", state)
276
+
}
277
+
if err != nil {
278
+
return nil, err
279
+
}
280
+
var accountDIDPtr *syntax.DID
281
+
if accountDIDStr.Valid && accountDIDStr.String != "" {
282
+
acc, err := syntax.ParseDID(accountDIDStr.String)
283
+
if err != nil {
284
+
return nil, fmt.Errorf("invalid account DID in auth request: %w", err)
241
285
}
286
+
accountDIDPtr = &acc
287
+
}
288
+
info := oauth.AuthRequestData{
289
+
State: state,
290
+
AuthServerURL: authServerURL,
291
+
AccountDID: accountDIDPtr,
292
+
Scopes: splitScopes(scopesStr),
293
+
RequestURI: requestURI,
294
+
AuthServerTokenEndpoint: authServerTokenEndpoint,
295
+
AuthServerRevocationEndpoint: authServerRevocationEndpoint,
296
+
PKCEVerifier: pkceVerifier,
297
+
DPoPAuthServerNonce: dpopAuthServerNonce,
298
+
DPoPPrivateKeyMultibase: dpopPrivateKeyMultibase,
299
+
}
300
+
return &info, nil
301
+
}
242
302
303
+
func (s *SqliteATProtoStore) SaveAuthRequestInfo(ctx context.Context, info oauth.AuthRequestData) error {
304
+
// ensure not already exists
305
+
var exists int
306
+
err := s.db.QueryRow(`SELECT 1 FROM atproto_state WHERE state = ?`, info.State).Scan(&exists)
307
+
if err == nil {
308
+
return fmt.Errorf("auth request already saved for state %s", info.State)
243
309
}
244
-
245
-
return &oauthSession, nil
310
+
if err != nil && err != sql.ErrNoRows {
311
+
return err
312
+
}
313
+
var accountDIDStr interface{}
314
+
if info.AccountDID != nil {
315
+
accountDIDStr = info.AccountDID.String()
316
+
} else {
317
+
accountDIDStr = nil
318
+
}
319
+
_, err = s.db.Exec(`
320
+
INSERT INTO atproto_state (
321
+
state,
322
+
authserver_url,
323
+
account_did,
324
+
scopes,
325
+
request_uri,
326
+
authserver_token_endpoint,
327
+
authserver_revocation_endpoint,
328
+
pkce_verifier,
329
+
dpop_authserver_nonce,
330
+
dpop_privatekey_multibase
331
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
332
+
`,
333
+
info.State,
334
+
info.AuthServerURL,
335
+
accountDIDStr,
336
+
joinScopes(info.Scopes),
337
+
info.RequestURI,
338
+
info.AuthServerTokenEndpoint,
339
+
info.AuthServerRevocationEndpoint,
340
+
info.PKCEVerifier,
341
+
info.DPoPAuthServerNonce,
342
+
info.DPoPPrivateKeyMultibase,
343
+
)
344
+
return err
246
345
}
247
346
248
-
func AtpSessionToAuthArgs(sess *models.ATprotoAuthSession) *oauth.XrpcAuthedRequestArgs {
249
-
//Commenting out so jwts and tokens are not in logs
250
-
//fmt.Printf("DID: %s\nPDS URL: %s\nISS: %s\nAccess Token: %s\nNonce: %s\nPrivate JWK: %s\n", sess.DID, sess.PDSUrl, sess.AuthServerIssuer, sess.AccessToken, sess.DpopPdsNonce, sess.DpopPrivateJWK)
251
-
return &oauth.XrpcAuthedRequestArgs{
252
-
Did: sess.DID,
253
-
PdsUrl: sess.PDSUrl,
254
-
Issuer: sess.AuthServerIssuer,
255
-
AccessToken: sess.AccessToken,
256
-
DpopPdsNonce: sess.DpopPdsNonce,
257
-
DpopPrivateJwk: sess.DpopPrivateJWK,
258
-
}
347
+
func (s *SqliteATProtoStore) DeleteAuthRequestInfo(ctx context.Context, state string) error {
348
+
_, err := s.db.Exec(`DELETE FROM atproto_state WHERE state = ?`, state)
349
+
return err
259
350
}
+57
-24
db/db.go
+57
-24
db/db.go
···
45
45
username TEXT, -- Made nullable, might not have username initially
46
46
email TEXT UNIQUE, -- Made nullable
47
47
atproto_did TEXT UNIQUE, -- Atproto DID (identifier)
48
-
atproto_pds_url TEXT,
49
-
atproto_authserver_issuer TEXT,
50
-
atproto_access_token TEXT, -- Atproto access token
51
-
atproto_refresh_token TEXT, -- Atproto refresh token
52
-
atproto_token_expiry TIMESTAMP, -- Atproto token expiry
53
-
atproto_sub TEXT,
54
-
atproto_scope TEXT, -- Atproto token scope
55
-
atproto_token_type TEXT, -- Atproto token type
56
-
atproto_authserver_nonce TEXT,
57
-
atproto_pds_nonce TEXT,
58
-
atproto_dpop_private_jwk TEXT,
48
+
most_recent_at_session_id TEXT, -- Most recent oAuth session id
59
49
spotify_id TEXT UNIQUE, -- Spotify specific ID
60
50
access_token TEXT, -- Spotify access token
61
51
refresh_token TEXT, -- Spotify refresh token
···
91
81
}
92
82
93
83
_, err = db.Exec(`
94
-
CREATE TABLE IF NOT EXISTS atproto_auth_data (
95
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
96
-
state TEXT NOT NULL,
97
-
did TEXT,
98
-
pds_url TEXT NOT NULL,
99
-
authserver_issuer TEXT NOT NULL,
100
-
pkce_verifier TEXT NOT NULL,
101
-
dpop_authserver_nonce TEXT NOT NULL,
102
-
dpop_private_jwk TEXT NOT NULL,
103
-
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
104
-
)`)
84
+
CREATE TABLE IF NOT EXISTS atproto_state (
85
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
86
+
state TEXT NOT NULL,
87
+
authserver_url TEXT,
88
+
account_did TEXT,
89
+
scopes TEXT,
90
+
request_uri TEXT,
91
+
authserver_token_endpoint TEXT,
92
+
authserver_revocation_endpoint TEXT,
93
+
pkce_verifier TEXT,
94
+
dpop_authserver_nonce TEXT,
95
+
dpop_privatekey_multibase TEXT,
96
+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
97
+
);
98
+
CREATE INDEX IF NOT EXISTS atproto_state_state ON atproto_state(state);
99
+
100
+
`)
101
+
if err != nil {
102
+
return err
103
+
}
104
+
105
+
_, err = db.Exec(`
106
+
CREATE TABLE IF NOT EXISTS atproto_sessions (
107
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
108
+
look_up_key TEXT NOT NULL,
109
+
account_did TEXT,
110
+
session_id TEXT,
111
+
host_url TEXT,
112
+
authserver_url TEXT,
113
+
authserver_token_endpoint TEXT,
114
+
authserver_revocation_endpoint TEXT,
115
+
scopes TEXT,
116
+
access_token TEXT,
117
+
refresh_token TEXT,
118
+
dpop_authserver_nonce TEXT,
119
+
dpop_host_nonce TEXT,
120
+
dpop_privatekey_multibase TEXT,
121
+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
122
+
);
123
+
CREATE INDEX IF NOT EXISTS idx_atproto_sessions_look_up_key ON atproto_sessions(look_up_key);
124
+
`)
105
125
if err != nil {
106
126
return err
107
127
}
···
162
182
user := &models.User{}
163
183
164
184
err := db.QueryRow(`
165
-
SELECT id, username, email, atproto_did, spotify_id, access_token, refresh_token, token_expiry, lastfm_username, created_at, updated_at
185
+
SELECT id,
186
+
username,
187
+
email,
188
+
atproto_did,
189
+
most_recent_at_session_id,
190
+
spotify_id,
191
+
access_token,
192
+
refresh_token,
193
+
token_expiry,
194
+
lastfm_username,
195
+
created_at,
196
+
updated_at
166
197
FROM users WHERE id = ?`, ID).Scan(
167
-
&user.ID, &user.Username, &user.Email, &user.ATProtoDID, &user.SpotifyID,
198
+
&user.ID, &user.Username, &user.Email, &user.ATProtoDID, &user.MostRecentAtProtoSessionID, &user.SpotifyID,
168
199
&user.AccessToken, &user.RefreshToken, &user.TokenExpiry,
169
200
&user.LastFMUsername,
170
201
&user.CreatedAt, &user.UpdatedAt)
···
472
503
473
504
return &lastTimestamp, nil
474
505
}
506
+
507
+
//
+2
-2
db/lfm.go
+2
-2
db/lfm.go
···
42
42
43
43
func (db *DB) GetUserByLastFM(lastfmUsername string) (*models.User, error) {
44
44
row := db.QueryRow(`
45
-
SELECT id, username, email, atproto_did, created_at, updated_at, lastfm_username
45
+
SELECT id, username, email, atproto_did, most_recent_at_session_id, created_at, updated_at, lastfm_username
46
46
FROM users
47
47
WHERE lastfm_username = ?`, lastfmUsername)
48
48
49
49
user := &models.User{}
50
50
err := row.Scan(
51
-
&user.ID, &user.Username, &user.Email, &user.ATProtoDID,
51
+
&user.ID, &user.MostRecentAtProtoSessionID, &user.Username, &user.Email, &user.ATProtoDID,
52
52
&user.CreatedAt, &user.UpdatedAt, &user.LastFMUsername)
53
53
if err != nil {
54
54
return nil, err
+29
-3
go.mod
+29
-3
go.mod
···
3
3
go 1.24.0
4
4
5
5
require (
6
-
github.com/bluesky-social/indigo v0.0.0-20250506174012-7075cf22f63e
6
+
github.com/bluesky-social/indigo v0.0.0-20251003000214-3259b215110e
7
7
github.com/dlclark/regexp2 v1.11.5
8
-
github.com/haileyok/atproto-oauth-golang v0.0.2
9
8
github.com/ipfs/go-cid v0.4.1
10
9
github.com/joho/godotenv v1.5.1
11
10
github.com/justinas/alice v1.2.0
···
21
20
require (
22
21
dario.cat/mergo v1.0.1 // indirect
23
22
github.com/air-verse/air v1.61.7 // indirect
23
+
github.com/beorn7/perks v1.0.1 // indirect
24
24
github.com/bep/godartsass v1.2.0 // indirect
25
25
github.com/bep/godartsass/v2 v2.1.0 // indirect
26
26
github.com/bep/golibsass v1.2.0 // indirect
27
27
github.com/carlmjohnson/versioninfo v0.22.5 // indirect
28
+
github.com/cespare/xxhash/v2 v2.3.0 // indirect
28
29
github.com/cli/safeexec v1.0.1 // indirect
29
30
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
30
31
github.com/creack/pty v1.1.23 // indirect
···
39
40
github.com/goccy/go-json v0.10.2 // indirect
40
41
github.com/gogo/protobuf v1.3.2 // indirect
41
42
github.com/gohugoio/hugo v0.134.3 // indirect
42
-
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
43
+
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
44
+
github.com/google/go-querystring v1.1.0 // indirect
43
45
github.com/google/uuid v1.6.0 // indirect
46
+
github.com/gorilla/context v1.1.2 // indirect
47
+
github.com/gorilla/securecookie v1.1.2 // indirect
48
+
github.com/gorilla/sessions v1.4.0 // indirect
44
49
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
45
50
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
46
51
github.com/hashicorp/golang-lru v1.0.2 // indirect
52
+
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
47
53
github.com/ipfs/bbloom v0.0.4 // indirect
48
54
github.com/ipfs/go-block-format v0.2.0 // indirect
49
55
github.com/ipfs/go-datastore v0.6.0 // indirect
···
56
62
github.com/ipfs/go-log/v2 v2.5.1 // indirect
57
63
github.com/ipfs/go-metrics-interface v0.0.1 // indirect
58
64
github.com/jbenet/goprocess v0.1.4 // indirect
65
+
github.com/jinzhu/inflection v1.0.0 // indirect
66
+
github.com/jinzhu/now v1.1.5 // indirect
59
67
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
68
+
github.com/labstack/echo-contrib v0.17.2 // indirect
69
+
github.com/labstack/echo/v4 v4.13.3 // indirect
70
+
github.com/labstack/gommon v0.4.2 // indirect
60
71
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
61
72
github.com/lestrrat-go/httpcc v1.0.1 // indirect
62
73
github.com/lestrrat-go/httprc v1.0.4 // indirect
···
64
75
github.com/lestrrat-go/option v1.0.1 // indirect
65
76
github.com/mattn/go-colorable v0.1.13 // indirect
66
77
github.com/mattn/go-isatty v0.0.20 // indirect
78
+
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
67
79
github.com/minio/sha256-simd v1.0.1 // indirect
68
80
github.com/mr-tron/base58 v1.2.0 // indirect
69
81
github.com/multiformats/go-base32 v0.1.0 // indirect
···
71
83
github.com/multiformats/go-multibase v0.2.0 // indirect
72
84
github.com/multiformats/go-multihash v0.2.3 // indirect
73
85
github.com/multiformats/go-varint v0.0.7 // indirect
86
+
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
74
87
github.com/opentracing/opentracing-go v1.2.0 // indirect
75
88
github.com/pelletier/go-toml v1.9.5 // indirect
76
89
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
77
90
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect
91
+
github.com/prometheus/client_golang v1.20.5 // indirect
92
+
github.com/prometheus/client_model v0.6.1 // indirect
93
+
github.com/prometheus/common v0.61.0 // indirect
94
+
github.com/prometheus/procfs v0.15.1 // indirect
78
95
github.com/russross/blackfriday/v2 v2.1.0 // indirect
79
96
github.com/sagikazarmark/locafero v0.7.0 // indirect
97
+
github.com/samber/lo v1.47.0 // indirect
98
+
github.com/samber/slog-echo v1.15.1 // indirect
80
99
github.com/segmentio/asm v1.2.0 // indirect
81
100
github.com/sourcegraph/conc v0.3.0 // indirect
82
101
github.com/spaolacci/murmur3 v1.1.0 // indirect
···
86
105
github.com/subosito/gotenv v1.6.0 // indirect
87
106
github.com/tdewolff/parse/v2 v2.7.15 // indirect
88
107
github.com/urfave/cli/v2 v2.27.5 // indirect
108
+
github.com/valyala/bytebufferpool v1.0.0 // indirect
109
+
github.com/valyala/fasttemplate v1.2.2 // indirect
89
110
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
111
+
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect
112
+
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect
90
113
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 // indirect
91
114
go.opentelemetry.io/otel v1.29.0 // indirect
92
115
go.opentelemetry.io/otel/metric v1.29.0 // indirect
···
96
119
go.uber.org/zap v1.26.0 // indirect
97
120
golang.org/x/crypto v0.32.0 // indirect
98
121
golang.org/x/mod v0.21.0 // indirect
122
+
golang.org/x/net v0.33.0 // indirect
99
123
golang.org/x/sys v0.29.0 // indirect
100
124
golang.org/x/text v0.21.0 // indirect
101
125
google.golang.org/protobuf v1.36.1 // indirect
102
126
gopkg.in/yaml.v3 v3.0.1 // indirect
127
+
gorm.io/driver/sqlite v1.5.7 // indirect
128
+
gorm.io/gorm v1.25.9 // indirect
103
129
lukechampine.com/blake3 v1.2.1 // indirect
104
130
)
105
131
+61
-4
go.sum
+61
-4
go.sum
···
11
11
github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c h1:651/eoCRnQ7YtSjAnSzRucrJz+3iGEFt+ysraELS81M=
12
12
github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
13
13
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
14
+
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
15
+
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
14
16
github.com/bep/clocks v0.5.0 h1:hhvKVGLPQWRVsBP/UB7ErrHYIO42gINVbvqxvYTPVps=
15
17
github.com/bep/clocks v0.5.0/go.mod h1:SUq3q+OOq41y2lRQqH5fsOoxN8GbxSiT6jvoVVLCVhU=
16
18
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
···
37
39
github.com/bep/overlayfs v0.9.2/go.mod h1:aYY9W7aXQsGcA7V9x/pzeR8LjEgIxbtisZm8Q7zPz40=
38
40
github.com/bep/tmc v0.5.1 h1:CsQnSC6MsomH64gw0cT5f+EwQDcvZz4AazKunFwTpuI=
39
41
github.com/bep/tmc v0.5.1/go.mod h1:tGYHN8fS85aJPhDLgXETVKp+PR382OvFi2+q2GkGsq0=
40
-
github.com/bluesky-social/indigo v0.0.0-20250506174012-7075cf22f63e h1:yEW1njmALj7i1AjLhq6Lsxts48JUCTT+wpM9m7GNLVY=
41
-
github.com/bluesky-social/indigo v0.0.0-20250506174012-7075cf22f63e/go.mod h1:ovyxp8AMO1Hoe838vMJUbqHTZaAR8ABM3g3TXu+A5Ng=
42
+
github.com/bluesky-social/indigo v0.0.0-20251003000214-3259b215110e h1:IutKPwmbU0LrYqw03EuwJtMdAe67rDTrL1U8S8dicRU=
43
+
github.com/bluesky-social/indigo v0.0.0-20251003000214-3259b215110e/go.mod h1:n6QE1NDPFoi7PRbMUZmc2y7FibCqiVU4ePpsvhHUBR8=
42
44
github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc=
43
45
github.com/carlmjohnson/versioninfo v0.22.5/go.mod h1:QT9mph3wcVfISUKd0i9sZfVrPviHuSF+cUtLjm2WSf8=
44
46
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
···
121
123
github.com/gohugoio/locales v0.14.0/go.mod h1:ip8cCAv/cnmVLzzXtiTpPwgJ4xhKZranqNqtoIu0b/4=
122
124
github.com/gohugoio/localescompressed v1.0.1 h1:KTYMi8fCWYLswFyJAeOtuk/EkXR/KPTHHNN9OS+RTxo=
123
125
github.com/gohugoio/localescompressed v1.0.1/go.mod h1:jBF6q8D7a0vaEmcWPNcAjUZLJaIVNiwvM3WlmTvooB0=
124
-
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
125
-
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
126
+
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
127
+
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
126
128
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
127
129
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
128
130
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
···
138
140
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
139
141
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
140
142
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
143
+
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
141
144
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
142
145
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
143
146
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
147
+
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
148
+
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
144
149
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
145
150
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
146
151
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
147
152
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
148
153
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
154
+
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
155
+
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
156
+
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
157
+
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
158
+
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
159
+
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
149
160
github.com/haileyok/atproto-oauth-golang v0.0.2 h1:61KPkLB615LQXR2f5x1v3sf6vPe6dOXqNpTYCgZ0Fz8=
150
161
github.com/haileyok/atproto-oauth-golang v0.0.2/go.mod h1:jcZ4GCjo5I5RuE/RsAXg1/b6udw7R4W+2rb/cGyTDK8=
151
162
github.com/hairyhenderson/go-codeowners v0.5.0 h1:dpQB+hVHiRc2VVvc2BHxkuM+tmu9Qej/as3apqUbsWc=
···
199
210
github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4=
200
211
github.com/jdkato/prose v1.2.1 h1:Fp3UnJmLVISmlc57BgKUzdjr0lOtjqTZicL3PaYy6cU=
201
212
github.com/jdkato/prose v1.2.1/go.mod h1:AiRHgVagnEx2JbQRQowVBKjG0bcs/vtkGCH1dYAL1rA=
213
+
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
214
+
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
215
+
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
216
+
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
202
217
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
203
218
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
204
219
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
···
221
236
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
222
237
github.com/kyokomi/emoji/v2 v2.2.13 h1:GhTfQa67venUUvmleTNFnb+bi7S3aocF7ZCXU9fSO7U=
223
238
github.com/kyokomi/emoji/v2 v2.2.13/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE=
239
+
github.com/labstack/echo-contrib v0.17.2 h1:K1zivqmtcC70X9VdBFdLomjPDEVHlrcAObqmuFj1c6w=
240
+
github.com/labstack/echo-contrib v0.17.2/go.mod h1:NeDh3PX7j/u+jR4iuDt1zHmWZSCz9c/p9mxXcDpyS8E=
241
+
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
242
+
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
243
+
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
244
+
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
224
245
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
225
246
github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
226
247
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
···
251
272
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
252
273
github.com/mattn/go-sqlite3 v1.14.27 h1:drZCnuvf37yPfs95E5jd9s3XhdVWLal+6BOK6qrv6IU=
253
274
github.com/mattn/go-sqlite3 v1.14.27/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
275
+
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
276
+
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
254
277
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
255
278
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
256
279
github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE=
···
273
296
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
274
297
github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8=
275
298
github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU=
299
+
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
300
+
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
276
301
github.com/niklasfasching/go-org v1.7.0 h1:vyMdcMWWTe/XmANk19F4k8XGBYg0GQ/gJGMimOjGMek=
277
302
github.com/niklasfasching/go-org v1.7.0/go.mod h1:WuVm4d45oePiE0eX25GqTDQIt/qPW1T9DGkRscqLW5o=
278
303
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
···
297
322
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
298
323
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0=
299
324
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw=
325
+
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
326
+
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
327
+
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
328
+
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
300
329
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
330
+
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
331
+
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
332
+
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
333
+
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
334
+
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
335
+
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
336
+
github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ=
337
+
github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s=
338
+
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
339
+
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
340
+
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
341
+
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
301
342
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
302
343
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
303
344
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
···
309
350
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
310
351
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
311
352
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
353
+
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
354
+
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
355
+
github.com/samber/slog-echo v1.15.1 h1:mzeQNPYPxmpehIRtgQJRgJMVvrRbZHp5D2maxSljTBw=
356
+
github.com/samber/slog-echo v1.15.1/go.mod h1:K21nbusPmai/MYm8PFactmZoFctkMmkeaTdXXyvhY1c=
312
357
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
313
358
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
314
359
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
···
356
401
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
357
402
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
358
403
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
404
+
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
405
+
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
406
+
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
407
+
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
359
408
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ=
360
409
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
361
410
github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11 h1:5HZfQkwe0mIfyDmc1Em5GqlNRzcdtlv4HTNmdpt7XH0=
···
372
421
github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
373
422
github.com/yuin/goldmark-emoji v1.0.3 h1:aLRkLHOuBR2czCY4R8olwMjID+tENfhyFDMCRhbIQY4=
374
423
github.com/yuin/goldmark-emoji v1.0.3/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
424
+
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA=
425
+
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8=
426
+
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q=
427
+
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I=
375
428
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk=
376
429
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8=
377
430
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
···
537
590
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
538
591
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
539
592
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
593
+
gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
594
+
gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
595
+
gorm.io/gorm v1.25.9 h1:wct0gxZIELDk8+ZqF/MVnHLkA1rvYlBWUMv2EdsK1g8=
596
+
gorm.io/gorm v1.25.9/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
540
597
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
541
598
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
542
599
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+7
-4
models/user.go
+7
-4
models/user.go
···
18
18
LastFMUsername *string
19
19
20
20
// atp info
21
-
ATProtoDID *string
22
-
ATProtoAccessToken *string
23
-
ATProtoRefreshToken *string
24
-
ATProtoTokenExpiry *time.Time
21
+
ATProtoDID *string
22
+
//This is meant to only be used by the automated music stamping service. If the user ever does an
23
+
//atproto action from the web ui use the atproto session id for the logged-in session
24
+
MostRecentAtProtoSessionID *string
25
+
//ATProtoAccessToken *string
26
+
//ATProtoRefreshToken *string
27
+
//ATProtoTokenExpiry *time.Time
25
28
26
29
CreatedAt time.Time
27
30
UpdatedAt time.Time
+104
-134
oauth/atproto/atproto.go
+104
-134
oauth/atproto/atproto.go
···
3
3
import (
4
4
"context"
5
5
"fmt"
6
+
7
+
"github.com/bluesky-social/indigo/atproto/auth/oauth"
8
+
_ "github.com/bluesky-social/indigo/atproto/auth/oauth"
9
+
"github.com/bluesky-social/indigo/atproto/client"
10
+
"github.com/bluesky-social/indigo/atproto/crypto"
11
+
"github.com/bluesky-social/indigo/atproto/syntax"
12
+
"github.com/teal-fm/piper/db"
13
+
14
+
"github.com/teal-fm/piper/session"
15
+
6
16
"log"
7
17
"net/http"
8
18
"net/url"
9
-
10
-
oauth "github.com/haileyok/atproto-oauth-golang"
11
-
"github.com/haileyok/atproto-oauth-golang/helpers"
12
-
"github.com/lestrrat-go/jwx/v2/jwk"
13
-
"github.com/teal-fm/piper/db"
14
-
"github.com/teal-fm/piper/models"
19
+
"os"
20
+
"slices"
15
21
)
16
22
17
23
type ATprotoAuthService struct {
18
-
client *oauth.Client
19
-
jwks jwk.Key
20
-
DB *db.DB
21
-
clientId string
22
-
callbackUrl string
23
-
xrpc *oauth.XrpcClient
24
+
clientApp *oauth.ClientApp
25
+
DB *db.DB
26
+
sessionManager *session.SessionManager
27
+
clientId string
28
+
callbackUrl string
29
+
logger *log.Logger
24
30
}
25
31
26
-
func NewATprotoAuthService(db *db.DB, jwks jwk.Key, clientId string, callbackUrl string) (*ATprotoAuthService, error) {
32
+
func NewATprotoAuthService(database *db.DB, sessionManager *session.SessionManager, clientSecretKey string, clientId string, callbackUrl string, clientSecretId string) (*ATprotoAuthService, error) {
27
33
fmt.Println(clientId, callbackUrl)
28
-
cli, err := oauth.NewClient(oauth.ClientArgs{
29
-
ClientJwk: jwks,
30
-
ClientId: clientId,
31
-
RedirectUri: callbackUrl,
32
-
})
34
+
35
+
scopes := []string{"atproto", "repo:fm.teal.alpha.feed.play", "repo:fm.teal.alpha.actor.status"}
36
+
37
+
var config oauth.ClientConfig
38
+
config = oauth.NewPublicConfig(clientId, callbackUrl, scopes)
39
+
40
+
priv, err := crypto.ParsePrivateMultibase(clientSecretKey)
33
41
if err != nil {
34
-
return nil, fmt.Errorf("failed to create atproto oauth client: %w", err)
42
+
return nil, err
35
43
}
44
+
if err := config.SetClientSecret(priv, clientSecretId); err != nil {
45
+
return nil, err
46
+
}
47
+
48
+
oauthClient := oauth.NewClientApp(&config, db.NewSqliteATProtoStore(database.DB))
49
+
50
+
logger := log.New(os.Stdout, "ATProto oauth: ", log.LstdFlags|log.Lmsgprefix)
51
+
36
52
svc := &ATprotoAuthService{
37
-
client: cli,
38
-
jwks: jwks,
39
-
callbackUrl: callbackUrl,
40
-
DB: db,
41
-
clientId: clientId,
53
+
clientApp: oauthClient,
54
+
callbackUrl: callbackUrl,
55
+
DB: database,
56
+
sessionManager: sessionManager,
57
+
clientId: clientId,
58
+
logger: logger,
42
59
}
43
-
svc.NewXrpcClient()
44
60
return svc, nil
45
61
}
46
62
47
-
func (a *ATprotoAuthService) GetATProtoClient() (*oauth.Client, error) {
48
-
if a.client != nil {
49
-
return a.client, nil
63
+
func (a *ATprotoAuthService) GetATProtoClient(accountDID string, sessionID string, ctx context.Context) (*client.APIClient, error) {
64
+
did, err := syntax.ParseDID(accountDID)
65
+
if err != nil {
66
+
return nil, err
50
67
}
51
68
52
-
if a.client == nil {
53
-
cli, err := oauth.NewClient(oauth.ClientArgs{
54
-
ClientJwk: a.jwks,
55
-
ClientId: a.clientId,
56
-
RedirectUri: a.callbackUrl,
57
-
})
58
-
if err != nil {
59
-
return nil, fmt.Errorf("failed to create atproto oauth client: %w", err)
60
-
}
61
-
a.client = cli
69
+
oauthSess, err := a.clientApp.ResumeSession(ctx, did, sessionID)
70
+
if err != nil {
71
+
return nil, err
62
72
}
63
73
64
-
return a.client, nil
65
-
}
74
+
return oauthSess.APIClient(), nil
66
75
67
-
func LoadJwks(jwksBytes []byte) (jwk.Key, error) {
68
-
key, err := helpers.ParseJWKFromBytes(jwksBytes)
69
-
if err != nil {
70
-
return nil, fmt.Errorf("failed to parse JWK from bytes: %w", err)
71
-
}
72
-
return key, nil
73
76
}
74
77
75
78
func (a *ATprotoAuthService) HandleLogin(w http.ResponseWriter, r *http.Request) {
76
79
handle := r.URL.Query().Get("handle")
77
80
if handle == "" {
78
-
log.Printf("ATProto Login Error: handle is required")
81
+
a.logger.Printf("ATProto Login Error: handle is required")
79
82
http.Error(w, "handle query parameter is required", http.StatusBadRequest)
80
83
return
81
84
}
82
-
83
-
authUrl, err := a.getLoginUrlAndSaveState(r.Context(), handle)
85
+
ctx := r.Context()
86
+
redirectURL, err := a.clientApp.StartAuthFlow(ctx, handle)
87
+
if err != nil {
88
+
http.Error(w, fmt.Sprintf("Error initiating login: %v", err), http.StatusInternalServerError)
89
+
}
90
+
authUrl, err := url.Parse(redirectURL)
84
91
if err != nil {
85
-
log.Printf("ATProto Login Error: Failed to get login URL for handle %s: %v", handle, err)
86
92
http.Error(w, fmt.Sprintf("Error initiating login: %v", err), http.StatusInternalServerError)
87
-
return
88
93
}
89
94
90
-
log.Printf("ATProto Login: Redirecting user %s to %s", handle, authUrl.String())
95
+
a.logger.Printf("ATProto Login: Redirecting user %s to %s", handle, authUrl.String())
91
96
http.Redirect(w, r, authUrl.String(), http.StatusFound)
92
97
}
93
98
94
-
func (a *ATprotoAuthService) getLoginUrlAndSaveState(ctx context.Context, handle string) (*url.URL, error) {
95
-
scope := "atproto transition:generic"
96
-
// resolve
97
-
ui, err := a.getUserInformation(ctx, handle)
98
-
if err != nil {
99
-
return nil, fmt.Errorf("failed to get user information for %s: %w", handle, err)
100
-
}
101
-
102
-
fmt.Println("user info: ", ui.AuthServer, ui.AuthService)
103
-
104
-
// create a dpop jwk for this session
105
-
k, err := helpers.GenerateKey(nil) // Generate ephemeral DPoP key for this flow
106
-
if err != nil {
107
-
return nil, fmt.Errorf("failed to generate DPoP key: %w", err)
108
-
}
99
+
func (a *ATprotoAuthService) HandleLogout(w http.ResponseWriter, r *http.Request) {
100
+
cookie, err := r.Cookie("session")
109
101
110
-
// Send PAR auth req
111
-
parResp, err := a.client.SendParAuthRequest(ctx, ui.AuthServer, ui.AuthMeta, ui.Handle, scope, k)
112
-
if err != nil {
113
-
return nil, fmt.Errorf("failed PAR request to %s: %w", ui.AuthServer, err)
114
-
}
102
+
if err == nil {
103
+
session, exists := a.sessionManager.GetSession(cookie.Value)
104
+
if !exists {
105
+
http.Redirect(w, r, "/", http.StatusSeeOther)
106
+
return
107
+
}
115
108
116
-
// Save state
117
-
data := &models.ATprotoAuthData{
118
-
State: parResp.State,
119
-
DID: ui.DID,
120
-
PDSUrl: ui.AuthService,
121
-
AuthServerIssuer: ui.AuthMeta.Issuer,
122
-
PKCEVerifier: parResp.PkceVerifier,
123
-
DPoPAuthServerNonce: parResp.DpopAuthserverNonce,
124
-
DPoPPrivateJWK: k,
125
-
}
109
+
dbUser, err := a.DB.GetUserByID(session.UserID)
110
+
if err != nil {
111
+
http.Redirect(w, r, "/", http.StatusSeeOther)
112
+
return
113
+
}
114
+
did, err := syntax.ParseDID(*dbUser.ATProtoDID)
126
115
127
-
// print data
128
-
fmt.Println(data)
116
+
if err != nil {
117
+
a.logger.Printf("Should not happen: %s", err)
118
+
a.sessionManager.ClearSessionCookie(w)
119
+
http.Redirect(w, r, "/", http.StatusSeeOther)
120
+
}
129
121
130
-
err = a.DB.SaveATprotoAuthData(data)
131
-
if err != nil {
132
-
return nil, fmt.Errorf("failed to save ATProto auth data for state %s: %w", parResp.State, err)
122
+
ctx := r.Context()
123
+
err = a.clientApp.Logout(ctx, did, session.ATProtoSessionID)
124
+
if err != nil {
125
+
a.logger.Printf("Error logging the user: %s out: %s", did, err)
126
+
}
127
+
a.sessionManager.DeleteSession(cookie.Value)
133
128
}
134
129
135
-
// Construct authorization URL using the request_uri from PAR response
136
-
authEndpointURL, err := url.Parse(ui.AuthMeta.AuthorizationEndpoint)
137
-
if err != nil {
138
-
return nil, fmt.Errorf("invalid authorization endpoint URL %s: %w", ui.AuthMeta.AuthorizationEndpoint, err)
139
-
}
140
-
q := authEndpointURL.Query()
141
-
q.Set("client_id", a.clientId)
142
-
q.Set("request_uri", parResp.RequestUri)
143
-
q.Set("state", parResp.State)
144
-
authEndpointURL.RawQuery = q.Encode()
130
+
a.sessionManager.ClearSessionCookie(w)
145
131
146
-
return authEndpointURL, nil
132
+
http.Redirect(w, r, "/", http.StatusSeeOther)
147
133
}
148
134
149
135
func (a *ATprotoAuthService) HandleCallback(w http.ResponseWriter, r *http.Request) (int64, error) {
150
-
state := r.URL.Query().Get("state")
151
-
code := r.URL.Query().Get("code")
152
-
issuer := r.URL.Query().Get("iss") // Issuer (auth base URL) is needed for token request
153
-
154
-
if state == "" || code == "" || issuer == "" {
155
-
errMsg := r.URL.Query().Get("error")
156
-
errDesc := r.URL.Query().Get("error_description")
157
-
log.Printf("ATProto Callback Error: Missing parameters. State: '%s', Code: '%s', Issuer: '%s'. Error: '%s', Description: '%s'", state, code, issuer, errMsg, errDesc)
158
-
http.Error(w, fmt.Sprintf("Authorization callback failed: %s (%s). Missing state, code, or issuer.", errMsg, errDesc), http.StatusBadRequest)
159
-
return 0, fmt.Errorf("missing state, code, or issuer")
160
-
}
136
+
ctx := r.Context()
161
137
162
-
// Retrieve saved data using state
163
-
data, err := a.DB.GetATprotoAuthData(state)
138
+
sessData, err := a.clientApp.ProcessCallback(ctx, r.URL.Query())
164
139
if err != nil {
165
-
log.Printf("ATProto Callback Error: Failed to retrieve auth data for state '%s': %v", state, err)
166
-
http.Error(w, "Invalid or expired state.", http.StatusBadRequest)
167
-
return 0, fmt.Errorf("invalid or expired state")
140
+
errMsg := fmt.Errorf("processing OAuth callback: %w", err)
141
+
http.Error(w, errMsg.Error(), http.StatusBadRequest)
142
+
return 0, errMsg
168
143
}
169
144
170
-
// Clean up the temporary auth data now that we've retrieved it
171
-
// defer a.DB.DeleteATprotoAuthData(state) // Consider adding deletion logic
172
-
// if issuers don't match, return an error
173
-
if data.AuthServerIssuer != issuer {
174
-
log.Printf("ATProto Callback Error: Issuer mismatch for state '%s', expected '%s', got '%s'", state, data.AuthServerIssuer, issuer)
175
-
http.Error(w, "Invalid or expired state.", http.StatusBadRequest)
176
-
return 0, fmt.Errorf("issuer mismatch")
145
+
// It's in the example repo and leaving for some debugging cause i've seen different scopes cause issues before
146
+
// so may be some nice debugging info to have
147
+
if !slices.Equal(sessData.Scopes, a.clientApp.Config.Scopes) {
148
+
a.logger.Printf("session auth scopes did not match those requested")
177
149
}
178
150
179
-
resp, err := a.client.InitialTokenRequest(r.Context(), code, issuer, data.PKCEVerifier, data.DPoPAuthServerNonce, data.DPoPPrivateJWK)
151
+
user, err := a.DB.FindOrCreateUserByDID(sessData.AccountDID.String())
180
152
if err != nil {
181
-
log.Printf("ATProto Callback Error: Failed initial token request for state '%s', issuer '%s': %v", state, issuer, err)
182
-
http.Error(w, fmt.Sprintf("Error exchanging code for token: %v", err), http.StatusInternalServerError)
183
-
return 0, fmt.Errorf("failed initial token request")
184
-
}
185
-
186
-
userID, err := a.DB.FindOrCreateUserByDID(data.DID)
187
-
if err != nil {
188
-
log.Printf("ATProto Callback Error: Failed to find or create user for DID %s: %v", data.DID, err)
153
+
a.logger.Printf("ATProto Callback Error: Failed to find or create user for DID %s: %v", sessData.AccountDID.String(), err)
189
154
http.Error(w, "Failed to process user information.", http.StatusInternalServerError)
190
155
return 0, fmt.Errorf("failed to find or create user")
191
156
}
192
157
193
-
err = a.DB.SaveATprotoSession(resp, data.AuthServerIssuer, data.DPoPPrivateJWK, data.PDSUrl)
158
+
//This is piper's session for manging piper, not atproto sessions
159
+
createdSession := a.sessionManager.CreateSession(user.ID, sessData.SessionID)
160
+
a.sessionManager.SetSessionCookie(w, createdSession)
161
+
a.logger.Printf("Created session for user %d via service atproto", user.ATProtoDID)
162
+
163
+
err = a.DB.SetLatestATProtoSessionId(sessData.AccountDID.String(), sessData.SessionID)
194
164
if err != nil {
195
-
log.Printf("ATProto Callback Error: Failed to save ATProto tokens for user %d (DID %s): %v", userID.ID, data.DID, err)
165
+
a.logger.Printf("Failed to set latest atproto session id for user %d: %v", user.ID, err)
196
166
}
197
167
198
-
log.Printf("ATProto Callback Success: User %d (DID: %s) authenticated.", userID.ID, data.DID)
199
-
return userID.ID, nil
168
+
a.logger.Printf("ATProto Callback Success: User %d (DID: %s) authenticated.", user.ID, user.ATProtoDID)
169
+
return user.ID, nil
200
170
}
+26
-30
oauth/atproto/http.go
+26
-30
oauth/atproto/http.go
···
4
4
import (
5
5
"encoding/json"
6
6
"fmt"
7
-
"log"
8
7
"net/http"
9
-
10
-
"github.com/haileyok/atproto-oauth-golang/helpers"
11
8
)
12
9
13
-
func (a *ATprotoAuthService) HandleJwks(w http.ResponseWriter, r *http.Request) {
14
-
pubKey, err := a.jwks.PublicKey()
15
-
if err != nil {
16
-
http.Error(w, fmt.Sprintf("Error getting public key from JWK: %v", err), http.StatusInternalServerError)
17
-
log.Printf("Error getting public key from JWK: %v", err)
18
-
return
19
-
}
10
+
func strPtr(raw string) *string {
11
+
return &raw
12
+
}
20
13
14
+
func (a *ATprotoAuthService) HandleJwks(w http.ResponseWriter, r *http.Request) {
21
15
w.Header().Set("Content-Type", "application/json")
22
-
if err := json.NewEncoder(w).Encode(helpers.CreateJwksResponseObject(pubKey)); err != nil {
23
-
log.Printf("Error encoding JWKS response: %v", err)
16
+
body := a.clientApp.Config.PublicJWKS()
17
+
if err := json.NewEncoder(w).Encode(body); err != nil {
18
+
http.Error(w, err.Error(), http.StatusInternalServerError)
19
+
return
24
20
}
25
21
}
26
22
27
23
func (a *ATprotoAuthService) HandleClientMetadata(w http.ResponseWriter, r *http.Request, serverUrlRoot, serverMetadataUrl, serverCallbackUrl string) {
28
-
metadata := map[string]any{
29
-
"client_id": serverMetadataUrl,
30
-
"client_name": "Piper Telekinesis",
31
-
"client_uri": serverUrlRoot,
32
-
"logo_uri": fmt.Sprintf("%s/logo.png", serverUrlRoot),
33
-
"tos_uri": fmt.Sprintf("%s/tos", serverUrlRoot),
34
-
"policy_url": fmt.Sprintf("%s/policy", serverUrlRoot),
35
-
"redirect_uris": []string{serverCallbackUrl},
36
-
"grant_types": []string{"authorization_code", "refresh_token"},
37
-
"response_types": []string{"code"},
38
-
"application_type": "web",
39
-
"dpop_bound_access_tokens": true,
40
-
"jwks_uri": fmt.Sprintf("%s/oauth/jwks.json", serverUrlRoot),
41
-
"scope": "atproto transition:generic",
42
-
"token_endpoint_auth_method": "private_key_jwt",
43
-
"token_endpoint_auth_signing_alg": "ES256",
24
+
25
+
meta := a.clientApp.Config.ClientMetadata()
26
+
if a.clientApp.Config.IsConfidential() {
27
+
meta.JWKSURI = strPtr(fmt.Sprintf("%s/oauth/jwks.json", serverUrlRoot))
44
28
}
29
+
meta.ClientName = strPtr("Piper Telekinesis")
30
+
meta.ClientURI = strPtr(serverUrlRoot)
31
+
32
+
// internal consistency check
33
+
if err := meta.Validate(a.clientApp.Config.ClientID); err != nil {
34
+
a.logger.Printf("validating client metadata", "err", err)
35
+
http.Error(w, err.Error(), http.StatusInternalServerError)
36
+
return
37
+
}
38
+
45
39
w.Header().Set("Content-Type", "application/json")
46
-
if err := json.NewEncoder(w).Encode(metadata); err != nil {
47
-
log.Printf("Error encoding client metadata: %v", err)
40
+
if err := json.NewEncoder(w).Encode(meta); err != nil {
41
+
http.Error(w, err.Error(), http.StatusInternalServerError)
42
+
return
48
43
}
44
+
49
45
}
+2
-55
oauth/atproto/resolve.go
+2
-55
oauth/atproto/resolve.go
···
12
12
"strings"
13
13
14
14
"github.com/bluesky-social/indigo/atproto/syntax"
15
-
oauth "github.com/haileyok/atproto-oauth-golang"
16
15
)
17
16
18
17
// user information struct
19
18
type UserInformation struct {
20
-
AuthService string `json:"authService"`
21
-
AuthServer string `json:"authServer"`
22
-
AuthMeta *oauth.OauthAuthorizationMetadata `json:"authMeta"`
19
+
AuthService string `json:"authService"`
20
+
AuthServer string `json:"authServer"`
23
21
// do NOT save the current handle permanently!
24
22
Handle string `json:"handle"`
25
23
DID string `json:"did"`
···
32
30
Type string `json:"type"`
33
31
ServiceEndpoint string `json:"serviceEndpoint"`
34
32
} `json:"service"`
35
-
}
36
-
37
-
func (a *ATprotoAuthService) getUserInformation(ctx context.Context, handleOrDid string) (*UserInformation, error) {
38
-
cli := a.client
39
-
40
-
// if we have a did skip this
41
-
did := handleOrDid
42
-
err := error(nil)
43
-
// technically checking SHOULD be more rigorous.
44
-
if !strings.HasPrefix(handleOrDid, "did:") {
45
-
did, err = resolveHandle(ctx, did)
46
-
if err != nil {
47
-
return nil, err
48
-
}
49
-
} else {
50
-
did = handleOrDid
51
-
}
52
-
53
-
doc, err := getIdentityDocument(ctx, did)
54
-
if err != nil {
55
-
return nil, err
56
-
}
57
-
58
-
service, err := getAtprotoPdsService(doc)
59
-
if err != nil {
60
-
return nil, err
61
-
}
62
-
63
-
authserver, err := cli.ResolvePdsAuthServer(ctx, service)
64
-
if err != nil {
65
-
return nil, err
66
-
}
67
-
68
-
authmeta, err := cli.FetchAuthServerMetadata(ctx, authserver)
69
-
if err != nil {
70
-
return nil, err
71
-
}
72
-
73
-
if len(doc.AlsoKnownAs) == 0 {
74
-
return nil, fmt.Errorf("alsoKnownAs is empty, couldn't acquire handle: %w", err)
75
-
76
-
}
77
-
handle := strings.Replace(doc.AlsoKnownAs[0], "at://", "", 1)
78
-
79
-
return &UserInformation{
80
-
AuthService: service,
81
-
AuthServer: authserver,
82
-
AuthMeta: authmeta,
83
-
Handle: handle,
84
-
DID: did,
85
-
}, nil
86
33
}
87
34
88
35
func resolveHandle(ctx context.Context, handle string) (string, error) {
-25
oauth/atproto/xrpc.go
-25
oauth/atproto/xrpc.go
···
1
-
package atproto
2
-
3
-
import (
4
-
"log/slog"
5
-
6
-
oauth "github.com/haileyok/atproto-oauth-golang"
7
-
)
8
-
9
-
func (atp *ATprotoAuthService) NewXrpcClient() {
10
-
atp.xrpc = &oauth.XrpcClient{
11
-
OnDpopPdsNonceChanged: func(did, newNonce string) {
12
-
_, err := atp.DB.Exec("UPDATE users SET atproto_pds_nonce = ? WHERE atproto_did = ?", newNonce, did)
13
-
if err != nil {
14
-
slog.Default().Error("error updating pds nonce", "err", err)
15
-
}
16
-
},
17
-
}
18
-
}
19
-
20
-
func (atp *ATprotoAuthService) GetXrpcClient() *oauth.XrpcClient {
21
-
if atp.xrpc == nil {
22
-
atp.NewXrpcClient()
23
-
}
24
-
return atp.xrpc
25
-
}
+5
oauth/oauth2.go
+5
oauth/oauth2.go
···
86
86
http.Redirect(w, r, authURL, http.StatusSeeOther)
87
87
}
88
88
89
+
func (o *OAuth2Service) HandleLogout(w http.ResponseWriter, r *http.Request) {
90
+
//TODO not implemented yet. not sure what the api call is for this package
91
+
http.Redirect(w, r, "/", http.StatusSeeOther)
92
+
}
93
+
89
94
func (o *OAuth2Service) HandleCallback(w http.ResponseWriter, r *http.Request) (int64, error) {
90
95
state := r.URL.Query().Get("state")
91
96
if state != o.state {
+22
-15
oauth/oauth_manager.go
+22
-15
oauth/oauth_manager.go
···
6
6
"log"
7
7
"net/http"
8
8
"sync"
9
-
10
-
"github.com/teal-fm/piper/session"
11
9
)
12
10
13
11
// manages multiple oauth client services
14
12
type OAuthServiceManager struct {
15
-
services map[string]AuthService
16
-
sessionManager *session.SessionManager
17
-
mu sync.RWMutex
18
-
logger *log.Logger
13
+
services map[string]AuthService
14
+
mu sync.RWMutex
15
+
logger *log.Logger
19
16
}
20
17
21
-
func NewOAuthServiceManager(sessionManager *session.SessionManager) *OAuthServiceManager {
18
+
func NewOAuthServiceManager() *OAuthServiceManager {
22
19
return &OAuthServiceManager{
23
-
services: make(map[string]AuthService),
24
-
sessionManager: sessionManager,
25
-
logger: log.New(log.Writer(), "oauth: ", log.LstdFlags|log.Lmsgprefix),
20
+
services: make(map[string]AuthService),
21
+
logger: log.New(log.Writer(), "oauth: ", log.LstdFlags|log.Lmsgprefix),
26
22
}
27
23
}
28
24
···
58
54
}
59
55
}
60
56
57
+
func (m *OAuthServiceManager) HandleLogout(serviceName string) http.HandlerFunc {
58
+
return func(w http.ResponseWriter, r *http.Request) {
59
+
m.mu.RLock()
60
+
service, exists := m.services[serviceName]
61
+
m.mu.RUnlock()
62
+
63
+
if exists {
64
+
service.HandleLogout(w, r)
65
+
return
66
+
}
67
+
68
+
m.logger.Printf("Auth service '%s' not found for login request", serviceName)
69
+
http.Error(w, fmt.Sprintf("Auth service '%s' not found", serviceName), http.StatusNotFound)
70
+
}
71
+
}
72
+
61
73
func (m *OAuthServiceManager) HandleCallback(serviceName string) http.HandlerFunc {
62
74
return func(w http.ResponseWriter, r *http.Request) {
63
75
m.mu.RLock()
···
81
93
}
82
94
83
95
if userID > 0 {
84
-
session := m.sessionManager.CreateSession(userID)
85
-
86
-
m.sessionManager.SetSessionCookie(w, session)
87
-
88
-
m.logger.Printf("Created session for user %d via service %s", userID, serviceName)
89
96
90
97
http.Redirect(w, r, "/", http.StatusSeeOther)
91
98
} else {
+2
oauth/service.go
+2
oauth/service.go
+6
-21
service/atproto/submission.go
+6
-21
service/atproto/submission.go
···
6
6
"log"
7
7
"time"
8
8
9
-
"github.com/bluesky-social/indigo/api/atproto"
9
+
comatproto "github.com/bluesky-social/indigo/api/atproto"
10
10
lexutil "github.com/bluesky-social/indigo/lex/util"
11
-
"github.com/bluesky-social/indigo/xrpc"
12
11
"github.com/spf13/viper"
13
12
"github.com/teal-fm/piper/api/teal"
14
-
"github.com/teal-fm/piper/db"
15
13
"github.com/teal-fm/piper/models"
16
14
atprotoauth "github.com/teal-fm/piper/oauth/atproto"
17
15
)
18
16
19
17
// SubmitPlayToPDS submits a track play to the ATProto PDS as a feed.play record
20
-
func SubmitPlayToPDS(ctx context.Context, did string, track *models.Track, atprotoService *atprotoauth.ATprotoAuthService) error {
18
+
func SubmitPlayToPDS(ctx context.Context, did string, mostRecentAtProtoSessionID string, track *models.Track, atprotoService *atprotoauth.ATprotoAuthService) error {
21
19
if did == "" {
22
20
return fmt.Errorf("DID cannot be empty")
23
21
}
24
22
25
23
// Get ATProto client
26
-
client, err := atprotoService.GetATProtoClient()
24
+
client, err := atprotoService.GetATProtoClient(did, mostRecentAtProtoSessionID, ctx)
27
25
if err != nil || client == nil {
28
26
return fmt.Errorf("failed to get ATProto client: %w", err)
29
27
}
30
28
31
-
xrpcClient := atprotoService.GetXrpcClient()
32
-
if xrpcClient == nil {
33
-
return fmt.Errorf("xrpc client is not available")
34
-
}
35
-
36
-
// Get user session
37
-
sess, err := atprotoService.DB.GetAtprotoSession(did, ctx, *client)
38
-
if err != nil {
39
-
return fmt.Errorf("couldn't get Atproto session for DID %s: %w", did, err)
40
-
}
41
-
42
29
// Convert track to feed.play record
43
30
playRecord, err := TrackToPlayRecord(track)
44
31
if err != nil {
···
46
33
}
47
34
48
35
// Create the record
49
-
input := atproto.RepoCreateRecord_Input{
36
+
input := comatproto.RepoCreateRecord_Input{
50
37
Collection: "fm.teal.alpha.feed.play",
51
-
Repo: sess.DID,
38
+
Repo: client.AccountDID.String(),
52
39
Record: &lexutil.LexiconTypeDecoder{Val: playRecord},
53
40
}
54
41
55
-
authArgs := db.AtpSessionToAuthArgs(sess)
56
-
var out atproto.RepoCreateRecord_Output
57
-
if err := xrpcClient.Do(ctx, authArgs, xrpc.Procedure, "application/json", "com.atproto.repo.createRecord", nil, input, &out); err != nil {
42
+
if _, err := comatproto.RepoCreateRecord(ctx, client, &input); err != nil {
58
43
return fmt.Errorf("failed to create play record for DID %s: %w", did, err)
59
44
}
60
45
+3
-3
service/lastfm/lastfm.go
+3
-3
service/lastfm/lastfm.go
···
390
390
}
391
391
l.db.SaveTrack(user.ID, hydratedTrack)
392
392
l.logger.Printf("Submitting track")
393
-
err = l.SubmitTrackToPDS(*user.ATProtoDID, hydratedTrack, ctx)
393
+
err = l.SubmitTrackToPDS(*user.ATProtoDID, *user.MostRecentAtProtoSessionID, hydratedTrack, ctx)
394
394
if err != nil {
395
395
l.logger.Printf("error submitting track for user %s: %s - %s: %v", username, track.Artist.Text, track.Name, err)
396
396
}
···
413
413
return nil
414
414
}
415
415
416
-
func (l *LastFMService) SubmitTrackToPDS(did string, track *models.Track, ctx context.Context) error {
416
+
func (l *LastFMService) SubmitTrackToPDS(did string, mostRecentAtProtoSessionID string, track *models.Track, ctx context.Context) error {
417
417
// Use shared atproto service for submission
418
-
return atprotoservice.SubmitPlayToPDS(ctx, did, track, l.atprotoService)
418
+
return atprotoservice.SubmitPlayToPDS(ctx, did, mostRecentAtProtoSessionID, track, l.atprotoService)
419
419
}
420
420
421
421
// convertLastFMTrackToModelsTrack converts a Last.fm Track to models.Track format
+31
-58
service/playingnow/playingnow.go
+31
-58
service/playingnow/playingnow.go
···
8
8
"strconv"
9
9
"time"
10
10
11
-
"github.com/bluesky-social/indigo/api/atproto"
11
+
"github.com/bluesky-social/indigo/atproto/client"
12
12
lexutil "github.com/bluesky-social/indigo/lex/util"
13
-
"github.com/bluesky-social/indigo/xrpc"
14
-
oauth "github.com/haileyok/atproto-oauth-golang"
15
13
"github.com/spf13/viper"
14
+
15
+
comatproto "github.com/bluesky-social/indigo/api/atproto"
16
16
"github.com/teal-fm/piper/api/teal"
17
17
"github.com/teal-fm/piper/db"
18
18
"github.com/teal-fm/piper/models"
···
52
52
53
53
did := *user.ATProtoDID
54
54
55
-
// Get ATProto client
56
-
client, err := p.atprotoService.GetATProtoClient()
57
-
if err != nil || client == nil {
58
-
return fmt.Errorf("failed to get ATProto client: %w", err)
59
-
}
60
-
61
-
xrpcClient := p.atprotoService.GetXrpcClient()
62
-
if xrpcClient == nil {
63
-
return fmt.Errorf("xrpc client is not available")
64
-
}
65
-
66
-
// Get user session
67
-
sess, err := p.db.GetAtprotoSession(did, ctx, *client)
68
-
if err != nil {
69
-
return fmt.Errorf("couldn't get Atproto session for DID %s: %w", did, err)
55
+
// Get ATProto atProtoClient
56
+
atProtoClient, err := p.atprotoService.GetATProtoClient(did, *user.MostRecentAtProtoSessionID, ctx)
57
+
if err != nil || atProtoClient == nil {
58
+
return fmt.Errorf("failed to get ATProto atProtoClient: %w", err)
70
59
}
71
60
72
61
// Convert track to PlayView format
···
86
75
Item: playView,
87
76
}
88
77
89
-
authArgs := db.AtpSessionToAuthArgs(sess)
90
78
var swapRecord *string
91
-
swapRecord, err = p.getStatusSwapRecord(ctx, xrpcClient, sess, authArgs)
79
+
swapRecord, err = p.getStatusSwapRecord(ctx, atProtoClient)
92
80
if err != nil {
93
81
return err
94
82
}
95
83
96
84
// Create the record input
97
-
input := atproto.RepoPutRecord_Input{
85
+
input := comatproto.RepoPutRecord_Input{
98
86
Collection: "fm.teal.alpha.actor.status",
99
-
Repo: sess.DID,
87
+
Repo: atProtoClient.AccountDID.String(),
100
88
Rkey: "self", // Use "self" as the record key for current status
101
89
Record: &lexutil.LexiconTypeDecoder{Val: status},
102
90
SwapRecord: swapRecord,
103
91
}
104
92
105
93
// Submit to PDS
106
-
var out atproto.RepoPutRecord_Output
107
-
if err := xrpcClient.Do(ctx, authArgs, xrpc.Procedure, "application/json", "com.atproto.repo.putRecord", nil, input, &out); err != nil {
94
+
if _, err := comatproto.RepoPutRecord(ctx, atProtoClient, &input); err != nil {
108
95
p.logger.Printf("Error creating playing now status for DID %s: %v", did, err)
109
96
return fmt.Errorf("failed to create playing now status for DID %s: %w", did, err)
110
97
}
···
131
118
did := *user.ATProtoDID
132
119
133
120
// Get ATProto clients
134
-
client, err := p.atprotoService.GetATProtoClient()
135
-
if err != nil || client == nil {
136
-
return fmt.Errorf("failed to get ATProto client: %w", err)
137
-
}
138
-
139
-
xrpcClient := p.atprotoService.GetXrpcClient()
140
-
if xrpcClient == nil {
141
-
return fmt.Errorf("xrpc client is not available")
142
-
}
143
-
144
-
// Get user session
145
-
sess, err := p.db.GetAtprotoSession(did, ctx, *client)
146
-
if err != nil {
147
-
return fmt.Errorf("couldn't get Atproto session for DID %s: %w", did, err)
121
+
atProtoClient, err := p.atprotoService.GetATProtoClient(did, *user.MostRecentAtProtoSessionID, ctx)
122
+
if err != nil || atProtoClient == nil {
123
+
return fmt.Errorf("failed to get ATProto atProtoClient: %w", err)
148
124
}
149
125
150
126
// Create an expired status (essentially clearing it)
···
164
140
Item: emptyPlayView,
165
141
}
166
142
167
-
authArgs := db.AtpSessionToAuthArgs(sess)
168
143
var swapRecord *string
169
-
swapRecord, err = p.getStatusSwapRecord(ctx, xrpcClient, sess, authArgs)
144
+
swapRecord, err = p.getStatusSwapRecord(ctx, atProtoClient)
170
145
if err != nil {
171
146
return err
172
147
}
173
148
174
149
// Update the record
175
-
input := atproto.RepoPutRecord_Input{
150
+
input := comatproto.RepoPutRecord_Input{
176
151
Collection: "fm.teal.alpha.actor.status",
177
-
Repo: sess.DID,
152
+
Repo: atProtoClient.AccountDID.String(),
178
153
Rkey: "self",
179
154
Record: &lexutil.LexiconTypeDecoder{Val: status},
180
155
SwapRecord: swapRecord,
181
156
}
182
157
183
-
var out atproto.RepoPutRecord_Output
184
-
if err := xrpcClient.Do(ctx, authArgs, xrpc.Procedure, "application/json", "com.atproto.repo.putRecord", nil, input, &out); err != nil {
158
+
if _, err := comatproto.RepoPutRecord(ctx, atProtoClient, &input); err != nil {
185
159
p.logger.Printf("Error clearing playing now status for DID %s: %v", did, err)
186
160
return fmt.Errorf("failed to clear playing now status for DID %s: %w", did, err)
187
161
}
···
242
216
// Get submission client agent
243
217
submissionAgent := viper.GetString("app.submission_agent")
244
218
if submissionAgent == "" {
245
-
submissionAgent = "piper/v0.0.1"
219
+
submissionAgent = "piper/v0.0.2"
246
220
}
247
221
248
222
playView := &teal.AlphaFeedDefs_PlayView{
···
264
238
265
239
// getStatusSwapRecord retrieves the current swap record (CID) for the actor status record.
266
240
// Returns (nil, nil) if the record does not exist yet.
267
-
func (p *PlayingNowService) getStatusSwapRecord(ctx context.Context, xrpcClient *oauth.XrpcClient, sess *models.ATprotoAuthSession, authArgs *oauth.XrpcAuthedRequestArgs) (*string, error) {
268
-
getOutput := atproto.RepoGetRecord_Output{}
269
-
if err := xrpcClient.Do(ctx, authArgs, xrpc.Query, "application/json", "com.atproto.repo.getRecord", map[string]any{
270
-
"repo": sess.DID,
271
-
"collection": "fm.teal.alpha.actor.status",
272
-
"rkey": "self",
273
-
}, nil, &getOutput); err != nil {
274
-
xErr, ok := err.(*xrpc.Error)
241
+
func (p *PlayingNowService) getStatusSwapRecord(ctx context.Context, atApiClient *client.APIClient) (*string, error) {
242
+
result, err := comatproto.RepoGetRecord(ctx, atApiClient, "", "fm.teal.alpha.actor.status", atApiClient.AccountDID.String(), "self")
243
+
244
+
if err != nil {
245
+
xErr, ok := err.(*client.APIError)
275
246
if !ok {
276
-
return nil, fmt.Errorf("could not get record: %w", err)
247
+
return nil, fmt.Errorf("error getting the record: %w", err)
277
248
}
278
-
if xErr.StatusCode != 400 { // 400 means not found in this API
279
-
return nil, fmt.Errorf("could not get record: %w", err)
249
+
if xErr.StatusCode == 400 { // 400 means not found in this API, which would be the case if the record does not exist yet
250
+
return nil, nil
280
251
}
281
-
return nil, nil
252
+
253
+
return nil, fmt.Errorf("error getting the record: %w", err)
254
+
282
255
}
283
-
return getOutput.Cid, nil
256
+
return result.Cid, nil
284
257
}
+3
-3
service/spotify/spotify.go
+3
-3
service/spotify/spotify.go
···
59
59
}
60
60
}
61
61
62
-
func (s *SpotifyService) SubmitTrackToPDS(did string, track *models.Track, ctx context.Context) error {
62
+
func (s *SpotifyService) SubmitTrackToPDS(did string, mostRecentAtProtoSessionID string, track *models.Track, ctx context.Context) error {
63
63
//Had a empty feed.play get submitted not sure why. Tracking here
64
64
if track.Name == "" {
65
65
s.logger.Println("Track name is empty. Skipping submission. Please record the logs before and send to the teal.fm Discord")
···
67
67
}
68
68
69
69
// Use shared atproto service for submission
70
-
return atprotoservice.SubmitPlayToPDS(ctx, did, track, s.atprotoService)
70
+
return atprotoservice.SubmitPlayToPDS(ctx, did, mostRecentAtProtoSessionID, track, s.atprotoService)
71
71
}
72
72
73
73
func (s *SpotifyService) SetAccessToken(token string, refreshToken string, userId int64, hasSession bool) (int64, error) {
···
657
657
658
658
s.logger.Printf("User %d (%d): Attempting to submit track '%s' by %s to PDS (DID: %s)", userID, dbUser.ATProtoDID, trackToSubmitToPDS.Name, artistName, *dbUser.ATProtoDID)
659
659
// Use context.Background() for now, or pass down a context if available
660
-
if errPDS := s.SubmitTrackToPDS(*dbUser.ATProtoDID, trackToSubmitToPDS, context.Background()); errPDS != nil {
660
+
if errPDS := s.SubmitTrackToPDS(*dbUser.ATProtoDID, *dbUser.MostRecentAtProtoSessionID, trackToSubmitToPDS, context.Background()); errPDS != nil {
661
661
s.logger.Printf("User %d (%d): Error submitting track '%s' to PDS: %v", userID, dbUser.ATProtoDID, trackToSubmitToPDS.Name, errPDS)
662
662
} else {
663
663
s.logger.Printf("User %d (%d): Successfully submitted track '%s' to PDS.", userID, dbUser.ATProtoDID, trackToSubmitToPDS.Name)
+22
-29
session/session.go
+22
-29
session/session.go
···
17
17
18
18
// session/session.go
19
19
type Session struct {
20
-
ID string
21
-
UserID int64
22
-
ATprotoDID string
23
-
ATprotoAccessToken string
24
-
ATprotoRefreshToken string
25
-
CreatedAt time.Time
26
-
ExpiresAt time.Time
20
+
21
+
//need to re work this. May add onto it for atproto oauth. But need to be careful about that expiresd
22
+
//Maybe a speerate oauth session store table and it has a created date? yeah do that then can look it up by session id from this table for user actions
23
+
24
+
ID string
25
+
UserID int64
26
+
ATProtoSessionID string
27
+
CreatedAt time.Time
28
+
ExpiresAt time.Time
27
29
}
28
30
29
31
type SessionManager struct {
···
38
40
_, err := database.Exec(`
39
41
CREATE TABLE IF NOT EXISTS sessions (
40
42
id TEXT PRIMARY KEY,
41
-
user_id INTEGER NOT NULL,
43
+
user_id INTEGER NOT NULL,
44
+
at_proto_session_id TEXT NOT NULL,
42
45
created_at TIMESTAMP,
43
46
expires_at TIMESTAMP,
44
47
FOREIGN KEY (user_id) REFERENCES users(id)
···
58
61
}
59
62
60
63
// create a new session for a user
61
-
func (sm *SessionManager) CreateSession(userID int64) *Session {
64
+
func (sm *SessionManager) CreateSession(userID int64, atProtoSessionId string) *Session {
62
65
sm.mu.Lock()
63
66
defer sm.mu.Unlock()
64
67
···
71
74
expiresAt := now.Add(24 * time.Hour) // 24-hour session
72
75
73
76
session := &Session{
74
-
ID: sessionID,
75
-
UserID: userID,
76
-
CreatedAt: now,
77
-
ExpiresAt: expiresAt,
77
+
ID: sessionID,
78
+
UserID: userID,
79
+
ATProtoSessionID: atProtoSessionId,
80
+
CreatedAt: now,
81
+
ExpiresAt: expiresAt,
78
82
}
79
83
80
84
// store session in memory
···
83
87
// store session in database if available
84
88
if sm.db != nil {
85
89
_, err := sm.db.Exec(`
86
-
INSERT INTO sessions (id, user_id, created_at, expires_at)
87
-
VALUES (?, ?, ?, ?)`,
88
-
sessionID, userID, now, expiresAt)
90
+
INSERT INTO sessions (id, user_id, at_proto_session_id, created_at, expires_at)
91
+
VALUES (?, ?, ?, ?, ?)`,
92
+
sessionID, userID, atProtoSessionId, now, expiresAt)
89
93
90
94
if err != nil {
91
95
log.Printf("Error storing session in database: %v", err)
···
116
120
session = &Session{ID: sessionID}
117
121
118
122
err := sm.db.QueryRow(`
119
-
SELECT user_id, created_at, expires_at
123
+
SELECT user_id, at_proto_session_id, created_at, expires_at
120
124
FROM sessions WHERE id = ?`, sessionID).Scan(
121
-
&session.UserID, &session.CreatedAt, &session.ExpiresAt)
125
+
&session.UserID, &session.ATProtoSessionID, &session.CreatedAt, &session.ExpiresAt)
122
126
123
127
if err != nil {
124
128
return nil, false
···
178
182
MaxAge: -1,
179
183
}
180
184
http.SetCookie(w, cookie)
181
-
}
182
-
183
-
func (sm *SessionManager) HandleLogout(w http.ResponseWriter, r *http.Request) {
184
-
cookie, err := r.Cookie("session")
185
-
if err == nil {
186
-
sm.DeleteSession(cookie.Value)
187
-
}
188
-
189
-
sm.ClearSessionCookie(w)
190
-
191
-
http.Redirect(w, r, "/", http.StatusSeeOther)
192
185
}
193
186
194
187
func (sm *SessionManager) GetAPIKeyManager() *apikey.ApiKeyManager {