require 'aasm' module JamRuby class Connection < ActiveRecord::Base include HtmlSanitize include AASM # client_types TYPE_CLIENT = 'client' TYPE_BROWSER = 'browser' TYPE_LATENCY_TESTER = 'latency_tester' CLIENT_TYPES = [TYPE_CLIENT, TYPE_BROWSER, TYPE_LATENCY_TESTER] attr_accessor :joining_session self.primary_key = 'id' belongs_to :user, :class_name => "JamRuby::User" belongs_to :music_session, :class_name => "JamRuby::ActiveMusicSession", foreign_key: :music_session_id has_one :latency_tester, class_name: 'JamRuby::LatencyTester', foreign_key: :client_id, primary_key: :client_id has_many :tracks, :class_name => "JamRuby::Track", :inverse_of => :connection, :foreign_key => 'connection_id', :dependent => :delete_all has_many :backing_tracks, :class_name => "JamRuby::BackingTrack", :inverse_of => :connection, :foreign_key => 'connection_id', :dependent => :delete_all has_many :video_sources, :class_name => "JamRuby::VideoSource", :inverse_of => :connection, :foreign_key => 'connection_id', :dependent => :delete_all has_one :jamblaster, class_name: "JamRuby::Jamblaster", foreign_key: "client_id" validates :metronome_open, :inclusion => {:in => [true, false]} validates :as_musician, :inclusion => {:in => [true, false, nil]} validates :client_type, :inclusion => {:in => CLIENT_TYPES} validates_numericality_of :last_jam_audio_latency, greater_than: 0, :allow_nil => true validate :can_join_music_session, :if => :joining_session? validate :user_or_latency_tester_present # this is no longer required with the new no-input profile #after_save :require_at_least_one_track_when_in_session, :if => :joining_session? after_create :did_create after_save :report_add_participant 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 in_session? !music_session_id.nil? end def in_scoring_timeout? scoring_timeout > Time.now end def did_expire self.destroy end def joining_session? joining_session end def same_network_jamblasters # return all jamblasters that are currently connected with the same public IP address (don't include this one though) Jamblaster.joins(:connection).where("connections.ip_address = ?", ip_address).where("connections.id != ?", id).limit(100) end def can_join_music_session # puts "can_join_music_session: #{music_session_id} was #{music_session_id_was}" if music_session_id_changed? if music_session_id_changed? and !(music_session_id_was.nil? or music_session_id_was.blank?) errors.add(:music_session, ValidationMessages::CANT_JOIN_MULTIPLE_SESSIONS) return false end if music_session.nil? errors.add(:music_session, ValidationMessages::MUSIC_SESSION_MUST_BE_SPECIFIED) return false end if as_musician unless self.user.musician errors.add(:as_musician, ValidationMessages::FAN_CAN_NOT_JOIN_AS_MUSICIAN) return false end if music_session.musician_access if music_session.approval_required if !(music_session.music_session.creator == user || music_session.creator == user || music_session.invited_musicians.exists?(user.id)) errors.add(:approval_required, ValidationMessages::INVITE_REQUIRED) return false end end else if !(music_session.music_session.creator == user || music_session.creator == user || music_session.invited_musicians.exists?(user.id)) errors.add(:musician_access, ValidationMessages::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, ValidationMessages::FANS_CAN_NOT_JOIN) return false end end if music_session.is_recording? errors.add(:music_session, ValidationMessages::CANT_JOIN_RECORDING_SESSION) end # unless user.admin? # num_sessions = Connection.where(:user_id => user_id) # .where(["(music_session_id IS NOT NULL) AND (aasm_state != ?)",EXPIRED_STATE.to_s]) # .count # if 0 < num_sessions # errors.add(:music_session, ValidationMessages::CANT_JOIN_MULTIPLE_SESSIONS) # 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 def did_create # self.user.update_lat_lng(self.ip_address) if self.user && self.ip_address end def report_add_participant if self.music_session_id_changed? && self.music_session.present? && self.connected? && self.as_musician? && 0 < (count = self.music_session.connected_participant_count) #GoogleAnalyticsEvent.report_session_participant(count) end true end def join_the_session(music_session, as_musician, tracks, user, audio_latency, client_role = nil, parent_client_id = nil, videos=nil) self.music_session_id = music_session.id self.as_musician = as_musician == true # this is deliberate; otherwise we create a warning in one our tests that passes 'blarg' (rails warning about casting strings to false) self.joining_session = true self.joined_session_at = Time.now self.client_role = client_role self.parent_client_id = parent_client_id associate_tracks(tracks) unless tracks.nil? associate_videos(videos) unless videos.nil? self.save # if user joins the session as a musician, update their addr and location if as_musician user.update_addr_loc(self, User::JAM_REASON_JOIN) user.update_audio_latency(self, audio_latency) if audio_latency # try not to let a previously recorded value get nil'ed end end def associate_tracks(tracks) self.tracks.clear() unless tracks.nil? tracks.each do |track| t = Track.new t.instrument = Instrument.find_by_id(track["instrument_id"]) || Instrument.find('acoustic guitar') t.connection = self t.sound = track["sound"] t.client_track_id = track["client_track_id"] t.client_resource_id = track["client_resource_id"] t.save # todo what if it fails? self.tracks << t end end end def associate_videos(videos) unless videos.nil? self.video_sources.clear() videos.each do |video| v = VideoSource.new v.connection = self v.client_video_source_id = video["client_video_source_id"] v.save # todo what if it fails? self.video_sources << v end end end def self.update_locidispids(use_copied = true) # using addr, we can rebuild locidispid # this will set a connections's _locidispid = 0 if there are no geoiplocations/blocks that match their IP address, or if there are no JamIsps that match the IP address # otherwise, locidispid will be updated to the correct new value. # updates all connections's locidispids table_suffix = use_copied ? '_copied' : '' Connection.connection.execute("UPDATE connections SET locidispid = COALESCE((SELECT geolocs.locid as geolocid FROM geoipblocks#{table_suffix} as geoblocks INNER JOIN geoiplocations#{table_suffix} as geolocs ON geoblocks.locid = geolocs.locid WHERE geoblocks.geom && ST_MakePoint(addr, 0) AND addr BETWEEN geoblocks.beginip AND geoblocks.endip LIMIT 1) * 1000000::bigint +(SELECT coid FROM jamisp#{table_suffix} as jisp WHERE geom && ST_MakePoint(addr, 0) AND addr BETWEEN beginip AND endip LIMIT 1), 0) ").check end def self.after_maxmind_import update_locidispids end def self.stats stats = {} CLIENT_TYPES.each do |type| stats[type] = 0 end Connection.select('count(client_type) AS client_type_count, client_type').group('client_type').all.each do |result| stats[result['client_type']] = result['client_type_count'].to_i end result = Connection.select('count(id) AS total, count(CASE WHEN scoring_timeout > NOW() THEN 1 ELSE null END) AS scoring_timeout_count, count(music_session_id) AS in_session, count(as_musician) AS musicians, count(CASE WHEN udp_reachable THEN 1 ELSE null END) AS udp_reachable_count, count(CASE WHEN is_network_testing THEN 1 ELSE null END) AS is_network_testing_count')[0] stats['count'] = result['total'].to_i stats['scoring_timeout'] = result['scoring_timeout_count'].to_i stats['in_session'] = result['in_session'].to_i stats['musicians'] = result['musicians'].to_i stats['udp_reachable'] = result['udp_reachable_count'].to_i stats['networking_testing'] = result['is_network_testing_count'].to_i stats end private def require_at_least_one_track_when_in_session if tracks.count == 0 errors.add(:tracks, ValidationMessages::SELECT_AT_LEAST_ONE) end end def user_or_latency_tester_present if user.nil? && client_type != TYPE_LATENCY_TESTER puts client_type errors.add(:connection, ValidationMessages::USER_OR_LATENCY_TESTER_PRESENT) end end end end