connector-ruby 0.1.1 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6c22469b3849b241124a069729e5b127cb3459d4f9f55458a79b089fc3f33a28
4
- data.tar.gz: 66fbf3cb744d58622775bed0a17840f6635984e9fc6a488ea507da5b2f5fbd6c
3
+ metadata.gz: d3564fee7248f47cf5a37285ef65601563ad281f509ac7e62a7a990219d81a87
4
+ data.tar.gz: 1edc94f66291c7d36116fb37af9d7546d3d275993e56ecdb49bac5b9afc1775e
5
5
  SHA512:
6
- metadata.gz: fef0557ed46edaf27e1c50b2e28e0cde341c8343fe394b96c51d3a46e17c1b62ccd2fbc88974abd41c3b59bf37f4b20349d2cd794ede451c7074107ebdd81824
7
- data.tar.gz: 706e414863cc4f8d556166a6fd60d2a092593ded6bea22c6617768148e2c596e2121bbf6f934df5170680eeaf40d5421805db07348099a854de5466afd841d0e
6
+ metadata.gz: 7b1f6ee12c630043c864c2a60e0684e388e50e2cbb53ce868377e6fb5254ce93fc2927dec99efb3841a960316a2924e8a47348d6dee1909f746098065973c3d3
7
+ data.tar.gz: 168d00e2d6bb2779f8e32a47224bd390a08e70cf13faa05eefcb44f8748aff2deb9e96b19e2a415bff468e3722122348537daae1d0a3e575252133fe781b913b
data/README.md CHANGED
@@ -1,58 +1,42 @@
1
1
  # connector-ruby
2
2
 
3
- Unified channel messaging SDK for Ruby. Framework-agnostic SDK for sending/receiving messages across chat platforms.
3
+ Unified channel messaging SDK for Ruby. Send and receive messages across WhatsApp and Telegram with a consistent API.
4
4
 
5
5
  ## Installation
6
6
 
7
7
  ```ruby
8
- gem "connector-ruby", "~> 0.1"
8
+ gem "connector-ruby"
9
9
  ```
10
10
 
11
- ## Supported Channels
12
-
13
- - WhatsApp Cloud API
14
- - Telegram Bot API
15
-
16
11
  ## Usage
17
12
 
18
- ### WhatsApp
19
-
20
13
  ```ruby
21
- client = ConnectorRuby::WhatsApp.new(
22
- phone_number_id: "...",
23
- access_token: "..."
24
- )
25
-
26
- client.send_text(to: "+62812...", text: "Hello!")
27
- client.send_buttons(to: "+62812...", body: "Choose:", buttons: [
28
- { id: "opt1", title: "Option 1" },
29
- { id: "opt2", title: "Option 2" }
30
- ])
31
- client.send_image(to: "+62812...", url: "https://...")
14
+ require "connector_ruby"
32
15
 
33
- event = ConnectorRuby::WhatsApp.parse_webhook(request_body)
34
- ```
35
-
36
- ### Telegram
16
+ # WhatsApp
17
+ wa = ConnectorRuby::Channels::WhatsApp.new(
18
+ access_token: ENV["WHATSAPP_TOKEN"],
19
+ phone_number_id: ENV["WHATSAPP_PHONE_ID"]
20
+ )
21
+ wa.send_text(to: "+1234567890", text: "Hello!")
37
22
 
38
- ```ruby
39
- client = ConnectorRuby::Telegram.new(bot_token: "...")
23
+ # Telegram
24
+ tg = ConnectorRuby::Channels::Telegram.new(bot_token: ENV["TELEGRAM_TOKEN"])
25
+ tg.send_text(to: "chat_id", text: "Hello!")
40
26
 
41
- client.send_text(chat_id: 12345, text: "Hello!")
42
- event = ConnectorRuby::Telegram.parse_webhook(request_body)
27
+ # Webhook verification
28
+ verifier = ConnectorRuby::WebhookVerifier.new(secret_token: "secret")
29
+ verifier.verify!(request_body, signature_header)
43
30
  ```
