GAS

GASでTwitter APIを使ってLINEからツイートを投稿する方法

thumbnail
n-mukineer
えぬ
えぬ

つぶやきたいだけなのに、ツイッターってタイムラインが面白くてついつい長時間見てしまうんですよね…そんなとき、Twitterを開かずにツイートができたらいいなと思ってLINEからツイートできるアプリを作ってみました。

今回作ったもの
LINEから画像付きツイート
LINEから画像付きツイート

↓実際のツイートはこちら

GASの初期設定

GASでTwitter APIを使えるようにする

GASでTwitter APIを使えるようにする方法は、以下の記事にまとめてありますので参考にしていただければと思います。

あわせて読みたい
【簡単】GASでTwitter APIを使えるようにする方法【3つの手順】
【簡単】GASでTwitter APIを使えるようにする方法【3つの手順】

LINE Messaging APIの設定

LINE Messaging APIの設定を行います。LINE Messaging APIをGASで使う方法については以下の記事を参考にしてください。

あわせて読みたい
【「今日晩御飯どうする?」を解決!】LINE Messaging APIとGASを使って晩御飯候補を決めてもらった
【「今日晩御飯どうする?」を解決!】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の設定方法については以下の記事を参考にしてください。

あわせて読みたい
GASからLINE Notifyを使ってLINE通知する方法
GASからLINE Notifyを使ってLINE通知する方法

動作確認

自分のLINEから友達登録をして、ツイートしてみます。

LINEからツイートしてみるテスト
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);
}
このブログを書いている人
えぬ
えぬ
N日後にムキムキになるエンジニア
WebアプリエンジニアとしてIoTシステムを開発中。30代折り返し。 趣味(モノづくり、プログラミング、筋トレ)や子育てのことを主に記事にします。 TOEIC: 900点/第一級陸上無線技術士/第3種電気主任技術者/技術士一次試験合格/基本情報技術者/第2種電気工事士/デジタル技術検定2級(情報・制御)
記事URLをコピーしました