50
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

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

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

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

青空と白い雲

Blueskyとは?🤔

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

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

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

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

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

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

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に投稿するプログラムを作成しました!

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

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

参考記事📚

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

50
22
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
50
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?