├── exporter.png ├── img ├── icon128.png ├── icon16.png ├── icon48.png ├── csv-icon.gif ├── gmail-logo.jpg └── ajax-loader.gif ├── .gitignore ├── js ├── settings.js ├── exporter.js ├── options.js ├── database.js ├── csv_exporter.js ├── grabber.js ├── content_script.js ├── controller.js ├── gmail_exporter.js └── lib │ ├── chrome_ex_oauthsimple.js │ ├── chrome_ex_oauth.js │ └── jquery-1.5.2.min.js ├── chrome_ex_oauth.html ├── manifest.json ├── LICENSE ├── options.html ├── css ├── options.css └── controller.css ├── README.md ├── controller.html └── background.html /exporter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamedmansour/fb-exporter/HEAD/exporter.png -------------------------------------------------------------------------------- /img/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamedmansour/fb-exporter/HEAD/img/icon128.png -------------------------------------------------------------------------------- /img/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamedmansour/fb-exporter/HEAD/img/icon16.png -------------------------------------------------------------------------------- /img/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamedmansour/fb-exporter/HEAD/img/icon48.png -------------------------------------------------------------------------------- /img/csv-icon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamedmansour/fb-exporter/HEAD/img/csv-icon.gif -------------------------------------------------------------------------------- /img/gmail-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamedmansour/fb-exporter/HEAD/img/gmail-logo.jpg -------------------------------------------------------------------------------- /img/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mohamedmansour/fb-exporter/HEAD/img/ajax-loader.gif -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source # 2 | ################### 3 | *.com 4 | *.class 5 | *.dll 6 | *.exe 7 | *.o 8 | *.so 9 | *.pyc 10 | 11 | # Logs and databases # 12 | ###################### 13 | *.log 14 | 15 | # OS generated files # 16 | ###################### 17 | .DS_Store* 18 | ehthumbs.db 19 | Icon? 20 | Thumbs.db 21 | -------------------------------------------------------------------------------- /js/settings.js: -------------------------------------------------------------------------------- 1 | // Global Settings. Getters and Setters. 2 | settings = { 3 | get version() { 4 | return localStorage['version']; 5 | }, 6 | set version(val) { 7 | localStorage['version'] = val; 8 | }, 9 | get opt_out() { 10 | var key = localStorage['opt_out']; 11 | return (typeof key == 'undefined') ? false : key === 'true'; 12 | }, 13 | set opt_out(val) { 14 | localStorage['opt_out'] = val; 15 | } 16 | }; -------------------------------------------------------------------------------- /js/exporter.js: -------------------------------------------------------------------------------- 1 | Exporter = {}; 2 | 3 | Exporter.getScreenNameType = function(screenname) { 4 | switch (screenname) { 5 | case '(Google Talk)': return 'GOOGLE_TALK'; 6 | case '(Skype)': return 'SKYPE'; 7 | case '(AIM)': return 'AIM'; 8 | case '(Windows Live Messenger)': return 'MSN'; 9 | case '(Yahoo! Messenger)': 10 | case '(Yahoo Japan)': return 'YAHOO'; 11 | case '(QQ)': return 'QQ'; 12 | case '(ICQ)': return 'ICQ'; 13 | } 14 | return null; 15 | }; 16 | 17 | Exporter.getPhoneType = function(phone) { 18 | return (phone != '(Mobile)') ? 'mobile' : 'other'; 19 | }; -------------------------------------------------------------------------------- /chrome_ex_oauth.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | OAuth Redirect Page 10 | 16 | 17 | 18 | 23 | 24 | 25 | Redirecting... 26 | 27 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Facebook Friend Exporter", 3 | "description": "Get *your* data contact out of Facebook to Google Contacts or CSV, whether they want you to or not.", 4 | "version": "2.2.0", 5 | "background_page": "background.html", 6 | "options_page": "options.html", 7 | "icons": { 8 | "16": "img/icon16.png", 9 | "48": "img/icon48.png", 10 | "128": "img/icon128.png" 11 | }, 12 | "content_scripts": [ { 13 | "js": [ "/js/lib/jquery-1.5.2.min.js", "/js/content_script.js" ], 14 | "matches": [ 15 | "http://*.facebook.com/*", 16 | "https://*.facebook.com/*" 17 | ], 18 | "run_at": "document_end" 19 | } ], 20 | "permissions": [ 21 | "unlimitedStorage", 22 | "tabs", 23 | "https://m.facebook.com/*", 24 | "http://m.facebook.com/*", 25 | "http://www.google.com/m8/feeds/*", 26 | "https://www.google.com/accounts/OAuthAuthorizeToken", 27 | "https://www.google.com/accounts/OAuthGetAccessToken" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2010 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 13 | all 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 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /js/options.js: -------------------------------------------------------------------------------- 1 | // Extensions pages can all have access to the bacground page. 2 | var bkg = chrome.extension.getBackgroundPage(); 3 | 4 | // When the DOM is loaded, make sure all the saved info is restored. 5 | window.addEventListener('load', onLoad, false); 6 | 7 | /** 8 | * Helper. 9 | */ 10 | function $(id) { 11 | return document.getElementById(id); 12 | } 13 | 14 | /** 15 | * When the options window has been loaded. 16 | */ 17 | function onLoad() { 18 | onRestore(); 19 | $('button-save').addEventListener('click', onSave, false); 20 | $('button-close').addEventListener('click', onClose, false); 21 | } 22 | 23 | /** 24 | * When the options window is closed; 25 | */ 26 | function onClose() { 27 | window.close(); 28 | } 29 | 30 | /** 31 | * Saves options to localStorage. 32 | */ 33 | function onSave() { 34 | // Save settings. 35 | bkg.settings.opt_out = $('opt_out').checked; 36 | 37 | // Update status to let user know options were saved. 38 | var info = $('info-message'); 39 | info.style.display = 'inline'; 40 | info.style.opacity = 1; 41 | setTimeout(function() { 42 | info.style.opacity = 0.0; 43 | }, 1000); 44 | } 45 | 46 | /** 47 | * Restore all options. 48 | */ 49 | function onRestore() { 50 | // Restore settings. 51 | $('version').innerHTML = ' (v' + bkg.settings.version + ')'; 52 | $('opt_out').checked = bkg.settings.opt_out; 53 | } 54 | -------------------------------------------------------------------------------- /options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Facebook Friend Exporter 5 | 6 | 7 | 8 | 9 |
10 |
11 | 14 |

