diff --git a/Gemfile b/Gemfile index 75a43a941..7f01fde4b 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,8 @@ #ruby=1.9.3 source 'https://rubygems.org' -source 'https://jamjam:blueberryjam@www.jamkazam.com/gems/' +unless ENV["LOCAL_DEV"] == "1" + source 'https://jamjam:blueberryjam@www.jamkazam.com/gems/' +end # Look for $WORKSPACE, otherwise use "workspace" as dev path. workspace = ENV["WORKSPACE"] || "~/workspace" diff --git a/lib/jam_ruby.rb b/lib/jam_ruby.rb index 0f82993eb..e2c652195 100644 --- a/lib/jam_ruby.rb +++ b/lib/jam_ruby.rb @@ -10,8 +10,9 @@ require "will_paginate/active_record" require "action_mailer" require "devise" require "sendgrid" -require "jam_ruby/constants/validation_messages" 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" @@ -59,6 +60,7 @@ require "jam_ruby/models/user_favorite" require "jam_ruby/models/search" require "jam_ruby/models/recording" require "jam_ruby/models/recorded_track" +require "jam_ruby/models/mix" include Jampb diff --git a/lib/jam_ruby/constants/notification_types.rb b/lib/jam_ruby/constants/notification_types.rb new file mode 100644 index 000000000..07a68a62c --- /dev/null +++ b/lib/jam_ruby/constants/notification_types.rb @@ -0,0 +1,8 @@ +module NotificationTypes + + FRIEND_UPDATE = "FRIEND_UPDATE" + FRIEND_REQUEST = "FRIEND_REQUEST" + FRIEND_REQUEST_ACCEPTED = "FRIEND_REQUEST_ACCEPTED" + SESSION_INVITATION = "SESSION_INVITATION" + +end \ No newline at end of file diff --git a/lib/jam_ruby/message_factory.rb b/lib/jam_ruby/message_factory.rb index fab86c1b9..6396895de 100644 --- a/lib/jam_ruby/message_factory.rb +++ b/lib/jam_ruby/message_factory.rb @@ -111,23 +111,30 @@ 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(user_id, name, photo_url, friend_id) - friend_request = Jampb::FriendRequest.new(:user_id => user_id, :name => name, :photo_url => photo_url, :friend_id => friend_id) + 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) - friend_request_accepted = Jampb::FriendRequestAccepted.new(:friend_id => friend_id, :name => name, :photo_url => photo_url, :user_id => user_id) + 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 - # create a friend update message - def friend_update(user_id, online) - friend = Jampb::FriendUpdate.new(:user_id => user_id, :online => online) - return Jampb::ClientMessage.new(:type => ClientMessage::Type::FRIEND_UPDATE, :route_to => USER_TARGET_PREFIX + user_id, :friend_update => friend) - end ############## P2P CLIENT MESSAGES ################# # send a request to do a ping diff --git a/lib/jam_ruby/models/artifact_update.rb b/lib/jam_ruby/models/artifact_update.rb index 9367b25c3..d224f6120 100644 --- a/lib/jam_ruby/models/artifact_update.rb +++ b/lib/jam_ruby/models/artifact_update.rb @@ -7,12 +7,25 @@ module JamRuby self.primary_key = 'id' attr_accessible :version, :uri, :sha1, :environment, :product + + # ORDER MATTERS HERE- before_save for this method must be declared before mount_uploader: https://github.com/jnicklas/carrierwave/wiki/Known-Issues + before_save :update_uri_attributes mount_uploader :uri, ArtifactUploader validate :version, :presence => true validate :uri, :presence => true - validate :sha1, :presence => false - validate :environment, presence => true + validate :sha1, :presence => true + validate :size, :presence => true + validate :environment, :presence => true validate :product, :inclusion => {:in => PRODUCTS} + + private + + def update_uri_attributes + if uri.present? && uri_changed? + self.size = uri.file.size + self.sha1 = Digest::MD5.hexdigest(File.read(uri.current_path)) + end + end end end diff --git a/lib/jam_ruby/models/friend_request.rb b/lib/jam_ruby/models/friend_request.rb index a221402ef..348da4162 100644 --- a/lib/jam_ruby/models/friend_request.rb +++ b/lib/jam_ruby/models/friend_request.rb @@ -27,7 +27,7 @@ module JamRuby friend_request.save # send notification - # Notification.send_friend_request(user_id, friend_id) + Notification.send_friend_request(friend_request.id, user_id, friend_id) else ActiveRecord::Base.transaction do @@ -41,7 +41,7 @@ module JamRuby Friendship.save(friend_request.user_id, friend_request.friend_id) # send notification - # Notification.send_friend_request_accepted(user_id, friend_id) + Notification.send_friend_request_accepted(friend_request.user_id, friend_request.friend_id) end end end diff --git a/lib/jam_ruby/models/mix.rb b/lib/jam_ruby/models/mix.rb new file mode 100644 index 000000000..9b9a4adac --- /dev/null +++ b/lib/jam_ruby/models/mix.rb @@ -0,0 +1,57 @@ +# FIXME: +# Need to pass in the JSON spec for the mix and put that in the migration. + +module JamRuby + class Mix < ActiveRecord::Base + MAX_MIX_TIME = 7200 # 2 hours + + self.primary_key = 'id' + belongs_to :recording, :class_name => "JamRuby::Recording", :inverse_of => :mix + + def self.schedule(recording_id, user_id, description, spec) + # This would have made it so you couldn't have more than one mix of a recording+owner + #raise unless self.where(:recording_id => recording_id, :owner_id => user_id).size == 0 + recording = Recording.find(recording_id) + raise if recording.nil? + raise if recording.owner_id != user_id + + mix = Mix.new + mix.recording_id = recording_id + mix.owner_id = user_id + mix.description = description + mix.spec = spec + 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(url) + raise if url.nil? + + self.completed_at = Time.now + self.url = url + save + end + + end +end diff --git a/lib/jam_ruby/models/musician_instrument.rb b/lib/jam_ruby/models/musician_instrument.rb index d8ef69378..503a4e6f3 100644 --- a/lib/jam_ruby/models/musician_instrument.rb +++ b/lib/jam_ruby/models/musician_instrument.rb @@ -14,6 +14,5 @@ module JamRuby def description @description = self.instrument.description end - end end \ No newline at end of file diff --git a/lib/jam_ruby/models/notification.rb b/lib/jam_ruby/models/notification.rb index 1247146ec..7e7dc44a8 100644 --- a/lib/jam_ruby/models/notification.rb +++ b/lib/jam_ruby/models/notification.rb @@ -1,6 +1,62 @@ 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 @@ -9,9 +65,8 @@ module JamRuby @@mq_router = MQRouter.new @@message_factory = MessageFactory.new - def index(user_id) - results = Notification.where(:user_id => user_id).limit(50) - return results + def delete_all(session_id) + Notification.delete_all "(session_id = '#{session_id}')" end ################### HELPERS ################### @@ -41,10 +96,39 @@ module JamRuby 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." + + else + return "" + # when "friend_joined_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 + end + end + ################### FRIEND UPDATE ################### def send_friend_update(user_id, online, connection) + user = User.find(user_id) + # (1) create notification - msg = @@message_factory.friend_update(user_id, online) + 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) # (2) get all of this user's friends friend_ids = retrieve_friends(connection, user_id) @@ -54,54 +138,58 @@ module JamRuby end ################### FRIEND REQUEST ################### - def send_friend_request(user_id, friend_id) + def send_friend_request(friend_request_id, user_id, friend_id) user = User.find(user_id) - # (1) create notification - msg = @@message_factory.friend_request(user_id, user.name, user.photo_url, friend_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) send notification + # (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) - - # (3) save to database - # notification = Notification.new - # notification.type = "friend_request" - # notification.source_user_id = user_id - # notification.target_user_id = friend_id - # notification.save end ############### FRIEND REQUEST ACCEPTED ############### def send_friend_request_accepted(user_id, friend_id) friend = User.find(friend_id) - # (1) create notification - msg = @@message_factory.friend_request_accepted(friend_id, friend.name, friend.photo_url, user_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) send notification + # (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) - - # (3) save to database - # notification = Notification.new - # notification.type = "friend_request_accepted" - # notification.source_user_id = friend_id - # notification.target_user_id = user_id - # notification.save end ################## SESSION INVITATION ################## def send_session_invitation(receiver_id, invitation_id) - # (1) create notification + # (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) - # (2) send notification + # (3) send notification @@mq_router.publish_to_user(receiver_id, msg) - - # (3) save to database - # notification = Notification.new - # notification.type = "session_invitation" - # notification.target_user_id = receiver_id end def send_session_left(music_session, connection, user) @@ -115,31 +203,28 @@ module JamRuby def send_join_request(music_session, join_request, sender, text) - # (1) create notification + # (1) save to database + # notification = Notification.new + + # (2) create notification msg = @@message_factory.join_request(music_session.id, join_request.id, sender.name, text) - # (2) send notification + # (3) send notification @@mq_router.server_publish_to_session(music_session, msg) - - # (3) save to database - # notification = Notification.new end def send_session_joined(connection, user) - # (1) create notification + # (1) save to database + + # (2) create notification msg = @@message_factory.user_joined_music_session(connection.music_session.id, user.id, user.name) - # (2a) send notification to session members + # (3a) send notification to session members @@mq_router.server_publish_to_session(connection.music_session, msg, sender = {:client_id => connection.client_id}) - # TODO: (2b) retrieve all friends and followers of user and send notification to them as well - - # (3) save to database + # TODO: (3b) retrieve all friends and followers of user and send notification to them as well end - - # TODO: add methods to delete Notifications based on user id, session id, etc. - end end end \ No newline at end of file diff --git a/lib/jam_ruby/models/recording.rb b/lib/jam_ruby/models/recording.rb index 3ea932606..07b88302b 100644 --- a/lib/jam_ruby/models/recording.rb +++ b/lib/jam_ruby/models/recording.rb @@ -7,6 +7,7 @@ module JamRuby 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_one :mix, :class_name => "JamRuby::Mix", :inverse_of => :recording has_many :recorded_tracks, :class_name => "JamRuby::RecordedTrack", :foreign_key => :recording_id @@ -35,6 +36,14 @@ module JamRuby recordings_users.recording_id = recordings.id } ) + .joins( + %Q{ + LEFT OUTER JOIN + mixes + ON + recordings.id = mixes.recording_id + } + ) .order( %Q{ recordings.created_at DESC diff --git a/lib/jam_ruby/models/user.rb b/lib/jam_ruby/models/user.rb index ff3860cd1..ba4b3e8a9 100644 --- a/lib/jam_ruby/models/user.rb +++ b/lib/jam_ruby/models/user.rb @@ -61,6 +61,10 @@ module JamRuby has_many :favorites, :class_name => "JamRuby::UserFavorite", :foreign_key => "user_id" has_many :inverse_favorites, :through => :favorites, :class_name => "JamRuby::User" + # 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" diff --git a/lib/jam_ruby/mq_router.rb b/lib/jam_ruby/mq_router.rb index 2aec9a117..8fb3b30a6 100644 --- a/lib/jam_ruby/mq_router.rb +++ b/lib/jam_ruby/mq_router.rb @@ -11,9 +11,7 @@ class MQRouter @@log = Logging.logger[MQRouter] end - def access_music_session(music_session, user) - if music_session.nil? raise ArgumentError, 'specified session not found' end @@ -50,7 +48,6 @@ class MQRouter # sends a message to a client with no checking of permissions (RAW USAGE) # this method deliberately has no database interactivity/active_record objects def publish_to_client(client_id, client_msg, sender = {:client_id => ""}) - EM.schedule do sender_client_id = sender[:client_id] @@ -64,7 +61,6 @@ class MQRouter # sends a message to a session with no checking of permissions (RAW USAGE) # this method deliberately has no database interactivity/active_record objects def publish_to_session(music_session_id, client_ids, client_msg, sender = {:client_id => ""}) - EM.schedule do sender_client_id = sender[:client_id] @@ -81,7 +77,6 @@ class MQRouter # sends a message to a user with no checking of permissions (RAW USAGE) # this method deliberately has no database interactivity/active_record objects def publish_to_user(user_id, user_msg) - EM.schedule do @@log.debug "publishing to user:#{user_id} from server" # put it on the topic exchange for users @@ -92,7 +87,6 @@ class MQRouter # sends a message to a list of friends with no checking of permissions (RAW USAGE) # this method deliberately has no database interactivity/active_record objects def publish_to_friends(friend_ids, user_msg, from_user_id) - EM.schedule do friend_ids.each do |friend_id| @@log.debug "publishing to friend:#{friend_id} from user #{from_user_id}" diff --git a/spec/jam_ruby/models/artifact_update_spec.rb b/spec/jam_ruby/models/artifact_update_spec.rb index c4ed36193..24075e310 100644 --- a/spec/jam_ruby/models/artifact_update_spec.rb +++ b/spec/jam_ruby/models/artifact_update_spec.rb @@ -1,4 +1,5 @@ require 'spec_helper' +require 'digest/md5' describe ArtifactUpdate do @@ -23,15 +24,14 @@ describe ArtifactUpdate do artifact.product = 'JamClient/Win32' artifact.version = '0.1.1' artifact.uri = File.open(ARTIFACT_FILE) - artifact.sha1 = 'blahablahblah' - artifact.save! artifact.environment.should == "public" artifact.product.should == "JamClient/Win32" artifact.version.should == "0.1.1" File.basename(artifact.uri.path).should == ARTIFACT_FILE - artifact.sha1.should == "blahablahblah" + artifact.sha1.should == Digest::MD5.hexdigest(File.read(ARTIFACT_FILE)) + artifact.size.should == File.size(ARTIFACT_FILE) found = ArtifactUpdate.find_by_product_and_version('JamClient/Win32', '0.1.1') artifact.should == found diff --git a/spec/jam_ruby/models/mix_spec.rb b/spec/jam_ruby/models/mix_spec.rb new file mode 100644 index 000000000..5e7753762 --- /dev/null +++ b/spec/jam_ruby/models/mix_spec.rb @@ -0,0 +1,64 @@ +require 'spec_helper' + +describe Mix do + before do + @user = FactoryGirl.create(:user) + @connection = FactoryGirl.create(:connection, :user => @user) + @instrument = FactoryGirl.create(:instrument, :description => 'a great instrument') + @track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument) + @music_session = FactoryGirl.create(:music_session, :creator => @user, :musician_access => true) + @music_session.connections << @connection + @music_session.save + @recording = Recording.start(@music_session.id, @user) + @recording.stop + @mix = Mix.schedule(@recording.id, @user.id, "description", "{}") + end + + it "should create a mix for a user's recording properly" do + @mix.recording_id.should == @recording.id + @mix.owner_id.should == @user.id + @mix.description.should == "description" + @mix.spec.should == "{}" + @mix.url.should be_nil + @mix.mix_server.should be_nil + @mix.started_at.should be_nil + @mix.completed_at.should be_nil + end + + it "should fail to create a mix if the userid doesn't own the recording" do + @user2 = FactoryGirl.create(:user) + expect { Mix.schedule(@recording.id, @user2.id) }.to raise_error + end + + it "should fail if the recording doesn't exist" do + expect { @mix2 = Mix.schedule("bad_recording_id", @user.id) }.to raise_error + end + + it "should return a mix when the cron asks for it" do + this_mix = Mix.next("server") + this_mix.id.should == @mix.id + @mix.reload + @mix.started_at.should_not be_nil + @mix.mix_server.should == "server" + @mix.completed_at.should be_nil + end + + it "should record when a mix has finished" do + Mix.find(@mix.id).finish("http://blah") + @mix.reload + @mix.completed_at.should_not be_nil + @mix.url.should == "http://blah" + end + + it "should re-run a mix if it was started a long time ago" do + this_mix = Mix.next("server") + @mix.reload + @mix.started_at -= 1000000 + @mix.save + this_mix = Mix.next("server") + this_mix.id.should == @mix.id + end + +end + +