1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

★GAS 画像つきポスト定期投稿

Last updated at Posted at 2025-02-05

毎日定時にスプレッドシートから内容取得してXにポストしてくれるやつ!つくれますよ!

1. 概要

本システムは、以下の処理を自動化します。

Googleスプレッドシート から「ステータス」が 1 の最上行のデータを取得して毎日定時にポストします
画像つきポストしたいときはGoogle Drive の「投稿用画像」フォルダにアップロードしたファイル名をスプレッドシートに記載してください
投稿成功後、スプレッドシートの「ステータス」を 0 に更新

やることはだいたいこんな感じ

  1. X Developer PlatformでAPIの設定
  2. Googlespreadsheet、GoogleDriveの設定
  3. GoogleAppScriptcodeにコードコピペ &認証

2. 必要な準備

2.1 X APIの設定

X Developer Platform へアクセスし、開発者アカウントを作成。
アカウントの設定等は下記 @neru-dev さんの記事参考に

{1B8CA30E-9C01-4941-B0AF-5C195D593760}.png

Callback URI / Redirect URL (required)
は下記****部分にGASのscriptIDを入力してください。
※scriptIDは AppScriptのURLのprojects/以降、または左のメニューのスクリプトの設定で確認してください。
https://script.google.com/macros/d/ /usercallback

image.png

2.2 Googleスプレッドシートの準備

以下のフォーマットでGoogleスプレッドシートを作成。

列に入力する内容
A列(投稿内容) B列(画像名) C列(ステータス)D列(画像ID:触らない!自動入力されます)

ポイント:

「ステータス」列が 1 である一番上のデータから投稿されます。

※scriptつきspreadsheet共有しますのでもしよければ活用ください 認証ライブラリ付です
https://docs.google.com/spreadsheets/d/1FC-SsdR62bYSTyM-o6NlBEae7RBCshZAfyKFrMOJIEw/edit?usp=sharing

2.3 投稿用画像の設定

GoogleDriveに「投稿用画像」というフォルダ準備して
そのなかに画像をアップロード
投稿したいファイル名をスプレッドシートB列に記載してください

2.4 Google Apps Script(GAS)の設定

Googleスプレッドシートを開く。
「拡張機能」→「Apps Script」を開き、新規スクリプトを作成。

認証ライブラリ追加

image.png
スクリプトIDの所に下記の文字列をコピペ

1CXDCY5sqT9ph64fFwSzVtXnbjpSfWdRymafDrtIZ7Z_hwysTY7IIhi7s

image.png

検索を押すとOAuth1と出てくるので追加ボタンを押す。

image.png

同じようにOAuth2も下記の文字列コピペで追加。

1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF

OAuth1 OAuth2 が追加されればOK
image.png

スクリプト

下記のスクリプトをコピー&ペースト。

3. スクリプト

GASファイル名は任意のお名前でどうぞ
GoogleAppScript

/********************************
 * OAuth2.0認証情報(Twitter API用)
 ********************************/
/********************************
 * OAuth1.0a認証情報(Twitter API用)
 ********************************/
var TWITTER_CONSUMER_KEY = 'XXXXXXXXXXXXXXXXXXXXXXXX';
var TWITTER_CONSUMER_SECRET = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
var TWITTER_ACCESS_TOKEN = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
var TWITTER_ACCESS_TOKEN_SECRET = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';

/********************************
 * onOpen() カスタムメニュー追加
 ********************************/
function onOpen() {
  var ui = SpreadsheetApp.getUi();
  ui.createMenu('ツイート操作')
    .addItem('投稿開始', 'processTweetRow')
    .addToUi();
}

/********************************
 * PKCEチャレンジ&バリファイアの生成(OAuth2.0用)
 ********************************/
function pkceChallengeVerifier() {
  var userProps = PropertiesService.getUserProperties();
  if (!userProps.getProperty("code_verifier")) {
    var verifier = "";
    var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
    for (var i = 0; i < 128; i++) {
      verifier += possible.charAt(Math.floor(Math.random() * possible.length));
    }
    var sha256Hash = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, verifier);
    var challenge = Utilities.base64Encode(sha256Hash)
      .replace(/\+/g, '-')
      .replace(/\//g, '_')
      .replace(/=+$/, '');
    userProps.setProperty("code_verifier", verifier);
    userProps.setProperty("code_challenge", challenge);
  }
}

/********************************
 * OAuth2サービスの取得(Twitter API用:OAuth2.0)
 ********************************/
