メインコンテンツへスキップ

RubyからBlueskyへの投稿

作成しているツールでRubyからBlueskyへ投稿したかったので、ShreyanJain9/bskyrb: Ruby Gem for interacting with BlueSky/AT Protocolというライブラリを使うことにした。

使用例を見ると、

require 'bskyrb'
username = 'your_username'
password = 'your_password'
pds_url = 'https://bsky.social'

credentials = Bskyrb::Credentials.new(username, password)
session = Bskyrb::Session.new(credentials, pds_url)
bsky = Bskyrb::RecordManager.new(session)
post_uri = bsky.create_post("Hello world from bskyrb!")["uri"]
bsky.like(post_uri)
bsky.repost(post_uri)
bsky.create_reply(post_uri, "Replying to post from bskyrb")

とあり、一見簡単そうに見えるがBlueskyの場合はXやMastodonのAPIとは異なり、投稿するメッセージの中に、

  • URLがあってもリンクは設定されない
  • ハッシュタグがあってもリンクは設定されない
  • URLのコンテンツにOGBタグがあってもリンクカードは表示されない

という仕様であり、なかなか面倒であることがわかった。

そんなわけで上記のドキュメントを参照しつつ、それぞれどうやれば良いのか調べてみた。

リンクの設定
#

メッセージ中にURLがあれば、その部分を自動的にリンク設定したかったため、Rich text facetsの例にある以下の例を参考に、

{
  text: 'Go to this site',
  facets: [
    {
      index: {
        byteStart: 6,
        byteEnd: 15
      },
      features: [{
        $type: 'app.bsky.richtext.facet#link',
        uri: 'https://example.com'
      }]
    }
  ]
}

以下のようなコードでfacetを作成した。

  def create_facets_for_urls(text)
    facets = []
    text_copy = text.dup

    URI.extract(text_copy, ['http', 'https']).each do |url|
      byte_start = text_copy[0, text_copy.index(url)].encode('UTF-8').bytesize
      byte_end = byte_start + url.bytesize

      facets << {
        'index' => {'byteStart' => byte_start, 'byteEnd' => byte_end},
        'features' => [
          {
            'uri' => url,
            '$type' => 'app.bsky.richtext.facet#link',
          },
        ],
      }

      text_copy[text_copy.index(url), url.length] = "\0" * url.length
    end

    facets
  end

ハッシュタグの設定
#

