Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit e787c0f

Browse files
authoredSep 10, 2023
Add support for optional CNContactUrlAddressesKey field. (#36)
1 parent 43001e9 commit e787c0f

File tree

5 files changed

+70
-16
lines changed

5 files changed

+70
-16
lines changed
 

‎README.md

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ Requests access to the [CNContactStore](https://developer.apple.com/documentatio
3434

3535
If the user has previously denied the request, this method will open the Contacts pane within the Privacy section of System Preferences.
3636

37+
*Note that access permission request prompts will not appear when `requestAccess()` is invoked in embedded terminals such as those found in Visual Studio Code. Run your code from an external terminal such as Terminal.app instead.*
38+
3739
### `contacts.getAuthStatus()`
3840

3941
Returns `String` - Can be one of 'Not Determined', 'Denied', 'Authorized', or 'Restricted'.
@@ -62,7 +64,7 @@ console.log(`Authorization access to contacts is: ${authStatus}`)
6264

6365
### `contacts.getAllContacts([extraProperties])`
6466

65-
* `extraProperties` string[] (optional) - an array of extra contact properties to fetch that can be any of: `jobTitle`, `departmentName`, `organizationName`, `middleName`, `note`, `contactImage`, `contactThumbnailImage`, `instantMessageAddresses`, or `socialProfiles`.
67+
* `extraProperties` string[] (optional) - an array of extra contact properties to fetch that can be any of: `jobTitle`, `departmentName`, `organizationName`, `middleName`, `note`, `contactImage`, `contactThumbnailImage`, `instantMessageAddresses`, `socialProfiles`, or `urlAddresses`.
6668

6769
Returns `Array<Object>` - Returns an array of contact objects.
6870

@@ -78,13 +80,14 @@ The returned objects will take the following format:
7880
* `postalAddresses` String[] - An array of postal as strings.
7981
* `jobTitle` String (optional) - The contact's job title.
8082
* `departmentName` String (optional) - The name of the department associated with the contact.
81-
* `organizationName` String (optional) - The name of the organization associated with the contact.
83+
* `organizationName` String (optional) - The name of the organization associated with the contact.
8284
* `middleName` String (optional) - The contact's middle name.
8385
* `note` String (optional) - The note associated with the contact.
8486
* `contactImage` Buffer (optional) - a Buffer representation of the contact's profile picture.
8587
* `contactThumbnailImage` Buffer (optional) - a Buffer representation of The thumbnail version of the contact’s profile picture.
8688
* `socialProfiles` Object[] (optional) - An array of labeled social profiles for a contact.
87-
* `instantMessageAddresses` Object[] (optional) - An array of labeled IM addresses for the contact.
89+
* `instantMessageAddresses` Object[] (optional) - An array of labeled IM addresses for the contact.
90+
* `urlAddresses` String[] (optional) - An array of url addresses as strings.
8891

8992
This method will return an empty array (`[]`) if access to Contacts has not been granted.
9093

@@ -112,7 +115,7 @@ console.log(allContacts[0])
112115
### `contacts.getContactsByName(name[, extraProperties])`
113116

114117
* `name` String (required) - The first, middle, last, or full name of a contact.
115-
* `extraProperties` String[] (optional) - an array of extra contact properties to fetch that can be any of: `jobTitle`, `departmentName`, `organizationName`, `middleName`, `note`, `contactImage`, `contactThumbnailImage`, `instantMessageAddresses`, or `socialProfiles`.
118+
* `extraProperties` String[] (optional) - an array of extra contact properties to fetch that can be any of: `jobTitle`, `departmentName`, `organizationName`, `middleName`, `note`, `contactImage`, `contactThumbnailImage`, `instantMessageAddresses`, `socialProfiles`, or `urlAddresses`.
116119

117120
Returns `Array<Object>` - Returns an array of contact objects where either the first or last name of the contact matches `name`.
118121

@@ -130,13 +133,14 @@ The returned object will take the following format:
130133
* `postalAddresses` String[] - An array of postal as strings.
131134
* `jobTitle` String (optional) - The contact's job title.
132135
* `departmentName` String (optional) - The name of the department associated with the contact.
133-
* `organizationName` String (optional) - The name of the organization associated with the contact.
136+
* `organizationName` String (optional) - The name of the organization associated with the contact.
134137
* `middleName` String (optional) - The contact's middle name.
135138
* `note` String (optional) - The note associated with the contact.
136139
* `contactImage` Buffer (optional) - a Buffer representation of the contact's profile picture.
137140
* `contactThumbnailImage` Buffer (optional) - a Buffer representation of The thumbnail version of the contact’s profile picture.
138141
* `socialProfiles` Object[] (optional) - An array of labeled social profiles for a contact.
139-
* `instantMessageAddresses` Object[] (optional) - An array of labeled IM addresses for the contact.
142+
* `instantMessageAddresses` Object[] (optional) - An array of labeled IM addresses for the contact.
143+
* `urlAddresses` String[] (optional) - An array of url addresses as strings.
140144

141145
This method will return an empty array (`[]`) if access to Contacts has not been granted.
142146

@@ -169,15 +173,16 @@ console.log(contacts)
169173
* `nickname` String (optional) - The nickname for the contact.
170174
* `jobTitle` String (optional) - The contact's job title.
171175
* `departmentName` String (optional) - The name of the department associated with the contact.
172-
* `organizationName` String (optional) - The name of the organization associated with the contact.
176+
* `organizationName` String (optional) - The name of the organization associated with the contact.
173177
* `middleName` String (optional) - The contact's middle name.
174178
* `birthday` String (optional) - The birthday for the contact in `YYYY-MM-DD` format.
175179
* `phoneNumbers` Array\<String\> (optional) - The phone numbers for the contact, as strings in [E.164 format](https://en.wikipedia.org/wiki/E.164): `+14155552671` or `+442071838750`.
176180
* `emailAddresses` Array\<String\> (optional) - The email addresses for the contact, as strings.
181+
* `urlAddresses` Array\<String\> (optional) - The url addresses for the contact, as strings.
177182

178183
Returns `Boolean` - whether the contact information was created successfully.
179184

180-
Creates and save a new contact to the user's contacts database.
185+
Creates and save a new contact to the user's contacts database.
181186

182187
This method will return `false` if access to Contacts has not been granted.
183188

@@ -189,8 +194,8 @@ const success = contacts.addNewContact({
189194
lastName: 'Grapeseed',
190195
nickname: 'Billy',
191196
birthday: '1990-09-09',
192-
phoneNumbers: [ '+1234567890' ],
193-
emailAddresses: ['billy@grapeseed.com' ]
197+
phoneNumbers: ['+1234567890'],
198+
emailAddresses: ['billy@grapeseed.com'],
194199
})
195200

196201
console.log(`New contact was ${success ? 'saved' : 'not saved'}.`)
@@ -227,11 +232,12 @@ console.log(`Contact ${name} was ${deleted ? 'deleted' : 'not deleted'}.`)
227232
* `nickname` String (optional) - The nickname for the contact.
228233
* `jobTitle` String (optional) - The contact's job title.
229234
* `departmentName` String (optional) - The name of the department associated with the contact.
230-
* `organizationName` String (optional) - The name of the organization associated with the contact.
235+
* `organizationName` String (optional) - The name of the organization associated with the contact.
231236
* `middleName` String (optional) - The contact's middle name.
232237
* `birthday` String (optional) - The birthday for the contact in `YYYY-MM-DD` format.
233238
* `phoneNumbers` Array\<String\> (optional) - The phone numbers for the contact, as strings in [E.164 format](https://en.wikipedia.org/wiki/E.164): `+14155552671` or `+442071838750`.
234239
* `emailAddresses` Array\<String\> (optional) - The email addresses for the contact, as strings.
240+
* `urlAddresses` Array\<String\> (optional) - The url addresses for the contact, as strings.
235241

236242
Returns `Boolean` - whether the contact was updated successfully.
237243

@@ -248,7 +254,7 @@ Example Usage:
248254
const updated = contacts.updateContact({
249255
firstName: 'William',
250256
lastName: 'Grapeseed',
251-
nickname: 'Will'
257+
nickname: 'Will',
252258
})
253259

254260
console.log(`Contact was ${updated ? 'updated' : 'not updated'}.`)

‎contacts.mm

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,21 @@
141141
return Napi::Buffer<uint8_t>::Copy(env, &data[0], data.size());
142142
}
143143

144+
// Parses and returns an array of URL addresses as strings.
145+
Napi::Array GetUrlAddresses(Napi::Env env, CNContact *cncontact) {
146+
int num_url_addresses = [[cncontact urlAddresses] count];
147+
148+
Napi::Array url_addresses = Napi::Array::New(env, num_url_addresses);
149+
NSArray<CNLabeledValue<NSString *> *> *urlAddresses =
150+
[cncontact urlAddresses];
151+
for (int i = 0; i < num_url_addresses; i++) {
152+
CNLabeledValue<NSString *> *url_address = [urlAddresses objectAtIndex:i];
153+
url_addresses[i] = std::string([[url_address value] UTF8String]);
154+
}
155+
156+
return url_addresses;
157+
}
158+
144159
// Creates an object containing all properties of a macOS contact.
145160
Napi::Object CreateContact(Napi::Env env, CNContact *cncontact) {
146161
Napi::Object contact = Napi::Object::New(env);
@@ -201,6 +216,11 @@
201216
if ([cncontact isKeyAvailable:CNContactSocialProfilesKey])
202217
contact.Set("socialProfiles", GetSocialProfiles(env, cncontact));
203218

219+
if ([cncontact isKeyAvailable:CNContactUrlAddressesKey]) {
220+
Napi::Array url_addresses = GetUrlAddresses(env, cncontact);
221+
contact.Set("urlAddresses", url_addresses);
222+
}
223+
204224
return contact;
205225
}
206226

@@ -261,6 +281,24 @@
261281
return birthday_components;
262282
}
263283

284+
// Parses an array of url address strings and converts them to an NSArray of
285+
// NSStrings.
286+
NSArray *ParseUrlAddresses(Napi::Array url_address_data) {
287+
NSMutableArray *url_addresses = [[NSMutableArray alloc] init];
288+
289+
int data_length = static_cast<int>(url_address_data.Length());
290+
for (int i = 0; i < data_length; i++) {
291+
std::string url_str =
292+
url_address_data.Get(i).As<Napi::String>().Utf8Value();
293+
NSString *url = [NSString stringWithUTF8String:url_str.c_str()];
294+
CNLabeledValue *labeled_value =
295+
[CNLabeledValue labeledValueWithLabel:CNLabelHome value:url];
296+
[url_addresses addObject:labeled_value];
297+
}
298+
299+
return url_addresses;
300+
}
301+
264302
// Returns a status indicating whether or not the user has authorized Contacts
265303
// access.
266304
CNAuthorizationStatus AuthStatus() {
@@ -322,6 +360,8 @@ CNAuthorizationStatus AuthStatus() {
322360
CNSocialProfileURLStringKey, CNSocialProfileUsernameKey,
323361
CNSocialProfileUserIdentifierKey
324362
]];
363+
} else if (key == "urlAddresses") {
364+
[keys addObject:CNContactUrlAddressesKey];
325365
}
326366
}
327367
}
@@ -434,6 +474,13 @@ CNAuthorizationStatus AuthStatus() {
434474
[contact setEmailAddresses:[NSArray arrayWithArray:email_addresses]];
435475
}
436476

