Compare commits
No commits in common. "9fe7060bf34b89578ff1832054c891cc40adb53a" and "ead9d18b3dfd4cf4c64fec5d2845f4f789204810" have entirely different histories.
9fe7060bf3
...
ead9d18b3d
30 changed files with 0 additions and 8513 deletions
|
|
@ -1,10 +0,0 @@
|
||||||
root = true
|
|
||||||
|
|
||||||
[*]
|
|
||||||
end_of_line = lf
|
|
||||||
insert_final_newline = true
|
|
||||||
|
|
||||||
[*.{js,json,yml}]
|
|
||||||
charset = utf-8
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 2
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
name: Semver
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- "**"
|
|
||||||
jobs:
|
|
||||||
semver:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions: write-all
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Enable Corepack
|
|
||||||
run: corepack enable
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: "21"
|
|
||||||
- name: Setup Yarn
|
|
||||||
run: |
|
|
||||||
yarn set version canary
|
|
||||||
yarn config set nodeLinker node-modules
|
|
||||||
|
|
||||||
- name: yarn install
|
|
||||||
run: yarn install --immutable
|
|
||||||
- name: Semantic Release
|
|
||||||
id: semantic
|
|
||||||
run: yarn run semantic-release
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
name: Build Docker Image
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- "**"
|
|
||||||
tags:
|
|
||||||
- "v*.*.*"
|
|
||||||
jobs:
|
|
||||||
build-image:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Docker Metadata
|
|
||||||
id: metadata
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
images: git.thornbush.dev/rose/fixbluesky
|
|
||||||
tags: |
|
|
||||||
type=ref,event=branch
|
|
||||||
type=sha,prefix=
|
|
||||||
type=edge,branch=devel
|
|
||||||
type=semver,pattern={{version}}
|
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
|
||||||
type=semver,pattern={{major}}
|
|
||||||
- name: Docker Buildx
|
|
||||||
uses: https://code.forgejo.org/docker/setup-buildx-action@v3
|
|
||||||
- name: Login to Docker Registry
|
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
uses: https://code.forgejo.org/docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: git.thornbush.dev
|
|
||||||
username: ${{github.actor}}
|
|
||||||
password: ${{secrets.OAUTH_TOKEN}}
|
|
||||||
- name: Build Image
|
|
||||||
uses: https://code.forgejo.org/docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: ./Dockerfile
|
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
|
||||||
tags: ${{ steps.metadata.outputs.tags }}
|
|
||||||
labels: ${{ steps.metadata.outputs.labels }}
|
|
||||||
4
.gitattributes
vendored
4
.gitattributes
vendored
|
|
@ -1,4 +0,0 @@
|
||||||
/.yarn/** linguist-vendored
|
|
||||||
/.yarn/releases/* binary
|
|
||||||
/.yarn/plugins/**/* binary
|
|
||||||
/.pnp.* binary linguist-generated
|
|
||||||
130
.gitignore
vendored
130
.gitignore
vendored
|
|
@ -1,130 +0,0 @@
|
||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
.pnpm-debug.log*
|
|
||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
|
||||||
|
|
||||||
# Runtime data
|
|
||||||
pids
|
|
||||||
*.pid
|
|
||||||
*.seed
|
|
||||||
*.pid.lock
|
|
||||||
|
|
||||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
|
||||||
lib-cov
|
|
||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
|
||||||
coverage
|
|
||||||
*.lcov
|
|
||||||
|
|
||||||
# nyc test coverage
|
|
||||||
.nyc_output
|
|
||||||
|
|
||||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
|
||||||
.grunt
|
|
||||||
|
|
||||||
# Bower dependency directory (https://bower.io/)
|
|
||||||
bower_components
|
|
||||||
|
|
||||||
# node-waf configuration
|
|
||||||
.lock-wscript
|
|
||||||
|
|
||||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
|
||||||
build/Release
|
|
||||||
|
|
||||||
# Dependency directories
|
|
||||||
node_modules/
|
|
||||||
jspm_packages/
|
|
||||||
|
|
||||||
# Snowpack dependency directory (https://snowpack.dev/)
|
|
||||||
web_modules/
|
|
||||||
|
|
||||||
# TypeScript cache
|
|
||||||
*.tsbuildinfo
|
|
||||||
|
|
||||||
# Optional npm cache directory
|
|
||||||
.npm
|
|
||||||
|
|
||||||
# Optional eslint cache
|
|
||||||
.eslintcache
|
|
||||||
|
|
||||||
# Optional stylelint cache
|
|
||||||
.stylelintcache
|
|
||||||
|
|
||||||
# Microbundle cache
|
|
||||||
.rpt2_cache/
|
|
||||||
.rts2_cache_cjs/
|
|
||||||
.rts2_cache_es/
|
|
||||||
.rts2_cache_umd/
|
|
||||||
|
|
||||||
# Optional REPL history
|
|
||||||
.node_repl_history
|
|
||||||
|
|
||||||
# Output of 'npm pack'
|
|
||||||
*.tgz
|
|
||||||
|
|
||||||
# Yarn Integrity file
|
|
||||||
.yarn-integrity
|
|
||||||
|
|
||||||
# dotenv environment variable files
|
|
||||||
.env
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
.env.local
|
|
||||||
|
|
||||||
# parcel-bundler cache (https://parceljs.org/)
|
|
||||||
.cache
|
|
||||||
.parcel-cache
|
|
||||||
|
|
||||||
# Next.js build output
|
|
||||||
.next
|
|
||||||
out
|
|
||||||
|
|
||||||
# Nuxt.js build / generate output
|
|
||||||
.nuxt
|
|
||||||
dist
|
|
||||||
|
|
||||||
# Gatsby files
|
|
||||||
.cache/
|
|
||||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
|
||||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
|
||||||
# public
|
|
||||||
|
|
||||||
# vuepress build output
|
|
||||||
.vuepress/dist
|
|
||||||
|
|
||||||
# vuepress v2.x temp and cache directory
|
|
||||||
.temp
|
|
||||||
.cache
|
|
||||||
|
|
||||||
# Docusaurus cache and generated files
|
|
||||||
.docusaurus
|
|
||||||
|
|
||||||
# Serverless directories
|
|
||||||
.serverless/
|
|
||||||
|
|
||||||
# FuseBox cache
|
|
||||||
.fusebox/
|
|
||||||
|
|
||||||
# DynamoDB Local files
|
|
||||||
.dynamodb/
|
|
||||||
|
|
||||||
# TernJS port file
|
|
||||||
.tern-port
|
|
||||||
|
|
||||||
# Stores VSCode versions used for testing VSCode extensions
|
|
||||||
.vscode-test
|
|
||||||
|
|
||||||
# yarn v2
|
|
||||||
.yarn/cache
|
|
||||||
.yarn/unplugged
|
|
||||||
.yarn/build-state.yml
|
|
||||||
.yarn/install-state.gz
|
|
||||||
.pnp.*
|
|
||||||
12
.vscode/settings.json
vendored
12
.vscode/settings.json
vendored
|
|
@ -1,12 +0,0 @@
|
||||||
{
|
|
||||||
"biome.enabled": true,
|
|
||||||
"editor.defaultFormatter": "biomejs.biome",
|
|
||||||
"editor.codeActionsOnSave": {
|
|
||||||
"source.fixAll": "explicit",
|
|
||||||
"source.organizeImports": "explicit"
|
|
||||||
},
|
|
||||||
"typescript.tsdk": "node_modules\\typescript\\lib",
|
|
||||||
"[typescript]": {
|
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
925
.yarn/releases/yarn-4.5.0.cjs
vendored
925
.yarn/releases/yarn-4.5.0.cjs
vendored
File diff suppressed because one or more lines are too long
|
|
@ -1,3 +0,0 @@
|
||||||
nodeLinker: node-modules
|
|
||||||
|
|
||||||
yarnPath: .yarn/releases/yarn-4.5.0.cjs
|
|
||||||
30
Dockerfile
30
Dockerfile
|
|
@ -1,30 +0,0 @@
|
||||||
FROM node:21-alpine AS base
|
|
||||||
ENV CI=true
|
|
||||||
|
|
||||||
FROM base AS builder
|
|
||||||
|
|
||||||
RUN corepack enable
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY .gitignore .gitignore
|
|
||||||
COPY yarn.lock ./
|
|
||||||
COPY package.json ./
|
|
||||||
RUN yarn set version canary
|
|
||||||
RUN yarn config set nodeLinker node-modules
|
|
||||||
RUN yarn install --immutable
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
RUN yarn build
|
|
||||||
|
|
||||||
FROM base AS runner
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Don't run production as root
|
|
||||||
RUN addgroup --system --gid 1001 fixbluesky
|
|
||||||
RUN adduser --system --uid 1001 fixbluesky
|
|
||||||
USER fixbluesky
|
|
||||||
COPY --from=builder /app .
|
|
||||||
|
|
||||||
CMD node ./dist/main.js
|
|
||||||
31
biome.json
31
biome.json
|
|
@ -1,31 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://biomejs.dev/schemas/1.7.1/schema.json",
|
|
||||||
"organizeImports": {
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
"linter": {
|
|
||||||
"enabled": true,
|
|
||||||
"rules": {
|
|
||||||
"recommended": true,
|
|
||||||
"style": {
|
|
||||||
"useTemplate": "warn",
|
|
||||||
"noNonNullAssertion": "warn"
|
|
||||||
},
|
|
||||||
"suspicious": {
|
|
||||||
"noExplicitAny": "off"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"formatter": {
|
|
||||||
"enabled": true,
|
|
||||||
"lineWidth": 72,
|
|
||||||
"indentStyle": "space",
|
|
||||||
"formatWithErrors": true,
|
|
||||||
"indentWidth": 2
|
|
||||||
},
|
|
||||||
"javascript": {
|
|
||||||
"formatter": {
|
|
||||||
"semicolons": "always"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
export default {
|
|
||||||
extends: ["gitmoji"],
|
|
||||||
rules: {
|
|
||||||
"type-empty": [0, "never"],
|
|
||||||
"subject-empty": [0, "never"],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
services:
|
|
||||||
keydb:
|
|
||||||
container_name: keydb
|
|
||||||
image: eqalpha/keydb
|
|
||||||
restart: always
|
|
||||||
networks:
|
|
||||||
- fixbluesky
|
|
||||||
fixbluesky:
|
|
||||||
container_name: fixbluesky
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: ./Dockerfile
|
|
||||||
restart: always
|
|
||||||
ports:
|
|
||||||
- 8787:8787
|
|
||||||
environment:
|
|
||||||
- REDIS_HOSTNAME=${REDIS_HOSTNAME:-keydb}
|
|
||||||
- BSKY_SERVICE_URL=${BSKY_SERVICE_URL}
|
|
||||||
- BSKY_AUTH_USERNAME=${BSKY_AUTH_USERNAME}
|
|
||||||
- BSKY_AUTH_PASSWORD=${BSKY_AUTH_PASSWORD}
|
|
||||||
- FIXBLUESKY_APP_DOMAIN=${FIXBLUESKY_APP_DOMAIN}
|
|
||||||
networks:
|
|
||||||
- fixbluesky
|
|
||||||
depends_on:
|
|
||||||
- keydb
|
|
||||||
networks:
|
|
||||||
fixbluesky:
|
|
||||||
name: fixbluesky
|
|
||||||
10
lefthook.yml
10
lefthook.yml
|
|
@ -1,10 +0,0 @@
|
||||||
pre-commit:
|
|
||||||
parallel: true
|
|
||||||
commands:
|
|
||||||
"biome lint":
|
|
||||||
run: yarn run lint
|
|
||||||
|
|
||||||
commit-msg:
|
|
||||||
commands:
|
|
||||||
"lint commit message":
|
|
||||||
run: yarn run commitlint --edit {1}
|
|
||||||
43
package.json
43
package.json
|
|
@ -1,43 +0,0 @@
|
||||||
{
|
|
||||||
"name": "fixbluesky",
|
|
||||||
"private": true,
|
|
||||||
"volta": {
|
|
||||||
"node": "21.7.3"
|
|
||||||
},
|
|
||||||
"type": "module",
|
|
||||||
"main": "./dist/main.js",
|
|
||||||
"module": "./dist/main.js",
|
|
||||||
"types": "./dist/main.d.ts",
|
|
||||||
"exports": {
|
|
||||||
"import": {
|
|
||||||
"types": "./dist/main.d.ts"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"dev": "tsx ./src/main.ts",
|
|
||||||
"lint": "biome ci ./src/**/*",
|
|
||||||
"build": "pkgroll"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@biomejs/biome": "1.7.1",
|
|
||||||
"@semantic-release/changelog": "^6.0.3",
|
|
||||||
"@semantic-release/exec": "^6.0.3",
|
|
||||||
"@semantic-release/git": "^10.0.1",
|
|
||||||
"@types/node": "20.12.7",
|
|
||||||
"commitlint": "^19.5.0",
|
|
||||||
"commitlint-config-gitmoji": "^2.3.1",
|
|
||||||
"lefthook": "^1.7.18",
|
|
||||||
"pkgroll": "2.0.2",
|
|
||||||
"semantic-release": "^24.1.2",
|
|
||||||
"semantic-release-gitmoji": "^1.6.8",
|
|
||||||
"tsx": "4.7.2",
|
|
||||||
"typescript": "5.4.5"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@atproto/api": "0.12.5",
|
|
||||||
"@hono/node-server": "1.11.0",
|
|
||||||
"hono": "4.2.7",
|
|
||||||
"ioredis": "5.4.1"
|
|
||||||
},
|
|
||||||
"packageManager": "yarn@4.5.0"
|
|
||||||
}
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
/**
|
|
||||||
* @type {import('semantic-release').GlobalConfig}
|
|
||||||
*/
|
|
||||||
export default {
|
|
||||||
branches: ["main", { name: "devel", prerelease: true }],
|
|
||||||
plugins: [
|
|
||||||
[
|
|
||||||
"semantic-release-gitmoji",
|
|
||||||
{
|
|
||||||
releaseRules: {
|
|
||||||
major: [":boom:"],
|
|
||||||
minor: [":sparkles:"],
|
|
||||||
patch: [
|
|
||||||
":bug:",
|
|
||||||
":ambulance:",
|
|
||||||
":lipstick:",
|
|
||||||
":lock:",
|
|
||||||
":zap:",
|
|
||||||
":chart_with_upwards_trend:",
|
|
||||||
":globe_with_meridians:",
|
|
||||||
":alien:",
|
|
||||||
":wheelchair:",
|
|
||||||
":mag:",
|
|
||||||
":children_crossing:",
|
|
||||||
":speech_balloon:",
|
|
||||||
":iphone:",
|
|
||||||
":pencil2:",
|
|
||||||
":bento:",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"@semantic-release/exec",
|
|
||||||
{
|
|
||||||
successCmd:
|
|
||||||
"echo 'NOTES<<EOF' >> $GITHUB_OUTPUT && echo '${nextRelease.notes}' >> $GITHUB_OUTPUT && echo 'EOF' >> $GITHUB_OUTPUT && echo 'VERSION=${nextRelease.version}' >> $GITHUB_OUTPUT",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
import { html } from "hono/html";
|
|
||||||
|
|
||||||
export interface LayoutProps {
|
|
||||||
url: string;
|
|
||||||
children: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Layout = ({ url, children }: LayoutProps) => {
|
|
||||||
const removeLeadingSlash = url.substring(1);
|
|
||||||
const redirectUrl = removeLeadingSlash.startsWith("https://")
|
|
||||||
? removeLeadingSlash
|
|
||||||
: `https://bsky.app/${removeLeadingSlash}`;
|
|
||||||
return html`
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<link rel="canonical" href="${url.substring(1)}" />
|
|
||||||
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
|
|
||||||
<meta content="#0085ff" name="theme-color" />
|
|
||||||
<meta property="og:site_name" content="FixBluesky" />
|
|
||||||
|
|
||||||
${children}
|
|
||||||
<meta http-equiv="refresh" content="0;url=${redirectUrl}" />
|
|
||||||
</head>
|
|
||||||
</html>
|
|
||||||
`;
|
|
||||||
};
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
import type { AppBskyFeedDefs } from "@atproto/api";
|
|
||||||
|
|
||||||
import { parseEmbedDescription } from "../lib/parseEmbedDescription.ts";
|
|
||||||
import { parseEmbedImage } from "../lib/parseEmbedImage.ts";
|
|
||||||
import { OEmbedTypes } from "../routes/getOEmbed.ts";
|
|
||||||
import { Layout } from "./Layout.tsx";
|
|
||||||
|
|
||||||
interface PostProps {
|
|
||||||
post: AppBskyFeedDefs.PostView;
|
|
||||||
url: string;
|
|
||||||
appDomain: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Post = ({ post, url, appDomain }: PostProps) => {
|
|
||||||
const image = parseEmbedImage(post);
|
|
||||||
return (
|
|
||||||
<Layout url={url}>
|
|
||||||
<meta name="twitter:creator" content={`@${post.author.handle}`} />
|
|
||||||
<meta
|
|
||||||
property="og:description"
|
|
||||||
content={parseEmbedDescription(post)}
|
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
property="og:title"
|
|
||||||
content={`${post.author.displayName} (@${post.author.handle})`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{!(image === post.author.avatar) && (
|
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{Array.isArray(image) ? (
|
|
||||||
image.map((i) => (
|
|
||||||
<meta property="og:image" content={i} key={i} />
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<meta property="og:image" content={image} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<link
|
|
||||||
type="application/json+oembed"
|
|
||||||
href={`https:/${appDomain}/oembed?type=${
|
|
||||||
OEmbedTypes.Post
|
|
||||||
}&replies=${post.replyCount}&reposts=${
|
|
||||||
post.repostCount
|
|
||||||
}&likes=${post.likeCount}&avatar=${encodeURIComponent(
|
|
||||||
post.author.avatar ?? "",
|
|
||||||
)}`}
|
|
||||||
/>
|
|
||||||
</Layout>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
import type { AppBskyActorDefs } from "@atproto/api";
|
|
||||||
|
|
||||||
import { OEmbedTypes } from "../routes/getOEmbed.ts";
|
|
||||||
import { Layout } from "./Layout.tsx";
|
|
||||||
|
|
||||||
interface ProfileProps {
|
|
||||||
profile: AppBskyActorDefs.ProfileViewDetailed;
|
|
||||||
url: string;
|
|
||||||
appDomain: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Profile = ({ profile, url, appDomain }: ProfileProps) => (
|
|
||||||
<Layout url={url}>
|
|
||||||
<meta name="twitter:creator" content={`@${profile.handle}`} />
|
|
||||||
<meta
|
|
||||||
property="og:description"
|
|
||||||
content={profile.description ?? ""}
|
|
||||||
/>
|
|
||||||
<meta
|
|
||||||
property="og:title"
|
|
||||||
content={`${profile.displayName} (@${profile.handle})`}
|
|
||||||
/>
|
|
||||||
<meta property="og:image" content={profile.avatar} />
|
|
||||||
|
|
||||||
<link
|
|
||||||
type="application/json+oembed"
|
|
||||||
href={`https://${appDomain}/oembed?type=${
|
|
||||||
OEmbedTypes.Profile
|
|
||||||
}&follows=${profile.followsCount}&posts=${
|
|
||||||
profile.postsCount
|
|
||||||
}&avatar=${encodeURIComponent(profile.avatar ?? "")}`}
|
|
||||||
/>
|
|
||||||
</Layout>
|
|
||||||
);
|
|
||||||
9
src/globals.d.ts
vendored
9
src/globals.d.ts
vendored
|
|
@ -1,9 +0,0 @@
|
||||||
import type { BskyAgent } from "@atproto/api";
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HonoEnv {
|
|
||||||
Variables: {
|
|
||||||
Agent: BskyAgent;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
import type { BskyAgent } from "@atproto/api";
|
|
||||||
|
|
||||||
export interface fetchPostOptions {
|
|
||||||
user: string;
|
|
||||||
post: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchPost(
|
|
||||||
agent: BskyAgent,
|
|
||||||
{ user, post }: fetchPostOptions,
|
|
||||||
) {
|
|
||||||
const { data: userData } = await agent.getProfile({
|
|
||||||
actor: user,
|
|
||||||
});
|
|
||||||
return agent.getPosts({
|
|
||||||
uris: [`at://${userData.did}/app.bsky.feed.post/${post}`],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
import type { BskyAgent } from "@atproto/api";
|
|
||||||
|
|
||||||
export interface fetchProfileOptions {
|
|
||||||
user: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function fetchProfile(
|
|
||||||
agent: BskyAgent,
|
|
||||||
{ user }: fetchProfileOptions,
|
|
||||||
) {
|
|
||||||
return agent.getProfile({
|
|
||||||
actor: user,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
import {
|
|
||||||
AppBskyEmbedRecord,
|
|
||||||
AppBskyEmbedRecordWithMedia,
|
|
||||||
AppBskyFeedPost,
|
|
||||||
type AppBskyFeedDefs,
|
|
||||||
} from "@atproto/api";
|
|
||||||
|
|
||||||
export function parseEmbedDescription(post: AppBskyFeedDefs.PostView) {
|
|
||||||
if (AppBskyFeedPost.isRecord(post.record)) {
|
|
||||||
if (AppBskyEmbedRecord.isView(post.embed)) {
|
|
||||||
const { success: isView } = AppBskyEmbedRecord.validateView(
|
|
||||||
post.embed,
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
isView &&
|
|
||||||
AppBskyEmbedRecord.isViewRecord(post.embed.record)
|
|
||||||
) {
|
|
||||||
const { success: isViewRecord } =
|
|
||||||
AppBskyEmbedRecord.validateViewRecord(post.embed.record);
|
|
||||||
if (isViewRecord) {
|
|
||||||
// @ts-expect-error For some reason the original post value is typed as {}
|
|
||||||
return `${post.record.text}\n\nQuoting @${post.embed.record.author.handle}\n➥ ${post.embed.record.value.text}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (AppBskyEmbedRecordWithMedia.isView(post.embed)) {
|
|
||||||
const { success: isView } =
|
|
||||||
AppBskyEmbedRecordWithMedia.validateView(post.embed);
|
|
||||||
if (
|
|
||||||
isView &&
|
|
||||||
AppBskyEmbedRecord.isViewRecord(post.embed.record.record)
|
|
||||||
) {
|
|
||||||
const { success: isViewRecord } =
|
|
||||||
AppBskyEmbedRecord.validateViewRecord(
|
|
||||||
post.embed.record.record,
|
|
||||||
);
|
|
||||||
if (isViewRecord) {
|
|
||||||
// @ts-expect-error For some reason the original post value is typed as {}
|
|
||||||
return `${post.record.text}\n\nQuoting @${post.embed.record.record.author.handle}\n➥ ${post.embed.record.record.value.text}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return post.record.text;
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
import {
|
|
||||||
AppBskyEmbedImages,
|
|
||||||
AppBskyEmbedRecord,
|
|
||||||
AppBskyEmbedRecordWithMedia,
|
|
||||||
type AppBskyFeedDefs,
|
|
||||||
} from "@atproto/api";
|
|
||||||
|
|
||||||
export function parseEmbedImage(post: AppBskyFeedDefs.PostView) {
|
|
||||||
if (AppBskyEmbedRecord.isView(post.embed)) {
|
|
||||||
const { success: isView } = AppBskyEmbedRecord.validateView(
|
|
||||||
post.embed,
|
|
||||||
);
|
|
||||||
if (isView && AppBskyEmbedRecord.isViewRecord(post.embed.record)) {
|
|
||||||
const { success: isViewRecord } =
|
|
||||||
AppBskyEmbedRecord.validateViewRecord(post.embed.record);
|
|
||||||
if (
|
|
||||||
isViewRecord &&
|
|
||||||
post.embed.record.embeds &&
|
|
||||||
post.embed.record.embeds.every((v) =>
|
|
||||||
AppBskyEmbedImages.isView(v),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
const validImageView = post.embed.record.embeds.every((v) =>
|
|
||||||
AppBskyEmbedImages.validateView(v),
|
|
||||||
);
|
|
||||||
if (validImageView) {
|
|
||||||
// return post.embed.record.embeds[0].images[0].fullsize;
|
|
||||||
const embeds = post.embed.record
|
|
||||||
.embeds as AppBskyEmbedImages.View[];
|
|
||||||
return embeds.flatMap((e) => e.images.map((i) => i.fullsize));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (AppBskyEmbedRecordWithMedia.isView(post.embed)) {
|
|
||||||
const { success: isView } =
|
|
||||||
AppBskyEmbedRecordWithMedia.validateView(post.embed);
|
|
||||||
if (isView && AppBskyEmbedImages.isView(post.embed.media)) {
|
|
||||||
const { success: isImageView } = AppBskyEmbedImages.validateView(
|
|
||||||
post.embed.media,
|
|
||||||
);
|
|
||||||
if (isImageView) {
|
|
||||||
// return post.embed.media.images[0].fullsize;
|
|
||||||
return post.embed.media.images.map((i) => i.fullsize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (AppBskyEmbedImages.isView(post.embed)) {
|
|
||||||
const { success: isImageView } = AppBskyEmbedImages.validateView(
|
|
||||||
post.embed,
|
|
||||||
);
|
|
||||||
if (isImageView) {
|
|
||||||
// return post.embed.images[0].fullsize;
|
|
||||||
return post.embed.images.map((i) => i.fullsize);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return post.author.avatar ?? "";
|
|
||||||
}
|
|
||||||
76
src/main.ts
76
src/main.ts
|
|
@ -1,76 +0,0 @@
|
||||||
import "./util/checkEnv.ts";
|
|
||||||
|
|
||||||
import { BskyAgent } from "@atproto/api";
|
|
||||||
import { serve } from "@hono/node-server";
|
|
||||||
import { Hono } from "hono";
|
|
||||||
import { HTTPException } from "hono/http-exception";
|
|
||||||
import { Redis } from "ioredis";
|
|
||||||
import { getOEmbed } from "./routes/getOEmbed.ts";
|
|
||||||
import { getPost } from "./routes/getPost.tsx";
|
|
||||||
import { getProfile } from "./routes/getProfile.tsx";
|
|
||||||
|
|
||||||
// biome-ignore lint/style/noNonNullAssertion: check is ran at app start
|
|
||||||
const redis = new Redis(6379, process.env.REDIS_HOSTNAME!);
|
|
||||||
|
|
||||||
const agent = new BskyAgent({
|
|
||||||
// biome-ignore lint/style/noNonNullAssertion: check is ran at app start
|
|
||||||
service: process.env.BSKY_SERVICE_URL!,
|
|
||||||
persistSession: async (_evt, session) => {
|
|
||||||
if (session) {
|
|
||||||
await redis.set("session", JSON.stringify(session));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const app = new Hono<HonoEnv>();
|
|
||||||
|
|
||||||
app.use("*", async (c, next) => {
|
|
||||||
const session = await redis.get("session");
|
|
||||||
try {
|
|
||||||
if (session) {
|
|
||||||
const login = await agent.resumeSession(JSON.parse(session));
|
|
||||||
if (!login.success) {
|
|
||||||
await agent.login({
|
|
||||||
// biome-ignore lint/style/noNonNullAssertion: check is ran at app start
|
|
||||||
identifier: process.env.BSKY_AUTH_USERNAME!,
|
|
||||||
// biome-ignore lint/style/noNonNullAssertion: check is ran at app start
|
|
||||||
password: process.env.BSKY_AUTH_PASSWORD!,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await agent.login({
|
|
||||||
// biome-ignore lint/style/noNonNullAssertion: check is ran at app start
|
|
||||||
identifier: process.env.BSKY_AUTH_USERNAME!,
|
|
||||||
// biome-ignore lint/style/noNonNullAssertion: check is ran at app start
|
|
||||||
password: process.env.BSKY_AUTH_PASSWORD!,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
c.set("Agent", agent);
|
|
||||||
} catch (error) {
|
|
||||||
const err = new Error("Failed to login to Bluesky!", {
|
|
||||||
cause: error,
|
|
||||||
});
|
|
||||||
throw new HTTPException(500, {
|
|
||||||
message: `${err.message} \n\n ${err.cause} \n\n ${err.stack}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return next();
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get("/profile/:user/post/:post", getPost);
|
|
||||||
app.get("/https://bsky.app/profile/:user/post/:post", getPost);
|
|
||||||
|
|
||||||
app.get("/profile/:user", getProfile);
|
|
||||||
app.get("/https://bsky.app/profile/:user", getProfile);
|
|
||||||
|
|
||||||
app.get("/oembed", getOEmbed);
|
|
||||||
|
|
||||||
serve(
|
|
||||||
{
|
|
||||||
...app,
|
|
||||||
port: Number(process.env.PORT ?? 8787),
|
|
||||||
},
|
|
||||||
(addr) => {
|
|
||||||
console.log(`Listening on http://localhost:${addr.port}`);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
import type { Handler } from "hono";
|
|
||||||
|
|
||||||
export enum OEmbedTypes {
|
|
||||||
Post = 1,
|
|
||||||
Profile = 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getOEmbed: Handler<HonoEnv, "/oembed"> = async (c) => {
|
|
||||||
const type = +(c.req.query("type") ?? 0);
|
|
||||||
const avatar = c.req.query("avatar");
|
|
||||||
|
|
||||||
const defaults = {
|
|
||||||
provider_name: "FixBluesky",
|
|
||||||
provider_url: "https://bsyy.app/",
|
|
||||||
thumbnail_url: avatar,
|
|
||||||
thumbnail_width: 1000,
|
|
||||||
thumbnail_height: 1000,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (type === OEmbedTypes.Post) {
|
|
||||||
const { replies, reposts, likes } = c.req.query();
|
|
||||||
return c.json({
|
|
||||||
author_name: `🗨️ ${replies} ♻️ ${reposts} 💙 ${likes}`,
|
|
||||||
...defaults,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (type === OEmbedTypes.Profile) {
|
|
||||||
const { follows, posts } = c.req.query();
|
|
||||||
return c.json({
|
|
||||||
author_name: `👤 ${follows} followers\n🗨️ ${posts} skeets`,
|
|
||||||
...defaults,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return c.json(defaults, 400);
|
|
||||||
};
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
import type { Handler } from "hono";
|
|
||||||
import { HTTPException } from "hono/http-exception";
|
|
||||||
import { Post } from "../components/Post.tsx";
|
|
||||||
import { fetchPost } from "../lib/fetchPostData.ts";
|
|
||||||
|
|
||||||
export const getPost: Handler<
|
|
||||||
HonoEnv,
|
|
||||||
| "/profile/:user/post/:post"
|
|
||||||
| "/https://bsky.app/profile/:user/post/:post"
|
|
||||||
> = async (c) => {
|
|
||||||
const { user, post } = c.req.param();
|
|
||||||
const agent = c.get("Agent");
|
|
||||||
const { data, success } = await fetchPost(agent, { user, post });
|
|
||||||
if (!success) {
|
|
||||||
throw new HTTPException(500, {
|
|
||||||
message: "Failed to fetch the post!",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.html(
|
|
||||||
<Post
|
|
||||||
post={data.posts[0]}
|
|
||||||
url={c.req.path}
|
|
||||||
appDomain={process.env.FIXBLUESKY_APP_DOMAIN ?? "bsyy.app"}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
import type { Handler } from "hono";
|
|
||||||
import { HTTPException } from "hono/http-exception";
|
|
||||||
import { Profile } from "../components/Profile.tsx";
|
|
||||||
import { fetchProfile } from "../lib/fetchProfile.ts";
|
|
||||||
|
|
||||||
export const getProfile: Handler<
|
|
||||||
HonoEnv,
|
|
||||||
"/profile/:user" | "/https://bsky.app/profile/:user"
|
|
||||||
> = async (c) => {
|
|
||||||
const { user } = c.req.param();
|
|
||||||
const agent = c.get("Agent");
|
|
||||||
const { data, success } = await fetchProfile(agent, { user });
|
|
||||||
if (!success) {
|
|
||||||
throw new HTTPException(500, {
|
|
||||||
message: "Failed to fetch the profile!",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return c.html(
|
|
||||||
<Profile
|
|
||||||
profile={data}
|
|
||||||
url={c.req.path}
|
|
||||||
appDomain={process.env.FIXBLUESKY_APP_DOMAIN ?? "bsyy.app"}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
if (!process.env.REDIS_HOSTNAME) {
|
|
||||||
throw new Error("REDIS_HOSTNAME is not defined");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!process.env.BSKY_SERVICE_URL) {
|
|
||||||
throw new Error("BSKY_SERVICE_URL is not defined");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!process.env.BSKY_AUTH_USERNAME) {
|
|
||||||
throw new Error("BSKY_AUTH_USERNAME is not defined");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!process.env.BSKY_AUTH_PASSWORD) {
|
|
||||||
throw new Error("BSKY_AUTH_PASSWORD is not defined");
|
|
||||||
}
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"allowImportingTsExtensions": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"alwaysStrict": true,
|
|
||||||
"declaration": true,
|
|
||||||
"declarationMap": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"importHelpers": false,
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
"jsxImportSource": "hono/jsx",
|
|
||||||
"lib": ["esnext"],
|
|
||||||
"module": "NodeNext",
|
|
||||||
"moduleResolution": "NodeNext",
|
|
||||||
"newLine": "lf",
|
|
||||||
"noEmit": true,
|
|
||||||
"noEmitHelpers": false,
|
|
||||||
"noFallthroughCasesInSwitch": true,
|
|
||||||
"noImplicitReturns": true,
|
|
||||||
"noUnusedLocals": true,
|
|
||||||
"noUnusedParameters": true,
|
|
||||||
"preserveConstEnums": true,
|
|
||||||
"pretty": true,
|
|
||||||
"removeComments": false,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"sourceMap": true,
|
|
||||||
"strict": true,
|
|
||||||
"target": "ESNext",
|
|
||||||
"useDefineForClassFields": true
|
|
||||||
},
|
|
||||||
"include": ["src/**/*"],
|
|
||||||
"exclude": ["node_modules", "dist/**/*"]
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue