|
|
|
# User model
|
|
|
|
class User < EntityModel
|
|
|
|
attr_reader :email, :name, :bio_text, :balance, :avatar_url, :pw_hash, :reputation
|
|
|
|
|
|
|
|
def initialize(data)
|
|
|
|
super data
|
|
|
|
@email = data["email"]
|
|
|
|
@name = data["name"]
|
|
|
|
@bio_text = data["bio_text"]
|
|
|
|
@balance = data["balance"].to_f
|
|
|
|
@avatar_url = data["avatar_url"]
|
|
|
|
@reputation = data["reputation"].to_i
|
|
|
|
@pw_hash = data["pw_hash"]
|
|
|
|
end
|
|
|
|
|
|
|
|
def avatar
|
|
|
|
return @avatar_url
|
|
|
|
end
|
|
|
|
|
|
|
|
def auctions
|
|
|
|
Auction.get_all "user_id = ?", @id
|
|
|
|
end
|
|
|
|
|
|
|
|
def role
|
|
|
|
return Role.find_by_id( ROLES[:admin][:id] ).name if self.admin?
|
|
|
|
|
|
|
|
user_roles = self.roles
|
|
|
|
if user_roles.length > 0 then
|
|
|
|
role = user_roles.max_by { |role| role.flags }
|
|
|
|
return role.name
|
|
|
|
end
|
|
|
|
return ""
|
|
|
|
end
|
|
|
|
|
|
|
|
def role_ids
|
|
|
|
User_Role_relation.get_user_roles_ids @id
|
|
|
|
end
|
|
|
|
|
|
|
|
def roles
|
|
|
|
User_Role_relation.get_user_roles @id
|
|
|
|
end
|
|
|
|
|
|
|
|
def rep_score
|
|
|
|
return BAD_REP if @reputation < 0
|
|
|
|
return GOOD_REP if @reputation > 0
|
|
|
|
return NEUTRAL_REP
|
|
|
|
end
|
|
|
|
|
|
|
|
def bio_html
|
|
|
|
md_parser = Redcarpet::Markdown.new(Redcarpet::Render::HTML)
|
|
|
|
md_parser.render @bio_text
|
|
|
|
end
|
|
|
|
|
|
|
|
def reputation_text
|
|
|
|
sign = @reputation > 0 ? "+" : ""
|
|
|
|
return "#{sign}#{@reputation}"
|
|
|
|
end
|
|
|
|
|
|
|
|
def reputation=(val)
|
|
|
|
val = val.clamp MIN_REP, MAX_REP
|
|
|
|
@reputation = val
|
|
|
|
User.update({reputation: val}, "id = ?", @id)
|
|
|
|
end
|
|
|
|
|
|
|
|
def balance=(val)
|
|
|
|
val = val >= 0 ? val : 0
|
|
|
|
@balance = val
|
|
|
|
User.update({balance: val}, "id = ?", @id)
|
|
|
|
end
|
|
|
|
|
|
|
|
def update_creds(data)
|
|
|
|
# Validate input
|
|
|
|
return false, SETTINGS_ERRORS[:name_len] unless data[:name].length.between?(MIN_NAME_LEN, MAX_NAME_LEN)
|
|
|
|
return false, SETTINGS_ERRORS[:bio_len] unless data[:bio_text].length.between?(MIN_BIO_LEN, MAX_BIO_LEN)
|
|
|
|
|
|
|
|
# Filter unchanged data
|
|
|
|
data.keys.each do |k|
|
|
|
|
data.delete(k) if @data[k.to_s] == data[k]
|
|
|
|
end
|
|
|
|
User.update(data, "id = ?", @id) unless data.length < 1
|
|
|
|
return true, nil
|
|
|
|
end
|
|
|
|
|
|
|
|
# Find user by email, same as above but for emails.
|
|
|
|
def self.find_by_email(email)
|
|
|
|
data = self.get("*", "email = ?", email).first
|
|
|
|
data && User.new(data)
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.validate_register_creds(email, name, password, password_confirm)
|
|
|
|
# Field check
|
|
|
|
check_all_fields = email != "" && name != "" && password != "" && password_confirm != ""
|
|
|
|
|
|
|
|
# Check email
|
|
|
|
check_email_dupe = self.find_by_email(email) == nil
|
|
|
|
check_email_valid = email.match(EMAIL_REGEX) != nil
|
|
|
|
|
|
|
|
# Name
|
|
|
|
check_name_len = name.length.between?(MIN_NAME_LEN, MAX_NAME_LEN)
|
|
|
|
|
|
|
|
# Password
|
|
|
|
check_pass_equals = password == password_confirm
|
|
|
|
check_pass_len = password.length >= MIN_PASSWORD_LEN
|
|
|
|
|
|
|
|
# This code is really ugly
|
|
|
|
return false, REGISTER_ERRORS[:fields] unless check_all_fields
|
|
|
|
return false, REGISTER_ERRORS[:email_dupe] unless check_email_dupe
|
|
|
|
return false, REGISTER_ERRORS[:email_fake] unless check_email_valid
|
|
|
|
return false, REGISTER_ERRORS[:name_len] unless check_name_len
|
|
|
|
return false, REGISTER_ERRORS[:pass_notequals] unless check_pass_equals
|
|
|
|
return false, REGISTER_ERRORS[:pass_len] unless check_pass_len
|
|
|
|
return true, ""
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.validate_password(pw_hash, password)
|
|
|
|
BCrypt::Password.new(pw_hash) == password
|
|
|
|
end
|
|
|
|
|
|
|
|
# Register a new user
|
|
|
|
# Returns: success?, data
|
|
|
|
def self.register(email, name, password, password_confirm)
|
|
|
|
check, errorstr = self.validate_register_creds(email, name, password, password_confirm)
|
|
|
|
|
|
|
|
if check then
|
|
|
|
pw_hash = BCrypt::Password.create password
|
|
|
|
data = { # payload
|
|
|
|
name: name,
|
|
|
|
email: email,
|
|
|
|
pw_hash: pw_hash
|
|
|
|
}
|
|
|
|
|
|
|
|
resp = self.insert data # insert into the db
|
|
|
|
return check, resp
|
|
|
|
else
|
|
|
|
return check, errorstr
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Log in user
|
|
|
|
# Returns: success?, user id
|
|
|
|
def self.login(email, password)
|
|
|
|
user = self.find_by_email email # get the user info
|
|
|
|
|
|
|
|
return false, LOGIN_ERRORS[:fail] unless user # Verify that the user exists
|
|
|
|
|
|
|
|
pw_check = self.validate_password(user.pw_hash, password)
|
|
|
|
return false, LOGIN_ERRORS[:fail] unless pw_check # Verify password
|
|
|
|
|
|
|
|
return true, user.id
|
|
|
|
end
|
|
|
|
|
|
|
|
# Get a users flags
|
|
|
|
# Returns: bitmap int thingie
|
|
|
|
def flags
|
|
|
|
flags = 0
|
|
|
|
self.roles.each do |role|
|
|
|
|
if role.is_a? Role then
|
|
|
|
flags |= role.flags
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return flags
|
|
|
|
end
|
|
|
|
|
|
|
|
def admin?
|
|
|
|
return self.flags[1] == 1
|
|
|
|
end
|
|
|
|
|
|
|
|
# Check if user has flags
|
|
|
|
# Returns: true or false depending whether the user has those flags
|
|
|
|
def permitted?(flag, *other_flags)
|
|
|
|
return true if self.admin?
|
|
|
|
|
|
|
|
flag_mask = PERM_LEVELS[flag]
|
|
|
|
if other_flags then
|
|
|
|
other_flags.each {|f| flag_mask |= PERM_LEVELS[f]}
|
|
|
|
end
|
|
|
|
|
|
|
|
return self.flags & flag_mask == flag_mask
|
|
|
|
end
|
|
|
|
|
|
|
|
def banned?
|
|
|
|
return self.flags[ PERM_LEVELS.keys.index(:banned) ] == 1
|
|
|
|
end
|
|
|
|
|
|
|
|
def banned=(b)
|
|
|
|
if b then
|
|
|
|
# Add the "banned" role
|
|
|
|
resp = User_Role_relation.give_role(@id, ROLES[:banned][:id])
|
|
|
|
else
|
|
|
|
# Remove the "banned" role
|
|
|
|
resp = User_Role_relation.revoke_role(@id, ROLES[:banned][:id])
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Role model
|
|
|
|
class Role < EntityModel
|
|
|
|
attr_reader :name, :color, :flags
|
|
|
|
def initialize(data)
|
|
|
|
super data
|
|
|
|
@name = data["name"]
|
|
|
|
@color = data["color"]
|
|
|
|
@flags = data["flags"]
|
|
|
|
end
|
|
|
|
|
|
|
|
def has_flag?(flag, *other_flags)
|
|
|
|
flag_mask = PERM_LEVELS[flag]
|
|
|
|
|
|
|
|
# Add other flags
|
|
|
|
if other_flags then
|
|
|
|
other_flags.each do |f|
|
|
|
|
flag_mask += PERM_LEVELS[f]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
return @flags & flag_mask == flag_mask # f AND m = m => flags exists
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.find_by_name(name)
|
|
|
|
data = self.get("*", "name = ?", name).first
|
|
|
|
data && Role.new(data)
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.create(name, color="#ffffff", flags=0)
|
|
|
|
return false, REGISTER_ERRORS[:name_len] unless name.length.between?(MIN_NAME_LEN, MAX_NAME_LEN)
|
|
|
|
|
|
|
|
data = {
|
|
|
|
name: name,
|
|
|
|
color: color,
|
|
|
|
flags: flags
|
|
|
|
}
|
|
|
|
self.insert data
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
class User_Role_relation < EntityModel
|
|
|
|
def self.init_table
|
|
|
|
super
|
|
|
|
|
|
|
|
# Add the "first user" to the admin role
|
|
|
|
search = self.get("role_id", "user_id=1") or []
|
|
|
|
if search.length <= 0 then
|
|
|
|
q = "INSERT INTO #{self.name} (user_id, role_id) VALUES (?, ?)"
|
|
|
|
self.query(q, 1, 1)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.give_role(user_id, role_id)
|
|
|
|
user = User.find_by_id user_id
|
|
|
|
|
|
|
|
if not user.role_ids.include?(role_id) then
|
|
|
|
data = {
|
|
|
|
role_id: role_id,
|
|
|
|
user_id: user_id
|
|
|
|
}
|
|
|
|
self.insert data
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.revoke_role(user_id, role_id)
|
|
|
|
user = User.find_by_id user_id
|
|
|
|
|
|
|
|
if user.role_ids.include?(role_id) then
|
|
|
|
self.delete "role_id = ? AND user_id = ?", role_id, user_id
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.get_user_roles_ids(user_id)
|
|
|
|
ids = self.get "role_id", "user_id = ?", user_id
|
|
|
|
ids.map! do |ent|
|
|
|
|
ent["role_id"].to_i
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.get_user_roles(user_id)
|
|
|
|
roleids = self.get_user_roles_ids user_id
|
|
|
|
roles = roleids.map do |id|
|
|
|
|
Role.find_by_id(id)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
# Auction model
|
|
|
|
class Auction < EntityModel
|
|
|
|
attr_reader :user_id, :title, :description, :init_price, :start_time, :end_time
|
|
|
|
def initialize(data)
|
|
|
|
super data
|
|
|
|
@user_id = data["user_id"].to_i
|
|
|
|
@title = data["title"]
|
|
|
|
@description = data["description"]
|
|
|
|
@init_price = data["price"].to_f
|
|
|
|
@start_time = data["start_time"].to_i
|
|
|
|
@end_time = data["end_time"].to_i
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.validate_ah(title, description, init_price, delta_time)
|
|
|
|
return false, AUCTION_ERRORS[:titlelen] unless title.length.between?(MIN_TITLE_LEN, MAX_TITLE_LEN)
|
|
|
|
return false, AUCTION_ERRORS[:initprice] unless init_price >= MIN_INIT_PRICE
|
|
|
|
return false, AUCTION_ERRORS[:deltatime] unless delta_time >= MIN_DELTA_TIME
|
|
|
|
return true, ""
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.create(user_id, title, description, init_price, delta_time)
|
|
|
|
# Validate the input
|
|
|
|
check, errorstr = self.validate_ah(title, description, init_price, delta_time)
|
|
|
|
return check, errorstr unless check
|
|
|
|
|
|
|
|
# Get current UNIX time
|
|
|
|
start_time = Time.now.to_i
|
|
|
|
end_time = start_time + delta_time
|
|
|
|
|
|
|
|
# Prep the payload
|
|
|
|
data = {
|
|
|
|
user_id: user_id,
|
|
|
|
title: title,
|
|
|
|
description: description,
|
|
|
|
price: init_price,
|
|
|
|
start_time: start_time,
|
|
|
|
end_time: end_time
|
|
|
|
}
|
|
|
|
|
|
|
|
self.insert data
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.compose_query_filters(title=nil, categories=nil, min_price=nil, max_price=nil, expired=nil)
|
|
|
|
querystr = "SELECT * FROM Auction WHERE "
|
|
|
|
filters = []
|
|
|
|
|
|
|
|
# Title filter
|
|
|
|
filters << "title LIKE '%#{title}%'" if title and title.length != 0
|
|
|
|
|
|
|
|
# Price filters
|
|
|
|
if min_price and max_price then
|
|
|
|
filters << "price BETWEEN #{min_price} AND #{max_price}"
|
|
|
|
elsif min_price then
|
|
|
|
filters << "price >= #{min_price}"
|
|
|
|
elsif max_price then
|
|
|
|
filters << "price <= #{max_price}"
|
|
|
|
end
|
|
|
|
|
|
|
|
# Time filter
|
|
|
|
filters << "end_time #{ expired == true ? "<" : ">" } #{Time.now.to_i}"
|
|
|
|
|
|
|
|
# Categories filter
|
|
|
|
if categories then
|
|
|
|
ah_ids = []
|
|
|
|
categories.each do |catid|
|
|
|
|
if ah_ids == [] then
|
|
|
|
ah_ids = Auction_Category_relation.category_auction_ids(catid) # first time then include all
|
|
|
|
else
|
|
|
|
ah_ids |= Auction_Category_relation.category_auction_ids(catid) # do union for all ids (prevent dupes)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
filters << "id IN (#{ah_ids.join(", ")})" # check if the auction id is any of the ids calculated above
|
|
|
|
end
|
|
|
|
|
|
|
|
querystr += filters.join " AND "
|
|
|
|
return querystr
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.search(title=nil, categories=nil, min_price=nil, max_price=nil, expired=nil)
|
|
|
|
q = self.compose_query_filters title, categories, min_price, max_price, expired
|
|
|
|
data = self.query(q)
|
|
|
|
data.map! {|dat| self.new(dat)}
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.expired?(id)
|
|
|
|
ah = self.find_by_id id
|
|
|
|
ah && ah.expired?
|
|
|
|
end
|
|
|
|
|
|
|
|
def poster
|
|
|
|
User.find_by_id @user_id
|
|
|
|
end
|
|
|
|
|
|
|
|
def images
|
|
|
|
Image.get_relation @id
|
|
|
|
end
|
|
|
|
|
|
|
|
def categories
|
|
|
|
data = Auction_Category_relation.get "category_id", "auction_id = ?", @id
|
|
|
|
data && data.map! { |category| Category.find_by_id category["category_id"]}
|
|
|
|
end
|
|
|
|
|
|
|
|
def expired?
|
|
|
|
Time.now.to_i > @end_time
|
|
|
|
end
|
|
|
|
|
|
|
|
def time_left
|
|
|
|
@end_time - Time.now.to_i
|
|
|
|
end
|
|
|
|
|
|
|
|
def time_left_s
|
|
|
|
left = self.time_left
|
|
|
|
result = []
|
|
|
|
TIME_FORMATS.each do |sym, count|
|
|
|
|
amount = left.to_i / count
|
|
|
|
if amount > 0 then
|
|
|
|
result << "#{amount}#{sym.to_s}"
|
|
|
|
left -= count*amount
|
|
|
|
end
|
|
|
|
end
|
|
|
|
result = result[0...2]
|
|
|
|
return result.join ", "
|
|
|
|
end
|
|
|
|
|
|
|
|
def bids
|
|
|
|
Bid.get_bids(@id)
|
|
|
|
end
|
|
|
|
|
|
|
|
def place_bid(uid, amount, message)
|
|
|
|
Bid.place(@id, uid, amount, message)
|
|
|
|
end
|
|
|
|
|
|
|
|
def max_bid
|
|
|
|
max_bid = self.bids.max_by {|bid| bid.amount}
|
|
|
|
end
|
|
|
|
|
|
|
|
def current_bid
|
|
|
|
mbid = self.max_bid
|
|
|
|
if mbid != nil then
|
|
|
|
return mbid.amount.to_f
|
|
|
|
else
|
|
|
|
return @init_price.to_f
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def min_new_bid
|
|
|
|
max_bid = self.max_bid
|
|
|
|
amount = max_bid.nil? ? @init_price : max_bid.amount
|
|
|
|
return amount * AH_BIDS_FACTOR
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Auction bids
|
|
|
|
class Bid < EntityModel
|
|
|
|
attr_reader :amount, :auction_id, :user_id, :message
|
|
|
|
def initialize(data)
|
|
|
|
super data
|
|
|
|
@amount = data["amount"].to_f
|
|
|
|
@auction_id = data["auction_id"].to_i
|
|
|
|
@user_id = data["user_id"].to_i
|
|
|
|
@message = data["message"]
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.get_bids(ahid)
|
|
|
|
data = self.get "*", "auction_id = ?", ahid
|
|
|
|
data && data.map! {|dat| self.new(dat)}
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.get_user_bids(uid)
|
|
|
|
data = self.get "*", "user_id = ?", uid
|
|
|
|
data && data.map! {|dat| self.new(dat)}
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.get_user_active_amount(uid)
|
|
|
|
bids = self.get_user_bids uid
|
|
|
|
return bids.sum {|bid| bid.amount}
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.get_delta_amount(ahid, uid, amount)
|
|
|
|
data = self.get "*", "auction_id = ? AND user_id = ?", ahid, uid
|
|
|
|
if data then
|
|
|
|
data.map! {|dat| self.new(dat)}
|
|
|
|
max_bid = data.max_by {|bid| bid.amount}
|
|
|
|
return amount - max_bid.amount
|
|
|
|
else
|
|
|
|
return amount
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.validate_bid(ahid, uid, amount, message)
|
|
|
|
ah = Auction.find_by_id ahid
|
|
|
|
return false, "Invalid auction" unless ah.is_a? Auction
|
|
|
|
return false, AUCTION_ERRORS[:expired] unless not ah.expired?
|
|
|
|
return false, AUCTION_ERRORS[:cantafford] unless User.find_by_id(uid).balance - amount >= 0
|
|
|
|
return false, AUCTION_ERRORS[:bidamount] unless amount >= ah.min_new_bid
|
|
|
|
return true, ""
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.place(ahid, uid, amount, message)
|
|
|
|
check, resp = self.validate_bid(ahid, uid, amount, message)
|
|
|
|
if check then
|
|
|
|
# Deduct delta amount from balance
|
|
|
|
delta_amount = self.get_delta_amount(ahid, uid, amount)
|
|
|
|
user = User.find_by_id uid
|
|
|
|
user.balance -= delta_amount
|
|
|
|
|
|
|
|
payload = {
|
|
|
|
auction_id: ahid,
|
|
|
|
user_id: uid,
|
|
|
|
amount: amount,
|
|
|
|
message: message
|
|
|
|
}
|
|
|
|
resp = self.insert(payload)
|
|
|
|
end
|
|
|
|
return check, resp
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
class Category < EntityModel
|
|
|
|
attr_reader :name, :color
|
|
|
|
def initialize(data)
|
|
|
|
super data
|
|
|
|
@name = data["name"]
|
|
|
|
@color = data["color"]
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.create(name, color)
|
|
|
|
data = {
|
|
|
|
name: name,
|
|
|
|
color: color
|
|
|
|
}
|
|
|
|
self.insert(data)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
class Auction_Category_relation < EntityModel
|
|
|
|
attr_reader :auction_id, :category_id
|
|
|
|
def initialize(data)
|
|
|
|
super data
|
|
|
|
@auction_id = data["auction_id"]
|
|
|
|
@category_id = data["category_id"]
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.category_auction_ids(catid)
|
|
|
|
ids = self.get "auction_id", "category_id = ?", catid
|
|
|
|
ids && ids.map! {|id| id["auction_id"].to_i}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
class Image < EntityModel
|
|
|
|
attr_reader :auction_id, :image_order, :url
|
|
|
|
def initialize(data)
|
|
|
|
super data
|
|
|
|
@auction_id = data["auction_id"]
|
|
|
|
@image_order = data["image_order"].to_i
|
|
|
|
@url = data["url"]
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.save(imgdata, ah_id, order)
|
|
|
|
FileUtils.mkdir_p "./public/auctions/#{ah_id}"
|
|
|
|
|
|
|
|
data = {
|
|
|
|
auction_id: ah_id,
|
|
|
|
image_order: order,
|
|
|
|
url: "/auctions/#{ah_id}/#{order}.png"
|
|
|
|
}
|
|
|
|
newid, resp = self.insert data
|
|
|
|
|
|
|
|
if newid then
|
|
|
|
image = Magick::Image.from_blob(imgdata).first
|
|
|
|
image.format = "PNG"
|
|
|
|
path = "./public/auctions/#{ah_id}/#{order}.png"
|
|
|
|
File.open(path, 'wb') do |f|
|
|
|
|
image.write(f) { self.quality = 50 }
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.get_relation(ah_id)
|
|
|
|
imgs = self.get "*", "auction_id = ?", ah_id
|
|
|
|
imgs.map! do |img|
|
|
|
|
self.new(img)
|
|
|
|
end
|
|
|
|
return imgs
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|