tangled
alpha
login
or
join now
willdot.net
/
statusphere-go
20
fork
atom
An implementation of the ATProto statusphere example app but in Go
20
fork
atom
overview
issues
pulls
pipelines
create statusphere-go app
willdot.net
10 months ago
64ff4c69
+2472
18 changed files
expand all
collapse all
unified
split
.gitignore
auth_handlers.go
cmd
main.go
consumer.go
database
database.go
oauth_requests.go
oauth_sessions.go
profile.go
status.go
go.mod
go.sum
home_handler.go
html
app.css
home.html
login.html
oauth
service.go
server.go
status.go
+2
.gitignore
···
1
1
+
.env
2
2
+
database.db
+254
auth_handlers.go
···
1
1
+
package statusphere
2
2
+
3
3
+
import (
4
4
+
"crypto/sha256"
5
5
+
_ "embed"
6
6
+
"encoding/base64"
7
7
+
"encoding/json"
8
8
+
"fmt"
9
9
+
"log/slog"
10
10
+
"net/http"
11
11
+
"net/url"
12
12
+
"time"
13
13
+
14
14
+
"github.com/golang-jwt/jwt"
15
15
+
"github.com/google/uuid"
16
16
+
"github.com/gorilla/sessions"
17
17
+
"github.com/lestrrat-go/jwx/v2/jwk"
18
18
+
"github.com/willdot/statusphere-go/oauth"
19
19
+
)
20
20
+
21
21
+
type LoginData struct {
22
22
+
Handle string
23
23
+
Error string
24
24
+
}
25
25
+
26
26
+
func (s *Server) authMiddleware(next func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
27
27
+
return func(w http.ResponseWriter, r *http.Request) {
28
28
+
_, ok := s.getDidFromSession(r)
29
29
+
if !ok {
30
30
+
http.Redirect(w, r, "/login", http.StatusFound)
31
31
+
return
32
32
+
}
33
33
+
34
34
+
next(w, r)
35
35
+
}
36
36
+
}
37
37
+
38
38
+
func (s *Server) HandleLogin(w http.ResponseWriter, r *http.Request) {
39
39
+
tmpl := s.getTemplate("login.html")
40
40
+
data := LoginData{}
41
41
+
tmpl.Execute(w, data)
42
42
+
}
43
43
+
44
44
+
func (s *Server) HandlePostLogin(w http.ResponseWriter, r *http.Request) {
45
45
+
tmpl := s.getTemplate("login.html")
46
46
+
data := LoginData{}
47
47
+
48
48
+
err := r.ParseForm()
49
49
+
if err != nil {
50
50
+
slog.Error("parsing form", "error", err)
51
51
+
data.Error = "error parsing data"
52
52
+
tmpl.Execute(w, data)
53
53
+
return
54
54
+
}
55
55
+
56
56
+
handle := r.FormValue("handle")
57
57
+
58
58
+
result, err := s.oauthService.StartOAuthFlow(r.Context(), handle)
59
59
+
if err != nil {
60
60
+
slog.Error("starting oauth flow", "error", err)
61
61
+
data.Error = "error logging in"
62
62
+
tmpl.Execute(w, data)
63
63
+
return
64
64
+
}
65
65
+
66
66
+
u, _ := url.Parse(result.AuthorizationEndpoint)
67
67
+
u.RawQuery = fmt.Sprintf("client_id=%s&request_uri=%s", url.QueryEscape(fmt.Sprintf("%s/client-metadata.json", s.host)), result.RequestURI)
68
68
+
69
69
+
// ignore error here as it only returns an error for decoding an existing session but it will always return a session anyway which
70
70
+
// is what we want
71
71
+
session, _ := s.sessionStore.Get(r, "oauth-session")
72
72
+
session.Values = map[any]any{}
73
73
+
74
74
+
session.Options = &sessions.Options{
75
75
+
Path: "/",
76
76
+
MaxAge: 300, // save for five minutes
77
77
+
HttpOnly: true,
78
78
+
}
79
79
+
80
80
+
session.Values["oauth_state"] = result.State
81
81
+
session.Values["oauth_did"] = result.DID
82
82
+
83
83
+
err = session.Save(r, w)
84
84
+
if err != nil {
85
85
+
slog.Error("save session", "error", err)
86
86
+
data.Error = "error logging in"
87
87
+
tmpl.Execute(w, data)
88
88
+
return
89
89
+
}
90
90
+
91
91
+
http.Redirect(w, r, u.String(), http.StatusFound)
92
92
+
}
93
93
+
94
94
+
func (s *Server) handleOauthCallback(w http.ResponseWriter, r *http.Request) {
95
95
+
tmpl := s.getTemplate("login.html")
96
96
+
data := LoginData{}
97
97
+
98
98
+
resState := r.FormValue("state")
99
99
+
resIss := r.FormValue("iss")
100
100
+
resCode := r.FormValue("code")
101
101
+
102
102
+
session, err := s.sessionStore.Get(r, "oauth-session")
103
103
+
if err != nil {
104
104
+
slog.Error("getting session", "error", err)
105
105
+
data.Error = "error logging in"
106
106
+
tmpl.Execute(w, data)
107
107
+
return
108
108
+
}
109
109
+
110
110
+
if resState == "" || resIss == "" || resCode == "" {
111
111
+
slog.Error("request missing needed parameters")
112
112
+
data.Error = "error logging in"
113
113
+
tmpl.Execute(w, data)
114
114
+
return
115
115
+
}
116
116
+
117
117
+
sessionState, ok := session.Values["oauth_state"].(string)
118
118
+
if !ok {
119
119
+
slog.Error("oauth_state not found in sesssion")
120
120
+
data.Error = "error logging in"
121
121
+
tmpl.Execute(w, data)
122
122
+
return
123
123
+
}
124
124
+
125
125
+
if resState != sessionState {
126
126
+
slog.Error("session state does not match response state")
127
127
+
data.Error = "error logging in"
128
128
+
tmpl.Execute(w, data)
129
129
+
return
130
130
+
}
131
131
+
132
132
+
params := oauth.CallBackParams{
133
133
+
Iss: resIss,
134
134
+
State: resState,
135
135
+
Code: resCode,
136
136
+
}
137
137
+
usersDID, err := s.oauthService.OAuthCallback(r.Context(), params)
138
138
+
if err != nil {
139
139
+
slog.Error("handling oauth callback", "error", err)
140
140
+
data.Error = "error logging in"
141
141
+
tmpl.Execute(w, data)
142
142
+
return
143
143
+
}
144
144
+
145
145
+
session.Options = &sessions.Options{
146
146
+
Path: "/",
147
147
+
MaxAge: 86400 * 7,
148
148
+
HttpOnly: true,
149
149
+
}
150
150
+
151
151
+
// make sure the session is empty before setting new values
152
152
+
session.Values = map[any]any{}
153
153
+
session.Values["did"] = usersDID
154
154
+
155
155
+
err = session.Save(r, w)
156
156
+
if err != nil {
157
157
+
slog.Error("save session", "error", err)
158
158
+
data.Error = "error logging in"
159
159
+
tmpl.Execute(w, data)
160
160
+
return
161
161
+
}
162
162
+
163
163
+
http.Redirect(w, r, "/", http.StatusFound)
164
164
+
}
165
165
+
166
166
+
func (s *Server) HandleLogOut(w http.ResponseWriter, r *http.Request) {
167
167
+
session, err := s.sessionStore.Get(r, "oauth-session")
168
168
+
if err != nil {
169
169
+
slog.Error("getting session", "error", err)
170
170
+
http.Redirect(w, r, "/", http.StatusFound)
171
171
+
return
172
172
+
}
173
173
+
174
174
+
did, ok := session.Values["did"]
175
175
+
if ok {
176
176
+
err = s.oauthService.DeleteOAuthSession(fmt.Sprintf("%s", did))
177
177
+
if err != nil {
178
178
+
slog.Error("deleting oauth session", "error", err)
179
179
+
}
180
180
+
}
181
181
+
182
182
+
session.Values = map[any]any{}
183
183
+
session.Options = &sessions.Options{
184
184
+
Path: "/",
185
185
+
MaxAge: -1,
186
186
+
HttpOnly: true,
187
187
+
}
188
188
+
189
189
+
err = session.Save(r, w)
190
190
+
if err != nil {
191
191
+
slog.Error("save session", "error", err)
192
192
+
http.Redirect(w, r, "/", http.StatusFound)
193
193
+
return
194
194
+
}
195
195
+
196
196
+
http.Redirect(w, r, "/", http.StatusFound)
197
197
+
}
198
198
+
199
199
+
func pdsDpopJwt(method, url, iss, accessToken, nonce string, privateJwk jwk.Key) (string, error) {
200
200
+
pubJwk, err := privateJwk.PublicKey()
201
201
+
if err != nil {
202
202
+
return "", err
203
203
+
}
204
204
+
205
205
+
b, err := json.Marshal(pubJwk)
206
206
+
if err != nil {
207
207
+
return "", err
208
208
+
}
209
209
+
210
210
+
var pubMap map[string]any
211
211
+
if err := json.Unmarshal(b, &pubMap); err != nil {
212
212
+
return "", err
213
213
+
}
214
214
+
215
215
+
now := time.Now().Unix()
216
216
+
217
217
+
claims := jwt.MapClaims{
218
218
+
"iss": iss,
219
219
+
"iat": now,
220
220
+
"exp": now + 30,
221
221
+
"jti": uuid.NewString(),
222
222
+
"htm": method,
223
223
+
"htu": url,
224
224
+
"ath": generateCodeChallenge(accessToken),
225
225
+
}
226
226
+
227
227
+
if nonce != "" {
228
228
+
claims["nonce"] = nonce
229
229
+
}
230
230
+
231
231
+
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
232
232
+
token.Header["typ"] = "dpop+jwt"
233
233
+
token.Header["alg"] = "ES256"
234
234
+
token.Header["jwk"] = pubMap
235
235
+
236
236
+
var rawKey any
237
237
+
if err := privateJwk.Raw(&rawKey); err != nil {
238
238
+
return "", err
239
239
+
}
240
240
+
241
241
+
tokenString, err := token.SignedString(rawKey)
242
242
+
if err != nil {
243
243
+
return "", fmt.Errorf("failed to sign token: %w", err)
244
244
+
}
245
245
+
246
246
+
return tokenString, nil
247
247
+
}
248
248
+
249
249
+
func generateCodeChallenge(pkceVerifier string) string {
250
250
+
h := sha256.New()
251
251
+
h.Write([]byte(pkceVerifier))
252
252
+
hash := h.Sum(nil)
253
253
+
return base64.RawURLEncoding.EncodeToString(hash)
254
254
+
}
+113
cmd/main.go
···
1
1
+
package main
2
2
+
3
3
+
import (
4
4
+
"context"
5
5
+
"errors"
6
6
+
"log"
7
7
+
"log/slog"
8
8
+
"net/http"
9
9
+
"os"
10
10
+
"os/signal"
11
11
+
"path"
12
12
+
"syscall"
13
13
+
"time"
14
14
+
15
15
+
"github.com/avast/retry-go/v4"
16
16
+
"github.com/joho/godotenv"
17
17
+
"github.com/willdot/statusphere-go"
18
18
+
"github.com/willdot/statusphere-go/database"
19
19
+
"github.com/willdot/statusphere-go/oauth"
20
20
+
)
21
21
+
22
22
+
const (
23
23
+
defaultServerAddr = "wss://jetstream.atproto.tools/subscribe"
24
24
+
httpClientTimeoutDuration = time.Second * 5
25
25
+
transportIdleConnTimeoutDuration = time.Second * 90
26
26
+
)
27
27
+
28
28
+
func main() {
29
29
+
err := godotenv.Load(".env")
30
30
+
if err != nil {
31
31
+
if !os.IsNotExist(err) {
32
32
+
log.Fatal("Error loading .env file")
33
33
+
}
34
34
+
}
35
35
+
36
36
+
host := os.Getenv("HOST")
37
37
+
if host == "" {
38
38
+
slog.Error("missing HOST env variable")
39
39
+
return
40
40
+
}
41
41
+
42
42
+
dbMountPath := os.Getenv("DATABASE_MOUNT_PATH")
43
43
+
if dbMountPath == "" {
44
44
+
slog.Error("DATABASE_MOUNT_PATH env not set")
45
45
+
return
46
46
+
}
47
47
+
48
48
+
dbFilename := path.Join(dbMountPath, "database.db")
49
49
+
db, err := database.New(dbFilename)
50
50
+
if err != nil {
51
51
+
slog.Error("create new database", "error", err)
52
52
+
return
53
53
+
}
54
54
+
defer db.Close()
55
55
+
56
56
+
httpClient := &http.Client{
57
57
+
Timeout: httpClientTimeoutDuration,
58
58
+
Transport: &http.Transport{
59
59
+
IdleConnTimeout: transportIdleConnTimeoutDuration,
60
60
+
},
61
61
+
}
62
62
+
63
63
+
oauthService, err := oauth.NewService(db, host, httpClient)
64
64
+
if err != nil {
65
65
+
slog.Error("creating new oauth service", "error", err)
66
66
+
return
67
67
+
}
68
68
+
69
69
+
server, err := statusphere.NewServer(host, 8080, db, oauthService, httpClient)
70
70
+
if err != nil {
71
71
+
slog.Error("create new server", "error", err)
72
72
+
return
73
73
+
}
74
74
+
75
75
+
signals := make(chan os.Signal, 1)
76
76
+
signal.Notify(signals, syscall.SIGTERM, syscall.SIGINT)
77
77
+
78
78
+
ctx, cancel := context.WithCancel(context.Background())
79
79
+
defer cancel()
80
80
+
81
81
+
go func() {
82
82
+
<-signals
83
83
+
cancel()
84
84
+
_ = server.Stop(context.Background())
85
85
+
}()
86
86
+
87
87
+
go consumeLoop(ctx, db)
88
88
+
89
89
+
server.Run()
90
90
+
}
91
91
+
92
92
+
func consumeLoop(ctx context.Context, db *database.DB) {
93
93
+
jsServerAddr := os.Getenv("JS_SERVER_ADDR")
94
94
+
if jsServerAddr == "" {
95
95
+
jsServerAddr = defaultServerAddr
96
96
+
}
97
97
+
98
98
+
consumer := statusphere.NewConsumer(jsServerAddr, slog.Default(), db)
99
99
+
100
100
+
err := retry.Do(func() error {
101
101
+
err := consumer.Consume(ctx)
102
102
+
if err != nil {
103
103
+
if errors.Is(err, context.Canceled) {
104
104
+
return nil
105
105
+
}
106
106
+
slog.Error("consume loop", "error", err)
107
107
+
return err
108
108
+
}
109
109
+
return nil
110
110
+
}, retry.UntilSucceeded()) // retry indefinitly until context canceled
111
111
+
slog.Error(err.Error())
112
112
+
slog.Warn("exiting consume loop")
113
113
+
}
+108
consumer.go
···
1
1
+
package statusphere
2
2
+
3
3
+
import (
4
4
+
"context"
5
5
+
"encoding/json"
6
6
+
7
7
+
"fmt"
8
8
+
"log/slog"
9
9
+
"time"
10
10
+
11
11
+
"github.com/bluesky-social/jetstream/pkg/client"
12
12
+
"github.com/bluesky-social/jetstream/pkg/client/schedulers/sequential"
13
13
+
"github.com/bluesky-social/jetstream/pkg/models"
14
14
+
)
15
15
+
16
16
+
type consumer struct {
17
17
+
cfg *client.ClientConfig
18
18
+
handler handler
19
19
+
logger *slog.Logger
20
20
+
}
21
21
+
22
22
+
func NewConsumer(jsAddr string, logger *slog.Logger, store HandlerStore) *consumer {
23
23
+
cfg := client.DefaultClientConfig()
24
24
+
if jsAddr != "" {
25
25
+
cfg.WebsocketURL = jsAddr
26
26
+
}
27
27
+
cfg.WantedCollections = []string{
28
28
+
"xyz.statusphere.status",
29
29
+
}
30
30
+
cfg.WantedDids = []string{}
31
31
+
32
32
+
return &consumer{
33
33
+
cfg: cfg,
34
34
+
logger: logger,
35
35
+
handler: handler{
36
36
+
store: store,
37
37
+
},
38
38
+
}
39
39
+
}
40
40
+
41
41
+
func (c *consumer) Consume(ctx context.Context) error {
42
42
+
scheduler := sequential.NewScheduler("jetstream_localdev", c.logger, c.handler.HandleEvent)
43
43
+
defer scheduler.Shutdown()
44
44
+
45
45
+
client, err := client.NewClient(c.cfg, c.logger, scheduler)
46
46
+
if err != nil {
47
47
+
return fmt.Errorf("failed to create client: %w", err)
48
48
+
}
49
49
+
50
50
+
cursor := time.Now().Add(1 * -time.Minute).UnixMicro()
51
51
+
52
52
+
if err := client.ConnectAndRead(ctx, &cursor); err != nil {
53
53
+
return fmt.Errorf("connect and read: %w", err)
54
54
+
}
55
55
+
56
56
+
slog.Info("stopping consume")
57
57
+
return nil
58
58
+
}
59
59
+
60
60
+
type HandlerStore interface {
61
61
+
CreateStatus(status Status) error
62
62
+
}
63
63
+
64
64
+
type handler struct {
65
65
+
store HandlerStore
66
66
+
}
67
67
+
68
68
+
func (h *handler) HandleEvent(ctx context.Context, event *models.Event) error {
69
69
+
if event.Commit == nil {
70
70
+
return nil
71
71
+
}
72
72
+
73
73
+
switch event.Commit.Operation {
74
74
+
case models.CommitOperationCreate:
75
75
+
return h.handleCreateEvent(ctx, event)
76
76
+
default:
77
77
+
return nil
78
78
+
}
79
79
+
}
80
80
+
81
81
+
type StatusRecord struct {
82
82
+
Status string `json:"status"`
83
83
+
CreatedAt time.Time `json:"createdAt"`
84
84
+
}
85
85
+
86
86
+
func (h *handler) handleCreateEvent(_ context.Context, event *models.Event) error {
87
87
+
var statusRecord StatusRecord
88
88
+
if err := json.Unmarshal(event.Commit.Record, &statusRecord); err != nil {
89
89
+
slog.Error("unmarshal record", "error", err)
90
90
+
return nil
91
91
+
}
92
92
+
93
93
+
uri := fmt.Sprintf("at://%s/%s/%s", event.Did, event.Commit.Collection, event.Commit.RKey)
94
94
+
95
95
+
status := Status{
96
96
+
URI: uri,
97
97
+
Did: event.Did,
98
98
+
Status: statusRecord.Status,
99
99
+
CreatedAt: statusRecord.CreatedAt.UnixMilli(),
100
100
+
IndexedAt: time.Now().UnixMilli(),
101
101
+
}
102
102
+
err := h.store.CreateStatus(status)
103
103
+
if err != nil {
104
104
+
slog.Error("failed to store status", "error", err)
105
105
+
}
106
106
+
107
107
+
return nil
108
108
+
}
+76
database/database.go
···
1
1
+
package database
2
2
+
3
3
+
import (
4
4
+
"database/sql"
5
5
+
"errors"
6
6
+
"fmt"
7
7
+
"log/slog"
8
8
+
"os"
9
9
+
10
10
+
_ "github.com/glebarez/go-sqlite"
11
11
+
)
12
12
+
13
13
+
type DB struct {
14
14
+
db *sql.DB
15
15
+
}
16
16
+
17
17
+
func New(dbPath string) (*DB, error) {
18
18
+
if dbPath != ":memory:" {
19
19
+
err := createDbFile(dbPath)
20
20
+
if err != nil {
21
21
+
return nil, fmt.Errorf("create db file: %w", err)
22
22
+
}
23
23
+
}
24
24
+
25
25
+
db, err := sql.Open("sqlite", dbPath)
26
26
+
if err != nil {
27
27
+
return nil, fmt.Errorf("open database: %w", err)
28
28
+
}
29
29
+
30
30
+
err = db.Ping()
31
31
+
if err != nil {
32
32
+
return nil, fmt.Errorf("ping db: %w", err)
33
33
+
}
34
34
+
35
35
+
err = createOauthRequestsTable(db)
36
36
+
if err != nil {
37
37
+
return nil, fmt.Errorf("creating oauth requests table: %w", err)
38
38
+
}
39
39
+
40
40
+
err = createOauthSessionsTable(db)
41
41
+
if err != nil {
42
42
+
return nil, fmt.Errorf("creating oauth sessions table: %w", err)
43
43
+
}
44
44
+
45
45
+
err = createStatusTable(db)
46
46
+
if err != nil {
47
47
+
return nil, fmt.Errorf("creating status table: %w", err)
48
48
+
}
49
49
+
50
50
+
err = createProfileTable(db)
51
51
+
if err != nil {
52
52
+
return nil, fmt.Errorf("creating profile table: %w", err)
53
53
+
}
54
54
+
55
55
+
return &DB{db: db}, nil
56
56
+
}
57
57
+
58
58
+
func (d *DB) Close() {
59
59
+
err := d.db.Close()
60
60
+
if err != nil {
61
61
+
slog.Error("failed to close db", "error", err)
62
62
+
}
63
63
+
}
64
64
+
65
65
+
func createDbFile(dbFilename string) error {
66
66
+
if _, err := os.Stat(dbFilename); !errors.Is(err, os.ErrNotExist) {
67
67
+
return nil
68
68
+
}
69
69
+
70
70
+
f, err := os.Create(dbFilename)
71
71
+
if err != nil {
72
72
+
return fmt.Errorf("create db file : %w", err)
73
73
+
}
74
74
+
f.Close()
75
75
+
return nil
76
76
+
}
+74
database/oauth_requests.go
···
1
1
+
package database
2
2
+
3
3
+
import (
4
4
+
"database/sql"
5
5
+
"fmt"
6
6
+
"log/slog"
7
7
+
8
8
+
"github.com/willdot/statusphere-go/oauth"
9
9
+
)
10
10
+
11
11
+
func createOauthRequestsTable(db *sql.DB) error {
12
12
+
createOauthRequestsTableSQL := `CREATE TABLE IF NOT EXISTS oauthrequests (
13
13
+
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
14
14
+
"authserverIss" TEXT,
15
15
+
"state" TEXT,
16
16
+
"did" TEXT,
17
17
+
"pdsUrl" TEXT,
18
18
+
"pkceVerifier" TEXT,
19
19
+
"dpopAuthserverNonce" TEXT,
20
20
+
"dpopPrivateJwk" TEXT,
21
21
+
UNIQUE(did,state)
22
22
+
);`
23
23
+
24
24
+
slog.Info("Create oauthrequests table...")
25
25
+
statement, err := db.Prepare(createOauthRequestsTableSQL)
26
26
+
if err != nil {
27
27
+
return fmt.Errorf("prepare DB statement to create oauthrequests table: %w", err)
28
28
+
}
29
29
+
_, err = statement.Exec()
30
30
+
if err != nil {
31
31
+
return fmt.Errorf("exec sql statement to create oauthrequests table: %w", err)
32
32
+
}
33
33
+
slog.Info("oauthrequests table created")
34
34
+
35
35
+
return nil
36
36
+
}
37
37
+
38
38
+
func (d *DB) CreateOauthRequest(request oauth.Request) error {
39
39
+
sql := `INSERT INTO oauthrequests (authserverIss, state, did, pdsUrl, pkceVerifier, dpopAuthServerNonce, dpopPrivateJwk) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(did,state) DO NOTHING;`
40
40
+
_, err := d.db.Exec(sql, request.AuthserverIss, request.State, request.Did, request.PdsURL, request.PkceVerifier, request.DpopAuthserverNonce, request.DpopPrivateJwk)
41
41
+
if err != nil {
42
42
+
return fmt.Errorf("exec insert oauth request: %w", err)
43
43
+
}
44
44
+
45
45
+
return nil
46
46
+
}
47
47
+
48
48
+
func (d *DB) GetOauthRequest(state string) (oauth.Request, error) {
49
49
+
var oauthRequest oauth.Request
50
50
+
sql := "SELECT authserverIss, state, did, pdsUrl, pkceVerifier, dpopAuthServerNonce, dpopPrivateJwk FROM oauthrequests WHERE state = ?;"
51
51
+
rows, err := d.db.Query(sql, state)
52
52
+
if err != nil {
53
53
+
return oauthRequest, fmt.Errorf("run query to get oauth request: %w", err)
54
54
+
}
55
55
+
defer rows.Close()
56
56
+
57
57
+
for rows.Next() {
58
58
+
if err := rows.Scan(&oauthRequest.AuthserverIss, &oauthRequest.State, &oauthRequest.Did, &oauthRequest.PdsURL, &oauthRequest.PkceVerifier, &oauthRequest.DpopAuthserverNonce, &oauthRequest.DpopPrivateJwk); err != nil {
59
59
+
return oauthRequest, fmt.Errorf("scan row: %w", err)
60
60
+
}
61
61
+
62
62
+
return oauthRequest, nil
63
63
+
}
64
64
+
return oauthRequest, fmt.Errorf("not found")
65
65
+
}
66
66
+
67
67
+
func (d *DB) DeleteOauthRequest(state string) error {
68
68
+
sql := "DELETE FROM oauthrequests WHERE state = ?;"
69
69
+
_, err := d.db.Exec(sql, state)
70
70
+
if err != nil {
71
71
+
return fmt.Errorf("exec delete oauth request: %w", err)
72
72
+
}
73
73
+
return nil
74
74
+
}
+96
database/oauth_sessions.go
···
1
1
+
package database
2
2
+
3
3
+
import (
4
4
+
"database/sql"
5
5
+
"fmt"
6
6
+
"log/slog"
7
7
+
8
8
+
"github.com/willdot/statusphere-go/oauth"
9
9
+
)
10
10
+
11
11
+
func createOauthSessionsTable(db *sql.DB) error {
12
12
+
createOauthSessionsTableSQL := `CREATE TABLE IF NOT EXISTS oauthsessions (
13
13
+
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
14
14
+
"did" TEXT,
15
15
+
"pdsUrl" TEXT,
16
16
+
"authserverIss" TEXT,
17
17
+
"accessToken" TEXT,
18
18
+
"refreshToken" TEXT,
19
19
+
"dpopPdsNonce" TEXT,
20
20
+
"dpopAuthserverNonce" TEXT,
21
21
+
"dpopPrivateJwk" TEXT,
22
22
+
"expiration" integer,
23
23
+
UNIQUE(did)
24
24
+
);`
25
25
+
26
26
+
slog.Info("Create oauthsessions table...")
27
27
+
statement, err := db.Prepare(createOauthSessionsTableSQL)
28
28
+
if err != nil {
29
29
+
return fmt.Errorf("prepare DB statement to create oauthsessions table: %w", err)
30
30
+
}
31
31
+
_, err = statement.Exec()
32
32
+
if err != nil {
33
33
+
return fmt.Errorf("exec sql statement to create oauthsessions table: %w", err)
34
34
+
}
35
35
+
slog.Info("oauthsessions table created")
36
36
+
37
37
+
return nil
38
38
+
}
39
39
+
40
40
+
func (d *DB) CreateOauthSession(session oauth.Session) error {
41
41
+
sql := `INSERT INTO oauthsessions (did, pdsUrl, authserverIss, accessToken, refreshToken, dpopPdsNonce, dpopAuthserverNonce, dpopPrivateJwk, expiration) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(did) DO NOTHING;` // TODO: update on conflict
42
42
+
_, err := d.db.Exec(sql, session.Did, session.PdsUrl, session.AuthserverIss, session.AccessToken, session.RefreshToken, session.DpopPdsNonce, session.DpopAuthserverNonce, session.DpopPrivateJwk, session.Expiration)
43
43
+
if err != nil {
44
44
+
return fmt.Errorf("exec insert oauth session: %w", err)
45
45
+
}
46
46
+
47
47
+
return nil
48
48
+
}
49
49
+
50
50
+
func (d *DB) GetOauthSession(did string) (oauth.Session, error) {
51
51
+
var session oauth.Session
52
52
+
sql := "SELECT * FROM oauthsessions WHERE did = ?;"
53
53
+
rows, err := d.db.Query(sql, did)
54
54
+
if err != nil {
55
55
+
return session, fmt.Errorf("run query to get oauth session: %w", err)
56
56
+
}
57
57
+
defer rows.Close()
58
58
+
59
59
+
for rows.Next() {
60
60
+
if err := rows.Scan(&session.ID, &session.Did, &session.PdsUrl, &session.AuthserverIss, &session.AccessToken, &session.RefreshToken, &session.DpopPdsNonce, &session.DpopAuthserverNonce, &session.DpopPrivateJwk, &session.Expiration); err != nil {
61
61
+
return session, fmt.Errorf("scan row: %w", err)
62
62
+
}
63
63
+
64
64
+
return session, nil
65
65
+
}
66
66
+
return session, fmt.Errorf("not found")
67
67
+
}
68
68
+
69
69
+
func (d *DB) UpdateOauthSession(accessToken, refreshToken, dpopAuthServerNonce, did string, expiration int64) error {
70
70
+
sql := `UPDATE oauthsessions SET accessToken = ?, refreshToken = ?, dpopAuthserverNonce = ?, expiration = ? where did = ?`
71
71
+
_, err := d.db.Exec(sql, accessToken, refreshToken, dpopAuthServerNonce, expiration, did)
72
72
+
if err != nil {
73
73
+
return fmt.Errorf("exec update oauth session: %w", err)
74
74
+
}
75
75
+
76
76
+
return nil
77
77
+
}
78
78
+
79
79
+
func (d *DB) UpdateOauthSessionDpopPdsNonce(dpopPdsServerNonce, did string) error {
80
80
+
sql := `UPDATE oauthsessions SET dpopPdsNonce = ? where did = ?`
81
81
+
_, err := d.db.Exec(sql, dpopPdsServerNonce, did)
82
82
+
if err != nil {
83
83
+
return fmt.Errorf("exec update oauth session dpop pds nonce: %w", err)
84
84
+
}
85
85
+
86
86
+
return nil
87
87
+
}
88
88
+
89
89
+
func (d *DB) DeleteOauthSession(did string) error {
90
90
+
sql := "DELETE FROM oauthsessions WHERE did = ?;"
91
91
+
_, err := d.db.Exec(sql, did)
92
92
+
if err != nil {
93
93
+
return fmt.Errorf("exec delete oauth session: %w", err)
94
94
+
}
95
95
+
return nil
96
96
+
}
+59
database/profile.go
···
1
1
+
package database
2
2
+
3
3
+
import (
4
4
+
"database/sql"
5
5
+
"fmt"
6
6
+
"log/slog"
7
7
+
8
8
+
"github.com/willdot/statusphere-go"
9
9
+
)
10
10
+
11
11
+
func createProfileTable(db *sql.DB) error {
12
12
+
createProfileTableSQL := `CREATE TABLE IF NOT EXISTS profile (
13
13
+
"did" TEXT NOT NULL PRIMARY KEY,
14
14
+
"handle" TEXT,
15
15
+
"displayName" TEXT
16
16
+
);`
17
17
+
18
18
+
slog.Info("Create profile table...")
19
19
+
statement, err := db.Prepare(createProfileTableSQL)
20
20
+
if err != nil {
21
21
+
return fmt.Errorf("prepare DB statement to create profile table: %w", err)
22
22
+
}
23
23
+
_, err = statement.Exec()
24
24
+
if err != nil {
25
25
+
return fmt.Errorf("exec sql statement to create profile table: %w", err)
26
26
+
}
27
27
+
slog.Info("profile table created")
28
28
+
29
29
+
return nil
30
30
+
}
31
31
+
32
32
+
func (d *DB) CreateProfile(profile statusphere.UserProfile) error {
33
33
+
sql := `INSERT INTO profile (did, handle, displayName) VALUES (?, ?, ?) ON CONFLICT(did) DO NOTHING;` // TODO: What about when users change their handle or display name???
34
34
+
_, err := d.db.Exec(sql, profile.Did, profile.Handle, profile.DisplayName)
35
35
+
if err != nil {
36
36
+
return fmt.Errorf("exec insert profile: %w", err)
37
37
+
}
38
38
+
39
39
+
return nil
40
40
+
}
41
41
+
42
42
+
func (d *DB) GetHandleAndDisplayNameForDid(did string) (statusphere.UserProfile, error) {
43
43
+
sql := "SELECT did, handle, displayName FROM profile WHERE did = ?;"
44
44
+
rows, err := d.db.Query(sql, did)
45
45
+
if err != nil {
46
46
+
return statusphere.UserProfile{}, fmt.Errorf("run query to get profile': %w", err)
47
47
+
}
48
48
+
defer rows.Close()
49
49
+
50
50
+
var profile statusphere.UserProfile
51
51
+
for rows.Next() {
52
52
+
if err := rows.Scan(&profile.Did, &profile.Handle, &profile.DisplayName); err != nil {
53
53
+
return statusphere.UserProfile{}, fmt.Errorf("scan row: %w", err)
54
54
+
}
55
55
+
56
56
+
return profile, nil
57
57
+
}
58
58
+
return profile, statusphere.ErrorNotFound
59
59
+
}
+62
database/status.go
···
1
1
+
package database
2
2
+
3
3
+
import (
4
4
+
"database/sql"
5
5
+
"fmt"
6
6
+
"log/slog"
7
7
+
8
8
+
statusphere "github.com/willdot/statusphere-go"
9
9
+
)
10
10
+
11
11
+
func createStatusTable(db *sql.DB) error {
12
12
+
createStatusTableSQL := `CREATE TABLE IF NOT EXISTS status (
13
13
+
"uri" TEXT NOT NULL PRIMARY KEY,
14
14
+
"did" TEXT,
15
15
+
"status" TEXT,
16
16
+
"createdAt" integer,
17
17
+
"indexedAt" integer
18
18
+
);`
19
19
+
20
20
+
slog.Info("Create status table...")
21
21
+
statement, err := db.Prepare(createStatusTableSQL)
22
22
+
if err != nil {
23
23
+
return fmt.Errorf("prepare DB statement to create status table: %w", err)
24
24
+
}
25
25
+
_, err = statement.Exec()
26
26
+
if err != nil {
27
27
+
return fmt.Errorf("exec sql statement to create status table: %w", err)
28
28
+
}
29
29
+
slog.Info("status table created")
30
30
+
31
31
+
return nil
32
32
+
}
33
33
+
34
34
+
func (d *DB) CreateStatus(status statusphere.Status) error {
35
35
+
sql := `INSERT INTO status (uri, did, status, createdAt, indexedAt) VALUES (?, ?, ?, ?, ?) ON CONFLICT(uri) DO NOTHING;`
36
36
+
_, err := d.db.Exec(sql, status.URI, status.Did, status.Status, status.CreatedAt, status.IndexedAt)
37
37
+
if err != nil {
38
38
+
return fmt.Errorf("exec insert status: %w", err)
39
39
+
}
40
40
+
41
41
+
return nil
42
42
+
}
43
43
+
44
44
+
func (d *DB) GetStatuses(limit int) ([]statusphere.Status, error) {
45
45
+
sql := "SELECT uri, did, status, createdAt FROM status ORDER BY createdAt desc LIMIT ?;"
46
46
+
rows, err := d.db.Query(sql, limit)
47
47
+
if err != nil {
48
48
+
return nil, fmt.Errorf("run query to get status': %w", err)
49
49
+
}
50
50
+
defer rows.Close()
51
51
+
52
52
+
var results []statusphere.Status
53
53
+
for rows.Next() {
54
54
+
var status statusphere.Status
55
55
+
if err := rows.Scan(&status.URI, &status.Did, &status.Status, &status.CreatedAt); err != nil {
56
56
+
return nil, fmt.Errorf("scan row: %w", err)
57
57
+
}
58
58
+
59
59
+
results = append(results, status)
60
60
+
}
61
61
+
return results, nil
62
62
+
}
+92
go.mod
···
1
1
+
module github.com/willdot/statusphere-go
2
2
+
3
3
+
go 1.24.0
4
4
+
5
5
+
toolchain go1.24.2
6
6
+
7
7
+
require (
8
8
+
github.com/avast/retry-go/v4 v4.6.1
9
9
+
github.com/bluesky-social/jetstream v0.0.0-20250414024304-d17bd81a945e
10
10
+
github.com/glebarez/go-sqlite v1.22.0
11
11
+
github.com/golang-jwt/jwt v3.2.2+incompatible
12
12
+
github.com/google/uuid v1.6.0
13
13
+
github.com/gorilla/sessions v1.4.0
14
14
+
github.com/haileyok/atproto-oauth-golang v0.0.2
15
15
+
github.com/joho/godotenv v1.5.1
16
16
+
github.com/lestrrat-go/jwx/v2 v2.0.12
17
17
+
)
18
18
+
19
19
+
require (
20
20
+
github.com/beorn7/perks v1.0.1 // indirect
21
21
+
github.com/bluesky-social/indigo v0.0.0-20250301025210-a4e0cc37e188 // indirect
22
22
+
github.com/carlmjohnson/versioninfo v0.22.5 // indirect
23
23
+
github.com/cespare/xxhash/v2 v2.3.0 // indirect
24
24
+
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
25
25
+
github.com/dustin/go-humanize v1.0.1 // indirect
26
26
+
github.com/felixge/httpsnoop v1.0.4 // indirect
27
27
+
github.com/go-logr/logr v1.4.2 // indirect
28
28
+
github.com/go-logr/stdr v1.2.2 // indirect
29
29
+
github.com/goccy/go-json v0.10.3 // indirect
30
30
+
github.com/gogo/protobuf v1.3.2 // indirect
31
31
+
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
32
32
+
github.com/gorilla/securecookie v1.1.2 // indirect
33
33
+
github.com/gorilla/websocket v1.5.1 // indirect
34
34
+
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
35
35
+
github.com/hashicorp/go-retryablehttp v0.7.5 // indirect
36
36
+
github.com/hashicorp/golang-lru v1.0.2 // indirect
37
37
+
github.com/ipfs/bbloom v0.0.4 // indirect
38
38
+
github.com/ipfs/go-block-format v0.2.0 // indirect
39
39
+
github.com/ipfs/go-cid v0.4.1 // indirect
40
40
+
github.com/ipfs/go-datastore v0.6.0 // indirect
41
41
+
github.com/ipfs/go-ipfs-blockstore v1.3.1 // indirect
42
42
+
github.com/ipfs/go-ipfs-ds-help v1.1.1 // indirect
43
43
+
github.com/ipfs/go-ipfs-util v0.0.3 // indirect
44
44
+
github.com/ipfs/go-ipld-cbor v0.1.0 // indirect
45
45
+
github.com/ipfs/go-ipld-format v0.6.0 // indirect
46
46
+
github.com/ipfs/go-log v1.0.5 // indirect
47
47
+
github.com/ipfs/go-log/v2 v2.5.1 // indirect
48
48
+
github.com/ipfs/go-metrics-interface v0.0.1 // indirect
49
49
+
github.com/jbenet/goprocess v0.1.4 // indirect
50
50
+
github.com/klauspost/compress v1.17.9 // indirect
51
51
+
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
52
52
+
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
53
53
+
github.com/lestrrat-go/httpcc v1.0.1 // indirect
54
54
+
github.com/lestrrat-go/httprc v1.0.4 // indirect
55
55
+
github.com/lestrrat-go/iter v1.0.2 // indirect
56
56
+
github.com/lestrrat-go/option v1.0.1 // indirect
57
57
+
github.com/mattn/go-isatty v0.0.20 // indirect
58
58
+
github.com/minio/sha256-simd v1.0.1 // indirect
59
59
+
github.com/mr-tron/base58 v1.2.0 // indirect
60
60
+
github.com/multiformats/go-base32 v0.1.0 // indirect
61
61
+
github.com/multiformats/go-base36 v0.2.0 // indirect
62
62
+
github.com/multiformats/go-multibase v0.2.0 // indirect
63
63
+
github.com/multiformats/go-multihash v0.2.3 // indirect
64
64
+
github.com/multiformats/go-varint v0.0.7 // indirect
65
65
+
github.com/opentracing/opentracing-go v1.2.0 // indirect
66
66
+
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f // indirect
67
67
+
github.com/prometheus/client_golang v1.19.1 // indirect
68
68
+
github.com/prometheus/client_model v0.6.1 // indirect
69
69
+
github.com/prometheus/common v0.54.0 // indirect
70
70
+
github.com/prometheus/procfs v0.15.1 // indirect
71
71
+
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
72
72
+
github.com/segmentio/asm v1.2.0 // indirect
73
73
+
github.com/spaolacci/murmur3 v1.1.0 // indirect
74
74
+
github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e // indirect
75
75
+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect
76
76
+
go.opentelemetry.io/otel v1.29.0 // indirect
77
77
+
go.opentelemetry.io/otel/metric v1.29.0 // indirect
78
78
+
go.opentelemetry.io/otel/trace v1.29.0 // indirect
79
79
+
go.uber.org/atomic v1.11.0 // indirect
80
80
+
go.uber.org/multierr v1.11.0 // indirect
81
81
+
go.uber.org/zap v1.26.0 // indirect
82
82
+
golang.org/x/crypto v0.32.0 // indirect
83
83
+
golang.org/x/net v0.33.0 // indirect
84
84
+
golang.org/x/sys v0.29.0 // indirect
85
85
+
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
86
86
+
google.golang.org/protobuf v1.34.2 // indirect
87
87
+
lukechampine.com/blake3 v1.2.1 // indirect
88
88
+
modernc.org/libc v1.37.6 // indirect
89
89
+
modernc.org/mathutil v1.6.0 // indirect
90
90
+
modernc.org/memory v1.7.2 // indirect
91
91
+
modernc.org/sqlite v1.28.0 // indirect
92
92
+
)
+329
go.sum
···
1
1
+
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
2
2
+
github.com/avast/retry-go/v4 v4.6.1 h1:VkOLRubHdisGrHnTu89g08aQEWEgRU7LVEop3GbIcMk=
3
3
+
github.com/avast/retry-go/v4 v4.6.1/go.mod h1:V6oF8njAwxJ5gRo1Q7Cxab24xs5NCWZBeaHHBklR8mA=
4
4
+
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
5
5
+
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
6
6
+
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
7
7
+
github.com/bluesky-social/indigo v0.0.0-20250301025210-a4e0cc37e188 h1:1sQaG37xk08/rpmdhrmMkfQWF9kZbnfHm9Zav3bbSMk=
8
8
+
github.com/bluesky-social/indigo v0.0.0-20250301025210-a4e0cc37e188/go.mod h1:NVBwZvbBSa93kfyweAmKwOLYawdVHdwZ9s+GZtBBVLA=
9
9
+
github.com/bluesky-social/jetstream v0.0.0-20250414024304-d17bd81a945e h1:P/O6TDHs53gwgV845uDHI+Nri889ixksRrh4bCkCdxo=
10
10
+
github.com/bluesky-social/jetstream v0.0.0-20250414024304-d17bd81a945e/go.mod h1:WiYEeyJSdUwqoaZ71KJSpTblemUCpwJfh5oVXplK6T4=
11
11
+
github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc=
12
12
+
github.com/carlmjohnson/versioninfo v0.22.5/go.mod h1:QT9mph3wcVfISUKd0i9sZfVrPviHuSF+cUtLjm2WSf8=
13
13
+
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
14
14
+
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
15
15
+
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
16
16
+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
17
17
+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
18
18
+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
19
19
+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
20
20
+
github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
21
21
+
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
22
22
+
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
23
23
+
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
24
24
+
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
25
25
+
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
26
26
+
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
27
27
+
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
28
28
+
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
29
29
+
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
30
30
+
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
31
31
+
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
32
32
+
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
33
33
+
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
34
34
+
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
35
35
+
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
36
36
+
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
37
37
+
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
38
38
+
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
39
39
+
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
40
40
+
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
41
41
+
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
42
42
+
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
43
43
+
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
44
44
+
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
45
45
+
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
46
46
+
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
47
47
+
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
48
48
+
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
49
49
+
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
50
50
+
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
51
51
+
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
52
52
+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
53
53
+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
54
54
+
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
55
55
+
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
56
56
+
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
57
57
+
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
58
58
+
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
59
59
+
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
60
60
+
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
61
61
+
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
62
62
+
github.com/haileyok/atproto-oauth-golang v0.0.2 h1:61KPkLB615LQXR2f5x1v3sf6vPe6dOXqNpTYCgZ0Fz8=
63
63
+
github.com/haileyok/atproto-oauth-golang v0.0.2/go.mod h1:jcZ4GCjo5I5RuE/RsAXg1/b6udw7R4W+2rb/cGyTDK8=
64
64
+
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
65
65
+
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
66
66
+
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
67
67
+
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
68
68
+
github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M=
69
69
+
github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
70
70
+
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
71
71
+
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
72
72
+
github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs=
73
73
+
github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0=
74
74
+
github.com/ipfs/go-block-format v0.2.0 h1:ZqrkxBA2ICbDRbK8KJs/u0O3dlp6gmAuuXUJNiW1Ycs=
75
75
+
github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM=
76
76
+
github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s=
77
77
+
github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk=
78
78
+
github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk=
79
79
+
github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8=
80
80
+
github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk=
81
81
+
github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps=
82
82
+
github.com/ipfs/go-ipfs-blockstore v1.3.1 h1:cEI9ci7V0sRNivqaOr0elDsamxXFxJMMMy7PTTDQNsQ=
83
83
+
github.com/ipfs/go-ipfs-blockstore v1.3.1/go.mod h1:KgtZyc9fq+P2xJUiCAzbRdhhqJHvsw8u2Dlqy2MyRTE=
84
84
+
github.com/ipfs/go-ipfs-ds-help v1.1.1 h1:B5UJOH52IbcfS56+Ul+sv8jnIV10lbjLF5eOO0C66Nw=
85
85
+
github.com/ipfs/go-ipfs-ds-help v1.1.1/go.mod h1:75vrVCkSdSFidJscs8n4W+77AtTpCIAdDGAwjitJMIo=
86
86
+
github.com/ipfs/go-ipfs-util v0.0.3 h1:2RFdGez6bu2ZlZdI+rWfIdbQb1KudQp3VGwPtdNCmE0=
87
87
+
github.com/ipfs/go-ipfs-util v0.0.3/go.mod h1:LHzG1a0Ig4G+iZ26UUOMjHd+lfM84LZCrn17xAKWBvs=
88
88
+
github.com/ipfs/go-ipld-cbor v0.1.0 h1:dx0nS0kILVivGhfWuB6dUpMa/LAwElHPw1yOGYopoYs=
89
89
+
github.com/ipfs/go-ipld-cbor v0.1.0/go.mod h1:U2aYlmVrJr2wsUBU67K4KgepApSZddGRDWBYR0H4sCk=
90
90
+
github.com/ipfs/go-ipld-format v0.6.0 h1:VEJlA2kQ3LqFSIm5Vu6eIlSxD/Ze90xtc4Meten1F5U=
91
91
+
github.com/ipfs/go-ipld-format v0.6.0/go.mod h1:g4QVMTn3marU3qXchwjpKPKgJv+zF+OlaKMyhJ4LHPg=
92
92
+
github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8=
93
93
+
github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo=
94
94
+
github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g=
95
95
+
github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY=
96
96
+
github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI=
97
97
+
github.com/ipfs/go-metrics-interface v0.0.1 h1:j+cpbjYvu4R8zbleSs36gvB7jR+wsL2fGD6n0jO4kdg=
98
98
+
github.com/ipfs/go-metrics-interface v0.0.1/go.mod h1:6s6euYU4zowdslK0GKHmqaIZ3j/b/tL7HTWtJ4VPgWY=
99
99
+
github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA=
100
100
+
github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o=
101
101
+
github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4=
102
102
+
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
103
103
+
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
104
104
+
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
105
105
+
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
106
106
+
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
107
107
+
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
108
108
+
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
109
109
+
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
110
110
+
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
111
111
+
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
112
112
+
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
113
113
+
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
114
114
+
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
115
115
+
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
116
116
+
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
117
117
+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
118
118
+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
119
119
+
github.com/lestrrat-go/blackmagic v1.0.1/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
120
120
+
github.com/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k=
121
121
+
github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU=
122
122
+
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
123
123
+
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
124
124
+
github.com/lestrrat-go/httprc v1.0.4 h1:bAZymwoZQb+Oq8MEbyipag7iSq6YIga8Wj6GOiJGdI8=
125
125
+
github.com/lestrrat-go/httprc v1.0.4/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo=
126
126
+
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI=
127
127
+
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4=
128
128
+
github.com/lestrrat-go/jwx/v2 v2.0.12 h1:3d589+5w/b9b7S3DneICPW16AqTyYXB7VRjgluSDWeA=
129
129
+
github.com/lestrrat-go/jwx/v2 v2.0.12/go.mod h1:Mq4KN1mM7bp+5z/W5HS8aCNs5RKZ911G/0y2qUjAQuQ=
130
130
+
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
131
131
+
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
132
132
+
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
133
133
+
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
134
134
+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
135
135
+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
136
136
+
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
137
137
+
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
138
138
+
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
139
139
+
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
140
140
+
github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE=
141
141
+
github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI=
142
142
+
github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
143
143
+
github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4=
144
144
+
github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=
145
145
+
github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
146
146
+
github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
147
147
+
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
148
148
+
github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8=
149
149
+
github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU=
150
150
+
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
151
151
+
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
152
152
+
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
153
153
+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
154
154
+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
155
155
+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
156
156
+
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f h1:VXTQfuJj9vKR4TCkEuWIckKvdHFeJH/huIFJ9/cXOB0=
157
157
+
github.com/polydawn/refmt v0.89.1-0.20221221234430-40501e09de1f/go.mod h1:/zvteZs/GwLtCgZ4BL6CBsk9IKIlexP43ObX9AxTqTw=
158
158
+
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
159
159
+
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
160
160
+
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
161
161
+
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
162
162
+
github.com/prometheus/common v0.54.0 h1:ZlZy0BgJhTwVZUn7dLOkwCZHUkrAqd3WYtcFCWnM1D8=
163
163
+
github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ=
164
164
+
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
165
165
+
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
166
166
+
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
167
167
+
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
168
168
+
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
169
169
+
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
170
170
+
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
171
171
+
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
172
172
+
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
173
173
+
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
174
174
+
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
175
175
+
github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
176
176
+
github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
177
177
+
github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
178
178
+
github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
179
179
+
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
180
180
+
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
181
181
+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
182
182
+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
183
183
+
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
184
184
+
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
185
185
+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
186
186
+
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
187
187
+
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
188
188
+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
189
189
+
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
190
190
+
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
191
191
+
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
192
192
+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
193
193
+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
194
194
+
github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
195
195
+
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ=
196
196
+
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
197
197
+
github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e h1:28X54ciEwwUxyHn9yrZfl5ojgF4CBNLWX7LR0rvBkf4=
198
198
+
github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so=
199
199
+
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
200
200
+
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
201
201
+
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
202
202
+
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
203
203
+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24=
204
204
+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo=
205
205
+
go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw=
206
206
+
go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8=
207
207
+
go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc=
208
208
+
go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8=
209
209
+
go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4=
210
210
+
go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ=
211
211
+
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
212
212
+
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
213
213
+
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
214
214
+
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
215
215
+
go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
216
216
+
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
217
217
+
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
218
218
+
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
219
219
+
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
220
220
+
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
221
221
+
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
222
222
+
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
223
223
+
go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
224
224
+
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
225
225
+
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
226
226
+
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
227
227
+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
228
228
+
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
229
229
+
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
230
230
+
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
231
231
+
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
232
232
+
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
233
233
+
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
234
234
+
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
235
235
+
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
236
236
+
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
237
237
+
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
238
238
+
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
239
239
+
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
240
240
+
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
241
241
+
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
242
242
+
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
243
243
+
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
244
244
+
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
245
245
+
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
246
246
+
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
247
247
+
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
248
248
+
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
249
249
+
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
250
250
+
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
251
251
+
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
252
252
+
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
253
253
+
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
254
254
+
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
255
255
+
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
256
256
+
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
257
257
+
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
258
258
+
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
259
259
+
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
260
260
+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
261
261
+
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
262
262
+
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
263
263
+
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
264
264
+
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
265
265
+
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
266
266
+
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
267
267
+
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
268
268
+
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
269
269
+
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
270
270
+
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
271
271
+
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
272
272
+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
273
273
+
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
274
274
+
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
275
275
+
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
276
276
+
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
277
277
+
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
278
278
+
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
279
279
+
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
280
280
+
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
281
281
+
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
282
282
+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
283
283
+
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
284
284
+
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
285
285
+
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
286
286
+
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
287
287
+
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
288
288
+
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
289
289
+
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
290
290
+
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
291
291
+
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
292
292
+
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
293
293
+
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
294
294
+
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
295
295
+
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
296
296
+
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
297
297
+
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
298
298
+
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
299
299
+
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
300
300
+
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
301
301
+
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
302
302
+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
303
303
+
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
304
304
+
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
305
305
+
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
306
306
+
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
307
307
+
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
308
308
+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
309
309
+
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
310
310
+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
311
311
+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
312
312
+
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
313
313
+
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
314
314
+
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
315
315
+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
316
316
+
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
317
317
+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
318
318
+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
319
319
+
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
320
320
+
lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI=
321
321
+
lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
322
322
+
modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw=
323
323
+
modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
324
324
+
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
325
325
+
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
326
326
+
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
327
327
+
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
328
328
+
modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
329
329
+
modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
+143
home_handler.go
···
1
1
+
package statusphere
2
2
+
3
3
+
import (
4
4
+
"fmt"
5
5
+
"log/slog"
6
6
+
"net/http"
7
7
+
"time"
8
8
+
)
9
9
+
10
10
+
var Availablestatus = []string{
11
11
+
"👍",
12
12
+
"👎",
13
13
+
"💙",
14
14
+
"🥹",
15
15
+
"😧",
16
16
+
"😤",
17
17
+
"🙃",
18
18
+
"😉",
19
19
+
"😎",
20
20
+
"🤓",
21
21
+
"🤨",
22
22
+
"🥳",
23
23
+
"😭",
24
24
+
"😤",
25
25
+
"🤯",
26
26
+
"🫡",
27
27
+
"💀",
28
28
+
"✊",
29
29
+
"🤘",
30
30
+
"👀",
31
31
+
"🧠",
32
32
+
"👩💻",
33
33
+
"🧑💻",
34
34
+
"🥷",
35
35
+
"🧌",
36
36
+
"🦋",
37
37
+
"🚀",
38
38
+
}
39
39
+
40
40
+
type HomeData struct {
41
41
+
DisplayName string
42
42
+
AvailableStatus []string
43
43
+
UsersStatus []UserStatus
44
44
+
}
45
45
+
46
46
+
type UserStatus struct {
47
47
+
Status string
48
48
+
Handle string
49
49
+
HandleURL string
50
50
+
Date string
51
51
+
IsToday bool
52
52
+
}
53
53
+
54
54
+
func (s *Server) HandleHome(w http.ResponseWriter, r *http.Request) {
55
55
+
tmpl := s.getTemplate("home.html")
56
56
+
data := HomeData{
57
57
+
AvailableStatus: Availablestatus,
58
58
+
}
59
59
+
usersDid, ok := s.getDidFromSession(r)
60
60
+
if ok {
61
61
+
profile, err := s.getUserProfileForDid(usersDid)
62
62
+
if err != nil {
63
63
+
slog.Error("getting logged in users profile", "error", err)
64
64
+
}
65
65
+
data.DisplayName = profile.DisplayName
66
66
+
}
67
67
+
68
68
+
today := time.Now().Format(time.DateOnly)
69
69
+
70
70
+
results, err := s.store.GetStatuses(10)
71
71
+
if err != nil {
72
72
+
slog.Error("get status'", "error", err)
73
73
+
}
74
74
+
75
75
+
for _, status := range results {
76
76
+
date := time.UnixMilli(status.CreatedAt).Format(time.DateOnly)
77
77
+
78
78
+
profile, err := s.getUserProfileForDid(status.Did)
79
79
+
if err != nil {
80
80
+
slog.Error("getting user profile for status - skipping", "error", err, "did", status.Did)
81
81
+
continue
82
82
+
}
83
83
+
84
84
+
data.UsersStatus = append(data.UsersStatus, UserStatus{
85
85
+
Status: status.Status,
86
86
+
Handle: profile.Handle,
87
87
+
HandleURL: fmt.Sprintf("https://bsky.app/profile/%s", status.Did),
88
88
+
Date: date,
89
89
+
IsToday: date == today,
90
90
+
})
91
91
+
}
92
92
+
93
93
+
tmpl.Execute(w, data)
94
94
+
}
95
95
+
96
96
+
func (s *Server) HandleStatus(w http.ResponseWriter, r *http.Request) {
97
97
+
err := r.ParseForm()
98
98
+
if err != nil {
99
99
+
slog.Error("parsing form", "error", err)
100
100
+
http.Error(w, "parsing form", http.StatusBadRequest)
101
101
+
return
102
102
+
}
103
103
+
104
104
+
status := r.FormValue("status")
105
105
+
if status == "" {
106
106
+
http.Error(w, "missing status", http.StatusBadRequest)
107
107
+
return
108
108
+
}
109
109
+
110
110
+
did, ok := s.getDidFromSession(r)
111
111
+
if !ok {
112
112
+
http.Error(w, "failed to get did from session", http.StatusBadRequest)
113
113
+
return
114
114
+
}
115
115
+
116
116
+
oauthSession, err := s.oauthService.GetOauthSession(r.Context(), did)
117
117
+
if err != nil {
118
118
+
http.Error(w, "failed to get oauth session", http.StatusInternalServerError)
119
119
+
return
120
120
+
}
121
121
+
122
122
+
createdAt := time.Now()
123
123
+
uri, err := s.CreateNewStatus(r.Context(), oauthSession, status, createdAt)
124
124
+
if err != nil {
125
125
+
slog.Error("failed to create new status", "error", err)
126
126
+
}
127
127
+
128
128
+
if uri != "" {
129
129
+
statusToStore := Status{
130
130
+
URI: uri,
131
131
+
Did: did,
132
132
+
Status: status,
133
133
+
CreatedAt: createdAt.UnixMilli(),
134
134
+
IndexedAt: time.Now().UnixMilli(),
135
135
+
}
136
136
+
err = s.store.CreateStatus(statusToStore)
137
137
+
if err != nil {
138
138
+
slog.Error("failed to store status that has been created", "error", err)
139
139
+
}
140
140
+
}
141
141
+
142
142
+
http.Redirect(w, r, "/", http.StatusFound)
143
143
+
}
+230
html/app.css
···
1
1
+
body {
2
2
+
font-family: Arial, Helvetica, sans-serif;
3
3
+
4
4
+
--border-color: #ddd;
5
5
+
--gray-100: #fafafa;
6
6
+
--gray-500: #666;
7
7
+
--gray-700: #333;
8
8
+
--primary-100: #d2e7ff;
9
9
+
--primary-200: #b1d3fa;
10
10
+
--primary-400: #2e8fff;
11
11
+
--primary-500: #0078ff;
12
12
+
--primary-600: #0066db;
13
13
+
--error-500: #f00;
14
14
+
--error-100: #fee;
15
15
+
}
16
16
+
17
17
+
/*
18
18
+
Josh's Custom CSS Reset
19
19
+
https://www.joshwcomeau.com/css/custom-css-reset/
20
20
+
*/
21
21
+
*,
22
22
+
*::before,
23
23
+
*::after {
24
24
+
box-sizing: border-box;
25
25
+
}
26
26
+
* {
27
27
+
margin: 0;
28
28
+
}
29
29
+
body {
30
30
+
line-height: 1.5;
31
31
+
-webkit-font-smoothing: antialiased;
32
32
+
}
33
33
+
img,
34
34
+
picture,
35
35
+
video,
36
36
+
canvas,
37
37
+
svg {
38
38
+
display: block;
39
39
+
max-width: 100%;
40
40
+
}
41
41
+
input,
42
42
+
button,
43
43
+
textarea,
44
44
+
select {
45
45
+
font: inherit;
46
46
+
}
47
47
+
p,
48
48
+
h1,
49
49
+
h2,
50
50
+
h3,
51
51
+
h4,
52
52
+
h5,
53
53
+
h6 {
54
54
+
overflow-wrap: break-word;
55
55
+
}
56
56
+
#root,
57
57
+
#__next {
58
58
+
isolation: isolate;
59
59
+
}
60
60
+
61
61
+
/*
62
62
+
Common components
63
63
+
*/
64
64
+
button,
65
65
+
.button {
66
66
+
display: inline-block;
67
67
+
border: 0;
68
68
+
background-color: var(--primary-500);
69
69
+
border-radius: 50px;
70
70
+
color: #fff;
71
71
+
padding: 2px 10px;
72
72
+
cursor: pointer;
73
73
+
text-decoration: none;
74
74
+
}
75
75
+
button:hover,
76
76
+
.button:hover {
77
77
+
background: var(--primary-400);
78
78
+
}
79
79
+
80
80
+
/*
81
81
+
Custom components
82
82
+
*/
83
83
+
.error {
84
84
+
background-color: var(--error-100);
85
85
+
color: var(--error-500);
86
86
+
text-align: center;
87
87
+
padding: 1rem;
88
88
+
display: none;
89
89
+
}
90
90
+
.error.visible {
91
91
+
display: block;
92
92
+
}
93
93
+
94
94
+
#header {
95
95
+
background-color: #fff;
96
96
+
text-align: center;
97
97
+
padding: 0.5rem 0 1.5rem;
98
98
+
}
99
99
+
100
100
+
#header h1 {
101
101
+
font-size: 5rem;
102
102
+
}
103
103
+
104
104
+
.container {
105
105
+
display: flex;
106
106
+
flex-direction: column;
107
107
+
gap: 4px;
108
108
+
margin: 0 auto;
109
109
+
max-width: 600px;
110
110
+
padding: 20px;
111
111
+
}
112
112
+
113
113
+
.card {
114
114
+
/* border: 1px solid var(--border-color); */
115
115
+
border-radius: 6px;
116
116
+
padding: 10px 16px;
117
117
+
background-color: #fff;
118
118
+
}
119
119
+
.card > :first-child {
120
120
+
margin-top: 0;
121
121
+
}
122
122
+
.card > :last-child {
123
123
+
margin-bottom: 0;
124
124
+
}
125
125
+
126
126
+
.session-form {
127
127
+
display: flex;
128
128
+
flex-direction: row;
129
129
+
align-items: center;
130
130
+
justify-content: space-between;
131
131
+
}
132
132
+
133
133
+
.login-form {
134
134
+
display: flex;
135
135
+
flex-direction: row;
136
136
+
gap: 6px;
137
137
+
border: 1px solid var(--border-color);
138
138
+
border-radius: 6px;
139
139
+
padding: 10px 16px;
140
140
+
background-color: #fff;
141
141
+
}
142
142
+
143
143
+
.login-form input {
144
144
+
flex: 1;
145
145
+
border: 0;
146
146
+
}
147
147
+
148
148
+
.status-options {
149
149
+
display: flex;
150
150
+
flex-direction: row;
151
151
+
flex-wrap: wrap;
152
152
+
gap: 8px;
153
153
+
margin: 10px 0;
154
154
+
}
155
155
+
156
156
+
.status-option {
157
157
+
font-size: 2rem;
158
158
+
width: 3rem;
159
159
+
height: 3rem;
160
160
+
padding: 0;
161
161
+
background-color: #fff;
162
162
+
border: 1px solid var(--border-color);
163
163
+
border-radius: 3rem;
164
164
+
text-align: center;
165
165
+
box-shadow: 0 1px 4px #0001;
166
166
+
cursor: pointer;
167
167
+
}
168
168
+
169
169
+
.status-option:hover {
170
170
+
background-color: var(--primary-100);
171
171
+
box-shadow: 0 0 0 1px var(--primary-400);
172
172
+
}
173
173
+
174
174
+
.status-option.selected {
175
175
+
box-shadow: 0 0 0 1px var(--primary-500);
176
176
+
background-color: var(--primary-100);
177
177
+
}
178
178
+
179
179
+
.status-option.selected:hover {
180
180
+
background-color: var(--primary-200);
181
181
+
}
182
182
+
183
183
+
.status-line {
184
184
+
display: flex;
185
185
+
flex-direction: row;
186
186
+
align-items: center;
187
187
+
gap: 10px;
188
188
+
position: relative;
189
189
+
margin-top: 15px;
190
190
+
}
191
191
+
192
192
+
.status-line:not(.no-line)::before {
193
193
+
content: "";
194
194
+
position: absolute;
195
195
+
width: 2px;
196
196
+
background-color: var(--border-color);
197
197
+
left: 1.45rem;
198
198
+
bottom: calc(100% + 2px);
199
199
+
height: 15px;
200
200
+
}
201
201
+
202
202
+
.status-line .status {
203
203
+
font-size: 2rem;
204
204
+
background-color: #fff;
205
205
+
width: 3rem;
206
206
+
height: 3rem;
207
207
+
border-radius: 1.5rem;
208
208
+
text-align: center;
209
209
+
border: 1px solid var(--border-color);
210
210
+
}
211
211
+
212
212
+
.status-line .desc {
213
213
+
color: var(--gray-500);
214
214
+
}
215
215
+
216
216
+
.status-line .author {
217
217
+
color: var(--gray-700);
218
218
+
font-weight: 600;
219
219
+
text-decoration: none;
220
220
+
}
221
221
+
222
222
+
.status-line .author:hover {
223
223
+
text-decoration: underline;
224
224
+
}
225
225
+
226
226
+
.signup-cta {
227
227
+
text-align: center;
228
228
+
text-wrap: balance;
229
229
+
margin-top: 1rem;
230
230
+
}
+49
html/home.html
···
1
1
+
<!doctype html>
2
2
+
<html lang="en">
3
3
+
<head>
4
4
+
<title>Statusphere-go</title>
5
5
+
<link rel="icon" type="image/x-icon" href="/public/favicon.ico" />
6
6
+
<meta charset="UTF-8" />
7
7
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
8
+
<link href="/public/app.css" rel="stylesheet" />
9
9
+
</head>
10
10
+
<body>
11
11
+
<div id="header">
12
12
+
<h1>Statusphere</h1>
13
13
+
<p>Set your status on the Atmosphere.</p>
14
14
+
</div>
15
15
+
<div class="container">
16
16
+
<div class="card">
17
17
+
<form action="/logout" method="post" class="session-form">
18
18
+
{{if .DisplayName}}
19
19
+
<div>Hi {{.DisplayName}}. What's your status today?</div>
20
20
+
{{else}}
21
21
+
<div>Hi. What's your status today?</div>
22
22
+
{{end}}
23
23
+
<div>
24
24
+
<button type="submit">Log out</button>
25
25
+
</div>
26
26
+
</form>
27
27
+
</div>
28
28
+
<form action="/status" method="post" class="status-options">
29
29
+
{{range .AvailableStatus}}
30
30
+
<button type="submit" name="status" value="{{ . }}">
31
31
+
{{.}}
32
32
+
</button>
33
33
+
{{end}}
34
34
+
</form>
35
35
+
{{range .UsersStatus}}
36
36
+
<div class="status-line">
37
37
+
<div>
38
38
+
<div class="status">{{.Status}}</div>
39
39
+
</div>
40
40
+
<div class="desc">
41
41
+
<a class="author" href="{{ .HandleURL }}">@{{.Handle}}</a>
42
42
+
{{if .IsToday}} is feeling {{.Status}} today {{else}} was
43
43
+
feeling {{.Status}} on {{.Date}} {{end}}
44
44
+
</div>
45
45
+
</div>
46
46
+
{{end}}
47
47
+
</div>
48
48
+
</body>
49
49
+
</html>
+39
html/login.html
···
1
1
+
<!doctype html>
2
2
+
<html lang="en">
3
3
+
<head>
4
4
+
<title>Statusphere-go</title>
5
5
+
<link rel="icon" type="image/x-icon" href="/public/favicon.ico" />
6
6
+
<meta charset="UTF-8" />
7
7
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
8
+
<link href="/public/app.css" rel="stylesheet" />
9
9
+
</head>
10
10
+
<body>
11
11
+
<div id="header">
12
12
+
<h1>Statusphere Go!</h1>
13
13
+
<p>Set your status on the Atmosphere.</p>
14
14
+
</div>
15
15
+
<div class="container">
16
16
+
<form action="/login" method="post" class="login-form">
17
17
+
<input
18
18
+
type="text"
19
19
+
name="handle"
20
20
+
placeholder="Enter your handle (eg alice.bsky.social)"
21
21
+
required
22
22
+
/>
23
23
+
<button type="submit">Log in</button>
24
24
+
</form>
25
25
+
{{if .Error}}
26
26
+
<div>{{ .Error }}</div>
27
27
+
{{else}}
28
28
+
<div>
29
29
+
<br />
30
30
+
</div>
31
31
+
{{end}}
32
32
+
<div class="signup-cta">
33
33
+
Don't have an account on the Atmosphere?
34
34
+
<a href="https://bsky.app">Sign up for Bluesky</a> to create one
35
35
+
now!
36
36
+
</div>
37
37
+
</div>
38
38
+
</body>
39
39
+
</html>
+404
oauth/service.go
···
1
1
+
package oauth
2
2
+
3
3
+
import (
4
4
+
"context"
5
5
+
"encoding/base64"
6
6
+
"encoding/json"
7
7
+
"fmt"
8
8
+
"io"
9
9
+
"net/http"
10
10
+
"net/url"
11
11
+
"os"
12
12
+
"strings"
13
13
+
"time"
14
14
+
15
15
+
atoauth "github.com/haileyok/atproto-oauth-golang"
16
16
+
oauthhelpers "github.com/haileyok/atproto-oauth-golang/helpers"
17
17
+
"github.com/lestrrat-go/jwx/v2/jwk"
18
18
+
)
19
19
+
20
20
+
const (
21
21
+
scope = "atproto transition:generic"
22
22
+
)
23
23
+
24
24
+
type Request struct {
25
25
+
ID uint
26
26
+
AuthserverIss string
27
27
+
State string
28
28
+
Did string
29
29
+
PdsURL string
30
30
+
PkceVerifier string
31
31
+
DpopAuthserverNonce string
32
32
+
DpopPrivateJwk string
33
33
+
}
34
34
+
35
35
+
type Session struct {
36
36
+
ID uint
37
37
+
Did string
38
38
+
PdsUrl string
39
39
+
AuthserverIss string
40
40
+
AccessToken string
41
41
+
RefreshToken string
42
42
+
DpopPdsNonce string
43
43
+
DpopAuthserverNonce string
44
44
+
DpopPrivateJwk string
45
45
+
Expiration int64
46
46
+
}
47
47
+
48
48
+
func (s *Session) CreatePrivateKey() (jwk.Key, error) {
49
49
+
privateJwk, err := oauthhelpers.ParseJWKFromBytes([]byte(s.DpopPrivateJwk))
50
50
+
if err != nil {
51
51
+
return nil, fmt.Errorf("create private jwk: %w", err)
52
52
+
}
53
53
+
return privateJwk, nil
54
54
+
}
55
55
+
56
56
+
type OAuthFlowResult struct {
57
57
+
AuthorizationEndpoint string
58
58
+
State string
59
59
+
DID string
60
60
+
RequestURI string
61
61
+
}
62
62
+
63
63
+
type CallBackParams struct {
64
64
+
State string
65
65
+
Iss string
66
66
+
Code string
67
67
+
}
68
68
+
69
69
+
type Store interface {
70
70
+
CreateOauthRequest(request Request) error
71
71
+
GetOauthRequest(state string) (Request, error)
72
72
+
DeleteOauthRequest(state string) error
73
73
+
CreateOauthSession(session Session) error
74
74
+
GetOauthSession(did string) (Session, error)
75
75
+
UpdateOauthSession(accessToken, refreshToken, dpopAuthServerNonce, did string, expiration int64) error
76
76
+
DeleteOauthSession(did string) error
77
77
+
UpdateOauthSessionDpopPdsNonce(dpopPdsServerNonce, did string) error
78
78
+
}
79
79
+
80
80
+
type Service struct {
81
81
+
store Store
82
82
+
oauthClient *atoauth.Client
83
83
+
httpClient *http.Client
84
84
+
jwks *JWKS
85
85
+
}
86
86
+
87
87
+
func NewService(store Store, serverBase string, httpClient *http.Client) (*Service, error) {
88
88
+
jwks, err := getJWKS()
89
89
+
if err != nil {
90
90
+
return nil, fmt.Errorf("getting JWKS: %w", err)
91
91
+
}
92
92
+
93
93
+
oauthClient, err := createOauthClient(jwks, serverBase, httpClient)
94
94
+
if err != nil {
95
95
+
return nil, fmt.Errorf("create oauth client: %w", err)
96
96
+
}
97
97
+
98
98
+
return &Service{
99
99
+
store: store,
100
100
+
oauthClient: oauthClient,
101
101
+
httpClient: httpClient,
102
102
+
jwks: jwks,
103
103
+
}, nil
104
104
+
}
105
105
+
106
106
+
func (s *Service) StartOAuthFlow(ctx context.Context, handle string) (*OAuthFlowResult, error) {
107
107
+
usersDID, err := s.resolveHandle(handle)
108
108
+
if err != nil {
109
109
+
return nil, fmt.Errorf("resolve handle: %w", err)
110
110
+
}
111
111
+
112
112
+
dpopPrivateKey, err := oauthhelpers.GenerateKey(nil)
113
113
+
if err != nil {
114
114
+
return nil, fmt.Errorf("generate private key: %w", err)
115
115
+
}
116
116
+
117
117
+
parResp, meta, service, err := s.makeOAuthRequest(ctx, usersDID, handle, dpopPrivateKey)
118
118
+
if err != nil {
119
119
+
return nil, fmt.Errorf("make oauth request: %w", err)
120
120
+
}
121
121
+
122
122
+
dpopPrivateKeyJson, err := json.Marshal(dpopPrivateKey)
123
123
+
if err != nil {
124
124
+
return nil, fmt.Errorf("marshal dpop private key: %w", err)
125
125
+
}
126
126
+
127
127
+
oauthRequst := Request{
128
128
+
AuthserverIss: meta.Issuer,
129
129
+
State: parResp.State,
130
130
+
Did: usersDID,
131
131
+
PkceVerifier: parResp.PkceVerifier,
132
132
+
DpopAuthserverNonce: parResp.DpopAuthserverNonce,
133
133
+
DpopPrivateJwk: string(dpopPrivateKeyJson),
134
134
+
PdsURL: service,
135
135
+
}
136
136
+
err = s.store.CreateOauthRequest(oauthRequst)
137
137
+
if err != nil {
138
138
+
return nil, fmt.Errorf("store oauth request: %w", err)
139
139
+
}
140
140
+
141
141
+
result := OAuthFlowResult{
142
142
+
AuthorizationEndpoint: meta.AuthorizationEndpoint,
143
143
+
State: parResp.State,
144
144
+
DID: usersDID,
145
145
+
RequestURI: parResp.RequestUri,
146
146
+
}
147
147
+
148
148
+
return &result, nil
149
149
+
}
150
150
+
151
151
+
func (s *Service) OAuthCallback(ctx context.Context, params CallBackParams) (string, error) {
152
152
+
oauthRequest, err := s.store.GetOauthRequest(fmt.Sprintf("%s", params.State))
153
153
+
if err != nil {
154
154
+
return "", fmt.Errorf("get oauth request from store: %w", err)
155
155
+
}
156
156
+
157
157
+
err = s.store.DeleteOauthRequest(fmt.Sprintf("%s", params.State))
158
158
+
if err != nil {
159
159
+
return "", fmt.Errorf("delete oauth request from store: %w", err)
160
160
+
}
161
161
+
162
162
+
jwk, err := oauthhelpers.ParseJWKFromBytes([]byte(oauthRequest.DpopPrivateJwk))
163
163
+
if err != nil {
164
164
+
return "", fmt.Errorf("parse dpop private key: %w", err)
165
165
+
}
166
166
+
167
167
+
initialTokenResp, err := s.oauthClient.InitialTokenRequest(ctx, params.Code, params.Iss, oauthRequest.PkceVerifier, oauthRequest.DpopAuthserverNonce, jwk)
168
168
+
if err != nil {
169
169
+
return "", fmt.Errorf("make oauth token request: %w", err)
170
170
+
}
171
171
+
172
172
+
if initialTokenResp.Scope != scope {
173
173
+
return "", fmt.Errorf("incorrect scope from token request")
174
174
+
}
175
175
+
176
176
+
oauthSession := Session{
177
177
+
Did: oauthRequest.Did,
178
178
+
PdsUrl: oauthRequest.PdsURL,
179
179
+
AuthserverIss: oauthRequest.AuthserverIss,
180
180
+
AccessToken: initialTokenResp.AccessToken,
181
181
+
RefreshToken: initialTokenResp.RefreshToken,
182
182
+
DpopAuthserverNonce: initialTokenResp.DpopAuthserverNonce,
183
183
+
DpopPrivateJwk: oauthRequest.DpopPrivateJwk,
184
184
+
Expiration: time.Now().Add(time.Duration(int(time.Second) * int(initialTokenResp.ExpiresIn))).UnixMilli(),
185
185
+
}
186
186
+
187
187
+
err = s.store.CreateOauthSession(oauthSession)
188
188
+
if err != nil {
189
189
+
return "", fmt.Errorf("create oauth session in store: %w", err)
190
190
+
}
191
191
+
return oauthRequest.Did, nil
192
192
+
}
193
193
+
194
194
+
func (s *Service) GetOauthSession(ctx context.Context, did string) (Session, error) {
195
195
+
session, err := s.store.GetOauthSession(did)
196
196
+
if err != nil {
197
197
+
return Session{}, fmt.Errorf("find oauth session: %w", err)
198
198
+
}
199
199
+
200
200
+
// if the session expires in more than 5 minutes, return it
201
201
+
if session.Expiration > time.Now().Add(time.Minute*5).UnixMilli() {
202
202
+
return session, nil
203
203
+
}
204
204
+
205
205
+
// refresh the session
206
206
+
privateJwk, err := oauthhelpers.ParseJWKFromBytes([]byte(session.DpopPrivateJwk))
207
207
+
if err != nil {
208
208
+
return Session{}, fmt.Errorf("parse sessions private JWK: %w", err)
209
209
+
}
210
210
+
211
211
+
resp, err := s.oauthClient.RefreshTokenRequest(ctx, session.RefreshToken, session.AuthserverIss, session.DpopAuthserverNonce, privateJwk)
212
212
+
if err != nil {
213
213
+
return Session{}, fmt.Errorf("refresh token: %w", err)
214
214
+
}
215
215
+
216
216
+
expiration := time.Now().Add(time.Duration(int(time.Second) * int(resp.ExpiresIn))).UnixMilli()
217
217
+
218
218
+
err = s.store.UpdateOauthSession(resp.AccessToken, resp.RefreshToken, resp.DpopAuthserverNonce, did, expiration)
219
219
+
if err != nil {
220
220
+
return Session{}, fmt.Errorf("update session after refresh: %w", err)
221
221
+
}
222
222
+
223
223
+
session.AccessToken = resp.AccessToken
224
224
+
session.RefreshToken = resp.RefreshToken
225
225
+
session.DpopAuthserverNonce = resp.DpopAuthserverNonce
226
226
+
session.Expiration = expiration
227
227
+
228
228
+
return session, nil
229
229
+
}
230
230
+
231
231
+
func (s *Service) DeleteOAuthSession(did string) error {
232
232
+
err := s.store.DeleteOauthSession(did)
233
233
+
if err != nil {
234
234
+
return fmt.Errorf("delete oauth session from store: %w", err)
235
235
+
}
236
236
+
return nil
237
237
+
}
238
238
+
239
239
+
func (s *Service) UpdateOAuthSessionDPopPDSNonce(did, newDPopNonce string) error {
240
240
+
return s.store.UpdateOauthSessionDpopPdsNonce(newDPopNonce, did)
241
241
+
}
242
242
+
243
243
+
func (s *Service) PublicKey() []byte {
244
244
+
return s.jwks.public
245
245
+
}
246
246
+
247
247
+
func (s *Service) makeOAuthRequest(ctx context.Context, did, handle string, dpopPrivateKey jwk.Key) (*atoauth.SendParAuthResponse, *atoauth.OauthAuthorizationMetadata, string, error) {
248
248
+
service, err := s.resolveService(ctx, did)
249
249
+
if err != nil {
250
250
+
return nil, nil, "", err
251
251
+
}
252
252
+
253
253
+
authserver, err := s.oauthClient.ResolvePdsAuthServer(ctx, service)
254
254
+
if err != nil {
255
255
+
return nil, nil, "", err
256
256
+
}
257
257
+
258
258
+
meta, err := s.oauthClient.FetchAuthServerMetadata(ctx, authserver)
259
259
+
if err != nil {
260
260
+
return nil, nil, "", err
261
261
+
}
262
262
+
263
263
+
resp, err := s.oauthClient.SendParAuthRequest(ctx, authserver, meta, handle, scope, dpopPrivateKey)
264
264
+
if err != nil {
265
265
+
return nil, nil, "", err
266
266
+
}
267
267
+
return resp, meta, service, nil
268
268
+
}
269
269
+
270
270
+
func (s *Service) resolveHandle(handle string) (string, error) {
271
271
+
params := url.Values{
272
272
+
"handle": []string{handle},
273
273
+
}
274
274
+
reqUrl := "https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?" + params.Encode()
275
275
+
276
276
+
resp, err := s.httpClient.Get(reqUrl)
277
277
+
if err != nil {
278
278
+
return "", fmt.Errorf("make http request: %w", err)
279
279
+
}
280
280
+
281
281
+
defer resp.Body.Close()
282
282
+
283
283
+
type did struct {
284
284
+
Did string
285
285
+
}
286
286
+
287
287
+
b, err := io.ReadAll(resp.Body)
288
288
+
if err != nil {
289
289
+
return "", fmt.Errorf("read response body: %w", err)
290
290
+
}
291
291
+
292
292
+
var resDid did
293
293
+
err = json.Unmarshal(b, &resDid)
294
294
+
if err != nil {
295
295
+
return "", fmt.Errorf("unmarshal response: %w", err)
296
296
+
}
297
297
+
298
298
+
return resDid.Did, nil
299
299
+
}
300
300
+
301
301
+
func (s *Service) resolveService(ctx context.Context, did string) (string, error) {
302
302
+
type Identity struct {
303
303
+
Service []struct {
304
304
+
ID string `json:"id"`
305
305
+
Type string `json:"type"`
306
306
+
ServiceEndpoint string `json:"serviceEndpoint"`
307
307
+
} `json:"service"`
308
308
+
}
309
309
+
310
310
+
var url string
311
311
+
if strings.HasPrefix(did, "did:plc:") {
312
312
+
url = fmt.Sprintf("https://plc.directory/%s", did)
313
313
+
} else if strings.HasPrefix(did, "did:web:") {
314
314
+
url = fmt.Sprintf("https://%s/.well-known/did.json", strings.TrimPrefix(did, "did:web:"))
315
315
+
} else {
316
316
+
return "", fmt.Errorf("did was not a supported did type")
317
317
+
}
318
318
+
319
319
+
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
320
320
+
if err != nil {
321
321
+
return "", err
322
322
+
}
323
323
+
324
324
+
resp, err := s.httpClient.Do(req)
325
325
+
if err != nil {
326
326
+
return "", fmt.Errorf("do http request: %w", err)
327
327
+
}
328
328
+
defer resp.Body.Close()
329
329
+
330
330
+
if resp.StatusCode != 200 {
331
331
+
return "", fmt.Errorf("could not find identity in plc registry")
332
332
+
}
333
333
+
334
334
+
b, err := io.ReadAll(resp.Body)
335
335
+
if err != nil {
336
336
+
return "", fmt.Errorf("read response body: %w", err)
337
337
+
}
338
338
+
339
339
+
var identity Identity
340
340
+
err = json.Unmarshal(b, &identity)
341
341
+
if err != nil {
342
342
+
return "", fmt.Errorf("unmarshal response: %w", err)
343
343
+
}
344
344
+
345
345
+
var service string
346
346
+
for _, svc := range identity.Service {
347
347
+
if svc.ID == "#atproto_pds" {
348
348
+
service = svc.ServiceEndpoint
349
349
+
}
350
350
+
}
351
351
+
352
352
+
if service == "" {
353
353
+
return "", fmt.Errorf("could not find atproto_pds service in identity services")
354
354
+
}
355
355
+
356
356
+
return service, nil
357
357
+
}
358
358
+
359
359
+
type JWKS struct {
360
360
+
public []byte
361
361
+
private jwk.Key
362
362
+
}
363
363
+
364
364
+
func getJWKS() (*JWKS, error) {
365
365
+
jwksB64 := os.Getenv("PRIVATEJWKS")
366
366
+
if jwksB64 == "" {
367
367
+
return nil, fmt.Errorf("PRIVATEJWKS env not set")
368
368
+
}
369
369
+
370
370
+
jwksB, err := base64.StdEncoding.DecodeString(jwksB64)
371
371
+
if err != nil {
372
372
+
return nil, fmt.Errorf("decode jwks env: %w", err)
373
373
+
}
374
374
+
375
375
+
k, err := oauthhelpers.ParseJWKFromBytes([]byte(jwksB))
376
376
+
if err != nil {
377
377
+
return nil, fmt.Errorf("parse JWK from bytes: %w", err)
378
378
+
}
379
379
+
380
380
+
pubkey, err := k.PublicKey()
381
381
+
if err != nil {
382
382
+
return nil, fmt.Errorf("get public key from JWKS: %w", err)
383
383
+
}
384
384
+
385
385
+
resp := oauthhelpers.CreateJwksResponseObject(pubkey)
386
386
+
b, err := json.Marshal(resp)
387
387
+
if err != nil {
388
388
+
return nil, fmt.Errorf("marshal public JWKS: %w", err)
389
389
+
}
390
390
+
391
391
+
return &JWKS{
392
392
+
public: b,
393
393
+
private: k,
394
394
+
}, nil
395
395
+
}
396
396
+
397
397
+
func createOauthClient(jwks *JWKS, serverBase string, httpClient *http.Client) (*atoauth.Client, error) {
398
398
+
return atoauth.NewClient(atoauth.ClientArgs{
399
399
+
Http: httpClient,
400
400
+
ClientJwk: jwks.private,
401
401
+
ClientId: fmt.Sprintf("%s/client-metadata.json", serverBase),
402
402
+
RedirectUri: fmt.Sprintf("%s/oauth-callback", serverBase),
403
403
+
})
404
404
+
}
+219
server.go
···
1
1
+
package statusphere
2
2
+
3
3
+
import (
4
4
+
"context"
5
5
+
_ "embed"
6
6
+
"encoding/json"
7
7
+
"errors"
8
8
+
"fmt"
9
9
+
"io"
10
10
+
"log/slog"
11
11
+
"net/http"
12
12
+
"net/url"
13
13
+
"os"
14
14
+
"text/template"
15
15
+
16
16
+
"github.com/gorilla/sessions"
17
17
+
18
18
+
"github.com/willdot/statusphere-go/oauth"
19
19
+
)
20
20
+
21
21
+
var ErrorNotFound = fmt.Errorf("not found")
22
22
+
23
23
+
type UserProfile struct {
24
24
+
Did string `json:"did"`
25
25
+
Handle string `json:"handle"`
26
26
+
DisplayName string `json:"displayName"`
27
27
+
}
28
28
+
29
29
+
type Store interface {
30
30
+
GetHandleAndDisplayNameForDid(did string) (UserProfile, error)
31
31
+
CreateProfile(profile UserProfile) error
32
32
+
GetStatuses(limit int) ([]Status, error)
33
33
+
CreateStatus(status Status) error
34
34
+
}
35
35
+
36
36
+
type Server struct {
37
37
+
host string
38
38
+
httpserver *http.Server
39
39
+
sessionStore *sessions.CookieStore
40
40
+
templates []*template.Template
41
41
+
oauthService *oauth.Service
42
42
+
store Store
43
43
+
httpClient *http.Client
44
44
+
}
45
45
+
46
46
+
func NewServer(host string, port int, store Store, oauthService *oauth.Service, httpClient *http.Client) (*Server, error) {
47
47
+
sessionStore := sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY")))
48
48
+
49
49
+
homeTemplate, err := template.ParseFiles("./html/home.html")
50
50
+
if err != nil {
51
51
+
return nil, fmt.Errorf("parsing home template: %w", err)
52
52
+
}
53
53
+
loginTemplate, err := template.ParseFiles("./html/login.html")
54
54
+
if err != nil {
55
55
+
return nil, fmt.Errorf("parsing login template: %w", err)
56
56
+
}
57
57
+
58
58
+
templates := []*template.Template{
59
59
+
homeTemplate,
60
60
+
loginTemplate,
61
61
+
}
62
62
+
63
63
+
srv := &Server{
64
64
+
host: host,
65
65
+
oauthService: oauthService,
66
66
+
sessionStore: sessionStore,
67
67
+
templates: templates,
68
68
+
store: store,
69
69
+
httpClient: httpClient,
70
70
+
}
71
71
+
72
72
+
mux := http.NewServeMux()
73
73
+
mux.HandleFunc("/", srv.authMiddleware(srv.HandleHome))
74
74
+
mux.HandleFunc("POST /status", srv.authMiddleware(srv.HandleStatus))
75
75
+
76
76
+
mux.HandleFunc("GET /login", srv.HandleLogin)
77
77
+
mux.HandleFunc("POST /login", srv.HandlePostLogin)
78
78
+
mux.HandleFunc("POST /logout", srv.HandleLogOut)
79
79
+
80
80
+
mux.HandleFunc("/public/app.css", serveCSS)
81
81
+
mux.HandleFunc("/jwks.json", srv.serveJwks)
82
82
+
mux.HandleFunc("/client-metadata.json", srv.serveClientMetadata)
83
83
+
mux.HandleFunc("/oauth-callback", srv.handleOauthCallback)
84
84
+
85
85
+
addr := fmt.Sprintf("0.0.0.0:%d", port)
86
86
+
srv.httpserver = &http.Server{
87
87
+
Addr: addr,
88
88
+
Handler: mux,
89
89
+
}
90
90
+
91
91
+
return srv, nil
92
92
+
}
93
93
+
94
94
+
func (s *Server) Run() {
95
95
+
err := s.httpserver.ListenAndServe()
96
96
+
if err != nil {
97
97
+
slog.Error("listen and serve", "error", err)
98
98
+
}
99
99
+
}
100
100
+
101
101
+
func (s *Server) Stop(ctx context.Context) error {
102
102
+
return s.httpserver.Shutdown(ctx)
103
103
+
}
104
104
+
105
105
+
func (s *Server) getTemplate(name string) *template.Template {
106
106
+
for _, template := range s.templates {
107
107
+
if template.Name() == name {
108
108
+
return template
109
109
+
}
110
110
+
}
111
111
+
return nil
112
112
+
}
113
113
+
114
114
+
func (s *Server) serveJwks(w http.ResponseWriter, _ *http.Request) {
115
115
+
w.Header().Set("Content-Type", "application/json")
116
116
+
_, _ = w.Write(s.oauthService.PublicKey())
117
117
+
}
118
118
+
119
119
+
//go:embed html/app.css
120
120
+
var cssFile []byte
121
121
+
122
122
+
func serveCSS(w http.ResponseWriter, r *http.Request) {
123
123
+
w.Header().Set("Content-Type", "text/css; charset=utf-8")
124
124
+
_, _ = w.Write(cssFile)
125
125
+
}
126
126
+
127
127
+
func (s *Server) serveClientMetadata(w http.ResponseWriter, r *http.Request) {
128
128
+
metadata := map[string]any{
129
129
+
"client_id": fmt.Sprintf("%s/client-metadata.json", s.host),
130
130
+
"client_name": "Bsky-bookmark",
131
131
+
"client_uri": s.host,
132
132
+
"redirect_uris": []string{fmt.Sprintf("%s/oauth-callback", s.host)},
133
133
+
"grant_types": []string{"authorization_code", "refresh_token"},
134
134
+
"response_types": []string{"code"},
135
135
+
"application_type": "web",
136
136
+
"dpop_bound_access_tokens": true,
137
137
+
"jwks_uri": fmt.Sprintf("%s/jwks.json", s.host),
138
138
+
"scope": "atproto transition:generic",
139
139
+
"token_endpoint_auth_method": "private_key_jwt",
140
140
+
"token_endpoint_auth_signing_alg": "ES256",
141
141
+
}
142
142
+
143
143
+
b, err := json.Marshal(metadata)
144
144
+
if err != nil {
145
145
+
slog.Error("failed to marshal client metadata", "error", err)
146
146
+
http.Error(w, "marshal response", http.StatusInternalServerError)
147
147
+
return
148
148
+
}
149
149
+
w.Header().Set("Content-Type", "application/json")
150
150
+
_, _ = w.Write(b)
151
151
+
}
152
152
+
153
153
+
func (s *Server) getDidFromSession(r *http.Request) (string, bool) {
154
154
+
session, err := s.sessionStore.Get(r, "oauth-session")
155
155
+
if err != nil {
156
156
+
slog.Error("getting session", "error", err)
157
157
+
return "", false
158
158
+
}
159
159
+
160
160
+
did, ok := session.Values["did"].(string)
161
161
+
if !ok {
162
162
+
return "", false
163
163
+
}
164
164
+
165
165
+
return did, true
166
166
+
}
167
167
+
168
168
+
func (s *Server) getUserProfileForDid(did string) (UserProfile, error) {
169
169
+
profile, err := s.store.GetHandleAndDisplayNameForDid(did)
170
170
+
if err == nil {
171
171
+
return UserProfile{
172
172
+
Did: did,
173
173
+
Handle: profile.Handle,
174
174
+
DisplayName: profile.DisplayName,
175
175
+
}, nil
176
176
+
}
177
177
+
178
178
+
if !errors.Is(err, ErrorNotFound) {
179
179
+
slog.Error("getting profile from database", "error", err)
180
180
+
}
181
181
+
182
182
+
profile, err = s.lookupUserProfile(did)
183
183
+
if err != nil {
184
184
+
return UserProfile{}, fmt.Errorf("looking up profile: %w", err)
185
185
+
}
186
186
+
err = s.store.CreateProfile(profile)
187
187
+
if err != nil {
188
188
+
slog.Error("store profile", "error", err)
189
189
+
}
190
190
+
191
191
+
return profile, nil
192
192
+
}
193
193
+
194
194
+
func (s *Server) lookupUserProfile(did string) (UserProfile, error) {
195
195
+
params := url.Values{
196
196
+
"actor": []string{did},
197
197
+
}
198
198
+
reqUrl := "https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?" + params.Encode()
199
199
+
200
200
+
resp, err := s.httpClient.Get(reqUrl)
201
201
+
if err != nil {
202
202
+
return UserProfile{}, fmt.Errorf("make http request: %w", err)
203
203
+
}
204
204
+
205
205
+
defer resp.Body.Close()
206
206
+
207
207
+
b, err := io.ReadAll(resp.Body)
208
208
+
if err != nil {
209
209
+
return UserProfile{}, fmt.Errorf("read response body: %w", err)
210
210
+
}
211
211
+
212
212
+
var profile UserProfile
213
213
+
err = json.Unmarshal(b, &profile)
214
214
+
if err != nil {
215
215
+
return UserProfile{}, fmt.Errorf("unmarshal response: %w", err)
216
216
+
}
217
217
+
218
218
+
return profile, nil
219
219
+
}
+123
status.go
···
1
1
+
package statusphere
2
2
+
3
3
+
import (
4
4
+
"bytes"
5
5
+
"context"
6
6
+
"encoding/json"
7
7
+
"fmt"
8
8
+
"io"
9
9
+
"log/slog"
10
10
+
"net/http"
11
11
+
"time"
12
12
+
13
13
+
"github.com/willdot/statusphere-go/oauth"
14
14
+
)
15
15
+
16
16
+
type Status struct {
17
17
+
URI string
18
18
+
Did string
19
19
+
Status string
20
20
+
CreatedAt int64
21
21
+
IndexedAt int64
22
22
+
}
23
23
+
24
24
+
type XRPCError struct {
25
25
+
ErrStr string `json:"error"`
26
26
+
Message string `json:"message"`
27
27
+
}
28
28
+
29
29
+
type CreateRecordResp struct {
30
30
+
URI string `json:"uri"`
31
31
+
}
32
32
+
33
33
+
func (s *Server) CreateNewStatus(ctx context.Context, oauthsession oauth.Session, status string, createdAt time.Time) (string, error) {
34
34
+
privateJwk, err := oauthsession.CreatePrivateKey()
35
35
+
if err != nil {
36
36
+
return "", fmt.Errorf("create private jwk: %w", err)
37
37
+
}
38
38
+
39
39
+
bodyReq := map[string]any{
40
40
+
"repo": oauthsession.Did,
41
41
+
"collection": "xyz.statusphere.status",
42
42
+
"record": map[string]any{
43
43
+
"status": status,
44
44
+
"createdAt": createdAt,
45
45
+
},
46
46
+
}
47
47
+
48
48
+
bodyB, err := json.Marshal(bodyReq)
49
49
+
if err != nil {
50
50
+
return "", fmt.Errorf("marshal update message request body: %w", err)
51
51
+
}
52
52
+
53
53
+
// TODO: redo this loop business
54
54
+
for range 2 {
55
55
+
r := bytes.NewReader(bodyB)
56
56
+
url := fmt.Sprintf("%s/xrpc/com.atproto.repo.createRecord", oauthsession.PdsUrl)
57
57
+
request, err := http.NewRequestWithContext(ctx, "POST", url, r)
58
58
+
if err != nil {
59
59
+
return "", fmt.Errorf("create http request: %w", err)
60
60
+
}
61
61
+
62
62
+
request.Header.Add("Content-Type", "application/json")
63
63
+
request.Header.Add("Accept", "application/json")
64
64
+
65
65
+
dpopJwt, err := pdsDpopJwt("POST", url, oauthsession.AuthserverIss, oauthsession.AccessToken, oauthsession.DpopPdsNonce, privateJwk)
66
66
+
if err != nil {
67
67
+
return "", err
68
68
+
}
69
69
+
70
70
+
request.Header.Set("DPoP", dpopJwt)
71
71
+
request.Header.Set("Authorization", "DPoP "+oauthsession.AccessToken)
72
72
+
73
73
+
resp, err := s.httpClient.Do(request)
74
74
+
if err != nil {
75
75
+
return "", fmt.Errorf("do http request: %w", err)
76
76
+
}
77
77
+
defer resp.Body.Close()
78
78
+
79
79
+
if resp.StatusCode == http.StatusOK {
80
80
+
var result CreateRecordResp
81
81
+
err = decodeResp(resp.Body, &result)
82
82
+
if err != nil {
83
83
+
// just log error because we got a 200 indicating that the record was created. If this were to be tried again due to an error
84
84
+
// returned here, there would be duplicate data
85
85
+
slog.Error("decode success response", "error", err)
86
86
+
}
87
87
+
return result.URI, nil
88
88
+
}
89
89
+
90
90
+
var errorResp XRPCError
91
91
+
err = decodeResp(resp.Body, &errorResp)
92
92
+
if err != nil {
93
93
+
return "", fmt.Errorf("decode error resp: %w", err)
94
94
+
}
95
95
+
96
96
+
if resp.StatusCode == 400 || resp.StatusCode == 401 && errorResp.ErrStr == "use_dpop_nonce" {
97
97
+
newNonce := resp.Header.Get("DPoP-Nonce")
98
98
+
oauthsession.DpopPdsNonce = newNonce
99
99
+
err := s.oauthService.UpdateOAuthSessionDPopPDSNonce(oauthsession.Did, newNonce)
100
100
+
if err != nil {
101
101
+
slog.Error("updating oauth session in store with new DPoP PDS nonce", "error", err)
102
102
+
}
103
103
+
continue
104
104
+
}
105
105
+
106
106
+
slog.Error("got error", "status code", resp.StatusCode, "message", errorResp.Message, "error", errorResp.ErrStr)
107
107
+
}
108
108
+
109
109
+
return "", fmt.Errorf("failed to create status record")
110
110
+
}
111
111
+
112
112
+
func decodeResp(body io.Reader, result any) error {
113
113
+
resBody, err := io.ReadAll(body)
114
114
+
if err != nil {
115
115
+
return fmt.Errorf("failed to read response: %w", err)
116
116
+
}
117
117
+
118
118
+
err = json.Unmarshal(resBody, result)
119
119
+
if err != nil {
120
120
+
return fmt.Errorf("failed to unmarshal response: %w", err)
121
121
+
}
122
122
+
return nil
123
123
+
}