GASでTwitter APIを使ってLINEからツイートを投稿する方法
つぶやきたいだけなのに、ツイッターってタイムラインが面白くてついつい長時間見てしまうんですよね…そんなとき、Twitterを開かずにツイートができたらいいなと思ってLINEからツイートできるアプリを作ってみました。
↓実際のツイートはこちら
GASの初期設定
GASでTwitter APIを使えるようにする
GASでTwitter APIを使えるようにする方法は、以下の記事にまとめてありますので参考にしていただければと思います。
LINE Messaging APIの設定
LINE Messaging APIの設定を行います。LINE Messaging APIをGASで使う方法については以下の記事を参考にしてください。
GASのソースコード
今回作成したソースコードを公開します。
const LINE_API_ACCESS_TOKEN = PropertiesService.getScriptProperties().getProperty('LINE_API_ACCESS_TOKEN');
const LINE_REPLY_URL = 'https://api.line.me/v2/bot/message/reply';
const TWITTER_API_KEY = PropertiesService.getScriptProperties().getProperty('TWITTER_API_KEY');
const TWITTER_API_SECRET = PropertiesService.getScriptProperties().getProperty('TWITTER_API_SECRET');
const TWITTER_TOKEN = PropertiesService.getScriptProperties().getProperty('TWITTER_TOKEN');
const TWITTER_TOKEN_SECRET = PropertiesService.getScriptProperties().getProperty('TWITTER_TOKEN_SECRET');
const TWITTER_BEARER_TOKEN = PropertiesService.getScriptProperties().getProperty('TWITTER_BEARER_TOKEN');
const TWITTER_POST_URL = 'https://api.twitter.com/2/tweets';
const TWITTER_ACCOUNT_NAME = 'nmukineer';
const LINE_USER_ID = PropertiesService.getScriptProperties().getProperty('LINE_USER_ID');
const LINE_NOTIFY_API = 'https://notify-api.line.me/api/notify';
const LINE_NOTIFY_TOKEN = PropertiesService.getScriptProperties().getProperty('LINE_NOTIFY_TOKEN');
const twitter = TwitterWebService.getInstance(TWITTER_API_KEY,TWITTER_API_SECRET);
// LINE送信したときに実行される関数
function doPost(e) {
const eventData = JSON.parse(e.postData.contents).events[0];
console.log(lineUserId(eventData));
const replyToken = eventData.replyToken;
const userMessage = eventData.message.text;
const messageType = eventData.message.type;
console.log(messageType);
console.log(userMessage);
let replyMessage = ""
// 特定のLINEユーザー以外からは利用できないようにする
if(lineUserId(eventData) !== LINE_USER_ID) {
replyMessage = "認証されていないユーザーは使用できません。";
reply(replyToken, replyMessage);
lineNotify('mukkitterに認証されていないユーザーからのアクセスがありました。直ちに確認してください!');
return;
}
// 特定のユーザーであることがわかれば正常処理
const tweetId = tweet(userMessage);
if (tweetId > 0) {
replyMessage = `ツイート成功!\nhttps://twitter.com/${TWITTER_ACCOUNT_NAME}/status/${tweetId}`;
} else {
replyMessage = "ツイート失敗!!";
}
reply(replyToken, replyMessage);
}
// ツイートを送信
function tweet(message) {
const payload = {
"text": message
};
const service = twitter.getService();
const response = service.fetch(TWITTER_POST_URL, {
'method': 'POST',
'contentType': 'application/json',
'payload': JSON.stringify(payload)
});
if (response.getResponseCode() >= 200 && response.getResponseCode() < 300) {
return JSON.parse(response.getContentText()).data.id;
} else {
return -1;
}
}
// LINE送信したユーザーIDを取得する
function lineUserId(eventData) {
return eventData.source.userId
}
// LINEへのリプライ
function reply(token, message) {
const payload = {
'replyToken': token,
'messages': [{
'type': 'text',
'text': message
}]
};
const options = {
'payload' : JSON.stringify(payload),
'method' : 'POST',
'headers' : {"Authorization" : "Bearer " + LINE_API_ACCESS_TOKEN},
'contentType' : 'application/json'
};
UrlFetchApp.fetch(LINE_REPLY_URL, options);
}
// LINE Notifyへの通知
function lineNotify(text) {
const options = {
"method" : "post",
"payload": {"message": text},
"headers": {"Authorization":"Bearer " + LINE_NOTIFY_TOKEN}
}
UrlFetchApp.fetch(LINE_NOTIFY_API, options)
}
// Twitter認証
function authorize() {
twitter.authorize();
}
// Twitter認証解除
function reset() {
twitter.reset();
}
// Twitter認証時のコールバック
function authCallback(request) {
return twitter.authCallback(request);
}
LINE BotはIDまたはQRコードがわかると誰でも利用できてしまうため、筆者が作成したLINE BotのIDを公開してしまうと誰でも筆者のTwitterに投稿できるようになってしまいます。
そこで今回は、事前にGASに登録した特定のLINEユーザーからのPOSTしか受け付けないような工夫を入れてみました(以下の部分)。
// 特定のLINEユーザー以外からは利用できないようにする
if(lineUserId(eventData) !== LINE_USER_ID) {
replyMessage = "認証されていないユーザーは使用できません。";
reply(replyToken, replyMessage);
lineNotify('mukkitterに認証されていないユーザーからのアクセスがありました。直ちに確認してください!');
return;
}
別途LineNotifyを使って自分のLINEに通知をすることで、いち早く気づくことができるようにしています。
LINE Notifyの設定方法については以下の記事を参考にしてください。
動作確認
自分のLINEから友達登録をして、ツイートしてみます。
実際のPOSTはこちらです!ちゃんと絵文字付きで投稿されていました。(よかった)
まとめ
今回は「LINEからTwitterへ投稿できるようにする」をやってみました。
※コピペ自由ですが、LINE BotのIDやQRコードは絶対公開しないようにしてください(Twitterが乗っ取られます)
お読みいただきありがとうございました!
おまけ
画像付きツイートにも対応しました!
GASに状態を持たせる必要があるため条件分岐が複雑になってしまいました(汗
const LINE_API_ACCESS_TOKEN = PropertiesService.getScriptProperties().getProperty('LINE_API_ACCESS_TOKEN');
const LINE_REPLY_URL = 'https://api.line.me/v2/bot/message/reply';
const TWITTER_API_KEY = PropertiesService.getScriptProperties().getProperty('TWITTER_API_KEY');
const TWITTER_API_SECRET = PropertiesService.getScriptProperties().getProperty('TWITTER_API_SECRET');
const TWITTER_TOKEN = PropertiesService.getScriptProperties().getProperty('TWITTER_TOKEN');
const TWITTER_TOKEN_SECRET = PropertiesService.getScriptProperties().getProperty('TWITTER_TOKEN_SECRET');
const TWITTER_BEARER_TOKEN = PropertiesService.getScriptProperties().getProperty('TWITTER_BEARER_TOKEN');
const TWITTER_POST_URL = 'https://api.twitter.com/2/tweets';
const TWITTER_MEDIA_POST_URL = 'https://upload.twitter.com/1.1/media/upload.json';
const TWITTER_ACCOUNT_NAME = 'nmukineer';
const LINE_USER_ID = PropertiesService.getScriptProperties().getProperty('LINE_USER_ID');
const LINE_NOTIFY_API = 'https://notify-api.line.me/api/notify';
const LINE_NOTIFY_TOKEN = PropertiesService.getScriptProperties().getProperty('LINE_NOTIFY_TOKEN');
const IMAGE_FOLDER_ID = PropertiesService.getScriptProperties().getProperty('IMAGE_FOLDER_ID');
const twitter = TwitterWebService.getInstance(TWITTER_API_KEY,TWITTER_API_SECRET);
const imageFolder = DriveApp.getFolderById(IMAGE_FOLDER_ID);
const STATE = {
origin: [
'origin'
],
tweet: [
'waitText'
],
tweetPhoto: [
'waitPhoto',
'waitText'
]
}
const PRESET_WORD = {
tweet: "ツイートしたい",
tweetPhoto: "画像付きツイートしたい",
reset: "リセット"
}
const PRESET_RESPONSE = {
tweet: [
"ツイートしたいテキストを送ってね"
],
tweetPhoto: [
"ツイートしたい画像をアップロードしてね",
"ツイートしたいテキストを送ってね"
]
}
function test() {
const endDate = "2022/10/20";
const dateRange = 7;
const to = new Date(endDate);
let from = new Date(endDate);
from.setDate(from.getDate() - dateRange);
const key = 'weight';
const fileId = CreateGraph.createGraph(from, to, key);
console.log(fileId);
}
// LINE送信したときに実行される関数
function doPost(e) {
const eventData = JSON.parse(e.postData.contents).events[0];
console.log(lineUserId(eventData));
const replyToken = eventData.replyToken;
const userMessage = eventData.message.text;
const messageType = eventData.message.type;
const messageId = eventData.message.id;
console.log(messageType);
console.log(userMessage);
let replyMessage = "どこの条件にもひっかかなかった場合はこの文字列が送られるよ。"
// 特定のLINEユーザー以外からは利用できないようにする
if(lineUserId(eventData) !== LINE_USER_ID) {
replyMessage = "認証されていないユーザーは使用できません。";
reply(replyToken, replyMessage);
lineNotify('mukkitterに認証されていないユーザーからのアクセスがありました。直ちに確認してください!');
return;
}
// 特定のユーザーであることがわかれば正常処理
const currentState = getCurrentState();
const currentSubState = getCurrentSubState();
console.log(currentState);
console.log(currentSubState);
if(userMessage == PRESET_WORD.reset) {
resetState();
resetProperty("IMAGE_ID");
replyMessage = "リセットしたよ";
reply(replyToken, replyMessage);
return;
}
if(currentState == 'origin') {
if(messageType != 'text') {
replyMessage = "最初はテキストで送ってね";
reply(replyToken, replyMessage);
return;
}
let nextState = "";
let nextSubState = "";
switch(userMessage) {
case PRESET_WORD.tweet:
nextState = 'tweet';
nextSubState = STATE[nextState][0];
replyMessage = PRESET_RESPONSE.tweet[0];
break;
case PRESET_WORD.tweetPhoto:
nextState = 'tweetPhoto';
nextSubState = STATE[nextState][0];
replyMessage = PRESET_RESPONSE.tweetPhoto[0];
break;
case PRESET_WORD.weight:
nextState = 'weight';
nextSubState = STATE[nextState][0];
replyMessage = PRESET_RESPONSE.weight[0];
break;
case PRESET_WORD.fat:
nextState = 'fat';
nextSubState = STATE[nextState][0];
replyMessage = PRESET_RESPONSE.fat[0];
break;
default:
nextState = currentState;
nextSubState = currentSubState;
replyMessage = "その言葉は理解できないよ"
}
setState(nextState, nextSubState);
} else if (Object.values(PRESET_WORD).includes(userMessage) ) {
resetState();
resetProperty("IMAGE_ID");
replyMessage = "連続で押すことはできないよ。最初からやり直してね。";
reply(replyToken, replyMessage);
return;
}
if (currentState == 'tweet') {
if(currentSubState == STATE.tweet[0]) {
if(messageType == 'text') {
const tweetId = tweet(userMessage);
if (tweetId > 0) {
replyMessage = `ツイート成功!\nhttps://twitter.com/${TWITTER_ACCOUNT_NAME}/status/${tweetId}`;
} else {
replyMessage = "ツイート失敗!!";
}
} else {
replyMessage = "テキストを送ってね。最初からやり直してね。";
}
resetState();
}
} else if (currentState == 'tweetPhoto') {
if(currentSubState == STATE.tweetPhoto[0]) {
if(messageType == 'image') {
saveImage(messageId);
replyMessage = PRESET_RESPONSE.tweetPhoto[1];
setToNextSubState(currentState, currentSubState);
} else if(messageType == 'text') {
replyMessage = "画像を送ってね。最初からやり直してね。"
resetState();
}
} else if (currentSubState == STATE.tweetPhoto[1]) {
if(messageType == 'text') {
const imageId = PropertiesService.getScriptProperties().getProperty('IMAGE_ID');
if(imageId != "") {
const tweetId = tweetWithMedia(userMessage, imageId);
if (tweetId > 0) {
replyMessage = `ツイート成功!\nhttps://twitter.com/${TWITTER_ACCOUNT_NAME}/status/${tweetId}`;
} else {
replyMessage = "ツイート失敗!!";
}
deleteMedia(imageId);
}
else {
replyMessage = "メディアが見つかりません。最初からやり直してね"
}
} else {
replyMessage = "テキストを送ってね。最初からやり直してね。";
}
resetState();
resetProperty("IMAGE_ID");
}
}
reply(replyToken, replyMessage);
}
// 現在の状態を取得
function getCurrentState() {
const state = PropertiesService.getScriptProperties().getProperty('CURRENT_STATE');
return state.split('/')[0];
}
// 現在の状態を取得(subState)
function getCurrentSubState() {
const state = PropertiesService.getScriptProperties().getProperty('CURRENT_STATE');
return state.split('/')[1];
}
// 状態をセット
function setState(state, subState) {
const newState = `${state}/${subState}`
PropertiesService.getScriptProperties().setProperty("CURRENT_STATE", newState);
return newState;
}
// 次のsubStateに遷移(最後の場合はoriginに戻る)
function setToNextSubState(state, subState) {
const substateLength = STATE[state].length;
const currentIndex = STATE[state].indexOf(subState);
let nextState = "";
let nextSubState = "";
if (currentIndex >= substateLength - 1) {
nextState = "origin";
nextSubState = STATE[nextState];
} else {
nextState = state;
nextSubState = STATE[state][currentIndex + 1];
}
const newState = `${nextState}/${nextSubState}`
return setState(nextState, nextSubState);
}
// 引数に指定したスクリプトプロパティをリセット(""に)する
function resetProperty(name) {
PropertiesService.getScriptProperties().setProperty(name, "");
}
// 所定のフォルダに画像を保存
function saveImage(messageId) {
const url = `https://api-data.line.me/v2/bot/message/${messageId}/content`
const options = {
"method" : "get",
"headers": {"Authorization":"Bearer " + LINE_API_ACCESS_TOKEN}
}
const response = UrlFetchApp.fetch(url, options)
const blob = response.getBlob();
let newFile = imageFolder.createFile(blob);
newFile.setName(messageId);
PropertiesService.getScriptProperties().setProperty("IMAGE_ID", newFile.getId());
}
// 文字だけツイート
function tweet(message) {
const payload = {
"text": message
};
const service = twitter.getService();
const response = service.fetch(TWITTER_POST_URL, {
'method': 'POST',
'contentType': 'application/json',
'payload': JSON.stringify(payload)
});
if (response.getResponseCode() >= 200 && response.getResponseCode() < 300) {
return JSON.parse(response.getContentText()).data.id;
} else {
return -1;
}
}
// メディア付きツイート
function tweetWithMedia(message, mediaId) {
const twitterMediaId = uploadMedia(mediaId);
const payload = {
"text": message,
"media": {
"media_ids": [
twitterMediaId
]
}
};
const service = twitter.getService();
const response = service.fetch(TWITTER_POST_URL, {
'method': 'POST',
'contentType': 'application/json',
'payload': JSON.stringify(payload)
});
if (response.getResponseCode() >= 200 && response.getResponseCode() < 300) {
return JSON.parse(response.getContentText()).data.id;
} else {
return -1;
}
}
// Twitterにメディアをアップロード
function uploadMedia(mediaId) {
const media = DriveApp.getFileById(mediaId);
const m = media.getBlob().getBytes();
const service = twitter.getService();
const payload = {
'media_data': Utilities.base64Encode(m)
};
console.log(JSON.stringify(payload))
const response = service.fetch(TWITTER_MEDIA_POST_URL, {
'method': 'POST',
'payload': payload
});
const twitterMediaId = JSON.parse(response.getContentText()).media_id_string
console.log(twitterMediaId)
return twitterMediaId;
}
// メディアの削除
function deleteMedia(mediaId) {
DriveApp.getFileById(mediaId).setTrashed(true);
}
// LINE送信したユーザーIDを取得する
function lineUserId(eventData) {
return eventData.source.userId
}
// LINEへのリプライ
function reply(token, message) {
const payload = {
'replyToken': token,
'messages': [{
'type': 'text',
'text': message
}]
};
const options = {
'payload' : JSON.stringify(payload),
'method' : 'POST',
'headers' : {"Authorization" : "Bearer " + LINE_API_ACCESS_TOKEN},
'contentType' : 'application/json'
};
UrlFetchApp.fetch(LINE_REPLY_URL, options);
}
// LINE Notifyへの通知
function lineNotify(text) {
const options = {
"method" : "post",
"payload": {"message": text},
"headers": {"Authorization":"Bearer " + LINE_NOTIFY_TOKEN}
}
UrlFetchApp.fetch(LINE_NOTIFY_API, options)
}
// 状態をリセット
function resetState() {
setState('origin','origin');
}
// Twitter認証
function authorize() {
twitter.authorize();
}
// Twitter認証解除
function reset() {
twitter.reset();
}
// Twitter認証時のコールバック
function authCallback(request) {
return twitter.authCallback(request);
}