Initial commit

This commit is contained in:
Senad Uka
2020-05-31 22:38:19 +02:00
commit 858fafc3c5
1280 changed files with 65918 additions and 0 deletions

0
lib/assets/.keep Normal file
View File

View 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;
};

View 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
View 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"

View File

@@ -0,0 +1,4 @@
module BrayniacAI
class AudioRecognition < Base
end
end

View 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

View 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

View File

@@ -0,0 +1,5 @@
module BrayniacAI
class AwsSignedConnection < ActiveResource::Connection
prepend AwsRequestSigning
end
end

16
lib/brayniac_ai/base.rb Normal file
View 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

View File

@@ -0,0 +1,4 @@
module BrayniacAI
class Collection < Base
end
end

View 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

View File

@@ -0,0 +1,4 @@
module BrayniacAI
class EdlParse < Base
end
end

View File

@@ -0,0 +1,5 @@
# TODO: Use this in EdlParse
module BrayniacAI
class EdlParseResult < Base
end
end

View File

@@ -0,0 +1,4 @@
module BrayniacAI
class FacialRecognition < Base
end
end

View 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
View 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

View File

@@ -0,0 +1,4 @@
module BrayniacAI
class Validation < Base
end
end

5
lib/dev_clockwork.rb Normal file
View 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
View 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
View File

50
lib/tasks/dev.rake Normal file
View 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
View 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
View 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
View 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