acts_as_avatar 1.8.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActsAsAvatar
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ end
7
+
8
+ module ClassMethods
9
+ def acts_as_avatar(**options)
10
+ random_image_engine = options.delete(:random_image_engine)
11
+ inline_svg_engine = options.delete(:inline_svg_engine)
12
+
13
+ define_singleton_method :random_image_engine do
14
+ random_image_engine.presence || ActsAsAvatar.configuration.random_image_engine
15
+ end
16
+
17
+ define_singleton_method :inline_svg_engine do
18
+ inline_svg_engine.presence || ActsAsAvatar.configuration.inline_svg_engine
19
+ end
20
+
21
+ has_one :avatar, as: :avatarable, class_name: "ActsAsAvatar::Avatar"
22
+ accepts_nested_attributes_for :avatar
23
+ delegate :current_avatar, :default_avatar, :upload_avatar, to: :avatar
24
+ include InstanceMethods
25
+ # after_create_commit { create_avatar }
26
+ end
27
+ end
28
+
29
+ module InstanceMethods
30
+ def avatar
31
+ super || create_avatar
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/numeric/bytes"
4
+
5
+ module ActsAsAvatar
6
+ @mutex = Mutex.new
7
+
8
+ class << self
9
+ def configuration
10
+ @mutex.synchronize do
11
+ @configuration ||= Configuration.new
12
+ end
13
+ end
14
+ end
15
+
16
+ def self.configure
17
+ yield configuration
18
+ end
19
+
20
+ class Configuration
21
+ attr_accessor :uifaces_gender,
22
+ :uifaces_limit,
23
+ :uifaces_uri,
24
+ :uifaces_api_key,
25
+ :random_image_engine,
26
+ :default_file_name,
27
+ :upload_max_size,
28
+ :class_type,
29
+ :avatar_name,
30
+ :inline_svg_engine,
31
+ :avatar_size,
32
+ :github_complexity,
33
+ :github_render_method,
34
+ :github_rounded_circle
35
+
36
+ def initialize
37
+ @uifaces_uri = "https://api.uifaces.co"
38
+ @uifaces_api_key = "72694e5b5dee4706edc0acad3d4291"
39
+ @uifaces_gender = nil
40
+ @uifaces_limit = 72
41
+ @random_image_engine = nil
42
+ @default_file_name = "default_avatar"
43
+ @upload_max_size = 2.megabytes
44
+ @class_type = %w[User]
45
+ @avatar_name = :name
46
+ @inline_svg_engine = :initial_avatar
47
+ @avatar_size = 60
48
+ @github_complexity = 5
49
+ @github_render_method = "square"
50
+ @github_rounded_circle = false
51
+ end
52
+
53
+ def default_options
54
+ instance_variables.to_h do |key|
55
+ [key.to_s.sub("@", "").to_sym, instance_variable_get(key)]
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActsAsAvatar
4
+ class Engine < Rails::Engine
5
+ isolate_namespace ActsAsAvatar
6
+
7
+ initializer "acts_as_avatar.insert_into_active_record" do
8
+ ActiveSupport.on_load :active_record do
9
+ # include ActsAsAvatar::ClassMethods
10
+ ActiveRecord::Base.include ActsAsAvatar
11
+ ActiveRecord::Base.include ActsAsAvatar::ClassMethods
12
+ end
13
+
14
+ ActiveSupport.on_load :action_controller do
15
+ ActionController::Base.send(:helper, ActsAsAvatar::Helper)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/migration"
5
+
6
+ module ActsAsAvatar
7
+ module Generators
8
+ class InstallGenerator < Rails::Generators::Base
9
+ include Rails::Generators::Migration
10
+
11
+ source_root File.join(__dir__, "templates")
12
+ desc "Install acts_as_avatar"
13
+
14
+ def self.next_migration_number(dirname)
15
+ if ActiveRecord::Base.timestamped_migrations
16
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
17
+ else
18
+ format("%<migration_number>.3d",
19
+ migration_number: current_migration_number(dirname) + 1)
20
+ end
21
+ end
22
+
23
+ def create_initializer
24
+ template "initializer.rb", "config/initializers/acts_as_avatar.rb"
25
+ end
26
+
27
+ def create_migration_file
28
+ migration_template(
29
+ "migration.rb.erb",
30
+ "db/migrate/acts_as_avatar_migration.rb",
31
+ migration_version: migration_version
32
+ )
33
+ puts "\nPlease run this migration:\n\n rails db:migrate"
34
+ end
35
+
36
+ private
37
+
38
+ def migration_version
39
+ "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ ActsAsAvatar.configure do |config|
4
+ #
5
+ # Global configuration
6
+ #
7
+ config.default_file_name = "default_avatar"
8
+ config.upload_max_size = 2.megabytes
9
+ config.class_type = %w[User]
10
+ config.avatar_name = :fullname # Name for show letter
11
+ config.avatar_size = 60
12
+
13
+ #
14
+ # Optional value is
15
+ # :letter_avatar, :uifaces_avatar, :github_avatar, or :identicon_avatar
16
+ # When value is nil, disable engine
17
+ #
18
+ config.random_image_engine = :letter_avatar
19
+
20
+ #
21
+ # When random_image_engine is :github_avatar
22
+ #
23
+ config.github_complexity = 5
24
+ config.github_render_method = "square"
25
+ config.github_rounded_circle = false
26
+
27
+ #
28
+ # Optional value is :initial_avatar, :initials or :icodi_avatar
29
+ #
30
+ config.inline_svg_engine = :initial_avatar
31
+
32
+ #
33
+ # When random_image_engine is :uifaces
34
+ #
35
+ config.uifaces_limit = 72
36
+ config.uifaces_gender = nil
37
+ config.uifaces_uri = "https://api.uifaces.co"
38
+ config.uifaces_api_key = ""
39
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateActsAsAvatars < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ create_table :acts_as_avatars do |t|
6
+ t.references :avatarable, polymorphic: true, index: true, null: false
7
+
8
+ t.timestamps
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "singleton"
4
+
5
+ module ActsAsAvatar
6
+ class GithubAvatar
7
+ include Singleton
8
+
9
+ #
10
+ # Default options:
11
+ #
12
+ # complexity: 16
13
+ #
14
+ # redner_method: squad
15
+ #
16
+ def random_svg_avatar(size:, github_complexity:, github_render_method:, github_rounded_circle:)
17
+ source = File.join(File.expand_path("../../", File.dirname(__FILE__)), "github_avatar.js")
18
+ js = File.read(source)
19
+ context = ExecJS.compile(js)
20
+ context.call("getRandomAvatar", github_complexity, github_render_method, size, github_rounded_circle)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "initials"
4
+ require "execjs"
5
+ require "initial_avatar"
6
+ require "icodi"
7
+ require "nokogiri"
8
+
9
+ module ActsAsAvatar
10
+ module Helper
11
+ include ActsAsAvatar::Scrubber
12
+
13
+ def acts_as_avatar_tag(object, name: nil, size: nil, **options)
14
+ size = size.presence || ActsAsAvatar.configuration.avatar_size
15
+
16
+ current_avatar = object.current_avatar
17
+ blob = current_avatar.blob
18
+
19
+ if current_avatar.attached?
20
+ if blob.content_type == "image/svg+xml"
21
+ render_svg blob, size, options
22
+ else
23
+ image_tag current_avatar.variant(resize_to_fill: [size, size]), **options
24
+ end
25
+ else
26
+ inline_avatar_tag(object, name: name, size: size, **options)
27
+ end
28
+ rescue StandardError => e
29
+ puts e.inspect
30
+ puts e.backtrace.inspect
31
+ "".html_safe
32
+ end
33
+
34
+ def inline_avatar_tag(object, size:, name: nil, **options)
35
+ name = name.presence || ActsAsAvatar.configuration.avatar_name
36
+ text = object.send(name.to_sym)
37
+
38
+ inline_svg_engine = object.class.inline_svg_engine.to_sym
39
+
40
+ case inline_svg_engine.to_sym
41
+ when :initial_avatar
42
+ initial_avatar_tag(text, size: size, **options)
43
+ when :icodi_avatar
44
+ icodi_avatar_tag(text, size: size, **options)
45
+ when :initials
46
+ initials_tag(text, size: size, **options)
47
+ end
48
+ end
49
+
50
+ def initial_avatar_tag(text, size:, **options)
51
+ opts = options.extract!(:colors, :text_color, :font_weight, :font_family).compact
52
+
53
+ limit = options[:limit] || 1
54
+
55
+ image_tag InitialAvatar.avatar_data_uri(text.first(limit), size: size, **opts), **options
56
+ end
57
+
58
+ def initials_tag(text, size:, **options)
59
+ #
60
+ # number of different colors, default: 12
61
+ # maximal initials length, default: 3
62
+ # background shape, default: :cirlce
63
+ #
64
+ opts = options.extract!(:colors, :limit, :shape).compact
65
+
66
+ Initials.svg text, size: size, **opts
67
+ end
68
+
69
+ def github_avatar_tag(size:, **options)
70
+ opts = options.extract!(:github_complexity, :github_render_method, :github_rounded_circle).compact
71
+
72
+ opts = ActsAsAvatar.configuration.default_options.merge(opts)
73
+
74
+ ActsAsAvatar::GithubAvatar.instance.random_svg_avatar(
75
+ size: size,
76
+ github_complexity: opts[:github_complexity],
77
+ github_render_method: opts[:github_render_method],
78
+ github_rounded_circle: opts[:github_rounded_circle]
79
+ ).html_safe
80
+ end
81
+
82
+ def icodi_avatar_tag(text, size:, **options)
83
+ #
84
+ # https://github.com/DannyBen/icodi
85
+ #
86
+ # Default value:
87
+ #
88
+ # custom attribute:
89
+ #
90
+ # rounded_circle: nil
91
+ #
92
+ # pixels: 5
93
+ # mirror: :x
94
+ # color:
95
+ # density: 0.5
96
+ # stroke: 0.1
97
+ # jitter: 0
98
+ # background: #fff
99
+ # id: icodi
100
+ #
101
+ # SVG template to use.
102
+ # Can be :default, :html or a path to a file. Read more on Victor SVG Templates.
103
+ #
104
+ # template: default
105
+ opts = options.extract!(:pixels,
106
+ :density,
107
+ :mirror,
108
+ :color,
109
+ :stroke,
110
+ :jitter,
111
+ :id,
112
+ :template,
113
+ :rounded_circle,
114
+ :background)
115
+
116
+ pixels = opts[:pixels] || 8
117
+ density = opts[:density] || 0.33
118
+
119
+ svg = Icodi.new text, size: size, pixels: pixels, density: density, **opts
120
+ svg.render.html_safe
121
+ end
122
+
123
+ private
124
+
125
+ def render_svg(blob, size, options)
126
+ doc = Nokogiri::XML::Document.parse blob.download.to_s
127
+ svg = doc.root
128
+
129
+ title = options.delete(:title)
130
+ svg.add_child("<title>#{title}</title>") if title
131
+
132
+ svg[:width] = size
133
+ svg[:height] = size
134
+
135
+ options.each do |key, value|
136
+ svg[key] = value
137
+ end
138
+
139
+ sanitize(svg.to_s).html_safe
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "singleton"
4
+ require "forwardable"
5
+ require "http"
6
+ require "uri"
7
+
8
+ module ActsAsAvatar
9
+ class Request
10
+ # private_class_method :new
11
+ include Singleton
12
+
13
+ URL = ActsAsAvatar.configuration.uifaces_uri
14
+ API_KEY = ActsAsAvatar.configuration.uifaces_api_key
15
+
16
+ %w[uifaces_gender uifaces_limit].each do |method_name|
17
+ attr_writer method_name.to_sym
18
+
19
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
20
+ #
21
+ # def uifaces_gender
22
+ # @uifaces_gender ||= ActsAsAvatar.configuration.instance_variable_get("@uifaces_gender")
23
+ # end
24
+ #
25
+ def #{method_name}
26
+ @#{method_name} ||= ActsAsAvatar.configuration.instance_variable_get("@#{method_name}")
27
+ end
28
+ RUBY
29
+ end
30
+
31
+ class << self
32
+ extend Forwardable
33
+ delegate %i[read_image] => :instance
34
+ end
35
+
36
+ def params
37
+ if uifaces_gender.nil?
38
+ "popular&limit=#{uifaces_limit}"
39
+ else
40
+ "gender%5B%5D=#{uifaces_gender}&limit=#{uifaces_limit}"
41
+ end
42
+ end
43
+
44
+ # Get a random image
45
+ #
46
+ # @example
47
+ # ActsAsAvatar::Request.read_image
48
+ #
49
+ # ActsAsAvatar::Request.read_image uifaces_limit: 20, uifaces_gender: "female"
50
+ #
51
+ # instance = ActsAsAvatar::Request.instance
52
+ # instance.uifaces_gender = "male"
53
+ # instance.uifaces_limit = 20
54
+ # instance.read_image
55
+ #
56
+ # @param [String] uifaces_gender
57
+ # the gender is male or female
58
+ # @param [Integer] uifaces_limit
59
+ # the count of the image records
60
+ #
61
+ # @api public
62
+ def read_image(options={})
63
+ # configuration = {
64
+ # uifaces_gender: ActsAsAvatar.configuration.uifaces_gender,
65
+ # uifaces_limit: ActsAsAvatar.configuration.uifaces_limit
66
+ # }
67
+ #
68
+ # configuration.update(options) if options.is_a?(Hash)
69
+
70
+ opts = ActsAsAvatar.configuration.default_options.merge(options)
71
+
72
+ @uifaces_gender = opts[:uifaces_gender]
73
+ @uifaces_limit = opts[:uifaces_limit]
74
+
75
+ URI.parse(image_url).open if image_url
76
+ end
77
+
78
+ private
79
+
80
+ def request
81
+ { success: true, res: HTTP.headers("x-api-key": API_KEY).get("#{URL}?#{params}") }
82
+ rescue StandardError => e
83
+ { success: false, message: e.to_s }
84
+ end
85
+
86
+ def image_url
87
+ return unless request[:success] && request[:res].status.success?
88
+
89
+ result = request[:res].parse(:json)
90
+ result[rand(uifaces_limit.to_i - 1)]["photo"]
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "loofah"
4
+
5
+ module ActsAsAvatar
6
+ module Scrubber
7
+ #
8
+ # rails: sanitize(svg.to_s, scrubber: scrubber).html_safe
9
+ #
10
+ def sanitize(unsafe_xml)
11
+ unsafe_xml = unsafe_xml.to_s
12
+ unsafe_xml.force_encoding "UTF-8"
13
+ return Loofah.xml_document(unsafe_xml).scrub!(scrubber).to_s if document?(unsafe_xml)
14
+
15
+ Loofah.xml_fragment(unsafe_xml).scrub!(scrubber).to_s
16
+ end
17
+
18
+ private
19
+
20
+ def scrubber
21
+ Loofah::Scrubber.new do |node|
22
+ node.remove if node.name == "script"
23
+ end
24
+ end
25
+
26
+ def document?(unsafe)
27
+ unsafe.include?("<?xml") || unsafe.include?("<!DOCTYPE")
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActsAsAvatar
4
+ VERSION = "1.8.4"
5
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "acts_as_avatar/generators/install_generator"
4
+
5
+ module ActsAsAvatar
6
+ autoload :VERSION, "acts_as_avatar/version"
7
+ autoload :Request, "acts_as_avatar/request"
8
+ autoload :GithubAvatar, "acts_as_avatar/github_avatar"
9
+ autoload :Scrubber, "acts_as_avatar/scrubber"
10
+
11
+ require_relative "acts_as_avatar/configuration"
12
+ require_relative "acts_as_avatar/class_methods"
13
+ require_relative "acts_as_avatar/helper"
14
+ require_relative "acts_as_avatar/engine" if defined? Rails::Engine
15
+ end