dip Engineer Blog

Engineer Blog
ディップ株式会社のエンジニアによる技術ブログです。
弊社はバイトル・はたらこねっとなど様々なサービスを運営しています。

テックブログに自動投稿システム導入してみた

はじめに

2020年1月にテックブログをリニューアルして以来、メンバーに記事を投稿してもらう度に記事のレビューや記事投稿を手作業で行ってきましたが、せっかくなので記事を自動投稿できるようにしたいと思います。

やること

  • 記事のアップロード(下書き状態)
  • 記事内の画像アップロード(フォトライフ)

をGitHubへのプルリクの作成時に起動するようにしたいと思います。

記事のアップロード(下書き状態)

はてなブログAPIのOAuth認証で記事をアップロードしたいと思います。 まずOAuth認証のアクセストークンを発行するためにConsumer KeyとConsumer Secretを取得します。 Consumer KeyとConsumer Secretを利用してAccess Tokenを取得するコードは以下となります。

require 'oauth'
require 'mechanize'

class HatenaAPI
  def initialize
    @consumer = OAuth::Consumer.new(
      ENV['CONSUMER_KEY'],
      ENV['CONSUMER_SECRET'],
      site: 'https://www.hatena.com',
      request_token_url: '/oauth/initiate?scope=read_public%2Cread_private%2Cwrite_public%2Cwrite_private',
      access_token_url: '/oauth/token',
      oauth_callback: 'oob',
      timeout: 300
    )
    @agent = Mechanize.new
  end

  def oauth_authorize
    request_token =  @consumer.get_request_token
    oauth_verifier = ''
    page = @agent.get(request_token.authorize_url)
    form = page.forms[0]
    form.field_with(name: 'name').value = ENV['USER_NAME']
    form.field_with(name: 'password').value = ENV['PASSWORD']
    @agent.submit(form)
    page = @agent.get(request_token.authorize_url)
    form = page.forms[1]
    page = @agent.submit(form)
    oauth_verifier = page.css('div.verifier').text
    @consumer.options.delete(:oauth_callback)
    request_token.get_access_token(oauth_verifier: oauth_verifier)
  end
end

hatena = HatenaAPI.new
access_token = hatena.oauth_authorize
puts "AccessToken: #{access_token[:oauth_token]}"
puts "AccessTokenSecret: #{access_token[:oauth_token_secret]}"

Consumer KeyとConsumer Secret、ユーザ名とパスワードを環境変数として定義しておくとAccess Tokenを取得することができます。

続いて、Access Tokenを利用して記事の投稿を行います。

class HatenaAPI
  attr_reader :header, :hatena_blog, :photolife
  class << self
    def generate_access_token(site)
      consumer = OAuth::Consumer.new(
        ENV['CONSUMER_KEY'],
        ENV['CONSUMER_SECRET'],
        site: site,
        timeout: 300
      )

      OAuth::AccessToken.new(
        consumer,
        ENV['ACCESS_TOKEN'],
        ENV['ACCESS_TOKEN_SECRET']
      )
    end
  end

  def initialize
    @hatena_blog = HatenaAPI.generate_access_token('http://blog.hatena.ne.jp')
    @photolife = HatenaAPI.generate_access_token('http://f.hatena.ne.jp')

    @header = { 'Accept' => 'application/xml', "Content-Type" => "application/xml" }
  end

  def upload_article(file_path)
    body = File.read(file_path)
    @hatena_blog.request(:post, "https://blog.hatena.ne.jp/#{ENV['USER_NAME']}/#{ENV['BLOG_ID']}/atom/entry", Oga.parse_xml(body).to_xml, @header)
  end
end

hatena = HatenaAPI.new
hatena.upload_article('article.xml')

また、アップロードする記事はxmlである必要があるため、以下のテンプレートに記事内容やタイトルを置き換えてアップロードします。

<?xml version="1.0" encoding="utf-8"?>
<entry xmlns="http://www.w3.org/2005/Atom"
       xmlns:app="http://www.w3.org/2007/app">
  <title>{{title}}</title>
  <author><name>{{author}}</name></author>
  <content type="text/x-markdown">
{{content}}
  </content>
  <updated>{{time}}</updated>
  <app:control>
    <app:draft>yes</app:draft>
  </app:control>
</entry>
  • title: 記事のタイトル
  • author: 執筆者のはてなID
  • content: 記事内容
  • time: 投稿時刻 となっています。 また、 <app:draft>yes</app:draft> とすることで下書きとしてアップロードしてくれます。

記事内の画像アップロード(フォトライフ)

最後に、画像のアップロードですがこちらも記事とほぼ同様の手順でアップロードすることができます。

@photolife.request(:post, '/atom/post', body, @header).body)

また、画像アップロードようのxmlのテンプレートは以下となります。

<entry xmlns="http://purl.org/atom/ns#">
  <title>{{title}}</title>
  <content mode="base64" type="{{mime_type}}">{{content}}</content>
  <dc:subject>{{dirname}}</dc:subject>
</entry>
  • title: 画像のタイトル
  • mime_type: 画像のmimeタイプ
  • content: 画像をBase64エンコードしたもの
  • dirname: フォトライフのアップロードするディレクトリ(Hatena Blog等)

最後に

はてなブログAPIを利用した記事の自動投稿を実装しました。 これらをGitHubのイベントと連携することでより快適なテックブログ投稿ができるようになりました。

参考