acts_as_avatar 1.8.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|