毎日定時にスプレッドシートから内容取得してXにポストしてくれるやつ!つくれますよ!
1. 概要
本システムは、以下の処理を自動化します。
Googleスプレッドシート から「ステータス」が 1 の最上行のデータを取得して毎日定時にポストします
画像つきポストしたいときはGoogle Drive の「投稿用画像」フォルダにアップロードしたファイル名をスプレッドシートに記載してください
投稿成功後、スプレッドシートの「ステータス」を 0 に更新
やることはだいたいこんな感じ
- X Developer PlatformでAPIの設定
- Googlespreadsheet、GoogleDriveの設定
- GoogleAppScriptcodeにコードコピペ &認証
2. 必要な準備
2.1 X APIの設定
X Developer Platform へアクセスし、開発者アカウントを作成。
アカウントの設定等は下記 @neru-dev さんの記事参考に
Callback URI / Redirect URL (required)
は下記****部分にGASのscriptIDを入力してください。
※scriptIDは AppScriptのURLのprojects/以降、または左のメニューのスクリプトの設定で確認してください。
https://script.google.com/macros/d/ /usercallback
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」を開き、新規スクリプトを作成。
認証ライブラリ追加
1CXDCY5sqT9ph64fFwSzVtXnbjpSfWdRymafDrtIZ7Z_hwysTY7IIhi7s
検索を押すとOAuth1と出てくるので追加ボタンを押す。
同じようにOAuth2も下記の文字列コピペで追加。
1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF
スクリプト
下記のスクリプトをコピー&ペースト。
3. スクリプト
GASファイル名は任意のお名前でどうぞ
/********************************
* 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';
4. 実行方法
4.0 トリガー設定
図の所から
createTimeTriggers
を選択して▷実行 ボタンを押しましょう
関数を走らせて
定期実行するためのトリガーを設定してくれます
4.1 認証
Xapiの認証のためにGASのmain関数を選択して ▷実行 ボタンを押す
自分のGoogleアカウント選択
「このアプリは~」という画面に来たら”詳細” 押して
”(安全ではないページ)に移動”を押してすべてチェック入れて完了
すると 実行ログにこんなメッセージ出るので
https://twitter.com/i/~ からの全文をブラウザの新規タブに張り付け
以上で設定終了です。
スクリプトの一番下定期実行の設定時間(上記コードでは午前八時に)に自動的に投稿されます
もしも定期実行したくない時はコメントアウトして下さい
4.2 即時ポスト
即時にポストしたい時はスプレッドシートの「ツイート操作」を押して下さい。
スクリプトの検証用にどうぞ
参考リンク
定期ポストする際の注意点など参考に
spreadsheetなど共有します
認証ライブラリとか設定済みなので
記事内容確認しつつ作業は飛ばしてOKです