app/models/reservation.rb
# frozen_string_literal: true
class Reservation < ActiveRecord::Base
belongs_to :user
belongs_to :server
belongs_to :server_config, optional: true
belongs_to :whitelist, optional: true
has_many :log_uploads
has_many :reservation_players
has_many :ratings
has_many :reservation_statuses
has_many :server_statistics
before_validation :calculate_duration
before_create :generate_logsecret
after_create :generate_initial_status
delegate :donator?, to: :user, prefix: false
include ReservationServerInformation
include ReservationValidations
include Mitigations
attr_accessor :extending, :rcon_command
def self.cleanup_age_in_days
(SITE_HOST == 'au.serveme.tf' && 7) || 30
end
def self.with_user_and_server
includes(user: :groups).includes(server: :location)
end
def self.ordered
with_user_and_server.order('starts_at DESC')
end
def self.within_time_range(start_time, end_time)
(where(starts_at: start_time...end_time).ordered +
where(ends_at: start_time...end_time).ordered)
end
def self.future
where('reservations.starts_at >= ?', Time.current)
end
def self.current
where('reservations.starts_at <= ? AND reservations.ends_at >= ?', Time.current, Time.current)
end
def to_s
"#{id}: #{human_timerange}"
end
def human_timerange
"#{I18n.l(starts_at, format: :datepicker)} - #{I18n.l(ends_at, format: :time)}"
end
def now?
times_entered? && (starts_at < Time.current && ends_at > Time.current)
end
def active?
now? && provisioned?
end
def past?
ends_at && ends_at <= Time.current
end
def younger_than_cleanup_age?
ends_at > self.class.cleanup_age_in_days.days.ago
end
def future?
starts_at > Time.current
end
def schedulable?
!persisted? || (persisted? && !active? && !past?)
end
def collides?
colliding_reservations.any?
end
def colliding_reservations
(own_colliding_reservations + other_users_colliding_reservations).uniq
end
def own_colliding_reservations
@own_colliding_reservations ||= CollisionFinder.new(Reservation.where(user_id: user.id), self).colliding_reservations
end
def other_users_colliding_reservations
@other_users_colliding_reservations ||= CollisionFinder.new(Reservation.where(server_id: server.id), self).colliding_reservations
end
def collides_with_own_reservation?
own_colliding_reservations.any?
end
def collides_with_own_reservation_on_same_server?
own_colliding_reservations.any? { |r| r.server_id == server_id }
end
def collides_with_other_users_reservation?
other_users_colliding_reservations.any?
end
def extend!
return unless less_than_1_hour_left?
self.extending = true
self.ends_at = ends_at + user.reservation_extension_time
self.inactive_minute_counter = 0
save
end
def less_than_1_hour_left?
active? && time_left < 1.hour
end
def just_started?
starts_at > 1.minute.ago
end
def nearly_over?
time_left < 10.minutes
end
def time_left
ends_at - Time.current
end
def warn_nearly_over
time_left_in_minutes = (time_left / 60.0).ceil
time_left_text = I18n.t(:timeleft, count: time_left_in_minutes)
server.rcon_say("This reservation will end in less than #{time_left_text}, if this server is not yet booked by someone else, you can say !extend for more time")
server.rcon_disconnect
end
def cancellable?
future?
end
def tv_password
self[:tv_password].presence || 'tv'
end
def tv_relaypassword
self[:tv_relaypassword].presence || self[:tv_password].presence || 'tv'
end
def formatted_starts_at
starts_at.utc.strftime('%Y%m%d')
end
def inactive_too_long?
inactive_minute_counter >= inactive_minute_limit
end
def inactive_minute_limit
return 240 if user && (user.admin? || user.donator?)
45
end
def custom_whitelist_content
WhitelistTf.find_by_tf_whitelist_id(custom_whitelist_id).try(:content)
end
def calculate_duration
self.duration = (ends_at.to_i - starts_at.to_i)
end
def generate_logsecret
self.logsecret ||= rand(2**128)
end
def self.played_in(steam_uid)
joins(:reservation_players).where('reservation_players.steam_uid = ? AND reservations.starts_at > ? AND reservations.ended = ?', steam_uid, 31.days.ago, true).ordered
end
def start_reservation
reservation_manager.start_reservation
end
def update_reservation
reservation_manager.update_reservation
end
def end_reservation
reservation_manager.end_reservation
end
def reusable_attributes
attributes.slice('server_id', 'password', 'rcon', 'tv_password', 'server_config_id', 'whitelist_id', 'custom_whitelist_id', 'first_map', 'enable_demos_tf')
end
def template_attributes
attributes.slice(*self.class.template_attribute_names)
end
def self.template_attribute_names
%w[server_config_id whitelist_id custom_whitelist_id first_map]
end
# rubocop:disable Naming/AccessorMethodName
def get_binding
binding
end
# rubocop:enable Naming/AccessorMethodName
def times_entered?
starts_at && ends_at
end
def status_update(status)
reservation_statuses.create!(status: status)
end
def lobby?
Rails.cache.fetch("reservation_#{id}_lobby") do
tags = server.rcon_exec('sv_tags').to_s
true if (tags && (tags.include?('TF2Center') || tags.include?('TF2Stadium'))) || tags.include?('TF2Pickup')
end
end
def api_keys_rcon_contents
contents = "logstf_apikey \"#{user.logs_tf_api_key.presence || Rails.application.credentials.dig(:logs_tf, :api_key)}\"; sm_web_rcon_url \"#{SITE_URL}/reservations/#{id}/rcon\""
if enable_demos_tf?
contents + "; sm_demostf_apikey \"#{user.demos_tf_api_key.presence || Rails.application.credentials.dig(:demos_tf, :api_key)}\""
else
contents
end
end
def status
return 'Ended' if past?
status_messages = reservation_statuses.pluck(:status)
return 'Ended' if status_messages.include?('Finished zipping logs and demos')
return 'Ending' if status_messages.include?('Ending')
return 'SDR Ready' if server&.sdr? && sdr_ip.present?
return 'Ready' if server_statistics.any? && !server.sdr?
return 'Ready' if status_messages.grep(/\AServer finished loading map/).any? && !server.sdr?
return 'Server updating, please be patient' if status_messages.grep(/\AServer outdated/).any?
return 'Starting' if status_messages.include?('Starting')
return 'Waiting to start' if status_messages.include?('Waiting to start')
'Unknown'
end
def poor_rcon_password?
rcon.nil? || rcon.size < 8
end
def generate_rcon_password!
self.rcon = FriendlyPasswordGenerator.generate
end
def whitelist_ip
return user.current_sign_in_ip if user.current_sign_in_ip && IPAddr.new(user.current_sign_in_ip).ipv4?
return user.reservation_players.last.ip if user.reservation_players.exists?
"direct.#{SITE_HOST}"
end
def logs_tf_url
"http://logs.tf/search/log?s=#{SITE_HOST}+%23#{id}"
end
def save_sdr_info(server_info)
return if server_info.ip.nil?
previous_server_sdr_ip = server.last_sdr_ip
previous_server_sdr_port = server.last_sdr_port
update_columns(
sdr_ip: server_info.ip,
sdr_port: server_info.port,
sdr_tv_port: server_info.port + 1
)
server.update_columns(
last_sdr_ip: server_info.ip,
last_sdr_port: server_info.port,
last_sdr_tv_port: server_info.port + 1
)
broadcast_connect_info
status_update("SDR ready, server available at #{server_info.ip}:#{server_info.port}")
return unless previous_server_sdr_ip != server_info.ip || previous_server_sdr_port != server_info.port
server.reload.add_sourcemod_servers(self)
server.rcon_exec('sm plugins reload serverhop')
end
def broadcast_connect_info
broadcast_replace_to self, target: "reservation_connect_info_#{id}", partial: 'reservations/connect_info', locals: { reservation: self }
broadcast_replace_to self, target: "reservation_sdr_connect_info_#{id}", partial: 'reservations/sdr_connect_info', locals: { reservation: self }
broadcast_replace_to self, target: "reservation_stv_connect_info_#{id}", partial: 'reservations/stv_connect_info', locals: { reservation: self }
broadcast_replace_to self, target: "reservation_actions_#{id}", partial: 'reservations/actions', locals: { reservation: self }
end
private
def reservation_manager
ReservationManager.new(self)
end
def generate_initial_status
status_update('Waiting to start') if future?
end
end