Back to APIs list

Bluesky API examples & templates

Use these vals as a playground to view and fork Bluesky API examples and templates on Val Town. Run any example below or find templates that can be used as a pre-built solution.

Evaluate tweet or Bluesky post

#bookmarklet

Readme
1
2
3
function run() {
window.location = new URL(`/${document.URL}`, "https://vladimyr-posteval.web.val.run/");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { getPostThread, resolveHandle } from "https://esm.town/v/vladimyr/libbluesky";
import { toPairs } from "https://esm.town/v/vladimyr/toPairs";
import ky from "npm:ky";
export async function fetchPost(url: string | URL) {
const postURL = new URL(url);
if (postURL.hostname !== "bsky.app") {
throw new TypeError("Invalid post URL");
}
const pathSegments = postURL.pathname.split("/").filter(Boolean);
const pairs = toPairs(pathSegments);
const { profile, post } = Object.fromEntries(pairs as Iterable<[PropertyKey, string]>);
const { did } = await resolveHandle(profile);
const { thread } = await getPostThread(`at://${did}/app.bsky.feed.post/${post}`);
return thread.post;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// import { fetchTweet } from "https://esm.town/v/dpetrouk/fetchTweet?v=35";
import { eval_ } from "https://esm.town/v/stevekrouse/eval_";
import { fetchPost as fetchBskyPost } from "https://esm.town/v/vladimyr/fetchBlueskyPost";
import { fetchTweet } from "https://esm.town/v/vladimyr/fetchTweet";
export default async function(req: Request): Promise<Response> {
const reqURL = new URL(req.url);
const query = reqURL.pathname.slice(1);
if (query.startsWith("https://")) {
const pathname = query.replace(/^https:\/\//, "/");
const redirectURL = new URL(pathname, reqURL);
return Response.redirect(redirectURL);
}
let post, code, result;
try {
post = await fetchPost(`https://${query}`);
result = await postEval(post, [req]);
return result;
} catch (e) {
return Response.json({ code, post, result }, { status: 500 });
}
}
export async function postEval(post, args?) {
const code = post.text.split("```")[1]
.trim()
.replaceAll(/&lt;/g, "<")
.replaceAll(/&gt;/g, ">")
.replaceAll(/&amp;/g, "&");
return eval_(code, args);
}
export async function fetchPost(url: string | URL) {
const postURL = new URL(url);
if (["x.com", "twitter.com"].includes(postURL.hostname)) {
const tweet = await fetchTweet(postURL.href);
return { text: tweet.text };
}
if (postURL.hostname === "bsky.app") {
const bskyPost = await fetchBskyPost(postURL);
return { text: bskyPost.record.text };
}
throw new TypeError("Invalid post URL");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import ky from "npm:ky";
const prefixUrl = "https://public.api.bsky.app/xrpc/";
// @see: https://docs.bsky.app/docs/api/com-atproto-identity-resolve-handle
export function resolveHandle(handle: string) {
return ky.get("com.atproto.identity.resolveHandle", {
searchParams: { handle },
prefixUrl,
}).json();
}
// @see: https://docs.bsky.app/docs/api/app-bsky-feed-get-post-thread
export function getPostThread(uri: string) {
return ky.get("app.bsky.feed.getPostThread", {
searchParams: { uri },
prefixUrl,
}).json();
}
Runs every 1 hrs
Fork
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import { encounteredIDs } from "https://esm.town/v/buttondown/encounteredIDs";
import { email } from "https://esm.town/v/std/email?v=9";
import { set } from "https://esm.town/v/std/set?v=11";
import { fetchJSON } from "https://esm.town/v/stevekrouse/fetchJSON?v=41";
export const runner = async () => {
const ALERT_STRINGS = [
"buttondown",
"buttondown.email",
];
// Let's not email ourselves when we post..
const USERNAME_DENYLIST = [
"buttondown.bsky.social",
];
const promises = ALERT_STRINGS.map(async function(keyword) {
let posts = await fetchJSON(
`https://search.bsky.social/search/posts?q=${keyword}`,
);
let newPosts = posts.filter((post) =>
!encounteredIDs.includes(post.tid)
&& !USERNAME_DENYLIST.includes(post.user.handle)
);
newPosts.map((post) => {
encounteredIDs.push(post.tid);
});
await Promise.all(newPosts.map(async (post) => {
const id = post.tid.split("/")[1];
// We intentionally dedupe subjects so that multiple responses all
// have their own threads; this makes it easier to figure out what
// I've seen and/or responded to over time.
await email({
html: `https://bsky.app/profile/${post.user.did}/post/${id}`,
subject: `New post in Bluesky for ${keyword} (${id})`,
});
}));
});
await Promise.all(promises);
await set("encounteredIDs", encounteredIDs);
};

Bluesky keyword alerts

Custom notifications for when you, your company, or anything you care about is mentioned on Bluesky.

1. Query

Specify your queries in the queries variable.

Bluesky doesn't support boolean OR yet so we do a separate search for each keyword.

2. Notification

Below I'm sending these mentions to a private channel in our company Discord, but you can customize that to whatever you want, @std/email, Slack, Telegram, whatever.

Readme
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import { blob } from "https://esm.town/v/std/blob";
import { discordWebhook } from "https://esm.town/v/stevekrouse/discordWebhook";
import { fetchJSON } from "https://esm.town/v/stevekrouse/fetchJSON";
import { searchBlueskyPosts } from "https://esm.town/v/stevekrouse/searchBlueskyPosts";
const encounteredIDs_KEY = "bluesky_encounteredIDs";
const queries = ["val town", "val.town"];
export const blueskyAlert = async () => {
let posts = (await Promise.all(queries.map(searchBlueskyPosts))).flat();
// filter for new posts
let encounteredIDs = await blob.getJSON(encounteredIDs_KEY) ?? [];
let newPosts = posts.filter((post) => !encounteredIDs.includes(post.tid));
await blob.setJSON(encounteredIDs_KEY, [
...encounteredIDs,
...newPosts.map((post) => post.tid),
]);
if (newPosts.length === 0) return;
// format
const content = posts.map(
post => `https://bsky.app/profile/${post.user.handle}/post/${post.tid.split("/")[1]}`,
).join(
"\n",
);
// notify
await discordWebhook({
url: Deno.env.get("mentionsDiscord"),
content,
});
return newPosts;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { fetchJSON } from "https://esm.town/v/stevekrouse/fetchJSON";
export function searchBlueskyPosts(query: string): Promise<BlueSkyPost[]> {
return fetchJSON(
`https://search.bsky.social/search/posts?q=${query}`,
);
}
interface BlueSkyPost {
tid: string;
cid: string;
user: { did: string; handle: string };
post: { createdAt: number; text: string; user: string };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import { fetch } from "https://esm.town/v/std/fetch";
import process from "node:process";
import { resume as resume2 } from "https://esm.town/v/ajax/resume";
export async function annoy() {
const resume = resume2;
// const boo = await import("https://esm.sh/@atproto/api");
const { default: BskyAgent } = await import("npm:@atproto/api");
console.log(BskyAgent);
const agent = new BskyAgent.BskyAgent({ service: "https://bsky.social" });
await agent.login({
identifier: process.env.BLUESKY_USERNAME!,
password: process.env.BLUESKY_PASSWORD!,
});
// const actor = await agent.getProfile();
// console.log({ actor });
// const followers = await agent.getFollowers();
// console.log(followers);
// const posts = await agent.getPosts({ uris: ["at://lordajax.bsky.social"] });
// console.log({ posts });
const timeline = await agent.getTimeline();
console.log({ timeline });
const mostRecentPost = timeline.data.feed[0];
console.log({ mostRecentPost });
const author = mostRecentPost.post.author.handle;
const recordText = mostRecentPost.post.record.text;
console.log({ author, recordText });
const prompt =
`Choose an esoteric word of the day. You will use this word in the following poem.
Write a poem about how all you want to do is drink and get high using the chosen word.
Make it funny and short. Talk about your friends Pam and Tom.
Make it sound like you are wise.
Be dark and moody.
Make sure your response is no longer than 270 characters.
Put the word first and definition and then the poem below.
The poem MUST contain the chosen word of the day.
You MUST include the definition of the word
Here is an example;
Bifurcate - divide into two branches or forks.
Bifurcate! My life is stale, my friends just wanna inebriate.
Tom wants a coke, Pam needs a beer, they laugh and joke as I just sneer.
Just leave me be, no need to beep, can't you see I just wanna sleep?
---
Copying the example above, find a new word and do as above.
`;
console.log({ prompt });
const response = await fetch("https://api.openai.com/v1/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + process.env.OPENAI_API_KEY, // Replace with your OpenAI API Key
},
body: JSON.stringify({
"prompt": prompt,
"model": "text-davinci-003",
"temperature": 1,
"max_tokens": 256,
"top_p": 1,
"frequency_penalty": 0,
"presence_penalty": 0,
}),
});
if (!response.ok) {
console.log(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log(data);
const message = data.choices[0].text.trim();
console.log({ message });
await agent.post({
text: message,
});
return "asd";
}

Bluesky RSS bot

This is a bot that polls an RSS feed for a new item every hour and posts it to Bluesky.

It's split into three parts:

  1. bsky_rss_poll
    • This function runs every hour and polls the provided RSS feed, turns it into XML and runs the check. If there is a new post, it tell rss_to_bskyto post a link (and the title) to Bluesky
  2. latest_rss
    • This is a stored object that keeps the latest object for the poll to test against
  3. rss_to_bsky
    • This function turns the text post into a rich text post and posts it to Bluesky
Readme
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import { fetch } from "https://esm.town/v/std/fetch";
import { rss_to_bsky } from "https://esm.town/v/jordan/rss_to_bsky";
import { set } from "https://esm.town/v/std/set?v=11";
import { latest_rss as latest_rss2 } from "https://esm.town/v/jordan/latest_rss";
export async function bsky_rss_poll() {
const { parseFeed } = await import("https://deno.land/x/rss/mod.ts");
const res = await fetch("https://v8.dev/blog.atom")
.then(res => res.text())
.then(res => parseFeed(res));
const title = res.entries[0].title.value, url = res.entries[0].id;
const latest_rss = JSON.stringify({ "title": title, "url": url });
if(latest_rss2 !== latest_rss) {
await set("latest_rss", latest_rss);
const post = `${title}
${url} `;
await Promise.all([
rss_to_bsky(post)
]);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import process from "node:process";
export let bskySocialEx = (async () => {
const { default: api } = await import("npm:@atproto/api");
const service = "https://bsky.social";
const agent = await new api.BskyAgent({ service });
await agent.login({
identifier: process.env.blueskyEmail,
password: process.env.blueskyPassword,
});
const follows = await agent.getFollowers({
actor: "stevekrouse.bsky.social",
});
return follows;
})();
// Forked from @lukas.bskySocial
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import { fetch } from "https://esm.town/v/std/fetch";
import process from "node:process";
import { resume as resume2 } from "https://esm.town/v/ajax/resume";
export async function annoy() {
const resume = resume2;
// const boo = await import("https://esm.sh/@atproto/api");
const { default: BskyAgent } = await import("npm:@atproto/api");
console.log(BskyAgent);
const agent = new BskyAgent.BskyAgent({ service: "https://bsky.social" });
await agent.login({
identifier: process.env.BLUESKY_USERNAME!,
password: process.env.BLUESKY_PASSWORD!,
});
// const actor = await agent.getProfile();
// console.log({ actor });
// const followers = await agent.getFollowers();
// console.log(followers);
// const posts = await agent.getPosts({ uris: ["at://lordajax.bsky.social"] });
// console.log({ posts });
const timeline = await agent.getTimeline();
console.log({ timeline });
const mostRecentPost = timeline.data.feed[0];
console.log({ mostRecentPost });
const author = mostRecentPost.post.author.handle;
const recordText = mostRecentPost.post.record.text;
console.log({ author, recordText });
const prompt =
`Choose an esoteric word of the day. You will use this word in the following poem.
Write a poem about how all you want to do is drink and get high using the chosen word.
Make it funny and short. Talk about your friends Pam and Tom.
Make it sound like you are wise.
Be dark and moody.
Make sure your response is no longer than 270 characters.
Put the word first and definition and then the poem below.
The poem MUST contain the chosen word of the day.
You MUST include the definition of the word
Here is an example;
Bifurcate - divide into two branches or forks.
Bifurcate! My life is stale, my friends just wanna inebriate.
Tom wants a coke, Pam needs a beer, they laugh and joke as I just sneer.
Just leave me be, no need to beep, can't you see I just wanna sleep?
---
Copying the example above, find a new word and do as above.
`;
console.log({ prompt });
const response = await fetch("https://api.openai.com/v1/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + process.env.OPENAI_API_KEY, // Replace with your OpenAI API Key
},
body: JSON.stringify({
"prompt": prompt,
"model": "text-davinci-003",
"temperature": 1,
"max_tokens": 256,
"top_p": 1,
"frequency_penalty": 0,
"presence_penalty": 0,
}),
});
if (!response.ok) {
console.log(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log(data);
const message = data.choices[0].text.trim();
console.log({ message });
await agent.post({
text: message,
});
return "asd";
}

Big League yourself! WARNING, this will unfollow every user you follow!

Readme
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
export const bigLeagueMe = async (req: Request) => {
const params = Object.fromEntries(new URL(req.url).searchParams.entries());
var {default: atproto} = await import("npm:@atproto/api");
const BskyAgent = atproto.
// YOUR bluesky handle
// Ex: user.bsky.social
const handle = params.HANDLE || "";
// YOUR bluesky password, or preferably an App Password (found in your client settings)
// Ex: abcd-1234-efgh-5678
const password = params.PASS || "";
// only update this if in a test environment
const agent = new BskyAgent({ service: "https://bsky.social" });
await agent.login({ identifier: handle, password });
let follows = [];
let cursor;
while (true) {
const following = await agent.app.bsky.graph.getFollows({
cursor,
actor: handle,
});
cursor = following.data.cursor;
follows = [...follows, ...following.data.follows];
if (!cursor)
break;
}
let resp = "";
for (const actor of follows) {
const [rkey, repo] = actor.viewer?.following?.split("/").reverse() || [];
resp += `Unfollowed ${actor.handle}\n`;
}
return new Response(resp, { headers: { "Content-Type": "text/text" } });
};
1
2
3
4
5
6
7
8
9
10
11
import { fetchJSON } from "https://esm.town/v/stevekrouse/fetchJSON?v=41";
export async function getInvitesFromReddit() {
const codeRegex = /bsky-social-[a-zA-Z0-9]+/g;
const result = await fetchJSON(
"https://www.reddit.com/r/blueskyinvites/.json"
);
const matches = JSON.stringify(result).match(codeRegex);
const codes = [...new Set(matches)];
return codes;
}

Passerelle RSS vers BlueSky

Ce script tourne une fois par heure et reposte les news de https://rezo.net/ vers le compte https://bsky.app/profile/rezo.net

Il utilise 3 éléments:

  • l'URL du flux RSS
  • une variable de stockage de l'état, qu'il faut créer initialement comme let storage_rss_rezo = {} et qui sera mise à jour par le script
  • les secrets du compte (username et mot de passe de l'application)

Il appelle @me.bsky_rss_poll qui lit le flux, vérifie avec l'état s'il y a du nouveau, et au besoin nettoie le post, puis l'envoie avec le script @me.post_to_bsky. Sans oublier de mettre à jour l'état pour le prochain run.

C'est un premier jet. Merci à @steve.krouse pour val.town et à @jordan pour ses scripts que j'ai bidouillés ici.

À faire éventuellement: améliorer la logique; poster vers twitter.

Readme
Runs every 1 hrs
Fork
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { set } from "https://esm.town/v/std/set?v=11";
import process from "node:process";
import { storage_rss_rezo } from "https://esm.town/v/fil/storage_rss_rezo";
import { bsky_rss_poll } from "https://esm.town/v/fil/bsky_rss_poll";
export async function cron_rezo_rss2bsky() {
await bsky_rss_poll(
"https://rezo.net/backend/tout",
storage_rss_rezo,
process.env.REZO_BSKY_USERNAME!,
process.env.REZO_BSKY_PASS!,
);
await set("storage_rss_rezo", storage_rss_rezo);
}
1
2
3
4
5
6
7
8
9
10
import { fetchJSON } from "https://esm.town/v/stevekrouse/fetchJSON?v=41";
// Use this to get the Bluesky posts from any user.
// Just pass in their username (like "vgr.bsky.social")
export const bskyPosts = async (repo = "scottraymond.com") => {
const { records } = await fetchJSON(
`https://bsky.social/xrpc/com.atproto.repo.listRecords?repo=${repo}&collection=app.bsky.feed.post`
);
return records;
};