diff --git a/README.md b/README.md index e9f3fe1..c0e0433 100644 --- a/README.md +++ b/README.md @@ -8,4 +8,15 @@ Simple command for scafolding a new bsky bot with bsky-event-handlers bunx create-bsky-bot ``` +The final prompt will ask if you also want to include a github action workflow to build and publish your docker containers + +### Available templates: + +**default** : includes basic code for jetstream and interval subscriptions + +**jetstream**: includes basic code for jetstream subscription + +**interval**: includes basic code for jetstream subscription + + For more bsky-event-handler information, see the docs [here](https://github.com/juni-b-queer/bsky-event-handlers) \ No newline at end of file diff --git a/src/cli.ts b/src/cli.ts index a00d3b8..660841b 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2,6 +2,8 @@ import { create } from 'create-create-app'; import { resolve } from 'path'; +import * as fs from 'fs'; +import * as path from 'path'; const templateRoot = resolve(__dirname, '..', 'templates'); @@ -12,9 +14,40 @@ Now go out and build a bot!`; // See https://github.com/uetchy/create-create-app/blob/master/README.md for other options. + create('create-bsky-bot', { templateRoot, - after: () => console.log(`Good to go!`), + after: (options) =>{ + if(options.answers.buildaction){ + const projectPath = options.packageDir; + const githubWorkflowsDir = path.join(projectPath, '.github', 'workflows'); + if (!fs.existsSync(githubWorkflowsDir)){ + fs.mkdirSync(githubWorkflowsDir, { recursive: true }); + } + + const defaultTemplateDir = options.templateDir; + + const templatesDir = path.join(defaultTemplateDir, '..'); + + const workflowsTemplateDir = path.join(templatesDir, 'workflows'); + + const sourceFile = path.join(workflowsTemplateDir, 'buildandpublishtoghcr.yml'); + const destFile = path.join(githubWorkflowsDir, 'buildandpublishtoghcr.yml'); + fs.copyFileSync(sourceFile, destFile); + console.log("Added GH action") + } + + + console.log("Good to go") + }, + extra: { + buildaction: { + type: 'confirm', + describe: 'Should include gh action to build the bot', + default: 'y', + prompt: 'always', + }, + }, caveat, skipGitInit: true, skipNpmInstall: true diff --git a/templates/default/.env.example b/templates/default/.env.example index b690f3c..be002c9 100644 --- a/templates/default/.env.example +++ b/templates/default/.env.example @@ -3,4 +3,6 @@ TEST_BSKY_PASSWORD= DEBUG_LOG_ACTIVE=true DEBUG_LOG_LEVEL=info -JETSTREAM_URL='ws://jetstream:6008/subscribe' \ No newline at end of file +JETSTREAM_URL='ws://jetstream:6008/subscribe' + +SESSION_DATA_PATH='/sessionData' \ No newline at end of file diff --git a/templates/default/Dockerfile b/templates/default/Dockerfile index 6fcf96c..3f07e35 100644 --- a/templates/default/Dockerfile +++ b/templates/default/Dockerfile @@ -1,4 +1,4 @@ -FROM oven/bun:1.0.15 as base +FROM oven/bun:latest as base WORKDIR /usr/src/app # install dependencies into temp directory diff --git a/templates/default/docker-compose.yml b/templates/default/docker-compose.yml index 3d81e08..d3975fc 100644 --- a/templates/default/docker-compose.yml +++ b/templates/default/docker-compose.yml @@ -5,6 +5,8 @@ services: - jetstream build: . restart: unless-stopped + volumes: + - ./sessionData:/sessionData env_file: - .env networks: @@ -14,6 +16,7 @@ services: jetstream: image: "junibqueer/jetstream:main" container_name: jetstream + restart: unless-stopped environment: - CURSOR_FILE=/data/cursor.json ports: diff --git a/templates/default/package.json b/templates/default/package.json index 0a0f275..6a17cbc 100644 --- a/templates/default/package.json +++ b/templates/default/package.json @@ -15,8 +15,8 @@ "typescript": "^5.0.0" }, "dependencies": { - "bsky-event-handlers": "1.1.0", - "@atproto/api": "^0.12.7" + "bsky-event-handlers": "^2.0.0", + "@atproto/api": "^0.13.19" }, "license": "{{license}}" } \ No newline at end of file diff --git a/templates/default/src/index.ts b/templates/default/src/index.ts index 0e962f9..a81e807 100644 --- a/templates/default/src/index.ts +++ b/templates/default/src/index.ts @@ -8,7 +8,12 @@ import { JetstreamSubscription, LogMessageAction, ReplyingToBotValidator, - ReplyToSkeetAction + MessageHandler, + IntervalSubscription, + IntervalSubscriptionHandlers, + AbstractHandler, + IsSpecifiedTimeValidator, + CreateSkeetAction } from 'bsky-event-handlers'; const testAgent = new HandlerAgent( @@ -17,14 +22,17 @@ const testAgent = new HandlerAgent( Bun.env.TEST_BSKY_PASSWORD ); +/** + * Jetstream Subscription setup + */ let jetstreamSubscription: JetstreamSubscription; let handlers = { post: { c: [ - new CreateSkeetHandler( - [new ReplyingToBotValidator(), new InputEqualsValidator("Hello")], - [new LogMessageAction(), new ReplyToSkeetAction("World!")], + new MessageHandler( + [ReplyingToBotValidator().make(), InputEqualsValidator("Hello").make()], + [LogMessageAction().make(), CreateSkeetAction.make('World!', MessageHandler.generateReplyFromMessage)], testAgent ), new GoodBotHandler(testAgent), @@ -33,16 +41,35 @@ let handlers = { } } +let intervalSubscription: IntervalSubscription; + +const intervalSubscriptionHandlers: IntervalSubscriptionHandlers = [ + { + intervalSeconds: 60, + handlers:[ + new AbstractHandler( + [IsSpecifiedTimeValidator.make("04:20", "16:20")], + [CreateSkeetAction.make("It's 4:20 somewhere!")], + testAgent) + ] + } +] + + async function initialize() { await testAgent.authenticate() jetstreamSubscription = new JetstreamSubscription( handlers, Bun.env.JETSTREAM_URL ); + intervalSubscription = new IntervalSubscription( + intervalSubscriptionHandlers + ) } initialize().then(() =>{ jetstreamSubscription.createSubscription() + intervalSubscription.createSubscription() DebugLog.info("INIT", 'Initialized!') }); diff --git a/templates/interval/.env.example b/templates/interval/.env.example new file mode 100644 index 0000000..4fc61b9 --- /dev/null +++ b/templates/interval/.env.example @@ -0,0 +1,4 @@ +TEST_BSKY_HANDLE= +TEST_BSKY_PASSWORD= +DEBUG_LOG_ACTIVE=true +DEBUG_LOG_LEVEL=info \ No newline at end of file diff --git a/templates/interval/Dockerfile b/templates/interval/Dockerfile new file mode 100644 index 0000000..3f07e35 --- /dev/null +++ b/templates/interval/Dockerfile @@ -0,0 +1,35 @@ +FROM oven/bun:latest as base +WORKDIR /usr/src/app + +# install dependencies into temp directory +# this will cache them and speed up future builds +FROM base AS install +RUN mkdir -p /temp/dev +COPY package.json bun.lockb /temp/dev/ +RUN cd /temp/dev && bun install --frozen-lockfile + +# install with --production (exclude devDependencies) +RUN mkdir -p /temp/prod +COPY package.json bun.lockb /temp/prod/ +RUN cd /temp/prod && bun install --frozen-lockfile --production + +# copy node_modules from temp directory +# then copy all (non-ignored) project files into the image +FROM install AS prerelease +COPY --from=install /temp/dev/node_modules node_modules +COPY . . + +# [optional] tests & build +ENV NODE_ENV=production +RUN bun test +RUN bun run build + +# copy production dependencies and source code into final image +FROM base AS release +COPY --from=install /temp/prod/node_modules node_modules +COPY --from=prerelease /usr/src/app/build/index.ts . +COPY --from=prerelease /usr/src/app/package.json . + +# run the app +USER bun +ENTRYPOINT [ "bun", "run", "index.ts" ] \ No newline at end of file diff --git a/templates/interval/README.md b/templates/interval/README.md new file mode 100644 index 0000000..6150faf --- /dev/null +++ b/templates/interval/README.md @@ -0,0 +1,15 @@ +# {{name}} + +{{description}} + +# Quickstart +- [ ] Install Bun +- [ ] Install Docker +- [ ] Fill in .env with your handle and **app** password (do not use your real password) +- [ ] Run `make up` +- [ ] Be free and create to your hearts content! + + +## Development + +Check out the bsky-event-handler docs [here](https://github.com/juni-b-queer/bsky-event-handlers) \ No newline at end of file diff --git a/templates/interval/docker-compose.yml b/templates/interval/docker-compose.yml new file mode 100644 index 0000000..77a7793 --- /dev/null +++ b/templates/interval/docker-compose.yml @@ -0,0 +1,15 @@ +version: "3.8" +services: + bskybot: + build: . + restart: unless-stopped + volumes: + - ./sessionData:/sessionData + env_file: + - .env + networks: + - bun + +networks: + bun: + driver: bridge diff --git a/templates/interval/makefile b/templates/interval/makefile new file mode 100644 index 0000000..6d93720 --- /dev/null +++ b/templates/interval/makefile @@ -0,0 +1,19 @@ +.PHONY: * + +dev: + bun run src/index.ts + +build: + docker compose build + +up: + docker compose up -d + +down: + docker compose down + +logs: + docker compose logs -f + +install: + bun install \ No newline at end of file diff --git a/templates/interval/package.json b/templates/interval/package.json new file mode 100644 index 0000000..6a17cbc --- /dev/null +++ b/templates/interval/package.json @@ -0,0 +1,22 @@ +{ + "name": "{{kebab name}}", + "description": "{{description}}", + "version": "0.0.0", + "author": "{{contact}}", + "module": "src/index.ts", + "type": "module", + "scripts": { + "build": "bun build --target=bun ./src/index.ts --outfile=./build/index.ts" + }, + "devDependencies": { + "bun-types": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "bsky-event-handlers": "^2.0.0", + "@atproto/api": "^0.13.19" + }, + "license": "{{license}}" +} \ No newline at end of file diff --git a/templates/interval/src/index.ts b/templates/interval/src/index.ts new file mode 100644 index 0000000..6c90072 --- /dev/null +++ b/templates/interval/src/index.ts @@ -0,0 +1,44 @@ +import { + DebugLog, + HandlerAgent, + IntervalSubscription, + IntervalSubscriptionHandlers, + AbstractHandler, + IsSpecifiedTimeValidator, + CreateSkeetAction +} from 'bsky-event-handlers'; + +const testAgent = new HandlerAgent( + 'test-bot', + Bun.env.TEST_BSKY_HANDLE, + Bun.env.TEST_BSKY_PASSWORD +); + +let intervalSubscription: IntervalSubscription; + +const intervalSubscriptionHandlers: IntervalSubscriptionHandlers = [ + { + intervalSeconds: 60, + handlers:[ + new AbstractHandler( + [IsSpecifiedTimeValidator.make("04:20", "16:20")], + [CreateSkeetAction.make("It's 4:20 somewhere!")], + testAgent) + ] + } +] + + +async function initialize() { + await testAgent.authenticate() + intervalSubscription = new IntervalSubscription( + intervalSubscriptionHandlers + ) +} + +initialize().then(() =>{ + intervalSubscription.createSubscription() + DebugLog.info("INIT", 'Initialized!') +}); + + diff --git a/templates/jetstream/.env.example b/templates/jetstream/.env.example new file mode 100644 index 0000000..be002c9 --- /dev/null +++ b/templates/jetstream/.env.example @@ -0,0 +1,8 @@ +TEST_BSKY_HANDLE= +TEST_BSKY_PASSWORD= +DEBUG_LOG_ACTIVE=true +DEBUG_LOG_LEVEL=info + +JETSTREAM_URL='ws://jetstream:6008/subscribe' + +SESSION_DATA_PATH='/sessionData' \ No newline at end of file diff --git a/templates/jetstream/Dockerfile b/templates/jetstream/Dockerfile new file mode 100644 index 0000000..6fcf96c --- /dev/null +++ b/templates/jetstream/Dockerfile @@ -0,0 +1,35 @@ +FROM oven/bun:1.0.15 as base +WORKDIR /usr/src/app + +# install dependencies into temp directory +# this will cache them and speed up future builds +FROM base AS install +RUN mkdir -p /temp/dev +COPY package.json bun.lockb /temp/dev/ +RUN cd /temp/dev && bun install --frozen-lockfile + +# install with --production (exclude devDependencies) +RUN mkdir -p /temp/prod +COPY package.json bun.lockb /temp/prod/ +RUN cd /temp/prod && bun install --frozen-lockfile --production + +# copy node_modules from temp directory +# then copy all (non-ignored) project files into the image +FROM install AS prerelease +COPY --from=install /temp/dev/node_modules node_modules +COPY . . + +# [optional] tests & build +ENV NODE_ENV=production +RUN bun test +RUN bun run build + +# copy production dependencies and source code into final image +FROM base AS release +COPY --from=install /temp/prod/node_modules node_modules +COPY --from=prerelease /usr/src/app/build/index.ts . +COPY --from=prerelease /usr/src/app/package.json . + +# run the app +USER bun +ENTRYPOINT [ "bun", "run", "index.ts" ] \ No newline at end of file diff --git a/templates/jetstream/README.md b/templates/jetstream/README.md new file mode 100644 index 0000000..872ed2b --- /dev/null +++ b/templates/jetstream/README.md @@ -0,0 +1,20 @@ +# {{name}} + +{{description}} + +# Quickstart +- [ ] Install Bun +- [ ] Install Docker +- [ ] Fill in .env with your handle and **app** password (do not use your real password) +- [ ] Run `make up` +- [ ] Be free and create to your hearts content! + + +## Development +Sometimes it may be easier to keep jetstream running and make quick changes to your code. To run jetstream on its own use `docker compose up jetstream -d` +Then when you make changes to your code, you can test it by running `make dev`, but be sure your .env jetstream url is set correctly. +When using the docker containers, it should be `ws://jetstream:6008/subscribe`, but when running code with bun run, it should be `ws://localhost:6008/subscribe` + +For the latest firehose content, start jetstream with an empty data folder/cursor.json file + +Check out the bsky-event-handler docs [here](https://github.com/juni-b-queer/bsky-event-handlers) \ No newline at end of file diff --git a/templates/jetstream/docker-compose.yml b/templates/jetstream/docker-compose.yml new file mode 100644 index 0000000..d3975fc --- /dev/null +++ b/templates/jetstream/docker-compose.yml @@ -0,0 +1,31 @@ +version: "3.8" +services: + bskybot: + depends_on: + - jetstream + build: . + restart: unless-stopped + volumes: + - ./sessionData:/sessionData + env_file: + - .env + networks: + - bun + + + jetstream: + image: "junibqueer/jetstream:main" + container_name: jetstream + restart: unless-stopped + environment: + - CURSOR_FILE=/data/cursor.json + ports: + - "6008:6008" + volumes: + - ./data:/data + networks: + - bun + +networks: + bun: + driver: bridge diff --git a/templates/jetstream/makefile b/templates/jetstream/makefile new file mode 100644 index 0000000..6d93720 --- /dev/null +++ b/templates/jetstream/makefile @@ -0,0 +1,19 @@ +.PHONY: * + +dev: + bun run src/index.ts + +build: + docker compose build + +up: + docker compose up -d + +down: + docker compose down + +logs: + docker compose logs -f + +install: + bun install \ No newline at end of file diff --git a/templates/jetstream/package.json b/templates/jetstream/package.json new file mode 100644 index 0000000..6a17cbc --- /dev/null +++ b/templates/jetstream/package.json @@ -0,0 +1,22 @@ +{ + "name": "{{kebab name}}", + "description": "{{description}}", + "version": "0.0.0", + "author": "{{contact}}", + "module": "src/index.ts", + "type": "module", + "scripts": { + "build": "bun build --target=bun ./src/index.ts --outfile=./build/index.ts" + }, + "devDependencies": { + "bun-types": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "bsky-event-handlers": "^2.0.0", + "@atproto/api": "^0.13.19" + }, + "license": "{{license}}" +} \ No newline at end of file diff --git a/templates/jetstream/src/index.ts b/templates/jetstream/src/index.ts new file mode 100644 index 0000000..df77131 --- /dev/null +++ b/templates/jetstream/src/index.ts @@ -0,0 +1,52 @@ +import { + BadBotHandler, + CreateSkeetAction, + DebugLog, + GoodBotHandler, + HandlerAgent, + InputEqualsValidator, + JetstreamSubscription, + LogMessageAction, + ReplyingToBotValidator, + MessageHandler +} from 'bsky-event-handlers'; + +const testAgent = new HandlerAgent( + 'test-bot', + Bun.env.TEST_BSKY_HANDLE, + Bun.env.TEST_BSKY_PASSWORD +); + +/** + * Jetstream Subscription setup + */ +let jetstreamSubscription: JetstreamSubscription; + +let handlers = { + post: { + c: [ + new MessageHandler( + [ReplyingToBotValidator().make(), InputEqualsValidator("Hello").make()], + [LogMessageAction().make(), CreateSkeetAction.make('World!', MessageHandler.generateReplyFromMessage)], + testAgent + ), + new GoodBotHandler(testAgent), + new BadBotHandler(testAgent) + ] + } +} + +async function initialize() { + await testAgent.authenticate() + jetstreamSubscription = new JetstreamSubscription( + handlers, + Bun.env.JETSTREAM_URL + ); +} + +initialize().then(() =>{ + jetstreamSubscription.createSubscription() + DebugLog.info("INIT", 'Initialized!') +}); + + diff --git a/templates/workflows/buildandpublishtoghcr.yml b/templates/workflows/buildandpublishtoghcr.yml new file mode 100644 index 0000000..bddb086 --- /dev/null +++ b/templates/workflows/buildandpublishtoghcr.yml @@ -0,0 +1,48 @@ +# +name: Create and publish the bot container + +# Configures this workflow to run every time a change is pushed to the branch called `release`. +on: + push: + branches: ['main'] + +# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds. +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu. +jobs: + build-and-push-image: + runs-on: ubuntu-latest + # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. + permissions: + contents: read + packages: write + # + steps: + - name: Checkout repository + uses: actions/checkout@v4 + # Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here. + - name: Log in to the Container registry + uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GHCR_PAT }} + # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels. + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + # This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages. + # It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository. + # It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step. + - name: Build and push Docker image + uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }}