aboutsummaryrefslogtreecommitdiffhomepage
path: root/scripts/post-x-events.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/post-x-events.mjs')
-rw-r--r--scripts/post-x-events.mjs55
1 files changed, 54 insertions, 1 deletions
diff --git a/scripts/post-x-events.mjs b/scripts/post-x-events.mjs
index 0f05302..64d291e 100644
--- a/scripts/post-x-events.mjs
+++ b/scripts/post-x-events.mjs
@@ -10,6 +10,7 @@
// node scripts/post-x-events.mjs --no-reply # X_REPLY_TO_ID を無視して通常のポストにする
// node scripts/post-x-events.mjs --days 10 # 10日分の予定をまとめて投稿(既定は7日分)
// node scripts/post-x-events.mjs --at 18:00 # 終了判定の時刻を指定(動作確認用)
+// node scripts/post-x-events.mjs --replace # 前回の投稿を削除してから投稿する
//
// 今日の分は、終了時刻を過ぎた予定を除く(終了時刻が読めないものは残す)。
//
@@ -63,6 +64,7 @@ const valueOf = (name) => {
const doPost = has('--post');
const force = has('--force');
const eventsOnly = has('--events-only');
+const replaceMode = has('--replace');
const dateArg = valueOf('--date');
const replyToArg = valueOf('--reply-to');
@@ -390,7 +392,7 @@ function oauthHeader({ method, url, apiKey, apiSecret, accessToken, accessSecret
.join(', ')}`;
}
-async function postTweet(text, replyTo) {
+function credentials() {
loadEnvFile();
const creds = {
apiKey: process.env.X_API_KEY,
@@ -406,6 +408,11 @@ async function postTweet(text, replyTo) {
`X の認証情報がありません(${missing.join(', ')})。リポジトリ直下の .env に設定してください。`,
);
}
+ return creds;
+}
+
+async function postTweet(text, replyTo) {
+ const creds = credentials();
const url = 'https://api.x.com/2/tweets';
const payload = { text };
@@ -425,6 +432,18 @@ async function postTweet(text, replyTo) {
return json.data?.id ?? '';
}
+/** 自分の投稿を削除する(--replace 用) */
+async function deleteTweet(id) {
+ const creds = credentials();
+ const url = `https://api.x.com/2/tweets/${id}`;
+ const res = await fetch(url, {
+ method: 'DELETE',
+ headers: { Authorization: oauthHeader({ method: 'DELETE', url, ...creds }) },
+ });
+ const body = await res.text();
+ if (!res.ok) throw new Error(`X API エラー ${res.status}: ${body}`);
+}
+
// ---------------------------------------------------------------- 認証の確認
/** 認証情報の形と、GET /2/users/me で実際に通るかを確かめる(投稿はしない) */
@@ -566,6 +585,19 @@ async function main() {
const state = readState();
const todaysPosts = postsOf(state, todayKey);
+ // --replace: 直近の前回投稿を削除してから投稿する(プロフィールに1件だけ残す)
+ const replaceTargets = (() => {
+ if (!replaceMode) return [];
+ const dates = Object.keys(state)
+ .filter((date) => date !== todayKey)
+ .sort();
+ const lastDate = dates[dates.length - 1];
+ if (!lastDate) return [];
+ return postsOf(state, lastDate)
+ .filter((post) => post.id)
+ .map((post) => ({ date: lastDate, id: post.id }));
+ })();
+
// 前回から一定日数経っていなければ投稿しない(毎日起動しても週1回に抑える)
const intervalDays = (() => {
const n = Number(process.env.X_POST_INTERVAL_DAYS);
@@ -584,6 +616,13 @@ async function main() {
? `※ 前回の投稿から ${elapsedDays.toFixed(1)} 日(間隔 ${intervalDays} 日)`
: `※ 投稿履歴なし(間隔 ${intervalDays} 日)`,
);
+ if (replaceMode) {
+ log(
+ replaceTargets.length
+ ? `※ --replace: 前回の投稿 ${replaceTargets.length} 件(${replaceTargets[0].date})を削除してから投稿します`
+ : '※ --replace: 削除する前回の投稿はありません',
+ );
+ }
return;
}
@@ -607,6 +646,20 @@ async function main() {
return;
}
+ // 前回の投稿を削除(失敗しても投稿は続行する)
+ for (const target of replaceTargets) {
+ try {
+ await deleteTweet(target.id);
+ const remaining = postsOf(state, target.date).filter((post) => post.id !== target.id);
+ if (remaining.length) state[target.date] = remaining;
+ else delete state[target.date];
+ writeState(state);
+ log(`前回の投稿を削除しました(${target.date} / id: ${target.id})`);
+ } catch (error) {
+ log(`前回の投稿の削除に失敗しました(id: ${target.id}): ${error.message}`);
+ }
+ }
+
try {
const id = await postTweet(text, replyTo);
state[todayKey] = [