-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathWebClapModoki-GAS_v11.txt
1575 lines (1333 loc) · 80.2 KB
/
WebClapModoki-GAS_v11.txt
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
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
//===========================================================================================
// WebClapModoki-GAS_v11
//===========================================================================================
//
// 【作成者】
// dullNeko (https://github.com/dullNeko/WebClapModoki-GAS)
//
//===========================================================================================
/*
【更新履歴】
△ … 機能追加など
※ … コードの整理・修正・バグ対応など
▼ … 機能の削除など
2023/01/17 v11 : メール通知機能の試験実装、
並びに SPREADSHEET_ID をプロパティサービスから読む記述を追加。
ただし、SPREADSHEET_ID をプロパティサービスから読みこむ処理に切り替える場合は、
●手動での key: SPREADSHEET_ID のプロパティサービスへの登録を行った上で、
●並びに記述のコメントアウトの切り替え、すなわち
const SPREADSHEET_ID = "***";
//const SPREADSHEET_ID = PropertiesService.getScriptProperties().getProperty('SPREADSHEET_ID');
から
//const SPREADSHEET_ID = "***";
const SPREADSHEET_ID = PropertiesService.getScriptProperties().getProperty('SPREADSHEET_ID');
への書き換えを行う必要があります。
△ checkPropertyValueAll(key) 関数の実装。
△ initPropertyValues() 関数の実装。
△ sendMailAlert(count) 関数の実装。
△ dailyCheckTotalCounts_v2() 関数の実装。
メール通知機能を実際に利用するには、
●プロパティ サービス の設定(EMAIL_ADDRESS, ROWS_COUNT の2つ)
●トリガー機能による定期的な関数の実行(dailyCheckTotalCounts_v2 の実行)
●Gmailへのアクセス許可の付与(「Gmailアカウントの操作」「本人に代わってのメール送信」の2権限)
が必要です。
具体的な手順は、下の (1)-(5) を参照くださいまし。
前回実行時の総行数を記録した値(前回記録値)と、今回実行時の総行数(今回取得値)を比較し、
前回記録値 < 今回取得値 の場合のみ、メールで通知を送付します。
GAS の Gmail 送信には 100通/日 の制限があるので、一日一回程度の頻度がおススメです。
メール通知機能を利用するために、
追加のセットアップ手順として、
(1) initPropertyValues() 関数を実行
(2) このエディタ画面の←にある歯車アイコン(プロジェクトの設定)を開き、
その画面の一番↓にある スクリプト プロパティ 内の EMAIL_ADDRESS の値を、
通知先(送信先)にしたいメールアドレスに書き換える
(※この時、「スクリプトプロパティの編集」→「値の書換」→「スクリプトプロパティの保存」のクリックを忘れずに!)
(3) sendMailAlert(count) 関数をエディタから実行してみて、
「新着 TEST 件」のメールが届くかチェック
(4) dailyCheckTotalCounts_v2() 関数をエディタから実行してみて、
「新着 n 件」のメールが届くかチェック
(※コメント数が0の場合は何も届きません、実行時エラーが無ければOKです)
(5) dailyCheckTotalCounts_v2() 関数を適当な時間(例えば毎日0時)に実行するように、
このエディタ画面の←にある時計アイコン(トリガー)からトリガ条件を設定しておく
の5ステップが必要です。
現状 dullNeko の技術力不足のため「新着件数のみの通知」です。
「新着のコメント内容を本文に入れる」ところまではいけませんでした…。
2022/12/27 v10 : 送信データ形式を「(送信元);(送信内容);(メモ)」に限定する制限を撤廃、同時に周辺処理変更。
※ 「◆ 加工し終わった後の配列から、各パラメータを取り出す処理」において、
配列要素数チェックを廃止し、代わりに undefined のデータを .filter メソッドで消すように変更
※ 「◆ コメントからNGワードを除去する処理」についても、
上記変更に伴い (送信内容) (=comment) が空になるパターンが生じたので対処
今の所、(送信元)(=sender)が空であっても受け付ける(左端に時刻だけ記録する)状態になっています。
2022/12/25 v9 : 「◆ 加工し終わった後の配列から、各パラメータを取り出す処理」内の return の記述ミス修正。
※ return "500"; の箇所を return; に修正。(修正漏れでした)
2022/12/24 v8 : 導入がスムーズに行くよう、v7を再調整。
※ 初期設定を簡単にするため、
Googleスプレッドシート(のテンプレート)のコピーを持ってきて、
その 'id' をこのスクリプト内の
const SPREADSHEET_ID = 'XXX...XXX';
に入れれば、
即 doPostTest() 関数でのテストを通過し、Webアプリとしてのデプロイまでできるように整理。
※ その一環として、 const DEBUG_DISABLE = 1; を既定値に変更。
問題があった時に使うだけで十分ですし、
DEBUG_DISABLE = 0 (=デバック関数によるセル参照・書込を有効) にした状態だと、
1回あたりの処理時間が 約2秒/回 から 約10秒/回 に跳ね上がる上、
50000回/日 のセル参照制限も浪費してしまうので。
2022/12/21 v7 : 公開用に調整した最初のバージョン。
△ 予め設定した("const SHEET_NAME_NGWL" で定義された 'NG_words_list' シートに入力された)
NGワード集を用いて、コメント中のNGワードを弾く関数
● checkContentTextByNGWordsList(raw_content, replace_str, del_mode)
を実装。
△ MastodonへのAPI経由でのトゥート関数
● tootByMastodonAPI(toot_content)
を試験実装するとともに、
セーフティとしてプロパティサービスを利用しないと動作しないように調整。
2022/12/15 v1 : Web拍手代替物がGASで組めるかのテスト版。
とりあえず動作確認できたレベル。
*/
//===========================================================================================
//===========================================================================================
// ★ 参考資料
//===========================================================================================
/*
覚えておいた方が良いページなど。
「直接コードを引用した」「大いに参考にさせて頂いたページ」等は
コード周辺に配置して明示するようにしています。
*/
//=================================================================================
// ★ GAS全体の制限について
//=================================================================================
/*
■ Google サービスの割り当て | Apps Script | Google Developers
https://developers.google.com/apps-script/guides/services/quotas
GASは基本的に無償で使用できるサービスですが、
代わりに、上記URLのページの通り「実行可能量」の制限があります。
> Apps Script サービスには、1 日あたりの割り当てと、一部の機能に対する制限があります。
> 割り当てまたは上限を超えると、スクリプトが例外をスローし、実行が停止します。
伺かのゴーストさんから送信される程度、かつ、本スクリプトの処理量程度なら、
1) スクリプト全体の実行時間が 6分/実行 を超える
2) カスタム関数(自分で実装した関数)の実行時間が 30秒/実行 を超える
3) スクリプト実行時間の合計が 90分/日 を超える
4) スクリプトが 20ユーザ以上に(doPostトリガーで)実行される
を超えるまでは行かない、と想定していますが…
過去の経験では、
少し凝った処理を実装した場合 1) 2) の制限に引っかかることが多かったです。
また、
3)のスクリプト実行時間の合計 90分/日 = 5400秒/日 も…意外に引っかかりやすいかもしれません。
doPostの処理が 2秒/回 で終わるとしても 2700回/日 で引っかかります。
うっかりデバッグ用関数を有効化した状態 (DEBUG_DISABLE = 0) でデプロイした場合、
大体 10秒/回 位になるので 540回/日 程度のPOSTがあったらアウトです。
私が方法を知らないだけかもしれませんが、
「誰からもメッセージを受け付ける」設定だと、
下記の制約ゆえ【悪意を持って DoS 攻撃をされた場合】は…厳しい感じです…
もし対処法をご存じであれば、ご教示頂ければ幸いです。
●GAS を Web App として公開し、"誰でも"実行できるようにして、
かつ、自身の Google Drive 内にあるスプレッドシートに受信した内容を書き込む場合、
(=今回のように、WEB拍手的に使って記録を取る場合)
必然的に「スクリプトの実行者」を「自分(GASを設置し公開した人のアカウント)の権限」で動作するように設定しなければならない。
●上記の設定で GAS を動作させる場合、doPost の呼び出し元の情報は一切取得できない。
「Googleアカウントでの認証を行う」ようにすれば、アカウントに紐づく情報を参照することは可能(らしいの)ですが…
「Web拍手と同等の手軽さで、誰からもメッセージを受け付ける」をコンセプトとしている以上、この方法は取れません。
●そもそも「doPostのトリガー起動自体をさせない」ようにする設定が見当たりません。
POSTメソッドによるHTTPリクエストがあった時点で、doPost関数が呼ばれることを止める手段は無さそうです。
「見たくないコメントを、NGワード置換機能で記録させないようにする」程度は対応してみましたが…
「特定の送信元からのPOSTリクエストに対して、doPostトリガー自体が起動しないようにする」は…私の能力では実現不可能でした。
【Tipsっぽいもの】
ご自身で色々機能を付加したり、改造を加えたりする際には…例えば、
■【GASの壁】実行時間6分の制限を回避するための対策とは | ワードプレステーマTCD
https://tcd-theme.com/2021/05/gas.html
で言及されているように、
「関数の呼び出しを少なくするために、できる限り配列を活用する」
程度は、心がけた方が良いかもしれません。
特に「セルから値を取得・書込を行う関数」の使用上限は最大 50,000回/日 なので、
セルを一つ一つ参照・書込するようなスクリプトを書くと、割と簡単に到達してしまいます。
Gmailを利用してメールを送信する場合も、
(無料アカウントでは) 100件/日 と上限が少な目なので、ご注意ください。
*/
//=================================================================================
// ★ return; で返ってくるレスポンスと、その周辺のメモ
//=================================================================================
/*
戻り値無しの return; で doPost 関数を終わらせると、下記のHTMLが返ってきます。
タイトルこそ「エラー」ですが、
通信そのものは成功している(HTML形式のデータが返ってきている)ため、
SSP側では通信失敗と判断されずに済んでいる…っぽいです。
(SSP側でレスポンスをどう処理しているのか、詳しい所が分かってないので…間違ってたらすみません…)
今の所、SSP側でレスポンスをどうこうする必要性が感じられなかったため、
return; で終了させる実装にしてあります。
=================================================================================
<!DOCTYPE html>
<html>
<head>
<link rel="shortcut icon" href="//ssl.gstatic.com/docs/script/images/favicon.ico">
<title>エラー</title>
<style type="text/css" nonce="DoNmcCHV1-Lto4bGtBjRDQ">
body {
background-color: #fff;
margin: 0;
padding: 0;
}
.errorMessage {
font-family: Arial, sans-serif;
font-size: 12pt;
font-weight: bold;
line-height: 150%;
padding-top: 25px;
}
</style>
</head>
<body style="margin:20px">
<div>
<img alt="Google Apps Script" src="//ssl.gstatic.com/docs/script/images/logo.png">
</div>
<div style="text-align:center;font-family:monospace;margin:50px auto 0;max-width:600px">スクリプトが完了しましたが、何も返されませんでした。</div>
</body>
</html>
=================================================================================
【補足情報】
試した限りでは、下手に
return ContentService.createTextOutput("200");
等と記述すると、
SSPの \![execute,http-POST,…] からは 405 で送信失敗となり、
master/varフォルダへの受信データ保存も行われませんでした。
GAS側の仕様上、ContentService を使った場合は
script.google.com → script.googleusercontent.com へのリダイレクトが発生するみたいです。
多分このあたりで引っかかっている…のだと思いますが…
前述の通り、レスポンスを利用する場面が思いつかなかったので、これ以上は検証していません。
■ Content Service | Apps Script | Google Developers
https://developers.google.com/apps-script/guides/content?hl=en
> Redirects
> For security reasons, content returned by the Content service isn't served from script.google.com,
> but instead redirected to a one-time URL at script.googleusercontent.com.
> This means that if you use the Content service to return data to another application,
> you must ensure that the HTTP client is configured to follow redirects.
> For example, in the cURL command line utility, add the flag -L.
> Check the documentation for your HTTP client for more information on how to enable this behavior.
=================================================================================
Status : 302 Found
Location : https://script.googleusercontent.com/macros/echo?user_content_key=r0p52JSTInaDuY9Dlbgw9Br2e9iNAJLHUV_DbKeLvHMcNRr70MrSwOqrzAdoqCTc5Qx4Zy_wAN8VE0Qv2CCyW-ypzfM_hZzQm5_BxDlH2jW0nuo2oDemN9CCS2h10ox_1xSncGQajx_ryfhECjZEnHJkIlr-U6_ovsASZJecurTObhGtIArhQHC76WLpv5T0gT431s0X5eyeDjwhUryF42cfmhYoKVhbPyk-Ze9WsPAleLzsN86EjA&lib=MdONwa6ZrRt9rOiRVrwu8CA7Sf_dSFFzI
Status Code: 200 OK
access-control-allow-origin: *
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"
cache-control: no-cache, no-store, max-age=0, must-revalidate
content-encoding: gzip
content-security-policy: frame-ancestors 'self'
content-type: text/plain; charset=utf-8
date: Tue, 20 Dec 2022 15:59:15 GMT
expires: Mon, 01 Jan 1990 00:00:00 GMT
pragma: no-cache
server: GSE
vary: Sec-Fetch-Dest, Sec-Fetch-Mode, Sec-Fetch-Site
x-content-type-options: nosniff
x-firefox-http3: h3
x-frame-options: SAMEORIGIN
x-xss-protection: 1; mode=block
=================================================================================
【補足情報2】
HTMLが返却されるならOK…ならこれは?と思って実行してみたもの。
return HtmlService.createHtmlOutput('<b>Thanks!</b>');
…確かにHTML形式のデータが返されるので、SSP側でエラーは出ないのですが…
本スクリプトの中身(関数名など)まで一緒に吐き出されるため、
利用が難しそうで断念しました…。
*/
//=================================================================================
// ★ 機密情報(APIキー等)を保管するプロパティサービスのススメ
//=================================================================================
/*
本コードでは「初心者への分かりやすさ」を優先し、
本来 "機密情報" として扱わねばならない情報(例:スプレッドシートを特定する 'id')について、
ハードコーディング(ソースコード内に直接書き込んで埋め込む)する手法を取りました…が、
当然ながら、セキュリティ的にも、管理の煩雑さ的にも、好ましい手法ではありません。
※dullNekoは、テストコードのコピペ時に置換するのを忘れ、
そういう大事な "機密情報" をうっかり人に晒してしまうという、
洒落にならないミスをしたことがあります…
まだ公開範囲が組織内で、かつ即座に指摘してくれた方がいらっしゃったため、
外部に漏洩する前に即座に変更&前データの無効化を確認して事なきを得ました…
…始末書だけで済んで良かったです…ほんとに…
※なお、本プロジェクトの
WebClapModoki_GAS_v7.txt と dic_wcm.txt の公開時にも、
自分のスプレッドシートのIDやWebアプリのURLを大公開するやらかしをしました。
(リポジトリが一回リセットされて消えたのはそのせいです)
まるで成長していない…
…えーと、そういう悲しく辛い事態を招く恐れがありますので、
ハードコーディングは基本的におすすめしません。
で、本題ですが、
そういう "機密にしておきたい情報" を、
ハードコーディングを回避しつつ保管するための機能として、
GASには「プロパティサービス」が実装されています。
【注意】
2022年12月現在、下記 Qiita 記事で触れられている
スクリプトプロパティ・ユーザープロパティの区分は Deprecated(非推奨) に指定されており、
また、一時期無くなっていた「新GUIでの管理操作」も復活しています。
■ Properties Service | Apps Script | Google Developers
https://developers.google.com/apps-script/reference/properties?hl=en
■ 【GAS】プロパティサービスについてまとめる - Qiita
https://qiita.com/chii-08/items/c8bb24c1141eb6ede83e
■ GASのProperties Serviceを使ってスクリプトのプロパティを読み書きをする - Qiita
https://qiita.com/unsoluble_sugar/items/ec5c935c4bfc2e06b246
2022年12月現在の仕様としては、
「プロパティの取得時に用いる関数が変われば、得た値のアクセス権(共有範囲)も変わる」仕様になっているようです。
先の例(アクセストークンやスプレッドシートのIDなど)については getScriptProperties() が最適…と思います。
getScriptProperties() -> アプリ全体の構成データ(デベロッパーの外部データベースのユーザー名やパスワードなど)
getUserProperties() -> ユーザー固有の設定(指標やヤードポンド単位など)
getDocumentProperties() -> ドキュメント固有のデータ(埋め込みグラフのソース URL など)
■ プロパティ サービス | Apps Script | Google Developers
https://developers.google.com/apps-script/guides/properties
【GUI での設定・管理方法】
GUI で管理する場合、画面左の
[歯車アイコン] -> [プロジェクトの設定] -> [スクリプト プロパティ]
から、キー・値の追加・変更・削除ができます。
「キーと値は、いずれも "文字列" に変換される」という制限がありますが、
保管すべき情報は概ね文字列で扱えるものでしょうから、問題になることは少ないと思います。
【使い方】
例えば、Mastodon の【アクセストークン】として、
[key : API_KEY_MASTODON]
[value : XXXXXXXXXXXXXXXXXXXX]
を上記 GUI から設定し、スクリプト中で参照したい場合、
const API_KEY_MASTODON = PropertiesService.getScriptProperties().getProperty("API_KEY_MASTODON");
等とすれば読み出せました。
*/
//============================================================================================
// ★ グローバル変数・定数群
//============================================================================================
//======================================
// ● SPREADSHEET_ID
//======================================
/*
受け取ったデータを書き込む対象スプレッドシートを 'id' で指定する用の定数。
【何を書けばいいの?】
スプレッドシートを開いた時のURLを見て、
https://docs.google.com/SPREADSHEETs/d/XXX...XXX/edit
↑の XXX...XXX の部分がここで指定すべき 'id' になります。
id を間違えていると、
Exception: Unexpected error while getting the method or property openById on object SpreadsheetApp.
が出て止まります。
余談ですが、スプレッドシートの名前を変更しても、
作成時に付与された "id" は不変です。
「別のスプレッドシートには、必ず別の "id" が割り振られる」ので、
記録先を変えたい場合は、ここの変更を忘れないようにしてくださいまし。
なお、記録対象のスプレッドシートならびに本スクリプトの共有設定は「制限付き」でOKです。
(=記録対象のスプレッドシート、このGAS本体(コード.gs)を公開する必要はありません)
ただし、本スクリプトのデプロイ時、
「アクセスできるユーザー」を「全員」にした上で発行されるWebアプリケーションのURL
https://script.google.com/macros/s/YYY...YYY/exec
については、辞書ファイル内に記述する(=ユーザさんに公開する)必要があります。
【!注意!】
ここの 'id' は、
「あなたのゴーストさんの感想が詰まったスプレッドシート」を特定できてしまう情報なので、
基本的に自分以外の誰か(ユーザさん、他のデべさん、友人知人家族)に公開してはいけません。
取り扱いには十分ご注意下さい。
【備忘録】
コードの記述を少なくするため、
「グローバル定数として SPREADSHEET_ID を定義し、全関数で参照する」ように記述しました…が、
この書き方だと、
「SPREADSHEET_ID を条件分岐で定義する」
(例えばプロパティサービスの値を優先する、等)
といったことが、簡単にはできなくなるようです。
というのも、関数を実行する前に、
グローバル変数・定数の定義・オブジェクト取得処理が必ず入るので、
「SPREADSHEET_ID の記述が正確で、正しくシートオブジェクトを取得できる状態」でないと、
SPREADSHEET_ID や SPREADSHEET の取得に失敗し、例外を吐いて止まるためです。
難儀なことに、if 文のみならず、try-catch さえも使えませんでした。
(条件分岐・try-catch の中でグローバル変数を定義しようとすると、
当該グローバル変数・定数が「未定義」になってしまうため、その後の処理を通過できません)
そのため、SPREADSHEET_ID を正しくハードコーディングしていない限り、
「initPropertyValues 関数のような初期化用関数を実行する」こともできませんし、
「条件分岐で、プロパティサービスに値が設定されていればそちらを優先する」といった処理も書けませんでした。
GUI から手動で SPREADSHEET_ID をプロパティサービスに登録し、
const SPREADSHEET_ID = PropertiesService.getScriptProperties().getProperty('SPREADSHEET_ID');
とすればよいだけ…ではあるのですが、
導入時の手順がやや煩雑になるため、現状、標準仕様にはしておりません。
*/
const SPREADSHEET_ID = '***';
//const SPREADSHEET_ID = PropertiesService.getScriptProperties().getProperty('SPREADSHEET_ID');
//===========================
// ● SHEET_NAME_COMMENT
//===========================
/*
↑の 'id' で特定したスプレッドシート内の、コメントを記録するシートを 'シート名' で指定する用。
私は 'posted_comments' で固定しています。
*/
const SHEET_NAME_COMMENT = 'posted_comments';
//===========================
// ● SHEET_NAME_NGWL
//===========================
/*
↑の 'id' で特定したスプレッドシート内の、
ブラックリスト式のNGワードリストを 'シート名' で指定する用。
私は 'NG_words_list' で固定しています。
*/
const SHEET_NAME_NGWL = 'NG_words_list';
//===========================
// ● SHEET_NAME_DEBUG
//===========================
/*
↑の 'id' で特定したスプレッドシート内の、
print文デバッグ的に値を書き込むシートを 'シート名' で指定する用。
私は 'debug' で固定しています。
*/
const SHEET_NAME_DEBUG = 'debug'
//===========================
// ● SPREADSHEET
//===========================
/*
スプレッドシートオブジェクト。
↓のシートオブジェクトの親玉さん。
これで「Googleドライブ上のどのスプレッドシートか」を特定し、取り扱うことになります。
*/
const SPREADSHEET = SpreadsheetApp.openById(SPREADSHEET_ID);
//===========================
// ● SHEET_COMMENT
// ● SHEET_DEBUG
// ● SHEET_NGWL
//===========================
/*
シートオブジェクト。
実際にデータ操作(セルを参照しての書き込み・読み出し等)を行う場合は、
これらシートオブジェクトに対して操作を行う必要があります。
*/
// SSPから送信されたコメント等の記録シート
const SHEET_COMMENT = SPREADSHEET.getSheetByName(SHEET_NAME_COMMENT);
// 本スクリプト内の debugPrint関数の記録先シート
const SHEET_DEBUG = SPREADSHEET.getSheetByName(SHEET_NAME_DEBUG);
// ブロックしたい単語群(NGワードリスト)の記録されたシート
const SHEET_NGWL = SPREADSHEET.getSheetByName(SHEET_NAME_NGWL);
//===========================
// 【デバッグ用】
// ● debug_row
// ● debug_col
// ● debug_pos
//===========================
/*
debugPrint関数で使う変数群。
A2-B2 は進捗状況記録用に、A3-B3はリターンコード記録用に使うので、
私は A4-B4 を初期位置にしています。
*/
let debug_row = 1; // 1=A列, 2=B列, 3=C列, ...
let debug_col = 4; // 1=1行目, 2=2行目, 3=3行目, ...
let debug_pos = 0; // print文デバッグ的セル書込の位置調整用変数。
// debug_col+debug_Pos で位置をずらしています。
//===========================
// 【デバッグ用】
// ● DEBUG_DISABLE
//===========================
/*
●debugPrint(label, content)
●debugPrintProgressCheck(content)
●debugPrintReturnCode(content)
の内部処理を実行するか否か、のフラグ。
DEBUG_DISABLE = 1 で「実行しない」(デバッグ無効)
DEBUG_DISABLE = 0 で「実行する」 (デバッグ有効)
になります。
※処理の都合上 1 以外の値であれば「実行する」になるのでご注意を。
冒頭で述べた「セル参照・書込回数の制限(50,000回/日)」を回避するため、
デバッグ時以外・実運用時は、
DEBUG_DISABLE = 1
として、不要なセル参照の呼び出しが起こらないようにした方が良いと思います。
*/
const DEBUG_DISABLE = 1;
//============================================================================================
// ★ 自作関数群
//============================================================================================
//======================================
// 【デバッグ用】
// ● showTest()
//======================================
/*
function showTest() {
// Logger.log(object) -> オブジェクト型の表示
// console.log(object) -> 利用できるメソッドの列挙
Logger.log(console);
console.log(console);
Logger.log(Logger);
console.log(Logger);
}
*/
//======================================
// 【初期化用】
// ● initPropertyValues()
//======================================
/*
初期化用関数。
最初に一回だけ実行して、プロパティサービスに必要なキーを作成します。
(一応、二回以上起動しても上書きしないようにはしました)
現状、スタンドアロンスクリプト&エディタの環境で、引数を与えて実行する手段が分からないので、
単なる空箱(プロパティのキーと、適当な初期値)を作るだけの、半端な初期化です…。
スクリプトプロパティは GUI から(画面←の歯車アイコンから)操作できるので、
実際の値はそちらから入力してやってくださいまし。
値の編集後は、
最後に「スクリプトプロパティの保存」をクリックするのをお忘れなく。
deleteAllUserPropertyValues()
deleteAllDocumentPropertyValues()
の2つは…
誤って(GUIからは不可視で、設定値を確認できない)UserProperty と DocumentProperty に
設定してしまうミスをやらかしたため、急遽作成した関数です。
(動作は、単にそれぞれのプロパティサービスの値を全削除するだけです)
同じミスをしない限りは不要なのですが、一応残してあります…。
▼引数
なし(求む、スタンドアロンスクリプトで引数指定して実行する方法)
▼戻り値
なし(※作成するだけです)
*/
//======================================
function initPropertyValues() {
// スクリプトプロパティ オブジェクト (ScriptProperties型)
let scriptProperties = PropertiesService.getScriptProperties();
// key: EMAIL_ADDRESS が存在していない場合のみ新規作成
// sendMailAlert(count) 関数内で利用する、
// 「通知先(送信先)のメールアドレス」です。
// 初期値の「t@t」では動作しませんので、ご自身のメールアドレスに変更してくださいまし。
if (scriptProperties.getProperty('EMAIL_ADDRESS') === null) {
scriptProperties.setProperty('EMAIL_ADDRESS', "t@t");
}
// key: ROWS_COUNT が存在していない場合のみ新規作成
// dailyCheckTotalCounts_v2() 関数内で利用する、
// 「前回実行時の総行数」の記録値です。
if (scriptProperties.getProperty('ROWS_COUNT') === null) {
scriptProperties.setProperty('ROWS_COUNT', "0");
}
// 一応、確認用にコンソールへ出力。
console.log("key : " + 'EMAIL_ADDRESS');
console.log("value : " + scriptProperties.getProperty('EMAIL_ADDRESS'));
console.log("key : " + 'ROWS_COUNT');
console.log("value : " + scriptProperties.getProperty('ROWS_COUNT'));
}
/*
function deleteAllUserPropertyValues() {
var userProperties = PropertiesService.getUserProperties();
userProperties.deleteAllProperties();
}
function deleteAllDocumentPropertyValues() {
var documentProperties = PropertiesService.getDocumentProperties();
documentProperties.deleteAllProperties();
}
*/
//======================================
// 【デバッグ用】
// ● checkPropertyValue(key)
//======================================
/*
プロパティサービスに登録された値を、キーを引数で指定して呼び出し、
コンソール上で確認する関数。
▼引数
●key
呼び出したい value の key
▼戻り値
なし(コンソールでの確認に限定しています)
*/
//======================================
function checkPropertyValue(key) {
const PROPERTY_VALUE = PropertiesService.getScriptProperties().getProperty(key);
console.log(key + ": " + PROPERTY_VALUE);
}
function checkPropertyValue_API_KEY_MASTODON() {
checkPropertyValue("API_KEY_MASTODON");
}
//======================================
// 【デバッグ用】
// ● checkPropertyValueAll(key)
//======================================
/*
プロパティサービスに登録された値を、キーを指定して呼び出し、コンソールログ上で確認する関数。
Script, User, Document の3つとも調べます。
(存在しない場合 null が返ってきます)
公式の例では、先に
var documentProperties = PropertiesService.getDocumentProperties();
var scriptProperties = PropertiesService.getScriptProperties();
var userProperties = PropertiesService.getUserProperties();
とオブジェクトを定義する書き方が示されていますが、
PropertiesService.getDocumentProperties().getProperty(key);
とまとめてしまっても大丈夫でした。
※ Browser.inputBox は、
スプレッドシートに紐づいた Container-bound Scripts でしか使用できないとのこと。
Standalone Scripts では… Ui クラスも使えないっぽいので、
エディタからは「引数を渡して実行する関数を記述する」以外になさそうです…。
▼引数
●key
呼び出したい value の key
▼戻り値
なし(※コンソールでの確認に限定しています)
*/
//======================================
function checkPropertyValueAll(key) {
// 引数の key が空だった場合は実行中止
if (key === null) {
console.error("ERROR: key が空だったため、処理を中断しました。")
}
// key が指定されたら、存在するか否かのチェックはすっ飛ばして表示する
// (定義されてなければ null が返ってくるだけなので)
const PROPERTY_VALUE_SCRIPT = PropertiesService.getScriptProperties().getProperty(key);
console.log(key + "[Script] : " + PROPERTY_VALUE_SCRIPT);
const PROPERTY_VALUE_USER = PropertiesService.getUserProperties().getProperty(key);
console.log(key + "[User] : " + PROPERTY_VALUE_USER);
const PROPERTY_VALUE_DOC = PropertiesService.getDocumentProperties().getProperty(key);
console.log(key + "[Document] : " + PROPERTY_VALUE_DOC);
}
//======================================
// 【デバッグ用】
// ● debugPrint(label, content)
//======================================
/*
print文デバッグ的なことをするための関数。
console.log 等もありますが、外部から POST された際のデバッグに使い辛いので、
SHEET_DEBUG にラベルと値を記入する形で実装してみました。
書き込み位置は、
debug_Pos, debug_row, debug_col を参照し、
呼び出す度にインクリメント(=一つ↓にずら)しつつ書き込むようにしてあります。
▼引数
●label
後から見た時に「何を記録したのか」分かりやすくメモるためのラベル文。
●content
print文で出力したい内容。
▼戻り値
0: 正常終了時
-1: DEBUG_DISABLE == 1 の場合
*/
//======================================
function debugPrint(label, content) {
// DEBUG_DISABLE が 1(デバッグ無効) の場合はセル操作を実行しません
if (DEBUG_DISABLE == 1) {
return -1;
}
// DEBUG_DISABLE が 1 でない場合は、通常通り処理を行います。
// 最初は A4:B4 に [label, content] を書き込み、
// 呼び出される度に、
// A5:B5, A6:B6, ... と一行づつ下に書き込みをずらし、前の出力が上書きされることを回避します。
// 地味に注意ポイント:
// setValuesに渡す配列は「二次元配列」である必要があります。
const VALS = [
[label, content]
];
// 【参考】getRange の矩形範囲選択
// .getRange(2, 1, 3, 4) // 2行A列(=A2)を起点とし、行数3×列数4の範囲を指定
// .getRange(4, 1, 1, 2) // 4行A列(=A4)を起点とし、行数1×列数2の範囲を指定
SHEET_DEBUG.getRange(debug_col+debug_pos, debug_row, 1, 2).setValues(VALS);
// 【参考】これじゃダメなの?
// 分かりやすさ優先なら、↓の書き方でも良いと思います。
// ただ、この書き方だと debugPrint 関数を呼び出す度に2回セル参照が行われるため、
// 単純計算で↑の実行回数の2倍、50000回/日の制限を消費してしまいます。
//SHEET_DEBUG.getRange(debug_col+debug_pos,debug_row).setValue(label); // (4,1) -> A4
//SHEET_DEBUG.getRange(debug_col+debug_pos,debug_row+1).setValue(content); // (4,2) -> B4
// 次に呼び出した際、1行↓にずらすためインクリメントしておきます。
debug_pos = debug_pos + 1
// 正常終了。
return 0;
}
//======================================
// 【デバッグ用】
// ● debugPrintProgressCheck(content)
//======================================
/*
SHEET_NAME_DEBUG にラベルと値を記入する、
print文デバッグ的なことをするための関数・その2。
進捗状況を記録してチェックする専用関数です。
主用途が
「A2 に "進捗状況", B2 に "どこまで進めたか" を書き込み、
どこでエラー吐いて止まったか追跡する」
なので、
書き込み先は関数内で直接指定することにしました。
▼引数
●content
print文で出力したい内容。
"step 1: get XXXX" などと入れて、コード中のどこまで進めたか記録。
▼戻り値
0: 正常終了時
-1: DEBUG_DISABLE == 1 の場合
*/
//======================================
function debugPrintProgressCheck(content) {
// DEBUG_DISABLE が 1(デバッグ無効) の場合はセル操作を実行しません
if (DEBUG_DISABLE == 1) {
return -1;
}
// 地味に注意ポイント:
// setValuesに渡す配列は「二次元配列」である必要があります。
const VALS = [
["進捗状況", content]
];
// セル範囲は (A2:B2) で固定、内容を書き込むだけです。
SHEET_DEBUG.getRange("A2:B2").setValues(VALS);
return 0;
}
//======================================
// 【デバッグ用】
// ● debugPrintReturnCode(content)
//======================================
/*
SHEET_NAME_DEBUG にラベルと値を記入する
print文デバッグ的なことをするための関数・その3。
リターンコードを記録するための専用関数です。
▼引数
●content
print文で吐き出したい中身。
debugPrintReturnCode(200);などと書いて、
SSPには返しませんが、内部的にどうだったか記録しておく使い方を想定。
▼戻り値
0: 正常終了時
-1: DEBUG_DISABLE == 1 の場合
*/
//======================================
function debugPrintReturnCode(content) {
// DEBUG_DISABLE が 1(デバッグ無効) の場合はセル操作を実行しません
if (DEBUG_DISABLE == 1) {
return -1;
}
// 地味に注意ポイント:
// setValuesに渡す配列は「二次元配列」である必要があります。
const VALS = [
["リターンコード", content]
];
// セル範囲は (A3:B3) で固定、内容を書き込むだけです。
SHEET_DEBUG.getRange("A3:B3").setValues(VALS);
return 0;
}
//======================================
// 【デバッグ用】
// ● doPostTest()
//======================================
/*
doPost の動作確認用関数。
Utilities.newBlob('XXXXX') の中身を適当に変更して使用してくださいまし。
▼引数
なし
▼戻り値
なし
*/
//======================================
function doPostTest() {
// POSTのテスト用文字列。
// SSPから送信されるであろうデータ(文字列)と同じものを指定してください。
// 本スクリプトの場合、
// 「送信元;コメント;メモ の3要素を ;(半角セミコロン) で区切った文字列」
// を想定しています。
// ex) 'TEST;いいね!;HOME'
//
// ※ v10 より、上記の「3要素が必ず含まれた文字列でないといけない」制限を撤廃し、
// 同時に要素数1~5まで受付可能なように仕様変更しました。
const POST_CONTENT = 'TEST'
// たまにやらかすので、ミス防止用のチェックを入れました。
// 空でもエラーなく動くのですが、
// 動作完了しても、何も変化がない(=スプレッドシートに記録されないし、コンソールにもエラーが出ない)のは
// 分かりにくくてよろしくないなあ、と思ったので…。
if (POST_CONTENT == '') {
console.error("ERROR: doPostTest() 内の POST_CONTENT が空っぽです。")
return;
}
// eを作成する…のにちょっと注意が必要でした。
// e.postData は FileUploadクラス(=blobクラスを継承したクラス)なので、
// 面倒でもBlobでつくらないといけない(らしい)です。
//
// 【参考】
// ■Google Apps ScriptのdoPostでJSONなパラメータのPOSTリクエストを受ける - Qiita
// https://qiita.com/shirakiya/items/db22de49f00710478cfc#json%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF%E3%82%92%E5%8F%97%E3%81%91parse%E3%81%99%E3%82%8B
let e = {
postData : Utilities.newBlob(POST_CONTENT)
};
// postData さえ作れれば、あとは doPost に渡すだけです。
// これで「毎回コードを編集して、デプロイして、SSPの記述を更新されたURLに書き直して、それからテスト」
// …という面倒から解放されました。
//
// ただし、
// 「デプロイしなければ、いくらコードを変更・保存しても、Webアプリとして呼び出した際の挙動は変わらない」ことと、
// 「動作確認して新規デプロイした場合は、WebアプリのURLも変わってしまう」ことを
// ついつい忘れがちになる点には、ご注意ください。
//
// コードを修正して、doPostTest関数での動作に問題が無いのを確認して、ふーいけたいけた…で安心していると、
// 「(SSP側なり何なり)外からPOSTしても動作が変わらない…なぜだ…」で悩むことになります(なりました)
doPost(e);
}
//======================================
// ● listupArray(target_array)
//======================================
/*
一次元配列の中身を表示用に整えた文字列にする関数。
▼引数
●target_array
対象とする配列。
▼戻り値
●all_data ->
target_array[0]: TEST
target_array[1]: いいね!
target_array[2]: HOME
な感じで、各要素が改行された文字列が返ってきます。
本当は "target_array" 部分を "引数で渡された配列の名前" に変えたかった…のですが、
無名関数を使うテクニックが(自分の技術レベルでは)使えず、実装できませんでした…。
*/
//======================================
function listupArray(target_array) {
// 戻り値用の変数 all_data を用意して、
let all_data = "";
// 配列の長さを取得して、
let array_count = target_array.length;
// all_data に全部ぶちこみます。
// 一応、各要素ごとに改行して見やすく…したつもりです。
for(let i=0; i<array_count; i++)
{
all_data += "target_array[" + String(i) + "]: " + target_array[i] + "\r\n";
}
// 戻り値は、改行を入れて整形し見やすくした文字列。
return String(all_data);
}
//======================================
// ● checkContentTextByNGWordsList(raw_content, replace_str, del_mode)
//======================================
/*
特定の文字列(NGワード)が含まれている場合に、
その文字列部分を別の文字列で置換する or 全文削除する関数。
ひとまず単純に、分かりやすさ優先で作ってみました。
※対象のシート(SHEET_NGWL)は、冒頭のグローバル変数定義から自動で取得しています。
NGワードは SHEET_NGWL の「A列2行目~最終行」までの範囲にあるデータを自動的に読み込み使用します。
(GASのRegExp関数で解釈される範囲での)正規表現の利用が可能です。
▼引数
●raw_content
NGワードでの置換を行う前の、元の文字列。
●del_mode
動作モード指定:
raw_content 内に、↑の SHEET_NGWL から読み出したNGワードが含まれていた場合、
0 = raw_content を REPLACED_STR に置換し、全削除します。
1 = raw_content 内の該当する文字列部分だけ REPLACED_STR に全て置換します。
※0,1以外 => *何もしません*
▼戻り値
●processed_content ->
上記の置換処理が行われた後の文字列
*/
//======================================
function checkContentTextByNGWordsList(raw_content, replace_str, del_mode) {
// SHEET_NGWL から NGワードの一覧を読み込み。
// ※A2~最終行までのデータを"全て"取得しているため、空白セルも読み込んでいる点に注意。
let NG_words_list = SHEET_NGWL.getRange('A2:A').getValues();
debugPrint("NG_words_list", listupArray(NG_words_list));
// このままだと NG_WORD_LIST に空のセルが含まれている可能性があり、
// 後の正規表現オブジェクト生成時に悪さをすることがある( /(?:)/g が生成されておかしなことになる)ので除去します。
// 具体的には、配列を探索して空セルの位置を探し、そこまでで切り落とすことにしました。
const NG_WORD_LIST_COUNT = NG_words_list.length; // 配列の全個数=最終行数
let valid_value_pos = 0; // 空文字が出てこない最後の位置
// 空セル(=Stringで変換した際、空文字('')になるもの)が出てくるまで探索。
for (let i=0; i<NG_WORD_LIST_COUNT; i++) {
debugPrint("NG_words_list[" + String(i) + "]", NG_words_list[i]);
if (!String(NG_words_list[i])) {
break;
}
valid_value_pos += 1;
}
debugPrint("valid_value_pos", valid_value_pos);
// 得られた(最初の)空セルの位置までで NG_WORD_LIST を切り落とします。
const NG_WORD_LIST_FILTERED = NG_words_list.slice(0, valid_value_pos);
debugPrint("NG_WORD_LIST_FILTERED", listupArray(NG_WORD_LIST_FILTERED));
/*
【dullNekoより】
↑の空セルの問題、もっとスマートな方法、例えば
・空のセルをそもそも読み込まないようにする
・配列のfilterメソッドでやる
等々をご存じの方、いらっしゃったら是非ご教示ください…
今の私には…理解できなかった…
*/
// 改めて配列の全個数を取得
const NG_WORD_LIST_FILTERED_COUNT = NG_WORD_LIST_FILTERED.length;
debugPrint("NG_WORD_LIST_FILTERED_COUNT", NG_WORD_LIST_FILTERED_COUNT);
// 戻り値を準備し、処理前の値を確認。
let processed_content = raw_content;
debugPrint("processed_content (before)", processed_content);
// NGワードリストを用いて、raw_content を del_mode に応じた動作で置換処理。
/*
【メモ】
変数名 = new RegExp("正規表現パターンの文字列" [, "フラグ" ]) で正規表現オブジェクト作成。
"正規表現パターンの文字列"に変数を使いたい場合は RegExp で指定する他なさそう。
フラグは g:最初の一個だけでなく全部マッチ対象にする i:大文字・小文字を区別しない m:改行の後も処理を続ける
【注意】
パターンをRegExp関数で文字列として指定する場合は、スラッシュ(/)で囲う必要無し。
また、RegExp関数が自動的にエスケープしてくれるので、\/のエスケープ処理は考えなくてOKでした。
※ + ? * とかは(当然ながら)そのまま正規表現として解釈されるので要注意。
*/
//=====================================================================
// del_mode == 0 の場合は、全文置換 / 一回でもマッチすれば、全文を置き換えます。
//=====================================================================
if (del_mode == 0) {
// 正規表現にマッチしたかどうかのチェック用フラグ
let test_flag = false;