souki-paranoiastのブログ

地方都市でプログラマーをやっている人のブログ。技術ネタ以外も少し書く。メインの言語はJava。https://paranoiastudio-japan.jimdo.com/ に所属

【JavaScript】GAS + TwitterAPIで共有アカウントからつぶやく

前置き

知人が小さなコミュニティを運営している。まあ、サークルみたいなものだ。私はたまーに手伝いをしている。一員なんだけど活動は殆ど出来ていないや🤪

その知人が、「コミュニティのTwttierアカウントから各自が更新情報を発信できると運営が楽になるなー」というような発言からタイトルのようなことを思いついた。

 

概要

  • TwitterアカウントのIDやパスワードはメンバーには通知しない

  • 必然的にアプリを何か挟まないといけない

  • コミュニティのページはWix.comで作られたメンバーページがある*1

  • メンバーサイトは、ログイン機能が必要なのである程度はクライアント側に情報を持たせても多分OK

  • GAS(Google Apps Script)でTwitterAPIを叩いて、コミュニティのページからはGASのAPIを叩く

 

詳細

Javascript「のみ」でTwitterAPIを叩いてみる - 動かざることバグの如し
今から10分ではじめる Google Apps Script(GAS) で Web API公開 - Qiita

殆どこちらのブログを参考にさせてもらった🎉

GAS側

補足するとすれば、参考にしたブログとは詳細がちょっと変わっている点だと思う。 参考にしたブログの環境は、多分ブラウザ(JQuery使ってるし)だし、ライブラリはリンク切れになっている。

  • fetch処理はGAS専用のに書き換え
  • ライブラリのファイルは、GASに中身をコピペしている(oauth.gsとsha1.gs)

f:id:souki-paranoiast:20200422021311p:plain
GAS_files

ちなみに、oauth.gsの中身はこちらで、 oauth.js · GitHub
sha1.gsの中身はこちらだ(↑のファイルの中に書いてあるリンク) http://pajhome.org.uk/crypt/md5/sha1.js

/**
 * @param request: {{
 *            parameter: {
 *                text: string, // ツイートする内容
 *                cbf: string // Callback function name
 *            }
 *        }}
 */
function doGet(request) {
  console.log(request);
  const requestBody = request.parameter;
  const text = requestBody.text;
  const callbackFunctionName = requestBody.cbf;

   const options = {
      method: "POST",
      apiURL: "https://api.twitter.com/1.1/statuses/update.json",
      // ★ ここを変更。TwitterAPIのキーを発行するページを見れば、名称が多少違っても雰囲気でわかるはず
      consumerKey: "",
      consumerSecret: "",
      accessToken: "",
      tokenSecret: "",
      body: text
  };

  const tweetResult = postTweets(options);

  const out = ContentService.createTextOutput();
  // セキュリティ的には微妙らしいが、JSONPとして扱う(そのつながりでGETを受け入れるようにする。意味違うんだけどしゃあない)
  out.setMimeType(ContentService.MimeType.JAVASCRIPT);
  out.setContent(callbackFunctionName + "(" + JSON.stringify(tweetResult) + ")");
  return out;
}

function postTweets(options) {
  const accessor = {
    consumerSecret: options.consumerSecret,
    tokenSecret: options.tokenSecret
  };
  const message = {
    method: options.method,
    action: options.apiURL,
    parameters: {
      oauth_consumer_key: options.consumerKey,
      oauth_version: "1.0",
      oauth_signature_method: "HMAC-SHA1",
      oauth_token: options.accessToken,
      status: options.body
    }
  };
  OAuth.setTimestampAndNonce(message);
  OAuth.SignatureMethod.sign(message, accessor);
  const url = OAuth.addToURL(message.action, message.parameters);
  const r = UrlFetchApp.fetch(url, {
    method: "POST",
  });
  let success;
  let content;
  try {
    content = JSON.parse(r.getContentText());
    success = r.getResponseCode() === 200;
  } catch(ex) {
    console.error(ex);
    content = {};
    success = false;
  }

  const twitterResult = success ? {
    success: true,
    msg: "ツイートしました",
    content: {
      postUrl: "https://twitter.com/" + content.user.screen_name + "/status/" + content.id_str
    }
  } : {
    success: false,
    msg: "ツイート中にエラーが発生しました。"
  };
  console.log(twitterResult);
  return twitterResult;
}

HTML側

若干不要な情報も入っていはいるが、テキストエリアに文字を入力し、ボタンを押したらAPIが叩かれる単純なものだ。 window.fetchXMLHttpRequestでもjsonpはできるのかもしれないが、それを調べるのも面倒だったのでjqueryを利用している。それ以外はjqueryも使っていないので、おそらく標準だけでブラウザ側はいけるはず。

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Use twitter api</title>
</head>
<body>
<main>
    <div>
        <textarea id="status"
                  placeholder="ツイートする情報を入力してください"
                  cols="60"
                  rows="4"
        ></textarea>
    </div>
    <div class="btn-container">
        <button class="tweet-btn" onclick="tweets();">ツイートする</button>
    </div>
    <div class="tweet-result">
    </div>

</main>
</textarea>
<div class="as-console"></div>
</body>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script>
    var endpoint = "https://script.google.com/macros/s/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";

    function tweets() {
        var text = document.getElementById("status").value;
        if (text == null || text.length === 0) {
            alert("無言ツイートはできません");
            return false;
        }
        if (text.length > 140) {
            alert("長い: " + text.length);
            return false;
        }
        $.ajax({
            type: 'GET',
            url: endpoint,
            dataType: 'jsonp',
            data: {
                text: text,
                cbf: "cbf"
            }
        });
    }

    /**
     * @param res {{
     *     success: true,
     *     msg: string,
     *     content: {
     *         postUrl: string,
     *     }
     * } | {
     *     success: false,
     *     msg: string,
     * }}
     */
    function cbf(res) {
        if (!res.success) {
            alert(res.msg);
            return;
        }
        document.getElementById("status").disabled = true;
        var currentTweetElement = document.createElement("span");
        var link = document.createElement("a");
        link.href = res.content.postUrl;
        link.text = res.content.postUrl;
        link.target = "_blank";
        link.rel = "noopener noreferrer";
        currentTweetElement.appendChild(document.createTextNode("今つぶやいたツイート"));
        currentTweetElement.appendChild(link);
        document.getElementsByClassName("tweet-result")[0].appendChild(currentTweetElement);
    }
</script>
</html>

 

雑感

jsonpを普段使ったことがないため、ここ回りが一番時間かかった気がする。流れでCORSの勉強も少しすることになったため、まあ良かったかな。 今回はTwitterAPIだったけど、別のAPIを叩くのにもGASは使えそう。 メンバーサイトのようにある程度の機密性がない場合は、ある程度のエラーチェックや何かあった時の通知とかGAS削除とかリスク対応は必要だろうけど。。

GASでconstとかアロー演算が使えるようになったのが地味に嬉しい。