夜田たんの知的生活実践

ほそく・ながく・ゆるく続くブログを目指します(だいたい週一で更新中)

mastodonの書き込みをGmail経由でEvernote投稿

group of Elephants crossing across the street during daytime photo
Photo by JAYAKODY ANTHANAS on Unsplash

これまでマストドンの投稿はIFTTT経由で直接Evernoteへ飛ばしていましたが、画像をくっつけるためにGAS(GoogleAppsScript)を叩くようにしました。

作った経緯

これまではマストドンのRSSが更新されたらIFTTTで検知して新しい投稿をEvernoteへ送信するだけだったのですが、そうすると画像は送られません。
あまり気にしていなかったのでそのままにしていたものの、反応が良い投稿は画像が張ってあることが多いため、後から見返すときに画像があった方がいいなと思い直したので。

RSSの構造

RSSフィードの構造は、JPサーバだとざっくり以下のような感じです。
具体例は合同会社分散型ソーシャルネットワーク機構(mstdn.jp運営)のRSSなどを参考にしてもらえればと。

<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:webfeeds="http://webfeeds.org/rss/1.0">
  <channel>
    <title>ユーザ名 (@ユーザID@mstdn.jp)</title>
    (略)
    <item>
      <title>New status by ユーザID</title>
      <guid isPermalink="true">パーマリンク</guid>
      <link>パーマリンク</link>
      <pubDate>投稿日時</pubDate>
      <description>投稿内容</description>
      <enclosure url="添付画像1" />
      <enclosure url="添付画像2" />
    </item>
    <item>
        次の投稿
    </item>
    (略)
  </channel>
</rss>

コード

全体の流れ

  1. IFTTTがRSSの更新を検知
  2. GAS起動
  3. 最後にEvernoteへ送信した投稿(トゥート)のPermalinkをスプレッドシートから取得
  4. RSSから先頭n個の投稿を取得(今回は10個)
  5. スプレッドシートから取得したPermalinkが出てくるまでの投稿だけ残す
  6. 残した投稿をGmail経由でEvernoteへ送信
  7. 送信した投稿の内、一番新しい投稿のPermalinkをスプレッドシートへ上書き

書いたコード

function doGet(e) {
  const FEED = 'https://my-instance/@my-mastodon-id.rss';
  var lastRun = getLastRun();
  var toots = getItems(FEED, 10, lastRun);
  var link = lastRun;
  for (var i = toots.length; i > 0; i--){
    //console.log(i);
    var toot = getToot(toots[i - 1]);
    link = sendToEvernote(toot);
    Utilities.sleep(5000);
  }
  Logger.log('LastRun: ' + link);
}

function getLastRun() {
  var sheet = SpreadsheetApp.getActiveSheet();
  var link = sheet.getRange(1, 1).getValue();
  return link;
}

function setLastRun(link){
  var sheet = SpreadsheetApp.getActiveSheet();
  sheet.getRange(1, 1).setValue(link);
}

// cf: https://developers.google.com/apps-script/reference/xml-service
function getItems(feed, num, last) {
  var toots = [];
  var xml = UrlFetchApp.fetch(feed).getContentText();
  var document = XmlService.parse(xml);
  var root = document.getRootElement();
  var ch = root.getChild('channel');
  // var username = ch.getChild('title').getText();
  var items = ch.getChildren('item');
  for (var i = 0; i < num; i++) {
    var link = items[i].getChild('link').getText();
    if (link != last) {
      toots.push(items[i]);
    } else {
      break;
    }
  }
  return toots;
}

function getToot(item) {
  var title = item.getChild('title').getText();
  var link = item.getChild('link').getText();
  var description = item.getChild('description').getText();
  description = description.replace(/<("[^"]*"|'[^']*'|[^'">])*>/g, '');
  var description16 = ''
  for (var i = 0; i < description.length; i++){
    description16 += '&#x' + description.codePointAt(i).toString(16);
  }

  // format date
  // cf. Fri, 18 Dec 2020 01:23:45 +0000
  // Date.getMonth returns 0 to 11
  var date = item.getChild('pubDate').getText();
  var dateSerial = new Date(date);
  var dateStr = dateSerial.getFullYear().toString() + '/'
                + (dateSerial.getMonth() + 1).toString() + '/'
                + dateSerial.getDate().toString() + ' '
                + dateSerial.getHours().toString() + ':'
                + dateSerial.getMinutes().toString();

  // get image
  var images = item.getChildren('enclosure');
  var imageBlobs = {};
  if (images.length > 0) {
    for (i = 0; i < images.length; i++){
      var url = images[i].getAttribute('url').getValue();
      imageBlobs[i] = UrlFetchApp.fetch(url).getBlob();
    }
  }

  var toot = {title: title,
              toot: description16,
              link: link,
              date: dateStr,
              images: imageBlobs};
  return toot;
}

function sendToEvernote(post){
  const MAIL_ADDRESS = 'my-evernote-address@m.evernote.com';
  const TITLE = 'DailyLog @lifelog +';
  const SENDER_NAME = 'Matodon Toot';
  const BODY = '';
  var html_body = '';

  html_body += '<p>--------------------</p>';
  html_body += '<p>&#x1F418 ' + post.title + '</p>';
  html_body += post.toot;

  if (Object.keys(post.images).length > 0) {
    html_body += '<p>';
    for (var i = 0; i < Object.keys(post.images).length; i++) {
      html_body += '<img src="cid:' + i + '" width="200">';
    }
    html_body += '</p>';
  }

  html_body += '<p>&#x231A ' + post.date + ' <a href="' + post.link + '">Permalink</a></p>';

  MailApp.sendEmail(MAIL_ADDRESS, TITLE, BODY,
                    {htmlBody: html_body,
                     inlineImages: post.images,
                     name: SENDER_NAME});
  Logger.log('Send Email: ' + post.link);
  return post.link;
}

コード補足

Evernoteへ送るトゥートの特定

最初は先頭1投稿のみを読み込んでEvernoteへ送るように作っていました。
しかし、その場合、Mastodonに連続で投稿した場合に、IFTTTのタイムラグのおかげで以下のように脱落する投稿が出てきてしまいます。

  1. 投稿その1
  2. IFTTTが動いてEvernoteへ送信
  3. 投稿その2
  4. 直後に投稿その3
  5. IFTTTが動いて投稿その3のみEvernoteへ送信され、投稿その2は送信されない
  6. 同上

そのため、スプレッドシートに前回の投稿を記録させておく方法をとりました。

メール送信は以前作った位置情報を1日の最後にまとめて送るのと一緒です。
IFTTTからGASを叩く方法も同じ記事で書いているので、参考になれば幸いです。

Unicode絵文字対応

絵文字入りのメールをGmailからEvernoteへ送るとEvernote側でなぜかスポイルされてしまいます。
(単純にIFTTTでRSSの内容を直接送るときには問題なし)
そのため、すべての文字を16進数化して送るようにしました。

このときにHTMLタグが含まれると、

<p>あいうえお</p>

のようにEvernoteでタグが丸見えになってしまうので、事前に削除しています。
(もっといいやり方があるかもしれませんが、急ごしらえなのでご容赦ください)

参考資料

大変お世話になりました!