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.
@@ -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