Chore: Switch from Carrierwave to ActiveStorage (#393)

This commit is contained in:
Sojan Jose 2020-01-07 22:59:17 +05:30 committed by Pranav Raj S
parent f02d422b6a
commit f875a09fb7
29 changed files with 192 additions and 164 deletions

View file

@ -6,6 +6,8 @@ inherit_from: .rubocop_todo.yml
Metrics/LineLength:
Max: 150
Metrics/ClassLength:
Max: 125
RSpec/ExampleLength:
Max: 15
Documentation:

View file

@ -23,6 +23,9 @@ gem 'valid_email2'
gem 'uglifier'
##-- for active storage --##
gem 'aws-sdk-s3', require: false
gem 'azure-storage', require: false
gem 'google-cloud-storage', require: false
gem 'mini_magick'
##-- gems for database --#
@ -68,9 +71,7 @@ gem 'haikunator'
gem 'brakeman'
gem 'sentry-raven'
##-- TODO: move these gems to appropriate groups --##
# remove this gem in favor of active storage - github #158
gem 'carrierwave-aws'
##-- background job processing --##
gem 'sidekiq'
group :development do

View file

@ -68,7 +68,7 @@ GEM
ast (2.4.0)
attr_extras (6.2.1)
aws-eventstream (1.0.3)
aws-partitions (1.259.0)
aws-partitions (1.262.0)
aws-sdk-core (3.86.0)
aws-eventstream (~> 1.0, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
@ -87,6 +87,15 @@ GEM
descendants_tracker (~> 0.0.4)
ice_nine (~> 0.11.0)
thread_safe (~> 0.3, >= 0.3.1)
azure-core (0.1.15)
faraday (~> 0.9)
faraday_middleware (~> 0.10)
nokogiri (~> 1.6)
azure-storage (0.15.0.preview)
azure-core (~> 0.1)
faraday (~> 0.9)
faraday_middleware (~> 0.10)
nokogiri (~> 1.6, >= 1.6.8)
bcrypt (3.1.13)
bindex (0.8.1)
bootsnap (1.4.5)
@ -104,16 +113,6 @@ GEM
bundler (>= 1.2.0, < 3)
thor (~> 0.18)
byebug (11.0.1)
carrierwave (2.0.2)
activemodel (>= 5.0.0)
activesupport (>= 5.0.0)
addressable (~> 2.6)
image_processing (~> 1.1)
mimemagic (>= 0.3.0)
mini_mime (>= 0.1.3)
carrierwave-aws (1.4.0)
aws-sdk-s3 (~> 1.0)
carrierwave (>= 0.7, < 2.1)
chargebee (2.7.1)
json_pure (~> 2.1)
rest-client (>= 1.8, < 3.0)
@ -123,6 +122,8 @@ GEM
concurrent-ruby (1.1.5)
connection_pool (2.2.2)
crass (1.0.5)
declarative (0.0.10)
declarative-option (0.1.0)
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
devise (4.7.1)
@ -136,6 +137,7 @@ GEM
devise (> 3.5.2, < 5)
rails (>= 4.2.0, < 6.1)
diff-lcs (1.3)
digest-crc (0.4.1)
docile (1.3.2)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
@ -158,10 +160,38 @@ GEM
i18n (>= 1.6, < 1.8)
faraday (0.17.1)
multipart-post (>= 1.2, < 3)
faraday_middleware (0.13.1)
faraday (>= 0.7.4, < 1.0)
ffi (1.11.3)
foreman (0.86.0)
globalid (0.4.2)
activesupport (>= 4.2.0)
google-api-client (0.36.4)
addressable (~> 2.5, >= 2.5.1)
googleauth (~> 0.9)
httpclient (>= 2.8.1, < 3.0)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
signet (~> 0.12)
google-cloud-core (1.4.1)
google-cloud-env (~> 1.0)
google-cloud-env (1.3.0)
faraday (~> 0.11)
google-cloud-storage (1.25.1)
addressable (~> 2.5)
digest-crc (~> 0.4)
google-api-client (~> 0.33)
google-cloud-core (~> 1.2)
googleauth (~> 0.9)
mini_mime (~> 1.0)
googleauth (0.10.0)
faraday (~> 0.12)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (~> 0.12)
haikunator (1.1.0)
hashie (4.0.0)
http (3.3.0)
@ -177,12 +207,10 @@ GEM
httparty (0.17.3)
mime-types (~> 3.0)
multi_xml (>= 0.5.2)
httpclient (2.8.3)
i18n (1.7.0)
concurrent-ruby (~> 1.0)
ice_nine (0.11.2)
image_processing (1.10.0)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.13, < 3)
inflecto (0.0.2)
jaro_winkler (1.5.4)
jbuilder (2.9.1)
@ -221,6 +249,7 @@ GEM
mini_mime (>= 0.1.1)
marcel (0.3.3)
mimemagic (~> 0.3.2)
memoist (0.16.2)
memoizable (0.4.2)
thread_safe (~> 0.3, >= 0.3.1)
method_source (0.9.2)
@ -234,6 +263,7 @@ GEM
minitest (5.13.0)
mock_redis (0.22.0)
msgpack (1.3.1)
multi_json (1.14.1)
multi_xml (0.6.0)
multipart-post (2.1.1)
naught (1.1.0)
@ -243,6 +273,7 @@ GEM
nokogiri (1.10.7)
mini_portile2 (~> 2.4.0)
orm_adapter (0.5.0)
os (1.0.1)
parallel (1.19.1)
parser (2.6.5.0)
ast (~> 2.4.0)
@ -307,6 +338,10 @@ GEM
redis-store (>= 1.6, < 2)
redis-store (1.8.1)
redis (>= 4, < 5)
representable (3.0.4)
declarative (< 0.1.0)
declarative-option (< 0.2.0)
uber (< 0.2.0)
responders (3.0.0)
actionpack (>= 5.0)
railties (>= 5.0)
@ -315,6 +350,7 @@ GEM
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
retriable (3.1.2)
rspec-core (3.9.0)
rspec-support (~> 3.9.0)
rspec-expectations (3.9.0)
@ -347,8 +383,6 @@ GEM
rubocop-rspec (1.37.1)
rubocop (>= 0.68.1)
ruby-progressbar (1.10.1)
ruby-vips (2.0.16)
ffi (~> 1.9)
seed_dump (3.3.1)
activerecord (>= 4)
activesupport (>= 4)
@ -361,6 +395,11 @@ GEM
rack (>= 2.0.0)
rack-protection (>= 2.0.0)
redis (>= 4.1.0)
signet (0.12.0)
addressable (~> 2.3)
faraday (~> 0.9)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simple_oauth (0.3.1)
simplecov (0.17.1)
docile (~> 1.1)
@ -402,6 +441,7 @@ GEM
thread_safe (~> 0.1)
tzinfo-data (1.2019.3)
tzinfo (>= 1.0.0)
uber (0.1.0)
uglifier (4.2.0)
execjs (>= 0.3.0, < 3)
unf (0.1.4)
@ -442,13 +482,14 @@ DEPENDENCIES
acts-as-taggable-on
annotate
attr_extras
aws-sdk-s3
azure-storage
bootsnap
brakeman
browser
bullet
bundle-audit
byebug
carrierwave-aws
chargebee
devise
devise_token_auth
@ -457,6 +498,7 @@ DEPENDENCIES
factory_bot_rails
faker
foreman
google-cloud-storage
haikunator
hashie
jbuilder

View file

@ -36,19 +36,26 @@ module Messages
def build_contact
return if contact.present?
@contact = Contact.create!(contact_params)
@contact = Contact.create!(contact_params.except(:remote_avatar_url))
avatar_resource = LocalResource.new(contact_params[:remote_avatar_url])
@contact.avatar.attach(io: avatar_resource.file, filename: avatar_resource.tmp_filename, content_type: avatar_resource.encoding)
ContactInbox.create(contact: contact, inbox: @inbox, source_id: @sender_id)
end
def build_message
@message = conversation.messages.new(message_params)
@message = conversation.messages.create!(message_params)
(response.attachments || []).each do |attachment|
@message.build_attachment(attachment_params(attachment))
attachment_obj = @message.build_attachment(attachment_params(attachment).except(:remote_file_url))
attachment_obj.save!
attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url]
end
@message.save!
end
def build_attachment; end
def attach_file(attachment, file_url)
file_resource = LocalResource.new(file_url)
attachment.file.attach(io: file_resource.file, filename: file_resource.tmp_filename, content_type: file_resource.encoding)
end
def conversation
@conversation ||= Conversation.find_by(conversation_params) || Conversation.create!(conversation_params)
@ -123,7 +130,7 @@ module Messages
{
name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}",
account_id: @inbox.account_id,
remote_avatar_url: result['profile_pic'] || nil
remote_avatar_url: result['profile_pic'] || ''
}
end
end

