方言を話すおしゃべり猫型ロボット『ミーア』をリリースしました(こちらをクリック)

【X(Twitter) bot】GASでスプレッドシートから自動投稿するX Botの作り方:③テキスト+画像投稿

この記事は約11分で読めます。

Google Apps Script(GAS)で、スプレッドシートからX(旧Twitter) API v2経由でXに自動投稿するX botの作成方法を備忘録として記載。

前回、下記記事で、スプレッドシートからGASを使ってテキストをTwitterに投稿する手順を記載した。テキストのみのTwitter Bot投稿作成はこちら。

今回は、テキスト+画像投稿に関して。想像以上にややこしかったのでまとめておく。

前提(2023年9月18日時点):OAuth1.0+Twitter API v1.1

まだ、Twitter API v2は画像アップロードに対応していない

現時点ではTwitter APIを使って画像をツイートするには、OAuth1.0aを使用する必要がある。

画像付きツイートを実装するには、

と、2つのバージョンのAPIエンドポイントを組み合わせる必要がある。Twitter API v2が、現時点でまだテキスト投稿のみにしか対応しておらず、メディアファイルのアップロードに対応していないため。

そして、Twitter API v2はOAuth2.0に対応しているが、Twitter API v1.1はO Auth2.0に対応していない。なので、OAuth1.0を使用する必要がある。

ややこしすぎるので、早くTwitter API v2にメディアファイル、アップロード機能対応してほしい。

OAuth1認証用のAPIキーを取得

というわけで、新たにDeveloper PortalからOAuth1認証用のAPIキーを取得する必要がある。

OAuth2の場合は、Client IDとClient Secretの2つのみで良かったが、OAuth1の場合は、下記の4つが必要

・Consumer API Key
・Consumer Secret
・Access Token
・Access Token Secret

全て、Keys and tokensタブに記載されているので、regenerateして値を控えておく。

OAuth1ライブラリを追加

GASスクリプトに戻り、左のライブラリから、新しくOAuth1ライブラリを追加する必要がある。

GASスクリプトもファイル名を、textのみ用とtext-with-image用とでわけで、今回の画像付きツイート投稿用のファイルを新規作成した方が、混乱しなくてよさそう。

OAuth1のスクリプトIDは下記
1CXDCY5sqT9ph64fFwSzVtXnbjpSfWdRymafDrtIZ7Z_hwysTY7IIhi7s

このようにOAuth1が表示されるので、追加する。

画像付きツイートのGASスクリプト

スプレッドシートに画像URLを用意

今回は、下記のようにスプレッドシートのF列に、image_urlというカラムを用意し、ここにpublicにアクセスできる画像URLをテストで配置。

今回の画像表示URLはこちら

https://kazulog.fun/wp-content/uploads/2023/08/dc-servo.jpg

GASスクリプト

下記コードをコピペ。

・Consumer API Key
・Consumer Secret
・Access Token
・Access Token Secret

は、先ほど控えた値を入れる。

GDScript
const CONSUMER_API_KEY = "XXX";
const CONSUMER_API_SECRET = "XXX";
const ACCESS_TOKEN = "XXX";
const ACCESS_TOKEN_SECRET = "XXX";
  
// OAuth1認証
const getTwitterService = function() {
  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( CONSUMER_API_KEY )
  .setConsumerSecret( CONSUMER_API_SECRET )
  .setAccessToken( ACCESS_TOKEN, ACCESS_TOKEN_SECRET )
  .setCallbackFunction('authCallback'); // コールバック関数名 
}

// OAuthコールバック
function authCallback(request) {
  const service = getTwitterService();
  const authorized = service.handleCallback(request);
  if (authorized) {
    return HtmlService.createHtmlOutput('Success!');
  } else {
    return HtmlService.createHtmlOutput('Denied.');
  }
}


