twi 0.0.1 → 0.3.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
- SHA1:
3
- metadata.gz: ca440c616d07a0318bd42e881904d028e8ab6317
4
- data.tar.gz: 079f8937ec1046155d9d242b4b9900c786a89f93
2
+ SHA256:
3
+ metadata.gz: a3da0ed4a9e8746dd8609affeb9cf316a156dafa9a20b0da5e5307c37ea95ad0
4
+ data.tar.gz: 647d377b97c2fc7951ecd61f3caeb100b5d4ceeb8e1a8d788a5ba0b623a60ef9
5
5
  SHA512:
6
- metadata.gz: f0cd8ac8913c7858c27dbcb95491bbb19e5fed172353604c83b8196e3caf0d100a6a2275c480da06cee007ec8d96a59f9071d885adb1e6cdf257033700cfd756
7
- data.tar.gz: 9e64a5e5fe653bae854be62bb77dab292b6573dedec79a7fda05cecff39b9774459d80188c530493aff84e17f89da5cf0bee1ec4d463ff761ddbb7e6c5cb82eb
6
+ metadata.gz: 375a364348e16d823426c5670e22a900f52d50d5ee9ac2405f80cd6cd9e3cd7052e20d2d8511f50d44036f579ce273d8a2929908f470fd01aeeaf576e0ec6993
7
+ data.tar.gz: 97361223e531df80107ed916fa9f81774a9a8694a2e9df8b7acc5dd58468f02c10616df901e798d88f3be3e93b9f1cd4b4c8a3952525ab618378ea1d5a520063
data/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ ## [0.3.0] - 2026-05-27
2
+
3
+ - [New] Add Twi::Message#create
4
+ - [New] Add Twi::Conversation#upload
5
+ - [New] Add Twi.event, Twi.mock.medium
6
+ - [New] Add Twi.conversation, Twi.mock.conversation, Twi.mock.conversation_error
7
+ - [New] Add Twi::Event.params_for
8
+ - [New] Add Twi.create_phone, Twi.mock.phone, Twi.mock.phone_error
9
+ - [New] Add Twi::Phone
10
+
11
+ ## [0.2.0] - 2026-05-25
12
+
13
+ - [New] Twi::Delivery as a wrapper for direct message deliveries
14
+ - [New] Twi::Message as a wrapper for incoming direct messages
data/LICENSE.txt CHANGED
@@ -1,22 +1,21 @@
1
- Copyright (c) 2014 claudiob
1
+ The MIT License (MIT)
2
2
 
3
- MIT License
3
+ Copyright (c) 2026 HouseAccount
4
4
 
5
- Permission is hereby granted, free of charge, to any person obtaining
6
- a copy of this software and associated documentation files (the
7
- "Software"), to deal in the Software without restriction, including
8
- without limitation the rights to use, copy, modify, merge, publish,
9
- distribute, sublicense, and/or sell copies of the Software, and to
10
- permit persons to whom the Software is furnished to do so, subject to
11
- the following conditions:
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:
12
11
 
13
- The above copyright notice and this permission notice shall be
14
- included in all copies or substantial portions of the Software.
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
15
14
 
16
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
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 CHANGED
@@ -1,29 +1,96 @@
1
- # Twi
1
+ # The enhanced Twilio API Ruby client
2
2
 
3
- TODO: Write a gem description
3
+ ## Available methods
4
4
 
5
- ## Installation
5
+ ### Twi::Message
6
6
 
7
- Add this line to your application's Gemfile:
7
+ When receiving an incoming direct message via webhook:
8
8
 
9
- gem 'twi'
9
+ ```ruby
10
+ message = Twi::Message.new params
11
+ message.id # => 'SM083e290bef7794c407f14e22a891aa6d'
12
+ message.content # => 'Hello world'
13
+ message.sender # => '8009007000'
14
+ message.recipient # => '8008008000'
15
+ message.wallflower # nil
16
+ message.opt_in? # false
17
+ message.opt_out? # false
18
+ message.image_urls # => ['https://example.com/image.png']
19
+ ```
10
20
 
11
- And then execute:
21
+ When building a Twilio-like webhook payload:
12
22
 
13
- $ bundle
23
+ ```ruby
24
+ Twi::Message.params_for id: 'SM12', content: 'Hello', sender: '8009007000', recipient: '8008008000'
25
+ # => { MessageSid: 'SM12', Body: 'Hello',From: '+18009007000', To: '+18008008000' }
26
+ ```
14
27
 
15
- Or install it yourself as:
28
+ ### Twi::Delivery
16
29
 
17
- $ gem install twi
30
+ When receiving a delivery notification via webhook:
18
31
 
19
- ## Usage
32
+ ```ruby
33
+ delivery = Twi::Delivery.new params
34
+ delivery.id # => 'SM083e290bef7794c407f14e22a891aa6d'
35
+ delivery.status # => 'draft'
36
+ delivery.code # => '30006'
37
+ ```
20
38
 