44
31
 
45
- ### Configuration
32
+ ## Features
46
33
 
47
- ```ruby
48
- ConnectorRuby.configure do |config|
49
- config.whatsapp_phone_number_id = ENV["WA_PHONE_ID"]
50
- config.whatsapp_access_token = ENV["WA_TOKEN"]
51
- config.telegram_bot_token = ENV["TG_TOKEN"]
52
- config.http_timeout = 30
53
- config.http_retries = 3
54
- end
55
- ```
34
+ - WhatsApp Business API (text, buttons, images)
35
+ - Telegram Bot API (text, callbacks)
36
+ - HMAC-SHA256 webhook verification
37
+ - HTTP retry with exponential backoff for 429/5xx
38
+ - Input validation and error handling
39
+ - Logging hooks (on_request, on_response, on_error)
56
40
 
57
41
  ## License
58
42
 
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConnectorRuby
4
+ class BatchSender
5
+ RATE_LIMITS = {
6
+ whatsapp: { messages_per_second: 80 },
7
+ telegram: { messages_per_second: 30 },
8
+ messenger: { messages_per_second: 200 },
9
+ line: { messages_per_second: 100 },
10
+ slack: { messages_per_second: 1 }
11
+ }.freeze
12
+
13
+ def initialize(channel:)
14
+ @channel = channel
15
+ @results = []
16
+ @errors = []
17
+ end
18
+
19
+ def send_batch(messages)
20
+ rate = rate_limit_for(@channel)
21
+ delay = 1.0 / rate
22
+
23
+ messages.each_with_index do |msg, i|
24
+ sleep(delay) if i > 0
25
+ begin
26
+ result = yield msg
27
+ @results << { index: i, status: :sent, result: result }
28
+ rescue => e
29
+ @errors << { index: i, status: :failed, error: e.message }
30
+ end
31
+ end
32
+
33
+ { sent: @results, failed: @errors, total: messages.size }
34
+ end
35
+
36
+ private
37
+
38
+ def rate_limit_for(channel)
39
+ channel_sym = channel.class.name.split("::").last.downcase.to_sym
40
+ config = RATE_LIMITS[channel_sym] || { messages_per_second: 10 }
41
+ config[:messages_per_second]
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConnectorRuby
4
+ module Channels
5
+ class Line < Base
6
+ BASE_URL = "https://api.line.me/v2/bot/message"
7
+
8
+ def initialize(channel_access_token: nil)
9
+ @channel_access_token = channel_access_token || ConnectorRuby.configuration.line_channel_access_token
10
+ raise ConfigurationError, "LINE channel_access_token is required" unless @channel_access_token
11
+ end
12
+
13
+ def send_text(to:, text:)
14
+ validate_send!(to: to, text: text)
15
+ push_message(to, [{ type: "text", text: text }])
16
+ end
17
+
18
+ def send_buttons(to:, text:, buttons:)
19
+ validate_send!(to: to, text: text)
20
+ actions = buttons.map do |btn|
21
+ { type: "postback", label: btn[:title], data: btn[:id] }
22
+ end
23
+
24
+ template = {
25
+ type: "template",
26
+ altText: text,
27
+ template: {
28
+ type: "buttons",
29
+ text: text,
30
+ actions: actions
31
+ }
32
+ }
33
+ push_message(to, [template])
34
+ end
35
+
36
+ def send_image(to:, url:, caption: nil)
37
+ validate_send!(to: to)
38
+ messages = [{ type: "image", originalContentUrl: url, previewImageUrl: url }]
39
+ messages << { type: "text", text: caption } if caption
40
+ push_message(to, messages)
41
+ end
42
+
43
+ def send_flex(to:, alt_text:, contents:)
44
+ validate_send!(to: to)
45
+ flex = {
46
+ type: "flex",
47
+ altText: alt_text,
48
+ contents: contents
49
+ }
50
+ push_message(to, [flex])
51
+ end
52
+
53
+ def self.parse_webhook(body)
54
+ data = body.is_a?(String) ? JSON.parse(body) : body
55
+
56
+ events = data["events"]
57
+ return nil unless events&.any?
58
+
59
+ event_data = events[0]
60
+ case event_data["type"]
61
+ when "message"
62
+ parse_message(event_data)
63
+ when "postback"
64
+ parse_postback(event_data)
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def push_message(to, messages)
71
+ payload = { to: to, messages: messages }
72
+ http_client.post("#{BASE_URL}/push", body: payload, headers: auth_headers)
73
+ end
74
+
75
+ def auth_headers
76
+ { "Authorization" => "Bearer #{@channel_access_token}" }
77
+ end
78
+
79
+ def validate_send!(to:, text: nil)
80
+ raise ConnectorRuby::Error, "Recipient 'to' cannot be nil or empty" if to.nil? || to.to_s.strip.empty?
81
+ if text
82
+ raise ConnectorRuby::Error, "Text cannot be nil or empty" if text.nil? || text.to_s.strip.empty?
83
+ raise ConnectorRuby::Error, "Text exceeds 5000 character limit" if text.length > 5000
84
+ end
85
+ end
86
+
87
+ def self.parse_message(event_data)
88
+ msg = event_data["message"]
89
+ Event.new(
90
+ type: :message,
91
+ channel: :line,
92
+ from: event_data.dig("source", "userId"),
93
+ text: msg["text"],
94
+ timestamp: event_data["timestamp"] ? Time.at(event_data["timestamp"].to_i / 1000) : nil,
95
+ message_id: msg["id"],
96
+ metadata: {
97
+ reply_token: event_data["replyToken"],
98
+ source_type: event_data.dig("source", "type"),
99
+ message_type: msg["type"]
100
+ }
101
+ )
102
+ end
103
+
104
+ def self.parse_postback(event_data)
105
+ Event.new(
106
+ type: :callback,
107
+ channel: :line,
108
+ from: event_data.dig("source", "userId"),
109
+ text: event_data.dig("postback", "data"),
110
+ timestamp: event_data["timestamp"] ? Time.at(event_data["timestamp"].to_i / 1000) : nil,
111
+ metadata: {
112
+ reply_token: event_data["replyToken"],
113
+ source_type: event_data.dig("source", "type")
114
+ }
115
+ )
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConnectorRuby
4
+ module Channels
5
+ class Messenger < Base
6
+ BASE_URL = "https://graph.facebook.com/v21.0/me/messages"
7
+
8
+ def initialize(page_access_token: nil)
9
+ @page_access_token = page_access_token || ConnectorRuby.configuration.messenger_page_access_token
10
+ raise ConfigurationError, "Messenger page_access_token is required" unless @page_access_token
11
+ end
12
+
13
+ def send_text(to:, text:)
14
+ validate_send!(to: to, text: text)
15
+ payload = {
16
+ recipient: { id: to },
17
+ message: { text: text }
18
+ }
19
+ post_message(payload)
20
+ end
21
+
22
+ def send_buttons(to:, text:, buttons:)
23
+ validate_send!(to: to, text: text)
24
+ formatted = buttons.map do |btn|
25
+ { type: "postback", title: btn[:title], payload: btn[:id] }
26
+ end
27
+
28
+ payload = {
29
+ recipient: { id: to },
30
+ message: {
31
+ attachment: {
32
+ type: "template",
33
+ payload: {
34
+ template_type: "button",
35
+ text: text,
36
+ buttons: formatted
37
+ }
38
+ }
39
+ }
40
+ }
41
+ post_message(payload)
42
+ end
43
+
44
+ def send_image(to:, url:, caption: nil)
45
+ validate_send!(to: to)
46
+ payload = {
47
+ recipient: { id: to },
48
+ message: {
49
+ attachment: {
50
+ type: "image",
51
+ payload: { url: url, is_reusable: true }
52
+ }
53
+ }
54
+ }
55
+ post_message(payload)
56
+ end
57
+
58
+ def send_quick_replies(to:, text:, replies:)
59
+ validate_send!(to: to, text: text)
60
+ formatted = replies.map do |r|
61
+ { content_type: "text", title: r[:title], payload: r[:id] }
62
+ end
63
+
64
+ payload = {
65
+ recipient: { id: to },
66
+ message: { text: text, quick_replies: formatted }
67
+ }
68
+ post_message(payload)
69
+ end
70
+
71
+ def self.parse_webhook(body)
72
+ data = body.is_a?(String) ? JSON.parse(body) : body
73
+
74
+ entry = data.dig("entry", 0)
75
+ return nil unless entry
76
+
77
+ messaging = entry.dig("messaging", 0)
78
+ return nil unless messaging
79
+
80
+ if messaging["message"]
81
+ parse_message(messaging)
82
+ elsif messaging["postback"]
83
+ parse_postback(messaging)
84
+ end
85
+ end
86
+
87
+ private
88
+
89
+ def post_message(payload)
90
+ http_client.post(BASE_URL, body: payload, headers: auth_headers)
91
+ end
92
+
93
+ def auth_headers
94
+ { "Authorization" => "Bearer #{@page_access_token}" }
95
+ end
96
+
97
+ def validate_send!(to:, text: nil)
98
+ raise ConnectorRuby::Error, "Recipient 'to' cannot be nil or empty" if to.nil? || to.to_s.strip.empty?
99
+ if text
100
+ raise ConnectorRuby::Error, "Text cannot be nil or empty" if text.nil? || text.to_s.strip.empty?
101
+ raise ConnectorRuby::Error, "Text exceeds 2000 character limit" if text.length > 2000
102
+ end
103
+ end
104
+
105
+ def self.parse_message(messaging)
106
+ msg = messaging["message"]
107
+ sender = messaging.dig("sender", "id")
108
+
109
+ Event.new(
110
+ type: :message,
111
+ channel: :messenger,
112
+ from: sender,
113
+ to: messaging.dig("recipient", "id"),
114
+ text: msg["text"],
115
+ timestamp: messaging["timestamp"] ? Time.at(messaging["timestamp"].to_i / 1000) : nil,
116
+ message_id: msg["mid"],
117
+ metadata: {
118
+ is_echo: msg["is_echo"],
119
+ quick_reply_payload: msg.dig("quick_reply", "payload")
120
+ }
121
+ )
122
+ end
123
+
124
+ def self.parse_postback(messaging)
125
+ postback = messaging["postback"]
126
+ sender = messaging.dig("sender", "id")
127
+
128
+ Event.new(
129
+ type: :callback,
130
+ channel: :messenger,
131
+ from: sender,
132
+ to: messaging.dig("recipient", "id"),
133
+ text: postback["payload"],
134
+ timestamp: messaging["timestamp"] ? Time.at(messaging["timestamp"].to_i / 1000) : nil,
135
+ metadata: {
136
+ title: postback["title"]
137
+ }
138
+ )
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConnectorRuby
4
+ module Channels
5
+ class Slack < Base
6
+ BASE_URL = "https://slack.com/api"
7
+
8
+ def initialize(bot_token: nil)
9
+ @bot_token = bot_token || ConnectorRuby.configuration.slack_bot_token
10
+ raise ConfigurationError, "Slack bot_token is required" unless @bot_token
11
+ end
12
+
13
+ def send_text(channel:, text:)
14
+ validate_send!(channel: channel, text: text)
15
+ payload = { channel: channel, text: text }
16
+ api_call("chat.postMessage", payload)
17
+ end
18
+
19
+ def send_buttons(channel:, text:, buttons:)
20
+ validate_send!(channel: channel, text: text)
21
+ actions = buttons.map do |btn|
22
+ {
23
+ type: "button",
24
+ text: { type: "plain_text", text: btn[:title] },
25
+ action_id: btn[:id],
26
+ value: btn[:id]
27
+ }
28
+ end
29
+
30
+ payload = {
31
+ channel: channel,
32
+ text: text,
33
+ blocks: [
34
+ { type: "section", text: { type: "mrkdwn", text: text } },
35
+ { type: "actions", elements: actions }
36
+ ]
37
+ }
38
+ api_call("chat.postMessage", payload)
39
+ end
40
+
41
+ def send_image(channel:, url:, caption: nil)
42
+ validate_send!(channel: channel)
43
+ blocks = [
44
+ {
45
+ type: "image",
46
+ image_url: url,
47
+ alt_text: caption || "image"
48
+ }
49
+ ]
50
+ blocks.first[:title] = { type: "plain_text", text: caption } if caption
51
+
52
+ payload = { channel: channel, text: caption || "Image", blocks: blocks }
53
+ api_call("chat.postMessage", payload)
54
+ end
55
+
56
+ def send_blocks(channel:, text:, blocks:)
57
+ validate_send!(channel: channel)
58
+ payload = { channel: channel, text: text, blocks: blocks }
59
+ api_call("chat.postMessage", payload)
60
+ end
61
+
62
+ def self.parse_webhook(body)
63
+ data = body.is_a?(String) ? JSON.parse(body) : body
64
+
65
+ # URL verification challenge
66
+ return { challenge: data["challenge"] } if data["type"] == "url_verification"
67
+
68
+ event = data["event"]
69
+ return nil unless event
70
+
71
+ case event["type"]
72
+ when "message"
73
+ return nil if event["subtype"] # skip bot messages, edits, etc.
74
+ parse_message(event)
75
+ when "app_mention"
76
+ parse_message(event)
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ def api_call(method, payload)
83
+ http_client.post("#{BASE_URL}/#{method}", body: payload, headers: auth_headers)
84
+ end
85
+
86
+ def auth_headers
87
+ { "Authorization" => "Bearer #{@bot_token}" }
88
+ end
89
+
90
+ def validate_send!(channel:, text: nil)
91
+ raise ConnectorRuby::Error, "Recipient 'channel' cannot be nil or empty" if channel.nil? || channel.to_s.strip.empty?
92
+ if text
93
+ raise ConnectorRuby::Error, "Text cannot be nil or empty" if text.nil? || text.to_s.strip.empty?
94
+ end
95
+ end
96
+
97
+ def self.parse_message(event)
98
+ Event.new(
99
+ type: :message,
100
+ channel: :slack,
101
+ from: event["user"],
102
+ text: event["text"],
103
+ timestamp: event["ts"] ? Time.at(event["ts"].to_f) : nil,
104
+ message_id: event["ts"],
105
+ metadata: {
106
+ channel_id: event["channel"],
107
+ channel_type: event["channel_type"],
108
+ team: event["team"],
109
+ thread_ts: event["thread_ts"]
110
+ }
111
+ )
112
+ end
113
+ end
114
+ end
115
+ end
@@ -38,6 +38,24 @@ module ConnectorRuby
38
38
  api_call("sendPhoto", payload)
