🌈

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

2023/07/06に公開

はじめに

こんにちは。夏休みは新潟からフェリーで北海道に行く予定を立てている@yug1224です。

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

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

青空と白い雲

Blueskyとは?🤔

まずそもそもBlueskyとは何か?

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

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

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

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

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

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

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

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にテキストを投稿する様子

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

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

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

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

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

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の情報を取得し、リンクカードとともに投稿する

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

GitHub Actionsで定期実行する⏰

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

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

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の実行結果

だいたい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

株式会社HRBrain

Discussion