Hi folks! I’m getting to the pulling-hair stages of debugging a script I’ve been building.
I have an external AppleScript executing from a smart rule. It does, well, a lot… but it works perfectly when I trigger it manually. However, when it is triggered via the Smart Rule, it fails with the error:
2022-04-24, 9:11:33 PM: ~/Dropbox/Portable/Meta/Workflow scripts, templates, and resources/Workflows/DEVONthink Scripts/Smart Rules/Create new resource file for new readings and add them to my reading list file.scpt on performSmartRule (Can’t get name of «class DTcn» id 1296597 of «class DTkb» id 2.)
The weirdest thing is that when I trigger it manually, I use:
tell application id "DNtp"
set theSelection to get the selection
my performSmartRule(theSelection)
end tell
So, I would have thought that my testing is completely simulating the Smart Rule trigger environment… Why is it failing?!
I’ll post the entire script below. I’ve highlighted where the script fails with -- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
. I imagine this is something simple I’m overlooking.
Thanks in advance (hopefully)!
use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions
use framework "Foundation"
property readingListFileUUID : "0B840A80-ACC6-422E-930B-01F31EA58DE6" -- must be a UUID that points to a plain text file.
property debug : false
property import : false
property overwriteCitekey : false -- if this value is true, it will overwrite the citekeys of publications
property abbreviatedTitleLength : 6 -- the number of words to append to the citekey from the title
on performSmartRule(theRecords)
tell application id "DNtp"
my addToReadingListFile(theRecords)
end tell
end performSmartRule
-- debug routine. if you set debug to true above, then run this script manually, it will test the script on the selection in DEVONthink. if debug is false, this doesn't do anything.
if debug then
tell application id "DNtp"
set theSelection to get the selection
my performSmartRule(theSelection)
end tell
end if
if not debug then
if import then
tell application id "DNtp"
set theSelection to get the selection
my addToReadingListFile(theSelection)
end tell
end if
end if
on addToReadingListFile(someRecords)
tell application id "DNtp"
log "Started the addToReadingListFile method."
-- initialize the list of items to be added to the reading list file
set listOfAddedLines to ""
-- get the details from each item to be added to the reading list
repeat with eachRecord in someRecords
-- ⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️⚠️
set theName to name of eachRecord
set theLink to reference URL of eachRecord
log "Record name is " & theName
set readingListItemLine to "- [ ] [[𖠫 " & theName & "]]" & return -- this is the line with reference details that will be added to the reading list file. edit it to customize what each added line looks like
if debug then
display dialog "Running smart rule on '" & readingListItemLine & "'"
end if
set listOfAddedLines to listOfAddedLines & readingListItemLine -- appends the readingListItemLine to the list of items to be added to the reading list file
my createNewResourceFile(eachRecord)
end repeat
-- get the reading list file and its current text
set theReadingListFile to get record with uuid readingListFileUUID
set currentReadingListText to plain text of theReadingListFile
-- update the text
set newReadingListText to currentReadingListText & listOfAddedLines
-- save the text into the reading list file
set plain text of theReadingListFile to newReadingListText
end tell
end addToReadingListFile
on createNewResourceFile(someRecord)
set indentationCharacter to tab -- tab is a reserved keyword in DEVONthink scripting, so I'm instantiating it here as the character we'll use to indent some text in variables below
tell application id "DNtp"
set YAMLdelimiter to "---"
set recordName to someRecord's name without extension
set citekeyAlias to ""
set citekeyYAML to "citekey: "
-- get the citekey from Bookends for the resource file's alias
tell application "Bookends"
try
set thePublications to (sql search "attachments REGEX '" & recordName & "'")
if (thePublications is not {}) then -- the item _is_ in Bookends
set thePublication to the first item in thePublications -- assume it's only attached to one publication
if debug then
display dialog ("'" & thePublication's title as text) & "' citekey is: " & thePublication's citekey as text
end if
if thePublication's citekey as text is "" then -- empty citekey
-- # the below items are commented out in favour of a title-appended citekey approach
-- set citekeyPromptMessage to ("Set the citekey for \"" & thePublication's title & "\" by :" & return & thePublication's authors & ".") -- prompt the user for a citekey
-- set publicationAuthors to thePublication's author names
-- set firstAuthor to the first item in publicationAuthors
-- try
-- set firstAuthorLastName to the first item in (my splitText(firstAuthor, ","))
--on error
-- set firstAuthorLastName to firstAuthor
--end try
--set defaultCitekey to (firstAuthorLastName & thePublication's publication date string)
--set citekeyPrompt to display dialog citekeyPromptMessage default answer defaultCitekey buttons {"Cancel", "Set citekey"} default button "Set citekey"
-- set publicationCitekey to my checkForRedundantCitekeys(text returned of citekeyPrompt)
if debug then
display dialog "Citekey is blank. Generating new citekey."
end if
set publicationCitekey to my generateNiceCitekey(thePublication)
if debug then
display dialog "New citekey: " & publicationCitekey
end if
set citekeyAlias to publicationCitekey
else -- citekey already exists
if overwriteCitekey then
set publicationCitekey to my generateNiceCitekey(thePublication)
end if
set citekeyAlias to publicationCitekey
end if
end if
end try
end tell
if citekeyAlias is not "" then
set aliasYAML to "alias:" & return & " - " & citekeyAlias
set citekeyYAML to citekeyYAML & citekeyAlias
else
set aliasYAML to "alias:"
end if
set devonthinkYAML to "devonthink-link: x-devonthink-item://" & someRecord's uuid
set theTagsAsString to ""
set recordTags to someRecord's tags
if (count of someRecord's tags) is 2 then
set theTagsAsString to " - " & the second item of recordTags as text
else
set firstTag to the second item in recordTags as text -- the first tag is always `.presets`, a utility tag that doesn't tell us anything.
set remainingTags to items 3 through (count of recordTags) of recordTags
set theTagsAsString to " - " & firstTag
repeat with eachTag in remainingTags
if eachTag as text does not contain "preset" then
set theTagsAsString to theTagsAsString & return & " - " & eachTag as text
end if
end repeat
end if
set tagsYAML to "tags:" & return & theTagsAsString
set wordCount to word count of someRecord
set estimatedReadingMinutes to wordCount / 200
set readtimeYAML to "minutes_to_read: " & estimatedReadingMinutes
set summaryYAML to "summary: "
set potentialImpactYAML to "potential_impact: "
set YAML to YAMLdelimiter & return & aliasYAML & return & tagsYAML & return & devonthinkYAML & return & citekeyYAML & return & readtimeYAML & return & summaryYAML & return & potentialImpactYAML & return & YAMLdelimiter & return
-- a Dataview Query for returning any annotation stream files
set annotationNotesDataviewQuery to "```dataviewjs" & return & "let summaryPages = dv.pages('\"Notes\"')" & return & indentationCharacter & ".where(p => p.file.name.includes(\"∎ " & name without extension of someRecord & "\"))" & return & indentationCharacter & ".map(p => [p.file.link, p.annotation_status])" & return & return & "dv.table([\"Annotation Summary\", \"Status\"], summaryPages)" & return & "```"
-- set the text of the resource file
set resourceFileText to YAML & return & return & "# [" & someRecord's name without extension & "](" & someRecord's reference URL & ")" & return & return & "![[" & someRecord's filename & "]]" & return & return & "## Annotations" & return & return & annotationNotesDataviewQuery
-- get the group of the resource file
set destinationGroup to someRecord's location group
set newRecord to create record with {name:"𖠫 " & name without extension of someRecord, content:resourceFileText, type:markdown} in destinationGroup
if debug then
set theWindow to open window for record newRecord
activate
end if
end tell
end createNewResourceFile
-- Generates and sets a citekey for the publication
on generateNiceCitekey(somePublication)
tell application "Bookends"
tell front library window
set thePublication to somePublication
if not overwriteCitekey then -- don't overwrite citekeys if they already exist
if thePublication's citekey is not "" then
return thePublication's citekey
end if
end if
-- Otherwise, generate a new citekey
set thePublication's citekey to "" as text
set theTitle to title of thePublication
set cleanedTitle to my cleanText(theTitle)
set titleAsList to my splitText(cleanedTitle, " ")
try
set abbreviatedTitle to my getFirstFiveWords(titleAsList) -- convert theTitle into its first five words
on error
set abbreviatedTitle to the first item of titleAsList & the second item of titleAsList & the third item of titleAsList
end try
if debug then
display dialog abbreviatedTitle as text
end if
set publicationDate to thePublication's publication date string
if debug then
display dialog abbreviatedTitle
display dialog publicationDate
end if
if length of publicationDate is 4 then
try
set theYear to publicationDate as number -- if this succeeds, the date string is a four-digit number, thereby likely a year.
end try
else --length of the date string is not 4, so let's try to get a year out of it
try
set theYear to my dateStringToYear(publicationDate) -- parse for year
on error
display notification "There was an issue extracting the publication's year. Using date added instead."
set dateAdded to thePublication's date added
set theYear to my dateStringToYear(dateAdded)
end try
if theYear is "" then -- deal with no year in the date string
set dateAdded to thePublication's date added
set theYear to my dateStringToYear(dateAdded)
end if
end if
set authorField to authors of thePublication
set firstAuthor to first item in my splitText(authorField, "
")
if firstAuthor contains "," then
set firstAuthor to first item in my splitText(firstAuthor, ",")
end if
set firstCitekey to firstAuthor & theYear & abbreviatedTitle
set newCitekey to my (checkForRedundantCitekeys(firstCitekey))
set thePublication's citekey to newCitekey
display notification "Generated a new citekey for '" & theTitle & "' in Bookends: " & newCitekey as text
return thePublication's citekey
end tell
end tell
end generateNiceCitekey
-- check for used citekeys
on checkForRedundantCitekeys(someCitekey)
tell application "Bookends"
set redundantCitekeyPublications to (sql search "user1 REGEX '" & someCitekey & "'")
if ((count of redundantCitekeyPublications) = 0) then
return someCitekey
else
set citekeyPromptMessage to ("The citekey " & someCitekey & " is already in use for the publication titled: " & return & return & "'" & title of first item of redundantCitekeyPublications & "'" & return & return & "Please provide a different citekey:")
set newTextPrompt to display dialog citekeyPromptMessage default answer "" buttons {"Cancel", "Try this citekey"} default button "Try this citekey"
set newCitekey to text returned of newTextPrompt
return my checkForRedundantCitekeys(newCitekey)
end if
end tell
end checkForRedundantCitekeys
on cleanText(someString)
set illegalCharacters to {"-", "/", "\\", ":", "|", "?", "'", ".", "&", "–", "_", "—", ","}
set newString to someString
repeat with illegalCharacter in illegalCharacters
if newString contains illegalCharacter then
if (illegalCharacter is "?") then
set newString to my findAndReplaceInText(newString, illegalCharacter, "")
else if (illegalCharacter is "-") then
set newString to my findAndReplaceInText(newString, illegalCharacter, " ")
else if (illegalCharacter is "'") then
set newString to my findAndReplaceInText(newString, illegalCharacter, "")
else if (illegalCharacter is "\"") then
set newString to my findAndReplaceInText(newString, illegalCharacter, "")
else if (illegalCharacter is ".") then
set newString to my findAndReplaceInText(newString, illegalCharacter, "")
else if (illegalCharacter is "&") then
set newString to my findAndReplaceInText(newString, illegalCharacter, "and")
else
set newString to my findAndReplaceInText(newString, illegalCharacter, " ")
end if
end if
end repeat
return newString
end cleanText
on getFirstFiveWords(someListOfWords)
-- set stopWords to {'all', 'just', 'being', 'over', 'both', 'through', 'yourselves', 'its', 'before', 'herself', 'had', 'should', 'to', 'only', 'under', 'ours', 'has', 'do', 'them', 'his', 'very', 'they', 'not', 'during', 'now', 'him', 'nor', 'did', 'this', 'she', 'each', 'further', 'where', 'few', 'because', 'doing', 'some', 'are', 'our', 'ourselves', 'out', 'what', 'for', 'while', 'does', 'above', 'between', 't', 'be', 'we', 'who', 'were', 'here', 'hers', 'by', 'on', 'about', 'of', 'against', 's', 'or', 'own', 'into', 'yourself', 'down', 'your', 'from', 'her', 'their', 'there', 'been', 'whom', 'too', 'themselves', 'was', 'until', 'more', 'himself', 'that', 'but', 'don', 'with', 'than', 'those', 'he', 'me', 'myself', 'these', 'up', 'will', 'below', 'can', 'theirs', 'my', 'and', 'then', 'is', 'am', 'it', 'an', 'as', 'itself', 'at', 'have', 'in', 'any', 'if', 'again', 'no', 'when', 'same', 'how', 'other', 'which', 'you', 'after', 'most', 'such', 'why', 'a', 'off', 'i', 'yours', 'so', 'the', 'having', 'once'}
set stopWords to {"of", "the", "and", "a", "A", "on", "is"}
set maxLength to length of someListOfWords
set wordIterator to 1
set abbreviatedTitleList to {}
-- iterate through someListOfWords until five words are obtained (removing common words) or the iterator reaches the end of the list
repeat while (wordIterator ≠ maxLength) -- for the entire possible word list
set theWord to item wordIterator of someListOfWords
if theWord is not in stopWords then
-- convert word case to first-letter-capitalized
try
set theWord to my changeCaseOfFirstCharacter(theWord, "upper")
end try
set abbreviatedTitleList to abbreviatedTitleList & theWord
end if
if length of abbreviatedTitleList is abbreviatedTitleLength then
return abbreviatedTitleList as string
end if
set wordIterator to wordIterator + 1
end repeat
return abbreviatedTitleList as string
end getFirstFiveWords
-- Utility functions from Apple
on changeCaseOfFirstCharacter(theText)
set uppercaseCharacters to "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
set lowercaseCharacters to "abcdefghijklmnopqrstuvwxyz"
set firstCharacter to character 1 of theText
set capitalizedWord to ""
-- capitalize first character
set theOffset to offset of firstCharacter in lowercaseCharacters
if theOffset is not 0 then -- the character was found in lowercaseCharacters, so it should be uppercased
set capitalizedWord to (capitalizedWord & character theOffset of uppercaseCharacters) as string
else -- character is already the correct case, just add it as-is
set capitalizedWord to (capitalizedWord & aCharacter) as string
end if
-- lowercase the rest of the word
set restOfWord to characters 2 thru (length of theText) of theText
repeat with aCharacter in restOfWord
set theOffset to offset of aCharacter in uppercaseCharacters
if theOffset is not 0 then -- the character was found in uppercaseCharacters, so it should be lowercased
set capitalizedWord to (capitalizedWord & character theOffset of lowercaseCharacters) as string
else -- character is already the correct case, just add it as-is
set capitalizedWord to (capitalizedWord & aCharacter) as string
end if
end repeat
return capitalizedWord
end changeCaseOfFirstCharacter
on splitText(theText, theDelimiter)
set AppleScript's text item delimiters to theDelimiter
set theTextItems to every text item of theText
set AppleScript's text item delimiters to ""
return theTextItems
end splitText
on findAndReplaceInText(theText, theSearchString, theReplacementString)
set AppleScript's text item delimiters to theSearchString
set theTextItems to every text item of theText
set AppleScript's text item delimiters to theReplacementString
set theText to theTextItems as string
set AppleScript's text item delimiters to ""
return theText
end findAndReplaceInText
(* this script works on these formats:
"Sunday 10th September 2017", "Sunday 10 September 2017", "Sunday 10 September 17", "September 10th, 2017", "September 10th 2017", "September 10 2017"
"10th September 2017", "10 September 2017", "10 Sep 17"
also work with the abbreviation ( e.g. Sep instead of September and Sun instead of Sunday)
also work with the localized name of (the month and the day)
"09/10/2017", "09.10.2017", "09-10-2017" : month_day_year only, or "2016/05/22", "2016-05-22", "2016.05.22" : year_month_day only
( month and days could be one or two digits)
*)
on dateStringToYear(thisString) --
tell current application
-- ** finds all the matches for a date, the result is a NSArray **
set m to its ((NSDataDetector's dataDetectorWithTypes:(its NSTextCheckingTypeDate) |error|:(missing value))'s matchesInString:thisString options:0 range:{0, length of thisString})
if (count m) > 0 then
set d to (item 1 of m)'s |date|() -- get the NSDate of the first item
set df to its NSDateFormatter's new() -- create a NSDateFormatter
df's setDateFormat:"yyyy" -- a specified output format: "day/month/year" (day and month = two digits, year = 4 digits)
return (df's stringFromDate:d) as text
end if
end tell
return "" -- no match in this string
end dateStringToYear