Initial commit
This commit is contained in:
0
lib/assets/.keep
Normal file
0
lib/assets/.keep
Normal file
48
lib/assets/javascripts/signature_pad-extensions.js
Normal file
48
lib/assets/javascripts/signature_pad-extensions.js
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Crop signature canvas to only contain the signature and no whitespace.
|
||||
*
|
||||
* @returns The cropped canvas
|
||||
* @credit - https://github.com/szimek/signature_pad/issues/49#issuecomment-260976909
|
||||
*/
|
||||
SignaturePad.prototype.crop = function() {
|
||||
var canvas = this._ctx.canvas;
|
||||
|
||||
// First duplicate the canvas to not alter the original
|
||||
var croppedCanvas = document.createElement('canvas'),
|
||||
croppedCtx = croppedCanvas.getContext('2d');
|
||||
|
||||
croppedCanvas.width = canvas.width;
|
||||
croppedCanvas.height = canvas.height;
|
||||
croppedCtx.drawImage(canvas, 0, 0);
|
||||
|
||||
// Next do the actual cropping
|
||||
var w = croppedCanvas.width,
|
||||
h = croppedCanvas.height,
|
||||
pix = {x:[], y:[]},
|
||||
imageData = croppedCtx.getImageData(0,0,croppedCanvas.width,croppedCanvas.height),
|
||||
x, y, index;
|
||||
|
||||
for (y = 0; y < h; y++) {
|
||||
for (x = 0; x < w; x++) {
|
||||
index = (y * w + x) * 4;
|
||||
if (imageData.data[index+3] > 0) {
|
||||
pix.x.push(x);
|
||||
pix.y.push(y);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
pix.x.sort(function(a,b){return a-b});
|
||||
pix.y.sort(function(a,b){return a-b});
|
||||
var n = pix.x.length-1;
|
||||
|
||||
w = pix.x[n] - pix.x[0];
|
||||
h = pix.y[n] - pix.y[0];
|
||||
var cut = croppedCtx.getImageData(pix.x[0], pix.y[0], w, h);
|
||||
|
||||
croppedCanvas.width = w;
|
||||
croppedCanvas.height = h;
|
||||
croppedCtx.putImageData(cut, 0, 0);
|
||||
|
||||
return croppedCanvas;
|
||||
};
|
||||
95
lib/bootstrap_form_extensions.rb
Normal file
95
lib/bootstrap_form_extensions.rb
Normal file
@@ -0,0 +1,95 @@
|
||||
module BootstrapFormExtensions
|
||||
# Extend help text to allow translations to be specified under 'helpers.help'
|
||||
def get_help_text_by_i18n_key(name)
|
||||
return if object.nil?
|
||||
|
||||
HelpText.new(object, name).to_s
|
||||
end
|
||||
|
||||
def required_attribute?(obj, attribute)
|
||||
RequiredAttribute.new(obj, attribute, options[:validation_context]).required?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
class RequiredAttribute
|
||||
def initialize(obj, attribute, context)
|
||||
@obj = obj
|
||||
@attribute = attribute
|
||||
@context = context
|
||||
end
|
||||
|
||||
def required?
|
||||
return false unless obj and attribute
|
||||
|
||||
presence_validator.present? && validates_in_context?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :obj, :attribute, :context
|
||||
|
||||
def model_validation_context
|
||||
if presence_validator
|
||||
presence_validator.options[:on]
|
||||
end
|
||||
end
|
||||
|
||||
def presence_validator
|
||||
target_validators.detect { |validator| validator.class.in? [ActiveModel::Validations::PresenceValidator, ActiveRecord::Validations::PresenceValidator] }
|
||||
end
|
||||
|
||||
def target
|
||||
(obj.class == Class) ? obj : obj.class
|
||||
end
|
||||
|
||||
def target_validators
|
||||
if target.respond_to? :validators_on
|
||||
target.validators_on(attribute)
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def validates_in_context?
|
||||
(model_validation_context.blank? || model_validation_context == context)
|
||||
end
|
||||
end
|
||||
|
||||
class HelpText
|
||||
attr_reader :object, :name
|
||||
|
||||
def initialize(object, name)
|
||||
@object = object
|
||||
@name = name
|
||||
end
|
||||
|
||||
def to_s
|
||||
namespaces.map { |namespace| help_text_for(namespace) }.compact.first
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def help_text_for(namespace)
|
||||
underscored_scope = "#{namespace}.#{partial_scope.underscore}"
|
||||
downcased_scope = "#{namespace}.#{partial_scope.downcase}"
|
||||
|
||||
help_text = I18n.t(name, scope: underscored_scope, default: '').presence
|
||||
help_text ||= if text = I18n.t(name, scope: downcased_scope, default: '').presence
|
||||
warn "I18n key '#{downcased_scope}.#{name}' is deprecated, use '#{underscored_scope}.#{name}' instead"
|
||||
text
|
||||
end
|
||||
|
||||
help_text
|
||||
end
|
||||
|
||||
def partial_scope
|
||||
# ActiveModel::Naming 3.X.X does not support .name; it is supported as of 4.X.X
|
||||
@partial_scope ||= object.class.model_name.respond_to?(:name) ? object.class.model_name.name : object.class.model_name
|
||||
end
|
||||
|
||||
def namespaces
|
||||
%w(activerecord.help helpers.help)
|
||||
end
|
||||
end
|
||||
end
|
||||
14
lib/brayniac_ai.rb
Normal file
14
lib/brayniac_ai.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
require_relative "./brayniac_ai/aws_signature_headers"
|
||||
require_relative "./brayniac_ai/aws_request_signing"
|
||||
require_relative "./brayniac_ai/aws_signed_connection"
|
||||
|
||||
require_relative "./brayniac_ai/base"
|
||||
require_relative "./brayniac_ai/audio_recognition"
|
||||
require_relative "./brayniac_ai/collection"
|
||||
require_relative "./brayniac_ai/document_analysis"
|
||||
require_relative "./brayniac_ai/edl_parse"
|
||||
require_relative "./brayniac_ai/edl_parse_result"
|
||||
require_relative "./brayniac_ai/facial_recognition"
|
||||
require_relative "./brayniac_ai/facial_recognition_result"
|
||||
require_relative "./brayniac_ai/tag"
|
||||
require_relative "./brayniac_ai/validation"
|
||||
4
lib/brayniac_ai/audio_recognition.rb
Normal file
4
lib/brayniac_ai/audio_recognition.rb
Normal file
@@ -0,0 +1,4 @@
|
||||
module BrayniacAI
|
||||
class AudioRecognition < Base
|
||||
end
|
||||
end
|
||||
19
lib/brayniac_ai/aws_request_signing.rb
Normal file
19
lib/brayniac_ai/aws_request_signing.rb
Normal file
@@ -0,0 +1,19 @@
|
||||
module BrayniacAI
|
||||
module AwsRequestSigning
|
||||
def request(method, path, *arguments)
|
||||
case method
|
||||
when :patch, :put, :post
|
||||
# These request types include a body and headers
|
||||
body = arguments.first
|
||||
headers = arguments.last
|
||||
new_headers = AwsSignatureHeaders.new(method, self.site.merge(path), body, headers)
|
||||
super(method, path, body, headers.merge(new_headers))
|
||||
else
|
||||
# All other request types only include headers
|
||||
headers = arguments.first
|
||||
new_headers = AwsSignatureHeaders.new(method, self.site.merge(path))
|
||||
super(method, path, headers.merge(new_headers))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
47
lib/brayniac_ai/aws_signature_headers.rb
Normal file
47
lib/brayniac_ai/aws_signature_headers.rb
Normal file
@@ -0,0 +1,47 @@
|
||||
module BrayniacAI
|
||||
class AwsSignatureHeaders < Hash
|
||||
def initialize(http_method, uri, body = "", headers = {})
|
||||
@http_method = http_method
|
||||
@uri = uri
|
||||
@body = body
|
||||
@headers = headers
|
||||
|
||||
set_headers
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :http_method, :uri, :body, :headers
|
||||
|
||||
def request_params
|
||||
{
|
||||
http_method: http_method,
|
||||
url: uri,
|
||||
body: body,
|
||||
headers: headers,
|
||||
}
|
||||
end
|
||||
|
||||
def set_headers
|
||||
# Set self to the signature headers
|
||||
signature.headers.each { |key, value| self[key] = value }
|
||||
end
|
||||
|
||||
def signature
|
||||
signer.sign_request(request_params)
|
||||
end
|
||||
|
||||
def signer
|
||||
Aws::Sigv4::Signer.new(signer_params)
|
||||
end
|
||||
|
||||
def signer_params
|
||||
{
|
||||
service: "execute-api", # TODO: can this be inferred from the URI?
|
||||
region: ENV["AWS_REGION"],
|
||||
access_key_id: ENV["AWS_ACCESS_KEY_ID"],
|
||||
secret_access_key: ENV["AWS_SECRET_ACCESS_KEY"],
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
5
lib/brayniac_ai/aws_signed_connection.rb
Normal file
5
lib/brayniac_ai/aws_signed_connection.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
module BrayniacAI
|
||||
class AwsSignedConnection < ActiveResource::Connection
|
||||
prepend AwsRequestSigning
|
||||
end
|
||||
end
|
||||
16
lib/brayniac_ai/base.rb
Normal file
16
lib/brayniac_ai/base.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
module BrayniacAI
|
||||
class Base < ActiveResource::Base
|
||||
API_ENDPOINT = ENV.fetch("BRAYNIAC_AI_API_ENDPOINT")
|
||||
|
||||
self.connection_class = AwsSignedConnection
|
||||
self.include_format_in_path = false
|
||||
self.site = API_ENDPOINT
|
||||
|
||||
|
||||
def self.enable_logging
|
||||
ActiveSupport::Notifications.subscribe('request.active_resource') do |name, start, finish, id, payload|
|
||||
puts payload
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
4
lib/brayniac_ai/collection.rb
Normal file
4
lib/brayniac_ai/collection.rb
Normal file
@@ -0,0 +1,4 @@
|
||||
module BrayniacAI
|
||||
class Collection < Base
|
||||
end
|
||||
end
|
||||
11
lib/brayniac_ai/document_analysis.rb
Normal file
11
lib/brayniac_ai/document_analysis.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
module BrayniacAI
|
||||
class DocumentAnalysis < Base
|
||||
def headshot_filename
|
||||
object_name
|
||||
end
|
||||
|
||||
def headshot_url
|
||||
"https://s3.amazonaws.com/#{bucket_name}/#{headshot_filename}"
|
||||
end
|
||||
end
|
||||
end
|
||||
4
lib/brayniac_ai/edl_parse.rb
Normal file
4
lib/brayniac_ai/edl_parse.rb
Normal file
@@ -0,0 +1,4 @@
|
||||
module BrayniacAI
|
||||
class EdlParse < Base
|
||||
end
|
||||
end
|
||||
5
lib/brayniac_ai/edl_parse_result.rb
Normal file
5
lib/brayniac_ai/edl_parse_result.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
# TODO: Use this in EdlParse
|
||||
module BrayniacAI
|
||||
class EdlParseResult < Base
|
||||
end
|
||||
end
|
||||
4
lib/brayniac_ai/facial_recognition.rb
Normal file
4
lib/brayniac_ai/facial_recognition.rb
Normal file
@@ -0,0 +1,4 @@
|
||||
module BrayniacAI
|
||||
class FacialRecognition < Base
|
||||
end
|
||||
end
|
||||
5
lib/brayniac_ai/facial_recognition_result.rb
Normal file
5
lib/brayniac_ai/facial_recognition_result.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
# TODO: Use this in FacialRecognition
|
||||
module BrayniacAI
|
||||
class FacialRecognitionResult < Base
|
||||
end
|
||||
end
|
||||
44
lib/brayniac_ai/tag.rb
Normal file
44
lib/brayniac_ai/tag.rb
Normal file
@@ -0,0 +1,44 @@
|
||||
module BrayniacAI
|
||||
class Tag < Base
|
||||
def to_a
|
||||
[faces_list, labels_list, texts_list].concat.flatten
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
CATEGORIES = %w( AgeRange Gender )
|
||||
BOOLEAN_CATEGORIES = %w( Beard Mustache Eyeglasses Sunglasses )
|
||||
|
||||
def faces_list
|
||||
[].tap do |tags|
|
||||
# Add the value of the category to the tags
|
||||
tags.append convert_category_values_to_array CATEGORIES
|
||||
|
||||
# If the value for the category is true, add that category name to the tags
|
||||
tags.append convert_category_booleans_to_array BOOLEAN_CATEGORIES
|
||||
end
|
||||
end
|
||||
|
||||
def labels_list
|
||||
labels
|
||||
end
|
||||
|
||||
def texts_list
|
||||
text.map { |tag| tag.text }.uniq
|
||||
end
|
||||
|
||||
def convert_category_values_to_array(categories)
|
||||
categories.map do |category|
|
||||
Array.wrap(faces.try(category)).map do |value|
|
||||
value.titleize
|
||||
end
|
||||
end.flatten
|
||||
end
|
||||
|
||||
def convert_category_booleans_to_array(categories)
|
||||
categories.map do |category|
|
||||
category if faces.try(category)
|
||||
end.compact
|
||||
end
|
||||
end
|
||||
end
|
||||
4
lib/brayniac_ai/validation.rb
Normal file
4
lib/brayniac_ai/validation.rb
Normal file
@@ -0,0 +1,4 @@
|
||||
module BrayniacAI
|
||||
class Validation < Base
|
||||
end
|
||||
end
|
||||
5
lib/dev_clockwork.rb
Normal file
5
lib/dev_clockwork.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
require "clockwork"
|
||||
|
||||
module Clockwork
|
||||
every(3.minutes, 'dev.poll_for_analysis_updates') { `rake dev:poll_for_analysis_updates` }
|
||||
end
|
||||
36
lib/duplicate_remover.rb
Normal file
36
lib/duplicate_remover.rb
Normal file
@@ -0,0 +1,36 @@
|
||||
# Updates an attribute for a given model to ensure it is unique
|
||||
class DuplicateRemover
|
||||
ERROR_MESSAGE_WHEN_INVALID = "has already been taken"
|
||||
|
||||
def initialize(record, attribute)
|
||||
@record = record
|
||||
@attribute = attribute
|
||||
@original_attribute_value = record.send(attribute)
|
||||
@current_index = 2
|
||||
end
|
||||
|
||||
def perform!
|
||||
while duplicate?
|
||||
record.send("#{attribute}=", new_name)
|
||||
increment_index
|
||||
end
|
||||
|
||||
record.save!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :attribute, :current_index, :original_attribute_value, :record
|
||||
|
||||
def duplicate?
|
||||
!record.valid? && record.errors[attribute].include?(ERROR_MESSAGE_WHEN_INVALID)
|
||||
end
|
||||
|
||||
def increment_index
|
||||
@current_index += 1
|
||||
end
|
||||
|
||||
def new_name
|
||||
[original_attribute_value, "(#{current_index})"].join(" ")
|
||||
end
|
||||
end
|
||||
0
lib/tasks/.keep
Normal file
0
lib/tasks/.keep
Normal file
50
lib/tasks/dev.rake
Normal file
50
lib/tasks/dev.rake
Normal file
@@ -0,0 +1,50 @@
|
||||
if Rails.env.development? || Rails.env.test? || Rails.env.review?
|
||||
require "factory_bot"
|
||||
|
||||
namespace :dev do
|
||||
desc "Sample data for local development environment"
|
||||
task :prime, [:skip_reset_db] => [:environment] do |_task, args|
|
||||
include FactoryBot::Syntax::Methods
|
||||
|
||||
Rake::Task["db:setup"].invoke unless args[:skip_reset_db].present?
|
||||
|
||||
data = {
|
||||
account_name: "Dev Account",
|
||||
account_plan: "me_suite",
|
||||
user_email: "dev@test.com",
|
||||
user_password: "password",
|
||||
}
|
||||
|
||||
# Account and Admin User
|
||||
dev_account = create(:account, name: data.fetch(:account_name), plan_uid: data.fetch(:account_plan))
|
||||
user = Oath::Services::SignUp.new(email: data.fetch(:user_email), password: data.fetch(:user_password), admin: true).perform
|
||||
|
||||
dev_account.account_auths.create(user: user, role: :account_manager)
|
||||
# Add Sample Project
|
||||
dev_account.projects << SampleProject.new
|
||||
dev_account.projects.first.save!
|
||||
|
||||
# Enable all sections for the sample project
|
||||
project = dev_account.projects.first
|
||||
project.settings(:features).update!({
|
||||
acquired_media_release: true,
|
||||
appearance_release: true,
|
||||
location_release: true,
|
||||
material_release: true,
|
||||
music_release: true,
|
||||
talent_release: true,
|
||||
video_analysis: true,
|
||||
})
|
||||
|
||||
# Add a ContractTemplate
|
||||
create(:contract_template, project: project)
|
||||
end
|
||||
|
||||
desc "Poll videos with pending analysis for updates"
|
||||
task poll_for_analysis_updates: :environment do
|
||||
puts "Polling videos with pending analysis for updates..."
|
||||
PendingAnalysis.poll
|
||||
puts "Done."
|
||||
end
|
||||
end
|
||||
end
|
||||
8
lib/tasks/scheduler.rake
Normal file
8
lib/tasks/scheduler.rake
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace :scheduler do
|
||||
desc "Expire videos which are still pending analysis after a period of time"
|
||||
task expire_videos_with_pending_analysis: :environment do
|
||||
puts "Updating videos with expired analysis..."
|
||||
PendingAnalysis.expire(1.hour.ago)
|
||||
puts "Done."
|
||||
end
|
||||
end
|
||||
20
lib/tasks/zoom.rake
Normal file
20
lib/tasks/zoom.rake
Normal file
@@ -0,0 +1,20 @@
|
||||
require 'zoom_gateway'
|
||||
namespace :zoom do
|
||||
desc "Setup necessary zoom roles and users"
|
||||
task :setup => :environment do
|
||||
zoom = Zoom.new
|
||||
|
||||
# Find or create DirectME host role
|
||||
host_role = zoom.roles_list["roles"].select{ |r| r["name"] == ZoomGateway.HOST_ROLE }.first
|
||||
if host_role.present?
|
||||
Rails.logger.info "Role #{host_role["name"]} already present."
|
||||
else
|
||||
host_role = zoom.roles_create({
|
||||
name: ZoomGateway.HOST_ROLE,
|
||||
description: "Directme meetings host",
|
||||
privileges: %w(Role:Read)
|
||||
})
|
||||
Rails.logger.info "Created role #{ZoomGateway.HOST_ROLE}."
|
||||
end
|
||||
end
|
||||
end
|
||||
104
lib/zoom_gateway.rb
Normal file
104
lib/zoom_gateway.rb
Normal file
@@ -0,0 +1,104 @@
|
||||
class ZoomGateway
|
||||
class AuthenticationError < StandardError; end
|
||||
class MeetingExpired < StandardError; end
|
||||
class UserNotFound < StandardError; end
|
||||
class TooManyHosts < StandardError; end
|
||||
|
||||
class << self
|
||||
def USER_TYPE_NAME
|
||||
default = 'basic'
|
||||
env_name = ENV['ZOOM_USER_TYPE'] || default
|
||||
%w[pro basic].include?(env_name) ? env_name : default
|
||||
end
|
||||
|
||||
def USER_TYPE
|
||||
self.USER_TYPE_NAME == 'pro' ? 2 : 1
|
||||
end
|
||||
|
||||
def PRO_USERS_LIMIT
|
||||
(ENV['ZOOM_PRO_USERS_LIMIT'] || 3).to_i
|
||||
end
|
||||
|
||||
def HOST_ROLE
|
||||
"#{self.USER_TYPE_NAME}-directme-host"
|
||||
end
|
||||
|
||||
def enable_recordings?
|
||||
ENV['ZOOM_ENABLE_RECORDINGS'] == '1'
|
||||
end
|
||||
|
||||
def apply_limits?
|
||||
self.USER_TYPE_NAME == 'pro'
|
||||
end
|
||||
end
|
||||
|
||||
def initialize
|
||||
@client = Zoom.new
|
||||
end
|
||||
|
||||
def create_meeting(host_id, **kwargs)
|
||||
recording_type = self.class.enable_recordings? ? 'cloud' : 'none'
|
||||
meeting = @client.meeting_create({ user_id: host_id,
|
||||
topic: kwargs[:topic],
|
||||
type: 1, # Instant meeting
|
||||
settings: {
|
||||
host_video: true,
|
||||
participant_video: true,
|
||||
auto_recording: recording_type,
|
||||
} })
|
||||
meeting["id"]
|
||||
end
|
||||
|
||||
def find_meeting(meeting_id)
|
||||
meeting = @client.meeting_get(meeting_id: meeting_id)
|
||||
HashWithIndifferentAccess.new(meeting)
|
||||
rescue Zoom::APIError => e
|
||||
parse_zoom_error(e)
|
||||
end
|
||||
|
||||
def create_host(host_email)
|
||||
# Find role
|
||||
host_role = @client.roles_list["roles"].try(:select) { |r| r["name"] == self.class.HOST_ROLE }.try(:first)
|
||||
raise StandardError.new("Zoom host role #{self.class.HOST_ROLE} does not exist, try running rails zoom:setup.") unless host_role.present?
|
||||
|
||||
if self.class.apply_limits?
|
||||
hosts_count = @client.roles_members(role_id: host_role["id"]).try(:[], 'total_records').try(:to_i)
|
||||
raise TooManyHosts, 'The limit of hosts has been reached' if hosts_count >= self.class.PRO_USERS_LIMIT
|
||||
end
|
||||
|
||||
# Create new user
|
||||
host_user = @client.user_create({
|
||||
action: "custCreate",
|
||||
email: host_email,
|
||||
type: self.class.USER_TYPE
|
||||
})
|
||||
|
||||
# Assign role to user
|
||||
@client.roles_assign role_id: host_role["id"], members: [{id: host_user["id"]}]
|
||||
|
||||
# Return user id
|
||||
host_user["id"]
|
||||
end
|
||||
|
||||
def delete_host(host_id)
|
||||
@client.user_delete(id: host_id)
|
||||
end
|
||||
|
||||
def delete_recording(meeting_id, recording_id)
|
||||
@client.recording_delete(meeting_id: meeting_id, recording_id: recording_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_zoom_error(error)
|
||||
if error.status_code == 104
|
||||
raise AuthenticationError, error.message
|
||||
elsif error.status_code == 1001
|
||||
raise UserNotFound, error.message
|
||||
elsif error.status_code == 3001
|
||||
raise MeetingExpired, error.message
|
||||
else
|
||||
raise error
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user