15 | Make sure you save the settings after your done editing! 16 | 18 | Visit extension page 19 | , 20 | 22 | File bugs and suggestions 23 |

24 | You should follow me on Twitter @mohamedmansour 25 |

26 |
27 |
Misc
28 |
29 |

If you would like to be notified of major upgrades to this extension, 30 | please make sure the box below is unchecked. You wont be spammed.

31 |
32 |
Opt-Out of future notifications:
33 |
34 | 35 |
36 |
37 |
38 |
39 | 44 |
45 | Extension developed by Mohamed Mansour, 46 | Source Code available in GitHub 47 |
48 |
49 |
50 | 51 | 52 | -------------------------------------------------------------------------------- /css/options.css: -------------------------------------------------------------------------------- 1 | /* Extension Options Default Style */ 2 | 3 | body { 4 | min-width: 650px; 5 | font-size: 87%; 6 | font-family: Arial, Helvetica, 'Helvetica Neue', Verdana, sans-serif; 7 | margin: 10px; 8 | } 9 | 10 | #header h1 { 11 | background: url("/img/icon48.png") 0px 30px no-repeat; 12 | display: inline; 13 | margin: 0; 14 | padding-bottom: 43px; 15 | padding-left: 55px; 16 | padding-top: 40px; 17 | font-size: 150%; 18 | font-weight: bold; 19 | } 20 | 21 | h3 { 22 | color: #444; 23 | } 24 | 25 | em { 26 | font-size: 50%; 27 | font-weight: normal; 28 | } 29 | 30 | dt { 31 | width: 15em; 32 | padding: .5em; 33 | float: left; 34 | margin: 0; 35 | } 36 | 37 | dd { 38 | margin-left: 16em; 39 | padding: .4em; 40 | } 41 | 42 | a { 43 | color: blue; 44 | font-size: 100%; 45 | } 46 | 47 | p { 48 | margin: 0; 49 | padding: 0; 50 | } 51 | 52 | .extension-template { 53 | border-bottom: 1px solid #cdcdcd; 54 | margin: 10px 0; 55 | } 56 | 57 | .extension-header { 58 | background: #ebeff9; 59 | border-top: 1px solid #b5c7de; 60 | padding: 3px 5px; 61 | font-weight: bold; 62 | } 63 | 64 | .extension-options { 65 | background: #f4f6fc; 66 | border-bottom: 1px solid #edeff5; 67 | font-size: 85%; 68 | padding: 0.8em 10px; 69 | } 70 | 71 | .note { 72 | background-color: #e0f2c0; 73 | border-radius: 5px; 74 | padding: 3px 5px; 75 | margin-right: 30px; 76 | } 77 | 78 | #extension-footer { 79 | text-align: right; 80 | padding: 10px 0px; 81 | } 82 | 83 | #header { 84 | margin-bottom: 1.05em; 85 | overflow: hidden; 86 | padding-bottom: 1.5em; 87 | padding-left: 0; 88 | padding-top: 1.5em; 89 | position: relative; 90 | } 91 | 92 | #info-message { 93 | -webkit-transition-duration: 1s; 94 | display: none; 95 | background:#FDFFBD none repeat scroll 0 0; 96 | border-radius: 5px; 97 | padding: 7px 10px; 98 | margin-right: 30px; 99 | } 100 | 101 | #credits { 102 | font-size: 85%; 103 | margin-top: 1em; 104 | text-align: right; 105 | } -------------------------------------------------------------------------------- /js/database.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Storage class responsible for managing the database 3 | * tansactions for friends 4 | */ 5 | FriendDB = function () { 6 | this.db = null; 7 | }; 8 | 9 | /** 10 | * Opens a connection to Web SQL table. 11 | */ 12 | FriendDB.prototype.open = function() { 13 | var db_size = 5 * 1024 * 1024; // 5MB 14 | this.db = openDatabase('Facebook Friend Export', '1.0', 'fb-export', db_size); 15 | }; 16 | 17 | /** 18 | * For simplicity, just show an alert when crazy error happens. 19 | */ 20 | FriendDB.prototype.onError = function(tx, e) { 21 | alert('Something unexpected happened: ' + e.message ); 22 | }; 23 | 24 | /** 25 | * Creats a table with the following columns: 26 | * id - Integer 27 | * data - String 28 | * ts - Timestamp 29 | */ 30 | FriendDB.prototype.createTable = function() { 31 | this.db.transaction(function(tx) { 32 | tx.executeSql('CREATE TABLE IF NOT EXISTS ' + 33 | 'friend(id INTEGER, data TEXT, ts DATETIME)', []); 34 | }); 35 | }; 36 | 37 | /** 38 | * Adds a |friend| to the database, the contents gets serialized to String. 39 | * Current time is tracked as well. 40 | */ 41 | FriendDB.prototype.persistFriend = function(friend, onSuccess) { 42 | var ts = new Date(); 43 | var data = JSON.stringify(friend); 44 | this.db.transaction(function(tx) { 45 | tx.executeSql('INSERT INTO friend(id, data, ts) VALUES (?,?,?)', 46 | [friend.id, data, ts], onSuccess, this.onError); 47 | }); 48 | }; 49 | 50 | /** 51 | * Retrieves a row from the table given |id|. The result goes to |response|. 52 | * This is an asynchronous action. 53 | */ 54 | FriendDB.prototype.getFriend = function(id, response) { 55 | this.db.transaction(function(tx) { 56 | tx.executeSql('SELECT data FROM friend WHERE id = ?', [id], 57 | function (tx, rs) { 58 | if (rs.rows.length != 0) { 59 | response({status: true, data: JSON.parse(rs.rows.item(0).data)}); 60 | } 61 | else { 62 | response({status: false}); 63 | } 64 | }, this.onError 65 | ); 66 | }); 67 | }; 68 | 69 | /** 70 | * Update the friend object. 71 | */ 72 | FriendDB.prototype.updateFriend = function(friend) { 73 | var ts = new Date(); 74 | var data = JSON.stringify(friend); 75 | this.db.transaction(function(tx) { 76 | tx.executeSql('UPDATE friend SET data = ?, ts = ? WHERE id = ?', 77 | [data, ts, friend.id], null, this.onError 78 | ); 79 | }); 80 | }; 81 | 82 | 83 | /** 84 | * Removes every row from the table 85 | */ 86 | FriendDB.prototype.clear = function() { 87 | this.db.transaction(function(tx) { 88 | tx.executeSql('DELETE FROM friend', [], null, this.onError); 89 | }); 90 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Facebook Exporter Contact Exporter 2 | ================================== 3 | 4 | ![Screenshot of the Chrome Extension](https://github.com/mohamedmansour/fb-exporter/raw/master/exporter.png) 5 | 6 | Your friends on Facebook made their information public to you, such as their 7 | email, phonenumber, and websites. They want you to know about them! But there 8 | is no way to extract such data and place it in your most used contact book so 9 | you can keep in touch with them. 10 | 11 | fb-exporter extension will allow you to get their contact information so you 12 | can contact them with your favorite email client. You can extract their 13 | information and transfer it to your client. 14 | 15 | Your not stealing their information, your friends are making it public for you 16 | to use them. They want you to contact them. Thats what friends do eh? 17 | 18 | Functionality 19 | ------------------- 20 | - I am reading your Facebook friends all at once in a variable. 21 | That variable allows us to instantly see the users photo in a nice collage, 22 | and export everyone at the same time without any manual process, or you can pick 23 | and choose anyone. 24 | 25 | - Not only exports your friends emails, it exports your contacts phone numbers, 26 | emails, end IM screen names such as Google Talk, Yahoo, ICQ, Skype, QQ, and MSN 27 | (Windows Live Messenger, Hotmail), and your friends birthday! 28 | 29 | - There are error handling routines as well, if Facebook doesn't like what your 30 | doing, it will stop instantaneously! You can start it back again next day or 31 | in a couple of hours. Just open up the log, and you can see errors when it happens. 32 | 33 | - The results are cached, so if you want to continue the process some other time, 34 | then no worries, it will continue from the last processed contact. 35 | 36 | - I have refactored some portions of the code and added comments where applicable 37 | I have added a better UI. For example, a Facbook style dialog instead of the 38 | custom one made before. I have changed the way the process works too. 39 | 40 | How to download 41 | --------------- 42 | - It is currently not available in the Chrome Web Store. I am decided to stop 43 | development to focus on better and more exciting things. 44 | 45 | Facebook 46 | ------------- 47 | They are converting emails into images so you cannot copy it somewhere (say what? 48 | 99% of the people copy the email into their email client). I added mechanism that catches 49 | that instantly. So you don't have to worry if they see something. The process will 50 | stop, and you can start it again the next day. When you start it again, it will pick up 51 | where it left off. 52 | 53 | 54 | Have fun! Feel free to submit patches and fork :) 55 | 56 | 57 | -------------------------------------------------------------------------------- /js/csv_exporter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Routines to handle csv exporting. 3 | */ 4 | CSVExporter = function(friends) { 5 | this.friends = friends; 6 | this.header = ['Name', 'E-mail Address 1', 'E-mail Address 2', 'E-mail Address 3', 7 | 'Phone 1', 'Phone 2', 8 | 'Google Talk', 'MSN', 'Skype', 'Yahoo', 9 | 'Website 1', 'Website 2', 'Website 3', 10 | 'Website Facebook', 'Home Address', 'Birthday' 11 | ]; 12 | this.dump = ''; 13 | }; 14 | 15 | /** 16 | * Start processing csv file. 17 | * @param {Function} callback to fire after each friend processed. 18 | */ 19 | CSVExporter.prototype.process = function(callback) { 20 | var csv_rows = []; 21 | var i = 0; 22 | var length = 0; 23 | 24 | for (var j = 0; j < this.friends.length; j++) { 25 | var friend = this.friends[j]; 26 | var csv_row = []; 27 | csv_row.push(friend.name); 28 | 29 | // Email parsing. 30 | for (i = 0; i < 3; i++){ 31 | this.addColumn_(csv_row, friend.emails[i]); 32 | } 33 | 34 | // Phones. 35 | for (i = 0; i < 2; i++){ 36 | this.addColumn_(csv_row, friend.phones[i]); 37 | } 38 | 39 | // IM Parsing just 4. 40 | this.addColumn_(csv_row, friend.im.gtalk); 41 | this.addColumn_(csv_row, friend.im.hotmail); 42 | this.addColumn_(csv_row, friend.im.skype); 43 | this.addColumn_(csv_row, friend.im.yahoo); 44 | 45 | // Website parsing. 46 | for (i = 0; i < 3; i++){ 47 | this.addColumn_(csv_row, friend.websites[i]); 48 | } 49 | 50 | // Friend FB parsing. 51 | this.addColumn_(csv_row, friend.fb); 52 | 53 | // Address parsing. 54 | this.addColumn_(csv_row, friend.address); 55 | 56 | // Birthday parsing. 57 | this.addColumn_(csv_row, friend.birthday); 58 | 59 | csv_rows.push(csv_row); 60 | 61 | // Callback to inform client 62 | callback({ 63 | finishedProcessingFriend: true, 64 | friend: friend, 65 | success: 1, 66 | message: "Added to CSV!" 67 | }); 68 | } 69 | 70 | this.dump = this.header.join(',') + '\n'; 71 | 72 | for (i = 0; i < csv_rows.length; i++) { 73 | this.dump += csv_rows[i].join(',') + '\n'; 74 | } 75 | }; 76 | 77 | /** 78 | * Get the dump CSV text. 79 | */ 80 | CSVExporter.prototype.getDump = function() { 81 | return this.dump; 82 | }; 83 | 84 | /** 85 | * Adds a column safely to each spot. It will wrap each column with quotes, and 86 | * escape quotes that exists within. 87 | * 88 | * @private 89 | * 90 | * @param {String[]} row A reference to a row that we need to push columns to. 91 | * @param {String} column A string that will be added to the row. 92 | */ 93 | CSVExporter.prototype.addColumn_ = function(row, column) { 94 | if (column) { 95 | row.push('"' + column.replace(/"/g, '\\"') + '"'); 96 | } else { 97 | row.push(''); 98 | } 99 | }; 100 | -------------------------------------------------------------------------------- /css/controller.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | font-family: Verdana; 4 | color: #fff; 5 | background-color: #ccc; 6 | } 7 | ::selection { 8 | background:#FF5E99; 9 | color:#fff; 10 | text-shadow: none; 11 | } 12 | a { 13 | color: #FF5E99; 14 | } 15 | .bubble { 16 | background-color: #444; 17 | margin: 15pt; 18 | border: thick solid; 19 | position: relative; 20 | border-radius: 10px; 21 | padding: 15pt; 22 | } 23 | 24 | p { 25 | margin: 10pt; 26 | } 27 | 28 | .friend-row { 29 | position: relative; 30 | } 31 | 32 | #note { 33 | background-color: #FBEC5D; 34 | color: #615E3F; 35 | border: 1px solid #FFE600; 36 | display: none; 37 | margin: 10px 0px; 38 | padding: 0px 10px; 39 | box-shadow: 0 0 5px 5px #333; 40 | } 41 | 42 | #friendlist { 43 | padding-top: 30px; 44 | } 45 | 46 | #friendlist li { 47 | display: inline-block; 48 | position: relative; 49 | } 50 | 51 | #friendlist li span { 52 | position: absolute; 53 | bottom: 0; 54 | left: 5px; 55 | height: 20px; 56 | width: 100px; 57 | background-color: rgba(0,0,0,0.4); 58 | border-radius: 10px; 59 | font-size: 8px; 60 | line-height: 20px; 61 | text-align: center; 62 | font-weight: bold; 63 | } 64 | 65 | #friendlist li.processed span { 66 | background-color: rgba(255,255,0,0.4); 67 | } 68 | 69 | #friendlist li.success span { 70 | background-color: rgba(0,255,0,0.4); 71 | } 72 | 73 | #friendlist li.failed span { 74 | background-color: rgba(255,0,0,0.4); 75 | } 76 | 77 | #friendlist li.cached span { 78 | background-color: rgba(255,0,255,0.4); 79 | } 80 | 81 | #friendlist li.starting span { 82 | background-color: rgba(255,140,0,0.4); 83 | } 84 | 85 | #friendlist li img { 86 | width: 80px; 87 | height: 80px; 88 | margin: 5px; 89 | border: 5px solid #444; 90 | } 91 | 92 | #friendlist li img:hover { 93 | cursor:pointer; 94 | border-color: #fff; 95 | border-radius: 10px; 96 | } 97 | 98 | #friendlist li.processed img:hover { 99 | border-color: #444; 100 | } 101 | 102 | .hilite { 103 | color: #266A2E; 104 | } 105 | 106 | .tos-guarded { 107 | } 108 | 109 | #friendlist, #step2, #step3, #step4 { 110 | display: none; 111 | } 112 | 113 | #ajax-loader { 114 | display: none; 115 | } 116 | 117 | #remaining-friend-count { 118 | font-size: 11pt; 119 | position: absolute; 120 | display: none; 121 | padding: 3pt; 122 | top: 0px; 123 | right: 0px; 124 | } 125 | 126 | .friend-detail { 127 | font-size: 13pt; 128 | } 129 | 130 | .checkbox { 131 | size: 18pt; 132 | position: absolute; 133 | top: 5px; 134 | left: 5px; 135 | } 136 | 137 | .export-method { 138 | font-size: 12pt; 139 | } 140 | 141 | .export-method input { 142 | border: outset; 143 | padding: 0pt; 144 | } 145 | 146 | .export-method input:active { 147 | border: inset; 148 | } 149 | 150 | #csv-popup { 151 | position: absolute; 152 | background-color: gray; 153 | top: 40px; 154 | bottom: 40px; 155 | right: 40px; 156 | left: 40px; 157 | padding: 12pt; 158 | border: thick solid; 159 | } 160 | 161 | #csv-popup a { 162 | position: absolute; 163 | top: 0px; 164 | right: 20px; 165 | } 166 | 167 | #csv-popup span { 168 | position: absolute; 169 | top: 0px; 170 | left: 20px; 171 | } 172 | 173 | #csv-popup textarea { 174 | position: absolute; 175 | top: 30px; 176 | left: 30px; 177 | right: 30px; 178 | bottom: 30px; 179 | 180 | font-size: 10pt; 181 | } 182 | 183 | #controls { 184 | position: absolute; 185 | top: 5px; 186 | left: 30px; 187 | } 188 | 189 | #log { 190 | width: 100%; 191 | height: 200px; 192 | } 193 | -------------------------------------------------------------------------------- /controller.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Facebook Friend Exporter 4 | 5 | 6 | 7 | 8 | 9 | 13 |
14 | Hi! 15 |

