Skip to content

Instantly share code, notes, and snippets.

@AntoineLemaire
Forked from SebastienTainon/DailyScrumHelper.gs
Last active April 20, 2020 08:22
Show Gist options
  • Save AntoineLemaire/cb5968cf01e1e3b3a11e4cb9eee8ecc5 to your computer and use it in GitHub Desktop.
Save AntoineLemaire/cb5968cf01e1e3b3a11e4cb9eee8ecc5 to your computer and use it in GitHub Desktop.
Daily Scrum Helper with integration with Jira, Github and Google Calendar API
// In Google Drive, create a new document of type Google Script. If it does not exist, use "Connect more apps" to enable Google Scripts
// Copy-paste this script into the content of the Google Script
// This script sends you at each execution an email with Github commits of last day, Jira modified tickets of last day, Google Calendar events of last day, and Google Calendar events of current day
// For Jira, just put your username and password. Use https://id.atlassian.com to get them, change password if necessary
// For Calendar access, in Google Script, go to Resources > Advanced Google Services and enable "Calendar API"
// For Github, it's complicated... you have to follow this tutorial: https://www.benlcollins.com/apps-script/oauth-github
// For Slack : Go to https://api.slack.com/legacy/custom-integrations/legacy-tokens, log in with your slack account, and refresh the page. You'll be able to get a Legacy Token. (This will end on May 5th, 2020, we need to move to Slack Apps...)
// Last step, make the script a daily cron: go to Edit > Current project's triggers and run "sendDailyReport" or "sendDailyReportSlack" every day between 5am and 6am
// You can run manually "sendDailyReportMail" or "sendDailyReportSlack" to test if it's working ;)
// Settings
var jiraUsername = ''; // Your Jira e-mail
var jiraApiToken = ''; // Your Jira API Token
var githubAuthorName = ''; // Your Github username
var githubAppClientId = ''; // Github client id (cf above, tutorial)
var githubAppClientSecret = ''; // Github client secret (cf above, tutorial)
var slackAccessToken = ''; // Slack Legacy Token.
var slackChannelId = ''; // Slack channel where you want your message be posted (can be found in URL of web app of Slack)
// Functions
function sendDailyReportMail() {
var date = new Date();
// If it's WE
if (date.getDay() === 6 || date.getDay() === 0) {
// Do nothing
return null;
}
// If it's monday
if (date.getDay() === 1) {
var yesterdayTextPreprosition = "of friday";
var yesterdayText = "friday";
var yesterdayDate = new Date(date.getTime() - 24 * 3 * 60 * 60 * 1000);
} else {
var yesterdayTextPreprosition = "of yesterday";
var yesterdayText = "yesterday";
var yesterdayDate = new Date(date.getTime() - 24 * 60 * 60 * 1000);
}
yesterdayDate.setHours(2, 0, 0);
var todayDate = new Date();
todayDate.setHours(2, 0, 0);
var tomorrowDate = new Date(date.getTime() + 24 * 60 * 60 * 1000);
tomorrowDate.setHours(2, 0, 0);
var content = '';
content += '<h2>Github\'s commits ' + yesterdayTextPreprosition + ' :</h2>';
var githubCommits = getGithubCommits(getFormattedDate(yesterdayDate), getFormattedDate(todayDate));
if (githubCommits.length === 0) {
content += '<p>Aucun</p>';
} else {
content += '<ul>';
for (var i = 0; i < githubCommits.length; i++) {
content += '<li>' + githubCommits[i] + '</li>';
}
content += '</ul>';
}
content += '<h2>JIRA tickets modified ' + yesterdayText + ' :</h2>';
var jiraEvents = getJiraEvents(yesterdayDate.toISOString().split('T')[0]);
if (jiraEvents.length === 0) {
content += '<p>Aucun</p>';
} else {
content += '<ul>';
for (var i = 0; i < jiraEvents.length; i++) {
content += '<li>' + jiraEvents[i] + '</li>';
}
content += '</ul>';
}
// Google Calendar: yesterday events
content += '<h2>' + yesterdayText.charAt(0).toUpperCase() + yesterdayText.slice(1) + '`s Events:</h2>';
var yesterdayEvents = getCalendarEvents(yesterdayDate.toISOString(), todayDate.toISOString());
if (yesterdayEvents.length === 0) {
content += '<p>Aucun</p>';
} else {
content += '<ul>';
for (var i = 0; i < yesterdayEvents.length; i++) {
content += '<li>' + yesterdayEvents[i] + '</li>';
}
content += '</ul>';
}
// Google Calendar: today events
content += '<h2>Today\'s Events :</h2>';
var todayEvents = getCalendarEvents(todayDate.toISOString(), tomorrowDate.toISOString());
if (todayEvents.length === 0) {
content += '<p>Aucun</p>';
} else {
content += '<ul>';
for (var i = 0; i < todayEvents.length; i++) {
content += '<li>' + todayEvents[i] + '</li>';
}
content += '</ul>';
}
if (githubCommits.length > 0 || jiraEvents.length > 0 || yesterdayEvents.length > 0 || todayEvents.length) {
// Send mail
var dateString = ('0' + date.getDate()).slice(-2) + '/' + ('0' + (date.getMonth() + 1)).slice(-2);
var email = Session.getActiveUser().getEmail();
var subject = 'Daily scrum ' + dateString;
var body = '<body><h1>Daily scrum ' + dateString + ' :</h1>' + content;
GmailApp.sendEmail(email, subject, body, {htmlBody: body});
}
}
function sendDailyReportSlack() {
var url = 'https://slack.com/api/chat.postMessage';
var headers = {
'Authorization': Utilities.formatString('Bearer %s', slackAccessToken),
'Content-type': 'application/json'
};
var data = {
'channel': slackChannelId,
'text': getSlackContentMessage()
};
var response = UrlFetchApp.fetch(url, {
'headers': headers,
'method': "POST",
'muteHttpExceptions': true,
'payload' : JSON.stringify(data)
});
Logger.log(response);
console.log(response);
}
function getSlackContentMessage() {
var date = new Date();
var dateString = ('0' + date.getDate()).slice(-2) + '/' + ('0' + (date.getMonth() + 1)).slice(-2);
// If it's WE
if (date.getDay() === 6 || date.getDay() === 0) {
// Do nothing
return null;
}
// If it's monday
if (date.getDay() === 1) {
var yesterdayTextPreprosition = "of friday";
var yesterdayText = "friday";
var yesterdayDate = new Date(date.getTime() - 24 * 3 * 60 * 60 * 1000);
} else {
var yesterdayTextPreprosition = "of yesterday";
var yesterdayText = "yesterday";
var yesterdayDate = new Date(date.getTime() - 24 * 60 * 60 * 1000);
}
yesterdayDate.setHours(2, 0, 0);
var todayDate = new Date();
todayDate.setHours(2, 0, 0);
var tomorrowDate = new Date(date.getTime() + 24 * 60 * 60 * 1000);
tomorrowDate.setHours(2, 0, 0);
var content = '*_Daily Scrum ' + dateString + '_*';
content += '\n*Github\'s commits ' + yesterdayTextPreprosition + ' :*\n';
var githubCommits = getGithubCommits(getFormattedDate(yesterdayDate), getFormattedDate(todayDate));
if (githubCommits.length === 0) {
content += 'Aucun\n';
} else {
for (var i = 0; i < githubCommits.length; i++) {
content += ' • ' + githubCommits[i] + '\n';
}
}
content += '\n*JIRA tickets modified ' + yesterdayText + ' :*\n';
var jiraEvents = getJiraEvents(yesterdayDate.toISOString().split('T')[0]);
if (jiraEvents.length === 0) {
content += 'Aucun';
} else {
for (var i = 0; i < jiraEvents.length; i++) {
content += ' • ' + jiraEvents[i] + '\n';
}
}
// Google Calendar: yesterday events
content += '\n*' + yesterdayText.charAt(0).toUpperCase() + yesterdayText.slice(1) + '`s Events:*\n';
var yesterdayEvents = getCalendarEvents(yesterdayDate.toISOString(), todayDate.toISOString());
if (yesterdayEvents.length === 0) {
content += 'Aucun';
} else {
for (var i = 0; i < yesterdayEvents.length; i++) {
content += ' • ' + yesterdayEvents[i] + '\n';
}
}
// Google Calendar: today events
content += '\n*Today\'s Events:*\n';
var todayEvents = getCalendarEvents(todayDate.toISOString(), tomorrowDate.toISOString());
if (todayEvents.length === 0) {
content += 'Aucun';
} else {
for (var i = 0; i < todayEvents.length; i++) {
content += ' • ' + todayEvents[i] + '\n';
}
}
if (githubCommits.length > 0 || jiraEvents.length > 0 || yesterdayEvents.length > 0 || todayEvents.length) {
return content;
}
return null;
}
function parseDate(string) {
var parts = string.split('T');
parts[0] = parts[0].replace(/-/g, '/');
parts[1] = parts[1].split('+')[0];
return new Date(parts.join(' '));
}
function getCalendarEvents(timeMin, timeMax) {
var calendarId = 'primary';
var events = Calendar.Events.list(calendarId, {
timeMin: timeMin,
timeMax: timeMax,
singleEvents: true,
orderBy: 'startTime',
maxResults: 10
});
console.log(events);
var finalEvents = [];
if (events.items && events.items.length > 0) {
for (var i = 0; i < events.items.length; i++) {
var event = events.items[i];
if (!event.start.date) {
var startString = Utilities.formatDate(parseDate(event.start.dateTime), 'Europe/Paris', 'HH:mm');
var endString = Utilities.formatDate(parseDate(event.end.dateTime), 'Europe/Paris', 'HH:mm');
console.log('%s (%s)', event.summary, startString);
finalEvents.push(Utilities.formatString('%s (%s - %s)', event.summary, startString, endString));
}
}
} else {
console.log('No events found.');
}
return finalEvents;
}
function getJiraEvents(timeMin) {
var baseURL = "https://yoopies.atlassian.net/rest/api/2/search";
var encCred = Utilities.base64Encode(jiraUsername + ":" + jiraApiToken);
var fetchArgs = {
contentType: "application/json",
headers: {"Authorization": "Basic " + encCred},
muteHttpExceptions: true
};
var issues = [];
console.log(timeMin);
var jql = "?jql=assignee = currentUser() AND updatedDate %3E= " + timeMin + " ORDER BY updated ASC";
var httpResponse = UrlFetchApp.fetch(baseURL + jql, fetchArgs);
if (httpResponse) {
var rspns = httpResponse.getResponseCode();
console.log(rspns);
switch (rspns) {
case 200:
var data = JSON.parse(httpResponse.getContentText());
console.log(data);
for (var id in data["issues"]) {
// Check the data is valid and the Jira fields exist
if (data["issues"][id] && data["issues"][id].fields) {
var summary = data["issues"][id].fields.summary;
var status = data['issues'][id].fields.status.name;
var key = data['issues'][id].key;
issues.push(Utilities.formatString('%s - %s (%s)', key, summary, status));
}
}
break;
case 404:
console.log("Jira 404, no item found");
break;
default:
console.log("Error: " + httpResponse.getContentText());
break;
}
} else {
console.log("Jira Error, unable to make requests to Jira!");
}
return issues;
}
function getGithubCommits(timeMin, timeMax) {
var service = getGithubService();
var commits = [];
var maybeAuthorized = service.hasAccess();
if (maybeAuthorized) {
console.log("App has access.");
var url = "https://api.github.com/search/commits?q=author-date:" + timeMin + ".." + timeMax + "+author:" + githubAuthorName + "&sort=author-date&order=desc";
console.log(url);
var accessToken = service.getAccessToken();
var headers = {
Authorization: Utilities.formatString('Bearer %s', accessToken),
Accept: 'application/vnd.github.cloak-preview'
};
console.log(headers);
var response = UrlFetchApp.fetch(url, {
'headers': headers,
'method': "GET",
'muteHttpExceptions': true
});
var code = response.getResponseCode();
if (code >= 200 && code < 300) {
var json = JSON.parse(response.getContentText());
console.log('LIST OF COMMITS');
for (var id in json.items) {
console.log(json.items[id]);
var message = json.items[id].commit.message;
if (message.indexOf("Merge branch") !== 0) {
var repository = json.items[id].repository.full_name;
var commit = Utilities.formatString('%s : %s', repository, message);
if (!commits.includes(commit)) {
commits.push(commit);
}
}
}
} else if (code == 401 || code == 403) {
// Not fully authorized for this action.
maybeAuthorized = false;
} else {
// Handle other response codes by logging them and throwing an
// exception.
console.log("Backend server error (%s): %s", code.toString(),
resp.getContentText("utf-8"));
throw ("Backend server error: " + code);
}
}
if (!maybeAuthorized) {
// Invoke the authorization flow using the default authorization
// prompt card.
var authorizationUrl = service.getAuthorizationUrl();
console.log(Utilities.formatString("Open the following URL and re-run the script: %s",
authorizationUrl));
console.log(Utilities.formatString("Open the following URL and re-run the script: %s",
authorizationUrl));
CardService.newAuthorizationException()
.setAuthorizationUrl(authorizationUrl)
.setResourceDisplayName("Display name to show to the user")
.throwException();
}
return commits;
}
function getGithubService() {
return OAuth2.createService('GitHub')
.setAuthorizationBaseUrl('https://github.com/login/oauth/authorize')
.setTokenUrl('https://github.com/login/oauth/access_token')
.setClientId(githubAppClientId)
.setClientSecret(githubAppClientSecret)
.setCallbackFunction('authCallback')
.setPropertyStore(PropertiesService.getUserProperties())
.setScope('user,repo');
}
// handle the callback
function authCallback(request) {
var githubService = getGithubService();
var isAuthorized = githubService.handleCallback(request);
if (isAuthorized) {
return HtmlService.createHtmlOutput('Success! You can close this tab.');
} else {
return HtmlService.createHtmlOutput('Denied. You can close this tab');
}
}
// Logs the redict URI to register
function logRedirectUri() {
var service = getGithubService();
console.log(service.getRedirectUri());
}
// Format date into YYYY-MM-DDTHH:MM:SS+00:00
function getFormattedDate(date) {
return date.getFullYear() + '-' + ('0' + (date.getMonth() + 1)).slice(-2) + '-' + ('0' + date.getDate()).slice(-2) + 'T' + ('0' + date.getHours()).slice(-2) + ':' + ('0' + date.getMinutes()).slice(-2) + ':' + ('0' + date.getSeconds()).slice(-2) + "%2B00:00"
}
// recreate Array.prototype.includes which does not exist in Google Script
if (!Array.prototype.includes) {
Object.defineProperty(Array.prototype, 'includes', {
value: function(searchElement, fromIndex) {
if (this == null) {
throw new TypeError('"this" is null or not defined');
}
// 1. Let O be ? ToObject(this value).
var o = Object(this);
// 2. Let len be ? ToLength(? Get(O, "length")).
var len = o.length >>> 0;
// 3. If len is 0, return false.
if (len === 0) {
return false;
}
// 4. Let n be ? ToInteger(fromIndex).
// (If fromIndex is undefined, this step produces the value 0.)
var n = fromIndex | 0;
// 5. If n ≥ 0, then
// a. Let k be n.
// 6. Else n < 0,
// a. Let k be len + n.
// b. If k < 0, let k be 0.
var k = Math.max(n >= 0 ? n : len - Math.abs(n), 0);
function sameValueZero(x, y) {
return x === y || (typeof x === 'number' && typeof y === 'number' && isNaN(x) && isNaN(y));
}
// 7. Repeat, while k < len
while (k < len) {
// a. Let elementK be the result of ? Get(O, ! ToString(k)).
// b. If SameValueZero(searchElement, elementK) is true, return true.
if (sameValueZero(o[k], searchElement)) {
return true;
}
// c. Increase k by 1.
k++;
}
// 8. Return false
return false;
}
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment