🔖

Google App Script で実現する「はてなブックマークに"あとで読む"で保存すると Instapaper に同期する仕組み」

2023/08/11に公開

はじめに

はてなブックマーク (はてブ)、私の好きなソーシャルブックマークサービスです。

話題の記事は Web や スマホアプリ でチェックしますが、本文が長い場合など後で読み返したい場合に「あとで読む」機能を使います。

はてなブックマークの「あとで読む」機能とは?

「あとで読む」機能 - はてなブックマークヘルプ

ブックマークにはタグ付けができるんですが、「あとで読む」もしくは「後で読む」タグを付けてブックマークすると、未読の記事について一覧化できたり、定期的に通知してもらえる機能です。

更に、「あとで読む」を付けるための様々なショートカットが用意されています。ここがポイント。

「Web であれば、インデックスページの右上にボタンがあります。便利。」

Web であれば、インデックスページの右上にボタンがあります。便利。

スマホアプリであれば、長押しで出てくるメニューに出てきます。便利。

スマホのWebブラウザ全般での共有メニュー(シェアシート)にもアクション(アクティビティ)が用意されています。便利。

Instapaper とは?


Instapaper

Webページを保存し、スマホやタブレットなど様々なデバイスで「あとで読む」ことができるサービスです。

ソーシャルブックマークサービス なので はてなブックマーク の競合とも言えますが、特徴の一つが「記事を読みやすく再フォーマットしつつ、デバイスごとにキャッシュされるオフライン機能」です。移動中・出先でも未読記事をスムーズに確認できます。

※ 同じような機能を持つ競合サービスは他にもありますが、最近アプリのUIなどの仕様が変わったため、Instapaper に帰ってきました

記事を同期するために Web API を利用するスクリプトを Google App Script で実装してみる

皆に当てはまるわけではないと思いますが、様々な事情によって(※) はてなブックマーク と Instapaper を同期したくなりました。幸い、どちらのサービスにも Web API が用意されているので、定期実行して同期する仕組みを用意してみます。

※ 「あとで読む」以外のブックマークの利用目的があるなど、はてなブックマークを主要なソーシャルブックマークとしつつ、出先で確認するために Instapaper を利用したい場合など。逆もあるかもしれませんね。更に、ページ追加(保存)のためのインターフェースが環境ごとに異なる、みたいな都合もあったりします。

定期的に実行するスクリプトが欲しくなった場合は、公私を問わず始めに Google App Script (GAS) で実装できないか検討します。


Google Apps Script: Google Workspace を自動化、統合、拡張。

GAS は Googleのサービス (Google Workspace) 同士を連携させたり自動化できるスクリプトプラットフォームですが、サービスとは無関係に任意の処理を実行できたりもします。任意の Web API を叩いたり、自身を Web API として稼働させることもできるなど、様々なことができます。

Google アカウントをお持ちであればどなたでも用意できる上、JavaScript を知っていればインフラの知識も不要です 👍

今回はとりあえず「はてなブックマークに"あとで読む"で保存すると Instapaper に同期する仕組み」(双方向ではない)を実装してみます。

実装手順

はてなブックマークAPI を利用するための Consumer key の取得

今回は自分自身のブックマーク一覧を取得したいので、ユーザ認証を伴います(※)。
はてなの OAuth 対応 API を使ったアプリケーションの準備が必要になります。

※ 公開情報の多くは、認証を伴わないAPI利用が可能な場合もあります

詳細は以下のヘルプページを参照してください。
アプリケーションは無料で複数登録できる上、審査不要で登録直後から利用できます 👍

Consumer key を取得して OAuth 開発をはじめよう | Hatena Developer Center

登録が完了すれば、設定ページ「外部アプリケーション認証」の該当アプリケーション情報にて、「OAuth Consumer Key」「OAuth Consumer Secret」が確認できます。

なお、「承認を求める操作」にて、アプリケーションとして最低限必要な権限を指定することができます。こちらは後の実装でも必要になることがあるので状態を把握しておくことをオススメします。

コードの実装

ここでは説明を分かりやすくするために、必要とする処理を個別かつ簡潔に記述します。最終的な実装ではそれぞれの処理を関連付けるなどの工夫が必要になります。参考程度の一例とさせてください。

1. 「OAuth 1.0a 認証」を実装する

はてなでは OAuth 1.0a 認証 に対応しています。
GAS では OAuth1 認証用ライブラリを公式で用意しているため、そちらを利用してみましょう。

googleworkspace/apps-script-oauth1: An OAuth1 library for Google Apps Script.

README の「Setup」にも記述がありますが、GASでは Script ID を入力することで任意のライブラリを 対象のGASプロジェクト内 で利用できるようになります。

function getHatenaOAuth() {
  const scope = 'read_public%2Cread_private%2Cwrite_public%2Cwrite_private'
  return service = OAuth1.createService('Hatena')
    .setAccessTokenUrl('https://www.hatena.com/oauth/token')
    .setRequestTokenUrl(`https://www.hatena.com/oauth/initiate?scope=${scope}`)
    .setAuthorizationUrl('https://www.hatena.ne.jp/oauth/authorize')
    .setConsumerKey('HATENA_CONSUMER_KEY')
    .setConsumerSecret('HATENA_CONSUMER_SECRET')
    .setCallbackFunction('callbackHetena')
    .setPropertyStore(PropertiesService.getScriptProperties())
}

function getAuthorizeUriHetena() {
  var hatenaService = getHatenaOAuth()
  console.log(hatenaService.authorize())
}

