+1
.gitignore
+1
.gitignore
···
1
+
.clasp.json
+10
appsscript.json
+10
appsscript.json
+301
code.js
+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
+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
+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>