39
39
  end
40
40
 
41
+ def send_document(chat_id:, url:, caption: nil)
42
+ validate_send!(chat_id: chat_id)
43
+ payload = { chat_id: chat_id, document: url }
44
+ payload[:caption] = caption if caption
45
+ api_call("sendDocument", payload)
46
+ end
47
+
48
+ def send_location(chat_id:, latitude:, longitude:)
49
+ validate_send!(chat_id: chat_id)
50
+ payload = { chat_id: chat_id, latitude: latitude, longitude: longitude }
51
+ api_call("sendLocation", payload)
52
+ end
53
+
54
+ def send_typing(chat_id:)
55
+ validate_send!(chat_id: chat_id)
56
+ api_call("sendChatAction", { chat_id: chat_id, action: "typing" })
57
+ end
58
+
41
59
  def self.parse_webhook(body)
42
60
  data = body.is_a?(String) ? JSON.parse(body) : body
43
61
 
@@ -57,6 +57,103 @@ module ConnectorRuby
57
57
  post_message(payload)
58
58
  end
59
59
 
60
+ def send_template(to:, template_name:, language: "en", components: [])
61
+ validate_send!(to: to)
62
+ payload = {
63
+ messaging_product: "whatsapp",
64
+ to: to,
65
+ type: "template",
66
+ template: {
67
+ name: template_name,
68
+ language: { code: language },
69
+ components: components
70
+ }
71
+ }
72
+ post_message(payload)
73
+ end
74
+
75
+ def send_document(to:, url:, filename: nil, caption: nil)
76
+ validate_send!(to: to)
77
+ doc = { link: url }
78
+ doc[:filename] = filename if filename
79
+ doc[:caption] = caption if caption
80
+
81
+ payload = {
82
+ messaging_product: "whatsapp",
83
+ to: to,
84
+ type: "document",
85
+ document: doc
86
+ }
87
+ post_message(payload)
88
+ end
89
+
90
+ def send_location(to:, latitude:, longitude:, name: nil, address: nil)
91
+ validate_send!(to: to)
92
+ location = { latitude: latitude, longitude: longitude }
93
+ location[:name] = name if name
94
+ location[:address] = address if address
95
+
96
+ payload = {
97
+ messaging_product: "whatsapp",
98
+ to: to,
99
+ type: "location",
100
+ location: location
101
+ }
102
+ post_message(payload)
103
+ end
104
+
105
+ def send_contact(to:, name:, phone:)
106
+ validate_send!(to: to)
107
+ payload = {
108
+ messaging_product: "whatsapp",
109
+ to: to,
110
+ type: "contacts",
111
+ contacts: [{
112
+ name: { formatted_name: name },
113
+ phones: [{ phone: phone }]
114
+ }]
115
+ }
116
+ post_message(payload)
117
+ end
118
+
119
+ def send_reaction(to:, message_id:, emoji:)
120
+ validate_send!(to: to)
121
+ payload = {
122
+ messaging_product: "whatsapp",
123
+ to: to,
124
+ type: "reaction",
125
+ reaction: { message_id: message_id, emoji: emoji }
126
+ }
127
+ post_message(payload)
128
+ end
129
+
130
+ def send_list(to:, body:, button_text:, sections:)
131
+ validate_send!(to: to, text: body)
132
+ payload = {
133
+ messaging_product: "whatsapp",
134
+ to: to,
135
+ type: "interactive",
136
+ interactive: {
137
+ type: "list",
138
+ body: { text: body },
139
+ action: {
140
+ button: button_text,
141
+ sections: sections
142
+ }
143
+ }
144
+ }
145
+ post_message(payload)
146
+ end
147
+
148
+ def mark_as_read(message_id:)
149
+ payload = {
150
+ messaging_product: "whatsapp",
151
+ status: "read",
152
+ message_id: message_id
153
+ }
154
+ post_message(payload)
155
+ end
156
+
60
157
  def self.parse_webhook(body, signature: nil)