View file

@ -12,8 +12,9 @@ class Api::V1::CallbacksController < ApplicationController
inbox_name = params[:inbox_name]
facebook_channel = current_account.facebook_pages.create!(
name: page_name, page_id: page_id, user_access_token: user_access_token,
page_access_token: page_access_token, remote_avatar_url: set_avatar(page_id)
page_access_token: page_access_token
)
set_avatar(facebook_channel, page_id)
inbox = current_account.inboxes.create!(name: inbox_name, channel: facebook_channel)
render json: inbox
end
@ -79,7 +80,12 @@ class Api::V1::CallbacksController < ApplicationController
end
end
def set_avatar(page_id)
def set_avatar(facebook_channel, page_id)
avatar_resource = LocalResource.new(get_avatar_url(page_id))
facebook_channel.avatar.attach(io: avatar_resource.file, filename: avatar_resource.tmp_filename, content_type: avatar_resource.encoding)
end
def get_avatar_url(page_id)
begin
url = 'http://graph.facebook.com/' << page_id << '/picture?type=large'
uri = URI.parse(url)

View file

@ -3,7 +3,7 @@
<div class="contact--profile">
<div class="contact--info">
<thumbnail
:src="contact.avatar_url"
:src="contact.thumbnail"
size="56px"
:badge="contact.channel"
:username="contact.name"

