enwrite 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/enwrite.rb ADDED
@@ -0,0 +1,299 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ #
4
+ # enwrite - power a web site using Evernote
5
+ #
6
+ # Diego Zamboni, March 2015
7
+ # Time-stamp: <2015-04-30 13:39:37 diego>
8
+
9
+ require 'rubygems'
10
+ require 'bundler/setup'
11
+
12
+ require "digest/md5"
13
+ require 'evernote-thrift'
14
+ require 'evernote-utils'
15
+ require "optparse"
16
+ require "ostruct"
17
+ require 'util'
18
+ require 'yaml'
19
+ require 'deep_merge'
20
+
21
+ class Enwrite
22
+ PLUGINS = %w[hugo]
23
+ module Version
24
+ MAJOR = 0
25
+ MINOR = 2
26
+ PATCH = 0
27
+
28
+ STRING = [MAJOR, MINOR, PATCH].compact.join('.')
29
+ end
30
+
31
+ def self.run
32
+
33
+ options = OpenStruct.new
34
+ options.removetags = []
35
+ options.verbose = false
36
+ options.debug = false
37
+ options.outputplugin = 'hugo'
38
+ options.configtag = '_enwrite_config'
39
+ options.filestagprefix = '_enwrite_files'
40
+
41
+ opts = OptionParser.new do |opts|
42
+ def opts.version_string
43
+ "Enwrite v#{Enwrite::Version::STRING}"
44
+ end
45
+
46
+ opts.banner = "#{opts.version_string}\n\nUsage: #{$0} [options] (at least one of -n or -s has to be specified)"
47
+
48
+ def opts.show_usage
49
+ puts self
50
+ exit
51
+ end
52
+
53
+ def opts.show_version
54
+ puts version_string
55
+ exit
56
+ end
57
+
58
+ opts.separator "\nSearch options:"
59
+ opts.on("-n", "--notebook NOTEBOOK",
60
+ "Process notes from specified notebook.") do |notebook|
61
+ options.notebook = notebook
62
+ end
63
+ opts.on("-t", "--tag TAG",
64
+ "Process only notes that have this tag",
65
+ "within the given notebook.") do |tag|
66
+ options.tag = tag
67
+ end
68
+ opts.on("--remove-tags [t1,t2,t3]", Array,
69
+ "List of tags to remove from output posts.",
70
+ "If no argument given, defaults to --tag.") do |removetags|
71
+ options.removetags = removetags || [options.tag]
72
+ end
73
+ opts.on("-s", "--search SEARCHEXP",
74
+ "Process notes that match given search",
75
+ "expression. If specified, --notebook",
76
+ "and --tag are ignored.") do |searchexp|
77
+ options.searchexp = searchexp
78
+ options.tag = nil
79
+ options.notebook = nil
80
+ end
81
+ opts.separator 'Output options:'
82
+ opts.on("-p", "--output-plugin PLUGIN", PLUGINS,
83
+ "Output plugin to use (Valid values: #{PLUGINS.join(', ')})") do |plugin|
84
+ options.outputplugin = plugin
85
+ end
86
+ opts.on("-o", "--output-dir OUTDIR",
87
+ "Base dir of hugo output installation") do |outdir|
88
+ options.outdir = outdir
89
+ end
90
+ opts.on("--rebuild-all",
91
+ "Process all notes that match the given",
92
+ "conditions (normally only updated notes",
93
+ "are processed)") { options.rebuild_all = true }
94
+ opts.separator 'Other options:'
95
+ opts.on("--auth [TOKEN]",
96
+ "Force Evernote reauthentication (will",
97
+ "happen automatically if needed). Use",
98
+ "TOKEN if given, otherwise get one",
99
+ "interactively.") do |forceauth|
100
+ options.forceauth = true
101
+ options.authtoken = forceauth
102
+ end
103
+ opts.on("--config-tag TAG",
104
+ "Specify tag to determine config notes",
105
+ "(default: #{options.configtag})") { |conftag|
106
+ options.configtag = conftag
107
+ }
108
+ opts.on_tail("-v", "--verbose", "Verbose mode") { options.verbose=true }
109
+ opts.on_tail("-d", "--debug", "Debug output mode") {
110
+ options.debug=true
111
+ options.verbose=true
112
+ }
113
+ opts.on_tail("--version", "Show version") { opts.show_version }
114
+ opts.on_tail("-h", "--help", "Shows this help message") { opts.show_usage }
115
+ end
116
+
117
+ opts.parse!
118
+
119
+ begin
120
+ eval "require 'output/#{options.outputplugin}'"
121
+ rescue LoadError
122
+ error "There was an error loading output module '#{plugin}': #{e.to_s}"
123
+ exit 1
124
+ end
125
+
126
+ $enwrite_verbose = options.verbose
127
+ $enwrite_debug = options.debug
128
+
129
+ verbose("Options: " + options.to_s)
130
+
131
+ if not (options.notebook or options.searchexp or options.forceauth)
132
+ error "You have to specify at least one of --notebook, --search or --auth"
133
+ exit(1)
134
+ end
135
+ exps = [ options.searchexp ? options.searchexp : nil,
136
+ options.notebook ? "notebook:#{options.notebook}" : nil,
137
+ options.tag ? "tag:#{options.tag}" : nil,
138
+ ].reject(&:nil?)
139
+ searchexp = exps.join(' ')
140
+
141
+ verbose "Output dir: #{options.outdir}"
142
+ verbose "Search expression: #{searchexp}"
143
+
144
+ begin
145
+
146
+ # Initialize Evernote access
147
+ Evernote_utils.init(options.forceauth, options.authtoken)
148
+
149
+ if not searchexp # Only --auth was specified
150
+ exit 0
151
+ end
152
+
153
+ updatecount_index = "updatecount_#{searchexp}"
154
+ latestUpdateCount = config(updatecount_index, 0)
155
+ if options.rebuild_all
156
+ msg "Processing ALL notes (--rebuild-all)."
157
+ latestUpdateCount = 0
158
+ end
159
+
160
+ verbose "Latest stored update count for #{searchexp}: #{latestUpdateCount}"
161
+
162
+ currentState = Evernote_utils.noteStore.getSyncState(Evernote_utils.authToken)
163
+ currentUpdateCount = currentState.updateCount
164
+
165
+ verbose "Current update count for the account: #{currentUpdateCount}"
166
+
167
+ if (currentUpdateCount > latestUpdateCount)
168
+ msg "Reading #{options.rebuild_all ? 'all' : 'updated'} notes that match #{searchexp}"
169
+
170
+ filter = Evernote::EDAM::NoteStore::NoteFilter.new
171
+ filter.words = searchexp
172
+ filter.order = Evernote::EDAM::Type::NoteSortOrder::UPDATE_SEQUENCE_NUMBER
173
+ filter.ascending = false
174
+
175
+ spec = Evernote::EDAM::NoteStore::NotesMetadataResultSpec.new
176
+ spec.includeTitle = true
177
+ spec.includeCreated = true
178
+ spec.includeTagGuids = true
179
+ spec.includeContentLength = true
180
+ spec.includeUpdateSequenceNum = true
181
+ spec.includeDeleted = true
182
+
183
+ results = Evernote_utils.noteStore.findNotesMetadata(Evernote_utils.authToken,
184
+ filter,
185
+ 0,
186
+ Evernote::EDAM::Limits::EDAM_USER_NOTES_MAX,
187
+ spec)
188
+
189
+ # Get also deleted notes so we can remove from the blog
190
+ filter.inactive = true
191
+ delresults = Evernote_utils.noteStore.findNotesMetadata(Evernote_utils.authToken,
192
+ filter,
193
+ 0,
194
+ Evernote::EDAM::Limits::EDAM_USER_NOTES_MAX,
195
+ spec)
196
+
197
+ # Go through the list looking for config notes, parse them and remove
198
+ # them from the list
199
+ enwriteconfig = { 'hugo' => {
200
+ 'base_dir' => options.outdir,
201
+ 'rebuild_all' => options.rebuild_all,
202
+ },
203
+ }
204
+
205
+ if Evernote_utils.tags.include?(options.configtag)
206
+ config_tag_guid = Evernote_utils.tags[options.configtag].guid
207
+ results.notes.select { |note|
208
+ note.tagGuids.include?(config_tag_guid)
209
+ }.each { |confignotemd|
210
+ msg "Found config note '#{confignotemd.title}'"
211
+ confignote = Evernote_utils.getWholeNote(confignotemd)
212
+ enml = ENML_utils.new(confignote.content)
213
+ configtext = enml.to_text
214
+ debug " Config note text: '#{configtext}'"
215
+ configyaml = YAML.load(configtext)
216
+ debug " Config note YAML: #{configyaml}"
217
+ enwriteconfig.deep_merge!(configyaml)
218
+ debug " enwriteconfig = #{enwriteconfig}"
219
+ results.notes.delete(confignotemd)
220
+ results.totalNotes -= 1
221
+ }
222
+ end
223
+ verbose "Final enwrite config: #{enwriteconfig}"
224
+
225
+ files_tag = "#{options.filestagprefix}_#{options.outputplugin}"
226
+
227
+ if Evernote_utils.tags.include?(files_tag)
228
+ files_tag_guid = Evernote_utils.tags[files_tag].guid
229
+ results.notes.select { |note|
230
+ note.tagGuids.include?(files_tag_guid) &&
231
+ note.updateSequenceNum > latestUpdateCount
232
+ }.each { |filesnotemd|
233
+ msg "Found files note '#{filesnotemd.title}'"
234
+ filesnote = Evernote_utils.getWholeNote(filesnotemd)
235
+ enml = ENML_utils.new(filesnote.content, filesnote.resources)
236
+ files = enml.resource_files
237
+ Dir.chdir("#{options.outdir}")
238
+ files.each do |file|
239
+ case
240
+ when file[:basename] =~ /\.tar.gz$/
241
+ f = Tempfile.new('enwrite')
242
+ begin
243
+ verbose " Saving file #{file[:basename]} to #{f.path}"
244
+ f.write(file[:data])
245
+ f.close
246
+ verbose " Unpacking file #{f.path} with tar"
247
+ ok = system("tar zxf #{f.path}")
248
+ unless ok
249
+ error " An error occurred when unpacking #{f.path}"
250
+ end
251
+ ensure
252
+ f.close
253
+ f.unlink
254
+ end
255
+ else
256
+ open("#{options.outdir}/#{file[:basename]}", "w") do |f|
257
+ verbose " Saving file #{f.path}"
258
+ f.write(file[:data])
259
+ end
260
+ end
261
+ end
262
+ results.notes.delete(filesnotemd)
263
+ results.totalNotes -= 1
264
+ }
265
+ end
266
+
267
+ debug "Evaluating: #{options.outputplugin.capitalize}.new(enwriteconfig[options.outputplugin])"
268
+ writer = eval "#{options.outputplugin.capitalize}.new(enwriteconfig[options.outputplugin])"
269
+
270
+ (results.notes + delresults.notes).select {
271
+ |note| note.updateSequenceNum > latestUpdateCount
272
+ }.sort_by {
273
+ |note| note.updateSequenceNum
274
+ }.each do |metadata|
275
+ verbose "######################################################################"
276
+ note = Evernote_utils.getWholeNote(metadata)
277
+ note.tagNames = note.tagNames - options.removetags
278
+ # This either creates or deletes posts as appropriate
279
+ writer.output_note(note)
280
+ end
281
+ # Persist the latest updatecount for next time
282
+ setconfig(updatecount_index, currentUpdateCount)
283
+
284
+ exit 0
285
+ else
286
+ msg "No updated notes that match #{searchexp}"
287
+ exit 1
288
+ end
289
+ rescue Evernote::EDAM::Error::EDAMUserException => e
290
+ #the exceptions that come back from Evernote are hard to read, but really important to keep track of
291
+ msg = "Caught an exception from Evernote trying to create a note. #{Evernote_utils.translate_error(e)}"
292
+ raise msg
293
+ rescue Evernote::EDAM::Error::EDAMSystemException => e
294
+ #the exceptions that come back from Evernote are hard to read, but really important to keep track of
295
+ msg = "Caught an exception from Evernote trying to create a note. #{Evernote_utils.translate_error(e)}"
296
+ raise msg
297
+ end
298
+ end
299
+ end
@@ -0,0 +1,234 @@
1
+ #
2
+ # Evernote access utilities
3
+ #
4
+ # Diego Zamboni, March 2015
5
+ # Time-stamp: <2015-04-16 23:09:37 diego>
6
+
7
+ # Load libraries required by the Evernote OAuth
8
+ require 'oauth'
9
+ require 'oauth/consumer'
10
+
11
+ # Load Thrift & Evernote Ruby libraries
12
+ require "evernote_oauth"
13
+
14
+ class Evernote_utils
15
+
16
+ # Client credentials for enwrite
17
+ OAUTH_CONSUMER_KEY = "zzamboni-2648"
18
+ OAUTH_CONSUMER_SECRET = "05f988c37b5e8c68"
19
+
20
+ # Connect to Sandbox server?
21
+ SANDBOX = false
22
+
23
+ # Environment variable in which to look for the token
24
+ ENVVAR = 'ENWRITE_AUTH_TOKEN'
25
+
26
+ @@authToken = nil
27
+ @@userStore = nil
28
+ @@noteStore = nil
29
+ @@notebooks = nil
30
+ @@tags = nil
31
+ @@forceAuth = false
32
+
33
+ def self.interactiveGetToken
34
+ callback_url = "http://zzamboni.org/enwrite-callback.html"
35
+ client = EvernoteOAuth::Client.new(token: nil, consumer_key: OAUTH_CONSUMER_KEY, consumer_secret: OAUTH_CONSUMER_SECRET, sandbox: SANDBOX)
36
+ request_token = client.request_token(:oauth_callback => callback_url)
37
+ authorize_url = request_token.authorize_url
38
+
39
+ puts("Welcome to enwrite's Evernote authentication.
40
+
41
+ Please open the following URL:
42
+ #{authorize_url}
43
+
44
+ Once you authenticate you will be redirected to
45
+ a page in the zzamboni.org domain that will show you an authentication verifier
46
+ token. Please enter that token now.")
47
+ print("> ")
48
+ $stdout.flush
49
+ oauth_verifier = gets.chomp
50
+
51
+ access_token = request_token.get_access_token(:oauth_verifier => oauth_verifier)
52
+
53
+ puts("Thank you! Your access token is the following string:
54
+ #{access_token.token}
55
+
56
+ I can store the token for you in the config file (#{config_file}),
57
+ then enwrite will use it automatically in the future.
58
+ ")
59
+ print "Would you like me to do that for you now (Y/n)? "
60
+ $stdout.flush
61
+ yesno = gets.chomp
62
+ if yesno =~ /^[yY]/
63
+ setconfig(:evernote_auth_token, access_token.token)
64
+ puts "Token stored."
65
+ else
66
+ puts "OK, I won't store the token, just use it for now.
67
+
68
+ You can also store it in the ENWRITE_AUTH_TOKEN environment variable."
69
+ end
70
+ # Cancel force mode after we've gotten the token
71
+ @@forceAuth = false
72
+
73
+ return access_token.token
74
+ end
75
+
76
+ def self.getToken
77
+ if @@forceAuth
78
+ return self.interactiveGetToken
79
+ elsif not ENV[ENVVAR].nil?
80
+ return ENV[ENVVAR]
81
+ elsif not config(:evernote_auth_token).nil?
82
+ return config(:evernote_auth_token)
83
+ else
84
+ return self.interactiveGetToken
85
+ end
86
+ end
87
+
88
+ def self.authToken
89
+ if @@authToken == nil || @@forceAuth
90
+ @@authToken = self.getToken
91
+ end
92
+ return @@authToken
93
+ end
94
+
95
+ def self.userStore
96
+ if @@userStore == nil
97
+ # Initial development is performed on our sandbox server. To use the production
98
+ # service, change "sandbox.evernote.com" to "www.evernote.com" and replace your
99
+ # developer token above with a token from
100
+ # https://www.evernote.com/api/DeveloperToken.action
101
+ evernoteHost = SANDBOX ? "sandbox.evernote.com" : "www.evernote.com"
102
+ userStoreUrl = "https://#{evernoteHost}/edam/user"
103
+
104
+ userStoreTransport = Thrift::HTTPClientTransport.new(userStoreUrl)
105
+ userStoreProtocol = Thrift::BinaryProtocol.new(userStoreTransport)
106
+ @@userStore = Evernote::EDAM::UserStore::UserStore::Client.new(userStoreProtocol)
107
+ end
108
+ return @@userStore
109
+ end
110
+
111
+ def self.checkVersion
112
+ versionOK = self.userStore.checkVersion("enwrite",
113
+ Evernote::EDAM::UserStore::EDAM_VERSION_MAJOR,
114
+ Evernote::EDAM::UserStore::EDAM_VERSION_MINOR)
115
+ verbose "Is my Evernote API version up to date? #{versionOK}"
116
+ unless versionOK
117
+ error "Please update the Evernote Ruby libraries - they are not up to date."
118
+ exit(1)
119
+ end
120
+ end
121
+
122
+ def self.noteStore
123
+ if @@noteStore == nil
124
+ # Get the URL used to interact with the contents of the user's account
125
+ # When your application authenticates using OAuth, the NoteStore URL will
126
+ # be returned along with the auth token in the final OAuth request.
127
+ # In that case, you don't need to make this call.
128
+ noteStoreUrl = self.userStore.getNoteStoreUrl(self.authToken)
129
+
130
+ noteStoreTransport = Thrift::HTTPClientTransport.new(noteStoreUrl)
131
+ noteStoreProtocol = Thrift::BinaryProtocol.new(noteStoreTransport)
132
+ @@noteStore = Evernote::EDAM::NoteStore::NoteStore::Client.new(noteStoreProtocol)
133
+ end
134
+ return @@noteStore
135
+ end
136
+
137
+ def self.notebooks(force=false)
138
+ if (@@notebooks == nil) or force
139
+ # List all of the notebooks in the user's account
140
+ @@notebooks = self.noteStore.listNotebooks(self.authToken)
141
+ verbose "Found #{notebooks.size} notebooks:"
142
+ defaultNotebook = notebooks.first
143
+ notebooks.each do |notebook|
144
+ verbose " * #{notebook.name}"
145
+ end
146
+ end
147
+ return @@notebooks
148
+ end
149
+
150
+ def self.tags(force=false)
151
+ if (@@tags == nil) or force
152
+ verbose "Reading all tags:"
153
+
154
+ # Get list of all tags, cache it for future use
155
+ taglist = self.noteStore.listTags(self.authToken)
156
+ # Create a hash for easier reference
157
+ @@tags = {}
158
+ tagstr = ""
159
+ for t in taglist
160
+ @@tags[t.guid] = t
161
+ @@tags[t.name] = t
162
+ tagstr += "#{t.name} "
163
+ end
164
+ verbose tagstr
165
+ end
166
+ return @@tags
167
+ end
168
+
169
+ def self.init(force=false, token=nil)
170
+ @@forceAuth = force
171
+ if not token.nil?
172
+ @@forceAuth = false
173
+ @@authToken = token
174
+ end
175
+ self.authToken
176
+ self.userStore
177
+ self.checkVersion
178
+ self.noteStore
179
+
180
+ self.notebooks
181
+ self.tags
182
+ end
183
+
184
+ def self.getWholeNote(metadata)
185
+ note = self.noteStore.getNote(self.authToken, metadata.guid, true, true, false, false)
186
+ note.tagNames = []
187
+ if metadata.tagGuids != nil
188
+ tags = Evernote_utils.tags
189
+ note.tagNames = metadata.tagGuids.map { |guid| tags[guid].name }
190
+ end
191
+ verbose "Tags: #{note.tagNames}"
192
+ return note
193
+ end
194
+
195
+ # From http://pollen.io/2012/12/creating-a-note-in-evernote-from-ruby/
196
+ # With changes to handle RATE_LIMIT_REACHED
197
+ def self.translate_error(e)
198
+ error_name = "unknown"
199
+ case e.errorCode
200
+ when Evernote::EDAM::Error::EDAMErrorCode::AUTH_EXPIRED
201
+ error_name = "AUTH_EXPIRED"
202
+ when Evernote::EDAM::Error::EDAMErrorCode::BAD_DATA_FORMAT
203
+ error_name = "BAD_DATA_FORMAT"
204
+ when Evernote::EDAM::Error::EDAMErrorCode::DATA_CONFLICT
205
+ error_name = "DATA_CONFLICT"
206
+ when Evernote::EDAM::Error::EDAMErrorCode::DATA_REQUIRED
207
+ error_name = "DATA_REQUIRED"
208
+ when Evernote::EDAM::Error::EDAMErrorCode::ENML_VALIDATION
209
+ error_name = "ENML_VALIDATION"
210
+ when Evernote::EDAM::Error::EDAMErrorCode::INTERNAL_ERROR
211
+ error_name = "INTERNAL_ERROR"
212
+ when Evernote::EDAM::Error::EDAMErrorCode::INVALID_AUTH
213
+ error_name = "INVALID_AUTH"
214
+ when Evernote::EDAM::Error::EDAMErrorCode::LIMIT_REACHED
215
+ error_name = "LIMIT_REACHED"
216
+ when Evernote::EDAM::Error::EDAMErrorCode::PERMISSION_DENIED
217
+ error_name = "PERMISSION_DENIED"
218
+ when Evernote::EDAM::Error::EDAMErrorCode::QUOTA_REACHED
219
+ error_name = "QUOTA_REACHED"
220
+ when Evernote::EDAM::Error::EDAMErrorCode::SHARD_UNAVAILABLE
221
+ error_name = "SHARD_UNAVAILABLE"
222
+ when Evernote::EDAM::Error::EDAMErrorCode::UNKNOWN
223
+ error_name = "UNKNOWN"
224
+ when Evernote::EDAM::Error::EDAMErrorCode::VALID_VALUES
225
+ error_name = "VALID_VALUES"
226
+ when Evernote::EDAM::Error::EDAMErrorCode::VALUE_MAP
227
+ error_name = "VALUE_MAP"
228
+ when Evernote::EDAM::Error::EDAMErrorCode::RATE_LIMIT_REACHED
229
+ error_name = "RATE_LIMIT_REACHED"
230
+ e.message = "Rate limit reached. Please retry in #{e.rateLimitDuration} seconds"
231
+ end
232
+ rv = "Error code was: #{error_name}[#{e.errorCode}] and parameter: [#{e.message}]"
233
+ end
234
+ end