61
158
  data = body.is_a?(String) ? JSON.parse(body) : body
62
159
 
@@ -4,6 +4,9 @@ module ConnectorRuby
4
4
  class Configuration
5
5
  attr_accessor :whatsapp_phone_number_id, :whatsapp_access_token,
6
6
  :telegram_bot_token,
7
+ :messenger_page_access_token,
8
+ :line_channel_access_token,
9
+ :slack_bot_token,
7
10
  :http_timeout, :http_retries, :http_open_timeout,
8
11
  :on_request, :on_response, :on_error
9
12
 
@@ -11,6 +14,9 @@ module ConnectorRuby
11
14
  @whatsapp_phone_number_id = nil
12
15
  @whatsapp_access_token = nil
13
16
  @telegram_bot_token = nil
17
+ @messenger_page_access_token = nil
18
+ @line_channel_access_token = nil
19
+ @slack_bot_token = nil
14
20
  @http_timeout = 30
15
21
  @http_retries = 3
16
22
  @http_open_timeout = 10
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConnectorRuby
4
+ class DeliveryTracker
5
+ attr_reader :entries
6
+
7
+ def initialize
8
+ @entries = {}
9
+ @callbacks = Hash.new { |h, k| h[k] = [] }
10
+ end
11
+
12
+ def track(message_id, metadata: {})
13
+ @entries[message_id] = {
14
+ status: :sent,
15
+ sent_at: Time.now,
16
+ metadata: metadata,
17
+ history: [{ status: :sent, at: Time.now }]
18
+ }
19
+ end
20
+
21
+ def update(message_id, status:)
22
+ entry = @entries[message_id]
23
+ return nil unless entry
24
+
25
+ entry[:status] = status.to_sym
26
+ entry[:history] << { status: status.to_sym, at: Time.now }
27
+
28
+ fire(status.to_sym, message_id, entry)
29
+ entry
30
+ end
31
+
32
+ def status(message_id)
33
+ @entries.dig(message_id, :status)
34
+ end
35
+
36
+ def on(status, &block)
37
+ @callbacks[status.to_sym] << block
38
+ end
39
+
40
+ def pending
41
+ @entries.select { |_, v| v[:status] == :sent }
42
+ end
43
+
44
+ def delivered
45
+ @entries.select { |_, v| v[:status] == :delivered }
46
+ end
47
+
48
+ def read
49
+ @entries.select { |_, v| v[:status] == :read }
50
+ end
51
+
52
+ private
53
+
54
+ def fire(status, message_id, entry)
55
+ @callbacks[status].each { |cb| cb.call(message_id, entry) }
56
+ end
57
+ end
58
+ end
@@ -4,9 +4,14 @@ module ConnectorRuby
4
4
  class Message
