├── README.md ├── .gitignore ├── public ├── handle-intent.js ├── test-intent.html └── index.html ├── package.json ├── app.js └── contacts-db.js /README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /public/handle-intent.js: -------------------------------------------------------------------------------- 1 | 2 | $(function () { 3 | if ("intent" in window && intent.action) { 4 | $("#install").hide(); 5 | $("#pick").show(); 6 | // show the picker 7 | } 8 | else { 9 | $("#install").show(); 10 | $("#pick").hide(); 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "contacts-intent" 3 | , "description": "Experimental Contacts over Intents server for the OS X Address Book" 4 | , "version": "0.0.1" 5 | , "author": "Robin Berjon " 6 | , "dependencies": { 7 | "sqlite3": "*" 8 | , "mime-magic": "*" 9 | , "express": "2.5.5" 10 | } 11 | , "devDependencies": { 12 | } 13 | , "repository": "git://github.com/darobin/contacts-intent" 14 | , "main": "app" 15 | } 16 | -------------------------------------------------------------------------------- /public/test-intent.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Intent Test 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Contacts 6 | 7 | 8 | 9 |

Contacts

10 |
11 |

12 | You have now installed the Contacts Picker Intents. It should work from any page 13 | that calls it. 14 |

15 |
16 |
17 |

Pick your Contact

18 |
19 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 2 | var express = require("express") 3 | , cdb = require("./contacts-db") 4 | , app = module.exports = express.createServer() 5 | ; 6 | 7 | // configuration 8 | app.configure(function(){ 9 | app.use(express.bodyParser()); 10 | app.use(app.router); 11 | app.use(express.static(__dirname + "/public")); 12 | }); 13 | app.configure("development", function(){ 14 | app.use(express.errorHandler({ dumpExceptions: true, showStack: true })); 15 | }); 16 | app.configure("production", function(){ 17 | app.use(express.errorHandler()); 18 | }); 19 | 20 | // routes 21 | app.get("/", function (req, res, next) { 22 | res.sendfile("public/index.html"); 23 | }); 24 | 25 | app.get("/contacts-api", function (req, res, next) { 26 | // XXX use the query arguments to get the right data 27 | db.findContacts("robin", "*", function (err, res) { 28 | for (var i = 0, n = res.length; i < n; i++) console.log(res[i].toLiteralObject()); 29 | }); 30 | }); 31 | 32 | app.listen(4001); 33 | console.log("Express server listening on port %d in %s mode", app.address().port, app.settings.env); 34 | -------------------------------------------------------------------------------- /contacts-db.js: -------------------------------------------------------------------------------- 1 | 2 | var sqlite3 = require("sqlite3").verbose() 3 | , magic = require("mime-magic") 4 | , pth = require("path") 5 | , fs = require("fs") 6 | ; 7 | 8 | // some things to search on for matching (should add more) 9 | var recordSearchFields = "ZNAME ZNAMENORMALIZED ZNAME1 ZNICKNAME ZORGANIZATION ZMAIDENNAME " + 10 | "ZPHONETICFIRSTNAME ZPHONETICLASTNAME ZMIDDLENAME ZFIRSTNAME " + 11 | "ZPHONETICMIDDLENAME ZLASTNAME ZSORTINGFIRSTNAME ZSORTINGLASTNAME"; 12 | recordSearchFields = recordSearchFields.split(" "); 13 | 14 | function ContactsDB (path) { 15 | // XXX stupid default, change later 16 | this.path = path || "/Users/robin/Library/Application Support/AddressBook/AddressBook-v22.abcddb"; 17 | this.db = new sqlite3.Database(this.path, sqlite3.OPEN_READONLY); 18 | }; 19 | ContactsDB.prototype = { 20 | findContacts: function (search, populate, cb) { 21 | var sql = search ? 22 | "SELECT * FROM ZABCDRECORD WHERE " + recordSearchFields.join(" LIKE ? OR ") + " LIKE ?" 23 | : 24 | "SELECT * FROM ZABCDRECORD" 25 | ; 26 | var params = [], res = []; 27 | if (search) { 28 | for (var i = 0, n = recordSearchFields.length; i < n; i++) params.push("%" + search + "%"); 29 | } 30 | var self = this, got = 0, need = 0; 31 | this.db.all( 32 | sql 33 | , params 34 | , function (err, rows) { 35 | if (err) return console.log("ERROR: " + err); 36 | need = rows.length; 37 | for (var i = 0, n = rows.length; i < n; i++) { 38 | var row = rows[i]; 39 | new Contact(row, populate, self, function (contact) { 40 | res.push(contact); 41 | got++; 42 | if (got == need) cb(null, res); 43 | }); 44 | } 45 | } 46 | ); 47 | } 48 | 49 | , close: function () { 50 | this.db.close(); 51 | } 52 | }; 53 | 54 | var allFields = "displayName name nickname phoneNumbers emails addresses ims organizations birthday note photos categories urls".split(" "); 55 | function Contact (row, populate, cdb, cb) { 56 | this.db = cdb.db; 57 | // default values 58 | this._id = row.Z_PK; 59 | this.id = row.ZUNIQUEID; 60 | this.displayName = null; 61 | this.name = null; 62 | this.nickname = null; 63 | this.phoneNumbers = null; 64 | this.emails = null; 65 | this.addresses = null; 66 | this.ims = null; 67 | this.organizations = null; 68 | this.birthday = null; 69 | this.note = null; 70 | this.photos = null; 71 | this.categories = null; 72 | this.urls = null; 73 | 74 | populate = populate ? populate.concat([]) : []; 75 | if (populate == "*") populate = allFields.concat([]); 76 | 77 | var self = this, next = function () { 78 | var want = populate.shift(); 79 | if (want) { 80 | switch (want) { 81 | case "displayName": 82 | var full = []; 83 | if (row.ZFIRSTNAME) full.push(row.ZFIRSTNAME); 84 | if (row.ZLASTNAME) full.push(row.ZLASTNAME); 85 | self.displayName = full.join(" "); 86 | next(); 87 | break; 88 | case "name": 89 | self.name = { 90 | familyName: row.ZLASTNAME 91 | , givenName: row.ZFIRSTNAME 92 | , middleName: row.ZMIDDLENAME 93 | , honorificPrefix: row.ZTITLE 94 | , honorificSuffix: row.ZSUFFIX 95 | }; 96 | next(); 97 | break; 98 | case "nickname": 99 | self.nickname = row.ZNICKNAME; 100 | next(); 101 | break; 102 | case "phoneNumbers": 103 | self.populatePhoneNumbers(next); 104 | break; 105 | case "emails": 106 | self.populateEmails(next); 107 | break; 108 | case "addresses": 109 | self.populateAddresses(next); 110 | break; 111 | case "ims": 112 | self.populateIMs(next); 113 | break; 114 | case "organizations": 115 | if (row.ZORGANIZATION || row.ZDEPARTMENT || row.ZJOBTITLE) { 116 | self.organizations = [{ 117 | pref: !!row.ZORGANIZATION 118 | , type: null 119 | , name: row.ZORGANIZATION 120 | , department: row.ZDEPARTMENT 121 | , title: row.ZJOBTITLE 122 | }]; 123 | } 124 | next(); 125 | break; 126 | case "birthday": 127 | if (row.ZBIRTHDAYYEARLESS) { 128 | self.birthday = new Date(row.ZBIRTHDAYYEARLESS * 1000); 129 | if (row.ZBIRTHDAYYEAR) self.birthday.setFullYear(1 * row.ZBIRTHDAYYEAR); 130 | } 131 | next(); 132 | break; 133 | case "note": 134 | self.populateNote(row.ZNOTE, next); 135 | break; 136 | case "categories": 137 | // XXX there's a grouping system but we're not using it now 138 | next(); 139 | break; 140 | case "urls": 141 | self.populateURLs(next); 142 | break; 143 | case "photos": 144 | self.populatePhotos(cdb.path, next); 145 | break; 146 | default: 147 | next(); 148 | break; 149 | } 150 | } 151 | else { 152 | cb(self); 153 | } 154 | }; 155 | next(); 156 | } 157 | function _simpleField (self, cb, table, objField, tblField, cleaner) { 158 | cleaner = cleaner || function (val) { return val; }; 159 | self.db.all("select * from " + table + " where ZOWNER = ?", [self._id], function (err, rows) { 160 | if (err) return cb(); 161 | if (rows) self[objField] = []; 162 | for (var i = 0, n = rows.length; i < n; i++) { 163 | var row = rows[i]; 164 | self[objField].push({ 165 | pref: !!row.ZISPRIMARY 166 | , type: row.ZLABEL ? row.ZLABEL.replace(/[\W_]/g, "").toLowerCase() : "" 167 | , value: cleaner(row[tblField]) 168 | }); 169 | } 170 | cb(); 171 | }); 172 | } 173 | Contact.prototype = { 174 | populateEmails: function (cb) { 175 | _simpleField(this, cb, "ZABCDEMAILADDRESS", "emails", "ZADDRESSNORMALIZED", function (val) { 176 | return "mailto:" + val; 177 | }); 178 | } 179 | , populatePhoneNumbers: function (cb) { 180 | _simpleField(this, cb, "ZABCDPHONENUMBER", "phoneNumbers", "ZFULLNUMBER", function (val) { 181 | return "tel:" + val.replace(/[^+\d]/g, ""); 182 | }); 183 | } 184 | , populateAddresses: function (cb) { 185 | var self = this; 186 | self.db.all("select * from ZABCDPOSTALADDRESS where ZOWNER = ?", [self._id], function (err, rows) { 187 | if (err) return cb(); 188 | if (rows) self.addresses = []; 189 | for (var i = 0, n = rows.length; i < n; i++) { 190 | var row = rows[i]; 191 | self.addresses.push({ 192 | pref: !!row.ZISPRIMARY 193 | , type: row.ZLABEL ? row.ZLABEL.replace(/[\W_]/g, "").toLowerCase() : "" 194 | , streetAddress: row.ZSTREET 195 | , locality: row.ZCITY 196 | , region: row.ZSTATE 197 | , postalCode: row.ZZIPCODE 198 | , country: row.ZCOUNTRYNAME 199 | , xxxCountryCode: row.ZCOUNTRYCODE 200 | }); 201 | } 202 | cb(); 203 | }); 204 | } 205 | , populateIMs: function (cb) { 206 | _simpleField(this, cb, "ZABCDMESSAGINGADDRESS", "ims", "ZADDRESS"); 207 | } 208 | , populateURLs: function (cb) { 209 | _simpleField(this, cb, "ZABCDURLADDRESS", "urls", "ZURL"); 210 | } 211 | , populateNote: function (id, cb) { 212 | var self = this; 213 | self.db.all("select * from ZABCDNOTE where Z_PK = ?", [id], function (err, rows) { 214 | if (err) return cb(); 215 | var notes = []; 216 | for (var i = 0, n = rows.length; i < n; i++) notes.push(rows[i].ZTEXT); 217 | self.note = notes.join("\n"); 218 | cb(); 219 | }); 220 | } 221 | , populatePhotos: function (path, cb) { 222 | if (!path) return cb(); 223 | var id = this.id.replace(/:.*/, "") 224 | , dir = pth.dirname(path) 225 | , img = pth.join(dir, "Images", id) 226 | , gotImg; 227 | if (pth.existsSync(img)) gotImg = img; 228 | else if (pth.existsSync(img + ".jpeg")) gotImg = img + ".jpeg"; 229 | else return cb(); 230 | var self = this; 231 | magic.fileWrapper(gotImg, function (err, mime) { 232 | if (err) mime = ""; 233 | fs.readFile(gotImg, function (err, data) { 234 | if (err) return cb(); 235 | self.photos = [{ 236 | pref: false 237 | , type: null 238 | , value: "data:" + mime + ";base64," + data.toString("base64") 239 | }]; 240 | cb(); 241 | }); 242 | }); 243 | } 244 | , toLiteralObject: function () { 245 | var ret = {} 246 | , fields = [].concat("id", allFields); 247 | ; 248 | for (var i = 0, n = fields.length; i < n; i++) { 249 | var fld = fields[i]; 250 | ret[fld] = this[fld]; 251 | } 252 | return ret; 253 | } 254 | }; 255 | 256 | exports.ContactsDB = ContactsDB; 257 | --------------------------------------------------------------------------------