+1
.gitignore
+1
.gitignore
···
···
1
+
.env
+66
README.md
+66
README.md
···
···
1
+
# Chicago ATProto Meetup Activities
2
+
3
+
Quick hands-on activities to get started with the AT Protocol and Bluesky API.
4
+
5
+
## Setup (5 minutes)
6
+
7
+
1. **Create a Bluesky account** at https://bsky.app
8
+
2. **Generate an app password**: Settings → App Passwords → Add
9
+
3. **Install Bun** (if needed): `curl -fsSL https://bun.sh/install | bash`
10
+
4. **Clone this repo** and create `.env` file:
11
+
```
12
+
BSKY_USERNAME=your-handle.bsky.social
13
+
BSKY_PASSWORD=xxxx-xxxx-xxxx-xxxx
14
+
```
15
+
16
+
## Activities
17
+
18
+
### Activity 1a: Hello World (5 minutes)
19
+
```bash
20
+
bun activities/01a-hello-world.js
21
+
```
22
+
Posts a simple message to Bluesky.
23
+
24
+
### Activity 1b: Emoji Art Generator (10 minutes)
25
+
```bash
26
+
bun activities/01b-emoji-art.js
27
+
```
28
+
Creates a random emoji art grid and posts it.
29
+
30
+
### Activity 2: Rich Text with Links & Hashtags (10 minutes)
31
+
```bash
32
+
bun activities/02-rich-text.js
33
+
```
34
+
Demonstrates auto-detection of links and hashtags in posts.
35
+
36
+
### Activity 3: Feed Generator Metadata (15 minutes)
37
+
```bash
38
+
bun activities/03-feed-generator.js
39
+
```
40
+
Creates metadata for a custom feed (note: actual feed requires a server).
41
+
42
+
### Activity 4: Profile Updater (10 minutes)
43
+
```bash
44
+
bun activities/04-profile-updater.js
45
+
```
46
+
Updates your profile description with a timestamp.
47
+
48
+
## Starter Template
49
+
50
+
Use `starter-template.js` to build your own ideas:
51
+
```bash
52
+
bun starter-template.js
53
+
```
54
+
55
+
## Tips
56
+
57
+
- All scripts use Bun's built-in TypeScript support
58
+
- The AT Protocol API docs: https://atproto.com
59
+
- View your posts at: https://bsky.app/profile/YOUR-HANDLE
60
+
- Use `#ATProtoChicago` to find other meetup participants!
61
+
62
+
## Common Issues
63
+
64
+
- **"Invalid identifier or password"**: Make sure you're using an app password, not your main password
65
+
- **Rate limits**: The API has rate limits, wait a minute if you hit them
66
+
- **Module not found**: Run the scripts from the repo root directory
+28
activities/01a-hello-world.js
+28
activities/01a-hello-world.js
···
···
1
+
#!/usr/bin/env bun
2
+
import { BskyAgent } from '@atproto/api'
3
+
4
+
// Read credentials from .env file
5
+
const username = process.env.BSKY_USERNAME
6
+
const password = process.env.BSKY_PASSWORD
7
+
8
+
if (!username || !password) {
9
+
console.error('Please provide BSKY_USERNAME and BSKY_PASSWORD in .env file')
10
+
process.exit(1)
11
+
}
12
+
13
+
const agent = new BskyAgent({ service: 'https://bsky.social' })
14
+
15
+
console.log('Logging in...')
16
+
await agent.login({
17
+
identifier: username,
18
+
password: password
19
+
})
20
+
21
+
console.log('Connected! Your DID:', agent.session?.did)
22
+
23
+
const post = await agent.post({
24
+
text: 'Hello from the Chicago ATProto meetup! 🚀'
25
+
})
26
+
27
+
console.log('Posted successfully!')
28
+
console.log('View your post at:', `https://bsky.app/profile/${username}/post/${post.uri.split('/').pop()}`)
+40
activities/01b-emoji-art.js
+40
activities/01b-emoji-art.js
···
···
1
+
#!/usr/bin/env bun
2
+
import { BskyAgent } from '@atproto/api'
3
+
4
+
// Read credentials from .env
5
+
const username = process.env.BSKY_USERNAME
6
+
const password = process.env.BSKY_PASSWORD
7
+
8
+
if (!username || !password) {
9
+
console.error('Please provide BSKY_USERNAME and BSKY_PASSWORD in .env file')
10
+
process.exit(1)
11
+
}
12
+
13
+
const agent = new BskyAgent({ service: 'https://bsky.social' })
14
+
15
+
console.log('Logging in...')
16
+
await agent.login({
17
+
identifier: username,
18
+
password: password
19
+
})
20
+
21
+
// Generate emoji art
22
+
const emojis = ['🎨', '🌟', '🔥', '💫', '🌈', '❄️', '🎭', '🎪', '✨', '🎯', '🚀', '💎']
23
+
const gridSize = 6
24
+
const grid = Array(gridSize).fill(null).map(() =>
25
+
Array(gridSize).fill(null).map(() =>
26
+
emojis[Math.floor(Math.random() * emojis.length)]
27
+
).join('')
28
+
).join('\n')
29
+
30
+
const postText = `Generated emoji art at ${new Date().toLocaleTimeString()}:\n\n${grid}\n\n#ATProtoChicago`
31
+
32
+
console.log('Posting emoji art...')
33
+
console.log(postText)
34
+
35
+
const post = await agent.post({
36
+
text: postText
37
+
})
38
+
39
+
console.log('\nPosted successfully!')
40
+
console.log('View your post at:', `https://bsky.app/profile/${username}/post/${post.uri.split('/').pop()}`)
+40
activities/02-rich-text.js
+40
activities/02-rich-text.js
···
···
1
+
#!/usr/bin/env bun
2
+
import { BskyAgent, RichText } from '@atproto/api'
3
+
4
+
// Read credentials from .env
5
+
const username = process.env.BSKY_USERNAME
6
+
const password = process.env.BSKY_PASSWORD
7
+
8
+
if (!username || !password) {
9
+
console.error('Please provide BSKY_USERNAME and BSKY_PASSWORD in .env file')
10
+
process.exit(1)
11
+
}
12
+
13
+
const agent = new BskyAgent({ service: 'https://bsky.social' })
14
+
15
+
console.log('Logging in...')
16
+
await agent.login({
17
+
identifier: username,
18
+
password: password
19
+
})
20
+
21
+
// Create rich text with auto-detected links and hashtags
22
+
const rt = new RichText({
23
+
text: 'Check out the ATProto docs at https://atproto.com! 🔗\n\nBuilding with the AT Protocol at the Chicago meetup! 🚀\n\n#ATProtoChicago #BuildingOnATProto'
24
+
})
25
+
26
+
console.log('Detecting facets (links and hashtags)...')
27
+
await rt.detectFacets(agent)
28
+
29
+
console.log('\nRich text details:')
30
+
console.log('Text:', rt.text)
31
+
console.log('Facets:', JSON.stringify(rt.facets, null, 2))
32
+
33
+
console.log('\nPosting with rich text...')
34
+
const post = await agent.post({
35
+
text: rt.text,
36
+
facets: rt.facets,
37
+
})
38
+
39
+
console.log('\nPosted successfully!')
40
+
console.log('View your post at:', `https://bsky.app/profile/${username}/post/${post.uri.split('/').pop()}`)
+72
activities/03-feed-generator.js
+72
activities/03-feed-generator.js
···
···
1
+
#!/usr/bin/env bun
2
+
import { BskyAgent } from '@atproto/api'
3
+
4
+
// Read credentials from .env
5
+
const username = process.env.BSKY_USERNAME
6
+
const password = process.env.BSKY_PASSWORD
7
+
8
+
if (!username || !password) {
9
+
console.error('Please provide BSKY_USERNAME and BSKY_PASSWORD in .env file')
10
+
process.exit(1)
11
+
}
12
+
13
+
const agent = new BskyAgent({ service: 'https://bsky.social' })
14
+
15
+
console.log('Logging in...')
16
+
await agent.login({
17
+
identifier: username,
18
+
password: password
19
+
})
20
+
21
+
console.log('Connected! Your DID:', agent.session?.did)
22
+
23
+
// Create a simple feed generator record
24
+
// Note: This creates the metadata for a feed, but you'd need a server to actually generate the feed content
25
+
const feedRecord = {
26
+
did: `did:plc:${agent.session.did.split(':')[2]}`, // Use your actual DID
27
+
displayName: 'Chicago ATProto Meetup',
28
+
description: 'Posts from our Chicago ATProto meetup! Tag your posts with #ATProtoChicago',
29
+
avatar: undefined,
30
+
createdAt: new Date().toISOString()
31
+
}
32
+
33
+
const rkey = 'chicago-meetup-feed'
34
+
35
+
console.log('\nCreating feed generator record...')
36
+
console.log('Feed details:', JSON.stringify(feedRecord, null, 2))
37
+
38
+
try {
39
+
// First check if it already exists
40
+
const existing = await agent.com.atproto.repo.getRecord({
41
+
repo: agent.session.did,
42
+
collection: 'app.bsky.feed.generator',
43
+
rkey: rkey
44
+
}).catch(() => null)
45
+
46
+
if (existing) {
47
+
console.log('\nFeed already exists! Updating...')
48
+
await agent.com.atproto.repo.putRecord({
49
+
repo: agent.session.did,
50
+
collection: 'app.bsky.feed.generator',
51
+
rkey: rkey,
52
+
record: feedRecord,
53
+
swapRecord: existing.data.cid
54
+
})
55
+
} else {
56
+
await agent.com.atproto.repo.putRecord({
57
+
repo: agent.session.did,
58
+
collection: 'app.bsky.feed.generator',
59
+
rkey: rkey,
60
+
record: feedRecord
61
+
})
62
+
}
63
+
64
+
console.log('\nFeed generator record created successfully!')
65
+
console.log(`\nNote: This creates the feed metadata. To make it functional, you would need:`)
66
+
console.log('1. A server running the feed generation logic')
67
+
console.log('2. The feed to be published and indexed by Bluesky')
68
+
console.log(`\nYour feed URI: at://${agent.session.did}/app.bsky.feed.generator/${rkey}`)
69
+
70
+
} catch (error) {
71
+
console.error('Error creating feed:', error.message)
72
+
}
+71
activities/04-profile-updater.js
+71
activities/04-profile-updater.js
···
···
1
+
#!/usr/bin/env bun
2
+
import { BskyAgent } from '@atproto/api'
3
+
4
+
// Read credentials from .env
5
+
const username = process.env.BSKY_USERNAME
6
+
const password = process.env.BSKY_PASSWORD
7
+
8
+
if (!username || !password) {
9
+
console.error('Please provide BSKY_USERNAME and BSKY_PASSWORD in .env file')
10
+
process.exit(1)
11
+
}
12
+
13
+
const agent = new BskyAgent({ service: 'https://bsky.social' })
14
+
15
+
console.log('Logging in...')
16
+
await agent.login({
17
+
identifier: username,
18
+
password: password
19
+
})
20
+
21
+
console.log('Fetching current profile...')
22
+
const profile = await agent.getProfile({ actor: agent.session.did })
23
+
24
+
console.log('\nCurrent profile:')
25
+
console.log('Display name:', profile.data.displayName || '(not set)')
26
+
console.log('Description:', profile.data.description || '(not set)')
27
+
28
+
// Simple approach: just add a timestamp to the description
29
+
const timestamp = `[Updated at Chicago Meetup ${new Date().toLocaleTimeString()}]`
30
+
const currentDescription = profile.data.description || ''
31
+
const newDescription = currentDescription.includes('[Updated at Chicago Meetup')
32
+
? currentDescription.replace(/\[Updated at Chicago Meetup .*?\]/, timestamp)
33
+
: `${currentDescription}\n\n${timestamp}`.trim()
34
+
35
+
console.log('\nUpdating profile description...')
36
+
console.log('New description:', newDescription)
37
+
38
+
try {
39
+
// Use the upsertProfile method if available
40
+
if (agent.upsertProfile) {
41
+
await agent.upsertProfile((existing) => ({
42
+
...existing,
43
+
displayName: profile.data.displayName,
44
+
description: newDescription
45
+
}))
46
+
} else {
47
+
// Fallback to manual update
48
+
const profileRecord = {
49
+
$type: 'app.bsky.actor.profile',
50
+
displayName: profile.data.displayName || '',
51
+
description: newDescription
52
+
}
53
+
54
+
// Copy avatar and banner if they exist
55
+
if (profile.data.avatar) profileRecord.avatar = profile.data.avatar
56
+
if (profile.data.banner) profileRecord.banner = profile.data.banner
57
+
58
+
await agent.com.atproto.repo.putRecord({
59
+
repo: agent.session.did,
60
+
collection: 'app.bsky.actor.profile',
61
+
rkey: 'self',
62
+
record: profileRecord
63
+
})
64
+
}
65
+
66
+
console.log('\nProfile updated successfully!')
67
+
console.log(`View your profile at: https://bsky.app/profile/${username}`)
68
+
} catch (error) {
69
+
console.error('Error updating profile:', error)
70
+
console.error('Details:', error.message)
71
+
}
+34
starter-template.js
+34
starter-template.js
···
···
1
+
#!/usr/bin/env bun
2
+
import { BskyAgent } from '@atproto/api'
3
+
4
+
// Read credentials from .env file
5
+
const username = process.env.BSKY_USERNAME
6
+
const password = process.env.BSKY_PASSWORD
7
+
8
+
if (!username || !password) {
9
+
console.error('Please provide BSKY_USERNAME and BSKY_PASSWORD in .env file')
10
+
console.error('Format:')
11
+
console.error('BSKY_USERNAME=your-handle.bsky.social')
12
+
console.error('BSKY_PASSWORD=xxxx-xxxx-xxxx-xxxx')
13
+
process.exit(1)
14
+
}
15
+
16
+
async function main() {
17
+
const agent = new BskyAgent({ service: 'https://bsky.social' })
18
+
19
+
await agent.login({
20
+
identifier: username,
21
+
password: password
22
+
})
23
+
24
+
console.log('Connected! Your DID:', agent.session?.did)
25
+
26
+
// Your code here!
27
+
// Try:
28
+
// - agent.post({ text: 'Hello world!' })
29
+
// - agent.getProfile({ actor: agent.session.did })
30
+
// - agent.getTimeline()
31
+
32
+
}
33
+
34
+
main().catch(console.error)