目次
概要
スプレッドシートに記入した内容をX(旧Twitter)に投稿するようなシステムを作ろうと思った。
株価を取得したものを自動で投稿させたりしできたらいいと思い、今回実装した。
準備
まず、自動投稿のツールを使うには下準備をする必要がある。
スプレッドシート関連
まず、スプレッドシートを作成する。
手順は以下だ。
- Googleスプレッドシートを開く
- 「拡張機能」->「Apps Script」を選択してGASのエディタを開く

- エディタの「ライブラリ」の「+」ボタンを押下する
- スクリプトIDに以下を入力する
1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF - 検索ボタンを押下する
- OAuth2が表示されていることを確認し、「追加」ボタンを押下する

スクリプトIDを入力して、検索

追加ボタンを押下する

X(旧Twitter)
- X Developer Platform(https://developer.twitter.com/en/apps)を開く。
- 投稿を行いたいアカウントでログインする
- 以下はこちらの「事前準備:APIの取得手順」を参照
- アカウント認証を求められるため、「認証する」ボタンを押下する
- 画面右上の「Create an App」ボタンを押下する
- 確認メッセージが表示されたら「Apply」ボタンを押下する
- APIプランを選択する(今回は無料のプランのため、「Sign up for Free Account」を選択)
- 利用目的を英語で記入、そして項目にチェックを入れて「Submit」ボタンを押下する
- トップ画面へ遷移する
以下がトップ画面になる。

そして、画面右にあるDefault project-XXXXXXXXの下にある項目をクリックすると以下の画面になる。
その「User authentication settings」の「Set up」ボタンを押下する。

その後、「App permissions」は「Read and write」を選択

Type of Appは「Web App, Automated App or Bot」を選択。

App infoはWebsite URLは任意。
Callback URI / Redirect URLは以下を入力する。
https://script.google.com/macros/d/{スクリプトID}/usercallback
スクリプトIDは以下から取得できる。
- エディタの左にある歯車アイコン(プロジェクトの設定)を押下する
- 黒塗りにされている箇所にスクリプトIDが表示されている。

「CLIENT_ID」と「CLIENT_SECRET」を取得する。
この二つは以下から取得できる。
- 画面右にあるDefault project-XXXXXXXXの下にある項目をクリックする
- 「Key and tokens」タブを選択
- CLIENT_IDは赤枠で囲われた黒塗り部分
- CLIENT_SECRETはその下の「Client Secret」の「Regenerate」ボタンを押下する。

ここまで取得できれば準備は完了だ。
ソースコード
スプレッドシートの内容

GASのコード
const CLIENT_ID = '[CLIENT_ID]';
const CLIENT_SECRET = '[CLIENT_SECRET]';
function getService() {
pkceChallengeVerifier();
const userProps = PropertiesService.getUserProperties();
const scriptProps = PropertiesService.getScriptProperties();
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'
})
}
function authCallback(request) {
const service = getService();
const authorized = service.handleCallback(request);
if (authorized) {
return HtmlService.createHtmlOutput('Success!');
} else {
return HtmlService.createHtmlOutput('Denied.');
}
}
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)
}
}
function logRedirectUri() {
var service = getService();
Logger.log(service.getRedirectUri());
}
function main() {
const service = getService();
if (service.hasAccess()) {
console.log("Already authorized");
// Logger.log("Already authorized");
} else {
const authorizationUrl = service.getAuthorizationUrl();
console.log('Open the following URL and re-run the script: %s', authorizationUrl);
// Logger.log('Open the following URL and re-run the script: %s', authorizationUrl);
}
}
/**
* Googleスプレッドシートからデータを取得します。
* この関数は、スプレッドシートの全データを読み込み、それを返します。
*/
function getSpreadsheetData() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("シート1"); // スプレッドシートの取得
const range = sheet.getDataRange(); // データ範囲の取得
console.log(range.getValues());
return range.getValues(); // データの取得
}
/**
* スケジュールされたツイートを投稿します。
* この関数は、指定された時間にツイートを自動的に投稿するために使用されます。
*/
function postScheduledTweets() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("シート1"); // スプレッドシートの取得
const rows = sheet.getDataRange().getValues(); // 全行のデータを取得
const now = new Date(); // 現在の日時を取得
// 各行に対する処理
for (let i = 1; i < rows.length; i++) {
const [scheduledTime, tweetContent, status] = rows[i]; // 各列のデータを取得
// スケジュールされた時間が現在時刻以前で、ツイート内容があり、まだ投稿されていない場合
if (scheduledTime && tweetContent && new Date(scheduledTime) <= now && status !== "投稿済") {
sendTweet(tweetContent); // ツイートを送信
sheet.getRange(i + 1, 3).setValue("投稿済"); // ステータスを「投稿済み」に更新
}
}
}
/**
* 指定された内容でツイートを送信します。
* この関数は、Twitter APIを使用してツイートを投稿します。
*/
function sendTweet(tweetContent) {
if (!tweetContent) {
Logger.log("No tweet content provided"); // ツイート内容がない場合のログ出力
return;
}
var service = getService(); // OAuth2サービスの取得
if (service.hasAccess()) {
var url = 'https://api.twitter.com/2/tweets'; // Twitter APIのURL
var response = UrlFetchApp.fetch(url, {
method: 'POST', // POSTリクエスト
contentType: 'application/json', // コンテンツタイプ
headers: {
Authorization: 'Bearer ' + service.getAccessToken() // 認証ヘッダー
},
muteHttpExceptions: true,
payload: JSON.stringify({ text: tweetContent }) // ツイート内容をJSON形式で送信
});
var result = JSON.parse(response.getContentText()); // レスポンスの解析
Logger.log(JSON.stringify(result, null, 2)); // レスポンスのログ出力
} else {
var authorizationUrl = service.getAuthorizationUrl(); // 認証URLの取得
Logger.log('Open the following URL and re-run the script: %s', authorizationUrl); // 認証URLのログ出力
}
}
詳細
スプレッドシート
まずはスプレッドシートを見ていこう。
今回のコードは今回のスプレッドシートの形式に則って書かれている。
プログラミングがわかる人ならアレンジしても問題ないだろう。
まず、一行目に項目名を、二行目以降に項目内容を書いていく。
そして、1列目は、2列目に投稿内容、3列目に投稿済みかどうかのフラグ
日時 | |
投稿内容 | ここに記載した内容がXに投稿される。 |
投稿済みかどうかのフラグ | ここに「投稿済み」と記載されていると自動投稿の対象から外れる。 そして、投稿されたタイミングで「投稿済み」と入力される。 |
GASコード
順番に見ていこう。
まずは「CLIENT_ID」と「CLIENT_SECRET」から。
それは準備の箇所に記載済み。
const CLIENT_ID = '[CLIENT_ID]';
const CLIENT_SECRET = '[CLIENT_SECRET]';
まずはX(旧Twitter)の認証を行う。
以下の部分を抜粋。mainメソッドを実行すると、ログにURLが出てくるのでそのWebページを開こう。