Couple of questions for you.

16 |
    17 |
  1. Does Facebook own your friends?
  2. 18 |
  3. Do you think Facebook owns the data about your friends, such as their 19 | email, their website, their IM name, etc. that they made public for you 20 | to see? 21 |
  4. 22 |
  5. Do you think your Facebook friends would mind if you, oh, we don't know, 23 | decided one day to email them, or visit their website? How about add them 24 | to your address book on your phone? 25 |
  6. 26 |
27 |

28 | If you answered "no" to the above questions, we can help you get your friends' 29 | contact info out of Facebook. We can export the info to your Google Contacts 30 | (GMail), among other things! 31 |

32 |

33 | Finally, this extension is open source. You can fork it now on GitHub! 34 | 35 | https://github.com/mohamedmansour/fb-exporter 36 | 37 |

38 |

39 | THIS CHROME EXTENSION IS NOT AFFILIATED WITH GOOGLE OR FACEBOOK. HERE IS A LINK 40 | TO THE FACEBOOK TERMS OF SERVICE. 41 |

42 |

43 | I have read the Terms of Service. 44 | 45 |

46 |
47 | 51 |
52 |

Fetching friend list... done. See below.

53 |

These are all the frends that will be exported, 54 | 55 | 56 |

57 |
58 | 64 |
65 |

