1
1
class User < ApplicationRecord
2
-
3
- FALLBACK_AVATAR_URL = 'https://pbs.twimg.com/profile_images/1581014308397502464/NPogKMyk_400x400 .jpg'
2
+ FALLBACK_AVATAR_URL = '/avatars/default_avatar.jpg'
3
+ FALLBACK_BANNER_URL = '/banners/default_banner .jpg'
4
4
5
5
attr_accessor :invite_code
6
6
devise :database_authenticatable , :registerable ,
@@ -16,15 +16,16 @@ class User < ApplicationRecord
16
16
VALID_USERNAME_REGEX = /\A [a-zA-Z0-9_]+\z /
17
17
18
18
validates :username , presence : true , uniqueness : true , format : { with : VALID_USERNAME_REGEX , message : 'can only contain letters, numbers, and underscores' }
19
-
20
- validates :username , uniqueness : true , allow_blank : true
21
19
validates :full_name , presence : true
22
- validate :ensure_username_presence
23
20
validates :avatar_border , inclusion : { in : [ 'white' , 'black' , 'none' , 'rainbow' , 'rainbow-overlay' ] }
24
21
validates :avatar , format : { with : /\A (https?:\/ \/ ).*\z /i , message : "must be a valid URL" } , allow_blank : true
22
+ validates :banner , format : { with : /\A (https?:\/ \/ ).*\z /i , message : "must be a valid URL" } , allow_blank : true
25
23
26
- after_save :generate_open_graph_image , unless : -> { Rails . env . test? }
27
- after_save :download_and_store_avatar
24
+ before_validation :ensure_username_presence
25
+ before_create :set_default_images
26
+ after_create :generate_open_graph_image , unless : -> { Rails . env . test? }
27
+ after_save :download_and_store_avatar , if : -> { saved_change_to_avatar? && avatar . present? }
28
+ after_save :download_and_store_banner , if : -> { saved_change_to_banner? && banner . present? }
28
29
29
30
serialize :tags , coder : JSON
30
31
@@ -36,7 +37,7 @@ def parsed_tags
36
37
[ ]
37
38
end
38
39
else
39
- tags
40
+ tags || [ ]
40
41
end
41
42
end
42
43
@@ -45,43 +46,93 @@ def generate_open_graph_image
45
46
end
46
47
47
48
def download_and_store_avatar
48
- if avatar . blank?
49
- self . avatar = FALLBACK_AVATAR_URL
50
- save ( validate : false )
49
+ download_and_store_image ( :avatar , FALLBACK_AVATAR_URL )
50
+ end
51
+
52
+ def download_and_store_banner
53
+ download_and_store_image ( :banner , FALLBACK_BANNER_URL )
54
+ end
55
+
56
+ def avatar_url
57
+ avatar_local_path . present? ? "/#{ avatar_local_path } " : ( avatar . presence || FALLBACK_AVATAR_URL )
58
+ end
59
+
60
+ def banner_url
61
+ banner_local_path . present? ? "/#{ banner_local_path } " : ( banner . presence || FALLBACK_BANNER_URL )
62
+ end
63
+
64
+ def valid_url? ( url )
65
+ uri = URI . parse ( url )
66
+ uri . is_a? ( URI ::HTTP ) && !uri . host . nil?
67
+ rescue URI ::InvalidURIError
68
+ false
69
+ end
70
+
71
+ private
72
+
73
+ def ensure_username_presence
74
+ if username . blank?
75
+ self . username = email . present? ? email . split ( '@' ) . first : "user#{ SecureRandom . hex ( 4 ) } "
76
+ end
77
+ end
78
+
79
+ def set_default_images
80
+ self . avatar ||= FALLBACK_AVATAR_URL
81
+ self . banner ||= FALLBACK_BANNER_URL
82
+ end
83
+
84
+ def download_and_store_image ( type , fallback_url )
85
+ url = send ( type )
86
+
87
+ Rails . logger . info "Downloading #{ type } from #{ url } "
88
+
89
+ if url . blank? || !valid_url? ( url )
90
+ Rails . logger . warn "#{ type . capitalize } URL invalid or blank. Using fallback."
91
+ update_column ( "#{ type } _local_path" , fallback_url )
51
92
return
52
93
end
53
94
54
95
begin
55
- avatar_dir = Rails . root . join ( 'public' , 'avatars' )
56
- FileUtils . mkdir_p ( avatar_dir ) unless File . directory? ( avatar_dir )
96
+ uri = URI . parse ( url )
97
+ Rails . logger . info "Attempting to download #{ type } from #{ uri } "
98
+ Net ::HTTP . start ( uri . host , uri . port , use_ssl : uri . scheme == 'https' ) do |http |
99
+ request = Net ::HTTP ::Get . new ( uri )
100
+ response = http . request ( request )
101
+
102
+ if response . is_a? ( Net ::HTTPSuccess )
103
+ content_type = response [ 'Content-Type' ]
104
+ Rails . logger . info "Downloaded #{ type } , content type: #{ content_type } "
105
+
106
+ unless content_type . start_with? ( 'image/' )
107
+ raise "Invalid content type: #{ content_type } "
108
+ end
109
+
110
+ extension = case content_type
111
+ when 'image/jpeg' then '.jpg'
112
+ when 'image/png' then '.png'
113
+ when 'image/gif' then '.gif'
114
+ else ''
115
+ end
116
+
117
+ image_dir = Rails . root . join ( 'public' , type . to_s . pluralize )
118
+ FileUtils . mkdir_p ( image_dir ) unless File . directory? ( image_dir )
119
+
120
+ filename = "#{ username } _#{ type } #{ extension } "
121
+ filepath = File . join ( image_dir , filename )
57
122
58
- uri = URI . parse ( avatar )
59
- filename = "#{ username } _avatar#{ File . extname ( avatar ) } "
60
- filepath = File . join ( avatar_dir , filename )
123
+ File . open ( filepath , 'wb' ) { |file | file . write ( response . body ) }
61
124
62
- response = Net ::HTTP . get_response ( uri )
63
- if response . is_a? ( Net ::HTTPSuccess )
64
- File . open ( filepath , 'wb' ) do |local_file |
65
- local_file . write ( response . body )
125
+ update_column ( "#{ type } _local_path" , "#{ type . to_s . pluralize } /#{ filename } " )
126
+ Rails . logger . info "#{ type . capitalize } successfully downloaded for user #{ username } "
127
+
128
+ else
129
+ Rails . logger . warn "Failed to download #{ type } for user #{ username } : HTTP Error: #{ response . code } #{ response . message } . Using local fallback."
130
+ update_column ( type , fallback_url )
66
131
end
67
- Rails . logger . info "Avatar downloaded for user #{ username } "
68
- else
69
- Rails . logger . error "Failed to download avatar for user #{ username } . HTTP Error: #{ response . code } #{ response . message } . Using fallback avatar."
70
- self . avatar = FALLBACK_AVATAR_URL
71
- save ( validate : false )
72
132
end
73
133
rescue StandardError => e
74
- Rails . logger . error "Failed to download avatar for user #{ username } : #{ e . message } . Using fallback avatar."
75
- self . avatar = FALLBACK_AVATAR_URL
76
- save ( validate : false )
134
+ Rails . logger . error "Failed to download #{ type } for user #{ username } : #{ e . message } . Using fallback."
135
+ update_column ( type , fallback_url )
77
136
end
78
137
end
79
-
80
- private
81
-
82
- def ensure_username_presence
83
- if username . blank?
84
- self . username = email . present? ? email . split ( '@' ) . first : "user#{ SecureRandom . hex ( 4 ) } "
85
- end
86
- end
87
- end
138
+ end
0 commit comments