イラストなどを投稿した自分のポストを異なる時間帯(昼や深夜など)にセルフRPして宣伝できるツールです script.google.com/macros/s/AKfycbwnbvG1hSPgYJOWz4yyH76WxJU3TEOmZIBk_3CeaImYNVn4uRpz21VlEjNPs06FojkJLQ/exec
bluesky gas
1const HOUR_LIST = [[6, 8], //Morning 2[11, 13],//Lunch 3[17, 19],//Evening 4[23, 25]]//MidNight 5const HOURS_NAME = ["朝", "昼", "夜", "深夜"]; 6const HOURS_NUM = 4;//HOUR_LIST.length() 7 8 9function 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 23function 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 37function 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 137const 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 169const 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 189const 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 209const 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 216const 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 251const 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 294const 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 299function getRandomInt(max) { 300 return Math.floor(Math.random() * max); 301}