├── 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 | 10 | 11 | 12 | 13 | 14 | 15 | <% @users.each do |user| %> 16 | 17 | 18 | 19 | 20 | <% end %> 21 | 22 |
NamePhone
<%= user.name %><%= user.phone %>
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 | [![Gem Version](https://img.shields.io/gem/v/textris.svg?style=flat-square&label=version)](https://rubygems.org/gems/textris) 4 | [![Downloads](https://img.shields.io/gem/dt/textris.svg?style=flat-square)](https://rubygems.org/gems/textris) 5 | [![Build Status](https://img.shields.io/travis/visualitypl/textris/master.svg?style=flat-square&label=build)](https://travis-ci.org/visualitypl/textris) 6 | [![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/g/visualitypl/textris.svg?style=flat-square)](https://scrutinizer-ci.com/g/visualitypl/textris/?branch=master) 7 | [![Code Climate](https://img.shields.io/codeclimate/github/visualitypl/textris.svg?style=flat-square)](https://codeclimate.com/github/visualitypl/textris) 8 | [![Test Coverage](https://img.shields.io/codeclimate/coverage/github/visualitypl/textris.svg?style=flat-square)](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 | --------------------------------------------------------------------------------