function callbackHetena(request) {
  const hatenaService = getHatenaOAuth()
  if (hatenaService.handleCallback(request)) {
    return HtmlService.createHtmlOutput('Success')
  } else {
    return HtmlService.createHtmlOutput('Denied')
  }
}

getHatenaOAuth() に関して補足します。

  • パラメータ scope にて必要な scope(権限)を指定する必要があります
    • 指定がないとエラーになります。また、管理画面でのアプリケーション設定と異なっていてもエラーになります
  • 今回は .setPropertyStore() で スクリプトプロパティ(PropertiesService.getScriptProperties())にアクセストークンを保存する実装にしています
    • これはスクリプトを実行するユーザが実装者1人だけで、他のユーザが利用するケースを想定する必要がないために割り切ったものです。ご了承ください

2. (操作)アクセストークンの取得・保存処理を行う

GASでは コンソール上で任意の function を実行できます。
まず、getAuthorizeUriHetena() を実行します。

実行ログとして「認証URL」が出力されますので、ブラウザでアクセスしましょう。

アカウントに対して、アプリケーションがリクエストしている権限を許可するための画面が表示されます。(はてなアカウントでログインしておく必要があります)

内容に問題が無いようであれば(明らかに自らが作成したアプリケーションであれば)「許可する」を押しましょう。

認証に成功すればリダイレクトで GAS 側の callback 処理が実行され、「Success」と表示されます。

このタイミングで、スクリプトプロパティにアクセストークンが保存されています。
次回以降はこのトークンを利用して認証処理を行えるようになります。

3. ブックマーク一覧の取得処理を実装する

マイブックマーク全文検索API | Hatena Developer Center

今回は「あとで読む」タグの付いたブックマーク一覧を取得するためにこちらのAPIを利用します。

function searchBookmarks() {
  const url = 'https://b.hatena.ne.jp/my/search/json'
  const query = 'あとで読む'
  const limit = 50
  const hatenaService = getHatenaOAuth()
  const response = hatenaService.fetch(`${url}?q=${query}&limit=${limit}`, {
    method: 'get',
    muteHttpExceptions: true
  })
  const data = JSON.parse(response.getContentText())
  return data.bookmarks
}
  • タグはコメント(bookmarks.comment)にて「[あとで読む]」のような形で記録されていますが、 query で「[あとで読む]」と指定してもコメントが空のエントリまで取得対象になってしまうため(事情は不明)、「あとで読む」としています
  • limit は任意で。検証時は少なめにすると良いと思います

4. ブックマーク一覧から取得した対象URLを Instapaper に登録する処理を実装する

Simple Developer API: Adding Pages to Instapaper

ここで Instapaper の API を利用しますが、今回はブックマークの登録のみですので、「Simple Developer API」を利用してみましょう。

こちらは先ほどの はてなブックマーク API での利用開始時に行った Consumer key の取得(アプリケーション登録)などが不要で、利用するユーザの ID・パスワード で実行できます。

※ 「Instapaper 側のブックマーク一覧を取得したい・削除したい」などその他の処理を行いたい場合は the Full API を利用しましょう。ただ、アプリ登録に申請を伴うため、利用開始までに数日を要します

※ 実は Instapaper には「ユーザごとに振られたユニークなメールアドレスにメールを送信することで登録できる仕組み」があります。さらに「GASにはメールを送信する仕組み」があるので、メール送信でも解決しそうなところですが、「Apps Script サービスには、1日の割り当てと機能の一部に制限があります」。ブクマの追加数によってはエラーが発生する可能性があるのであまりオススメしません。

function addInstapaper(url) {
  console.log('[addInstapaper] url = ' + url)
  const res = UrlFetchApp.fetch('https://www.instapaper.com/api/add', {
    method: 'post',
    payload: {
      username: 'INSTAPAPER_USERNAME',
      password: 'INSTAPAPER_PASSWORD',
      url: url
    }
  })
}

function main() {
  const searched = searchBookmarks()
  // comment に [あとで読む] が含まれているブックマークを取得
  const filtered = searched.filter(bookmark => bookmark.comment.includes('[あとで読む]'))
  // Instapaperに送信
  for (let url of filtered) {
    addInstapaper(url)
    Utilities.sleep(100)
  }
}
  • タグ情報は コメント(bookmark.comment)に「[あとで読む]」ような形で記録されていると書きましたが、searched.filter() で検証しています
  • 連続してAPIを実行する際は Utilities.sleep(100) など適度に間隔を空けると良いと思います
  • マイブックマーク全文検索API (はてなブックマーク)は全文検索なので、更新から反映まで少しラグがあります。検証の場合はご注意ください
  • 複雑になるためここでは省略していますが、この処理をそのまま定期的に実行するとその度に重複したURLの登録が行われるのでご注意ください
    • bookmark.entry.eid が記事IDなので、「ブックマーク済みの記事ID」を記録・参照すると、(簡易的ではありますが)同期処理の進行状況として考慮できるかと思います

5. (操作)同期処理を手動実行する

ここまででおおよその処理は実装できています。

main() を実行すると、「あとで読む タグの付いたブックマークを Instapaper に同期する」処理が実行できるはずです。

トリガーの設定

実装に問題が無いようであれば、 main() を定期実行するようトリガーを設定します。

任意の間隔で実行できますので、問い合わせ先のAPIの負荷などを配慮しつつ設定しましょう。

まとめ

こんな感じでどうでしょうか。各サービスのAPI仕様さえ把握しマッチできれば、GASでインテグレーションの仕組みを実装・定期実行できます。ご存じなかった方は是非活用をご検討ください 💪

そして、APIをご提供いただいている各サービスの皆さんには日頃から感謝申し上げます 🙏

参考資料

Discussion