Experimenting with AT Protocol to hit up your friends

Actually lookup identities and write records!

Major milestone! We're writing ping records to the network, and using
the target's public key to do it!

+7
api/api.go
··· 6 6 // https://github.com/bluesky-social/indigo/pull/716 7 7 8 8 package api 9 + 10 + const ( 11 + ActorProfileNSID = "app.atyo.actor.profile" 12 + PingNSID = "app.atyo.ping" 13 + 14 + SelfActorProfile = "self" 15 + )
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 19 19 atyo.Ping_Location{}, 20 20 atyo.Ping_Contents{}, 21 21 atyo.PublicKey{}, 22 + atyo.ActorProfile_PublicKeys_Elem{}, 23 + atyo.ActorProfile{}, 22 24 ); err != nil { 23 25 panic(err) 24 26 }
+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
··· 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 - }