merging jam-ruby into ruby

This commit is contained in:
Seth Call 2013-09-15 18:04:03 +00:00
commit 6ea4bae05c
134 changed files with 8542 additions and 0 deletions

22
ruby/.gitignore vendored Normal file
View File

@ -0,0 +1,22 @@
*.gem
*.rbc
.bundle
.config
.yardoc
Gemfile.lock
InstalledFiles
_yardoc
coverage
doc/
lib/bundler/man
pkg
rdoc
spec/reports
test/tmp
test/version_tmp
tmp
.idea
*~
*.swp
*.iml

1
ruby/.pg_migrate Normal file
View File

@ -0,0 +1 @@
up.connopts=dbname:jam_ruby

2
ruby/.rspec Normal file
View File

@ -0,0 +1,2 @@
--color
--format progress

1
ruby/.ruby-gemset Normal file
View File

@ -0,0 +1 @@
jamruby

1
ruby/.ruby-version Normal file
View File

@ -0,0 +1 @@
ruby-2.0.0-p247

45
ruby/Gemfile Normal file
View File

@ -0,0 +1,45 @@
#ruby=1.9.3
source 'http://rubygems.org'
unless ENV["LOCAL_DEV"] == "1"
source 'https://jamjam:blueberryjam@int.jamkazam.com/gems/'
end
# Look for $WORKSPACE, otherwise use "workspace" as dev path.
workspace = ENV["WORKSPACE"] || "~/workspace"
devenv = ENV["BUILD_NUMBER"].nil? # Jenkins sets a build number environment variable
gem 'pg', '0.15.1', :platform => [:mri, :mswin, :mingw]
gem 'jdbc_postgres', :platform => [:jruby]
gem 'activerecord', '3.2.13'
gem 'uuidtools', '2.1.2'
gem 'bcrypt-ruby', '3.0.1'
gem 'ruby-protocol-buffers', '1.2.2'
gem 'eventmachine', '1.0.3'
gem 'amqp', '1.0.2'
gem 'will_paginate'
gem 'actionmailer', '3.2.13'
gem 'sendgrid'
gem 'aws-sdk', '1.8.0'
gem 'carrierwave'
gem 'aasm', '3.0.16'
gem 'devise', '>= 1.1.2'
gem 'postgres-copy'
if devenv
gem 'jam_db', :path=> "#{workspace}/jam-db/target/ruby_package"
gem 'jampb', :path => "#{workspace}/jam-pb/target/ruby/jampb"
else
gem 'jam_db'
gem 'jampb'
end
group :test do
gem "factory_girl", '4.1.0'
gem "rspec", "2.10.0"
gem 'spork', '0.9.0'
gem 'database_cleaner', '0.7.0'
end
# Specify your gem's dependencies in jam_ruby.gemspec
gemspec

22
ruby/LICENSE Normal file
View File

@ -0,0 +1,22 @@
Copyright (c) 2012 Seth Call
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

8
ruby/README.md Normal file
View File

@ -0,0 +1,8 @@
# JamRuby
## Environment
Create development database 'jam_ruby'
`createdb jam_ruby`
Once you've created your database, migrate it:
`bundle exec jam_ruby up`

2
ruby/Rakefile Normal file
View File

@ -0,0 +1,2 @@
#!/usr/bin/env rake
require "bundler/gem_tasks"

97
ruby/bin/mix_cron.rb Executable file
View File

@ -0,0 +1,97 @@
# Ensure the cron is using the proper version of ruby, and simply run this with:
# ruby mix_cron.rb
#
require 'faraday'
require 'json'
require 'tempfile'
MIXER_EXECUTABLE = "/usr/local/bin/audiomixerapp"
S3CMD = "s3cmd"
# FIXME: This should probably come from an environments file or something
BASE_URL = "http://www.jamkazam.com"
# This must be present on requests from the cron to prevent hackers from
# hitting these routes.
CRON_TOKEN = "2kkl39sjjf3ijdsflje2923j"
AUDIOMIXER_LOG_FILE = "/var/log/audiomixer"
MIX_CRON_WATCH_FILE = "/var/run/mix_cron"
# Don't do anything if the cron is arleady running. There's a theoretical race
# condition here, but it should never actually be a problem because the cron should
# only run every minute or two.
if File.exist?(MIX_CRON_WATCH_FILE)
psret = `ps axuw | grep mix_cron.rb | grep ruby`
unless psret.empty?
puts "Cron still running"
exit
end
end
`touch #{MIX_CRON_WATCH_FILE}`
# Get the next manifest to mix
response = Faraday.get "#{BASE_URL}/mixes/next", :token => CRON_TOKEN
if response.status > 299
puts "Error response getting next mix: #{response.status}, #{response.body}"
do_exit
end
# This just means no mixes available.
if response.status == 204
do_exit
end
if response.status != 200
puts "Unexpected response received: #{response.status}, #{response.body}"
do_exit
end
json = JSON.parse(response.body)
# This needs to download all the vorbis files, mix and then upload
# the finished one, and tell the server about that.
json['manifest']['files'].map! do |file|
file['filename'] = Dir::Tmpname.make_tmpname ['/tmp/', '.ogg'], nil
file_response = Faraday.get file.url
if file_response.status != 200
puts "Error downloading url: #{file.url}"
do_exit
end
File.open(file['filename'], 'wb') { |fp| fp.write(file_response.body) }
end
output_filename = "/tmp/mixout-#{json['id']}.ogg"
IO.popen("#{MIXER_EXECUTABLE} #{output_filename} vorbis >>& #{AUDIOMIXER_LOG_FILE}", "w") do |f|
f.puts JSON.generate(json)
f.close
end
# First maybe make sure the length is reasonable or something? I bet sox can check that (duration i mean).
# FIXME?: Need to check that the put succeeded before carrying on. Probably can use the exit code or some such.
# Or maybe just do an ls to sanity check it.
`#{S3CMD} -P put #{output_filename} #{json['destination']}`
finish_response = Faraday.put "#{BASE_URL}/mixes/finish", :token => CRON_TOKEN, :id => json['id']
if finish_response.status != 204
puts "Error calling finish on server for mix_id #{json['id']}: #{finish_response.status}, #{finish_response.body}"
do_exit
end
puts "Mix complete and uploaded to: #{json['destination']}"
do_exit
def do_exit
`rm -f #{MIX_CRON_WATCH_FILE}`
end

18
ruby/build Executable file
View File

@ -0,0 +1,18 @@
#!/bin/bash
echo "updating dependencies"
bundle install --path vendor/bundle --local
bundle update
echo "running rspec tests"
bundle exec rspec
if [ "$?" = "0" ]; then
echo "tests completed"
else
echo "tests failed."
exit 1
fi
echo "build complete"

11
ruby/config/aws.yml Normal file
View File

@ -0,0 +1,11 @@
development:
access_key_id: AKIAIFFBNBRQG5YQ5WHA
secret_access_key: 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
test:
access_key_id: AKIAIFFBNBRQG5YQ5WHA
secret_access_key: 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
production:
access_key_id: AKIAIFFBNBRQG5YQ5WHA
secret_access_key: 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

9
ruby/config/database.yml Normal file
View File

@ -0,0 +1,9 @@
test:
adapter: postgresql
database: jam_ruby_test
host: localhost
pool: 3
username: postgres
password: postgres
timeout: 2000
encoding: unicode

202
ruby/config/profanity.yml Normal file
View File

@ -0,0 +1,202 @@
# Note: I got this from here: https://github.com/intridea/profanity_filter/blob/master/config/dictionary.yml
# I doubt that this list can be copyrighted
# the filter currently checks for words that are 3 or more characters.
---
ass: "*ss"
asses: "*ss*s"
asshole: "*ssh*l*"
assholes: "*ssh*l*s"
bastard: b*st*rd
beastial: b**st**l
beastiality: b**st**l*ty
beastility: b**st*l*ty
bestial: b*st**l
bestiality: b*st**l*ty
bitch: b*tch
bitcher: b*tch*r
bitchers: b*tch*rs
bitches: b*tch*s
bitchin: b*tch*n
bitching: b*tch*ng
blowjob: bl*wj*b
blowjobs: bl*wj*bs
bullshit: b*llsh*t
clit: cl*t
cock: c*ck
cocks: c*cks
cocksuck: c*cks*ck
cocksucked: c*cks*ck*d
cocksucker: c*cks*ck*r
cocksucking: c*cks*ck*ng
cocksucks: c*cks*cks
cum: c*m
cummer: c*mm*r
cumming: c*mm*ng
cums: c*ms
cumshot: c*msh*t
cunillingus: c*n*ll*ng*s
cunnilingus: c*nn*l*ng*s
cunt: c*nt
cuntlick: c*ntl*ck
cuntlicker: c*ntl*ck*r
cuntlicking: c*ntl*ck*ng
cunts: c*nts
cyberfuc: cyb*rf*c
cyberfuck: cyb*rf*ck
cyberfucked: cyb*rf*ck*d
cyberfucker: cyb*rf*ck*r
cyberfuckers: cyb*rf*ck*rs
cyberfucking: cyb*rf*ck*ng
damn: d*mn
dildo: d*ld*
dildos: d*ld*s
dick: d*ck
dink: d*nk
dinks: d*nks
ejaculate: "*j*c*l*t*"
ejaculated: "*j*c*l*t*d"
ejaculates: "*j*c*l*t*s"
ejaculating: "*j*c*l*t*ng"
ejaculatings: "*j*c*l*t*ngs"
ejaculation: "*j*c*l*t**n"
fag: f*g
fagging: f*gg*ng
faggot: f*gg*t
faggs: f*ggs
fagot: f*g*t
fagots: f*g*ts
fags: f*gs
fart: f*rt
farted: f*rt*d
farting: f*rt*ng
fartings: f*rt*ngs
farts: f*rts
farty: f*rty
felatio: f*l*t**
fellatio: f*ll*t**
fingerfuck: f*ng*rf*ck
fingerfucked: f*ng*rf*ck*d
fingerfucker: f*ng*rf*ck*r
fingerfuckers: f*ng*rf*ck*rs
fingerfucking: f*ng*rf*ck*ng
fingerfucks: f*ng*rf*cks
fistfuck: f*stf*ck
fistfucked: f*stf*ck*d
fistfucker: f*stf*ck*r
fistfuckers: f*stf*ck*rs
fistfucking: f*stf*ck*ng
fistfuckings: f*stf*ck*ngs
fistfucks: f*stf*cks
fuck: f*ck
fucked: f*ck*d
fucker: f*ck*r
fuckers: f*ck*rs
fuckin: f*ck*n
fucking: f*ck*ng
fuckings: f*ck*ngs
fuckme: f*ckm*
fucks: f*cks
fuk: f*k
fuks: f*ks
gangbang: g*ngb*ng
gangbanged: g*ngb*ng*d
gangbangs: g*ngb*ngs
gaysex: g*ys*x
goddamn: g*dd*mn
hardcoresex: h*rdc*r*s*x
hell: h*ll
horniest: h*rn**st
horny: h*rny
hotsex: h*ts*x
jism: j*sm
jiz: j*z
jizm: j*zm
kock: k*ck
kondum: k*nd*m
kondums: k*nd*ms
kum: k*m
kumer: k*mm*r
kummer: k*mm*r
kumming: k*mm*ng
kums: k*ms
kunilingus: k*n*l*ng*s
lust: l*st
lusting: l*st*ng
mothafuck: m*th*f*ck
mothafucka: m*th*f*ck*
mothafuckas: m*th*f*ck*s
mothafuckaz: m*th*f*ck*z
mothafucked: m*th*f*ck*d
mothafucker: m*th*f*ck*r
mothafuckers: m*th*f*ck*rs
mothafuckin: m*th*f*ck*n
mothafucking: m*th*f*ck*ng
mothafuckings: m*th*f*ck*ngs
mothafucks: m*th*f*cks
motherfuck: m*th*rf*ck
motherfucked: m*th*rf*ck*d
motherfucker: m*th*rf*ck*r
motherfuckers: m*th*rf*ck*rs
motherfuckin: m*th*rf*ck*n
motherfucking: m*th*rf*ck*ng
motherfuckings: m*th*rf*ck*ngs
motherfucks: m*th*rf*cks
niger: n*gg*r
nigger: n*gg*r
niggers: n*gg*rs
orgasim: "*rg*s*m"
orgasims: "*rg*s*ms"
orgasm: "*rg*sm"
orgasms: "*rg*sms"
phonesex: ph*n*s*x
phuk: ph*k
phuked: ph*k*d
phuking: ph*k*ng
phukked: ph*kk*d
phukking: ph*kk*ng
phuks: ph*ks
phuq: ph*q
pis: p*ss
piss: p*ss
pisser: p*ss*r
pissed: p*ss*d
pisser: p*ss*r
pissers: p*ss*rs
pises: p*ss*s
pisses: p*ss*s
pisin: p*ss*n
pissin: p*ss*n
pising: p*ss*ng
pissing: p*ss*ng
pisof: p*ss*ff
pissoff: p*ss*ff
porn: p*rn
porno: p*rn*
pornography: p*rn*gr*phy
pornos: p*rn*s
prick: pr*ck
pricks: pr*cks
pussies: p*ss**s
pusies: p*ss**s
pussy: p*ssy
pusy: p*ssy
pussys: p*ssys
pusys: p*ssys
shit: sh*t
shited: sh*t*d
shitfull: sh*tf*ll
shiting: sh*t*ng
shitings: sh*t*ngs
shits: sh*ts
shitted: sh*tt*d
shitter: sh*tt*r
shitters: sh*tt*rs
shitting: sh*tt*ng
shittings: sh*tt*ngs
shitty: sh*tty
shity: sh*tty
slut: sl*t
sluts: sl*ts
smut: sm*t
spunk: sp*nk
twat: tw*t

17
ruby/jam_ruby.gemspec Normal file
View File

@ -0,0 +1,17 @@
# -*- encoding: utf-8 -*-
require File.expand_path('../lib/jam_ruby/version', __FILE__)
Gem::Specification.new do |gem|
gem.authors = ["Seth Call"]
gem.email = ["seth@jamkazam.com"]
gem.description = %q{Common library for JamKazam Ruby code}
gem.summary = %q{Common library for JamKazam Ruby code}
gem.homepage = ""
gem.files = `git ls-files`.split($\)
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
gem.name = "jam_ruby"
gem.require_paths = ["lib"]
gem.version = JamRuby::VERSION
end

40
ruby/jenkins Executable file
View File

@ -0,0 +1,40 @@
#!/bin/bash
GEM_SERVER=http://localhost:9000/gems
echo "starting build..."
./build
if [ "$?" = "0" ]; then
echo "build succeeded"
# generate gem version based on jenkins build number
if [ -z $BUILD_NUMBER ]; then
BUILD_NUMBER="1"
fi
VERSION="0.0.${BUILD_NUMBER}"
echo "packaging gem jam_ruby-$VERSION"
cat > lib/jam_ruby/version.rb << EOF
module JamRuby
VERSION = "$VERSION"
end
EOF
gem build jam_ruby.gemspec
GEMNAME="jam_ruby-${VERSION}.gem"
echo "publishing gem"
curl -f -T $GEMNAME $GEM_SERVER/$GEMNAME
if [ "$?" != "0" ]; then
echo "publish failed"
exit 1
fi
echo "done publishing gems"
else
echo "build failed"
exit 1
fi

83
ruby/lib/jam_ruby.rb Executable file
View File

@ -0,0 +1,83 @@
require "pg"
require "active_record"
require "carrierwave"
require "carrierwave/orm/activerecord"
require "jampb"
require "uuidtools"
require "logging"
require "will_paginate"
require "will_paginate/active_record"
require "action_mailer"
require "devise"
require "sendgrid"
require 'postgres-copy'
require "jam_ruby/constants/limits"
require "jam_ruby/constants/notification_types"
require "jam_ruby/constants/validation_messages"
require "jam_ruby/errors/permission_error"
require "jam_ruby/errors/state_error"
require "jam_ruby/errors/jam_argument_error"
require "jam_ruby/mq_router"
require "jam_ruby/base_manager"
require "jam_ruby/connection_manager"
require "jam_ruby/version"
require "jam_ruby/environment"
require "jam_ruby/init"
require "jam_ruby/app/mailers/user_mailer"
require "jam_ruby/app/mailers/invited_user_mailer"
require "jam_ruby/app/mailers/corp_mailer"
require "jam_ruby/app/uploaders/artifact_uploader"
require "jam_ruby/app/uploaders/perf_data_uploader"
require "jam_ruby/lib/desk_multipass"
require "jam_ruby/lib/s3_util"
require "jam_ruby/lib/s3_manager"
require "jam_ruby/lib/profanity"
require "jam_ruby/amqp/amqp_connection_manager"
require "jam_ruby/message_factory"
require "jam_ruby/models/feedback"
require "jam_ruby/models/feedback_observer"
require "jam_ruby/models/max_mind_geo"
require "jam_ruby/models/max_mind_isp"
require "jam_ruby/models/genre"
require "jam_ruby/models/user"
require "jam_ruby/models/user_observer"
require "jam_ruby/models/user_authorization"
require "jam_ruby/models/join_request"
require "jam_ruby/models/band"
require "jam_ruby/models/invited_user"
require "jam_ruby/models/invited_user_observer"
require "jam_ruby/models/artifact_update"
require "jam_ruby/models/band_invitation"
require "jam_ruby/models/band_liker"
require "jam_ruby/models/band_follower"
require "jam_ruby/models/band_following"
require "jam_ruby/models/band_musician"
require "jam_ruby/models/connection"
require "jam_ruby/models/friendship"
require "jam_ruby/models/music_session"
require "jam_ruby/models/music_session_history"
require "jam_ruby/models/music_session_user_history"
require "jam_ruby/models/music_session_perf_data"
require "jam_ruby/models/invitation"
require "jam_ruby/models/fan_invitation"
require "jam_ruby/models/friend_request"
require "jam_ruby/models/instrument"
require "jam_ruby/models/musician_instrument"
require "jam_ruby/models/notification"
require "jam_ruby/models/track"
require "jam_ruby/models/user_liker"
require "jam_ruby/models/user_like"
require "jam_ruby/models/user_follower"
require "jam_ruby/models/user_following"
require "jam_ruby/models/search"
require "jam_ruby/models/recording"
require "jam_ruby/models/recorded_track"
require "jam_ruby/models/mix"
require "jam_ruby/models/claimed_recording"
require "jam_ruby/models/crash_dump"
include Jampb
module JamRuby
end

View File

@ -0,0 +1,101 @@
module JamRuby
# The purpose of this class is to handle reconnect logic and 'recover' logic (which means automatically resubscribe to topics/queues).
# It's 'leaky' in that it will give you a AMQP::Channel to do these subscriptions yourself in the block you pass to connect.
# Use the *connected* property to check if the connection is currently up.
class AmqpConnectionManager
attr_accessor :should_reconnect, :reconnect_interval, :connection, :connect, :connect_options, :connect_block, :channel,
:connected
def initialize(should_reconnect, reconnect_interval, connect_options = {})
@should_reconnect = should_reconnect
@reconnect_interval = reconnect_interval
@connect_options = connect_options
@connected = false
@log = Logging.logger[self]
end
# the block you pass in will be passed a channel upon successful connect. You need
#
def connect(&block)
@connect = true # indicate that we should be connected
@connect_block = block
try_connect
end
def try_connect
@connection = AMQP.connect(@connect_options, &method(:successful_connect))
@connection.on_tcp_connection_failure(&method(:on_tcp_connection_failure))
@connection.on_tcp_connection_loss(&method(:on_tcp_connection_loss))
@connection.on_recovery(&method(:on_recovery))
@connection.on_error(&method(:on_error))
end
def successful_connect(connection)
@log.debug "connected to #{@connect_options}"
@connected = true
@channel = AMQP::Channel.new(connection)
@channel.auto_recovery = true
unless @connect_block.nil?
@connect_block.call(@channel)
end
end
def on_tcp_connection_failure(settings)
@connected = false
if @connect && @should_reconnect
@log.warn "[network failure] Trying to connect in 4 seconds to #{@connect_options}"
EventMachine.add_timer(@reconnect_interval, &method(:try_connect))
end
end
def on_tcp_connection_loss(conn, settings)
@connected = false
if @connect && @should_reconnect
@log.warn "[network failure] Trying to reconnect..."
conn.reconnect(false, @reconnect_interval)
end
end
def on_recovery(conn, settings)
@connected = true
@log.debug "reconnected #{conn} #{settings}"
#puts "#channel before #{@channel}"
#puts "recovered channel: #{@channel.reuse}"
end
def disconnect
@connect = false # indicate that we should no longer be connected
unless @connection.nil?
if @connection.connected?
@connection.disconnect do
@connected = false
@log.debug "disconnected"
end
end
end
end
def on_error(connection, connection_close)
@log.error "Handling a connection-level exception."
@log.error "AMQP class id : #{connection_close.class_id}"
@log.error "AMQP method id: #{connection_close.method_id}"
@log.error "Status code : #{connection_close.reply_code}"
@log.error "Error message : #{connection_close.reply_text}"
end
def connected?
return @connected
end
end
end

