블루스카이 봇(선택봇 @choicebot.bsky.social)을 만든 일지.
봇 기능
공백으로 구분된 선택지와 함께 봇을 멘션하면, 그 중 하나를 골라서 reply 해주는 봇
예: “고양이 강아지 망아지 부엉이” 멘션 ➡️ “부엉이” 응답
기술 스택
기능이 매우 간단하므로 Deno를 썼다. 무료 오라클 클라우드 인스턴스에 배포했다.
Jetstream
https://github.com/bluesky-social/jetstream : 블루스카이는 개발자를 위해서 모든 데이터 스트림을 ws로 제공한다. 봇 핸들을 태그한 포스트를 감지하기 위해 사용.
websocket을 그냥 쓰면 타입이 안 되어 있으니 @skyware/jetstream을 사용한다.
const jetstream = new Jetstream({
wantedCollections: ["app.bsky.feed.post"], // 포스트 정보만 가져오기
});
jetstream.onCreate("app.bsky.feed.post", (event) => {
if (shouldSendMessage()) {
sendMessage();
}
});
jetstream.start();
event
속에 본문, 멘션 정보 등 봇 동작에 필요한 정보가 담겨 있다.
포스트 감지
멘션 감지
export function isMention(
event: CommitCreateEvent<"app.bsky.feed.post">,
botDid: string
) {
return (
event.commit.record.facets?.some((facet) =>
facet.features.some(
(feature) =>
feature.$type === "app.bsky.richtext.facet#mention" &&
feature.did === botDid
)
) ?? false
);
}
event.commit.record.facets
안에 멘션 정보가 들어있다. 멘션한 DID가 봇의 DID와 같을 경우 true.
reply 감지
reply(스레드)에는 멘션 정보가 없기 때문에 따로 처리해줘야 한다.
export function isReply(
event: CommitCreateEvent<"app.bsky.feed.post">,
botDid: string
) {
if (event.commit.record.reply) {
const uri = new AtUri(event.commit.record.reply.parent.uri);
if (uri.host === botDid) {
return true;
}
}
return false;
}
reply의 부모 포스트의 uri에 봇 DID가 있는 경우 true.
보내기
@atproto/api를 써서 포스트를 생성하려면 com.atproto.repo.createRecord
를 사용하면 된다.
agent.com.atproto.repo.createRecord({
collection: "app.bsky.feed.post",
record: {
$type: "app.bsky.feed.post",
text,
createdAt: new Date().toISOString(),
reply: {
root: { uri, cid },
parent: { uri, cid },
},
},
repo: BOT_DID,
});
포스트의 reply가 되는 것이므로 멘션 정보는 적지 않고 부모와 최상위 포스트의 uri
cid
를 작성해준다. repo
는 봇의 DID.
Rate Limiting
블루스카이의 rate limits는 포스트 생성 기준 1시간 1666개다(https://docs.bsky.app/docs/advanced-guides/rate-limits). p-ratelimit을 써서 널널하게 1시간에 1600개로 설정했다.
배포
오라클 클라우드의 인스턴스에 pup을 써서 구동했다(왜 pm2를 안 썼냐면… 인스턴스에 node.js를 깔고 싶지 않았음).
‼️미래의 나에게 조언: 절대로 오라클 리눅스를 쓰지 말 것. 한국 패키지 매니저 서버가 너무 느림. 우분투를 써야 함.
후기
봇 기능이 간단해서 그런지 코드도 100줄 정도밖에 안 된다. SNS 자동봇 주제에 기능이 복잡하면 그게 이상한 거지만…
단어 선택 말고 다른 기능을 넣는다고 해도 기존 코드 90%는 재활용이 가능하니까 아주 간단하게 만들 수 있을 듯.