├── example
└── rails-4.2
│ ├── log
│ └── .keep
│ ├── lib
│ ├── tasks
│ │ └── .keep
│ └── assets
│ │ └── .keep
│ ├── app
│ ├── mailers
│ │ └── .keep
│ ├── models
│ │ ├── .keep
│ │ └── user.rb
│ ├── assets
│ │ ├── images
│ │ │ └── .keep
│ │ ├── javascripts
│ │ │ └── application.js
│ │ └── stylesheets
│ │ │ ├── application.css
│ │ │ └── scaffold.css
│ ├── helpers
│ │ └── application_helper.rb
│ ├── views
│ │ ├── user_texter
│ │ │ └── welcome.text.erb
│ │ ├── users
│ │ │ ├── new.html.erb
│ │ │ ├── index.html.erb
│ │ │ └── _form.html.erb
│ │ └── layouts
│ │ │ └── application.html.erb
│ ├── controllers
│ │ ├── application_controller.rb
│ │ └── users_controller.rb
│ └── texters
│ │ └── user_texter.rb
│ ├── public
│ ├── favicon.ico
│ ├── robots.txt
│ ├── 500.html
│ ├── 422.html
│ └── 404.html
│ ├── vendor
│ └── assets
│ │ ├── javascripts
│ │ └── .keep
│ │ └── stylesheets
│ │ └── .keep
│ ├── Gemfile
│ ├── bin
│ ├── bundle
│ ├── rake
│ ├── rails
│ ├── spring
│ └── setup
│ ├── config
│ ├── routes.rb
│ ├── boot.rb
│ ├── initializers
│ │ ├── cookies_serializer.rb
│ │ ├── session_store.rb
│ │ ├── mime_types.rb
│ │ ├── filter_parameter_logging.rb
│ │ ├── backtrace_silencers.rb
│ │ ├── assets.rb
│ │ ├── wrap_parameters.rb
│ │ └── inflections.rb
│ ├── environment.rb
│ ├── database.yml
│ ├── locales
│ │ └── en.yml
│ ├── secrets.yml
│ ├── application.rb
│ └── environments
│ │ ├── development.rb
│ │ ├── test.rb
│ │ └── production.rb
│ ├── config.ru
│ ├── db
│ ├── migrate
│ │ └── 20150220111225_create_users.rb
│ ├── seeds.rb
│ └── schema.rb
│ ├── Rakefile
│ ├── .gitignore
│ └── Gemfile.lock
├── .rspec
├── Gemfile
├── lib
├── textris
│ ├── version.rb
│ ├── delivery
│ │ ├── base.rb
│ │ ├── nexmo.rb
│ │ ├── test.rb
│ │ ├── twilio.rb
│ │ ├── log.rb
│ │ └── mail.rb
│ ├── delay
│ │ ├── active_job.rb
│ │ ├── active_job
│ │ │ ├── missing.rb
│ │ │ └── job.rb
│ │ ├── sidekiq
│ │ │ ├── missing.rb
│ │ │ ├── worker.rb
│ │ │ ├── proxy.rb
│ │ │ └── serializer.rb
│ │ └── sidekiq.rb
│ ├── delivery.rb
│ ├── phone_formatter.rb
│ ├── base.rb
│ └── message.rb
└── textris.rb
├── .gitignore
├── Rakefile
├── .scrutinizer.yml
├── gemfiles
├── rails_5.gemfile
└── rails_4.gemfile
├── .travis.yml
├── Appraisals
├── CHANGELOG.md
├── LICENSE.md
├── spec
├── textris
│ ├── delivery
│ │ ├── nexmo_spec.rb
│ │ ├── test_spec.rb
│ │ ├── twilio_spec.rb
│ │ ├── log_spec.rb
│ │ └── mail_spec.rb
│ ├── phone_formatter_spec.rb
│ ├── delivery_spec.rb
│ ├── delay
│ │ ├── active_job_spec.rb
│ │ └── sidekiq_spec.rb
│ ├── base_spec.rb
│ └── message_spec.rb
└── spec_helper.rb
├── textris.gemspec
└── README.md
/example/rails-4.2/log/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example/rails-4.2/lib/tasks/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example/rails-4.2/app/mailers/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example/rails-4.2/app/models/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example/rails-4.2/lib/assets/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example/rails-4.2/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example/rails-4.2/app/assets/images/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 | --require spec_helper
3 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gemspec
--------------------------------------------------------------------------------
/example/rails-4.2/vendor/assets/javascripts/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/example/rails-4.2/vendor/assets/stylesheets/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/textris/version.rb:
--------------------------------------------------------------------------------
1 | module Textris
2 | VERSION = '0.7.1'
3 | end
4 |
--------------------------------------------------------------------------------
/example/rails-4.2/app/assets/javascripts/application.js:
--------------------------------------------------------------------------------
1 | //= require_tree .
2 |
--------------------------------------------------------------------------------
/example/rails-4.2/app/helpers/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ApplicationHelper
2 | end
3 |
--------------------------------------------------------------------------------
/example/rails-4.2/app/views/user_texter/welcome.text.erb:
--------------------------------------------------------------------------------
1 | Welcome to our system, <%= @user.name %>!
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | coverage
3 | pkg
4 | .bundle
5 | .ruby-version
6 | *emfile.lock
7 | .idea
8 | textris-*.gem
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/gem_tasks"
2 | require 'rspec/core/rake_task'
3 | task :default => :spec
4 | RSpec::Core::RakeTask.new
--------------------------------------------------------------------------------
/example/rails-4.2/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gem 'rails', '4.2.11'
4 | gem 'sqlite3'
5 | gem 'textris', :path => '../..'
--------------------------------------------------------------------------------
/.scrutinizer.yml:
--------------------------------------------------------------------------------
1 | checks:
2 | ruby:
3 | code_rating: true
4 | duplicate_code: true
5 |
6 | tools:
7 | external_code_coverage: true
8 |
--------------------------------------------------------------------------------
/example/rails-4.2/app/controllers/application_controller.rb:
--------------------------------------------------------------------------------
1 | class ApplicationController < ActionController::Base
2 | protect_from_forgery with: :exception
3 | end
4 |
--------------------------------------------------------------------------------
/example/rails-4.2/bin/bundle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
3 | load Gem.bin_path('bundler', 'bundle')
4 |
--------------------------------------------------------------------------------
/example/rails-4.2/config/routes.rb:
--------------------------------------------------------------------------------
1 | Rails.application.routes.draw do
2 | resources :users, :only => [:index, :new, :create]
3 |
4 | root :to => 'users#new'
5 | end
6 |
--------------------------------------------------------------------------------
/example/rails-4.2/config/boot.rb:
--------------------------------------------------------------------------------
1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
2 |
3 | require 'bundler/setup' # Set up gems listed in the Gemfile.
4 |
--------------------------------------------------------------------------------
/example/rails-4.2/config.ru:
--------------------------------------------------------------------------------
1 | # This file is used by Rack-based servers to start the application.
2 |
3 | require ::File.expand_path('../config/environment', __FILE__)
4 | run Rails.application
5 |
--------------------------------------------------------------------------------
/example/rails-4.2/config/initializers/cookies_serializer.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | Rails.application.config.action_dispatch.cookies_serializer = :json
4 |
--------------------------------------------------------------------------------
/example/rails-4.2/config/environment.rb:
--------------------------------------------------------------------------------
1 | # Load the Rails application.
2 | require File.expand_path('../application', __FILE__)
3 |
4 | # Initialize the Rails application.
5 | Rails.application.initialize!
6 |
--------------------------------------------------------------------------------
/example/rails-4.2/config/initializers/session_store.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | Rails.application.config.session_store :cookie_store, key: '_textris_example_session'
4 |
--------------------------------------------------------------------------------
/example/rails-4.2/bin/rake:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | begin
3 | load File.expand_path("../spring", __FILE__)
4 | rescue LoadError
5 | end
6 | require_relative '../config/boot'
7 | require 'rake'
8 | Rake.application.run
9 |
--------------------------------------------------------------------------------
/example/rails-4.2/config/initializers/mime_types.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new mime types for use in respond_to blocks:
4 | # Mime::Type.register "text/richtext", :rtf
5 |
--------------------------------------------------------------------------------
/example/rails-4.2/app/texters/user_texter.rb:
--------------------------------------------------------------------------------
1 | class UserTexter < Textris::Base
2 | default :from => "Our Team <+48 666-777-888>"
3 |
4 | def welcome(user)
5 | @user = user
6 |
7 | text :to => @user.phone
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/example/rails-4.2/app/views/users/new.html.erb:
--------------------------------------------------------------------------------
1 |
New user
2 |
3 | Fill in a new user, so that application will send SMS with welcome notification.
4 |
5 | <%= render 'form' %>
6 |
7 | <%= link_to 'Back', users_path, :class => 'btn' %>
8 |
--------------------------------------------------------------------------------
/example/rails-4.2/public/robots.txt:
--------------------------------------------------------------------------------
1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 | #
3 | # To ban all spiders from the entire site uncomment the next two lines:
4 | # User-agent: *
5 | # Disallow: /
6 |
--------------------------------------------------------------------------------
/example/rails-4.2/bin/rails:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | begin
3 | load File.expand_path("../spring", __FILE__)
4 | rescue LoadError
5 | end
6 | APP_PATH = File.expand_path('../../config/application', __FILE__)
7 | require_relative '../config/boot'
8 | require 'rails/commands'
9 |
--------------------------------------------------------------------------------
/example/rails-4.2/config/initializers/filter_parameter_logging.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Configure sensitive parameters which will be filtered from the log file.
4 | Rails.application.config.filter_parameters += [:password]
5 |
--------------------------------------------------------------------------------
/gemfiles/rails_5.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "actionmailer", "~> 5.0"
6 | gem "activejob", "~> 5.0"
7 | gem "activesupport", "~> 5.0"
8 | gem "nokogiri", "~> 1.10.4"
9 |
10 | gemspec path: "../"
11 |
--------------------------------------------------------------------------------
/example/rails-4.2/db/migrate/20150220111225_create_users.rb:
--------------------------------------------------------------------------------
1 | class CreateUsers < ActiveRecord::Migration
2 | def change
3 | create_table :users do |t|
4 | t.string :name
5 | t.string :phone
6 |
7 | t.timestamps null: false
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/example/rails-4.2/Rakefile:
--------------------------------------------------------------------------------
1 | # Add your own tasks in files placed in lib/tasks ending in .rake,
2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
3 |
4 | require File.expand_path('../config/application', __FILE__)
5 |
6 | Rails.application.load_tasks
7 |
--------------------------------------------------------------------------------
/gemfiles/rails_4.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "https://rubygems.org"
4 |
5 | gem "actionmailer", "~> 4.2"
6 | gem "activejob", "~> 4.2"
7 | gem "activesupport", "~> 4.2"
8 | gem "rack", "> 1", "< 2"
9 | gem "nokogiri", "~> 1.10.4"
10 |
11 | gemspec path: "../"
12 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | cache: bundler
3 | rvm:
4 | - 2.4.0
5 | - 2.5.0
6 | - 2.6.0
7 | gemfile:
8 | - gemfiles/rails_4.gemfile
9 | - gemfiles/rails_5.gemfile
10 | before_install:
11 | - gem uninstall -v '>= 2' -i $(rvm gemdir)@global -ax bundler || true
12 | - gem install bundler -v '< 2'
13 |
--------------------------------------------------------------------------------
/lib/textris/delivery/base.rb:
--------------------------------------------------------------------------------
1 | module Textris
2 | module Delivery
3 | class Base
4 | attr_reader :message
5 |
6 | def initialize(message)
7 | @message = message
8 | end
9 |
10 | def deliver_to_all
11 | message.to.each do |to|
12 | deliver(to)
13 | end
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/textris/delay/active_job.rb:
--------------------------------------------------------------------------------
1 | module Textris
2 | module Delay
3 | module ActiveJob
4 | def deliver_now
5 | deliver
6 | end
7 |
8 | def deliver_later(options = {})
9 | job = Textris::Delay::ActiveJob::Job
10 |
11 | job.new(texter(:raw => true).to_s, action.to_s, args).enqueue(options)
12 | end
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/example/rails-4.2/db/seeds.rb:
--------------------------------------------------------------------------------
1 | # This file should contain all the record creation needed to seed the database with its default values.
2 | # The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
3 | #
4 | # Examples:
5 | #
6 | # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }])
7 | # Mayor.create(name: 'Emanuel', city: cities.first)
8 |
--------------------------------------------------------------------------------
/Appraisals:
--------------------------------------------------------------------------------
1 | appraise "rails-5" do
2 | gem 'actionmailer', '~> 5.0'
3 | gem 'activejob', '~> 5.0'
4 | gem 'activesupport', '~> 5.0'
5 | gem 'nokogiri', '~> 1.10.4'
6 | end
7 |
8 | appraise "rails-4" do
9 | gem 'actionmailer', '~> 4.2'
10 | gem 'activejob', '~> 4.2'
11 | gem 'activesupport', '~> 4.2'
12 | gem 'rack', '> 1', '< 2'
13 | gem 'nokogiri', '~> 1.10.4'
14 | end
15 |
--------------------------------------------------------------------------------
/lib/textris/delay/active_job/missing.rb:
--------------------------------------------------------------------------------
1 | module Textris
2 | module Delay
3 | module ActiveJob
4 | module Missing
5 | def active_job_missing(*args)
6 | raise(LoadError, "ActiveJob is required to delay sending messages")
7 | end
8 |
9 | alias_method :deliver_now, :active_job_missing
10 | alias_method :deliver_later, :active_job_missing
11 | end
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/lib/textris/delay/active_job/job.rb:
--------------------------------------------------------------------------------
1 | module Textris
2 | module Delay
3 | module ActiveJob
4 | class Job < ::ActiveJob::Base
5 | queue_as :textris
6 |
7 | def perform(texter, action, args)
8 | texter = texter.safe_constantize
9 |
10 | if texter.present?
11 | texter.new(action, *args).call_action.deliver_now
12 | end
13 | end
14 | end
15 | end
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/example/rails-4.2/config/initializers/backtrace_silencers.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
5 |
6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
7 | # Rails.backtrace_cleaner.remove_silencers!
8 |
--------------------------------------------------------------------------------
/lib/textris/delay/sidekiq/missing.rb:
--------------------------------------------------------------------------------
1 | module Textris
2 | module Delay
3 | module Sidekiq
4 | module Missing
5 | def sidekiq_missing(*args)
6 | raise(LoadError, "Sidekiq is required to delay sending messages")
7 | end
8 |
9 | alias_method :delay, :sidekiq_missing
10 | alias_method :delay_for, :sidekiq_missing
11 | alias_method :delay_until, :sidekiq_missing
12 | end
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/textris/delay/sidekiq/worker.rb:
--------------------------------------------------------------------------------
1 | module Textris
2 | module Delay
3 | module Sidekiq
4 | class Worker
5 | include ::Sidekiq::Worker
6 |
7 | def perform(texter, action, args)
8 | texter = texter.safe_constantize
9 |
10 | if texter.present?
11 | args = ::Textris::Delay::Sidekiq::Serializer.deserialize(args)
12 |
13 | texter.new(action, *args).call_action.deliver
14 | end
15 | end
16 | end
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/example/rails-4.2/bin/spring:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | # This file loads spring without using Bundler, in order to be fast.
4 | # It gets overwritten when you run the `spring binstub` command.
5 |
6 | unless defined?(Spring)
7 | require "rubygems"
8 | require "bundler"
9 |
10 | if match = Bundler.default_lockfile.read.match(/^GEM$.*?^ (?: )*spring \((.*?)\)$.*?^$/m)
11 | Gem.paths = { "GEM_PATH" => Bundler.bundle_path.to_s }
12 | gem "spring", match[1]
13 | require "spring/binstub"
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/textris/delivery/nexmo.rb:
--------------------------------------------------------------------------------
1 | module Textris
2 | module Delivery
3 | class Nexmo < Textris::Delivery::Base
4 | def deliver(phone)
5 | client.send_message(
6 | from: sender_id,
7 | to: phone,
8 | text: message.content
9 | )
10 | end
11 |
12 | private
13 | def client
14 | @client ||= ::Nexmo::Client.new
15 | end
16 |
17 | def sender_id
18 | message.from_phone || message.from_name
19 | end
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/example/rails-4.2/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files for more about ignoring files.
2 | #
3 | # If you find yourself ignoring temporary files generated by your text editor
4 | # or operating system, you probably want to add a global ignore instead:
5 | # git config --global core.excludesfile '~/.gitignore_global'
6 |
7 | # Ignore bundler config.
8 | /.bundle
9 |
10 | # Ignore the default SQLite database.
11 | /db/*.sqlite3
12 | /db/*.sqlite3-journal
13 |
14 | # Ignore all logfiles and tempfiles.
15 | /log/*
16 | !/log/.keep
17 | /tmp
18 |
--------------------------------------------------------------------------------
/example/rails-4.2/config/initializers/assets.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Version of your assets, change this if you want to expire all your assets.
4 | Rails.application.config.assets.version = '1.0'
5 |
6 | # Add additional assets to the asset load path
7 | # Rails.application.config.assets.paths << Emoji.images_path
8 |
9 | # Precompile additional assets.
10 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
11 | # Rails.application.config.assets.precompile += %w( search.js )
12 |
--------------------------------------------------------------------------------
/example/rails-4.2/app/controllers/users_controller.rb:
--------------------------------------------------------------------------------
1 | class UsersController < ApplicationController
2 | def index
3 | @users = User.order('created_at DESC, id DESC')
4 | end
5 |
6 | def new
7 | @user = User.new
8 | end
9 |
10 | def create
11 | @user = User.new(user_params)
12 |
13 | if @user.save
14 | redirect_to users_url, notice: 'User was created and SMS notification was sent. Check server log for yourself!'
15 | else
16 | render :new
17 | end
18 | end
19 |
20 | private
21 |
22 | def user_params
23 | params.require(:user).permit(:name, :phone)
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/example/rails-4.2/config/initializers/wrap_parameters.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # This file contains settings for ActionController::ParamsWrapper which
4 | # is enabled by default.
5 |
6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
7 | ActiveSupport.on_load(:action_controller) do
8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters)
9 | end
10 |
11 | # To enable root element in JSON for ActiveRecord objects.
12 | # ActiveSupport.on_load(:active_record) do
13 | # self.include_root_in_json = true
14 | # end
15 |
--------------------------------------------------------------------------------
/example/rails-4.2/app/models/user.rb:
--------------------------------------------------------------------------------
1 | class User < ActiveRecord::Base
2 | validates :name, :phone, :presence => true, :uniqueness => true
3 | validate :phone_plausible
4 |
5 | def phone_plausible
6 | errors.add(:phone, :invalid) if phone.present? && !Phony.plausible?(phone)
7 | end
8 |
9 | def phone=(value)
10 | Phony.plausible?(value) ? super(Phony.normalize(value)) : super(value)
11 | end
12 |
13 | after_create do
14 | ## This would send SMS instantly and slow app down...
15 | # UserTexter.welcome(self).deliver_now
16 |
17 | ## ...so let's use shiny, async ActiveJob instead
18 | UserTexter.welcome(self).deliver_later
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/textris/delivery/test.rb:
--------------------------------------------------------------------------------
1 | module Textris
2 | module Delivery
3 | class Test < Textris::Delivery::Base
4 | class << self
5 | def deliveries
6 | @deliveries ||= []
7 | end
8 | end
9 |
10 | def deliver(to)
11 | self.class.deliveries.push(::Textris::Message.new(
12 | :content => message.content,
13 | :from_name => message.from_name,
14 | :from_phone => message.from_phone,
15 | :texter => message.texter,
16 | :action => message.action,
17 | :to => to,
18 | :media_urls => message.media_urls))
19 | end
20 | end
21 | end
22 | end
23 |
--------------------------------------------------------------------------------
/example/rails-4.2/app/views/users/index.html.erb:
--------------------------------------------------------------------------------
1 |
<%= notice %>
2 |
3 | Previously added users
4 |
5 | <% if @users.any? %>
6 |
7 |
8 |
9 | | Name |
10 | Phone |
11 |
12 |
13 |
14 |
15 | <% @users.each do |user| %>
16 |
17 | | <%= user.name %> |
18 | <%= user.phone %> |
19 |
20 | <% end %>
21 |
22 |
23 | <% else %>
24 | You haven't added any users yet.
25 | <% end %>
26 |
27 |
28 |
29 |
30 | <%= link_to 'Add new user', new_user_path, :class => 'btn' %>
31 |
--------------------------------------------------------------------------------
/example/rails-4.2/config/database.yml:
--------------------------------------------------------------------------------
1 | # SQLite version 3.x
2 | # gem install sqlite3
3 | #
4 | # Ensure the SQLite 3 gem is defined in your Gemfile
5 | # gem 'sqlite3'
6 | #
7 | default: &default
8 | adapter: sqlite3
9 | pool: 5
10 | timeout: 5000
11 |
12 | development:
13 | <<: *default
14 | database: db/development.sqlite3
15 |
16 | # Warning: The database defined as "test" will be erased and
17 | # re-generated from your development database when you run "rake".
18 | # Do not set this db to the same as development or production.
19 | test:
20 | <<: *default
21 | database: db/test.sqlite3
22 |
23 | production:
24 | <<: *default
25 | database: db/production.sqlite3
26 |
--------------------------------------------------------------------------------
/lib/textris/delivery.rb:
--------------------------------------------------------------------------------
1 | module Textris
2 | module Delivery
3 | module_function
4 |
5 | def get
6 | methods = Rails.application.config.try(:textris_delivery_method)
7 | methods = [*methods].compact
8 | if methods.blank?
9 | if Rails.env.development?
10 | methods = [:log]
11 | elsif Rails.env.test?
12 | methods = [:test]
13 | else
14 | methods = [:mail]
15 | end
16 | end
17 |
18 | methods.map do |method|
19 | "Textris::Delivery::#{method.to_s.camelize}".safe_constantize ||
20 | "#{method.to_s.camelize}Delivery".safe_constantize
21 | end.compact
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/example/rails-4.2/app/assets/stylesheets/application.css:
--------------------------------------------------------------------------------
1 | /*
2 | *= require_tree .
3 | *= require_self
4 | */
5 |
6 | body { padding-top: 40px; width: 600px; margin: 0 auto; text-align: center; }
7 |
8 | h1, h3, th { font-family: serif; }
9 | h3 { margin-top: 50px; }
10 |
11 | .field-error { color: red; }
12 |
13 | .btn { display: inline-block; border: 0px solid black; background: #fff; text-decoration: none; padding: 5px 10px; line-height: 1.5em; color: #888 !important; }
14 |
15 | .btn:hover { color: #fff !important; background: #000; cursor: pointer; }
16 |
17 | table { margin: 0 auto; }
18 | table td:first-child, table th:first-child { text-align: right; }
19 | table td:last-child, table th:last-child { text-align: left; padding-left: 20px; }
20 |
--------------------------------------------------------------------------------
/example/rails-4.2/config/initializers/inflections.rb:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Add new inflection rules using the following format. Inflections
4 | # are locale specific, and you may define rules for as many different
5 | # locales as you wish. All of these examples are active by default:
6 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
7 | # inflect.plural /^(ox)$/i, '\1en'
8 | # inflect.singular /^(ox)en/i, '\1'
9 | # inflect.irregular 'person', 'people'
10 | # inflect.uncountable %w( fish sheep )
11 | # end
12 |
13 | # These inflection rules are supported but not enabled by default:
14 | # ActiveSupport::Inflector.inflections(:en) do |inflect|
15 | # inflect.acronym 'RESTful'
16 | # end
17 |
--------------------------------------------------------------------------------
/example/rails-4.2/config/locales/en.yml:
--------------------------------------------------------------------------------
1 | # Files in the config/locales directory are used for internationalization
2 | # and are automatically loaded by Rails. If you want to use locales other
3 | # than English, add the necessary files in this directory.
4 | #
5 | # To use the locales, use `I18n.t`:
6 | #
7 | # I18n.t 'hello'
8 | #
9 | # In views, this is aliased to just `t`:
10 | #
11 | # <%= t('hello') %>
12 | #
13 | # To use a different locale, set it with `I18n.locale`:
14 | #
15 | # I18n.locale = :es
16 | #
17 | # This would use the information in config/locales/es.yml.
18 | #
19 | # To learn more, please read the Rails Internationalization guide
20 | # available at http://guides.rubyonrails.org/i18n.html.
21 |
22 | en:
23 | hello: "Hello world"
24 |
--------------------------------------------------------------------------------
/lib/textris/delay/sidekiq.rb:
--------------------------------------------------------------------------------
1 | module Textris
2 | module Delay
3 | module Sidekiq
4 | def delay
5 | ::Textris::Delay::Sidekiq::Proxy.new(self.to_s)
6 | end
7 |
8 | def delay_for(interval)
9 | unless interval.is_a?(Integer)
10 | raise(ArgumentError, "Proper interval must be provided")
11 | end
12 |
13 | ::Textris::Delay::Sidekiq::Proxy.new(self.to_s, :perform_in => interval)
14 | end
15 |
16 | def delay_until(timestamp)
17 | unless timestamp.respond_to?(:to_time)
18 | raise(ArgumentError, "Proper timestamp must be provided")
19 | end
20 |
21 | ::Textris::Delay::Sidekiq::Proxy.new(self.to_s, :perform_at => timestamp)
22 | end
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/example/rails-4.2/app/views/users/_form.html.erb:
--------------------------------------------------------------------------------
1 | <%= form_for(@user) do |f| %>
2 |
3 | <%= f.label :name %>
4 | <%= f.text_field :name %>
5 |
6 | <% if @user.errors[:name].present? %>
7 | <% @user.errors[:name].each do |error| %>
8 |
<%= error %>
9 | <% end %>
10 | <% end %>
11 |
12 |
13 | <%= f.label :phone %>
14 | <%= f.text_field :phone %>
15 |
16 | <% if @user.errors[:phone].present? %>
17 | <% @user.errors[:phone].each do |error| %>
18 |
<%= error %>
19 | <% end %>
20 | <% end %>
21 |
22 | <%= f.submit :class => 'btn' %>
23 |
24 | <% end %>
25 |
--------------------------------------------------------------------------------
/example/rails-4.2/app/views/layouts/application.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | TextrisExample
5 | <%= stylesheet_link_tag 'application', media: 'all' %>
6 | <%= javascript_include_tag 'application' %>
7 | <%= csrf_meta_tags %>
8 |
9 |
10 |
11 | Textris example
12 |
13 | This simple application demonstrates the usage scenario from <%= link_to 'textris documentation', 'https://github.com/visualitypl/textris' %>. You can add new users after which the application will send SMS with welcome notification.
14 |
15 | In this Rails 4.2 example, ActiveJob is used to send SMSes asynchronously. Of course, there's no backend configured by default, so job will be invoked instantly as an inline job.
16 |
17 | <%= yield %>
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/lib/textris/delivery/twilio.rb:
--------------------------------------------------------------------------------
1 | module Textris
2 | module Delivery
3 | class Twilio < Textris::Delivery::Base
4 | def deliver(to)
5 | options = {
6 | :to => PhoneFormatter.format(to),
7 | :body => message.content
8 | }
9 |
10 | if message.twilio_messaging_service_sid
11 | options[:messaging_service_sid] = message.twilio_messaging_service_sid
12 | else
13 | options[:from] = PhoneFormatter.format(message.from_phone)
14 | end
15 |
16 | if message.media_urls.is_a?(Array)
17 | options[:media_url] = message.media_urls
18 | end
19 |
20 | client.messages.create(options)
21 | end
22 |
23 | private
24 |
25 | def client
26 | @client ||= ::Twilio::REST::Client.new
27 | end
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 0.7.0 (latest release)
2 |
3 | * added support for using Twilio Copilot via messaging_service_sid
4 |
5 | # 0.6.0
6 |
7 | * new release to support changes made with Nexmo features,
8 | * `respond_to_missing?` defined for base texter,
9 | * corrected an error typo in Message Recipients validation
10 |
11 | # 0.5.0
12 |
13 | - **Breaking change**. `Textris::Message#parse_content` no longer strips any
14 | whitespace characters except for trailing characters. This allows to send
15 | messages with newlines etc.
16 | - Added support for sending MMS messages via Twilio with `media_urls` option.
17 | - Moved to TravisCI
18 | - Added support for using Twilio Copilot which depends on having a `messaging_service_sid`.
19 | - Fix defaults inheritance (see issue #11)
20 | - Add support for Alphanumeric sender ID
21 | - Add support for Short Codes
22 |
23 |
--------------------------------------------------------------------------------
/lib/textris/delay/sidekiq/proxy.rb:
--------------------------------------------------------------------------------
1 | module Textris
2 | module Delay
3 | module Sidekiq
4 | class Proxy
5 | def initialize(texter, options = {})
6 | @texter = texter
7 | @perform_in = options[:perform_in]
8 | @perform_at = options[:perform_at]
9 | end
10 |
11 | def method_missing(method_name, *args)
12 | args = ::Textris::Delay::Sidekiq::Serializer.serialize(args)
13 | args = [@texter, method_name, args]
14 |
15 | if @perform_in
16 | ::Textris::Delay::Sidekiq::Worker.perform_in(@perform_in, *args)
17 | elsif @perform_at
18 | ::Textris::Delay::Sidekiq::Worker.perform_at(@perform_at, *args)
19 | else
20 | ::Textris::Delay::Sidekiq::Worker.perform_async(*args)
21 | end
22 | end
23 | end
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/example/rails-4.2/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 | require 'pathname'
3 |
4 | # path to your application root.
5 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
6 |
7 | Dir.chdir APP_ROOT do
8 | # This script is a starting point to setup your application.
9 | # Add necessary setup steps to this file:
10 |
11 | puts "== Installing dependencies =="
12 | system "gem install bundler --conservative"
13 | system "bundle check || bundle install"
14 |
15 | # puts "\n== Copying sample files =="
16 | # unless File.exist?("config/database.yml")
17 | # system "cp config/database.yml.sample config/database.yml"
18 | # end
19 |
20 | puts "\n== Preparing database =="
21 | system "bin/rake db:setup"
22 |
23 | puts "\n== Removing old logs and tempfiles =="
24 | system "rm -f log/*"
25 | system "rm -rf tmp/cache"
26 |
27 | puts "\n== Restarting application server =="
28 | system "touch tmp/restart.txt"
29 | end
30 |
--------------------------------------------------------------------------------
/lib/textris/phone_formatter.rb:
--------------------------------------------------------------------------------
1 | module Textris
2 | class PhoneFormatter
3 | class << self
4 | def format(phone = '')
5 | return phone if is_a_short_code?(phone) || is_alphameric?(phone) || phone.nil?
6 | "#{'+' unless phone.start_with?('+')}#{phone}"
7 | end
8 |
9 | # Short codes have more dependencies and limitations;
10 | # but this is a good general start
11 | def is_a_short_code?(phone)
12 | !!phone.to_s.match(/\A\d{4,6}\z/)
13 | end
14 |
15 | def is_a_phone_number?(phone)
16 | Phony.plausible?(phone)
17 | end
18 |
19 | def is_alphameric?(phone)
20 | # \A # Start of the string
21 | # (?=.*[a-zA-Z]) # Lookahead to ensure there is at least one letter in the entire string
22 | # [a-zA-z\d]{1,11} # Between 1 and 11 characters in the string
23 | # \z # End of the string
24 | !!phone.to_s.match(/\A(?=.*[a-zA-Z])[a-zA-z\d]{1,11}\z/)
25 | end
26 | end
27 | end
28 | end
--------------------------------------------------------------------------------
/example/rails-4.2/config/secrets.yml:
--------------------------------------------------------------------------------
1 | # Be sure to restart your server when you modify this file.
2 |
3 | # Your secret key is used for verifying the integrity of signed cookies.
4 | # If you change this key, all old signed cookies will become invalid!
5 |
6 | # Make sure the secret is at least 30 characters and all random,
7 | # no regular words or you'll be exposed to dictionary attacks.
8 | # You can use `rake secret` to generate a secure secret key.
9 |
10 | # Make sure the secrets in this file are kept private
11 | # if you're sharing your code publicly.
12 |
13 | development:
14 | secret_key_base: 91d708771df5bb455fcee594f6add61fb0abb64ecc10b36541fd2b015ad87ffb073b124a1242e0416a5f98dbf6644801eb6743a621941e7d736d634b33357dec
15 |
16 | test:
17 | secret_key_base: 8e5c53f5270a61ed46038a2dec435cd189bac17c73cb71efc76521e6b38efb75af66dbd348dc19a57d544488c83b7ac542163e68dd93b7f8488e60fac2ab9d22
18 |
19 | # Do not keep production secrets in the repository,
20 | # instead read values from the environment.
21 | production:
22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
23 |
--------------------------------------------------------------------------------
/example/rails-4.2/db/schema.rb:
--------------------------------------------------------------------------------
1 | # encoding: UTF-8
2 | # This file is auto-generated from the current state of the database. Instead
3 | # of editing this file, please use the migrations feature of Active Record to
4 | # incrementally modify your database, and then regenerate this schema definition.
5 | #
6 | # Note that this schema.rb definition is the authoritative source for your
7 | # database schema. If you need to create the application database on another
8 | # system, you should be using db:schema:load, not running all the migrations
9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations
10 | # you'll amass, the slower it'll run and the greater likelihood for issues).
11 | #
12 | # It's strongly recommended that you check this file into your version control system.
13 |
14 | ActiveRecord::Schema.define(version: 20150220111225) do
15 |
16 | create_table "users", force: :cascade do |t|
17 | t.string "name"
18 | t.string "phone"
19 | t.datetime "created_at", null: false
20 | t.datetime "updated_at", null: false
21 | end
22 |
23 | end
24 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014 Visuality
2 |
3 | MIT License
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:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
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.
23 |
--------------------------------------------------------------------------------
/example/rails-4.2/app/assets/stylesheets/scaffold.css:
--------------------------------------------------------------------------------
1 | body { background-color: #fff; color: #333; }
2 |
3 | body, p, ol, ul, td, input, label {
4 | font-family: verdana, arial, helvetica, sans-serif;
5 | font-size: 13px;
6 | line-height: 18px;
7 | }
8 |
9 | pre {
10 | background-color: #eee;
11 | padding: 10px;
12 | font-size: 11px;
13 | }
14 |
15 | a { color: #000; }
16 | a:visited { color: #666; }
17 | a:hover { color: #fff; background-color:#000; }
18 |
19 | div.field, div.actions {
20 | margin-bottom: 10px;
21 | }
22 |
23 | #notice {
24 | color: green;
25 | }
26 |
27 | .field_with_errors input {
28 | border-color: red;
29 | }
30 |
31 | #error_explanation {
32 | text-align: left;
33 | border: 2px solid red;
34 | padding: 7px;
35 | padding-bottom: 0;
36 | margin-bottom: 20px;
37 | background-color: #f0f0f0;
38 | }
39 |
40 | #error_explanation h2 {
41 | text-align: left;
42 | font-weight: bold;
43 | padding: 5px 5px 5px 15px;
44 | font-size: 12px;
45 | margin: -7px;
46 | margin-bottom: 0px;
47 | background-color: #c00;
48 | color: #fff;
49 | }
50 |
51 | #error_explanation ul li {
52 | font-size: 12px;
53 | list-style: square;
54 | }
55 |
--------------------------------------------------------------------------------
/spec/textris/delivery/nexmo_spec.rb:
--------------------------------------------------------------------------------
1 | describe Textris::Delivery::Nexmo do
2 | let(:message) do
3 | Textris::Message.new(
4 | :to => ['+48 600 700 800', '+48 100 200 300'],
5 | :content => 'Some text',
6 | :from => 'Alpha ID')
7 | end
8 |
9 | let(:delivery) { Textris::Delivery::Nexmo.new(message) }
10 |
11 | before do
12 | module Nexmo
13 | class Client
14 | def send_message(params)
15 | params
16 | end
17 | end
18 | end
19 | end
20 |
21 | it 'responds to :deliver_to_all' do
22 | expect(delivery).to respond_to(:deliver_to_all)
23 | end
24 |
25 | it 'invokes Nexmo::Client#send_message twice for each recipient' do
26 | expect_any_instance_of(Nexmo::Client).to receive(:send_message).twice do |context, msg|
27 | expect(msg).to have_key(:from)
28 | expect(msg).to have_key(:to)
29 | expect(msg).to have_key(:text)
30 | end
31 |
32 | delivery.deliver_to_all
33 | end
34 |
35 | describe '#deliver' do
36 | subject { delivery.deliver('48600700800') }
37 |
38 | context 'when from_phone is nil' do
39 | it 'will use from_name' do
40 | expect(subject[:from]).to eq 'Alpha ID'
41 | end
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/example/rails-4.2/config/application.rb:
--------------------------------------------------------------------------------
1 | require File.expand_path('../boot', __FILE__)
2 |
3 | require 'rails/all'
4 |
5 | # Require the gems listed in Gemfile, including any gems
6 | # you've limited to :test, :development, or :production.
7 | Bundler.require(*Rails.groups)
8 |
9 | module TextrisExample
10 | class Application < Rails::Application
11 | # Settings in config/environments/* take precedence over those specified here.
12 | # Application configuration should go into files in config/initializers
13 | # -- all .rb files in that directory are automatically loaded.
14 |
15 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
16 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
17 | # config.time_zone = 'Central Time (US & Canada)'
18 |
19 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
20 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
21 | # config.i18n.default_locale = :de
22 |
23 | # Do not swallow errors in after_commit/after_rollback callbacks.
24 | config.active_record.raise_in_transactional_callbacks = true
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/textris.rb:
--------------------------------------------------------------------------------
1 | require 'action_controller'
2 | require 'action_mailer'
3 | require 'active_support'
4 | require 'phony'
5 |
6 | begin
7 | require 'twilio-ruby'
8 | rescue LoadError
9 | end
10 |
11 | begin
12 | require 'sidekiq'
13 | rescue LoadError
14 | require 'textris/delay/sidekiq/missing'
15 |
16 | Textris::Delay::Sidekiq.include(Textris::Delay::Sidekiq::Missing)
17 | else
18 | require 'textris/delay/sidekiq'
19 | require 'textris/delay/sidekiq/proxy'
20 | require 'textris/delay/sidekiq/serializer'
21 | require 'textris/delay/sidekiq/worker'
22 | end
23 |
24 | require 'textris/base'
25 | require 'textris/phone_formatter'
26 | require 'textris/message'
27 |
28 | begin
29 | require 'active_job'
30 | rescue LoadError
31 | require 'textris/delay/active_job/missing'
32 |
33 | Textris::Message.include(Textris::Delay::ActiveJob::Missing)
34 | else
35 | require 'textris/delay/active_job'
36 | require 'textris/delay/active_job/job'
37 |
38 | Textris::Message.include(Textris::Delay::ActiveJob)
39 | end
40 |
41 | require 'textris/delivery'
42 | require 'textris/delivery/base'
43 | require 'textris/delivery/test'
44 | require 'textris/delivery/mail'
45 | require 'textris/delivery/log'
46 | require 'textris/delivery/twilio'
47 | require 'textris/delivery/nexmo'
48 |
--------------------------------------------------------------------------------
/lib/textris/delivery/log.rb:
--------------------------------------------------------------------------------
1 | module Textris
2 | module Delivery
3 | class Log < Textris::Delivery::Base
4 | AVAILABLE_LOG_LEVELS = %w{debug info warn error fatal unknown}
5 |
6 | def deliver(to)
7 | log :info, "Sent text to #{Phony.format(to)}"
8 | log :debug, "Texter: #{message.texter || 'UnknownTexter'}" + "#" +
9 | "#{message.action || 'unknown_action'}"
10 | log :debug, "Date: #{Time.now}"
11 | log :debug, "From: #{message.from || message.twilio_messaging_service_sid || 'unknown'}"
12 | log :debug, "To: #{message.to.map { |i| Phony.format(to) }.join(', ')}"
13 | log :debug, "Content: #{message.content}"
14 | (message.media_urls || []).each_with_index do |media_url, index|
15 | logged_message = index == 0 ? "Media URLs: " : " "
16 | logged_message << media_url
17 | log :debug, logged_message
18 | end
19 | end
20 |
21 | private
22 |
23 | def log(level, message)
24 | level = Rails.application.config.try(:textris_log_level) || level
25 |
26 | unless AVAILABLE_LOG_LEVELS.include?(level.to_s)
27 | raise(ArgumentError, "Wrong log level: #{level}")
28 | end
29 |
30 | Rails.logger.send(level, message)
31 | end
32 | end
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require 'simplecov'
2 | require 'scrutinizer/ocular'
3 | require "scrutinizer/ocular/formatter"
4 | require "codeclimate-test-reporter"
5 | require "sidekiq/testing"
6 | require 'textris/delay/active_job/missing'
7 | require 'textris/delay/sidekiq/missing'
8 |
9 | CodeClimate::TestReporter.configuration.logger = Logger.new("/dev/null")
10 |
11 | if Scrutinizer::Ocular.should_run? ||
12 | CodeClimate::TestReporter.run? ||
13 | ENV["COVERAGE"]
14 | formatters = [SimpleCov::Formatter::HTMLFormatter]
15 | if Scrutinizer::Ocular.should_run?
16 | formatters << Scrutinizer::Ocular::UploadingFormatter
17 | end
18 | if CodeClimate::TestReporter.run?
19 | formatters << CodeClimate::TestReporter::Formatter
20 | end
21 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[*formatters]
22 |
23 | CodeClimate::TestReporter.configuration.logger = nil
24 |
25 | SimpleCov.start do
26 | add_filter "/lib/textris.rb"
27 | add_filter "/spec/"
28 | add_filter "vendor"
29 | end
30 | end
31 |
32 | require_relative '../lib/textris'
33 |
34 | RSpec.configure do |config|
35 | config.expect_with :rspec do |expectations|
36 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true
37 | end
38 |
39 | config.mock_with :rspec do |mocks|
40 | mocks.verify_partial_doubles = true
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/spec/textris/delivery/test_spec.rb:
--------------------------------------------------------------------------------
1 | describe Textris::Delivery::Test do
2 | let(:message) do
3 | Textris::Message.new(
4 | :to => ['+48 600 700 800', '+48 100 200 300'],
5 | :content => 'Some text',
6 | :media_urls => ["http://example.com/hilarious.gif"])
7 | end
8 |
9 | let(:delivery) { Textris::Delivery::Test.new(message) }
10 |
11 | it 'responds to :deliver_to_all' do
12 | expect(delivery).to respond_to(:deliver_to_all)
13 | end
14 |
15 | it 'adds proper deliveries to deliveries array' do
16 | delivery.deliver_to_all
17 |
18 | expect(Textris::Delivery::Test.deliveries.count).to eq 2
19 |
20 | last_message = Textris::Delivery::Test.deliveries.last
21 |
22 | expect(last_message).to be_present
23 | expect(last_message.content).to eq message.content
24 | expect(last_message.from_name).to eq message.from_name
25 | expect(last_message.from_phone).to eq message.from_phone
26 | expect(last_message.texter).to eq message.texter
27 | expect(last_message.action).to eq message.action
28 | expect(last_message.to[0]).to eq message.to[1]
29 | expect(last_message.media_urls[0]).to eq message.media_urls[0]
30 | end
31 |
32 | it 'allows clearing messages array' do
33 | delivery.deliver_to_all
34 |
35 | Textris::Delivery::Test.deliveries.clear
36 |
37 | expect(Textris::Delivery::Test.deliveries).to be_empty
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/spec/textris/phone_formatter_spec.rb:
--------------------------------------------------------------------------------
1 | describe Textris::PhoneFormatter do
2 | it 'should recognise a 4 digit short code' do
3 | expect(described_class.format("4437")).to eq('4437')
4 | expect(described_class.is_a_short_code?("4437")).to eq(true)
5 | end
6 |
7 | it 'should recognise a 5 digit short code' do
8 | expect(described_class.format("44397")).to eq('44397')
9 | expect(described_class.is_a_short_code?("44397")).to eq(true)
10 | end
11 |
12 | it 'should recognise a 6 digit short code' do
13 | expect(described_class.format("443975")).to eq('443975')
14 | expect(described_class.is_a_short_code?("443975")).to eq(true)
15 | end
16 |
17 | it 'treat strings containing at least 1 letter as alphamerics' do
18 | ['a', '1a', '21a', '321a', '4321a', '54321a', '654321a', '7654321a', '87654321a', '987654321a', '0987654321a'].each do |alphameric|
19 | expect(described_class.format(alphameric)).to eq(alphameric)
20 | expect(described_class.is_alphameric?(alphameric)).to eq(true)
21 | end
22 | end
23 |
24 | it 'prepends phone number with +' do
25 | expect(described_class.format('48123456789')).to eq('+48123456789')
26 | expect(described_class.is_a_phone_number?('48123456789')).to eq(true)
27 | end
28 |
29 | it 'does not prepend phone number with + if it already is prepended' do
30 | expect(described_class.format('+48123456789')).to eq('+48123456789')
31 | expect(described_class.is_a_phone_number?('+48123456789')).to eq(true)
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/example/rails-4.2/public/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | We're sorry, but something went wrong (500)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
We're sorry, but something went wrong.
62 |
63 |
If you are the application owner check the logs for more information.
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/example/rails-4.2/config/environments/development.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # In the development environment your application's code is reloaded on
5 | # every request. This slows down response time but is perfect for development
6 | # since you don't have to restart the web server when you make code changes.
7 | config.cache_classes = false
8 |
9 | # Do not eager load code on boot.
10 | config.eager_load = false
11 |
12 | # Show full error reports and disable caching.
13 | config.consider_all_requests_local = true
14 | config.action_controller.perform_caching = false
15 |
16 | # Don't care if the mailer can't send.
17 | config.action_mailer.raise_delivery_errors = false
18 |
19 | # Print deprecation notices to the Rails logger.
20 | config.active_support.deprecation = :log
21 |
22 | # Raise an error on page load if there are pending migrations.
23 | config.active_record.migration_error = :page_load
24 |
25 | # Debug mode disables concatenation and preprocessing of assets.
26 | # This option may cause significant delays in view rendering with a large
27 | # number of complex assets.
28 | config.assets.debug = true
29 |
30 | # Asset digests allow you to set far-future HTTP expiration dates on all assets,
31 | # yet still be able to expire them through the digest params.
32 | config.assets.digest = true
33 |
34 | # Adds additional error checking when serving assets at runtime.
35 | # Checks for improperly declared sprockets dependencies.
36 | # Raises helpful error messages.
37 | config.assets.raise_runtime_errors = true
38 |
39 | # Raises error for missing translations
40 | # config.action_view.raise_on_missing_translations = true
41 | end
42 |
--------------------------------------------------------------------------------
/example/rails-4.2/public/422.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The change you wanted was rejected (422)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The change you wanted was rejected.
62 |
Maybe you tried to change something you didn't have access to.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/example/rails-4.2/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | The page you were looking for doesn't exist (404)
5 |
6 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
The page you were looking for doesn't exist.
62 |
You may have mistyped the address or the page may have moved.
63 |
64 |
If you are the application owner check the logs for more information.
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/example/rails-4.2/config/environments/test.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # The test environment is used exclusively to run your application's
5 | # test suite. You never need to work with it otherwise. Remember that
6 | # your test database is "scratch space" for the test suite and is wiped
7 | # and recreated between test runs. Don't rely on the data there!
8 | config.cache_classes = true
9 |
10 | # Do not eager load code on boot. This avoids loading your whole application
11 | # just for the purpose of running a single test. If you are using a tool that
12 | # preloads Rails for running tests, you may have to set it to true.
13 | config.eager_load = false
14 |
15 | # Configure static file server for tests with Cache-Control for performance.
16 | config.serve_static_files = true
17 | config.static_cache_control = 'public, max-age=3600'
18 |
19 | # Show full error reports and disable caching.
20 | config.consider_all_requests_local = true
21 | config.action_controller.perform_caching = false
22 |
23 | # Raise exceptions instead of rendering exception templates.
24 | config.action_dispatch.show_exceptions = false
25 |
26 | # Disable request forgery protection in test environment.
27 | config.action_controller.allow_forgery_protection = false
28 |
29 | # Tell Action Mailer not to deliver emails to the real world.
30 | # The :test delivery method accumulates sent emails in the
31 | # ActionMailer::Base.deliveries array.
32 | config.action_mailer.delivery_method = :test
33 |
34 | # Randomize the order test cases are executed.
35 | config.active_support.test_order = :random
36 |
37 | # Print deprecation notices to the stderr.
38 | config.active_support.deprecation = :stderr
39 |
40 | # Raises error for missing translations
41 | # config.action_view.raise_on_missing_translations = true
42 | end
43 |
--------------------------------------------------------------------------------
/textris.gemspec:
--------------------------------------------------------------------------------
1 | lib = File.expand_path('../lib', __FILE__)
2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3 | require 'textris/version'
4 |
5 | Gem::Specification.new do |spec|
6 | spec.name = 'textris'
7 | spec.version = Textris::VERSION
8 | spec.authors = ['Visuality', 'Karol Słuszniak']
9 | spec.email = 'contact@visuality.pl'
10 | spec.homepage = 'http://github.com/visualitypl/textris'
11 | spec.license = 'MIT'
12 | spec.platform = Gem::Platform::RUBY
13 |
14 | spec.summary = 'Simple SMS messaging gem for Rails based on concepts and conventions similar to ActionMailer, with some extra features.'
15 |
16 | spec.description = "Implement texter classes for sending SMS messages in similar way to how e-mails are sent with ActionMailer-based mailers. Take advantage of e-mail proxying and enhanced phone number parsing, among others."
17 |
18 | spec.files = Dir["lib/**/*.rb"]
19 | spec.has_rdoc = false
20 | spec.extra_rdoc_files = ["README.md"]
21 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
22 | spec.require_paths = ["lib"]
23 |
24 | spec.add_development_dependency 'bundler', '~> 1.6'
25 | spec.add_development_dependency 'codeclimate-test-reporter', '~> 0.4'
26 | spec.add_development_dependency 'rake', '~> 10.0'
27 | spec.add_development_dependency 'rspec', '~> 3.1'
28 | spec.add_development_dependency 'rspec-sidekiq', '~> 2.0'
29 | spec.add_development_dependency 'scrutinizer-ocular', '~> 1.0'
30 | spec.add_development_dependency 'simplecov', '~> 0.9'
31 | spec.add_development_dependency 'twilio-ruby', '~> 3.12'
32 | spec.add_development_dependency 'nexmo', '~> 2.0'
33 | spec.add_development_dependency 'appraisal', '~> 2.1'
34 |
35 | spec.add_runtime_dependency 'actionmailer', '>= 4.0'
36 | spec.add_runtime_dependency 'activejob', '>= 4.2'
37 | spec.add_runtime_dependency 'activesupport', '>= 4.2'
38 | spec.add_runtime_dependency 'phony', '~> 2.8'
39 | spec.add_runtime_dependency 'render_anywhere', '~> 0.0'
40 | spec.add_runtime_dependency 'nokogiri', '~> 1.10.4'
41 | end
42 |
--------------------------------------------------------------------------------
/lib/textris/base.rb:
--------------------------------------------------------------------------------
1 | require 'render_anywhere'
2 |
3 | module Textris
4 | class Base
5 | class RenderingController < RenderAnywhere::RenderingController
6 | layout false
7 |
8 | def default_url_options
9 | ActionMailer::Base.default_url_options || {}
10 | end
11 | end
12 |
13 | include RenderAnywhere
14 | extend Textris::Delay::Sidekiq
15 |
16 | class << self
17 | def deliveries
18 | ::Textris::Delivery::Test.deliveries
19 | end
20 |
21 | def with_defaults(options)
22 | defaults.merge(options)
23 | end
24 |
25 | def defaults
26 | @defaults ||= superclass.respond_to?(:defaults) ? superclass.defaults.dup : {}
27 | end
28 |
29 | protected
30 |
31 | def default(options)
32 | defaults.merge!(options)
33 | end
34 |
35 | private
36 |
37 | def method_missing(method_name, *args)
38 | new(method_name, *args).call_action
39 | end
40 |
41 | def respond_to_missing?(method, *args)
42 | public_instance_methods(true).include?(method) || super
43 | end
44 | end
45 |
46 | def initialize(action, *args)
47 | @action = action
48 | @args = args
49 | end
50 |
51 | def call_action
52 | send(@action, *@args)
53 | end
54 |
55 | def render_content
56 | set_instance_variables_for_rendering
57 |
58 | render(:template => template_name, :formats => ['text'], :locale => @locale)
59 | end
60 |
61 | protected
62 |
63 | def text(options = {})
64 | @locale = options[:locale] || I18n.locale
65 |
66 | options = self.class.with_defaults(options)
67 | options.merge!(
68 | :texter => self.class,
69 | :action => @action,
70 | :args => @args,
71 | :content => options[:body].is_a?(String) ? options[:body] : nil,
72 | :renderer => self)
73 |
74 | ::Textris::Message.new(options)
75 | end
76 |
77 | private
78 |
79 | def template_name
80 | class_name = self.class.to_s.underscore.sub('texter/', '')
81 | action_name = @action
82 |
83 | "#{class_name}/#{action_name}"
84 | end
85 |
86 | def set_instance_variables_for_rendering
87 | instance_variables.each do |var|
88 | set_instance_variable(var.to_s.sub('@', ''), instance_variable_get(var))
89 | end
90 | end
91 | end
92 | end
93 |
--------------------------------------------------------------------------------
/lib/textris/delay/sidekiq/serializer.rb:
--------------------------------------------------------------------------------
1 | module Textris
2 | module Delay
3 | module Sidekiq
4 | module Serializer
5 | ACTIVERECORD_POINTER = 'Textris::ActiveRecordPointer'
6 | ACTIVERECORD_ARRAY_POINTER = 'Textris::ActiveRecordArrayPointer'
7 |
8 | class << self
9 | def serialize(objects)
10 | objects.collect do |object|
11 | serialize_active_record_object(object) ||
12 | serialize_active_record_array(object) ||
13 | serialize_active_record_relation(object) ||
14 | object
15 | end
16 | rescue NameError
17 | objects
18 | end
19 |
20 | def deserialize(objects)
21 | objects.collect do |object|
22 | deserialize_active_record_object(object) ||
23 | object
24 | end
25 | end
26 |
27 | private
28 |
29 | def serialize_active_record_object(object)
30 | if object.class < ActiveRecord::Base && object.id.present?
31 | [ACTIVERECORD_POINTER, object.class.to_s, object.id]
32 | end
33 | end
34 |
35 | def serialize_active_record_relation(array)
36 | if array.class < ActiveRecord::Relation
37 | [ACTIVERECORD_ARRAY_POINTER, array.model.to_s, array.map(&:id)]
38 | end
39 | end
40 |
41 | def serialize_active_record_array(array)
42 | if array.is_a?(Array) &&
43 | (model = get_active_record_common_model(array))
44 | [ACTIVERECORD_ARRAY_POINTER, model, array.map(&:id)]
45 | end
46 | end
47 |
48 | def deserialize_active_record_object(object)
49 | if object.is_a?(Array) &&
50 | object.try(:length) == 3 &&
51 | [ACTIVERECORD_POINTER,
52 | ACTIVERECORD_ARRAY_POINTER].include?(object[0])
53 | object[1].constantize.find(object[2])
54 | end
55 | end
56 |
57 | def get_active_record_common_model(items)
58 | items = items.collect do |item|
59 | if item.class < ActiveRecord::Base
60 | item.class.to_s
61 | end
62 | end.uniq
63 |
64 | if items.size == 1
65 | items.first
66 | end
67 | end
68 | end
69 | end
70 | end
71 | end
72 | end
73 |
--------------------------------------------------------------------------------
/spec/textris/delivery_spec.rb:
--------------------------------------------------------------------------------
1 | describe Textris::Delivery do
2 | describe '#get' do
3 | before do
4 | Object.send(:remove_const, :Rails) if defined?(Rails)
5 |
6 | class FakeEnv
7 | def initialize(options = {})
8 | self.name = 'development'
9 | end
10 |
11 | def name=(value)
12 | @development = false
13 | @test = false
14 | @production = false
15 |
16 | case value.to_s
17 | when 'development'
18 | @development = true
19 | when 'test'
20 | @test = true
21 | when 'production'
22 | @production = true
23 | end
24 | end
25 |
26 | def development?
27 | @development
28 | end
29 |
30 | def test?
31 | @test
32 | end
33 |
34 | def production?
35 | @production
36 | end
37 | end
38 |
39 | Rails = OpenStruct.new(
40 | :application => OpenStruct.new(
41 | :config => OpenStruct.new(
42 | :textris_delivery_method => ['mail', 'test']
43 | )
44 | ),
45 | :env => FakeEnv.new(
46 | :test? => false
47 | )
48 | )
49 | end
50 |
51 | after do
52 | Object.send(:remove_const, :Rails) if defined?(Rails)
53 | end
54 |
55 | it 'maps delivery methods from Rails config to delivery classes' do
56 | expect(Textris::Delivery.get).to eq([
57 | Textris::Delivery::Mail,
58 | Textris::Delivery::Test])
59 | end
60 |
61 | it 'returns an array even for single delivery method' do
62 | Rails.application.config.textris_delivery_method = 'mail'
63 |
64 | expect(Textris::Delivery.get).to eq([
65 | Textris::Delivery::Mail])
66 | end
67 |
68 | it 'defaults to "log" method in development environment' do
69 | Rails.application.config.textris_delivery_method = nil
70 | Rails.env.name = 'development'
71 |
72 | expect(Textris::Delivery.get).to eq([
73 | Textris::Delivery::Log])
74 | end
75 |
76 | it 'defaults to "test" method in test enviroment' do
77 | Rails.application.config.textris_delivery_method = nil
78 | Rails.env.name = 'test'
79 |
80 | expect(Textris::Delivery.get).to eq([
81 | Textris::Delivery::Test])
82 | end
83 |
84 | it 'defaults to "mail" method in production enviroment' do
85 | Rails.application.config.textris_delivery_method = nil
86 | Rails.env.name = 'production'
87 |
88 | expect(Textris::Delivery.get).to eq([
89 | Textris::Delivery::Mail])
90 | end
91 | end
92 | end
93 |
--------------------------------------------------------------------------------
/spec/textris/delay/active_job_spec.rb:
--------------------------------------------------------------------------------
1 | describe Textris::Delay::ActiveJob do
2 | before do
3 | class MyTexter < Textris::Base
4 | def delayed_action(phone)
5 | text :to => phone
6 | end
7 | end
8 |
9 | class ActiveJob::Logging::LogSubscriber
10 | def info(*args, &block)
11 | end
12 | end
13 | end
14 |
15 | context 'ActiveJob not present' do
16 | let(:message) do
17 | Textris::Message.new(
18 | :content => 'X',
19 | :from => 'X',
20 | :to => '+48 111 222 333')
21 | end
22 |
23 | before do
24 | delegate = Class.new.include(Textris::Delay::ActiveJob::Missing)
25 | delegate = delegate.new
26 |
27 | [:deliver_now, :deliver_later].each do |method|
28 | allow(message).to receive(method) { delegate.send(method) }
29 | end
30 | end
31 |
32 | describe '#deliver_now' do
33 | it 'raises' do
34 | expect do
35 | message.deliver_now
36 | end.to raise_error(LoadError)
37 | end
38 | end
39 |
40 | describe '#deliver_later' do
41 | it 'raises' do
42 | expect do
43 | message.deliver_later
44 | end.to raise_error(LoadError)
45 | end
46 | end
47 | end
48 |
49 | context 'ActiveJob present' do
50 | describe '#deliver_now' do
51 | before do
52 | class XDelivery < Textris::Delivery::Base
53 | def deliver(to); end
54 | end
55 |
56 | class YDelivery < Textris::Delivery::Base
57 | def deliver(to); end
58 | end
59 | end
60 |
61 | it 'works the same as #deliver' do
62 | expect(Textris::Delivery).to receive(:get).
63 | and_return([XDelivery, YDelivery])
64 |
65 | message = Textris::Message.new(
66 | :content => 'X',
67 | :from => 'X',
68 | :to => '+48 111 222 333')
69 |
70 | expect_any_instance_of(XDelivery).to receive(:deliver_to_all)
71 | expect_any_instance_of(YDelivery).to receive(:deliver_to_all)
72 |
73 | message.deliver_now
74 | end
75 | end
76 |
77 | describe '#deliver_later' do
78 | before do
79 | Object.send(:remove_const, :Rails) if defined?(Rails)
80 |
81 | Rails = OpenStruct.new(
82 | :application => OpenStruct.new(
83 | :config => OpenStruct.new(
84 | :textris_delivery_method => [:null]
85 | )
86 | )
87 | )
88 | end
89 |
90 | after do
91 | Object.send(:remove_const, :Rails) if defined?(Rails)
92 | end
93 |
94 | it 'schedules action with proper params' do
95 | job = MyTexter.delayed_action('48111222333').deliver_later
96 | expect(job.queue_name).to eq 'textris'
97 |
98 | job = MyTexter.delayed_action('48111222333').deliver_later(:queue => :custom)
99 | expect(job.queue_name).to eq 'custom'
100 | end
101 |
102 | it 'executes job properly' do
103 | job = Textris::Delay::ActiveJob::Job.new
104 |
105 | expect_any_instance_of(Textris::Message).to receive(:deliver_now)
106 |
107 | job.perform('MyTexter', :delayed_action, ['48111222333'])
108 | end
109 | end
110 | end
111 | end
112 |
--------------------------------------------------------------------------------
/lib/textris/delivery/mail.rb:
--------------------------------------------------------------------------------
1 | module Textris
2 | module Delivery
3 | class Mail < Textris::Delivery::Base
4 | class Mailer < ActionMailer::Base
5 | def notify(from, to, subject, body)
6 | mail :from => from, :to => to, :subject => subject, :body => body
7 | end
8 | end
9 |
10 | def deliver(to)
11 | template_vars = { :to_phone => to }
12 |
13 | from = apply_template from_template, template_vars
14 | to = apply_template to_template, template_vars
15 | subject = apply_template subject_template, template_vars
16 | body = apply_template body_template, template_vars
17 |
18 | ::Textris::Delivery::Mail::Mailer.notify(
19 | from, to, subject, body).deliver
20 | end
21 |
22 | private
23 |
24 | def from_template
25 | Rails.application.config.try(:textris_mail_from_template) ||
26 | "#{from_format}@%{env:d}.%{app:d}.com"
27 | end
28 |
29 | def from_format
30 | if message.twilio_messaging_service_sid
31 | '%{twilio_messaging_service_sid}'
32 | else
33 | '%{from_name:d}-%{from_phone}'
34 | end
35 | end
36 |
37 | def to_template
38 | Rails.application.config.try(:textris_mail_to_template) ||
39 | "%{app:d}-%{env:d}-%{to_phone}-texts@mailinator.com"
40 | end
41 |
42 | def subject_template
43 | Rails.application.config.try(:textris_mail_subject_template) ||
44 | "%{texter:dh} texter: %{action:h}"
45 | end
46 |
47 | def body_template
48 | Rails.application.config.try(:textris_mail_body_template) ||
49 | "%{content}"
50 | end
51 |
52 | def apply_template(template, variables)
53 | template.gsub(/\%\{[a-z_:]+\}/) do |match|
54 | directive = match.gsub(/[%{}]/, '')
55 | key = directive.split(':').first
56 | modifiers = directive.split(':')[1] || ''
57 |
58 | content = get_template_interpolation(key, variables)
59 | content = apply_template_modifiers(content, modifiers.chars)
60 | content = 'unknown' unless content.present?
61 |
62 | content
63 | end
64 | end
65 |
66 | def get_template_interpolation(key, variables)
67 | case key
68 | when 'app', 'env'
69 | get_rails_variable(key)
70 | when 'texter', 'action', 'from_name', 'from_phone', 'content', 'twilio_messaging_service_sid'
71 | message.send(key)
72 | when 'media_urls'
73 | message.media_urls.join(', ')
74 | else
75 | variables[key.to_sym]
76 | end.to_s.strip
77 | end
78 |
79 | def get_rails_variable(var)
80 | case var
81 | when 'app'
82 | Rails.application.class.parent_name
83 | when 'env'
84 | Rails.env
85 | end
86 | end
87 |
88 | def apply_template_modifiers(content, modifiers)
89 | modifiers.each do |modifier|
90 | case modifier
91 | when 'd'
92 | content = content.underscore.dasherize
93 | when 'h'
94 | content = content.humanize.gsub(/[-_]/, ' ')
95 | when 'p'
96 | content = Phony.format(content) rescue content
97 | end
98 | end
99 |
100 | content
101 | end
102 | end
103 | end
104 | end
105 |
--------------------------------------------------------------------------------
/example/rails-4.2/Gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: ../..
3 | specs:
4 | textris (0.5.0)
5 | actionmailer (>= 4.0)
6 | activejob (>= 4.2)
7 | activesupport (>= 4.2)
8 | phony (~> 2.8)
9 | render_anywhere (~> 0.0)
10 |
11 | GEM
12 | remote: https://rubygems.org/
13 | specs:
14 | actionmailer (4.2.11)
15 | actionpack (= 4.2.11)
16 | actionview (= 4.2.11)
17 | activejob (= 4.2.11)
18 | mail (~> 2.5, >= 2.5.4)
19 | rails-dom-testing (~> 1.0, >= 1.0.5)
20 | actionpack (4.2.11)
21 | actionview (= 4.2.11)
22 | activesupport (= 4.2.11)
23 | rack (~> 1.6)
24 | rack-test (~> 0.6.2)
25 | rails-dom-testing (~> 1.0, >= 1.0.5)
26 | rails-html-sanitizer (~> 1.0, >= 1.0.2)
27 | actionview (4.2.11)
28 | activesupport (= 4.2.11)
29 | builder (~> 3.1)
30 | erubis (~> 2.7.0)
31 | rails-dom-testing (~> 1.0, >= 1.0.5)
32 | rails-html-sanitizer (~> 1.0, >= 1.0.3)
33 | activejob (4.2.11)
34 | activesupport (= 4.2.11)
35 | globalid (>= 0.3.0)
36 | activemodel (4.2.11)
37 | activesupport (= 4.2.11)
38 | builder (~> 3.1)
39 | activerecord (4.2.11)
40 | activemodel (= 4.2.11)
41 | activesupport (= 4.2.11)
42 | arel (~> 6.0)
43 | activesupport (4.2.11)
44 | i18n (~> 0.7)
45 | minitest (~> 5.1)
46 | thread_safe (~> 0.3, >= 0.3.4)
47 | tzinfo (~> 1.1)
48 | arel (6.0.4)
49 | builder (3.2.3)
50 | concurrent-ruby (1.1.5)
51 | crass (1.0.4)
52 | erubis (2.7.0)
53 | globalid (0.4.2)
54 | activesupport (>= 4.2.0)
55 | i18n (0.9.5)
56 | concurrent-ruby (~> 1.0)
57 | loofah (2.2.3)
58 | crass (~> 1.0.2)
59 | nokogiri (>= 1.5.9)
60 | mail (2.7.1)
61 | mini_mime (>= 0.1.1)
62 | mini_mime (1.0.1)
63 | mini_portile2 (2.4.0)
64 | minitest (5.11.3)
65 | nokogiri (1.10.2)
66 | mini_portile2 (~> 2.4.0)
67 | phony (2.17.1)
68 | rack (1.6.11)
69 | rack-test (0.6.3)
70 | rack (>= 1.0)
71 | rails (4.2.11)
72 | actionmailer (= 4.2.11)
73 | actionpack (= 4.2.11)
74 | actionview (= 4.2.11)
75 | activejob (= 4.2.11)
76 | activemodel (= 4.2.11)
77 | activerecord (= 4.2.11)
78 | activesupport (= 4.2.11)
79 | bundler (>= 1.3.0, < 2.0)
80 | railties (= 4.2.11)
81 | sprockets-rails
82 | rails-deprecated_sanitizer (1.0.3)
83 | activesupport (>= 4.2.0.alpha)
84 | rails-dom-testing (1.0.9)
85 | activesupport (>= 4.2.0, < 5.0)
86 | nokogiri (~> 1.6)
87 | rails-deprecated_sanitizer (>= 1.0.1)
88 | rails-html-sanitizer (1.0.4)
89 | loofah (~> 2.2, >= 2.2.2)
90 | railties (4.2.11)
91 | actionpack (= 4.2.11)
92 | activesupport (= 4.2.11)
93 | rake (>= 0.8.7)
94 | thor (>= 0.18.1, < 2.0)
95 | rake (12.3.2)
96 | render_anywhere (0.0.12)
97 | rails (>= 3.0.7)
98 | sprockets (3.7.2)
99 | concurrent-ruby (~> 1.0)
100 | rack (> 1, < 3)
101 | sprockets-rails (3.2.1)
102 | actionpack (>= 4.0)
103 | activesupport (>= 4.0)
104 | sprockets (>= 3.0.0)
105 | sqlite3 (1.4.0)
106 | thor (0.20.3)
107 | thread_safe (0.3.6)
108 | tzinfo (1.2.5)
109 | thread_safe (~> 0.1)
110 |
111 | PLATFORMS
112 | ruby
113 |
114 | DEPENDENCIES
115 | rails (= 4.2.11)
116 | sqlite3
117 | textris!
118 |
119 | BUNDLED WITH
120 | 1.16.1
121 |
--------------------------------------------------------------------------------
/example/rails-4.2/config/environments/production.rb:
--------------------------------------------------------------------------------
1 | Rails.application.configure do
2 | # Settings specified here will take precedence over those in config/application.rb.
3 |
4 | # Code is not reloaded between requests.
5 | config.cache_classes = true
6 |
7 | # Eager load code on boot. This eager loads most of Rails and
8 | # your application in memory, allowing both threaded web servers
9 | # and those relying on copy on write to perform better.
10 | # Rake tasks automatically ignore this option for performance.
11 | config.eager_load = true
12 |
13 | # Full error reports are disabled and caching is turned on.
14 | config.consider_all_requests_local = false
15 | config.action_controller.perform_caching = true
16 |
17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application
18 | # Add `rack-cache` to your Gemfile before enabling this.
19 | # For large-scale production use, consider using a caching reverse proxy like
20 | # NGINX, varnish or squid.
21 | # config.action_dispatch.rack_cache = true
22 |
23 | # Disable serving static files from the `/public` folder by default since
24 | # Apache or NGINX already handles this.
25 | config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present?
26 |
27 | # Compress JavaScripts and CSS.
28 | config.assets.js_compressor = :uglifier
29 | # config.assets.css_compressor = :sass
30 |
31 | # Do not fallback to assets pipeline if a precompiled asset is missed.
32 | config.assets.compile = false
33 |
34 | # Asset digests allow you to set far-future HTTP expiration dates on all assets,
35 | # yet still be able to expire them through the digest params.
36 | config.assets.digest = true
37 |
38 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
39 |
40 | # Specifies the header that your server uses for sending files.
41 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
42 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
43 |
44 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
45 | # config.force_ssl = true
46 |
47 | # Use the lowest log level to ensure availability of diagnostic information
48 | # when problems arise.
49 | config.log_level = :debug
50 |
51 | # Prepend all log lines with the following tags.
52 | # config.log_tags = [ :subdomain, :uuid ]
53 |
54 | # Use a different logger for distributed setups.
55 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new)
56 |
57 | # Use a different cache store in production.
58 | # config.cache_store = :mem_cache_store
59 |
60 | # Enable serving of images, stylesheets, and JavaScripts from an asset server.
61 | # config.action_controller.asset_host = 'http://assets.example.com'
62 |
63 | # Ignore bad email addresses and do not raise email delivery errors.
64 | # Set this to true and configure the email server for immediate delivery to raise delivery errors.
65 | # config.action_mailer.raise_delivery_errors = false
66 |
67 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
68 | # the I18n.default_locale when a translation cannot be found).
69 | config.i18n.fallbacks = true
70 |
71 | # Send deprecation notices to registered listeners.
72 | config.active_support.deprecation = :notify
73 |
74 | # Use default logging formatter so that PID and timestamp are not suppressed.
75 | config.log_formatter = ::Logger::Formatter.new
76 |
77 | # Do not dump schema after migrations.
78 | config.active_record.dump_schema_after_migration = false
79 | end
80 |
--------------------------------------------------------------------------------
/lib/textris/message.rb:
--------------------------------------------------------------------------------
1 | module Textris
2 | class Message
3 | attr_reader :content, :from_name, :from_phone, :to, :texter, :action, :args,
4 | :media_urls, :twilio_messaging_service_sid
5 |
6 | def initialize(options = {})
7 | initialize_content(options)
8 | initialize_author(options)
9 | initialize_recipients(options)
10 |
11 | @texter = options[:texter]
12 | @action = options[:action]
13 | @args = options[:args]
14 | @media_urls = options[:media_urls]
15 | end
16 |
17 | def deliver
18 | deliveries = ::Textris::Delivery.get
19 | deliveries.each do |delivery|
20 | delivery.new(self).deliver_to_all
21 | end
22 |
23 | self
24 | end
25 |
26 | def texter(options = {})
27 | if options[:raw]
28 | @texter
29 | elsif @texter.present?
30 | @texter.to_s.split('::').last.to_s.sub(/Texter$/, '')
31 | end
32 | end
33 |
34 | def from
35 | if @from_phone.present?
36 | if @from_name.present?
37 | if PhoneFormatter.is_alphameric?(@from_phone)
38 | @from_phone
39 | else
40 | if PhoneFormatter.is_a_short_code?(@from_phone)
41 | "#{@from_name} <#{@from_phone}>"
42 | else
43 | "#{@from_name} <#{Phony.format(@from_phone)}>"
44 | end
45 | end
46 | else
47 | Phony.format(@from_phone)
48 | end
49 | elsif @from_name.present?
50 | @from_name
51 | end
52 | end
53 |
54 | def content
55 | @content ||= parse_content(@renderer.render_content)
56 | end
57 |
58 | private
59 |
60 | def initialize_content(options)
61 | if options[:content].present?
62 | @content = parse_content options[:content]
63 | elsif options[:renderer].present?
64 | @renderer = options[:renderer]
65 | else
66 | raise(ArgumentError, "Content must be provided")
67 | end
68 | end
69 |
70 | def initialize_author(options)
71 | if options.has_key?(:twilio_messaging_service_sid)
72 | @twilio_messaging_service_sid = options[:twilio_messaging_service_sid]
73 | elsif options.has_key?(:from)
74 | @from_name, @from_phone = parse_from options[:from]
75 | else
76 | @from_name = options[:from_name]
77 | @from_phone = options[:from_phone]
78 | end
79 | end
80 |
81 | def initialize_recipients(options)
82 | @to = parse_to options[:to]
83 |
84 | unless @to.present?
85 | raise(ArgumentError, "Recipients must be provided and E.164 compliant")
86 | end
87 | end
88 |
89 | def parse_from(from)
90 | parse_from_dual(from) || parse_from_singular(from)
91 | end
92 |
93 | def parse_from_dual(from)
94 | matches = from.match(/(.*)\<(.*)\>\s*$/)
95 | return unless matches
96 | name, sender_id = matches.captures
97 | return unless name && sender_id
98 |
99 | if Phony.plausible?(sender_id) || PhoneFormatter.is_a_short_code?(sender_id)
100 | [name.strip, Phony.normalize(sender_id)]
101 | elsif PhoneFormatter.is_alphameric?(sender_id)
102 | [name.strip, sender_id]
103 | end
104 | end
105 |
106 | def parse_from_singular(from)
107 | if Phony.plausible?(from)
108 | [nil, Phony.normalize(from)]
109 | elsif PhoneFormatter.is_a_short_code?(from)
110 | [nil, from.to_s]
111 | elsif from.present?
112 | [from.strip, nil]
113 | end
114 | end
115 |
116 | def parse_to(to)
117 | to = [*to]
118 | to = to.select { |phone| Phony.plausible?(phone.to_s) }
119 | to = to.map { |phone| Phony.normalize(phone.to_s) }
120 |
121 | to
122 | end
123 |
124 | def parse_content(content)
125 | content = content.to_s
126 | content = content.rstrip
127 |
128 | content
129 | end
130 | end
131 | end
132 |
--------------------------------------------------------------------------------
/spec/textris/delivery/twilio_spec.rb:
--------------------------------------------------------------------------------
1 | describe Textris::Delivery::Twilio do
2 |
3 | before do
4 | class MessageArray
5 | @created = []
6 |
7 | class << self
8 | attr_reader :created
9 | end
10 |
11 | def create(message)
12 | self.class.created.push(message)
13 | end
14 | end
15 |
16 | module Twilio
17 | module REST
18 | class Client
19 | attr_reader :messages
20 |
21 | def initialize
22 | @messages = MessageArray.new
23 | end
24 | end
25 | end
26 | end
27 | end
28 |
29 | describe "sending multiple messages" do
30 | let(:message) do
31 | Textris::Message.new(
32 | :to => ['+48 600 700 800', '+48 100 200 300'],
33 | :content => 'Some text')
34 | end
35 |
36 | let(:delivery) { Textris::Delivery::Twilio.new(message) }
37 |
38 | it 'responds to :deliver_to_all' do
39 | expect(delivery).to respond_to(:deliver_to_all)
40 | end
41 |
42 | it 'invokes Twilio REST client for each recipient' do
43 | expect_any_instance_of(MessageArray).to receive(:create).twice do |context, msg|
44 | expect(msg).to have_key(:to)
45 | expect(msg).to have_key(:body)
46 | expect(msg).not_to have_key(:media_url)
47 | expect(msg[:body]).to eq(message.content)
48 | end
49 |
50 | delivery.deliver_to_all
51 | end
52 | end
53 |
54 | describe "sending media messages" do
55 | let(:message) do
56 | Textris::Message.new(
57 | :to => ['+48 600 700 800', '+48 100 200 300'],
58 | :content => 'Some text',
59 | :media_urls => [
60 | 'http://example.com/boo.gif',
61 | 'http://example.com/yay.gif'])
62 | end
63 |
64 | let(:delivery) { Textris::Delivery::Twilio.new(message) }
65 |
66 | it 'invokes Twilio REST client for each recipient' do
67 | expect_any_instance_of(MessageArray).to receive(:create).twice do |context, msg|
68 | expect(msg).to have_key(:media_url)
69 | expect(msg[:media_url]).to eq(message.media_urls)
70 | end
71 |
72 | delivery.deliver_to_all
73 | end
74 | end
75 |
76 | describe 'sending a message using messaging service sid' do
77 | let(:message) do
78 | Textris::Message.new(
79 | to: '+48 600 700 800',
80 | content: 'Some text',
81 | twilio_messaging_service_sid: 'MG9752274e9e519418a7406176694466fa')
82 | end
83 |
84 | let(:delivery) { Textris::Delivery::Twilio.new(message) }
85 |
86 | it 'uses the sid instead of from for the message' do
87 | delivery.deliver('+11234567890')
88 |
89 | expect(MessageArray.created.last[:from]).to be_nil
90 | expect(MessageArray.created.last[:messaging_service_sid])
91 | .to eq('MG9752274e9e519418a7406176694466fa')
92 | end
93 | end
94 |
95 | describe "sending from short codes" do
96 | it 'prepends regular phone numbers code with a +' do
97 | number = '48 600 700 800'
98 | message = Textris::Message.new(
99 | :from => number,
100 | :content => 'Some text',
101 | :to => '+48 100 200 300'
102 | )
103 | delivery = Textris::Delivery::Twilio.new(message)
104 |
105 | expect_any_instance_of(MessageArray).to receive(:create).once do |context, msg|
106 | expect(msg).to have_key(:from)
107 | expect(msg[:from]).to eq("+#{number.gsub(/\s/, '')}")
108 | end
109 |
110 | delivery.deliver_to_all
111 | end
112 |
113 | it 'doesn\'t prepend a 6 digit short code with a +' do
114 | number = '894546'
115 | message = Textris::Message.new(
116 | :from => number,
117 | :content => 'Some text',
118 | :to => '+48 100 200 300'
119 | )
120 | delivery = Textris::Delivery::Twilio.new(message)
121 |
122 | expect_any_instance_of(MessageArray).to receive(:create).once do |context, msg|
123 | expect(msg).to have_key(:from)
124 | expect(msg[:from]).to eq(number)
125 | end
126 |
127 | delivery.deliver_to_all
128 | end
129 |
130 | it 'doesn\'t prepend a 5 digit short code with a +' do
131 | number = '44397'
132 | message = Textris::Message.new(
133 | :from => number,
134 | :content => 'Some text',
135 | :to => '+48 100 200 300'
136 | )
137 | delivery = Textris::Delivery::Twilio.new(message)
138 |
139 | expect_any_instance_of(MessageArray).to receive(:create).once do |context, msg|
140 | expect(msg).to have_key(:from)
141 | expect(msg[:from]).to eq(number)
142 | end
143 |
144 | delivery.deliver_to_all
145 | end
146 | end
147 | end
148 |
--------------------------------------------------------------------------------
/spec/textris/base_spec.rb:
--------------------------------------------------------------------------------
1 | describe Textris::Base do
2 | describe '#default' do
3 | it 'sets defaults' do
4 | some_texter = Class.new(Textris::Base)
5 |
6 | expect do
7 | some_texter.instance_eval do
8 | default :from => "Me"
9 | end
10 | end.not_to raise_error
11 |
12 | defaults = some_texter.instance_variable_get('@defaults')[:from]
13 |
14 | expect(some_texter.instance_variable_get('@defaults')).to have_key(:from)
15 | end
16 |
17 | it 'keeps separate defaults for each descendant' do
18 | some_texter = Class.new(Textris::Base)
19 | other_texter = Class.new(Textris::Base)
20 | deep_texter = Class.new(some_texter)
21 |
22 | some_texter.instance_eval do
23 | default :from => "Me"
24 | end
25 |
26 | other_texter.instance_eval do
27 | default :to => "123"
28 | end
29 |
30 | deep_texter.instance_eval do
31 | default :from => "Us", :to => "456"
32 | end
33 |
34 | defaults = some_texter.instance_variable_get('@defaults')
35 | expect(defaults).to have_key(:from)
36 | expect(defaults).not_to have_key(:to)
37 |
38 | defaults = other_texter.instance_variable_get('@defaults')
39 | expect(defaults).not_to have_key(:from)
40 | expect(defaults).to have_key(:to)
41 |
42 | defaults = deep_texter.instance_variable_get('@defaults')
43 | expect(defaults[:from]).to eq 'Us'
44 | expect(defaults[:to]).to eq '456'
45 | end
46 |
47 | it "inherits defaults from parent class" do
48 | parent = Class.new(Textris::Base)
49 | parent.instance_eval do
50 | default :from => "Me"
51 | end
52 |
53 | child = Class.new(parent)
54 |
55 | expect(child.with_defaults({})).to eq({ :from => "Me" })
56 | end
57 |
58 | it "overrides defaults from parent class" do
59 | parent = Class.new(Textris::Base)
60 | parent.instance_eval do
61 | default :from => "Me"
62 | end
63 |
64 | child = Class.new(parent)
65 | child.instance_eval do
66 | default :from => "Not me", :to => "Me"
67 | end
68 |
69 | expect(child.with_defaults({})).to eq({ :from => "Not me", :to => "Me" })
70 | end
71 | end
72 |
73 | describe '#with_defaults' do
74 | it 'merges back defaults' do
75 | some_texter = Class.new(Textris::Base)
76 |
77 | some_texter.instance_eval do
78 | default :from => "Me"
79 | end
80 |
81 | options = some_texter.with_defaults(:to => '123')
82 |
83 | expect(options[:from]).to eq 'Me'
84 | expect(options[:to]).to eq '123'
85 | end
86 | end
87 |
88 | describe '#deliveries' do
89 | it 'maps to Textris::Delivery::Test.deliveries' do
90 | allow(Textris::Delivery::Test).to receive_messages(:deliveries => ['x'])
91 |
92 | expect(Textris::Base.deliveries).to eq (['x'])
93 | end
94 | end
95 |
96 | describe '#text' do
97 | before do
98 | class MyTexter < Textris::Base
99 | def action_with_inline_body
100 | text :to => '48 600 700 800', :body => 'asd'
101 | end
102 |
103 | def action_with_template
104 | text :to => '48 600 700 800'
105 | end
106 |
107 | def set_instance_variable(key, value)
108 | end
109 | end
110 | end
111 |
112 | it 'renders inline content when :body provided' do
113 | MyTexter.action_with_inline_body
114 | end
115 |
116 | it 'defers template rendering when :body not provided' do
117 | render_options = {}
118 |
119 | expect_any_instance_of(MyTexter).not_to receive(:render)
120 |
121 | MyTexter.action_with_template
122 | end
123 | end
124 |
125 | describe '#my_action' do
126 | before do
127 | class MyTexter < Textris::Base
128 | def my_action(p)
129 | p[:calls] += 1
130 | end
131 | end
132 | end
133 |
134 | it 'calls actions on newly created instances' do
135 | call_info = { :calls => 0 }
136 |
137 | MyTexter.my_action(call_info)
138 |
139 | expect(call_info[:calls]).to eq(1)
140 | end
141 |
142 | it 'raises no method error on undefined actions' do
143 | expect { MyTexter.fake_action }.to raise_error NoMethodError
144 | end
145 |
146 | it 'responds to defined actions' do
147 | expect(MyTexter.respond_to?(:my_action)).to eq true
148 | end
149 |
150 | it 'does not respond to undefined actions' do
151 | expect(MyTexter.respond_to?(:fake_action)).to eq false
152 | end
153 | end
154 |
155 | describe Textris::Base::RenderingController do
156 | before do
157 | class Textris::Base::RenderingController
158 | def initialize(*args)
159 | end
160 | end
161 |
162 | class ActionMailer::Base
163 | def self.default_url_options
164 | 'x'
165 | end
166 | end
167 | end
168 |
169 | it 'maps default_url_options to ActionMailer configuration' do
170 | rendering_controller = Textris::Base::RenderingController.new
171 |
172 | expect(rendering_controller.default_url_options).to eq 'x'
173 | end
174 | end
175 | end
176 |
--------------------------------------------------------------------------------
/spec/textris/delivery/log_spec.rb:
--------------------------------------------------------------------------------
1 | describe Textris::Delivery::Log do
2 | let(:message) do
3 | Textris::Message.new(
4 | :from => 'Mr Jones <+48 555 666 777>',
5 | :to => ['+48 600 700 800', '48100200300'],
6 | :content => 'Some text')
7 | end
8 |
9 | let(:delivery) { Textris::Delivery::Log.new(message) }
10 | let(:logger) { FakeLogger.new }
11 |
12 | before do
13 | class FakeLogger
14 | def log(kind = :all)
15 | @log[kind.to_s] || ""
16 | end
17 |
18 | def method_missing(name, *args)
19 | if Textris::Delivery::Log::AVAILABLE_LOG_LEVELS.include?(name.to_s)
20 | @log ||= {}
21 | @log[name.to_s] ||= ""
22 | @log[name.to_s] += args[0] + "\n"
23 | @log["all"] ||= ""
24 | @log["all"] += args[0] + "\n"
25 | end
26 | end
27 | end
28 |
29 | Object.send(:remove_const, :Rails) if defined?(Rails)
30 |
31 | Rails = OpenStruct.new(
32 | :logger => logger,
33 | :application => OpenStruct.new(
34 | :config => OpenStruct.new
35 | )
36 | )
37 | end
38 |
39 | after do
40 | Object.send(:remove_const, :Rails) if defined?(Rails)
41 | end
42 |
43 | it 'responds to :deliver_to_all' do
44 | expect(delivery).to respond_to(:deliver_to_all)
45 | end
46 |
47 | it 'prints proper delivery information to log' do
48 | delivery.deliver_to_all
49 |
50 | expect(logger.log(:info)).to include "Sent text to +48 600 700 800"
51 | expect(logger.log(:info)).to include "Sent text to +48 10 020 03 00"
52 |
53 | expect(logger.log(:debug)).to include "Date: "
54 | expect(logger.log(:debug)).to include "To: +48 600 700 800, +48 600 700 800"
55 | expect(logger.log(:debug)).to include "Texter: UnknownTexter#unknown_action"
56 | expect(logger.log(:debug)).to include "From: Mr Jones <+48 55 566 67 77>"
57 | expect(logger.log(:debug)).to include "Content: Some text"
58 | end
59 |
60 | it 'applies configured log level' do
61 | Rails.application.config.textris_log_level = :unknown
62 |
63 | delivery.deliver_to_all
64 |
65 | expect(logger.log(:info)).to be_blank
66 | expect(logger.log(:debug)).to be_blank
67 | expect(logger.log(:unknown)).not_to be_blank
68 | end
69 |
70 | it 'throws error if configured log level is wrong' do
71 | Rails.application.config.textris_log_level = :wronglevel
72 |
73 | expect do
74 | delivery.deliver_to_all
75 | end.to raise_error(ArgumentError)
76 | end
77 |
78 | context "message with from name and no from phone" do
79 | let(:message) do
80 | Textris::Message.new(
81 | :from => 'Mr Jones',
82 | :to => ['+48 600 700 800', '48100200300'],
83 | :content => 'Some text')
84 | end
85 |
86 | it 'prints proper delivery information to log' do
87 | delivery.deliver_to_all
88 |
89 | expect(logger.log).to include "From: Mr Jones"
90 | end
91 | end
92 |
93 | context "message with from phone and no from name" do
94 | let(:message) do
95 | Textris::Message.new(
96 | :from => '+48 55 566 67 77',
97 | :to => ['+48 600 700 800', '48100200300'],
98 | :content => 'Some text')
99 | end
100 |
101 | it 'prints proper delivery information to log' do
102 | delivery.deliver_to_all
103 |
104 | expect(logger.log).to include "From: +48 55 566 67 77"
105 | end
106 | end
107 |
108 | context "message with no from" do
109 | let(:message) do
110 | Textris::Message.new(
111 | :to => ['+48 600 700 800', '48100200300'],
112 | :content => 'Some text')
113 | end
114 |
115 | it 'prints proper delivery information to log' do
116 | delivery.deliver_to_all
117 |
118 | expect(logger.log).to include "From: unknown"
119 | end
120 | end
121 |
122 | context "message with twilio messaging service sid" do
123 | let(:message) do
124 | Textris::Message.new(
125 | :twilio_messaging_service_sid => 'MG9752274e9e519418a7406176694466fa',
126 | :to => ['+48 600 700 800', '48100200300'],
127 | :content => 'Some text')
128 | end
129 |
130 | it 'prints proper delivery information to log' do
131 | delivery.deliver_to_all
132 |
133 | expect(logger.log).to include "From: MG9752274e9e519418a7406176694466fa"
134 | end
135 | end
136 |
137 | context "message with texter and action" do
138 | let(:message) do
139 | Textris::Message.new(
140 | :texter => "MyClass",
141 | :action => "my_action",
142 | :to => ['+48 600 700 800', '48100200300'],
143 | :content => 'Some text')
144 | end
145 |
146 | it 'prints proper delivery information to log' do
147 | delivery.deliver_to_all
148 |
149 | expect(logger.log).to include "Texter: MyClass#my_action"
150 | end
151 | end
152 |
153 | context "message with media urls" do
154 | let(:message) do
155 | Textris::Message.new(
156 | :from => 'Mr Jones <+48 555 666 777>',
157 | :to => ['+48 600 700 800', '48100200300'],
158 | :content => 'Some text',
159 | :media_urls => [
160 | "http://example.com/hilarious.gif",
161 | "http://example.org/serious.gif"])
162 | end
163 |
164 | it 'prints all the media URLs' do
165 | delivery.deliver_to_all
166 |
167 | expect(logger.log).to include "Media URLs: http://example.com/hilarious.gif"
168 | expect(logger.log).to include " http://example.org/serious.gif"
169 | end
170 | end
171 | end
172 |
--------------------------------------------------------------------------------
/spec/textris/delivery/mail_spec.rb:
--------------------------------------------------------------------------------
1 | describe Textris::Delivery::Mail do
2 | let(:message) do
3 | Textris::Message.new(
4 | :from => 'Mr Jones <+48 555 666 777>',
5 | :to => ['+48 600 700 800', '+48 100 200 300'],
6 | :content => 'Some text',
7 | :texter => 'Namespace::MyCuteTexter',
8 | :action => 'my_action',
9 | :media_urls => ['http://example.com/hilarious.gif', 'http://example.org/serious.gif'])
10 | end
11 |
12 | let(:delivery) { Textris::Delivery::Mail.new(message) }
13 |
14 | before do
15 | Object.send(:remove_const, :Rails) if defined?(Rails)
16 |
17 | module MyAppName
18 | class Application < OpenStruct; end
19 | end
20 |
21 | Rails = OpenStruct.new(
22 | :application => MyAppName::Application.new(
23 | :config => OpenStruct.new
24 | ),
25 | :env => 'test'
26 | )
27 |
28 | class FakeMail
29 | def self.deliveries
30 | @deliveries || []
31 | end
32 |
33 | def self.deliver(message)
34 | @deliveries ||= []
35 | @deliveries.push(message)
36 | end
37 |
38 | def initialize(message)
39 | @message = message
40 | end
41 |
42 | def deliver
43 | self.class.deliver(@message)
44 | end
45 | end
46 |
47 | allow(Textris::Delivery::Mail::Mailer
48 | ).to receive(:notify) do |from, to, subject, body|
49 | FakeMail.new(
50 | :from => from,
51 | :to => to,
52 | :subject => subject,
53 | :body => body)
54 | end
55 | end
56 |
57 | after do
58 | Object.send(:remove_const, :Rails) if defined?(Rails)
59 | end
60 |
61 | it 'responds to :deliver_to_all' do
62 | expect(delivery).to respond_to(:deliver_to_all)
63 | end
64 |
65 | it 'invokes ActionMailer for each recipient' do
66 | expect(Textris::Delivery::Mail::Mailer).to receive(:notify)
67 |
68 | delivery.deliver_to_all
69 |
70 | expect(FakeMail.deliveries.count).to eq 2
71 | end
72 |
73 | it 'reads templates from configuration' do
74 | Rails.application.config = OpenStruct.new(
75 | :textris_mail_from_template => 'a',
76 | :textris_mail_to_template => 'b',
77 | :textris_mail_subject_template => 'c',
78 | :textris_mail_body_template => 'd')
79 |
80 | delivery.deliver_to_all
81 |
82 | expect(FakeMail.deliveries.last).to eq(
83 | :from => 'a',
84 | :to => 'b',
85 | :subject => 'c',
86 | :body => 'd')
87 | end
88 |
89 | it 'defines default templates' do
90 | Rails.application.config = OpenStruct.new
91 |
92 | delivery.deliver_to_all
93 |
94 | expect(FakeMail.deliveries.last[:from]).to be_present
95 | expect(FakeMail.deliveries.last[:to]).to be_present
96 | expect(FakeMail.deliveries.last[:subject]).to be_present
97 | expect(FakeMail.deliveries.last[:body]).to be_present
98 | end
99 |
100 | it 'applies all template interpolations properly' do
101 | interpolations = %w{app env texter action from_name
102 | from_phone to_phone content media_urls}
103 |
104 | Rails.application.config = OpenStruct.new(
105 | :textris_mail_to_template => interpolations.map { |i| "%{#{i}}" }.join('-'))
106 |
107 | delivery.deliver_to_all
108 |
109 | expect(FakeMail.deliveries.last[:to].split('-')).to eq([
110 | 'MyAppName', 'test', 'MyCute', 'my_action', 'Mr Jones', '48555666777', '48100200300', 'Some text', 'http://example.com/hilarious.gif, http://example.org/serious.gif'])
111 | end
112 |
113 | it 'applies all template interpolation modifiers properly' do
114 | interpolations = %w{app:d texter:dhx action:h from_phone:p}
115 |
116 | Rails.application.config = OpenStruct.new(
117 | :textris_mail_to_template => interpolations.map { |i| "%{#{i}}" }.join('--'))
118 |
119 | delivery.deliver_to_all
120 |
121 | expect(FakeMail.deliveries.last[:to].split('--')).to eq([
122 | 'my-app-name', 'My cute', 'My action', '+48 55 566 67 77'])
123 | end
124 |
125 | context 'with incomplete message' do
126 | let(:message) do
127 | Textris::Message.new(
128 | :to => ['+48 600 700 800', '+48 100 200 300'],
129 | :content => 'Some text')
130 | end
131 |
132 | it 'applies all template interpolations properly when values missing' do
133 | interpolations = %w{app env texter action from_name
134 | from_phone to_phone content}
135 |
136 | Rails.env = nil
137 | Rails.application = OpenStruct.new
138 | Rails.application.config = OpenStruct.new(
139 | :textris_mail_to_template => interpolations.map { |i| "%{#{i}}" }.join('-'))
140 |
141 | delivery.deliver_to_all
142 |
143 | expect(FakeMail.deliveries.last[:to].split('-')).to eq([
144 | 'unknown', 'unknown', 'unknown', 'unknown', 'unknown', 'unknown', '48100200300', 'Some text'])
145 | end
146 | end
147 |
148 | context 'when sending using twilio messaging service sid' do
149 | let(:message) do
150 | Textris::Message.new(
151 | :to => ['+48 600 700 800', '+48 100 200 300'],
152 | :content => 'Some text',
153 | :twilio_messaging_service_sid => 'NG9752274e9e519418a7406176694466fb')
154 | end
155 |
156 | it 'uses the sid in from instead of name and phone' do
157 | delivery.deliver_to_all
158 |
159 | expect(FakeMail.deliveries.last[:from])
160 | .to eq('NG9752274e9e519418a7406176694466fb@test.my-app-name.com')
161 | end
162 | end
163 | end
164 |
165 | describe Textris::Delivery::Mail::Mailer do
166 | describe '#notify' do
167 | it 'invokes mail with given from, to subject and body' do
168 | mailer = Textris::Delivery::Mail::Mailer
169 |
170 | expect_any_instance_of(mailer).to receive(:mail).with(
171 | :from => "a", :to => "b" , :subject => "c", :body => "d")
172 |
173 | message = mailer.notify('a', 'b', 'c', 'd')
174 |
175 | if message.respond_to?(:deliver_now)
176 | message.deliver_now
177 | end
178 | end
179 | end
180 | end
181 |
--------------------------------------------------------------------------------
/spec/textris/delay/sidekiq_spec.rb:
--------------------------------------------------------------------------------
1 | describe Textris::Delay::Sidekiq do
2 | before do
3 | class MyTexter < Textris::Base
4 | def delayed_action(phone, body)
5 | text :to => phone, :body => body
6 | end
7 |
8 | def serialized_action(user)
9 | text :to => user.id, :body => 'Hello'
10 | end
11 |
12 | def serialized_array_action(users)
13 | text :to => users.first.id, :body => 'Hello all'
14 | end
15 | end
16 |
17 | module ActiveRecord
18 | class RecordNotFound < Exception; end
19 |
20 | class Base
21 | attr_reader :id
22 |
23 | def initialize(id)
24 | @id = id
25 | end
26 |
27 | def self.find(id)
28 | if id.is_a?(Array)
29 | id.collect do |id|
30 | id.to_i > 0 ? new(id) : raise(RecordNotFound)
31 | end
32 | else
33 | id.to_i > 0 ? new(id) : raise(RecordNotFound)
34 | end
35 | end
36 | end
37 |
38 | class Relation
39 | attr_reader :model, :items
40 |
41 | delegate :map, :to => :items
42 |
43 | def initialize(model, items)
44 | @model = model
45 | @items = items
46 | end
47 | end
48 | end
49 |
50 | class XModel < ActiveRecord::Base; end
51 | class YModel < ActiveRecord::Base; end
52 | class XRelation < ActiveRecord::Relation; end
53 | end
54 |
55 | context 'sidekiq gem present' do
56 | describe '#delay' do
57 | it 'schedules action with proper params' do
58 | MyTexter.delay.delayed_action('48111222333', 'Hi')
59 |
60 | expect_any_instance_of(MyTexter).to receive(:text).with(
61 | :to => "48111222333", :body => "Hi").and_call_original
62 | expect_any_instance_of(Textris::Message).to receive(:deliver)
63 |
64 | Textris::Delay::Sidekiq::Worker.drain
65 | end
66 |
67 | it 'serializes and deserializes ActiveRecord records' do
68 | user = XModel.new('48666777888')
69 |
70 | MyTexter.delay.serialized_action(user)
71 |
72 | expect_any_instance_of(MyTexter).to receive(:text).with(
73 | :to => "48666777888", :body => "Hello").and_call_original
74 | expect_any_instance_of(Textris::Message).to receive(:deliver)
75 |
76 | expect do
77 | Textris::Delay::Sidekiq::Worker.drain
78 | end.not_to raise_error
79 | end
80 |
81 | it 'serializes and deserializes ActiveRecord relations' do
82 | users = XRelation.new(XModel, [XModel.new('48666777888'), XModel.new('48666777889')])
83 |
84 | MyTexter.delay.serialized_array_action(users)
85 |
86 | expect_any_instance_of(MyTexter).to receive(:text).with(
87 | :to => "48666777888", :body => "Hello all").and_call_original
88 | expect_any_instance_of(Textris::Message).to receive(:deliver)
89 |
90 | expect do
91 | Textris::Delay::Sidekiq::Worker.drain
92 | end.not_to raise_error
93 | end
94 |
95 | it 'serializes and deserializes ActiveRecord object arrays' do
96 | users = [XModel.new('48666777888'), XModel.new('48666777889')]
97 |
98 | MyTexter.delay.serialized_array_action(users)
99 |
100 | expect_any_instance_of(MyTexter).to receive(:text).with(
101 | :to => "48666777888", :body => "Hello all").and_call_original
102 | expect_any_instance_of(Textris::Message).to receive(:deliver)
103 |
104 | expect do
105 | Textris::Delay::Sidekiq::Worker.drain
106 | end.not_to raise_error
107 | end
108 |
109 | it 'does not serialize wrong ActiveRecord object arrays' do
110 | users = [XModel.new('48666777888'), YModel.new('48666777889')]
111 |
112 | MyTexter.delay.serialized_array_action(users)
113 |
114 | expect do
115 | Textris::Delay::Sidekiq::Worker.drain
116 | end.to raise_error(NoMethodError)
117 | end
118 |
119 | it 'does not raise when ActiveRecord not loaded' do
120 | Object.send(:remove_const, :XModel)
121 | Object.send(:remove_const, :YModel)
122 | Object.send(:remove_const, :XRelation)
123 | Object.send(:remove_const, :ActiveRecord)
124 |
125 | MyTexter.delay.serialized_array_action('x')
126 |
127 | expect do
128 | Textris::Delay::Sidekiq::Worker.drain
129 | end.to raise_error(NoMethodError)
130 | end
131 | end
132 |
133 | describe '#delay_for' do
134 | it 'schedules action with proper params and execution time' do
135 | MyTexter.delay_for(300).delayed_action('48111222333', 'Hi')
136 |
137 | expect_any_instance_of(MyTexter).to receive(:text).with(
138 | :to => "48111222333", :body => "Hi").and_call_original
139 | expect_any_instance_of(Textris::Message).to receive(:deliver)
140 |
141 | scheduled_at = Time.at(Textris::Delay::Sidekiq::Worker.jobs.last['at'])
142 |
143 | expect(scheduled_at).to be > Time.now + 250
144 |
145 | Textris::Delay::Sidekiq::Worker.drain
146 | end
147 |
148 | it 'raises with wrong interval' do
149 | expect do
150 | MyTexter.delay_for('x')
151 | end.to raise_error(ArgumentError)
152 | end
153 | end
154 |
155 | describe '#delay_until' do
156 | it 'schedules action with proper params and execution time' do
157 | MyTexter.delay_until(Time.new(2020, 1, 1)).delayed_action(
158 | '48111222333', 'Hi')
159 |
160 | expect_any_instance_of(MyTexter).to receive(:text).with(
161 | :to => "48111222333", :body => "Hi").and_call_original
162 | expect_any_instance_of(Textris::Message).to receive(:deliver)
163 |
164 | scheduled_at = Time.at(Textris::Delay::Sidekiq::Worker.jobs.last['at'])
165 |
166 | expect(scheduled_at).to eq Time.new(2020, 1, 1)
167 |
168 | Textris::Delay::Sidekiq::Worker.drain
169 | end
170 |
171 | it 'raises with wrong timestamp' do
172 | expect do
173 | MyTexter.delay_until(nil)
174 | end.to raise_error(ArgumentError)
175 | end
176 | end
177 | end
178 |
179 | context 'sidekiq gem not present' do
180 | before do
181 | delegate = Class.new.extend(Textris::Delay::Sidekiq::Missing)
182 |
183 | [:delay, :delay_for, :delay_until].each do |method|
184 | allow(Textris::Base).to receive(method) { delegate.send(method) }
185 | end
186 | end
187 |
188 | describe '#delay' do
189 | it 'raises' do
190 | expect do
191 | MyTexter.delay
192 | end.to raise_error(LoadError)
193 | end
194 | end
195 |
196 | describe '#delay_for' do
197 | it 'raises' do
198 | expect do
199 | MyTexter.delay_for(300)
200 | end.to raise_error(LoadError)
201 | end
202 | end
203 |
204 | describe '#delay_until' do
205 | it 'raises' do
206 | expect do
207 | MyTexter.delay_until(Time.new(2005, 1, 1))
208 | end.to raise_error(LoadError)
209 | end
210 | end
211 | end
212 | end
213 |
--------------------------------------------------------------------------------
/spec/textris/message_spec.rb:
--------------------------------------------------------------------------------
1 | describe Textris::Message do
2 | let(:message) do
3 | Textris::Message.new(
4 | :content => 'X',
5 | :from => 'X',
6 | :to => '+48 111 222 333')
7 | end
8 |
9 | describe '#initialize' do
10 | describe 'parsing :from' do
11 | it 'parses "name " syntax properly' do
12 | message = Textris::Message.new(
13 | :content => 'X',
14 | :from => 'Mr Jones <+48 111 222 333> ',
15 | :to => '+48 111 222 333')
16 |
17 | expect(message.from_name).to eq('Mr Jones')
18 | expect(message.from_phone).to eq('48111222333')
19 | end
20 |
21 | it 'parses phone only properly' do
22 | message = Textris::Message.new(
23 | :content => 'X',
24 | :from => '+48 111 222 333',
25 | :to => '+48 111 222 444')
26 |
27 | expect(message.from_name).to be_nil
28 | expect(message.from_phone).to eq('48111222333')
29 | end
30 |
31 | it 'parses name only properly' do
32 | message = Textris::Message.new(
33 | :content => 'X',
34 | :from => 'Mr Jones',
35 | :to => '+48 111 222 444')
36 |
37 | expect(message.from_name).to eq('Mr Jones')
38 | expect(message.from_phone).to be_nil
39 | end
40 |
41 | it 'parses short codes properly' do
42 | message = Textris::Message.new(
43 | :content => 'X',
44 | :from => '894546',
45 | :to => '+48 111 222 444')
46 |
47 | expect(message.from_name).to be_nil
48 | expect(message.from_phone).to eq('894546')
49 | end
50 |
51 | it 'parses short codes and names properly' do
52 | message = Textris::Message.new(
53 | :content => 'X',
54 | :from => 'Mr Jones <894546> ',
55 | :to => '+48 111 222 444')
56 |
57 | expect(message.from_name).to eq('Mr Jones')
58 | expect(message.from_phone).to eq('894546')
59 | end
60 |
61 | it 'parses alphameric IDs and names properly' do
62 | message = Textris::Message.new(
63 | :content => 'X',
64 | :from => 'Mr Jones ',
65 | :to => '+48 111 222 444')
66 |
67 | expect(message.from_name).to eq('Mr Jones')
68 | expect(message.from_phone).to eq('Company')
69 | end
70 | end
71 |
72 | describe 'parsing :twilio_messaging_service_sid' do
73 | it 'stores the sid' do
74 | message = Textris::Message.new(
75 | content: 'X',
76 | twilio_messaging_service_sid: 'MG9752274e9e519418a7406176694466fa',
77 | to: '+48 111 222 444')
78 |
79 | expect(message.twilio_messaging_service_sid)
80 | .to eq('MG9752274e9e519418a7406176694466fa')
81 | end
82 | end
83 |
84 | describe 'parsing :to' do
85 | it 'normalizes phone numbers' do
86 | message = Textris::Message.new(
87 | :content => 'X',
88 | :from => 'X',
89 | :to => '+48 111 222 333')
90 |
91 | expect(message.to).to eq(['48111222333'])
92 | end
93 |
94 | it 'returns array for singular strings' do
95 | message = Textris::Message.new(
96 | :content => 'X',
97 | :from => 'X',
98 | :to => '+48 111 222 333')
99 |
100 | expect(message.to).to be_a(Array)
101 | end
102 |
103 | it 'takes arrays of strings' do
104 | message = Textris::Message.new(
105 | :content => 'X',
106 | :from => 'X',
107 | :to => ['+48 111 222 333', '+48 444 555 666'])
108 |
109 | expect(message.to).to eq(['48111222333', '48444555666'])
110 | end
111 |
112 | it 'filters out unplausible phone numbers' do
113 | message = Textris::Message.new(
114 | :content => 'X',
115 | :from => 'X',
116 | :to => ['+48 111 222 333', 'wrong'])
117 |
118 | expect(message.to).to eq(['48111222333'])
119 | end
120 | end
121 |
122 | describe 'parsing :content' do
123 | it 'preserves newlines and duplicated whitespace' do
124 | message = Textris::Message.new(
125 | :content => "a\nb. \n\n c",
126 | :from => 'X',
127 | :to => '+48 111 222 333')
128 |
129 | expect(message.content).to eq("a\nb. \n\n c")
130 | end
131 |
132 | it 'preserves leading whitespace, but strips trailing whitespace' do
133 | message = Textris::Message.new(
134 | :content => " a b. c ",
135 | :from => 'X',
136 | :to => '+48 111 222 333')
137 |
138 | expect(message.content).to eq(" a b. c")
139 | end
140 | end
141 |
142 | it 'raises if :to not provided' do
143 | expect do
144 | Textris::Message.new(
145 | :content => 'X',
146 | :from => 'X',
147 | :to => nil)
148 | end.to raise_error(ArgumentError)
149 | end
150 |
151 | it 'raises if :content not provided' do
152 | expect do
153 | Textris::Message.new(
154 | :content => nil,
155 | :from => 'X',
156 | :to => '+48 111 222 333')
157 | end.to raise_error(ArgumentError)
158 | end
159 | end
160 |
161 | describe '#texter' do
162 | it 'returns raw texter class for :raw => true' do
163 | message = Textris::Message.new(
164 | :texter => String,
165 | :content => 'X',
166 | :from => 'X',
167 | :to => '+48 111 222 333')
168 |
169 | expect(message.texter(:raw => true)).to eq String
170 | end
171 |
172 | it 'returns texter class without modules and texter suffix' do
173 | module SampleModule
174 | class SomeSampleTexter; end
175 | end
176 |
177 | message = Textris::Message.new(
178 | :texter => SampleModule::SomeSampleTexter,
179 | :content => 'X',
180 | :from => 'X',
181 | :to => '+48 111 222 333')
182 |
183 | expect(message.texter).to eq 'SomeSample'
184 | end
185 | end
186 |
187 | describe '#content' do
188 | before do
189 | class Textris::Base::RenderingController
190 | def initialize(*args)
191 | end
192 | end
193 |
194 | class RenderingTexter < Textris::Base
195 | def action_with_template
196 | text :to => '48 600 700 800'
197 | end
198 | end
199 | end
200 |
201 | it 'lazily renders content' do
202 | renderer = RenderingTexter.new(:action_with_template, [])
203 |
204 | message = Textris::Message.new(
205 | :renderer => renderer,
206 | :from => 'X',
207 | :to => '+48 111 222 333')
208 |
209 | expect { message.content }.to raise_error(ActionView::MissingTemplate)
210 | end
211 | end
212 |
213 | describe '#deliver' do
214 | before do
215 | class XDelivery < Textris::Delivery::Base
216 | def deliver(to); end
217 | end
218 |
219 | class YDelivery < Textris::Delivery::Base
220 | def deliver(to); end
221 | end
222 | end
223 |
224 | it 'invokes delivery classes properly' do
225 | expect(Textris::Delivery).to receive(:get) { [XDelivery, YDelivery] }
226 |
227 | message = Textris::Message.new(
228 | :content => 'X',
229 | :from => 'X',
230 | :to => '+48 111 222 333')
231 |
232 | expect_any_instance_of(XDelivery).to receive(:deliver_to_all)
233 | expect_any_instance_of(YDelivery).to receive(:deliver_to_all)
234 |
235 | message.deliver
236 | end
237 | end
238 | end
239 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # textris
2 |
3 | [](https://rubygems.org/gems/textris)
4 | [](https://rubygems.org/gems/textris)
5 | [](https://travis-ci.org/visualitypl/textris)
6 | [](https://scrutinizer-ci.com/g/visualitypl/textris/?branch=master)
7 | [](https://codeclimate.com/github/visualitypl/textris)
8 | [](https://codeclimate.com/github/visualitypl/textris)
9 |
10 | Simple gem for implementing texter classes which allow sending SMS messages in similar way to how e-mails are implemented and sent with ActionMailer-based mailers.
11 |
12 | Unlike similar gems, **textris** has some unique features:
13 |
14 | - e-mail proxy allowing to inspect messages using [Mailinator](https://mailinator.com/) or similar service
15 | - phone number E164 validation and normalization with the [phony](https://github.com/floere/phony) gem
16 | - built-in support for the Twilio and Nexmo APIs with [twilio-ruby](https://github.com/twilio/twilio-ruby) and [nexmo](https://github.com/timcraft/nexmo) gems
17 | - multiple, per-environment configurable and chainable delivery methods
18 | - extensible with any number of custom delivery methods (also chainable)
19 | - background and scheduled texting for Rails 4.2+ thanks to integration with [ActiveJob](http://edgeguides.rubyonrails.org/active_job_basics.html)
20 | - scheduled texting for Rails 4.1 and older thanks to integration with the [sidekiq](https://github.com/mperham/sidekiq) gem
21 | - support for testing using self-explanatory `Textris::Base.deliveries`
22 | - simple, extensible, fully tested code written from the ground up instead of copying *ActionMailer*
23 |
24 | See the [blog entry](http://www.visuality.pl/posts/txt-messaging-with-textris-gem) for the whole story and a practical usage example.
25 |
26 | ## Installation
27 |
28 | Add to `Gemfile`:
29 |
30 | ```ruby
31 | gem 'textris'
32 | ```
33 |
34 | Then run:
35 |
36 | bundle install
37 |
38 | ## Usage
39 |
40 | Place texter classes in `app/texters` (e.g. `app/texters/user_texter.rb`):
41 |
42 | ```ruby
43 | class UserTexter < Textris::Base
44 | default :from => "Our Team <+48 666-777-888>"
45 |
46 | def welcome(user)
47 | @user = user
48 |
49 | text :to => @user.phone
50 | end
51 | end
52 | ```
53 |
54 | Place relevant view templates in `app/views//.text.*` (e.g. `app/views/user_texter/welcome.text.erb`):
55 |
56 | ```erb
57 | Welcome to our system, <%= @user.name %>!
58 | ```
59 |
60 | Invoke them from application logic:
61 |
62 | ```ruby
63 | class User < ActiveRecord::Base
64 | after_create do
65 | UserTexter.welcome(self).deliver
66 | end
67 | end
68 | ```
69 |
70 | ### MMS
71 |
72 | Media messages are supported if you are using the [Twilio](#twilio), [Log](#log) or [Mail](#configuring-the-mail-delivery) adapter. [Twilio currently supports sending MMS in the US and Canada](https://support.twilio.com/hc/en-us/articles/223181608-Can-I-send-or-receive-MMS-messages-).
73 |
74 | Media messages aren't part of a template, but must be specified as an array of URLs when sending the message, like:
75 |
76 | ```ruby
77 | class UserMediaTexter < Textris::Base
78 | default :from => "Our Team <+48 666-777-888>"
79 |
80 | def welcome(user)
81 | @user = user
82 |
83 | text(
84 | :to => @user.phone,
85 | :media_urls => ["http://example.com/hilarious.gif"]
86 | )
87 | end
88 | end
89 | ```
90 |
91 | ### Background and scheduled
92 |
93 | #### ActiveJob integration
94 |
95 | As of version 0.4, **textris** supports native Rails 4.2+ way of background job handling, the [ActiveJob](http://edgeguides.rubyonrails.org/active_job_basics.html). You can delay delivery of your texters the same way as with ActionMailer mailers, like:
96 |
97 | ```ruby
98 | UserTexter.welcome(user).deliver_later
99 | UserTexter.welcome(user).deliver_later(:wait => 1.hour)
100 | UserTexter.welcome(user).deliver_later(:wait_until => 1.day.from_now)
101 | UserTexter.welcome(user).deliver_later(:queue => :custom_queue)
102 | UserTexter.welcome(user).deliver_now
103 | ```
104 |
105 | > You can safely pass ActiveRecord records as delayed action arguments. ActiveJob uses [GlobalID](https://github.com/rails/activemodel-globalid/) to serialize them for scheduled delivery.
106 |
107 | By default, `textris` queue will be used by the *Textris::Delay::ActiveJob::Job* job.
108 |
109 | #### Direct Sidekiq integration
110 |
111 | > As of Rails 4.2, ActiveJob is the recommended way for background job handling and it does support Sidekiq as its backend, so please see [chapter above](#activejob-integration) if you're using Rails 4.2 or above. Otherwise, keep on reading to use textris with Sidekiq regardless of your Rails version.
112 |
113 | Thanks to Sidekiq integration, you can send text messages in the background to speed things up, retry in case of failures or just to do it at specific time. To do so, use one of three delay methods:
114 |
115 | ```ruby
116 | UserTexter.delay.welcome(user)
117 | UserTexter.delay_for(1.hour).welcome(user)
118 | UserTexter.delay_until(1.day.from_now).welcome(user)
119 | ```
120 |
121 | Remember not to call `deliver` after the action invocation when using delay. It will be called by the *Textris::Delay::Sidekiq::Worker* worker.
122 |
123 | > You can safely pass ActiveRecord records and arrays as delayed action arguments. **textris** will store their `id`s and find them upon scheduled delivery.
124 |
125 | Keep in mind that **textris** does not install *sidekiq* for you. If you don't have it yet, [install Redis](http://redis.io/topics/quickstart) on your machine and add the *sidekiq* gem to `Gemfile`:
126 |
127 | ```ruby
128 | gem 'sidekiq'
129 | ```
130 |
131 | Then run:
132 |
133 | bundle install
134 | bundle exec sidekiq
135 |
136 | ## Testing
137 |
138 | Access all messages that were sent with the `:test` delivery:
139 |
140 | ```ruby
141 | Textris::Base.deliveries
142 | ```
143 |
144 | You may want to clear the delivery queue before each test:
145 |
146 | ```ruby
147 | before(:each) do
148 | Textris::Base.deliveries.clear
149 | end
150 | ```
151 |
152 | Keep in mind that messages targeting multiple phone numbers, like:
153 |
154 | ```ruby
155 | text :to => ['48111222333', '48222333444']
156 | ```
157 |
158 | will yield multiple message deliveries, each for specific phone number.
159 |
160 | ## Configuration
161 |
162 | You can change default settings by placing them in any of environment files, like `development.rb` or `test.rb`, or setting them globally in `application.rb`.
163 |
164 | ### Choosing and chaining delivery methods
165 |
166 | Below you'll find sample settings for any of supported delivery methods along with short description of each:
167 |
168 | ```ruby
169 | # Send messages via the Twilio REST API
170 | config.textris_delivery_method = :twilio
171 |
172 | # Send messages via the Nexmo API
173 | config.textris_delivery_method = :nexmo
174 |
175 | # Don't send anything, log messages into Rails logger
176 | config.textris_delivery_method = :log
177 |
178 | # Don't send anything, access your messages via Textris::Base.deliveries
179 | config.textris_delivery_method = :test
180 |
181 | # Send e-mails instead of SMSes in order to inspect their content
182 | config.textris_delivery_method = :mail
183 |
184 | # Chain multiple delivery methods (e.g. to have e-mail and log backups of messages)
185 | config.textris_delivery_method = [:twilio, :mail, :log]
186 | ```
187 |
188 | > Unless otherwise configured, default delivery methods will be: *log* in `development` environment, *test* in `test` environment and *mail* in `production` environment. All these come with reasonable defaults and will work with no further configuration.
189 |
190 | #### Twilio
191 |
192 | **textris** connects with the Twilio API using *twilio-ruby* gem. It does not, however, install the gem for you. If you don't have it yet, add the *twilio-ruby* gem to `Gemfile`:
193 |
194 | ```ruby
195 | gem 'twilio-ruby'
196 | ```
197 |
198 | Then, pre-configure the *twilio-ruby* settings by creating the `config/initializers/twilio.rb` file:
199 |
200 | ```ruby
201 | Twilio.configure do |config|
202 | config.account_sid = 'some_sid'
203 | config.auth_token = 'some_auth_token'
204 | end
205 | ```
206 |
207 | To use Twilio's Copilot use `twilio_messaging_service_sid` in place of `from` when sending a text or setting defaults.
208 |
209 | #### Nexmo
210 |
211 | In order to use Nexmo with **textris**, you need to include the `nexmo` gem in your `Gemfile`:
212 |
213 | ```ruby
214 | gem 'nexmo', '~> 4'
215 | ```
216 |
217 | The Nexmo gem uses the environment variables `NEXMO_API_KEY` and `NEXMO_API_SECRET` to authenticate with the API.
218 | Therefore the safest way to provide authentication credentials is to set these variables in your application environment.
219 |
220 | #### Log
221 |
222 | **textris** logger has similar logging behavior to ActionMailer. It will log single line to *info* log with production in mind and then a couple details to *debug* log. You can change the log level for the whole output:
223 |
224 | ```ruby
225 | config.textris_log_level = :info
226 | ```
227 |
228 | #### Custom delivery methods
229 |
230 | Currently, **textris** comes with several delivery methods built-in, but you can easily implement your own. Place desired delivery class in `app/deliveries/_delivery.rb` (e.g. `app/deliveries/my_provider_delivery.rb`):
231 |
232 | ```ruby
233 | class MyProviderDelivery < Textris::Delivery::Base
234 | # Implement sending message to single phone number
235 | def deliver(phone)
236 | send_sms(:phone => phone, :text => message.content)
237 | end
238 |
239 | # ...or implement sending message to multiple phone numbers at once
240 | def deliver_to_all
241 | send_multiple_sms(:phone_array => message.to, :text => message.content)
242 | end
243 | end
244 | ```
245 |
246 | Only one of methods above must be implemented for the delivery class to work. In case of multiple phone numbers and no implementation of *deliver_to_all*, the *deliver* method will be invoked multiple times.
247 |
248 | > You can place your custom deliveries in `app/texters` or `app/models` instead of `app/deliveries` if you don't want to clutter the *app* directory too much.
249 |
250 | After implementing your own deliveries, you can activate them by setting app configuration:
251 |
252 | ```ruby
253 | # Use your new delivery
254 | config.textris_delivery_method = :my_provider
255 |
256 | # Chain your new delivery with others, including stock ones
257 | config.textris_delivery_method = [:my_provider, :twilio, :mail]
258 | ```
259 |
260 | ### Configuring the mail delivery
261 |
262 | **textris** comes with reasonable defaults for the `mail` delivery method. It will send messages to a Mailinator address specific to the application name, environment and target phone number. You can customize the mail delivery by setting appropriate templates presented below.
263 |
264 | > Arguably, the *textris_mail_to_template* setting is the most important here as it specifies the target e-mail address scheme.
265 |
266 | ```ruby
267 | # E-mail target, here: "app-name-test-48111222333-texts@mailinator.com"
268 | config.textris_mail_to_template = '%{app:d}-%{env:d}-%{to_phone}-texts@mailinator.com'
269 |
270 | # E-mail sender, here: "our-team-48666777888@test.app-name.com"
271 | config.textris_mail_from_template = '%{from_name:d}-%{from_phone}@%{env:d}.%{app:d}.com'
272 |
273 | # E-mail subject, here: "User texter: Welcome"
274 | config.textris_mail_subject_template = '%{texter:dh} texter: %{action:h}'
275 |
276 | # E-mail body, here: "Welcome to our system, Mr Jones!"
277 | config.textris_mail_body_template = '%{content}'
278 | ```
279 |
280 | #### Template interpolation
281 |
282 | You can use the following interpolations in your mail templates:
283 |
284 | - `%{app}`: application name (e.g. `AppName`)
285 | - `%{env}`: enviroment name (e.g. `test` or `production`)
286 | - `%{texter}`: texter name (e.g. `User`)
287 | - `%{action}`: action name (e.g. `welcome`)
288 | - `%{from_name}`: name of the sender (e.g. `Our Team`)
289 | - `%{from_phone}`: phone number of the sender (e.g. `48666777888`)
290 | - `%{to_phone}`: phone number of the recipient (e.g. `48111222333`)
291 | - `%{content}`: message content (e.g. `Welcome to our system, Mr Jones!`)
292 | - `%{media_urls}`: comma separated string of media URLs (e.g. `http://example.com/hilarious.gif`)
293 |
294 | You can add optional interpolation modifiers using the `%{variable:modifiers}` syntax. These are most useful for making names e-mail friendly. The following modifiers are available:
295 |
296 | - `d`: dasherize (for instance, `AppName` becomes `app-name`)
297 | - `h`: humanize (for instance, `user_name` becomes `User name`)
298 | - `p`: format phone (for instance, `48111222333` becomes `+48 111 222 333`)
299 |
300 | ## Example project
301 |
302 | [Here](https://github.com/visualitypl/textris/tree/master/example/rails-4.2) you can find a simple example project that demonstrates **textris** usage with Rails 4.2. In order to see how it works or experiment with it, just go to project's directory and invoke:
303 |
304 | ```
305 | bundle install
306 | rake db:migrate
307 | rails server
308 | ```
309 |
310 | Open [application page](http://localhost:3000/) and fill in some user information. Sample texter will be invoked and you'll see an output similar to following in your server log:
311 |
312 | ```
313 | [ActiveJob] Enqueued Textris::Delay::ActiveJob::Job (Job ID: 71ed54f7-02e8-4205-9093-6f2a0ff7f483) to Inline(textris) with arguments: "UserTexter", "welcome", [#]
314 | [ActiveJob] User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 1]]
315 | [ActiveJob] [Textris::Delay::ActiveJob::Job] [71ed54f7-02e8-4205-9093-6f2a0ff7f483] Performing Textris::Delay::ActiveJob::Job from Inline(textris) with arguments: "UserTexter", "welcome", [#]
316 | [ActiveJob] [Textris::Delay::ActiveJob::Job] [71ed54f7-02e8-4205-9093-6f2a0ff7f483] Sent text to +48 666 777 888
317 | [ActiveJob] [Textris::Delay::ActiveJob::Job] [71ed54f7-02e8-4205-9093-6f2a0ff7f483] Texter: User#welcome
318 | [ActiveJob] [Textris::Delay::ActiveJob::Job] [71ed54f7-02e8-4205-9093-6f2a0ff7f483] Date: 2015-02-20 18:17:16 +0100
319 | [ActiveJob] [Textris::Delay::ActiveJob::Job] [71ed54f7-02e8-4205-9093-6f2a0ff7f483] From: Our Team <+48 666 777 888>
320 | [ActiveJob] [Textris::Delay::ActiveJob::Job] [71ed54f7-02e8-4205-9093-6f2a0ff7f483] To: +48 666 777 888
321 | [ActiveJob] [Textris::Delay::ActiveJob::Job] [71ed54f7-02e8-4205-9093-6f2a0ff7f483] Rendered user_texter/welcome.text.erb (0.4ms)
322 | [ActiveJob] [Textris::Delay::ActiveJob::Job] [71ed54f7-02e8-4205-9093-6f2a0ff7f483] Content: Welcome to our system, Mr Jones!
323 | [ActiveJob] [Textris::Delay::ActiveJob::Job] [71ed54f7-02e8-4205-9093-6f2a0ff7f483] Performed Textris::Delay::ActiveJob::Job from Inline(textris) in 9.98ms
324 | ```
325 |
326 | Example project may serve as a convenient sandbox for [developing custom delivery methods](#custom-delivery-methods).
327 |
328 | ## Contributing
329 |
330 | 1. Fork it (https://github.com/visualitypl/textris/fork)
331 | 2. Create your feature branch (`git checkout -b my-new-feature`)
332 | 3. Commit your changes (`git commit -am 'Add some feature'`)
333 | 4. Push to the branch (`git push origin my-new-feature`)
334 | 5. Create a new Pull Request
335 |
336 | ### Adding delivery methods
337 |
338 | Implementing new delivery methods in Pull Requests is strongly encouraged. Start by [implementing custom delivery method](#custom-delivery-methods). Then, you can prepare it for a Pull Request by adhering to following guidelines:
339 |
340 | 1. Delivery class should be placed in `lib/textris/delivery/service_name.rb` and named in a way that will best indicate the service with which it's supposed to work with.
341 | 5. Add your method to code example in [Choosing and chaining delivery methods](#choosing-and-chaining-delivery-methods) in README. Also, add sub-chapter for it if it depends on other gems or requires explanation.
342 | 6. Your delivery code is expected to throw exceptions with self-explanatory messages upon failure. Include specs that test this. Mock external API requests with [webmock](https://github.com/bblimke/webmock).
343 | 2. If delivery depends on any gems, don't add them as runtime dependencies. You can (and should in order to write complete specs) add them as development dependencies.
344 | 3. Delivery code must load without exceptions even when dependent libraries are missing. Specs should test such case (you can use `remove_const` to undefine loaded consts).
345 | 4. New deliveries are expected to have 100% test coverage. Run `COVERAGE=1 bundle exec rake spec` to generate *simplecov* coverage into the **coverage/index.html** file.
346 |
347 | The commit in which [the log delivery was added](https://github.com/visualitypl/textris/commit/7c3231ca5eeb94cca01a3beced19a1a909299faf) is an example of delivery method addition that meets all guidelines listed above.
348 |
--------------------------------------------------------------------------------