mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 02:57:57 +00:00 
			
		
		
		
	Chore: Switch from Carrierwave to ActiveStorage (#393)
This commit is contained in:
		| @@ -6,6 +6,8 @@ inherit_from: .rubocop_todo.yml | |||||||
|  |  | ||||||
| Metrics/LineLength: | Metrics/LineLength: | ||||||
|   Max: 150 |   Max: 150 | ||||||
|  | Metrics/ClassLength: | ||||||
|  |   Max: 125 | ||||||
| RSpec/ExampleLength: | RSpec/ExampleLength: | ||||||
|   Max: 15 |   Max: 15 | ||||||
| Documentation: | Documentation: | ||||||
|   | |||||||
							
								
								
									
										7
									
								
								Gemfile
									
									
									
									
									
								
							
							
						
						
									
										7
									
								
								Gemfile
									
									
									
									
									
								
							| @@ -23,6 +23,9 @@ gem 'valid_email2' | |||||||
| gem 'uglifier' | gem 'uglifier' | ||||||
|  |  | ||||||
| ##-- for active storage --## | ##-- for active storage --## | ||||||
|  | gem 'aws-sdk-s3', require: false | ||||||
|  | gem 'azure-storage', require: false | ||||||
|  | gem 'google-cloud-storage', require: false | ||||||
| gem 'mini_magick' | gem 'mini_magick' | ||||||
|  |  | ||||||
| ##-- gems for database --# | ##-- gems for database --# | ||||||
| @@ -68,9 +71,7 @@ gem 'haikunator' | |||||||
| gem 'brakeman' | gem 'brakeman' | ||||||
| gem 'sentry-raven' | gem 'sentry-raven' | ||||||
|  |  | ||||||
| ##-- TODO: move these gems to appropriate groups --## | ##-- background job processing --## | ||||||
| # remove this gem in favor of active storage -  github #158 |  | ||||||
| gem 'carrierwave-aws' |  | ||||||
| gem 'sidekiq' | gem 'sidekiq' | ||||||
|  |  | ||||||
| group :development do | group :development do | ||||||
|   | |||||||
							
								
								
									
										76
									
								
								Gemfile.lock
									
									
									
									
									
								
							
							
						
						
									
										76
									
								
								Gemfile.lock
									
									
									
									
									
								
							| @@ -68,7 +68,7 @@ GEM | |||||||
|     ast (2.4.0) |     ast (2.4.0) | ||||||
|     attr_extras (6.2.1) |     attr_extras (6.2.1) | ||||||
|     aws-eventstream (1.0.3) |     aws-eventstream (1.0.3) | ||||||
|     aws-partitions (1.259.0) |     aws-partitions (1.262.0) | ||||||
|     aws-sdk-core (3.86.0) |     aws-sdk-core (3.86.0) | ||||||
|       aws-eventstream (~> 1.0, >= 1.0.2) |       aws-eventstream (~> 1.0, >= 1.0.2) | ||||||
|       aws-partitions (~> 1, >= 1.239.0) |       aws-partitions (~> 1, >= 1.239.0) | ||||||
| @@ -87,6 +87,15 @@ GEM | |||||||
|       descendants_tracker (~> 0.0.4) |       descendants_tracker (~> 0.0.4) | ||||||
|       ice_nine (~> 0.11.0) |       ice_nine (~> 0.11.0) | ||||||
|       thread_safe (~> 0.3, >= 0.3.1) |       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) |     bcrypt (3.1.13) | ||||||
|     bindex (0.8.1) |     bindex (0.8.1) | ||||||
|     bootsnap (1.4.5) |     bootsnap (1.4.5) | ||||||
| @@ -104,16 +113,6 @@ GEM | |||||||
|       bundler (>= 1.2.0, < 3) |       bundler (>= 1.2.0, < 3) | ||||||
|       thor (~> 0.18) |       thor (~> 0.18) | ||||||
|     byebug (11.0.1) |     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) |     chargebee (2.7.1) | ||||||
|       json_pure (~> 2.1) |       json_pure (~> 2.1) | ||||||
|       rest-client (>= 1.8, < 3.0) |       rest-client (>= 1.8, < 3.0) | ||||||
| @@ -123,6 +122,8 @@ GEM | |||||||
|     concurrent-ruby (1.1.5) |     concurrent-ruby (1.1.5) | ||||||
|     connection_pool (2.2.2) |     connection_pool (2.2.2) | ||||||
|     crass (1.0.5) |     crass (1.0.5) | ||||||
|  |     declarative (0.0.10) | ||||||
|  |     declarative-option (0.1.0) | ||||||
|     descendants_tracker (0.0.4) |     descendants_tracker (0.0.4) | ||||||
|       thread_safe (~> 0.3, >= 0.3.1) |       thread_safe (~> 0.3, >= 0.3.1) | ||||||
|     devise (4.7.1) |     devise (4.7.1) | ||||||
| @@ -136,6 +137,7 @@ GEM | |||||||
|       devise (> 3.5.2, < 5) |       devise (> 3.5.2, < 5) | ||||||
|       rails (>= 4.2.0, < 6.1) |       rails (>= 4.2.0, < 6.1) | ||||||
|     diff-lcs (1.3) |     diff-lcs (1.3) | ||||||
|  |     digest-crc (0.4.1) | ||||||
|     docile (1.3.2) |     docile (1.3.2) | ||||||
|     domain_name (0.5.20190701) |     domain_name (0.5.20190701) | ||||||
|       unf (>= 0.0.5, < 1.0.0) |       unf (>= 0.0.5, < 1.0.0) | ||||||
| @@ -158,10 +160,38 @@ GEM | |||||||
|       i18n (>= 1.6, < 1.8) |       i18n (>= 1.6, < 1.8) | ||||||
|     faraday (0.17.1) |     faraday (0.17.1) | ||||||
|       multipart-post (>= 1.2, < 3) |       multipart-post (>= 1.2, < 3) | ||||||
|  |     faraday_middleware (0.13.1) | ||||||
|  |       faraday (>= 0.7.4, < 1.0) | ||||||
|     ffi (1.11.3) |     ffi (1.11.3) | ||||||
|     foreman (0.86.0) |     foreman (0.86.0) | ||||||
|     globalid (0.4.2) |     globalid (0.4.2) | ||||||
|       activesupport (>= 4.2.0) |       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) |     haikunator (1.1.0) | ||||||
|     hashie (4.0.0) |     hashie (4.0.0) | ||||||
|     http (3.3.0) |     http (3.3.0) | ||||||
| @@ -177,12 +207,10 @@ GEM | |||||||
|     httparty (0.17.3) |     httparty (0.17.3) | ||||||
|       mime-types (~> 3.0) |       mime-types (~> 3.0) | ||||||
|       multi_xml (>= 0.5.2) |       multi_xml (>= 0.5.2) | ||||||
|  |     httpclient (2.8.3) | ||||||
|     i18n (1.7.0) |     i18n (1.7.0) | ||||||
|       concurrent-ruby (~> 1.0) |       concurrent-ruby (~> 1.0) | ||||||
|     ice_nine (0.11.2) |     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) |     inflecto (0.0.2) | ||||||
|     jaro_winkler (1.5.4) |     jaro_winkler (1.5.4) | ||||||
|     jbuilder (2.9.1) |     jbuilder (2.9.1) | ||||||
| @@ -221,6 +249,7 @@ GEM | |||||||
|       mini_mime (>= 0.1.1) |       mini_mime (>= 0.1.1) | ||||||
|     marcel (0.3.3) |     marcel (0.3.3) | ||||||
|       mimemagic (~> 0.3.2) |       mimemagic (~> 0.3.2) | ||||||
|  |     memoist (0.16.2) | ||||||
|     memoizable (0.4.2) |     memoizable (0.4.2) | ||||||
|       thread_safe (~> 0.3, >= 0.3.1) |       thread_safe (~> 0.3, >= 0.3.1) | ||||||
|     method_source (0.9.2) |     method_source (0.9.2) | ||||||
| @@ -234,6 +263,7 @@ GEM | |||||||
|     minitest (5.13.0) |     minitest (5.13.0) | ||||||
|     mock_redis (0.22.0) |     mock_redis (0.22.0) | ||||||
|     msgpack (1.3.1) |     msgpack (1.3.1) | ||||||
|  |     multi_json (1.14.1) | ||||||
|     multi_xml (0.6.0) |     multi_xml (0.6.0) | ||||||
|     multipart-post (2.1.1) |     multipart-post (2.1.1) | ||||||
|     naught (1.1.0) |     naught (1.1.0) | ||||||
| @@ -243,6 +273,7 @@ GEM | |||||||
|     nokogiri (1.10.7) |     nokogiri (1.10.7) | ||||||
|       mini_portile2 (~> 2.4.0) |       mini_portile2 (~> 2.4.0) | ||||||
|     orm_adapter (0.5.0) |     orm_adapter (0.5.0) | ||||||
|  |     os (1.0.1) | ||||||
|     parallel (1.19.1) |     parallel (1.19.1) | ||||||
|     parser (2.6.5.0) |     parser (2.6.5.0) | ||||||
|       ast (~> 2.4.0) |       ast (~> 2.4.0) | ||||||
| @@ -307,6 +338,10 @@ GEM | |||||||
|       redis-store (>= 1.6, < 2) |       redis-store (>= 1.6, < 2) | ||||||
|     redis-store (1.8.1) |     redis-store (1.8.1) | ||||||
|       redis (>= 4, < 5) |       redis (>= 4, < 5) | ||||||
|  |     representable (3.0.4) | ||||||
|  |       declarative (< 0.1.0) | ||||||
|  |       declarative-option (< 0.2.0) | ||||||
|  |       uber (< 0.2.0) | ||||||
|     responders (3.0.0) |     responders (3.0.0) | ||||||
|       actionpack (>= 5.0) |       actionpack (>= 5.0) | ||||||
|       railties (>= 5.0) |       railties (>= 5.0) | ||||||
| @@ -315,6 +350,7 @@ GEM | |||||||
|       http-cookie (>= 1.0.2, < 2.0) |       http-cookie (>= 1.0.2, < 2.0) | ||||||
|       mime-types (>= 1.16, < 4.0) |       mime-types (>= 1.16, < 4.0) | ||||||
|       netrc (~> 0.8) |       netrc (~> 0.8) | ||||||
|  |     retriable (3.1.2) | ||||||
|     rspec-core (3.9.0) |     rspec-core (3.9.0) | ||||||
|       rspec-support (~> 3.9.0) |       rspec-support (~> 3.9.0) | ||||||
|     rspec-expectations (3.9.0) |     rspec-expectations (3.9.0) | ||||||
| @@ -347,8 +383,6 @@ GEM | |||||||
|     rubocop-rspec (1.37.1) |     rubocop-rspec (1.37.1) | ||||||
|       rubocop (>= 0.68.1) |       rubocop (>= 0.68.1) | ||||||
|     ruby-progressbar (1.10.1) |     ruby-progressbar (1.10.1) | ||||||
|     ruby-vips (2.0.16) |  | ||||||
|       ffi (~> 1.9) |  | ||||||
|     seed_dump (3.3.1) |     seed_dump (3.3.1) | ||||||
|       activerecord (>= 4) |       activerecord (>= 4) | ||||||
|       activesupport (>= 4) |       activesupport (>= 4) | ||||||
| @@ -361,6 +395,11 @@ GEM | |||||||
|       rack (>= 2.0.0) |       rack (>= 2.0.0) | ||||||
|       rack-protection (>= 2.0.0) |       rack-protection (>= 2.0.0) | ||||||
|       redis (>= 4.1.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) |     simple_oauth (0.3.1) | ||||||
|     simplecov (0.17.1) |     simplecov (0.17.1) | ||||||
|       docile (~> 1.1) |       docile (~> 1.1) | ||||||
| @@ -402,6 +441,7 @@ GEM | |||||||
|       thread_safe (~> 0.1) |       thread_safe (~> 0.1) | ||||||
|     tzinfo-data (1.2019.3) |     tzinfo-data (1.2019.3) | ||||||
|       tzinfo (>= 1.0.0) |       tzinfo (>= 1.0.0) | ||||||
|  |     uber (0.1.0) | ||||||
|     uglifier (4.2.0) |     uglifier (4.2.0) | ||||||
|       execjs (>= 0.3.0, < 3) |       execjs (>= 0.3.0, < 3) | ||||||
|     unf (0.1.4) |     unf (0.1.4) | ||||||
| @@ -442,13 +482,14 @@ DEPENDENCIES | |||||||
|   acts-as-taggable-on |   acts-as-taggable-on | ||||||
|   annotate |   annotate | ||||||
|   attr_extras |   attr_extras | ||||||
|  |   aws-sdk-s3 | ||||||
|  |   azure-storage | ||||||
|   bootsnap |   bootsnap | ||||||
|   brakeman |   brakeman | ||||||
|   browser |   browser | ||||||
|   bullet |   bullet | ||||||
|   bundle-audit |   bundle-audit | ||||||
|   byebug |   byebug | ||||||
|   carrierwave-aws |  | ||||||
|   chargebee |   chargebee | ||||||
|   devise |   devise | ||||||
|   devise_token_auth |   devise_token_auth | ||||||
| @@ -457,6 +498,7 @@ DEPENDENCIES | |||||||
|   factory_bot_rails |   factory_bot_rails | ||||||
|   faker |   faker | ||||||
|   foreman |   foreman | ||||||
|  |   google-cloud-storage | ||||||
|   haikunator |   haikunator | ||||||
|   hashie |   hashie | ||||||
|   jbuilder |   jbuilder | ||||||
|   | |||||||
| @@ -36,19 +36,26 @@ module Messages | |||||||
|     def build_contact |     def build_contact | ||||||
|       return if contact.present? |       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) |       ContactInbox.create(contact: contact, inbox: @inbox, source_id: @sender_id) | ||||||
|     end |     end | ||||||
|  |  | ||||||
|     def build_message |     def build_message | ||||||
|       @message = conversation.messages.new(message_params) |       @message = conversation.messages.create!(message_params) | ||||||
|       (response.attachments || []).each do |attachment| |       (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 |       end | ||||||
|       @message.save! |  | ||||||
|     end |     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 |     def conversation | ||||||
|       @conversation ||= Conversation.find_by(conversation_params) || Conversation.create!(conversation_params) |       @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'}", |         name: "#{result['first_name'] || 'John'} #{result['last_name'] || 'Doe'}", | ||||||
|         account_id: @inbox.account_id, |         account_id: @inbox.account_id, | ||||||
|         remote_avatar_url: result['profile_pic'] || nil |         remote_avatar_url: result['profile_pic'] || '' | ||||||
|       } |       } | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|   | |||||||
| @@ -12,8 +12,9 @@ class Api::V1::CallbacksController < ApplicationController | |||||||
|     inbox_name = params[:inbox_name] |     inbox_name = params[:inbox_name] | ||||||
|     facebook_channel = current_account.facebook_pages.create!( |     facebook_channel = current_account.facebook_pages.create!( | ||||||
|       name: page_name, page_id: page_id, user_access_token: user_access_token, |       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) |     inbox = current_account.inboxes.create!(name: inbox_name, channel: facebook_channel) | ||||||
|     render json: inbox |     render json: inbox | ||||||
|   end |   end | ||||||
| @@ -79,7 +80,12 @@ class Api::V1::CallbacksController < ApplicationController | |||||||
|     end |     end | ||||||
|   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 |     begin | ||||||
|       url = 'http://graph.facebook.com/' << page_id << '/picture?type=large' |       url = 'http://graph.facebook.com/' << page_id << '/picture?type=large' | ||||||
|       uri = URI.parse(url) |       uri = URI.parse(url) | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ | |||||||
|     <div class="contact--profile"> |     <div class="contact--profile"> | ||||||
|       <div class="contact--info"> |       <div class="contact--info"> | ||||||
|         <thumbnail |         <thumbnail | ||||||
|           :src="contact.avatar_url" |           :src="contact.thumbnail" | ||||||
|           size="56px" |           size="56px" | ||||||
|           :badge="contact.channel" |           :badge="contact.channel" | ||||||
|           :username="contact.name" |           :username="contact.name" | ||||||
|   | |||||||
| @@ -8,7 +8,6 @@ | |||||||
| #  extension        :string | #  extension        :string | ||||||
| #  external_url     :string | #  external_url     :string | ||||||
| #  fallback_title   :string | #  fallback_title   :string | ||||||
| #  file             :string |  | ||||||
| #  file_type        :integer          default("image") | #  file_type        :integer          default("image") | ||||||
| #  created_at       :datetime         not null | #  created_at       :datetime         not null | ||||||
| #  updated_at       :datetime         not null | #  updated_at       :datetime         not null | ||||||
| @@ -19,12 +18,12 @@ | |||||||
| require 'uri' | require 'uri' | ||||||
| require 'open-uri' | require 'open-uri' | ||||||
| class Attachment < ApplicationRecord | class Attachment < ApplicationRecord | ||||||
|  |   include Rails.application.routes.url_helpers | ||||||
|   belongs_to :account |   belongs_to :account | ||||||
|   belongs_to :message |   belongs_to :message | ||||||
|   mount_uploader :file, AttachmentUploader # used for images |   has_one_attached :file | ||||||
|   enum file_type: [:image, :audio, :video, :file, :location, :fallback] |  | ||||||
|  |  | ||||||
|   before_create :set_file_extension |   enum file_type: [:image, :audio, :video, :file, :location, :fallback] | ||||||
|  |  | ||||||
|   def push_event_data |   def push_event_data | ||||||
|     return base_data.merge(location_metadata) if file_type.to_sym == :location |     return base_data.merge(location_metadata) if file_type.to_sym == :location | ||||||
| @@ -68,13 +67,7 @@ class Attachment < ApplicationRecord | |||||||
|     } |     } | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def set_file_extension |   def file_url | ||||||
|     if external_url && !fallback? |     file.attached? ? url_for(file) : '' | ||||||
|       self.extension = begin |  | ||||||
|                          Pathname.new(URI(external_url).path).extname |  | ||||||
|                        rescue StandardError |  | ||||||
|                          nil |  | ||||||
|                        end |  | ||||||
|     end |  | ||||||
|   end |   end | ||||||
| end | end | ||||||
|   | |||||||
| @@ -3,7 +3,6 @@ | |||||||
| # Table name: channel_facebook_pages | # Table name: channel_facebook_pages | ||||||
| # | # | ||||||
| #  id                :integer          not null, primary key | #  id                :integer          not null, primary key | ||||||
| #  avatar            :string |  | ||||||
| #  name              :string           not null | #  name              :string           not null | ||||||
| #  page_access_token :string           not null | #  page_access_token :string           not null | ||||||
| #  user_access_token :string           not null | #  user_access_token :string           not null | ||||||
| @@ -20,11 +19,13 @@ | |||||||
|  |  | ||||||
| module Channel | module Channel | ||||||
|   class FacebookPage < ApplicationRecord |   class FacebookPage < ApplicationRecord | ||||||
|  |     include Avatarable | ||||||
|  |  | ||||||
|     self.table_name = 'channel_facebook_pages' |     self.table_name = 'channel_facebook_pages' | ||||||
|  |  | ||||||
|     validates :account_id, presence: true |     validates :account_id, presence: true | ||||||
|     validates :page_id, uniqueness: { scope: :account_id } |     validates :page_id, uniqueness: { scope: :account_id } | ||||||
|     mount_uploader :avatar, AvatarUploader |     has_one_attached :avatar | ||||||
|     belongs_to :account |     belongs_to :account | ||||||
|  |  | ||||||
|     has_one :inbox, as: :channel, dependent: :destroy |     has_one :inbox, as: :channel, dependent: :destroy | ||||||
|   | |||||||
							
								
								
									
										18
									
								
								app/models/concerns/avatarable.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								app/models/concerns/avatarable.rb
									
									
									
									
									
										Normal 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 | ||||||