View File

@ -0,0 +1,35 @@
module JamRuby
# CorpMail must be configured to work
# Some common configs occur in jam_ruby/init.rb
# Environment specific configs occur in spec_helper.rb in jam-ruby and jam-web (to put it into test mode),
# and in config/initializers/email.rb in rails to configure sendmail account settings
# If UserMailer were to be used in another project, it would need to be configured there, as well.
# Templates for UserMailer can be found in jam_ruby/app/views/jam_ruby/user_mailer
class CorpMailer < ActionMailer::Base
include SendGrid
layout "user_mailer"
DEFAULT_SENDER = "noreply@jamkazam.com"
default :from => DEFAULT_SENDER
sendgrid_category :use_subject_lines
#sendgrid_enable :opentrack, :clicktrack # this makes our emails creepy, imo (seth)
sendgrid_unique_args :env => Environment.mode
def feedback(feedback)
@email = feedback.email
@body = feedback.body
sendgrid_category "Corporate"
sendgrid_unique_args :type => "feedback"
mail(:to => "info@jamkazam.com", :subject => "Feedback received from #{@email} ") do |format|
format.text
format.html
end
end
end
end

View File

@ -0,0 +1,54 @@
module JamRuby
# InvitedUserMailer must be configured to work
# Some common configs occur in jam_ruby/init.rb
# Environment specific configs occur in spec_helper.rb in jam-ruby and jam-web (to put it into test mode),
# and in config/initializers/email.rb in rails to configure sendmail account settings
# If InvitedUserMailer were to be used in another project, it would need to be configured there, as well.
# Templates for InvitedUserMailer can be found in jam_ruby/app/views/jam_ruby/user_mailer
class InvitedUserMailer < ActionMailer::Base
include SendGrid
DEFAULT_SENDER = "support@jamkazam.com"
default :from => DEFAULT_SENDER
sendgrid_category :use_subject_lines
#sendgrid_enable :opentrack, :clicktrack # this makes our emails creepy, imo (seth)
sendgrid_unique_args :env => Environment.mode
# sent in the case of a general 'service invitation', from no one person in particular
def welcome_betauser(invited_user)
@signup_url = generate_signup_url(invited_user)
@suppress_user_has_account_footer = true
sendgrid_category "Welcome"
sendgrid_unique_args :type => "welcome_betauser"
mail(:to => invited_user.email, :subject => "Welcome to the JamKazam Beta release") do |format|
format.text
format.html { render :layout => "user_mailer" }
end
end
# used when sending an invitation from one user to another
def friend_invitation(invited_user)
@signup_url = generate_signup_url(invited_user)
@sender = invited_user.sender
@note = invited_user.note
@suppress_user_has_account_footer = true
sendgrid_category "Invitation"
sendgrid_unique_args :type => "friend_invitation"
mail(:to => invited_user.email, :subject => "You've been invited to JamKazam by #{invited_user.sender.name}") do |format|
format.text
format.html { render :layout => "from_user_mailer" }
end
end
def generate_signup_url(invited_user)
"http://www.jamkazam.com/signup?invitation_code=#{invited_user.invitation_code}"
end
end
end

View File

@ -0,0 +1,71 @@
module JamRuby
# UserMailer must be configured to work
# Some common configs occur in jam_ruby/init.rb
# Environment specific configs occur in spec_helper.rb in jam-ruby and jam-web (to put it into test mode),
# and in config/initializers/email.rb in rails to configure sendmail account settings
# If UserMailer were to be used in another project, it would need to be configured there, as well.
# Templates for UserMailer can be found in jam_ruby/app/views/jam_ruby/user_mailer
class UserMailer < ActionMailer::Base
include SendGrid
layout "user_mailer"
DEFAULT_SENDER = "support@jamkazam.com"
default :from => DEFAULT_SENDER
sendgrid_category :use_subject_lines
#sendgrid_enable :opentrack, :clicktrack # this makes our emails creepy, imo (seth)
sendgrid_unique_args :env => Environment.mode
def welcome_message(user, signup_confirm_url)
@user = user
@signup_confirm_url = signup_confirm_url
sendgrid_category "Welcome"
sendgrid_unique_args :type => "welcome_message"
mail(:to => user.email, :subject => "Welcome to JamKazam, #{user.first_name} ") do |format|
format.text
format.html
end
end
def password_changed(user)
@user = user
sendgrid_unique_args :type => "password_changed"
mail(:to => user.email, :subject => "JamKazam Password Changed") do |format|
format.text
format.html
end
end
def password_reset(user, password_reset_url)
@user = user
@password_reset_url = password_reset_url
sendgrid_unique_args :type => "password_reset"
mail(:to => user.email, :subject => "JamKazam Password Reset") do |format|
format.text
format.html
end
end
def updating_email(user)
@user = user
sendgrid_unique_args :type => "updating_email"
mail(:to => user.update_email, :subject => "JamKazam Email Change Confirmation") do |format|
format.text
format.html
end
end
def updated_email(user)
@user = user
sendgrid_unique_args :type => "updated_email"
mail(:to => user.email, :subject => "JamKazam Email Changed") do |format|
format.text
format.html
end
end
end
end

View File

@ -0,0 +1,59 @@
# encoding: utf-8
class ArtifactUploader < CarrierWave::Uploader::Base
# Include RMagick or MiniMagick support:
# include CarrierWave::RMagick
# include CarrierWave::MiniMagick
# Include the Sprockets helpers for Rails 3.1+ asset pipeline compatibility:
# include Sprockets::Helpers::RailsHelper
# include Sprockets::Helpers::IsolatedHelper
# Choose what kind of storage to use for this uploader:
# storage :file
# storage :fog
# Override the directory where uploaded files will be stored.
# This is a sensible default for uploaders that are meant to be mounted:
def store_dir
"artifacts/#{model.product}/#{model.version}"
end
def md5
@md5 ||= ::Digest::MD5.file(current_path).hexdigest
end
# Provide a default URL as a default if there hasn't been a file uploaded:
# def default_url
# # For Rails 3.1+ asset pipeline compatibility:
# # asset_path("fallback/" + [version_name, "default.png"].compact.join('_'))
#
# "/images/fallback/" + [version_name, "default.png"].compact.join('_')
# end
# Process files as they are uploaded:
# process :scale => [200, 300]
#
# def scale(width, height)
# # do something
# end
# Create different versions of your uploaded files:
# version :thumb do
# process :scale => [50, 50]
# end
# Add a white list of extensions which are allowed to be uploaded.
# For images you might use something like this:
def extension_white_list
%w(exe msi dmg)
end
# Override the filename of the uploaded files:
# Avoid using model.id or version_name here, see uploader/store.rb for details.
# def filename
# "something.jpg" if original_filename
# end
end

View File

@ -0,0 +1,59 @@
# encoding: utf-8
class PerfDataUploader < CarrierWave::Uploader::Base
# Include RMagick or MiniMagick support:
# include CarrierWave::RMagick
# include CarrierWave::MiniMagick
# Include the Sprockets helpers for Rails 3.1+ asset pipeline compatibility:
# include Sprockets::Helpers::RailsHelper
# include Sprockets::Helpers::IsolatedHelper
# Choose what kind of storage to use for this uploader:
# storage :file
# storage :fog
# Override the directory where uploaded files will be stored.
# This is a sensible default for uploaders that are meant to be mounted:
def store_dir
"perf_data/#{model.id}/#{model.client_id}-#{model.created_at}"
end
def md5
@md5 ||= ::Digest::MD5.file(current_path).hexdigest
end
# Provide a default URL as a default if there hasn't been a file uploaded:
# def default_url
# # For Rails 3.1+ asset pipeline compatibility:
# # asset_path("fallback/" + [version_name, "default.png"].compact.join('_'))
#
# "/images/fallback/" + [version_name, "default.png"].compact.join('_')
# end
# Process files as they are uploaded:
# process :scale => [200, 300]
#
# def scale(width, height)
# # do something
# end
# Create different versions of your uploaded files:
# version :thumb do
# process :scale => [50, 50]
# end
# Add a white list of extensions which are allowed to be uploaded.
# For images you might use something like this:
def extension_white_list
%w(exe msi dmg)
end
# Override the filename of the uploaded files:
# Avoid using model.id or version_name here, see uploader/store.rb for details.
# def filename
# "something.jpg" if original_filename
# end
end

View File

@ -0,0 +1,14 @@
<html>
<body>
<h3>Feedback Received</h3>
<h4>From <%= @email %>:</h4>
<p><%= @body %></p>
<br/>
<br/>
<br/>
<p>
This email was received because someone left feedback at <a href="http://www.jamkazam.com/corp/contact">http://www.jamkazam.com/corp/contact</a>
</p>
</body>
</html>

View File

@ -0,0 +1,8 @@
Feedback Received
From <%= @email %>:
<%= @body %>
This email was received because someone left feedback at http://www.jamkazam.com/corp/contact

View File

@ -0,0 +1,12 @@
<% provide(:title, "You've been invited to JamKazam by #{@sender.name}!") %>
<% provide(:photo_url, @sender.resolved_photo_url) %>
To signup, please go to the <a href="<%= @signup_url %>">create account</a> page.
<% content_for :note do %>
<% if @note %>
<%= @sender.name %> says: <%= @note %>
<% else %>
<%= @sender.name %> would like you to join JamKazam.
<% end %>
<% end %>

View File

@ -0,0 +1,7 @@
You've been invited to JamKazam by <%= @sender.name %>!
<% unless @note.nil? %>
<%= @sender.name %> says:
<%= @note %>
<% end %>
To signup, please go to the 'create account' page: <%= @signup_url %>.

View File

@ -0,0 +1,3 @@
<% provide(:title, 'Welcome to the JamKazam Beta!') %>
To signup, please go to the <a href="<%= @signup_url %>">create account</a> page.

View File

@ -0,0 +1,2 @@
Welcome to the JamKazam Beta!
To signup, please go to the 'create account' page: <%= @signup_url %>.

View File

@ -0,0 +1,3 @@
<% provide(:title, 'Jamkazam Password Changed') %>
You just changed your password at Jamkazam.

View File

@ -0,0 +1 @@
You just changed your password!

View File

@ -0,0 +1,3 @@
<% provide(:title, 'Jamkazam Password Reset') %>
Visit this link so that you can change your Jamkazam password: <a href="<%= @password_reset_url %>">reset password</a>.

View File

@ -0,0 +1 @@
Visit this link so that you can change your Jamkazam password: <a href="<%= @password_reset_url %>">Reset Password</a>.

View File

@ -0,0 +1,3 @@
<% provide(:title, 'Jamkazam Email Confirmed') %>
<b><%= @user.email %></b> has been confirmed as your new email address.

View File

@ -0,0 +1 @@
<%= @user.email %> has been confirmed as your new email address.

View File

@ -0,0 +1,3 @@
<% provide(:title, 'Please Confirm New Jamkazam Email') %>
Please click the following link to confirm your change in email: <a href="<%= @user.update_email_confirmation_url %>">confirm email</a>.

View File

@ -0,0 +1 @@
Please click the following link to confirm your change in email: <%= @user.update_email_confirmation_url %>.

View File

@ -0,0 +1,4 @@
<% provide(:title, 'Welcome to Jamkazam') %>
<p>Welcome to Jamkazam, <%= @user.first_name %>!</p>
<p>To confirm this email address, please go to the <a href="<%= @signup_confirm_url %>">signup confirmation page.</a>.</p>

View File

@ -0,0 +1,3 @@
Welcome to Jamkazam, <%= @user.first_name %>!
To confirm this email address, please go to the signup confirmation page at: <%= @signup_confirm_url %>.

View File

@ -0,0 +1,15 @@
Understanding Jamkazam Email Layouts
====================================
We have two types of layouts;
* a 'standard' email layout which is an email to a user (user_mailer.html.erb/text)
* a 'from a user to another user' email layout (from_user_mailer.html.erb/text)
## user_mailer.html.erb
To use the user_mailer.html.erb layout, you must provide a title section, as well as the body.
Look at 'password_changed.html.erb' for an example.
## from_user_mailer.html.erb
To use the from_user_mailer.html.erb layout, you must provide a title section, photo_url section (photo of the person who sent the email), and a note section (any personalized note that the sender may have said, or boilerplate)
Look at 'friend_invitation.html.erb' for an example.

View File

@ -0,0 +1,67 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>JamKazam</title>
<style>
p {
margin-bottom:0px;
line-height:140%;
}
</style>
</head>
<body bgcolor="#000000" style="margin-top:10px;font-family:Arial, Helvetica, sans-serif;">
<table bgcolor="#262626" width="650" align="center" cellpadding="0" cellspacing="0">
<tr>
<td><img src="http://www.jamkazam.com/assets/email/header.png" width="650" height="183" alt="JamKazam"></td>
</tr>
</table>
<table bgcolor="#262626" width="650" align="center" cellpadding="30" cellspacing="0">
<tr>
<td align="left"><h1 style="font-size:22px;font-weight:normal;margin-top:0px"><font color="#F34E1C" face="Arial, Helvetica, sans-serif"><%= yield(:title) %></font></h1>
<p><font size="3" color="#AAAAAA" face="Arial, Helvetica, sans-serif"><%= yield %></font></p>
<br>
<table>
<tr>
<td width="58" align="left" valign="top"><p style="padding:2px;width:54px;height:54px;background-color:#ed3618;-webkit-border-radius:28px;-moz-border-radius:28px;border-radius:28px;margin-right:10px"><img src="<%= yield(:photo_url) %>" width="54" height="54" style="-webkit-border-radius:26px;-moz-border-radius:26px;border-radius:26px;"></p></td>
<td><p><font size="3" color="#AAAAAA" face="Arial, Helvetica, sans-serif"><%= yield(:note) %></font></p>
</td></tr></table>
</td>
</tr>
<% unless @suppress_user_has_account_footer == true %>
<tr>
<td>
<table bgcolor="#21474C" cellpadding="10" cellspacing="0">
<tr>
<td align="left">
<!-- CALL OUT BOX -->
</font></p>
<p style="margin-top:0px"><font size="2" color="#7FACBA" face="Arial, Helvetica, sans-serif">This email was sent to you because you have an account at <a href="http://www.jamkazam.com">Jamkazam</a>.
</td></tr></table>
</td>
</tr>
<% end %>
</table>
<table align="center" width="650" cellpadding="10" bgcolor="#156572" cellspacing="0">
<tr>
<td align="center"><font size="1" color="#ffffff" face="Arial, Helvetica, sans-serif">Copyright &copy; <%= Time.now.year %> JamKazam, Inc. All rights reserved.</font>
</td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,8 @@
<%= yield %>
<% unless @suppress_user_has_account_footer == true %>
This email was sent to you because you have an account at Jamkazam / http://www.jamkazam.com.
<% end %>
Copyright <%= Time.now.year %> JamKazam, Inc. All rights reserved.

View File

@ -0,0 +1,59 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>JamKazam</title>
<style>
p {
margin-bottom:0px;
line-height:140%;
}
</style>
</head>
<body bgcolor="#000000" style="margin-top:10px;font-family:Arial, Helvetica, sans-serif;">
<table bgcolor="#262626" width="650" align="center" cellpadding="0" cellspacing="0">
<tr>
<td><img src="http://www.jamkazam.com/assets/email/header.png" width="650" height="183" alt="JamKazam"></td>
</tr>
</table>
<table bgcolor="#262626" width="650" align="center" cellpadding="30" cellspacing="0">
<tr>
<td align="left"><h1 style="font-size:22px;font-weight:normal;margin-top:0px"><font color="#F34E1C" face="Arial, Helvetica, sans-serif"><%= yield(:title) %></font></h1>
<p><font size="3" color="#AAAAAA" face="Arial, Helvetica, sans-serif"><%= yield %></font></p>
<br>
</td>
</tr>
<% unless @suppress_user_has_account_footer == true %>
<tr>
<td>
<table bgcolor="#21474C" cellpadding="10" cellspacing="0">
<tr>
<td align="left">
<!-- CALL OUT BOX -->
</font></p>
<p style="margin-top:0px"><font size="2" color="#7FACBA" face="Arial, Helvetica, sans-serif">This email was sent to you because you have an account at <a href="http://www.jamkazam.com">Jamkazam</a>.
</td></tr></table>
</td>
</tr>
<% end %>
</table>
<table align="center" width="650" cellpadding="10" bgcolor="#156572" cellspacing="0">
<tr>
<td align="center"><font size="1" color="#ffffff" face="Arial, Helvetica, sans-serif">Copyright &copy; <%= Time.now.year %> JamKazam, Inc. All rights reserved.</font>
</td>
</tr>
</table>
</body>
</html>

View File

@ -0,0 +1,8 @@
<%= yield %>
<% unless @suppress_user_has_account_footer == true %>
This email was sent to you because you have an account at Jamkazam / http://www.jamkazam.com.
<% end %>
Copyright <%= Time.now.year %> JamKazam, Inc. All rights reserved.

View File

@ -0,0 +1,31 @@
module JamRuby
class BaseManager
attr_accessor :pg_conn
def initialize(options={})
@log = Logging.logger[self]
@pg_conn = options[:conn]
unless PG.threadsafe?
raise Exception, "a non-threadsafe build of libpq is present."
end
end
# Creates a connection manager, and associates the connection created by active_record with ourselves
def self.active_record_transaction
manager = self.new
ActiveRecord::Base.connection_pool.with_connection do |connection|
# create a transaction, and pass the current connection to ConnectionManager.
# this lets the entire operation work with the same transaction,
# across Rails ActiveRecord and the pg-gem based code in ConnectionManager.
manager.pg_conn = connection.instance_variable_get("@connection")
connection.transaction do
yield manager
end
end
end
end
end

View File

