diff --git a/.github/workflows/process _fork.yml b/.github/workflows/process _fork.yml new file mode 100644 index 0000000..cc112ad --- /dev/null +++ b/.github/workflows/process _fork.yml @@ -0,0 +1,73 @@ +name: Process newly added JSON + +on: + pull_request: + types: [closed] + branches: + - main + +permissions: + contents: write + +concurrency: + group: process-json-${{ github.ref }} + cancel-in-progress: true + +jobs: + process-json: + # Only run if the PR was merged and it is opened from a forked repo + if: github.event.pull_request.merged == true && github.event.pull_request.head.repo.full_name == github.repository + + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + # Needed to push changes back to the repo + fetch-depth: 0 + + # Must be done before setup-node. + - name: Enable Corepack + run: corepack enable + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: "yarn" + cache-dependency-path: actions/yarn.lock + + - name: Install Dependencies + run: yarn install --frozen-lockfile + working-directory: ./actions + + - name: Find newly added JSON files + id: find-json + run: | + # Get the list of added JSON files in the records/new/ directory + ADDED_FILES=$(git diff ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }} --diff-filter=A --name-only | grep '^records/new/.*\.json$' || true) + echo "NEW_JSON_FILES=$ADDED_FILES" >> $GITHUB_ENV + if [ -z "$ADDED_FILES" ]; then + echo "No new JSON files found." + fi + + - name: Process and move files + if: env.NEW_JSON_FILES + env: + BLUESKY_IDENTIFIER_NODEJS_ORG: nodejs.org + BLUESKY_APP_PASSWORD_NODEJS_ORG: ${{ secrets.BLUESKY_APP_PASSWORD_NODEJS_ORG }} + run: | + for file in $NEW_JSON_FILES; do + echo "Processing $file..." + node actions/process.js "$file" validate_cred_fork.js + done + + - name: Commit and push changes + if: env.NEW_JSON_FILES + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git add records/* + git commit -m "Process new JSON files from #${{ github.event.pull_request.number }}" || exit 0 + git push diff --git a/.github/workflows/process.yml b/.github/workflows/process.yml index d811254..4208435 100644 --- a/.github/workflows/process.yml +++ b/.github/workflows/process.yml @@ -15,8 +15,8 @@ concurrency: jobs: process-json: - # Only run if the PR was merged - if: github.event.pull_request.merged == true + # Only run if the PR was merged and it is opened from same repo + if: github.event.pull_request.merged == true && github.event.pull_request.head.repo.full_name == github.repository runs-on: ubuntu-latest @@ -60,7 +60,7 @@ jobs: run: | for file in $NEW_JSON_FILES; do echo "Processing $file..." - node actions/process.js "$file" + node actions/process.js "$file" validate_cred.js done - name: Commit and push changes diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 6a8e83d..17db54f 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -44,11 +44,8 @@ jobs: - name: Validate files if: env.NEW_JSON_FILES - env: - BLUESKY_IDENTIFIER_NODEJS_ORG: nodejs.org - BLUESKY_APP_PASSWORD_NODEJS_ORG: ${{ secrets.BLUESKY_APP_PASSWORD_NODEJS_ORG }} run: | for file in $NEW_JSON_FILES; do echo "Processing $file..." - node actions/login-and-validate.js "$file" + node actions/validate_request.js "$file" done diff --git a/actions/lib/posts.js b/actions/lib/posts.js index 2d65227..7522ecf 100644 --- a/actions/lib/posts.js +++ b/actions/lib/posts.js @@ -1,23 +1,15 @@ import AtpAgent, { RichText } from "@atproto/api"; -import assert from 'node:assert'; - -// URL format: -// 1. https://bsky.app/profile/${handle}/post/${postId} -// 2. https://bsky.app/profile/${did}/post/${postId} -// TODO(joyeecheung): consider supporting base other than bsky.app. -const kURLPattern = /https:\/\/bsky\.app\/profile\/(.+)\/post\/(.+)/; /** * @param {string} url */ -export function validatePostURL(url) { - const match = url.match(kURLPattern); - assert(match, `Post URL ${url} does not match the expected pattern`); - +export function PostURL(url) { + const kURLPattern = /https:\/\/bsky\.app\/profile\/(.+)\/post\/(.+)/; + const match_url = url.match(kURLPattern); return { - handle: match[1], - postId: match[2], - isDid: match[1].startsWith('did:') + handle: match_url[1], + postId: match_url[2], + isDid: match_url[1].startsWith('did:') }; } @@ -26,7 +18,7 @@ export function validatePostURL(url) { * @param {string} postUrl */ export async function getPostInfoFromUrl(agent, postUrl) { - const { handle, postId, isDid } = validatePostURL(postUrl); + const { handle, postId, isDid } = PostURL(postUrl); let did; if (isDid) { did = handle; @@ -42,15 +34,13 @@ export async function getPostInfoFromUrl(agent, postUrl) { return { uri, cid }; } -// URI format: at://${did}/app.bsky.feed.post/${postId} -const kURIPattern = /at:\/\/(.*)+\/app\.bsky\.feed\.post\/(.*)+/ -export function validatePostURI(uri) { - const match = uri.match(kURIPattern); - assert(match, `Post URI ${uri} does not match the expected pattern`); +export function PostURI(uri) { + const kURIPattern = /at:\/\/(.*)+\/app\.bsky\.feed\.post\/(.*)+/ + const match_uri = uri.match(kURIPattern); return { - did: match[1], - postId: match[2] + did: match_uri[1], + postId: match_uri[2] }; } @@ -59,7 +49,7 @@ export function validatePostURI(uri) { * @param {string} uri */ export async function getPostURLFromURI(agent, uri) { - const { did, postId } = validatePostURI(uri); + const { did, postId } = PostURI(uri); const profile = await agent.getProfile({ actor: did }); const handle = profile.data.handle; diff --git a/actions/lib/validator.js b/actions/lib/validator.js index bc38a8a..6929d8e 100644 --- a/actions/lib/validator.js +++ b/actions/lib/validator.js @@ -47,7 +47,7 @@ export function validateRequest(request) { * @param {object} request * @param {string} fieldName */ -async function validatePostURLInRequest(agent, request, fieldName) { +async function PostURLInRequest(agent, request, fieldName) { let result; try { result = await getPostInfoFromUrl(agent, request[fieldName]); @@ -60,21 +60,21 @@ async function validatePostURLInRequest(agent, request, fieldName) { } /** - * Validate the post URLs in the request and extend them into { uri, cid } pairs + * Extend them into { uri, cid } pairs * if necessary. * @param {import('@atproto/api').AtpAgent} agent * @param {object} request */ -export async function validateAndExtendRequestReferences(agent, request) { +export async function ExtendRequestReferences(agent, request) { switch(request.action) { case 'repost': case 'quote-post': { - const info = await validatePostURLInRequest(agent, request, 'repostURL'); + const info = await PostURLInRequest(agent, request, 'repostURL'); request.repostInfo = info; break; } case 'reply': { - const info = await validatePostURLInRequest(agent, request, 'replyURL'); + const info = await PostURLInRequest(agent, request, 'replyURL'); request.replyInfo = info; break; } diff --git a/actions/process.js b/actions/process.js index 61f07a0..0f2f0f0 100644 --- a/actions/process.js +++ b/actions/process.js @@ -4,7 +4,7 @@ import process from 'node:process'; import path from 'node:path'; import { login } from './lib/login.js'; import { post } from './lib/posts.js'; -import { validateAccount, validateRequest, validateAndExtendRequestReferences } from './lib/validator.js'; +import { validateAccount, validateRequest, ExtendRequestReferences } from './lib/validator.js'; // This script takes a path to a JSON with the pattern $base_path/new/$any_name.json, // where $any_name can be anything, and then performs the action specified in it. @@ -12,9 +12,11 @@ import { validateAccount, validateRequest, validateAndExtendRequestReferences } // $base_path/processed/$YYYY-$MM-$DD-$ID.json, where $ID is an incremental number // starting from 0 based on the number of existing JSONs processed on the same date // and already in the processed directory. +// Validation checks performed based on where pr is opened assert(process.argv[2], `Usage: node process.js $base_path/new/$any_name.json`); -const { agent, request, requestFilePath } = await import('./login-and-validate.js'); +assert(process.argv[3], `Usage: node process.js $base_path/new/$any_name.json required_auth.js`); +const { agent, request, requestFilePath } = await import(`./${process.argv[3]}`); let result; switch(request.action) { @@ -25,7 +27,7 @@ switch(request.action) { }; case 'repost': { console.log('Reposting...', request.repostURL); - assert(request.repostInfo); // Extended by validateAndExtendRequestReferences. + assert(request.repostInfo); // Extended by ExtendRequestReferences. result = await agent.repost(request.repostInfo.uri, request.repostInfo.cid); break; } diff --git a/actions/login-and-validate.js b/actions/validate_cred.js similarity index 75% rename from actions/login-and-validate.js rename to actions/validate_cred.js index 37f4c8c..3c88a90 100644 --- a/actions/login-and-validate.js +++ b/actions/validate_cred.js @@ -2,7 +2,7 @@ import fs from 'node:fs'; import process from 'node:process'; import path from 'node:path'; import { login } from './lib/login.js'; -import { validateAccount, validateRequest, validateAndExtendRequestReferences } from './lib/validator.js'; +import { validateAccount, ExtendRequestReferences } from './lib/validator.js'; // The JSON file must contains the following fields: // - "account": a string field indicating the account to use to perform the action. @@ -15,12 +15,11 @@ const request = JSON.parse(fs.readFileSync(requestFilePath, 'utf8')); // Validate the account field. const account = validateAccount(request, process.env); -validateRequest(request); // Authenticate. const agent = await login(account); -// Validate and extend the post URLs in the request into { cid, uri } records. -await validateAndExtendRequestReferences(agent, request); +// Extend the post URLs in the request into { cid, uri } records. +await ExtendRequestReferences(agent, request); export { agent, request, requestFilePath }; diff --git a/actions/validate_cred_fork.js b/actions/validate_cred_fork.js new file mode 100644 index 0000000..3c88a90 --- /dev/null +++ b/actions/validate_cred_fork.js @@ -0,0 +1,25 @@ +import fs from 'node:fs'; +import process from 'node:process'; +import path from 'node:path'; +import { login } from './lib/login.js'; +import { validateAccount, ExtendRequestReferences } from './lib/validator.js'; + +// The JSON file must contains the following fields: +// - "account": a string field indicating the account to use to perform the action. +// For it to work, this script expects BLUESKY_IDENTIFIER_$account and +// BLUESKY_APP_PASSWORD_$account to be set in the environment variables. +// - "action": currently "post", "repost", "quote-post", "reply" are supported. + +const requestFilePath = path.resolve(process.argv[2]); +const request = JSON.parse(fs.readFileSync(requestFilePath, 'utf8')); + +// Validate the account field. +const account = validateAccount(request, process.env); + +// Authenticate. +const agent = await login(account); + +// Extend the post URLs in the request into { cid, uri } records. +await ExtendRequestReferences(agent, request); + +export { agent, request, requestFilePath }; diff --git a/actions/validate_request.js b/actions/validate_request.js new file mode 100644 index 0000000..5a26881 --- /dev/null +++ b/actions/validate_request.js @@ -0,0 +1,48 @@ +import fs from 'node:fs'; +import process from 'node:process'; +import path from 'node:path'; +import { validateRequest } from './lib/validator'; +import assert from 'node:assert'; + +const requestFilePath = path.resolve(process.argv[2]); +const request = JSON.parse(fs.readFileSync(requestFilePath, 'utf8')); + +//Check the format of the request +validateRequest(request); + + +// URL format: +// 1. https://bsky.app/profile/${handle}/post/${postId} +// 2. https://bsky.app/profile/${did}/post/${postId} +// TODO(joyeecheung): consider supporting base other than bsky.app. +const kURLPattern = /https:\/\/bsky\.app\/profile\/(.+)\/post\/(.+)/; +let url; +if (request.action!='post'){ + switch(request.action) { + case 'repost': + case 'quote-post': { + url=request.repostURL; + break; + } + case 'reply': { + url=request.replyURL; + break; + } + default: + break; + } + match_url = url.match(kURLPattern); + assert(match_url, `Post URL ${url} does not match the expected pattern`); +} + + + + + + + +// // URI format: at://${did}/app.bsky.feed.post/${postId} +// const kURIPattern = /at:\/\/(.*)+\/app\.bsky\.feed\.post\/(.*)+/ + +// const match_uri = uri.match(kURIPattern); +// assert(match_uri, `Post URI ${uri} does not match the expected pattern`); \ No newline at end of file