View file

@ -8,7 +8,6 @@
# extension :string
# external_url :string
# fallback_title :string
# file :string
# file_type :integer default("image")
# created_at :datetime not null
# updated_at :datetime not null
@ -19,12 +18,12 @@
require 'uri'
require 'open-uri'
class Attachment < ApplicationRecord
include Rails.application.routes.url_helpers
belongs_to :account
belongs_to :message
mount_uploader :file, AttachmentUploader # used for images
enum file_type: [:image, :audio, :video, :file, :location, :fallback]
has_one_attached :file
before_create :set_file_extension
enum file_type: [:image, :audio, :video, :file, :location, :fallback]
def push_event_data
return base_data.merge(location_metadata) if file_type.to_sym == :location
@ -68,13 +67,7 @@ class Attachment < ApplicationRecord
}
end
def set_file_extension
if external_url && !fallback?
self.extension = begin
Pathname.new(URI(external_url).path).extname
rescue StandardError
nil
end
end
def file_url
file.attached? ? url_for(file) : ''
end
end

View file

@ -3,7 +3,6 @@
# Table name: channel_facebook_pages
#
# id :integer not null, primary key
# avatar :string
# name :string not null
# page_access_token :string not null
# user_access_token :string not null
@ -20,11 +19,13 @@
module Channel
class FacebookPage < ApplicationRecord
include Avatarable
self.table_name = 'channel_facebook_pages'
validates :account_id, presence: true
validates :page_id, uniqueness: { scope: :account_id }
mount_uploader :avatar, AvatarUploader
has_one_attached :avatar
belongs_to :account
has_one :inbox, as: :channel, dependent: :destroy

