-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathmain.ts
376 lines (351 loc) · 12.7 KB
/
main.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
const WRITER_REGEXP = /@[a-zA-Z0-9_-]+/g;
const TRIGGER_FUNC_NAME = "main";
type CrowiInfo = {
host: string;
pagePath: string;
token: string;
};
type traQInfo = {
host: string;
channelId: string;
logChannelId: string;
buriChannelPath: string;
reviewChannelPath: string;
webhookId: string;
webhookSecret: string;
};
type BlogRelayInfo = {
tag: string;
title: string;
startDate: string;
};
type InitResult = {
crowi: CrowiInfo;
traQ: traQInfo;
blogRelay: BlogRelayInfo;
noticeMessage: string;
};
type InitSetTriggerResult = {
year: string;
month: string;
date: string;
hours: string;
minutes: string;
};
function init(): InitResult | null {
const props = PropertiesService.getScriptProperties();
const crowiHost = props.getProperty("CROWI_HOST");
const crowiPath = props.getProperty("CROWI_PAGE_PATH");
const crowiToken = props.getProperty("CROWI_ACCESS_TOKEN");
if (crowiHost === null || crowiPath === null || crowiToken === null) {
return null;
}
const traQHost = props.getProperty("TRAQ_HOST");
const traQChannelId = props.getProperty("TRAQ_CHANNEL_ID");
const traQLogChannelId = props.getProperty("TRAQ_LOG_CHANNEL_ID");
const traQBuriChannelPath = props.getProperty("TRAQ_BURI_CHANNEL_PATH");
const traQReviewChannelPath = props.getProperty("TRAQ_REVIEW_CHANNEL_PATH");
const traQWebhookId = props.getProperty("WEBHOOK_ID");
const traQWebhookSecret = props.getProperty("WEBHOOK_SECRET");
if (
traQHost === null ||
traQChannelId === null ||
traQLogChannelId === null ||
traQBuriChannelPath === null ||
traQReviewChannelPath === null ||
traQWebhookId === null ||
traQWebhookSecret === null
) {
return null;
}
const blogRelayTag = props.getProperty("TAG");
const blogRelayTitle = props.getProperty("TITLE");
const blogRelayStartDate = props.getProperty("START_DATE");
if (blogRelayTag === null || blogRelayTitle === null || blogRelayStartDate === null) {
return null;
}
const url = `https://${crowiHost}${crowiPath}`;
const noticeMessage = `
## 注意事項
- \`${blogRelayTag}\`のタグをつけてください
- 記事の初めにブログリレー何日目の記事かを明記してください
- 記事の最後に次の日の担当者を紹介してください
- **post imageを設定して**ください
- わからないことがあれば気軽に ${traQBuriChannelPath} まで
- 記事内容の添削や相談は、気軽に ${traQReviewChannelPath} へ
- 詳細は ${url}`;
return {
crowi: {
host: crowiHost,
pagePath: crowiPath,
token: crowiToken,
},
traQ: {
host: traQHost,
channelId: traQChannelId,
logChannelId: traQLogChannelId,
buriChannelPath: traQBuriChannelPath,
reviewChannelPath: traQReviewChannelPath,
webhookId: traQWebhookId,
webhookSecret: traQWebhookSecret,
},
blogRelay: {
tag: blogRelayTag,
title: blogRelayTitle,
startDate: blogRelayStartDate,
},
noticeMessage,
};
}
function main(): void {
const v = init();
if (v === null) {
Logger.log("init failed");
return;
}
const { crowi, traQ, blogRelay, noticeMessage } = v;
const pageBody = getCrowiPageBody(crowi);
const schedules = extractSchedule(pageBody);
const dateDiff = calcDateDiff(blogRelay);
const messageHead =
dateDiff < 0
? getBeforeMessage(blogRelay.title, -dateDiff)
: getDuringMessage(blogRelay.title, dateDiff, schedules);
const res = postMessage(traQ, messageHead + noticeMessage, false);
Logger.log(res.getResponseCode());
Logger.log(messageHead + noticeMessage);
// const logMessage = extractScheduleStr(pageBody)
const logMessage = schedulesToCalendar(blogRelay, schedules);
const res2 = postMessage(traQ, logMessage, true);
Logger.log(res2.getResponseCode());
Logger.log(logMessage);
deleteAllTrigger();
}
function getCrowiPageBody({ host, pagePath, token }: CrowiInfo): string {
const encodedPath = encodeURI(pagePath);
const url = `https://${host}/_api/pages.get?access_token=${token}&path=${encodedPath}`;
const res = UrlFetchApp.fetch(url);
const payload = JSON.parse(res.getContentText());
return payload.page.revision.body as string;
}
function hmacSha1(key: string, message: string): string {
const algorithm = Utilities.MacAlgorithm.HMAC_SHA_1;
const charset = Utilities.Charset.UTF_8;
return Utilities.computeHmacSignature(algorithm, message, key, charset)
.map((v) => (v < 0 ? v + 256 : v))
.map((v) => v.toString(16).padStart(2, "0"))
.join("");
}
function postMessage(
{ host, channelId, logChannelId, webhookId, webhookSecret }: traQInfo,
content: string,
log: boolean,
): GoogleAppsScript.URL_Fetch.HTTPResponse {
const signature = hmacSha1(webhookSecret, content);
const params: GoogleAppsScript.URL_Fetch.URLFetchRequestOptions = {
method: "post",
contentType: "text/plain; charset=utf-8",
headers: {
"X-TRAQ-Signature": signature,
"X-TRAQ-Channel-Id": log ? logChannelId : channelId,
},
payload: content,
};
const url = `https://${host}/api/v3/webhooks/${webhookId}?embed=${(!log).toString()}`;
return UrlFetchApp.fetch(url, params);
}
type Schedule = {
date: string;
day: number;
writer: string;
summary: string;
};
function scheduleToString(s: Schedule): string {
const writers = Array.from(s.writer.matchAll(WRITER_REGEXP))
.map((match) => match[0])
.join(", ");
return `| ${s.date} | ${s.day} | ${writers} | ${s.summary} |`;
}
function schedulesToTable(schedules: Schedule[]): string {
return `\
| 日付 | 日目 | 担当者 | タイトル(内容) |
| :-: | :-: | :-: | :-- |
${schedules.map(scheduleToString).join("\n")}`;
}
function dateOffset(date: Date, offset: number): Date {
const dateMs = date.getTime();
const offsetMs = offset * 24 * 60 * 60 * 1000;
return new Date(dateMs + offsetMs);
}
function actualDateOfSchedule({ startDate }: BlogRelayInfo, schedule: Schedule): Date {
// UNIXタイムスタンプ
const startDateParsed = new Date(startDate);
// 経過日数のms
const offset = schedule.day - 1;
return dateOffset(startDateParsed, offset);
}
function scheduleToStringInCalendar(schedule: Schedule): string {
const writerIcons = Array.from(schedule.writer.matchAll(WRITER_REGEXP))
.map((match) => `:${match[0]}:`)
.join("");
return writerIcons;
}
function schedulesToCalendar(
blogRelayInfo: BlogRelayInfo,
schedules: Schedule[],
): string {
const weeks: Array<Array<[Date, Schedule[]]>> = [];
let i = 0;
const scheduleLength = schedules.length;
const startDate = new Date(blogRelayInfo.startDate);
const calendarStartDate = dateOffset(startDate, -startDate.getDay());
while (i < scheduleLength) {
const week: Array<[Date, Schedule[]]> = [];
const weekStartDate = dateOffset(calendarStartDate, weeks.length * 7);
for (let weekDay = 0; weekDay < 7; weekDay++) {
const day: Schedule[] = [];
const date = dateOffset(weekStartDate, weekDay);
while (
i < scheduleLength &&
actualDateOfSchedule(blogRelayInfo, schedules[i]).getDay() === weekDay
) {
day.push(schedules[i]);
i++;
}
week.push([date, day]);
}
weeks.push(week);
}
const calendarBody = weeks
.map((week) =>
week
.map((dayInfo) => {
const date = dayInfo[0];
const day = dayInfo[1];
const dateStr = `${date.getMonth() + 1}/${date.getDate()}`;
const dayStr = day
.map((schedule) => scheduleToStringInCalendar(schedule))
.join(" ");
return `**${dateStr}**${dayStr}`;
})
.join(" | "),
)
.join("\n");
return `\
:day0_sunday: | :day1_monday: | :day2_tuesday: | :day3_wednesday: | :day4_thursday: | :day5_friday: | :day6_saturday:
--- | --- | --- | --- | --- | --- | ---
${calendarBody}`;
}
function extractScheduleStr(pageBody: string): string {
const lines = pageBody.split(/\r\n|\r|\n/);
const startIndex = lines.findIndex((l: string): boolean => /^\|\s*日付.*/.test(l));
let table = "";
for (let i = startIndex; i < lines.length; ++i) {
const l = lines[i];
if (/^\s*\|.*/.test(l)) {
table += `${l}\n`;
} else {
break;
}
}
return table;
}
function extractSchedule(pageBody: string): Schedule[] {
const tableStr = extractScheduleStr(pageBody);
const lines = tableStr.split("\n").filter((l: string): boolean => l.startsWith("|"));
const table: Schedule[] = [];
for (let i = 2; i < lines.length; ++i) {
// | 日付 | 日目 | 担当者 | タイトル(内容) |
const cells = lines[i]
.split("|")
.slice(1, -1)
.map((c: string): string => c.trim());
const s: Schedule = {
date: cells[0],
day: Number.parseInt(cells[1]),
writer: cells[2],
summary: cells[3],
};
if (s.writer.length === 0) {
continue;
}
if (s.date === "同上") {
s.date = table[table.length - 1].date;
}
table.push(s);
}
return table;
}
// START_DATEとの差分を取得する
// now - date
function calcDateDiff({ startDate }: BlogRelayInfo): number {
const date = new Date(startDate);
const dateUtcTime = date.getTime() + date.getTimezoneOffset() * 60 * 1000;
const now = new Date();
const nowUtcTime = now.getTime() + now.getTimezoneOffset() * 60 * 1000;
const diff = nowUtcTime - dateUtcTime;
return Math.floor(diff / (1000 * 60 * 60 * 24));
}
// ブログリレー期間前のメッセージを取得する関数
// diff > 0
function getBeforeMessage(title: string, diff: number): string {
return `# ${title}まであと ${diff}日`;
}
// ブログリレー期間中のメッセージを取得する関数
// diff >= 0
function getDuringMessage(title: string, diff: number, schedules: Schedule[]): string {
const d = diff + 1;
const ss = schedules.filter((s: Schedule): boolean => d <= s.day && s.day <= d + 1);
if (ss.length > 0) {
return `# ${title} ${d}日目\n${schedulesToTable(ss)}`;
}
return `# ${title} ${d}日目\n担当者はいません`;
}
function InitSetTrigger(): InitSetTriggerResult | null {
const now = new Date();
const props = PropertiesService.getScriptProperties();
const setYear = now.getFullYear().toString();
const setMonth = (now.getMonth() + 1).toString().padStart(2, "0");
const setDate = now.getDate().toString().padStart(2, "0");
const setHours = props.getProperty("TRIGGER_SET_HOURS")?.padStart(2, "0");
const setMinutes = props.getProperty("TRIGGER_SET_MINUTES")?.padStart(2, "0");
if (setMinutes === undefined || setHours === undefined) {
return null;
}
return {
year: setYear,
month: setMonth,
date: setDate,
hours: setHours,
minutes: setMinutes,
};
}
// TRIGGER_FUNC_NAMEで指定した関数を特定の時間に実行する関数
function setTrigger(): void {
const v = InitSetTrigger();
if (v === null) {
Logger.log("InitSetTrigger faled");
return;
}
// トリガー登録したい時間を設定
const setTime = new Date(
`${v.year}-${v.month}-${v.date}T${v.hours}:${v.minutes}:00+09:00`,
);
// newTriggerメソッドでtriggerTestを特定日時でトリガー登録
ScriptApp.newTrigger(TRIGGER_FUNC_NAME).timeBased().at(setTime).create();
Logger.log(`made ${TRIGGER_FUNC_NAME} trigger at ${setTime.toISOString()}`);
}
// 実行し終わったTRIGGER_FUNC_NAMEのトリガーを削除する関数
function deleteAllTrigger(): void {
// GASプロジェクトに設定したトリガーをすべて取得
const triggers = ScriptApp.getProjectTriggers();
// トリガー登録のforループを実行
for (const trigger of triggers) {
// 取得したトリガーの関数が TRIGGER_FUNC_NAMEと一致したとき、deleteTriggerで削除
if (trigger.getHandlerFunction() !== TRIGGER_FUNC_NAME) {
continue;
}
ScriptApp.deleteTrigger(trigger);
Logger.log(`deleted ${TRIGGER_FUNC_NAME} trigger`);
}
}