function getService() {
  pkceChallengeVerifier();
  const userProps = PropertiesService.getUserProperties();
  return OAuth2.createService('twitter')
    .setAuthorizationBaseUrl('https://twitter.com/i/oauth2/authorize')
    .setTokenUrl('https://api.twitter.com/2/oauth2/token?code_verifier=' + userProps.getProperty("code_verifier"))
    .setClientId(CLIENT_ID)
    .setClientSecret(CLIENT_SECRET)
    .setCallbackFunction('authCallback')
    .setPropertyStore(userProps)
    .setScope('users.read tweet.read tweet.write offline.access')
    .setParam('response_type', 'code')
    .setParam('code_challenge_method', 'S256')
    .setParam('code_challenge', userProps.getProperty("code_challenge"))
    .setTokenHeaders({
      'Authorization': 'Basic ' + Utilities.base64Encode(CLIENT_ID + ':' + CLIENT_SECRET),
      'Content-Type': 'application/x-www-form-urlencoded'
    });
}

/********************************
 * 初回認証用メイン処理(OAuth2.0)
 ********************************/
function main() {
  const service = getService();
  if (service.hasAccess()) {
    Logger.log("Already authorized");
  } else {
    const authorizationUrl = service.getAuthorizationUrl();
    Logger.log('次のURLを開いて認証後、スクリプトを再実行してください: %s', authorizationUrl);
  }
}

/********************************
 * 認証確認コールバック処理(OAuth2.0)
 ********************************/
function authCallback(request) {
  const service = getService();
  const authorized = service.handleCallback(request);
  if (authorized) {
    return HtmlService.createHtmlOutput('Success!');
  } else {
    return HtmlService.createHtmlOutput('Denied.');
  }
}

/********************************
 * OAuth1.0aサービスの取得(Twitter API用)
 ********************************/
function getOAuthServiceV1() {
  return OAuth1.createService('twitter')
    .setAccessTokenUrl('https://api.twitter.com/oauth/access_token')
    .setRequestTokenUrl('https://api.twitter.com/oauth/request_token')
    .setAuthorizationUrl('https://api.twitter.com/oauth/authorize')
    .setConsumerKey(TWITTER_CONSUMER_KEY)
    .setConsumerSecret(TWITTER_CONSUMER_SECRET)
    .setAccessToken(TWITTER_ACCESS_TOKEN, TWITTER_ACCESS_TOKEN_SECRET);
}

/********************************
 * 画像アップロード&メディアID取得(OAuth1.0a)
 * 画像ファイル名を引数として受け取り、Drive内の「投稿用画像」フォルダから該当ファイルを取得
 ********************************/
function uploadMedia(fileName) {
  var service = getOAuthServiceV1();
  if (!service.hasAccess()) {
    Logger.log('OAuth1認証が必要です。');
    return null;
  }
  
  // Drive内の「投稿用画像」フォルダから指定ファイルを取得
  var folderIterator = DriveApp.getFoldersByName('投稿用画像');
  if (!folderIterator.hasNext()) {
    Logger.log('フォルダ「投稿用画像」が見つかりません。');
    return null;
  }
  var folder = folderIterator.next();
  
  var fileIterator = folder.getFilesByName(fileName);
  if (!fileIterator.hasNext()) {
    Logger.log('ファイル「' + fileName + '」が見つかりません。');
    return null;
  }
  var file = fileIterator.next();
  var blob = file.getBlob();
  
  // BlobをBase64エンコード
  var base64Data = Utilities.base64Encode(blob.getBytes());
  
  // メディアアップロードエンドポイント(v1.1)
  var url = 'https://upload.twitter.com/1.1/media/upload.json';
  var options = {
    method: 'post',
    payload: {
      media_data: base64Data
    },
    muteHttpExceptions: true
  };
  
  var response = service.fetch(url, options);
  var result = JSON.parse(response.getContentText());
  
  if (result.media_id_string) {
    var mediaId = result.media_id_string;
    Logger.log('取得したメディアID: ' + mediaId);
    return mediaId;
  } else {
    Logger.log('メディアアップロード失敗: ' + response.getContentText());
    return null;
  }
}

/********************************
 * スプレッドシートの対象行を処理するメイン関数
 * ・「投稿メッセージリスト」シートで、C列が 1 となっている一番上の行を対象とする。
 * ・その行のB列に記載の画像ファイル名を使って、OAuth1.0aでアップロードし取得したメディアIDを同じ行のD列に記録。
 * ・さらに、同じ行のA列のテキストと取得したメディアIDを使って、OAuth2.0でツイート投稿する。
 * ・処理後、C列の値を0に更新(再処理防止)。
 ********************************/