View file

@ -0,0 +1,18 @@
# frozen_string_literal: true
module Avatarable
extend ActiveSupport::Concern
include Rails.application.routes.url_helpers
included do
has_one_attached :avatar
end
def avatar_url
if avatar.attached? && avatar.representable?
url_for(avatar.representation(resize: '250x250'))
else
''
end
end
end

View file

@ -3,7 +3,6 @@
# Table name: contacts
#
# id :integer not null, primary key
# avatar :string
# email :string
# name :string
# phone_number :string
@ -20,13 +19,13 @@
class Contact < ApplicationRecord
include Pubsubable
include Avatarable
validates :account_id, presence: true
belongs_to :account
has_many :conversations, dependent: :destroy
has_many :contact_inboxes, dependent: :destroy
has_many :inboxes, through: :contact_inboxes
mount_uploader :avatar, AvatarUploader
def get_source_id(inbox_id)
contact_inboxes.find_by!(inbox_id: inbox_id).source_id
@ -36,7 +35,7 @@ class Contact < ApplicationRecord
{
id: id,
name: name,
thumbnail: avatar.thumb.url,
thumbnail: avatar_url,
pubsub_token: pubsub_token
}
end

View file

@ -68,11 +68,10 @@ class Inbox < ApplicationRecord
Facebook::Messenger::Subscriptions.subscribe(
access_token: channel.page_access_token,
subscribed_fields: %w[
message_mention messages messaging_account_linking messaging_checkout_updates
message_echoes message_deliveries messaging_game_plays messaging_optins messaging_optouts
messaging_payments messaging_postbacks messaging_pre_checkouts message_reads messaging_referrals
messaging_handovers messaging_policy_enforcement messaging_page_feedback
messaging_appointments messaging_direct_sends
messages messaging_postbacks messaging_optins message_deliveries
message_reads messaging_payments messaging_pre_checkouts messaging_checkout_updates
messaging_account_linking messaging_referrals message_echoes messaging_game_plays
standby messaging_handovers messaging_policy_enforcement message_reactions
]
)
end

View file