21
- TODO: Write usage instructions here
39
+ When building a Twilio-like webhook payload:
22
40
 
23
- ## Contributing
41
+ ```ruby
42
+ Twi::Delivery.params_for id: 'SM12', status: 'sent'
43
+ # => { SmsSid: 'SM12', MessgeStatus: 'sent', ErrorCode: nil }
44
+ ```
45
+
46
+ ### Twi::Event
47
+
48
+ When receiving events about a conversation:
49
+
50
+ ```ruby
51
+ event = Twi::Event.new params
52
+ event.id # => 'SM083e290bef7794c407f14e22a891aa6d'
53
+ event.conversation_id # => 'CH123'
54
+ event.target # => :participant
55
+ event.participant # => #<Participant id: 'SH12', phone: '9008009000', identity: nil>
56
+ ```
57
+
58
+ ### Twi::Phone
59
+
60
+ To create an incoming phone number:
61
+
62
+ ```ruby
63
+ phone = Twi.create_phone area_code:, friendly_name:
64
+ phone.id # => 'SM083e290bef7794c407f14e65a891aa6d'
65
+ phone.number # => '5556667777'
66
+ ```
67
+
68
+ ## Available mocks
69
+
70
+ Use these methods to mock request to Twilio when testing an app:
71
+
72
+ ### Credentials
73
+
74
+ Mock an error when creating an incoming phone number:
75
+
76
+ ```ruby
77
+ Jbr.mock.phone_error = { code: '21452' }
78
+ ```
79
+
80
+ Mock successfully creating an incoming phone number:
81
+
82
+ ```ruby
83
+ Jbr.mock.phone = { id: 'SM083e290bef7794c407f14e65a891aa6d', number: '8009005000' }
84
+ ```
85
+
86
+
87
+ ## To Do
88
+
89
+ 4. have an interface to send and receive SMS with photos
90
+ 8. have an error code URL for each error code and a sid_url
91
+ 9. Declare some phones like Twilio.homeowner_phone or Twilio.numbers[:ddd] and a default Twilio.number and similar Twilio.messaging_service
92
+ 10. a way to reopen closed conversations
93
+ 11. a way to create conversations
94
+ 12. and upload pictures in a conversation
95
+ 13. x_twilio_webhook_enabled: true
24
96
 