5
5
  attr_reader :type, :to, :text, :buttons, :image_url, :caption, :metadata
6
6
 
7
- TYPES = %i[text buttons image template].freeze
7
+ TYPES = %i[text buttons image template document location contact reaction list].freeze
8
8
 
9
- def initialize(type:, to:, text: nil, buttons: nil, image_url: nil, caption: nil, metadata: {})
9
+ attr_reader :document_url, :filename, :latitude, :longitude,
10
+ :location_name, :address, :phone, :contact_name,
11
+ :template_name, :language, :components,
12
+ :emoji, :sections, :button_text
13
+
14
+ def initialize(type:, to:, text: nil, buttons: nil, image_url: nil, caption: nil, metadata: {}, **extra)
10
15
  @type = type
11
16
  @to = to
12
17
  @text = text
@@ -14,8 +19,10 @@ module ConnectorRuby
14
19
  @image_url = image_url
15
20
  @caption = caption
16
21
  @metadata = metadata
22
+ extra.each { |k, v| instance_variable_set(:"@#{k}", v) }
17
23
  end
18
24
 
25
+ # Factory methods
19
26
  def self.text(to:, text:)
20
27
  new(type: :text, to: to, text: text)
21
28
  end
@@ -28,6 +35,24 @@ module ConnectorRuby
28
35
  new(type: :image, to: to, image_url: url, caption: caption)
