├── Procfile ├── .gitignore ├── example_config.json ├── package.json ├── mac_daemon.js ├── README.md ├── app.js ├── googleplus.js └── LICENSE /Procfile: -------------------------------------------------------------------------------- 1 | web: node app.js -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | config.json 3 | *.png -------------------------------------------------------------------------------- /example_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "buffer_conf": { 3 | "service": "linkedin", 4 | "access_token": "" 5 | }, 6 | "google_conf": { 7 | "email": "YOUREMAILHERE", 8 | "password": "YOURPASSWORDHERE" 9 | } 10 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "googleplusapi", 3 | "description": "Node application to watch buffer and post to google+", 4 | "version": "0.0.1", 5 | "dependencies": { 6 | "request": "2.27.0", 7 | "underscore": "~1.5.1", 8 | "node-mac": "~0.1.3" 9 | }, 10 | "engines": { 11 | "node": "0.10.x", 12 | "npm": "1.2.x" 13 | }, 14 | "scripts": { 15 | "start": "node app.js" 16 | }, 17 | "readmeFilename": "README.md", 18 | "main": "app.js", 19 | "devDependencies": {}, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/altryne/google-plus-api.git" 23 | }, 24 | "keywords": [ 25 | "node", 26 | "googleplus", 27 | "api", 28 | "buffer" 29 | ], 30 | "author": "Alex Volkov", 31 | "license": "BSD" 32 | } 33 | -------------------------------------------------------------------------------- /mac_daemon.js: -------------------------------------------------------------------------------- 1 | var Service = require('node-mac').Service; 2 | var exec = require('child_process').exec; 3 | 4 | // Create a new service object 5 | var svc = new Service({ 6 | name: 'Buffer to Google+', 7 | description: 'Daemon waiting on buffer to post something and cross-post it to google+', 8 | script: 'app.js', 9 | env: [{ 10 | name: "PATH", 11 | value: process.env["PATH"] // service is now able to access the user who created its home directory 12 | },{ 13 | name : "WORKING_DIR", 14 | value: require('path').join(__dirname) 15 | }] 16 | }); 17 | 18 | // Listen for the "install" event, which indicates the 19 | // process is available as a service. 20 | svc.on('install', function () { 21 | svc.start(); 22 | }); 23 | 24 | svc.on('uninstall', function () { 25 | console.log('Uninstall complete.'); 26 | }); 27 | 28 | svc.on('alreadyinstalled', function () { 29 | svc.restart(); 30 | }); 31 | 32 | var args = process.argv.splice(2); 33 | 34 | //svc.install(); 35 | switch(args[0]){ 36 | case "install": 37 | svc.install(); 38 | break; 39 | case "uninstall": 40 | svc.uninstall(); 41 | break; 42 | case "restart": 43 | svc.restart(); 44 | break; 45 | case "test": 46 | console.log('HERE BE DRAGONS'); 47 | console.log(process.env["WORKING_DIR"]); 48 | break; 49 | default: 50 | console.log('Please run with install,uninstall,restart args'); 51 | break; 52 | } 53 | 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Buffer to Google+ via casperjs/phantomjs 2 | 3 | >A nodejs script/daemon that will connect with Buffer API and post to google+ when new updates were submitted via buffer 4 | # 5 | 6 | 7 | 8 | ##How does this work? 9 | If you've ever wondered why buffer does not share to google+, it's because google+ doesn't have an publicly available API. 10 | 11 | Using Casper.js, the script opens google+ sharing page in a hidden(headless) browser, and posts the update received from buffer API. 12 | 13 | 14 | ##Requirenments 15 | 16 | - Node.js 17 | - Phantomjs 18 | - Casperjs 19 | 20 | ##Setup 21 | - Clone the repo 22 | - Make sure you have casperjs on your path - ``casperjs --version`` 23 | - Install npm requirenments - ``npm install`` 24 | - Obtain a buffer __Access Token__ 25 | - Go here : [Buffer Developer site](https://bufferapp.com/developers/apps/create) 26 | - Create an example app, and copy the __Access Token__ you receive 27 | - Wait 2 min and go to [http://bufferapp.com/developers/apps](http://bufferapp.com/developers/apps) to obtain your Access Token. 28 | - Save __example_config.json__ as __config.json__ and edit it with your Access Token, Google+ Username and password. 29 | - In the __service__ field, choose to which of your buffer accounts this script will listen. Can be one of `twitter`, `facebook`, `linkedin` 30 | 31 | 32 | ```json 33 | { 34 | "buffer_conf": { 35 | "service": "linkedin", 36 | "access_token": "BUFFER_ACCESS_TOKEN" 37 | }, 38 | "google_conf": { 39 | "email": "YOUREMAILHERE", 40 | "password": "YOURPASSWORDHERE" 41 | } 42 | } 43 | ``` 44 | 45 | - This script requires your google __username/password__, but as it's running locally, it's not a HUGE security risk, just a mild one. 46 | 47 | ##Running the server 48 | ```bash 49 | node app.js 50 | ``` 51 | 52 | ##Installing as a service on mac osx 53 | ``` 54 | sudo node mac_daemon.js install 55 | ``` 56 | This will install the node server as a damon on your mac, that should work and reset itself on fails / restarts. 57 | This requires sudo priveleges. 58 | 59 | After service is Installed, logs can be viewed in ``/Library/Logs/Buffer to Google+/buffertogoogle.log`` 60 | 61 | To uninstall the server ``sudo node mac_daemon.js uninstall`` 62 | 63 | ##Why? 64 | This was done as a proof of concept. Getting Casperjs script to post to google was challenging and it was fun. 65 | 66 | ##Known Issues 67 | Because this is a hack, and google+ doesn't have a public API, some issues arrose. 68 | 69 | - Doesn't work if you have 2 step authentication enabled 70 | - Doesn't work on remote servers, I initially wanted this to run on heroku, can't be done 71 | - Has to run from any location google already seen you logging in 72 | 73 | ##Desclaimer 74 | WHile this code doesn't do anything harmful like saving your password (check for youself, it's all here), it doesn't stop someone from accessing your computer and reading the config.json. 75 | ###USE AT YOUR OWN RISK 76 | I'm not responsible for any damages that using this script may provide you. 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var sys = require('sys') 2 | var fs = require('fs'); 3 | var exec = require('child_process').exec; 4 | var request = require('request'); 5 | var _ = require('underscore'); 6 | 7 | try { 8 | var config = require('./config'); 9 | } catch (e) { 10 | console.log('Please check your required config file'); 11 | process.exit(1) 12 | } 13 | var updates = []; 14 | var last_buffer_fetch_utc = parseInt(+Date.now() / 1000); 15 | init(); 16 | 17 | console.log('Starting monitoring, go to buffer and post something, you will see it appear here'); 18 | 19 | function init() { 20 | if (!config.buffer_conf.profile_id) { 21 | console.log('Getting buffer profile id'); 22 | updateBufferProfileID(); 23 | } else { 24 | 25 | } 26 | getBufferUpdates(); 27 | setInterval(getBufferUpdates,1000 * 60) 28 | 29 | postUpdatedToGoogle(); 30 | setInterval(postUpdatedToGoogle,1000 * 5) 31 | } 32 | 33 | function getBufferUpdates(is_first_time){ 34 | if(!config.buffer_conf.profile_id) return; 35 | var params = {since : last_buffer_fetch_utc ,count:5}; 36 | url = 'profiles/' + config.buffer_conf.profile_id + '/updates/sent.json'; 37 | bufferAPI(url,function(body){ 38 | if(body.updates.legth > 0){ 39 | console.log('Got ' + body.total + ' updates from buffer, number of new : ' + body.updates.length); 40 | } 41 | updates = updates.concat(body.updates); 42 | last_buffer_fetch_utc = parseInt(+Date.now() / 1000); 43 | },params) 44 | } 45 | 46 | function postUpdatedToGoogle(){ 47 | if(updates.length < 1){ 48 | // console.log('Nothing to post'); 49 | return 50 | } 51 | _.each(updates,function(el,i,list){ 52 | console.log('Posting update to google+ : ' ,el.text); 53 | work_dir = (process.env["WORKING_DIR"])? process.env["WORKING_DIR"] : require('path').join(__dirname); 54 | exec('cd ' +work_dir+';casperjs googleplus.js --cookies-file=cookies.txt --username='+ config.google_conf.email +' --password='+config.google_conf.password+' --text="'+ el.text +'"',function(e,stdout,stderr){ 55 | sys.puts(stdout); 56 | console.log(e); 57 | }); 58 | list.splice(i, 1); 59 | }) 60 | 61 | } 62 | 63 | function updateBufferProfileID() { 64 | //get buffer profile id and save to conf 65 | bufferAPI('profiles.json',function(body){ 66 | arr = body; 67 | config.buffer_conf.profile_id = _.findWhere(arr, {'service': config.buffer_conf.service}).id 68 | console.log('Buffer profile ID is ' + config.buffer_conf.profile_id); 69 | fs.writeFile('./config.json', JSON.stringify(config, null, 4), function (err) { 70 | if (err) { 71 | console.log(err); 72 | } else { 73 | console.log("JSON saved"); 74 | } 75 | }); 76 | }) 77 | } 78 | 79 | function bufferAPI(url,callback,params){ 80 | params = params || {} 81 | params_str = "&"; 82 | 83 | if(_.size(params) > 0){ 84 | for (var key in params) { 85 | if (params_str != "") { 86 | params_str += "&"; 87 | } 88 | params_str += key + "=" + params[key]; 89 | } 90 | } 91 | url = 'https://api.bufferapp.com/1/' + url + '?access_token=' + config.buffer_conf.access_token + params_str; 92 | // console.log('Buffer api called', url, params); 93 | request(url,function(error,response,body){ 94 | if (!error && response.statusCode == 200) { 95 | callback(JSON.parse(body)) 96 | }else{ 97 | console.log('There was an error with buffer api',error); 98 | } 99 | }); 100 | } 101 | 102 | function puts(error, stdout, stderr) { 103 | sys.puts(stdout) 104 | } 105 | -------------------------------------------------------------------------------- /googleplus.js: -------------------------------------------------------------------------------- 1 | /*** 2 | * Google+ unofficial post api 3 | * Usage : casperjs googleplus.js --username=YOURUSERNAME --password=YOURGOOGLEPASSWORD --text="whatever text you want to post" 4 | * google is not sure this is you, so either provide a verify email or verify phone 5 | * This needs to be done 1 time 6 | * 7 | * Note : 8 | */ 9 | 10 | var casper = require('casper').create( 11 | { verbose: false, logLevel: "warning"} 12 | ); 13 | /** 14 | * Get the variables from the cli call 15 | * @type {*} 16 | */ 17 | username = casper.cli.get("username") 18 | password = casper.cli.get("password") 19 | text = casper.cli.get("text") 20 | 21 | casper.start('https://www.google.com/ncr', function () { 22 | this.viewport(800, 600); 23 | console.info('Starting Casper'); 24 | 25 | if (! (this.getHTML().indexOf(username) > -1)) { 26 | console.info('## Login ...'); 27 | this.thenOpen('https://accounts.google.com/ServiceLogin?hl=en&continue=https://www.google.com/ncr', function() { 28 | this.fillSelectors('form#gaia_loginform', { 29 | 'input[name="Email"]': username, 30 | 'input[name="Passwd"]': password 31 | }, true); 32 | }); 33 | } 34 | }); 35 | 36 | casper.then(function CheckIfGoogleWantsVerification(){ 37 | _url = this.getCurrentUrl(); 38 | if(_url.indexOf(/SmsAuthInterstitial/) > 0){ 39 | this.open('https://google.com/ncr') 40 | } 41 | },function(){ 42 | console.warn('Oops something went wrong') 43 | },1000) 44 | 45 | casper.waitForUrl(/LoginVerification/g,function VerifyGoogleAccount(){ 46 | console.warn('google asked for login verification') 47 | // this.debugPage() 48 | challengetype_answer = (verify_email.length > 0) ? 'RecoveryEmailChallenge' : 'PhoneVerificationChallenge' ; 49 | this.fill('#challengeform',{ 50 | 'challengetype' : challengetype_answer, 51 | 'phoneNumber' : verify_phone, 52 | 'emailAnswer' : verify_email 53 | },true) 54 | },function(){ 55 | // console.warn('Google did not ask for verification') 56 | },1000) 57 | 58 | casper.waitForUrl(/google.com/g,function GoToGoogleCom(){ 59 | _url = this.getCurrentUrl(); 60 | if(_url.indexOf('ServiceLoginAuth') > 0){ 61 | casper.die('Please check your password in config.json and try again') 62 | } 63 | },function(){ 64 | 65 | },1000) 66 | 67 | casper.waitForSelector('a[href="https://plus.google.com/u/0/stream/all?hl=en"]',function(){ 68 | this.click('a[href="https://plus.google.com/u/0/stream/all?hl=en"]') 69 | }); 70 | 71 | casper.wait(4000); 72 | 73 | casper.withFrame('gbsf', function SubmitPostToGooglePlus() { 74 | this.click('div[guidedhelpid="sharebox_editor"]') 75 | this.sendKeys('div[guidedhelpid="sharebox_editor"] [role="textbox"]',text + '\r\n',{keepFocus: true}) 76 | this.wait(2000,function WaitingForShare(){ 77 | this.capture('shared_screenshot.png', {top: 0, left: 0, width: 960, height: 600 }); 78 | this.clickLabel('Share') 79 | }) 80 | 81 | this.wait(5000,function WaitingForGoogleToSubmit(){ 82 | casper.warn('Posted to google+ !') 83 | }) 84 | }); 85 | 86 | casper.on('load.failed', function() { 87 | console.log('Could not load webpage.'); 88 | this.capture('error_1.png', {top: 0, left: 0, width: 800, height: 600 }); 89 | this.exit(); 90 | }); 91 | 92 | casper.on('error', function(msg, backtrace) { 93 | console.log('Error: ' + msg); 94 | this.debugPage(); 95 | this.capture('error_2.png', {top: 0, left: 0, width: 800, height: 600 }); 96 | this.exit(); 97 | }); 98 | 99 | casper.on('timeout', function() { 100 | this.echo('PAGE TIMEOUTED!') 101 | console.log('The webpage timed out.'); 102 | this.debugPage(); 103 | this.capture('error_3.png', {top: 0, left: 0, width: 800, height: 600 }); 104 | this.exit(); 105 | }); 106 | 107 | casper.on('step.timeout', function() { 108 | this.echo('PAGE TIMEOUTED!') 109 | this.debugPage(); 110 | this.capture('error_4.png', {top: 0, left: 0, width: 800, height: 600 }); 111 | this.exit(); 112 | }); 113 | 114 | casper.on('step.error', function(err) { 115 | this.debugPage(); 116 | this.capture('error_6.png', {top: 0, left: 0, width: 800, height: 600 }); 117 | this.die("Step has failed: " + err); 118 | }); 119 | 120 | casper.on('complete.error', function() { 121 | console.log('Something went wrong!'); 122 | this.debugPage(); 123 | this.capture('error_7.png', {top: 0, left: 0, width: 800, height: 600 }); 124 | this.exit(); 125 | }); 126 | 127 | casper.on('page.error', function(msg, backtrace) { 128 | console.log('There was an error loading the webpage.'); 129 | this.debugPage(); 130 | this.capture('error_8.png', {top: 0, left: 0, width: 800, height: 600 }); 131 | this.exit(); 132 | }); 133 | 134 | 135 | casper.run(function() { 136 | // echo results in some pretty fashion 137 | this.exit() 138 | }); 139 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright [yyyy] [name of copyright owner] 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | --------------------------------------------------------------------------------