#前置き
TwitterのツイートをMastodonに転載してトゥートするbotをGoogle Apps Script(以下GAS)で動かすという内容です。
GASやJavaScriptには不慣れなので拙いコードにはなりますが備忘録として残しておきます。
そして何よりQiitaへの投稿は初めてなので、見づらい点多く見受けられると思いますがご容赦ください。
GASを一切使ったことがない前提で進めていきます。
また、このサンプルコードは https://imastodon.net/@imascg_stage_bot で実際に使用しているものです。
下準備
Googleドライブを開き、「新規」→「その他」→「アプリの追加」と進み、「Google Apps Script」を追加したら、新規作成。
プロジェクトに名前をつけた後、「ファイル」から「プロジェクトのプロパティ」を開いておく。
Twitter側
https://apps.twitter.com よりTwitterのAppsを作成。その際Callback URLには
https://script.google.com/macros/d/[スクリプトID]/usercallback
と入力する。スクリプトIDは「プロジェクトのプロパティ」に記載されているものを入力。
KeyとSecretを表示させたらGASに戻り、スクリプトプロパティに「Key, Secret, Mastodonのインスタンス」をそれぞれ入力する。
Mastodonのインスタンスの例:https://mstdn.jp
さらに、「リソース」→「ライブラリ」→「ライブラリを追加」に
MFE2ytR_vQqYfZ9VodecRE0qO0XQ_ydfb
を入力して、バージョン1を選択して完了。
その後以下のコードをそのまま入力し、「関数を選択」から"authorize"を選び実行。
その後「表示」→「ログ」に出てくるURLに進みTwitter認証をする。(この時ログインしているアカウントでアクセスすることになる。)
var twitter = TwitterWebService.getInstance(ckey,csecret);
function authorize() {
twitter.authorize();
}
function authCallback(request) {
return twitter.authCallback(request);
}
最後に下のコードを追加してTwitter側は終了。使用例は後ほど。
var scrprop = PropertiesService.getScriptProperties();
var userprop = PropertiesService.getUserProperties();
var ckey = scrprop.getProperty("consumer_key");
var csecret = scrprop.getProperty("consumer_secret");
var twitter = TwitterWebService.getInstance(ckey,csecret);
var request = "https://api.twitter.com/1.1/statuses/user_timeline.json?tweet_mode=extended&screen_name=" // extendedは必須
function getUserTimeline(targetname) { // targetnameに対象のスクリーンネームを入れる
var res = twitter.getService().fetch(request+targetname+"&since_id="+
userprop.getProperty(targetname)); //user_timelineへリクエスト
var json = JSON.parse(res.getContentText());
if (json.length>0) {
userprop.setProperty(targetname, json[0].id_str); // 次の参照用
var statuses = [];
json.forEach(function(status,i){
var e = status.entities;
if (e.user_mentions.length==0) {
var text = status.full_text;
var medias = [];
if (e.urls != undefined && e.urls.length>0) { // URLを置き換える
for (var j=0;j<e.urls.length;j++) {
text = text.replace(/https:\/\/t.co\/[a-zA-Z0-9]+/,e.urls[j].expanded_url);
}
}
if (e.media != undefined && e.media.length>0) { // 画像DL
for (var j=0;j<e.media.length;j++) {
text = text.replace(/https:\/\/t.co\/[a-zA-Z0-9]+/,"");
medias[j] = UrlFetchApp.fetch(e.media[j].media_url_https).getBlob();
}
}
text = text+"#official_bot\nhttps://twitter.com/"+targetname+"/status/"+status.id_str+"/"; // ハッシュタグやURLを付ける
statuses[i] = {
"text" : text,
"media" : medias
};
}
});
return statuses.reverse();
}
}
function initialize(target) { // プロパティのセット
if (userprop.getProperty(target)==null) {
var res = twitter.getService().fetch(request+target);
userprop.setProperty(target, JSON.parse(res.getContentText())[0].id_str);
}
}
Mastodon側
初めにAppsの取得、Tokenの取得をしなければならない。
これといったライブラリもなさそうなので、直接APIを叩いていく。
クライアント名を入力した後、authorize1 を指定して実行、ログに出たURLにアクセスし承認を押した後、出てきたやつを authorization_code の中に入れてから authorize2 を指定して実行することで、ログに Bearer ではじまるトークンが出てくるので控えておく。
var scrprop = PropertiesService.getScriptProperties();
var instance = scrprop.getProperty("instance");
function authorize1() {
if (scrprop.getProperty("client_id")==null) { // 1度もAppsを作成したことがない場合に作成する
var client_name = ""; // ここにクライアント名入力してから実行すること
var payload = {
"client_name" : client_name,
"redirect_uris" : "urn:ietf:wg:oauth:2.0:oob",
"scopes" : "write"
};
var params = {
"method" : "post",
"contentType" : "application/json",
"payload" : JSON.stringify(payload)
};
var res = UrlFetchApp.fetch(instance+"/api/v1/apps", params);
var json = JSON.parse(res.getContentText());
scrprop.setProperties({"client_id":json.client_id,"client_secret":json.client_secret});
}
Logger.log(instance+
"/oauth/authorize?response_type=code&redirect_uri=urn:ietf:wg:oauth:2.0:oob&scope=write&client_id="+
scrprop.getProperty("client_id"));
}
function authorize2() {
var authorization_code = ""; // ↑で入手したコードを入力してから実行すること
var payload = {
"grant_type" : "authorization_code",
"redirect_uri" : "urn:ietf:wg:oauth:2.0:oob",
"code" : authorization_code,
"client_id" : scrprop.getProperty("client_id"),
"client_secret" : scrprop.getProperty("client_secret")
};
var params = {
"method" : "post",
"contentType" : "application/json",
"payload" : JSON.stringify(payload)
};
var res = UrlFetchApp.fetch(instance+"/oauth/token", params);
Logger.log("Bearer "+JSON.parse(res.getContentText()).access_token); // トークンの前に必ずBearerを付けないと失敗する
}
最後に下のコードを追加して準備が完了。
var scrprop = PropertiesService.getScriptProperties();
var instance = scrprop.getProperty("instance");
function postStatuses(statuses,authorize) {
return postStatuses(statuses,authorize,"unlisted");
}
function postStatuses(statuses,authorize,visibility) {
if (statuses!=null&&authorize!=null) {
statuses.forEach(function(status){
var payload = {
"status" : status.text,
"media_ids" : postMedia(status.media,authorize),
"visibility" : visibility
};
var params = {
"method" : "post",
"headers" : {"authorization":authorize},
"contentType" : "application/json", // これをつけないと500を返される
"payload" : JSON.stringify(payload) // JSONに直さないと画像投稿に失敗する
};
UrlFetchApp.fetch(instance+"/api/v1/statuses",params);
});
}
}
function postMedia(medias,authorize) {
var media_ids = [];
if (medias.length>0) {
medias.forEach(function(media,i){
var params = {
"method" : "post",
"headers" : {"authorization":authorize},
"payload" : {"file":media}
};
var res = UrlFetchApp.fetch(instance+"/api/v1/media",params);
media_ids[i] = JSON.parse(res.getContentText()).id; // 画像URLは必要ない
});
}
return media_ids;
}
トリガーの準備
以下のようなコードを追加した後、「編集」→「現在のプロジェクトのトリガー」と進み、
実行したい関数(ここではimascg_stage) 、どのくらいの時間で確認/投稿させたいかを入力して保存。(5分以上でないと失敗する可能性がある。)
function imascg_stage() {
var target = "imascg_stage"; // 転載したいユーザーのID ここではデレステ公式
var authorize = ""; // 上で控えた Bearer で始まるトークンを入力
initialize(target);
var statuses = getUserTimeline(target);
postStatuses(statuses,authorize,"unlisted"); // 省略した場合はunlisted(未収載)、他にも"public","private","direct"に指定できる。
// サーバーごとにbot運用のルールが異なるので必ず確認すること。
}
以上
#参考文献
MastodonのAPI全集 https://github.com/tootsuite/documentation/blob/master/Using-the-API/API.md#apps
MastodonのAccess Token取得について https://qiita.com/m13o/items/7798f09f16523d5693d5
#あとがき
- 画像は?
画像の投稿は最後まで成功しませんでした。助言お待ちしております。- 成功しました。GASのURLFetchApp.fetch()のparamsはデフォルトで "application/x-www-form-urlencoded" で送信されるらしいです。知らないでドツボにはまってました。
- 汚ねぇコードだな
- すいません許してください、Advent Calendarの次の方がなんでもしますから。
- なんで作ろうと思ったの?
- 1つは単にMastodon見てるのにTwitterに見に行くのが不便だったからです。TwitterがActivityPubに対応してくれたら良いんですけどね。
- もう1つは、Mastodonのあり方を自分なりに模索してみたかったからです。これについてはnullkalさんの記事に触発されました。 http://blog.nil.nu/entry/2017/12/01/211959
- 最後に一言
- botも記事も2日間の突貫工事でごめんなさい。多分しばらく経った頃に修正すると思います。
こちらは アイマストドン内非公式「ジョンベベベント・カレンダー」 Advent Calendar 2017 12月5日分の記事になります。
prev: わかりやすいプレゼン・ダイマ資料を作るために試行錯誤した話 author: ぽよすけ(@sand)氏
next: 姉の日、音の日だって author: 周平P(@syuheiP)氏