+7
api/api.go
+7
api/api.go
+24
api/atyo/actorprofile.go
+24
api/atyo/actorprofile.go
···
1
+
// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
2
+
3
+
package atyo
4
+
5
+
// schema: app.atyo.actor.profile
6
+
7
+
import (
8
+
"github.com/bluesky-social/indigo/lex/util"
9
+
)
10
+
11
+
func init() {
12
+
util.RegisterType("app.atyo.actor.profile", &ActorProfile{})
13
+
} //
14
+
// RECORDTYPE: ActorProfile
15
+
type ActorProfile struct {
16
+
LexiconTypeID string `json:"$type,const=app.atyo.actor.profile" cborgen:"$type,const=app.atyo.actor.profile"`
17
+
PublicKeys []*ActorProfile_PublicKeys_Elem `json:"publicKeys" cborgen:"publicKeys"`
18
+
}
19
+
20
+
type ActorProfile_PublicKeys_Elem struct {
21
+
CreatedAt string `json:"createdAt" cborgen:"createdAt"`
22
+
ExpiresAt *string `json:"expiresAt,omitempty" cborgen:"expiresAt,omitempty"`
23
+
Key util.LexBytes `json:"key,omitempty" cborgen:"key,omitempty"`
24
+
}
+384
api/atyo/cbor_gen.go
+384
api/atyo/cbor_gen.go
···
1105
1105
1106
1106
return nil
1107
1107
}
1108
+
func (t *ActorProfile_PublicKeys_Elem) MarshalCBOR(w io.Writer) error {
1109
+
if t == nil {
1110
+
_, err := w.Write(cbg.CborNull)
1111
+
return err
1112
+
}
1113
+
1114
+
cw := cbg.NewCborWriter(w)
1115
+
fieldCount := 3
1116
+
1117
+
if t.ExpiresAt == nil {
1118
+
fieldCount--
1119
+
}
1120
+
1121
+
if t.Key == nil {
1122
+
fieldCount--
1123
+
}
1124
+
1125
+
if _, err := cw.Write(cbg.CborEncodeMajorType(cbg.MajMap, uint64(fieldCount))); err != nil {
1126
+
return err
1127
+
}
1128
+
1129
+
// t.Key (util.LexBytes) (slice)
1130
+
if t.Key != nil {
1131
+
1132
+
if len("key") > 1000000 {
1133
+
return xerrors.Errorf("Value in field \"key\" was too long")
1134
+
}
1135
+
1136
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("key"))); err != nil {
1137
+
return err
1138
+
}
1139
+
if _, err := cw.WriteString(string("key")); err != nil {
1140
+
return err
1141
+
}
1142
+
1143
+
if len(t.Key) > 2097152 {
1144
+
return xerrors.Errorf("Byte array in field t.Key was too long")
1145
+
}
1146
+
1147
+
if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.Key))); err != nil {
1148
+
return err
1149
+
}
1150
+
1151
+
if _, err := cw.Write(t.Key); err != nil {
1152
+
return err
1153
+
}
1154
+
1155
+
}
1156
+
1157
+
// t.CreatedAt (string) (string)
1158
+
if len("createdAt") > 1000000 {
1159
+
return xerrors.Errorf("Value in field \"createdAt\" was too long")
1160
+
}
1161
+
1162
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil {
1163
+
return err
1164
+
}
1165
+
if _, err := cw.WriteString(string("createdAt")); err != nil {
1166
+
return err
1167
+
}
1168
+
1169
+
if len(t.CreatedAt) > 1000000 {
1170
+
return xerrors.Errorf("Value in field t.CreatedAt was too long")
1171
+
}
1172
+
1173
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil {
1174
+
return err
1175
+
}
1176
+
if _, err := cw.WriteString(string(t.CreatedAt)); err != nil {
1177
+
return err
1178
+
}
1179
+
1180
+
// t.ExpiresAt (string) (string)
1181
+
if t.ExpiresAt != nil {
1182
+
1183
+
if len("expiresAt") > 1000000 {
1184
+
return xerrors.Errorf("Value in field \"expiresAt\" was too long")
1185
+
}
1186
+
1187
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("expiresAt"))); err != nil {
1188
+
return err
1189
+
}
1190
+
if _, err := cw.WriteString(string("expiresAt")); err != nil {
1191
+
return err
1192
+
}
1193
+
1194
+
if t.ExpiresAt == nil {
1195
+
if _, err := cw.Write(cbg.CborNull); err != nil {
1196
+
return err
1197
+
}
1198
+
} else {
1199
+
if len(*t.ExpiresAt) > 1000000 {
1200
+
return xerrors.Errorf("Value in field t.ExpiresAt was too long")
1201
+
}
1202
+
1203
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(*t.ExpiresAt))); err != nil {
1204
+
return err
1205
+
}
1206
+
if _, err := cw.WriteString(string(*t.ExpiresAt)); err != nil {
1207
+
return err
1208
+
}
1209
+
}
1210
+
}
1211
+
return nil
1212
+
}
1213
+
1214
+
func (t *ActorProfile_PublicKeys_Elem) UnmarshalCBOR(r io.Reader) (err error) {
1215
+
*t = ActorProfile_PublicKeys_Elem{}
1216
+
1217
+
cr := cbg.NewCborReader(r)
1218
+
1219
+
maj, extra, err := cr.ReadHeader()
1220
+
if err != nil {
1221
+
return err
1222
+
}
1223
+
defer func() {
1224
+
if err == io.EOF {
1225
+
err = io.ErrUnexpectedEOF
1226
+
}
1227
+
}()
1228
+
1229
+
if maj != cbg.MajMap {
1230
+
return fmt.Errorf("cbor input should be of type map")
1231
+
}
1232
+
1233
+
if extra > cbg.MaxLength {
1234
+
return fmt.Errorf("ActorProfile_PublicKeys_Elem: map struct too large (%d)", extra)
1235
+
}
1236
+
1237
+
n := extra
1238
+
1239
+
nameBuf := make([]byte, 9)
1240
+
for i := uint64(0); i < n; i++ {
1241
+
nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
1242
+
if err != nil {
1243
+
return err
1244
+
}
1245
+
1246
+
if !ok {
1247
+
// Field doesn't exist on this type, so ignore it
1248
+
if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil {
1249
+
return err
1250
+
}
1251
+
continue
1252
+
}
1253
+
1254
+
switch string(nameBuf[:nameLen]) {
1255
+
// t.Key (util.LexBytes) (slice)
1256
+
case "key":
1257
+
1258
+
maj, extra, err = cr.ReadHeader()
1259
+
if err != nil {
1260
+
return err
1261
+
}
1262
+
1263
+
if extra > 2097152 {
1264
+
return fmt.Errorf("t.Key: byte array too large (%d)", extra)
1265
+
}
1266
+
if maj != cbg.MajByteString {
1267
+
return fmt.Errorf("expected byte array")
1268
+
}
1269
+
1270
+
if extra > 0 {
1271
+
t.Key = make([]uint8, extra)
1272
+
}
1273
+
1274
+
if _, err := io.ReadFull(cr, t.Key); err != nil {
1275
+
return err
1276
+
}
1277
+
1278
+
// t.CreatedAt (string) (string)
1279
+
case "createdAt":
1280
+
1281
+
{
1282
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
1283
+
if err != nil {
1284
+
return err
1285
+
}
1286
+
1287
+
t.CreatedAt = string(sval)
1288
+
}
1289
+
// t.ExpiresAt (string) (string)
1290
+
case "expiresAt":
1291
+
1292
+
{
1293
+
b, err := cr.ReadByte()
1294
+
if err != nil {
1295
+
return err
1296
+
}
1297
+
if b != cbg.CborNull[0] {
1298
+
if err := cr.UnreadByte(); err != nil {
1299
+
return err
1300
+
}
1301
+
1302
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
1303
+
if err != nil {
1304
+
return err
1305
+
}
1306
+
1307
+
t.ExpiresAt = (*string)(&sval)
1308
+
}
1309
+
}
1310
+
1311
+
default:
1312
+
// Field doesn't exist on this type, so ignore it
1313
+
if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil {
1314
+
return err
1315
+
}
1316
+
}
1317
+
}
1318
+
1319
+
return nil
1320
+
}
1321
+
func (t *ActorProfile) MarshalCBOR(w io.Writer) error {
1322
+
if t == nil {
1323
+
_, err := w.Write(cbg.CborNull)
1324
+
return err
1325
+
}
1326
+
1327
+
cw := cbg.NewCborWriter(w)
1328
+
1329
+
if _, err := cw.Write([]byte{162}); err != nil {
1330
+
return err
1331
+
}
1332
+
1333
+
// t.LexiconTypeID (string) (string)
1334
+
if len("$type") > 1000000 {
1335
+
return xerrors.Errorf("Value in field \"$type\" was too long")
1336
+
}
1337
+
1338
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil {
1339
+
return err
1340
+
}
1341
+
if _, err := cw.WriteString(string("$type")); err != nil {
1342
+
return err
1343
+
}
1344
+
1345
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("app.atyo.actor.profile"))); err != nil {
1346
+
return err
1347
+
}
1348
+
if _, err := cw.WriteString(string("app.atyo.actor.profile")); err != nil {
1349
+
return err
1350
+
}
1351
+
1352
+
// t.PublicKeys ([]*atyo.ActorProfile_PublicKeys_Elem) (slice)
1353
+
if len("publicKeys") > 1000000 {
1354
+
return xerrors.Errorf("Value in field \"publicKeys\" was too long")
1355
+
}
1356
+
1357
+
if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("publicKeys"))); err != nil {
1358
+
return err
1359
+
}
1360
+
if _, err := cw.WriteString(string("publicKeys")); err != nil {
1361
+
return err
1362
+
}
1363
+
1364
+
if len(t.PublicKeys) > 8192 {
1365
+
return xerrors.Errorf("Slice value in field t.PublicKeys was too long")
1366
+
}
1367
+
1368
+
if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.PublicKeys))); err != nil {
1369
+
return err
1370
+
}
1371
+
for _, v := range t.PublicKeys {
1372
+
if err := v.MarshalCBOR(cw); err != nil {
1373
+
return err
1374
+
}
1375
+
1376
+
}
1377
+
return nil
1378
+
}
1379
+
1380
+
func (t *ActorProfile) UnmarshalCBOR(r io.Reader) (err error) {
1381
+
*t = ActorProfile{}
1382
+
1383
+
cr := cbg.NewCborReader(r)
1384
+
1385
+
maj, extra, err := cr.ReadHeader()
1386
+
if err != nil {
1387
+
return err
1388
+
}
1389
+
defer func() {
1390
+
if err == io.EOF {
1391
+
err = io.ErrUnexpectedEOF
1392
+
}
1393
+
}()
1394
+
1395
+
if maj != cbg.MajMap {
1396
+
return fmt.Errorf("cbor input should be of type map")
1397
+
}
1398
+
1399
+
if extra > cbg.MaxLength {
1400
+
return fmt.Errorf("ActorProfile: map struct too large (%d)", extra)
1401
+
}
1402
+
1403
+
n := extra
1404
+
1405
+
nameBuf := make([]byte, 10)
1406
+
for i := uint64(0); i < n; i++ {
1407
+
nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000)
1408
+
if err != nil {
1409
+
return err
1410
+
}
1411
+
1412
+
if !ok {
1413
+
// Field doesn't exist on this type, so ignore it
1414
+
if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil {
1415
+
return err
1416
+
}
1417
+
continue
1418
+
}
1419
+
1420
+
switch string(nameBuf[:nameLen]) {
1421
+
// t.LexiconTypeID (string) (string)
1422
+
case "$type":
1423
+
1424
+
{
1425
+
sval, err := cbg.ReadStringWithMax(cr, 1000000)
1426
+
if err != nil {
1427
+
return err
1428
+
}
1429
+
1430
+
t.LexiconTypeID = string(sval)
1431
+
}
1432
+
// t.PublicKeys ([]*atyo.ActorProfile_PublicKeys_Elem) (slice)
1433
+
case "publicKeys":
1434
+
1435
+
maj, extra, err = cr.ReadHeader()
1436
+
if err != nil {
1437
+
return err
1438
+
}
1439
+
1440
+
if extra > 8192 {
1441
+
return fmt.Errorf("t.PublicKeys: array too large (%d)", extra)
1442
+
}
1443
+
1444
+
if maj != cbg.MajArray {
1445
+
return fmt.Errorf("expected cbor array")
1446
+
}
1447
+
1448
+
if extra > 0 {
1449
+
t.PublicKeys = make([]*ActorProfile_PublicKeys_Elem, extra)
1450
+
}
1451
+
1452
+
for i := 0; i < int(extra); i++ {
1453
+
{
1454
+
var maj byte
1455
+
var extra uint64
1456
+
var err error
1457
+
_ = maj
1458
+
_ = extra
1459
+
_ = err
1460
+
1461
+
{
1462
+
1463
+
b, err := cr.ReadByte()
1464
+
if err != nil {
1465
+
return err
1466
+
}
1467
+
if b != cbg.CborNull[0] {
1468
+
if err := cr.UnreadByte(); err != nil {
1469
+
return err
1470
+
}
1471
+
t.PublicKeys[i] = new(ActorProfile_PublicKeys_Elem)
1472
+
if err := t.PublicKeys[i].UnmarshalCBOR(cr); err != nil {
1473
+
return xerrors.Errorf("unmarshaling t.PublicKeys[i] pointer: %w", err)
1474
+
}
1475
+
}
1476
+
1477
+
}
1478
+
1479
+
}
1480
+
}
1481
+
1482
+
default:
1483
+
// Field doesn't exist on this type, so ignore it
1484
+
if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil {
1485
+
return err
1486
+
}
1487
+
}
1488
+
}
1489
+
1490
+
return nil
1491
+
}
+113
-12
appview/keys.go
+113
-12
appview/keys.go
···
1
1
package appview
2
2
3
3
import (
4
-
"net/http"
4
+
"context"
5
+
"crypto/rand"
6
+
"fmt"
7
+
"os"
5
8
9
+
"atyo.app/api"
10
+
"atyo.app/api/atyo"
11
+
"github.com/bluesky-social/indigo/api/atproto"
6
12
"github.com/bluesky-social/indigo/atproto/identity"
13
+
"github.com/bluesky-social/indigo/atproto/syntax"
14
+
"github.com/bluesky-social/indigo/lex/util"
15
+
"github.com/bluesky-social/indigo/xrpc"
16
+
"golang.org/x/crypto/nacl/box"
7
17
)
8
18
9
-
func (*Server) addPublicKey(w http.ResponseWriter, req *http.Request) {
10
-
// TODO
19
+
type KeyManager struct {
20
+
client *xrpc.Client
21
+
selfPrivateKey *[32]byte
22
+
cache map[string]*[32]byte
11
23
}
12
24
13
-
func (*Server) removePublicKey(w http.ResponseWriter, req *http.Request) {
14
-
// TODO
25
+
// TODO: get pubkey from atproto profile; probably needs a new lexicon
26
+
// also maybe if there are multiple records we should do one for each?
27
+
func (k *KeyManager) fetchUserPubKey(ctx context.Context, id *identity.Identity) (key *[32]byte, err error) {
28
+
record, profile, err := k.fetchProfile(ctx, id)
29
+
if err != nil {
30
+
return nil, err
31
+
}
32
+
33
+
key = new([32]byte)
34
+
35
+
if len(profile.PublicKeys) < 1 ||
36
+
profile.PublicKeys[0] == nil ||
37
+
copy(key[:], profile.PublicKeys[0].Key) != 32 {
38
+
return nil, fmt.Errorf(
39
+
"record %s/%s has invalid format",
40
+
id.DID.String(), record.Cid,
41
+
)
42
+
}
43
+
44
+
return key, nil
15
45
}
16
46
17
-
type KeyManager struct{}
47
+
func (k *KeyManager) fetchProfile(ctx context.Context, id *identity.Identity) (*atproto.RepoGetRecord_Output, *atyo.ActorProfile, error) {
48
+
record, err := atproto.RepoGetRecord(
49
+
ctx,
50
+
// hmm should this be a relay instance or something? Need to check this
51
+
// when fetching a record for someone with a different PDS (ugh which means
52
+
// I need an account that uses a different PDS which maybe means selfhosting)
53
+
k.client,
54
+
"",
55
+
api.ActorProfileNSID,
56
+
id.DID.String(),
57
+
api.SelfActorProfile,
58
+
)
59
+
if err != nil {
60
+
return nil, nil, err
61
+
}
18
62
19
-
// TODO: get pubkey from atproto profile; probably needs a new lexicon
20
-
// also maybe if there are multiple records we should do one for each?
21
-
func (*KeyManager) fetchUserPubKey(id *identity.Identity) (key [32]byte, err error) {
22
-
return
63
+
profile, ok := record.Value.Val.(*atyo.ActorProfile)
64
+
if !ok {
65
+
b, err := record.Value.MarshalJSON()
66
+
return nil, nil, fmt.Errorf(
67
+
"failed to deserialize record: %w, JSON repr: `%s`",
68
+
err, b,
69
+
)
70
+
}
71
+
return record, profile, nil
23
72
}
24
73
25
74
// TODO: load from local DB or something like that...
26
75
// There should probably be expiration dates associated with these too
27
-
func (*KeyManager) fetchPrivKey() (key [32]byte, err error) {
28
-
return
76
+
func (k *KeyManager) fetchPrivKey() (key *[32]byte, err error) {
77
+
if k.selfPrivateKey != nil {
78
+
return k.selfPrivateKey, nil
79
+
}
80
+
81
+
return nil, fmt.Errorf("no private key set, maybe login required?")
82
+
}
83
+
84
+
func (k *KeyManager) genAndPublishKeyPair(ctx context.Context, did string) error {
85
+
selfPubKey, selfPrivKey, err := box.GenerateKey(rand.Reader)
86
+
if err != nil {
87
+
panic(err)
88
+
}
89
+
90
+
// ugh, generated API type doesn't work for swapping because it needs an
91
+
// `omitempty` on the SwapRecord field. Two approaches here; manually impl
92
+
// the XRPC or fetch a CID and pass it along if found. the latter seems easier
93
+
// for now (and is also what tangled.sh appears to do).
94
+
95
+
var swapRecord *string
96
+
existing, _ := atproto.RepoGetRecord(ctx, k.client, "", api.ActorProfileNSID, did, api.SelfActorProfile)
97
+
if existing != nil {
98
+
swapRecord = existing.Cid
99
+
}
100
+
101
+
_, err = atproto.RepoPutRecord(
102
+
ctx,
103
+
k.client,
104
+
&atproto.RepoPutRecord_Input{
105
+
Collection: api.ActorProfileNSID,
106
+
Repo: did,
107
+
Rkey: api.SelfActorProfile,
108
+
SwapRecord: swapRecord,
109
+
Record: &util.LexiconTypeDecoder{
110
+
Val: &atyo.ActorProfile{
111
+
PublicKeys: []*atyo.ActorProfile_PublicKeys_Elem{
112
+
{
113
+
CreatedAt: syntax.DatetimeNow().String(),
114
+
Key: util.LexBytes(selfPubKey[:]),
115
+
// TODO reasonable expiration, idk like 1mo?
116
+
},
117
+
},
118
+
},
119
+
},
120
+
},
121
+
)
122
+
if err != nil {
123
+
return err
124
+
}
125
+
126
+
fmt.Fprintln(os.Stderr, "created pubkey record")
127
+
128
+
k.selfPrivateKey = selfPrivKey
129
+
return nil
29
130
}
+19
-7
appview/login.go
+19
-7
appview/login.go
···
37
37
38
38
fmt.Fprintln(os.Stderr, "sending login request to "+s.client.Host)
39
39
40
-
result, err := atproto.ServerCreateSession(
40
+
loginResult, err := atproto.ServerCreateSession(
41
41
req.Context(),
42
42
s.client,
43
43
&atproto.ServerCreateSession_Input{
···
52
52
}
53
53
54
54
s.client.Auth = &xrpc.AuthInfo{
55
-
AccessJwt: result.AccessJwt,
56
-
RefreshJwt: result.RefreshJwt, // TODO persist in db or something?
57
-
Handle: result.Handle,
58
-
Did: result.Did,
55
+
AccessJwt: loginResult.AccessJwt,
56
+
RefreshJwt: loginResult.RefreshJwt, // TODO persist in db or something?
57
+
Handle: loginResult.Handle,
58
+
Did: loginResult.Did,
59
59
}
60
60
61
-
fmt.Fprintln(os.Stderr, "logged into account: ")
61
+
// for now, just create a key and publish it on every login. In the future
62
+
// this private key should live in sqlite db instead or something,
63
+
// so we don't need to republish it every time lmao. Also we should in theory
64
+
// make this more of an update than a replace but it's fine for now
62
65
63
-
resp, err := json.Marshal(result)
66
+
if err = s.keys.genAndPublishKeyPair(req.Context(), loginResult.Did); err != nil {
67
+
http.Error(
68
+
w,
69
+
"Failed to create profile record: "+err.Error(),
70
+
http.StatusInternalServerError,
71
+
)
72
+
73
+
}
74
+
75
+
resp, err := json.Marshal(loginResult)
64
76
if err != nil {
65
77
http.Error(w, "failed to re-serialize login response", http.StatusInternalServerError)
66
78
}
+8
-3
appview/router.go
+8
-3
appview/router.go
···
4
4
"net/http"
5
5
6
6
"github.com/bluesky-social/indigo/atproto/identity"
7
+
"github.com/bluesky-social/indigo/atproto/syntax"
7
8
"github.com/bluesky-social/indigo/xrpc"
8
9
)
9
10
···
11
12
directory *identity.BaseDirectory
12
13
keys *KeyManager
13
14
client *xrpc.Client
15
+
tidClock syntax.TIDClock
14
16
}
15
17
16
18
func NewServer() *Server {
19
+
client := &xrpc.Client{}
17
20
return &Server{
18
21
directory: &identity.BaseDirectory{},
19
-
client: &xrpc.Client{},
22
+
client: client,
23
+
keys: &KeyManager{
24
+
client: client,
25
+
},
26
+
tidClock: syntax.NewTIDClock(0), // this int needs to be unique per instance-ish
20
27
}
21
28
}
22
29
···
28
35
29
36
// app functionality
30
37
mux.HandleFunc("POST /ping/{target}", r.sendPing)
31
-
mux.HandleFunc("PUT /self/key", r.addPublicKey)
32
-
mux.HandleFunc("DELETE /self/key", r.removePublicKey)
33
38
34
39
mux.HandleFunc("POST /login", r.login)
35
40
+21
-21
appview/send_ping.go
+21
-21
appview/send_ping.go
···
2
2
3
3
import (
4
4
"bytes"
5
+
"context"
5
6
"crypto/rand"
6
-
"encoding/hex"
7
7
"encoding/json"
8
8
"fmt"
9
9
"net/http"
10
10
"os"
11
11
12
+
"atyo.app/api"
12
13
"atyo.app/api/atyo"
14
+
"github.com/bluesky-social/indigo/api/atproto"
13
15
"github.com/bluesky-social/indigo/atproto/identity"
14
16
"github.com/bluesky-social/indigo/atproto/syntax"
15
17
"github.com/bluesky-social/indigo/lex/util"
···
33
35
http.Error(w, hErr.Message, hErr.Code)
34
36
}
35
37
36
-
ping, hErr := s.buildPing(targetId)
38
+
ping, hErr := s.buildPing(req.Context(), targetId)
37
39
if hErr != nil {
38
40
http.Error(w, hErr.Error(), hErr.Code)
39
41
return
···
49
51
return
50
52
}
51
53
52
-
cborBuf := bytes.NewBuffer([]byte{})
53
-
err = ping.MarshalCBOR(cborBuf)
54
-
if err == nil {
55
-
fmt.Fprintln(os.Stderr, "ping length as CBOR:", cborBuf.Len())
56
-
} else {
57
-
fmt.Fprintln(os.Stderr, "failed to marshal as CBOR", err)
58
-
}
59
-
60
-
fmt.Fprintln(os.Stderr, hex.EncodeToString(cborBuf.Bytes()))
61
-
62
-
fmt.Fprintln(
63
-
os.Stderr,
64
-
"would write ping to PDS: "+string(jsonPing),
54
+
_, err = atproto.RepoPutRecord(
55
+
req.Context(),
56
+
s.client,
57
+
&atproto.RepoPutRecord_Input{
58
+
Collection: api.PingNSID,
59
+
Record: &util.LexiconTypeDecoder{Val: ping},
60
+
Repo: s.client.Auth.Did,
61
+
Rkey: s.tidClock.Next().String(),
62
+
},
65
63
)
66
-
67
-
// TODO write records to our PDS
64
+
if err != nil {
65
+
http.Error(w, "failed to put record: "+err.Error(), http.StatusInternalServerError)
66
+
return
67
+
}
68
68
69
69
_, err = w.Write(jsonPing)
70
70
if err != nil {
···
76
76
//
77
77
// Roughly based on the Scuttlebutt protocol for pivate messages:
78
78
// https://ssbc.github.io/scuttlebutt-protocol-guide/#private-messages
79
-
func (s *Server) buildPing(target *identity.Identity) (*atyo.Ping, *httpError) {
79
+
func (s *Server) buildPing(ctx context.Context, target *identity.Identity) (*atyo.Ping, *httpError) {
80
80
// NOTE: CBOR marshaller needs to serialize Ping_Empty{} directly,
81
81
// while the JSON serializer needs Ping_Contents{}. Just an odd quirk
82
82
emptyPing := &atyo.Ping_Empty{}
···
104
104
}
105
105
}
106
106
107
-
targetPubkey, err := s.keys.fetchUserPubKey(target)
107
+
targetPubkey, err := s.keys.fetchUserPubKey(ctx, target)
108
108
if err != nil {
109
109
return nil, &httpError{
110
110
http.StatusNotFound,
111
-
"Target `" + target.Handle.String() + "` does not have an atyo.app identity",
111
+
"Could not find atyo.app id for `" + target.Handle.String() + "`: " + err.Error(),
112
112
}
113
113
}
114
114
···
122
122
123
123
encryptedMessage := secretbox.Seal(nil, secretMessageBytes, &nonce, &sharedSecretKey)
124
124
125
-
encSharedKey := box.Seal(nil, sharedSecretKey[:], &nonce, &targetPubkey, ephemeralPrivKey)
125
+
encSharedKey := box.Seal(nil, sharedSecretKey[:], &nonce, targetPubkey, ephemeralPrivKey)
126
126
127
127
// possible future enhancement: use a high few bits on the first byte here to indicate
128
128
// CBOR/JSON encoding, or a schema version? idk if that's really useful for any reason
+2
gen/main.go
+2
gen/main.go
+43
lexicons/actor/profile.json
+43
lexicons/actor/profile.json
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "app.atyo.actor.profile",
4
+
"defs": {
5
+
"main": {
6
+
"type": "record",
7
+
"description": "Information about a user's profile. At least one public key is required, since otherwise there's no way to send pings to the user.",
8
+
"key": "literal:self",
9
+
"record": {
10
+
"type": "object",
11
+
"required": [
12
+
"publicKeys"
13
+
],
14
+
"properties": {
15
+
"publicKeys": {
16
+
"type": "array",
17
+
"minLength": "1",
18
+
"items": {
19
+
"type": "object",
20
+
"required": [
21
+
"key",
22
+
"createdAt"
23
+
],
24
+
"properties": {
25
+
"key": {
26
+
"type": "bytes"
27
+
},
28
+
"createdAt": {
29
+
"type": "string",
30
+
"format": "datetime"
31
+
},
32
+
"expiresAt": {
33
+
"type": "string",
34
+
"format": "datetime"
35
+
}
36
+
}
37
+
}
38
+
}
39
+
}
40
+
}
41
+
}
42
+
}
43
+
}
-20
lexicons/public_key.json
-20
lexicons/public_key.json
···
1
-
{
2
-
"lexicon": 1,
3
-
"id": "app.atyo.publicKey",
4
-
"defs": {
5
-
"main": {
6
-
"type": "record",
7
-
"description": "A public key that can be used to encrypt `app.atyo.ping`s directed to this user",
8
-
"key": "tid",
9
-
"record": {
10
-
"type": "object",
11
-
"required": ["key", "createdAt"],
12
-
"properties": {
13
-
"key": { "type": "bytes" },
14
-
"createdAt": { "type": "string", "format": "datetime" },
15
-
"expiresAt": { "type": "string", "format": "datetime" }
16
-
}
17
-
}
18
-
}
19
-
}
20
-
}