diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx
new file mode 100644
index 0000000..598547d
--- /dev/null
+++ b/src/components/Layout.tsx
@@ -0,0 +1,30 @@
+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`
+
+
+
+
+
+
+
+
+ ${children}
+
+
+ `;
+};
+
+{
+ /* */
+}
diff --git a/src/components/Post.tsx b/src/components/Post.tsx
new file mode 100644
index 0000000..59d8aea
--- /dev/null
+++ b/src/components/Post.tsx
@@ -0,0 +1,38 @@
+import { AppBskyFeedDefs } from "@atproto/api";
+
+import { Layout } from "./Layout";
+import { OEmbedTypes } from "../routes/getOEmbed";
+import { parseEmbedImage } from "../lib/parseEmbedImage";
+import { parseEmbedDescription } from "../lib/parseEmbedDescription";
+
+interface PostProps {
+ post: AppBskyFeedDefs.PostView;
+ url: string;
+ appDomain: string;
+}
+
+export const Post = ({ post, url, appDomain }: PostProps) => (
+
+
+
+
+
+ {!(parseEmbedImage(post) === post.author.avatar) && (
+
+ )}
+
+
+
+
+
+);
diff --git a/src/components/Profile.tsx b/src/components/Profile.tsx
new file mode 100644
index 0000000..b2e9008
--- /dev/null
+++ b/src/components/Profile.tsx
@@ -0,0 +1,31 @@
+import { AppBskyActorDefs } from "@atproto/api";
+
+import { Layout } from "./Layout";
+import { OEmbedTypes } from "../routes/getOEmbed";
+
+interface ProfileProps {
+ profile: AppBskyActorDefs.ProfileViewDetailed;
+ url: string;
+ appDomain: string;
+}
+
+export const Profile = ({ profile, url, appDomain }: ProfileProps) => (
+
+
+
+
+
+
+
+
+);
diff --git a/src/globals.d.ts b/src/globals.d.ts
index 47d605d..b4b9687 100644
--- a/src/globals.d.ts
+++ b/src/globals.d.ts
@@ -6,6 +6,7 @@ declare global {
BSKY_SERVICE_URL: string;
BSKY_AUTH_USERNAME: string;
BSKY_AUTH_PASSWORD: string;
+ FIXBLUESKY_APP_DOMAIN: string;
};
Variables: {
Agent: BskyAgent;
diff --git a/src/index.ts b/src/index.ts
index e818869..04788f4 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,8 +1,10 @@
import { Hono } from "hono";
import { BskyAgent } from "@atproto/api";
-import { getPost } from "./lib/getPost";
-import { getPostData } from "./lib/getPostData";
-import { getPostOEmbed } from "./lib/getPostOEmbed";
+import { getPost } from "./routes/getPost";
+import { getPostData } from "./routes/getPostData";
+import { getOEmbed } from "./routes/getOEmbed";
+import { getProfileData } from "./routes/getProfileData";
+import { getProfile } from "./routes/getProfile";
const app = new Hono();
@@ -26,6 +28,12 @@ app.get("/https://bsky.app/profile/:user/post/:post", getPost);
app.get("/profile/:user/post/:post/json", getPostData);
app.get("/https://bsky.app/profile/:user/post/:post/json", getPostData);
-app.get("/oembed", getPostOEmbed);
+app.get("/profile/:user", getProfile);
+app.get("/https://bsky.app/profile/:user", getProfile);
+
+app.get("/profile/:user/json", getProfileData);
+app.get("/https://bsky.app/profile/:user/json", getProfileData);
+
+app.get("/oembed", getOEmbed);
export default app;
diff --git a/src/lib/fetchPostData.ts b/src/lib/fetchPostData.ts
new file mode 100644
index 0000000..3bd4a51
--- /dev/null
+++ b/src/lib/fetchPostData.ts
@@ -0,0 +1,18 @@
+import { 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}`],
+ });
+}
diff --git a/src/lib/fetchProfile.ts b/src/lib/fetchProfile.ts
new file mode 100644
index 0000000..4a82a3e
--- /dev/null
+++ b/src/lib/fetchProfile.ts
@@ -0,0 +1,14 @@
+import { BskyAgent } from "@atproto/api";
+
+export interface fetchProfileOptions {
+ user: string;
+}
+
+export async function fetchProfile(
+ agent: BskyAgent,
+ { user }: fetchProfileOptions
+) {
+ return agent.getProfile({
+ actor: user,
+ });
+}
diff --git a/src/lib/getPost.ts b/src/lib/getPost.ts
deleted file mode 100644
index 4da1b6a..0000000
--- a/src/lib/getPost.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { Handler } from "hono";
-import { genHTML } from "../util/genHTML";
-
-export const getPost: Handler<
- Env,
- "/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: userData } = await agent.getProfile({
- actor: user,
- });
- const { data: postData } = await agent.getPosts({
- uris: [`at://${userData.did}/app.bsky.feed.post/${post}`],
- });
- return c.html(genHTML(postData.posts[0], c.req.path));
-};
diff --git a/src/lib/getPostOEmbed.ts b/src/lib/getPostOEmbed.ts
deleted file mode 100644
index 6553661..0000000
--- a/src/lib/getPostOEmbed.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { Handler } from "hono";
-
-export const getPostOEmbed: Handler = async (c) => {
- const { handle, display_name, avatar } = c.req.query();
- return c.json({
- author_name: `${display_name} (@${handle})`,
- author_url: `https://bsky.app/profile/${handle}`,
- provider_name: "FixBluesky",
- provider_url: "https://bsyy.app/",
- thumbnail_url: avatar,
- thumbnail_width: 1000,
- thumbnail_height: 1000,
- });
-};
diff --git a/src/lib/parseEmbedDescription.ts b/src/lib/parseEmbedDescription.ts
new file mode 100644
index 0000000..7e47ca9
--- /dev/null
+++ b/src/lib/parseEmbedDescription.ts
@@ -0,0 +1,39 @@
+import {
+ AppBskyEmbedRecord,
+ AppBskyEmbedRecordWithMedia,
+ AppBskyFeedDefs,
+ AppBskyFeedPost,
+} 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 "";
+}
diff --git a/src/lib/parseEmbedImage.ts b/src/lib/parseEmbedImage.ts
new file mode 100644
index 0000000..c7a2014
--- /dev/null
+++ b/src/lib/parseEmbedImage.ts
@@ -0,0 +1,51 @@
+import {
+ AppBskyEmbedImages,
+ AppBskyEmbedRecord,
+ AppBskyEmbedRecordWithMedia,
+ 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 &&
+ AppBskyEmbedImages.isView(post.embed.record.embeds[0])
+ ) {
+ const { success: isImageView } = AppBskyEmbedImages.validateView(
+ post.embed.record.embeds[0]
+ );
+ if (isImageView) {
+ return post.embed.record.embeds[0].images[0].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;
+ }
+ }
+ }
+ if (AppBskyEmbedImages.isView(post.embed)) {
+ const { success: isImageView } = AppBskyEmbedImages.validateView(
+ post.embed
+ );
+ if (isImageView) {
+ return post.embed.images[0].fullsize;
+ }
+ }
+ return post.author.avatar ?? "";
+}
diff --git a/src/routes/getOEmbed.ts b/src/routes/getOEmbed.ts
new file mode 100644
index 0000000..d935f8f
--- /dev/null
+++ b/src/routes/getOEmbed.ts
@@ -0,0 +1,35 @@
+import { Handler } from "hono";
+
+export enum OEmbedTypes {
+ Post = 1,
+ Profile,
+}
+
+export const getOEmbed: Handler = 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);
+};
diff --git a/src/routes/getPost.tsx b/src/routes/getPost.tsx
new file mode 100644
index 0000000..1f253d0
--- /dev/null
+++ b/src/routes/getPost.tsx
@@ -0,0 +1,26 @@
+import { Handler } from "hono";
+import { HTTPException } from "hono/http-exception";
+import { fetchPost } from "../lib/fetchPostData";
+import { Post } from "../components/Post";
+
+export const getPost: Handler<
+ Env,
+ "/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(genHTML(data.posts[0], c.req.path));
+ return c.html(
+
+ );
+};
diff --git a/src/lib/getPostData.ts b/src/routes/getPostData.ts
similarity index 50%
rename from src/lib/getPostData.ts
rename to src/routes/getPostData.ts
index a30cbeb..b1b5212 100644
--- a/src/lib/getPostData.ts
+++ b/src/routes/getPostData.ts
@@ -1,4 +1,6 @@
import { Handler } from "hono";
+import { HTTPException } from "hono/http-exception";
+import { fetchPost } from "../lib/fetchPostData";
export const getPostData: Handler<
Env,
@@ -7,11 +9,11 @@ export const getPostData: Handler<
> = async (c) => {
const { user, post } = c.req.param();
const agent = c.get("Agent");
- const { data: userData } = await agent.getProfile({
- actor: user,
- });
- const { data } = await agent.getPosts({
- uris: [`at://${userData.did}/app.bsky.feed.post/${post}`],
- });
+ const { data, success } = await fetchPost(agent, { user, post });
+ if (!success) {
+ throw new HTTPException(500, {
+ message: "Failed to fetch the post!",
+ });
+ }
return c.json(data);
};
diff --git a/src/routes/getProfile.tsx b/src/routes/getProfile.tsx
new file mode 100644
index 0000000..8068f9d
--- /dev/null
+++ b/src/routes/getProfile.tsx
@@ -0,0 +1,25 @@
+import { Handler } from "hono";
+import { HTTPException } from "hono/http-exception";
+import { fetchProfile } from "../lib/fetchProfile";
+import { Profile } from "../components/Profile";
+
+export const getProfile: Handler<
+ Env,
+ "/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(
+
+ );
+};
diff --git a/src/routes/getProfileData.ts b/src/routes/getProfileData.ts
new file mode 100644
index 0000000..96c2414
--- /dev/null
+++ b/src/routes/getProfileData.ts
@@ -0,0 +1,20 @@
+/** @jsx jsx */
+import { jsx } from "hono/jsx";
+import { Handler } from "hono";
+import { HTTPException } from "hono/http-exception";
+import { fetchProfile } from "../lib/fetchProfile";
+
+export const getProfileData: Handler<
+ Env,
+ "/profile/:user/json" | "/https://bsky.app/profile/:user/json"
+> = 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.json(data);
+};
diff --git a/src/util/genHTML.ts b/src/util/genHTML.ts
deleted file mode 100644
index fd2124e..0000000
--- a/src/util/genHTML.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import {
- AppBskyFeedDefs,
- AppBskyEmbedImages,
- AppBskyFeedPost,
-} from "@atproto/api";
-import { html } from "hono/html";
-
-export function genHTML(post: AppBskyFeedDefs.PostView, url: string) {
- const removeLeadingSlash = url.substring(1);
- const redirectUrl = removeLeadingSlash.startsWith("https://")
- ? removeLeadingSlash
- : `https://bsky.app/${removeLeadingSlash}`;
- return html`
-
-
-
-
-
-
-
-
-
-
- ${AppBskyEmbedImages.isView(post.embed) &&
- html``}
-
-
-
-
-
-
-
- `;
-}
diff --git a/tsconfig.json b/tsconfig.json
index 9cd8489..4a2abdb 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -5,13 +5,9 @@
"moduleResolution": "node",
"esModuleInterop": true,
"strict": true,
- "lib": [
- "esnext"
- ],
- "types": [
- "@cloudflare/workers-types"
- ],
+ "lib": ["esnext"],
+ "types": ["@cloudflare/workers-types"],
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx"
- },
-}
\ No newline at end of file
+ }
+}