イラストなどを投稿した自分のポストを異なる時間帯(昼や深夜など)にセルフ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}