merging jam-ruby into ruby
This commit is contained in:
commit
6ea4bae05c
|
|
@ -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
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
up.connopts=dbname:jam_ruby
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
--color
|
||||||
|
--format progress
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
jamruby
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
ruby-2.0.0-p247
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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`
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
#!/usr/bin/env rake
|
||||||
|
require "bundler/gem_tasks"
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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"
|
||||||
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
development:
|
||||||
|
access_key_id: AKIAIFFBNBRQG5YQ5WHA
|
||||||
|
secret_access_key: XLq2mpJHNyA0bN7GBSdYyF/pWjfzGkDx92b1C+Wv
|
||||||
|
|
||||||
|
test:
|
||||||
|
access_key_id: AKIAIFFBNBRQG5YQ5WHA
|
||||||
|
secret_access_key: XLq2mpJHNyA0bN7GBSdYyF/pWjfzGkDx92b1C+Wv
|
||||||
|
|
||||||
|
production:
|
||||||
|
access_key_id: AKIAIFFBNBRQG5YQ5WHA
|
||||||
|
secret_access_key: XLq2mpJHNyA0bN7GBSdYyF/pWjfzGkDx92b1C+Wv
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
test:
|
||||||
|
adapter: postgresql
|
||||||
|
database: jam_ruby_test
|
||||||
|
host: localhost
|
||||||
|
pool: 3
|
||||||
|
username: postgres
|
||||||
|
password: postgres
|
||||||
|
timeout: 2000
|
||||||
|
encoding: unicode
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
Feedback Received
|
||||||
|
|
||||||
|
From <%= @email %>:
|
||||||
|
|
||||||
|
<%= @body %>
|
||||||
|
|
||||||
|
|
||||||
|
This email was received because someone left feedback at http://www.jamkazam.com/corp/contact
|
||||||
|
|
@ -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 %>
|
||||||
|
|
@ -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 %>.
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
Welcome to the JamKazam Beta!
|
||||||
|
To signup, please go to the 'create account' page: <%= @signup_url %>.
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
<% provide(:title, 'Jamkazam Password Changed') %>
|
||||||
|
|
||||||
|
You just changed your password at Jamkazam.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
You just changed your password!
|
||||||
|
|
@ -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>.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Visit this link so that you can change your Jamkazam password: <a href="<%= @password_reset_url %>">Reset Password</a>.
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
<% provide(:title, 'Jamkazam Email Confirmed') %>
|
||||||
|
|
||||||
|
<b><%= @user.email %></b> has been confirmed as your new email address.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
<%= @user.email %> has been confirmed as your new email address.
|
||||||
|
|
@ -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>.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Please click the following link to confirm your change in email: <%= @user.update_email_confirmation_url %>.
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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 %>.
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
|
|
@ -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 © <%= Time.now.year %> JamKazam, Inc. All rights reserved.</font>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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 © <%= Time.now.year %> JamKazam, Inc. All rights reserved.</font>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
module JamRuby
|
||||||
|
class DbUtil
|
||||||
|
def self.create(connection_hash)
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
module JamRuby
|
||||||
|
class PermissionError < Exception
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# initialize actionmailer
|
||||||
|
ActionMailer::Base.raise_delivery_errors = true
|
||||||
|
ActionMailer::Base.view_paths = File.expand_path('../../jam_ruby/app/views/', __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
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
Loading…
Reference in New Issue