Birthdays, AppleScript, 1604

Two of my high school friends had their birthdays last week, and among other things, (the restaurant we went to - big discount for birthdays!) someone brought up the question of how they keep track of birthdays. I think they said that they used Snapchat, and added this information to their contacts app.


Part 1: The extension

This got me thinking, and my next deep dive on the internet resulted in me discovering this chrome extension, which has the ability to extract birthdays out of Snapchat all at once. Neat, right?

I’m a FOSS developer?

However, I didn’t want the export of that app to be a calendar file, as I wanted to add this birthday information to my contacts app. Lucky for me, this chrome extension was open source! I forked the repo on Github, and within 30 minutes, I had figured out how to install Plasmo, the framework they built the chrome extension with, how to add a button to the existing extension’s UI, and how to export the birthdays into a .csv file intstead of directly to an .ical file. I even submitted a pull request. Open source, baby!

Side note: Who builds chrome extensions with a build tool? What kind of nonsense is that? It’s a completely different environment to run javascript in, you might as well LEARN IT. It’s the same script-injection nonsense no matter which framework you want to use. I already had node installed on my mac, so installing plasmo wasn’t too difficult. Loading hundreds of megabytes of dependencies hurt my soul more than it hurt my SSD. Getting it to build properly only required creating a .env file, and adding two variables. Why this extension won’t build without a proper google analytics key? I really couldn’t care less. I just wanted to get it running.


Part 2: AppleScript tomfoolery

Once I got my neat little .csv file, I got to work trying to insert these birthday values into my contacts. My main plan of attack was to use AppleScript, this weird scripting language that Apple made. The syntax is annoying, but it was the quickest way to natively interface with my mac’s Contacts app. Finding a contact is as easy as

tell application "Contacts"
    set theName to "John Smith"
    set matchingContacts to (every person whose name contains theName)
    if (count of matchingContacts) > 0 then
        set theContact to item 1 of matchingContacts
    end if
end tell

See what I mean by weird syntax? It’s like they were deathly afraid of introducing people to “real coding”. It’s too difficult for the beginner audience it targeted, and too limited to help the power users it had the potential to serve.

Regardless, I got my AppleScript in order. While I was building the script, I deliberately left out the actual line of code that would change the birthday value in a contact, in case I accidentally ran the script and it did something like overwrite all the existing birthdays (which there probably weren’t too many of). I would have rather had the script ask me every time if I wanted to edit the contact’s birthday instead of a bug setting everyone’s birthday to the same day.

set birth date of theContact to dateVar

Because I was waiting until my script was safe to run, I never tested if the command above would even do what I asked for.

The entire script, if you're curious
set DELIM to {","}
set L1 to {} -- column 1 items (name) 
set L2 to {} -- column 2 items (username)
set L3 to {} -- column 3 items (birthday)
set L4 to {} -- column 4 items (snap code)
set Lx to {L1, L2, L3, L4}

set acsv to ("/Users/tadhgj/Downloads/SnapchatFriendsBirthdays 5.28.35 PM.csv")

set csvList to read acsv using delimiter linefeed
set {TID, AppleScript's text item delimiters} to {AppleScript's text item delimiters, DELIM}
repeat with arow in csvList
	set ritems to text items of arow
	if item 1 of ritems is not "" then
		repeat with i from 1 to (count of ritems)
			copy (item i of ritems) to the end of (item i of Lx)
		end repeat
	end if
end repeat

set AppleScript's text item delimiters to TID

tell application "Contacts"
	repeat with i from 1 to (count of item 1 of Lx)
		set theName to item i of item 1 of Lx
		set theBirthday to item i of item 3 of Lx
		
		set AppleScript's text item delimiters to "/"
		set birthdayStr to words of theBirthday
		set birthdayMonth to item 1 of birthdayStr
		set birthdayDay to item 2 of birthdayStr
		--set AppleScript's text item delimiters to ""
		--set theBirthdayWithNoYear to birthdayStr as string
		--log birthdayMonth
		--log birthdayDay
		set dateVar to the current date
		set the month of dateVar to birthdayMonth
		set the day of dateVar to birthdayDay
		set the year of dateVar to 2023
		--log "created date object:"
		--log dateVar
		set AppleScript's text item delimiters to TID
		
		set matchingContacts to (every person whose name contains theName)
		if (count of matchingContacts) > 0 then
			
			set theContact to item 1 of matchingContacts
			set theContactPhoneNumber to value of first phone of theContact
			log "found matching contact"
			log theName
			--log theBirthday
			--log theContact
			log theContactPhoneNumber
			
			-- check if there's already a birthday
			set existingBirthday to the birth date of theContact
			log "existing birthday is:"
			log existingBirthday
			
			
            -- ignore people who already have birthdays
			if existingBirthday is missing value then
			
			--if theBirthday is not "" then
				-- open contacts to show this contact
				--tell application "Contacts" to set selection to person "Firstname Lastname"
				set selection to person theName
				set AppleScript's text item delimiters to "/"
				set birthdayText to {birthdayMonth, birthdayDay} as string
				log birthdayText
				set AppleScript's text item delimiters to ""
				set theDialogText to {"Found birthday (", birthdayText, ") match of ", theName, ". Should the birthday be updated?"} as string
				display dialog theDialogText buttons {"Exit", "No", "Yes"}
				if button returned of result = "Yes" then
					log "Should change to"
					log dateVar
					-- set the birth date of theContact to dateVar -- does not seem to stick
					set birth date of theContact to dateVar
					-- set AppleScript's text item delimiters to "-"
					-- set the birth date of theContact to {"1604", birthdayMonth, birthdayDay} as string
				else
					if button returned of result = "Exit" then
						exit repeat
					end if
				end if
				
			else
				log "already has birthday"
			end if
			--end if
		end if
	end repeat
