async-vikunja 1.0.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.
- checksums.yaml +7 -0
- data/README.md +362 -0
- data/lib/async/vikunja/admin.rb +97 -0
- data/lib/async/vikunja/assignees.rb +20 -0
- data/lib/async/vikunja/attachments.rb +51 -0
- data/lib/async/vikunja/auth.rb +52 -0
- data/lib/async/vikunja/bots.rb +43 -0
- data/lib/async/vikunja/buckets.rb +51 -0
- data/lib/async/vikunja/client.rb +76 -0
- data/lib/async/vikunja/comments.rb +31 -0
- data/lib/async/vikunja/error.rb +52 -0
- data/lib/async/vikunja/escape.rb +9 -0
- data/lib/async/vikunja/filters.rb +39 -0
- data/lib/async/vikunja/labels.rb +71 -0
- data/lib/async/vikunja/migration.rb +89 -0
- data/lib/async/vikunja/notifications.rb +45 -0
- data/lib/async/vikunja/page.rb +112 -0
- data/lib/async/vikunja/projects.rb +133 -0
- data/lib/async/vikunja/reactions.rb +19 -0
- data/lib/async/vikunja/redacted.rb +37 -0
- data/lib/async/vikunja/representation.rb +73 -0
- data/lib/async/vikunja/service.rb +137 -0
- data/lib/async/vikunja/sessions.rb +29 -0
- data/lib/async/vikunja/settings.rb +67 -0
- data/lib/async/vikunja/sharing.rb +131 -0
- data/lib/async/vikunja/submit.rb +97 -0
- data/lib/async/vikunja/subscriptions.rb +21 -0
- data/lib/async/vikunja/tasks.rb +207 -0
- data/lib/async/vikunja/teams.rb +85 -0
- data/lib/async/vikunja/time_entries.rb +55 -0
- data/lib/async/vikunja/tokens.rb +59 -0
- data/lib/async/vikunja/users.rb +107 -0
- data/lib/async/vikunja/version.rb +10 -0
- data/lib/async/vikunja/views.rb +75 -0
- data/lib/async/vikunja/webhooks.rb +55 -0
- data/lib/async/vikunja/wrapper.rb +75 -0
- data/lib/async/vikunja.rb +2 -0
- data/license.md +21 -0
- metadata +105 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Async
|
|
2
|
+
module Vikunja
|
|
3
|
+
module Redacted
|
|
4
|
+
SECRET = [
|
|
5
|
+
:access_token,
|
|
6
|
+
:basic_auth_password,
|
|
7
|
+
:client_secret,
|
|
8
|
+
:code_verifier,
|
|
9
|
+
:new_password,
|
|
10
|
+
:old_password,
|
|
11
|
+
:passcode,
|
|
12
|
+
:password,
|
|
13
|
+
:refresh_token,
|
|
14
|
+
:secret,
|
|
15
|
+
:token,
|
|
16
|
+
:totp_passcode,
|
|
17
|
+
].freeze
|
|
18
|
+
|
|
19
|
+
REDACTED = "[redacted]"
|
|
20
|
+
|
|
21
|
+
def self.redact(value)
|
|
22
|
+
case value
|
|
23
|
+
when Hash
|
|
24
|
+
value.to_h{|key, nested| SECRET.include?(key) ? [key, REDACTED] : [key, self.redact(nested)]}
|
|
25
|
+
when Array
|
|
26
|
+
value.map{|each| self.redact(each)}
|
|
27
|
+
else
|
|
28
|
+
value
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def inspect
|
|
33
|
+
"\#<" + self.class.to_s + " " + @resource.path.to_s.inspect + " value=" + Redacted.redact(@value).inspect + ">"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
require "async/rest/representation"
|
|
2
|
+
|
|
3
|
+
require_relative "escape"
|
|
4
|
+
require_relative "redacted"
|
|
5
|
+
require_relative "submit"
|
|
6
|
+
require_relative "wrapper"
|
|
7
|
+
|
|
8
|
+
module Async
|
|
9
|
+
module Vikunja
|
|
10
|
+
class Representation < Async::REST::Representation[Wrapper]
|
|
11
|
+
include Redacted
|
|
12
|
+
include Submit
|
|
13
|
+
|
|
14
|
+
def self.schema = self.name.to_s.split("::").last
|
|
15
|
+
|
|
16
|
+
def deconstruct_keys(keys)
|
|
17
|
+
case self.value
|
|
18
|
+
in Hash => value
|
|
19
|
+
value
|
|
20
|
+
else
|
|
21
|
+
{}
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def deconstruct
|
|
26
|
+
case self.value
|
|
27
|
+
in Array => value
|
|
28
|
+
value
|
|
29
|
+
else
|
|
30
|
+
[]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def cacheable?
|
|
35
|
+
cache_control = self.metadata["cache-control"]
|
|
36
|
+
|
|
37
|
+
if cache_control
|
|
38
|
+
!cache_control.no_store?
|
|
39
|
+
else
|
|
40
|
+
true
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def nested(klass, value)
|
|
45
|
+
if value
|
|
46
|
+
klass.new(@resource, value: value, metadata: self.metadata)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def nested_list(klass, values)
|
|
51
|
+
Array(values).map do |value|
|
|
52
|
+
self.nested(klass, value)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class PatchRepresentation < Representation
|
|
58
|
+
WRAPPER = MergePatch.new
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class UploadRepresentation < Representation
|
|
62
|
+
WRAPPER = Multipart.new
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
class Download < Representation
|
|
66
|
+
WRAPPER = Wrapper.new
|
|
67
|
+
|
|
68
|
+
def bytes = self.value
|
|
69
|
+
|
|
70
|
+
def media_type = self.metadata["content-type"]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
require_relative "page"
|
|
2
|
+
|
|
3
|
+
module Async
|
|
4
|
+
module Vikunja
|
|
5
|
+
class LocalAuthInfo < Representation
|
|
6
|
+
def enabled = self.value[:enabled]
|
|
7
|
+
|
|
8
|
+
def registration_enabled = self.value[:registration_enabled]
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class LdapAuthInfo < Representation
|
|
12
|
+
def enabled = self.value[:enabled]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class Provider < Representation
|
|
16
|
+
def auth_url = self.value[:auth_url]
|
|
17
|
+
|
|
18
|
+
def client_id = self.value[:client_id]
|
|
19
|
+
|
|
20
|
+
def email_fallback = self.value[:email_fallback]
|
|
21
|
+
|
|
22
|
+
def force_user_info = self.value[:force_user_info]
|
|
23
|
+
|
|
24
|
+
def key = self.value[:key]
|
|
25
|
+
|
|
26
|
+
def logout_url = self.value[:logout_url]
|
|
27
|
+
|
|
28
|
+
def name = self.value[:name]
|
|
29
|
+
|
|
30
|
+
def scope = self.value[:scope]
|
|
31
|
+
|
|
32
|
+
def username_fallback = self.value[:username_fallback]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class OpenIDAuthInfo < Representation
|
|
36
|
+
def enabled = self.value[:enabled]
|
|
37
|
+
|
|
38
|
+
def providers = self.nested_list(Provider, self.value[:providers])
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class AuthInfo < Representation
|
|
42
|
+
def ldap = self.nested(LdapAuthInfo, self.value[:ldap])
|
|
43
|
+
|
|
44
|
+
def local = self.nested(LocalAuthInfo, self.value[:local])
|
|
45
|
+
|
|
46
|
+
def openid_connect = self.nested(OpenIDAuthInfo, self.value[:openid_connect])
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class LegalInfo < Representation
|
|
50
|
+
def imprint_url = self.value[:imprint_url]
|
|
51
|
+
|
|
52
|
+
def privacy_policy_url = self.value[:privacy_policy_url]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
class VikunjaInfos < Representation
|
|
56
|
+
def allow_icon_changes = self.value[:allow_icon_changes]
|
|
57
|
+
|
|
58
|
+
def auth = self.nested(AuthInfo, self.value[:auth])
|
|
59
|
+
|
|
60
|
+
def available_migrators = self.value[:available_migrators]
|
|
61
|
+
|
|
62
|
+
def caldav_enabled = self.value[:caldav_enabled]
|
|
63
|
+
|
|
64
|
+
def concurrent_writes = self.value[:concurrent_writes]
|
|
65
|
+
|
|
66
|
+
def demo_mode_enabled = self.value[:demo_mode_enabled]
|
|
67
|
+
|
|
68
|
+
def email_reminders_enabled = self.value[:email_reminders_enabled]
|
|
69
|
+
|
|
70
|
+
def enabled_background_providers = self.value[:enabled_background_providers]
|
|
71
|
+
|
|
72
|
+
def enabled_pro_features = self.value[:enabled_pro_features]
|
|
73
|
+
|
|
74
|
+
def frontend_url = self.value[:frontend_url]
|
|
75
|
+
|
|
76
|
+
def legal = self.nested(LegalInfo, self.value[:legal])
|
|
77
|
+
|
|
78
|
+
def link_sharing_enabled = self.value[:link_sharing_enabled]
|
|
79
|
+
|
|
80
|
+
def max_file_size = self.value[:max_file_size]
|
|
81
|
+
|
|
82
|
+
def max_items_per_page = self.value[:max_items_per_page]
|
|
83
|
+
|
|
84
|
+
def motd = self.value[:motd]
|
|
85
|
+
|
|
86
|
+
def public_teams_enabled = self.value[:public_teams_enabled]
|
|
87
|
+
|
|
88
|
+
def task_attachments_enabled = self.value[:task_attachments_enabled]
|
|
89
|
+
|
|
90
|
+
def task_comments_enabled = self.value[:task_comments_enabled]
|
|
91
|
+
|
|
92
|
+
def totp_enabled = self.value[:totp_enabled]
|
|
93
|
+
|
|
94
|
+
def user_deletion_enabled = self.value[:user_deletion_enabled]
|
|
95
|
+
|
|
96
|
+
def version = self.value[:version]
|
|
97
|
+
|
|
98
|
+
def webhooks_enabled = self.value[:webhooks_enabled]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
class ProviderStatus < Representation
|
|
102
|
+
def available = self.value[:available]
|
|
103
|
+
|
|
104
|
+
def key = self.value[:key]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
class Health < Representation
|
|
108
|
+
def self.schema = "HealthBodyBody"
|
|
109
|
+
|
|
110
|
+
def openid_providers = self.nested_list(ProviderStatus, self.value[:openid_providers])
|
|
111
|
+
|
|
112
|
+
def status = self.value[:status]
|
|
113
|
+
|
|
114
|
+
def ok? = self.status == "OK"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
class RouteDetail < Representation
|
|
118
|
+
def method = self.value[:method]
|
|
119
|
+
|
|
120
|
+
def path = self.value[:path]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
class Message < Representation
|
|
124
|
+
def message = self.value[:message]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
module Service
|
|
128
|
+
def info = VikunjaInfos.new(self.with(path: "info"))
|
|
129
|
+
|
|
130
|
+
def health = Health.new(self.with(path: "health"))
|
|
131
|
+
|
|
132
|
+
def routes = Representation.new(self.with(path: "routes"))
|
|
133
|
+
|
|
134
|
+
def notifications_atom = Download.new(self.with(path: "notifications.atom"))
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require_relative "page"
|
|
2
|
+
|
|
3
|
+
module Async
|
|
4
|
+
module Vikunja
|
|
5
|
+
class Session < Representation
|
|
6
|
+
def id = self.value[:id]
|
|
7
|
+
|
|
8
|
+
def created = self.value[:created]
|
|
9
|
+
|
|
10
|
+
def device_info = self.value[:device_info]
|
|
11
|
+
|
|
12
|
+
def ip_address = self.value[:ip_address]
|
|
13
|
+
|
|
14
|
+
def last_active = self.value[:last_active]
|
|
15
|
+
|
|
16
|
+
def refresh_token = self.value[:refresh_token]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class SessionPage < Page
|
|
20
|
+
MEMBER = Session
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
module Sessions
|
|
24
|
+
def sessions(**parameters) = Collection.for(self, "user/sessions", SessionPage, **parameters)
|
|
25
|
+
|
|
26
|
+
def delete_session(id) = self.discard(self.with(path: "user/sessions/" + Vikunja.escape(id)))
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
require_relative "page"
|
|
2
|
+
|
|
3
|
+
module Async
|
|
4
|
+
module Vikunja
|
|
5
|
+
class UserGeneralSettings < Representation
|
|
6
|
+
def default_project_id = self.value[:default_project_id]
|
|
7
|
+
|
|
8
|
+
def discoverable_by_email = self.value[:discoverable_by_email]
|
|
9
|
+
|
|
10
|
+
def discoverable_by_name = self.value[:discoverable_by_name]
|
|
11
|
+
|
|
12
|
+
def email_reminders_enabled = self.value[:email_reminders_enabled]
|
|
13
|
+
|
|
14
|
+
def extra_settings_links = self.value[:extra_settings_links]
|
|
15
|
+
|
|
16
|
+
def frontend_settings = self.value[:frontend_settings]
|
|
17
|
+
|
|
18
|
+
def language = self.value[:language]
|
|
19
|
+
|
|
20
|
+
def name = self.value[:name]
|
|
21
|
+
|
|
22
|
+
def overdue_tasks_reminders_enabled = self.value[:overdue_tasks_reminders_enabled]
|
|
23
|
+
|
|
24
|
+
def overdue_tasks_reminders_time = self.value[:overdue_tasks_reminders_time]
|
|
25
|
+
|
|
26
|
+
def timezone = self.value[:timezone]
|
|
27
|
+
|
|
28
|
+
def week_start = self.value[:week_start]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class UserAvatarProviderBody < Representation
|
|
32
|
+
def avatar_provider = self.value[:avatar_provider]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class TOTP < Representation
|
|
36
|
+
def enabled = self.value[:enabled]
|
|
37
|
+
|
|
38
|
+
def secret = self.value[:secret]
|
|
39
|
+
|
|
40
|
+
def url = self.value[:url]
|
|
41
|
+
|
|
42
|
+
def enabled? = self.enabled == true
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
module Settings
|
|
46
|
+
def update_settings(payload) = self.submit(UserActionMessageBody, self.with(path: "user/settings/general"), payload, method: :put)
|
|
47
|
+
|
|
48
|
+
def avatar_provider = UserAvatarProviderBody.new(self.with(path: "user/settings/avatar/provider"))
|
|
49
|
+
|
|
50
|
+
def set_avatar_provider(avatar_provider:) = self.submit(UserAvatarProviderBody, self.with(path: "user/settings/avatar/provider"), {avatar_provider: avatar_provider}, method: :put)
|
|
51
|
+
|
|
52
|
+
def patch_avatar_provider(payload) = self.merge_patch(UserAvatarProviderBody, self.with(path: "user/settings/avatar/provider"), payload)
|
|
53
|
+
|
|
54
|
+
def upload_avatar(part) = self.upload(Message, self.with(path: "user/settings/avatar"), [part], method: :put)
|
|
55
|
+
|
|
56
|
+
def totp = TOTP.new(self.with(path: "user/settings/totp"))
|
|
57
|
+
|
|
58
|
+
def enroll_totp = self.submit(TOTP, self.with(path: "user/settings/totp/enroll"), nil)
|
|
59
|
+
|
|
60
|
+
def enable_totp(passcode:) = self.submit(Message, self.with(path: "user/settings/totp/enable"), {passcode: passcode})
|
|
61
|
+
|
|
62
|
+
def disable_totp(password:) = self.submit(Message, self.with(path: "user/settings/totp/disable"), {password: password})
|
|
63
|
+
|
|
64
|
+
def totp_qrcode = Download.new(self.with(path: "user/settings/totp/qrcode"))
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
require_relative "page"
|
|
2
|
+
|
|
3
|
+
module Async
|
|
4
|
+
module Vikunja
|
|
5
|
+
class LinkSharing < Representation
|
|
6
|
+
def id = self.value[:id]
|
|
7
|
+
|
|
8
|
+
def name = self.value[:name]
|
|
9
|
+
|
|
10
|
+
def hash = self.value[:hash]
|
|
11
|
+
|
|
12
|
+
def password = self.value[:password]
|
|
13
|
+
|
|
14
|
+
def permission = self.value[:permission]
|
|
15
|
+
|
|
16
|
+
def sharing_type = self.value[:sharing_type]
|
|
17
|
+
|
|
18
|
+
def created = self.value[:created]
|
|
19
|
+
|
|
20
|
+
def updated = self.value[:updated]
|
|
21
|
+
|
|
22
|
+
def shared_by = self.nested(User, self.value[:shared_by])
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class LinkShareToken < Representation
|
|
26
|
+
def id = self.value[:id]
|
|
27
|
+
|
|
28
|
+
def name = self.value[:name]
|
|
29
|
+
|
|
30
|
+
def hash = self.value[:hash]
|
|
31
|
+
|
|
32
|
+
def password = self.value[:password]
|
|
33
|
+
|
|
34
|
+
def permission = self.value[:permission]
|
|
35
|
+
|
|
36
|
+
def project_id = self.value[:project_id]
|
|
37
|
+
|
|
38
|
+
def sharing_type = self.value[:sharing_type]
|
|
39
|
+
|
|
40
|
+
def token = self.value[:token]
|
|
41
|
+
|
|
42
|
+
def created = self.value[:created]
|
|
43
|
+
|
|
44
|
+
def updated = self.value[:updated]
|
|
45
|
+
|
|
46
|
+
def shared_by = self.nested(User, self.value[:shared_by])
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class ProjectUser < Representation
|
|
50
|
+
def id = self.value[:id]
|
|
51
|
+
|
|
52
|
+
def username = self.value[:username]
|
|
53
|
+
|
|
54
|
+
def permission = self.value[:permission]
|
|
55
|
+
|
|
56
|
+
def created = self.value[:created]
|
|
57
|
+
|
|
58
|
+
def updated = self.value[:updated]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class TeamProject < Representation
|
|
62
|
+
def id = self.value[:id]
|
|
63
|
+
|
|
64
|
+
def team_id = self.value[:team_id]
|
|
65
|
+
|
|
66
|
+
def permission = self.value[:permission]
|
|
67
|
+
|
|
68
|
+
def created = self.value[:created]
|
|
69
|
+
|
|
70
|
+
def updated = self.value[:updated]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
class TeamWithPermission < Representation
|
|
74
|
+
def id = self.value[:id]
|
|
75
|
+
|
|
76
|
+
def name = self.value[:name]
|
|
77
|
+
|
|
78
|
+
def description = self.value[:description]
|
|
79
|
+
|
|
80
|
+
def is_public = self.value[:is_public]
|
|
81
|
+
|
|
82
|
+
def external_id = self.value[:external_id]
|
|
83
|
+
|
|
84
|
+
def permission = self.value[:permission]
|
|
85
|
+
|
|
86
|
+
def created = self.value[:created]
|
|
87
|
+
|
|
88
|
+
def updated = self.value[:updated]
|
|
89
|
+
|
|
90
|
+
def created_by = self.nested(User, self.value[:created_by])
|
|
91
|
+
|
|
92
|
+
def members = self.nested_list(TeamUser, self.value[:members])
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
class UserWithPermission < Representation
|
|
96
|
+
def id = self.value[:id]
|
|
97
|
+
|
|
98
|
+
def username = self.value[:username]
|
|
99
|
+
|
|
100
|
+
def name = self.value[:name]
|
|
101
|
+
|
|
102
|
+
def email = self.value[:email]
|
|
103
|
+
|
|
104
|
+
def permission = self.value[:permission]
|
|
105
|
+
|
|
106
|
+
def bot_owner_id = self.value[:bot_owner_id]
|
|
107
|
+
|
|
108
|
+
def created = self.value[:created]
|
|
109
|
+
|
|
110
|
+
def updated = self.value[:updated]
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
class LinkSharingPage < Page
|
|
114
|
+
MEMBER = LinkSharing
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
class TeamWithPermissionPage < Page
|
|
118
|
+
MEMBER = TeamWithPermission
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
class ProjectUserPage < Page
|
|
122
|
+
MEMBER = UserWithPermission
|
|
123
|
+
|
|
124
|
+
def member_path(record) = Vikunja.escape(record[:username])
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
module Sharing
|
|
128
|
+
def authenticate_share(share, payload = {}) = self.submit(LinkShareToken, self.with(path: "shares/" + Vikunja.escape(share) + "/auth"), payload)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
require_relative "error"
|
|
2
|
+
|
|
3
|
+
module Async
|
|
4
|
+
module Vikunja
|
|
5
|
+
module Submit
|
|
6
|
+
TOO_MANY_REQUESTS = 429
|
|
7
|
+
|
|
8
|
+
MAXIMUM_ATTEMPTS = 4
|
|
9
|
+
|
|
10
|
+
MAXIMUM_DELAY = 30.0
|
|
11
|
+
|
|
12
|
+
def submit(klass, resource, payload = nil, method: :post, through: nil, identify: nil)
|
|
13
|
+
carrier = through || klass
|
|
14
|
+
|
|
15
|
+
self.with_retry do
|
|
16
|
+
carrier.public_send(method, resource, payload) do |target, response|
|
|
17
|
+
if response.success?
|
|
18
|
+
self.representation_of(klass, target, response, identify)
|
|
19
|
+
else
|
|
20
|
+
raise Failure.for(response)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def representation_of(klass, resource, response, identify)
|
|
27
|
+
value = response.read
|
|
28
|
+
|
|
29
|
+
klass.new(self.identified(resource, identify, value), value: value, metadata: response.headers)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def identified(resource, identify, value)
|
|
33
|
+
if identify.nil? || !value.is_a?(Hash) || value[:id].nil?
|
|
34
|
+
resource
|
|
35
|
+
else
|
|
36
|
+
resource.with(path: self.prefix_of(identify) + Vikunja.escape(value[:id]), parameters: nil, merge: false)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def prefix_of(identify)
|
|
41
|
+
if identify == true
|
|
42
|
+
""
|
|
43
|
+
else
|
|
44
|
+
identify
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def merge_patch(klass, resource, payload)
|
|
49
|
+
self.submit(klass, resource, payload, method: :patch, through: PatchRepresentation)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def upload(klass, resource, parts, method: :post)
|
|
53
|
+
self.submit(klass, resource, parts, method: method, through: UploadRepresentation)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def discard(resource, payload = nil, method: :delete)
|
|
57
|
+
self.with_retry do
|
|
58
|
+
Representation.public_send(method, resource, payload) do |target, response|
|
|
59
|
+
if response.success?
|
|
60
|
+
true
|
|
61
|
+
else
|
|
62
|
+
raise Failure.for(response)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def with_retry(attempts: MAXIMUM_ATTEMPTS, &block)
|
|
69
|
+
attempt = 1
|
|
70
|
+
|
|
71
|
+
while true
|
|
72
|
+
begin
|
|
73
|
+
break block.call
|
|
74
|
+
rescue Failure => failure
|
|
75
|
+
if attempt >= attempts
|
|
76
|
+
raise
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
unless failure.status == TOO_MANY_REQUESTS
|
|
80
|
+
raise
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
delay = failure.retry_after.to_f
|
|
84
|
+
|
|
85
|
+
if delay <= 0.0 || delay > MAXIMUM_DELAY
|
|
86
|
+
raise
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
attempt += 1
|
|
90
|
+
|
|
91
|
+
sleep(delay)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require_relative "page"
|
|
2
|
+
|
|
3
|
+
module Async
|
|
4
|
+
module Vikunja
|
|
5
|
+
class Subscription < Representation
|
|
6
|
+
def created = self.value[:created]
|
|
7
|
+
|
|
8
|
+
def entity = self.value[:entity]
|
|
9
|
+
|
|
10
|
+
def entity_id = self.value[:entity_id]
|
|
11
|
+
|
|
12
|
+
def id = self.value[:id]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module Subscriptions
|
|
16
|
+
def subscribe(entity, entity_id) = self.submit(Subscription, self.with(path: "subscriptions/" + Vikunja.escape(entity) + "/" + Vikunja.escape(entity_id)))
|
|
17
|
+
|
|
18
|
+
def unsubscribe(entity, entity_id) = self.discard(self.with(path: "subscriptions/" + Vikunja.escape(entity) + "/" + Vikunja.escape(entity_id)))
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|