| @@ -3,7 +3,6 @@ | |||||||
| # Table name: contacts | # Table name: contacts | ||||||
| # | # | ||||||
| #  id           :integer          not null, primary key | #  id           :integer          not null, primary key | ||||||
| #  avatar       :string |  | ||||||
| #  email        :string | #  email        :string | ||||||
| #  name         :string | #  name         :string | ||||||
| #  phone_number :string | #  phone_number :string | ||||||
| @@ -20,13 +19,13 @@ | |||||||
|  |  | ||||||
| class Contact < ApplicationRecord | class Contact < ApplicationRecord | ||||||
|   include Pubsubable |   include Pubsubable | ||||||
|  |   include Avatarable | ||||||
|   validates :account_id, presence: true |   validates :account_id, presence: true | ||||||
|  |  | ||||||
|   belongs_to :account |   belongs_to :account | ||||||
|   has_many :conversations, dependent: :destroy |   has_many :conversations, dependent: :destroy | ||||||
|   has_many :contact_inboxes, dependent: :destroy |   has_many :contact_inboxes, dependent: :destroy | ||||||
|   has_many :inboxes, through: :contact_inboxes |   has_many :inboxes, through: :contact_inboxes | ||||||
|   mount_uploader :avatar, AvatarUploader |  | ||||||
|  |  | ||||||
|   def get_source_id(inbox_id) |   def get_source_id(inbox_id) | ||||||
|     contact_inboxes.find_by!(inbox_id: inbox_id).source_id |     contact_inboxes.find_by!(inbox_id: inbox_id).source_id | ||||||
| @@ -36,7 +35,7 @@ class Contact < ApplicationRecord | |||||||
|     { |     { | ||||||
|       id: id, |       id: id, | ||||||
|       name: name, |       name: name, | ||||||
|       thumbnail: avatar.thumb.url, |       thumbnail: avatar_url, | ||||||
|       pubsub_token: pubsub_token |       pubsub_token: pubsub_token | ||||||
|     } |     } | ||||||
|   end |   end | ||||||
|   | |||||||
| @@ -68,11 +68,10 @@ class Inbox < ApplicationRecord | |||||||
|     Facebook::Messenger::Subscriptions.subscribe( |     Facebook::Messenger::Subscriptions.subscribe( | ||||||
|       access_token: channel.page_access_token, |       access_token: channel.page_access_token, | ||||||
|       subscribed_fields: %w[ |       subscribed_fields: %w[ | ||||||
|         message_mention messages messaging_account_linking messaging_checkout_updates |         messages messaging_postbacks messaging_optins message_deliveries | ||||||
|         message_echoes message_deliveries messaging_game_plays messaging_optins messaging_optouts |         message_reads messaging_payments messaging_pre_checkouts messaging_checkout_updates | ||||||
|         messaging_payments messaging_postbacks messaging_pre_checkouts message_reads messaging_referrals |         messaging_account_linking messaging_referrals message_echoes messaging_game_plays | ||||||
|         messaging_handovers messaging_policy_enforcement messaging_page_feedback |         standby messaging_handovers messaging_policy_enforcement message_reactions | ||||||
|         messaging_appointments messaging_direct_sends |  | ||||||
|       ] |       ] | ||||||
|     ) |     ) | ||||||
|   end |   end | ||||||
|   | |||||||
| @@ -47,6 +47,7 @@ class User < ApplicationRecord | |||||||
|   include DeviseTokenAuth::Concerns::User |   include DeviseTokenAuth::Concerns::User | ||||||
|   include Events::Types |   include Events::Types | ||||||
|   include Pubsubable |   include Pubsubable | ||||||
|  |   include Avatarable | ||||||
|   include Rails.application.routes.url_helpers |   include Rails.application.routes.url_helpers | ||||||
|  |  | ||||||
|   devise :database_authenticatable, |   devise :database_authenticatable, | ||||||
| @@ -57,12 +58,6 @@ class User < ApplicationRecord | |||||||
|          :validatable, |          :validatable, | ||||||
|          :confirmable |          :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 |   # The validation below has been commented out as it does not | ||||||
|   # work because :validatable in devise overrides this. |   # work because :validatable in devise overrides this. | ||||||
|   # validates_uniqueness_of :email, scope: :account_id |   # 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) |     Rails.configuration.dispatcher.dispatch(AGENT_REMOVED, Time.zone.now, account: account) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def avatar_url |  | ||||||
|     if avatar.attached? && avatar.representable? |  | ||||||
|       url_for(avatar.representation(resize: '250x250')) |  | ||||||
|     else |  | ||||||
|       '' |  | ||||||
|     end |  | ||||||
|   end |  | ||||||
|  |  | ||||||
|   def push_event_data |   def push_event_data | ||||||
|     { |     { | ||||||
|       name: name, |       name: name, | ||||||
|   | |||||||
| @@ -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 |  | ||||||
| @@ -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 |  | ||||||
| @@ -3,5 +3,5 @@ json.payload do | |||||||
|   json.name @contact.name |   json.name @contact.name | ||||||
|   json.email @contact.email |   json.email @contact.email | ||||||
|   json.phone_number @contact.phone_number |   json.phone_number @contact.phone_number | ||||||
|   json.thumbnail @contact.avatar.thumb.url |   json.thumbnail @contact.avatar_url | ||||||
| end | end | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ json.data do | |||||||
|         json.sender do |         json.sender do | ||||||
|           json.id conversation.contact.id |           json.id conversation.contact.id | ||||||
|           json.name conversation.contact.name |           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) |           json.channel conversation.inbox.try(:channel_type) | ||||||
|         end |         end | ||||||
|         json.assignee conversation.assignee |         json.assignee conversation.assignee | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ json.payload do | |||||||
|     json.channel_id inbox.channel_id |     json.channel_id inbox.channel_id | ||||||
|     json.name inbox.name |     json.name inbox.name | ||||||
|     json.channel_type inbox.channel_type |     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.page_id inbox.channel.try(:page_id) | ||||||
|     json.widget_color inbox.channel.try(:widget_color) |     json.widget_color inbox.channel.try(:widget_color) | ||||||
|     json.website_token inbox.channel.try(:website_token) |     json.website_token inbox.channel.try(:website_token) | ||||||
|   | |||||||
| @@ -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 |  | ||||||
| @@ -15,18 +15,18 @@ amazon: | |||||||
|   bucket: <%= ENV.fetch('S3_BUCKET_NAME', '') %> |   bucket: <%= ENV.fetch('S3_BUCKET_NAME', '') %> | ||||||
|  |  | ||||||
| # Remember not to checkin your GCS keyfile to a repository | # Remember not to checkin your GCS keyfile to a repository | ||||||
| # google: | google: | ||||||
| #   service: GCS |   service: GCS | ||||||
| #   project: your_project |   project: <%= ENV.fetch('GCS_PROJECT', '') %> | ||||||
| #   credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> |   credentials: <%= ENV.fetch('GCS_CREDENTIALS', '').to_json %> | ||||||
| #   bucket: your_own_bucket |   bucket: <%= ENV.fetch('GCS_BUCKET', '') %> | ||||||
|  |  | ||||||
| # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) | # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) | ||||||
| # microsoft: | microsoft: | ||||||
| #   service: AzureStorage |   service: AzureStorage | ||||||
| #   storage_account_name: your_account_name |   storage_account_name: <%= ENV.fetch('AZURE_STORAGE_ACCOUNT_NAME', '') %> | ||||||
| #   storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> |   storage_access_key: <%= ENV.fetch('AZURE_STORAGE_ACCESS_KEY', '') %> | ||||||
| #   container: your_container_name |   container: <%= ENV.fetch('AZURE_STORAGE_CONTAINER', '') %> | ||||||
|  |  | ||||||
| # mirror: | # mirror: | ||||||
| #   service: Mirror | #   service: Mirror | ||||||
|   | |||||||
| @@ -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 | ||||||
| @@ -10,7 +10,7 @@ | |||||||
| # | # | ||||||
| # It's strongly recommended that you check this file into your version control system. | # 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 |   # These are extensions that must be enabled in order to support this database | ||||||
|   enable_extension "plpgsql" |   enable_extension "plpgsql" | ||||||
| @@ -43,7 +43,6 @@ ActiveRecord::Schema.define(version: 2019_12_09_202758) do | |||||||
|   end |   end | ||||||
|  |  | ||||||
|   create_table "attachments", id: :serial, force: :cascade do |t| |   create_table "attachments", id: :serial, force: :cascade do |t| | ||||||
|     t.string "file" |  | ||||||
|     t.integer "file_type", default: 0 |     t.integer "file_type", default: 0 | ||||||
|     t.string "external_url" |     t.string "external_url" | ||||||
|     t.float "coordinates_lat", default: 0.0 |     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.integer "account_id", null: false | ||||||
|     t.datetime "created_at", null: false |     t.datetime "created_at", null: false | ||||||
|     t.datetime "updated_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", "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" |     t.index ["page_id"], name: "index_channel_facebook_pages_on_page_id" | ||||||
|   end |   end | ||||||
| @@ -107,7 +105,6 @@ ActiveRecord::Schema.define(version: 2019_12_09_202758) do | |||||||
|     t.integer "account_id", null: false |     t.integer "account_id", null: false | ||||||
|     t.datetime "created_at", null: false |     t.datetime "created_at", null: false | ||||||
|     t.datetime "updated_at", null: false |     t.datetime "updated_at", null: false | ||||||
|     t.string "avatar" |  | ||||||
|     t.string "pubsub_token" |     t.string "pubsub_token" | ||||||
|     t.index ["account_id"], name: "index_contacts_on_account_id" |     t.index ["account_id"], name: "index_contacts_on_account_id" | ||||||
|     t.index ["pubsub_token"], name: "index_contacts_on_pubsub_token", unique: true |     t.index ["pubsub_token"], name: "index_contacts_on_pubsub_token", unique: true | ||||||
|   | |||||||
| @@ -70,6 +70,7 @@ RUN apk add --update --no-cache \ | |||||||
|     openssl \ |     openssl \ | ||||||
|     tzdata \ |     tzdata \ | ||||||
|     postgresql-client \ |     postgresql-client \ | ||||||
|  |     imagemagick \ | ||||||
|   && gem install bundler |   && gem install bundler | ||||||
|  |  | ||||||
| RUN if [ "$RAILS_ENV" = "production" ]; then \ | RUN if [ "$RAILS_ENV" = "production" ]; then \ | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ class LocalResource | |||||||
|   attr_reader :uri |   attr_reader :uri | ||||||
|  |  | ||||||
|   def initialize(uri) |   def initialize(uri) | ||||||
|     @uri = uri |     @uri = URI(uri) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def file |   def file | ||||||
| @@ -11,6 +11,7 @@ class LocalResource | |||||||
|       f.write(io.read) |       f.write(io.read) | ||||||
|       f.close |       f.close | ||||||
|     end |     end | ||||||
|  |     @file.open | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def io |   def io | ||||||
| @@ -30,9 +31,6 @@ class LocalResource | |||||||
|   end |   end | ||||||
|  |  | ||||||
|   def tmp_folder |   def tmp_folder | ||||||
|     # If we're using Rails: |  | ||||||
|     Rails.root.join('tmp') |     Rails.root.join('tmp') | ||||||
|     # Otherwise: |  | ||||||
|     # '/wherever/you/want' |  | ||||||
|   end |   end | ||||||
| end | end | ||||||
|   | |||||||
							
								
								
									
										34
									
								
								spec/builders/messages/message_builder_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								spec/builders/messages/message_builder_spec.rb
									
									
									
									
									
										Normal 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 | ||||||
| @@ -6,6 +6,7 @@ FactoryBot.define do | |||||||
|     page_access_token { SecureRandom.uuid } |     page_access_token { SecureRandom.uuid } | ||||||
|     user_access_token { SecureRandom.uuid } |     user_access_token { SecureRandom.uuid } | ||||||
|     page_id { SecureRandom.uuid } |     page_id { SecureRandom.uuid } | ||||||
|  |     inbox | ||||||
|     account |     account | ||||||
|   end |   end | ||||||
| end | end | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								spec/factories/facebook_message/incoming_fb_text_message.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								spec/factories/facebook_message/incoming_fb_text_message.rb
									
									
									
									
									
										Normal 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 | ||||||
| @@ -6,7 +6,7 @@ RSpec.describe Channel::FacebookPage do | |||||||
|   before { create(:channel_facebook_page) } |   before { create(:channel_facebook_page) } | ||||||
|  |  | ||||||
|   it { is_expected.to validate_presence_of(:account_id) } |   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 belong_to(:account) } | ||||||
|   it { is_expected.to have_one(:inbox).dependent(:destroy) } |   it { is_expected.to have_one(:inbox).dependent(:destroy) } | ||||||
| end | end | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Sojan Jose
					Sojan Jose