function processTweetRow() {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName('投稿メッセージリスト');
  if (!sheet) {
    Logger.log('シート「投稿メッセージリスト」が見つかりません。');
    return;
  }
  
  var data = sheet.getDataRange().getValues();
  var targetRowIndex = -1;
  for (var i = 1; i < data.length; i++) {
    if (data[i][2] === 1) {
      targetRowIndex = i + 1;
      break;
    }
  }
  
  if (targetRowIndex === -1) {
    Logger.log('C列が1の行が見つかりません。');
    return;
  }
  
  var tweetText = sheet.getRange(targetRowIndex, 1).getValue().toString();
  var imageFileName = sheet.getRange(targetRowIndex, 2).getValue();
  var mediaId = null;
  
  if (imageFileName) {
    mediaId = uploadMedia(imageFileName);
    if (!mediaId) {
      Logger.log('メディアIDの取得に失敗しました。');
      return;
    }
    sheet.getRange(targetRowIndex, 4).setValue(mediaId);
  }
  
  var service = getService();
  if (!service.hasAccess()) {
    Logger.log('OAuth2認証が必要です。');
    return;
  }
  
  var payload = { text: tweetText };
  if (mediaId) {
    payload.media = { media_ids: [mediaId] };
  }
  
  var options = {
    method: 'post',
    contentType: 'application/json',
    headers: { "Authorization": "Bearer " + service.getAccessToken() },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  };
  
  var response = UrlFetchApp.fetch('https://api.twitter.com/2/tweets', options);
  var result = JSON.parse(response.getContentText());
  
  var historySheet = ss.getSheetByName('履歴');
  if (!historySheet) {
    historySheet = ss.insertSheet('履歴');
  }
  historySheet.appendRow([result.data ? result.data.id : '', result.data ? result.data.text : '', new Date()]);
  
  sheet.getRange(targetRowIndex, 3).setValue(0);
}


/********************************
 * 定期実行用トリガー設定関数(必要に応じて)
 * 例:毎日午前8時と午後6時に processTweetRow() を実行
 ********************************/
function createTimeTriggers() {
  // 既存のトリガーを全て削除(重複防止)
  var triggers = ScriptApp.getProjectTriggers();
  for (var i = 0; i < triggers.length; i++) {
    ScriptApp.deleteTrigger(triggers[i]);
  }
  // 午前8時に実行
  ScriptApp.newTrigger('processTweetRow')
    .timeBased()
    .everyDays(1)
    .atHour(8)
    .create();
  // // 午後6時に実行
  // ScriptApp.newTrigger('processTweetRow')
  //   .timeBased()
  //   .everyDays(1)
  //   .atHour(18)
  //   .create();
}

APIキー、APIシークレット、アクセストークン、アクセストークンシークレットを取得して
XXXXXXの所 を修正。
'' は消さないで!

var TWITTER_CONSUMER_KEY = 'XXXXXXXXXXXXXXXXXXXXXXXX';
var TWITTER_CONSUMER_SECRET = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
var TWITTER_ACCESS_TOKEN = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
var TWITTER_ACCESS_TOKEN_SECRET = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';

PIキー、APIシークレット、アクセストークン、アクセストークンシークレット取得方法
Developer Portalの左メニューから開いて

image.png

image.png

4. 実行方法

4.0 トリガー設定

図の所から
createTimeTriggers
を選択して▷実行 ボタンを押しましょう
関数を走らせて
定期実行するためのトリガーを設定してくれます

image.png

4.1 認証

Xapiの認証のためにGASのmain関数を選択して ▷実行 ボタンを押す

image.png

権限を確認
{9E2AA8E7-7A45-4BA7-A4C7-40B2AD795DC4}.png

自分のGoogleアカウント選択

「このアプリは~」という画面に来たら”詳細” 押して
”(安全ではないページ)に移動”を押してすべてチェック入れて完了

すると 実行ログにこんなメッセージ出るので
https://twitter.com/i/~ からの全文をブラウザの新規タブに張り付け
image.png

Xとの連携認証画面に行くので認証!
image.png

以上で設定終了です。

スクリプトの一番下定期実行の設定時間(上記コードでは午前八時に)に自動的に投稿されます
もしも定期実行したくない時はコメントアウトして下さい

スプレッドシートの感じ
image.png

4.2 即時ポスト

即時にポストしたい時はスプレッドシートの「ツイート操作」を押して下さい。
スクリプトの検証用にどうぞ

image.png

参考リンク

定期ポストする際の注意点など参考に

spreadsheetなど共有します
認証ライブラリとか設定済みなので
記事内容確認しつつ作業は飛ばしてOKです

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?