自分用メモ書き。適宜追記していきます。指摘等あればコメントください。
やりたいこと
- GASでGoogleCalendarに登録した予定を取得してX APIでツイートする
準備
GASプロジェクトの準備
X(旧Twitter)開発者アカウントの準備
-
Developer Platformへアクセス
FreeのGet Start
をクリック - 使用したいTwitterアカウントでログイン
- 以下のような画面が出るので
Sign up for Free Account
をクリック
- アプリケーションの名前、説明、ウェブサイトなどを設定し、
Submit
をクリック
ClientID/ClientSecretの取得
- Project&Appsから作成したプロジェクトを選択し、画面上部の
Settings
をクリック
User authentication settings
のSet up
をクリック
(※添付の画像は設定済みのためEdit
になっています)
- 以下のような画面が出るので次のように入力
- App permissionsを
Read and write
に設定 - Type of Appを
Web App, Automated App or Bot
に設定 - App infoのCallback URI/Redirect URLとWebsite URLを下記のように設定
MY_SCRIPT_ID
はGASプロジェクトの準備でメモしたスクリプトIDに置き換えてくださいCallback URI/Redirect URLhttps://script.google.com/macros/d/MY_SCRIPT_ID/usercallback
Website URLhttps://script.google.com/macros/d/MY_SCRIPT_ID/
- App permissionsを
-
Save
をクリックするとClient IDとClient Secretが表示されるのでメモしておく
GASプロジェクトのスクリプトプロパティの設定
- GASのプロジェクトの
設定
を開き、スクリプトプロパティを編集
→スクリプトプロパティを追加
をクリック - プロパティに
MY_CLIENT_ID
を追加し、値にClientID/ClientSecretの取得で取得したX(旧Twitter)APIのClient IDをペーストする - プロパティに
MY_CLIENT_SECRET
を追加し、値にClientID/ClientSecretの取得で取得したX(旧Twitter)APIのClient Secretをペーストする -
スクリプトプロパティを保存
をクリックして保存する
カレンダーIDの取得
こちらの記事を参考に使用したいカレンダーのカレンダーIDを取得する
設定ファイルの作成
基本変更することがない値を別ファイルに分けて定義します。
スクリプトプロパティに保存するでも良いかも。スプレッドシートでも良いかも。
どこに保存するのが良いかは要検討。
文言はスプレッドシート、URL等はスクリプトプロパティなのかなぁ…。
定型文はお好きな文言に変更してください。
MY_CALENDAR_ID
はカレンダーIDの取得で取得したIDに置き換えてください。
/**
* 定型文
* 改行を入れたい場合は\nと入力してください
*/
const remindTommorowEvent = "明日の予定\n"
const startEventAnnounce = "まもなく開始\n"
/**
* カレンダーID
* eventCalendarID:カレンダー登録時と前日と当日開始時刻にツイート。
*/
const eventCalendarID = "MY_CALENDAR_ID"
// 以下は変更不要 ////////////////////////////////////////////////////////////////////////////////////////////////
/**
* OAuth2.0用ライブラリgithub
* https://github.com/googleworkspace/apps-script-oauth2
* サンプル
* https://github.com/googleworkspace/apps-script-oauth2/blob/main/samples/Twitter.gs
*/
const authorizationBaseUrl = 'https://twitter.com/i/oauth2/authorize';
const tokenUrl = 'https://api.twitter.com/2/oauth2/token';
/**
* ツイート投稿用エンドポイント
*/
const postUrl = "https://api.twitter.com/2/tweets";
/**
* 日時計算用
*/
const one_minute = 60 * 1000
const one_hour = 60 * one_minute
const one_day = 24 * one_hour
認証処理の実装
使用ライブラリ
OAuth2.0で認証を行う。
apps-script-oauth2を使用する。
手順
- ライブラリを導入する
GASの画面でエディタ
→ライブラリ
→ライブラリを追加
をクリック
スクリプトID
に以下を入力して検索
をクリックスクリプトID1B7FSrk5Zi6L1rSxxTDgDEUsPzlukDsi4KGuTMorsTQHhGBzBkMun4iDF
追加
をクリック
-
使用ライブラリに記載のリンクのGitHubのサンプルを参考に以下のコードを作成
Auth.gs
/** * アクセスが許可されている場合はアクセストークンを返却 * アクセスが未許可の場合は認証用URLを表示 */ const getXAPIToken = () => { const service = getOAuthService(); if (!service.hasAccess()) { const authorizationUrl = service.getAuthorizationUrl(); Logger.log('次のURLを開き、再度スクリプトを実行してください: %s', authorizationUrl); } else { const accessToken = service.getAccessToken(); return accessToken; } } /** * OAuth2Serviceオブジェクトの作成 */ const getOAuthService = () => { const serviceName = "xapi"; const properties = PropertiesService.getScriptProperties(); const clientId = properties.getProperty("MY_CLIENT_ID"); const clientSecret = properties.getProperty("MY_CLIENT_SECRET"); const scope = "tweet.read tweet.write users.read offline.access" return OAuth2.createService(serviceName) .setAuthorizationBaseUrl(authorizationBaseUrl) .setTokenUrl(tokenUrl) .setClientId(clientId) .setClientSecret(clientSecret) .setCallbackFunction('authCallback') .setPropertyStore(PropertiesService.getUserProperties()) .setScope(scope) .generateCodeVerifier() .setTokenHeaders({ 'Authorization': 'Basic ' + Utilities.base64Encode(clientId + ':' + clientSecret), 'Content-Type': 'application/x-www-form-urlencoded' }) } /** * 認証終了時に呼び出される関数 */ const authCallback = (request) => { var service = getOAuthService(); var authorized = service.handleCallback(request); if (authorized) { return HtmlService.createHtmlOutput('認証が完了しました。このウィンドウを閉じてください。'); } else { return HtmlService.createHtmlOutput('認証に失敗しました。'); } }
なんで再取得してくれないの~?!と思ったら抜けてた。(アホすぎる)
同じ悩みを持つ人に届け…。 - getXAPITokenを実行すると「次のURLを開き、再度スクリプトを実行してください:…」と出るので、表示されたURLにアクセス
- 認証を求められるので
Authorize.app
をクリック
-
認証が完了しました。このウィンドウを閉じてください。
と表示されたらOAuth2を使った認証は完了です◎
ツイート投稿処理の実装
ツイートの文章整形
ツイートする文章からHTMLタグを除去する処理を実装します。
/**
* 文章からHTMLタグを除去します
*
* @param {string} text - 対象の文章
* @return {string} - HTMLタグを除去した文章
*/
const removeHTMLTag = (text) => {
try {
text = text.replace(/<br>/g, "\n");
return text.replace(/<("[^"]*"|'[^']*'|[^'">])*>/g, '');
} catch (e) {
Logger.log("Error at removeHTMLTag:")
Logger.log(e)
}
}
X(旧Twitter)APIでツイートを投稿
ツイート内容を引数に受け取って、認証処理の実装で取得したアクセストークンを使用してツイートを行う処理を実装します。
/**
* postリクエストを行う関数
* @param {string} url - リクエストを行うURL
* @param header - リクエストヘッダ
* @param message - ポストを行うメッセージ
*/
const fetchPost = (url, headers, message) => {
const options = {
method: "post",
headers,
muteHttpExceptions: true,
payload: JSON.stringify(message),
contentType: "application/json"
};
try {
const response = UrlFetchApp.fetch(url, options);
const result = JSON.parse(response.getContentText());
Logger.log(JSON.stringify(result, null, 2));
} catch (e) {
Logger.log("Error:")
Logger.log(e)
}
}
/**
* ツイートをポストする
* @param {string} tweetText - ツイート内容
*/
const postTweet = (tweetText) => {
try {
const message = {
text: removeHTMLTag(tweetText)
}
const headers = {
Authorization: 'Bearer ' + getXAPIToken()
}
fetchPost(postUrl, headers, message);
} catch (e) {
Logger.log("Error at postTweet:")
Logger.log(e)
}
}
GoogleカレンダーAPIの有効化
Google Apps ScriptからGoogleカレンダーを読み書きするためにGoogle Calendar APIを有効化します。
カレンダー編集時に予定の「説明」をツイートする
予定編集時に実行する関数の用意
カレンダーの変更を取得のあったイベントを取得し、非公開の予定でなければ説明欄に記入されている内容をpostTweet
でツイートします
/**
* カレンダー新規登録・編集時に非公開の予定でなければポストする
* トリガー「カレンダーから」で設定
*/
const onCalendarEventAdded = (e) => {
try {
const calendarId = e.calendarId
Logger.log("onCalendarEventAdded calendarId:%s", calendarId)
const properties = PropertiesService.getScriptProperties();
// スクリプトプロパティにSYNC_TOKEN_CALENDAR_ID=XXXXというカレンダーIDに紐づいた名前でnextSyncTokenが保存される
const syncTokenProperty = "SYNC_TOKEN_CALENDAR_ID=" + calendarId
const nextSyncToken = properties.getProperty(syncTokenProperty);
const optionalArgs = (!nextSyncToken) ? {} : { syncToken: nextSyncToken }
// 変更のあったイベントを取得
const events = Calendar.Events.list(calendarId, optionalArgs);
Logger.log("onCalendarEventAdded events.items.length:%d", events.items.length)
for (event of events.items) {
Logger.log("onCalendarEventAdded event:%s", JSON.stringify(event, null, 2))
// カレンダー新規登録/編集時にstatus=confirmedになる
if (event.status == 'confirmed') {
// 非公開の予定の場合はツイートしない
if (event.visibility == "private") continue;
// カレンダーに登録されている予定の「説明」をツイートする
if (event.description) postTweet(event.description);
}
}
// スクリプトプロパティのnextSyncTokenを更新する
properties.setProperty(syncTokenProperty, events["nextSyncToken"]);
} catch (e) {
Logger.log("Error at onCalendarEventAdded:")
Logger.log(e)
}
}
トリガーの作成
-
GASで
トリガー
→トリガーを作成
をクリック -
イベントのソースを
カレンダーから
に設定するイベントのソースを
カレンダーから
に設定すると、指定したカレンダーが編集された際に自動で指定した関数を実行してくれる。 -
実行する関数に予定編集時に実行する関数の用意で作成した関数
onCalendarEventAdded
を指定する。
毎日決まった時間帯に次の日の予定一覧を取得し、予定の開始時刻にツイートする
トリガーでイベントのソースを時間主導型
にすると、日付ベースや時間ベースのトリガーをセットすることができる。
処理の流れ
- 日付ベースのトリガーで、毎日0~1時の間に「当日の20:00に指定の関数を実行する」トリガーをセットする
- 20:00になったら実行される関数で、翌日のイベントを取得する
- 取得したイベントの「説明」をツイートする
- 取得したイベントの「開始時刻」を取得し、「開始時刻」に「説明」をツイートするトリガーをセットする
トリガーの削除処理の実装
実行済みのトリガーを削除するため、指定されたトリガーIDのトリガーを削除する関数を用意する
const deleteTrigger = (triggerId) => {
try {
const triggers = ScriptApp.getProjectTriggers();
for (trigger of triggers) {
if (trigger.getUniqueId() === triggerId) {
ScriptApp.deleteTrigger(trigger);
}
}
} catch (e) {
Logger.log("Error at deleteTrigger:")
Logger.log(e)
}
}
指定された時刻に指定された内容を呟くための実装
以下の2つの関数を実装する
- postByTrigger
- セットされた時間になったら実行される
- スクリプトプロパティにトリガーIDに紐づいて保存されているツイート内容を取得する
- 取得したツイート内容を投稿する
- スクリプトプロパティからツイート内容を削除する
- 実行済みのトリガーを削除する
- setTriggerPostDesignatedTime
- 時間とツイート内容を受け取り、指定された時間にpostByTrigger関数を実行するトリガーをセットする
- セットしたトリガーのIDを取得し、IDに紐づけてスクリプトプロパティにツイート内容を保存する
/**
* セットされたトリガーの時刻になったらトリガーにセットされたツイート内容をポストする関数
*/
const postByTrigger = (e) => {
try {
const triggerId = e.triggerUid;
const property = PropertiesService.getScriptProperties();
const body = property.getProperty(triggerId);
postTweet(body);
// 後処理
property.deleteProperty(triggerId);
const triggers = ScriptApp.getProjectTriggers();
for (let i = 0; i < triggers.length; i++) {
if (triggers[i].getUniqueId() === triggerId) {
ScriptApp.deleteTrigger(triggers[i]);
}
}
} catch (e) {
Logger.log("Error at postByTrigger:")
Logger.log(e)
}
}
/**
* 指定された時刻に指定された内容を呟くためのトリガーをセットする
*/
const setTriggerPostDesignatedTime = (designatedTime, tweetDescription) => {
try {
const trigger = ScriptApp.newTrigger('postByTrigger').timeBased().at(designatedTime).create();
const triggerId = trigger.getUniqueId();
const properties = PropertiesService.getScriptProperties();
properties.setProperty(triggerId, tweetDescription);
} catch (e) {
Logger.log("Error at setTriggerPostDesignatedTime:")
Logger.log(e)
}
}
任意のカレンダーから任意の期間の予定を取得する関数の実装
指定されたカレンダーの指定された区間の予定を取得して返す関数を実装する。
/**
* 任意のカレンダーIDのカレンダーから指定区間の予定を取得して返す
* @param {string} calendarId - カレンダーID(空文字が渡された場合はデフォルトのカレンダー)
* @param {date} startTime - 検索する期間の開始日時
* @param {date} endTime - 検索する期間の終了日時
* @return {event[]} - イベントのリスト
*/
const getCalendarEvents = (calendarId, startTime, endTime) => {
try {
if (!calendarId) {
calendarId = 'primary';
}
const calendar = CalendarApp.getCalendarById(calendarId);
const events = calendar.getEvents(startTime, endTime);
return events
} catch (e) {
Logger.log("Error at getCalendarEvents:")
Logger.log(e)
}
}
翌日の予定の取得&ツイートと予定開始時刻に呟くトリガーをセットする関数の実装
以下のような処理を行う
- 実行済みトリガーの削除(本関数がトリガーによって実行されるため)
- 任意のカレンダーから任意の期間の予定を取得する関数の実装で作成した関数を用いて次の日の予定を取得
- 取得した予定の「説明」に文言を付加してツイート
- イベント開始時刻を取得
- 取得した予定の「説明」に文言を付加してイベント開始時刻にツイートしたい内容を作成
- 指定された時刻に指定された内容を呟くための実装で作成した関数を用いてイベント開始時刻にツイートを行うトリガーをセット
/**
* イベントカレンダーに登録された明日の予定をsendTweetに渡し、予定開始時刻に走るトリガーを作成する
*/
const postTommorowEvent = (e) => {
try {
const triggerId = e.triggerUid;
deleteTrigger(triggerId);
// 現在の日時を取得
const now = new Date();
// 現在の日付を基に翌日の 0:00 を計算
const nextDayStart = new Date(now);
nextDayStart.setDate(now.getDate() + 1);
nextDayStart.setHours(0, 0, 0, 0); // 時間、分、秒、ミリ秒を 0 に設定
// 翌日の 23:59 を計算
const nextDayEnd = new Date(nextDayStart);
nextDayEnd.setHours(23, 59, 59, 999); // 時間、分、秒、ミリ秒を 23:59:59.999 に設定
const events = getCalendarEvents(eventCalendarID, nextDayStart, nextDayEnd)
if (!events.length) return
for (event of events) {
const description = event.getDescription();
if (!description) continue;
const tommorowEventPost = remindTommorowEvent + `\n` + description;
postTweet(tommorowEventPost);
// イベント開始時刻に呟くトリガーをセット
const eventStartTime = event.getStartTime();
const startEventPost = startEventAnnounce + `\n` + description;
setTriggerPostDesignatedTime(eventStartTime, startEventPost);
}
} catch (e) {
Logger.log("Error at postTommorowEvent:")
Logger.log(e)
}
}
日付ベースのトリガーにより実行される関数の実装
以下の2つの関数を実装する。
- setTodaysTrigger
- 日付ベースのトリガーにより、本関数を毎日0~1時の時間帯で実行する。
- 20:00になったら
postTommorowEvent
を実行するトリガーをセットする。
- setTrigger
- 指定された日時に指定された関数を実行するトリガーを作成する。
/**
* 本日実行するトリガーを作成する
* この関数を毎日0-1時の間に呼ぶようトリガーの設定画面から設定する
*/
const setTodaysTrigger = () => {
try {
setTrigger('postTommorowEvent', 0, 20, 00);
} catch (e) {
Logger.log("Error at setTodaysTrigger:")
Logger.log(e)
}
}
const setTrigger = (func, day, hour, min) => {
try {
//Dateオブジェクトで実行した時間を取得
const time = new Date();
time.setHours(hour);
time.setMinutes(min);
ScriptApp.newTrigger(func).timeBased().at(time).create();
} catch (e) {
Logger.log("Error at setTrigger:")
Logger.log(e)
}
}
日付ベースのトリガーをセットする
日付ベースのトリガーにより実行される関数の実装で作成した関数を毎日0~1時の間で実行されるようトリガーを以下のようにセットする
まとめ
Googleカレンダーに登録された予定から自動でツイートを行えるようにしました。
具体的には行ったことは以下です。
- カレンダー編集時に予定の「説明」を呟く
- 毎日20:00に翌日0:00-23:59の予定一覧を取得し、予定の「説明」に文言を付加して呟く
- 毎日20:00に翌日0:00-23:59の予定一覧を取得し、予定の開始時刻に予定の「説明」に文言を付加して呟くトリガーをセットする
参考にさせていただいたリンク