するとログイン画面へ遷移するため、ログインする。
ログイン済みの場合、console.log(“Already authorized”);の部分「Already authorized」が出力される。
function getService() {
pkceChallengeVerifier();
const userProps = PropertiesService.getUserProperties();
const scriptProps = PropertiesService.getScriptProperties();
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'
})
}
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)
}
}
~ 中略 ~
function main() {
const service = getService();
if (service.hasAccess()) {
console.log("Already authorized");
} else {
const authorizationUrl = service.getAuthorizationUrl();
console.log('Open the following URL and re-run the script: %s', authorizationUrl);
}
}
そのあと、postScheduledTweetsメソッドを実行することで投稿ができる。

関係あるコードは以下になる。
function getService() {
pkceChallengeVerifier();
const userProps = PropertiesService.getUserProperties();
const scriptProps = PropertiesService.getScriptProperties();
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'
})
}
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)
}
}
~ 中略 ~
function postScheduledTweets() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("シート1"); // スプレッドシートの取得
const rows = sheet.getDataRange().getValues(); // 全行のデータを取得
const now = new Date(); // 現在の日時を取得
// 各行に対する処理
for (let i = 1; i < rows.length; i++) {
const [scheduledTime, tweetContent, status] = rows[i]; // 各列のデータを取得
// スケジュールされた時間が現在時刻以前で、ツイート内容があり、まだ投稿されていない場合
if (scheduledTime && tweetContent && new Date(scheduledTime) <= now && status !== "投稿済") {
sendTweet(tweetContent); // ツイートを送信
sheet.getRange(i + 1, 3).setValue("投稿済"); // ステータスを「投稿済み」に更新
}
}
}
function sendTweet(tweetContent) {
if (!tweetContent) {
Logger.log("No tweet content provided"); // ツイート内容がない場合のログ出力
return;
}
var service = getService(); // OAuth2サービスの取得
if (service.hasAccess()) {
var url = 'https://api.twitter.com/2/tweets'; // Twitter APIのURL
var response = UrlFetchApp.fetch(url, {
method: 'POST', // POSTリクエスト
contentType: 'application/json', // コンテンツタイプ
headers: {
Authorization: 'Bearer ' + service.getAccessToken() // 認証ヘッダー
},
muteHttpExceptions: true,
payload: JSON.stringify({ text: tweetContent }) // ツイート内容をJSON形式で送信
});
var result = JSON.parse(response.getContentText()); // レスポンスの解析
Logger.log(JSON.stringify(result, null, 2)); // レスポンスのログ出力
} else {
var authorizationUrl = service.getAuthorizationUrl(); // 認証URLの取得
Logger.log('Open the following URL and re-run the script: %s', authorizationUrl); // 認証URLのログ出力
}
}
では、細かく見ていこう。
まずはpostScheduledTweetsメソッドから。
function postScheduledTweets() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("シート1"); // スプレッドシートの取得
const rows = sheet.getDataRange().getValues(); // 全行のデータを取得
const now = new Date(); // 現在の日時を取得
// 各行に対する処理
for (let i = 1; i < rows.length; i++) {
const [scheduledTime, tweetContent, status] = rows[i]; // 各列のデータを取得
// スケジュールされた時間が現在時刻以前で、ツイート内容があり、まだ投稿されていない場合
if (scheduledTime && tweetContent && new Date(scheduledTime) <= now && status !== "投稿済") {
sendTweet(tweetContent); // ツイートを送信
sheet.getRange(i + 1, 3).setValue("投稿済"); // ステータスを「投稿済み」に更新
}
}
}
以下の部分でまずはデータを取得。
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("シート1"); // スプレッドシートの取得
const rows = sheet.getDataRange().getValues();
そのデータを格納してsendTweetメソッドを呼び出す。
ただし、投稿する条件を満たした場合飲み。
// 各行に対する処理
for (let i = 1; i < rows.length; i++) {
const [scheduledTime, tweetContent, status] = rows[i]; // 各列のデータを取得
// スケジュールされた時間が現在時刻以前で、ツイート内容があり、まだ投稿されていない場合
if (scheduledTime && tweetContent && new Date(scheduledTime) <= now && status !== "投稿済") {
sendTweet(tweetContent); // ツイートを送信
sheet.getRange(i + 1, 3).setValue("投稿済"); // ステータスを「投稿済み」に更新
}
}
sendTweetメソッド内を見てみよう。
function sendTweet(tweetContent) {
if (!tweetContent) {
Logger.log("No tweet content provided"); // ツイート内容がない場合のログ出力
return;
}
var service = getService(); // OAuth2サービスの取得
if (service.hasAccess()) {
var url = 'https://api.twitter.com/2/tweets'; // Twitter APIのURL
var response = UrlFetchApp.fetch(url, {
method: 'POST', // POSTリクエスト
contentType: 'application/json', // コンテンツタイプ
headers: {
Authorization: 'Bearer ' + service.getAccessToken() // 認証ヘッダー
},
muteHttpExceptions: true,
payload: JSON.stringify({ text: tweetContent }) // ツイート内容をJSON形式で送信
});
var result = JSON.parse(response.getContentText()); // レスポンスの解析
Logger.log(JSON.stringify(result, null, 2)); // レスポンスのログ出力
} else {
var authorizationUrl = service.getAuthorizationUrl(); // 認証URLの取得
Logger.log('Open the following URL and re-run the script: %s', authorizationUrl); // 認証URLのログ出力
}
}
まずはデータが格納されているかチェック。格納されていなければ今回の処理は行わない。
if (!tweetContent) {
Logger.log("No tweet content provided"); // ツイート内容がない場合のログ出力
return;
}
ここでOAuth2サービスが取得できるかチェック
var service = getService(); // OAuth2サービスの取得
if (service.hasAccess()) {
投稿のためのAPIを呼び出す。
var url = 'https://api.twitter.com/2/tweets'; // Twitter APIのURL
そして、投稿内容のデータはJSON形式にしてpayloadに格納。
var response = UrlFetchApp.fetch(url, {
method: 'POST', // POSTリクエスト
contentType: 'application/json', // コンテンツタイプ
headers: {
Authorization: 'Bearer ' + service.getAccessToken() // 認証ヘッダー
},
muteHttpExceptions: true,
payload: JSON.stringify({ text: tweetContent }) // ツイート内容をJSON形式で送信
});
そして、APIを呼び出した結果のデータは以下のように取得。
その結果をログに出力する。
var result = JSON.parse(response.getContentText()); // レスポンスの解析
Logger.log(JSON.stringify(result, null, 2)); // レスポンスのログ出力
ここまでで投稿のシステムはOK。次に自動投稿されるようにしよう。
そのためには「トリガー」を設定する。その設定は以下からできる。
まずは画面左にある「トリガー」を選択。

そして、右下の「トリガーを追加」ボタンを押下する。

すると、以下のポップアップが表示される。
設定内容は以下の画像を参照。
- 実行する関数は「postScheduledTweets」
- 実行するデプロイは「Head」
- イベントのソースは「時間主導型」
- 時間ベースのトリガータイプ、時間の間隔は任意。
画像のように「時間ベース」にすると「時間(hour)単位」で設定できる。

そして、新しくトリガーが登録されたことを確認。

あとはスプレッドシートの概要箇所に「投稿済み」と記入されてないものがあればX(旧Twitte)を確認すれば投稿されてることが確認できる。