todoist-ruby 0.1.1
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.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +201 -0
- data/Rakefile +20 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/fixtures/.gitignore +6 -0
- data/fixtures/uuid/.gitignore +4 -0
- data/fixtures/vcr_cassettes/.gitignore +4 -0
- data/lib/todoist.rb +23 -0
- data/lib/todoist/misc/activity.rb +51 -0
- data/lib/todoist/misc/backups.rb +13 -0
- data/lib/todoist/misc/completed.rb +34 -0
- data/lib/todoist/misc/items.rb +55 -0
- data/lib/todoist/misc/projects.rb +31 -0
- data/lib/todoist/misc/query.rb +30 -0
- data/lib/todoist/misc/quick.rb +15 -0
- data/lib/todoist/misc/templates.rb +29 -0
- data/lib/todoist/misc/uploads.rb +28 -0
- data/lib/todoist/misc/user.rb +16 -0
- data/lib/todoist/sync/filters.rb +44 -0
- data/lib/todoist/sync/items.rb +107 -0
- data/lib/todoist/sync/labels.rb +39 -0
- data/lib/todoist/sync/notes.rb +32 -0
- data/lib/todoist/sync/projects.rb +56 -0
- data/lib/todoist/sync/reminders.rb +36 -0
- data/lib/todoist/util/api_helper.rb +89 -0
- data/lib/todoist/util/command_synchronizer.rb +54 -0
- data/lib/todoist/util/config.rb +78 -0
- data/lib/todoist/util/network_helper.rb +100 -0
- data/lib/todoist/util/parse_helper.rb +62 -0
- data/lib/todoist/util/uuid.rb +17 -0
- data/lib/todoist/version.rb +3 -0
- data/todoist.gemspec +44 -0
- metadata +227 -0
@@ -0,0 +1,32 @@
|
|
1
|
+
module Todoist
|
2
|
+
module Sync
|
3
|
+
class Notes
|
4
|
+
include Todoist::Util
|
5
|
+
|
6
|
+
# Return a Hash of notes where key is the id of a note and value is a note
|
7
|
+
def collection
|
8
|
+
return ApiHelper.collection("notes")
|
9
|
+
end
|
10
|
+
|
11
|
+
# Add a note with a given hash of attributes and returns the note id.
|
12
|
+
# Please note that item_id or project_id key is required. In addition,
|
13
|
+
# content is also a required key in the hash.
|
14
|
+
def add(args)
|
15
|
+
return ApiHelper.add(args, "note_add")
|
16
|
+
end
|
17
|
+
|
18
|
+
# Update a note given a note
|
19
|
+
def update(note)
|
20
|
+
return ApiHelper.command(note.to_h, "note_update")
|
21
|
+
end
|
22
|
+
|
23
|
+
# Delete notes given an a note
|
24
|
+
def delete(note)
|
25
|
+
args = {id: note.id}
|
26
|
+
return ApiHelper.command(args, "note_delete")
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Todoist
|
2
|
+
module Sync
|
3
|
+
|
4
|
+
|
5
|
+
class Projects
|
6
|
+
include Todoist::Util
|
7
|
+
|
8
|
+
# Return a Hash of projects where key is the id of a project and value is a project
|
9
|
+
def collection
|
10
|
+
return ApiHelper.collection("projects")
|
11
|
+
end
|
12
|
+
|
13
|
+
# Add a project with a given hash of attributes and returns the project id
|
14
|
+
def add(args)
|
15
|
+
return ApiHelper.add(args, "project_add")
|
16
|
+
end
|
17
|
+
|
18
|
+
# Delete projects given an array of projects
|
19
|
+
def delete(projects)
|
20
|
+
project_ids = projects.collect { |project| project.id }
|
21
|
+
args = {ids: project_ids.to_json}
|
22
|
+
return ApiHelper.command(args, "project_delete")
|
23
|
+
end
|
24
|
+
|
25
|
+
# Archive projects given an array of projects
|
26
|
+
def archive(projects)
|
27
|
+
project_ids = projects.collect { |project| project.id }
|
28
|
+
args = {ids: project_ids.to_json}
|
29
|
+
return ApiHelper.command(args, "project_archive")
|
30
|
+
end
|
31
|
+
|
32
|
+
# Unarchive projects given an array of projects
|
33
|
+
def unarchive(projects)
|
34
|
+
project_ids = projects.collect { |project| project.id }
|
35
|
+
args = {ids: project_ids.to_json}
|
36
|
+
return ApiHelper.command(args, "project_unarchive")
|
37
|
+
end
|
38
|
+
|
39
|
+
# Update project given a project
|
40
|
+
def update(project)
|
41
|
+
return ApiHelper.command(project.to_h, "project_update")
|
42
|
+
end
|
43
|
+
|
44
|
+
# Update orders and indents for an array of projects
|
45
|
+
def update_multiple_orders_and_indents(projects)
|
46
|
+
tuples = {}
|
47
|
+
projects.each do |project|
|
48
|
+
tuples[project.id] = [project.item_order, project.indent]
|
49
|
+
end
|
50
|
+
args = {ids_to_orders_indents: tuples.to_json}
|
51
|
+
return ApiHelper.command(args, "project_update_orders_indents")
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Todoist
|
2
|
+
module Sync
|
3
|
+
class Reminders
|
4
|
+
include Todoist::Util
|
5
|
+
|
6
|
+
# Return a Hash of reminders where key is the id of a reminder and value is a reminder
|
7
|
+
def collection
|
8
|
+
return ApiHelper.collection("reminders")
|
9
|
+
end
|
10
|
+
|
11
|
+
# Add a reminder with a given hash of attributes and returns the reminder id.
|
12
|
+
# Please note that item_id is required as is a date as specific in the
|
13
|
+
# documentation. This method can be tricky to all.
|
14
|
+
def add(args)
|
15
|
+
return ApiHelper.add(args, "reminder_add")
|
16
|
+
end
|
17
|
+
|
18
|
+
# Update a reminder given a reminder
|
19
|
+
def update(reminder)
|
20
|
+
return ApiHelper.command(reminder.to_h, "reminder_update")
|
21
|
+
end
|
22
|
+
|
23
|
+
# Delete reminder given an array of reminders
|
24
|
+
def delete(reminder)
|
25
|
+
args = {id: reminder.id}
|
26
|
+
return ApiHelper.command(args, "reminder_delete")
|
27
|
+
end
|
28
|
+
|
29
|
+
# Clear locations which is used for location reminders
|
30
|
+
def clear_locations
|
31
|
+
args = {}
|
32
|
+
return ApiHelper.command(args, "clear_locations")
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require "net/http"
|
2
|
+
require "json"
|
3
|
+
require "todoist/util/config"
|
4
|
+
require "todoist/util/network_helper"
|
5
|
+
require "todoist/util/parse_helper"
|
6
|
+
require "todoist/util/uuid"
|
7
|
+
require "todoist/util/command_synchronizer"
|
8
|
+
require "ostruct"
|
9
|
+
require 'concurrent'
|
10
|
+
|
11
|
+
|
12
|
+
|
13
|
+
module Todoist
|
14
|
+
|
15
|
+
module Util
|
16
|
+
|
17
|
+
class ApiHelper
|
18
|
+
|
19
|
+
@@object_cache = {"projects" => Concurrent::Hash.new({}), "labels" => Concurrent::Hash.new({}),
|
20
|
+
"items" => Concurrent::Hash.new({}), "notes" => Concurrent::Hash.new({}),
|
21
|
+
"reminders" => Concurrent::Hash.new({}), "filters" => Concurrent::Hash.new({})
|
22
|
+
}
|
23
|
+
@@sync_token_cache = Concurrent::Hash.new({"projects" => "*", "labels" => "*",
|
24
|
+
"items" => "*", "notes" => "*", "reminders" => "*", "filters" => "*"})
|
25
|
+
|
26
|
+
def self.collection(type)
|
27
|
+
CommandSynchronizer.sync
|
28
|
+
|
29
|
+
response = getSyncResponse({sync_token: sync_token(type), resource_types: "[\"#{type}\"]"})
|
30
|
+
response[type].each do |object_data|
|
31
|
+
object = OpenStruct.new(object_data)
|
32
|
+
objects(type)[object.id] = object
|
33
|
+
end
|
34
|
+
set_sync_token(type, response["sync_token"])
|
35
|
+
return objects(type)
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.exec(args, command, temporary_resource_id)
|
39
|
+
command_uuid = Uuid.command_uuid
|
40
|
+
commands = {type: command, temp_id: temporary_resource_id, uuid: command_uuid, args: args}
|
41
|
+
response = getSyncResponse({commands: "[#{commands.to_json}]"})
|
42
|
+
raise RuntimeError, "Response returned is not ok" unless response["sync_status"][command_uuid] == "ok"
|
43
|
+
return response
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.command(args, command)
|
47
|
+
temporary_resource_id = Uuid.temporary_resource_id
|
48
|
+
command_uuid = Uuid.command_uuid
|
49
|
+
command = {type: command, temp_id: temporary_resource_id, uuid: command_uuid, args: args}
|
50
|
+
CommandSynchronizer.queue(command)
|
51
|
+
return true
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.add(args, command)
|
55
|
+
temporary_resource_id = Uuid.temporary_resource_id
|
56
|
+
command_uuid = Uuid.command_uuid
|
57
|
+
command = {type: command, temp_id: temporary_resource_id, uuid: command_uuid, args: args}
|
58
|
+
object = OpenStruct.new({temp_id: temporary_resource_id, id: temporary_resource_id})
|
59
|
+
temp_id_callback = Proc.new do |temp_id_mappings|
|
60
|
+
object.id = temp_id_mappings[temporary_resource_id] if temp_id_mappings[temporary_resource_id]
|
61
|
+
end
|
62
|
+
|
63
|
+
CommandSynchronizer.queue(command, temp_id_callback)
|
64
|
+
return object
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.getSyncResponse(params)
|
68
|
+
NetworkHelper.getResponse(Config::TODOIST_SYNC_COMMAND, params)
|
69
|
+
end
|
70
|
+
|
71
|
+
protected
|
72
|
+
|
73
|
+
|
74
|
+
|
75
|
+
def self.objects(type)
|
76
|
+
@@object_cache[type]
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.sync_token(type)
|
80
|
+
@@sync_token_cache[type]
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.set_sync_token(type, value)
|
84
|
+
@@sync_token_cache[type] = value
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'concurrent'
|
2
|
+
require "todoist/util/api_helper"
|
3
|
+
|
4
|
+
module Todoist
|
5
|
+
|
6
|
+
module Util
|
7
|
+
class CommandSynchronizer
|
8
|
+
|
9
|
+
@@command_cache = Concurrent::Array.new([])
|
10
|
+
@@command_mutex = Mutex.new
|
11
|
+
@@temp_id_callback_cache = Concurrent::Array.new([])
|
12
|
+
|
13
|
+
def self.start
|
14
|
+
@@sync_thread = Thread.new do
|
15
|
+
while(true) do
|
16
|
+
process()
|
17
|
+
sleep(3)
|
18
|
+
end
|
19
|
+
end unless @@sync_thread
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.stop
|
23
|
+
Thread.kill(@@sync_thread) if @@sync_thread
|
24
|
+
@@sync_thread = nil
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.queue(command, callback = nil)
|
28
|
+
@@command_mutex.synchronize do
|
29
|
+
@@command_cache.push(command)
|
30
|
+
@@temp_id_callback_cache.push(callback) if callback
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.sync
|
35
|
+
@@command_mutex.synchronize do
|
36
|
+
response = ApiHelper.getSyncResponse({commands: @@command_cache.to_json})
|
37
|
+
@@command_cache.clear
|
38
|
+
# Process callbacks here
|
39
|
+
temp_id_mappings = response["temp_id_mapping"]
|
40
|
+
@@temp_id_callback_cache.each do |callback|
|
41
|
+
callback.(temp_id_mappings)
|
42
|
+
end
|
43
|
+
@@temp_id_callback_cache.clear
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
protected
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Todoist
|
2
|
+
module Util
|
3
|
+
class Config
|
4
|
+
TODOIST_API_URL = "https://todoist.com/API/v7"
|
5
|
+
|
6
|
+
# List of commands supported
|
7
|
+
@@command_list = [
|
8
|
+
TODOIST_SYNC_COMMAND = "/sync",
|
9
|
+
TODOIST_QUERY_COMMAND = "/query",
|
10
|
+
TODOIST_TEMPLATES_IMPORT_INTO_PROJECT_COMMAND = "/templates/import_into_project",
|
11
|
+
TODOIST_TEMPLATES_EXPORT_AS_FILE_COMMAND = "/templates/export_as_file",
|
12
|
+
TODOIST_TEMPLATES_EXPORT_AS_URL_COMMAND = "/templates/export_as_url",
|
13
|
+
TODOIST_UPLOADS_ADD_COMMAND = "/uploads/add",
|
14
|
+
TODOIST_UPLOADS_GET_COMMAND = "/uploads/get",
|
15
|
+
TODOIST_UPLOADS_DELETE_COMMAND = "/uploads/delete",
|
16
|
+
TODOIST_COMPLETED_GET_STATS_COMMAND = "/completed/get_stats",
|
17
|
+
TODOIST_COMPLETED_GET_ALL_COMMAND = "/completed/get_all",
|
18
|
+
TODOIST_PROJECTS_GET_ARCHIVED_COMMAND = "/projects/get_archived",
|
19
|
+
TODOIST_PROJECTS_GET_COMMAND = "/projects/get",
|
20
|
+
TODOIST_PROJECTS_GET_DATA_COMMAND = "/projects/get_data",
|
21
|
+
TODOIST_ITEMS_ADD_COMMAND = "/items/add",
|
22
|
+
TODOIST_ITEMS_GET_COMMAND = "/items/get",
|
23
|
+
TODOIST_QUICK_ADD_COMMAND = "/quick/add",
|
24
|
+
TODOIST_ACTIVITY_GET_COMMAND = "/activity/get",
|
25
|
+
TODOIST_BACKUPS_GET_COMMAND = "/backups/get",
|
26
|
+
TODOIST_USER_LOGIN_COMMAND = "/user/login"
|
27
|
+
]
|
28
|
+
|
29
|
+
# Map of commands to URIs
|
30
|
+
@@uri = nil
|
31
|
+
|
32
|
+
# User token
|
33
|
+
@@token = nil
|
34
|
+
|
35
|
+
# Artificial delay between requests to avoid API throttling
|
36
|
+
@@delay_between_requests = 0
|
37
|
+
|
38
|
+
# Should API throttling happen (HTTP Error 429), retry_time between requests
|
39
|
+
# with exponential backoff
|
40
|
+
@@retry_time = 20
|
41
|
+
|
42
|
+
def self.token=(token)
|
43
|
+
@@token = token
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.token
|
47
|
+
@@token
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.retry_time=(retry_time)
|
51
|
+
@@retry_time = retry_time
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.retry_time
|
55
|
+
@@retry_time
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.delay_between_requests=(delay_between_requests)
|
59
|
+
@@delay_between_requests = delay_between_requests
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.delay_between_requests
|
63
|
+
@@delay_between_requests
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.getURI
|
67
|
+
if @@uri == nil
|
68
|
+
@@uri = {}
|
69
|
+
@@command_list.each do |command|
|
70
|
+
@@uri[command] = URI.parse(TODOIST_API_URL + command)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
return @@uri
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require "net/http"
|
2
|
+
require "json"
|
3
|
+
require "todoist/util/config"
|
4
|
+
require 'net/http/post/multipart'
|
5
|
+
require 'mimemagic'
|
6
|
+
|
7
|
+
module Todoist
|
8
|
+
module Util
|
9
|
+
class NetworkHelper
|
10
|
+
|
11
|
+
@@last_request_time = 0.0
|
12
|
+
|
13
|
+
def self.configureHTTP(command)
|
14
|
+
http = Net::HTTP.new(Config.getURI()[command].host, Config.getURI()[command].port)
|
15
|
+
http.use_ssl = true
|
16
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
17
|
+
return http
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.configureRequest(command, params)
|
21
|
+
request = Net::HTTP::Post.new(Config.getURI()[command].request_uri)
|
22
|
+
request.set_form_data(params)
|
23
|
+
return request
|
24
|
+
end
|
25
|
+
|
26
|
+
# Files need to be of class UploadIO
|
27
|
+
|
28
|
+
def self.getMultipartResponse(command, params={})
|
29
|
+
token = {token: Todoist::Util::Config.token}
|
30
|
+
http = configureHTTP(command)
|
31
|
+
url = Config.getURI()[command]
|
32
|
+
http.start do
|
33
|
+
req = Net::HTTP::Post::Multipart.new(url, token.merge(params))
|
34
|
+
response = http.request(req)
|
35
|
+
begin
|
36
|
+
return JSON.parse(response.body)
|
37
|
+
rescue JSON::ParserError
|
38
|
+
return response.body
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.getResponse(command, params ={}, token = true)
|
44
|
+
token = token ? {token: Todoist::Util::Config.token} : {}
|
45
|
+
http = configureHTTP(command)
|
46
|
+
request = configureRequest(command, token.merge(params))
|
47
|
+
retry_after_secs = Todoist::Util::Config.retry_time
|
48
|
+
# Hack to fix encoding issues with Net:HTTP for login case
|
49
|
+
request.body = request.body.gsub '%40', '@' unless token
|
50
|
+
while true
|
51
|
+
response = throttle_request(http, request)
|
52
|
+
case response.code.to_i
|
53
|
+
when 200
|
54
|
+
begin
|
55
|
+
return JSON.parse(response.body)
|
56
|
+
rescue JSON::ParserError
|
57
|
+
return response.body
|
58
|
+
end
|
59
|
+
when 400
|
60
|
+
raise StandardError, "HTTP 400 Error - The request was incorrect."
|
61
|
+
when 401
|
62
|
+
raise StandardError, "HTTP 401 Error - Authentication is required, and has failed, or has not yet been provided."
|
63
|
+
when 403
|
64
|
+
raise StandardError, "HTTP 403 Error - The request was valid, but for something that is forbidden."
|
65
|
+
when 404
|
66
|
+
raise StandardError, "HTTP 404 Error - The requested resource could not be found."
|
67
|
+
when 429
|
68
|
+
puts("Encountered 429 - retry after #{retry_after_secs}")
|
69
|
+
sleep(retry_after_secs)
|
70
|
+
retry_after_secs *= Todoist::Util::Config.retry_time
|
71
|
+
when 500
|
72
|
+
raise StandardError, "HTTP 500 Error - The request failed due to a server error."
|
73
|
+
when 503
|
74
|
+
raise StandardError, "HTTP 503 Error - The server is currently unable to handle the request."
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.throttle_request(http, request)
|
81
|
+
time_since_last_request = Time.now.to_f - @@last_request_time
|
82
|
+
|
83
|
+
if (time_since_last_request < Todoist::Util::Config.delay_between_requests)
|
84
|
+
wait = Todoist::Util::Config.delay_between_requests - time_since_last_request
|
85
|
+
puts("Throttling request by: #{wait}")
|
86
|
+
sleep(wait)
|
87
|
+
end
|
88
|
+
@@last_request_time = Time.now.to_f
|
89
|
+
http.request(request)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Prepares a file for multipart upload
|
93
|
+
def self.multipart_file(file)
|
94
|
+
filename = File.basename(file)
|
95
|
+
mime_type = MimeMagic.by_path(filename).type
|
96
|
+
return UploadIO.new(file, mime_type, filename)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|