diff options
Diffstat (limited to 'scripts')
| -rw-r--r-- | scripts/post-x-events.mjs | 55 |
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] = [ |