@ -0,0 +1,371 @@
module JamRuby
# All writes should occur through the ConnectionManager for the connection table
# Reads can occur freely elsewhere, though
# Because connections are tied to the websocket-connection and we bookkeep them in the database purely
# for 'SQL convenience', this is a obvious place we can go away from a database
# as an optimization if we find it's too much db traffic created'
# At a minimum, though, we could make connections an UNLOGGED table because if the database crashes,
# all clients should reconnect and restablish their connection anyway
#
# All methods in here could also be refactored as stored procedures, if we stick with a database.
# This may make sense in the short term if we are still managing connections in the database, but
# we move to the node-js in the websocket gateway (because the websocket gateway needs to call some of these methods).
# Or of course we could just port the relevant methods to node-js
#
# Also we don't send notifications from ConnectionManager;
# we just return enough data so that a caller can make the determination if it needs to
class ConnectionManager < BaseManager
def initialize(options={})
super(options)
@log = Logging.logger[self]
end
def update_staleness()
#TODO
end
##### TODO: refactored to notification.rb but left here for backwards compatibility w/ connection_manager_spec.rb
def gather_friends(connection, user_id)
friend_ids = []
connection.exec("SELECT f1.friend_id as friend_id FROM friendships f1 WHERE f1.user_id = $1 AND f1.friend_id IN (SELECT f2.user_id FROM friendships f2 WHERE f2.friend_id = $1)", [user_id]) do |friend_results|
friend_results.each do |friend_result|
friend_ids.push(friend_result['friend_id'])
end
end
return friend_ids
end
# reclaim the existing connection,
def reconnect(conn, reconnect_music_session_id)
music_session_id = nil
reconnected = false
# we will reconnect the same music_session that the connection was previously in,
# if it matches the same value currently in the database for music_session_id
music_session_id_expression = 'NULL'
unless reconnect_music_session_id.nil?
music_session_id_expression = "(CASE WHEN music_session_id='#{reconnect_music_session_id}' THEN music_session_id ELSE NULL END)"
end
sql =<<SQL
UPDATE connections SET (aasm_state, updated_at, music_session_id) = ('#{Connection::CONNECT_STATE.to_s}', NOW(), #{music_session_id_expression})
WHERE
client_id = '#{conn.client_id}'
RETURNING music_session_id
SQL
self.pg_conn.exec(sql) do |result|
if result.cmd_tuples == 1
music_session_id = result[0]['music_session_id']
end
end
# we tell the client they reconnected if they specified a reconnect music_session_id, and if that is now the
# current value in the database
reconnected = true if !reconnect_music_session_id.nil? && reconnect_music_session_id == music_session_id
return music_session_id, reconnected
end
# returns the music_session_id, if any, associated with the client
def flag_connection_stale_with_client_id(client_id)
music_session_id = nil
sql =<<SQL
UPDATE connections SET aasm_state = '#{Connection::STALE_STATE.to_s}'
WHERE
client_id = '#{client_id}' AND
aasm_state = '#{Connection::CONNECT_STATE.to_s}'
RETURNING music_session_id
SQL
# @log.info("*** flag_connection_stale_with_client_id: client_id = #{client_id}; sql = #{sql}")
self.pg_conn.exec(sql) do |result|
# if we did update a client to stale, retriee music_session_id
if result.cmd_tuples == 1
music_session_id = result[0]['music_session_id']
end
end
music_session_id
end
# flag connections as stale
def flag_stale_connections(max_seconds)
ConnectionManager.active_record_transaction do |connection_manager|
conn = connection_manager.pg_conn
sql =<<SQL
SELECT count(user_id) FROM connections
WHERE
updated_at < (NOW() - interval '#{max_seconds} second') AND
aasm_state = '#{Connection::CONNECT_STATE.to_s}'
SQL
conn.exec(sql) do |result|
count = result.getvalue(0, 0)
# @log.info("flag_stale_connections: flagging #{count} stale connections")
if 0 < count.to_i
# @log.info("flag_stale_connections: flagging #{count} stale connections")
sql =<<SQL
UPDATE connections SET aasm_state = '#{Connection::STALE_STATE.to_s}'
WHERE
updated_at < (NOW() - interval '#{max_seconds} second') AND
aasm_state = '#{Connection::CONNECT_STATE.to_s}'
SQL
conn.exec(sql)
end
end
end
end
# NOTE this is only used for testing purposes;
# actual deletes will be processed in the websocket context which cleans up dependencies
def expire_stale_connections(max_seconds)
self.stale_connection_client_ids(max_seconds).each { |cid| self.delete_connection(cid) }
end
# expiring connections in stale state, which deletes them
def stale_connection_client_ids(max_seconds)
client_ids = []
ConnectionManager.active_record_transaction do |connection_manager|
conn = connection_manager.pg_conn
sql =<<SQL
SELECT client_id, music_session_id, user_id FROM connections
WHERE
updated_at < (NOW() - interval '#{max_seconds} second') AND
aasm_state = '#{Connection::STALE_STATE.to_s}'
SQL
conn.exec(sql) do |result|
result.each { |row|
client_id = row['client_id']
music_session_id = row['music_session_id']
user_id = row['user_id']
client_ids << client_id
}
end
end
client_ids
end
# returns the number of connections that this user currently has across all clients
# this number is used by notification logic elsewhere to know
# 'oh the user joined for the 1st time, so send a friend update', or
# 'don't bother because the user has connected somewhere else already'
def create_connection(user_id, client_id, ip_address, &blk)
count = 0
ConnectionManager.active_record_transaction do |connection_manager|
conn = connection_manager.pg_conn
lock_connections(conn)
conn.exec("INSERT INTO connections (user_id, client_id, ip_address, aasm_state) VALUES ($1, $2, $3, $4)",
[user_id, client_id, ip_address, Connection::CONNECT_STATE.to_s]).clear
# we just created a new connection-if this is the first time the user has shown up, we need to send out a message to his friends
conn.exec("SELECT count(user_id) FROM connections WHERE user_id = $1", [user_id]) do |result|
count = result.getvalue(0, 0) .to_i
blk.call(conn, count) unless blk.nil?
end
return count
end
end
# once a connection is known gone (whether timeout or because a TCP connection is observed lost)
# this code is responsible for all cleanup logic associated with a connection going away
# returns how many connections are left for this user; this data is used by callers to know whether
# to tell friends if the user is offline (count==0) or not (count > 0)
# If a blk is passed in, on success, count is also passed back an the db connection, allowing for
# notifications to go out within the table log. music_session_id is also passed, if the music_session still exists
# and this connection was in a session
def delete_connection(client_id, &blk)
ConnectionManager.active_record_transaction do |connection_manager|
conn = connection_manager.pg_conn
count = 0
user_id = nil
music_session_id = nil
lock_connections(conn)
previous_music_session_id = check_already_session(conn, client_id)
conn.exec("DELETE FROM connections WHERE client_id = $1 RETURNING user_id, music_session_id", [client_id]) do |result|
if result.cmd_tuples == 0
# the client is already gone from the database... do nothing but log error
@log.warn("unable to delete client #{client_id}")
return
elsif result.cmd_tuples == 1
user_id = result[0]['user_id']
music_session_id = result[0]['music_session_id']
else
raise Exception, 'uniqueness constraint has been lost on client_id'
end
end
session_checks(conn, previous_music_session_id, user_id)
# since we did delete a row, check and see if any more connections for that user exist
# if we are down to zero, send out user gone message
conn.exec("SELECT count(user_id) FROM connections where user_id = $1", [user_id]) do |result|
count = result.getvalue(0, 0).to_i
end
# same for session-if we are down to the last participant, delete the session
unless music_session_id.nil?
result = conn.exec("DELETE FROM music_sessions WHERE id = $1 AND 0 = (select count(music_session_id) FROM connections where music_session_id = $1)", [music_session_id])
if result.cmd_tuples == 1
music_session_id = nil
end
end
blk.call(conn, count, music_session_id, user_id) unless blk.nil?
return count
end
end
def check_already_session(conn, client_id)
conn.exec("SELECT music_session_id FROM connections WHERE client_id = $1", [client_id]) do |result|
if result.num_tuples == 1
previous_music_session_id = result.getvalue(0, 0)
return previous_music_session_id
elsif result.num_tuples == 0
# there is no connection found matching this criteria; we are done.
@log.debug("when checking for existing session, no connection found with client=#{client_id}")
return nil
else
@log.error("connection table data integrity violation; multiple rows found. client_id=#{client_id}")
raise Exception, "connection table data integrity violation; multiple rows found. client_id=#{client_id}"
end
end
end
def session_checks(conn, previous_music_session_id, user_id)
unless previous_music_session_id.nil?
# TODO: send notification to friends that this user left this session?
@log.debug("user #{user_id} left music_session #{previous_music_session_id}")
# destroy the music_session if it's empty
num_participants = nil
conn.exec("SELECT count(*) FROM connections WHERE music_session_id = $1",
[previous_music_session_id]) do |result|
num_participants = result.getvalue(0, 0).to_i
end
if num_participants == 0
# delete the music_session
conn.exec("DELETE from music_sessions WHERE id = $1",
[previous_music_session_id]) do |result|
if result.cmd_tuples == 1
# music session deleted!
@log.debug("deleted music session #{previous_music_session_id}")
JamRuby::MusicSessionHistory.removed_music_session(user_id,
previous_music_session_id)
elsif 1 < result.cmd_tuples
msg = "music_sessions table data integrity violation; multiple rows found with music_session_id=#{previous_music_session_id}"
@log.error(msg)
raise Exception, msg
end
end
end
end
end
# if a blk is passed in, upon success, it will be called and you can issue notifications
# within the connection table lock
def join_music_session(user, client_id, music_session, as_musician, tracks, &blk)
connection = nil
user_id = user.id
music_session_id = music_session.id
ConnectionManager.active_record_transaction do |connection_manager|
db_conn = connection_manager.pg_conn
connection = Connection.find_by_client_id_and_user_id!(client_id, user_id)
connection.music_session_id = music_session_id
connection.as_musician = as_musician
connection.joining_session = true
associate_tracks(connection, tracks)
connection.save
if connection.errors.any?
raise ActiveRecord::Rollback
else
blk.call(db_conn, connection) unless blk.nil?
MusicSessionUserHistory.save(music_session_id, user_id, client_id)
end
end
return connection
end
# if a blk is passed in, upon success, it will be called and you can issue notifications
# within the connection table lock
def leave_music_session(user, connection, music_session, &blk)
ConnectionManager.active_record_transaction do |connection_manager|
conn = connection_manager.pg_conn
lock_connections(conn)
music_session_id = music_session.id
user_id = user.id
client_id = connection.client_id
previous_music_session_id = check_already_session(conn, client_id)
if previous_music_session_id == nil
@log.debug "the client is not in a session. user=#{user_id}, client=#{client_id}, music_session=#{music_session_id}"
raise StateError, "not in session"
elsif previous_music_session_id != music_session_id
@log.debug "the client is in a different session. user=#{user_id}, client=#{client_id}, music_session=#{music_session_id}"
raise StateError, "in a session different than that specified"
end
# can throw exception if the session is deleted just before this
conn.exec("UPDATE connections SET music_session_id = NULL, as_musician = NULL WHERE client_id = $1 AND user_id =$2", [client_id, user_id]) do |result|
if result.cmd_tuples == 1
@log.debug("disassociated music_session with connection for client_id=#{client_id}, user_id=#{user_id}")
JamRuby::MusicSessionUserHistory.removed_music_session(user_id, music_session_id)
session_checks(conn, previous_music_session_id, user_id)
blk.call() unless blk.nil?
elsif result.cmd_tuples == 0
@log.debug "leave_music_session no connection found with client_id=#{client_id}"
raise ActiveRecord::RecordNotFound
else
@log.error("database failure or logic error; this path should be impossible if the table is locked (leave_music_session)")
raise Exception, "locked table changed state"
end
end
end
end
def lock_connections(conn)
conn.exec("LOCK connections IN EXCLUSIVE MODE").clear
end
def associate_tracks(connection, tracks)
@log.debug "Tracks:"
@log.debug tracks
connection.tracks.clear()
unless tracks.nil?
tracks.each do |track|
instrument = Instrument.find(track["instrument_id"])
t = Track.new
t.instrument = instrument
t.connection = connection
t.sound = track["sound"]
t.save
connection.tracks << t
end
end
end
end
end

View File

@ -0,0 +1,18 @@
module Limits
# band genres
MIN_GENRES_PER_BAND = 1
MAX_GENRES_PER_BAND = 1
# recording genres
MIN_GENRES_PER_RECORDING = 1
MAX_GENRES_PER_RECORDING = 1
# instruments
MIN_INSTRUMENTS_PER_MUSICIAN = 1
MAX_INSTRUMENTS_PER_MUSICIAN = 5
# users
USERS_CAN_INVITE = false # in BETA release, only first level users can invite others
end

View File

@ -0,0 +1,19 @@
module NotificationTypes
# friend notifications
FRIEND_UPDATE = "FRIEND_UPDATE"
FRIEND_REQUEST = "FRIEND_REQUEST"
FRIEND_REQUEST_ACCEPTED = "FRIEND_REQUEST_ACCEPTED"
FRIEND_SESSION_JOIN = "FRIEND_SESSION_JOIN"
# session notifications
SESSION_INVITATION = "SESSION_INVITATION"
# musician notifications
MUSICIAN_SESSION_JOIN = "MUSICIAN_SESSION_JOIN"
MUSICIAN_SESSION_DEPART = "MUSICIAN_SESSION_DEPART"
# recording notifications
RECORDING_CREATED = "RECORDING_CREATED"
end

View File

@ -0,0 +1,37 @@
module ValidationMessages
# Note that these are not set up to be internationalizable
# general messages
PERMISSION_VALIDATION_ERROR = "You do not have permissions to perform this action."
USER_NOT_MUSICIAN_VALIDATION_ERROR = "You must be a Musician to perform this action."
USER_NOT_BAND_MEMBER_VALIDATION_ERROR = "You must be a band member to perform this action."
# band invitations
BAND_INVITATION_NOT_FOUND = "Band invitation not found."
# recordings
RECORDING_NOT_FOUND = "Recording not found."
# tracks
TRACK_NOT_FOUND = "Track not found."
# sessions
SESSION_NOT_FOUND = "Session not found."
# genres
GENRE_LIMIT_EXCEEDED = "No more than 1 genre is allowed."
GENRE_MINIMUM_NOT_MET = "At least 1 genre is required."
# instruments
INSTRUMENT_LIMIT_EXCEEDED = "No more than 5 instruments are allowed."
INSTRUMENT_MINIMUM_NOT_MET = "At least 1 instrument is required."
# user
OLD_PASSWORD_DOESNT_MATCH = "Your old password is incorrect."
EMAIL_NOT_FOUND = "Email address not found."
NOT_YOUR_PASSWORD = "is not your current password"
EMAIL_ALREADY_TAKEN = "has already been taken"
EMAIL_MATCHES_CURRENT = "is same as your current email"
INVALID_FPFILE = "is not valid"
end

View File

@ -0,0 +1,7 @@
module JamRuby
class DbUtil
def self.create(connection_hash)
end
end
end

View File

@ -0,0 +1,21 @@
module JamRuby
class Environment
def self.mode
if Object.const_defined?('Rails')
return Rails.env
else
# right now, there is no idea of a non-test jam-ruby usage, because it's solely a library
# this will need to change if we add executables to jam-ruby
return "test"
end
end
def self.application
if Object.const_defined?('Rails')
return 'jamweb'
else
return 'jamruby'
end
end
end
end

View File

@ -0,0 +1,7 @@
module JamRuby
# if a bad argument is supplied.
# Why not use the default ruby argument error? Using this one allows us to know our API layer threw this, versus us using some core library incorrectly
class JamArgumentError < ArgumentError
end
end

View File

@ -0,0 +1,5 @@
module JamRuby
class PermissionError < Exception
end
end

View File

@ -0,0 +1,7 @@
module JamRuby
# this exception can be thrown if the server is not in a state that allows the operation to succeed
# however, it's not necessarily a bad thing; it just means
class StateError < Exception
end
end

View File

@ -0,0 +1,3 @@
# initialize actionmailer
ActionMailer::Base.raise_delivery_errors = true
ActionMailer::Base.view_paths = File.expand_path('../../jam_ruby/app/views/', __FILE__)

View File

@ -0,0 +1,57 @@
require 'openssl'
require 'digest/sha1'
require 'base64'
require 'cgi'
require 'time'
require 'json'
module JamRuby
# Most of the code below was taken from the example located here:
# https://github.com/assistly/multipass-examples/blob/master/ruby.rb
class DeskMultipass
API_KEY = "453ddfc0bab00130a9c13bc9a68cf24c"
SITE_KEY = "jamkazam"
def initialize(user)
@user = user
generate_token_and_signature
end
def token
@token
end
def signature
@signature
end
private
def generate_token_and_signature
key = Digest::SHA1.digest(API_KEY + SITE_KEY)[0...16]
# Generate a random 16 byte IV
iv = OpenSSL::Random.random_bytes(16)
json = JSON.generate(
:uid => @user.id,
:expires => (Time.now + 300).iso8601,
:customer_name => @user.name,
:customer_email => @user.email)
cipher = OpenSSL::Cipher::Cipher.new("aes-128-cbc")
cipher.encrypt
cipher.key = key
cipher.iv = iv
encrypted = cipher.update(json) + cipher.final
prepended = iv + encrypted
token = Base64.encode64(prepended)
signature = Base64.encode64(OpenSSL::HMAC.digest('sha1', API_KEY, token))
@token = CGI.escape(token)
@signature = CGI.escape(signature)
end
end
end

View File

@ -0,0 +1,40 @@
module JamRuby
class Profanity
@@dictionary_file = File.join('config/profanity.yml')
@@dictionary = nil
def self.dictionary
if File.exists? @@dictionary_file
@@dictionary ||= YAML.load_file(@@dictionary_file)
else
@@dictionary = []
end
@@dictionary
end
def self.check_word(word)
dictionary.include?(word.downcase)
end
def self.is_profane?(text)
return false if text.nil?
text.split(/\W+/).each do |word|
return true if check_word(word)
end
return false
end
end
end
# This needs to be outside the module to work.
class NoProfanityValidator < ActiveModel::EachValidator
# implement the method called during validation
def validate_each(record, attribute, value)
record.errors[attribute] << 'Cannot contain profanity' if Profanity.is_profane?(value)
end
end

View File

@ -0,0 +1,72 @@
require 'aws-sdk'
require 'active_support/all'
module JamRuby
class S3Manager
SECRET = "krQP3fKpjAtWkApBEJwJJrCZ"
def self.s3_url(filename)
"s3://#{aws_bucket}/#{filename}"
end
def self.url(filename)
"https://s3.amazonaws.com/#{aws_bucket}/#{filename}"
end
def self.upload_sign(filename, content_md5, upload_id)
hdt = http_date_time
str_to_sign = "PUT\n#{content_md5}\n#{content_type}\n#{hdt}\n/#{aws_bucket}/#{filename}"
signature = Base64.encode64(HMAC::SHA1.digest(aws_secret_key, str_to_sign)).chomp
{ :filename => filename, :signature => signature, :datetime => hdt, :upload_id => upload_id }
end
def self.hashed_filename(type, id)
Digest::SHA1.hexdigest "#{SECRET}_#{type}_#{id}"
end
def self.multipart_upload_start(upload_filename)
return 0 if @is_unit_test
s3_bucket.objects[upload_filename].multipart_upload.id
end
def self.multipart_upload_complete(upload_id)
return if @is_unit_test
s3_bucket.objects[upload_filename].multipart_uploads[upload_id].upload_complete(:remote_parts)
end
def self.delete(filename)
return if @is_unit_test
s3_bucket.objects[filename].delete
end
def self.set_unit_test
@is_unit_test = true
end
private
def self.s3_bucket
@s3 ||= AWS::S3.new
@s3.buckets[aws_bucket]
end
def self.aws_bucket
"jamkazam-dev"
end
def self.aws_secret_key
"XLq2mpJHNyA0bN7GBSdYyF/pWjfzGkDx92b1C+Wv"
end
def self.content_type
"application/octet-stream"
end
def self.http_date_time
Time.now.strftime("%a, %d %b %Y %H:%M:%S %z")
end
end
end

View File

@ -0,0 +1,29 @@
require 'aws-sdk'
require 'active_support/all'
module JamRuby
class S3Util
@@def_opts = { :expires => 3600 * 24, :secure => true } # 24 hours from now
@@s3 = AWS::S3.new(:access_key_id => ENV['AWS_KEY'], :secret_access_key => ENV['AWS_SECRET'])
def self.sign_url(bucket, path, options = @@def_opts)
bucket_gen = @@s3.buckets[bucket]
return "#{bucket_gen.objects[path].url_for(:read, :expires => options[:expires]).to_s}"
end
def self.url(aws_bucket, filename, options = @@def_opts)
"http#{options[:secure] ? "s" : ""}://s3.amazonaws.com/#{aws_bucket}/#{filename}"
end
def self.move(aws_bucket, source, destination)
@@s3.buckets[aws_bucket].objects[source].move_to[destination]
end
def self.delete(aws_bucket, path)
@@s3.buckets[aws_bucket].objects[path].delete()
end
end
end

View File

@ -0,0 +1,231 @@
module JamRuby
# creates messages (implementation: protocol buffer) objects cleanly
class MessageFactory
CLIENT_TARGET = "client"
SERVER_TARGET = "server"
SESSION_TARGET_PREFIX = "session:"
USER_TARGET_PREFIX = "user:"
CLIENT_TARGET_PREFIX = "client:"
def initialize()
@type_values = {}
Jampb::ClientMessage::Type.constants.each do |constant|
@type_values[Jampb::ClientMessage::Type.const_get(constant)] = constant
end
end
# given a string (bytes) payload, return a client message
def parse_client_msg(payload)
return Jampb::ClientMessage.parse(payload)
end
# create a login message using user/pass
def login_with_user_pass(username, password, options = {})
login = Jampb::Login.new(:username => username, :password => password, :client_id => options[:client_id])
return Jampb::ClientMessage.new(:type => ClientMessage::Type::LOGIN, :route_to => SERVER_TARGET, :login => login)
end
# create a login message using token (a cookie or similar)
def login_with_token(token, options = {})
login = Jampb::Login.new(:token => token, :client_id => options[:client_id])
return Jampb::ClientMessage.new(:type => ClientMessage::Type::LOGIN, :route_to => SERVER_TARGET, :login => login)
end
# create a login ack (login was successful)
def login_ack(public_ip, client_id, token, heartbeat_interval, music_session_id, reconnected)
login_ack = Jampb::LoginAck.new(:public_ip => public_ip, :client_id => client_id, :token => token, :heartbeat_interval => heartbeat_interval, :music_session_id => music_session_id, :reconnected => reconnected)
return Jampb::ClientMessage.new(:type => ClientMessage::Type::LOGIN_ACK, :route_to => CLIENT_TARGET, :login_ack => login_ack)
end
# create a music session login message
def login_music_session(music_session)
login_music_session = Jampb::LoginMusicSession.new(:music_session => music_session)
return Jampb::ClientMessage.new(:type => ClientMessage::Type::LOGIN_MUSIC_SESSION, :route_to => SERVER_TARGET, :login_music_session => login_music_session)
end
# create a music session login message ack (success or on failure)
def login_music_session_ack(error, error_reason)
login_music_session_ack = Jampb::LoginMusicSessionAck.new(:error => error, :error_reason => error_reason)
return Jampb::ClientMessage.new(:type => ClientMessage::Type::LOGIN_MUSIC_SESSION_ACK, :route_to => CLIENT_TARGET, :login_music_session_ack => login_music_session_ack)
end
# create a music session 'leave session' message
def leave_music_session(music_session)
leave_music_session = Jampb::LeaveMusicSession.new(:music_session => music_session)
return Jampb::ClientMessage.new(:type => ClientMessage::Type::LEAVE_MUSIC_SESSION, :route_to => SERVER_TARGET, :leave_music_session => leave_music_session)
end
# create a music session leave message ack (success or on failure)
def leave_music_session_ack(error, error_reason)
leave_music_session_ack = Jampb::LeaveMusicSessionAck.new(:error => error, :error_reason => error_reason)
return Jampb::ClientMessage.new(:type => ClientMessage::Type::LEAVE_MUSIC_SESSION_ACK, :route_to => CLIENT_TARGET, :leave_music_session_ack => leave_music_session_ack)
end
# create a server bad state recovered msg
def server_bad_state_recovered(original_message_id)
recovered = Jampb::ServerBadStateRecovered.new()
return Jampb::ClientMessage.new(:type => ClientMessage::Type::SERVER_BAD_STATE_RECOVERED, :route_to => CLIENT_TARGET, :server_bad_state_recovered => recovered, :in_reply_to => original_message_id)
end
# create a server error
def server_generic_error(error_msg)
error = Jampb::ServerGenericError.new(:error_msg => error_msg)
return Jampb::ClientMessage.new(:type => ClientMessage::Type::SERVER_GENERIC_ERROR, :route_to => CLIENT_TARGET, :server_generic_error => error)
end
# create a server rejection error
def server_rejection_error(error_msg)
error = Jampb::ServerRejectionError.new(:error_msg => error_msg)
return Jampb::ClientMessage.new(:type => ClientMessage::Type::SERVER_REJECTION_ERROR, :route_to => CLIENT_TARGET, :server_rejection_error => error)
end
# create a server rejection error
def server_permission_error(original_message_id, error_msg)
error = Jampb::ServerPermissionError.new(:error_msg => error_msg)
return Jampb::ClientMessage.new(:type => ClientMessage::Type::SERVER_PERMISSION_ERROR, :route_to => CLIENT_TARGET, :server_permission_error => error, :in_reply_to => original_message_id)
end
# create a server bad state error
def server_bad_state_error(original_message_id, error_msg)
error = Jampb::ServerBadStateError.new(:error_msg => error_msg)
return Jampb::ClientMessage.new(:type => ClientMessage::Type::SERVER_BAD_STATE_ERROR, :route_to => CLIENT_TARGET, :server_bad_state_error => error, :in_reply_to => original_message_id)
end
# create a friend joined session message
def friend_session_join(session_id, user_id, username, photo_url)
join = Jampb::FriendSessionJoin.new(:session_id => session_id, :user_id => user_id, :username => username, :photo_url => photo_url)
return Jampb::ClientMessage.new(:type => ClientMessage::Type::FRIEND_SESSION_JOIN, :route_to => CLIENT_TARGET, :friend_session_join => join)
end
# create a musician joined session message
def musician_session_join(session_id, user_id, username, photo_url)
join = Jampb::MusicianSessionJoin.new(:session_id => session_id, :user_id => user_id, :username => username, :photo_url => photo_url)
return Jampb::ClientMessage.new(:type => ClientMessage::Type::MUSICIAN_SESSION_JOIN, :route_to => CLIENT_TARGET, :musician_session_join => join)
end
# create a musician left session message
def musician_session_depart(session_id, user_id, username, photo_url)
left = Jampb::MusicianSessionDepart.new(:session_id => session_id, :user_id => user_id, :username => username, :photo_url => photo_url)
return Jampb::ClientMessage.new(:type => ClientMessage::Type::MUSICIAN_SESSION_DEPART, :route_to => CLIENT_TARGET, :musician_session_depart => left)
end
# create a musician fresh session message
def musician_session_fresh(session_id, user_id, username, photo_url)
fresh = Jampb::MusicianSessionFresh.new(:session_id => session_id, :user_id => user_id, :username => username, :photo_url => photo_url)
return Jampb::ClientMessage.new(:type => ClientMessage::Type::MUSICIAN_SESSION_FRESH, :route_to => CLIENT_TARGET, :musician_session_fresh => fresh)
end
# create a musician stale session message
def musician_session_stale(session_id, user_id, username, photo_url)
stale = Jampb::MusicianSessionStale.new(:session_id => session_id, :user_id => user_id, :username => username, :photo_url => photo_url)
return Jampb::ClientMessage.new(:type => ClientMessage::Type::MUSICIAN_SESSION_STALE, :route_to => CLIENT_TARGET, :musician_session_stale => stale)
end
# create a user-joined session message
def join_request(session_id, join_request_id, username, text)
join_request = Jampb::JoinRequest.new(:join_request_id => join_request_id, :username => username, :text => text)
return Jampb::ClientMessage.new(:type => ClientMessage::Type::JOIN_REQUEST, :route_to => SESSION_TARGET_PREFIX + session_id, :join_request => join_request)
end
# create a test message to send in session
def test_session_message(session_id, msg)
test = Jampb::TestSessionMessage.new(:msg => msg)
return Jampb::ClientMessage.new(:type => ClientMessage::Type::TEST_SESSION_MESSAGE, :route_to => SESSION_TARGET_PREFIX + session_id, :test_session_message => test)
end
def session_invitation(receiver_id, invitation_id)
session_invitation = Jampb::SessionInvitation.new(:invitation => invitation_id)
return Jampb::ClientMessage.new(:type => ClientMessage::Type::SESSION_INVITATION, :route_to => USER_TARGET_PREFIX + receiver_id, :session_invitation => session_invitation)
end
# create a friend update message
def friend_update(user_id, name, photo_url, online, msg)
friend = Jampb::FriendUpdate.new(:user_id => user_id, :name => name, :photo_url => photo_url, :online => online, :msg => msg)
return Jampb::ClientMessage.new(:type => ClientMessage::Type::FRIEND_UPDATE, :route_to => USER_TARGET_PREFIX + user_id, :friend_update => friend)
end
# create a friend request message
def friend_request(friend_request_id, user_id, name, photo_url, friend_id, msg, notification_id, created_at)
friend_request = Jampb::FriendRequest.new(:friend_request_id => friend_request_id,
:user_id => user_id, :name => name, :photo_url => photo_url, :friend_id => friend_id, :msg => msg,
:notification_id => notification_id, :created_at => created_at)
return Jampb::ClientMessage.new(:type => ClientMessage::Type::FRIEND_REQUEST, :route_to => USER_TARGET_PREFIX + friend_id, :friend_request => friend_request)
end
# create a friend request acceptance message
def friend_request_accepted(friend_id, name, photo_url, user_id, msg, notification_id, created_at)
friend_request_accepted = Jampb::FriendRequestAccepted.new(:friend_id => friend_id,
:name => name, :photo_url => photo_url, :user_id => user_id, :msg => msg,
:notification_id => notification_id, :created_at => created_at)
return Jampb::ClientMessage.new(:type => ClientMessage::Type::FRIEND_REQUEST_ACCEPTED, :route_to => USER_TARGET_PREFIX + user_id, :friend_request_accepted => friend_request_accepted)
end
############## P2P CLIENT MESSAGES #################
# send a request to do a ping
def ping_request(client_id, from)
ping_request = Jampb::PingRequest.new()
return Jampb::ClientMessage.new(:type => ClientMessage::Type::PING_REQUEST, :route_to => CLIENT_TARGET_PREFIX + client_id, :from => from, :ping_request => ping_request)
end
# respond to a ping_request with an ack
def ping_ack(client_id, from)
ping_ack = Jampb::PingAck.new()
return Jampb::ClientMessage.new(:type => ClientMessage::Type::PING_ACK, :route_to => CLIENT_TARGET_PREFIX + client_id, :from => from, :ping_ack => ping_ack)
end
# create a test message to send in session
def test_client_message(client_id, from, msg)
test = Jampb::TestClientMessage.new(:msg => msg)
return Jampb::ClientMessage.new(:type => ClientMessage::Type::TEST_CLIENT_MESSAGE, :route_to => CLIENT_TARGET_PREFIX + client_id, :from => from, :test_client_message => test)
end
####################################################
# create a heartbeat
def heartbeat()
heartbeat = Jampb::Heartbeat.new
return Jampb::ClientMessage.new(:type => ClientMessage::Type::HEARTBEAT, :route_to => SERVER_TARGET, :heartbeat => heartbeat)
end
# create a heartbeat ack
def heartbeat_ack()
heartbeat_ack = Jampb::HeartbeatAck.new()
return Jampb::ClientMessage.new(:type => ClientMessage::Type::HEARTBEAT_ACK, :route_to => CLIENT_TARGET, :heartbeat_ack => heartbeat_ack)
end
# is this message directed to the server?
def server_directed? msg
return msg.route_to == MessageFactory::SERVER_TARGET
end
# is this message directed to the client?
def client_directed? msg
return msg.route_to.start_with? MessageFactory::CLIENT_TARGET_PREFIX
end
# is this message directed to a (music) session?
def session_directed? msg
return msg.route_to.start_with? MessageFactory::SESSION_TARGET_PREFIX
end
# is this message directed to a user?
def user_directed? msg
return msg.route_to.start_with? MessageFactory::USER_TARGET_PREFIX
end
def extract_session(msg)
return msg.route_to[MessageFactory::SESSION_TARGET_PREFIX..-1]
end
def get_message_type msg
return @type_values[msg.type]
end
end
end

View File

@ -0,0 +1,29 @@
module JamRuby
class ArtifactUpdate < ActiveRecord::Base
DEFAULT_ENVIRONMENT = 'public'
PRODUCTS = ['JamClient/Win32', 'JamClient/MacOSX']
self.primary_key = 'id'
attr_accessible :version, :uri, :sha1, :environment, :product
mount_uploader :uri, ArtifactUploader
validates :version, :presence => true
validates :uri, :presence => true
validates :sha1, :presence => true
validates :size, :presence => true
validates :environment, :presence => true
validates :product, :inclusion => {:in => PRODUCTS}
before_validation do
if uri.present? && uri_changed?
self.size = uri.file.size
self.sha1 = Digest::MD5.hexdigest(File.read(uri.current_path))
end
end
end
end

View File

@ -0,0 +1,215 @@
module JamRuby
class Band < ActiveRecord::Base
attr_accessible :name, :website, :biography, :city, :state, :country
self.primary_key = 'id'
validates :biography, no_profanity: true
# musicians
has_many :band_musicians, :class_name => "JamRuby::BandMusician"
has_many :users, :through => :band_musicians, :class_name => "JamRuby::User"
# genres
has_and_belongs_to_many :genres, :class_name => "JamRuby::Genre", :join_table => "bands_genres"
# recordings
has_many :recordings, :class_name => "JamRuby::Recording", :foreign_key => "band_id"
# likers
has_many :likers, :class_name => "JamRuby::BandLiker", :foreign_key => "band_id", :inverse_of => :band
has_many :inverse_likers, :through => :likers, :class_name => "JamRuby::User", :foreign_key => "liker_id"
# followers
has_many :band_followers, :class_name => "JamRuby::BandFollower", :foreign_key => "band_id"
has_many :followers, :through => :band_followers, :class_name => "JamRuby::User"
has_many :inverse_band_followers, :through => :followers, :class_name => "JamRuby::BandFollower", :foreign_key => "follower_id"
has_many :inverse_followers, :through => :inverse_band_followers, :source => :band, :class_name => "JamRuby::Band"
# invitations
has_many :invitations, :inverse_of => :band, :class_name => "JamRuby::BandInvitation", :foreign_key => "band_id"
# music_sessions
has_many :music_sessions, :class_name => "JamRuby::MusicSession", :foreign_key => "band_id"
has_many :music_session_history, :class_name => "JamRuby::MusicSessionHistory", :foreign_key => "band_id", :inverse_of => :band
def liker_count
return self.likers.size
end
def follower_count
return self.followers.size
end
def recording_count
return self.recordings.size
end
def session_count
return self.music_sessions.size
end
def location
loc = self.city.blank? ? '' : self.city
loc = loc.blank? ? self.state : "#{loc}, #{self.state}" unless self.state.blank?
#loc = loc.blank? ? self.country : "#{loc}, #{self.country}" unless self.country.blank?
loc
end
def add_member(user_id, admin)
BandMusician.create(:band_id => self.id, :user_id => user_id, :admin => admin)
end
def self.recording_index(current_user, band_id)
hide_private = false
band = Band.find(band_id)
# hide private Recordings from anyone who's not in the Band
unless band.users.exists? current_user
hide_private = true
end
if hide_private
recordings = Recording.joins(:band_recordings)
.where(:bands_recordings => {:band_id => "#{band_id}"}, :public => true)
else
recordings = Recording.joins(:band_recordings)
.where(:bands_recordings => {:band_id => "#{band_id}"})
end
return recordings
end
def self.search(query, options = { :limit => 10 })
# only issue search if at least 2 characters are specified
if query.nil? || query.length < 2
return []
end
# create 'anded' statement
query = Search.create_tsquery(query)
if query.nil? || query.length == 0
return []
end
return Band.where("name_tsv @@ to_tsquery('jamenglish', ?)", query).limit(options[:limit])
end
# helper method for creating / updating a Band
def self.save(id, name, website, biography, city, state, country, genres, user_id, photo_url, logo_url)
user = User.find(user_id)
# new band
if id.nil?
# ensure person creating this Band is a Musician
unless user.musician?
raise JamRuby::PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR
end
validate_genres(genres, false)
band = Band.new()
# band update
else
validate_genres(genres, true)
band = Band.find(id)
# ensure user updating Band details is a Band member
unless band.users.exists? user
raise PermissionError, ValidationMessages::USER_NOT_BAND_MEMBER_VALIDATION_ERROR
end
end
# name
unless name.nil?
band.name = name
end
# website
unless website.nil?
band.website = website
end
# biography
unless biography.nil?
band.biography = biography
end
# city
unless city.nil?
band.city = city
end
# state
unless state.nil?
band.state = state
end
# country
unless country.nil?
band.country = country
end
# genres
unless genres.nil?
ActiveRecord::Base.transaction do
# delete all genres for this band first
unless band.id.nil? || band.id.length == 0
band.genres.delete_all
end
# loop through each genre in the array and save to the db
genres.each do |genre_id|
g = Genre.find(genre_id)
band.genres << g
end
end
end
# photo url
unless photo_url.nil?
band.photo_url = photo_url
end
# logo url
unless logo_url.nil?
band.logo_url = logo_url
end
band.updated_at = Time.now.getutc
band.save
# add the creator as the admin
if id.nil?
BandMusician.create(:band_id => band.id, :user_id => user_id, :admin => true)
end
return band
end
private
def self.validate_genres(genres, is_nil_ok)
if is_nil_ok && genres.nil?
return
end
if genres.nil?
raise JamRuby::JamArgumentError, ValidationMessages::GENRE_MINIMUM_NOT_MET
else
if genres.size < Limits::MIN_GENRES_PER_BAND
raise JamRuby::JamArgumentError, ValidationMessages::GENRE_MINIMUM_NOT_MET
end
if genres.size > Limits::MAX_GENRES_PER_BAND
raise JamRuby::JamArgumentError, ValidationMessages::GENRE_LIMIT_EXCEEDED
end
end
end
end
end

View File

@ -0,0 +1,11 @@
module JamRuby
class BandFollower < ActiveRecord::Base
self.table_name = "bands_followers"
self.primary_key = 'id'
belongs_to :band, :class_name => "JamRuby::Band", :foreign_key => "band_id"
belongs_to :follower, :class_name => "JamRuby::User", :foreign_key => "follower_id"
end
end

View File

@ -0,0 +1,11 @@
module JamRuby
class BandFollowing < ActiveRecord::Base
self.table_name = "bands_followers"
self.primary_key = 'id'
belongs_to :user, :class_name => "JamRuby::User", :foreign_key => "follower_id", :inverse_of => :inverse_band_followings
belongs_to :band_following, :class_name => "JamRuby::Band", :foreign_key => "band_id", :inverse_of => :band_followings
end
end

View File

@ -0,0 +1,56 @@
module JamRuby
class BandInvitation < ActiveRecord::Base
self.table_name = "band_invitations"
self.primary_key = 'id'
BAND_INVITATION_FAN_RECIPIENT_ERROR = "A Band invitation can only be sent to a Musician."
belongs_to :receiver, :inverse_of => :received_band_invitations, :foreign_key => "user_id", :class_name => "JamRuby::User"
belongs_to :sender, :inverse_of => :sent_band_invitations, :foreign_key => "creator_id", :class_name => "JamRuby::User"
belongs_to :band, :inverse_of => :invitations, :foreign_key => "band_id", :class_name => "JamRuby::Band"
def self.save(id, band_id, user_id, creator_id, accepted)
band_invitation = BandInvitation.new()
ActiveRecord::Base.transaction do
# ensure certain fields are only updated on creation
if id.nil?
# ensure recipient is a Musician
user = User.find(user_id)
unless user.musician?
raise JamRuby::JamArgumentError, BAND_INVITATION_FAN_RECIPIENT_ERROR
end
band_invitation.band_id = band_id
band_invitation.user_id = user_id
band_invitation.creator_id = creator_id
# only the accepted flag can be updated after initial creation
else
band_invitation = BandInvitation.find(id)
band_invitation.accepted = accepted
end
band_invitation.updated_at = Time.now.getutc
band_invitation.save
# accept logic => (1) auto-friend each band member and (2) add the musician to the band
if accepted
band_musicians = BandMusician.where(:band_id => band_invitation.band.id)
unless band_musicians.nil?
band_musicians.each do |bm|
Friendship.save(band_invitation.receiver.id, bm.user_id)
end
end
# accepting an invitation adds the musician to the band
BandMusician.create(:band_id => band_invitation.band.id, :user_id => band_invitation.receiver.id, :admin => false)
end
end
return band_invitation
end
end
end

View File

@ -0,0 +1,11 @@
module JamRuby
class BandLiker < ActiveRecord::Base
self.table_name = "bands_likers"
self.primary_key = 'id'
belongs_to :band, :class_name => "JamRuby::Band", :foreign_key => "band_id", :inverse_of => :likers
belongs_to :user, :class_name => "JamRuby::User", :foreign_key => "liker_id", :inverse_of => :band_likes
end
end

View File

@ -0,0 +1,31 @@
module JamRuby
class BandMusician < ActiveRecord::Base
self.table_name = "bands_musicians"
attr_accessible :band_id, :user_id, :admin
self.primary_key = 'id'
belongs_to :user
belongs_to :band
# name, genres, photo_url, and logo_url are needed here for the RABL file
def name
@name = self.band.name
end
def genres
@genres = self.band.genres
end
def photo_url
@photo_url = self.band.photo_url
end
def logo_url
@logo_url = self.band.logo_url
end
end
end

View File

@ -0,0 +1,38 @@
module JamRuby
class ClaimedRecording < ActiveRecord::Base
validates :name, no_profanity: true
belongs_to :recording, :class_name => "JamRuby::Recording", :inverse_of => :claimed_recordings
belongs_to :user, :class_name => "JamRuby::User", :inverse_of => :claimed_recordings
belongs_to :genre, :class_name => "JamRuby::Genre"
has_many :recorded_tracks, :through => :recording, :class_name => "JamRuby::RecordedTrack"
# user must own this object
# params is a hash, and everything is optional
def update_fields(user, params)
if user != self.user
raise PermissionError, "user doesn't own claimed_recording"
end
self.name = params[:name] unless params[:name].nil?
self.genre = Genre.find(params[:genre]) unless params[:genre].nil?
self.is_public = params[:is_public] unless params[:is_public].nil?
self.is_downloadable = params[:is_downloadable] unless params[:is_downloadable].nil?
save
end
def discard(user)
if user != self.user
raise PermissionError, "user doesn't own claimed_recording"
end
# If this is the only copy, destroy the entire recording. Otherwise, just destroy this claimed_recording
if recording.claimed_recordings.count == 1
recording.discard
else
self.destroy
end
end
end
end

View File

@ -0,0 +1,123 @@
require 'aasm'
module JamRuby
class Connection < ActiveRecord::Base
SELECT_AT_LEAST_ONE = "Please select at least one track"
FAN_CAN_NOT_JOIN_AS_MUSICIAN = "A fan can not join a music session as a musician"
MUSIC_SESSION_MUST_BE_SPECIFIED = "A music session must be specified"
INVITE_REQUIRED = "You must be invited to join this session"
FANS_CAN_NOT_JOIN = "Fans can not join this session"
attr_accessor :joining_session
self.primary_key = 'id'
belongs_to :user, :class_name => "JamRuby::User"
belongs_to :music_session, :class_name => "JamRuby::MusicSession"
has_many :tracks, :class_name => "JamRuby::Track", :inverse_of => :connection
validates :as_musician, :inclusion => {:in => [true, false]}
validate :can_join_music_session, :if => :joining_session?
after_save :require_at_least_one_track_when_in_session, :if => :joining_session?
include AASM
IDLE_STATE = :idle
CONNECT_STATE = :connected
STALE_STATE = :stale
EXPIRED_STATE = :expired
aasm do
state IDLE_STATE, :initial => true
state CONNECT_STATE
state STALE_STATE
state EXPIRED_STATE
event :connect do
transitions :from => IDLE_STATE, :to => CONNECT_STATE
transitions :from => STALE_STATE, :to => CONNECT_STATE
end
event :stale do
transitions :from => CONNECT_STATE, :to => STALE_STATE
transitions :from => IDLE_STATE, :to => STALE_STATE
end
event :expire, :after => :did_expire do
transitions :from => CONNECT_STATE, :to => EXPIRED_STATE
transitions :from => STALE_STATE, :to => EXPIRED_STATE
transitions :from => IDLE_STATE, :to => EXPIRED_STATE
end
end
def state_message
case self.aasm_state.to_sym
when CONNECT_STATE
'Connected'
when STALE_STATE
'Stale'
else
'Idle'
end
end
def did_expire
self.destroy
end
def joining_session?
return joining_session
end
def can_join_music_session
if music_session.nil?
errors.add(:music_session, MUSIC_SESSION_MUST_BE_SPECIFIED)
return false
end
if as_musician
unless self.user.musician
errors.add(:as_musician, FAN_CAN_NOT_JOIN_AS_MUSICIAN)
return false
end
if music_session.musician_access
if music_session.approval_required
unless music_session.creator == user || music_session.invited_musicians.exists?(user)
errors.add(:approval_required, INVITE_REQUIRED)
return false
end
end
else
unless music_session.creator == user || music_session.invited_musicians.exists?(user)
errors.add(:musician_access, INVITE_REQUIRED)
return false
end
end
else
unless self.music_session.fan_access
# it's someone joining as a fan, and the only way a fan can join is if fan_access is true
errors.add(:fan_access, FANS_CAN_NOT_JOIN)
return false
end
end
return true
end
# decides if a given user can access this client with p2p messaging
# the answer is yes if the user is in the same music session
def access_p2p?(user)
return self.music_session.users.exists?(user)
end
private
def require_at_least_one_track_when_in_session
if tracks.count == 0
errors.add(:genres, SELECT_AT_LEAST_ONE)
end
end
end
end

View File

@ -0,0 +1,27 @@
module JamRuby
class CrashDump < ActiveRecord::Base
self.table_name = "crash_dumps"
self.primary_key = 'id'
belongs_to :user, :inverse_of => :crash_dumps, :class_name => "JamRuby::User"
validates :client_type, presence: true
validates :client_version, presence: true
attr_accessor :user_email
before_validation(:on => :create) do
self.created_at ||= Time.now
self.id = SecureRandom.uuid
self.uri = "dump/#{self.id}-#{self.created_at.to_i}"
end
def user_email
nil if user_id.nil?
self.user.email
end
end
end

View File

@ -0,0 +1,32 @@
module JamRuby
class FanInvitation < ActiveRecord::Base
FRIENDSHIP_REQUIRED_VALIDATION_ERROR = "You can only invite friends"
MEMBERSHIP_REQUIRED_OF_MUSIC_SESSION = "You must be a member of the music session to send invitations on behalf of it"
self.primary_key = 'id'
belongs_to :sender, :inverse_of => :sent_fan_invitations, :class_name => "JamRuby::User", :foreign_key => "sender_id"
belongs_to :receiver, :inverse_of => :received_fan_invitations, :class_name => "JamRuby::User", :foreign_key => "receiver_id"
belongs_to :music_session, :inverse_of => :fan_invitations, :class_name => "JamRuby::MusicSession"
validates :sender, :presence => true
validates :receiver, :presence => true
validates :music_session, :presence => true
validate :require_sender_in_music_session, :require_are_friends
private
def require_sender_in_music_session
unless music_session.users.exists? sender
errors.add(:music_session, MEMBERSHIP_REQUIRED_OF_MUSIC_SESSION)
end
end
def require_are_friends
unless receiver.friends.exists? sender
errors.add(:receiver, FRIENDSHIP_REQUIRED_VALIDATION_ERROR)
end
end
end
end

View File

@ -0,0 +1,18 @@
module JamRuby
class Feedback
include ActiveModel::Validations
include ActiveModel::Validations::Callbacks
include ActiveModel::Observing
extend ActiveModel::Callbacks
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, format: {with: VALID_EMAIL_REGEX}
validates :body, :presence => true
attr_accessor :email, :body
def save
return valid?
end
end
end

View File

@ -0,0 +1,10 @@
module JamRuby
class FeedbackObserver < ActiveRecord::Observer
observe JamRuby::Feedback
def after_validation(feedback)
CorpMailer.feedback(feedback).deliver unless feedback.errors.any?
end
end
end

View File

@ -0,0 +1,71 @@
module JamRuby
class FriendRequest < ActiveRecord::Base
self.primary_key = 'id'
STATUS = %w(accept block spam ignore)
belongs_to :user, :class_name => "JamRuby::User"
belongs_to :friend, :class_name => "JamRuby::User"
validates :user_id, :presence => true
validates :friend_id, :presence => true
#validates :status, :inclusion => {:in => STATUS}
validates :message, no_profanity: true
def to_s
return "#{self.id} => #{self.user.to_s}:#{self.friend.to_s}"
end
def self.save(id, user_id, friend_id, status, message)
# new friend request
if id.nil?
friend_request = FriendRequest.new()
friend_request = validate_friend_request(friend_request, user_id, friend_id)
friend_request.user_id = user_id
friend_request.friend_id = friend_id
friend_request.message = message
friend_request.save
# send notification
Notification.send_friend_request(friend_request.id, user_id, friend_id)
else
ActiveRecord::Base.transaction do
friend_request = FriendRequest.find(id)
friend_request.status = status
friend_request.updated_at = Time.now.getutc
friend_request.save
# create both records for this friendship
if friend_request.status == "accept"
Friendship.save(friend_request.user_id, friend_request.friend_id)
# send notification
Notification.send_friend_request_accepted(friend_request.user_id, friend_request.friend_id)
end
end
end
return friend_request
end
private
def self.validate_friend_request(friend_request, user_id, friend_id)
friend_requests = FriendRequest.where("user_id='#{user_id}' AND friend_id='#{friend_id}'")
# check if there are any friend requests for this source/target user combo, and if
# any have been marked as spam or blocked, set the status of this friend request
# to match so it doesn't show up in the queue
unless friend_requests.nil? || friend_requests.size == 0
if friend_requests.exists?(:status => "spam")
friend_request.status = "spam"
elsif friend_requests.exists?(:status => "block")
friend_request.status = "block"
end
end
return friend_request
end
end
end

View File

@ -0,0 +1,64 @@
module JamRuby
class Friendship < ActiveRecord::Base
attr_accessible :user_id, :friend_id
self.primary_key = 'id'
belongs_to :user, :class_name => "JamRuby::User", :foreign_key => "user_id", :inverse_of => :inverse_friendships
belongs_to :friend, :class_name => "JamRuby::User", :foreign_key => "friend_id", :inverse_of => :friendships
def self.save(user_id, friend_id)
friendship = Friendship.where("user_id='#{user_id}' AND friend_id='#{friend_id}'")
if friendship.nil? || friendship.size == 0
Friendship.create(:user_id => user_id, :friend_id => friend_id)
Friendship.create(:user_id => friend_id, :friend_id => user_id)
end
end
# not like .save() in that it does not check for an existing friendship. The caller is responsible
# for checking for errors on the models
def self.save_using_models(user, friend)
this = Friendship.new
this.user = user
this.friend = friend
that = Friendship.new
that.user = friend
that.friend = user
this.save
that.save
return [this, that]
end
def self.search(query, user_id, options = { :limit => 10 })
# only issue search if at least 2 characters are specified
if query.nil? || query.length < 2 || user_id.nil?
return []
end
# create 'anded' statement
query = Search.create_tsquery(query)
if query.nil? || query.length == 0
return []
end
friends = Friendship.joins(
%Q{
INNER JOIN
users
ON friendships.friend_id = users.id
WHERE friendships.user_id = '#{user_id}'
AND users.name_tsv @@ to_tsquery('jamenglish', '#{query}')
}
)
friends = friends.limit(options[:limit])
return friends
end
end
end

View File

@ -0,0 +1,16 @@
module JamRuby
class Genre < ActiveRecord::Base
self.primary_key = 'id'
# bands
has_and_belongs_to_many :bands, :class_name => "JamRuby::Band", :join_table => "bands_genres"
# genres
has_and_belongs_to_many :recordings, :class_name => "JamRuby::Recording", :join_table => "recordings_genres"
# music sessions
has_and_belongs_to_many :music_sessions, :class_name => "JamRuby::MusicSession", :join_table => "genres_music_sessions"
end
end

View File

@ -0,0 +1,20 @@
module JamRuby
class Instrument < ActiveRecord::Base
self.primary_key = 'id'
# users
has_many :musician_instruments, :class_name => "JamRuby::MusicianInstrument"
has_many :users, :through => :musician_instruments, :class_name => "JamRuby::User"
has_many :tracks, :class_name => "JamRuby::Track", :inverse_of => :instrument
has_many :recorded_tracks, :class_name => "JamRuby::RecordedTrack", :inverse_of => :instrument
# music sessions
has_and_belongs_to_many :music_sessions, :class_name => "JamRuby::MusicSession", :join_table => "genres_music_sessions"
def self.standard_list
return Instrument.where('instruments.popularity > 0').order('instruments.popularity DESC, instruments.description ASC')
end
end
end

View File

@ -0,0 +1,39 @@
module JamRuby
class Invitation < ActiveRecord::Base
FRIENDSHIP_REQUIRED_VALIDATION_ERROR = "You can only invite friends"
MEMBERSHIP_REQUIRED_OF_MUSIC_SESSION = "You must be a member of the music session to send invitations on behalf of it"
JOIN_REQUEST_IS_NOT_FOR_RECEIVER_AND_MUSIC_SESSION = "You can only associate a join request with an invitation if that join request comes from the invited user and if it's for the same music session"
self.primary_key = 'id'
belongs_to :sender, :inverse_of => :sent_invitations, :class_name => "JamRuby::User", :foreign_key => "sender_id"
belongs_to :receiver, :inverse_of => :received_invitations, :class_name => "JamRuby::User", :foreign_key => "receiver_id"
belongs_to :music_session, :inverse_of => :invitations, :class_name => "JamRuby::MusicSession"
belongs_to :join_request, :inverse_of => :invitations, :class_name => "JamRuby::JoinRequest"
validates :sender, :presence => true
validates :receiver, :presence => true
validates :music_session, :presence => true
validate :require_sender_in_music_session, :require_are_friends_or_requested_to_join
private
def require_sender_in_music_session
unless music_session.users.exists? sender
errors.add(:music_session, MEMBERSHIP_REQUIRED_OF_MUSIC_SESSION)
end
end
def require_are_friends_or_requested_to_join
if !join_request.nil? && (join_request.user != receiver || join_request.music_session != music_session)
errors.add(:join_request, JOIN_REQUEST_IS_NOT_FOR_RECEIVER_AND_MUSIC_SESSION )
elsif join_request.nil?
# we only check for friendship requirement if this was not in response to a join_request
unless receiver.friends.exists? sender
errors.add(:receiver, FRIENDSHIP_REQUIRED_VALIDATION_ERROR)
end
end
end
end
end

View File

@ -0,0 +1,66 @@
module JamRuby
class InvitedUser < ActiveRecord::Base
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
attr_accessible :email, :sender_id, :autofriend, :note
attr_accessor :accepted_twice
self.primary_key = 'id'
### Who sent this invitatio?
# either admin_sender or user_sender is not null. If an administrator sends the invitation, then
belongs_to :sender , :inverse_of => :invited_users, :class_name => "JamRuby::User", :foreign_key => "sender_id"
# who is the invitation sent to?
validates :email, :presence => true, format: {with: VALID_EMAIL_REGEX}
validates :autofriend, :inclusion => {:in => [nil, true, false]}
validates :invitation_code, :presence => true
validates :note, length: {maximum: 400}, no_profanity: true # 400 == arbitrary.
validate :valid_personalized_invitation
validate :not_accepted_twice
validate :can_invite?
# ensure invitation code is always created
before_validation(:on => :create) do
self.invitation_code = SecureRandom.urlsafe_base64 if self.invitation_code.nil?
self.sender_id = nil if self.sender_id.blank? # this coercion was done just to make activeadmin work
end
def self.index(user)
return InvitedUser.where(:sender_id => user).order(:updated_at)
end
def sender_display_name
return sender.name
end
def accept!
if self.accepted
accepted_twice = true
end
self.accepted = true
end
def invited_by_administrator?
sender.nil? || sender.admin # a nil sender can only be created by someone using jam-admin
end
private
def can_invite?
errors.add(:sender, "can not invite others") if !invited_by_administrator? && !sender.can_invite?
end
def valid_personalized_invitation
errors.add(:autofriend, "must be true if sender is specified") if autofriend && sender.nil?
end
def not_accepted_twice
errors.add(:accepted, "you can only accept an invitation once") if accepted_twice
end
end
end

View File

@ -0,0 +1,14 @@
module JamRuby
class InvitedUserObserver < ActiveRecord::Observer
observe JamRuby::InvitedUser
def after_create(invited_user)
if invited_user.sender.nil?
InvitedUserMailer.welcome_betauser(invited_user).deliver
else
InvitedUserMailer.friend_invitation(invited_user).deliver
end
end
end
end

View File

@ -0,0 +1,44 @@
module JamRuby
class JoinRequest < ActiveRecord::Base
REQUESTOR_MUST_BE_A_MUSICIAN = "requestor must be a musician"
self.primary_key = 'id'
belongs_to :user, :class_name => "JamRuby::User"
belongs_to :music_session, :class_name => "JamRuby::MusicSession"
has_many :invitations, :inverse_of => :join_request, :class_name => "JamRuby::Invitation"
validates :user, :presence => true
validates :music_session, :presence => true
validates :text, presence: false, no_profanity: true, length: {maximum: 140} # arbitrary decision of 140. the database is at 2000 max on this field
validates_uniqueness_of :user_id, :scope => :music_session_id
validate :requestor_is_musician
# list all paginations for the current user
def self.index(current_user)
# TODO pagination
return JoinRequest.where("join_requests.user_id = '#{current_user.id}'").order('join_requests.created_at DESC')
end
def requestor_is_musician
unless user.musician?
errors.add(:user, REQUESTOR_MUST_BE_A_MUSICIAN)
end
end
def to_s
return "#{self.user.to_s}:#{self.music_session.to_s}"
end
# permissions:
# only the creator of the join request can do a get
# or a member of the music_session that the join_request is designated for
def self.show(id, user)
return JoinRequest.find(id, :conditions => ["user_id = ? OR music_session_id IN (select music_session_id from connections WHERE user_id = ?)", user.id, user.id])
end
end
end

View File

@ -0,0 +1,38 @@
module JamRuby
class MaxMindGeo < ActiveRecord::Base
self.table_name = 'max_mind_geo'
def self.import_from_max_mind(file)
# File Geo-124
# Format:
# startIpNum,endIpNum,country,region,city,postalCode,latitude,longitude,dmaCode,areaCode
MaxMindGeo.transaction do
MaxMindGeo.delete_all
File.open(file, 'r:ISO-8859-1') do |io|
MaxMindGeo.pg_copy_from io, :map => { 'startIpNum' => 'ip_bottom', 'endIpNum' => 'ip_top', 'country' => 'country', 'region' => 'region', 'city' => 'city'}, :columns => [:startIpNum, :endIpNum, :country, :region, :city] do |row|
row[0] = ip_address_to_int(row[0])
row[1] = ip_address_to_int(row[1])
row.delete_at(5)
row.delete_at(5)
row.delete_at(5)
row.delete_at(5)
row.delete_at(5)
end
end
end
end
# Make an IP address fit in a signed int. Just divide it by 2, as the least significant part
# just can't possibly matter. We can verify this if needed. My guess is the entire bottom octet is
# actually irrelevant
def self.ip_address_to_int(ip)
ip.split('.').inject(0) {|total,value| (total << 8 ) + value.to_i} / 2
end
end
end

View File

@ -0,0 +1,57 @@
module JamRuby
class MaxMindIsp < ActiveRecord::Base
self.table_name = 'max_mind_isp'
def self.import_from_max_mind(file)
# File Geo-142
# Format:
# "beginIp","endIp","countryCode","ISP"
MaxMindIsp.transaction do
MaxMindIsp.delete_all
File.open(file, 'r:ISO-8859-1') do |io|
io.gets # eat the copyright line. gah, why do they have that in their file??
MaxMindIsp.pg_copy_from io, :map => { 'beginIp' => 'ip_bottom', 'endIp' => 'ip_top', 'countryCode' => 'country', 'ISP' => 'isp'}, :columns => [:beginIp, :endIp, :countryCode, :ISP] do |row|
row[0] = ip_address_to_int(strip_quotes(row[0]))
row[1] = ip_address_to_int(strip_quotes(row[1]))
row[2] = row[2]
row[3] = row[3..-1].join(',') # this is because the parser just cuts on any ',' and ignores double quotes. essentially postgres-copy isn't a great csv parser -- or I need to configure it better
while row.length > 4
row.delete_at(4)
end
end
end
end
end
# Make an IP address fit in a signed int. Just divide it by 2, as the least significant part
# just can't possibly matter. We can verify this if needed. My guess is the entire bottom octet is
# actually irrelevant
def self.ip_address_to_int(ip)
ip.split('.').inject(0) {|total,value| (total << 8 ) + value.to_i} / 2
end
private
def self.strip_quotes str
return nil if str.nil?
if str.chr == '"'
str = str[1..-1]
end
if str.rindex('"') == str.length - 1
str = str.chop
end
return str
end
def self.escape str
str.gsub(/\"/, '""')
end
end
end

View File

@ -0,0 +1,71 @@
module JamRuby
class Mix < ActiveRecord::Base
MAX_MIX_TIME = 7200 # 2 hours
before_destroy :delete_s3_files
self.primary_key = 'id'
belongs_to :recording, :class_name => "JamRuby::Recording", :inverse_of => :mixes
def self.schedule(recording, manifest)
raise if recording.nil?
mix = Mix.new
mix.recording = recording
mix.manifest = manifest
mix.save
mix
end
def self.next(mix_server)
# First check if there are any mixes started so long ago that we want to re-run them
Mix.where("completed_at IS NULL AND started_at < ?", Time.now - MAX_MIX_TIME).each do |mix|
# FIXME: This should probably throw some kind of log, since it means something went wrong
mix.started_at = nil
mix.mix_server = nil
mix.save
end
mix = Mix.where(:started_at => nil).limit(1).first
return nil if mix.nil?
mix.started_at = Time.now
mix.mix_server = mix_server
mix.save
mix
end
def finish(length, md5)
self.completed_at = Time.now
self.length = length
self.md5 = md5
save
end
def s3_url
S3Manager.s3_url(hashed_filename)
end
def url
S3Manager.url(hashed_filename)
end
def is_completed
!completed_at.nil?
end
private
def delete_s3_files
S3Manager.delete(hashed_filename)
end
def hashed_filename
S3Manager.hashed_filename('mix', id)
end
end
end

View File

@ -0,0 +1,194 @@
module JamRuby
class MusicSession < ActiveRecord::Base
self.primary_key = 'id'
attr_accessor :legal_terms, :skip_genre_validation
attr_accessible :creator, :description, :musician_access, :approval_required, :fan_chat, :fan_access, :genres
belongs_to :creator, :inverse_of => :music_sessions, :class_name => "JamRuby::User", :foreign_key => "user_id"
has_many :connections, :class_name => "JamRuby::Connection"
has_many :users, :through => :connections, :class_name => "JamRuby::User"
has_and_belongs_to_many :genres, :class_name => "::JamRuby::Genre", :join_table => "genres_music_sessions"
has_many :join_requests, :foreign_key => "music_session_id", :inverse_of => :music_session, :class_name => "JamRuby::JoinRequest"
has_many :invitations, :foreign_key => "music_session_id", :inverse_of => :music_session, :class_name => "JamRuby::Invitation"
has_many :invited_musicians, :through => :invitations, :class_name => "JamRuby::User", :foreign_key => "receiver_id", :source => :receiver
has_many :fan_invitations, :foreign_key => "music_session_id", :inverse_of => :music_session, :class_name => "JamRuby::FanInvitation"
has_many :invited_fans, :through => :fan_invitations, :class_name => "JamRuby::User", :foreign_key => "receiver_id", :source => :receiver
has_one :recording, :class_name => "JamRuby::Recording", :inverse_of => :music_session
belongs_to :band, :inverse_of => :music_sessions, :class_name => "JamRuby::Band", :foreign_key => "band_id"
after_save :require_at_least_one_genre, :limit_max_genres
after_destroy do |obj|
JamRuby::MusicSessionHistory.removed_music_session(obj.user_id, obj.id)
end
validates :description, :presence => true, :no_profanity => true
validates :fan_chat, :inclusion => {:in => [true, false]}
validates :fan_access, :inclusion => {:in => [true, false]}
validates :approval_required, :inclusion => {:in => [true, false]}
validates :musician_access, :inclusion => {:in => [true, false]}
validates :legal_terms, :inclusion => {:in => [true]}, :on => :create
validates :creator, :presence => true
validate :creator_is_musician
def creator_is_musician
unless creator.musician?
errors.add(:creator, "creator must be a musician")
end
end
# This is a little confusing. You can specify *BOTH* friends_only and my_bands_only to be true
# If so, then it's an OR condition. If both are false, you can get sessions with anyone.
def self.index(current_user, participants = nil, genres = nil, friends_only = false, my_bands_only = false, keyword = nil)
query = MusicSession
.joins(
%Q{
LEFT OUTER JOIN
connections
ON
music_sessions.id = connections.music_session_id
}
)
.joins(
%Q{
LEFT OUTER JOIN
friendships
ON
connections.user_id = friendships.user_id
AND
friendships.friend_id = '#{current_user.id}'
}
)
.joins(
%Q{
LEFT OUTER JOIN
invitations
ON
invitations.music_session_id = music_sessions.id
AND
invitations.receiver_id = '#{current_user.id}'
}
)
.group(
%Q{
music_sessions.id
}
)
.order(
%Q{
SUM(CASE WHEN invitations.id IS NULL THEN 0 ELSE 1 END) DESC,
SUM(CASE WHEN friendships.user_id IS NULL THEN 0 ELSE 1 END) DESC,
music_sessions.created_at DESC
}
)
.where(
%Q{
musician_access = true
OR
invitations.id IS NOT NULL
}
)
query = query.where("music_sessions.description like '%#{keyword}%'") unless keyword.nil?
query = query.where("connections.user_id" => participants.split(',')) unless participants.nil?
query = query.joins(:genres).where("genres.id" => genres.split(',')) unless genres.nil?
if my_bands_only
query = query.joins(
%Q{
LEFT OUTER JOIN
bands_musicians
ON
bands_musicians.user_id = '#{current_user.id}'
}
)
end
if my_bands_only || friends_only
query = query.where(
%Q{
#{friends_only ? "friendships.user_id IS NOT NULL" : "false"}
OR
#{my_bands_only ? "bands_musicians.band_id = music_sessions.band_id" : "false"}
}
)
end
return query
end
# Verifies that the specified user can join this music session
def can_join? user, as_musician
if as_musician
if !user.musician
return false # "a fan can not join a music session as a musician"
raise PermissionError, "a fan can not join a music session as a musician"
end
if self.musician_access
if self.approval_required
return self.invited_musicians.exists?(user)
else
return true
end
else
# the creator can always join, and the invited users can join
return self.creator == user || self.invited_musicians.exists?(user)
end
else
# it's a fan, and the only way a fan can join is if fan_access is true
return self.fan_access
end
end
# Verifies that the specified user can see this music session
def can_see? user
if self.musician_access
return true
else
# the creator can always see, and the invited users can see it too
return self.creator == user || self.invited_musicians.exists?(user)
end
end
# Verifies that the specified user can delete this music session
def can_delete? user
# the creator can delete
return self.creator == user
end
def access? user
return self.users.exists? user
end
def to_s
return description
end
private
def require_at_least_one_genre
unless skip_genre_validation
if self.genres.count < Limits::MIN_GENRES_PER_RECORDING
errors.add(:genres, ValidationMessages::GENRE_MINIMUM_NOT_MET)
end
end
end
def limit_max_genres
unless skip_genre_validation
if self.genres.count > Limits::MAX_GENRES_PER_RECORDING
errors.add(:genres, ValidationMessages::GENRE_LIMIT_EXCEEDED)
end
end
end
end
end

View File

@ -0,0 +1,107 @@
module JamRuby
class MusicSessionHistory < ActiveRecord::Base
self.table_name = "music_sessions_history"
self.primary_key = 'id'
# for some reason the association is not working, i suspect has to do with the foreign key
def music_session_user_histories
@msuh ||= JamRuby::MusicSessionUserHistory
.where(:music_session_id => self.music_session_id)
.order('created_at DESC')
end
# has_many(:music_session_user_histories,
# :class_name => "JamRuby::MusicSessionUserHistory",
# :foreign_key => :music_session_id,
# :order => 'created_at DESC',
# :inverse_of => :music_session_history)
has_one(:perf_data,
:class_name => "JamRuby::MusicSessionPerfData",
:foreign_key => "music_session_id",
:inverse_of => :music_session)
belongs_to(:user,
:class_name => 'JamRuby::User',
:foreign_key => :user_id,
:inverse_of => :music_session_histories)
belongs_to(:band,
:class_name => 'JamRuby::Band',
:foreign_key => :band_id,
:inverse_of => :music_session_history)
GENRE_SEPARATOR = '|'
def self.index(current_user, user_id, band_id = nil, genre = nil)
hide_private = false
if current_user.id != user_id
hide_private = false # TODO: change to true once public flag exists
end
query = MusicSessionHistory
.joins(
%Q{
LEFT OUTER JOIN
music_sessions_user_history
ON
music_sessions_history.music_session_id = music_sessions_user_history.music_session_id
}
)
.where(
%Q{
music_sessions_history.user_id = '#{user_id}'
}
)
#query = query.where("public = false") unless !hide_private
query = query.where("music_sessions_history.band_id = '#{band_id}") unless band_id.nil?
query = query.where("music_sessions_history.genres like '%#{genre}%'") unless genre.nil?
return query
end
def unique_users
User
.joins(:music_session_user_histories)
.group("users.id")
.order("users.id")
.where(%Q{ music_sessions_user_history.music_session_id = '#{music_session_id}'})
end
def self.save(music_session)
session_history = MusicSessionHistory.find_by_music_session_id(music_session.id)
if session_history.nil?
session_history = MusicSessionHistory.new()
end
session_history.music_session_id = music_session.id
session_history.description = music_session.description unless music_session.description.nil?
session_history.user_id = music_session.creator.id
session_history.band_id = music_session.band.id unless music_session.band.nil?
session_history.genres = music_session.genres.map { |g| g.id }.join GENRE_SEPARATOR
session_history.save!
end
def self.removed_music_session(user_id, session_id)
hist = self
.where(:user_id => user_id)
.where(:music_session_id => session_id)
.limit(1)
.first
hist.update_attribute(:session_removed_at, Time.now) if hist
JamRuby::MusicSessionUserHistory.removed_music_session(user_id, session_id)
end
def duration_minutes
end_time = self.session_removed_at || Time.now
(end_time - self.created_at) / 60.0
end
def perf_uri
self.perf_data.try(:uri)
end
end
end

View File

@ -0,0 +1,28 @@
require 'securerandom'
module JamRuby
class MusicSessionPerfData < ActiveRecord::Base
self.primary_key = 'id'
attr_accessible :uri
belongs_to(:music_session,
:class_name => "JamRuby::MusicSessionHistory",
:foreign_key => :music_session_id,
:inverse_of => :perf_data)
# mount_uploader :uri, PerfDataUploader
validates :music_session, :presence => true
validates :client_id, :presence => true
validates :uri, :presence => true
before_validation(:on => :create) do
self.created_at ||= Time.now
self.id = SecureRandom.uuid
self.uri = "perf_data/#{self.music_session_id}/#{self.client_id}-#{self.created_at.to_i}"
end
end
end

View File

@ -0,0 +1,52 @@
module JamRuby
class MusicSessionUserHistory < ActiveRecord::Base
self.table_name = "music_sessions_user_history"
self.primary_key = 'id'
belongs_to(:user,
:class_name => "JamRuby::User",
:foreign_key => "user_id",
:inverse_of => :music_session_user_histories)
# for some reason the association is not working, i suspect has to do with the foreign key
def music_session_history
@msh ||= JamRuby::MusicSessionHistory
.where(:music_session_id => self.music_session_id)
.limit(1)
.first
end
# belongs_to(:music_session_history,
# :class_name => "JamRuby::MusicSessionHistory",
# :foreign_key => :music_session_id,
# :inverse_of => :music_session_user_histories)
def self.save(music_session_id, user_id, client_id)
session_user_history = MusicSessionUserHistory.new()
session_user_history.music_session_id = music_session_id
session_user_history.user_id = user_id
session_user_history.client_id = client_id
session_user_history.save
end
def user_email
self.user ? self.user.email : '<user deleted>'
end
def duration_minutes
end_time = self.session_removed_at || Time.now
(end_time - self.created_at) / 60.0
end
def self.removed_music_session(user_id, session_id)
hist = self
.where(:user_id => user_id)
.where(:music_session_id => session_id)
.limit(1)
.first
hist.update_attribute(:session_removed_at, Time.now) if hist
end
end
end

View File

@ -0,0 +1,18 @@
module JamRuby
class MusicianInstrument < ActiveRecord::Base
self.table_name = "musicians_instruments"
self.primary_key = 'id'
# ensure most proficient, highest priority
default_scope order('proficiency_level DESC, priority ASC')
belongs_to :user, :class_name => "JamRuby::User"
belongs_to :instrument, :class_name => "JamRuby::Instrument"
def description
@description = self.instrument.description
end
end
end

View File

@ -0,0 +1,280 @@
module JamRuby
class Notification < ActiveRecord::Base
self.primary_key = 'id'
default_scope order('created_at DESC')
belongs_to :target_user, :class_name => "JamRuby::User", :foreign_key => "target_user_id"
belongs_to :source_user, :class_name => "JamRuby::User", :foreign_key => "source_user_id"
belongs_to :band, :class_name => "JamRuby::Band", :foreign_key => "band_id"
belongs_to :session, :class_name => "JamRuby::MusicSession", :foreign_key => "session_id"
belongs_to :recording, :class_name => "JamRuby::Recording", :foreign_key => "recording_id"
def index(user_id)
results = Notification.where(:target_user_id => user_id).limit(50)
return results
end
def photo_url
unless self.source_user.nil?
self.source_user.photo_url
end
end
# used for persisted notifications
def formatted_msg
target_user, source_user, band, session, recording, invitation, join_request = nil
unless self.target_user_id.nil?
target_user = User.find(self.target_user_id)
end
unless self.source_user_id.nil?
source_user = User.find(self.source_user_id)
end
unless self.band_id.nil?
band = Band.find(self.band_id)
end
unless self.session_id.nil?
session = MusicSession.find(self.session_id)
end
unless self.recording_id.nil?
recording = Recording.find(self.recording_id)
end
unless self.invitation_id.nil?
invitation = Invitation.find(self.invitation_id)
end
unless self.join_request_id.nil?
join_request = JoinRequest.find(self.join_request_id)
end
return self.class.format_msg(self.description, source_user)
end
# TODO: MAKE ALL METHODS BELOW ASYNC SO THE CLIENT DOESN'T BLOCK ON NOTIFICATION LOGIC
# TODO: ADD TESTS FOR THIS CLASS
class << self
@@mq_router = MQRouter.new
@@message_factory = MessageFactory.new
def delete_all(session_id)
Notification.delete_all "(session_id = '#{session_id}')"
end
################### HELPERS ###################
def retrieve_friends(connection, user_id)
friend_ids = []
connection.exec("SELECT f.friend_id as friend_id FROM friendships f WHERE f.user_id = $1", [user_id]) do |friend_results|
friend_results.each do |friend_result|
friend_ids.push(friend_result['friend_id'])
end
end
return friend_ids
end
def retrieve_followers(connection, user_id)
follower_ids = []
connection.exec("SELECT uf.follower_id as friend_id FROM users_followers uf WHERE uf.user_id = $1", [user_id]) do |follower_results|
follower_results.each do |follower_result|
follower_ids.push(follower_result['follower_id'])
end
end
return follower_ids
end
def retrieve_friends_and_followers(connection, user_id)
ids = retrieve_friends(connection, user_id)
ids.concat(retrieve_followers(connection, user_id))
ids.uniq! {|id| id}
return ids
end
def retrieve_friends_and_followers_not_in_session(connection, user_id, session_id)
ids = retrieve_friends_and_followers(connection, user_id)
connection.exec("SELECT c.user_id as musician_id FROM connections c WHERE c.music_session_id = $1", [session_id]) do |musicians|
musicians.each do |musician_result|
# remove users who are in the session
ids.reject! {|item| item == musician_result['musician_id']}
end
end
return ids
end
def format_msg(description, user)
case description
when NotificationTypes::FRIEND_UPDATE
return "#{user.name} is now "
when NotificationTypes::FRIEND_REQUEST
return "#{user.name} has sent you a friend request."
when NotificationTypes::FRIEND_REQUEST_ACCEPTED
return "#{user.name} has accepted your friend request."
when NotificationTypes::FRIEND_SESSION_JOIN
return "#{user.name} has joined the session."
when NotificationTypes::MUSICIAN_SESSION_JOIN
return "#{user.name} has joined the session."
when NotificationTypes::MUSICIAN_SESSION_DEPART
return "#{user.name} has left the session."
# when "social_media_friend_joined"
# when "join_request_approved"
# when "join_request_rejected"
# when "session_invitation"
# when "band_invitation"
# when "band_invitation_accepted"
# when "recording_available"
else
return ""
end
end
################### FRIEND UPDATE ###################
def send_friend_update(user_id, online, connection)
# (1) get all of this user's friends
friend_ids = retrieve_friends(connection, user_id)
if friend_ids.length > 0
user = User.find(user_id)
# (2) create notification
online_msg = online ? "online." : "offline."
notification_msg = format_msg(NotificationTypes::FRIEND_UPDATE, user) + online_msg
msg = @@message_factory.friend_update(user_id, user.name, user.photo_url, online, notification_msg)
# (3) send notification
@@mq_router.publish_to_friends(friend_ids, msg, user_id)
end
end
################### FRIEND REQUEST ###################
def send_friend_request(friend_request_id, user_id, friend_id)
user = User.find(user_id)
# (1) save to database
notification = Notification.new
notification.description = NotificationTypes::FRIEND_REQUEST
notification.source_user_id = user_id
notification.target_user_id = friend_id
notification.friend_request_id = friend_request_id
notification.save
# (2) create notification
notification_msg = format_msg(NotificationTypes::FRIEND_REQUEST, user)
msg = @@message_factory.friend_request(friend_request_id, user_id, user.name, user.photo_url, friend_id, notification_msg, notification.id, notification.created_at.to_s)
# (3) send notification
@@mq_router.publish_to_user(friend_id, msg)
end
############### FRIEND REQUEST ACCEPTED ###############
def send_friend_request_accepted(user_id, friend_id)
friend = User.find(friend_id)
# (1) save to database
notification = Notification.new
notification.description = NotificationTypes::FRIEND_REQUEST_ACCEPTED
notification.source_user_id = friend_id
notification.target_user_id = user_id
notification.save
# (2) create notification
notification_msg = format_msg(NotificationTypes::FRIEND_REQUEST_ACCEPTED, friend)
msg = @@message_factory.friend_request_accepted(friend_id, friend.name, friend.photo_url, user_id, notification_msg, notification.id, notification.created_at.to_s)
# (3) send notification
@@mq_router.publish_to_user(user_id, msg)
end
################## SESSION INVITATION ##################
def send_session_invitation(receiver_id, invitation_id)
# (1) save to database
notification = Notification.new
notification.description = NotificationTypes::SESSION_INVITATION
notification.target_user_id = receiver_id
notification.save
# (2) create notification
msg = @@message_factory.session_invitation(receiver_id, invitation_id)
# (3) send notification
@@mq_router.publish_to_user(receiver_id, msg)
end
def send_musician_session_join(music_session, connection, user)
# (1) create notification
msg = @@message_factory.musician_session_join(music_session.id, user.id, user.name, user.photo_url)
# (2) send notification
@@mq_router.server_publish_to_session(music_session, msg, sender = {:client_id => connection.client_id})
end
def send_musician_session_depart(music_session, client_id, user)
# (1) create notification
msg = @@message_factory.musician_session_depart(music_session.id, user.id, user.name, user.photo_url)
# (2) send notification
@@mq_router.server_publish_to_session(music_session, msg, sender = {:client_id => client_id})
end
def send_musician_session_fresh(music_session, client_id, user)
# (1) create notification
msg = @@message_factory.musician_session_fresh(music_session.id, user.id, user.name, user.photo_url)
# (2) send notification
@@mq_router.server_publish_to_session(music_session, msg, sender = {:client_id => client_id})
end
def send_musician_session_stale(music_session, client_id, user)
# (1) create notification
msg = @@message_factory.musician_session_stale(music_session.id, user.id, user.name, user.photo_url)
# (2) send notification
@@mq_router.server_publish_to_session(music_session, msg, sender = {:client_id => client_id})
end
def send_friend_session_join(db_conn, connection, user)
ids = retrieve_friends_and_followers_not_in_session(db_conn, user.id, connection.music_session.id)
if ids.length > 0
# (1) save to database
# (2) create notification
msg = @@message_factory.friend_session_join(connection.music_session.id, user.id, user.name, user.photo_url)
# (3) send notification
@@mq_router.publish_to_friends(ids, msg, sender = {:client_id => connection.client_id})
end
end
def send_join_request(music_session, join_request, sender, text)
# (1) save to database
# (2) create notification
msg = @@message_factory.join_request(music_session.id, join_request.id, sender.name, text)
# (3) send notification
@@mq_router.server_publish_to_session(music_session, msg)
end
end
end
end

View File

@ -0,0 +1,83 @@
module JamRuby
class RecordedTrack < ActiveRecord::Base
self.table_name = "recorded_tracks"
self.primary_key = 'id'
SOUND = %w(mono stereo)
belongs_to :user, :class_name => "JamRuby::User", :inverse_of => :recorded_tracks
belongs_to :recording, :class_name => "JamRuby::Recording", :inverse_of => :recorded_tracks
belongs_to :instrument, :class_name => "JamRuby::Instrument"
validates :sound, :inclusion => {:in => SOUND}
before_destroy :delete_s3_files
# Copy an ephemeral track to create a saved one. Some fields are ok with defaults
def self.create_from_track(track, recording)
recorded_track = self.new
recorded_track.recording = recording
recorded_track.user = track.connection.user
recorded_track.instrument = track.instrument
recorded_track.sound = track.sound
recorded_track.save
recorded_track
end
def upload_start(length, md5)
self.upload_id = S3Manager.multipart_upload_start(hashed_filename)
self.length = length
self.md5 = md5
save
end
def upload_sign(content_md5)
S3Manager.upload_sign(hashed_filename, content_md5, upload_id)
end
def upload_part_complete(part)
raise JamRuby::JamArgumentError unless part == next_part_to_upload
self.next_part_to_upload = part + 1
save
end
def upload_complete
S3Manager.multipart_upload_complete(upload_id)
self.fully_uploaded = true
save
recording.upload_complete
end
def url
S3Manager.url(hashed_filename)
end
def filename
hashed_filename
end
# Format: "recording_#{recorded_track_id}"
# File extension is irrelevant actually.
def self.find_by_upload_filename(filename)
matches = /^recording_([\w-]+)$/.match(filename)
return nil unless matches && matches.length > 1
RecordedTrack.find(matches[1])
end
private
def delete_s3_files
S3Manager.delete(hashed_filename)
end
def hashed_filename
S3Manager.hashed_filename('recorded_track', id)
end
end
end

View File

@ -0,0 +1,281 @@
module JamRuby
class Recording < ActiveRecord::Base
self.primary_key = 'id'
has_many :claimed_recordings, :class_name => "JamRuby::ClaimedRecording", :inverse_of => :recording
has_many :users, :through => :claimed_recordings, :class_name => "JamRuby::User"
belongs_to :owner, :class_name => "JamRuby::User", :inverse_of => :owned_recordings
belongs_to :band, :class_name => "JamRuby::Band", :inverse_of => :recordings
belongs_to :music_session, :class_name => "JamRuby::MusicSession", :inverse_of => :recording
has_many :mixes, :class_name => "JamRuby::Mix", :inverse_of => :recording
has_many :recorded_tracks, :class_name => "JamRuby::RecordedTrack", :foreign_key => :recording_id
# Start recording a session.
def self.start(music_session_id, owner)
recording = nil
# Use a transaction and lock to avoid races.
ActiveRecord::Base.transaction do
music_session = MusicSession.find(music_session_id, :lock => true)
if music_session.nil?
raise PermissionError, "the session has ended"
end
unless music_session.recording.nil?
raise PermissionError, "the session is already being recorded"
end
recording = Recording.new
recording.music_session = music_session
recording.owner = owner
music_session.connections.each do |connection|
# Note that we do NOT connect the recording to any users at this point.
# That ONLY happens if a user clicks 'save'
# recording.users << connection.user
connection.tracks.each do |track|
RecordedTrack.create_from_track(track, recording)
end
end
# Note that I believe this can be nil.
recording.band = music_session.band
recording.save
music_session.recording = recording
music_session.save
end
# FIXME:
# NEED TO SEND NOTIFICATION TO ALL USERS IN THE SESSION THAT RECORDING HAS STARTED HERE.
# I'LL STUB IT A BIT. NOTE THAT I REDO THE FIND HERE BECAUSE I DON'T WANT TO SEND THESE
# NOTIFICATIONS WHILE THE DB ROW IS LOCKED
music_session = MusicSession.find(music_session_id)
music_session.connections.each do |connection|
# connection.notify_recording_has_started
end
recording
end
# Stop recording a session
def stop
# Use a transaction and lock to avoid races.
ActiveRecord::Base.transaction do
music_session = MusicSession.find(self.music_session_id, :lock => true)
if music_session.nil?
raise PermissionError, "the session has ended"
end
unless music_session.recording
raise PermissionError, "the session is not currently being recorded"
end
music_session.recording = nil
music_session.save
end
self.duration = Time.now - created_at
save
end
# Called when a user wants to "claim" a recording. To do this, the user must have been one of the tracks in the recording.
def claim(user, name, genre, is_public, is_downloadable)
if self.users.include?(user)
raise PermissionError, "user already claimed this recording"
end
unless self.recorded_tracks.find { |recorded_track| recorded_track.user == user }
raise PermissionError, "user was not in this session"
end
unless self.music_session.nil?
raise PermissionError, "recording cannot be claimed while it is being recorded"
end
if name.nil? || genre.nil? || is_public.nil? || is_downloadable.nil?
raise PermissionError, "recording must have name, genre and flags"
end
claimed_recording = ClaimedRecording.new
claimed_recording.user = user
claimed_recording.recording = self
claimed_recording.name = name
claimed_recording.genre = genre
claimed_recording.is_public = is_public
claimed_recording.is_downloadable = is_downloadable
self.claimed_recordings << claimed_recording
save
claimed_recording
end
# Find out if all the tracks for this recording have been uploaded
def uploaded?
self.recorded_tracks.each do |recorded_track|
return false unless recorded_track.fully_uploaded
end
return true
end
# Discards this recording and schedules deletion of all files associated with it.
def discard
self.destroy
end
# Returns the list of files the user needs to upload. This will only ever be recordings
def self.upload_file_list(user)
files = []
User.joins(:recordings).joins(:recordings => :recorded_tracks)
.where(%Q{ recordings.duration IS NOT NULL })
.where("recorded_tracks.user_id = '#{user.id}'")
.where(%Q{ recorded_tracks.fully_uploaded = FALSE }).each do |user|
user.recordings.each.do |recording|
recording.recorded_tracks.each do |recorded_track|
files.push(
{
:type => "recorded_track",
:id => recorded_track.id,
:url => recorded_track.url # FIXME IS THIS RIGHT?
}
)
end
end
files
end
# Returns the list of files this user should have synced to his computer, along with md5s and lengths
def self.list(user)
downloads = []
# That second join is important. It's saying join off of recordings, NOT user. If you take out the
# ":recordings =>" part, you'll just get the recorded_tracks that I played. Very different!
User.joins(:recordings).joins(:recordings => :recorded_tracks)
.order(%Q{ recordings.created_at DESC })
.where(%Q{ recorded_tracks.fully_uploaded = TRUE })
.where(:id => user.id).each do |theuser|
theuser.recordings.each do |recording|
recording.recorded_tracks.each do |recorded_track|
recorded_track = user.claimed_recordings.first.recording.recorded_tracks.first
downloads.push(
{
:type => "recorded_track",
:id => recorded_track.id,
:length => recorded_track.length,
:md5 => recorded_track.md5,
:url => recorded_track.url
}
)
end
end
end
User.joins(:recordings).joins(:recordings => :mixes)
.order(%Q{ recordings.created_at DESC })
.where(%Q{ mixes.completed_at IS NOT NULL }).each do |theuser|
theuser.recordings.each do |recording|
recording.mixes.each do |mix|
downloads.push(
{
:type => "mix",
:id => mix.id,
:length => mix.length,
:md5 => mix.md5,
:url => mix.url
}
)
end
end
end
uploads = []
RecordedTrack
.joins(:recording)
.where(:user_id => user.id)
.where(:fully_uploaded => false)
.where("duration IS NOT NULL").each do |recorded_track|
uploads.push(recorded_track.filename)
end
{
"downloads" => downloads,
"uploads" => uploads
}
end
# Check to see if all files have been uploaded. If so, kick off a mix.
def upload_complete
# Don't allow multiple mixes for now.
raise JamRuby::JamArgumentError unless self.mixes.length == 0
# FIXME: There's a possible race condition here. If two users complete
# uploads at the same time, we'll schedule 2 mixes.
recorded_tracks.each do |recorded_track|
return unless recorded_track.fully_uploaded
end
self.mixes << Mix.schedule(self, base_mix_manifest.to_json)
save
end
=begin
# This is no longer remotely right.
def self.search(query, options = { :limit => 10 })
# only issue search if at least 2 characters are specified
if query.nil? || query.length < 2
return []
end
# create 'anded' statement
query = Search.create_tsquery(query)
if query.nil? || query.length == 0
return []
end
return Recording.where("description_tsv @@ to_tsquery('jamenglish', ?)", query).limit(options[:limit])
end
=end
def base_mix_manifest
manifest = { "files" => [], "timeline" => [] }
mix_params = []
recorded_tracks.each do |recorded_track|
return nil unless recorded_track.fully_uploaded
manifest["files"] << { "url" => recorded_track.url, "codec" => "vorbis", "offset" => 0 }
mix_params << { "level" => 100, "balance" => 0 }
end
manifest["timeline"] << { "timestamp" => 0, "mix" => mix_params }
manifest["timeline"] << { "timestamp" => duration, "end" => true }
manifest
end
private
def self.validate_user_is_band_member(user, band)
unless band.users.exists? user
raise PermissionError, ValidationMessages::USER_NOT_BAND_MEMBER_VALIDATION_ERROR
end
end
def self.validate_user_is_creator(user, creator)
unless user.id == creator.id
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR
end
end
def self.validate_user_is_musician(user)
unless user.musician?
raise PermissionError, ValidationMessages::USER_NOT_MUSICIAN_VALIDATION_ERROR
end
end
end
end

View File

@ -0,0 +1,85 @@
module JamRuby
# not a active_record model; just a search result
class Search
attr_accessor :bands, :musicians, :fans, :recordings, :friends
LIMIT = 10
# performs a site-white search
def self.search(query, user_id = nil)
users = User.search(query, :limit => LIMIT)
bands = Band.search(query, :limit => LIMIT)
# NOTE: I removed recordings from search here. This is because we switched
# to "claimed_recordings" so it's not clear what should be searched.
friends = Friendship.search(query, user_id, :limit => LIMIT)
return Search.new(users + bands + friends)
end
# performs a friend search scoped to a specific user
# def self.search_by_user(query, user_id)
# friends = Friendship.search(query, user_id, :limit => LIMIT)
# return Search.new(friends)
# end
# search_results - results from a Tire search across band/user/recording
def initialize(search_results)
@bands = []
@musicians = []
@fans = []
@recordings = []
@friends = []
if search_results.nil?
return
end
search_results.take(LIMIT).each do |result|
if result.class == User
if result.musician
@musicians.push(result)
else
@fans.push(result)
end
elsif result.class == Band
@bands.push(result)
elsif result.class == Recording
@recordings.push(result)
elsif result.class == Friendship
@friends.push(result.friend)
else
raise Exception, "unknown class #{result.class} returned in search results"
end
end
end
def self.create_tsquery(query)
# empty queries don't hit back to elasticsearch
if query.nil? || query.length == 0
return nil
end
search_terms = query.split
if search_terms.length == 0
return nil
end
args = nil
search_terms.each do |search_term|
if args == nil
args = search_term
else
args = args + " & " + search_term
end
end
args = args + ":*"
return args
end
end
end

View File

@ -0,0 +1,62 @@
module JamRuby
class Track < ActiveRecord::Base
self.table_name = "tracks"
self.primary_key = 'id'
default_scope order('created_at ASC')
SOUND = %w(mono stereo)
belongs_to :connection, :class_name => "JamRuby::Connection", :inverse_of => :tracks
belongs_to :instrument, :class_name => "JamRuby::Instrument", :inverse_of => :tracks
validates :sound, :inclusion => {:in => SOUND}
def self.index(current_user, music_session_id)
query = Track
.joins(
%Q{
INNER JOIN
connections
ON
connections.music_session_id = '#{music_session_id}'
AND
connections.id = connection_id
AND
connections.user_id = '#{current_user.id}'
INNER JOIN
music_sessions
ON
music_sessions.id = '#{music_session_id}'
AND
music_sessions.user_id = '#{current_user.id}'
}
)
return query
end
def self.save(id, connection_id, instrument_id, sound)
if id.nil?
track = Track.new()
track.connection_id = connection_id
else
track = Track.find(id)
end
unless instrument_id.nil?
track.instrument_id = instrument_id
end
unless sound.nil?
track.sound = sound
end
track.updated_at = Time.now.getutc
track.save
return track
end
end
end

View File

@ -0,0 +1,931 @@
include Devise::Models
module JamRuby
class User < ActiveRecord::Base
#devise: for later: :trackable
devise :database_authenticatable,
:recoverable, :rememberable
attr_accessible :first_name, :last_name, :email, :city, :password, :password_confirmation, :state, :country, :birth_date, :subscribe_email, :terms_of_service, :original_fpfile, :cropped_fpfile, :cropped_s3_path, :photo_url, :crop_selection
# updating_password corresponds to a lost_password
attr_accessor :updating_password, :updating_email, :updated_email, :update_email_confirmation_url, :administratively_created, :current_password, :setting_password, :confirm_current_password, :updating_avatar
# authorizations (for facebook, etc -- omniauth)
has_many :user_authorizations, :class_name => "JamRuby::UserAuthorization"
# connections (websocket-gateway)
has_many :connections, :class_name => "JamRuby::Connection"
# friend requests
has_many :friend_requests, :class_name => "JamRuby::FriendRequest"
# instruments
has_many :musician_instruments, :class_name => "JamRuby::MusicianInstrument"
has_many :instruments, :through => :musician_instruments, :class_name => "JamRuby::Instrument"
# bands
has_many :band_musicians, :class_name => "JamRuby::BandMusician"
has_many :bands, :through => :band_musicians, :class_name => "JamRuby::Band"
# recordings
has_many :owned_recordings, :class_name => "JamRuby::Recording"
has_many :recordings, :through => :claimed_recordings, :class_name => "JamRuby::Recording"
has_many :claimed_recordings, :class_name => "JamRuby::ClaimedRecording", :inverse_of => :user
# user likers (a musician has likers and may have likes too; fans do not have likers)
has_many :likers, :class_name => "JamRuby::UserLiker", :foreign_key => "user_id", :inverse_of => :user
has_many :inverse_likers, :through => :likers, :class_name => "JamRuby::User", :foreign_key => "liker_id"
# user likes (fans and musicians have likes)
has_many :likes, :class_name => "JamRuby::UserLike", :foreign_key => "liker_id", :inverse_of => :user
has_many :inverse_likes, :through => :followings, :class_name => "JamRuby::User", :foreign_key => "user_id"
# band likes
has_many :band_likes, :class_name => "JamRuby::BandLiker", :foreign_key => "liker_id", :inverse_of => :user
has_many :inverse_band_likes, :through => :band_likes, :class_name => "JamRuby::Band", :foreign_key => "band_id"
# followers
has_many :user_followers, :class_name => "JamRuby::UserFollower", :foreign_key => "user_id"
has_many :followers, :through => :user_followers, :class_name => "JamRuby::User"
has_many :inverse_user_followers, :through => :followers, :class_name => "JamRuby::UserFollower", :foreign_key => "follower_id"
has_many :inverse_followers, :through => :inverse_user_followers, :source => :user, :class_name => "JamRuby::User"
# user followings
has_many :user_followings, :class_name => "JamRuby::UserFollowing", :foreign_key => "follower_id"
has_many :followings, :through => :user_followings, :class_name => "JamRuby::User"
has_many :inverse_user_followings, :through => :followings, :class_name => "JamRuby::UserFollowing", :foreign_key => "user_id"
has_many :inverse_followings, :through => :inverse_user_followings, :source => :user, :class_name => "JamRuby::User"
# band followings
has_many :b_followings, :class_name => "JamRuby::BandFollowing", :foreign_key => "follower_id"
has_many :band_followings, :through => :b_followings, :class_name => "JamRuby::Band"
has_many :inverse_b_followings, :through => :band_followings, :class_name => "JamRuby::BandFollowing", :foreign_key => "band_id"
has_many :inverse_band_followings, :through => :inverse_band_followings, :source => :band, :class_name => "JamRuby::Band"
# notifications
has_many :notifications, :class_name => "JamRuby::Notification", :foreign_key => "target_user_id"
has_many :inverse_notifications, :through => :notifications, :class_name => "JamRuby::User"
# friends
has_many :friendships, :class_name => "JamRuby::Friendship", :foreign_key => "user_id"
has_many :friends, :through => :friendships, :class_name => "JamRuby::User"
has_many :inverse_friendships, :class_name => "JamRuby::Friendship", :foreign_key => "friend_id"
has_many :inverse_friends, :through => :inverse_friendships, :source => :user, :class_name => "JamRuby::User"
# connections / music sessions
has_many :created_music_sessions, :foreign_key => "user_id", :inverse_of => :user, :class_name => "JamRuby::MusicSession" # sessions *created* by the user
has_many :music_sessions, :through => :connections, :class_name => "JamRuby::MusicSession"
# invitations
has_many :received_invitations, :foreign_key => "receiver_id", :inverse_of => :receiver, :class_name => "JamRuby::Invitation"
has_many :sent_invitations, :foreign_key => "sender_id", :inverse_of => :sender, :class_name => "JamRuby::Invitation"
# fan invitations
has_many :received_fan_invitations, :foreign_key => "receiver_id", :inverse_of => :receiver, :class_name => "JamRuby::FanInvitation"
has_many :sent_fan_invitations, :foreign_key => "sender_id", :inverse_of => :sender, :class_name => "JamRuby::FanInvitation"
# band invitations
has_many :received_band_invitations, :inverse_of => :receiver, :foreign_key => "user_id", :class_name => "JamRuby::BandInvitation"
has_many :sent_band_invitations, :inverse_of => :sender, :foreign_key => "creator_id", :class_name => "JamRuby::BandInvitation"
# session history
has_many :music_session_histories, :foreign_key => "user_id", :class_name => "JamRuby::MusicSessionHistory", :inverse_of => :user
has_many :music_session_user_histories, :foreign_key => "user_id", :class_name => "JamRuby::MusicSessionUserHistory", :inverse_of => :user
# saved tracks
has_many :recorded_tracks, :foreign_key => "user_id", :class_name => "JamRuby::RecordedTrack", :inverse_of => :user
# invited users
has_many :invited_users, :foreign_key => "sender_id", :class_name => "JamRuby::InvitedUser"
# crash dumps
has_many :crash_dumps, :foreign_key => "user_id", :class_name => "JamRuby::CrashDump"
# This causes the authenticate method to be generated (among other stuff)
#has_secure_password
before_save :create_remember_token, :if => :should_validate_password?
before_save :stringify_avatar_info , :if => :updating_avatar
validates :first_name, presence: true, length: {maximum: 50}, no_profanity: true
validates :last_name, presence: true, length: {maximum: 50}, no_profanity: true
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, format: {with: VALID_EMAIL_REGEX}
validates :update_email, presence: true, format: {with: VALID_EMAIL_REGEX}, :if => :updating_email
validates_length_of :password, minimum: 6, maximum: 100, :if => :should_validate_password?
validates_presence_of :password_confirmation, :if => :should_validate_password?
validates_confirmation_of :password, :if => :should_validate_password?
validates :terms_of_service, :acceptance => {:accept => true, :on => :create, :allow_nil => false }
validates :subscribe_email, :inclusion => {:in => [nil, true, false]}
validates :musician, :inclusion => {:in => [true, false]}
# custom validators
validate :validate_musician_instruments
validate :validate_current_password
validate :validate_update_email
validate :validate_avatar_info
validate :email_case_insensitive_uniqueness
validate :update_email_case_insensitive_uniqueness, :if => :updating_email
def validate_musician_instruments
errors.add(:musician_instruments, ValidationMessages::INSTRUMENT_MINIMUM_NOT_MET) if !administratively_created && musician && musician_instruments.length == 0
errors.add(:musician_instruments, ValidationMessages::INSTRUMENT_LIMIT_EXCEEDED) if !administratively_created && musician && musician_instruments.length > 5
end
def validate_current_password
# checks if the user put in their current password (used when changing your email, for instance)
errors.add(:current_password, ValidationMessages::NOT_YOUR_PASSWORD) if should_confirm_existing_password? && !valid_password?(self.current_password)
end
def validate_update_email
if updating_email && self.update_email == self.email
errors.add(:update_email, ValidationMessages::EMAIL_MATCHES_CURRENT)
elsif updating_email && User.where("email ILIKE ?", self.update_email).first != nil
errors.add(:update_email, ValidationMessages::EMAIL_ALREADY_TAKEN)
end
end
def validate_avatar_info
if updating_avatar
# we want to mak sure that original_fpfile and cropped_fpfile seems like real fpfile info objects (i.e, json objects from filepicker.io)
errors.add(:original_fpfile, ValidationMessages::INVALID_FPFILE) if self.original_fpfile.nil? || self.original_fpfile["key"].nil? || self.original_fpfile["url"].nil?
errors.add(:cropped_fpfile, ValidationMessages::INVALID_FPFILE) if self.cropped_fpfile.nil? || self.cropped_fpfile["key"].nil? || self.cropped_fpfile["url"].nil?
end
end
def email_case_insensitive_uniqueness
# using the case insensitive unique check of active record will downcase the field, which is not what we want--we want to preserve original casing
search = User.where("email ILIKE ?", self.email).first
if search != nil && search != self
errors.add(:email, ValidationMessages::EMAIL_ALREADY_TAKEN)
end
end
def update_email_case_insensitive_uniqueness
# using the case insensitive unique check of active record will downcase the field, which is not what we want--we want to preserve original casing
search = User.where("update_email ILIKE ?", self.update_email).first
if search != nil && search != self
errors.add(:update_email, ValidationMessages::EMAIL_ALREADY_TAKEN)
end
end
def online
@online ||= !self.connections.nil? && self.connections.size > 0
end
def name
return "#{first_name} #{last_name}"
end
def location
loc = self.city.blank? ? '' : self.city
loc = loc.blank? ? self.state : "#{loc}, #{self.state}" unless self.state.blank?
#loc = loc.blank? ? self.country : "#{loc}, #{self.country}" unless self.country.blank?
loc
end
def location= location_hash
unless location_hash.blank?
self.city = location_hash[:city]
self.state = location_hash[:state]
self.country = location_hash[:country]
end if self.city.blank?
end
def musician?
return musician
end
def should_validate_password?
(updating_password || new_record?)
end
def should_confirm_existing_password?
confirm_current_password
end
def end_user_created?
return !administratively_created
end
def friends?(user)
return self.friends.exists?(user)
end
def friend_count
return self.friends.size
end
def liker_count
return self.likers.size
end
def like_count
return self.likes.size
end
def band_like_count
return self.band_likes.size
end
def follower_count
return self.followers.size
end
def following_count
return self.followings.size
end
def band_following_count
return self.band_followings.size
end
def recording_count
return self.recordings.size
end
def session_count
return self.music_sessions.size
end
def confirm_email!
self.email_confirmed = true
end
def my_session_settings
unless self.session_settings.nil?
return JSON.parse(self.session_settings)
else
return ""
end
end
def session_history(user_id, band_id = nil, genre = nil)
return MusicSessionHistory.index(self, user_id, band_id, genre)
end
def session_user_history(user_id, session_id)
return MusicSessionUserHistory.where("music_session_id='#{session_id}'")
end
# always returns a non-null value for photo-url,
# using the generic avatar if no user photo available
def resolved_photo_url
if self.photo_url == nil || self.photo_url == ''
# lame that this isn't environment, but boy this is hard to pass all the way down from jam-web!
"http://www.jamkazam.com/assets/shared/avatar_generic.png"
else
return self.photo_url
end
end
def to_s
return email unless email.nil?
if !first_name.nil? && !last_name.nil?
return first_name + ' ' + last_name
end
return id
end
def set_password(old_password, new_password, new_password_confirmation)
# so that UserObserver knows to send a confirmation email on success
self.setting_password = true
# so that should_validate_password? fires
self.updating_password = true
attributes = { :password => new_password, :password_confirmation => new_password_confirmation }
# taken liberally from Devise::DatabaseAuthenticatable.update_with_password
if valid_password?(old_password)
update_attributes(attributes)
else
self.assign_attributes(attributes)
self.valid?
self.errors.add(:current_password, old_password.blank? ? :blank : :invalid)
end
#clean_up_passwords
end
def self.set_password_from_token(email, token, new_password, new_password_confirmation)
user = User.where("email ILIKE ?", email).first
if user.nil? || user.reset_password_token != token || Time.now - user.reset_password_token_created > 3.days || new_password.length < 6 || new_password != new_password_confirmation
raise JamRuby::JamArgumentError
end
user.reset_password_token = nil
user.reset_password_token_created = nil
user.change_password(new_password, new_password_confirmation)
user.save
end
def change_password(new_password, new_password_confirmation)
# FIXME: Should verify that the new password meets certain quality criteria. Really, maybe that should be a
# verification step.
self.updating_password = true
self.password = new_password
self.password_confirmation = new_password_confirmation
UserMailer.password_changed(self).deliver
end
def self.reset_password(email, base_uri)
user = User.where("email ILIKE ?", email).first
raise JamRuby::JamArgumentError if user.nil?
user.reset_password_token = SecureRandom.urlsafe_base64
user.reset_password_token_created = Time.now
user.save
reset_url = "#{base_uri}/reset_password_token?token=#{user.reset_password_token}&email=#{CGI.escape(email)}"
UserMailer.password_reset(user, reset_url).deliver
user
end
def self.band_index(user_id)
bands = Band.joins(:band_musicians)
.where(:bands_musicians => {:user_id => "#{user_id}"})
return bands
end
def self.recording_index(current_user, user_id)
hide_private = false
# hide private recordings from anyone but the current user
if current_user.id != user_id
hide_private = true
end
if hide_private
recordings = Recording.joins(:musician_recordings)
.where(:musicians_recordings => {:user_id => "#{user_id}"}, :public => true)
else
recordings = Recording.joins(:musician_recordings)
.where(:musicians_recordings => {:user_id => "#{user_id}"})
end
return recordings
end
# given an array of instruments, update a user's instruments
def update_instruments(instruments)
# delete all instruments for this user first
unless self.new_record?
MusicianInstrument.delete_all(["user_id = ?", self.id])
end
# loop through each instrument in the array and save to the db
instruments.each do |musician_instrument_param|
instrument = Instrument.find(musician_instrument_param[:instrument_id])
musician_instrument = MusicianInstrument.new
musician_instrument.user = self
musician_instrument.instrument = instrument
musician_instrument.proficiency_level = musician_instrument_param[:proficiency_level]
musician_instrument.priority = musician_instrument_param[:priority]
musician_instrument.save
self.musician_instruments << musician_instrument
end
end
# this easy_save routine guards against nil sets, but many of these fields can be set to null.
# I've started to use it less as I go forward
def easy_save(first_name, last_name, email, password, password_confirmation, musician, gender,
birth_date, internet_service_provider, city, state, country, instruments, photo_url)
# first name
unless first_name.nil?
self.first_name = first_name
end
# last name
unless last_name.nil?
self.last_name = last_name
end
# email
# !! Email is changed in a dedicated method, 'update_email'
#unless email.nil?
# self.email = email
#end
# password
unless password.nil?
self.password = password
end
# password confirmation
unless password_confirmation.nil?
self.password_confirmation = password_confirmation
end
# musician flag
unless musician.nil?
self.musician = musician
end
# gender
unless gender.nil?
self.gender = gender
end
# birthdate
unless birth_date.nil?
self.birth_date = birth_date
end
# ISP
unless internet_service_provider.nil?
self.internet_service_provider = internet_service_provider
end
# city
unless city.nil?
self.city = city
end
# state
unless state.nil?
self.state = state
end
# country
unless country.nil?
self.country = country
end
# instruments
unless instruments.nil?
update_instruments(instruments)
end
# photo url
unless photo_url.nil?
self.photo_url = photo_url
end
self.updated_at = Time.now.getutc
self.save
end
# helper method for creating / updating a User
def self.save(id, updater_id, first_name, last_name, email, password, password_confirmation, musician, gender,
birth_date, internet_service_provider, city, state, country, instruments, photo_url)
if id.nil?
user = User.new()
else
user = User.find(id)
end
if user.id != updater_id
raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR
end
user.easy_save(first_name, last_name, email, password, password_confirmation, musician, gender,
birth_date, internet_service_provider, city, state, country, instruments, photo_url)
return user
end
def begin_update_email(email, current_password, confirmation_url)
# sets the user model in a state such that it's expecting to have it's email updated
# two columns matter for this; 'update_email_token' and 'update_email'
# confirmation_link is odd in the sense that it can likely only come from www.jamkazam.com (jam-web)
# an observer should be set up to send an email based on this activity
self.updating_email = self.confirm_current_password = true
self.current_password = current_password
self.update_email = email
self.update_email_token = SecureRandom.urlsafe_base64
self.update_email_confirmation_url = "#{confirmation_url}#{self.update_email_token}"
self.save
end
def self.finalize_update_email(update_email_token)
# updates the user model to have a new email address
user = User.find_by_update_email_token!(update_email_token)
user.updated_email = true
user.email = user.update_email
user.update_email_token = nil
user.save
return user
end
def self.create_user_like(user_id, liker_id)
liker = UserLiker.new()
liker.user_id = user_id
liker.liker_id = liker_id
liker.save
end
def self.delete_like(user_id, band_id, liker_id)
if !user_id.nil?
JamRuby::UserLiker.delete_all "(user_id = '#{user_id}' AND liker_id = '#{liker_id}')"
elsif !band_id.nil?
JamRuby::BandLiker.delete_all "(band_id = '#{band_id}' AND liker_id = '#{liker_id}')"
end
end
def self.create_band_like(band_id, liker_id)
liker = BandLiker.new()
liker.band_id = band_id
liker.liker_id = liker_id
liker.save
end
def self.delete_band_like(band_id, liker_id)
JamRuby::BandLiker.delete_all "(band_id = '#{band_id}' AND liker_id = '#{liker_id}')"
end
def self.create_user_following(user_id, follower_id)
follower = UserFollower.new()
follower.user_id = user_id
follower.follower_id = follower_id
follower.save
end
def self.delete_following(user_id, band_id, follower_id)
if !user_id.nil?
JamRuby::UserFollower.delete_all "(user_id = '#{user_id}' AND follower_id = '#{follower_id}')"
elsif !band_id.nil?
JamRuby::BandFollower.delete_all "(band_id = '#{band_id}' AND follower_id = '#{follower_id}')"
end
end
def self.create_band_following(band_id, follower_id)
follower = BandFollower.new()
follower.band_id = band_id
follower.follower_id = follower_id
follower.save
end
def self.delete_band_following(band_id, follower_id)
JamRuby::BandFollower.delete_all "(band_id = '#{band_id}' AND follower_id = '#{follower_id}')"
end
def self.create_favorite(user_id, recording_id)
favorite = UserFavorite.new()
favorite.user_id = user_id
favorite.recording_id = recording_id
favorite.save
end
def self.delete_favorite(user_id, recording_id)
JamRuby::UserFavorite.delete_all "(user_id = '#{user_id}' AND recording_id = '#{recording_id}')"
end
def self.save_session_settings(user, music_session)
unless user.nil?
# only save genre id and description
genres = []
unless music_session.genres.nil?
music_session.genres.each do |genre|
g = Hash.new
g["id"] = genre.id
g["description"] = genre.description
genres << g
end
end
# only save invitation receiver id and name
invitees = []
unless music_session.invitations.nil?
music_session.invitations.each do |invitation|
i = Hash.new
i["id"] = invitation.receiver.id
i["name"] = invitation.receiver.name
invitees << i
end
end
session_settings = { :band_id => music_session.band_id,
:musician_access => music_session.musician_access,
:approval_required => music_session.approval_required,
:fan_chat => music_session.fan_chat,
:fan_access => music_session.fan_access,
:description => music_session.description,
:genres => genres,
:invitees => invitees
}.to_json
user.session_settings = session_settings
user.save
end
end
# throws ActiveRecord::RecordNotFound if instrument is invalid
# throws an email delivery error if unable to connect out to SMTP
def self.signup(first_name, last_name, email, password, password_confirmation, terms_of_service, subscribe_email,
location, instruments, birth_date, musician, photo_url, invited_user, signup_confirm_url)
user = User.new
UserManager.active_record_transaction do |user_manager|
user.first_name = first_name
user.last_name = last_name
user.email = email
user.subscribe_email = subscribe_email
user.terms_of_service = terms_of_service
user.musician = musician
# FIXME: Setting random password for social network logins. This
# is because we have validations all over the place on this.
# The right thing would be to have this null
# Seth: I think we need a flag in the signature of signup to say 'social_signup=true'. If that flag is set,
# then you can do use.updating_password = false and instead set a null password
if password.nil?
user.password = user.password_confirmation = SecureRandom.urlsafe_base64
else
user.password = password
user.password_confirmation = password_confirmation
end
user.admin = false
user.city = location[:city]
user.state = location[:state]
user.country = location[:country]
user.birth_date = birth_date
if user.musician # only update instruments if the user is a musician
unless instruments.nil?
instruments.each do |musician_instrument_param|
instrument = Instrument.find(musician_instrument_param[:instrument_id])
musician_instrument = MusicianInstrument.new
musician_instrument.user = user
musician_instrument.instrument = instrument
musician_instrument.proficiency_level = musician_instrument_param[:proficiency_level]
musician_instrument.priority = musician_instrument_param[:priority]
user.musician_instruments << musician_instrument
end
end
end
user.photo_url = photo_url
if invited_user.nil?
user.can_invite = Limits::USERS_CAN_INVITE
user.email_confirmed = false
user.signup_token = SecureRandom.urlsafe_base64
else
# if you are invited by an admin, we'll say you can invite too.
# but if not, then you can not invite
user.can_invite = invited_user.invited_by_administrator?
# if you came in from an invite and used the same email to signup,
# then we know you are a real human and that your email is valid.
# lucky! we'll log you in immediately
if invited_user.email.casecmp(user.email).zero?
user.email_confirmed = true
user.signup_token = nil
else
user.email_confirmed = false
user.signup_token = SecureRandom.urlsafe_base64
end
# now that the user is saved, let's
if invited_user.autofriend && !invited_user.sender.nil?
# hookup this user with the sender
Friendship.save_using_models(user, invited_user.sender)
end
invited_user.accept!
invited_user.save
if invited_user.errors.any?
raise ActiveRecord::Rollback
end
end
user.save
if user.errors.any?
raise ActiveRecord::Rollback
else
# don't send an signup email if the user was invited already *and* they used the same email that they were invited with
if !invited_user.nil? && invited_user.email.casecmp(user.email).zero?
else
# FIXME:
# It's not standard to require a confirmation when a user signs up with Facebook.
# We should stop asking for it.
#
# any errors here should also rollback the transaction; that's OK. If emails aren't going to be delivered,
# it's already a really bad situation; make user signup again
UserMailer.welcome_message(user, signup_confirm_url.nil? ? nil : (signup_confirm_url + "/" + user.signup_token) ).deliver
end
end
end
return user
end
# this is intended to be development-mode or test-mode only; VRFS-149
# it creates or updates one user per developer, so that we aren't in the business
# of constantly recreating users as we create new dev environments
# We guard against this code running in production mode,
# because otherwise it's a bit of uncomfortable code
# to have sitting around
def self.create_dev_user(first_name, last_name, email, password,
city, state, country, instruments, photo_url)
if Environment.mode == "production"
# short-circuit out
return
end
user = User.find_or_create_by_email(email)
User.transaction do
user.first_name = first_name
user.last_name = last_name
user.email = email
user.password = password
user.password_confirmation = password
user.admin = true
user.email_confirmed = true
user.musician = true
user.city = city
user.state = state
user.country = country
user.terms_of_service = true
if instruments.nil?
instruments = [{:instrument_id => "acoustic guitar", :proficiency_level => 3, :priority => 1}]
end
unless user.new_record?
MusicianInstrument.delete_all(["user_id = ?", user.id])
end
instruments.each do |musician_instrument_param|
instrument = Instrument.find(musician_instrument_param[:instrument_id])
musician_instrument = MusicianInstrument.new
musician_instrument.user = user
musician_instrument.instrument = instrument
musician_instrument.proficiency_level = musician_instrument_param[:proficiency_level]
musician_instrument.priority = musician_instrument_param[:priority]
user.musician_instruments << musician_instrument
end
if photo_url.nil?
user.photo_url = photo_url
end
user.signup_token = nil
user.save
if user.errors.any?
raise ActiveRecord::Rollback
end
end
return user
end
def signup_confirm
self.signup_token = nil
self.confirm_email!
self.save
end
def update_avatar(original_fpfile, cropped_fpfile, crop_selection, aws_bucket)
self.updating_avatar = true
cropped_s3_path = cropped_fpfile["key"]
return self.update_attributes(
:original_fpfile => original_fpfile,
:cropped_fpfile => cropped_fpfile,
:cropped_s3_path => cropped_s3_path,
:crop_selection => crop_selection,
:photo_url => S3Util.url(aws_bucket, cropped_s3_path, :secure => false)
)
end
def delete_avatar(aws_bucket)
User.transaction do
unless self.cropped_s3_path.nil?
S3Util.delete(aws_bucket, File.dirname(self.cropped_s3_path) + '/cropped.jpg')
S3Util.delete(aws_bucket, self.cropped_s3_path)
end
return self.update_attributes(
:original_fpfile => nil,
:cropped_fpfile => nil,
:cropped_s3_path => nil,
:photo_url => nil,
:crop_selection => nil
)
end
end
# throws RecordNotFound if signup token is invalid; i.e., if it's nil, empty string, or not belonging to a user
def self.signup_confirm(signup_token)
if signup_token.nil? || signup_token.empty?
# there are plenty of confirmed users with nil signup_tokens, so we can't look on it
raise ActiveRecord::RecordNotFound
else
UserManager.active_record_transaction do |user_manager|
# throws ActiveRecord::RecordNotFound if invalid
user = User.find_by_signup_token!(signup_token)
user.signup_confirm
return user
end
end
end
# if valid credentials are supplied for an 'active' user, returns the user
# if not authenticated, returns nil
def self.authenticate(email, password)
# remove email_confirmed restriction due to VRFS-378
# we only allow users that have confirmed email to authenticate
# user = User.where('email_confirmed=true').find_by_email(email)
# do a case insensitive search for email, because we store it case sensitive
user = User.where("email ILIKE ?", email).first
if user && user.valid_password?(password)
return user
else
return nil
end
end
def self.search(query, options = { :limit => 10 })
# only issue search if at least 2 characters are specified
if query.nil? || query.length < 2
return []
end
# save query for use in instrument search
search_criteria = query
# create 'anded' statement
query = Search.create_tsquery(query)
if query.nil? || query.length == 0
return []
end
# remove email_confirmed restriction due to VRFS-378
# .where("email_confirmed = true AND (name_tsv @@ to_tsquery('jamenglish', ?) OR users.id in (select user_id from musicians_instruments where instrument_id like '%#{search_criteria.downcase}%'))", query)
return query = User
.where("(name_tsv @@ to_tsquery('jamenglish', ?) OR users.id in (select user_id from musicians_instruments where instrument_id like '%#{search_criteria.downcase}%'))", query)
.limit(options[:limit])
end
# devise compatibility
#def encrypted_password
# logger.debug("password digest returned #{self.password_digest}")
# self.password_digest
#end
#def encrypted_password=(encrypted_password)
# self.password_digest = encrypted_password
#end
# end devise compatibility
private
def create_remember_token
self.remember_token = SecureRandom.urlsafe_base64
end
def stringify_avatar_info
# fpfile comes in as a hash, which is a easy-to-use and validate form. However, we store it as a VARCHAR,
# so we need t oconvert it to JSON before storing it (otherwise it gets serialized as a ruby object)
# later, when serving this data out to the REST API, we currently just leave it as a string and make a JSON capable
# client parse it, because it's very rare when it's needed at all
self.original_fpfile = original_fpfile.to_json if !original_fpfile.nil?
self.cropped_fpfile = cropped_fpfile.to_json if !cropped_fpfile.nil?
self.crop_selection = crop_selection.to_json if !crop_selection.nil?
end
end
end

View File

@ -0,0 +1,16 @@
module JamRuby
class UserAuthorization < ActiveRecord::Base
attr_accessible :provider, :uid, :token, :token_expiration
self.table_name = "user_authorizations"
self.primary_key = 'id'
belongs_to :user, :class_name => "JamRuby::User", :foreign_key => "user_id"
validates :provider, :uid, :presence => true
# token and token_expiration can be missing
end
end

View File

@ -0,0 +1,11 @@
module JamRuby
class UserFollower < ActiveRecord::Base
self.table_name = "users_followers"
self.primary_key = 'id'
belongs_to :user, :class_name => "JamRuby::User", :foreign_key => "user_id", :inverse_of => :inverse_followers
belongs_to :follower, :class_name => "JamRuby::User", :foreign_key => "follower_id", :inverse_of => :followers
end
end

View File

@ -0,0 +1,11 @@
module JamRuby
class UserFollowing < ActiveRecord::Base
self.table_name = "users_followers"
self.primary_key = 'id'
belongs_to :user, :class_name => "JamRuby::User", :foreign_key => "follower_id", :inverse_of => :inverse_followings
belongs_to :following, :class_name => "JamRuby::User", :foreign_key => "user_id", :inverse_of => :followings
end
end

View File

@ -0,0 +1,10 @@
module JamRuby
class UserLike < ActiveRecord::Base
self.table_name = "users_likers"
self.primary_key = 'id'
belongs_to :user, :class_name => "JamRuby::User", :foreign_key => "user_id", :inverse_of => :inverse_likes
end
end

View File

@ -0,0 +1,10 @@
module JamRuby
class UserLiker < ActiveRecord::Base
self.table_name = "users_likers"
self.primary_key = 'id'
belongs_to :user, :class_name => "JamRuby::User", :foreign_key => "liker_id", :inverse_of => :inverse_likers
end
end

Some files were not shown because too many files have changed in this diff Show More