GitHubでStarを付けたらBlueskyに投稿する


## はじめに

こんにちは。夏休みは新潟からフェリーで北海道に行く予定を立てている[@yug1224](https://twitter.com/yug1224)です。

最近はTwitterの突発的な仕様変更により、Twitter以外の分散型SNSも注目されるようになってきましたね。自分もちょうど[Bluesky](https://blueskyweb.xyz/)の招待コードをいただいたので登録して遊んでいます。

今回はGitHubでStarを付けたらBlueskyに投稿するプログラムを作ってみたので紹介します！

![青空と白い雲](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106236/706785ae-a439-2413-2bd4-6eb269fe3ba9.jpeg)

## Blueskyとは？🤔

まずそもそも[Bluesky](https://blueskyweb.xyz/)とは何か？

BlueskyとはTwitterの創業者であるジャック・ドーシー氏が支援する分散型SNSであり、現在はプライベートベータ中のサービスですね。

今のBlueskyはIT系の人が多く、2010年前後のTwitterのような雰囲気もあり、個人的には居心地の良さを感じていますｗ

ざっくりと知るならギズモードの記事がわかりやすいかなと思います。

https://www.gizmodo.jp/2023/07/bluesky-now.html

## Blueskyに投稿するプログラムを書く💻

Blueskyは[AT Protocol](https://atproto.com/)と呼ばれるプロトコルを使って情報のやり取りを行いますが、すでに[atproto](https://github.com/bluesky-social/atproto/)というライブラリが公開されているので、まずはこれを使ってBlueskyにテキストを投稿するプログラムを実装していきます。

https://github.com/bluesky-social/atproto/

```ts
import 'https://deno.land/std@0.193.0/dotenv/load.ts'
import AtprotoAPI from 'npm:@atproto/api'

// Blueskyに接続
const { BskyAgent, RichText } = AtprotoAPI
const service = 'https://bsky.social'
const agent = new BskyAgent({ service })
const identifier = Deno.env.get('BLUESKY_IDENTIFIER') || ''
const password = Deno.env.get('BLUESKY_PASSWORD') || ''
await agent.login({ identifier, password })

// Blueskyに投稿
const postObj = {
  $type: 'app.bsky.feed.post',
  text: 'Hello, AT Protocol!',
}
const result = await agent.post(postObj)
console.log(result)
```

![Blueskyにテキストを投稿する様子](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106236/b64482f9-444c-3e84-9faa-76634d8aaba3.gif)

`agent.login()`をしてから`agent.post()`をするだけ！簡単ですね！

## リンクカード情報を投稿に含める🃏

次は一気に実装していきます。

Blueskyで投稿にリンクカードを表示するためには、リンクカードに表示する情報を投稿に含める必要があるので、OGP情報を事前に取得します。

GitHubプロフィールリンクの末尾に`.atom`を付けることで自身のActivityをRSSとして取得できるので、これを利用して情報を集めていきます。例: [https://github.com/yug1224.atom](https://github.com/yug1224.atom)

```ts
import 'https://deno.land/std@0.193.0/dotenv/load.ts'
import { Image } from 'https://deno.land/x/imagescript@1.2.15/mod.ts'
import { parseFeed } from 'https://deno.land/x/rss@0.6.0/mod.ts'
import AtprotoAPI from 'npm:@atproto/api'
import ogs from 'npm:open-graph-scraper'

// rss feedから最新のスターを付けた記事リストを取得
const getStarredItemList = async () => {
  const response = await fetch(Deno.env.get('RSS_URL') || '')
  const xml = await response.text()
  const feed = await parseFeed(xml)

  const foundList = feed.entries.reverse().filter((item) => {
    return (
      new Date(new Date(Deno.env.get('LAST_EXECUTION_TIME') || '')) <
        new Date(item.published) &&
      new RegExp('starred', 'g').test(item.title.value)
    )
  })
  return foundList
}
const starredItemList = await getStarredItemList()
console.log(starredItemList)

// 対象がなかったら終了
if (!starredItemList.length) {
  console.log('not found starred item')
  Deno.exit(0)
}

// Blueskyに接続
const { BskyAgent, RichText } = AtprotoAPI
const service = 'https://bsky.social'
const agent = new BskyAgent({ service })
const identifier = Deno.env.get('BLUESKY_IDENTIFIER') || ''
const password = Deno.env.get('BLUESKY_PASSWORD') || ''
await agent.login({ identifier, password })

// 取得した記事リストをループ処理
for await (const starredItem of starredItemList) {
  // 投稿予定のテキストを作成
  const text = `${starredItem.title.value}\n${starredItem.links[0].href}`

  const pattern =
    /https?:\/\/[-_.!~*\'()a-zA-Z0-9;\/?:\@&=+\$,%#\u3000-\u30FE\u4E00-\u9FA0\uFF01-\uFFE3]+/g
  const [url] = text.match(pattern) || ['']

  // URLからOGPの取得
  const getOgp = async (url: string) => {
    const { result } = await ogs({ url })

    const ogImage = result.ogImage?.at(0)
    const response = await fetch(ogImage?.url || '')
    const buffer = await response.arrayBuffer()

    const image = await Image.decode(buffer)
    const resizedImage = await image
      .resize(800, Image.RESIZE_AUTO)
      .encodeJPEG(80)

    return {
      url: ogImage?.url || '',
      type: ogImage?.type || '',
      description: result.ogDescription || '',
      title: result.ogTitle || '',
      image: resizedImage,
    }
  }
  const og = await getOgp(url)

  // 画像をアップロード
  const uploadedImage = await agent.uploadBlob(og.image, {
    encoding: 'image/jpeg',
  })

  // Blueskyに投稿
  const rt = new RichText({ text })
  await rt.detectFacets(agent)
  console.log(rt.text, rt.facets)

  const postObj = {
    $type: 'app.bsky.feed.post',
    text: rt.text,
    facets: rt.facets,
    embed: {
      $type: 'app.bsky.embed.external',
      external: {
        uri: url,
        thumb: {
          $type: 'blob',
          ref: {
            $link: uploadedImage.data.blob.ref.toString(),
          },
          mimeType: uploadedImage.data.blob.mimeType,
          size: uploadedImage.data.blob.size,
        },
        title: og.title,
        description: og.description,
      },
    },
  }

  const result = await agent.post(postObj)
  console.log(result)
}
```

![GitHub Starの情報を取得し、リンクカードとともに投稿する](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106236/2ad18dd9-0d2a-f3e5-a618-bac06e8dcfb6.gif)

OGP情報を元にリンクカードも表示できました！簡単ですね！

## GitHub Actionsで定期実行する⏰

最後にGitHub Actionsで定期実行するように設定します。

`BLUESKY_IDENTIFIER` `BLUESKY_PASSWORD`はBlueskyのSettingsから取得して、secretsに登録しておきます。

```yaml
name: Star to Bluesky
on:
  schedule:
    # 5分ごとに実行
    - cron: '0/5 * * * *'
  workflow_dispatch:
jobs:
  star-to-bluesky:
    runs-on: ubuntu-latest
    steps:
      # リポジトリのチェックアウト
      - name: Checkout
        uses: actions/checkout@v4
      # 前回の実行時刻を環境変数に設定
      - name: Get Last Execution Time
        id: last-execution
        run: |
          url="https://api.github.com/repos/yug1224/star-to-bluesky/actions/workflows/star-to-bluesky.yml/runs?status=success&per_page=1"
          echo "LAST_EXECUTION_TIME=$(curl -fsSL "$url" | jq -r '.workflow_runs[0].updated_at')" >> $GITHUB_ENV
      # Denoのセットアップ
      - name: Setup Deno
        uses: denoland/setup-deno@v1
        with:
          deno-version: v1.x
      # Denoの実行
      - name: Deno Run
        run: deno run --allow-read --allow-env --allow-net main.ts
        env:
          BLUESKY_IDENTIFIER: ${{secrets.BLUESKY_IDENTIFIER}}
          BLUESKY_PASSWORD: ${{secrets.BLUESKY_PASSWORD}}
          RSS_URL: ${{secrets.RSS_URL}}
```

![GitHub Actionsの実行結果](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106236/e1776848-e246-916d-ab79-a260396a6f14.png)

だいたい5分ごとに定期実行できました！簡単ですね！

## 最後に🎉

今回はGitHubでStarを付けたらBlueskyに投稿するプログラムを作成しました！

下記リポジトリで公開しているので、是非動かしてみてください！🙌

https://github.com/yug1224/github-star-to-bluesky

Blueskyアカウントへのリンクも載せておきます。もし良ければフォローお願いします！🎉

https://bsky.app/profile/yug1224.com

## 参考記事📚

下記記事を参考にさせていただきました！ありがとうございました！

https://zenn.dev/kawarimidoll/articles/42efe3f1e59c13

https://zenn.dev/ryo_kawamata/articles/8d1966f6bb0a82

https://zenn.dev/hashrock/articles/dbd868e390d60f

https://qiita.com/kt3k/items/e22422ea8e481b99703a