Uses atcute to show how you can do upserts with atproto records
1import 'dotenv/config';
2import { Client, CredentialManager, ok } from '@atcute/client';
3import * as TID from '@atcute/tid';
4
5//You want to make sure this is always the same for the applications you are generating upsert tids for
6//If you use the same timestamp but a different clock id, you will get different tids
7const CLOCK_ID = 23;
8
9const logErrorAndThrow = (err) => {
10 console.error(err);
11 throw err;
12};
13
14
15//Loading in env variables can ignore
16const handle = process.env.HANDLE;
17if (!handle) logErrorAndThrow('HANDLE is not set');
18const appPassword = process.env.APP_PASSWORD;
19if (!appPassword) logErrorAndThrow('APP_PASSWORD is not set');
20const pdsHost = process.env.PDS_HOST;
21if (!pdsHost) logErrorAndThrow('PDS_HOST is not set');
22
23
24//Sets up the atcute client
25const manager = new CredentialManager({ service: pdsHost });
26const rpc = new Client({ handler: manager });
27
28// sign in with handle/email and password (or app password (please be an app password))
29await manager.login({ identifier: handle, password: appPassword});
30
31
32
33// The actual example starts here, everything else before is boilerplate
34
35
36//reversing your handle to make a fun example lexicon
37const reversedHandle = handle.split('.').reverse().map(word => word).join('.');
38//An activity here is like a workout. running, walking, lifting weights, etc.
39let collection = `${reversedHandle}.feed.activity`
40
41//A list of activities that may be gotten from your phone or wherever, but you get the whole list everytime
42let activities = []
43
44// Helper function to upload activities
45const uploadActivities = async (activities, handle, collection, rpc) => {
46
47 for (const activity of activities) {
48
49 //Creates that unique key from the startTime of the activity so we don't have duplicates
50 let rKey = TID.create(activity.startTime.getTime() * 1000, CLOCK_ID);
51
52 await ok(rpc.post('com.atproto.repo.putRecord', {
53 input: {
54 repo: handle,
55 collection,
56 rkey: rKey,
57 record: activity,
58
59 }
60 }));
61 console.log(`Uploaded activity with rkey: ${rKey}`);
62 }
63
64}
65
66
67//I just finished a run, it's saved to my phone, now uploading it to the PDS
68activities.push({
69 $type: collection,
70 type: 'run',
71 startTime: new Date()
72})
73
74console.log('You just finished a run. Uploading it to the PDS.');
75
76//Upload my activities
77await uploadActivities(activities, handle, collection, rpc);
78
79//Going to generate the key here to show you can find the record from it easily if you have a date
80const rkey = TID.create(activities[0].startTime.getTime() * 1000, CLOCK_ID);
81
82const activityFromPDS = await ok(rpc.get('com.atproto.repo.getRecord', {
83 params: {
84 repo: handle,
85 collection,
86 rkey,
87 }
88}));
89
90console.log(`The PDS shows you went on a ${activityFromPDS.value.type} at ${activityFromPDS.value.startTime.toLocaleString()}.`);
91
92
93//I decide I want to go on a walk later, so I add it to the list
94activities.push({
95 $type: collection,
96 type: 'walk',
97 startTime: new Date()
98})
99
100console.log('You just finished a walk. Uploading it to the PDS.');
101
102//upload the new activities so the newest one can sync
103await uploadActivities(activities, handle, collection, rpc);
104
105const listResult = await ok(rpc.get('com.atproto.repo.listRecords', {
106 params: {
107 repo: handle,
108 collection,
109 limit: 10,
110 }
111}));
112
113
114console.log("Since you did an upsert you should only have 2 records even tho you uploaded 3.")
115console.log(`You have ${listResult.records.length} activities saved in the PDS.`);
116for (const recordIndex in listResult.records){
117 const record = listResult.records[recordIndex]
118 console.log(`The PDS shows you went on a ${record.value.type} at ${record.value.startTime.toLocaleString()}.`);
119}