25
- 1. Fork it ( https://github.com/[my-github-username]/twi/fork )
26
- 2. Create your feature branch (`git checkout -b my-new-feature`)
27
- 3. Commit your changes (`git commit -am 'Add some feature'`)
28
- 4. Push to the branch (`git push origin my-new-feature`)
29
- 5. Create a new Pull Request
data/lib/twi/config.rb ADDED
@@ -0,0 +1,37 @@
1
+ # Enhances the Twilio Ruby gem with an object-oriented approach.
2
+ module Twi
3
+ # Provides methods to read and write global configuration settings.
4
+ #
5
+ # A typical usage is to set the +sid+ and +secret+ to interact with Twilio API.
6
+ #
7
+ # @example Set the +sid+ and +secret+ to interact with Twilio API:
8
+ # Twi.configure do |twilio|
9
+ # twilio.sid = 'AC8776d'
10
+ # twilio.secret = '588213'
11
+ # end
12
+ #
13
+ # Note that Twi.configure has precedence over values through with
14
+ # environment variables (see {Twi::Lio}).
15
+ #
16
+ module Config
17
+ # Yields the global configuration to the given block.
18
+ #
19
+ # @example
20
+ # Twi.configure do |twilio|
21
+ # twilio.sid = 'AC8776d'
22
+ # end
23
+ #
24
+ # @yield [Twi::Lio] The global configuration.
25
+ def configure
26
+ yield lio if block_given?
27
+ end
28
+
29
+ # @return [Twi::Lio] The global configuration.
30
+ def lio
31
+ @lio ||= Twi::Lio.new
32
+ end
33
+ end
34
+
35
+ # @note Config is tauto-loaded in the Twi module to be able to invoke +Twi.configure+.
36
+ extend Config
37
+ end
@@ -0,0 +1,82 @@
1
+ # Enhances the Twilio Ruby gem with an object-oriented approach.
2
+ module Twi
3
+ # The representation of a (classic) group conversation.
4
+ class Conversation < Resource
5
+ attr_reader :id, :status
6
+
7
+ def create_with(participants:)
8
+ params = create_params_for participants
9
+ conversation = conversation_service.conversation_with_participants.create **params
10
+
11
+ @id = conversation.sid
12
+ @status = conversation.state
13
+ rescue Twilio::REST::RestError => error
14
+ case error.code
15
+ when 50438 then raise ExistingConversationError.new(error.error_message)
16
+ when 50214 then raise TooManyConversationsError.new(error.error_message)
17
+ else raise
18
+ end
19
+ end
20
+
21
+ def rename(friendly_name)
22
+ conversation.update friendly_name: friendly_name
23
+ end
24
+
25
+ def close
26
+ conversation.update state: :closed
27
+ end
28
+
29
+ def delete
30
+ conversation.delete
31
+ end
32
+
33
+ def create_message(content:, image_ids: [])
34
+ conversation.messages.create **message_params_for(content, image_ids)
35
+ end
36
+
37
+ # TODO: Move into Medium -- this method doesn't use anything from this class
38
+ def upload(file)
39
+ uri = URI "https://mcs.us1.twilio.com/v1/Services/#{Twi.lio.conversation_sid}/Media"
40
+ http = Net::HTTP.new(uri.host, uri.port)
41
+ http.use_ssl = true
42
+
43
+ headers = { 'Content-Type' => file.content_type, 'Content-Size' => file.byte_size.to_s }
44
+ request = Net::HTTP::Post.new uri.request_uri, headers
45
+ request.basic_auth Twi.lio.api_key, Twi.lio.secret
46
+ request.body = file.download
47
+ response = http.request request
48
+ JSON(response.body)['sid']
49
+ end
50
+
51
+ private
52
+
53
+ def conversation
54
+ conversation_service.conversations @params[:id]
55
+ end
56
+
57
+ def create_params_for(participants)
58
+ {
59
+ messaging_service_sid: Twi.lio.messaging_sid, x_twilio_webhook_enabled: 'true',
60
+ friendly_name: @params[:friendly_name], participant: participant_params_for(participants),
61
+ }
62
+ end
63
+
64
+ def participant_params_for(participants)
65
+ participants.map do |participant|
66
+ phone = "+1#{participant[:phone]}"
67
+ if participant[:identity]
68
+ { messaging_binding: { projected_address: phone }, identity: participant[:identity] }
69
+ else
70
+ { messaging_binding: { address: phone } }
71
+ end
72
+ end.map(&:to_json)
73
+ end
74
+
75
+ def message_params_for(content, image_ids)
76
+ {
77
+ author: @params[:author], body: content,
78
+ media_sid: image_ids, x_twilio_webhook_enabled: 'true',
79
+ }.compact_blank
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,10 @@
1
+ # Enhances the Twilio Ruby gem with an object-oriented approach.
2
+ module Twi
3
+ # An event signaling a status change for a conversation.
4
+ class ConversationEvent < Event
5
+ # @return [Hash] the shape of the payload send by Twilio to the callback URL.
6
+ def self.params_for(id:, status:)
7
+ { ConversationSid: id, EventType: 'onConversationChanged', State: status.to_s }
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,20 @@
1
+ # Enhances the Twilio Ruby gem with an object-oriented approach.
2
+ module Twi
3
+ # The representation of a direct message delivery notification.
4
+ class Delivery < Resource
5
+ # @return [String] unique identifier
6
+ def id = @params['SmsSid']
7
+
8
+ # @return [String] delivery status, one of accepted, scheduled, queued, sending, sent,
9
+ # delivery_unknown, delivered, undelivered, failed # TODO: make an enum
10
+ def status = @params['MessageStatus']
11
+
12
+ # @return [String, nil] error code
13
+ def code = @params['ErrorCode']
14
+
15
+ # @return [Hash] the shape of the payload send by Twilio to the callback URL.
16
+ def self.params_for(id:, status:, code: nil)
17
+ { SmsSid: id, MessageStatus: status, ErrorCode: code }
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ # Enhances the Twilio Ruby gem with an object-oriented approach.
2
+ module Twi
3
+ # An event signaling the delivery of a message in a conversation.
4
+ class DeliveryEvent < Event
5
+ # @return [Hash] the shape of the payload send by Twilio to the callback URL.
6
+ def self.params_for(id:, participant_id:, message_id:, status:, code: nil)
7
+ {
8
+ ConversationSid: id, EventType: 'onDeliveryChanged', ParticipantSid: participant_id.to_s,
9
+ MessageSid: message_id, Status: status.to_s, ErrorCode: code
10
+ }.compact
11
+ end
12
+ end
13
+ end
data/lib/twi/error.rb ADDED
@@ -0,0 +1,11 @@
1
+ module Twi
2
+ class Error < StandardError
3
+ end
4
+
5
+ class ExistingConversationError < Error
6
+ def conversation_id = message[/Conversation (.+)/, 1]
7
+ end
8
+
9
+ class TooManyConversationsError < Error
10
+ end
11
+ end
data/lib/twi/event.rb ADDED
@@ -0,0 +1,56 @@
1
+ # Enhances the Twilio Ruby gem with an object-oriented approach.
2
+ module Twi
3
+ # An event tied to a (classic) conversation.
4
+ class Event < Resource
5
+ # @return [Symbol] event target type, can be :conversation, :participant, :message, :delivery.
6
+ def target = @params['EventType'].underscore.split('_').second.to_sym
7
+
8
+ # @return [String] conversation state, one of active, inactive, closed, initializing.
9
+ def status = @params['Status'] || @params['StateTo'] || @params['State']
10
+
11
+ # @return [Participant] the participant the event is about (e.g.: joined the conversation).
12
+ def participant = Participant.new @params
13
+
14
+ # @return [String] unique message identifier
15
+ def id = @params['MessageSid']
16
+
17
+ # @return [String, nil] content
18
+ def content = @params['Body']&.squish
19
+
20
+ # @return [Array<String>] URLs of image attachments
21
+ def image_urls
22
+ media = JSON(@params.fetch('Media', '[]')).map { |params| medium_for params }
23
+ media.filter(&:image?).map { |image| image.url }
24
+ end
25
+
26
+ # @return [String] unique conversation identifier
27
+ def conversation_id = @params['ConversationSid']
28
+
29
+ # @return [String, nil] error code
30
+ def code = @params['ErrorCode']
31
+
32
+ # @return [Hash] the shape of the payload send by Twilio to the callback URL.
33
+ def self.params_for(id:, type:, status: nil, participant: nil)
34
+ {
35
+ ConversationSid: id, EventType: "on_#{type}_changed".camelize(:lower),
36
+ State: status&.to_s,
37
+ }.merge(participant_params_for participant).compact
38
+ end
39
+
40
+ def self.participant_params_for(participant = nil)
41
+ if participant
42
+ if participant[:identity]
43
+ { ParticipantSid: participant[:id], Identity: participant[:identity] }
44
+ else
45
+ { ParticipantSid: participant[:id], 'MessagingBinding.Address' => "+1#{participant[:phone]}" }
46
+ end
47
+ else
48
+ {}
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def medium_for(params) = Medium.new params
55
+ end
56
+ end
data/lib/twi/lio.rb ADDED
@@ -0,0 +1,63 @@
1
+ # Enhances the Twilio Ruby gem with an object-oriented approach.
2
+ module Twi
3
+ # Provides an object to store global configuration settings.
4
+ #
5
+ # This class is not used directly, but by calling {Twi::Config#configure Twi.configure},
6
+ # which creates and updates a single instance of {Twi::Lio}.
7
+ #
8
+ # @example Set the +sid+ and +secret+ to interact with Twilio API:
9
+ # Twi.configure do |config|
10
+ # twilio.sid = 'AC8776d'
11
+ # twilio.secret = '588213'
12
+ # end
13
+ #
14
+ # @see Twi::Lio for more examples.
15
+ #
16
+ # An alternative way to set global configuration settings is by storing
17
+ # them in the following environment variables:
18
+ #
19
+ # * +TWILIO_SID+ to store the Twilio SID
20
+ # * +TWILIO_SECRET+ to store the Twilio secret
21
+ # * +TWILIO_CONVERSATION_SERVICE_SID+ to store the Twilio Conversation service ID
22
+ #
23
+ # In case both methods are used together,
24
+ # {Twi::Config#configure Twi.configure} takes precedence.
25
+ #
26
+ # @example Set the +sid+ and +secret+ to interact with Twilio API:
27
+ # ENV['TWILIO_SID'] = 'AC8776d'
28
+ # ENV['TWILIO_SECRET'] = '588213'
29
+ #
30
+ class Lio
31
+ # Initialize the global configuration settings defaulting to matching environment variables.
32
+ def initialize
33
+ @api_key = ENV['TWILIO_SID']
34
+ @secret = ENV['TWILIO_SECRET']
35
+ @account_sid = ENV['TWILIO_ACCOUNT_SID']
36
+ @auth_token = ENV['TWILIO_AUTH_TOKEN']
37
+ @conversation_sid = ENV['TWILIO_CONVERSATION_SERVICE_SID']
38
+ @messaging_sid = ENV['TWILIO_MESSAGING_SID']
39
+ @emergency_address_sid = ENV['TWILIO_EMERGENCY_SID']
40
+ end
41
+
42
+ # @return [String] API key - used for basic operations like sending messages.
43
+ attr_reader :api_key
44
+
45
+ # @return [String] secret - to authenticate API requests made with +api_key+.
46
+ attr_reader :secret
47
+
48
+ # @return [String] Account SID - used for advanced operations like creating numbers.
49
+ attr_reader :account_sid
50
+
51
+ # @return [String] OAuth token - to authenticate API requests made with +account_sid+.
52
+ attr_reader :auth_token
53
+
54
+ # @return [String] the SID of the default Conversation service.
55
+ attr_reader :conversation_sid
56
+
57
+ # @return [String] the SID of the default Messaging service.
58
+ attr_reader :messaging_sid
59
+
60
+ # @return [String] the SID of the emergency address.
61
+ attr_reader :emergency_address_sid
62
+ end
63
+ end
data/lib/twi/medium.rb ADDED
@@ -0,0 +1,33 @@
1
+ # Enhances the Twilio Ruby gem with an object-oriented approach.
2
+ module Twi
3
+ # The representation of a medium (image) attached to a message.
4
+ class Medium < Resource
5
+ # @return [String] unique identifier
6
+ def id = @params['Sid']
7
+
8
+ # @return [Boolean] whether the medium has the content type of an image.
9
+ def image? = content_type.match? %r{^image/}
10
+
11
+ # @return [String] the content type
12
+ def content_type = @params['ContentType']
13
+
14
+ # @return [String] a URL where the image can be accessed at least for a few minutes.
15
+ def url
16
+ uri = URI service_url
17
+ http = Net::HTTP.new(uri.host, uri.port)
18
+ http.use_ssl = true
19
+
20
+ request = Net::HTTP::Get.new(uri.request_uri)
21
+ request.basic_auth Twi.lio.api_key, Twi.lio.secret
22
+ response = http.request(request)
23
+
24
+ JSON(response.body).dig 'links', 'content_direct_temporary'
25
+ end
26
+
27
+ private
28
+
29
+ def service_url
30
+ "https://mcs.us1.twilio.com/v1/Services/#{Twi.lio.conversation_sid}/Media/#{id}"
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,77 @@
1
+ # Enhances the Twilio Ruby gem with an object-oriented approach.
2
+ module Twi
3
+ # The representation of a direct message.
4
+ class Message < Resource
5
+ # @return [String] unique identifier
6
+ def id = @params['MessageSid']
7
+
8
+ # @return [String, nil] content
9
+ def content = @params['Body']&.squish
10
+
11
+ # @return [String] 10-digit phone number sending the SMS.
12
+ def sender = remove_prefix_from @params['From']
13
+
14
+ # @return [String] 10-digit phone number receiving the SMS.
15
+ def recipient = remove_prefix_from @params['To']
16
+
17
+ # @return [String] 10-digit phone number of any CC'd recipient.
18
+ def wallflower = remove_prefix_from @params['OtherRecipients0']
19
+
20
+ # @return [Boolean] whether the sender replied STOP to unsubscribe.
21
+ def opt_out? = @params['OptOutType'] == 'STOP'
22
+
23
+ # @return [Boolean] whether the sender replied START to resubscribe.
24
+ def opt_in? = @params['OptOutType'] == 'START'
25
+
26
+ # @return [Array<String>] URLs of image attachments
27
+ def image_urls
28
+ (0...@params['NumMedia'].to_i).filter_map do |index|
29
+ @params["MediaUrl#{index}"] if @params["MediaContentType#{index}"].match? %r{^image/}
30
+ end
31
+ end
32
+
33
+ attr_reader :status
34
+
35
+ # Sends a message.
36
+ def create
37
+ message = api_client.messages.create messaging_service_sid: Twi.lio.messaging_sid.to_s,
38
+ from: "+1#{@params[:sender]}", to: "+1#{@params[:recipient]}", body: @params[:content]
39
+
40
+ @id = message.sid
41
+ @status = message.status
42
+ # todo rescue and then set @code
43
+ end
44
+
45
+ # @return [Hash] the shape of the payload send by Twilio to the callback URL.
46
+ def self.params_for(id:, sender:, recipient:, wallflower: nil, content: nil, opt: nil, media: [])
47
+ {
48
+ MessageSid: id,
49
+ From: "+1#{sender}",
50
+ To: "+1#{recipient}",
51
+ Body: content,
52
+ }.merge media_params_for(media).merge opt_params_for(opt).merge wallflower_params_for(wallflower)
53
+ end
54
+
55
+ private
56
+
57
+ def self.media_params_for(media = [])
58
+ media.each_with_index.inject({ NumMedia: media.size.to_s }) do |hash, (item, index)|
59
+ url_key = "MediaUrl#{index}".to_sym
60
+ content_type_key = "MediaContentType#{index}".to_sym
61
+ hash.merge url_key => item[:url], content_type_key => item[:content_type]
62
+ end
63
+ end
64
+
65
+ def self.opt_params_for(opt = nil)
66
+ case opt
67
+ when :out then { OptOutType: 'STOP' }
68
+ when :in then { OptOutType: 'START' }
69
+ else {}
70
+ end
71
+ end
72
+
73
+ def self.wallflower_params_for(wallflower = nil)
74
+ wallflower ? { OtherRecipients0: "+1#{wallflower}" } : {}
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,21 @@
1
+ # Enhances the Twilio Ruby gem with an object-oriented approach.
2
+ module Twi
3
+ # An event signaling a new message in a conversation.
4
+ class MessageEvent < Event
5
+ # @return [Hash] the shape of the payload send by Twilio to the callback URL.
6
+ def self.params_for(id:, participant_id:, content: nil, media: [])
7
+ {
8
+ ConversationSid: id, EventType: 'onMessageChanged', ParticipantSid: participant_id.to_s,
9
+ MessageSid: "SM#{rand}", Body: content, Media: media_params_for(media),
10
+ }.compact_blank
11
+ end
12
+
13
+ private
14
+
15
+ def self.media_params_for(media)
16
+ media.map do |medium|
17
+ { Sid: medium[:id], ContentType: medium[:content_type] }
18
+ end.to_json if media.present?
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,29 @@
1
+ module Twi
2
+ class Mock::Conversation < Conversation
3
+ def create_with(participants:)
4
+ if Twi.mock.conversation_error
5
+ if Twi.mock.conversation_error[:code] == 50438
6
+ raise ExistingConversationError.new(Twi.mock.conversation_error[:message])
7
+ elsif Twi.mock.conversation_error[:code] == 50214
8
+ Twi.mock.conversation_error = nil
9
+ raise TooManyConversationsError
10
+ end
11
+ elsif Twi.mock.conversation
12
+ @id = Twi.mock.conversation[:id]
13
+ @status = Twi.mock.conversation[:status]
14
+ end
15
+ end
16
+
17
+ def upload(file) = 'fake-sid'
18
+
19
+ def rename(friendly_name); end
20
+
21
+ def close; end
22
+
23
+ def delete; end
24
+
25
+ def create_message(content:, image_ids: [])
26
+ Mock::Message.new Twi.mock.message
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,6 @@
1
+ module Twi
2
+ class Mock::Event < Event
3
+ private
4
+ def medium_for(params) = Mock::Medium.new params
5
+ end
6
+ end
@@ -0,0 +1,8 @@
1
+ module Twi
2
+ class Mock::Medium < Medium
3
+ # @return [String] a mock URL for the image.
4
+ def url
5
+ Twi.mock.medium[:url] if Twi.mock.medium
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,12 @@
1
+ module Twi
2
+ class Mock::Message < Message
3
+ # @return [String] unique identifier
4
+ def id = @params[:id]
5
+
6
+ def create
7
+ @id = "SM#{rand}"
8
+ @status = 'delivered'
9
+ # todo rescue and then set @code
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ module Twi
2
+ class Mock::Phone < Phone
3
+ def create
4
+ if Twi.mock.phone
5
+ @id = Twi.mock.phone[:id]
6
+ @number = Twi.mock.phone[:number]
7
+ elsif Twi.mock.phone_error
8
+ raise Error, Twi.mock.phone_error
9
+ end
10
+ end
11
+ end
12
+ end
data/lib/twi/mock.rb ADDED
@@ -0,0 +1,5 @@
1
+ module Twi
2
+ class Mock
3
+ attr_accessor :phone, :phone_error, :message, :conversation, :conversation_error, :medium
4
+ end
5
+ end
@@ -0,0 +1,33 @@
1
+ module Twi
2
+ module Mocking
3
+ def mock
4
+ @mock ||= Twi::Mock.new
5
+ end
6
+
7
+ def create_phone(...)
8
+ phone(...).tap &:create
9
+ end
10
+
11
+ def create_message(...)
12
+ message(...).tap &:create
13
+ end
14
+
15
+ def conversation(...)
16
+ (@mock ? Mock::Conversation : Conversation).new(...)
17
+ end
18
+
19
+ def event(...)
20
+ (@mock ? Mock::Event : Event).new(...)
21
+ end
22
+
23
+ def phone(...)
24
+ (@mock ? Mock::Phone : Phone).new(...)
25
+ end
26
+
27
+ def message(...)
28
+ (@mock ? Mock::Message : Message).new(...)
29
+ end
30
+ end
31
+
32
+ extend Mocking
33
+ end
@@ -0,0 +1,14 @@
1
+ # Enhances the Twilio Ruby gem with an object-oriented approach.
2
+ module Twi
3
+ # The representation of a participant in a (clasic) conversation.
4
+ class Participant < Resource
5
+ # @return [String] unique identifier.
6
+ def id = @params['ParticipantSid']
7
+
8
+ # @return [String] 10-digit phone number.
9
+ def phone = remove_prefix_from @params['MessagingBinding.Address']
10
+
11
+ # @return [String, nil] optional identifier.
12
+ def identity = @params['Identity']
13
+ end
14
+ end
@@ -0,0 +1,19 @@
1
+ # Enhances the Twilio Ruby gem with an object-oriented approach.
2
+ module Twi
3
+ # An event signaling a participant added to a conversation.
4
+ class ParticipantEvent < Event
5
+ # @return [Hash] the shape of the payload send by Twilio to the callback URL.
6
+ def self.params_for(id:, participant_id:, identity: nil, phone: nil)
7
+ {
8
+ ConversationSid: id, EventType: 'onParticipantChanged',
9
+ ParticipantSid: participant_id.to_s
10
+ }.merge participant_params_for(identity, phone)
11
+ end
12
+
13
+ private
14
+
15
+ def self.participant_params_for(identity, phone)
16
+ identity ? { Identity: identity } : { 'MessagingBinding.Address' => "+1#{phone}" }
17
+ end
18
+ end
19
+ end
data/lib/twi/phone.rb ADDED
@@ -0,0 +1,21 @@
1
+ # Enhances the Twilio Ruby gem with an object-oriented approach.
2
+ module Twi
3
+ # The representation of a phone number associated to the default messaging service.
4
+ class Phone < Resource
5
+ # Create an incoming phone number within the area code and friendly name.
6
+ def create
7
+ phone = client.incoming_phone_numbers.create area_code: @params[:area_code],
8
+ emergency_address_sid: Twi.lio.emergency_address_sid, friendly_name: @params[:friendly_name]
9
+ messaging_service.phone_numbers.create phone_number_sid: phone.sid
10
+
11
+ @id = phone.sid
12
+ @number = remove_prefix_from phone.phone_number
13
+ end
14
+
15
+ # @return [String] unique identifier of the phone.
16
+ attr_reader :id
17
+
18
+ # @return [String] 10-digit number.
19
+ attr_reader :number
20
+ end
21
+ end
@@ -0,0 +1,22 @@
1
+ # Enhances the Twilio Ruby gem with an object-oriented approach.
2
+ module Twi
3
+ # An abtract class for all Twilio resources.
4
+ class Resource
5
+ # @param params [ActionController::Parameters] the payload of Twilio hitting a callback URL.
6
+ def initialize(params = {})
7
+ @params = params
8
+ end
9
+
10
+ private
11
+
12
+ def remove_prefix_from(number) = number&.strip&.delete_prefix '+1'
13
+
14
+ def client = Twilio::REST::Client.new Twi.lio.account_sid, Twi.lio.auth_token
15
+
16
+ def api_client = Twilio::REST::Client.new Twi.lio.api_key, Twi.lio.secret
17
+
18
+ def messaging_service = client.messaging.v1.services Twi.lio.messaging_sid.to_s
19
+
20
+ def conversation_service = client.conversations.v1.services Twi.lio.conversation_sid.to_s
21
+ end
22
+ end
data/lib/twi/version.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Twi
2
- VERSION = "0.0.1"
4
+ VERSION = '0.3.0'
3
5
  end
data/lib/twi.rb CHANGED
@@ -1,5 +1,33 @@
1
- require "twi/version"
1
+ # frozen_string_literal: true
2
2
 
3
- module Twi
4
- # Your code goes here...
5
- end
3
+ require 'action_controller'
4
+ require 'action_controller/metal/strong_parameters'
5
+ require 'active_support/core_ext/enumerable'
6
+ require 'active_support/core_ext/object/blank'
7
+ require 'json'
8
+ require 'net/http'
9
+ require 'twilio-ruby'
10
+
11
+ require 'twi/lio'
12
+ require 'twi/error'
13
+ require 'twi/config'
14
+ require 'twi/resource'
15
+ require 'twi/message'
16
+ require 'twi/delivery'
17
+ require 'twi/participant'
18
+ require 'twi/medium'
19
+ require 'twi/event'
20
+ require 'twi/conversation_event'
21
+ require 'twi/delivery_event'
22
+ require 'twi/message_event'
23
+ require 'twi/participant_event'
24
+ require 'twi/phone'
25
+ require 'twi/conversation'
26
+
27
+ require 'twi/mock'
28
+ require 'twi/mock/phone'
29
+ require 'twi/mock/conversation'
30
+ require 'twi/mock/event'
31
+ require 'twi/mock/medium'
32
+ require 'twi/mock/message'
33
+ require 'twi/mocking'
metadata CHANGED
@@ -1,63 +1,83 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: twi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
- - claudiob
8
- autorequire:
9
- bindir: bin
7
+ - Claudio Baccigalupo
8
+ bindir: exe
10
9
  cert_chain: []
11
- date: 2014-10-10 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
- name: bundler
13
+ name: actionpack
15
14
  requirement: !ruby/object:Gem::Requirement
16
15
  requirements:
17
- - - "~>"
16
+ - - ">="
18
17
  - !ruby/object:Gem::Version
19
- version: '1.6'
20
- type: :development
18
+ version: '0'
19
+ type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
- - - "~>"
23
+ - - ">="
25
24
  - !ruby/object:Gem::Version
26
- version: '1.6'
25
+ version: '0'
27
26
  - !ruby/object:Gem::Dependency
28
- name: rake
27
+ name: twilio-ruby
29
28
  requirement: !ruby/object:Gem::Requirement
30
29
  requirements:
31
30
  - - ">="
32
31
  - !ruby/object:Gem::Version
33
32
  version: '0'
34
- type: :development
33
+ type: :runtime
35
34
  prerelease: false
36
35
  version_requirements: !ruby/object:Gem::Requirement
37
36
  requirements:
38
37
  - - ">="
39
38
  - !ruby/object:Gem::Version
40
39
  version: '0'
41
- description: Write a longer description. Optional.
40
+ description: Enhances OpenSSL::HMAC with timestamp.
42
41
  email:
43
- - claudiob@gmail.com
42
+ - claudiob@users.noreply.github.com
44
43
  executables: []
45
44
  extensions: []
46
45
  extra_rdoc_files: []
47
46
  files:
48
- - ".gitignore"
49
- - Gemfile
47
+ - CHANGELOG.md
50
48
  - LICENSE.txt
51
49
  - README.md
52
- - Rakefile
53
50
  - lib/twi.rb
51
+ - lib/twi/config.rb
52
+ - lib/twi/conversation.rb
53
+ - lib/twi/conversation_event.rb
54
+ - lib/twi/delivery.rb
55
+ - lib/twi/delivery_event.rb
56
+ - lib/twi/error.rb
57
+ - lib/twi/event.rb
58
+ - lib/twi/lio.rb
59
+ - lib/twi/medium.rb
60
+ - lib/twi/message.rb
61
+ - lib/twi/message_event.rb
62
+ - lib/twi/mock.rb
63
+ - lib/twi/mock/conversation.rb
64
+ - lib/twi/mock/event.rb
65
+ - lib/twi/mock/medium.rb
66
+ - lib/twi/mock/message.rb
67
+ - lib/twi/mock/phone.rb
68
+ - lib/twi/mocking.rb
69
+ - lib/twi/participant.rb
70
+ - lib/twi/participant_event.rb
71
+ - lib/twi/phone.rb
72
+ - lib/twi/resource.rb
54
73
  - lib/twi/version.rb
55
- - twi.gemspec
56
- homepage: ''
74
+ homepage: https://github.com/claudiob/twi
57
75
  licenses:
58
76
  - MIT
59
- metadata: {}
60
- post_install_message:
77
+ metadata:
78
+ homepage_uri: https://github.com/claudiob/twi
79
+ source_code_uri: https://github.com/claudiob/twi
80
+ changelog_uri: https://github.com/claudiob/twi
61
81
  rdoc_options: []
62
82
  require_paths:
63
83
  - lib
@@ -65,17 +85,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
65
85
  requirements:
66
86
  - - ">="
67
87
  - !ruby/object:Gem::Version
68
- version: '0'
88
+ version: 3.2.0
69
89
  required_rubygems_version: !ruby/object:Gem::Requirement
70
90
  requirements:
71
91
  - - ">="
72
92
  - !ruby/object:Gem::Version
73
93
  version: '0'
74
94
  requirements: []
75
- rubyforge_project:
76
- rubygems_version: 2.2.2
77
- signing_key:
95
+ rubygems_version: 4.0.3
78
96
  specification_version: 4
79
- summary: Write a short summary. Required.
97
+ summary: API Signature with timestamped MAC (Message Authentication Code).
80
98
  test_files: []
81
- has_rdoc:
data/.gitignore DELETED
@@ -1,22 +0,0 @@
1
- *.gem
2
- *.rbc
3
- .bundle
4
- .config
5
- .yardoc
6
- Gemfile.lock
7
- InstalledFiles
8
- _yardoc
9
- coverage
10
- doc/
11
- lib/bundler/man
12
- pkg
13
- rdoc
14
- spec/reports
15
- test/tmp
16
- test/version_tmp
17
- tmp
18
- *.bundle
19
- *.so
20
- *.o
21
- *.a
22
- mkmf.log
data/Gemfile DELETED
@@ -1,4 +0,0 @@
1
- source 'https://rubygems.org'
2
-
3
- # Specify your gem's dependencies in twi.gemspec
4
- gemspec
data/Rakefile DELETED
@@ -1,2 +0,0 @@
1
- require "bundler/gem_tasks"
2
-
data/twi.gemspec DELETED
@@ -1,23 +0,0 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'twi/version'
5
-
6
- Gem::Specification.new do |spec|
7
- spec.name = "twi"
8
- spec.version = Twi::VERSION
9
- spec.authors = ["claudiob"]
10
- spec.email = ["claudiob@gmail.com"]
11
- spec.summary = %q{Write a short summary. Required.}
12
- spec.description = %q{Write a longer description. Optional.}
13
- spec.homepage = ""
14
- spec.license = "MIT"
15
-
16
- spec.files = `git ls-files -z`.split("\x0")
17
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
- spec.require_paths = ["lib"]
20
-
21
- spec.add_development_dependency "bundler", "~> 1.6"
22
- spec.add_development_dependency "rake"
23
- end