@@ -108,6 +108,11 @@ DROPBOX_OAUTH_SECRET= |
||
108 | 108 |
WUNDERLIST_OAUTH_KEY= |
109 | 109 |
WUNDERLIST_OAUTH_SECRET= |
110 | 110 |
|
111 |
+EVERNOTE_OAUTH_KEY= |
|
112 |
+EVERNOTE_OAUTH_SECRET= |
|
113 |
+# Set to true in development, false in production |
|
114 |
+USE_EVERNOTE_SANDBOX=true |
|
115 |
+ |
|
111 | 116 |
############################# |
112 | 117 |
# AWS and Mechanical Turk # |
113 | 118 |
############################# |
@@ -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: use_sandbox? |
|
20 |
+ ) |
|
21 |
+ end |
|
22 |
+ |
|
23 |
+ private |
|
24 |
+ |
|
25 |
+ def use_sandbox? |
|
26 |
+ ENV["USE_EVERNOTE_SANDBOX"] == "true" |
|
27 |
+ end |
|
28 |
+ |
|
29 |
+ def validate_evernote_options |
|
30 |
+ unless evernote_consumer_key.present? && |
|
31 |
+ evernote_consumer_secret.present? && |
|
32 |
+ evernote_oauth_token.present? |
|
33 |
+ errors.add(:base, "Evernote ENV variables and a Service are required") |
|
34 |
+ end |
|
35 |
+ end |
|
36 |
+ |
|
37 |
+ def evernote_consumer_key |
|
38 |
+ (config = Devise.omniauth_configs[:evernote]) && config.strategy.consumer_key |
|
39 |
+ end |
|
40 |
+ |
|
41 |
+ def evernote_consumer_secret |
|
42 |
+ (config = Devise.omniauth_configs[:evernote]) && config.strategy.consumer_secret |
|
43 |
+ end |
|
44 |
+ |
|
45 |
+ def evernote_oauth_token |
|
46 |
+ service && service.token |
|
47 |
+ end |
|
48 |
+end |
@@ -0,0 +1,383 @@ |
||
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 |
+ Visit [Evernote](https://dev.evernote.com/doc/) to set up an Evernote app and receive an api key and secret. |
|
9 |
+ Store these in the Evernote environment variables in the .env file. |
|
10 |
+ You will also need to create a [Sandbox](https://sandbox.evernote.com/Registration.action) account to use during development. |
|
11 |
+ |
|
12 |
+ Next, you'll need to authenticate with Evernote in the [Services](/services) section. |
|
13 |
+ |
|
14 |
+ Options: |
|
15 |
+ |
|
16 |
+ * `mode` - Two possible values: |
|
17 |
+ |
|
18 |
+ - `update` Based on events it receives, the agent will create notes |
|
19 |
+ or update notes with the same `title` and `notebook` |
|
20 |
+ |
|
21 |
+ - `read` On a schedule, it will generate events containing data for newly |
|
22 |
+ added or updated notes |
|
23 |
+ |
|
24 |
+ * `include_xhtml_content` - Set to `true` to include the content in ENML (Evernote Markup Language) of the note |
|
25 |
+ |
|
26 |
+ * `note` |
|
27 |
+ |
|
28 |
+ - When `mode` is `update` the parameters of `note` are the attributes of the note to be added/edited. |
|
29 |
+ To edit a note, both `title` and `notebook` must be set. |
|
30 |
+ |
|
31 |
+ For example, to add the tags 'comic' and 'CS' to a note titled 'xkcd Survey' in the notebook 'xkcd', use: |
|
32 |
+ |
|
33 |
+ "notes": { |
|
34 |
+ "title": "xkcd Survey", |
|
35 |
+ "content": "", |
|
36 |
+ "notebook": "xkcd", |
|
37 |
+ "tagNames": "comic, CS" |
|
38 |
+ } |
|
39 |
+ |
|
40 |
+ If a note with the above title and notebook did note exist already, one would be created. |
|
41 |
+ |
|
42 |
+ - When `mode` is `read` the values are search parameters. |
|
43 |
+ Note: The `content` parameter is not used for searching. Setting `title` only filters |
|
44 |
+ notes whose titles contain `title` as a substring, not as the exact title. |
|
45 |
+ |
|
46 |
+ For example, to find all notes with tag 'CS' in the notebook 'xkcd', use: |
|
47 |
+ |
|
48 |
+ "notes": { |
|
49 |
+ "title": "", |
|
50 |
+ "content": "", |
|
51 |
+ "notebook": "xkcd", |
|
52 |
+ "tagNames": "CS" |
|
53 |
+ } |
|
54 |
+ MD |
|
55 |
+ |
|
56 |
+ event_description <<-MD |
|
57 |
+ When `mode` is `update`, events look like: |
|
58 |
+ |
|
59 |
+ { |
|
60 |
+ "title": "...", |
|
61 |
+ "content": "...", |
|
62 |
+ "notebook": "...", |
|
63 |
+ "tags": "...", |
|
64 |
+ "source": "...", |
|
65 |
+ "sourceURL": "..." |
|
66 |
+ } |
|
67 |
+ |
|
68 |
+ When `mode` is `read`, events look like: |
|
69 |
+ |
|
70 |
+ { |
|
71 |
+ "title": "...", |
|
72 |
+ "content": "...", |
|
73 |
+ "notebook": "...", |
|
74 |
+ "tags": "...", |
|
75 |
+ "source": "...", |
|
76 |
+ "sourceURL": "...", |
|
77 |
+ "resources" : [ |
|
78 |
+ { |
|
79 |
+ "url": "resource1_url", |
|
80 |
+ "name": "resource1_name", |
|
81 |
+ "mime_type": "resource1_mime_type" |
|
82 |
+ } |
|
83 |
+ ... |
|
84 |
+ ] |
|
85 |
+ } |
|
86 |
+ MD |
|
87 |
+ |
|
88 |
+ default_schedule "never" |
|
89 |
+ |
|
90 |
+ def working? |
|
91 |
+ event_created_within?(interpolated['expected_update_period_in_days']) && !recent_error_logs? |
|
92 |
+ end |
|
93 |
+ |
|
94 |
+ def default_options |
|
95 |
+ { |
|
96 |
+ "expected_update_period_in_days" => "2", |
|
97 |
+ "mode" => "update", |
|
98 |
+ "include_xhtml_content" => "false", |
|
99 |
+ "note" => { |
|
100 |
+ "title" => "{{title}}", |
|
101 |
+ "content" => "{{content}}", |
|
102 |
+ "notebook" => "{{notebook}}", |
|
103 |
+ "tagNames" => "{{tag1}}, {{tag2}}" |
|
104 |
+ } |
|
105 |
+ } |
|
106 |
+ end |
|
107 |
+ |
|
108 |
+ def validate_options |
|
109 |
+ errors.add(:base, "mode must be 'update' or 'read'") unless %w(read update).include?(options[:mode]) |
|
110 |
+ |
|
111 |
+ if options[:mode] == "update" && schedule != "never" |
|
112 |
+ errors.add(:base, "when mode is set to 'update', schedule must be 'never'") |
|
113 |
+ end |
|
114 |
+ |
|
115 |
+ if options[:mode] == "read" && schedule == "never" |
|
116 |
+ errors.add(:base, "when mode is set to 'read', agent must have a schedule") |
|
117 |
+ end |
|
118 |
+ |
|
119 |
+ errors.add(:base, "expected_update_period_in_days is required") unless options['expected_update_period_in_days'].present? |
|
120 |
+ |
|
121 |
+ if options[:mode] == "update" && options[:note].values.all?(&:empty?) |
|
122 |
+ errors.add(:base, "you must specify at least one note parameter to create or update a note") |
|
123 |
+ end |
|
124 |
+ end |
|
125 |
+ |
|
126 |
+ def include_xhtml_content? |
|
127 |
+ options[:include_xhtml_content] == "true" |
|
128 |
+ end |
|
129 |
+ |
|
130 |
+ def receive(incoming_events) |
|
131 |
+ if options[:mode] == "update" |
|
132 |
+ incoming_events.each do |event| |
|
133 |
+ note = note_store.create_or_update_note(note_params(event)) |
|
134 |
+ create_event :payload => note.attr(include_content: include_xhtml_content?) |
|
135 |
+ end |
|
136 |
+ end |
|
137 |
+ end |
|
138 |
+ |
|
139 |
+ def check |
|
140 |
+ if options[:mode] == "read" |
|
141 |
+ opts = note_params(options) |
|
142 |
+ |
|
143 |
+ # convert time to evernote timestamp format: |
|
144 |
+ # https://dev.evernote.com/doc/reference/Types.html#Typedef_Timestamp |
|
145 |
+ opts.merge!(agent_created_at: created_at.to_i * 1000) |
|
146 |
+ opts.merge!(last_checked_at: (memory[:last_checked_at] ||= created_at.to_i * 1000)) |
|
147 |
+ |
|
148 |
+ if opts[:tagNames] |
|
149 |
+ opts.merge!(notes_with_tags: (memory[:notes_with_tags] ||= |
|
150 |
+ NoteStore::Search.new(note_store, {tagNames: opts[:tagNames]}).note_guids)) |
|
151 |
+ end |
|
152 |
+ |
|
153 |
+ notes = NoteStore::Search.new(note_store, opts).notes |
|
154 |
+ notes.each do |note| |
|
155 |
+ memory[:notes_with_tags] << note.guid unless memory[:notes_with_tags].include?(note.guid) |
|
156 |
+ |
|
157 |
+ create_event :payload => note.attr(include_resources: true, include_content: include_xhtml_content?) |
|
158 |
+ end |
|
159 |
+ |
|
160 |
+ memory[:last_checked_at] = Time.now.to_i * 1000 |
|
161 |
+ end |
|
162 |
+ end |
|
163 |
+ |
|
164 |
+ private |
|
165 |
+ |
|
166 |
+ def note_params(options) |
|
167 |
+ params = interpolated(options)[:note] |
|
168 |
+ errors.add(:base, "only one notebook allowed") unless params[:notebook].to_s.split(/\s*,\s*/) == 1 |
|
169 |
+ |
|
170 |
+ params[:tagNames] = params[:tagNames].to_s.split(/\s*,\s*/) |
|
171 |
+ params[:title].strip! |
|
172 |
+ params[:notebook].strip! |
|
173 |
+ params |
|
174 |
+ end |
|
175 |
+ |
|
176 |
+ def evernote_note_store |
|
177 |
+ evernote_client.note_store |
|
178 |
+ end |
|
179 |
+ |
|
180 |
+ def note_store |
|
181 |
+ @note_store ||= NoteStore.new(evernote_note_store) |
|
182 |
+ end |
|
183 |
+ |
|
184 |
+ # wrapper for evernote api NoteStore |
|
185 |
+ # https://dev.evernote.com/doc/reference/ |
|
186 |
+ class NoteStore |
|
187 |
+ attr_reader :en_note_store |
|
188 |
+ delegate :createNote, :updateNote, :getNote, :listNotebooks, :listTags, :getNotebook, |
|
189 |
+ :createNotebook, :findNotesMetadata, :getNoteTagNames, :to => :en_note_store |
|
190 |
+ |
|
191 |
+ def initialize(en_note_store) |
|
192 |
+ @en_note_store = en_note_store |
|
193 |
+ end |
|
194 |
+ |
|
195 |
+ def create_or_update_note(params) |
|
196 |
+ search = Search.new(self, {title: params[:title], notebook: params[:notebook]}) |
|
197 |
+ |
|
198 |
+ # evernote search can only filter notes with titles containing a substring; |
|
199 |
+ # this finds a note with the exact title |
|
200 |
+ note = search.notes.detect {|note| note.title == params[:title]} |
|
201 |
+ |
|
202 |
+ if note |
|
203 |
+ # a note with specified title and notebook exists, so update it |
|
204 |
+ update_note(params.merge(guid: note.guid, notebookGuid: note.notebookGuid)) |
|
205 |
+ else |
|
206 |
+ # create the notebook unless it already exists |
|
207 |
+ notebook = find_notebook(name: params[:notebook]) |
|
208 |
+ notebook_guid = |
|
209 |
+ notebook ? notebook.guid : create_notebook(params[:notebook]).guid |
|
210 |
+ |
|
211 |
+ create_note(params.merge(notebookGuid: notebook_guid)) |
|
212 |
+ end |
|
213 |
+ end |
|
214 |
+ |
|
215 |
+ def create_note(params) |
|
216 |
+ note = Evernote::EDAM::Type::Note.new(with_wrapped_content(params)) |
|
217 |
+ en_note = createNote(note) |
|
218 |
+ find_note(en_note.guid) |
|
219 |
+ end |
|
220 |
+ |
|
221 |
+ def update_note(params) |
|
222 |
+ # do not empty note properties that have not been set in `params` |
|
223 |
+ params.keys.each { |key| params.delete(key) unless params[key].present? } |
|
224 |
+ params = with_wrapped_content(params) |
|
225 |
+ |
|
226 |
+ # append specified tags instead of replacing current tags |
|
227 |
+ # evernote will create any new tags |
|
228 |
+ tags = getNoteTagNames(params[:guid]) |
|
229 |
+ tags.each { |tag| |
|
230 |
+ params[:tagNames] << tag unless params[:tagNames].include?(tag) } |
|
231 |
+ |
|
232 |
+ note = Evernote::EDAM::Type::Note.new(params) |
|
233 |
+ updateNote(note) |
|
234 |
+ find_note(params[:guid]) |
|
235 |
+ end |
|
236 |
+ |
|
237 |
+ def find_note(guid) |
|
238 |
+ # https://dev.evernote.com/doc/reference/NoteStore.html#Fn_NoteStore_getNote |
|
239 |
+ en_note = getNote(guid, true, false, false, false) |
|
240 |
+ build_note(en_note) |
|
241 |
+ end |
|
242 |
+ |
|
243 |
+ def build_note(en_note) |
|
244 |
+ notebook = find_notebook(guid: en_note.notebookGuid).try(:name) |
|
245 |
+ tags = en_note.tagNames || find_tags(en_note.tagGuids.to_a).map(&:name) |
|
246 |
+ Note.new(en_note, notebook, tags) |
|
247 |
+ end |
|
248 |
+ |
|
249 |
+ def find_tags(guids) |
|
250 |
+ listTags.select {|tag| guids.include?(tag.guid)} |
|
251 |
+ end |
|
252 |
+ |
|
253 |
+ def find_notebook(params) |
|
254 |
+ if params[:guid] |
|
255 |
+ listNotebooks.detect {|notebook| notebook.guid == params[:guid]} |
|
256 |
+ elsif params[:name] |
|
257 |
+ listNotebooks.detect {|notebook| notebook.name == params[:name]} |
|
258 |
+ end |
|
259 |
+ end |
|
260 |
+ |
|
261 |
+ def create_notebook(name) |
|
262 |
+ notebook = Evernote::EDAM::Type::Notebook.new(name: name) |
|
263 |
+ createNotebook(notebook) |
|
264 |
+ end |
|
265 |
+ |
|
266 |
+ def with_wrapped_content(params) |
|
267 |
+ params.delete(:notebook) |
|
268 |
+ |
|
269 |
+ if params[:content] |
|
270 |
+ params[:content] = |
|
271 |
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" \ |
|
272 |
+ "<!DOCTYPE en-note SYSTEM \"http://xml.evernote.com/pub/enml2.dtd\">" \ |
|
273 |
+ "<en-note>#{params[:content].encode(:xml => :text)}</en-note>" |
|
274 |
+ end |
|
275 |
+ |
|
276 |
+ params |
|
277 |
+ end |
|
278 |
+ |
|
279 |
+ class Search |
|
280 |
+ attr_reader :note_store, :opts |
|
281 |
+ def initialize(note_store, opts) |
|
282 |
+ @note_store = note_store |
|
283 |
+ @opts = opts |
|
284 |
+ end |
|
285 |
+ |
|
286 |
+ def note_guids |
|
287 |
+ filtered_metadata.map(&:guid) |
|
288 |
+ end |
|
289 |
+ |
|
290 |
+ def notes |
|
291 |
+ metadata = filtered_metadata |
|
292 |
+ |
|
293 |
+ if opts[:last_checked_at] && opts[:tagNames] |
|
294 |
+ |
|
295 |
+ # evernote does note change Note#updated timestamp when a tag is added to a note |
|
296 |
+ # the following selects recently updated notes |
|
297 |
+ # and notes that recently had the specified tags added |
|
298 |
+ metadata.select! do |note_data| |
|
299 |
+ note_data.updated > opts[:last_checked_at] || |
|
300 |
+ !opts[:notes_with_tags].include?(note_data.guid) |
|
301 |
+ end |
|
302 |
+ |
|
303 |
+ elsif opts[:last_checked_at] |
|
304 |
+ metadata.select! { |note_data| note_data.updated > opts[:last_checked_at] } |
|
305 |
+ end |
|
306 |
+ |
|
307 |
+ metadata.map! { |note_data| note_store.find_note(note_data.guid) } |
|
308 |
+ metadata |
|
309 |
+ end |
|
310 |
+ |
|
311 |
+ def create_filter |
|
312 |
+ filter = Evernote::EDAM::NoteStore::NoteFilter.new |
|
313 |
+ |
|
314 |
+ # evernote search grammar: |
|
315 |
+ # https://dev.evernote.com/doc/articles/search_grammar.php#Search_Terms |
|
316 |
+ query_terms = [] |
|
317 |
+ query_terms << "notebook:\"#{opts[:notebook]}\"" if opts[:notebook].present? |
|
318 |
+ query_terms << "intitle:\"#{opts[:title]}\"" if opts[:title].present? |
|
319 |
+ query_terms << "updated:day-1" if opts[:last_checked_at].present? |
|
320 |
+ opts[:tagNames].to_a.each { |tag| query_terms << "tag:#{tag}" } |
|
321 |
+ |
|
322 |
+ filter.words = query_terms.join(" ") |
|
323 |
+ filter |
|
324 |
+ end |
|
325 |
+ |
|
326 |
+ private |
|
327 |
+ |
|
328 |
+ def filtered_metadata |
|
329 |
+ filter, spec = create_filter, create_spec |
|
330 |
+ metadata = note_store.findNotesMetadata(filter, 0, 100, spec).notes |
|
331 |
+ end |
|
332 |
+ |
|
333 |
+ def create_spec |
|
334 |
+ Evernote::EDAM::NoteStore::NotesMetadataResultSpec.new( |
|
335 |
+ includeTitle: true, |
|
336 |
+ includeAttributes: true, |
|
337 |
+ includeNotebookGuid: true, |
|
338 |
+ includeTagGuids: true, |
|
339 |
+ includeUpdated: true, |
|
340 |
+ includeCreated: true |
|
341 |
+ ) |
|
342 |
+ end |
|
343 |
+ end |
|
344 |
+ end |
|
345 |
+ |
|
346 |
+ class Note |
|
347 |
+ attr_accessor :en_note |
|
348 |
+ attr_reader :notebook, :tags |
|
349 |
+ delegate :guid, :notebookGuid, :title, :tagGuids, :content, :resources, |
|
350 |
+ :attributes, :to => :en_note |
|
351 |
+ |
|
352 |
+ def initialize(en_note, notebook, tags) |
|
353 |
+ @en_note = en_note |
|
354 |
+ @notebook = notebook |
|
355 |
+ @tags = tags |
|
356 |
+ end |
|
357 |
+ |
|
358 |
+ def attr(opts = {}) |
|
359 |
+ return_attr = { |
|
360 |
+ title: title, |
|
361 |
+ notebook: notebook, |
|
362 |
+ tags: tags, |
|
363 |
+ source: attributes.source, |
|
364 |
+ source_url: attributes.sourceURL |
|
365 |
+ } |
|
366 |
+ |
|
367 |
+ return_attr[:content] = content if opts[:include_content] |
|
368 |
+ |
|
369 |
+ if opts[:include_resources] && resources |
|
370 |
+ return_attr[:resources] = [] |
|
371 |
+ resources.each do |resource| |
|
372 |
+ return_attr[:resources] << { |
|
373 |
+ url: resource.attributes.sourceURL, |
|
374 |
+ name: resource.attributes.fileName, |
|
375 |
+ mime_type: resource.mime |
|
376 |
+ } |
|
377 |
+ end |
|
378 |
+ end |
|
379 |
+ return_attr |
|
380 |
+ end |
|
381 |
+ end |
|
382 |
+ end |
|
383 |
+end |
@@ -263,6 +263,17 @@ 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 |
+ |
|
270 |
+ if ENV["USE_EVERNOTE_SANDBOX"] == "true" |
|
271 |
+ config.omniauth :evernote, key, secret, client_options: { :site => 'https://sandbox.evernote.com' } |
|
272 |
+ else |
|
273 |
+ config.omniauth :evernote, key, secret |
|
274 |
+ end |
|
275 |
+ end |
|
276 |
+ |
|
266 | 277 |
# ==> Warden configuration |
267 | 278 |
# If you want to use other strategies, that are not supported by Devise, or |
268 | 279 |
# 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." |
@@ -8,4 +8,6 @@ THIRTY_SEVEN_SIGNALS_OAUTH_SECRET=TESTSECRET |
||
8 | 8 |
DROPBOX_OAUTH_KEY=dropboxoauthkey |
9 | 9 |
DROPBOX_OAUTH_SECRET=dropboxoauthsecret |
10 | 10 |
WUNDERLIST_OAUTH_KEY=wunderoauthkey |
11 |
-FAILED_JOBS_TO_KEEP=2 |
|
11 |
+EVERNOTE_OAUTH_KEY=evernoteoauthkey |
|
12 |
+EVERNOTE_OAUTH_SECRET=evernoteoauthsecret |
|
13 |
+FAILED_JOBS_TO_KEEP=2 |
@@ -0,0 +1,576 @@ |
||
1 |
+require 'spec_helper' |
|
2 |
+ |
|
3 |
+describe Agents::EvernoteAgent do |
|
4 |
+ class FakeEvernoteNoteStore |
|
5 |
+ attr_accessor :notes, :tags, :notebooks |
|
6 |
+ def initialize |
|
7 |
+ @notes, @tags, @notebooks = [], [], [] |
|
8 |
+ end |
|
9 |
+ |
|
10 |
+ def createNote(note) |
|
11 |
+ note.attributes = OpenStruct.new(source: nil, sourceURL: nil) |
|
12 |
+ note.guid = @notes.length + 1 |
|
13 |
+ @notes << note |
|
14 |
+ note |
|
15 |
+ end |
|
16 |
+ |
|
17 |
+ def updateNote(note) |
|
18 |
+ note.attributes = OpenStruct.new(source: nil, sourceURL: nil) |
|
19 |
+ old_note = @notes.find {|en_note| en_note.guid == note.guid} |
|
20 |
+ @notes[@notes.index(old_note)] = note |
|
21 |
+ note |
|
22 |
+ end |
|
23 |
+ |
|
24 |
+ def getNote(guid, *other_args) |
|
25 |
+ @notes.find {|note| note.guid == guid} |
|
26 |
+ end |
|
27 |
+ |
|
28 |
+ def createNotebook(notebook) |
|
29 |
+ notebook.guid = @notebooks.length + 1 |
|
30 |
+ @notebooks << notebook |
|
31 |
+ notebook |
|
32 |
+ end |
|
33 |
+ |
|
34 |
+ def createTag(tag) |
|
35 |
+ tag.guid = @tags.length + 1 |
|
36 |
+ @tags << tag |
|
37 |
+ tag |
|
38 |
+ end |
|
39 |
+ |
|
40 |
+ def listNotebooks; @notebooks; end |
|
41 |
+ |
|
42 |
+ def listTags; @tags; end |
|
43 |
+ |
|
44 |
+ def getNoteTagNames(guid) |
|
45 |
+ getNote(guid).try(:tagNames) || [] |
|
46 |
+ end |
|
47 |
+ |
|
48 |
+ def findNotesMetadata(*args); end |
|
49 |
+ end |
|
50 |
+ |
|
51 |
+ let(:en_note_store) do |
|
52 |
+ FakeEvernoteNoteStore.new |
|
53 |
+ end |
|
54 |
+ |
|
55 |
+ before do |
|
56 |
+ stub.any_instance_of(Agents::EvernoteAgent).evernote_note_store { en_note_store } |
|
57 |
+ end |
|
58 |
+ |
|
59 |
+ describe "#receive" do |
|
60 |
+ context "when mode is set to 'update'" do |
|
61 |
+ before do |
|
62 |
+ @options = { |
|
63 |
+ :mode => "update", |
|
64 |
+ :include_xhtml_content => "false", |
|
65 |
+ :expected_update_period_in_days => "2", |
|
66 |
+ :note => { |
|
67 |
+ :title => "{{title}}", |
|
68 |
+ :content => "{{content}}", |
|
69 |
+ :notebook => "{{notebook}}", |
|
70 |
+ :tagNames => "{{tag1}}, {{tag2}}" |
|
71 |
+ } |
|
72 |
+ } |
|
73 |
+ @agent = Agents::EvernoteAgent.new(:name => "evernote updater", :options => @options) |
|
74 |
+ @agent.service = services(:generic) |
|
75 |
+ @agent.user = users(:bob) |
|
76 |
+ @agent.save! |
|
77 |
+ |
|
78 |
+ @event = Event.new |
|
79 |
+ @event.agent = agents(:bob_website_agent) |
|
80 |
+ @event.payload = { :title => "xkcd Survey", |
|
81 |
+ :content => "The xkcd Survey: Big Data for a Big Planet", |
|
82 |
+ :notebook => "xkcd", |
|
83 |
+ :tag1 => "funny", |
|
84 |
+ :tag2 => "data" } |
|
85 |
+ @event.save! |
|
86 |
+ |
|
87 |
+ tag1 = OpenStruct.new(name: "funny") |
|
88 |
+ tag2 = OpenStruct.new(name: "data") |
|
89 |
+ [tag1, tag2].each { |tag| en_note_store.createTag(tag) } |
|
90 |
+ end |
|
91 |
+ |
|
92 |
+ it "adds a note for any payload it receives" do |
|
93 |
+ stub(en_note_store).findNotesMetadata { OpenStruct.new(notes: []) } |
|
94 |
+ Agents::EvernoteAgent.async_receive(@agent.id, [@event.id]) |
|
95 |
+ |
|
96 |
+ expect(en_note_store.notes.size).to eq(1) |
|
97 |
+ expect(en_note_store.notes.first.title).to eq("xkcd Survey") |
|
98 |
+ expect(en_note_store.notebooks.size).to eq(1) |
|
99 |
+ expect(en_note_store.tags.size).to eq(2) |
|
100 |
+ |
|
101 |
+ expect(@agent.events.count).to eq(1) |
|
102 |
+ expect(@agent.events.first.payload).to eq({ |
|
103 |
+ "title" => "xkcd Survey", |
|
104 |
+ "notebook" => "xkcd", |
|
105 |
+ "tags" => ["funny", "data"], |
|
106 |
+ "source" => nil, |
|
107 |
+ "source_url" => nil |
|
108 |
+ }) |
|
109 |
+ end |
|
110 |
+ |
|
111 |
+ context "a note with the same title and notebook exists" do |
|
112 |
+ before do |
|
113 |
+ note1 = OpenStruct.new(title: "xkcd Survey", notebookGuid: 1) |
|
114 |
+ note2 = OpenStruct.new(title: "Footprints", notebookGuid: 1) |
|
115 |
+ [note1, note2].each { |note| en_note_store.createNote(note) } |
|
116 |
+ en_note_store.createNotebook(OpenStruct.new(name: "xkcd")) |
|
117 |
+ |
|
118 |
+ stub(en_note_store).findNotesMetadata { |
|
119 |
+ OpenStruct.new(notes: [note1]) } |
|
120 |
+ end |
|
121 |
+ |
|
122 |
+ it "updates the existing note" do |
|
123 |
+ Agents::EvernoteAgent.async_receive(@agent.id, [@event.id]) |
|
124 |
+ |
|
125 |
+ expect(en_note_store.notes.size).to eq(2) |
|
126 |
+ expect(en_note_store.getNote(1).tagNames).to eq(["funny", "data"]) |
|
127 |
+ expect(@agent.events.count).to eq(1) |
|
128 |
+ end |
|
129 |
+ end |
|
130 |
+ |
|
131 |
+ context "include_xhtml_content is set to 'true'" do |
|
132 |
+ before do |
|
133 |
+ @agent.options[:include_xhtml_content] = "true" |
|
134 |
+ @agent.save! |
|
135 |
+ end |
|
136 |
+ |
|
137 |
+ it "creates an event with note content wrapped in ENML" do |
|
138 |
+ stub(en_note_store).findNotesMetadata { OpenStruct.new(notes: []) } |
|
139 |
+ Agents::EvernoteAgent.async_receive(@agent.id, [@event.id]) |
|
140 |
+ |
|
141 |
+ payload = @agent.events.first.payload |
|
142 |
+ |
|
143 |
+ expect(payload[:content]).to eq( |
|
144 |
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" \ |
|
145 |
+ "<!DOCTYPE en-note SYSTEM \"http://xml.evernote.com/pub/enml2.dtd\">" \ |
|
146 |
+ "<en-note>The xkcd Survey: Big Data for a Big Planet</en-note>" |
|
147 |
+ ) |
|
148 |
+ end |
|
149 |
+ end |
|
150 |
+ end |
|
151 |
+ end |
|
152 |
+ |
|
153 |
+ describe "#check" do |
|
154 |
+ context "when mode is set to 'read'" do |
|
155 |
+ before do |
|
156 |
+ @options = { |
|
157 |
+ :mode => "read", |
|
158 |
+ :include_xhtml_content => "false", |
|
159 |
+ :expected_update_period_in_days => "2", |
|
160 |
+ :note => { |
|
161 |
+ :title => "", |
|
162 |
+ :content => "", |
|
163 |
+ :notebook => "xkcd", |
|
164 |
+ :tagNames => "funny, comic" |
|
165 |
+ } |
|
166 |
+ } |
|
167 |
+ @checker = Agents::EvernoteAgent.new(:name => "evernote reader", :options => @options) |
|
168 |
+ |
|
169 |
+ @checker.service = services(:generic) |
|
170 |
+ @checker.user = users(:bob) |
|
171 |
+ @checker.schedule = "every_2h" |
|
172 |
+ |
|
173 |
+ @checker.save! |
|
174 |
+ @checker.created_at = 1.minute.ago |
|
175 |
+ |
|
176 |
+ en_note_store.createNote( |
|
177 |
+ OpenStruct.new(title: "xkcd Survey", |
|
178 |
+ notebookGuid: 1, |
|
179 |
+ updated: 2.minutes.ago.to_i * 1000, |
|
180 |
+ tagNames: ["funny", "comic"]) |
|
181 |
+ ) |
|
182 |
+ en_note_store.createNotebook(OpenStruct.new(name: "xkcd")) |
|
183 |
+ tag1 = OpenStruct.new(name: "funny") |
|
184 |
+ tag2 = OpenStruct.new(name: "comic") |
|
185 |
+ [tag1, tag2].each { |tag| en_note_store.createTag(tag) } |
|
186 |
+ |
|
187 |
+ stub(en_note_store).findNotesMetadata { |
|
188 |
+ notes = en_note_store.notes.select do |note| |
|
189 |
+ note.notebookGuid == 1 && |
|
190 |
+ %w(funny comic).all? { |tag_name| note.tagNames.include?(tag_name) } |
|
191 |
+ end |
|
192 |
+ OpenStruct.new(notes: notes) |
|
193 |
+ } |
|
194 |
+ end |
|
195 |
+ |
|
196 |
+ context "the first time it checks" do |
|
197 |
+ it "returns only notes created/updated since it was created" do |
|
198 |
+ expect { @checker.check }.to change { Event.count }.by(0) |
|
199 |
+ end |
|
200 |
+ end |
|
201 |
+ |
|
202 |
+ context "on subsequent checks" do |
|
203 |
+ it "returns notes created/updated since the last time it checked" do |
|
204 |
+ expect { @checker.check }.to change { Event.count }.by(0) |
|
205 |
+ |
|
206 |
+ future_time = (Time.now + 1.minute).to_i * 1000 |
|
207 |
+ en_note_store.createNote( |
|
208 |
+ OpenStruct.new(title: "Footprints", |
|
209 |
+ notebookGuid: 1, |
|
210 |
+ tagNames: ["funny", "comic", "recent"], |
|
211 |
+ updated: future_time)) |
|
212 |
+ |
|
213 |
+ en_note_store.createNote( |
|
214 |
+ OpenStruct.new(title: "something else", |
|
215 |
+ notebookGuid: 2, |
|
216 |
+ tagNames: ["funny", "comic"], |
|
217 |
+ updated: future_time)) |
|
218 |
+ |
|
219 |
+ expect { @checker.check }.to change { Event.count }.by(1) |
|
220 |
+ end |
|
221 |
+ |
|
222 |
+ it "returns notes tagged since the last time it checked" do |
|
223 |
+ en_note_store.createNote( |
|
224 |
+ OpenStruct.new(title: "Footprints", |
|
225 |
+ notebookGuid: 1, |
|
226 |
+ tagNames: [], |
|
227 |
+ created: Time.now.to_i * 1000, |
|
228 |
+ updated: Time.now.to_i * 1000)) |
|
229 |
+ @checker.check |
|
230 |
+ |
|
231 |
+ en_note_store.getNote(2).tagNames = ["funny", "comic"] |
|
232 |
+ |
|
233 |
+ expect { @checker.check }.to change { Event.count }.by(1) |
|
234 |
+ end |
|
235 |
+ end |
|
236 |
+ end |
|
237 |
+ end |
|
238 |
+ |
|
239 |
+ describe "#validation" do |
|
240 |
+ before do |
|
241 |
+ @options = { |
|
242 |
+ :mode => "update", |
|
243 |
+ :include_xhtml_content => "false", |
|
244 |
+ :expected_update_period_in_days => "2", |
|
245 |
+ :note => { |
|
246 |
+ :title => "{{title}}", |
|
247 |
+ :content => "{{content}}", |
|
248 |
+ :notebook => "{{notebook}}", |
|
249 |
+ :tagNames => "{{tag1}}, {{tag2}}" |
|
250 |
+ } |
|
251 |
+ } |
|
252 |
+ @agent = Agents::EvernoteAgent.new(:name => "evernote updater", :options => @options) |
|
253 |
+ @agent.service = services(:generic) |
|
254 |
+ @agent.user = users(:bob) |
|
255 |
+ @agent.save! |
|
256 |
+ |
|
257 |
+ expect(@agent).to be_valid |
|
258 |
+ end |
|
259 |
+ |
|
260 |
+ it "requires the mode to be 'update' or 'read'" do |
|
261 |
+ @agent.options[:mode] = "" |
|
262 |
+ expect(@agent).not_to be_valid |
|
263 |
+ end |
|
264 |
+ |
|
265 |
+ context "mode is set to 'update'" do |
|
266 |
+ before do |
|
267 |
+ @agent.options[:mode] = "update" |
|
268 |
+ end |
|
269 |
+ |
|
270 |
+ it "requires some note parameter to be present" do |
|
271 |
+ @agent.options[:note].keys.each { |k| @agent.options[:note][k] = "" } |
|
272 |
+ expect(@agent).not_to be_valid |
|
273 |
+ end |
|
274 |
+ |
|
275 |
+ it "requires schedule to be 'never'" do |
|
276 |
+ @agent.schedule = 'never' |
|
277 |
+ expect(@agent).to be_valid |
|
278 |
+ |
|
279 |
+ @agent.schedule = 'every_1m' |
|
280 |
+ expect(@agent).not_to be_valid |
|
281 |
+ end |
|
282 |
+ end |
|
283 |
+ |
|
284 |
+ context "mode is set to 'read'" do |
|
285 |
+ before do |
|
286 |
+ @agent.options[:mode] = "read" |
|
287 |
+ end |
|
288 |
+ |
|
289 |
+ it "requires a schedule to be set" do |
|
290 |
+ @agent.schedule = 'every_1m' |
|
291 |
+ expect(@agent).to be_valid |
|
292 |
+ |
|
293 |
+ @agent.schedule = 'never' |
|
294 |
+ expect(@agent).not_to be_valid |
|
295 |
+ end |
|
296 |
+ end |
|
297 |
+ end |
|
298 |
+ |
|
299 |
+ # api wrapper classes |
|
300 |
+ describe Agents::EvernoteAgent::NoteStore do |
|
301 |
+ let(:note_store) { Agents::EvernoteAgent::NoteStore.new(en_note_store) } |
|
302 |
+ |
|
303 |
+ let(:note1) { OpenStruct.new(title: "first note") } |
|
304 |
+ let(:note2) { OpenStruct.new(title: "second note") } |
|
305 |
+ |
|
306 |
+ before do |
|
307 |
+ en_note_store.createNote(note1) |
|
308 |
+ en_note_store.createNote(note2) |
|
309 |
+ end |
|
310 |
+ |
|
311 |
+ describe "#create_note" do |
|
312 |
+ it "creates a note with given params in evernote note store" do |
|
313 |
+ note_store.create_note(title: "third note") |
|
314 |
+ |
|
315 |
+ expect(en_note_store.notes.size).to eq(3) |
|
316 |
+ expect(en_note_store.notes.last.title).to eq("third note") |
|
317 |
+ end |
|
318 |
+ |
|
319 |
+ it "returns a note" do |
|
320 |
+ expect(note_store.create_note(title: "third note")).to be_a(Agents::EvernoteAgent::Note) |
|
321 |
+ end |
|
322 |
+ end |
|
323 |
+ |
|
324 |
+ describe "#update_note" do |
|
325 |
+ it "updates an existing note with given params" do |
|
326 |
+ note_store.update_note(guid: 1, content: "some words") |
|
327 |
+ |
|
328 |
+ expect(en_note_store.notes.first.content).not_to be_nil |
|
329 |
+ expect(en_note_store.notes.size).to eq(2) |
|
330 |
+ end |
|
331 |
+ |
|
332 |
+ it "returns a note" do |
|
333 |
+ expect(note_store.update_note(guid: 1, content: "some words")).to be_a(Agents::EvernoteAgent::Note) |
|
334 |
+ end |
|
335 |
+ end |
|
336 |
+ |
|
337 |
+ describe "#find_note" do |
|
338 |
+ it "gets a note with the given guid" do |
|
339 |
+ note = note_store.find_note(2) |
|
340 |
+ |
|
341 |
+ expect(note.title).to eq("second note") |
|
342 |
+ expect(note).to be_a(Agents::EvernoteAgent::Note) |
|
343 |
+ end |
|
344 |
+ end |
|
345 |
+ |
|
346 |
+ describe "#find_tags" do |
|
347 |
+ let(:tag1) { OpenStruct.new(name: "tag1") } |
|
348 |
+ let(:tag2) { OpenStruct.new(name: "tag2") } |
|
349 |
+ let(:tag3) { OpenStruct.new(name: "tag3") } |
|
350 |
+ |
|
351 |
+ before do |
|
352 |
+ [tag1, tag2, tag3].each { |tag| en_note_store.createTag(tag) } |
|
353 |
+ end |
|
354 |
+ |
|
355 |
+ it "finds tags with the given guids" do |
|
356 |
+ expect(note_store.find_tags([1,3])).to eq([tag1, tag3]) |
|
357 |
+ end |
|
358 |
+ end |
|
359 |
+ |
|
360 |
+ describe "#find_notebook" do |
|
361 |
+ let(:notebook1) { OpenStruct.new(name: "notebook1") } |
|
362 |
+ let(:notebook2) { OpenStruct.new(name: "notebook2") } |
|
363 |
+ |
|
364 |
+ before do |
|
365 |
+ [notebook1, notebook2].each {|notebook| en_note_store.createNotebook(notebook)} |
|
366 |
+ end |
|
367 |
+ |
|
368 |
+ it "finds a notebook with given name" do |
|
369 |
+ expect(note_store.find_notebook(name: "notebook1")).to eq(notebook1) |
|
370 |
+ expect(note_store.find_notebook(name: "notebook3")).to be_nil |
|
371 |
+ end |
|
372 |
+ |
|
373 |
+ it "finds a notebook with a given guid" do |
|
374 |
+ expect(note_store.find_notebook(guid: 2)).to eq(notebook2) |
|
375 |
+ expect(note_store.find_notebook(guid: 3)).to be_nil |
|
376 |
+ end |
|
377 |
+ end |
|
378 |
+ |
|
379 |
+ describe "#create_or_update_note" do |
|
380 |
+ let(:notebook1) { OpenStruct.new(name: "first notebook")} |
|
381 |
+ |
|
382 |
+ before do |
|
383 |
+ en_note_store.createNotebook(notebook1) |
|
384 |
+ end |
|
385 |
+ |
|
386 |
+ context "a note with given title and notebook does not exist" do |
|
387 |
+ before do |
|
388 |
+ stub(en_note_store).findNotesMetadata { OpenStruct.new(notes: []) } |
|
389 |
+ end |
|
390 |
+ |
|
391 |
+ it "creates a note" do |
|
392 |
+ result = note_store.create_or_update_note(title: "third note", notebook: "first notebook") |
|
393 |
+ |
|
394 |
+ expect(result).to be_a(Agents::EvernoteAgent::Note) |
|
395 |
+ expect(en_note_store.getNote(3)).to_not be_nil |
|
396 |
+ end |
|
397 |
+ |
|
398 |
+ it "also creates the notebook if it does not exist" do |
|
399 |
+ note_store.create_or_update_note(title: "third note", notebook: "second notebook") |
|
400 |
+ |
|
401 |
+ expect(note_store.find_notebook(name: "second notebook")).to_not be_nil |
|
402 |
+ end |
|
403 |
+ end |
|
404 |
+ |
|
405 |
+ context "such a note does exist" do |
|
406 |
+ let(:note) { OpenStruct.new(title: "a note", notebookGuid: 1) } |
|
407 |
+ |
|
408 |
+ before do |
|
409 |
+ en_note_store.createNote(note) |
|
410 |
+ stub(en_note_store).findNotesMetadata { OpenStruct.new(notes: [note]) } |
|
411 |
+ end |
|
412 |
+ |
|
413 |
+ it "updates the note" do |
|
414 |
+ prior_note_count = en_note_store.notes.size |
|
415 |
+ |
|
416 |
+ result = note_store.create_or_update_note( |
|
417 |
+ title: "a note", notebook: "first notebook", content: "test content") |
|
418 |
+ |
|
419 |
+ expect(result).to be_a(Agents::EvernoteAgent::Note) |
|
420 |
+ expect(en_note_store.notes.size).to eq(prior_note_count) |
|
421 |
+ expect(en_note_store.getNote(3).content).to include("test content") |
|
422 |
+ end |
|
423 |
+ end |
|
424 |
+ end |
|
425 |
+ end |
|
426 |
+ |
|
427 |
+ describe Agents::EvernoteAgent::NoteStore::Search do |
|
428 |
+ let(:note_store) { Agents::EvernoteAgent::NoteStore.new(en_note_store) } |
|
429 |
+ |
|
430 |
+ let(:note1) { |
|
431 |
+ OpenStruct.new(title: "first note", notebookGuid: 1, tagNames: ["funny", "comic"], updated: Time.now) } |
|
432 |
+ let(:note2) { |
|
433 |
+ OpenStruct.new(title: "second note", tagNames: ["funny", "comic"], updated: Time.now) } |
|
434 |
+ let(:note3) { |
|
435 |
+ OpenStruct.new(title: "third note", notebookGuid: 1, updated: Time.now - 2.minutes) } |
|
436 |
+ |
|
437 |
+ let(:search) do |
|
438 |
+ Agents::EvernoteAgent::NoteStore::Search.new(note_store, |
|
439 |
+ { tagNames: ["funny", "comic"], notebook: "xkcd" }) |
|
440 |
+ end |
|
441 |
+ |
|
442 |
+ let(:search_with_time) do |
|
443 |
+ Agents::EvernoteAgent::NoteStore::Search.new(note_store, |
|
444 |
+ { notebook: "xkcd", last_checked_at: Time.now - 1.minute }) |
|
445 |
+ end |
|
446 |
+ |
|
447 |
+ let(:search_with_time_and_tags) do |
|
448 |
+ Agents::EvernoteAgent::NoteStore::Search.new(note_store, |
|
449 |
+ { notebook: "xkcd", tagNames: ["funny", "comic"], notes_with_tags: [1], last_checked_at: Time.now - 1.minute }) |
|
450 |
+ end |
|
451 |
+ |
|
452 |
+ before do |
|
453 |
+ en_note_store.createTag(OpenStruct.new(name: "funny")) |
|
454 |
+ en_note_store.createTag(OpenStruct.new(name: "comic")) |
|
455 |
+ en_note_store.createNotebook(OpenStruct.new(name: "xkcd")) |
|
456 |
+ |
|
457 |
+ [note1, note2, note3].each { |note| en_note_store.createNote(note) } |
|
458 |
+ end |
|
459 |
+ |
|
460 |
+ describe "#note_guids" do |
|
461 |
+ it "returns the guids of notes satisfying search options" do |
|
462 |
+ stub(en_note_store).findNotesMetadata { OpenStruct.new(notes: [note1]) } |
|
463 |
+ result = search.note_guids |
|
464 |
+ |
|
465 |
+ expect(result.size).to eq(1) |
|
466 |
+ expect(result.first).to eq(1) |
|
467 |
+ end |
|
468 |
+ end |
|
469 |
+ |
|
470 |
+ describe "#notes" do |
|
471 |
+ context "last_checked_at is not set" do |
|
472 |
+ it "returns notes satisfying the search options" do |
|
473 |
+ stub(en_note_store).findNotesMetadata { OpenStruct.new(notes: [note1]) } |
|
474 |
+ result = search.notes |
|
475 |
+ |
|
476 |
+ expect(result.size).to eq(1) |
|
477 |
+ expect(result.first.title).to eq("first note") |
|
478 |
+ expect(result.first).to be_a(Agents::EvernoteAgent::Note) |
|
479 |
+ end |
|
480 |
+ end |
|
481 |
+ |
|
482 |
+ context "last_checked_at is set" do |
|
483 |
+ context "notes_with_tags is not set" do |
|
484 |
+ it "only returns notes updated since then" do |
|
485 |
+ stub(en_note_store).findNotesMetadata { OpenStruct.new(notes: [note1, note3]) } |
|
486 |
+ result = search_with_time.notes |
|
487 |
+ |
|
488 |
+ expect(result.size).to eq(1) |
|
489 |
+ expect(result.first.title).to eq("first note") |
|
490 |
+ end |
|
491 |
+ end |
|
492 |
+ |
|
493 |
+ context "notes_with_tags is set" do |
|
494 |
+ it "returns notes updated since then or notes with recently added tags" do |
|
495 |
+ note3.tagNames = ["funny", "comic"] |
|
496 |
+ stub(en_note_store).findNotesMetadata { OpenStruct.new(notes: [note1, note3]) } |
|
497 |
+ |
|
498 |
+ result = search_with_time_and_tags.notes |
|
499 |
+ expect(result.size).to eq(2) |
|
500 |
+ expect(result.last.title).to eq("third note") |
|
501 |
+ end |
|
502 |
+ end |
|
503 |
+ end |
|
504 |
+ end |
|
505 |
+ |
|
506 |
+ describe "#create_filter" do |
|
507 |
+ it "builds an evernote search filter using search grammar" do |
|
508 |
+ filter = search.create_filter |
|
509 |
+ expect(filter.words).to eq("notebook:\"xkcd\" tag:funny tag:comic") |
|
510 |
+ end |
|
511 |
+ end |
|
512 |
+ end |
|
513 |
+ |
|
514 |
+ describe Agents::EvernoteAgent::Note do |
|
515 |
+ let(:resource) { |
|
516 |
+ OpenStruct.new(mime: "image/png", |
|
517 |
+ attributes: OpenStruct.new(sourceURL: "http://imgs.xkcd.com/comics/xkcd_survey.png", fileName: "xkcd_survey.png")) |
|
518 |
+ } |
|
519 |
+ |
|
520 |
+ let(:en_note_attributes) { |
|
521 |
+ OpenStruct.new(source: "web.clip", sourceURL: "http://xkcd.com/1572/") |
|
522 |
+ } |
|
523 |
+ |
|
524 |
+ let(:en_note) { |
|
525 |
+ OpenStruct.new(title: "xkcd Survey", |
|
526 |
+ tagNames: ["funny", "data"], |
|
527 |
+ content: "The xkcd Survey: Big Data for a Big Planet", |
|
528 |
+ attributes: en_note_attributes, |
|
529 |
+ resources: [resource]) |
|
530 |
+ } |
|
531 |
+ |
|
532 |
+ describe "#attr" do |
|
533 |
+ let(:note) { |
|
534 |
+ Agents::EvernoteAgent::Note.new(en_note, "xkcd", ["funny", "data"]) |
|
535 |
+ } |
|
536 |
+ |
|
537 |
+ context "when no option is set" do |
|
538 |
+ it "returns a hash with title, tags, notebook, source and source url" do |
|
539 |
+ expect(note.attr).to eq( |
|
540 |
+ { |
|
541 |
+ title: en_note.title, |
|
542 |
+ notebook: "xkcd", |
|
543 |
+ tags: ["funny", "data"], |
|
544 |
+ source: en_note.attributes.source, |
|
545 |
+ source_url: en_note.attributes.sourceURL |
|
546 |
+ } |
|
547 |
+ ) |
|
548 |
+ end |
|
549 |
+ end |
|
550 |
+ |
|
551 |
+ context "when include_content is set to true" do |
|
552 |
+ it "includes content" do |
|
553 |
+ note_attr = note.attr(include_content: true) |
|
554 |
+ |
|
555 |
+ expect(note_attr[:content]).to eq( |
|
556 |
+ "The xkcd Survey: Big Data for a Big Planet" |
|
557 |
+ ) |
|
558 |
+ end |
|
559 |
+ end |
|
560 |
+ |
|
561 |
+ context "when include_resources is set to true" do |
|
562 |
+ it "includes resources" do |
|
563 |
+ note_attr = note.attr(include_resources: true) |
|
564 |
+ |
|
565 |
+ expect(note_attr[:resources].first).to eq( |
|
566 |
+ { |
|
567 |
+ url: resource.attributes.sourceURL, |
|
568 |
+ name: resource.attributes.fileName, |
|
569 |
+ mime_type: resource.mime |
|
570 |
+ } |
|
571 |
+ ) |
|
572 |
+ end |
|
573 |
+ end |
|
574 |
+ end |
|
575 |
+ end |
|
576 |
+end |