29
36
  end
30
37
 
38
+ def self.document(to:, url:, filename: nil, caption: nil)
39
+ new(type: :document, to: to, document_url: url, filename: filename, caption: caption)
40
+ end
41
+
42
+ def self.location(to:, latitude:, longitude:, name: nil, address: nil)
43
+ new(type: :location, to: to, latitude: latitude, longitude: longitude,
44
+ location_name: name, address: address)
45
+ end
46
+
47
+ def self.contact(to:, name:, phone:)
48
+ new(type: :contact, to: to, contact_name: name, phone: phone)
49
+ end
50
+
51
+ # Builder DSL
52
+ def self.build
53
+ Builder.new
54
+ end
55
+
31
56
  def to_h
32
57
  {
33
58
  type: @type,
@@ -39,5 +64,61 @@ module ConnectorRuby
39
64
  metadata: @metadata
40
65
  }.compact
41
66
  end
67
+
68
+ class Builder
69
+ def initialize
70
+ @attrs = { metadata: {} }
71
+ end
72
+
73
+ def to(recipient)
74
+ @attrs[:to] = recipient
75
+ self
76
+ end
77
+
78
+ def text(content)
79
+ @attrs[:type] = :text
80
+ @attrs[:text] = content
81
+ self
82
+ end
83
+
84
+ def buttons(btns)
85
+ @attrs[:type] = :buttons
86
+ @attrs[:buttons] = btns
87
+ self
88
+ end
89
+
90
+ def image(url, caption: nil)
91
+ @attrs[:type] = :image
92
+ @attrs[:image_url] = url
93
+ @attrs[:caption] = caption
94
+ self
95
+ end
96
+
97
+ def document(url, filename: nil)
98
+ @attrs[:type] = :document
99
+ @attrs[:document_url] = url
100
+ @attrs[:filename] = filename
101
+ self
102
+ end
103
+
104
+ def location(lat, lng, name: nil)
105
+ @attrs[:type] = :location
106
+ @attrs[:latitude] = lat
107
+ @attrs[:longitude] = lng
108
+ @attrs[:location_name] = name
109
+ self
110
+ end
111
+
112
+ def metadata(hash)
113
+ @attrs[:metadata] = hash
114
+ self
115
+ end
116
+
117
+ def build
118
+ raise ConnectorRuby::Error, "Message requires a recipient" unless @attrs[:to]
119
+ raise ConnectorRuby::Error, "Message requires a type" unless @attrs[:type]
120
+ Message.new(**@attrs)
121
+ end
122
+ end
42
123
  end
