listmonk 0.1.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/CHANGELOG.md +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +175 -0
- data/lib/listmonk/client.rb +145 -0
- data/lib/listmonk/errors.rb +45 -0
- data/lib/listmonk/resource.rb +33 -0
- data/lib/listmonk/resources/admin.rb +9 -0
- data/lib/listmonk/resources/bounces.rb +15 -0
- data/lib/listmonk/resources/campaigns.rb +33 -0
- data/lib/listmonk/resources/imports.rb +22 -0
- data/lib/listmonk/resources/lists.rb +13 -0
- data/lib/listmonk/resources/logs.rb +9 -0
- data/lib/listmonk/resources/maintenance.rb +11 -0
- data/lib/listmonk/resources/media.rb +17 -0
- data/lib/listmonk/resources/miscellaneous.rb +13 -0
- data/lib/listmonk/resources/public.rb +14 -0
- data/lib/listmonk/resources/settings.rb +11 -0
- data/lib/listmonk/resources/subscribers.rb +33 -0
- data/lib/listmonk/resources/templates.rb +16 -0
- data/lib/listmonk/resources/transactional.rb +9 -0
- data/lib/listmonk/response.rb +23 -0
- data/lib/listmonk/version.rb +5 -0
- data/lib/listmonk.rb +14 -0
- data/sig/listmonk.rbs +64 -0
- metadata +201 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 220f8532f40722ac0b228e586c8a3f8ecc765153783fb1aaa7195663f2f29e48
|
|
4
|
+
data.tar.gz: 145a0d505fce70f1527992b7aaab70c9b409752b740d710679f325d0f8454f50
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 67792a4fc6008979036ecef64510cf56def26c26d824cc1d98fa33de6914283881410288fc0cc8ef41da5d9e2ffa5486c6f7bedab0e16c1b2693afaae5e10348
|
|
7
|
+
data.tar.gz: 67c86af4e3c7322cc336183af6dcefd38ce94c9c5452117c628b0885c716ee5c3ad722836d5782b36693fbdb873da148d388d10167823b806a69a04b16c24db3
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
- Add a resource-oriented client covering all operations in listmonk's published Swagger collection.
|
|
6
|
+
- Support Basic and listmonk token-header authentication.
|
|
7
|
+
- Add JSON response envelopes, typed HTTP errors, multipart uploads, and repeated query parameters.
|
|
8
|
+
- Add an opt-in Docker integration suite against Listmonk 6.1.0 and PostgreSQL 17.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TODO: Write your name
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Listmonk
|
|
2
|
+
|
|
3
|
+
A resource-oriented Ruby client for the [listmonk HTTP API](https://listmonk.app/docs/apis/apis/).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add the gem to your bundle:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bundle add listmonk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The gem requires Ruby 3.3 or newer.
|
|
14
|
+
|
|
15
|
+
## Configuration
|
|
16
|
+
|
|
17
|
+
Listmonk supports Basic authentication and its `Authorization: token` header. Basic authentication is the default:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
client = Listmonk::Client.new(
|
|
21
|
+
base_url: "https://newsletter.example.com",
|
|
22
|
+
username: ENV.fetch("LISTMONK_USERNAME"),
|
|
23
|
+
token: ENV.fetch("LISTMONK_TOKEN")
|
|
24
|
+
)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Use token-header authentication when needed:
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
client = Listmonk.client(
|
|
31
|
+
base_url: "https://newsletter.example.com/api",
|
|
32
|
+
username: ENV.fetch("LISTMONK_USERNAME"),
|
|
33
|
+
token: ENV.fetch("LISTMONK_TOKEN"),
|
|
34
|
+
auth: :token,
|
|
35
|
+
timeout: 30,
|
|
36
|
+
open_timeout: 10
|
|
37
|
+
)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The client accepts a server root or an URL ending in `/api`. Credentials may be omitted for public endpoints.
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
Every method returns a `Listmonk::Response`. Use `data` for the API payload, or inspect `status`, `headers`, and `body` when needed.
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
response = client.subscribers.list(page: 1, per_page: 50, list_id: [1, 2])
|
|
48
|
+
subscribers = response.data.fetch("results")
|
|
49
|
+
|
|
50
|
+
subscriber = client.subscribers.create(
|
|
51
|
+
email: "reader@example.com",
|
|
52
|
+
name: "A Reader",
|
|
53
|
+
lists: [1],
|
|
54
|
+
attribs: {source: "website"}
|
|
55
|
+
).data
|
|
56
|
+
|
|
57
|
+
client.subscribers.update(subscriber.fetch("id"), name: "Reader")
|
|
58
|
+
client.subscribers.send_optin(subscriber.fetch("id"))
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Campaigns and transactional messages:
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
campaign = client.campaigns.create(
|
|
65
|
+
name: "July news",
|
|
66
|
+
subject: "What's new",
|
|
67
|
+
lists: [1],
|
|
68
|
+
from_email: "News <news@example.com>",
|
|
69
|
+
content_type: "html",
|
|
70
|
+
messenger: "email",
|
|
71
|
+
type: "regular"
|
|
72
|
+
).data
|
|
73
|
+
|
|
74
|
+
client.campaigns.create_content(campaign.fetch("id"), body: "<h1>Hello</h1>")
|
|
75
|
+
client.campaigns.start(campaign.fetch("id"))
|
|
76
|
+
|
|
77
|
+
client.transactional.deliver(
|
|
78
|
+
template_id: 3,
|
|
79
|
+
subscriber_email: "reader@example.com",
|
|
80
|
+
data: {confirmation_url: "https://example.com/confirm"}
|
|
81
|
+
)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Subscriber imports and media uploads use multipart requests:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
client.imports.create(
|
|
88
|
+
file: "/tmp/subscribers.zip",
|
|
89
|
+
params: {
|
|
90
|
+
mode: "subscribe",
|
|
91
|
+
subscription_status: "confirmed",
|
|
92
|
+
lists: [1],
|
|
93
|
+
overwrite: true
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
client.media.upload(file: "/tmp/banner.png", content_type: "image/png")
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Resources cover all operations in the published Swagger collection:
|
|
101
|
+
|
|
102
|
+
- `miscellaneous`, `settings`, `admin`, and `logs`
|
|
103
|
+
- `subscribers`, `lists`, `imports`, and `bounces`
|
|
104
|
+
- `campaigns`, `templates`, `media`, and `transactional`
|
|
105
|
+
- `maintenance` and `public`
|
|
106
|
+
|
|
107
|
+
For an undocumented or newly added endpoint, use the low-level request method:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
client.request(:get, "new-endpoint", params: {page: 1})
|
|
111
|
+
client.request(:post, "new-endpoint", json: {enabled: true})
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Errors
|
|
115
|
+
|
|
116
|
+
Non-successful responses raise typed exceptions. Every HTTP exception exposes `status`, `body`, and `response`.
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
begin
|
|
120
|
+
client.lists.retrieve(999)
|
|
121
|
+
rescue Listmonk::NotFoundError => error
|
|
122
|
+
warn "List not found: #{error.body.inspect}"
|
|
123
|
+
rescue Listmonk::RateLimitError
|
|
124
|
+
# Retry with application-specific backoff.
|
|
125
|
+
end
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Connection failures raise `Listmonk::ConnectionError`, timeouts raise `Listmonk::TimeoutError`, and malformed JSON raises `Listmonk::ParseError`.
|
|
129
|
+
|
|
130
|
+
## Development
|
|
131
|
+
|
|
132
|
+
Install dependencies and run the complete verification task:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
bin/setup
|
|
136
|
+
bundle exec rake
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
RSpec uses WebMock for request-level tests and SimpleCov for coverage.
|
|
140
|
+
|
|
141
|
+
### Docker integration tests
|
|
142
|
+
|
|
143
|
+
Run the opt-in integration suite against an ephemeral Listmonk 6.1.0 and PostgreSQL 17 environment:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
bundle exec rake integration
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
The task starts Docker Compose on `127.0.0.1:19000`, creates a temporary API user, runs live list, subscriber, template, authentication, and health checks, then removes the containers and database volume. Override the defaults with `LISTMONK_PORT` or `LISTMONK_IMAGE`.
|
|
150
|
+
|
|
151
|
+
Normal `bundle exec rake` runs only the WebMock suite and does not require Docker.
|
|
152
|
+
|
|
153
|
+
## Releasing
|
|
154
|
+
|
|
155
|
+
Publishing uses [RubyGems trusted publishing](https://guides.rubygems.org/trusted-publishing/), so no long-lived RubyGems API key is stored in GitHub.
|
|
156
|
+
|
|
157
|
+
Before the first release, create a pending trusted publisher in your RubyGems.org profile with:
|
|
158
|
+
|
|
159
|
+
- Gem name: `listmonk`
|
|
160
|
+
- Repository owner: `polydice`
|
|
161
|
+
- Repository name: `listmonk-ruby`
|
|
162
|
+
- Workflow filename: `release.yml`
|
|
163
|
+
- Environment: `release`
|
|
164
|
+
|
|
165
|
+
To publish a version:
|
|
166
|
+
|
|
167
|
+
1. Update `lib/listmonk/version.rb` and `CHANGELOG.md`.
|
|
168
|
+
2. Run `bundle exec rake`, `bundle exec rake integration`, and `bundle exec rbs validate`.
|
|
169
|
+
3. Run `bundle exec rake build` and inspect the generated package under `pkg/`.
|
|
170
|
+
4. Commit the release and create a matching version tag, for example `git tag v0.1.0`.
|
|
171
|
+
5. Push `main` and the tag. The `Publish gem` GitHub workflow validates that the tag matches `Listmonk::VERSION` and publishes the gem.
|
|
172
|
+
|
|
173
|
+
## License
|
|
174
|
+
|
|
175
|
+
The gem is available under the terms of the [MIT License](LICENSE.txt).
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "faraday"
|
|
6
|
+
require "faraday/multipart"
|
|
7
|
+
require_relative "resource"
|
|
8
|
+
|
|
9
|
+
module Listmonk
|
|
10
|
+
class Client
|
|
11
|
+
RESOURCE_NAMES = %i[
|
|
12
|
+
miscellaneous settings admin logs subscribers bounces lists imports campaigns
|
|
13
|
+
media templates transactional maintenance public
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
attr_reader :base_url
|
|
17
|
+
|
|
18
|
+
def initialize(base_url:, username: nil, token: nil, auth: :basic, timeout: 30, open_timeout: 10,
|
|
19
|
+
connection: nil)
|
|
20
|
+
@base_url = normalize_base_url(base_url)
|
|
21
|
+
@connection = connection || build_connection(username, token, auth, timeout, open_timeout)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
RESOURCE_NAMES.each do |name|
|
|
25
|
+
define_method(name) do
|
|
26
|
+
instance_variable_get("@#{name}") || instance_variable_set(
|
|
27
|
+
"@#{name}",
|
|
28
|
+
resource_class(name).new(self)
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def request(method, path, params: nil, json: nil, body: nil, headers: {})
|
|
34
|
+
raw = @connection.run_request(method, clean_path(path), nil, headers) do |request|
|
|
35
|
+
request.params.update(compact(params)) if params
|
|
36
|
+
if json.nil?
|
|
37
|
+
request.body = body
|
|
38
|
+
else
|
|
39
|
+
request.headers["Content-Type"] = "application/json"
|
|
40
|
+
request.body = JSON.generate(json)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
response = Response.new(
|
|
45
|
+
status: raw.status,
|
|
46
|
+
headers: raw.headers.to_h,
|
|
47
|
+
body: parse_body(raw.body, raw.headers["content-type"])
|
|
48
|
+
)
|
|
49
|
+
raise_http_error(response) unless response.success?
|
|
50
|
+
|
|
51
|
+
response
|
|
52
|
+
rescue Faraday::TimeoutError => e
|
|
53
|
+
raise TimeoutError, e.message
|
|
54
|
+
rescue Faraday::ConnectionFailed => e
|
|
55
|
+
raise ConnectionError, e.message
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def file_part(file, content_type: "application/octet-stream", filename: nil)
|
|
59
|
+
return file if file.is_a?(Faraday::Multipart::FilePart)
|
|
60
|
+
|
|
61
|
+
filename ||= File.basename(file.respond_to?(:path) ? file.path : file.to_s)
|
|
62
|
+
Faraday::Multipart::FilePart.new(file, content_type, filename)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def build_connection(username, token, auth, timeout, open_timeout)
|
|
68
|
+
validate_credentials!(username, token)
|
|
69
|
+
Faraday.new(
|
|
70
|
+
url: base_url,
|
|
71
|
+
request: {
|
|
72
|
+
timeout: timeout,
|
|
73
|
+
open_timeout: open_timeout,
|
|
74
|
+
params_encoder: Faraday::FlatParamsEncoder
|
|
75
|
+
}
|
|
76
|
+
) do |faraday|
|
|
77
|
+
faraday.request :multipart
|
|
78
|
+
faraday.request :url_encoded
|
|
79
|
+
faraday.headers["Accept"] = "application/json"
|
|
80
|
+
faraday.headers["User-Agent"] = "listmonk-ruby/#{VERSION}"
|
|
81
|
+
apply_auth(faraday, username, token, auth) if username && token
|
|
82
|
+
faraday.adapter Faraday.default_adapter
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def apply_auth(faraday, username, token, auth)
|
|
87
|
+
case auth.to_sym
|
|
88
|
+
when :basic
|
|
89
|
+
credentials = ["#{username}:#{token}"].pack("m0")
|
|
90
|
+
faraday.headers["Authorization"] = "Basic #{credentials}"
|
|
91
|
+
when :token
|
|
92
|
+
faraday.headers["Authorization"] = "token #{username}:#{token}"
|
|
93
|
+
else
|
|
94
|
+
raise ConfigurationError, "auth must be :basic or :token"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def validate_credentials!(username, token)
|
|
99
|
+
return if username.nil? == token.nil?
|
|
100
|
+
|
|
101
|
+
raise ConfigurationError, "username and token must be provided together"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def normalize_base_url(value)
|
|
105
|
+
uri = URI.parse(value.to_s)
|
|
106
|
+
valid = %w[http https].include?(uri.scheme) && uri.host
|
|
107
|
+
raise ConfigurationError, "base_url must be an absolute HTTP(S) URL" unless valid
|
|
108
|
+
|
|
109
|
+
uri.path = uri.path.sub(%r{/+\z}, "")
|
|
110
|
+
uri.path = "#{uri.path}/api" unless uri.path.end_with?("/api")
|
|
111
|
+
uri.to_s
|
|
112
|
+
rescue URI::InvalidURIError
|
|
113
|
+
raise ConfigurationError, "base_url must be an absolute HTTP(S) URL"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def clean_path(path)
|
|
117
|
+
path.to_s.sub(%r{\A/+}, "")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def compact(hash)
|
|
121
|
+
hash.compact
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def parse_body(body, content_type)
|
|
125
|
+
return nil if body.nil? || body.empty?
|
|
126
|
+
return body unless content_type.to_s.include?("json") || body.lstrip.start_with?("{", "[")
|
|
127
|
+
|
|
128
|
+
JSON.parse(body)
|
|
129
|
+
rescue JSON::ParserError => e
|
|
130
|
+
raise ParseError, "Listmonk returned invalid JSON: #{e.message}"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def raise_http_error(response)
|
|
134
|
+
klass = HTTP_ERRORS.fetch(response.status, response.status >= 500 ? ServerError : Error)
|
|
135
|
+
message = (response.body["message"] || response.body["error"] if response.body.is_a?(Hash))
|
|
136
|
+
message ||= "Listmonk request failed with HTTP #{response.status}"
|
|
137
|
+
raise klass.new(message, response: response)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def resource_class(name)
|
|
141
|
+
require_relative "resources/#{name}"
|
|
142
|
+
Listmonk::Resources.const_get(name.to_s.capitalize)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Listmonk
|
|
4
|
+
class Error < StandardError
|
|
5
|
+
attr_reader :response
|
|
6
|
+
|
|
7
|
+
def initialize(message = nil, response: nil)
|
|
8
|
+
@response = response
|
|
9
|
+
super(message)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def status
|
|
13
|
+
response&.status
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def body
|
|
17
|
+
response&.body
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class ConfigurationError < Error; end
|
|
22
|
+
class ConnectionError < Error; end
|
|
23
|
+
class TimeoutError < ConnectionError; end
|
|
24
|
+
class ParseError < Error; end
|
|
25
|
+
class BadRequestError < Error; end
|
|
26
|
+
class AuthenticationError < Error; end
|
|
27
|
+
class ForbiddenError < Error; end
|
|
28
|
+
class NotFoundError < Error; end
|
|
29
|
+
class MethodNotAllowedError < Error; end
|
|
30
|
+
class GoneError < Error; end
|
|
31
|
+
class ValidationError < Error; end
|
|
32
|
+
class RateLimitError < Error; end
|
|
33
|
+
class ServerError < Error; end
|
|
34
|
+
|
|
35
|
+
HTTP_ERRORS = {
|
|
36
|
+
400 => BadRequestError,
|
|
37
|
+
401 => AuthenticationError,
|
|
38
|
+
403 => ForbiddenError,
|
|
39
|
+
404 => NotFoundError,
|
|
40
|
+
405 => MethodNotAllowedError,
|
|
41
|
+
410 => GoneError,
|
|
42
|
+
422 => ValidationError,
|
|
43
|
+
429 => RateLimitError
|
|
44
|
+
}.freeze
|
|
45
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Listmonk
|
|
4
|
+
class Resource
|
|
5
|
+
def initialize(client)
|
|
6
|
+
@client = client
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
attr_reader :client
|
|
12
|
+
|
|
13
|
+
def segment(value)
|
|
14
|
+
URI.encode_www_form_component(value.to_s)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def get(path, params = nil)
|
|
18
|
+
client.request(:get, path, params: params)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def post(path, body = nil)
|
|
22
|
+
client.request(:post, path, json: body)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def put(path, body = nil)
|
|
26
|
+
client.request(:put, path, json: body)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def delete_request(path, params = nil)
|
|
30
|
+
client.request(:delete, path, params: params)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Listmonk
|
|
4
|
+
module Resources
|
|
5
|
+
class Bounces < Resource
|
|
6
|
+
def list(**params) = get("bounces", params)
|
|
7
|
+
def retrieve(id) = get("bounces/#{segment(id)}")
|
|
8
|
+
def delete(id) = delete_request("bounces/#{segment(id)}")
|
|
9
|
+
|
|
10
|
+
def delete_many(ids: nil, all: nil)
|
|
11
|
+
delete_request("bounces", id: ids && Array(ids).join(","), all: all)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Listmonk
|
|
4
|
+
module Resources
|
|
5
|
+
class Campaigns < Resource
|
|
6
|
+
def list(**params) = get("campaigns", params)
|
|
7
|
+
def create(**attributes) = post("campaigns", attributes)
|
|
8
|
+
def retrieve(id) = get("campaigns/#{segment(id)}")
|
|
9
|
+
def update(id, **attributes) = put("campaigns/#{segment(id)}", attributes)
|
|
10
|
+
def delete(id) = delete_request("campaigns/#{segment(id)}")
|
|
11
|
+
def running_stats = get("campaigns/running/stats")
|
|
12
|
+
|
|
13
|
+
def analytics(type, **params)
|
|
14
|
+
get("campaigns/analytics/#{segment(type)}", params)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def preview(id) = get("campaigns/#{segment(id)}/preview")
|
|
18
|
+
def update_preview(id, **attributes) = post("campaigns/#{segment(id)}/preview", attributes)
|
|
19
|
+
def preview_text(id, **attributes) = post("campaigns/#{segment(id)}/text", attributes)
|
|
20
|
+
def update_status(id, status:) = put("campaigns/#{segment(id)}/status", status: status)
|
|
21
|
+
def start(id) = update_status(id, status: "running")
|
|
22
|
+
def pause(id) = update_status(id, status: "paused")
|
|
23
|
+
def cancel(id) = update_status(id, status: "cancelled")
|
|
24
|
+
|
|
25
|
+
def update_archive(id, archive:, **attributes)
|
|
26
|
+
put("campaigns/#{segment(id)}/archive", attributes.merge(archive: archive))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def create_content(id, **attributes) = post("campaigns/#{segment(id)}/content", attributes)
|
|
30
|
+
def test(id, **attributes) = post("campaigns/#{segment(id)}/test", attributes)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Listmonk
|
|
6
|
+
module Resources
|
|
7
|
+
class Imports < Resource
|
|
8
|
+
def status = get("import/subscribers")
|
|
9
|
+
|
|
10
|
+
def create(file:, params:, content_type: "application/zip")
|
|
11
|
+
body = {
|
|
12
|
+
file: client.file_part(file, content_type: content_type),
|
|
13
|
+
params: JSON.generate(params)
|
|
14
|
+
}
|
|
15
|
+
client.request(:post, "import/subscribers", body: body)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def stop = delete_request("import/subscribers")
|
|
19
|
+
def logs = get("import/subscribers/logs")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Listmonk
|
|
4
|
+
module Resources
|
|
5
|
+
class Lists < Resource
|
|
6
|
+
def list(**params) = get("lists", params)
|
|
7
|
+
def create(**attributes) = post("lists", attributes)
|
|
8
|
+
def retrieve(id) = get("lists/#{segment(id)}")
|
|
9
|
+
def update(id, **attributes) = put("lists/#{segment(id)}", attributes)
|
|
10
|
+
def delete(id) = delete_request("lists/#{segment(id)}")
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Listmonk
|
|
4
|
+
module Resources
|
|
5
|
+
class Maintenance < Resource
|
|
6
|
+
def delete_subscribers(type) = delete_request("maintenance/subscribers/#{segment(type)}")
|
|
7
|
+
def delete_analytics(type) = delete_request("maintenance/analytics/#{segment(type)}")
|
|
8
|
+
def delete_unconfirmed_subscriptions = delete_request("maintenance/subscriptions/unconfirmed")
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Listmonk
|
|
4
|
+
module Resources
|
|
5
|
+
class Media < Resource
|
|
6
|
+
def list = get("media")
|
|
7
|
+
|
|
8
|
+
def upload(file:, content_type: "application/octet-stream")
|
|
9
|
+
body = { file: client.file_part(file, content_type: content_type) }
|
|
10
|
+
client.request(:post, "media", body: body)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def retrieve(id) = get("media/#{segment(id)}")
|
|
14
|
+
def delete(id) = delete_request("media/#{segment(id)}")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Listmonk
|
|
4
|
+
module Resources
|
|
5
|
+
class Miscellaneous < Resource
|
|
6
|
+
def health = get("health")
|
|
7
|
+
def config = get("config")
|
|
8
|
+
def language(lang) = get("lang/#{segment(lang)}")
|
|
9
|
+
def dashboard_charts = get("dashboard/charts")
|
|
10
|
+
def dashboard_counts = get("dashboard/counts")
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Listmonk
|
|
4
|
+
module Resources
|
|
5
|
+
class Public < Resource
|
|
6
|
+
def lists = get("public/lists")
|
|
7
|
+
|
|
8
|
+
def subscribe(name:, email:,
|
|
9
|
+
list_uuids:)
|
|
10
|
+
post("public/subscription", name: name, email: email, list_uuids: list_uuids)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Listmonk
|
|
4
|
+
module Resources
|
|
5
|
+
class Settings < Resource
|
|
6
|
+
def retrieve = get("settings")
|
|
7
|
+
def update(**attributes) = put("settings", attributes)
|
|
8
|
+
def test_smtp(**attributes) = post("settings/smtp/test", attributes)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Listmonk
|
|
4
|
+
module Resources
|
|
5
|
+
class Subscribers < Resource
|
|
6
|
+
def list(**params) = get("subscribers", params)
|
|
7
|
+
def create(**attributes) = post("subscribers", attributes)
|
|
8
|
+
def retrieve(id) = get("subscribers/#{segment(id)}")
|
|
9
|
+
def update(id, **attributes) = put("subscribers/#{segment(id)}", attributes)
|
|
10
|
+
def delete(id) = delete_request("subscribers/#{segment(id)}")
|
|
11
|
+
|
|
12
|
+
def delete_many(ids)
|
|
13
|
+
delete_request("subscribers", id: Array(ids).join(","))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def manage_lists(**attributes) = put("subscribers/lists", attributes)
|
|
17
|
+
def manage_list(list_id, **attributes) = put("subscribers/lists/#{segment(list_id)}", attributes)
|
|
18
|
+
def manage_blocklist(**attributes) = put("subscribers/blocklist", attributes)
|
|
19
|
+
|
|
20
|
+
def manage_blocklist_for(id, **attributes)
|
|
21
|
+
put("subscribers/#{segment(id)}/blocklist", attributes)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def export(id) = get("subscribers/#{segment(id)}/export")
|
|
25
|
+
def bounces(id) = get("subscribers/#{segment(id)}/bounces")
|
|
26
|
+
def delete_bounces(id) = delete_request("subscribers/#{segment(id)}/bounces")
|
|
27
|
+
def send_optin(id) = post("subscribers/#{segment(id)}/optin")
|
|
28
|
+
def delete_by_query(**attributes) = post("subscribers/query/delete", attributes)
|
|
29
|
+
def blocklist_by_query(**attributes) = put("subscribers/query/blocklist", attributes)
|
|
30
|
+
def manage_lists_by_query(**attributes) = put("subscribers/query/lists", attributes)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Listmonk
|
|
4
|
+
module Resources
|
|
5
|
+
class Templates < Resource
|
|
6
|
+
def list(**params) = get("templates", params)
|
|
7
|
+
def create(**attributes) = post("templates", attributes)
|
|
8
|
+
def retrieve(id) = get("templates/#{segment(id)}")
|
|
9
|
+
def update(id, **attributes) = put("templates/#{segment(id)}", attributes)
|
|
10
|
+
def delete(id) = delete_request("templates/#{segment(id)}")
|
|
11
|
+
def preview(**attributes) = post("templates/preview", attributes)
|
|
12
|
+
def preview_by_id(id) = get("templates/#{segment(id)}/preview")
|
|
13
|
+
def set_default(id) = put("templates/#{segment(id)}/default")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Listmonk
|
|
4
|
+
class Response
|
|
5
|
+
attr_reader :status, :headers, :body
|
|
6
|
+
|
|
7
|
+
def initialize(status:, headers:, body:)
|
|
8
|
+
@status = status
|
|
9
|
+
@headers = headers.freeze
|
|
10
|
+
@body = body
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def success?
|
|
14
|
+
status.between?(200, 299)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def data
|
|
18
|
+
return body unless body.is_a?(Hash)
|
|
19
|
+
|
|
20
|
+
body.key?("data") ? body["data"] : body
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
data/lib/listmonk.rb
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "listmonk/version"
|
|
4
|
+
require_relative "listmonk/errors"
|
|
5
|
+
require_relative "listmonk/response"
|
|
6
|
+
require_relative "listmonk/client"
|
|
7
|
+
|
|
8
|
+
module Listmonk
|
|
9
|
+
class << self
|
|
10
|
+
def client(**)
|
|
11
|
+
Client.new(**)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
data/sig/listmonk.rbs
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
module Listmonk
|
|
2
|
+
VERSION: String
|
|
3
|
+
|
|
4
|
+
def self.client: (**untyped) -> Client
|
|
5
|
+
|
|
6
|
+
class Response
|
|
7
|
+
attr_reader status: Integer
|
|
8
|
+
attr_reader headers: Hash[String, String]
|
|
9
|
+
attr_reader body: untyped
|
|
10
|
+
|
|
11
|
+
def success?: () -> bool
|
|
12
|
+
def data: () -> untyped
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class Error < StandardError
|
|
16
|
+
attr_reader response: Response?
|
|
17
|
+
def status: () -> Integer?
|
|
18
|
+
def body: () -> untyped
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class ConfigurationError < Error
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class ConnectionError < Error
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class TimeoutError < ConnectionError
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class ParseError < Error
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class Client
|
|
34
|
+
attr_reader base_url: String
|
|
35
|
+
|
|
36
|
+
def initialize: (
|
|
37
|
+
base_url: String,
|
|
38
|
+
?username: String?,
|
|
39
|
+
?token: String?,
|
|
40
|
+
?auth: Symbol,
|
|
41
|
+
?timeout: Numeric,
|
|
42
|
+
?open_timeout: Numeric,
|
|
43
|
+
?connection: untyped
|
|
44
|
+
) -> void
|
|
45
|
+
|
|
46
|
+
def request: (Symbol method, String path, ?params: Hash[Symbol, untyped]?, ?json: untyped, ?body: untyped, ?headers: Hash[String, String]) -> Response
|
|
47
|
+
def file_part: (untyped file, ?content_type: String, ?filename: String?) -> untyped
|
|
48
|
+
|
|
49
|
+
def miscellaneous: () -> untyped
|
|
50
|
+
def settings: () -> untyped
|
|
51
|
+
def admin: () -> untyped
|
|
52
|
+
def logs: () -> untyped
|
|
53
|
+
def subscribers: () -> untyped
|
|
54
|
+
def bounces: () -> untyped
|
|
55
|
+
def lists: () -> untyped
|
|
56
|
+
def imports: () -> untyped
|
|
57
|
+
def campaigns: () -> untyped
|
|
58
|
+
def media: () -> untyped
|
|
59
|
+
def templates: () -> untyped
|
|
60
|
+
def transactional: () -> untyped
|
|
61
|
+
def maintenance: () -> untyped
|
|
62
|
+
def public: () -> untyped
|
|
63
|
+
end
|
|
64
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: listmonk
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Richard Lee
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: faraday
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.0'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '3.0'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '2.0'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '3.0'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: faraday-multipart
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - "~>"
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '1.0'
|
|
39
|
+
type: :runtime
|
|
40
|
+
prerelease: false
|
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - "~>"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '1.0'
|
|
46
|
+
- !ruby/object:Gem::Dependency
|
|
47
|
+
name: rake
|
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - "~>"
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '13.0'
|
|
53
|
+
type: :development
|
|
54
|
+
prerelease: false
|
|
55
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - "~>"
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '13.0'
|
|
60
|
+
- !ruby/object:Gem::Dependency
|
|
61
|
+
name: rbs
|
|
62
|
+
requirement: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - "~>"
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '4.0'
|
|
67
|
+
type: :development
|
|
68
|
+
prerelease: false
|
|
69
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - "~>"
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '4.0'
|
|
74
|
+
- !ruby/object:Gem::Dependency
|
|
75
|
+
name: rspec
|
|
76
|
+
requirement: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - "~>"
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '3.13'
|
|
81
|
+
type: :development
|
|
82
|
+
prerelease: false
|
|
83
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - "~>"
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '3.13'
|
|
88
|
+
- !ruby/object:Gem::Dependency
|
|
89
|
+
name: rubocop
|
|
90
|
+
requirement: !ruby/object:Gem::Requirement
|
|
91
|
+
requirements:
|
|
92
|
+
- - "~>"
|
|
93
|
+
- !ruby/object:Gem::Version
|
|
94
|
+
version: '1.0'
|
|
95
|
+
type: :development
|
|
96
|
+
prerelease: false
|
|
97
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
98
|
+
requirements:
|
|
99
|
+
- - "~>"
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
version: '1.0'
|
|
102
|
+
- !ruby/object:Gem::Dependency
|
|
103
|
+
name: rubocop-rspec
|
|
104
|
+
requirement: !ruby/object:Gem::Requirement
|
|
105
|
+
requirements:
|
|
106
|
+
- - "~>"
|
|
107
|
+
- !ruby/object:Gem::Version
|
|
108
|
+
version: '3.0'
|
|
109
|
+
type: :development
|
|
110
|
+
prerelease: false
|
|
111
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
112
|
+
requirements:
|
|
113
|
+
- - "~>"
|
|
114
|
+
- !ruby/object:Gem::Version
|
|
115
|
+
version: '3.0'
|
|
116
|
+
- !ruby/object:Gem::Dependency
|
|
117
|
+
name: simplecov
|
|
118
|
+
requirement: !ruby/object:Gem::Requirement
|
|
119
|
+
requirements:
|
|
120
|
+
- - "~>"
|
|
121
|
+
- !ruby/object:Gem::Version
|
|
122
|
+
version: '0.22'
|
|
123
|
+
type: :development
|
|
124
|
+
prerelease: false
|
|
125
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
126
|
+
requirements:
|
|
127
|
+
- - "~>"
|
|
128
|
+
- !ruby/object:Gem::Version
|
|
129
|
+
version: '0.22'
|
|
130
|
+
- !ruby/object:Gem::Dependency
|
|
131
|
+
name: webmock
|
|
132
|
+
requirement: !ruby/object:Gem::Requirement
|
|
133
|
+
requirements:
|
|
134
|
+
- - "~>"
|
|
135
|
+
- !ruby/object:Gem::Version
|
|
136
|
+
version: '3.0'
|
|
137
|
+
type: :development
|
|
138
|
+
prerelease: false
|
|
139
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
140
|
+
requirements:
|
|
141
|
+
- - "~>"
|
|
142
|
+
- !ruby/object:Gem::Version
|
|
143
|
+
version: '3.0'
|
|
144
|
+
description: A resource-oriented Ruby client for listmonk's HTTP API.
|
|
145
|
+
email:
|
|
146
|
+
- 14349+dlackty@users.noreply.github.com
|
|
147
|
+
executables: []
|
|
148
|
+
extensions: []
|
|
149
|
+
extra_rdoc_files: []
|
|
150
|
+
files:
|
|
151
|
+
- CHANGELOG.md
|
|
152
|
+
- LICENSE.txt
|
|
153
|
+
- README.md
|
|
154
|
+
- lib/listmonk.rb
|
|
155
|
+
- lib/listmonk/client.rb
|
|
156
|
+
- lib/listmonk/errors.rb
|
|
157
|
+
- lib/listmonk/resource.rb
|
|
158
|
+
- lib/listmonk/resources/admin.rb
|
|
159
|
+
- lib/listmonk/resources/bounces.rb
|
|
160
|
+
- lib/listmonk/resources/campaigns.rb
|
|
161
|
+
- lib/listmonk/resources/imports.rb
|
|
162
|
+
- lib/listmonk/resources/lists.rb
|
|
163
|
+
- lib/listmonk/resources/logs.rb
|
|
164
|
+
- lib/listmonk/resources/maintenance.rb
|
|
165
|
+
- lib/listmonk/resources/media.rb
|
|
166
|
+
- lib/listmonk/resources/miscellaneous.rb
|
|
167
|
+
- lib/listmonk/resources/public.rb
|
|
168
|
+
- lib/listmonk/resources/settings.rb
|
|
169
|
+
- lib/listmonk/resources/subscribers.rb
|
|
170
|
+
- lib/listmonk/resources/templates.rb
|
|
171
|
+
- lib/listmonk/resources/transactional.rb
|
|
172
|
+
- lib/listmonk/response.rb
|
|
173
|
+
- lib/listmonk/version.rb
|
|
174
|
+
- sig/listmonk.rbs
|
|
175
|
+
homepage: https://github.com/polydice/listmonk-ruby
|
|
176
|
+
licenses:
|
|
177
|
+
- MIT
|
|
178
|
+
metadata:
|
|
179
|
+
allowed_push_host: https://rubygems.org
|
|
180
|
+
homepage_uri: https://github.com/polydice/listmonk-ruby
|
|
181
|
+
source_code_uri: https://github.com/polydice/listmonk-ruby
|
|
182
|
+
changelog_uri: https://github.com/polydice/listmonk-ruby/blob/main/CHANGELOG.md
|
|
183
|
+
rubygems_mfa_required: 'true'
|
|
184
|
+
rdoc_options: []
|
|
185
|
+
require_paths:
|
|
186
|
+
- lib
|
|
187
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
188
|
+
requirements:
|
|
189
|
+
- - ">="
|
|
190
|
+
- !ruby/object:Gem::Version
|
|
191
|
+
version: 3.3.0
|
|
192
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
193
|
+
requirements:
|
|
194
|
+
- - ">="
|
|
195
|
+
- !ruby/object:Gem::Version
|
|
196
|
+
version: '0'
|
|
197
|
+
requirements: []
|
|
198
|
+
rubygems_version: 4.0.16
|
|
199
|
+
specification_version: 4
|
|
200
|
+
summary: A Ruby client for the listmonk API
|
|
201
|
+
test_files: []
|