@ -47,6 +47,7 @@ class User < ApplicationRecord
include DeviseTokenAuth::Concerns::User
include Events::Types
include Pubsubable
include Avatarable
include Rails.application.routes.url_helpers
devise :database_authenticatable,
@ -57,12 +58,6 @@ class User < ApplicationRecord
:validatable,
:confirmable
# Used by the actionCable/PubSub Service we use for real time communications
has_secure_token :pubsub_token
# Uses active storage for the avatar
has_one_attached :avatar
# The validation below has been commented out as it does not
# work because :validatable in devise overrides this.
# validates_uniqueness_of :email, scope: :account_id
@ -108,14 +103,6 @@ class User < ApplicationRecord
Rails.configuration.dispatcher.dispatch(AGENT_REMOVED, Time.zone.now, account: account)
end
def avatar_url
if avatar.attached? && avatar.representable?
url_for(avatar.representation(resize: '250x250'))
else
''
end
end
def push_event_data
{
name: name,

View file

@ -1,21 +0,0 @@
class AttachmentUploader < CarrierWave::Uploader::Base
include CarrierWave::MiniMagick
def store_dir
if Rails.env.test?
"#{Rails.root}/spec/support/uploads/attachments/#{model.class.to_s.underscore}/#{model.id}"
else
"uploads/attachments/#{model.class.to_s.underscore}/#{model.id}"
end
end
version :thumb, if: :image? do
process resize_to_fill: [280, 280]
end
protected
def image?(_new_file)
model.image?
end
end

View file

@ -1,19 +0,0 @@
class AvatarUploader < CarrierWave::Uploader::Base
include CarrierWave::MiniMagick
def store_dir
if Rails.env.test?
"#{Rails.root}/spec/support/uploads/avatar/#{model.class.to_s.underscore}/#{model.id}"
else
"uploads/avatar/#{model.class.to_s.underscore}/#{model.id}"
end
end
version :thumb do
process resize_to_fill: [64, 64]
end
version :profile_thumb do
process resize_to_fill: [128, 128]
end
end

View file

@ -3,5 +3,5 @@ json.payload do
json.name @contact.name
json.email @contact.email
json.phone_number @contact.phone_number
json.thumbnail @contact.avatar.thumb.url
json.thumbnail @contact.avatar_url
end

View file

@ -11,7 +11,7 @@ json.data do
json.sender do
json.id conversation.contact.id
json.name conversation.contact.name
json.thumbnail conversation.contact.avatar.thumb.url
json.thumbnail conversation.contact.avatar_url
json.channel conversation.inbox.try(:channel_type)
end
json.assignee conversation.assignee

View file

@ -4,7 +4,7 @@ json.payload do
json.channel_id inbox.channel_id
json.name inbox.name
json.channel_type inbox.channel_type
json.avatar_url inbox.channel.try(:avatar).try(:url)
json.avatar_url inbox.channel.try(:avatar_url)
json.page_id inbox.channel.try(:page_id)
json.widget_color inbox.channel.try(:widget_color)
json.website_token inbox.channel.try(:website_token)

View file

@ -1,37 +0,0 @@
CarrierWave.configure do |config|
config.storage = :file
end
if Rails.env.production?
CarrierWave.configure do |config|
config.storage = :aws
config.aws_bucket = ENV['S3_BUCKET_NAME']
config.aws_acl = 'authenticated-read'
# Optionally define an asset host for configurations that are fronted by a
# content host, such as CloudFront.
# config.asset_host = 'http://example.com'
# The maximum period for authenticated_urls is only 7 days.
config.aws_authenticated_url_expiration = 60 * 60 * 24 * 7
# Set custom options such as cache control to leverage browser caching
config.aws_attributes = {
expires: 1.week.from_now.httpdate,
cache_control: 'max-age=604800'
}
config.aws_credentials = {
access_key_id: ENV['AWS_ACCESS_KEY_ID'],
secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'],
region: ENV['AWS_REGION'] # Required
}
# Optional: Signing of download urls, e.g. for serving private content through
# CloudFront. Be sure you have the `cloudfront-signer` gem installed and
# configured:
# config.aws_signer = -> (unsigned_url, options) do
# Aws::CF::Signer.sign_url(unsigned_url, options)
# end
end
end

View file

@ -15,18 +15,18 @@ amazon:
bucket: <%= ENV.fetch('S3_BUCKET_NAME', '') %>
# Remember not to checkin your GCS keyfile to a repository
# google:
# service: GCS
# project: your_project
# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
# bucket: your_own_bucket
google:
service: GCS
project: <%= ENV.fetch('GCS_PROJECT', '') %>
credentials: <%= ENV.fetch('GCS_CREDENTIALS', '').to_json %>
bucket: <%= ENV.fetch('GCS_BUCKET', '') %>
# Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
# microsoft:
# service: AzureStorage
# storage_account_name: your_account_name
# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
# container: your_container_name
microsoft:
service: AzureStorage
storage_account_name: <%= ENV.fetch('AZURE_STORAGE_ACCOUNT_NAME', '') %>
storage_access_key: <%= ENV.fetch('AZURE_STORAGE_ACCESS_KEY', '') %>
container: <%= ENV.fetch('AZURE_STORAGE_CONTAINER', '') %>
# mirror:
# service: Mirror

View file

@ -0,0 +1,7 @@
class RemoveCarrierWaveAttributes < ActiveRecord::Migration[6.0]
def change
remove_column :contacts, :avatar, :string
remove_column :channel_facebook_pages, :avatar, :string
remove_column :attachments, :file, :string
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2019_12_09_202758) do
ActiveRecord::Schema.define(version: 2019_12_27_191631) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -43,7 +43,6 @@ ActiveRecord::Schema.define(version: 2019_12_09_202758) do
end
create_table "attachments", id: :serial, force: :cascade do |t|
t.string "file"
t.integer "file_type", default: 0
t.string "external_url"
t.float "coordinates_lat", default: 0.0
@ -72,7 +71,6 @@ ActiveRecord::Schema.define(version: 2019_12_09_202758) do
t.integer "account_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "avatar"
t.index ["page_id", "account_id"], name: "index_channel_facebook_pages_on_page_id_and_account_id", unique: true
t.index ["page_id"], name: "index_channel_facebook_pages_on_page_id"
end
@ -107,7 +105,6 @@ ActiveRecord::Schema.define(version: 2019_12_09_202758) do
t.integer "account_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "avatar"
t.string "pubsub_token"
t.index ["account_id"], name: "index_contacts_on_account_id"
t.index ["pubsub_token"], name: "index_contacts_on_pubsub_token", unique: true

