todoist-ruby 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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