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.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +362 -0
  3. data/lib/async/vikunja/admin.rb +97 -0
  4. data/lib/async/vikunja/assignees.rb +20 -0
  5. data/lib/async/vikunja/attachments.rb +51 -0
  6. data/lib/async/vikunja/auth.rb +52 -0
  7. data/lib/async/vikunja/bots.rb +43 -0
  8. data/lib/async/vikunja/buckets.rb +51 -0
  9. data/lib/async/vikunja/client.rb +76 -0
  10. data/lib/async/vikunja/comments.rb +31 -0
  11. data/lib/async/vikunja/error.rb +52 -0
  12. data/lib/async/vikunja/escape.rb +9 -0
  13. data/lib/async/vikunja/filters.rb +39 -0
  14. data/lib/async/vikunja/labels.rb +71 -0
  15. data/lib/async/vikunja/migration.rb +89 -0
  16. data/lib/async/vikunja/notifications.rb +45 -0
  17. data/lib/async/vikunja/page.rb +112 -0
  18. data/lib/async/vikunja/projects.rb +133 -0
  19. data/lib/async/vikunja/reactions.rb +19 -0
  20. data/lib/async/vikunja/redacted.rb +37 -0
  21. data/lib/async/vikunja/representation.rb +73 -0
  22. data/lib/async/vikunja/service.rb +137 -0
  23. data/lib/async/vikunja/sessions.rb +29 -0
  24. data/lib/async/vikunja/settings.rb +67 -0
  25. data/lib/async/vikunja/sharing.rb +131 -0
  26. data/lib/async/vikunja/submit.rb +97 -0
  27. data/lib/async/vikunja/subscriptions.rb +21 -0
  28. data/lib/async/vikunja/tasks.rb +207 -0
  29. data/lib/async/vikunja/teams.rb +85 -0
  30. data/lib/async/vikunja/time_entries.rb +55 -0
  31. data/lib/async/vikunja/tokens.rb +59 -0
  32. data/lib/async/vikunja/users.rb +107 -0
  33. data/lib/async/vikunja/version.rb +10 -0
  34. data/lib/async/vikunja/views.rb +75 -0
  35. data/lib/async/vikunja/webhooks.rb +55 -0
  36. data/lib/async/vikunja/wrapper.rb +75 -0
  37. data/lib/async/vikunja.rb +2 -0
  38. data/license.md +21 -0
  39. 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