├── .gitignore ├── package.json ├── meetup-api.js ├── license.md ├── cli.js ├── readme.md ├── Code_of_Conduct.md └── google-calendar-auth.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | # OS generated files # 4 | ###################### 5 | .DS_Store 6 | .DS_Store? 7 | ._* 8 | .Spotlight-V100 9 | .Trashes 10 | ehthumbs.db 11 | Thumbs.db 12 | 13 | 14 | 15 | # IDE 16 | ###################### 17 | .idea 18 | 19 | 20 | # project datas 21 | ###################### 22 | googleApi_clientSecret.json -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "meetsy", 3 | "version": "1.0.2", 4 | "description": "Sync your Meetup events with Google Calendar", 5 | "bin": "./cli.js", 6 | "author": "Felicitas Kugland", 7 | "license": "MIT", 8 | "dependencies": { 9 | "google-auth-library": "^0.10.0", 10 | "googleapis": "^19.0.0", 11 | "meow": "^3.7.0", 12 | "node-fetch": "^1.7.2" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/kotzendekrabbe/meetsy.git" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/kotzendekrabbe/meetsy/issues" 20 | }, 21 | "homepage": "https://github.com/kotzendekrabbe/meetsy#readme" 22 | } 23 | -------------------------------------------------------------------------------- /meetup-api.js: -------------------------------------------------------------------------------- 1 | var fetch = require('node-fetch'); 2 | 3 | module.exports = { 4 | fetchMeetups: function(meetupApiKey){ 5 | var meetupEvents = []; 6 | return fetch('https://api.meetup.com/self/calendar?key='+ meetupApiKey +'&page=30') 7 | .then(function(res) { 8 | return res.json(); 9 | }) 10 | .then(function(json) { 11 | for(var key in json) { 12 | var venue = 'Needs a location'; 13 | var date = new Date(json[key].time); 14 | var dateEnd = new Date(json[key].time); 15 | dateEnd.setHours(dateEnd.getHours() + 3); 16 | 17 | 18 | if(json[key].venue) { 19 | venue = json[key].venue['name'] + 20 | ', ' + json[key].venue['address_1'] + 21 | ', ' + json[key].venue['city']; 22 | } 23 | 24 | meetupEvents.push({ 25 | "start" : {'dateTime' : date.toISOString()}, 26 | 'end': { 'dateTime': dateEnd.toISOString()}, 27 | "summary" : json[key].name, 28 | "location": venue, 29 | "description": json[key].link 30 | }); 31 | } 32 | 33 | return meetupEvents; 34 | }) 35 | .catch(function(err){ 36 | console.log('error: ', err); 37 | }) 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 SinnerSchrader Deutschland GmbH and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var meow = require('meow'); 4 | var fetchMeetupEventData = require('./meetup-api'); 5 | var googleCalAuth = require('./google-calendar-auth'); 6 | 7 | var calID; 8 | var googleConnect; 9 | var meetupApiKey; 10 | var clientSecret; 11 | 12 | var cli = meow(` 13 | Usage 14 | $ node meetup-calendar-sync.js 15 | 16 | Options 17 | --meetupApiKey meetup Api url of your account 18 | --calID google calendar ID (sinnerschrader.com_un89alcfa2f0orh9bmnhpdosic@group.calendar.google.com) 19 | --secret google API client Secret json 20 | 21 | Examples 22 | $ foo unicorns --rainbow 23 | unicorns 24 | `); 25 | 26 | 27 | function fetchMeetupEvent(meetupApiKey){ 28 | return fetchMeetupEventData.fetchMeetups(meetupApiKey) 29 | .then(function(meetupEvents){ 30 | return meetupEvents; 31 | }); 32 | } 33 | 34 | function compareEvents(meetupEvent, calEvents) { 35 | // Check which event already exist and which one not. 36 | // unique identifier is URL in description 37 | 38 | return meetupEvent.filter(i => !calEvents.find(e => e.description.match(/\bhttps?:\/\/\S+/gi) == i.description)); 39 | 40 | } 41 | 42 | 43 | function main(opts) { 44 | calID = opts.calID; 45 | meetupApiKey = opts.meetupApiKey; 46 | googleConnect = googleCalAuth.connect(calID, opts.secret); 47 | 48 | return googleConnect; 49 | } 50 | 51 | main(cli.flags) 52 | .then(function(googleConnect){ 53 | return fetchMeetupEvent(meetupApiKey).then(function(meetup){ 54 | 55 | if(googleConnect.length > 0){ 56 | return compareEvents(meetup, googleConnect); 57 | } 58 | else { 59 | meetup.forEach(event => googleCalAuth.insertEvent(event)); 60 | return 0; 61 | } 62 | }); 63 | }).then(function(existNot){ 64 | if (!Array.isArray(existNot)) { 65 | return; 66 | } 67 | if(existNot.length === 0){ 68 | console.log('Everything is up to date'); 69 | } 70 | else { 71 | existNot.forEach(event => googleCalAuth.insertEvent(event)); 72 | } 73 | }) 74 | .catch(function(err){ 75 | console.log('Ups - something went wrong ', err); 76 | }); 77 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | > Sync your Meetup events with Google Calendar 2 | 3 | # meetsy # 4 | 5 | Is a CLI tool which syncs your meetup events with your (google) calendar. 6 | It could help you to have a better overview on upcoming events. 7 | You can also sync it with a shared google calendar with others, 8 | e.g.: you can create a google calendar for your company to share Meetup events with your colleagues. 9 | 10 | 11 | ## At the moment it's only a MVP: ## 12 | * get the next 30 meetup events 13 | * get the next 100 google events 14 | * compare events with each other, if there are events which are not existing insert these into the calendar 15 | 16 | ## What you need ## 17 | * google calendar for the meetup events 18 | * [meetup account](https://meetup.com/) 19 | * [npm and node](https://www.npmjs.com/get-np) 20 | 21 | --------------- 22 | 23 | 24 | ## Installation ## 25 | 26 | ### Install meetsy ### 27 | 28 | ```node 29 | npm install -g meetsy 30 | ``` 31 | 32 | ## Setup instructions ## 33 | 34 | ### Google calendar ### 35 | https://developers.google.com/google-apps/calendar/quickstart/nodejs 36 | 37 | * follow the instructions from the link above on step 1 to get your `client_secret.json` 38 | * save this JSON file anywhere for later use as --secret (remember the path to the file!) 39 | * * in the meetsy example it's in the project root and named as googleApi_clientSecret.json 40 | 41 | --------------- 42 | 43 | 44 | ## Usage instructions ## 45 | 46 | ```node 47 | meetsy --calID 'yourGoogleCalenderID' --meetupApiKey 'yourMeetupApiKey' --secret './googleApi_clientSecret.json' 48 | ``` 49 | 50 | *Note:* On first use you have to authorize the calender tool by a URL shown 51 | in your terminal. 52 | 53 | 54 | ### How to get your Google calendar ID/address ### 55 | https://support.google.com/calendar/answer/37083#link 56 | 57 | 58 | ### How to get your meetup api key ### 59 | https://secure.meetup.com/de-DE/meetup_api/key/ 60 | 61 | 62 | 63 | --------------- 64 | 65 | 66 | ## Next features ## 67 | [See Issues](https://github.com/kotzendekrabbe/meetsy/issues?q=is%3Aissue+is%3Aopen+label%3Afeature) 68 | 69 | 70 | 71 | ## Contribute ## 72 | Feel free to dive in! Open an 73 | [issue](https://github.com/kotzendekrabbe/meetsy/issues/new) or 74 | submit a [Pull Request](https://github.com/kotzendekrabbe/meetsy/compare). ❤️ 75 | 76 | meetsy follows the [Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md). 77 | 78 | --------------- 79 | 80 | Copyright 2017 by SinnerSchrader Deutschland GmbH and contributors. 81 | Released under the MIT license. 82 | -------------------------------------------------------------------------------- /Code_of_Conduct.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /google-calendar-auth.js: -------------------------------------------------------------------------------- 1 | var fetch = require('node-fetch'); 2 | var fs = require('fs'); 3 | var readline = require('readline'); 4 | var google = require('googleapis'); 5 | var googleAuth = require('google-auth-library'); 6 | 7 | // If modifying these scopes, delete your previously saved credentials 8 | // at ~/.credentials/calendar-nodejs-quickstart.json 9 | var SCOPES = ['https://www.googleapis.com/auth/calendar']; 10 | var TOKEN_DIR = (process.env.HOME || process.env.HOMEPATH || 11 | process.env.USERPROFILE) + '/.credentials/'; 12 | var TOKEN_PATH = TOKEN_DIR + 'calendar-nodejs-quickstart.json'; 13 | 14 | var calendar = google.calendar('v3'); 15 | 16 | var authGoogle; 17 | var googleCalID; 18 | 19 | 20 | fs.readFileAsync = function (filename) { 21 | return new Promise(function (resolve, reject) { 22 | try { 23 | fs.readFile(filename, function(err, buffer){ 24 | if (err) reject(err); else resolve(buffer); 25 | }); 26 | } catch (err) { 27 | reject(err); 28 | } 29 | }); 30 | }; 31 | 32 | var authorize = function(credentials){ 33 | var clientSecret = credentials.installed.client_secret; 34 | var clientId = credentials.installed.client_id; 35 | var redirectUrl = credentials.installed.redirect_uris[0]; 36 | var auth = new googleAuth(); 37 | var oauth2Client = new auth.OAuth2(clientId, clientSecret, redirectUrl); 38 | 39 | return fs.readFileAsync(TOKEN_PATH).then(function (token){ 40 | oauth2Client.credentials = JSON.parse(token); 41 | return oauth2Client; 42 | }).catch(function () { 43 | return getNewToken(oauth2Client); 44 | }); 45 | }; 46 | 47 | var getNewToken = function(oauth2Client){ 48 | return new Promise(function (resolve, reject) { 49 | var authUrl = oauth2Client.generateAuthUrl({ 50 | access_type: 'offline', 51 | scope: SCOPES 52 | }); 53 | 54 | var readlineI = readline.createInterface({ 55 | input: process.stdin, 56 | output: process.stdout 57 | }); 58 | 59 | 60 | console.log('Authorize this app by visiting this url: ', authUrl); 61 | 62 | 63 | readlineI.question('Enter the code from that page here: ', function(code) { 64 | readlineI.close(); 65 | oauth2Client.getToken(code, function(err, token) { 66 | if (err) { 67 | console.log('Error while trying to retrieve access token', err); 68 | return; 69 | } 70 | oauth2Client.credentials = token; 71 | storeToken(token); 72 | return oauth2Client; 73 | }); 74 | }); 75 | }); 76 | }; 77 | 78 | var storeToken = function(token) { 79 | try { 80 | fs.mkdirSync(TOKEN_DIR); 81 | } catch (err) { 82 | if (err.code != 'EXIST') { 83 | throw err; 84 | } 85 | } 86 | fs.writeFile(TOKEN_PATH, JSON.stringify(token)); 87 | }; 88 | 89 | var getEvents = function(auth, calID) { 90 | return new Promise(function(resolve,reject){ 91 | calendar.events.list({ 92 | auth: auth, 93 | calendarId: calID, 94 | timeMin: (new Date()).toISOString(), 95 | maxResults: 100, 96 | singleEvents: true, 97 | orderBy: 'startTime' 98 | }, function(err, response) { 99 | if(err) reject(err); 100 | else resolve(response.items); 101 | }); 102 | }); 103 | }; 104 | 105 | function insertEvent(eventData, auth){ 106 | calendar.events.insert({ 107 | auth: auth, 108 | calendarId: googleCalID, 109 | resource: eventData 110 | }, function(err) { 111 | if (err) { 112 | console.log('The API returned an error: ' + err + ' '); 113 | return; 114 | } 115 | console.log('Event created: ' + eventData.start.dateTime + ' ' + eventData.summary); 116 | }); 117 | } 118 | 119 | 120 | module.exports = { 121 | connect: function(calID, clientSecret){ 122 | return fs.readFileAsync(clientSecret).then(function (content){ 123 | googleCalID = calID; 124 | return authorize(JSON.parse(content)).then(function(auth){ 125 | authGoogle = auth; 126 | return getEvents(auth, calID); 127 | }); 128 | 129 | }).catch(function (err) { 130 | console.log('Error loading client secret file: ' + err); 131 | return; 132 | }); 133 | }, 134 | insertEvent: function(eventData){ 135 | return insertEvent(eventData, authGoogle); 136 | } 137 | }; 138 | --------------------------------------------------------------------------------