@@ -108,6 +108,9 @@ DROPBOX_OAUTH_SECRET= |
||
| 108 | 108 |
WUNDERLIST_OAUTH_KEY= |
| 109 | 109 |
WUNDERLIST_OAUTH_SECRET= |
| 110 | 110 |
|
| 111 |
+EVERNOTE_OAUTH_KEY= |
|
| 112 |
+EVERNOTE_OAUTH_SECRET= |
|
| 113 |
+ |
|
| 111 | 114 |
############################# |
| 112 | 115 |
# AWS and Mechanical Turk # |
| 113 | 116 |
############################# |
@@ -39,6 +39,10 @@ gem 'omniauth-dropbox' |
||
| 39 | 39 |
# UserLocationAgent |
| 40 | 40 |
gem 'haversine' |
| 41 | 41 |
|
| 42 |
+# EvernoteAgent |
|
| 43 |
+gem 'omniauth-evernote' |
|
| 44 |
+gem 'evernote_oauth' |
|
| 45 |
+ |
|
| 42 | 46 |
# Optional Services. |
| 43 | 47 |
gem 'omniauth-37signals' # BasecampAgent |
| 44 | 48 |
gem 'omniauth-wunderlist', github: 'wunderlist/omniauth-wunderlist', ref: 'd0910d0396107b9302aa1bc50e74bb140990ccb8' |
@@ -179,6 +179,10 @@ GEM |
||
| 179 | 179 |
ethon (0.7.1) |
| 180 | 180 |
ffi (>= 1.3.0) |
| 181 | 181 |
eventmachine (1.0.7) |
| 182 |
+ evernote-thrift (1.25.1) |
|
| 183 |
+ evernote_oauth (0.2.3) |
|
| 184 |
+ evernote-thrift |
|
| 185 |
+ oauth (>= 0.4.1) |
|
| 182 | 186 |
execjs (2.3.0) |
| 183 | 187 |
extlib (0.9.16) |
| 184 | 188 |
faraday (0.9.1) |
@@ -318,6 +322,10 @@ GEM |
||
| 318 | 322 |
omniauth-oauth2 (~> 1.0) |
| 319 | 323 |
omniauth-dropbox (0.2.0) |
| 320 | 324 |
omniauth-oauth (~> 1.0) |
| 325 |
+ omniauth-evernote (1.2.1) |
|
| 326 |
+ evernote-thrift |
|
| 327 |
+ multi_json (~> 1.0) |
|
| 328 |
+ omniauth-oauth (~> 1.0) |
|
| 321 | 329 |
omniauth-oauth (1.0.1) |
| 322 | 330 |
oauth |
| 323 | 331 |
omniauth (~> 1.0) |
@@ -543,6 +551,7 @@ DEPENDENCIES |
||
| 543 | 551 |
dotenv-rails (~> 2.0.1) |
| 544 | 552 |
dropbox-api |
| 545 | 553 |
em-http-request (~> 1.1.2) |
| 554 |
+ evernote_oauth |
|
| 546 | 555 |
faraday (~> 0.9.0) |
| 547 | 556 |
faraday_middleware (>= 0.10.0) |
| 548 | 557 |
feed-normalizer |
@@ -576,6 +585,7 @@ DEPENDENCIES |
||
| 576 | 585 |
omniauth |
| 577 | 586 |
omniauth-37signals |
| 578 | 587 |
omniauth-dropbox |
| 588 |
+ omniauth-evernote |
|
| 579 | 589 |
omniauth-tumblr |
| 580 | 590 |
omniauth-twitter |
| 581 | 591 |
omniauth-wunderlist! |
@@ -0,0 +1,48 @@ |
||
| 1 |
+module EvernoteConcern |
|
| 2 |
+ extend ActiveSupport::Concern |
|
| 3 |
+ |
|
| 4 |
+ included do |
|
| 5 |
+ include Oauthable |
|
| 6 |
+ |
|
| 7 |
+ validate :validate_evernote_options |
|
| 8 |
+ |
|
| 9 |
+ valid_oauth_providers :evernote |
|
| 10 |
+ |
|
| 11 |
+ gem_dependency_check { defined?(EvernoteOAuth) && Devise.omniauth_providers.include?(:evernote) }
|
|
| 12 |
+ end |
|
| 13 |
+ |
|
| 14 |
+ def evernote_client |
|
| 15 |
+ EvernoteOAuth::Client.new( |
|
| 16 |
+ token: evernote_oauth_token, |
|
| 17 |
+ consumer_key: evernote_consumer_key, |
|
| 18 |
+ consumer_secret: evernote_consumer_secret, |
|
| 19 |
+ sandbox: false |
|
| 20 |
+ ) |
|
| 21 |
+ end |
|
| 22 |
+ |
|
| 23 |
+ private |
|
| 24 |
+ |
|
| 25 |
+ def validate_evernote_options |
|
| 26 |
+ unless evernote_consumer_key.present? && |
|
| 27 |
+ evernote_consumer_secret.present? && |
|
| 28 |
+ evernote_oauth_token.present? |
|
| 29 |
+ errors.add(:base, "Evernote consumer_key, consumer_secret, oauth_token, and oauth_token_secret are required to authenticate with the Twitter API. You can provide these as options to this Agent, or as Credentials with the same names, but starting with 'evernote_'.") |
|
| 30 |
+ end |
|
| 31 |
+ end |
|
| 32 |
+ |
|
| 33 |
+ def evernote_consumer_key |
|
| 34 |
+ (config = Devise.omniauth_configs[:evernote]) && config.strategy.consumer_key |
|
| 35 |
+ end |
|
| 36 |
+ |
|
| 37 |
+ def evernote_consumer_secret |
|
| 38 |
+ (config = Devise.omniauth_configs[:evernote]) && config.strategy.consumer_secret |
|
| 39 |
+ end |
|
| 40 |
+ |
|
| 41 |
+ def evernote_oauth_token |
|
| 42 |
+ service && service.token |
|
| 43 |
+ end |
|
| 44 |
+ |
|
| 45 |
+ def evernote_oauth_token_secret |
|
| 46 |
+ service && service.secret |
|
| 47 |
+ end |
|
| 48 |
+end |
@@ -0,0 +1,373 @@ |
||
| 1 |
+module Agents |
|
| 2 |
+ class EvernoteAgent < Agent |
|
| 3 |
+ include EvernoteConcern |
|
| 4 |
+ |
|
| 5 |
+ description <<-MD |
|
| 6 |
+ The Evernote Agent connects with a user's Evernote note store. |
|
| 7 |
+ |
|
| 8 |
+ To be able to use this agent with your account you need to authenticate with Evernote in the [Services](/services) section. |
|
| 9 |
+ |
|
| 10 |
+ Options: |
|
| 11 |
+ |
|
| 12 |
+ * `mode` - Two possible values: |
|
| 13 |
+ |
|
| 14 |
+ - `update` Based on events it receives, the agent will create notes |
|
| 15 |
+ or update notes with the same `title` and `notebook` |
|
| 16 |
+ |
|
| 17 |
+ - `read` On a schedule, it will generate events containing data for newly |
|
| 18 |
+ added or updated notes |
|
| 19 |
+ |
|
| 20 |
+ * `include_xhtml_content` - Set to `true` to include the content in ENML (Evernote Markup Language) of the note |
|
| 21 |
+ |
|
| 22 |
+ * `note` |
|
| 23 |
+ |
|
| 24 |
+ - When `mode` is `update` the parameters of `note` are the attributes of the note to be added/edited. |
|
| 25 |
+ To edit a note, both `title` and `notebook` must be set. |
|
| 26 |
+ |
|
| 27 |
+ For example, to add the tags 'comic' and 'CS' to a note titled 'xkcd Survey' in the notebook 'xkcd', use: |
|
| 28 |
+ |
|
| 29 |
+ "notes": {
|
|
| 30 |
+ "title": "xkcd Survey", |
|
| 31 |
+ "content": "", |
|
| 32 |
+ "notebook": "xkcd", |
|
| 33 |
+ "tagNames": "comic, CS" |
|
| 34 |
+ } |
|
| 35 |
+ |
|
| 36 |
+ If a note with the above title and notebook did note exist already, one would be created. |
|
| 37 |
+ |
|
| 38 |
+ - When `mode` is `read` the values are search parameters. |
|
| 39 |
+ Note: The `content` parameter is not used for searching. |
|
| 40 |
+ |
|
| 41 |
+ For example, to find all notes with tag 'CS' in the notebook 'xkcd', use: |
|
| 42 |
+ |
|
| 43 |
+ "notes": {
|
|
| 44 |
+ "title": "", |
|
| 45 |
+ "content": "", |
|
| 46 |
+ "notebook": "xkcd", |
|
| 47 |
+ "tagNames": "CS" |
|
| 48 |
+ } |
|
| 49 |
+ MD |
|
| 50 |
+ |
|
| 51 |
+ event_description <<-MD |
|
| 52 |
+ When `mode` is `update`, events look like: |
|
| 53 |
+ |
|
| 54 |
+ {
|
|
| 55 |
+ "title": "...", |
|
| 56 |
+ "content": "...", |
|
| 57 |
+ "notebook": "...", |
|
| 58 |
+ "tags": "...", |
|
| 59 |
+ "source": "...", |
|
| 60 |
+ "sourceURL": "..." |
|
| 61 |
+ } |
|
| 62 |
+ |
|
| 63 |
+ When `mode` is `read`, events look like: |
|
| 64 |
+ |
|
| 65 |
+ {
|
|
| 66 |
+ "title": "...", |
|
| 67 |
+ "content": "...", |
|
| 68 |
+ "notebook": "...", |
|
| 69 |
+ "tags": "...", |
|
| 70 |
+ "source": "...", |
|
| 71 |
+ "sourceURL": "...", |
|
| 72 |
+ "resources" : [ |
|
| 73 |
+ {
|
|
| 74 |
+ "url": "resource1_url", |
|
| 75 |
+ "name": "resource1_name", |
|
| 76 |
+ "mime_type": "resource1_mime_type" |
|
| 77 |
+ } |
|
| 78 |
+ ... |
|
| 79 |
+ ] |
|
| 80 |
+ } |
|
| 81 |
+ MD |
|
| 82 |
+ |
|
| 83 |
+ default_schedule "never" |
|
| 84 |
+ |
|
| 85 |
+ def working? |
|
| 86 |
+ event_created_within?(interpolated['expected_update_period_in_days']) && !recent_error_logs? |
|
| 87 |
+ end |
|
| 88 |
+ |
|
| 89 |
+ def default_options |
|
| 90 |
+ {
|
|
| 91 |
+ "expected_update_period_in_days" => "2", |
|
| 92 |
+ "mode" => "update", |
|
| 93 |
+ "include_xhtml_content" => "false", |
|
| 94 |
+ "note" => {
|
|
| 95 |
+ "title" => "{{title}}",
|
|
| 96 |
+ "content" => "{{content}}",
|
|
| 97 |
+ "notebook" => "{{notebook}}",
|
|
| 98 |
+ "tagNames" => "{{tag1}}, {{tag2}}"
|
|
| 99 |
+ } |
|
| 100 |
+ } |
|
| 101 |
+ end |
|
| 102 |
+ |
|
| 103 |
+ def validate_options |
|
| 104 |
+ errors.add(:base, "mode must be 'update' or 'read'") unless %w(read update).include?(options[:mode]) |
|
| 105 |
+ |
|
| 106 |
+ if options[:mode] == "update" && schedule != "never" |
|
| 107 |
+ errors.add(:base, "when mode is set to 'update', schedule must be 'never'") |
|
| 108 |
+ end |
|
| 109 |
+ |
|
| 110 |
+ if options[:mode] == "read" && schedule == "never" |
|
| 111 |
+ errors.add(:base, "when mode is set to 'read', agent must have a schedule") |
|
| 112 |
+ end |
|
| 113 |
+ |
|
| 114 |
+ errors.add(:base, "expected_update_period_in_days is required") unless options['expected_update_period_in_days'].present? |
|
| 115 |
+ |
|
| 116 |
+ if options[:mode] == "update" && options[:note].values.all?(&:empty?) |
|
| 117 |
+ errors.add(:base, "you must specify at least one note parameter to create or update a note") |
|
| 118 |
+ end |
|
| 119 |
+ end |
|
| 120 |
+ |
|
| 121 |
+ def include_xhtml_content? |
|
| 122 |
+ options[:include_xhtml_content] == "true" |
|
| 123 |
+ end |
|
| 124 |
+ |
|
| 125 |
+ def receive(incoming_events) |
|
| 126 |
+ if options[:mode] == "update" |
|
| 127 |
+ incoming_events.each do |event| |
|
| 128 |
+ note = note_store.create_or_update_note(note_params(event)) |
|
| 129 |
+ create_event :payload => note.attr(include_content: include_xhtml_content?) |
|
| 130 |
+ end |
|
| 131 |
+ end |
|
| 132 |
+ end |
|
| 133 |
+ |
|
| 134 |
+ def check |
|
| 135 |
+ if options[:mode] == "read" |
|
| 136 |
+ opts = note_params(options) |
|
| 137 |
+ |
|
| 138 |
+ # convert time to evernote timestamp format: |
|
| 139 |
+ # https://dev.evernote.com/doc/reference/Types.html#Typedef_Timestamp |
|
| 140 |
+ opts.merge!(agent_created_at: created_at.to_i * 1000) |
|
| 141 |
+ opts.merge!(last_checked_at: (memory[:last_checked_at] ||= created_at.to_i * 1000)) |
|
| 142 |
+ |
|
| 143 |
+ if opts[:tagNames] |
|
| 144 |
+ opts.merge!(notes_with_tags: (memory[:notes_with_tags] ||= |
|
| 145 |
+ NoteStore::Search.new(note_store, {tagNames: opts[:tagNames]}).note_guids))
|
|
| 146 |
+ end |
|
| 147 |
+ |
|
| 148 |
+ notes = NoteStore::Search.new(note_store, opts).notes |
|
| 149 |
+ notes.each do |note| |
|
| 150 |
+ memory[:notes_with_tags] << note.guid unless memory[:notes_with_tags].include?(note.guid) |
|
| 151 |
+ |
|
| 152 |
+ create_event :payload => note.attr(include_resources: true, include_content: include_xhtml_content?) |
|
| 153 |
+ end |
|
| 154 |
+ |
|
| 155 |
+ memory[:last_checked_at] = Time.now.to_i * 1000 |
|
| 156 |
+ save! |
|
| 157 |
+ end |
|
| 158 |
+ end |
|
| 159 |
+ |
|
| 160 |
+ private |
|
| 161 |
+ |
|
| 162 |
+ def note_params(options) |
|
| 163 |
+ params = interpolated(options)[:note] |
|
| 164 |
+ errors.add(:base, "only one notebook allowed") unless params[:notebook].to_s.split(", ") == 1
|
|
| 165 |
+ params[:tagNames] = params[:tagNames].to_s.split(", ")
|
|
| 166 |
+ params |
|
| 167 |
+ end |
|
| 168 |
+ |
|
| 169 |
+ def evernote_note_store |
|
| 170 |
+ evernote_client.note_store |
|
| 171 |
+ end |
|
| 172 |
+ |
|
| 173 |
+ def note_store |
|
| 174 |
+ NoteStore.new(evernote_note_store) |
|
| 175 |
+ end |
|
| 176 |
+ |
|
| 177 |
+ # wrapper for evernote api NoteStore |
|
| 178 |
+ # https://dev.evernote.com/doc/reference/ |
|
| 179 |
+ class NoteStore |
|
| 180 |
+ attr_reader :en_note_store |
|
| 181 |
+ delegate :createNote, :updateNote, :getNote, :listNotebooks, :listTags, :getNotebook, |
|
| 182 |
+ :createNotebook, :findNotesMetadata, :getNoteTagNames, :to => :en_note_store |
|
| 183 |
+ |
|
| 184 |
+ def initialize(en_note_store) |
|
| 185 |
+ @en_note_store = en_note_store |
|
| 186 |
+ end |
|
| 187 |
+ |
|
| 188 |
+ def create_or_update_note(params) |
|
| 189 |
+ search = Search.new(self, {title: params[:title], notebook: params[:notebook]})
|
|
| 190 |
+ # evernote search can only filter notes with titles containing a substring; |
|
| 191 |
+ # this finds a note with the exact title |
|
| 192 |
+ note = search.notes.detect {|note| note.title == params[:title]}
|
|
| 193 |
+ if note |
|
| 194 |
+ # a note with specified title and notebook exists, so update it |
|
| 195 |
+ update_note(params.merge(guid: note.guid, notebookGuid: note.notebookGuid)) |
|
| 196 |
+ else |
|
| 197 |
+ # create the notebook unless it already exists |
|
| 198 |
+ notebook = find_notebook(name: params[:notebook]) |
|
| 199 |
+ notebook_guid = |
|
| 200 |
+ notebook ? notebook.guid : create_notebook(params[:notebook]).guid |
|
| 201 |
+ |
|
| 202 |
+ create_note(params.merge(notebookGuid: notebook_guid)) |
|
| 203 |
+ end |
|
| 204 |
+ end |
|
| 205 |
+ |
|
| 206 |
+ def create_note(params) |
|
| 207 |
+ note = Evernote::EDAM::Type::Note.new(with_wrapped_content(params)) |
|
| 208 |
+ en_note = createNote(note) |
|
| 209 |
+ find_note(en_note.guid) |
|
| 210 |
+ end |
|
| 211 |
+ |
|
| 212 |
+ def update_note(params) |
|
| 213 |
+ # do not empty note properties that have not been set in `params` |
|
| 214 |
+ params.keys.each { |key| params.delete(key) unless params[key].present? }
|
|
| 215 |
+ params = with_wrapped_content(params) |
|
| 216 |
+ |
|
| 217 |
+ # append specified tags instead of replacing current tags |
|
| 218 |
+ tags = getNoteTagNames(params[:guid]) |
|
| 219 |
+ tags.each { |tag|
|
|
| 220 |
+ params[:tagNames] << tag unless params[:tagNames].include?(tag) } |
|
| 221 |
+ |
|
| 222 |
+ note = Evernote::EDAM::Type::Note.new(params) |
|
| 223 |
+ updateNote(note) |
|
| 224 |
+ find_note(params[:guid]) |
|
| 225 |
+ end |
|
| 226 |
+ |
|
| 227 |
+ def find_note(guid) |
|
| 228 |
+ # https://dev.evernote.com/doc/reference/NoteStore.html#Fn_NoteStore_getNote |
|
| 229 |
+ en_note = getNote(guid, true, false, false, false) |
|
| 230 |
+ build_note(en_note) |
|
| 231 |
+ end |
|
| 232 |
+ |
|
| 233 |
+ def build_note(en_note) |
|
| 234 |
+ notebook = find_notebook(guid: en_note.notebookGuid).name |
|
| 235 |
+ tags = en_note.tagNames || find_tags(en_note.tagGuids.to_a).map(&:name) |
|
| 236 |
+ Note.new(en_note, notebook, tags) |
|
| 237 |
+ end |
|
| 238 |
+ |
|
| 239 |
+ def find_tags(guids) |
|
| 240 |
+ listTags.select {|tag| guids.include?(tag.guid)}
|
|
| 241 |
+ end |
|
| 242 |
+ |
|
| 243 |
+ def find_notebook(params) |
|
| 244 |
+ if params[:guid] |
|
| 245 |
+ listNotebooks.detect {|notebook| notebook.guid == params[:guid]}
|
|
| 246 |
+ elsif params[:name] |
|
| 247 |
+ listNotebooks.detect {|notebook| notebook.name == params[:name]}
|
|
| 248 |
+ end |
|
| 249 |
+ end |
|
| 250 |
+ |
|
| 251 |
+ def create_notebook(name) |
|
| 252 |
+ notebook = Evernote::EDAM::Type::Notebook.new(name: name) |
|
| 253 |
+ createNotebook(notebook) |
|
| 254 |
+ end |
|
| 255 |
+ |
|
| 256 |
+ def with_wrapped_content(params) |
|
| 257 |
+ params.delete(:notebook) |
|
| 258 |
+ |
|
| 259 |
+ if params[:content] |
|
| 260 |
+ params[:content] = |
|
| 261 |
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" \ |
|
| 262 |
+ "<!DOCTYPE en-note SYSTEM \"http://xml.evernote.com/pub/enml2.dtd\">" \ |
|
| 263 |
+ "<en-note>#{params[:content]}</en-note>"
|
|
| 264 |
+ end |
|
| 265 |
+ |
|
| 266 |
+ params |
|
| 267 |
+ end |
|
| 268 |
+ |
|
| 269 |
+ class Search |
|
| 270 |
+ attr_reader :note_store, :opts |
|
| 271 |
+ def initialize(note_store, opts) |
|
| 272 |
+ @note_store = note_store |
|
| 273 |
+ @opts = opts |
|
| 274 |
+ end |
|
| 275 |
+ |
|
| 276 |
+ def filtered_metadata |
|
| 277 |
+ filter, spec = create_filter, create_spec |
|
| 278 |
+ metadata = note_store.findNotesMetadata(filter, 0, 100, spec).notes |
|
| 279 |
+ end |
|
| 280 |
+ |
|
| 281 |
+ def note_guids |
|
| 282 |
+ filtered_metadata.map(&:guid) |
|
| 283 |
+ end |
|
| 284 |
+ |
|
| 285 |
+ def notes |
|
| 286 |
+ metadata = filtered_metadata |
|
| 287 |
+ |
|
| 288 |
+ if opts[:last_checked_at] && opts[:tagNames] |
|
| 289 |
+ |
|
| 290 |
+ # evernote does note change Note#updated timestamp when a tag is added to a note |
|
| 291 |
+ # the following selects recently updated notes |
|
| 292 |
+ # and notes that recently had the specified tags added |
|
| 293 |
+ metadata.select! do |note_data| |
|
| 294 |
+ note_data.updated > opts[:last_checked_at] || |
|
| 295 |
+ (!opts[:notes_with_tags].include?(note_data.guid) && note_data.created > opts[:agent_created_at]) |
|
| 296 |
+ end |
|
| 297 |
+ |
|
| 298 |
+ elsif opts[:last_checked_at] |
|
| 299 |
+ metadata.select! { |note_data| note_data.updated > opts[:last_checked_at] }
|
|
| 300 |
+ end |
|
| 301 |
+ |
|
| 302 |
+ metadata.map! { |note_data| note_store.find_note(note_data.guid) }
|
|
| 303 |
+ metadata |
|
| 304 |
+ end |
|
| 305 |
+ |
|
| 306 |
+ private |
|
| 307 |
+ |
|
| 308 |
+ def create_filter |
|
| 309 |
+ filter = Evernote::EDAM::NoteStore::NoteFilter.new |
|
| 310 |
+ |
|
| 311 |
+ # evernote search grammar: |
|
| 312 |
+ # https://dev.evernote.com/doc/articles/search_grammar.php#Search_Terms |
|
| 313 |
+ query_terms = [] |
|
| 314 |
+ query_terms << "notebook:\"#{opts[:notebook]}\"" if opts[:notebook].present?
|
|
| 315 |
+ query_terms << "intitle:\"#{opts[:title]}\"" if opts[:title].present?
|
|
| 316 |
+ query_terms << "updated:day-1" if opts[:last_checked_at].present? |
|
| 317 |
+ opts[:tagNames].to_a.each { |tag| query_terms << "tag:#{tag}" }
|
|
| 318 |
+ |
|
| 319 |
+ filter.words = query_terms.join(" ")
|
|
| 320 |
+ filter |
|
| 321 |
+ end |
|
| 322 |
+ |
|
| 323 |
+ def create_spec |
|
| 324 |
+ Evernote::EDAM::NoteStore::NotesMetadataResultSpec.new( |
|
| 325 |
+ includeTitle: true, |
|
| 326 |
+ includeAttributes: true, |
|
| 327 |
+ includeNotebookGuid: true, |
|
| 328 |
+ includeTagGuids: true, |
|
| 329 |
+ includeUpdated: true, |
|
| 330 |
+ includeCreated: true |
|
| 331 |
+ ) |
|
| 332 |
+ end |
|
| 333 |
+ end |
|
| 334 |
+ end |
|
| 335 |
+ |
|
| 336 |
+ class Note |
|
| 337 |
+ attr_accessor :en_note |
|
| 338 |
+ attr_reader :notebook, :tags |
|
| 339 |
+ delegate :guid, :notebookGuid, :title, :tagGuids, :content, :resources, |
|
| 340 |
+ :attributes, :to => :en_note |
|
| 341 |
+ |
|
| 342 |
+ def initialize(en_note, notebook, tags) |
|
| 343 |
+ @en_note = en_note |
|
| 344 |
+ @notebook = notebook |
|
| 345 |
+ @tags = tags |
|
| 346 |
+ end |
|
| 347 |
+ |
|
| 348 |
+ def attr(opts = {})
|
|
| 349 |
+ return_attr = {
|
|
| 350 |
+ title: title, |
|
| 351 |
+ notebook: notebook, |
|
| 352 |
+ tags: tags, |
|
| 353 |
+ source: attributes.source, |
|
| 354 |
+ source_url: attributes.sourceURL |
|
| 355 |
+ } |
|
| 356 |
+ |
|
| 357 |
+ return_attr[:content] = content if opts[:include_content] |
|
| 358 |
+ |
|
| 359 |
+ if opts[:include_resources] && resources |
|
| 360 |
+ return_attr[:resources] = [] |
|
| 361 |
+ resources.each do |resource| |
|
| 362 |
+ return_attr[:resources] << {
|
|
| 363 |
+ url: resource.attributes.sourceURL, |
|
| 364 |
+ name: resource.attributes.fileName, |
|
| 365 |
+ mime_type: resource.mime |
|
| 366 |
+ } |
|
| 367 |
+ end |
|
| 368 |
+ end |
|
| 369 |
+ return_attr |
|
| 370 |
+ end |
|
| 371 |
+ end |
|
| 372 |
+ end |
|
| 373 |
+end |
@@ -263,6 +263,16 @@ Devise.setup do |config| |
||
| 263 | 263 |
config.omniauth :wunderlist, key, secret |
| 264 | 264 |
end |
| 265 | 265 |
|
| 266 |
+ if defined?(OmniAuth::Strategies::Evernote) && |
|
| 267 |
+ (key = ENV["EVERNOTE_OAUTH_KEY"]).present? && |
|
| 268 |
+ (secret = ENV["EVERNOTE_OAUTH_SECRET"]).present? |
|
| 269 |
+ # for production: |
|
| 270 |
+ config.omniauth :evernote, key, secret |
|
| 271 |
+ |
|
| 272 |
+ # for development: |
|
| 273 |
+ # config.omniauth :evernote, key, secret, client_options: { :site => 'https://sandbox.evernote.com' }
|
|
| 274 |
+ end |
|
| 275 |
+ |
|
| 266 | 276 |
# ==> Warden configuration |
| 267 | 277 |
# If you want to use other strategies, that are not supported by Devise, or |
| 268 | 278 |
# change the failure app, you can configure them inside the config.warden block. |
@@ -33,6 +33,7 @@ en: |
||
| 33 | 33 |
37signals: "37Signals (Basecamp)" |
| 34 | 34 |
dropbox: "Dropbox" |
| 35 | 35 |
wunderlist: 'Wunderlist' |
| 36 |
+ evernote: "Evernote" |
|
| 36 | 37 |
passwords: |
| 37 | 38 |
no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." |
| 38 | 39 |
send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." |
@@ -0,0 +1,297 @@ |
||
| 1 |
+require 'spec_helper' |
|
| 2 |
+ |
|
| 3 |
+describe Agents::EvernoteAgent do |
|
| 4 |
+ |
|
| 5 |
+ let(:note_store) do |
|
| 6 |
+ class FakeEvernoteNoteStore |
|
| 7 |
+ attr_accessor :notes, :tags, :notebooks |
|
| 8 |
+ def initialize |
|
| 9 |
+ @notes, @tags, @notebooks = [], [], [] |
|
| 10 |
+ end |
|
| 11 |
+ |
|
| 12 |
+ def createNote(note) |
|
| 13 |
+ note.attributes = OpenStruct.new(source: nil, sourceURL: nil) |
|
| 14 |
+ note.guid = @notes.length + 1 |
|
| 15 |
+ @notes << note |
|
| 16 |
+ note |
|
| 17 |
+ end |
|
| 18 |
+ |
|
| 19 |
+ def updateNote(note) |
|
| 20 |
+ note.attributes = OpenStruct.new(source: nil, sourceURL: nil) |
|
| 21 |
+ old_note = @notes.find {|en_note| en_note.guid == note.guid}
|
|
| 22 |
+ @notes[@notes.index(old_note)] = note |
|
| 23 |
+ note |
|
| 24 |
+ end |
|
| 25 |
+ |
|
| 26 |
+ def getNote(guid, *other_args) |
|
| 27 |
+ @notes.find {|note| note.guid == guid}
|
|
| 28 |
+ end |
|
| 29 |
+ |
|
| 30 |
+ def createNotebook(notebook) |
|
| 31 |
+ notebook.guid = @notebooks.length + 1 |
|
| 32 |
+ @notebooks << notebook |
|
| 33 |
+ notebook |
|
| 34 |
+ end |
|
| 35 |
+ |
|
| 36 |
+ def createTag(tag) |
|
| 37 |
+ tag.guid = @tags.length + 1 |
|
| 38 |
+ @tags << tag |
|
| 39 |
+ tag |
|
| 40 |
+ end |
|
| 41 |
+ |
|
| 42 |
+ def listNotebooks; @notebooks; end |
|
| 43 |
+ |
|
| 44 |
+ def listTags; @tags; end |
|
| 45 |
+ |
|
| 46 |
+ def getNoteTagNames(guid) |
|
| 47 |
+ getNote(guid).try(:tagNames) || [] |
|
| 48 |
+ end |
|
| 49 |
+ |
|
| 50 |
+ def findNotesMetadata(*args); end |
|
| 51 |
+ end |
|
| 52 |
+ |
|
| 53 |
+ note_store = FakeEvernoteNoteStore.new |
|
| 54 |
+ stub.any_instance_of(Agents::EvernoteAgent).evernote_note_store { note_store }
|
|
| 55 |
+ note_store |
|
| 56 |
+ end |
|
| 57 |
+ |
|
| 58 |
+ describe "#receive" do |
|
| 59 |
+ context "when mode is set to 'update'" do |
|
| 60 |
+ before do |
|
| 61 |
+ @options = {
|
|
| 62 |
+ :mode => "update", |
|
| 63 |
+ :include_xhtml_content => "false", |
|
| 64 |
+ :expected_update_period_in_days => "2", |
|
| 65 |
+ :note => {
|
|
| 66 |
+ :title => "{{title}}",
|
|
| 67 |
+ :content => "{{content}}",
|
|
| 68 |
+ :notebook => "{{notebook}}",
|
|
| 69 |
+ :tagNames => "{{tag1}}, {{tag2}}"
|
|
| 70 |
+ } |
|
| 71 |
+ } |
|
| 72 |
+ @agent = Agents::EvernoteAgent.new(:name => "evernote updater", :options => @options) |
|
| 73 |
+ @agent.service = services(:generic) |
|
| 74 |
+ @agent.user = users(:bob) |
|
| 75 |
+ @agent.save! |
|
| 76 |
+ |
|
| 77 |
+ @event = Event.new |
|
| 78 |
+ @event.agent = agents(:bob_website_agent) |
|
| 79 |
+ @event.payload = { :title => "xkcd Survey",
|
|
| 80 |
+ :content => "The xkcd Survey: Big Data for a Big Planet", |
|
| 81 |
+ :notebook => "xkcd", |
|
| 82 |
+ :tag1 => "funny", |
|
| 83 |
+ :tag2 => "data" } |
|
| 84 |
+ @event.save! |
|
| 85 |
+ |
|
| 86 |
+ tag1 = OpenStruct.new(name: "funny") |
|
| 87 |
+ tag2 = OpenStruct.new(name: "data") |
|
| 88 |
+ [tag1, tag2].each { |tag| note_store.createTag(tag) }
|
|
| 89 |
+ end |
|
| 90 |
+ |
|
| 91 |
+ it "adds a note for any payload it receives" do |
|
| 92 |
+ stub(note_store).findNotesMetadata { OpenStruct.new(notes: []) }
|
|
| 93 |
+ Agents::EvernoteAgent.async_receive(@agent.id, [@event.id]) |
|
| 94 |
+ |
|
| 95 |
+ expect(note_store.notes.size).to eq(1) |
|
| 96 |
+ expect(note_store.notes.first.title).to eq("xkcd Survey")
|
|
| 97 |
+ expect(note_store.notebooks.size).to eq(1) |
|
| 98 |
+ expect(note_store.tags.size).to eq(2) |
|
| 99 |
+ |
|
| 100 |
+ expect(@agent.events.count).to eq(1) |
|
| 101 |
+ expect(@agent.events.first.payload).to eq({
|
|
| 102 |
+ "title" => "xkcd Survey", |
|
| 103 |
+ "notebook" => "xkcd", |
|
| 104 |
+ "tags" => ["funny", "data"], |
|
| 105 |
+ "source" => nil, |
|
| 106 |
+ "source_url" => nil |
|
| 107 |
+ }) |
|
| 108 |
+ end |
|
| 109 |
+ |
|
| 110 |
+ context "a note with the same title and notebook exists" do |
|
| 111 |
+ before do |
|
| 112 |
+ note1 = OpenStruct.new(title: "xkcd Survey", notebookGuid: 1) |
|
| 113 |
+ note2 = OpenStruct.new(title: "Footprints", notebookGuid: 1) |
|
| 114 |
+ [note1, note2].each { |note| note_store.createNote(note) }
|
|
| 115 |
+ note_store.createNotebook(OpenStruct.new(name: "xkcd")) |
|
| 116 |
+ |
|
| 117 |
+ stub(note_store).findNotesMetadata {
|
|
| 118 |
+ OpenStruct.new(notes: [note1]) } |
|
| 119 |
+ end |
|
| 120 |
+ |
|
| 121 |
+ it "updates the existing note" do |
|
| 122 |
+ Agents::EvernoteAgent.async_receive(@agent.id, [@event.id]) |
|
| 123 |
+ |
|
| 124 |
+ expect(note_store.notes.size).to eq(2) |
|
| 125 |
+ expect(note_store.getNote(1).tagNames).to eq(["funny", "data"]) |
|
| 126 |
+ expect(@agent.events.count).to eq(1) |
|
| 127 |
+ end |
|
| 128 |
+ end |
|
| 129 |
+ |
|
| 130 |
+ context "include_xhtml_content is set to 'true'" do |
|
| 131 |
+ before do |
|
| 132 |
+ @agent.options[:include_xhtml_content] = "true" |
|
| 133 |
+ @agent.save! |
|
| 134 |
+ end |
|
| 135 |
+ |
|
| 136 |
+ it "creates an event with note content wrapped in ENML" do |
|
| 137 |
+ stub(note_store).findNotesMetadata { OpenStruct.new(notes: []) }
|
|
| 138 |
+ Agents::EvernoteAgent.async_receive(@agent.id, [@event.id]) |
|
| 139 |
+ |
|
| 140 |
+ payload = @agent.events.first.payload |
|
| 141 |
+ |
|
| 142 |
+ expect(payload[:content]).to eq( |
|
| 143 |
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" \ |
|
| 144 |
+ "<!DOCTYPE en-note SYSTEM \"http://xml.evernote.com/pub/enml2.dtd\">" \ |
|
| 145 |
+ "<en-note>The xkcd Survey: Big Data for a Big Planet</en-note>" |
|
| 146 |
+ ) |
|
| 147 |
+ end |
|
| 148 |
+ end |
|
| 149 |
+ end |
|
| 150 |
+ end |
|
| 151 |
+ |
|
| 152 |
+ describe "#check" do |
|
| 153 |
+ context "when mode is set to 'read'" do |
|
| 154 |
+ before do |
|
| 155 |
+ @options = {
|
|
| 156 |
+ :mode => "read", |
|
| 157 |
+ :include_xhtml_content => "false", |
|
| 158 |
+ :expected_update_period_in_days => "2", |
|
| 159 |
+ :note => {
|
|
| 160 |
+ :title => "", |
|
| 161 |
+ :content => "", |
|
| 162 |
+ :notebook => "xkcd", |
|
| 163 |
+ :tagNames => "funny, comic" |
|
| 164 |
+ } |
|
| 165 |
+ } |
|
| 166 |
+ @checker = Agents::EvernoteAgent.new(:name => "evernote reader", :options => @options) |
|
| 167 |
+ |
|
| 168 |
+ @checker.service = services(:generic) |
|
| 169 |
+ @checker.user = users(:bob) |
|
| 170 |
+ @checker.schedule = "every_2h" |
|
| 171 |
+ |
|
| 172 |
+ @checker.save! |
|
| 173 |
+ @checker.created_at = 1.minute.ago |
|
| 174 |
+ |
|
| 175 |
+ note_store.createNote( |
|
| 176 |
+ OpenStruct.new(title: "xkcd Survey", |
|
| 177 |
+ notebookGuid: 1, |
|
| 178 |
+ updated: 2.minutes.ago.to_i * 1000, |
|
| 179 |
+ tagNames: ["funny", "comic"]) |
|
| 180 |
+ ) |
|
| 181 |
+ note_store.createNotebook(OpenStruct.new(name: "xkcd")) |
|
| 182 |
+ tag1 = OpenStruct.new(name: "funny") |
|
| 183 |
+ tag2 = OpenStruct.new(name: "comic") |
|
| 184 |
+ [tag1, tag2].each { |tag| note_store.createTag(tag) }
|
|
| 185 |
+ |
|
| 186 |
+ stub(note_store).findNotesMetadata {
|
|
| 187 |
+ notes = note_store.notes.select do |note| |
|
| 188 |
+ note.notebookGuid == 1 && |
|
| 189 |
+ %w(funny comic).all? { |tag_name| note.tagNames.include?(tag_name) }
|
|
| 190 |
+ end |
|
| 191 |
+ OpenStruct.new(notes: notes) |
|
| 192 |
+ } |
|
| 193 |
+ end |
|
| 194 |
+ |
|
| 195 |
+ context "the first time it checks" do |
|
| 196 |
+ it "returns only notes created/updated since it was created" do |
|
| 197 |
+ expect { @checker.check }.to change { Event.count }.by(0)
|
|
| 198 |
+ end |
|
| 199 |
+ end |
|
| 200 |
+ |
|
| 201 |
+ context "on subsequent checks" do |
|
| 202 |
+ it "returns notes created/updated since the last time it checked" do |
|
| 203 |
+ expect { @checker.check }.to change { Event.count }.by(0)
|
|
| 204 |
+ |
|
| 205 |
+ future_time = (Time.now + 1.minute).to_i * 1000 |
|
| 206 |
+ note_store.createNote( |
|
| 207 |
+ OpenStruct.new(title: "Footprints", |
|
| 208 |
+ notebookGuid: 1, |
|
| 209 |
+ tagNames: ["funny", "comic", "recent"], |
|
| 210 |
+ updated: future_time)) |
|
| 211 |
+ |
|
| 212 |
+ note_store.createNote( |
|
| 213 |
+ OpenStruct.new(title: "something else", |
|
| 214 |
+ notebookGuid: 2, |
|
| 215 |
+ tagNames: ["funny", "comic"], |
|
| 216 |
+ updated: future_time)) |
|
| 217 |
+ |
|
| 218 |
+ expect { @checker.check }.to change { Event.count }.by(1)
|
|
| 219 |
+ end |
|
| 220 |
+ |
|
| 221 |
+ it "returns notes tagged since the last time it checked" do |
|
| 222 |
+ note_store.createNote( |
|
| 223 |
+ OpenStruct.new(title: "Footprints", |
|
| 224 |
+ notebookGuid: 1, |
|
| 225 |
+ tagNames: [], |
|
| 226 |
+ created: Time.now.to_i * 1000, |
|
| 227 |
+ updated: Time.now.to_i * 1000)) |
|
| 228 |
+ @checker.check |
|
| 229 |
+ |
|
| 230 |
+ note_store.getNote(2).tagNames = ["funny", "comic"] |
|
| 231 |
+ |
|
| 232 |
+ expect { @checker.check }.to change { Event.count }.by(1)
|
|
| 233 |
+ end |
|
| 234 |
+ end |
|
| 235 |
+ end |
|
| 236 |
+ end |
|
| 237 |
+ |
|
| 238 |
+ describe "#validation" do |
|
| 239 |
+ before do |
|
| 240 |
+ @options = {
|
|
| 241 |
+ :mode => "update", |
|
| 242 |
+ :include_xhtml_content => "false", |
|
| 243 |
+ :expected_update_period_in_days => "2", |
|
| 244 |
+ :note => {
|
|
| 245 |
+ :title => "{{title}}",
|
|
| 246 |
+ :content => "{{content}}",
|
|
| 247 |
+ :notebook => "{{notebook}}",
|
|
| 248 |
+ :tagNames => "{{tag1}}, {{tag2}}"
|
|
| 249 |
+ } |
|
| 250 |
+ } |
|
| 251 |
+ @agent = Agents::EvernoteAgent.new(:name => "evernote updater", :options => @options) |
|
| 252 |
+ @agent.service = services(:generic) |
|
| 253 |
+ @agent.user = users(:bob) |
|
| 254 |
+ @agent.save! |
|
| 255 |
+ |
|
| 256 |
+ expect(@agent).to be_valid |
|
| 257 |
+ end |
|
| 258 |
+ |
|
| 259 |
+ it "requires the mode to be 'update' or 'read'" do |
|
| 260 |
+ @agent.options[:mode] = "" |
|
| 261 |
+ expect(@agent).not_to be_valid |
|
| 262 |
+ end |
|
| 263 |
+ |
|
| 264 |
+ context "mode is set to 'update'" do |
|
| 265 |
+ before do |
|
| 266 |
+ @agent.options[:mode] = "update" |
|
| 267 |
+ end |
|
| 268 |
+ |
|
| 269 |
+ it "requires some note parameter to be present" do |
|
| 270 |
+ @agent.options[:note].keys.each { |k| @agent.options[:note][k] = "" }
|
|
| 271 |
+ expect(@agent).not_to be_valid |
|
| 272 |
+ end |
|
| 273 |
+ |
|
| 274 |
+ it "requires schedule to be 'never'" do |
|
| 275 |
+ @agent.schedule = 'never' |
|
| 276 |
+ expect(@agent).to be_valid |
|
| 277 |
+ |
|
| 278 |
+ @agent.schedule = 'every_1m' |
|
| 279 |
+ expect(@agent).not_to be_valid |
|
| 280 |
+ end |
|
| 281 |
+ end |
|
| 282 |
+ |
|
| 283 |
+ context "mode is set to 'read'" do |
|
| 284 |
+ before do |
|
| 285 |
+ @agent.options[:mode] = "read" |
|
| 286 |
+ end |
|
| 287 |
+ |
|
| 288 |
+ it "requires a schedule to be set" do |
|
| 289 |
+ @agent.schedule = 'every_1m' |
|
| 290 |
+ expect(@agent).to be_valid |
|
| 291 |
+ |
|
| 292 |
+ @agent.schedule = 'never' |
|
| 293 |
+ expect(@agent).not_to be_valid |
|
| 294 |
+ end |
|
| 295 |
+ end |
|
| 296 |
+ end |
|
| 297 |
+end |