Scratch space for learning atproto app development

Add comments

Changed files
+44 -11
src
+13 -11
src/index.ts
···
import { createResolver, Resolver } from '#/firehose/resolver'
import type { Database } from '#/db'
+
// Application state passed to the router and elsewhere
export type AppContext = {
db: Database
ingester: Ingester
···
static async create() {
const { NODE_ENV, HOST, PORT, DB_PATH } = env
+
const logger = pino({ name: 'server start' })
-
const logger = pino({ name: 'server start' })
+
// Set up the SQLite database
const db = createDb(DB_PATH)
await migrateToLatest(db)
-
const ingester = new Ingester(db)
+
+
// Create the atproto utilities
const oauthClient = await createClient(db)
+
const ingester = new Ingester(db)
const resolver = createResolver()
-
ingester.start()
const ctx = {
db,
ingester,
···
resolver,
}
-
const app: Express = express()
+
// Subscribe to events on the firehose
+
ingester.start()
-
// Set the application to trust the reverse proxy
+
// Create our server
+
const app: Express = express()
app.set('trust proxy', true)
-
// Middlewares
+
// Routes & middlewares
+
const router = createRouter(ctx)
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
-
-
// Routes
-
const router = createRouter(ctx)
app.use(router)
-
-
// Error handlers
app.use((_req, res) => res.sendStatus(404))
+
// Bind our server to the port
const server = app.listen(env.PORT)
await events.once(server, 'listening')
logger.info(`Server (${NODE_ENV}) running on port http://${HOST}:${PORT}`)
+31
src/routes.ts
···
import { page } from '#/lib/view'
import * as Status from '#/lexicon/types/com/example/status'
+
// Helper function for defining routes
const handler =
(fn: express.Handler) =>
async (
···
export const createRouter = (ctx: AppContext) => {
const router = express.Router()
+
// Static assets
router.use('/public', express.static(path.join(__dirname, 'pages', 'public')))
+
// OAuth metadata
router.get(
'/client-metadata.json',
handler((_req, res) => {
···
})
)
+
// OAuth callback to complete session creation
router.get(
'/oauth/callback',
handler(async (req, res) => {
···
})
)
+
// Login page
router.get(
'/login',
handler(async (_req, res) => {
···
})
)
+
// Login handler
router.post(
'/login',
handler(async (req, res) => {
+
// Validate
const handle = req.body?.handle
if (typeof handle !== 'string' || !isValidHandle(handle)) {
return res.type('html').send(page(login({ error: 'invalid handle' })))
}
+
+
// Initiate the OAuth flow
try {
const url = await ctx.oauthClient.authorize(handle)
return res.redirect(url.toString())
···
})
)
+
// Logout handler
router.post(
'/logout',
handler(async (req, res) => {
···
})
)
+
// Homepage
router.get(
'/',
handler(async (req, res) => {
+
// If the user is signed in, get an agent which communicates with their server
const agent = await getSessionAgent(req, res, ctx)
+
+
// Fetch data stored in our SQLite
const statuses = await ctx.db
.selectFrom('status')
.selectAll()
···
.where('authorDid', '=', agent.accountDid)
.executeTakeFirst()
: undefined
+
+
// Map user DIDs to their domain-name handles
const didHandleMap = await ctx.resolver.resolveDidsToHandles(
statuses.map((s) => s.authorDid)
)
+
if (!agent) {
+
// Serve the logged-out view
return res.type('html').send(page(home({ statuses, didHandleMap })))
}
+
+
// Fetch additional information about the logged-in user
const { data: profile } = await agent.getProfile({
actor: agent.accountDid,
})
+
didHandleMap[profile.handle] = agent.accountDid
+
+
// Serve the logged-in view
return res
.type('html')
.send(page(home({ statuses, didHandleMap, profile, myStatus })))
})
)
+
// "Set status" handler
router.post(
'/status',
handler(async (req, res) => {
+
// If the user is signed in, get an agent which communicates with their server
const agent = await getSessionAgent(req, res, ctx)
if (!agent) {
return res.status(401).json({ error: 'Session required' })
}
+
// Construct & validate their status record
const record = {
$type: 'com.example.status',
status: req.body?.status,
···
}
try {
+
// Write the status record to the user's repository
await agent.com.atproto.repo.putRecord({
repo: agent.accountDid,
collection: 'com.example.status',
···
}
try {
+
// Optimistically update our SQLite
+
// This isn't strictly necessary because the write event will be
+
// handled in #/firehose/ingestor.ts, but it ensures that future reads
+
// will be up-to-date after this method finishes.
await ctx.db
.insertInto('status')
.values({