function tweetWithImage() {
  const twitter = getTwitterService()
  var tweetData = pickUpTweet("twitter_text"); // ツイートの内容と画像URLを取得

  if(twitter.hasAccess()) {

    const endpoint1 = "https://upload.twitter.com/1.1/media/upload.json"
    const endpoint2 = "https://api.twitter.com/2/tweets";

    if (tweetData.imageUrl) {
      var imgBlob = UrlFetchApp.fetch(tweetData.imageUrl).getBlob();
      var img_64  = Utilities.base64Encode(imgBlob.getBytes()); //Blobを経由してBase64に変換

      var img_option = { 'method':"POST", 'payload':{'media_data':img_64} };
      var image_repsponse = twitter.fetch(endpoint1,img_option); 
      var image_result = JSON.parse(image_repsponse.getContentText());
      
      var mediainfo = {
        media_ids: [image_result['media_id_string']]
      }
      var payload = {
        text: tweetData.text,
        media: mediainfo
      }

      var response = twitter.fetch(endpoint2, {
        method: "post",
        muteHttpExceptions: true,
        payload: JSON.stringify(payload),
        contentType: "application/json"
      });

      var result = JSON.parse(response.getContentText());

    } else {
      Logger.log(service.getLastError())
    }
  }
}


function pickUpTweet(sheetName) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
  const lastRow = sheet.getLastRow();
  const data = sheet.getRange("A1:F" + lastRow).getValues();

  const availableTweets = data.filter(function(value) {
    return value[1] == '';
  });

  if (availableTweets.length == 0) return { text: '', imageUrl: '' };

  const weights = availableTweets.map(function(value) {
    return value[4]; // Assuming the weight is in column E
  });

  const alias = new AliasMethod(weights);
  const selectedIndex = alias.next();

  const selectedRow = availableTweets[selectedIndex][0];
  Logger.log("Selected row: " + selectedRow); // Log the selected row number

  sheet.getRange(selectedRow + 1, 2).setValue(new Date());

  return {
    text: availableTweets[selectedIndex][2], // Assuming the text is in column C
    imageUrl: availableTweets[selectedIndex][5] // Assuming the image URL is in column F
  };
}

class AliasMethod {
  constructor(weights) {
    this.prob = [];
    this.alias = [];
    this.n = weights.length;

    const small = [];
    const large = [];
    const scaledWeights = weights.map((w) => w * this.n);

    for (let i = 0; i < this.n; i++) {
      if (scaledWeights[i] < 1) {
        small.push(i);
      } else {
        large.push(i);
      }
    }

    while (small.length > 0 && large.length > 0) {
      const l = small.pop();
      const g = large.pop();

      this.prob[l] = scaledWeights[l];
      this.alias[l] = g;

      scaledWeights[g] = scaledWeights[g] + scaledWeights[l] - 1;

      if (scaledWeights[g] < 1) {
        small.push(g);
      } else {
        large.push(g);
      }
    }

    while (large.length > 0) {
      const g = large.pop();
      this.prob[g] = 1;
    }

    while (small.length > 0) {
      const l = small.pop();
      this.prob[l] = 1;
    }
  }

  next() {
    const i = Math.floor(Math.random() * this.n);
    return Math.random() < this.prob[i] ? i : this.alias[i];
  }
}

実行!

下記のように、無事、画像付きテキストが実行された。

Bot投稿間隔を設定

GASのトリガー機能

Twitterへの投稿機能はできたので、最後にGASのトリガー機能を使って投稿間隔を設定する

左サイドバーから「トリガー」を選択し、開いた画面の右下の「トリガーを追加」をクリック

実行する関数を選択(今回は、先ほど作成した画像付きツイート)

イベントのソースを選択 から「時間主導型」を選択。

すると、時間間隔を設定する画面が表示されるので、適当に選択して保存

これで無事、定期間隔でスプレッドシートから、ランダムに重みに応じて画像付きテキストを抽出して投稿することができるようになりました。

コメント

タイトルとURLをコピーしました