acts_as_avatar 1.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +320 -0
- data/README.md +59 -0
- data/app/jobs/acts_as_avatar/sanitize_svg_job.rb +23 -0
- data/app/jobs/acts_as_avatar/ui_faces_avatar_job.rb +11 -0
- data/app/models/acts_as_avatar/avatar.rb +142 -0
- data/github_avatar.js +121 -0
- data/lib/acts_as_avatar/class_methods.rb +34 -0
- data/lib/acts_as_avatar/configuration.rb +59 -0
- data/lib/acts_as_avatar/engine.rb +19 -0
- data/lib/acts_as_avatar/generators/install_generator.rb +43 -0
- data/lib/acts_as_avatar/generators/templates/initializer.rb.tt +39 -0
- data/lib/acts_as_avatar/generators/templates/migration.rb.erb +11 -0
- data/lib/acts_as_avatar/github_avatar.rb +23 -0
- data/lib/acts_as_avatar/helper.rb +142 -0
- data/lib/acts_as_avatar/request.rb +93 -0
- data/lib/acts_as_avatar/scrubber.rb +30 -0
- data/lib/acts_as_avatar/version.rb +5 -0
- data/lib/acts_as_avatar.rb +15 -0
- metadata +304 -0
@@ -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,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
|