keith / reminders-cli

A simple CLI for interacting with macOS reminders
MIT License
663 stars 52 forks source link

Add option for tags #74

Open theuberuser opened 7 months ago

theuberuser commented 7 months ago

Tags were introduced in the Reminders app in macOS Monterey and can be extremely useful when managing various types of tasks. I think adding this to the reminders CLI would make it all the more powerful than it already is, especially when used with scripts and automation.

Usage examples:

$ reminders add work -t projectX

$ reminders add work -t teamZ

$ reminders add work -t jira_FOOBAR-123456

$ reminders add errands -t groceries

$ reminders add home -t chores

NOTE: Adding support for multiple tags would be ideal but not required

These are just some basic examples but you can probably see how useful this could be.

Also, yes, I did see that there was one comment about this already on https://github.com/keith/reminders-cli/issues/59, but, I felt it was worth explicitly calling it out in a separate issue, so as not to get lost. If you'd rather work on this in the already open issue for enhancements, then please feel free to delete this one.

keith commented 7 months ago

AFAICT tags aren't in the public API https://developer.apple.com/documentation/eventkit/ekevent

0xdevalias commented 5 months ago

Semi-related:


After some investigation it seems that Tags themselves aren't exposed in the API for the EKReminders class

When you say 'tags', what specifically are you referring to? Do you mean the specific 'hashtags' tags assigned to a reminder? Or are you referring to something else?


The rules for smart lists can have many different aspects, not just tags, so even if they aren't currently accessible, the others may be:

Originally posted by @0xdevalias in https://github.com/keith/reminders-cli/issues/72#issuecomment-1829203825


It looks like the underlying sqlite database backing the Reminders app is stored at:

  • /Users/devalias/Library/Reminders/Container_v1/Stores/Data-SOME-UUID-TYPE-THING.sqlite

Within that I can see the tags information in the ZREMCDHASHTAGLABEL table.

The ZREMCDOBJECT table seems to contain a lot of data, possibly related to the reminders themselves? The ZNAME1 column seems to include some of the tags as well.

The ZREMCDREMINDER table seems to have a lot of the actual reminder data, and seemingly foreign keys to link to some of the other tables.

While I wouldn't personally risk writing to this DB for fear of corrupting it or similar; it might be possible to extract some relevant details from it in a 'read only' mode, that could then be used to filter the reminders returned from the official API's 'in app'.

Originally posted by @0xdevalias in https://github.com/keith/reminders-cli/issues/72#issuecomment-1829214924


In /Users/devalias/Library/Reminders/Container_v1/Stores/Data-SOME-UUID-TYPE-THING.sqlite:

That seems to be enough basic info to figure out resolving both this issue, and maybe also #72

Would just need to figure out how to match up the ID/data that the API is currently able to provide, with an ID that can be looked up in the sqlite database; and then extract the associated hashtags/etc.

I was thinking that maybe externalId from reminders show Reminders --sort creation-date --sort-order ascending --format json might have worked.. but it only seems to show up in ZREMCDOBJECT within some fields with LOTS of other IDs in them, so doesn't seem ideal; though ZREMCDREMINDER seems to have a single row match with the ID being in ZCKIDENTIFIER / ZDACALENDARITEMUNIQUEIDENTIFIER, so maybe we can do it that way in like 2 steps..


Edit: Collated/cross-posted the above on the following gist for future reference: https://gist.github.com/0xdevalias/ccc2b083ff58b52aa701462f2cfb3cc8#accessing--exporting-apples-reminders-data-on-macos

Edit 2: Found this cool blog post laying out a lot of the specifics of how the internals of Apple CoreData based SQLite databases are laid out: https://fatbobman.com/en/posts/tables_and_fields_of_coredata/

_Edit 3: Based on that blog post, we can see that we can look up what the Z_ENT's represent within the Z_PRIMARYKEY table to see that what 'class'/'superclass' it corresponds to (eg. REMCDObject). Using that method, we can confirm that Z_ENT 30 is REMCDSmartList, 24 is REMCDHashtag, 18 is REMCDURLAttachment, etc._

_Edit 4: I wrote a basic python script to extract the CoreData models + hierarchy from the Z_PRIMARYKEY table and display them as an indented markdown list: https://gist.github.com/0xdevalias/ccc2b083ff58b52aa701462f2cfb3cc8#file-extract-coredata-model-hierarchy-py_

