···11+# Chicago ATProto Meetup Activities
22+33+Quick hands-on activities to get started with the AT Protocol and Bluesky API.
44+55+## Setup (5 minutes)
66+77+1. **Create a Bluesky account** at https://bsky.app
88+2. **Generate an app password**: Settings → App Passwords → Add
99+3. **Install Bun** (if needed): `curl -fsSL https://bun.sh/install | bash`
1010+4. **Clone this repo** and create `.env` file:
1111+ ```
1212+ BSKY_USERNAME=your-handle.bsky.social
1313+ BSKY_PASSWORD=xxxx-xxxx-xxxx-xxxx
1414+ ```
1515+1616+## Activities
1717+1818+### Activity 1a: Hello World (5 minutes)
1919+```bash
2020+bun activities/01a-hello-world.js
2121+```
2222+Posts a simple message to Bluesky.
2323+2424+### Activity 1b: Emoji Art Generator (10 minutes)
2525+```bash
2626+bun activities/01b-emoji-art.js
2727+```
2828+Creates a random emoji art grid and posts it.
2929+3030+### Activity 2: Rich Text with Links & Hashtags (10 minutes)
3131+```bash
3232+bun activities/02-rich-text.js
3333+```
3434+Demonstrates auto-detection of links and hashtags in posts.
3535+3636+### Activity 3: Feed Generator Metadata (15 minutes)
3737+```bash
3838+bun activities/03-feed-generator.js
3939+```
4040+Creates metadata for a custom feed (note: actual feed requires a server).
4141+4242+### Activity 4: Profile Updater (10 minutes)
4343+```bash
4444+bun activities/04-profile-updater.js
4545+```
4646+Updates your profile description with a timestamp.
4747+4848+## Starter Template
4949+5050+Use `starter-template.js` to build your own ideas:
5151+```bash
5252+bun starter-template.js
5353+```
5454+5555+## Tips
5656+5757+- All scripts use Bun's built-in TypeScript support
5858+- The AT Protocol API docs: https://atproto.com
5959+- View your posts at: https://bsky.app/profile/YOUR-HANDLE
6060+- Use `#ATProtoChicago` to find other meetup participants!
6161+6262+## Common Issues
6363+6464+- **"Invalid identifier or password"**: Make sure you're using an app password, not your main password
6565+- **Rate limits**: The API has rate limits, wait a minute if you hit them
6666+- **Module not found**: Run the scripts from the repo root directory
+28
activities/01a-hello-world.js
···11+#!/usr/bin/env bun
22+import { BskyAgent } from '@atproto/api'
33+44+// Read credentials from .env file
55+const username = process.env.BSKY_USERNAME
66+const password = process.env.BSKY_PASSWORD
77+88+if (!username || !password) {
99+ console.error('Please provide BSKY_USERNAME and BSKY_PASSWORD in .env file')
1010+ process.exit(1)
1111+}
1212+1313+const agent = new BskyAgent({ service: 'https://bsky.social' })
1414+1515+console.log('Logging in...')
1616+await agent.login({
1717+ identifier: username,
1818+ password: password
1919+})
2020+2121+console.log('Connected! Your DID:', agent.session?.did)
2222+2323+const post = await agent.post({
2424+ text: 'Hello from the Chicago ATProto meetup! 🚀'
2525+})
2626+2727+console.log('Posted successfully!')
2828+console.log('View your post at:', `https://bsky.app/profile/${username}/post/${post.uri.split('/').pop()}`)
+40
activities/01b-emoji-art.js
···11+#!/usr/bin/env bun
22+import { BskyAgent } from '@atproto/api'
33+44+// Read credentials from .env
55+const username = process.env.BSKY_USERNAME
66+const password = process.env.BSKY_PASSWORD
77+88+if (!username || !password) {
99+ console.error('Please provide BSKY_USERNAME and BSKY_PASSWORD in .env file')
1010+ process.exit(1)
1111+}
1212+1313+const agent = new BskyAgent({ service: 'https://bsky.social' })
1414+1515+console.log('Logging in...')
1616+await agent.login({
1717+ identifier: username,
1818+ password: password
1919+})
2020+2121+// Generate emoji art
2222+const emojis = ['🎨', '🌟', '🔥', '💫', '🌈', '❄️', '🎭', '🎪', '✨', '🎯', '🚀', '💎']
2323+const gridSize = 6
2424+const grid = Array(gridSize).fill(null).map(() =>
2525+ Array(gridSize).fill(null).map(() =>
2626+ emojis[Math.floor(Math.random() * emojis.length)]
2727+ ).join('')
2828+).join('\n')
2929+3030+const postText = `Generated emoji art at ${new Date().toLocaleTimeString()}:\n\n${grid}\n\n#ATProtoChicago`
3131+3232+console.log('Posting emoji art...')
3333+console.log(postText)
3434+3535+const post = await agent.post({
3636+ text: postText
3737+})
3838+3939+console.log('\nPosted successfully!')
4040+console.log('View your post at:', `https://bsky.app/profile/${username}/post/${post.uri.split('/').pop()}`)
+40
activities/02-rich-text.js
···11+#!/usr/bin/env bun
22+import { BskyAgent, RichText } from '@atproto/api'
33+44+// Read credentials from .env
55+const username = process.env.BSKY_USERNAME
66+const password = process.env.BSKY_PASSWORD
77+88+if (!username || !password) {
99+ console.error('Please provide BSKY_USERNAME and BSKY_PASSWORD in .env file')
1010+ process.exit(1)
1111+}
1212+1313+const agent = new BskyAgent({ service: 'https://bsky.social' })
1414+1515+console.log('Logging in...')
1616+await agent.login({
1717+ identifier: username,
1818+ password: password
1919+})
2020+2121+// Create rich text with auto-detected links and hashtags
2222+const rt = new RichText({
2323+ text: 'Check out the ATProto docs at https://atproto.com! 🔗\n\nBuilding with the AT Protocol at the Chicago meetup! 🚀\n\n#ATProtoChicago #BuildingOnATProto'
2424+})
2525+2626+console.log('Detecting facets (links and hashtags)...')
2727+await rt.detectFacets(agent)
2828+2929+console.log('\nRich text details:')
3030+console.log('Text:', rt.text)
3131+console.log('Facets:', JSON.stringify(rt.facets, null, 2))
3232+3333+console.log('\nPosting with rich text...')
3434+const post = await agent.post({
3535+ text: rt.text,
3636+ facets: rt.facets,
3737+})
3838+3939+console.log('\nPosted successfully!')
4040+console.log('View your post at:', `https://bsky.app/profile/${username}/post/${post.uri.split('/').pop()}`)
+72
activities/03-feed-generator.js
···11+#!/usr/bin/env bun
22+import { BskyAgent } from '@atproto/api'
33+44+// Read credentials from .env
55+const username = process.env.BSKY_USERNAME
66+const password = process.env.BSKY_PASSWORD
77+88+if (!username || !password) {
99+ console.error('Please provide BSKY_USERNAME and BSKY_PASSWORD in .env file')
1010+ process.exit(1)
1111+}
1212+1313+const agent = new BskyAgent({ service: 'https://bsky.social' })
1414+1515+console.log('Logging in...')
1616+await agent.login({
1717+ identifier: username,
1818+ password: password
1919+})
2020+2121+console.log('Connected! Your DID:', agent.session?.did)
2222+2323+// Create a simple feed generator record
2424+// Note: This creates the metadata for a feed, but you'd need a server to actually generate the feed content
2525+const feedRecord = {
2626+ did: `did:plc:${agent.session.did.split(':')[2]}`, // Use your actual DID
2727+ displayName: 'Chicago ATProto Meetup',
2828+ description: 'Posts from our Chicago ATProto meetup! Tag your posts with #ATProtoChicago',
2929+ avatar: undefined,
3030+ createdAt: new Date().toISOString()
3131+}
3232+3333+const rkey = 'chicago-meetup-feed'
3434+3535+console.log('\nCreating feed generator record...')
3636+console.log('Feed details:', JSON.stringify(feedRecord, null, 2))
3737+3838+try {
3939+ // First check if it already exists
4040+ const existing = await agent.com.atproto.repo.getRecord({
4141+ repo: agent.session.did,
4242+ collection: 'app.bsky.feed.generator',
4343+ rkey: rkey
4444+ }).catch(() => null)
4545+4646+ if (existing) {
4747+ console.log('\nFeed already exists! Updating...')
4848+ await agent.com.atproto.repo.putRecord({
4949+ repo: agent.session.did,
5050+ collection: 'app.bsky.feed.generator',
5151+ rkey: rkey,
5252+ record: feedRecord,
5353+ swapRecord: existing.data.cid
5454+ })
5555+ } else {
5656+ await agent.com.atproto.repo.putRecord({
5757+ repo: agent.session.did,
5858+ collection: 'app.bsky.feed.generator',
5959+ rkey: rkey,
6060+ record: feedRecord
6161+ })
6262+ }
6363+6464+ console.log('\nFeed generator record created successfully!')
6565+ console.log(`\nNote: This creates the feed metadata. To make it functional, you would need:`)
6666+ console.log('1. A server running the feed generation logic')
6767+ console.log('2. The feed to be published and indexed by Bluesky')
6868+ console.log(`\nYour feed URI: at://${agent.session.did}/app.bsky.feed.generator/${rkey}`)
6969+7070+} catch (error) {
7171+ console.error('Error creating feed:', error.message)
7272+}
+71
activities/04-profile-updater.js
···11+#!/usr/bin/env bun
22+import { BskyAgent } from '@atproto/api'
33+44+// Read credentials from .env
55+const username = process.env.BSKY_USERNAME
66+const password = process.env.BSKY_PASSWORD
77+88+if (!username || !password) {
99+ console.error('Please provide BSKY_USERNAME and BSKY_PASSWORD in .env file')
1010+ process.exit(1)
1111+}
1212+1313+const agent = new BskyAgent({ service: 'https://bsky.social' })
1414+1515+console.log('Logging in...')
1616+await agent.login({
1717+ identifier: username,
1818+ password: password
1919+})
2020+2121+console.log('Fetching current profile...')
2222+const profile = await agent.getProfile({ actor: agent.session.did })
2323+2424+console.log('\nCurrent profile:')
2525+console.log('Display name:', profile.data.displayName || '(not set)')
2626+console.log('Description:', profile.data.description || '(not set)')
2727+2828+// Simple approach: just add a timestamp to the description
2929+const timestamp = `[Updated at Chicago Meetup ${new Date().toLocaleTimeString()}]`
3030+const currentDescription = profile.data.description || ''
3131+const newDescription = currentDescription.includes('[Updated at Chicago Meetup')
3232+ ? currentDescription.replace(/\[Updated at Chicago Meetup .*?\]/, timestamp)
3333+ : `${currentDescription}\n\n${timestamp}`.trim()
3434+3535+console.log('\nUpdating profile description...')
3636+console.log('New description:', newDescription)
3737+3838+try {
3939+ // Use the upsertProfile method if available
4040+ if (agent.upsertProfile) {
4141+ await agent.upsertProfile((existing) => ({
4242+ ...existing,
4343+ displayName: profile.data.displayName,
4444+ description: newDescription
4545+ }))
4646+ } else {
4747+ // Fallback to manual update
4848+ const profileRecord = {
4949+ $type: 'app.bsky.actor.profile',
5050+ displayName: profile.data.displayName || '',
5151+ description: newDescription
5252+ }
5353+5454+ // Copy avatar and banner if they exist
5555+ if (profile.data.avatar) profileRecord.avatar = profile.data.avatar
5656+ if (profile.data.banner) profileRecord.banner = profile.data.banner
5757+5858+ await agent.com.atproto.repo.putRecord({
5959+ repo: agent.session.did,
6060+ collection: 'app.bsky.actor.profile',
6161+ rkey: 'self',
6262+ record: profileRecord
6363+ })
6464+ }
6565+6666+ console.log('\nProfile updated successfully!')
6767+ console.log(`View your profile at: https://bsky.app/profile/${username}`)
6868+} catch (error) {
6969+ console.error('Error updating profile:', error)
7070+ console.error('Details:', error.message)
7171+}