avatar.rb 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. class Avatar < ApplicationModel
  3. include HasDefaultModelUserRelations
  4. belongs_to :object_lookup, optional: true
  5. =begin
  6. add an avatar based on auto detection (email address)
  7. Avatar.auto_detection(
  8. object: 'User',
  9. o_id: user.id,
  10. url: 'somebody@example.com',
  11. updated_by_id: 1,
  12. created_by_id: 1,
  13. )
  14. =end
  15. def self.auto_detection(data)
  16. # return if we run import mode
  17. return if Setting.get('import_mode')
  18. return if data[:url].blank?
  19. Avatar.add(
  20. object: data[:object],
  21. o_id: data[:o_id],
  22. url: data[:url],
  23. source: 'zammad.com',
  24. deletable: false,
  25. updated_by_id: 1,
  26. created_by_id: 1,
  27. )
  28. end
  29. =begin
  30. add avatar by upload
  31. Avatar.add(
  32. object: 'User',
  33. o_id: user.id,
  34. default: true,
  35. full: {
  36. content: '...',
  37. mime_type: 'image/png',
  38. },
  39. resize: {
  40. content: '...',
  41. mime_type: 'image/png',
  42. },
  43. source: 'web',
  44. deletable: true,
  45. updated_by_id: 1,
  46. created_by_id: 1,
  47. )
  48. add avatar by url
  49. Avatar.add(
  50. object: 'User',
  51. o_id: user.id,
  52. default: true,
  53. url: ...,
  54. source: 'web',
  55. deletable: true,
  56. updated_by_id: 1,
  57. created_by_id: 1,
  58. )
  59. =end
  60. def self.add(data)
  61. # lookups
  62. if data[:object]
  63. object_id = ObjectLookup.by_name(data[:object])
  64. end
  65. # add initial avatar
  66. _add_init_avatar(object_id, data[:o_id])
  67. record = {
  68. o_id: data[:o_id],
  69. object_lookup_id: object_id,
  70. default: true,
  71. deletable: data[:deletable],
  72. initial: false,
  73. source: data[:source],
  74. source_url: data[:url],
  75. updated_by_id: data[:updated_by_id],
  76. created_by_id: data[:created_by_id],
  77. }
  78. # check if avatar with url already exists
  79. avatar_already_exists = nil
  80. if data[:source].present?
  81. avatar_already_exists = Avatar.find_by(
  82. object_lookup_id: object_id,
  83. o_id: data[:o_id],
  84. source: data[:source],
  85. )
  86. end
  87. # fetch image based on http url
  88. if data[:url].present?
  89. if data[:url].instance_of?(Tempfile)
  90. logger.info "Reading image from tempfile '#{data[:url].inspect}'"
  91. content = data[:url].read
  92. filename = data[:url].path
  93. mime_type = 'image'
  94. if filename.match?(%r{\.png}i)
  95. mime_type = 'image/png'
  96. end
  97. if filename.match?(%r{\.(jpg|jpeg)}i)
  98. mime_type = 'image/jpeg'
  99. end
  100. # forbid creation of avatars without a specified mime_type (image is not displayed in the UI)
  101. if mime_type == 'image'
  102. logger.info "Could not determine mime_type for image '#{data[:url].inspect}'"
  103. return
  104. end
  105. data[:resize] ||= {}
  106. data[:resize][:content] = content
  107. data[:resize][:mime_type] = mime_type
  108. data[:full] ||= {}
  109. data[:full][:content] = content
  110. data[:full][:mime_type] = mime_type
  111. elsif data[:url].to_s.match?(%r{^https?://})
  112. url = data[:url].to_s
  113. # check if source was updated within last 2 minutes
  114. return if avatar_already_exists&.source_url == url && avatar_already_exists.updated_at > 2.minutes.ago
  115. # twitter workaround to get bigger avatar images
  116. # see also https://dev.twitter.com/overview/general/user-profile-images-and-banners
  117. if url.match?(%r{//pbs.twimg.com/}i)
  118. url.sub!(%r{normal\.(png|jpg|gif)$}, 'bigger.\1')
  119. end
  120. # fetch image
  121. response = UserAgent.get(
  122. url,
  123. {},
  124. {
  125. open_timeout: 4,
  126. read_timeout: 6,
  127. total_timeout: 6,
  128. },
  129. )
  130. if !response.success?
  131. logger.info "Can't fetch '#{url}' (maybe no avatar available), http code: #{response.code}"
  132. return
  133. end
  134. logger.info "Fetched image '#{url}', http code: #{response.code}"
  135. mime_type = 'image'
  136. if url.match?(%r{\.png}i)
  137. mime_type = 'image/png'
  138. end
  139. if url.match?(%r{\.(jpg|jpeg)}i)
  140. mime_type = 'image/jpeg'
  141. end
  142. # fallback to content-type of the response if url does not end with png, jpg or jpeg
  143. # see https://github.com/zammad/zammad/issues/3829
  144. if mime_type == 'image' &&
  145. response.header['content-type'].present? &&
  146. Rails.application.config.active_storage.web_image_content_types.include?(response.header['content-type'])
  147. mime_type = response.header['content-type']
  148. end
  149. # forbid creation of avatars without a specified mime_type (image is not displayed in the UI)
  150. if mime_type == 'image'
  151. logger.info "Could not determine mime_type for image '#{url}'"
  152. return
  153. end
  154. data[:resize] ||= {}
  155. data[:resize][:content] = response.body
  156. data[:resize][:mime_type] = mime_type
  157. data[:full] ||= {}
  158. data[:full][:content] = response.body
  159. data[:full][:mime_type] = mime_type
  160. # try zammad backend to find image based on email
  161. elsif data[:url].to_s.match?(URI::MailTo::EMAIL_REGEXP)
  162. url = data[:url].to_s
  163. # check if source ist already updated within last 3 minutes
  164. return if avatar_already_exists&.source_url == url && avatar_already_exists.updated_at > 2.minutes.ago
  165. # fetch image
  166. image = Service::Image.user(url)
  167. return if !image
  168. data[:resize] = image
  169. data[:full] = image
  170. end
  171. end
  172. # check if avatar needs to be updated
  173. if data[:resize].present? && data[:resize][:content].present?
  174. record[:store_hash] = Digest::MD5.hexdigest(data[:resize][:content])
  175. if avatar_already_exists&.store_hash == record[:store_hash]
  176. avatar_already_exists.touch # rubocop:disable Rails/SkipsModelValidations
  177. return avatar_already_exists
  178. end
  179. end
  180. # store images
  181. object_name = "Avatar::#{data[:object]}"
  182. if data[:full].present?
  183. store_full = Store.create!(
  184. object: "#{object_name}::Full",
  185. o_id: data[:o_id],
  186. data: data[:full][:content],
  187. filename: 'avatar_full',
  188. preferences: {
  189. 'Mime-Type' => data[:full][:mime_type]
  190. },
  191. created_by_id: data[:created_by_id],
  192. )
  193. record[:store_full_id] = store_full.id
  194. record[:store_hash] = Digest::MD5.hexdigest(data[:full][:content])
  195. end
  196. if data[:resize].present?
  197. store_resize = Store.create!(
  198. object: "#{object_name}::Resize",
  199. o_id: data[:o_id],
  200. data: data[:resize][:content],
  201. filename: 'avatar',
  202. preferences: {
  203. 'Mime-Type' => data[:resize][:mime_type]
  204. },
  205. created_by_id: data[:created_by_id],
  206. )
  207. record[:store_resize_id] = store_resize.id
  208. record[:store_hash] = Digest::MD5.hexdigest(data[:resize][:content])
  209. end
  210. return if record[:store_resize_id].blank? || record[:store_hash].blank?
  211. # update existing
  212. if avatar_already_exists
  213. avatar_already_exists.update!(record)
  214. avatar = avatar_already_exists
  215. # add new one and set it as default
  216. else
  217. avatar = Avatar.create(record)
  218. set_default_items(object_id, data[:o_id], avatar.id)
  219. end
  220. avatar
  221. end
  222. =begin
  223. set avatars as default
  224. Avatar.set_default('User', 123, avatar_id)
  225. =end
  226. def self.set_default(object_name, o_id, avatar_id)
  227. object_id = ObjectLookup.by_name(object_name)
  228. avatar = Avatar.find_by(
  229. object_lookup_id: object_id,
  230. o_id: o_id,
  231. id: avatar_id,
  232. )
  233. avatar.default = true
  234. avatar.save!
  235. # set all other to default false
  236. set_default_items(object_id, o_id, avatar_id)
  237. avatar
  238. end
  239. =begin
  240. remove all avatars of an object
  241. Avatar.remove('User', 123)
  242. =end
  243. def self.remove(object_name, o_id)
  244. object_id = ObjectLookup.by_name(object_name)
  245. Avatar.where(
  246. object_lookup_id: object_id,
  247. o_id: o_id,
  248. ).destroy_all
  249. object_name_store = "Avatar::#{object_name}"
  250. Store.remove(
  251. object: "#{object_name_store}::Full",
  252. o_id: o_id,
  253. )
  254. Store.remove(
  255. object: "#{object_name_store}::Resize",
  256. o_id: o_id,
  257. )
  258. end
  259. =begin
  260. remove one avatars of an object
  261. Avatar.remove_one('User', 123, avatar_id)
  262. =end
  263. def self.remove_one(object_name, o_id, avatar_id)
  264. object_id = ObjectLookup.by_name(object_name)
  265. Avatar.where(
  266. object_lookup_id: object_id,
  267. o_id: o_id,
  268. id: avatar_id,
  269. ).destroy_all
  270. end
  271. =begin
  272. return all avatars of an user
  273. avatars = Avatar.list('User', 123)
  274. avatars = Avatar.list('User', 123, no_init_add_as_boolean) # per default true
  275. =end
  276. def self.list(object_name, o_id, no_init_add_as_boolean = true)
  277. object_id = ObjectLookup.by_name(object_name)
  278. avatars = Avatar.where(
  279. object_lookup_id: object_id,
  280. o_id: o_id,
  281. ).reorder(initial: :desc, deletable: :asc, created_at: :asc)
  282. # add initial avatar
  283. if no_init_add_as_boolean
  284. _add_init_avatar(object_id, o_id)
  285. end
  286. avatar_list = []
  287. avatars.each do |avatar|
  288. data = avatar.attributes
  289. if avatar.store_resize_id
  290. file = Store.find(avatar.store_resize_id)
  291. data['content'] = "data:#{file.preferences['Mime-Type']};base64,#{Base64.strict_encode64(file.content)}"
  292. end
  293. avatar_list.push data
  294. end
  295. avatar_list
  296. end
  297. =begin
  298. get default avatar image of user by hash
  299. store = Avatar.get_by_hash(hash)
  300. returns:
  301. store object
  302. =end
  303. def self.get_by_hash(hash)
  304. avatar = Avatar.find_by(
  305. store_hash: hash,
  306. )
  307. return if !avatar
  308. Store.find(avatar.store_resize_id)
  309. end
  310. =begin
  311. get default avatar of user by user id
  312. avatar = Avatar.get_default('User', user_id)
  313. returns:
  314. avatar object
  315. =end
  316. def self.get_default(object_name, o_id)
  317. object_id = ObjectLookup.by_name(object_name)
  318. Avatar.find_by(
  319. object_lookup_id: object_id,
  320. o_id: o_id,
  321. default: true,
  322. )
  323. end
  324. def self.set_default_items(object_id, o_id, avatar_id)
  325. avatars = Avatar.where(
  326. object_lookup_id: object_id,
  327. o_id: o_id,
  328. ).reorder(created_at: :asc)
  329. avatars.each do |avatar|
  330. next if avatar.id == avatar_id
  331. avatar.default = false
  332. avatar.save!
  333. end
  334. end
  335. def self._add_init_avatar(object_id, o_id)
  336. count = Avatar.where(
  337. object_lookup_id: object_id,
  338. o_id: o_id,
  339. ).count
  340. return if count.positive?
  341. object_name = ObjectLookup.by_id(object_id)
  342. return if !object_name.constantize.exists?(id: o_id)
  343. Avatar.create!(
  344. o_id: o_id,
  345. object_lookup_id: object_id,
  346. default: true,
  347. source: 'init',
  348. initial: true,
  349. deletable: false,
  350. updated_by_id: 1,
  351. created_by_id: 1,
  352. )
  353. end
  354. end