見出し画像

GASでBlueskyのBotをつくった備忘録

なぜBotをGoogle Apps Scriptでつくるのか?

だからです。

面倒なコンパイルやデプロイをする必要がない。Botとしての動作を簡単につくれる。JavaScriptなので気楽に書ける。他のサービスとも組み合わせやすい。

ゴリゴリのプログラマーでない個人でつくるのなら、気楽なGASが一番やりやすいと思います。ちなみに以前はTwitterやSlackのBotもGASでつくっていました。

コードを書いてみよう

今回は、noteのメンバーシップページを宣伝するBotを作成します。細かい例外処理(想定外の戻り値だった場合など)は書いてないので、そのへんは各自で書き足してください。

ログイン処理

function loadData() {
  const url = 'https://bsky.social/xrpc/com.atproto.server.createSession';

  const data = {
    'identifier': 'ユーザーID',
    'password': 'パスワード'
  };

  const options = {
    'method': 'post',
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
    },
    'payload': JSON.stringify(data),
  };

  const response = UrlFetchApp.fetch(url, options);
  return JSON.parse(response.getContentText());
}

ユーザーIDとパスワードは適宜自分のものに置き換えてください。パスワードはアプリパスワードを使用しましょう。

この関数を叩くと、accessJwtやdidを含んだオブジェクトが返ってきます。

URLのタイトルやら説明やら画像やらを引っ張ってくる

function getUrlInfo(url) {
  const content = UrlFetchApp.fetch(url).getContentText();
  const $ = Cheerio.load(content);

  const imageUrl = $('meta[property="og:image"]').attr('content');
  const title = $('title').text();
  const description = $('meta[name="description"]').attr('content');

  return {
    'title': title,
    'description': description,
    'imageUrl': imageUrl
  }
}

データを拾ってくるのにはCheerioライブラリを使用しました。スクリプトIDは以下ですので、ライブラリに追加しておいてください。

1ReeQ6WO8kKNxoaA_O0XEQ589cIrRvEBA9qcWpNqdOP17i47u6N9M5Xh0

この関数を叩くと、title、description, imageUrlの値をもつオブジェクトが返ってきます。これらを使ってリンクカードを設定します。

imageUrlをBlueskyにアップロードする

function uploadImage(imageUlr) {
  const loadedData = loadData();
  const accessJwt = loadedData.accessJwt;

  const blob = UrlFetchApp.fetch(imageUlr).getBlob();

  const options = {
    'method': 'post',
    'headers': {
      'Authorization': `Bearer ${accessJwt}`
    },
    'payload': blob,
  };

  const url = 'https://bsky.social/xrpc/com.atproto.repo.uploadBlob';

  const response = UrlFetchApp.fetch(url, options);
  return JSON.parse(response.getContentText());
}

画像のURLからBLOBを作成し、それをBlueskyにぶん投げます。戻り値のオブジェクトのcidとmimeTypeを後で使います。

画像のサイズが大きすぎると怒られるのですが、GAS上で画像のサイズをいじる良い方法が見つからなかったので一旦放置しています。解決法が見つかったら追記します。

サムネイルを作成する

function createThumb(imageUrl) {
  const imageData = uploadImage(imageUrl);

  const cid = imageData.blob.ref.$link;
  const mimeType = imageData.blob.mimeType;

  const thumb = {
    'cid': cid,
    'mimeType': mimeType
  };

  return thumb;
}

画像のURLを受け取り、それを先程の関数でBlueskyにぶん投げ、戻り値をサムネイルの作成に必要なオブジェクトの形にして返します。

【2024/02/23追記】APIの仕様が変更になったので、こちらの関数は以下のように書き換えをしてください。

function createThumb(imageUrl) {
  const imageData = uploadImage(imageUrl);
  return imageData.blob;
}

中身を見るとわかりますが、もはや何もしていない関数になってしまいました(笑)。書き直せる人は適宜書き直してください。

postに必要なレコードを作成する

function createRecord(text, url, title, description, thumb) {
  const record = {
    'text': text,
    'createdAt': (new Date()).toISOString(),
    'embed': {
      '$type': 'app.bsky.embed.external',
      'external': {
        'uri': url,
        'title': title,
        'description': description,
        'thumb': thumb
      }
    }
  }

  return record;
}

ここまで集めたデータを引数にもち、それらをpostに必要なオブジェクトの形にして返します。

postする

function post(record) {
  const loadedData = loadData();
  const accessJwt = loadedData.accessJwt;
  const did = loadedData.did;

  const url = 'https://bsky.social/xrpc/com.atproto.repo.createRecord';

  const data = {
    'repo': did,
    'collection': 'app.bsky.feed.post',
    'record': record
  };

  const options = {
    'method': 'post',
    'headers': {
      'Authorization': `Bearer ${accessJwt}`,
      'Content-Type': 'application/json; charset=UTF-8'
    },
    'payload': JSON.stringify(data),
  };

  const response = UrlFetchApp.fetch(url, options);
  return responseJSON = JSON.parse(response.getContentText());
}

つくったレコードを引数にもち、それをBlueskyにpostする関数です。

URLと宣伝文言をポストする

function postUrl(url, text) {
  const urlInfo = getUrlInfo(url);
  const thumb = createThumb(urlInfo.imageUrl);
  const record = createRecord(text, url, urlInfo.title, urlInfo.description, thumb)

  post(record);
}

ここまでの処理をまとめて実行する関数です。投稿したいページとそれに対する文言を引数にもちます。

まとめる

function prNote() {
  const text = '【定期】noteでパウパーに関するメンバーシップを開設しています。普段のパブリックな内容とは違う、パーソナルな記事を公開しています。メンバーシップ限定の掲示板では、記事の没ネタなども書いていたりします。サポートしていただけると嬉しいです。';
  const url = 'https://note.com/keiga/membership';

  postUrl(url, text);
}

実際に実行する関数です。投稿したいページとそれに対する文言をもち、それらを引数として実際のポストを動かします。

できました。

おわりに

BlueskyがTwitterの代替品になるのかどうかはさておき、とりあえずBotの中身をつくることはできました。後はGASのトリガーで定期実行すればBotとして動くはずです。

最初にも書いたとおり、GASを使うと、例えば少し長い固定文言をスプレッドシート側に書き、それをスクリプト側で引っ張ってくることにより、プログラマーでない人に対する保守性が上がります。Botを動かす際に数字をインクリメントする作業も、スプレッドシートに値を保存すれば簡単にできます。IFTTTなどのサービスを使えば、もっと応用的なBotもつくれると思います。今回の記事が、その一助になれば幸いです。

なお、今回の記事は、にがうりさんの以下の記事が大元となっています。参考にさせていただいたことに、お礼申し上げます。

以上です。ここまで読んでいただきありがとうございました。質問等ありましたらコメントかTwitterでのリプライでお願いします。

この記事が気に入ったらサポートをしてみませんか?