Fetching friend details... this could take a while, especially if you're 66 | really popular!

67 |

IMPORTANT: Please don't touch your Facebook tab 68 | until we're done, thanks! If you really must have your 69 | Facebook fix right now, here you go (this will open in a new window). 71 |

72 |
73 | 77 |
78 |

All done! Please check the list below. Uncheck the friends you want to skip.

79 |

Now the fun part! Pick an export button below, and we'll get started 80 | copying your friend data there!

81 | 94 |
95 | 98 |
99 | 100 |
101 | 102 |
103 | 104 |
  • Processed
  • 105 |
  • Success
  • 106 |
  • Failed
  • 107 |
  • Cached
  • 108 |
    109 |
    110 |
    111 |

    NOTICE

    112 |

    You are logged off the mobile site. The mobile site is where we 113 | check your friends info. Please log back in and restart the process. 114 | The steps to follow are mentioned below:

    115 |
      116 |
    1. Log into http://m.facebook.com
    2. 117 |
    3. Close this window
    4. 118 |
    5. Visit http://facebook.com
    6. 119 |
    7. Click on the "Export Friends" and resume the process!
    8. 120 |
    121 |
    122 |
      123 |
    124 |
    125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /background.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 178 | 179 | -------------------------------------------------------------------------------- /js/grabber.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Profile Grabbers main responsibility to parse profiles data. 4 | */ 5 | ProfileGrabber = function() { 6 | this.domParser = new DOMParser(); 7 | // For some reason $.ajax doesn't work with Facebook. Anyhow, jQuery will 8 | // be remove in the future, too overkill for what we need. 9 | this.xhr = new XMLHttpRequest(); 10 | this.xhr.overrideMimeType('application/xml'); 11 | }; 12 | 13 | /** 14 | * Add a leading 0 if necessary. 15 | * 16 | * @param {number} num the number to format. 17 | * @return {string} formatted number with two digits. 18 | */ 19 | ProfileGrabber.prototype.twoDigitsFormat = function(num) { 20 | return (num < 10) ? '0'+ num : num; 21 | }; 22 | 23 | /** 24 | * Parses friends birthday given in format YYYY-MM-DD (with the year), 25 | * or --MM-DD (without the year). 26 | * 27 | * @param birthday string representation, ex: January 1st or January 1st, 2009. 28 | * @return correctly formatted birthday. 29 | */ 30 | ProfileGrabber.prototype.parseBirthday = function(birthday) { 31 | var valid_birthday = birthday.match(/^\w+\s\d+(?:,\s(\d{4}))?$/) 32 | if (valid_birthday) { 33 | var date = new Date(birthday); 34 | var month = this.twoDigitsFormat(date.getMonth() + 1); 35 | var day = this.twoDigitsFormat(date.getDate()); 36 | var year = '-'; 37 | if (valid_birthday[1]) { 38 | year = valid_birthday[1]; 39 | } 40 | birthday = year + '-' + month + '-' + day; 41 | } 42 | return birthday; 43 | }; 44 | 45 | /** 46 | * Parses friends facebook address... 47 | * Note, some users don't have a unique textual profile id, if thats the case, 48 | * the url can be constructed via unique id instead. 49 | * 50 | * @param {string} fb The FB unique profile. 51 | * @param {string} id The FB unique ID. 52 | * @return {string} the url for the FB page. 53 | */ 54 | ProfileGrabber.prototype.parseFacebookURL = function(fb, id) { 55 | if (fb == '') { 56 | fb = 'facebook.com/profile.php?id=' + id; 57 | } 58 | return 'http://' + fb; 59 | }; 60 | 61 | /** 62 | * Parses friends list of emails. 63 | * 64 | * @param {Array} emails An array of emails. 65 | * @return {Array} emails as an array of strings. 66 | */ 67 | ProfileGrabber.prototype.parseEmails = function(emails) { 68 | return emails.map(function() { 69 | return $(this).text(); 70 | }).get(); 71 | }; 72 | 73 | /** 74 | * Parses friends list of phones. 75 | * 76 | * @param {Array} phones An array of emails. 77 | * @return {Array} phones as an array of strings. 78 | */ 79 | ProfileGrabber.prototype.parsePhones = function(phones) { 80 | return phones.map(function() { 81 | return $(this).text(); 82 | }).get(); 83 | }; 84 | 85 | /** 86 | * Parses friends websites that they like to share. 87 | * This will remove the garbage from the beginning of the string if exists and 88 | * just extracts the href from each link. 89 | * 90 | * @param {Array} websites An array of websites as a DOM. 91 | * @return {Array} websites as an array of strings. 92 | */ 93 | ProfileGrabber.prototype.parseWebsites = function(websites) { 94 | return websites.map(function() { 95 | var url = decodeURIComponent($(this).attr('href')); 96 | // The patten extracts the URL from the following patterns if they match: 97 | // - A clean URL 98 | // - Prefixed with "/l.php?u=" stripped from URL. 99 | // - Request parameters in the form of ?h=\w+&refid=\d+ 100 | // - Request parameters in an irregular form of &h=\w+&refid=\d+ 101 | var pattern = /^(?:\/l.php\?u=)?(.*?)(?:(?:&|\?|&)h=\w+(?:&|&)refid=\d+)?$/; 102 | return url.replace(pattern, '$1'); 103 | }).get(); 104 | }; 105 | 106 | /** 107 | * Fetches the friends information live. 108 | * 109 | * NOTE TO FUTURE SELF: If this extension breaks, it's probably because the 110 | * following lines no longer work. Namely, the fragile selector access 111 | * being done below to get at the corresponding fields might need to be 112 | * updated. Look at the actual FB page in question to make the right 113 | * fixes. 114 | * 115 | * @param {object} friend FB's internal friend storage. 116 | * @param {string} url Friends FB page. 117 | * @param {Function} callback This is needed somehow to do synchronous loading. 118 | * once a profile been fetched, calls this callback. 119 | */ 120 | ProfileGrabber.prototype.extractInfo = function(friend, url, callback) { 121 | that = this; 122 | this.xhr.onload = function() { 123 | var dom = that.xhr.responseXML; 124 | // Some users have problem reading the response type for XML. Perhaps this 125 | // is due to the mangled headers or perhaps firewall denying access. 126 | // This is a small fix to read it from responseText since it should be 127 | // available then. 128 | if (!dom) { 129 | dom = that.domParser.parseFromString(that.xhr.responseText, 'application/xml'); 130 | } 131 | else { 132 | dom = $(dom); 133 | } 134 | 135 | // Check if your not logged into the mobile page. This hack is dangerous. 136 | // But it makes sure none of your contacts are invalid. A better approach 137 | // would be to do an XHR to the main page, but that will waste resources. 138 | if ($('*', dom).html().indexOf('"/login.php') != -1) { 139 | friend.error = true; 140 | callback(friend); 141 | } 142 | 143 | // This is the extreme case, I believe there is a bug in Google Chrome the 144 | // way it handles irregular Facebook pages. It just quits without indication 145 | // somehow the DOMParser fails and doesn't continue. 146 | // https://github.com/mohamedmansour/fb-exporter/issues/#issue/4 147 | // 148 | // Use jQuery to convert it automatically. This will throw and error on the 149 | // page every single time. I really hate this approach, but there is no way 150 | // to supress the error as far as I know without doing mucky stuff. 151 | if (!dom || dom.querySelector('parsererror')) { 152 | dom = $(that.xhr.responseText); 153 | } 154 | 155 | 156 | // To gather additional friend information, add the right selector here. 157 | var emails = $('td:last a', $('td.label:contains("Email")', dom).parent()); 158 | var fb = $('td:last', $('td.label:contains("Profile")', dom).parent()); 159 | var phones = $('td:last a', $('td.label:contains("Phone")', dom).parent()); 160 | var address = $('td:last', $('td.label:contains("Address")', dom).parent()); 161 | var skype = $('td:last', $('td.label:contains("Skype")', dom).parent()); 162 | var gtalk = $('td:last', $('td.label:contains("Google Talk")', dom).parent()); 163 | var hotmail = $('td:last', $('td.label:contains("Windows")', dom).parent()); 164 | var yahoo = $('td:last', $('td.label:contains("Yahoo! Messenger")', dom).parent()); 165 | var websites = $('td a', $('td.label:contains("Website")', dom).parent()); 166 | var birthday = $('td:last', $('td.label:contains("Birthday")', dom).parent()); 167 | 168 | // Storage for post processing. Cleanup and parse groups. 169 | friend.fb = that.parseFacebookURL(fb.text(), friend.id); 170 | friend.phones = that.parsePhones(phones); 171 | friend.address = address.text(); 172 | friend.birthday = that.parseBirthday(birthday.text()); 173 | friend.im = {}; 174 | friend.im.skype = skype.text(); 175 | friend.im.gtalk = gtalk.text(); 176 | friend.im.yahoo = gtalk.text(); 177 | friend.im.hotmail = hotmail.text(); 178 | friend.emails = that.parseEmails(emails); 179 | friend.websites = that.parseWebsites(websites); 180 | callback(friend); 181 | }; 182 | this.xhr.open('GET', url); 183 | this.xhr.send(null); 184 | }; 185 | -------------------------------------------------------------------------------- /js/content_script.js: -------------------------------------------------------------------------------- 1 | // Polling time. 2 | var PROFILE_POLL_DELAY=2000; 3 | 4 | // Communicate to both worlds, extension and website. 5 | var exportEvent = document.createEvent('Event'); 6 | exportEvent.initEvent('friendExported', true, true); 7 | 8 | // Just draw the export friends link on the top next to the other links. 9 | renderExportFriendsLink(); 10 | 11 | // Listen on extension requests. 12 | chrome.extension.onRequest.addListener(function(request, sender, sendResponse) { 13 | if(request.retrieveFriendsMap) { 14 | exportFacebookContacts(); 15 | } 16 | }); 17 | 18 | // Listen on the real DOM requests to check if friend has been exported. 19 | window.addEventListener('friendExported', function() { 20 | // Save the map to this content script world, so our extension can read it. 21 | var transferDOM = $('#fb-transfer-dom-area'); 22 | var friendsMap = JSON.parse(transferDOM.text()); 23 | 24 | // For testing, lets just view 2 users. 25 | if (0) { 26 | var i = 0; 27 | var testMap = {}; 28 | $.each(friendsMap, function(key, value) { 29 | if (i == 2) { 30 | return false; 31 | } 32 | testMap[key] = value; 33 | i++; 34 | }); 35 | delete friendsMap; 36 | friendsMap = testMap; 37 | } 38 | // Clean up since we no longer need this. 39 | $(transferDOM).remove(); 40 | $('#fb-inject-area').remove(); 41 | 42 | // Count the number of friends. 43 | var i = 0; 44 | $.each(friendsMap, function(key, value) { 45 | i++; 46 | }); 47 | 48 | // Tell the worker tab that we are set! 49 | chrome.extension.sendRequest({friendsListReceived: friendsMap}); 50 | chrome.extension.sendRequest({renderFriendsList: friendsMap, count: i}); 51 | }); 52 | 53 | /** 54 | * Switches back to the worker tab where you can see all your friends being 55 | * processed. 56 | */ 57 | function switchToWorkerTab() { 58 | // The extension will handle the case if the worker tab already exists. 59 | chrome.extension.sendRequest({switchToWorkerTab: 1}); 60 | } 61 | 62 | /** 63 | * Creates a custom Facebook lookalike dialog. 64 | * @param {Object} data A map with the following keys: 65 | * - id: The ID for this dialog. 66 | * - title: The title the appears on the header. 67 | * - message: The description of the dialog, appears middle. 68 | * - yes_text: The text for the yes button. 69 | * - yes_callback: The callback when yes clicked. 70 | * - cancel_callback: The callback when cancel clicked. 71 | * @returns {HTMLElement} The DOM of the dialog returned. 72 | */ 73 | function createFacebookDialog(data) { 74 | // Creates the confirm component. 75 | var lblConfirm = $('