-
Notifications
You must be signed in to change notification settings - Fork 0
/
Code.gs
371 lines (330 loc) · 10.9 KB
/
Code.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
/*******
** TheDayAhead Google Apps Script
**
** Crawls a list of given RSS feeds and returns a set of headlines, which
** are sent via Gmail to the given email address along with information from
** the user's calendar on events for the day and events that have not been
** responded to yet.
**
** NOTE: This uses the Advanced Calendar service, which must be enabled in both
** your script's resources AND in the Google Developer Console.
*/
// data feed URLs
var dataSources = [
"http://gigaom.com/feed/",
"http://feeds.reuters.com/reuters/technologyNews?format=xml",
"http://www.engadget.com/rss-hd.xml",
"http://feeds2.feedburner.com/thenextweb",
"http://feeds.arstechnica.com/arstechnica/index?format=xml",
"http://www.forbes.com/technology/feed/",
"http://www.pcworld.com/index.rss",
"http://rss.nytimes.com/services/xml/rss/nyt/Technology.xml",
"http://recode.net/feed/",
"http://omgchrome.com/feed"
];
// categories and keywords to look for news in. each stories array gets filled in
var categories = [
{
"name": "Google",
"keywords": ["google","chrome","android","gmail","compute engine","app engine","appengine"],
"stories": []
},
{
"name": "Microsoft",
"keywords" : ["microsoft","windows","azure","typescript","bing","surface pro"],
"stories": []
},
{
"name": "Apple",
"keywords" : ["ios","ipad","siri","iphone"],
"stories": []
},
{
"name": "Amazon",
"keywords" : ["amazon"],
"stories": []
},
{
"name": "Twitter",
"keywords" : ["twitter"],
"stories": []
},
{
"name": "Facebook",
"keywords" : ["facebook"],
"stories": []
}
];
// Settings
var HEADLINE_LIMIT = 15; // Number of headlines per news source
var EMAIL_TITLE = "Top News And Events"; // What to title the email
var DAYS_AHEAD = 7; // Number of days out to scan events
/*******
** deliverNews
**
** Generates a summary of daily news items, tasks, and calendar information
** and delivers it to the user's inbox in one complete email message
*/
function deliverNews()
{
var newsMsg = ""; // will hold the completed HTML to email
var deliverAddress = Session.getActiveUser().getEmail();
var calEventsStr = buildCalendarEventsHTML();
// Collect the headlines from the feeds and filter the top stories
var feedStoriesStr = "";
for (var i=0; i < dataSources.length; i++) {
feedStoriesStr += retrieveFeedItems(dataSources[i]);
}
// Generate the Top Stories list that was created based on keywords
var topStoriesStr = "<h2>Top Stories</h2>";
topStoriesStr += categorizedNews();
// put all the data together
newsMsg = "<h1>" + EMAIL_TITLE + "</h1>\n" + calEventsStr + topStoriesStr + feedStoriesStr;
// Deliver the email message as HTML to the recipient
GmailApp.sendEmail(deliverAddress, EMAIL_TITLE, "", { htmlBody: newsMsg });
Logger.log(newsMsg.length);
}
/*******
** categorizedNews
**
** Loops over the generated list of stories in the categories array and
** builds a table of news items arranged by category
**
** @returns {string} formatted HTML of categorized news as a table
*/
function categorizedNews() {
var resultHTML = "";
// build the table that will hold the categorized results
resultHTML += "<table style='border: 1px solid gray;border-spacing:0'><tbody>";
// loop over each category and look for keywords
for (var i=0; i<categories.length; i++) {
var item = categories[i];
// only add a section for the category if it contains any stories
if (item.stories.length > 0) {
resultHTML += "<tr><td style='font-size:large;background-color:#ddd;border-bottom: 1px solid gray;border-top: 1px solid gray;padding:2pt'>" + item.name + "</td>";
for (var j=0; j < item.stories.length; j++) {
resultHTML += "<tr><td style='padding:2pt'>";
resultHTML += "<a href='" + item.stories[j].link + "'>" + item.stories[j].title + "</a>";
resultHTML += "</td></tr>";
}
resultHTML += "</tr>";
}
}
resultHTML += "</tbody></table>";
return resultHTML;
}
/*******
** buildCalendarEventsHTML
**
** @returns {string} Formatted HTML of the calendar events
*/
function buildCalendarEventsHTML() {
var calEventsStr = "<h2>Calendar: ";
// get a list of today's events
var calEvents = getEventsForToday();
if (calEvents.length > 0) {
calEventsStr += calEvents.length + " Events</h2>";
calEventsStr += buildEventsHTML(calEvents);
}
else {
calEventsStr += "0 Events</h2>";
}
// Get upcoming calendar events that have not been responded to
calEvents = getEventsMissingResponse();
if (calEvents.length > 0) {
calEventsStr += "<p>You have " + calEvents.length + " events in the next " +
DAYS_AHEAD + " days that you have not RSVP'd to:</p>";
calEventsStr += buildEventsHTML(calEvents);
}
return calEventsStr;
}
/*******
** retrieveFeedItems
**
** returns a formatted HTML list for the given URL data feed
**
** @param {URL} feedUrl the URL of the feed to process
** @returns {string} Formatted HTML of the feed headlines
*/
function retrieveFeedItems(feedUrl) {
var feedSrc = null;
var feedDoc = null;
var str = "";
var itemCount = 0;
var root = null;
var type = "unknown";
// to avoid having one bad XML feed take down the entire script,
// wrap the parsing in a try-catch block
try {
feedSrc = UrlFetchApp.fetch(feedUrl).getContentText();
feedDoc = XmlService.parse(feedSrc);
if (feedDoc)
root = feedDoc.getRootElement();
}
catch (e) {
Logger.log("Error reading feed: " + feedUrl);
Logger.log(e);
}
// detect the kind of feed this is. Right now only handles RSS 2.0
// but adding other formats would be easy enough
if (root && root.getName() == "rss") {
var version = root.getAttribute("version").getValue();
if (version == "2.0")
type = "rss2";
}
if (type == "rss2") {
str += "<div>";
var channel = root.getChild("channel");
var items = channel.getChildren("item");
str += "<h2><a href='"+channel.getChildText("link")+"'>"+channel.getChildText("title")+"</a></h2>\n";
Logger.log("%s items from %s", items.length, channel.getChildText("title"));
// Limit the number of headlines
itemCount = (items.length > HEADLINE_LIMIT ? HEADLINE_LIMIT : items.length);
str += "<ul>";
for (var i=0; i < itemCount; i++) {
var keywordFound = false;
var strTitle = items[i].getChildText("title");
var strLink = items[i].getChildText("link");
for (var n=0; n < categories.length; n++) {
for (var m=0; m < categories[n].keywords.length; m++) {
// simple index search, could probably be vastly improved
var searchWord = categories[n].keywords[m];
if ( strTitle.toLowerCase().indexOf(searchWord) != -1) {
categories[n].stories.push( {title: strTitle, link: strLink} );
keywordFound=true;
break;
}
}
}
// If we didn't add this item to the topStories, add it to the main news
if (!keywordFound) {
str += "<li><a href='" + strLink + "'>" + strTitle + "</a></li>\n";
}
Logger.log(strTitle);
}
str += "</ul></div>\n";
}
return str;
}
/*******
** getMissingResponseEvents
**
** Get a list of Calendar events that have not been responded to.
**
** @returns {CalendarEvent []} array of CalendarEvent objects
*/
function getEventsMissingResponse() {
var d = new Date();
var now = d.toISOString();
var then = new Date(d.getTime() + (1000 * 60 * 60 * 24 * DAYS_AHEAD)).toISOString();
var events = [];
var returnEvents = [];
// Find future events that have not been responded to yet
events = Calendar.Events.list("primary", {singleEvents: true, timeMin: now, timeMax: then});
for (var i=0; i < events.items.length; i++) {
var attendees = events.items[i].attendees;
if (attendees) {
for (var j=0; j<attendees.length; j++) {
if (attendees[j].email && attendees[j].email == Session.getActiveUser().getEmail()) {
if (attendees[j].responseStatus == "needsAction") {
returnEvents.push(events.items[i]);
break;
}
}
}
}
}
Logger.log("%s Calendar events with no RSVP",events.length);
return returnEvents;
}
/*******
** getEventsForToday
**
** retrieves the Calendar events for today.
**
** @returns {Event []} list of Calendar Events
*/
function getEventsForToday() {
var returnEvents = [];
var calendars, pageToken;
// set the lower bound at midnight
var today1 = new Date();
today1.setHours(0,0,0);
// set the upper bound at 23:59:59
var today2 = new Date();
today2.setHours(23, 59, 59);
// Create ISO strings to pass to Calendar API
var ds1 = today1.toISOString();
var ds2 = today2.toISOString();
// loop through all Calendars to get events
do {
calendars = Calendar.CalendarList.list({
maxResults: 100,
pageToken: pageToken
});
if (calendars.items && calendars.items.length > 0) {
for (var i = 0; i < calendars.items.length; i++) {
var calendar = calendars.items[i];
var tempResult = Calendar.Events.list(calendar.id, {singleEvents: true, timeMin: ds1, timeMax: ds2});
returnEvents = returnEvents.concat(tempResult.items);
}
}
else {
Logger.log('No calendars found.');
}
pageToken = calendars.nextPageToken;
} while (pageToken);
// Get the events
return returnEvents;
}
/*******
** buildEventsHTML
**
** given a set of calendar events, build an HTML list.
**
** @returns {string} string of HTML representing the events
*/
function buildEventsHTML(calEvents) {
var str="";
str += "<ul>";
for (var i=0; i < calEvents.length; i++) {
// Gotcha! All-day events don't have a dateTime, just a date, so need to check
var dateStr = convertDate(calEvents[i].start.dateTime ?
calEvents[i].start.dateTime :
calEvents[i].start.date).toLocaleString();
str += "<li><a href='" + calEvents[i].htmlLink + "'>" +
calEvents[i].summary + "</a> " + dateStr + "</li>";
}
str += "</ul>";
return str;
}
/*******
** convertDate
**
** Converts an ISO Date string into a JavaScript Date Object.
**
** @param {string} t The date string to parse. Can be either a full datetime or just a date
** @returns {Date} newly constructed Date object
*/
function convertDate(tStr) {
var dateTimeRE = /(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)([+\-]\d+):(\d+)/;
var dateRE = /(\d+)-(\d+)-(\d+)/;
var match = tStr.match(dateTimeRE);
if (!match)
match = tStr.match(dateRE);
var nums = [];
if (match) {
for (var i = 1; i < match.length; i++) {
nums.push(parseInt(match[i], 10));
}
if (match.length > 4) {
// YYYY-MM-DDTHH:MM:SS
return(new Date(nums[0], nums[1] - 1, nums[2], nums[3], nums[4], nums[5]));
}
else {
// YYYY-MM-DD
return(new Date(nums[0], nums[1] - 1, nums[2]));
}
}
else return null;
}