avatar.rb 11 KB

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