477+
if (contact_data.Has("urlAddresses")) {
478+
Napi::Array url_address_data =
479+
contact_data.Get("urlAddresses").As<Napi::Array>();
480+
NSArray *url_addresses = ParseUrlAddresses(url_address_data);
481+
[contact setUrlAddresses:[NSArray arrayWithArray:url_addresses]];
482+
}
483+
437484
return contact;
438485
}
439486

‎index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const optionalProperties = [
2424
'contactThumbnailImage',
2525
'instantMessageAddresses',
2626
'socialProfiles',
27+
'urlAddresses',
2728
]
2829

2930
function getAllContacts(extraProperties = []) {
@@ -81,7 +82,7 @@ function validateContactArg(contact) {
8182
throw new TypeError(`${prop} must be a string`)
8283
}
8384
}
84-
for (const prop of ['phoneNumbers', 'emailAddresses']) {
85+
for (const prop of ['phoneNumbers', 'emailAddresses', 'urlAddresses']) {
8586
const hasProp = contact.hasOwnProperty(prop)
8687
if (hasProp && !Array.isArray(contact[prop])) {
8788
throw new TypeError(`${prop} must be an array`)

‎package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"clean": "node-gyp clean",
1010
"lint": "prettier --check '**/*.js'",
1111
"format": "clang-format -i contacts.mm && prettier --write '**/*.js'",
12-
"rebuild": "node-gyp 2ebuild",
12+
"rebuild": "node-gyp rebuild",
1313
"rebuild:dev": "node-gyp rebuild --debug",
1414
"test": "./node_modules/.bin/mocha --reporter spec",
1515
"prepare": "husky install"

‎test/module.spec.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ describe('node-mac-contacts', () => {
5151

5252
it('should throw if extraProperties contains invalid properties', () => {
5353
const errorMessage =
54-
'properties in extraProperties must be one of jobTitle, departmentName, organizationName, middleName, note, contactImage, contactThumbnailImage, instantMessageAddresses, socialProfiles'
54+
'properties in extraProperties must be one of jobTitle, departmentName, organizationName, middleName, note, contactImage, contactThumbnailImage, instantMessageAddresses, socialProfiles, urlAddresses'
5555

5656
expect(() => {
5757
getAllContacts(['bad-property'])
@@ -135,7 +135,7 @@ describe('node-mac-contacts', () => {
135135

136136
it('should throw if extraProperties contains invalid properties', () => {
137137
const errorMessage =
138-
'properties in extraProperties must be one of jobTitle, departmentName, organizationName, middleName, note, contactImage, contactThumbnailImage, instantMessageAddresses, socialProfiles'
138+
'properties in extraProperties must be one of jobTitle, departmentName, organizationName, middleName, note, contactImage, contactThumbnailImage, instantMessageAddresses, socialProfiles, urlAddresses'
139139

140140
expect(() => {
141141
getContactsByName('jim-bob', ['bad-property'])

0 commit comments

Comments
 (0)
Please sign in to comment.