メッセージ中にハッシュタグがあれば、その部分を自動的にリンク設定したかったため、同じようなロジックでfacetを作成する。

  # Creates facets for hashtags in the text
  def create_facets_for_hashtags(text)
    facets = []

    text.scan(/#[\w\p{Han}ー]+/) do |hashtag|
      byte_start = text[0, text.index(hashtag)].encode('UTF-8').bytesize
      byte_end = byte_start + hashtag.bytesize

      facets << {
        'index' => {'byteStart' => byte_start, 'byteEnd' => byte_end},
        'features' => [
          {
            'tag' => hashtag.gsub('#', ''),
            '$type' => 'app.bsky.richtext.facet#tag',
          },
        ],
      }
    end

    facets
  end

リンクカードの表示
#

メッセージ中のURLにあるコンテンツにOGPタグがあれば、リンクカードを表示するため以下の流れで対応した。

  1. URLにあるコンテンツにOGPタグがあるかを見に行く。無ければ以降の処理はスキップ
  2. og:title、og:descriptionなど処理に必要なタグ情報を取得する
  3. og:imageのサムネイル画像をダウンロードしBlueskyへアップロードする
  4. 3の戻り値を使いembedを作成する

3〜4のコードはこんなイメージ

  # Handles the creation of embeds by uploading images and generating embed data
  def create_embeds(ogp_data)
    embeds = []
    if ogp_data['og:image']
      begin
        Tempfile.create(['thumbnail', File.extname(URI.parse(ogp_data['og:image']).path)]) do |file|
          file.binmode
          file.write URI.open(ogp_data['og:image']).read
          file.rewind

          content_type = determine_content_type(file.path)
          response = upload_image_to_bluesky(file, content_type)
          
          if response && response.parsed_response
            thumb_blob = response.parsed_response['blob']
            embed = create_embed_data(ogp_data, thumb_blob)
            embeds << embed
          else
            warn "Failed to upload image to Bluesky: Invalid response #{response.inspect}"
          end
        end
      rescue StandardError => e
        warn "Failed to process OGP image: #{e.message}"
      end
    end
    embeds
  end

  # Creates the embed data structure for Bluesky post
  def create_embed_data(ogp_data, thumb_blob)
    {
      '$type' => 'app.bsky.embed.external',
      'external' => {
        'uri' => ogp_data['og:url'],
        'title' => ogp_data['og:title'],
        'description' => ogp_data['og:description'],
        'thumb' => thumb_blob
      }
    }
  end

関連記事

Platypus - RubyスクリプトをMac OSのアプリに変換できるツール

sveinbjornt/Platypus: Create native Mac applications from command line scripts. スクリプト(Shellスクリプト、Perl、Python、Rubyなど)を標準のMac OS Xアプリケーション(.app)に変換するツール。 Platypusを使用すると、コマンドラインやターミナルを介さずに、スクリプトを直接実行可能なアプリケーションとして実行できる。 生成されるappのinfo.plistを編集することで、スクリプトをURLスキームに対応させることができる。 RubyではURLスキームで指定された文字列へARGVでアクセスできた。

OAuthでアクセストークンを取得するスクリプト

OAuthでアクセストークン、アクセストークンシークレットを取得するサンプル。毎回、同じようなスクリプトを書いて使い捨てにするのはムダなのでメモしておく。 ブラウザで表示されるURLへアクセスする リダイレクトされたURLのoauth_verifierの値を入力 access_token.token、access_token.secretが表示される require 'oauth' URL = "https://www.tumblr.com" consumer_key = 'Cls**********yaqV4' consumer_secret = 'RG****3KwFy' oauth = OAuth::Consumer.new(consumer_key, consumer_secret, site: URL) request = oauth.get_request_token(exclude_callback: true) puts "Access this URL: #{request.authorize_url}" print "oauth_verifier: " verifier = gets.chomp access_token = request.get_access_token(oauth_verifier: verifier) pp access_token

Webページから本文らしき部分を抽出する

さまざまなパターンの日本語文章をデータとして欲しいケースがあったので、指定したURLから本文らしき内容を抽出するスクリプトを書いた。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 require 'playwright' require 'readability' require 'html2text' # 指定したURLから本文らしき内容を抽出して返却する def html2text(url) Playwright.create(playwright_cli_executable_path: 'npx playwright') do |playwright| playwright.chromium.launch(headless: true) do |browser| begin page = browser.new_page page.goto(url, waitUntil: 'load') doc = Readability::Document.new(page.content) sleep 1 return {:title => page.title, :content => Html2Text.convert(doc.content) } rescue return {:title => nil, :content => nil } end end end end url = ARGV.shift doc = html2text(url) puts "#{doc[:title]}\n#{doc[:content]}" Javascriptでコンテンツを生成するページに対応するためPlaywrightを使用。 使用したライブラリ # cantino/ruby-readability: Port of arc90’s readability project to Ruby YusukeIwaki/playwright-ruby-client: Playwright client for Ruby soundasleep/html2text_ruby: A Ruby component to convert HTML into a plain text format.

2ホップリンク

ホップ数 # flowchart LR B1 & B2 --> A G --> O1 H --> O1 A --> O1 & O2 O2 --> E F --> O2 B1:::hop1 B2:::hop1 O1:::hop1 O2:::hop1 F:::hop2 G:::hop2 H:::hop2 style A fill:#f9f,stroke:#333,stroke-width:4px classDef hop1 fill:#a9a classDef hop2 fill:#f96 ホップ数とは自身から任意のページまでたどり着くまでのリンクの数である。 このようなリンク関係がある場合、ページAから見て、B1 B2 O1 O2は1ホップリンクと呼ばれる。 2ホップリンクの意味 # このため、上記のリンク関係で言えば、ページAから見たG H E Fは2ホップリンクとなる。 Scrapboxでは2ホップリンクの内、アウトゴーイングリンクのページへリンクしているページ、つまり、 AからリンクしているO1へリンクしている Gと H AからリンクしているO2へリンクしている F を関連リンクとして表示している。これは、 A→C, B→Cというリンクが存在するとき、AとBの間にはなんらかの関連があると考えてよい。 「和歌山」→「みかん」、「愛媛県」→「みかん」 ならば「和歌山県」と「愛媛県」はみかんつながりになっているわけだし、 「増井」→「Rubyプログラミング」、「高林」→「Rubyプログラミング」 ならば「増井」⇔「高林」は意味がある。 出典: 2ホップリンクの考察 - 増井俊之 上記の考え方に基づく仕様であり、本サイトでもQuartzを拡張し同様の2ホップリンクを表示している(各ページ配下の「関連リンク」) QuartzにScrapbox的な2ホップリンクを追加する Obsidiaのリンク表示 参考情報 # 2ホップリンクの考察 - 増井俊之

指定したWebページをMarkdownへ変換するRubyスクリプト

指定したWebページをMarkdownへ変換するRubyスクリプト require 'reverse_markdown' require 'open-uri' ReverseMarkdown.config do |config| config.unknown_tags = :bypass config.github_flavored = true config.tag_border = '' end def usage puts "usage: #{$0} uri" exit 1 end usage if ARGV.size != 1 html = OpenURI.open_uri(ARGV.shift).read print ReverseMarkdown.convert html

画像ファイルをまとめてリサイズする

RubyからImageMagickを使うためにRMagickで画像を一括してリサイズするスクリプト。Obsidianへ移行したメモに添付されていた画像サイズを圧縮するためにやっつけで作成 require 'RMagick' include Magick MAX_PIXEL = 800 MAX_FILE_SIZE = 300000 ARGV.each do |f| next unless %w(.jpeg .jpg .png .heic .webp).index File.extname(f).downcase size = File.size(f) next if size < MAX_FILE_SIZE img = Magick::Image.read(f).first next if img.columns <= MAX_PIXEL && img.rows <= MAX_PIXEL img.resize_to_fit(MAX_PIXEL, MAX_PIXEL).write(f) puts "#{f}: #{size} -> #{File.size(f)}" end