イラストなどを投稿した自分のポストを異なる時間帯(昼や深夜など)にセルフRPして宣伝できるツールです script.google.com/macros/s/AKfycbwnbvG1hSPgYJOWz4yyH76WxJU3TEOmZIBk_3CeaImYNVn4uRpz21VlEjNPs06FojkJLQ/exec
bluesky gas

first commit

Lamron e5145fb8 87a3cc16

+1
.gitignore
···
··· 1 + .clasp.json
+10
appsscript.json
···
··· 1 + { 2 + "timeZone": "Asia/Tokyo", 3 + "dependencies": {}, 4 + "exceptionLogging": "STACKDRIVER", 5 + "runtimeVersion": "V8", 6 + "webapp": { 7 + "executeAs": "USER_DEPLOYING", 8 + "access": "ANYONE_ANONYMOUS" 9 + } 10 + }
+301
code.js
···
··· 1 + const HOUR_LIST = [[6, 8], //Morning 2 + [11, 13],//Lunch 3 + [17, 19],//Evening 4 + [23, 25]]//MidNight 5 + const HOURS_NAME = ["朝", "昼", "夜", "深夜"]; 6 + const HOURS_NUM = 4;//HOUR_LIST.length() 7 + 8 + 9 + function doGet(e) { 10 + const userProperties = PropertiesService.getUserProperties(); 11 + const username = userProperties.getProperty("username"); 12 + const appPassword = userProperties.getProperty("appPassword"); 13 + const timerError = userProperties.getProperty("timerError"); 14 + const template = HtmlService.createTemplateFromFile('index'); 15 + template.deployURL = ScriptApp.getService().getUrl();; 16 + template.username = username 17 + template.appPassword = appPassword; 18 + template.timerError = timerError; 19 + const htmlOutput = template.evaluate().setTitle("SkySpread - 自動化セルフRPで宣伝!"); 20 + return htmlOutput; 21 + } 22 + 23 + function doPost(e) { 24 + var template; 25 + if (e.parameter.confirm) { 26 + template = confirmTemplate(e) 27 + } 28 + // else if (e.parameter.submit) { 29 + // template = completeTemplate(e) 30 + // } 31 + 32 + template.deployURL = ScriptApp.getService().getUrl(); 33 + const htmlOutput = template.evaluate(); 34 + return htmlOutput; 35 + } 36 + 37 + function confirmTemplate(e) { 38 + const template = HtmlService.createTemplateFromFile('confirm'); 39 + const postUrl = e.parameter.postUrl 40 + 41 + template.username = e.parameter.username; 42 + template.deployURL = ScriptApp.getService().getUrl(); 43 + template.postUrl = postUrl; 44 + template.errMessage = ""; 45 + 46 + const sessionData = createSession(e.parameter.username, e.parameter.appPassword); 47 + gasLog(sessionData); 48 + if (!sessionData.did) { 49 + template.errMessage = "ログインに失敗しました: " + sessionData.message; 50 + const htmlOutput = template.evaluate(); 51 + return htmlOutput; 52 + } 53 + 54 + const userProperties = PropertiesService.getUserProperties(); 55 + var username = userProperties.getProperty("username"); 56 + var appPassword = userProperties.getProperty("appPassword"); 57 + if (e.parameter.username != username) { 58 + username = e.parameter.username; 59 + userProperties.setProperty("username", username); 60 + template.username = username; 61 + } 62 + if (e.parameter.appPassword != appPassword) { 63 + appPassword = e.parameter.appPassword; 64 + userProperties.setProperty("appPassword", appPassword); 65 + } 66 + 67 + const post = getPost(sessionData.accessJwt, sessionData.did, postUrl) 68 + if (post == null) {// (post.author.did != did) { 69 + template.errMessage = "ポストが存在しない、または違う人のポストです"; 70 + const htmlOutput = template.evaluate(); 71 + return htmlOutput; 72 + } 73 + 74 + userProperties.setProperty("sessionData", JSON.stringify(sessionData)); 75 + 76 + const repo = { uri: post.uri, cid: post.cid } 77 + // gasLog(post) 78 + const unixTimeZero = new Date(post.record.createdAt); 79 + const hour = unixTimeZero.getHours(); 80 + var minHourDiff = 24; 81 + var nearestIdx = -1; 82 + var idx = 0; 83 + for (const hours of HOUR_LIST) { 84 + const start = hours[0]; 85 + const end = hours[1]; 86 + const tmpHour = (end > 24) ? hour + 24 : hour; 87 + if (start < tmpHour && tmpHour < end) { 88 + nearestIdx = idx; 89 + break; 90 + } 91 + 92 + const diff = (tmpHour < start) ? start - tmpHour : tmpHour - end; 93 + if (diff < minHourDiff) { 94 + minHourDiff = diff; 95 + nearestIdx = idx; 96 + } 97 + 98 + idx++; 99 + } 100 + 101 + idx = (nearestIdx + 1) % HOURS_NUM; 102 + // var hourNames = []; 103 + var rpList = JSON.parse(userProperties.getProperty("rpList")); 104 + gasLog(rpList) 105 + if (rpList == null) rpList = new Array(); 106 + // gasLog(typeof rpList); 107 + // gasLog(rpList.length); 108 + 109 + var tableHtml = ` 110 + <table class="table"> 111 + <tbody> 112 + `; 113 + 114 + for (var i = 0; i < HOURS_NUM - 1; i++) { 115 + idx = (idx + 1) % HOURS_NUM 116 + const startHour = (HOUR_LIST[idx][0] + getRandomInt(2)) % 24; 117 + // hourNames.push(HOURS_NAME[idx]); 118 + tableHtml += `<tr><td>${HOURS_NAME[idx]}</td><td>${HOUR_LIST[idx][0]}時から${HOUR_LIST[idx][1]}時まで</td>` + "\n"; 119 + rpList.push({ repo: repo, hour: startHour }) 120 + } 121 + userProperties.setProperty("rpList", JSON.stringify(rpList)); 122 + tableHtml += `</tbody></table>`; 123 + template.listHtml = tableHtml; 124 + 125 + userProperties.setProperty("timerError", ""); 126 + 127 + return template; 128 + } 129 + 130 + // function completeTemplate(e) { 131 + // gasLog(e); 132 + // const template = HtmlService.createTemplateFromFile('complete'); 133 + // return template; 134 + // } 135 + 136 + //@see https://note.com/keiga/n/n527865bcf0d5 137 + const createSession = (username, appPassword) => { 138 + const url = 'https://bsky.social/xrpc/com.atproto.server.createSession'; 139 + 140 + const data = { 141 + 'identifier': username, 142 + 'password': appPassword 143 + }; 144 + 145 + const params = { 146 + 'method': 'post', 147 + 'headers': { 148 + 'Content-Type': 'application/json; charset=UTF-8', 149 + }, 150 + 'payload': JSON.stringify(data), 151 + 'muteHttpExceptions': true, 152 + }; 153 + const response = UrlFetchApp.fetch(url, params); 154 + const result = JSON.parse(response.getContentText()); 155 + 156 + if (response.getResponseCode() != 200) { 157 + gasLog(response.getContentText()) 158 + return result 159 + } 160 + const sessionData = { 161 + accessJwt: result.accessJwt, 162 + refreshJwt: result.refreshJwt, 163 + handle: result.handle, 164 + did: result.did 165 + } 166 + return sessionData; 167 + } 168 + 169 + const getPost = (accessJwt, did, postUrl) => { 170 + const url = 'https://bsky.social/xrpc/app.bsky.feed.getPosts?uris=' + convertToAtUri(did, postUrl); 171 + 172 + const params = { 173 + 'method': 'get', 174 + 'headers': { 175 + 'Authorization': `Bearer ${accessJwt}`, 176 + "Accept": "application/json" 177 + }, 178 + 'muteHttpExceptions': true, 179 + }; 180 + const response = JSON.parse(UrlFetchApp.fetch(url, params)); 181 + posts = response.posts; 182 + if (posts.length == 0) { 183 + return null; 184 + } else { 185 + return posts[0]; 186 + } 187 + } 188 + 189 + const refreshSession = (refreshJwt) => { 190 + const url = 'https://bsky.social/xrpc/com.atproto.server.refreshSession'; 191 + 192 + const params = { 193 + 'method': 'post', 194 + 'headers': { 195 + 'Authorization': `Bearer ${refreshJwt}`, 196 + }, 197 + 'muteHttpExceptions': true, 198 + }; 199 + const response = UrlFetchApp.fetch(url, params); 200 + const result = JSON.parse(response.getContentText()); 201 + 202 + if (response.getResponseCode() != 200) { 203 + gasLog(response.getContentText()) 204 + return result 205 + } 206 + return result; 207 + } 208 + 209 + const convertToAtUri = (did, postUrl) => { 210 + const idx = postUrl.lastIndexOf("/") 211 + if (idx == -1) return "" 212 + 213 + return "at://" + did + "/app.bsky.feed.post/" + postUrl.substring(idx + 1) 214 + } 215 + 216 + const repost = (sessionData, repo) => { 217 + const url = 'https://bsky.social/xrpc/com.atproto.repo.createRecord'; 218 + 219 + const record = { 220 + 'createdAt': (new Date()).toISOString(), 221 + 'subject': repo 222 + } 223 + 224 + const data = { 225 + 'repo': sessionData.did, 226 + 'collection': 'app.bsky.feed.repost', 227 + 'record': record 228 + }; 229 + 230 + const options = { 231 + 'method': 'post', 232 + 'headers': { 233 + 'Authorization': `Bearer ${sessionData.accessJwt}`, 234 + 'Content-Type': 'application/json; charset=UTF-8' 235 + }, 236 + 'payload': JSON.stringify(data), 237 + }; 238 + 239 + const response = UrlFetchApp.fetch(url, options); 240 + const result = JSON.parse(response.getContentText()) 241 + result.responseCode = response.getResponseCode(); 242 + 243 + if (response.getResponseCode() != 200) { 244 + gasLog(response.getContentText()) 245 + return result 246 + } 247 + 248 + return result; 249 + } 250 + 251 + const timerRepost = () => { 252 + //always refresh session 253 + const userProperties = PropertiesService.getUserProperties(); 254 + var sessionData = JSON.parse(userProperties.getProperty("sessionData")); 255 + gasLog(sessionData) 256 + 257 + result = refreshSession(sessionData.refreshJwt); 258 + sessionData.accessJwt = result.accessJwt; 259 + sessionData.refreshJwt = result.refreshJwt; 260 + 261 + if (!sessionData.did) { 262 + userProperties.deleteProperty("sessionData"); 263 + userProperties.deleteProperty("rpList"); 264 + 265 + userProperties.setProperty("timerError", "前回設定した自動セルフRPは失敗しました。"); 266 + return 267 + } 268 + userProperties.setProperty("sessionData", JSON.stringify(sessionData)); 269 + 270 + const nowHour = new Date().getHours(); 271 + var rpList = JSON.parse(userProperties.getProperty("rpList")); 272 + var idx = 0 273 + for (const rpInfo of rpList) { 274 + if (rpInfo.hour == nowHour) { 275 + break 276 + } 277 + idx++; 278 + } 279 + if (idx == rpList.length) { 280 + return 281 + } 282 + 283 + const repo = rpList[idx].repo; 284 + const res = repost(sessionData, repo); 285 + gasLog(res); 286 + if (res.responseCode != 200) { 287 + userProperties.setProperty("timerError", "前回設定した自動セルフRPは失敗しました:" + res); 288 + } 289 + rpList.splice(idx, 1); 290 + gasLog(rpList.length) 291 + userProperties.setProperty("rpList", JSON.stringify(rpList)); 292 + } 293 + 294 + const gasLog = (obj) => { 295 + Logger.log(JSON.stringify(obj)); 296 + } 297 + 298 + //@see https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Math/random 299 + function getRandomInt(max) { 300 + return Math.floor(Math.random() * max); 301 + }
+33
confirm.html
···
··· 1 + <!DOCTYPE html> 2 + <html> 3 + 4 + <head> 5 + <base target="_top"> 6 + <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" 7 + integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous"> 8 + </head> 9 + 10 + <body> 11 + <div class="container" style="max-width: 600px;"> 12 + <h2 class="text-center m-4">以下の時刻でセルフRPします</h2> 13 + <form class="mb-5" method="POST" action="<?= deployURL ?>"> 14 + <div class="input-group mb-3"> 15 + <span class="input-group-text" id="basic-addon1">@ 16 + <?= username ?> 17 + </span> 18 + </div> 19 + 20 + <p class="mb-3">ポストURL: 21 + <?= postUrl ?> 22 + </p> 23 + <? output._ = listHtml ?> 24 + <p class="mb-3"> 25 + <strong> 26 + <?= errMessage ?> 27 + </strong> 28 + </p> 29 + <!-- <button type="submit" class="btn btn-outline-primary" name="submit" value="true">セルフRPを設定</button> --> 30 + </div> 31 + </body> 32 + 33 + </html>
+54
index.html
···
··· 1 + <!DOCTYPE html> 2 + <html> 3 + 4 + <head> 5 + <base target="_top"> 6 + <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" 7 + integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous"> 8 + <!-- <meta property="og:url" content="https://lamrongol.github.io/gakumas_auto_calculator/" /> --> 9 + <meta property="og:type" content="website"> 10 + <meta property="og:title" content="SkySpread - 自動化セルフRPで宣伝!"> 11 + <meta property="og:description" content="イラストなどを投稿した自分のポストを異なる時間帯(昼や深夜など)にセルフRPして宣伝できるツールです"> 12 + <!-- <meta property="og:image" content="thumb.jpg" /> --> 13 + <!-- <meta name="twitter:card" content="summary"> --> 14 + <meta name="twitter:site" content="@lamrongol" /> 15 + </head> 16 + 17 + <body> 18 + <div class="container" style="max-width: 600px;"> 19 + <h2 class="text-center m-4">自動化セルフRPで宣伝!</h2> 20 + <p>自分のポストを異なる時間帯(昼や深夜など)にセルフRPして宣伝できるツールです。</p> 21 + <form class="mb-5" method="POST" action="<?= deployURL ?>"> 22 + <div class="input-group mb-3"> 23 + <span class="input-group-text" id="basic-addon1">@</span> 24 + <input type="username" class="form-control" placeholder="user-name.bsky.social" name="username" 25 + value="<?= username ?>" required> 26 + </div> 27 + <div class="input-group mb-3"> 28 + <input type="password" class="form-control" placeholder="xxxx-xxxx-xxxx-xxxx(アプリパスワード)" name="appPassword" 29 + aria-describedby="appPasswordHelp" value="<?= appPassword ?>" required> 30 + <div id="appPasswordHelp" class="form-text">※Blueskyのパスワードでは<a 31 + href="https://scrapbox.io/Bluesky/Bluesky%E3%82%92%E5%A7%8B%E3%82%81%E3%82%8B%E4%B8%8A%E3%81%A7%E3%81%93%E3%82%8C%E3%81%A0%E3%81%91%E3%81%AF%E7%9F%A5%E3%81%A3%E3%81%A6%E3%81%8A%E3%81%84%E3%81%9F%E6%96%B9%E3%81%8C%E3%81%84%E3%81%84%E3%81%93%E3%81%A8">ありません</a> 32 + </div> 33 + </div> 34 + 35 + <p class="mb-3">宣伝したいポストのURL:</p> 36 + <div class="input-group mb-3"> 37 + <input type="text" class="form-control" name="postUrl" 38 + placeholder="https://bsky.app/profile/user-name.bsky.social/post/xxxxxxxxxxx" required> 39 + </div> 40 + <button type="submit" class="btn btn-outline-primary" name="confirm" value="true">セルフRPを設定</button> 41 + <p class="mb-3"> 42 + <strong> 43 + <?= timerError ?> 44 + </strong> 45 + </p> 46 + <p> 47 + 質問・要望などあれば<a href="https://bsky.app/profile/did:plc:wwqlk2n45es2ywkwrf4dwsr2">開発者(@lamrongol)</a>にお気軽に問い合わせください。 48 + </p> 49 + <p> 50 + <a href="https://github.com/lamrongol/SkySpread">GitHub</a> 51 + </p> 52 + </body> 53 + 54 + </html>