43
124
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ConnectorRuby
4
- VERSION = "0.1.1"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -10,6 +10,11 @@ require_relative "connector_ruby/webhook_verifier"
10
10
  require_relative "connector_ruby/channels/base"
11
11
  require_relative "connector_ruby/channels/whatsapp"
12
12
  require_relative "connector_ruby/channels/telegram"
13
+ require_relative "connector_ruby/channels/messenger"
14
+ require_relative "connector_ruby/channels/line"
15
+ require_relative "connector_ruby/channels/slack"
16
+ require_relative "connector_ruby/batch_sender"
17
+ require_relative "connector_ruby/delivery_tracker"
13
18
 
14
19
  module ConnectorRuby
15
20
  class << self
@@ -29,4 +34,7 @@ module ConnectorRuby
29
34
  # Convenience aliases
30
35
  WhatsApp = Channels::WhatsApp
31
36
  Telegram = Channels::Telegram
37
+ Messenger = Channels::Messenger
38
+ Line = Channels::Line
39
+ Slack = Channels::Slack
32
40
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: connector-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Johannes Dwi Cahyo
@@ -65,10 +65,15 @@ files:
65
65
  - Rakefile
66
66
  - connector-ruby.gemspec
67
67
  - lib/connector_ruby.rb
68
+ - lib/connector_ruby/batch_sender.rb
68
69
  - lib/connector_ruby/channels/base.rb
70
+ - lib/connector_ruby/channels/line.rb
71
+ - lib/connector_ruby/channels/messenger.rb
72
+ - lib/connector_ruby/channels/slack.rb
69
73
  - lib/connector_ruby/channels/telegram.rb
70
74
  - lib/connector_ruby/channels/whatsapp.rb
71
75
  - lib/connector_ruby/configuration.rb
76
+ - lib/connector_ruby/delivery_tracker.rb
72
77
  - lib/connector_ruby/error.rb
73
78
  - lib/connector_ruby/event.rb
74
79
  - lib/connector_ruby/http_client.rb