イラストなどを投稿した自分のポストを異なる時間帯(昼や深夜など)にセルフRPして宣伝できるツールです script.google.com/macros/s/AKfycbwnbvG1hSPgYJOWz4yyH76WxJU3TEOmZIBk_3CeaImYNVn4uRpz21VlEjNPs06FojkJLQ/exec
bluesky gas
at main 12 kB view raw
1/** 2 * @OnlyCurrentDoc 3 */ 4 5const HOUR_LIST = [[6, 8], //Morning 6[11, 13],//Lunch 7[17, 19],//Evening 8[23, 25]]//MidNight 9const HOURS_NAME_JA = ["朝", "昼", "夜", "深夜"]; 10const HOURS_NAME_EN = ["Morning", "daytime", "evening", "midnight"]; 11const HOURS_NUM = 4;//HOUR_LIST.length() 12 13function doGet(e) { 14 const template = HtmlService.createTemplateFromFile('index'); 15 16 if (Session.getActiveUserLocale().startsWith("ja")) { 17 template.title = "自動化セルフRPで宣伝!"; 18 template.description = "自分のポストを異なる時間帯(昼や深夜など)にセルフRPして宣伝できるツールです。"; 19 template.appPassword = "アプリパスワード"; 20 template.appPasswordDescription = `※Blueskyのパスワードでは<a 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>` 21 template.postUrlDescription = "宣伝したいポストのURL" 22 template.setSelfRP = "セルフRPを設定" 23 template.support = `質問・要望などあれば<a href="https://bsky.app/profile/did:plc:wwqlk2n45es2ywkwrf4dwsr2">開発者(@lamrongol)</a>にお気軽に問い合わせください。` 24 } else { 25 template.title = "Spread your posts by automated self repost!"; 26 template.description = "This tool reposts your post(by your own account) at different times of day(such as daytime or midnight)"; 27 template.appPassword = "App Password"; 28 template.appPasswordDescription = `※This is NOT Bluesky password.` 29 template.postUrlDescription = "Post url you want to spread" 30 template.setSelfRP = "Set self repost" 31 template.support = `If you have question or request, please tell <a href="https://bsky.app/profile/did:plc:wwqlk2n45es2ywkwrf4dwsr2">developer(@lamrongol)</a>` 32 } 33 34 const userProperties = PropertiesService.getUserProperties(); 35 const username = userProperties.getProperty("username"); 36 const appPassword = userProperties.getProperty("appPassword"); 37 const timerError = userProperties.getProperty("timerError"); 38 template.deployURL = ScriptApp.getService().getUrl(); 39 template.username = username; 40 template.appPassword = appPassword; 41 template.timerError = timerError; 42 const htmlOutput = template.evaluate() 43 .setTitle("SkySpread - " + template.title) 44 // .addMetaTag("og:type", "website") 45 // .addMetaTag("og:title", template.title) 46 // .addMetaTag("og:description", template.description); 47 48 return htmlOutput; 49} 50 51function doPost(e) { 52 var template; 53 if (e.parameter.confirm) { 54 template = confirmTemplate(e) 55 } 56 // else if (e.parameter.submit) { 57 // template = completeTemplate(e) 58 // } 59 60 template.deployURL = ScriptApp.getService().getUrl(); 61 const htmlOutput = template.evaluate(); 62 return htmlOutput; 63} 64 65function resetData() { 66 const userProperties = PropertiesService.getUserProperties(); 67 userProperties.deleteAllProperties() 68} 69 70function confirmTemplate(e) { 71 const isJa = Session.getActiveUserLocale().startsWith("ja"); 72 73 const template = HtmlService.createTemplateFromFile('confirm'); 74 const postUrl = e.parameter.postUrl 75 76 template.username = e.parameter.username; 77 template.deployURL = ScriptApp.getService().getUrl(); 78 template.postUrl = postUrl; 79 if (isJa) { 80 template.message = "以下の時刻でセルフRPします" 81 } else { 82 template.message = "Repost by your own account at following time" 83 } 84 template.errMessage = ""; 85 86 const sessionData = createSession(e.parameter.username, e.parameter.appPassword); 87 gasLog(sessionData); 88 if (!sessionData.did) { 89 if (isJa) { 90 template.errMessage = "ログインに失敗しました: " + sessionData.message; 91 } else { 92 template.errMessage = "login failed: " + sessionData.message; 93 } 94 const htmlOutput = template.evaluate(); 95 return htmlOutput; 96 } 97 98 const userProperties = PropertiesService.getUserProperties(); 99 var username = userProperties.getProperty("username"); 100 var appPassword = userProperties.getProperty("appPassword"); 101 if (e.parameter.username != username) { 102 username = e.parameter.username; 103 userProperties.setProperty("username", username); 104 template.username = username; 105 } 106 if (e.parameter.appPassword != appPassword) { 107 appPassword = e.parameter.appPassword; 108 userProperties.setProperty("appPassword", appPassword); 109 } 110 111 const post = getPost(sessionData.accessJwt, sessionData.did, postUrl) 112 if (post == null) {// (post.author.did != did) { 113 if (isJa) { 114 template.errMessage = "ポストが存在しない、または違う人のポストです"; 115 } else { 116 template.errMessage = "Post doesn't exist, or it is not yours"; 117 } 118 const htmlOutput = template.evaluate(); 119 return htmlOutput; 120 } 121 122 userProperties.setProperty("sessionData", JSON.stringify(sessionData)); 123 124 const repo = { uri: post.uri, cid: post.cid } 125 // gasLog(post) 126 const unixTimeZero = new Date(post.record.createdAt); 127 128 const hour = getLocalHour(unixTimeZero); 129 var minHourDiff = 24; 130 var nearestIdx = -1; 131 var idx = 0; 132 for (const hours of HOUR_LIST) { 133 const start = hours[0]; 134 const end = hours[1]; 135 const tmpHour = (end > 24) ? hour + 24 : hour; 136 if (start < tmpHour && tmpHour < end) { 137 nearestIdx = idx; 138 break; 139 } 140 141 const diff = (tmpHour < start) ? start - tmpHour : tmpHour - end; 142 if (diff < minHourDiff) { 143 minHourDiff = diff; 144 nearestIdx = idx; 145 } 146 147 idx++; 148 } 149 150 idx = nearestIdx; 151 // var hourNames = []; 152 var rpList = JSON.parse(userProperties.getProperty("rpList")); 153 gasLog(rpList) 154 if (rpList == null) rpList = new Array(); 155 // gasLog(typeof rpList); 156 // gasLog(rpList.length); 157 158 var tableHtml = ` 159 <table class="table"> 160 <tbody> 161`; 162 163 for (var i = 0; i < HOURS_NUM - 1; i++) { 164 idx = (idx + 1) % HOURS_NUM 165 const startHour = (HOUR_LIST[idx][0] + getRandomInt(2)) % 24; 166 // hourNames.push(HOURS_NAME[idx]); 167 if (isJa) { 168 tableHtml += `<tr><td>${HOURS_NAME_JA[idx]}</td><td>${HOUR_LIST[idx][0]}時から${HOUR_LIST[idx][1]}時まで</td>` + "\n"; 169 } else { 170 tableHtml += `<tr><td>${HOURS_NAME_EN[idx]}</td><td>${HOUR_LIST[idx][0]}:00~${HOUR_LIST[idx][1]}:00</td>` + "\n"; 171 } 172 rpList.push({ repo: repo, hour: startHour }) 173 } 174 userProperties.setProperty("rpList", JSON.stringify(rpList)); 175 tableHtml += `</tbody></table>`; 176 template.listHtml = tableHtml; 177 178 userProperties.setProperty("timerError", ""); 179 180 return template; 181} 182 183// function completeTemplate(e) { 184// gasLog(e); 185// const template = HtmlService.createTemplateFromFile('complete'); 186// return template; 187// } 188 189//@see https://note.com/keiga/n/n527865bcf0d5 190const createSession = (username, appPassword) => { 191 const url = 'https://bsky.social/xrpc/com.atproto.server.createSession'; 192 193 const data = { 194 'identifier': username, 195 'password': appPassword 196 }; 197 198 const params = { 199 'method': 'post', 200 'headers': { 201 'Content-Type': 'application/json; charset=UTF-8', 202 }, 203 'payload': JSON.stringify(data), 204 'muteHttpExceptions': true, 205 }; 206 const response = UrlFetchApp.fetch(url, params); 207 const result = JSON.parse(response.getContentText()); 208 209 if (response.getResponseCode() != 200) { 210 gasLog(response.getContentText()) 211 return result 212 } 213 const sessionData = { 214 accessJwt: result.accessJwt, 215 refreshJwt: result.refreshJwt, 216 handle: result.handle, 217 did: result.did 218 } 219 return sessionData; 220} 221 222const getPost = (accessJwt, did, postUrl) => { 223 const url = 'https://bsky.social/xrpc/app.bsky.feed.getPosts?uris=' + convertToAtUri(did, postUrl); 224 225 const params = { 226 'method': 'get', 227 'headers': { 228 'Authorization': `Bearer ${accessJwt}`, 229 "Accept": "application/json" 230 }, 231 'muteHttpExceptions': true, 232 }; 233 const response = JSON.parse(UrlFetchApp.fetch(url, params)); 234 posts = response.posts; 235 if (posts.length == 0) { 236 return null; 237 } else { 238 return posts[0]; 239 } 240} 241 242const refreshSession = (refreshJwt) => { 243 const url = 'https://bsky.social/xrpc/com.atproto.server.refreshSession'; 244 245 const params = { 246 'method': 'post', 247 'headers': { 248 'Authorization': `Bearer ${refreshJwt}`, 249 }, 250 'muteHttpExceptions': true, 251 }; 252 const response = UrlFetchApp.fetch(url, params); 253 const result = JSON.parse(response.getContentText()); 254 255 if (response.getResponseCode() != 200) { 256 gasLog(response.getContentText()) 257 return result 258 } 259 return result; 260} 261 262const convertToAtUri = (did, postUrl) => { 263 const idx = postUrl.lastIndexOf("/") 264 if (idx == -1) return "" 265 266 return "at://" + did + "/app.bsky.feed.post/" + postUrl.substring(idx + 1) 267} 268 269const repost = (sessionData, repo) => { 270 const url = 'https://bsky.social/xrpc/com.atproto.repo.createRecord'; 271 272 const record = { 273 'createdAt': (new Date()).toISOString(), 274 'subject': repo 275 } 276 277 const data = { 278 'repo': sessionData.did, 279 'collection': 'app.bsky.feed.repost', 280 'record': record 281 }; 282 283 const options = { 284 'method': 'post', 285 'headers': { 286 'Authorization': `Bearer ${sessionData.accessJwt}`, 287 'Content-Type': 'application/json; charset=UTF-8' 288 }, 289 'payload': JSON.stringify(data), 290 }; 291 292 const response = UrlFetchApp.fetch(url, options); 293 const result = JSON.parse(response.getContentText()) 294 result.responseCode = response.getResponseCode(); 295 296 if (response.getResponseCode() != 200) { 297 gasLog(response.getContentText()) 298 return result 299 } 300 301 return result; 302} 303 304const timerRepost = () => { 305 //always refresh session 306 const userProperties = PropertiesService.getUserProperties(); 307 var sessionData = JSON.parse(userProperties.getProperty("sessionData")); 308 gasLog(sessionData) 309 310 result = refreshSession(sessionData.refreshJwt); 311 sessionData.accessJwt = result.accessJwt; 312 sessionData.refreshJwt = result.refreshJwt; 313 314 if (!sessionData.did) { 315 userProperties.deleteProperty("sessionData"); 316 userProperties.deleteProperty("rpList"); 317 318 userProperties.setProperty("timerError", "前回設定した自動セルフRPは失敗しました。"); 319 return 320 } 321 userProperties.setProperty("sessionData", JSON.stringify(sessionData)); 322 323 const nowHour = getLocalHour(new Date()); 324 var rpList = JSON.parse(userProperties.getProperty("rpList")); 325 var idx = 0 326 for (const rpInfo of rpList) { 327 if (rpInfo.hour == nowHour) { 328 break 329 } 330 idx++; 331 } 332 if (idx == rpList.length) { 333 return 334 } 335 336 const repo = rpList[idx].repo; 337 const res = repost(sessionData, repo); 338 gasLog(res); 339 if (res.responseCode != 200) { 340 userProperties.setProperty("timerError", "前回設定した自動セルフRPは失敗しました:" + res); 341 } 342 rpList.splice(idx, 1); 343 gasLog(rpList.length) 344 userProperties.setProperty("rpList", JSON.stringify(rpList)); 345} 346 347const getLocalHour = (date) => { 348 //https://stackoverflow.com/a/74683660/3809427 349 const offset = Number(Intl.DateTimeFormat("ia", { 350 timeZoneName: "shortOffset", 351 //https://stackoverflow.com/a/24764307/3809427 352 timeZone: CalendarApp.getDefaultCalendar().getTimeZone() 353 }) 354 .formatToParts() 355 .find((i) => i.type === "timeZoneName").value // => "GMT+/-hh:mm" 356 .slice(3)); 357 358 return (date.getUTCHours() + offset + 24) % 24; 359} 360 361const gasLog = (obj) => { 362 Logger.log(JSON.stringify(obj)); 363} 364 365//@see https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Math/random 366function getRandomInt(max) { 367 return Math.floor(Math.random() * max); 368}