avatar.rb 11 KB

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