123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391 |
- module Agents
- class EvernoteAgent < Agent
- include EvernoteConcern
- description <<~MD
- The Evernote Agent connects with a user's Evernote note store.
- Visit [Evernote](https://dev.evernote.com/doc/) to set up an Evernote app and receive an api key and secret.
- Store these in the Evernote environment variables in the .env file.
- You will also need to create a [Sandbox](https://sandbox.evernote.com/Registration.action) account to use during development.
- Next, you'll need to authenticate with Evernote in the [Services](/services) section.
- Options:
- * `mode` - Two possible values:
- - `update` Based on events it receives, the agent will create notes
- or update notes with the same `title` and `notebook`
- - `read` On a schedule, it will generate events containing data for newly
- added or updated notes
- * `include_xhtml_content` - Set to `true` to include the content in ENML (Evernote Markup Language) of the note
- * `note`
- - When `mode` is `update` the parameters of `note` are the attributes of the note to be added/edited.
- To edit a note, both `title` and `notebook` must be set.
- For example, to add the tags 'comic' and 'CS' to a note titled 'xkcd Survey' in the notebook 'xkcd', use:
- "notes": {
- "title": "xkcd Survey",
- "content": "",
- "notebook": "xkcd",
- "tagNames": "comic, CS"
- }
- If a note with the above title and notebook did note exist already, one would be created.
- - When `mode` is `read` the values are search parameters.
- Note: The `content` parameter is not used for searching. Setting `title` only filters
- notes whose titles contain `title` as a substring, not as the exact title.
- For example, to find all notes with tag 'CS' in the notebook 'xkcd', use:
- "notes": {
- "title": "",
- "content": "",
- "notebook": "xkcd",
- "tagNames": "CS"
- }
- MD
- event_description <<~MD
- When `mode` is `update`, events look like:
- {
- "title": "...",
- "content": "...",
- "notebook": "...",
- "tags": "...",
- "source": "...",
- "sourceURL": "..."
- }
- When `mode` is `read`, events look like:
- {
- "title": "...",
- "content": "...",
- "notebook": "...",
- "tags": "...",
- "source": "...",
- "sourceURL": "...",
- "resources" : [
- {
- "url": "resource1_url",
- "name": "resource1_name",
- "mime_type": "resource1_mime_type"
- }
- ...
- ]
- }
- MD
- default_schedule "never"
- def working?
- event_created_within?(interpolated['expected_update_period_in_days']) && !recent_error_logs?
- end
- def default_options
- {
- "expected_update_period_in_days" => "2",
- "mode" => "update",
- "include_xhtml_content" => "false",
- "note" => {
- "title" => "{{title}}",
- "content" => "{{content}}",
- "notebook" => "{{notebook}}",
- "tagNames" => "{{tag1}}, {{tag2}}"
- }
- }
- end
- def validate_options
- errors.add(:base, "mode must be 'update' or 'read'") unless %w[read update].include?(options[:mode])
- if options[:mode] == "update" && schedule != "never"
- errors.add(:base, "when mode is set to 'update', schedule must be 'never'")
- end
- if options[:mode] == "read" && schedule == "never"
- errors.add(:base, "when mode is set to 'read', agent must have a schedule")
- end
- errors.add(:base,
- "expected_update_period_in_days is required") unless options['expected_update_period_in_days'].present?
- if options[:mode] == "update" && options[:note].values.all?(&:empty?)
- errors.add(:base, "you must specify at least one note parameter to create or update a note")
- end
- end
- def include_xhtml_content?
- options[:include_xhtml_content] == "true"
- end
- def receive(incoming_events)
- if options[:mode] == "update"
- incoming_events.each do |event|
- note = note_store.create_or_update_note(note_params(event))
- create_event payload: note.attr(include_content: include_xhtml_content?)
- end
- end
- end
- def check
- if options[:mode] == "read"
- opts = note_params(options)
- # convert time to evernote timestamp format:
- # https://dev.evernote.com/doc/reference/Types.html#Typedef_Timestamp
- opts.merge!(agent_created_at: created_at.to_i * 1000)
- opts.merge!(last_checked_at: (memory[:last_checked_at] ||= created_at.to_i * 1000))
- if opts[:tagNames]
- notes_with_tags =
- memory[:notes_with_tags] ||=
- NoteStore::Search.new(note_store, { tagNames: opts[:tagNames] }).note_guids
- opts.merge!(notes_with_tags:)
- end
- notes = NoteStore::Search.new(note_store, opts).notes
- notes.each do |note|
- memory[:notes_with_tags] << note.guid unless memory[:notes_with_tags].include?(note.guid)
- create_event payload: note.attr(include_resources: true, include_content: include_xhtml_content?)
- end
- memory[:last_checked_at] = Time.now.to_i * 1000
- end
- end
- private
- def note_params(options)
- params = interpolated(options)[:note]
- errors.add(:base, "only one notebook allowed") unless params[:notebook].to_s.split(/\s*,\s*/) == 1
- params[:tagNames] = params[:tagNames].to_s.split(/\s*,\s*/)
- params[:title].strip!
- params[:notebook].strip!
- params
- end
- def evernote_note_store
- evernote_client.note_store
- end
- def note_store
- @note_store ||= NoteStore.new(evernote_note_store)
- end
- # wrapper for evernote api NoteStore
- # https://dev.evernote.com/doc/reference/
- class NoteStore
- attr_reader :en_note_store
- delegate :createNote, :updateNote, :getNote, :listNotebooks, :listTags, :getNotebook,
- :createNotebook, :findNotesMetadata, :getNoteTagNames, to: :en_note_store
- def initialize(en_note_store)
- @en_note_store = en_note_store
- end
- def create_or_update_note(params)
- search = Search.new(self, { title: params[:title], notebook: params[:notebook] })
- # evernote search can only filter notes with titles containing a substring;
- # this finds a note with the exact title
- note = search.notes.detect { |note| note.title == params[:title] }
- if note
- # a note with specified title and notebook exists, so update it
- update_note(params.merge(guid: note.guid, notebookGuid: note.notebookGuid))
- else
- # create the notebook unless it already exists
- notebook = find_notebook(name: params[:notebook])
- notebook_guid =
- notebook ? notebook.guid : create_notebook(params[:notebook]).guid
- create_note(params.merge(notebookGuid: notebook_guid))
- end
- end
- def create_note(params)
- note = Evernote::EDAM::Type::Note.new(with_wrapped_content(params))
- en_note = createNote(note)
- find_note(en_note.guid)
- end
- def update_note(params)
- # do not empty note properties that have not been set in `params`
- params.keys.each { |key| params.delete(key) unless params[key].present? }
- params = with_wrapped_content(params)
- # append specified tags instead of replacing current tags
- # evernote will create any new tags
- tags = getNoteTagNames(params[:guid])
- tags.each { |tag|
- params[:tagNames] << tag unless params[:tagNames].include?(tag)
- }
- note = Evernote::EDAM::Type::Note.new(params)
- updateNote(note)
- find_note(params[:guid])
- end
- def find_note(guid)
- # https://dev.evernote.com/doc/reference/NoteStore.html#Fn_NoteStore_getNote
- en_note = getNote(guid, true, false, false, false)
- build_note(en_note)
- end
- def build_note(en_note)
- notebook = find_notebook(guid: en_note.notebookGuid).try(:name)
- tags = en_note.tagNames || find_tags(en_note.tagGuids.to_a).map(&:name)
- Note.new(en_note, notebook, tags)
- end
- def find_tags(guids)
- listTags.select { |tag| guids.include?(tag.guid) }
- end
- def find_notebook(params)
- if params[:guid]
- listNotebooks.detect { |notebook| notebook.guid == params[:guid] }
- elsif params[:name]
- listNotebooks.detect { |notebook| notebook.name == params[:name] }
- end
- end
- def create_notebook(name)
- notebook = Evernote::EDAM::Type::Notebook.new(name:)
- createNotebook(notebook)
- end
- def with_wrapped_content(params)
- params.delete(:notebook)
- if params[:content]
- params[:content] =
- "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" \
- "<!DOCTYPE en-note SYSTEM \"http://xml.evernote.com/pub/enml2.dtd\">" \
- "<en-note>#{params[:content].encode(xml: :text)}</en-note>"
- end
- params
- end
- class Search
- attr_reader :note_store, :opts
- def initialize(note_store, opts)
- @note_store = note_store
- @opts = opts
- end
- def note_guids
- filtered_metadata.map(&:guid)
- end
- def notes
- metadata = filtered_metadata
- if opts[:last_checked_at] && opts[:tagNames]
- # evernote does note change Note#updated timestamp when a tag is added to a note
- # the following selects recently updated notes
- # and notes that recently had the specified tags added
- metadata.select! do |note_data|
- note_data.updated > opts[:last_checked_at] ||
- !opts[:notes_with_tags].include?(note_data.guid)
- end
- elsif opts[:last_checked_at]
- metadata.select! { |note_data| note_data.updated > opts[:last_checked_at] }
- end
- metadata.map! { |note_data| note_store.find_note(note_data.guid) }
- metadata
- end
- def create_filter
- filter = Evernote::EDAM::NoteStore::NoteFilter.new
- # evernote search grammar:
- # https://dev.evernote.com/doc/articles/search_grammar.php#Search_Terms
- query_terms = []
- query_terms << "notebook:\"#{opts[:notebook]}\"" if opts[:notebook].present?
- query_terms << "intitle:\"#{opts[:title]}\"" if opts[:title].present?
- query_terms << "updated:day-1" if opts[:last_checked_at].present?
- opts[:tagNames].to_a.each { |tag| query_terms << "tag:#{tag}" }
- filter.words = query_terms.join(" ")
- filter
- end
- private
- def filtered_metadata
- filter = create_filter
- spec = create_spec
- metadata = note_store.findNotesMetadata(filter, 0, 100, spec).notes
- end
- def create_spec
- Evernote::EDAM::NoteStore::NotesMetadataResultSpec.new(
- includeTitle: true,
- includeAttributes: true,
- includeNotebookGuid: true,
- includeTagGuids: true,
- includeUpdated: true,
- includeCreated: true
- )
- end
- end
- end
- class Note
- attr_accessor :en_note
- attr_reader :notebook, :tags
- delegate :guid, :notebookGuid, :title, :tagGuids, :content, :resources,
- :attributes, to: :en_note
- def initialize(en_note, notebook, tags)
- @en_note = en_note
- @notebook = notebook
- @tags = tags
- end
- def attr(opts = {})
- return_attr = {
- title:,
- notebook:,
- tags:,
- source: attributes.source,
- source_url: attributes.sourceURL
- }
- return_attr[:content] = content if opts[:include_content]
- if opts[:include_resources] && resources
- return_attr[:resources] = []
- resources.each do |resource|
- return_attr[:resources] << {
- url: resource.attributes.sourceURL,
- name: resource.attributes.fileName,
- mime_type: resource.mime
- }
- end
- end
- return_attr
- end
- end
- end
- end
|