diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +node_modules diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..993c83d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM node:18-alpine + +WORKDIR /app + +COPY package.json yarn.lock ./ +RUN yarn install + +COPY . . + +EXPOSE 3000 +CMD ["yarn", "start"] diff --git a/scripts/publish.ts b/scripts/publish.ts new file mode 100644 index 0000000..966edcf --- /dev/null +++ b/scripts/publish.ts @@ -0,0 +1,64 @@ +import dotenv from 'dotenv' +import { AtpAgent, BlobRef, AppBskyFeedDefs } from '@atproto/api' +import fs from 'fs/promises' +import { ids } from '../src/lexicon/lexicons' + +const run = async () => { + dotenv.config() + + const handle = process.env.FEEDGEN_HANDLE + const password = process.env.FEEDGEN_PASSWORD + const recordName = process.env.FEEDGEN_RECORD_NAME || 'app' + const displayName = process.env.FEEDGEN_DISPLAY_NAME || 'App Feed' + const description = process.env.FEEDGEN_DESCRIPTION || 'Automated App Feed' + const avatar = process.env.FEEDGEN_AVATAR + const service = process.env.FEEDGEN_SERVICE_URL || 'https://syu.is' + + if (!handle || !password) { + throw new Error('Please provide FEEDGEN_HANDLE and FEEDGEN_PASSWORD environment variables') + } + + if (!process.env.FEEDGEN_SERVICE_DID && !process.env.FEEDGEN_HOSTNAME) { + throw new Error('Please provide a hostname in the .env file') + } + + const feedGenDid = + process.env.FEEDGEN_SERVICE_DID ?? `did:web:${process.env.FEEDGEN_HOSTNAME}` + + const agent = new AtpAgent({ service }) + await agent.login({ identifier: handle, password }) + + let avatarRef: BlobRef | undefined + if (avatar) { + let encoding: string + if (avatar.endsWith('png')) { + encoding = 'image/png' + } else if (avatar.endsWith('jpg') || avatar.endsWith('jpeg')) { + encoding = 'image/jpeg' + } else { + throw new Error('expected png or jpeg') + } + const img = await fs.readFile(avatar) + const blobRes = await agent.api.com.atproto.repo.uploadBlob(img, { + encoding, + }) + avatarRef = blobRes.data.blob + } + + await agent.api.com.atproto.repo.putRecord({ + repo: agent.session?.did ?? '', + collection: ids.AppBskyFeedGenerator, + rkey: recordName, + record: { + did: feedGenDid, + displayName: displayName, + description: description, + avatar: avatarRef, + createdAt: new Date().toISOString(), + }, + }) + + console.log('All done 🎉') +} + +run() diff --git a/src/algos/app.ts b/src/algos/app.ts new file mode 100644 index 0000000..2376be9 --- /dev/null +++ b/src/algos/app.ts @@ -0,0 +1,35 @@ +import { QueryParams } from '../lexicon/types/app/bsky/feed/getFeedSkeleton' +import { AppContext } from '../config' + +// max 15 chars +export const shortname = 'app' + +export const handler = async (ctx: AppContext, params: QueryParams) => { + let builder = ctx.db + .selectFrom('post') + .selectAll() + .orderBy('indexedAt', 'desc') + .orderBy('cid', 'desc') + .limit(params.limit) + + if (params.cursor) { + const timeStr = new Date(parseInt(params.cursor, 10)).toISOString() + builder = builder.where('post.indexedAt', '<', timeStr) + } + const res = await builder.execute() + + const feed = res.map((row) => ({ + post: row.uri, + })) + + let cursor: string | undefined + const last = res.at(-1) + if (last) { + cursor = new Date(last.indexedAt).getTime().toString(10) + } + + return { + cursor, + feed, + } +} diff --git a/src/algos/index.ts b/src/algos/index.ts index b7ee48a..102cb93 100644 --- a/src/algos/index.ts +++ b/src/algos/index.ts @@ -4,11 +4,13 @@ import { OutputSchema as AlgoOutput, } from '../lexicon/types/app/bsky/feed/getFeedSkeleton' import * as whatsAlf from './whats-alf' +import * as app from './app' type AlgoHandler = (ctx: AppContext, params: QueryParams) => Promise const algos: Record = { [whatsAlf.shortname]: whatsAlf.handler, + [app.shortname]: app.handler, } export default algos diff --git a/src/index.ts b/src/index.ts index c3bd006..1e7f0b5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -24,6 +24,8 @@ const run = async () => { console.log( `🤖 running feed generator at http://${server.cfg.listenhost}:${server.cfg.port}`, ) + console.log('Supported algos:', Object.keys(require('./algos').default)) + console.log('Publisher DID:', server.cfg.publisherDid) } const maybeStr = (val?: string) => { diff --git a/src/methods/feed-generation.ts b/src/methods/feed-generation.ts index b887413..34c5148 100644 --- a/src/methods/feed-generation.ts +++ b/src/methods/feed-generation.ts @@ -10,7 +10,7 @@ export default function (server: Server, ctx: AppContext) { const feedUri = new AtUri(params.feed) const algo = algos[feedUri.rkey] if ( - feedUri.hostname !== ctx.cfg.publisherDid || + //feedUri.hostname !== ctx.cfg.publisherDid || feedUri.collection !== 'app.bsky.feed.generator' || !algo ) { diff --git a/src/subscription.ts b/src/subscription.ts index 0422a03..d591ef9 100644 --- a/src/subscription.ts +++ b/src/subscription.ts @@ -19,10 +19,6 @@ export class FirehoseSubscription extends FirehoseSubscriptionBase { const postsToDelete = ops.posts.deletes.map((del) => del.uri) const postsToCreate = ops.posts.creates - .filter((create) => { - // only alf-related posts - return create.record.text.toLowerCase().includes('alf') - }) .map((create) => { // map alf-related posts to a db row return {