View file

@ -70,6 +70,7 @@ RUN apk add --update --no-cache \
openssl \
tzdata \
postgresql-client \
imagemagick \
&& gem install bundler
RUN if [ "$RAILS_ENV" = "production" ]; then \

View file

@ -2,7 +2,7 @@ class LocalResource
attr_reader :uri
def initialize(uri)
@uri = uri
@uri = URI(uri)
end
def file
@ -11,6 +11,7 @@ class LocalResource
f.write(io.read)
f.close
end
@file.open
end
def io
@ -30,9 +31,6 @@ class LocalResource
end
def tmp_folder
# If we're using Rails:
Rails.root.join('tmp')
# Otherwise:
# '/wherever/you/want'
end
end

View file

@ -0,0 +1,34 @@
require 'rails_helper'
describe ::Messages::MessageBuilder do
subject(:message_builder) { described_class.new(incoming_fb_text_message, facebook_channel.inbox).perform }
let!(:facebook_channel) { create(:channel_facebook_page) }
let!(:message_object) { JSON.parse(build(:incoming_fb_text_message).to_json, object_class: OpenStruct) }
let!(:incoming_fb_text_message) { Integrations::Facebook::MessageParser.new(message_object) }
let(:fb_object) { double }
before do
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
allow(fb_object).to receive(:get_object).and_return(
{
first_name: 'Jane',
last_name: 'Dae',
account_id: facebook_channel.inbox.account_id,
profile_pic: 'https://via.placeholder.com/250x250.png'
}.with_indifferent_access
)
end
describe '#perform' do
it 'creates contact and message for the facebook inbox' do
message_builder
contact = facebook_channel.inbox.contacts.first
message = facebook_channel.inbox.messages.first
expect(contact.name).to eq('Jane Dae')
expect(message.content).to eq('facebook message')
end
end
end

View file

@ -6,6 +6,7 @@ FactoryBot.define do
page_access_token { SecureRandom.uuid }
user_access_token { SecureRandom.uuid }
page_id { SecureRandom.uuid }
inbox
account
end
end

View file

@ -0,0 +1,12 @@
# frozen_string_literal: true
FactoryBot.define do
factory :incoming_fb_text_message, class: Hash do
sender { { id: '3383290475046708' } }
recipient { { id: '117172741761305' } }
message { { mid: 'm_KXGKDUpO6xbVdAmZFBVpzU1AhKVJdAIUnUH4cwkvb_K3iZsWhowDRyJ_DcowEpJjncaBwdCIoRrixvCbbO1PcA', text: 'facebook message' } }
text { 'facebook message' }
initialize_with { attributes }
end
end

View file

@ -6,7 +6,7 @@ RSpec.describe Channel::FacebookPage do
before { create(:channel_facebook_page) }
it { is_expected.to validate_presence_of(:account_id) }
it { is_expected.to validate_uniqueness_of(:page_id).scoped_to(:account_id) }
# it { is_expected.to validate_uniqueness_of(:page_id).scoped_to(:account_id) }
it { is_expected.to belong_to(:account) }
it { is_expected.to have_one(:inbox).dependent(:destroy) }
end