everdone 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f6a233c262926369ff671d05bc39ca7c4851473b
4
+ data.tar.gz: 243e02fe226c50a3ef411ba9579dbf83522fde1e
5
+ SHA512:
6
+ metadata.gz: 57f67c9233a73baddda008eda39c9d503ec45e4ddc70f8c6cf32cb2703eebca87af26c803d361442171ae1f43855ac5e066f7856b6ccac2e431944af34d72380
7
+ data.tar.gz: 123e4d163fe238d566d665ae132dc6abf60f828adaf06917d36fdc99cd0f1d1815e4a68d560e1d735a2de2aa3f3b06f8816dde34502bb702f6b5a8d13d99c05e
@@ -0,0 +1,21 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ everdone.sublime-project
19
+ everdone.sublime-workspace
20
+ .DS_Store
21
+ *.swp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in everdone.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Steve Heckt
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,95 @@
1
+ # Everdone
2
+
3
+ A command line utility that takes completed tasks from Todooist and inserts them as entries into Evernote.
4
+
5
+ _Full disclosure: [Zapier](http://zapier.com) now supports pushing Todoist items to Evernote_
6
+
7
+ I still like my solution better as it gives finer control - and I wrote it. :)
8
+
9
+ Likely this best serves as simple examples of using the Evernote and Todoist API's from Ruby.
10
+
11
+ ## Installation
12
+
13
+ Add this line to your application's Gemfile:
14
+
15
+ gem 'everdone'
16
+
17
+ And then execute:
18
+
19
+ $ bundle
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install everdone
24
+
25
+ This utility depends on Todoist having a project named "EvernoteSync". An item is added to this project to track all the Todoist item IDs that have already been added to Evernote. This keeps the utility from needing to query Evernote to see if an item has already been copied. Not as elegant a solution as I'd like. (BTW, the utility does double-check with Evernote before adding a new item.)
26
+
27
+ ## Usage
28
+
29
+ Everdone was written to be a command line utility, but to use it from a Ruby script...
30
+
31
+ require 'everdone'
32
+
33
+ Everdone.sync
34
+
35
+ Someday it should take options at the sync() call, but for now it starts with default options (included in this gem) and merges in options specified in the '.everdone' file in the caller's home directory. The full set of options are:
36
+
37
+ ```json
38
+ {
39
+ "default_notebook" : "<set to one of your Evernote notebooks>",
40
+ "tag" : "Todoist",
41
+ "evernote_datetime_format" : "%a %e %b %Y %H:%M",
42
+ "use_evernote_sandbox" : false,
43
+ "evernote_token" : "<get from: https://www.evernote.com/api/DeveloperToken.action>",
44
+
45
+ "todoist_token" : "<get from your Todoist settings Account tab>",
46
+ "todoist_completed_window" : "past 2 weeks",
47
+ "todoist_datetime_format" : "%a %e %b %Y %H:%M:%S %z",
48
+ "todoist_project_url" : "https://todoist.com/app?v=204#project/",
49
+ "todoist_label_url" : "https://todoist.com/app?v=204#agenda/@",
50
+ "todoist_sync_tracking_project" : "EvernoteSync",
51
+ "todoist_content_tag" : "TodoistItemId",
52
+ "todoist_link_title_regex" : "/\[\[[\w\s]+=[\h]+\s*,\s*([ -~]+)\]\]/",
53
+
54
+ "todoist_evernote_map" : {
55
+ }
56
+ }
57
+ ```
58
+
59
+ A basic set of options, in JSON format within your .everdone file, might look like:
60
+
61
+ $ cat ~/.everdone
62
+ {
63
+ "default_notebook" : "My Work Notebook",
64
+ "evernote_token" : "S=s1:U=1f14:E=14c1<your Evernote dev token bits>de0626b34bc5ede7",
65
+ "todoist_token" : "582a9<your Todoist API token bits>71ef5",
66
+ "todoist_evernote_map" : {
67
+ "Home" : "My Personal Notebook",
68
+ "Hobby" : "My Hobby Notebook"
69
+ }
70
+ }
71
+
72
+ From the command line it looks like:
73
+
74
+ $ everdone
75
+ INFO: Returned 39 items
76
+ INFO: Done! Of 39 found...
77
+ 1 added to Evernote
78
+ All time total processed now 340
79
+
80
+ ## The Present
81
+
82
+ Contains a nacent set of tests.
83
+
84
+ ## The Future
85
+
86
+ See everdone.rb for a list of TODO's I have in mind. Other, more minor TODO ideas are sprinked throughout the code.
87
+ But I suspect this need will soon be fully met by Zapier.com or IFTTT.com.
88
+
89
+ ## Contributing
90
+
91
+ 1. Fork it ( http://github.com/heck/everdone/fork )
92
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
93
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
94
+ 4. Push to the branch (`git push origin my-new-feature`)
95
+ 5. Create new Pull Request
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rake'
4
+ require 'rake/testtask'
5
+
6
+ Rake::TestTask.new do |t|
7
+ # t.libs << './lib'
8
+ t.libs << 'test'
9
+ t.pattern = 'test/**/*Test.rb'
10
+ end
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/ruby
2
+
3
+ require 'everdone'
4
+ Everdone.sync
5
+
@@ -0,0 +1,31 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'everdone/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "everdone"
8
+ spec.version = Everdone::VERSION
9
+ spec.authors = ["Steve Heckt"]
10
+ spec.email = ["home@u2me.com"]
11
+ spec.summary = %q{Syncs completed Todoist items into Evernote}
12
+ spec.description = %q{Uses Todoist and Evernote web services (and their respective API tokesn) }
13
+ spec.homepage = "http://github.com/heck/everdone"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_runtime_dependency "evernote-thrift", "~> 1.25"
22
+ spec.add_runtime_dependency "evernote_oauth", "~> 0.2"
23
+ spec.add_runtime_dependency "httparty", "~> 0.13"
24
+ spec.add_runtime_dependency "json", "~> 1.8"
25
+ spec.add_runtime_dependency "oauth", "~> 0.4"
26
+
27
+ spec.add_development_dependency "bundler", "~> 1.5"
28
+ spec.add_development_dependency "rake"
29
+ spec.add_development_dependency "minitest"
30
+ spec.add_development_dependency "awesome_print"
31
+ end
@@ -0,0 +1,89 @@
1
+ # TODO: Add real error and corner case handling
2
+ # TODO: command line options such as "create user config", "pre-flight test", "dry run", "verbose"
3
+ # TODO: Test on Windows
4
+ # TODO: Templatize Evernote entry format
5
+ # TODO: Allow Todoist content to be in markdown and covert before posting to Evernote
6
+
7
+ require "everdone/version"
8
+
9
+ require 'awesome_print'
10
+ require 'time'
11
+
12
+ # Everdone local stuff
13
+ require 'everdone/config'
14
+ require 'everdone/evernote'
15
+ require 'everdone/enmlformatter'
16
+ require 'everdone/todoist'
17
+ require 'everdone/sync'
18
+
19
+ module Everdone
20
+ def self.init
21
+ @@config = Config.new(File.expand_path("../everdone/default_config.json", __FILE__), "#{Dir.home}/.everdone")
22
+ @@evernote = Evernote.new(@@config)
23
+ @@todoist = Todoist.new(@@config)
24
+ end
25
+
26
+ def self.get_notebook_from_project(project)
27
+ notebook = @@config.todoist_evernote_map[project] # It is possible the project is in the map but the value is nil. This means: don't add items from this project to Evernote
28
+ if not @@config.todoist_evernote_map.has_key?(project) # If the project is not in the map at all then put it in the default notebook
29
+ notebook = @@config.default_notebook
30
+ end
31
+ return notebook
32
+ end
33
+
34
+ def self.sync
35
+ self.init
36
+
37
+ sync = Sync.new(@@config, @@todoist)
38
+
39
+ items = @@todoist.get_completed_items()
40
+ puts "INFO: Returned #{items.length} items"
41
+ processed = [] # list of Todoist item ids that were processed
42
+ found = [] # list of Todoist item ids not in the already processed list but subsequently found in Evernote
43
+ excluded = [] # a list of Todoist item ids that were not added because they belong to a project that is blacklisted
44
+ items.each { |item|
45
+ if not sync.is_already_processed(item.id)
46
+ # Map the project to an Evernote notebook
47
+ notebook = get_notebook_from_project(item.projects[0])
48
+ find_count = @@evernote.find_note_counts("#{@@config.todoist_content_tag}#{item.id}", notebook) if notebook
49
+ if notebook.nil?
50
+ excluded.push(item.id) # project did not map to any notebook => don't add to Evernote
51
+ elsif find_count > 0
52
+ found.push(item.id) # Todoist item already in Evernote => don't add
53
+ else # not already processed nor in an ignored project nor was it found in Evernote. Make a new one!
54
+ # Create the note's content
55
+ content = EnmlFormatter.new(@@config)
56
+ content.text("Project: ").link(item.projects[0], item.get_project_url(0))
57
+ if item.labels.length > 0
58
+ content.space.space.space.space
59
+ content.text("Labels: ")
60
+ item.labels.each { |label|
61
+ content.link(label, item.get_label_url(label)).space
62
+ }
63
+ end
64
+ content.space.space.space.text("#{@@config.todoist_content_tag}#{item.id}")
65
+ item.notes.each { |note|
66
+ content.h3("Note created #{content.datetime_to_string(note.created, @@config.todoist_datetime_format)} [Todoist note id: #{note.id}]")
67
+ content.rawtext(note.content)
68
+ }
69
+
70
+ # Create the note in Evernote
71
+ @@evernote.create_note(item.title,
72
+ content.to_s,
73
+ notebook,
74
+ Evernote.convert_text_to_timestamp(item.created, @@config.todoist_datetime_format))
75
+ end
76
+ processed.push(item.id) # whether item was added to Evernote or not don't process it again.
77
+ end
78
+ }
79
+
80
+ # Finish up by doing the syncing bookwork
81
+ sync.close(processed)
82
+
83
+ puts "INFO: Done! Of #{items.length} found..."
84
+ puts " #{processed.length - (found.length+excluded.length)} added to Evernote"
85
+ puts " #{found.length} were already in Evernote" if found.length > 0
86
+ puts " #{excluded.length} were not added as they are in blacklisted Todoist projects" if excluded.length > 0
87
+ puts " All time total processed now #{sync.get_processed_total()}"
88
+ end
89
+ end
@@ -0,0 +1,65 @@
1
+ #
2
+ # Class to handle reading, writing, and encapsulating JSON-based app config
3
+
4
+ # TODO: Preflight check vs. config. Tokens work? Projects, notebooks, tags needed all there?
5
+
6
+ require 'json'
7
+
8
+ module Everdone
9
+ class Config
10
+ def initialize(default_hash_file, user_settings_file)
11
+ @defaults = unmarshall_from_file(default_hash_file)
12
+ @user_settings_file = user_settings_file
13
+ @users = unmarshall_from_file(@user_settings_file)
14
+ @current = {}
15
+
16
+ collate_settings()
17
+ end
18
+
19
+ private
20
+
21
+ def define_instance_var(name, value)
22
+ self.class.module_eval { attr_accessor name.to_sym }
23
+ instance_variable_set("@#{name}", value)
24
+ end
25
+
26
+ # return a string of JSON for the current settings
27
+ def marshall_to_string()
28
+ current = JSON.generate(@current)
29
+ return current
30
+ end
31
+
32
+ def unmarshall_from_string(new_config)
33
+ return JSON.parse(new_config)
34
+ end
35
+
36
+ def marshall_to_file(filename)
37
+ end
38
+
39
+ def unmarshall_from_file(filename)
40
+ users = {}
41
+ return users if not File.exist?(filename)
42
+ File.open(filename, "r") do |f|
43
+ users = JSON.load(f)
44
+ end
45
+ return users
46
+ end
47
+
48
+ def collate_settings
49
+ # add all the settings (every setting has a default) as a member variable of this class
50
+ @defaults.each { |name, val|
51
+ define_instance_var(name,val)
52
+ }
53
+ # combine the default settings and the settings from the user file into the current settings
54
+ @current = @defaults
55
+ @users.each { |name, val|
56
+ if @current.has_key?(name)
57
+ @current[name] = val
58
+ instance_variable_set("@#{name}", val)
59
+ else
60
+ puts "WARN: the settings file #{@user_settings_file} contained an unrecognized setting #{name} (value = #{val})"
61
+ end
62
+ }
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,19 @@
1
+ {
2
+ "default_notebook" : "<set to one of your Evernote notebooks>",
3
+ "tag" : "Todoist",
4
+ "evernote_datetime_format" : "%a %e %b %Y %H:%M",
5
+ "use_evernote_sandbox" : false,
6
+ "evernote_token" : "<get from: https://www.evernote.com/api/DeveloperToken.action>",
7
+
8
+ "todoist_token" : "<get from your Todoist settings Account tab>",
9
+ "todoist_completed_window" : "past 2 weeks",
10
+ "todoist_datetime_format" : "%a %e %b %Y %H:%M:%S %z",
11
+ "todoist_project_url" : "https://todoist.com/app?v=204#project/",
12
+ "todoist_label_url" : "https://todoist.com/app?v=204#agenda/@",
13
+ "todoist_sync_tracking_project" : "EvernoteSync",
14
+ "todoist_content_tag" : "TodoistItemId",
15
+ "todoist_link_title_regex" : "/\[\[[\w\s]+=[\h]+\s*,\s*([ -~]+)\]\]/",
16
+
17
+ "todoist_evernote_map" : {
18
+ }
19
+ }
@@ -0,0 +1,103 @@
1
+ require 'cgi'
2
+ require 'time'
3
+ require 'awesome_print'
4
+
5
+ require 'everdone/config'
6
+
7
+ #
8
+ # Class to help create the ENML (EverNote Markup Language) content
9
+ #
10
+
11
+ module Everdone
12
+ class EnmlFormatter
13
+ attr_reader :body
14
+
15
+ def initialize(config)
16
+ @config = config
17
+ @body = ""
18
+ end
19
+
20
+ def text(text)
21
+ @body = @body + text
22
+ return self
23
+ end
24
+
25
+ # from: http://code.tutsplus.com/tutorials/8-regular-expressions-you-should-know--net-6149
26
+ # match only url's that take the form http://..., https://...
27
+ # but NOT <whatever>@<url> or <host>.<domain>.<tld>
28
+ URL_REGEX = /((https?:\/\/)([\da-zA-Z\.-]+)\.([a-z\.]{2,6})(:[\d]+)?([\/\w \.?%_&=+-]*)*\/?)/
29
+ def rawtext(text)
30
+ # Take text and do some cooking
31
+ #
32
+ # Escape HTML tags so Evernote doesn't freak out on them
33
+ text = CGI::escapeHTML(text)
34
+ # Remove newlines and insert HTML breaks (<br/>)
35
+ text_lines = text.split(/\n/)
36
+ text = ""
37
+ text_lines.each { |line|
38
+ # Find URL-looking text and turn it into a link
39
+ url_match = line.match(URL_REGEX)
40
+ if url_match
41
+ url = url_match[1]
42
+ line.gsub!(url, "<a href='#{url}'>#{url}</a>") if url
43
+ end
44
+ text = text + "#{line}<br/>"
45
+ }
46
+ # Fix up some Todoist crap: It’s -> It's
47
+ text.gsub!("’", "'");
48
+ # Put it in the body
49
+ @body = @body + text
50
+ return self
51
+ end
52
+
53
+ def newline()
54
+ @body = @body + "<br/>"
55
+ return self
56
+ end
57
+
58
+ def h1(text)
59
+ @body = @body + "<h1>#{text}</h1>"
60
+ return self
61
+ end
62
+
63
+ def h2(text)
64
+ @body = @body + "<h2>#{text}</h2>"
65
+ return self
66
+ end
67
+
68
+ def h3(text)
69
+ @body = @body + "<h3>#{text}</h3>"
70
+ return self
71
+ end
72
+
73
+ def space
74
+ @body = @body + "&nbsp;"
75
+ return self
76
+ end
77
+
78
+ def link(text, url)
79
+ @body = @body + "<a href='#{url}'>#{text}</a>"
80
+ return self
81
+ end
82
+
83
+ def datetime_to_string(text, source_format)
84
+ return DateTime.strptime(text, source_format).new_offset(DateTime.now.offset).strftime(@config.evernote_datetime_format)
85
+ end
86
+
87
+ def datetime(text, source_format)
88
+ @body = @body + self.datetime_to_string(text, source_format)
89
+ return self
90
+ end
91
+
92
+ def clear
93
+ @body = ""
94
+ end
95
+
96
+ def to_s
97
+ ret = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
98
+ ret = "<!DOCTYPE en-note SYSTEM \"http://xml.evernote.com/pub/enml2.dtd\">"
99
+ ret += "<en-note>#{@body}</en-note>"
100
+ return ret
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,73 @@
1
+ # Load libraries required by the Evernote OAuth
2
+ require 'time'
3
+ require 'oauth'
4
+ require 'oauth/consumer'
5
+
6
+ # Load Thrift & Evernote Ruby libraries
7
+ require "evernote_oauth"
8
+
9
+ require "everdone/config"
10
+
11
+ module Everdone
12
+ class Evernote
13
+ def initialize(config)
14
+ @config = config
15
+ # Set up the NoteStore client
16
+ @evernote_client = EvernoteOAuth::Client.new(
17
+ token: @config.evernote_token,
18
+ sandbox: @config.use_evernote_sandbox
19
+ )
20
+ @note_store = @evernote_client.note_store
21
+ @notebook_guids = {}
22
+ notebooks = @note_store.listNotebooks
23
+ notebooks.each do |notebook|
24
+ @notebook_guids[notebook.name] = notebook.guid
25
+ end
26
+ @tags = @note_store.listTags
27
+ @tags.each { |tag|
28
+ if @config.tag and tag.name == @config.tag then
29
+ @tag_guid = tag.guid
30
+ break
31
+ end
32
+ }
33
+ end
34
+
35
+ def create_note(title, content, notebook, created)
36
+ new_note = ::Evernote::EDAM::Type::Note.new()
37
+ new_note.notebookGuid = @notebook_guids[notebook]
38
+ new_note.title = title.strip.slice(0..254).scan(/[[:print:]]/).join
39
+ new_note.created = created
40
+ new_note.content = content
41
+ new_note.tagNames = [@config.tag] if @config.tag
42
+
43
+ begin
44
+ created_note = @note_store.createNote(@config.evernote_token, new_note)
45
+ rescue => err
46
+ puts "ERROR: ----------- Evernote exception ------------------!!!"
47
+ ap err
48
+ puts "Note info:"
49
+ puts "Title: #{title}"
50
+ puts "Created: #{created}"
51
+ puts "Content:\n#{content}"
52
+ end
53
+
54
+ end
55
+
56
+ # returns the count of notes within the context:
57
+ # In the notebook NOTEBOOK_TARGET with tag (if defined) TAG_WITH and having content (if defined) content_text
58
+ def find_note_counts(content_text, notebook)
59
+ filter = ::Evernote::EDAM::NoteStore::NoteFilter.new()
60
+ filter.notebookGuid = @notebook_guids[notebook] if notebook
61
+ filter.tagGuids = [@tag_guid] if @config.tag
62
+ filter.words = content_text if content_text
63
+ ret = @note_store.findNoteCounts(filter, false)
64
+ # also: ret.tagCounts[@@tag_guid]
65
+ return !ret.notebookCounts.nil? && notebook && ret.notebookCounts[@notebook_guids[notebook]] ? ret.notebookCounts[@notebook_guids[notebook]] : 0
66
+ end
67
+
68
+ def self.convert_text_to_timestamp(dateText, date_format)
69
+ ret = DateTime.strptime(dateText, date_format).to_time.to_i * 1000
70
+ return ret
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,63 @@
1
+ #
2
+ # Class that keeps track of what's been synced
3
+ #
4
+ require 'time'
5
+ require 'json'
6
+
7
+ require 'everdone/todoist.rb'
8
+
9
+ module Everdone
10
+ class Sync
11
+ def initialize(config, todoist)
12
+ @config = config
13
+ @todoist = todoist
14
+ @data = {}
15
+ @current_item_id = nil
16
+ items = @todoist.get_items_by_project_name(@config.todoist_sync_tracking_project)
17
+ raise "Too many items in Todoist project used for syncing (#{@config.todoist_sync_tracking_project}): #{items.length}" if items.length > 1
18
+ if items.length == 0 then
19
+ initData()
20
+ else
21
+ @current_item_id = items[0]['id']
22
+ notes = @todoist.get_notes(@current_item_id)
23
+ unmarshall(notes[0]['content']) if notes and notes.length > 0
24
+ end
25
+ end
26
+
27
+ def init_data
28
+ @data['created'] = DateTime.now
29
+ @data['processed'] = []
30
+ end
31
+
32
+ def is_already_processed(item_id)
33
+ return !@data['processed'].find_index(item_id).nil?
34
+ end
35
+
36
+ def close(new_processed)
37
+ @data['processed'].concat(new_processed)
38
+ new_item_id = @todoist.add_item_to_project_by_name(@config.todoist_sync_tracking_project,
39
+ "Evernote sync data from #{DateTime.now.to_s} total processed = #{self.get_processed_total()}",
40
+ 1)
41
+ new_data = self.marshall
42
+ @todoist.add_note(new_item_id, new_data)
43
+ @todoist.delete_item_by_id(@current_item_id) if @current_item_id
44
+ end
45
+
46
+ def get_processed_total()
47
+ return @data['processed'].length
48
+ end
49
+
50
+ def add_to_processed(new_processed)
51
+ @data['processed'].push(new_processed)
52
+ end
53
+
54
+ def marshall
55
+ data = JSON.generate(@data)
56
+ return data
57
+ end
58
+
59
+ def unmarshall(data)
60
+ @data = JSON.parse(data)
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,61 @@
1
+ #
2
+ # TodoItem
3
+ # Handle mapping a single Todoist itme to something EverTodo can work with
4
+ #
5
+ require 'awesome_print'
6
+
7
+ require 'everdone/config'
8
+
9
+ module Everdone
10
+ class TodoNote
11
+ attr_reader :id, :created, :content
12
+ def initialize(todoist_note)
13
+ note = todoist_note
14
+ @id = note['id']
15
+ @created = note['posted']
16
+ @content = note['content']
17
+ end
18
+ end
19
+
20
+ class TodoItem
21
+ attr_reader :id, :title, :created, :projects, :project_ids, :labels, :notes, :priority, :due_date
22
+ def initialize(config, todoist_item, projects, labels)
23
+ @config = config
24
+ item = todoist_item
25
+ @id = item['id']
26
+ @title = item['content']
27
+ clean_title() # strip off all (known) Todoist detritus
28
+ @created = item['completed_date']
29
+ @projects = [] # Just the parent for now. TODO: array of projects starting with senior parent and working down to immediate parent
30
+ @projects.push(projects[item['project_id']])
31
+ @project_ids = [] # Just the parent for now. TODO: array of project ids starting with senior... (see above)
32
+ @project_ids.push(item['project_id'])
33
+ @labels = [] # arrary of associated labels (random order)
34
+ item['labels'].each { |labelId|
35
+ @labels.push(labels[labelId])
36
+ }
37
+ @priority = item['priority']
38
+ @due_date = item['due_date']
39
+ @notes = [] # array of notes
40
+ if not item['notes'].nil?
41
+ item['notes'].each { |note|
42
+ new_note = TodoNote.new(note)
43
+ @notes.push(new_note)
44
+ }
45
+ end
46
+ end
47
+
48
+ def clean_title()
49
+ cleaned_title = @title.match(@config.todoist_link_title_regex)
50
+ @title = cleaned_title.nil? ? @title : cleaned_title[1].strip
51
+ end
52
+
53
+ def get_project_url(project_index)
54
+ return @config.todoist_project_url + @project_ids[project_index].to_s
55
+ end
56
+
57
+ def get_label_url(label)
58
+ return @config.todoist_label_url + label
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,111 @@
1
+ require 'httparty'
2
+ require 'awesome_print'
3
+
4
+ require 'everdone/config'
5
+
6
+ require 'everdone/todoItem'
7
+
8
+ module Everdone
9
+ class Todoist
10
+ def initialize(config)
11
+ @config = config
12
+ @projects = {} # map of project id's to project names
13
+ @project_id_by_name = {} # map of project names to id's
14
+ projects = self.call_api('https://todoist.com/API/getProjects')
15
+ projects.each { |project|
16
+ @projects[project['id']] = project['name']
17
+ @project_id_by_name[project['name']] = project['id']
18
+ }
19
+ @labels = {} # map of label id's to their name strings
20
+ labels = self.call_api('https://todoist.com/API/getLabels')
21
+ labels.each { |label|
22
+ @labels[label[1]['id']] = label[1]['name']
23
+ }
24
+ end
25
+
26
+ # helper function for making RESTful calls
27
+ def call_api(url, params=nil)
28
+ query = { 'token' => @config.todoist_token }
29
+ query.merge!(params) if params
30
+ ret = HTTParty.post(url, {
31
+ :query => query
32
+ })
33
+
34
+ if ret.response.code != "200"
35
+ raise "ERROR: Error calling Todoist API #{url}\n Response Code: #{ret.response.code}\n Response: \n#{ret.response.body}"
36
+ end
37
+ return ret
38
+ end
39
+
40
+ def add_item_to_project_by_name(project_name, content, priority)
41
+ ret = self.call_api('https://todoist.com/API/addItem',
42
+ params={'project_id'=>@project_id_by_name[project_name],'content'=>content, 'priority'=>priority})
43
+ return ret['id']
44
+ end
45
+
46
+ def add_note(item_id, content)
47
+ ret = self.call_api('https://todoist.com/API/addNote', params={'item_id'=>item_id,'content'=>content})
48
+ return ret
49
+ end
50
+
51
+ def get_completed_items()
52
+ ret = [] # list of completed items converted into TodoItem objects
53
+ interval = @config.todoist_completed_window
54
+ items = self.call_api('https://todoist.com/API/getAllCompletedItems', params={'interval'=>interval})
55
+ items['items'].each { |item|
56
+ todo_item = TodoItem.new(@config, item, @projects, @labels)
57
+ ret.push(todo_item)
58
+ }
59
+ return ret
60
+ end
61
+
62
+ def get_item_by_id(id)
63
+ items = self.call_api('https://todoist.com/API/getItemsById', params={'ids'=>id}) # get a single item based on id
64
+ todo_item = TodoItem.new(@config, items[0], @projects, @labels)
65
+ return todo_item
66
+ end
67
+
68
+ def get_items_by_project_name(project_name)
69
+ ret = self.call_api('https://todoist.com/API/getUncompletedItems', params={'project_id'=>@project_id_by_name[project_name]})
70
+ return ret
71
+ end
72
+
73
+ def get_notes(item_id)
74
+ ret = self.call_api('https://todoist.com/API/getNotes', params={'item_id'=>item_id})
75
+ return ret
76
+ end
77
+
78
+ def complete_item_by_id(id)
79
+ ret = self.call_api('https://todoist.com/API/completeItems', params={'ids'=>"[#{id}]"})
80
+ return ret
81
+ end
82
+
83
+ def delete_item_by_id(id)
84
+ ret = self.call_api('https://todoist.com/API/deleteItems', params={'ids'=>"[#{id}]"})
85
+ return ret
86
+ end
87
+ end
88
+
89
+ #
90
+ ## getting a token via login
91
+ #
92
+ #login = HTTParty.post('https://todoist.com/API/login', {
93
+ # :query => {
94
+ # 'email'=>'home@u2me.com',
95
+ # 'password'=>'<insert password here>'
96
+ # }
97
+ #})
98
+ #
99
+ #if login.response.code != "200"
100
+ # raise "Login unsuccessful.\n#{login.response.code}\n#{login.response.body}"
101
+ #end
102
+ #
103
+ #puts "Login response was:"
104
+ #puts login.body
105
+ #puts
106
+ #
107
+
108
+ # for JSON responses, you can address keys in the response directly
109
+ #token = login['token']
110
+
111
+ end
@@ -0,0 +1,3 @@
1
+ module Everdone
2
+ VERSION = "0.0.3"
3
+ end
@@ -0,0 +1,28 @@
1
+ require 'everdone'
2
+ require 'awesome_print'
3
+
4
+ require 'minitest/autorun'
5
+
6
+ require 'everdone/config.rb'
7
+ require 'json'
8
+
9
+ class ConfigTest < MiniTest::Unit::TestCase
10
+ def setup
11
+ end
12
+
13
+ def test_initialize
14
+ config = Everdone::Config.new("test/config_1.json", "test/config_empty")
15
+ assert_equal "this is string_1", config.string_1
16
+ assert_equal 1, config.int_1
17
+ assert_equal 1.1, config.float_1
18
+ assert_equal "second_string", config.string_array_1[1]
19
+ assert_equal "value 1", config.hash_1["key_1"]
20
+ end
21
+
22
+ def test_initialze_with_user_settings
23
+ config = Everdone::Config.new("test/config_1.json", "test/config_partial")
24
+ assert_equal "this is THE NEW string_2", config.string_2
25
+ assert_equal 2.2, config.float_1
26
+ assert_equal 3, config.hash_1["key_2"]
27
+ end
28
+ end
@@ -0,0 +1,10 @@
1
+ {
2
+ "string_1": "this is string_1",
3
+ "string_2": "this is string_2",
4
+ "int_1": 1,
5
+ "float_1": 1.1,
6
+ "string_array_1": ["first_string", "second_string"],
7
+ "hash_1": {
8
+ "key_1":"value 1", "key_2":2
9
+ }
10
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "string_2": "this is THE NEW string_2",
3
+ "float_1": 2.2,
4
+ "hash_1": {
5
+ "key_1":"value replaced", "key_2":3
6
+ }
7
+ }
@@ -0,0 +1,57 @@
1
+ require 'everdone'
2
+
3
+ require 'minitest/autorun'
4
+
5
+ require 'everdone/config'
6
+ require 'everdone/enmlformatter'
7
+
8
+ class EnmlFormatterTest < MiniTest::Unit::TestCase
9
+ def setup
10
+ config = Everdone::Config.new("lib/everdone/default_config.json", "#{Dir.home}/.everdone")
11
+ @targ = Everdone::EnmlFormatter.new(config)
12
+ end
13
+
14
+ def test_text
15
+ # assert_equal "Hello World!", @main.hello
16
+ added_text = "this is some text"
17
+ @targ.text(added_text)
18
+ assert_equal added_text, @targ.body
19
+ end
20
+
21
+ def test_rawtext_todoist_crap
22
+ @targ.rawtext("It’s")
23
+ assert_match "It's", @targ.body
24
+ end
25
+
26
+ def check_url_good(url_text)
27
+ @targ.rawtext(url_text)
28
+ assert_match "<a href='#{url_text}'>#{url_text}</a>", @targ.body
29
+ @targ.clear
30
+ end
31
+
32
+ def check_url_bad(url_text)
33
+ @targ.rawtext(url_text)
34
+ refute_match "<a href=", @targ.body
35
+ @targ.clear
36
+ end
37
+
38
+ def test_rawtext_url_links
39
+ # the good
40
+ check_url_good "http://www.google.com"
41
+ check_url_good "https://www.google.com"
42
+ check_url_good "http://code.tutsplus.com/tutorials/8-regular-expressions-you-should-know--net-6149"
43
+ check_url_good "http://jira.nmwco.com:8080"
44
+ check_url_good "http://jira.nmwco.com:8080/browse/LOC-6847"
45
+ check_url_good "http://jira.nmwco.com:8080/browse/LOC-6847?filter=14192"
46
+ check_url_good "https://d1x0mwiac2rqwt.cloudfront.net/fec77b1148704f78bdf87a49d0a6e6b3/as/2014-03-18_Performance_log_Olson%2C_Erik.docx"
47
+ # the bad
48
+ check_url_bad "www.google.com"
49
+ check_url_bad "google.com"
50
+ check_url_bad "Knopf@NetMotionWireless.com"
51
+ end
52
+
53
+ TESTING_DATETIME_FORMAT = "%a %e %b %Y %H:%M:%S %z"
54
+ def test_datetimeToString
55
+ assert_equal "Mon 10 Mar 2014 08:45", @targ.datetime_to_string("Mon 10 Mar 2014 15:45:01 +0000", TESTING_DATETIME_FORMAT)
56
+ end
57
+ end
@@ -0,0 +1,19 @@
1
+ require 'everdone'
2
+
3
+ require 'minitest/autorun'
4
+
5
+ require 'everdone/config'
6
+ require 'everdone/evernote'
7
+
8
+ class EvernoteTest < MiniTest::Unit::TestCase
9
+ def setup
10
+ @config = Everdone::Config.new("lib/everdone/default_config.json", "#{Dir.home}/.everdone")
11
+ @evernote = Everdone::Evernote.new(@config)
12
+ end
13
+
14
+ def test_findNoteCounts
15
+ assert_equal 0, @evernote.find_note_counts("%$$%%$^@&& NOT IN EVERNOTE RIGHT? ^^{$^&*", @config.default_notebook) # should not be found
16
+ assert_equal 1, @evernote.find_note_counts("#{@config.todoist_content_tag}4653498", @config.default_notebook) # known to be
17
+ assert_equal 0, @evernote.find_note_counts("#{@config.todoist_content_tag}4653498", nil) # known but not in this notebook
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ require 'everdone'
2
+
3
+ require 'minitest/autorun'
4
+
5
+ require 'everdone/config'
6
+ require 'everdone/todoist'
7
+ require 'everdone/todoItem'
8
+
9
+ class TodoistTest < MiniTest::Unit::TestCase
10
+ def setup
11
+ @config = Everdone::Config.new("lib/everdone/default_config.json", "#{Dir.home}/.everdone")
12
+ @todoist = Everdone::Todoist.new(@config)
13
+ end
14
+
15
+ KNOWN_TODOIST_COMPLETED_ID = 1155724
16
+ def test_todoItem
17
+ todo_item = @todoist.get_item_by_id(KNOWN_TODOIST_COMPLETED_ID)
18
+ end
19
+ end
metadata ADDED
@@ -0,0 +1,199 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: everdone
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.3
5
+ platform: ruby
6
+ authors:
7
+ - Steve Heckt
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-04-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: evernote-thrift
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '1.25'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '1.25'
27
+ - !ruby/object:Gem::Dependency
28
+ name: evernote_oauth
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '0.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '0.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: httparty
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '0.13'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '0.13'
55
+ - !ruby/object:Gem::Dependency
56
+ name: json
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '1.8'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: '1.8'
69
+ - !ruby/object:Gem::Dependency
70
+ name: oauth
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ~>
74
+ - !ruby/object:Gem::Version
75
+ version: '0.4'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ~>
81
+ - !ruby/object:Gem::Version
82
+ version: '0.4'
83
+ - !ruby/object:Gem::Dependency
84
+ name: bundler
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ~>
88
+ - !ruby/object:Gem::Version
89
+ version: '1.5'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ~>
95
+ - !ruby/object:Gem::Version
96
+ version: '1.5'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: minitest
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - '>='
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: awesome_print
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - '>='
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - '>='
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ description: 'Uses Todoist and Evernote web services (and their respective API tokesn) '
140
+ email:
141
+ - home@u2me.com
142
+ executables:
143
+ - everdone
144
+ extensions: []
145
+ extra_rdoc_files: []
146
+ files:
147
+ - .gitignore
148
+ - Gemfile
149
+ - LICENSE.txt
150
+ - README.md
151
+ - Rakefile
152
+ - bin/everdone
153
+ - everdone.gemspec
154
+ - lib/everdone.rb
155
+ - lib/everdone/config.rb
156
+ - lib/everdone/default_config.json
157
+ - lib/everdone/enmlformatter.rb
158
+ - lib/everdone/evernote.rb
159
+ - lib/everdone/sync.rb
160
+ - lib/everdone/todoItem.rb
161
+ - lib/everdone/todoist.rb
162
+ - lib/everdone/version.rb
163
+ - test/configTest.rb
164
+ - test/config_1.json
165
+ - test/config_partial
166
+ - test/enmlformatterTest.rb
167
+ - test/evernoteTest.rb
168
+ - test/todoistTest.rb
169
+ homepage: http://github.com/heck/everdone
170
+ licenses:
171
+ - MIT
172
+ metadata: {}
173
+ post_install_message:
174
+ rdoc_options: []
175
+ require_paths:
176
+ - lib
177
+ required_ruby_version: !ruby/object:Gem::Requirement
178
+ requirements:
179
+ - - '>='
180
+ - !ruby/object:Gem::Version
181
+ version: '0'
182
+ required_rubygems_version: !ruby/object:Gem::Requirement
183
+ requirements:
184
+ - - '>='
185
+ - !ruby/object:Gem::Version
186
+ version: '0'
187
+ requirements: []
188
+ rubyforge_project:
189
+ rubygems_version: 2.0.3
190
+ signing_key:
191
+ specification_version: 4
192
+ summary: Syncs completed Todoist items into Evernote
193
+ test_files:
194
+ - test/configTest.rb
195
+ - test/config_1.json
196
+ - test/config_partial
197
+ - test/enmlformatterTest.rb
198
+ - test/evernoteTest.rb
199
+ - test/todoistTest.rb