├── .clasp.json ├── Logging.js ├── .claspignore ├── ExtMattermost.js ├── Vars.js ├── appsscript.json ├── Test.js ├── package.json ├── README.md ├── Web Handler.js ├── ExtCalendar.js ├── Storage.js └── Commands.js /.clasp.json: -------------------------------------------------------------------------------- 1 | {"scriptId":"1c7fngRh-mLe0bFSNyQBGUgMiOt3ANpzcR0Z6exCMQppnM1Fagug0bxZX"} 2 | -------------------------------------------------------------------------------- /Logging.js: -------------------------------------------------------------------------------- 1 | function doLog(toLog) { 2 | // Doc used during development.... 3 | var doc = DocumentApp.openById(GDOC_ID_LOG); 4 | var body = doc.getBody(); 5 | body.appendParagraph(toLog) 6 | Logger.log(toLog); 7 | } -------------------------------------------------------------------------------- /.claspignore: -------------------------------------------------------------------------------- 1 | # ignore all files… 2 | **/** 3 | 4 | # except the extensions… 5 | !appsscript.json 6 | !**/*.gs 7 | !**/*.js 8 | !**/*.ts 9 | !**/*.html 10 | 11 | # ignore even valid files if in… 12 | .git/** 13 | node_modules/** -------------------------------------------------------------------------------- /ExtMattermost.js: -------------------------------------------------------------------------------- 1 | function sendToMattermost(message) { 2 | var url = MM_SEND_HOOK; 3 | var options = { 4 | "method": "post", 5 | "contentType": "application/json", 6 | "payload": JSON.stringify( 7 | { 8 | "text": message, 9 | }) 10 | }; 11 | var response = UrlFetchApp.fetch(url, options); 12 | } -------------------------------------------------------------------------------- /Vars.js: -------------------------------------------------------------------------------- 1 | var MM_TOKEN = PropertiesService.getScriptProperties().getProperty('SECRET_MM_TOKEN'); 2 | var MM_SEND_HOOK = PropertiesService.getScriptProperties().getProperty('SECRET_MM_SEND_HOOK'); 3 | var GDOC_ID_LOG = PropertiesService.getScriptProperties().getProperty('SECRET_GDOC_ID_LOG'); 4 | var GCAL_ID = PropertiesService.getScriptProperties().getProperty('SECRET_GCAL_ID'); 5 | -------------------------------------------------------------------------------- /appsscript.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeZone": "Europe/London", 3 | "dependencies": { 4 | "enabledAdvancedServices": [{ 5 | "userSymbol": "Calendar", 6 | "serviceId": "calendar", 7 | "version": "v3" 8 | }] 9 | }, 10 | "webapp": { 11 | "access": "ANYONE_ANONYMOUS", 12 | "executeAs": "USER_DEPLOYING" 13 | }, 14 | "exceptionLogging": "STACKDRIVER", 15 | "runtimeVersion": "V8" 16 | } -------------------------------------------------------------------------------- /Test.js: -------------------------------------------------------------------------------- 1 | // This method can be run to pretend you have written a command in MatterMost (triggering the webhook) 2 | function doTest(){ 3 | //var text = '!register adam.shorland@wikimedia.de'; 4 | //var text = '!coffee 10:00->11:00'; 5 | //var text = '!cleanup' 6 | var text = '!soon thar' 7 | var user = 'adsh' 8 | 9 | doPost({ 10 | parameter: { 11 | text: text, 12 | trigger_word: text.split(' ')[0], 13 | user_name: user, 14 | token: MM_TOKEN 15 | } 16 | }) 17 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coffeebot", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "dependencies": {}, 7 | "devDependencies": {}, 8 | "scripts": { 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/wmde/coffeebot.git" 14 | }, 15 | "author": "", 16 | "license": "ISC", 17 | "bugs": { 18 | "url": "https://github.com/wmde/coffeebot/issues" 19 | }, 20 | "homepage": "https://github.com/wmde/coffeebot#readme" 21 | } 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # coffeebot 2 | 3 | https://github.com/google/clasp 4 | 5 | `npm install -g @google/clasp` 6 | 7 | `clasp login` 8 | 9 | https://script.google.com/home/usersettings 10 | 11 | ## Get latest code from the scripts site? 12 | 13 | This can be used incase development has happened on script.google.com 14 | 15 | `clasp pull` 16 | 17 | ## Push code from repo to scirpt site 18 | 19 | This can be used in case you have merged a PR in the git repo 20 | 21 | `clasp push` 22 | 23 | ## Deploy the latest code that has been pushed to the site 24 | 25 | The needs to be retrieved from script.google.com 26 | 27 | `clasp deploy -i ` -------------------------------------------------------------------------------- /Web Handler.js: -------------------------------------------------------------------------------- 1 | // Handlers for the "Web app" part of this project. 2 | // This is called by mattermost 3 | 4 | // TODO probably turn this get off? 5 | function doGet(e){ 6 | doPost(e) 7 | } 8 | 9 | function doPost(e){ 10 | if(e.parameter['token']!==MM_TOKEN){ 11 | doLog("Called with bad token") 12 | return; 13 | } 14 | 15 | var triggerWord = e.parameter['trigger_word'] 16 | var mmUser = e.parameter['user_name'] 17 | var triggerWords = e.parameter['text'].split(' ') 18 | 19 | switch (triggerWord) { 20 | // REMEMBER!!! Whenever a new trigger word is added here, you need to add it to the hook in the mattermost UI 21 | case '!coffee': 22 | cmdCoffee(mmUser, triggerWords[1]) 23 | break; 24 | case '!now': 25 | cmdNow(mmUser, triggerWords[1]) 26 | break; 27 | case '!register': 28 | cmdRegister(mmUser, triggerWords[1]) 29 | break; 30 | case '!list': 31 | cmdList(triggerWords[1]) 32 | break; 33 | case '!cleanup': 34 | cmdCleanup() 35 | break; 36 | case '!soon': 37 | cmdSoon(mmUser, triggerWords[1]) 38 | break; 39 | case '!help': 40 | cmdHelp() 41 | break; 42 | default: 43 | doLog('No Command!') 44 | } 45 | } -------------------------------------------------------------------------------- /ExtCalendar.js: -------------------------------------------------------------------------------- 1 | function createCalEventNow( gEmail1, gEmail2 ) { 2 | var now = new Date() 3 | var end = new Date(now.getTime() + 30*60000) 4 | createCalEvent( gEmail1, gEmail2, now, end) 5 | } 6 | 7 | function createCalEvent( gEmail1, gEmail2, startTime, endTime ) { 8 | var jitsiUrl = 'https://jitsi.wikimedia.de/coffeebot-' + gEmail1 + '_' + gEmail2 9 | var event = CalendarApp.getCalendarById(GCAL_ID).createEvent('CoffeeBot Meeting', 10 | startTime, 11 | endTime, 12 | { 13 | guests: gEmail1 + "," + gEmail2, 14 | sendInvites: false, 15 | location: jitsiUrl 16 | } 17 | ); 18 | Logger.log('Created GCal Event ID: ' + event.getId()); 19 | return event 20 | } 21 | 22 | function findSoonestFreeTime( gEmail1, gEmail2 ) { 23 | var now = new Date((new Date()).toUTCString()); 24 | var response = Calendar.Freebusy.query({ 25 | timeMin: now.toISOString(), 26 | timeMax: ( new Date(now.getTime() + 120*60000) ).toISOString(), 27 | items: [ 28 | { id: gEmail1}, 29 | { id: gEmail2} 30 | ] 31 | }); 32 | var busyTimeArray = [].concat(response.calendars[gEmail1].busy, response.calendars[gEmail2].busy) 33 | console.log(busyTimeArray) 34 | for (let step = 0; step < 18; step++) { 35 | var trialStartTime = new Date(now.getTime() + step*5*60000); 36 | var trialEndTime = new Date(now.getTime() + step*5*60000 + 30*60000); 37 | var periodFree = true; 38 | for ( let busyPeriodIdx = 0; busyPeriodIdx < busyTimeArray.length; busyPeriodIdx++) { 39 | if (datesHaveAnyOverlap( trialStartTime, new Date( busyTimeArray[busyPeriodIdx].start ), trialEndTime, new Date (busyTimeArray[busyPeriodIdx].end ) )) { 40 | periodFree = false; 41 | } 42 | } 43 | if (periodFree) { 44 | return {start: trialStartTime, end: trialEndTime} 45 | } 46 | } 47 | return null 48 | } 49 | 50 | function datesHaveAnyOverlap( startTrial, startBusy, endTrial, endBusy) { 51 | return (!(endBusy < startTrial || startBusy > endTrial)) 52 | } 53 | 54 | function testRun() { 55 | var time = findSoonestFreeTime( 'thomas.arrow_ext@wikimedia.de', 'adam.shorland@wikimedia.de' ) 56 | console.log(time) 57 | } -------------------------------------------------------------------------------- /Storage.js: -------------------------------------------------------------------------------- 1 | // Crude storage, using the script property store service.... 2 | // TODO daily cleanup of the storage for coffee requests? 3 | 4 | var STORE_PREFIX_USER = 'USER_' 5 | var STORE_PREFIX_REQUEST = 'REQUEST_' 6 | 7 | /** 8 | ** Require Meetings 9 | **/ 10 | 11 | function coffeeRequestKey(mmUser){ 12 | return new Date().toLocaleDateString() + '@' + mmUser; 13 | } 14 | 15 | function storeCoffeeRequest(mmUser, from, to) { 16 | setProperty(STORE_PREFIX_REQUEST, coffeeRequestKey(mmUser), mmUser + "@" + from + "->" + to) 17 | } 18 | 19 | function getCoffeeRequests() { 20 | return getPropertiesWithPrefix(STORE_PREFIX_REQUEST) 21 | } 22 | 23 | function getCoffeeRequestsForToday() { 24 | return getPropertiesWithPrefix(STORE_PREFIX_REQUEST + new Date().toLocaleDateString()) 25 | } 26 | 27 | function deleteNonTodayRequests() { 28 | return deletePropertiesWithPrefixWithoutPrefix(STORE_PREFIX_REQUEST, STORE_PREFIX_REQUEST + new Date().toLocaleDateString()) 29 | } 30 | 31 | /** 32 | ** Users 33 | **/ 34 | 35 | function registerMmUser( mmUser, gEmail ){ 36 | setProperty(STORE_PREFIX_USER, mmUser, gEmail) 37 | } 38 | 39 | function isMmUserRegistered(mmUser){ 40 | return getProperty(STORE_PREFIX_USER, mmUser) !== null 41 | } 42 | 43 | function getEmailFromMmUser( mmUser ){ 44 | return getProperty(STORE_PREFIX_USER, mmUser) 45 | } 46 | 47 | function getRegisteredUserEmails() { 48 | return getPropertiesWithPrefix('USER_') 49 | } 50 | 51 | /** 52 | ** General 53 | **/ 54 | 55 | function setProperty( prefix, key, value ){ 56 | var scriptProperties = PropertiesService.getScriptProperties(); 57 | scriptProperties.setProperty(prefix+key, value); 58 | } 59 | 60 | function getProperty( prefix, key, value ){ 61 | var scriptProperties = PropertiesService.getScriptProperties(); 62 | return scriptProperties.getProperty(prefix + key); 63 | } 64 | 65 | function getPropertiesWithPrefix(prefix){ 66 | var scriptProperties = PropertiesService.getScriptProperties(); 67 | var allPropKeys = scriptProperties.getKeys(); 68 | var values = []; 69 | allPropKeys.forEach(function (item, index) { 70 | if( item.startsWith(prefix) ) { 71 | values.push(scriptProperties.getProperty(item)); 72 | } 73 | }); 74 | return values; 75 | } 76 | 77 | function deletePropertiesWithPrefixWithoutPrefix(hasPrefix, doesntHavePrefix){ 78 | var scriptProperties = PropertiesService.getScriptProperties(); 79 | var cleaned = 0; 80 | scriptProperties.getKeys().forEach(function (item, index) { 81 | if( item.startsWith(hasPrefix) && item.startsWith(doesntHavePrefix) === false ) { 82 | scriptProperties.deleteProperty(item) 83 | cleaned++ 84 | } 85 | }); 86 | return cleaned; 87 | } -------------------------------------------------------------------------------- /Commands.js: -------------------------------------------------------------------------------- 1 | // Each method here maps to a command that you can use within Mattermost 2 | 3 | function cmdHelp() { 4 | sendToMattermost("Available commands: !help !list !register !coffee !cleanup" ) 5 | } 6 | 7 | function cmdRegister( mmUser, gEmail ) { 8 | registerMmUser(mmUser, gEmail) 9 | sendToMattermost(mmUser + " has been registered with the email " + gEmail + ".") 10 | } 11 | 12 | function cmdCoffee( mmUser, requiredTimeSlot ) { 13 | if(!isMmUserRegistered(mmUser)) { 14 | sendToMattermost("You (" + mmUser + ') are not registered.') 15 | return; 16 | } 17 | 18 | var splitTime = requiredTimeSlot.split('->') 19 | var from = splitTime[0] 20 | var to = splitTime[1] 21 | 22 | // TODO Implement doing coffee 23 | storeCoffeeRequest(mmUser, from, to) 24 | sendToMattermost("You (" + mmUser + ') are registered and requesting coffee between ' + from + " and " + to + " (Berlin time).") 25 | } 26 | 27 | function cmdSoon( mmUser, otherUser ) { 28 | if(!isMmUserRegistered(mmUser)) { 29 | sendToMattermost("You (" + mmUser + ') are not registered.') 30 | return; 31 | } 32 | if(!isMmUserRegistered(otherUser)) { 33 | sendToMattermost("The user you're requesting coffee with (" + otherUser + ') is not registered.') 34 | return; 35 | } 36 | var time = findSoonestFreeTime(getEmailFromMmUser(mmUser), getEmailFromMmUser(otherUser)) 37 | if (!time) { 38 | sendToMattermost(mmUser + ", you have no overlapping free time with " + otherUser + " in the next two hours") 39 | } else { 40 | var event = createCalEvent(getEmailFromMmUser(mmUser), getEmailFromMmUser(otherUser), time.start, time.end) 41 | } 42 | sendToMattermost(mmUser + ", you have created a 30-minute coffee call with " + otherUser + "starting at "+ time.start) 43 | } 44 | 45 | function cmdSoon( mmUser, otherUser ) { 46 | if(!isMmUserRegistered(mmUser)) { 47 | sendToMattermost("You (" + mmUser + ') are not registered') 48 | return; 49 | } 50 | if(!isMmUserRegistered(otherUser)) { 51 | sendToMattermost("The user you want to coffee with (" + otherUser + ') is not registered') 52 | return; 53 | } 54 | var time = findSoonestFreeTime(getEmailFromMmUser(mmUser), getEmailFromMmUser(otherUser)) 55 | if (!time) { 56 | sendToMattermost(mmUser + ", you have no overlapping free time with" + otherUser + "in the next two hours") 57 | } else { 58 | var event = createCalEvent(getEmailFromMmUser(mmUser), getEmailFromMmUser(otherUser), time.start, time.end) 59 | } 60 | sendToMattermost("@" + mmUser + ", you have created a coffee call for 30 mins with @" + otherUser + " starting at "+ time.start) 61 | } 62 | 63 | function cmdNow( mmUser, otherUser ) { 64 | if(!isMmUserRegistered(mmUser)) { 65 | sendToMattermost("You (" + mmUser + ') are not registered.') 66 | return; 67 | } 68 | if(!isMmUserRegistered(otherUser)) { 69 | sendToMattermost("The user you're requesting coffee with (" + otherUser + ') is not registered.') 70 | return; 71 | } 72 | 73 | var event = createCalEventNow(getEmailFromMmUser(mmUser), getEmailFromMmUser(otherUser)) 74 | 75 | sendToMattermost("@" + mmUser + ", you have created a 30-minute coffee call with @" + otherUser + " ( TBA link to call)") 76 | //sendToMattermost(mmUser + ", you have created a coffee call for 30 mins with " + otherUser + " (" + event.getLocation() + ")") 77 | } 78 | 79 | function cmdCleanup(){ 80 | var deleted = deleteNonTodayRequests() 81 | sendToMattermost("Deleted " + deleted + " old requests." ) 82 | } 83 | 84 | function cmdList(what) { 85 | switch (what) { 86 | case 'users': 87 | sendToMattermost("The following users are registered:\n" + getRegisteredUserEmails().join("\n") ) 88 | break; 89 | case 'today': 90 | sendToMattermost("Today's active coffee requests:\n" + getCoffeeRequestsForToday().join("\n") ) 91 | break; 92 | case 'requests': 93 | sendToMattermost("All existing coffee requests:\n" + getCoffeeRequests().join("\n") ) 94 | break; 95 | default: 96 | sendToMattermost('Don\'t know how to list that! Valid arguments: users, today, requests.') 97 | } 98 | } 99 | --------------------------------------------------------------------------------