-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathGmailNewsletterScript.gs
401 lines (327 loc) · 15 KB
/
GmailNewsletterScript.gs
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
// TODO: Potential things to include for the next (August) newsletter:
// - DevFestDC recap videos/content (https://www.youtube.com/watch?v=oYQh0ZR0Lec for starters)
// - Google I/O recap videos
// - Android Summit 2019 (https://www.androidsummit.org/ ; 50% DISCOUNT courtesty of WWCODE https://www.eventbrite.com/e/android-summit-2019-tickets-59378886849?discount=wwcode50)
// - ... and of course some other relevant content from the previous newsletter drafts
// Despite Apps Script being based on JS, classes aren't supported
// yet (as of now) on this engine. That said, here's an anonymous
// "class" used instead.
var Post = function(url, title, description) {
this.url = url;
this.title = title;
this.description = description;
this.logPost = function() {
var logStr = Utilities.formatString("url: %s\ntitle: %s\ndescription: %s\n",
this.url,
this.title,
this.description
);
Logger.log(logStr);
}
};
// The following constants ("const" modifier not included in Apps Script)
// serve as a part of the newsletter draft template.
var jared = "[email protected]";
var joni = "[email protected]";
var monthNames = ["January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
];
var disclaimer = "\/\*\* This draft was initially run and created by Google Apps Script \*\*\/";
var greeting = "Greetings Green Robots and Kolleagues,";
var callForTalksHeader = "Call for Talks";
var mainEventsHeader = "Main Affiliated Upcoming Events";
var otherEventsHeader = "Other Upcoming Events";
var newsMediaHeader = "News/Media";
var callForTalksBody = "What\'s first is first - if you\'re ever interested in giving a talk on an Android-related (or Kotlin, Flutter, or heck, a Google-related) technology sometime soon, please don\'t hesitate to ask! We\'re very open about it.";
var salutation = "Best,";
var sender = "DCAndroid/Kotlin Organizers";
var dcAndroidHandle = "@DCAndroid";
var dcKotlinHandle = "@DCKotlin";
var dcAndroidUrl = "https://twitter.com/DCAndroid";
var dcKotlinUrl = "https://twitter.com/DCKotlin";
// Android Weekly:
// Match starting from the following headers succeeding with all chars
// and white space chars up until a double line break (the paragraph
// spacing for this newsletter).
var artAndTutRegexAndroid = /(\*\* Articles & Tutorials[\s\S]*?)(?:\r?\n){3}/;
var newsRegexAndroid = /(\*\* News[\s\S]*?)(?:\r?\n){3}/;
var specialsRegexAndroid = /(\*\* Specials[\s\S]*?)(?:\r?\n){3}/;
// TODO: Not the prettiest regexs for the Flutter Weekly newsletter, but gets the job done for now
var announcementRegexFlutter = /\*\* Announcements[\s\S]*?\*\* Articles and tutorials/;
var artAndTutRegexFlutter = /\*\* Articles and tutorials[\s\S]*?\*\* Videos and media/;
// TODO: Not the prettiest regex for the Kotlin Weekly newsletter, but gets the job done for now
var mediaRegexKotlin = /\*\* Kotlin Weekly Newsletter[\s\S]*?\*\* /;
// URL regex, and a regex for the title of a post (matches sequential
// text excluding what looks like a URL), respectively.
var urlRegex = /(http|https|ftp|ftps)\:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(\/[^ \)]*)?/;
var titleRegex = /(?:^|\s+)((?:(?!:\/\/).)*)(?=\s|$)/;
// Two separate arrays since we wanna prioritize announcements over
// media (i.e. articles and tutorials) within the same "News/Media"
// header.
var announcementArray = [];
var mediaArray = [];
// Initiates all of the essential functions in order to create a draft
// template consisting of HTML-encoded, formatted news/media content:
//
// searchAndroidWeekly() -> searchKotlinWeekly() -> searchFlutterWeekly() -> createDraft()
//
// Basically, this whole script assumes that you're subscribed to at
// least one of the weekly newsletters - Android Weekly, Kotlin Weekly,
// or Flutter Weekly - in order to get a compiled newsletter draft
// template as a result instead of an empty one.
function main() {
searchAndroidWeekly();
searchKotlinWeekly();
searchFlutterWeekly();
createDraft();
}
// Debugging purposes: Logs a class' properties/functions/etc.
// since Apps Script lacks some attributes compared to JS.
function getOwnPropertyNames() {
Logger.log(Object.getOwnPropertyNames(Array.prototype));
}
// Region Newsletter Search Queries that "locally" queries and searches the
// runner's (of this script) inbox for news/media from weekly newsletter
// subscriptions (Android, Kotlin, and Flutter Weekly) for the past 30 days
// to eventually push the Posts into their respective arrays by using Gmail
// Service's (https://developers.google.com/apps-script/reference/gmail)
// relevant classes and functions to retrieve a thread/message/body, and
// complex regexes to match patterns against news/media content worth
// considering.
// This function will basically store the top article & tutorial of each
// Android Weekly newsletter while prioritizing and storing all of the
// news onto the draft template.
function searchAndroidWeekly() {
var query = "in:inbox subject:(Android Weekly) newer_than:30d";
var threads = GmailApp.search(query);
for each (thread in threads) {
var messages = thread.getMessages();
var body = messages[0].getPlainBody();
var containsArtAndTut = artAndTutRegexAndroid.test(body);
if (containsArtAndTut) {
var matches = artAndTutRegexAndroid.exec(body);
var elements = matches[0].split("\n");
var filteredElements = getFilteredElementsAndroid(elements);
pushPostIntoMedia(filteredElements, mediaArray);
}
var containsNews = newsRegexAndroid.test(body);
if (containsNews) {
var matches = newsRegexAndroid.exec(body);
var elements = matches[0].split("\n");
var filteredElements = getFilteredElementsAndroid(elements);
pushPostsIntoAnnouncements(filteredElements, announcementArray);
}
var containsSpecials = specialsRegexAndroid.test(body);
if (containsSpecials) {
var matches = specialsRegexAndroid.exec(body);
var elements = matches[0].split("\n");
var filteredElements = getFilteredElementsAndroid(elements);
pushPostsIntoAnnouncements(filteredElements, announcementArray);
}
}
}
// If any, this function will basically store the top article & tutorial of
// each Kotlin Weekly newsletter.
function searchKotlinWeekly() {
var query = "in:inbox subject:(Kotlin Weekly) newer_than:30d";
var threads = GmailApp.search(query);
for each (thread in threads) {
var messages = thread.getMessages();
var body = messages[0].getPlainBody();
var containsMedia = mediaRegexKotlin.test(body);
if (containsMedia) {
var matches = mediaRegexKotlin.exec(body);
var elements = matches[0].split("\n");
var filteredElements = getFilteredElementsKotlin(elements);
pushPostIntoMedia(filteredElements, mediaArray);
}
}
}
// If any, this function will basically store the top article & tutorial
// while prioritizing and storing all announcements from each Flutter
// Weekly newsletter.
function searchFlutterWeekly() {
var query = "in:inbox subject:(Flutter Weekly) newer_than:30d";
var threads = GmailApp.search(query);
for each (thread in threads) {
var messages = thread.getMessages();
var body = messages[0].getPlainBody();
var containsAnnouncements = announcementRegexFlutter.test(body);
if (containsAnnouncements) {
var matches = announcementRegexFlutter.exec(body);
var elements = matches[0].split("\n");
var filteredElements = getFilteredElementsFlutter(elements);
pushPostsIntoAnnouncements(filteredElements, announcementArray);
}
var containsArtAndTut = artAndTutRegexFlutter.test(body);
if (containsArtAndTut) {
var matches = artAndTutRegexFlutter.exec(body);
var elements = matches[0].split("\n");
var filteredElements = getFilteredElementsFlutter(elements);
pushPostIntoMedia(filteredElements, mediaArray);
}
}
}
// End Region
// Region Draft Creation
// Invoked after all of the functions (responsible for pushing
// out the relevant Posts consisting of the relevant news/media content
// initially retrieved from the three weekly newsletters mentioned
// throughout the script) are completed. With these Posts, this function
// will basically encode its properties into an HTML-encoded template
// body prior to actually creating the Gmail draft.
function createDraft() {
var date = new Date();
var month = "";
if (date.getMonth() === monthNames.length - 1) { // Iterates back to January when reaching the end
month = monthNames[0];
} else { // Otherwise, assigns the next upcoming month
month = monthNames[date.getMonth() + 1];
}
var subject = Utilities.formatString("DCAndroid/Kotlin %s Newsletter Draft", month);
// Here's where the magic happens.
GmailApp.createDraft(jared, subject, "Body to be replaced", {
cc: joni,
htmlBody: getEncodedHtml()
});
}
function getEncodedHtml() {
var html = '<!DOCTYPE html><html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"></head><body style="font-family: Verdana, sans-serif; font-size: 9pt; line-height: 1.15;">%s</body></html>';
var encodedBody = Utilities.formatString("<i>%s</i><br />%s<br /><b>%s</b><br />%s<br /><b>%s</b><br /><i>// TODO: Fill out manually</i><br /><b>%s</b><br /><i>// TODO: Fill out manually</i><br /><b>%s</b><br />",
disclaimer,
greeting,
callForTalksHeader,
callForTalksBody,
mainEventsHeader,
otherEventsHeader,
newsMediaHeader
);
for each (post in announcementArray) {
var htmlEncoding = Utilities.formatString(
"\-<a href=%s>%s</a>: %s<br />",
post.url || "",
post.title || "",
post.description || ""
);
encodedBody += htmlEncoding;
}
for each (post in mediaArray) {
var htmlEncoding = Utilities.formatString(
"\-<a href=%s>%s</a>: %s<br />",
post.url || "",
post.title || "",
post.description || ""
);
encodedBody += htmlEncoding;
}
var salutationTxt = Utilities.formatString("%s<br />%s<br><a href=%s>%s</a> / <a href=%s>%s</a>",
salutation,
sender,
dcAndroidUrl,
dcAndroidHandle,
dcKotlinUrl,
dcKotlinHandle
);
encodedBody += salutationTxt;
var encodedHtml = Utilities.formatString(html, encodedBody);
Logger.log('Encoded HTML: ' + encodedHtml);
return encodedHtml;
}
// End Region
// Region Helper Functions for retrieving a filtered array of regex-splitted
// elements from a Gmail thread of the respective newsletter
function getFilteredElementsAndroid(elements) {
var filteredElements = elements.filter(function(item) {
var startCharRegex = /^[a-zA-Z]/;
var startsWithChar = startCharRegex.test(item);
// Just because simply String equality doesn't suffice
// here.
var sponsRegex = /Sponsored/;
var containsSponsTxt = sponsRegex.test(item);
var isLegitimatePost = startsWithChar && !containsSponsTxt
return isLegitimatePost;
});
return filteredElements;
}
function getFilteredElementsFlutter(elements) {
var filteredElements = elements.filter(function(item) {
// Asterisk literal is an exception here since all plain message posts
// are bold headers.
var startCharRegex = /^[a-zA-Z\*]/;
var startsWithChar = startCharRegex.test(item);
// Excludes the "Announcements" header that was originally part of the
// regex match.
var announcementHeadRegex = /\*\* Announcements/;
var containsAnnouncementHead = announcementHeadRegex.test(item);
// Excludes the "Articles & Tutorials" header that was originally part of
// the regex match.
var artAndTutHeadRegex = /\*\* Articles and tutorials/;
var containsArtAndTutHead = artAndTutHeadRegex.test(item);
// Just because simply String equality doesn't suffice
// here.
var sponsRegex = /Sponsored/;
var containsSponsTxt = sponsRegex.test(item);
var isLegitimatePost = startsWithChar && !containsAnnouncementHead && !containsArtAndTutHead && !containsSponsTxt
return isLegitimatePost;
});
return filteredElements;
}
function getFilteredElementsKotlin(elements) {
var filteredElements = elements.filter(function(item) {
var startCharRegex = /^[a-zA-Z]/;
var startsWithChar = startCharRegex.test(item);
// Hard-coded regex specifically for this newsletter in
// order to run the logic afterwards.
var helloRegex = /Hello Kotliners/;
var containsHello = helloRegex.test(item);
var helloRegex2 = /Hello from /;
var containsHello2 = helloRegex2.test(item);
// Just because simply String equality doesn't suffice
// here.
var sponsRegex = /Sponsored/;
var containsSponsTxt = sponsRegex.test(item);
var sponsPusherRegex = /Pusher/;
var containsPusherSponsTxt = sponsPusherRegex.test(item);
var isLegitimatePost = startsWithChar && !containsHello && !containsHello2 && !containsSponsTxt && !containsPusherSponsTxt
return isLegitimatePost;
});
return filteredElements;
}
// End Region
// Region Helper Functions for pushing a top article/tutorial and every
// announcement from a thread into the respective array, respectively
function pushPostIntoMedia(filteredElements, array) {
var post = new Post();
for each (element in filteredElements) {
var containsUrl = urlRegex.test(element);
if (containsUrl) { // Assuming it contains a title as well
post.url = urlRegex.exec(element)[0];
var containsTitle = titleRegex.test(element);
if (containsTitle) {
post.title = titleRegex.exec(element)[0];
}
} else { // Assuming it's a description instead
post.description = element;
mediaArray.push(post);
break;
}
}
}
function pushPostsIntoAnnouncements(filteredElements, array) {
var post = new Post();
for each (element in filteredElements) {
var containsUrl = urlRegex.test(element);
if (containsUrl) { // Assuming it contains a title as well
post.url = urlRegex.exec(element)[0];
var containsTitle = titleRegex.test(element);
if (containsTitle) {
post.title = titleRegex.exec(element)[0];
}
} else { // Assuming it's a description instead
post.description = element;
announcementArray.push(post);
post = new Post();
}
}
}
// End Region