Reminders SQLite CoreData Models + Associated tables ```bash ⇒ ./extract-coredata-model-hierarchy.py /Users/devalias/Library/Reminders/Container_v1/Stores/Data-5070B790-D66D-40F7-8F4A-EC8E0FA88F3A.sqlite - 1: REMCDAccountListData (Table: ZREMCDACCOUNTLISTDATA) - 2: REMCDChangeTrackingState (Table: ZREMCDCHANGETRACKINGSTATE) - 3: REMCDHashtagLabel (Table: ZREMCDHASHTAGLABEL) - 4: REMCDMigrationState (Table: ZREMCDMIGRATIONSTATE) - 5: REMCDObject (Table: ZREMCDOBJECT) - 6: REMCDAccount - 7: REMCDAlarm - 8: REMCDAlarmTrigger - 9: REMCDAlarmDateTrigger - 10: REMCDAlarmLocationTrigger - 11: REMCDAlarmTimeIntervalTrigger - 12: REMCDAlarmVehicleTrigger - 13: REMCDAssignment - 14: REMCDAttachment - 15: REMCDFileAttachment - 16: REMCDAudioAttachment - 17: REMCDImageAttachment - 18: REMCDURLAttachment - 19: REMCDAuxiliaryChangeInfo - 20: REMCDAuxiliaryReminderChangeInfo - 21: REMCDAuxiliaryReminderChangeDeleteInfo - 22: REMCDAuxiliaryReminderChangeMoveInfo - 23: REMCDCalDAVNotification - 24: REMCDHashtag - 25: REMCDList - 26: REMCDManualSortHint_v1 - 27: REMCDRecurrenceRule - 28: REMCDSharedToMeReminderPlaceholder - 29: REMCDSharee - 30: REMCDSmartList - 31: REMCDPublicTemplate (Table: ZREMCDPUBLICTEMPLATE) - 32: REMCDReminder (Table: ZREMCDREMINDER) - 33: REMCDReplicaManager (Table: ZREMCDREPLICAMANAGER) - 34: REMCDSavedAttachment (Table: ZREMCDSAVEDATTACHMENT) - 35: REMCDSavedReminder (Table: ZREMCDSAVEDREMINDER) - 36: REMCDTemplate (Table: ZREMCDTEMPLATE) - 37: REMCDTemplateOperationQueueItem (Table: ZREMCDTEMPLATEOPERATIONQUEUEITEM) - 38: REMCKCloudState (Table: ZREMCKCLOUDSTATE) - 39: REMCKServerChangeToken (Table: ZREMCKSERVERCHANGETOKEN) - 40: REMCKSharedEntitySyncActivity (Table: ZREMCKSHAREDENTITYSYNCACTIVITY) - 41: REMCKSharedObjectOwnerName (Table: ZREMCKSHAREDOBJECTOWNERNAME) - 16001: CHANGE - 16002: TRANSACTION - 16003: TRANSACTIONSTRING ```
0xdevalias commented 5 months ago

Working with the above rough notes (and remembering how SQL joins work), I eventually came up with this, that would seem to extract all of the relevant entries from ZREMCDOBJECT based on the single entry in ZREMINDER that matches the externalId in the data from a command like follows:

⇒ reminders show Reminders --sort creation-date --sort-order ascending --format json | jq '[limit(1;.[])]'
[
  {
    "dueDate": "2023-08-13T14:00:00Z",
    "externalId": "EAA1A308-368E-4F90-B9D5-6E6A12AE7E6D",
    "isCompleted": false,
    "list": "Reminders",
    "priority": 0,
    "title": "Foo Bar Baz"
  }
]
SELECT ZO.*
FROM ZREMCDOBJECT ZO
JOIN (
    SELECT Z_PK
    FROM ZREMCDREMINDER
    WHERE ZCKIDENTIFIER = 'EAA1A308-368E-4F90-B9D5-6E6A12AE7E6D'
    OR ZDACALENDARITEMUNIQUEIDENTIFIER = 'EAA1A308-368E-4F90-B9D5-6E6A12AE7E6D'
) AS ZR ON ZO.ZREMINDER = ZR.Z_PK 
    OR ZO.ZREMINDER1 = ZR.Z_PK 
    OR ZO.ZREMINDER2 = ZR.Z_PK 
    OR ZO.ZREMINDER3 = ZR.Z_PK 
    OR ZO.ZREMINDER4 = ZR.Z_PK 
    OR ZO.ZREMINDER5 = ZR.Z_PK
ORDER BY ZO.Z_ENT;

This gives me 3 rows:

⇒ EXTERNAL_ID='EAA1A308-368E-4F90-B9D5-6E6A12AE7E6D' &&
sqlite3 -readonly -json /Users/devalias/Library/Reminders/Container_v1/Stores/Data-5070B790-D66D-40F7-8F4A-EC8E0FA88F3A.sqlite "
  SELECT ZO.*
  FROM ZREMCDOBJECT ZO
  JOIN (
      SELECT Z_PK
      FROM ZREMCDREMINDER
      WHERE ZCKIDENTIFIER = '$EXTERNAL_ID'
      OR ZDACALENDARITEMUNIQUEIDENTIFIER = '$EXTERNAL_ID'
  ) AS ZR ON ZO.ZREMINDER = ZR.Z_PK
      OR ZO.ZREMINDER1 = ZR.Z_PK
      OR ZO.ZREMINDER2 = ZR.Z_PK
      OR ZO.ZREMINDER3 = ZR.Z_PK
      OR ZO.ZREMINDER4 = ZR.Z_PK
      OR ZO.ZREMINDER5 = ZR.Z_PK
  ORDER BY ZO.Z_ENT;
" | jq 'length'
3

If there were other associated rows, they would obviously have been included as well.

This would allow for a much more robust/complete amount of reminders data to be shown/exported; without needing to wait for the official API's to be updated to support it (and then if/when the API's are updated, these methods could then use the official API)


Edit: Collated/cross-posted the above on the following gist for future reference: https://gist.github.com/0xdevalias/ccc2b083ff58b52aa701462f2cfb3cc8#accessing--exporting-apples-reminders-data-on-macos