require "uri" require "base64" include CrystalGauntlet # things might break if you modify this levels_per_page = 10 CrystalGauntlet.endpoints["/getGJLevels21.php"] = ->(body : String): String { params = URI::Params.parse(body) LOG.debug { params.inspect } can_see_unlisted = false # where [...] queryParams = [] of String # order by [...] order = "levels.created_at desc" # joins go here joins = [] of String page_offset = (params["page"]? || "0").to_i * levels_per_page searchQuery = params["str"]? || "" if searchQuery != "" && params["type"] != "5" && params["type"] != "10" && params["type"] != "19" if searchQuery.to_i? # we do this to get rid of the initial "unlisted = 0" bit can_see_unlisted = true queryParams << "levels.id = #{searchQuery.to_i}" else # no sql injections to see here; clean_char only leaves A-Za-z0-9 intact # todo: make this configurable w/ fuzzy search queryParams << "levels.name like \"#{Clean.clean_char(searchQuery)}%\"" end end # filters if params["featured"]? == "1" queryParams << "featured = 1" end if params["original"]? == "1" queryParams << "original is null" end if params["coins"]? == "1" queryParams << "rated_coins = 1 and levels.coins != 0" end if params["epic"]? == "1" queryParams << "epic = 1" end if params["uncompleted"]? == "1" # todo # $completedLevels = ExploitPatch::numbercolon($_POST["completedLevels"]); # $params[] = "NOT levelID IN ($completedLevels)"; end if params["onlyCompleted"]? == "1" # todo # $completedLevels = ExploitPatch::numbercolon($_POST["completedLevels"]); # $params[] = "levelID IN ($completedLevels)"; end if params["song"]? if params["customSong"]? && params["customSong"]? != "" queryParams << "song_id = '#{params["customSong"].to_i}'" else queryParams << "song_id = '#{params["song"].to_i}'" end end if params["twoPlayer"]? == "1" queryParams << "two_player = 1" end if params["star"]? == "1" queryParams << "levels.stars is not null" end if params["noStar"]? == "1" queryParams << "levels.stars is null" end if params["gauntlet"]? # todo end if params["len"]? # todo end if params["diff"]? && params["diff"]? != "-" case params["diff"]? when "-1" queryParams << "difficulty is null and community_difficulty is null" # NA when "-2" case params["demonFilter"]? when "1" queryParams << "demon_difficulty = #{DemonDifficulty::Easy.value}" when "2" queryParams << "demon_difficulty = #{DemonDifficulty::Medium.value}" when "3" queryParams << "demon_difficulty = #{DemonDifficulty::Hard.value}" when "4" queryParams << "demon_difficulty = #{DemonDifficulty::Insane.value}" when "5" queryParams << "demon_difficulty = #{DemonDifficulty::Extreme.value}" end queryParams << "difficulty = #{LevelDifficulty::Demon.value} or (difficulty is null and community_difficulty = #{LevelDifficulty::Demon.value})" when "-3" queryParams << "difficulty = #{LevelDifficulty::Auto.value} or (difficulty is null and community_difficulty = #{LevelDifficulty::Auto.value})" else # easy, normal, hard, harder, insane diffs = params["diff"].split(",").map{ |v| v.to_i }.join(",") queryParams << "difficulty in (#{diffs}) or (difficulty is null and community_difficulty in (#{diffs}))" end end # level search type case params["type"] when "0", "15", nil # default sort (gdw is 15) order = "likes desc" when "1" # most downloaded order = "downloads desc" when "2" # most liked order = "likes desc" when "3" # trending # todo: make configurable? order = "likes desc" queryParams << "levels.created_at > \"#{(Time.utc - 7.days).to_s(Format::TIME_FORMAT)}\"" when "5" # made by user if params["local"] == "1" user_id, account_id = Accounts.auth(params) if !(user_id && account_id) return "-1" end if user_id == searchQuery.to_i can_see_unlisted = true queryParams << "levels.user_id = #{searchQuery.to_i}" else return "-1" end else queryParams << "levels.user_id = #{searchQuery.to_i}" # (you can't sql inject with numbers) end when "6", "17" # featured (gdw is 17) # todo: order by feature date queryParams << "featured = 1" when "16" # hall of fame (epic) # todo: order by epic date queryParams << "epic = 1" when "7" # magic # todo: make configurable? queryParams << "objects > 4000" when "10", "19" # map packs order = "map_pack_links.idx asc" queryParams << "levels.id in (#{searchQuery.split(",").map{|v| v.to_i}.join(",")})" joins << "left join map_pack_links on map_pack_links.level_id = levels.id" when "11" # rated # todo: order by rate date queryParams << "levels.stars is not null" when "12" # followed # todo when "13" # friends # todo when "21" # daily # todo when "22" # weekly # todo when "23" # event (unused) # todo end if !can_see_unlisted queryParams << "unlisted = 0" end where_str = "where (#{queryParams.join(") and (")})" # todo: switch join users to left join to avoid losing levels to the shadow realm after a user vanishes query_base = "from levels join users on levels.user_id = users.id #{joins.join(" ")} #{where_str} order by #{order}" LOG.debug { "query: #{query_base}" } level_count = DATABASE.scalar("select count(*) #{query_base}").as(Int64) results = [] of String users = [] of String songs = [] of String hash_data = [] of Tuple(Int32, Int32, Bool) # fucking help DATABASE.query_all("select levels.id, levels.name, levels.user_id, levels.description, levels.original, levels.game_version, levels.requested_stars, levels.version, levels.song_id, levels.length, levels.objects, levels.coins, levels.has_ldm, levels.two_player, levels.downloads, levels.likes, levels.difficulty, levels.community_difficulty, levels.demon_difficulty, levels.stars, levels.featured, levels.epic, levels.rated_coins, users.username, users.udid, users.account_id, users.registered, wt1, wt2 #{query_base} limit #{levels_per_page} offset #{page_offset}", as: {Int32, String, Int32, String, Int32 | Nil, Int32, Int32 | Nil, Int32, Int32, Int32, Int32, Int32, Bool, Bool, Int32, Int32, Int32 | Nil, Int32 | Nil, Int32 | Nil, Int32 | Nil, Bool, Bool, Bool, String, String | Nil, Int32 | Nil, Bool, String, String}).map() do |id, name, user_id, description, original, game_version, requested_stars, version, song_id, length, objects, coins, has_ldm, two_player, downloads, likes, set_difficulty_int, community_difficulty_int, demon_difficulty_int, stars, featured, epic, rated_coins, user_username, user_udid, user_account_id, user_registered, wt1, wt2| set_difficulty = set_difficulty_int && LevelDifficulty.new(set_difficulty_int) community_difficulty = community_difficulty_int && LevelDifficulty.new(community_difficulty_int) difficulty = set_difficulty || community_difficulty demon_difficulty = demon_difficulty_int && DemonDifficulty.new(demon_difficulty_int) # https://github.com/Cvolton/GMDprivateServer/blob/master/incl/levels/getGJLevels.php#L266 results << Format.fmt_hash({ 1 => id, 2 => name, 3 => GDBase64.encode(description), 5 => version, 6 => user_id, # this is suppoused to be the amount of people who have # voted on a level, but this is unused by the game, so # we just always tell the game 10 people have voted 8 => 10, # 0=N/A 10=EASY 20=NORMAL 30=HARD 40=HARDER 50=INSANE 50=AUTO 50=DEMON # divided by above value, which is why its multiplied by 10 9 => (difficulty ? difficulty.to_star_difficulty : 0).not_nil! * 10, 10 => downloads, 12 => !Songs.is_custom_song(song_id) ? song_id : 0, 13 => game_version, # likes - dislikes 14 => likes, # dislikes - likes 16 => -likes, 15 => length, 17 => difficulty && difficulty.demon?, 18 => stars || 0, 19 => featured, # 0 for n/a, 10 for easy, 20, for medium, ... 25 => difficulty && difficulty.auto?, 30 => original || 0, 31 => two_player, 35 => Songs.is_custom_song(song_id) ? song_id : 0, 37 => coins, 38 => rated_coins, 39 => requested_stars || 0, 40 => has_ldm, 42 => epic, 43 => (demon_difficulty || DemonDifficulty::Hard).to_demon_difficulty, # is in gauntlet # todo 44 => false, 45 => objects, 46 => wt1, 47 => wt2 }) users << "#{user_id}:#{user_username}:#{user_registered ? user_account_id : user_udid}" if Songs.is_custom_song(song_id) begin song = Songs.fetch_song(song_id, false) rescue else if song != nil song_name, song_author_id, song_author_name, song_size, song_download = song.not_nil! songs << Format.fmt_song({ 1 => song_id, 2 => song_name, 3 => song_author_id, 4 => song_author_name, 5 => (song_size || 0) / (1000 * 1000), 6 => "", 10 => song_download || "", 7 => "", 8 => "1" }) end end end hash_data << {id, stars || 0, rated_coins} end # `${amount}:${offset}:${levelsPerPage}` searchMeta = "#{level_count}:#{page_offset}:#{levels_per_page}" res = [results.join("|"), users.join("|"), songs.join("~:~"), searchMeta, CrystalGauntlet::Hashes.gen_multi(hash_data)].join("#") res }