end tell

Finally, once the script was running exactly as I wanted it to, I started thinking about the finer details of actually adding the birthday in. Snapchat stores a user’s birthday, but not the year of birth. This is fine for me, because for most of my friends, I can probably guess the year.

I wanted my script to add these birthdays without specifying the year, as my intention isn’t to keep track of how old everyone is, but when to text them “happy birthday”.

What is 1604 doing here?

In the Contacts app for the iPhone and Mac, you’re able to enter a birthday without providing the year.

A contact card for a ficticious person, John Smith, with a birthday of April 4th. There is no year shown

However, the way they did this is somewhat of a hack, and leads to issues that I’ll discuss shortly. I added a birthday without a year to a contact, and then exported the contact card of that person, so I could peek at how they were storing the date in the .vcf file.

We get John Smith.vcf, which looks like

BEGIN:VCARD
VERSION:3.0
PRODID:-//Apple Inc.//macOS 14.5//EN
N:Smith;John;;;
FN:John Smith
TEL;type=CELL;type=VOICE;type=pref:18001234567
BDAY;X-APPLE-OMIT-YEAR=1604:1604-04-04
END:VCARD

So, this is what “no year specified” looks like. Apple uses the year 1604 as the placeholder. In addition to using 1604, it includes a tag that says “X-APPLE-OMIT-YEAR=1604”, which alerts the operating system that yes, this contact isn’t over 400 years old. It’s only slightly neater than hard-coding a “ignore birth year if equal to 1604” into every version of their operating system that deals with contact cards. Nothing of this sort is mentioned in the RFC documentation for the vCard spec.

Side note: Actually, no. It is hardcoded. You can't set the birth year to 1604! iOS and macOS treat this as "no year provided", and set the same OMIT-YEAR tag. I don't think the contacts app knows the difference between the user not providing a year and setting it to 1604, because both ways, it's the exact same exported `.vcf` file.

BDAY:1605-04-04 This is what a birthday with a valid year is saved as

Discovering this special year value, I decided to use “1604” as the year for the date object that I was setting contacts’ birthdays to, in hopes that it would then figure out that I meant “I don’t have a birth year” to the contact app when my AppleScript ran.

Surprise! AppleScript doesn’t work

However, I soon discovered that no matter what I tried setting the birth year to, the contact never got updated. Confusingly, if I used AppleScript to ask the contacts app what the user’s birthday was, it would return the date object that I provided previously.

Screenshot of AppleScript logs showing that it stores the birth date somewhere

These are logs from the AppleScript. I deleted the birthday from John Smith for this screenshot. After running the “set birth date to newDate” command, it returns the new value.

However, this value is never stored to the proper user-facing field for the contact. There was no change to the exported .vcf file. Even after closing and re-opening the contact app, it seemed that I was only changing some internal date variable that wasn’t actually observed by the contacts app.

So, I was defeated.

I spent half a day figuring out the AppleScript’s nooks and crannies, only to discover that the only thing I needed it to do seemed nonfunctional. So, I did what my friend did. I just entered the birthdays manually into my contacts app. You’d think the saga would be over, right? Nope. Not even close. Well, it’s pretty close.


Part 3: 1604 bites back

Going back to this 1604 year from Apple. It’s a weird choice, but unfortunately, in implementation, it’s a little worse than weird.

Many of my contacts on my phone are synced with Google (through two gmail accounts). I have discovered that Google does not like Apple’s little “1604” stunt, and punishes users accordingly.

When setting the birthday for a contact on the iPhone for a contact synced to Google’s servers, it seems to be fine. You add the birthday, you press done (to save the contact), and you see the birthday field updated in the contacts app.

I would do this, dismiss the contact, and move onto the next one. Remember, I have a lot of birthdays to get through, since the AppleScript didn’t work!

However, if I had stayed just a second longer on the contact page, I would notice a peculiar detail. After saving the contact, (presumably,) the contacts app sends this new contact file to Google’s servers to sync. After pressing “done”, you see a visible flash a moment later as the contacts app redraws the UI with new information it gets back.

For whatever reason, the contacts app flashes multiple times when updating a contact. Not sure why.

I’m assuming that Google sends the contact information back, but in this process, it removes the birthday entirely. What the hell?

I swear, it legitimately just deletes the birthday!

It turns out that Google doesn’t like something about the updated date field. Maybe it’s the “OMIT-YEAR” tag that Apple added to the “bday” key, or maybe it’s the fact that the year is pretty old. Either way, it just silently ignores the value altogether.

This nonsense has been around since Apple introduced it in 2011, and has since documented on numerous online forums. Fun fact: Oracle uses 1753 to specify “no year”! Refer to xkcd #927 for my thoughts on this.

Closing up

Through all this nonsense, I have decided that in hindsight, none of this needed to be automated.

Sure, the export got nearly 400 birthdays out of Snapchat, but most of them weren’t people that I had any need in recording their birthday. I never got the automation scripts running, and they probably would have failed on all the contacts stored with Google. So, I used the calendar export feature that was first included in the chrome extension. Maybe that guy was onto something.

p.s.
Well, I accidentally imported 400 repeating calendar events and alerts into the wrong calendar, and I spent the next half hour figuring out how I was going to remove them. Luckily for me, [Bulk Edit Calendar Events](https://bulkeditcalendarevents.com/) saved my sorry ass. If you see me, I'll probably be at Target buying a physical calendar so I can manually write in the handful of birthdays that really mattered in the first place.