Make player popunder

Make player popunder

/**
 * vim: set ts=4 :
 * =============================================================================
 * in_game_audio
 * TODO - Add your project's description
 *
 * Copyright 2013 ???
 * =============================================================================
 *
 */

#pragma semicolon 1

#include <sourcemod>
#include <steamtools>
#include <base64>
#include <smjansson>

#define PLUGIN_VERSION "0.1"

public Plugin:myinfo =
{
    name = "InGameAudio",
    author = "CrimsonTautology",
    description = "Interact with the In Game Audio web api",
    version = PLUGIN_VERSION,
    url = "https://github.com/CrimsonTautology/sm_in_game_audio"
};

#define QUERY_SONG_ROUTE "/v1/api/query_song"
#define RANDOM_SONG_ROUTE ""
#define SONGS_ROUTE "/songs"

#define MAX_STEAMID_LENGTH 21
#define MAX_SONG_LENGTH 64

new Handle:g_Cvar_IGAApiKey = INVALID_HANDLE;
new Handle:g_Cvar_IGAUrl = INVALID_HANDLE;
new Handle:g_Cvar_IGAport = INVALID_HANDLE;
new Handle:g_Cvar_IGADonatorsOnly = INVALID_HANDLE;
new Handle:g_Cvar_IGAEnabled = INVALID_HANDLE;
new Handle:g_Cvar_IGARequestCooldownTime = INVALID_HANDLE;

new bool:g_IsInCooldown[MAXPLAYERS+1];
new g_PallNextFree = 0;

public OnPluginStart()
{

    g_Cvar_IGAApiKey = CreateConVar(sm_iga_api_key, "", "API Key for your IGA webpage");
    g_Cvar_IGAUrl = CreateConVar(sm_iga_url, "", "URL to your IGA webpage");
    g_Cvar_IGADonatorsOnly = CreateConVar(sm_iga_donators_only, "1", "Whether or not only donators have access to pall");
    g_Cvar_IGAEnabled = CreateConVar(sm_iga_enabled, "1", "Whether or not pall is enabled");
    g_Cvar_IGARequestCooldownTime = CreateConVar("sm_iga_request_cooldown_time", "2.0", "How long in seconds before a client can send another http request");

    RegConsoleCmd("sm_p", Command_P, "Play a song for yourself");
    RegConsoleCmd("sm_pall", Command_Pall, "Play a song for everyone");
    RegConsoleCmd("sm_plist", Command_Plist, "Pop-up the song list");
    RegConsoleCmd("sm_stop", Command_Stop, "Stop the current song");
    RegConsoleCmd("sm_fstop", Command_Fstop, "[ADMIN] Stop the current pall for everyone");
    RegConsoleCmd("sm_fpall", Command_Fpall, "[ADMIN] Force everyone to listen to a song");
    RegConsoleCmd("sm_vol", Command_Vol, "Adjust your play volume");
    RegConsoleCmd("sm_nopall", Command_Nopall, "Turn off pall for yourself");
    RegConsoleCmd("sm_plast", Command_Plast, "Play the last played song for yourself");

    HookEvent("map_change", Event_MapChange);
    HookEvent("client_connect", Event_ClientConnect);
}

public Action:Command_P(client, args)
{
    if (args < 1)
    {
        ReplyToCommand(client, "[IGA] Usage: !p <song>");
        return Plugin_Handled;
    }
    if(IsClientInCooldown(client))
    {
        ReplyToCommand(client, "[IGA] User in cooldown");
        return Plugin_Handled;
    }

    if(!GetConVarBool(g_Cvar_IGAEnabled))
    {
        ReplyToCommand(client, "[IGA] IGA not enabled");
        return Plugin_Handled;
    }

    if(client && IsClientAuthorized(client)){
        decl String:song[MAX_SONG_LENGTH];
        GetCmdArgString(song, sizeof(song));
        QuerySong(client, song, ACTION_P);
    }

    return Plugin_Handled;
}

public Action:Command_Pall(client, args)
{
    //TODO
}

public Action:Command_Plist(client, args)
{
    //TODO
}

public Action:Command_Stop(client, args)
{
    //TODO
}

public Action:Command_Fstop(client, args)
{
    //TODO
}

public Action:Command_Fpall(client, args)
{
    //TODO
}

public Action:Command_Vol(client, args)
{
    //TODO
}

public Action:Command_Nopall(client, args)
{
    //TODO
}

public Action:Command_Plast(client, args)
{
    //TODO
}

public Event_MapChange(Handle:event, const String:name[], bool:dontBroadcast)
{
    //TODO
}

public Event_ClientConnect(Handle:event, const String:name[], bool:dontBroadcast)
{
    //TODO
}

public Steam_SetHTTPRequestGetOrPostParameterInt(&HTTPRequestHandle:request, const String:param[], value)
{
    new String:tmp[64];
    IntToString(value, tmp, sizeof(tmp));
    Steam_SetHTTPRequestGetOrPostParameter(request, param, tmp);
}

public SetAccessCode(&HTTPRequestHandle:request)
{
    decl String:api_key[128];
    GetConVarString(g_Cvar_IGAApiKey, api_key, sizeof(api_key));
    Steam_SetHTTPRequestGetOrPostParameter(request, "access_token", api_key);
}

public HTTPRequestHandle:CreateIGARequest(const String:route[])
{
    decl String:base_url[256], String:url[512];
    GetConVarString(g_Cvar_IGAUrl, base_url, sizeof(base_url));
    TrimString(base_url);
    new trim_length = strlen(base_url) - 1;

    if(trim_length < 0)
    {
        //IGA Url not set
        return INVALID_HTTP_HANDLE;
    }

    //check for forward slash after base_url;
    if(base_url[trim_length] == '/')
    {
        strcopy(base_url, trim_length + 1, base_url);
    }

    Format(url, sizeof(url),
            "%s%s", base_url, route);

    new HTTPRequestHandle:request = Steam_CreateHTTPRequest(HTTPMethod_POST, url);
    SetAccessCode(request);

    return request;
}

public StartCooldown(client)
{
    //Ignore the server console
    if (client == 0)
        return;

    g_IsInCooldown[client] = true;
    CreateTimer(GetConVarFloat(g_Cvar_IGARequestCooldownTime), RemoveCooldown, client);
}

public bool:IsClientInCooldown(client)
{
    if(client == 0)
        return false;
    else
        return g_IsInCooldown[client];
}

public Action:RemoveCooldown(Handle:timer, any:client)
{
    g_IsInCooldown[client] = false;
}

public QuerySong(client, String:song[MAX_SONG_LENGTH], bool:pall=false, client_theme=0, map_theme="")
{
    decl String:uid[MAX_COMMUNITYID_LENGTH];
    Steam_GetCSteamIDForClient(client, uid, sizeof(uid));

    new HTTPRequestHandle:request = CreateIGARequest(QUERY_SONG_ROUTE);

    if(request == INVALID_HTTP_HANDLE)
    {
        ReplyToCommand(client, "[IGA] sm_iga_url invalid; cannot create HTTP request");
        return;
    }

    Steam_SetHTTPRequestGetOrPostParameter(request, "uid", uid);
    Steam_SetHTTPRequestGetOrPostParameterInt(request, "pall", pall);
    Steam_SetHTTPRequestGetOrPostParameterInt(request, "player", GetClientUserId(client));
    Steam_SetHTTPRequestGetOrPostParameter(request, "song", song);

    if(client_theme > 0)
    {
        //Find the user's theme
        decl String:uid_theme[MAX_COMMUNITYID_LENGTH];
        Steam_GetCSteamIDForClient(client, uid_theme, sizeof(uid_theme));
        Steam_SetHTTPRequestGetOrPostParameter(request, "uid_theme", uid_theme);
    }else if(strlen(map_theme) > 0){
        //Find the map's theme
        Steam_SetHTTPRequestGetOrPostParameter(request, "map_theme", map_theme);
    }

    Steam_SendHTTPRequest(request, ReceiveSongQuery, GetClientUserId(client));

    StartCooldown(client);
}

public ReceiveQuerySong(HTTPRequestHandle:request, bool:successful, HTTPStatusCode:code, any:userid)
{
    new client = GetClientOfUserId(userid);
    if(!successful || code != HTTPStatusCode_OK)
    {
        LogError("[IGA] Error at RecivedQuerySong (HTTP Code %d; success %d)", code, successful);
        Steam_ReleaseHTTPRequest(request);
        return;
    }

    decl String:data[4096];
    Steam_GetHTTPResponseBodyData(request, data, sizeof(data));
    Steam_ReleaseHTTPRequest(request);

    new Handle:json = json_load(data);
    new bool:found = json_object_get_bool(json, "found");

    if(found)
    {
        new duration = json_object_get_int(json, "duration");
        new bool:pall = json_object_get_bool(json, "pall");
        new String:song[MAX_SONG_LENGTH], String:description[64];
        json_object_get_string(json, "song", song, sizeof(song));
        json_object_get_string(json, "description", description, sizeof(description));

        if(pall)
        {
            PrintToChatAll("Now Playing: %s", description)
                PlaySongAll(song);
        }else if(client > 0){
            PrintToChat(client, "Now Playing: %s", description)
                PlaySong(client, song);
        }
    }

    CloseHandle(json);
    Steam_ReleaseHTTPRequest(request);
}

public PlaySongAll(song[MAX_SONG_LENGTH])
{
    //TODO update PALL duration
    for (new client=1; client <= MaxClients; client++)
    {
        if (ClientHasPallEnabled(client))
        {
            PlaySong(client, song);
        }
    }
}

public PlaySong(client, song[MAX_SONG_LENGTH])
{
    decl String:url[256], String:base_url[128];
    GetConVarString(g_Cvar_IGAUrl, base_url, sizeof(base_url));

    TrimString(base_url);
    new trim_length = strlen(base_url) - 1;

    if(base_url[trim_length] == '/')
    {
        strcopy(base_url, trim_length + 1, base_url);
    }

    Format(url, sizeof(url),
            "%s%s/%s", base_url, SONGS_ROUTE, song);

    new Handle:panel = CreateKeyValues("data");
    KvSetString(panel, "title", "In Game Audio");
    KvSetNum(panel, "type", MOTDPANEL_TYPE_URL);
    KvSetString(panel, "msg", url);

    ShowVGUIPanel(client, "info", panel, false);
    CloseHandle(panel);
    return;
}

public bool:ClientHasPallEnabled(client)
{
    //TODO do cookie check
    return IsClientAuthorized(client);
}

/**
 * vim: set ts=4 :
 * =============================================================================
 * in_game_audio
 * TODO - Add your project's description
 *
 * Copyright 2013 ???
 * =============================================================================
 *
 */

#pragma semicolon 1

#include <sourcemod>
#include <steamtools>
#include <base64>
#include <smjansson>

#define PLUGIN_VERSION "0.1"

public Plugin:myinfo =
{
    name = "InGameAudio",
    author = "CrimsonTautology",
    description = "Interact with the In Game Audio web api",
    version = PLUGIN_VERSION,
    url = "https://github.com/CrimsonTautology/sm_in_game_audio"
};

#define QUERY_SONG_ROUTE "/v1/api/query_song"
#define RANDOM_SONG_ROUTE ""
#define SONGS_ROUTE "/songs"

#define MAX_STEAMID_LENGTH 21
#define MAX_SONG_LENGTH 64

new Handle:g_Cvar_IGAApiKey = INVALID_HANDLE;
new Handle:g_Cvar_IGAUrl = INVALID_HANDLE;
new Handle:g_Cvar_IGAport = INVALID_HANDLE;
new Handle:g_Cvar_IGADonatorsOnly = INVALID_HANDLE;
new Handle:g_Cvar_IGAEnabled = INVALID_HANDLE;
new Handle:g_Cvar_IGARequestCooldownTime = INVALID_HANDLE;

new bool:g_IsInCooldown[MAXPLAYERS+1];
new g_PallNextFree = 0;

public OnPluginStart()
{

    g_Cvar_IGAApiKey = CreateConVar(sm_iga_api_key, "", "API Key for your IGA webpage");
    g_Cvar_IGAUrl = CreateConVar(sm_iga_url, "", "URL to your IGA webpage");
    g_Cvar_IGADonatorsOnly = CreateConVar(sm_iga_donators_only, "1", "Whether or not only donators have access to pall");
    g_Cvar_IGAEnabled = CreateConVar(sm_iga_enabled, "1", "Whether or not pall is enabled");
    g_Cvar_IGARequestCooldownTime = CreateConVar("sm_iga_request_cooldown_time", "2.0", "How long in seconds before a client can send another http request");

    RegConsoleCmd("sm_p", Command_P, "Play a song for yourself");
    RegConsoleCmd("sm_pall", Command_Pall, "Play a song for everyone");
    RegConsoleCmd("sm_plist", Command_Plist, "Pop-up the song list");
    RegConsoleCmd("sm_stop", Command_Stop, "Stop the current song");
    RegConsoleCmd("sm_fstop", Command_Fstop, "[ADMIN] Stop the current pall for everyone");
    RegConsoleCmd("sm_fpall", Command_Fpall, "[ADMIN] Force everyone to listen to a song");
    RegConsoleCmd("sm_vol", Command_Vol, "Adjust your play volume");
    RegConsoleCmd("sm_nopall", Command_Nopall, "Turn off pall for yourself");
    RegConsoleCmd("sm_plast", Command_Plast, "Play the last played song for yourself");

    HookEvent("map_change", Event_MapChange);
    HookEvent("client_connect", Event_ClientConnect);
}

public Action:Command_P(client, args)
{
    if (args < 1)
    {
        ReplyToCommand(client, "[IGA] Usage: !p <song>");
        return Plugin_Handled;
    }
    if(IsClientInCooldown(client))
    {
        ReplyToCommand(client, "[IGA] User in cooldown");
        return Plugin_Handled;
    }

    if(!GetConVarBool(g_Cvar_IGAEnabled))
    {
        ReplyToCommand(client, "[IGA] IGA not enabled");
        return Plugin_Handled;
    }

    if(client && IsClientAuthorized(client)){
        decl String:song[MAX_SONG_LENGTH];
        GetCmdArgString(song, sizeof(song));
        QuerySong(client, song, ACTION_P);
    }

    return Plugin_Handled;
}

public Action:Command_Pall(client, args)
{
    //TODO
}

public Action:Command_Plist(client, args)
{
    //TODO
}

public Action:Command_Stop(client, args)
{
    //TODO
}

public Action:Command_Fstop(client, args)
{
    //TODO
}

public Action:Command_Fpall(client, args)
{
    //TODO
}

public Action:Command_Vol(client, args)
{
    //TODO
}

public Action:Command_Nopall(client, args)
{
    //TODO
}

public Action:Command_Plast(client, args)
{
    //TODO
}

public Event_MapChange(Handle:event, const String:name[], bool:dontBroadcast)
{
    //TODO
}

public Event_ClientConnect(Handle:event, const String:name[], bool:dontBroadcast)
{
    //TODO
}

public Steam_SetHTTPRequestGetOrPostParameterInt(&HTTPRequestHandle:request, const String:param[], value)
{
    new String:tmp[64];
    IntToString(value, tmp, sizeof(tmp));
    Steam_SetHTTPRequestGetOrPostParameter(request, param, tmp);
}

public SetAccessCode(&HTTPRequestHandle:request)
{
    decl String:api_key[128];
    GetConVarString(g_Cvar_IGAApiKey, api_key, sizeof(api_key));
    Steam_SetHTTPRequestGetOrPostParameter(request, "access_token", api_key);
}

public HTTPRequestHandle:CreateIGARequest(const String:route[])
{
    decl String:base_url[256], String:url[512];
    GetConVarString(g_Cvar_IGAUrl, base_url, sizeof(base_url));
    TrimString(base_url);
    new trim_length = strlen(base_url) - 1;

    if(trim_length < 0)
    {
        //IGA Url not set
        return INVALID_HTTP_HANDLE;
    }

    //check for forward slash after base_url;
    if(base_url[trim_length] == '/')
    {
        strcopy(base_url, trim_length + 1, base_url);
    }

    Format(url, sizeof(url),
            "%s%s", base_url, route);

    new HTTPRequestHandle:request = Steam_CreateHTTPRequest(HTTPMethod_POST, url);
    SetAccessCode(request);

    return request;
}

public StartCooldown(client)
{
    //Ignore the server console
    if (client == 0)
        return;

    g_IsInCooldown[client] = true;
    CreateTimer(GetConVarFloat(g_Cvar_IGARequestCooldownTime), RemoveCooldown, client);
}

public bool:IsClientInCooldown(client)
{
    if(client == 0)
        return false;
    else
        return g_IsInCooldown[client];
}

public Action:RemoveCooldown(Handle:timer, any:client)
{
    g_IsInCooldown[client] = false;
}

public QuerySong(client, String:song[MAX_SONG_LENGTH], bool:pall=false, client_theme=0, map_theme="")
{
    decl String:uid[MAX_COMMUNITYID_LENGTH];
    Steam_GetCSteamIDForClient(client, uid, sizeof(uid));

    new HTTPRequestHandle:request = CreateIGARequest(QUERY_SONG_ROUTE);

    if(request == INVALID_HTTP_HANDLE)
    {
        ReplyToCommand(client, "[IGA] sm_iga_url invalid; cannot create HTTP request");
        return;
    }

    Steam_SetHTTPRequestGetOrPostParameter(request, "uid", uid);
    Steam_SetHTTPRequestGetOrPostParameterInt(request, "pall", pall);
    Steam_SetHTTPRequestGetOrPostParameterInt(request, "player", GetClientUserId(client));
    Steam_SetHTTPRequestGetOrPostParameter(request, "song", song);

    if(client_theme > 0)
    {
        //Find the user's theme
        decl String:uid_theme[MAX_COMMUNITYID_LENGTH];
        Steam_GetCSteamIDForClient(client, uid_theme, sizeof(uid_theme));
        Steam_SetHTTPRequestGetOrPostParameter(request, "uid_theme", uid_theme);
    }else if(strlen(map_theme) > 0){
        //Find the map's theme
        Steam_SetHTTPRequestGetOrPostParameter(request, "map_theme", map_theme);
    }

    Steam_SendHTTPRequest(request, ReceiveSongQuery, GetClientUserId(client));

    StartCooldown(client);
}

public ReceiveQuerySong(HTTPRequestHandle:request, bool:successful, HTTPStatusCode:code, any:userid)
{
    new client = GetClientOfUserId(userid);
    if(!successful || code != HTTPStatusCode_OK)
    {
        LogError("[IGA] Error at RecivedQuerySong (HTTP Code %d; success %d)", code, successful);
        Steam_ReleaseHTTPRequest(request);
        return;
    }

    decl String:data[4096];
    Steam_GetHTTPResponseBodyData(request, data, sizeof(data));
    Steam_ReleaseHTTPRequest(request);

    new Handle:json = json_load(data);
    new bool:found = json_object_get_bool(json, "found");

    if(found)
    {
        new duration = json_object_get_int(json, "duration");
        new bool:pall = json_object_get_bool(json, "pall");
        new String:song[MAX_SONG_LENGTH], String:description[64];
        json_object_get_string(json, "song", song, sizeof(song));
        json_object_get_string(json, "description", description, sizeof(description));

        if(pall)
        {
            PrintToChatAll("Now Playing: %s", description)
                PlaySongAll(song);
        }else if(client > 0){
            PrintToChat(client, "Now Playing: %s", description)
                PlaySong(client, song);
        }
    }

    CloseHandle(json);
    Steam_ReleaseHTTPRequest(request);
}

public PlaySongAll(song[MAX_SONG_LENGTH])
{
    //TODO update PALL duration
    for (new client=1; client <= MaxClients; client++)
    {
        if (ClientHasPallEnabled(client))
        {
            PlaySong(client, song);
        }
    }
}

public PlaySong(client, song[MAX_SONG_LENGTH])
{
    decl String:url[256], String:base_url[128];
    GetConVarString(g_Cvar_IGAUrl, base_url, sizeof(base_url));
    ReplaceString(base_url, sizeof(base_url), "http://", "", false);
    ReplaceString(base_url, sizeof(base_url), "https://", "", false);

    Format(url, sizeof(url),
            "http://%s%s/%s", base_url, SONGS_ROUTE, song);

    //TODO make popunder
    ShowMOTDPanel(client, "Song Player", url, MOTDPANEL_TYPE_URL);

}

public bool:ClientHasPallEnabled(client)
{
    //TODO do cookie check
    return IsClientAuthorized(client);
}


Code condensation and clarification.

Code condensation and clarification.

Added a bunch of constants.

/**
 * Download Preferences
 * Author(s): nosoop
 * File:  downloadprefs.sp
 * Description:	 Allows clients to select categories of files to download.
 * License:	 MIT License
 */

#pragma semicolon 1
#include <sourcemod>

#define PLUGIN_VERSION			"0.6.1"

public Plugin:myinfo = {
	name = "Download Preferences",
	author = "nosoop",
	description = "Client download preferences",
	version = PLUGIN_VERSION,
	url = "http://github.com/nosoop/SM-DownloadPrefs"
}

#define INVALID_DOWNLOAD_CATEGORY -1
#define MAX_SQL_QUERY_LENGTH 512
#define MAX_URL_LENGTH 256

#define MAX_DOWNLOAD_PREFERENCES 64
new g_rgiDownloadPrefs[MAX_DOWNLOAD_PREFERENCES], g_nDownloadPrefs;

#define DATABASE_NAME			"downloadprefs" // Database name in config
new Handle:g_hDatabase = INVALID_HANDLE;

// ConVar handles
new Handle:g_hCDownloadURL = INVALID_HANDLE, // sv_downloadurl
	Handle:g_hCDPrefURL = INVALID_HANDLE; // sm_dprefs_downloadurl

public OnPluginStart() {
	CreateConVar("sm_dprefs_version", PLUGIN_VERSION, _, FCVAR_PLUGIN | FCVAR_NOTIFY);

	g_nDownloadPrefs = 0;
	for (new i = 0; i < MAX_DOWNLOAD_PREFERENCES; i++) {
		g_rgiDownloadPrefs[i] = INVALID_DOWNLOAD_CATEGORY;
	}

	g_hCDownloadURL = FindConVar("sv_downloadurl");

	// Set redirect downloadurl.  If blank, the transmission of sv_downloadurl to the client is not changed.
	g_hCDPrefURL = CreateConVar("sm_dprefs_downloadurl", "", "Download URL to send to the client.  See README for details.", FCVAR_PLUGIN | FCVAR_SPONLY);

	AutoExecConfig(true);
}

public OnPluginEnd() {
	CloseHandle(g_hDatabase);
}

public APLRes:AskPluginLoad2(Handle:hMySelf, bool:bLate, String:strError[], iMaxErrors) {
	RegPluginLibrary("downloadprefs");
	CreateNative("RegClientDownloadCategory", Native_RegClientDownloadCategory);
	CreateNative("RegClientDownloadFile", Native_RegClientDownloadFile);
	CreateNative("SetClientDownloadPreference", Native_SetClientDownloadPreference);
	CreateNative("GetClientDownloadPreference", Native_GetClientDownloadPreference);
	CreateNative("ClientHasDownloadPreference", Native_ClientHasDownloadPreference);
	CreateNative("GetDownloadCategoryInfo", Native_GetDownloadCategoryInfo);
	CreateNative("GetLoadedDownloadCategories", Native_GetLoadedDownloadCategories);

	g_hDatabase = GetDatabase();

	return APLRes_Success;
}

/**
 * Sends a custom download URL when the client is connecting.
 * This occurs every time the client reconnects (via map change, etc.)
 */
public OnClientAuthorized(client, const String:auth[]) {
	if (!IsFakeClient(client)) {
		SendCustomDownloadURL(client);
	}
}

SendCustomDownloadURL(client) {
	new iSteamID3 = GetSteamAccountID(client);
	new String:sClientDownloadURL[MAX_URL_LENGTH];
	GetConVarString(g_hCDPrefURL, sClientDownloadURL, sizeof(sClientDownloadURL));

	// TODO strip trailing slash off of sm_dprefs_downloadurl?

	if (strlen(sClientDownloadURL) > 0) {
		new String:sSteamID3[32];
		IntToString(iSteamID3, sSteamID3, sizeof(sSteamID3));
		ReplaceString(sClientDownloadURL, sizeof(sClientDownloadURL), "$STEAMID", sSteamID3);
		SendConVarValue(client, g_hCDownloadURL, sClientDownloadURL);
	}
}

/**
 * Resets the download URL to the server-supplied value once the client is fully in-game.
 * This occurs after the client has performed any fast download requests.
 */
public OnClientPostAdminCheck(client) {
	if (!IsFakeClient(client)) {
		new String:sDefaultDownloadURL[MAX_URL_LENGTH];
		GetConVarString(g_hCDownloadURL, sDefaultDownloadURL, sizeof(sDefaultDownloadURL));
		SendConVarValue(client, g_hCDownloadURL, sDefaultDownloadURL);
	}
}

/**
 * Registers a group of files to download.
 * Store the category plus description into the database if nonexistent,
 * returning an ID to the corresponding category.
 */
_:RegClientDownloadCategory(const String:category[], const String:description[], bool:enabled = true) {
	new Handle:hQuery = INVALID_HANDLE, iCategoryID;
	decl String:sQuery[MAX_SQL_QUERY_LENGTH];

	Format(sQuery, sizeof(sQuery), "SELECT categoryid FROM categories WHERE categoryname='%s'",
			category);
	hQuery = SQL_Query(g_hDatabase, sQuery);

	// Category does not exist; create it.
	if (SQL_GetRowCount(hQuery) == 0) {
		Format(sQuery, sizeof(sQuery),
				"INSERT OR REPLACE INTO categories (categoryid, categoryname, categorydesc, enabled) VALUES (NULL, '%s', '%s', '%b')",
				category, description, enabled);
		SQL_FastQuery(g_hDatabase, sQuery);
	}
	CloseHandle(hQuery);

	Format(sQuery, sizeof(sQuery), "SELECT categoryid FROM categories WHERE categoryname='%s'",
			category);
	hQuery = SQL_Query(g_hDatabase, sQuery);
	SQL_FetchRow(hQuery);
	iCategoryID = SQL_FetchInt(hQuery, 0);

	CloseHandle(hQuery);

	DownloadCategoryAdded(iCategoryID);

	return iCategoryID;
}

public Native_RegClientDownloadCategory(Handle:hPlugin, nParams) {
	decl String:category[512], String:description[512];

	GetNativeString(1, category, sizeof(category));
	GetNativeString(2, description, sizeof(description));
	new bool:bDefault = GetNativeCell(3);

	return RegClientDownloadCategory(category, description, bDefault);
}

/**
 * Registers a file to a category.
 */
RegClientDownloadFile(categoryid, const String:filepath[]) {
	decl String:sQuery[MAX_SQL_QUERY_LENGTH];
	Format(sQuery, sizeof(sQuery),
			"INSERT OR REPLACE INTO files (categoryid, filepath) VALUES (%d, '%s')",
			categoryid, filepath);
	SQL_FastQuery(g_hDatabase, sQuery);
}

public Native_RegClientDownloadFile(Handle:hPlugin, nParams) {
	new categoryid = GetNativeCell(1);
	decl String:filepath[PLATFORM_MAX_PATH]; GetNativeString(2, filepath, sizeof(filepath));

	RegClientDownloadFile(categoryid, filepath);
}

/**
 * Stores the client's download preference.
 */
SetClientDownloadPreference(client, categoryid, bool:enabled) {
	decl String:sQuery[MAX_SQL_QUERY_LENGTH];
	new sid3 = GetSteamAccountID(client);

	if (ClientHasDownloadPreference(client, categoryid)) {
		// Update existing entry.
		Format(sQuery, sizeof(sQuery), "UPDATE downloadprefs SET enabled = %d WHERE sid3 = %d AND categoryid = %d",
				_:enabled, sid3, categoryid);
	} else {
		// Create new entry.
		Format(sQuery, sizeof(sQuery), "INSERT INTO downloadprefs (sid3, categoryid, enabled) VALUES (%d, %d, %d)",
				sid3, categoryid, _:enabled);
	}
	SQL_FastQuery(g_hDatabase, sQuery);
}

public Native_SetClientDownloadPreference(Handle:hPlugin, nParams) {
	new client = GetNativeCell(1), categoryid = GetNativeCell(2), bool:enabled = GetNativeCell(3);
	SetClientDownloadPreference(client, categoryid, enabled);
}

/**
 * Retrieves a client's download preference.  If non-existent, will return the default setting.
 * A client will keep their existing download preference until a map change or reconnect.
 */
bool:GetClientDownloadPreference(client, categoryid) {
	decl String:sQuery[MAX_SQL_QUERY_LENGTH];
	new bool:bEnabled;

	if (!ClientHasDownloadPreference(client, categoryid, bEnabled)) {
		Format(sQuery, sizeof(sQuery), "SELECT enabled FROM categories WHERE categoryid=%d",
				categoryid);
		bEnabled = bool:SQL_QuerySingleRowInt(g_hDatabase, sQuery);
	}

	return bEnabled;
}

public Native_GetClientDownloadPreference(Handle:hPlugin, nParams) {
	new client = GetNativeCell(1), categoryid = GetNativeCell(2);
	return GetClientDownloadPreference(client, categoryid);
}

/**
 * Checks whether or not the client has their own download preference set.
 */
bool:ClientHasDownloadPreference(client, categoryid, &any:result = 0) {
	new sid3 = GetSteamAccountID(client);
	decl String:sQuery[MAX_SQL_QUERY_LENGTH];
	new Handle:hQuery = INVALID_HANDLE;
	new bool:bHasRows;

	Format(sQuery, sizeof(sQuery), "SELECT enabled FROM downloadprefs WHERE sid3=%d AND categoryid=%d",
			sid3, categoryid);
	hQuery = SQL_Query(g_hDatabase, sQuery);

	bHasRows = (SQL_GetRowCount(hQuery) > 0);

	if (bHasRows) {
		SQL_FetchRow(hQuery);
		result = bool:SQL_FetchInt(hQuery, 0);
	}

	CloseHandle(hQuery);

	return bHasRows;
}

public Native_ClientHasDownloadPreference(Handle:hPlugin, nParams) {
	new client = GetNativeCell(1), categoryid = GetNativeCell(2), result = GetNativeCellRef(3);
	return ClientHasDownloadPreference(client, categoryid, result);
}

/**
 * Gets the description of the download category.
 */
bool:GetDownloadCategoryInfo(categoryid, String:title[], maxTitleLength, String:description[], maxDescLength) {
	decl String:sQuery[MAX_SQL_QUERY_LENGTH];
	new Handle:hQuery = INVALID_HANDLE;

	Format(sQuery, sizeof(sQuery), "SELECT categoryname, categorydesc FROM categories WHERE categoryid=%d",
			categoryid);
	hQuery = SQL_Query(g_hDatabase, sQuery);

	new bool:bHasRows = (SQL_GetRowCount(hQuery) > 0);

	if (bHasRows) {
		SQL_FetchRow(hQuery);
		SQL_FetchString(hQuery, 0, title, maxTitleLength);
		SQL_FetchString(hQuery, 1, description, maxDescLength);
	}

	CloseHandle(hQuery);
	return bHasRows;
}

public Native_GetDownloadCategoryInfo(Handle:hPlugin, nParams) {
	new categoryid = GetNativeCell(1), maxTitleLength = GetNativeCell(3), maxDescLength = GetNativeCell(5);
	new String:title[maxTitleLength], String:description[maxDescLength];

	new bool:bResult = GetDownloadCategoryInfo(categoryid, title, maxTitleLength, description, maxDescLength);

	SetNativeString(2, title, maxTitleLength);
	SetNativeString(3, description, maxDescLength);

	return bResult;
}

DownloadCategoryAdded(categoryid) {
	new bool:bFound = false;

	for (new i = 0; i < g_nDownloadPrefs; i++) {
		bFound |= (categoryid == g_rgiDownloadPrefs[i]);
	}

	if (!bFound && g_nDownloadPrefs < MAX_DOWNLOAD_PREFERENCES) {
		g_rgiDownloadPrefs[g_nDownloadPrefs++] = categoryid;
	}
}

public Native_GetLoadedDownloadCategories(Handle:hPlugin, nParams) {
	new size = GetNativeCell(2), start = GetNativeCell(3), nCategories;
	new categoryids[size];

	for (new i = start; i < g_nDownloadPrefs; i++) {
		if (g_rgiDownloadPrefs[i] != INVALID_DOWNLOAD_CATEGORY) {
			categoryids[nCategories++] = g_rgiDownloadPrefs[i];
		}
	}

	SetNativeArray(1, categoryids, size);

	return nCategories;
}

/**
 * Runs a query, returning the first integer from the first row.
 */
_:SQL_QuerySingleRowInt(Handle:database, const String:query[]) {
	new result;
	new Handle:hQuery = SQL_Query(database, query);

	result = SQL_FetchInt(hQuery, 0);
	CloseHandle(hQuery);

	return result;
}

/**
 * Reads the database configuration.
 */
Handle:GetDatabase() {
	new Handle:hDatabase = INVALID_HANDLE;

	if (SQL_CheckConfig(DATABASE_NAME)) {
		decl String:sErrorBuffer[256];
		if ( (hDatabase = SQL_Connect(DATABASE_NAME, true, sErrorBuffer, sizeof(sErrorBuffer))) == INVALID_HANDLE ) {
			SetFailState("[downloadprefs] Could not connect to database: %s", sErrorBuffer);
		} else {
			return hDatabase;
		}
	} else {
		SetFailState("[downloadprefs] Could not find configuration %s to load database.", DATABASE_NAME);
	}
	return INVALID_HANDLE;
}

/**
 * Flow to check if a download is allowed.
 * When a client is authorized: -> Send custom virtual fastdl directory data
 * When a client downloads a file: ->
 *	  - Get SteamID3 from query string
 *	  - Get 'categoryid' by 'filepath' in table files (filepath obtained in query string),
 *	  - Get 'enabled' by 'sid3' and 'category' in downloadprefs,
 *	  - (If not found, get 'enabled' by 'categoryid' in table categories),
 *	  - If enabled, allow the download, otherwise redirect / 404.
 */

/**
 * Download Preferences
 * Author(s): nosoop
 * File:  downloadprefs.sp
 * Description:	 Allows clients to select categories of files to download.
 * License:	 MIT License
 */

#pragma semicolon 1
#include <sourcemod>

#define PLUGIN_VERSION			"0.6.0"

public Plugin:myinfo = {
	name = "Download Preferences",
	author = "nosoop",
	description = "Client download preferences",
	version = PLUGIN_VERSION,
	url = "http://github.com/nosoop/SM-DownloadPrefs"
}

#define MAX_DOWNLOAD_PREFERENCES 64
new g_rgiDownloadPrefs[MAX_DOWNLOAD_PREFERENCES],
	g_nDownloadPrefs;

#define DATABASE_NAME			"downloadprefs" // Database name in config
new Handle:g_hDatabase = INVALID_HANDLE;

new Handle:g_hCDownloadURL = INVALID_HANDLE, // sv_downloadurl
	Handle:g_hCDPrefURL = INVALID_HANDLE; // sm_dprefs_downloadurl

public OnPluginStart() {
	CreateConVar("sm_dprefs_version", PLUGIN_VERSION, _, FCVAR_PLUGIN | FCVAR_NOTIFY);

	g_nDownloadPrefs = 0;
	for (new i = 0; i < MAX_DOWNLOAD_PREFERENCES; i++) {
		g_rgiDownloadPrefs[i] = -1;
	}

	g_hCDownloadURL = FindConVar("sv_downloadurl");

	// Set redirect downloadurl.  If blank, the transmission of sv_downloadurl to the client is not changed.
	g_hCDPrefURL = CreateConVar("sm_dprefs_downloadurl", "", "Download URL to send to the client.  See README for details.", FCVAR_PLUGIN | FCVAR_SPONLY);

	AutoExecConfig(true);
}

public OnPluginEnd() {
	CloseHandle(g_hDatabase);
}

public APLRes:AskPluginLoad2(Handle:hMySelf, bool:bLate, String:strError[], iMaxErrors) {
	RegPluginLibrary("downloadprefs");

	CreateNative("RegClientDownloadCategory", Native_RegClientDownloadCategory);
	CreateNative("RegClientDownloadFile", Native_RegClientDownloadFile);
	CreateNative("SetClientDownloadPreference", Native_SetClientDownloadPreference);
	CreateNative("GetClientDownloadPreference", Native_GetClientDownloadPreference);
	CreateNative("ClientHasDownloadPreference", Native_ClientHasDownloadPreference);
	CreateNative("GetDownloadCategoryInfo", Native_GetDownloadCategoryInfo);
	CreateNative("GetLoadedDownloadCategories", Native_GetLoadedDownloadCategories);

	g_hDatabase = GetDatabase();

	return APLRes_Success;
}

/**
 * Sends a custom download URL when the client is connecting.
 * This occurs every time the client reconnects (via map change, etc.)
 */
public OnClientAuthorized(client, const String:auth[]) {
	if (!IsFakeClient(client)) {
		SendCustomDownloadURL(client);
	}
}

SendCustomDownloadURL(client) {
	new iSteamID3 = GetSteamAccountID(client);

	// downloadurl has no trailing slash
	new String:sClientDownloadURL[256];
	GetConVarString(g_hCDPrefURL, sClientDownloadURL, sizeof(sClientDownloadURL));

	if (strlen(sClientDownloadURL) > 0) {
		new String:sSteamID3[32];
		IntToString(iSteamID3, sSteamID3, sizeof(sSteamID3));

		// Replace $STEAMID with client's SteamID3 and transmit to client
		ReplaceString(sClientDownloadURL, sizeof(sClientDownloadURL), "$STEAMID", sSteamID3);

		SendConVarValue(client, g_hCDownloadURL, sClientDownloadURL);
	}
}

/**
 * Resets the download URL to the server-supplied value once the client is fully in-game.
 * This occurs after the client has performed any fast download requests.
 */
public OnClientPostAdminCheck(client) {
	if (!IsFakeClient(client)) {
		new String:sDefaultDownloadURL[256];
		GetConVarString(g_hCDownloadURL, sDefaultDownloadURL, sizeof(sDefaultDownloadURL));
		SendConVarValue(client, g_hCDownloadURL, sDefaultDownloadURL);
	}
}

/**
 * Registers a group of files to download.
 * Store the category plus description into the database if nonexistent,
 * returning an ID to the corresponding category.
 */
_:RegClientDownloadCategory(const String:category[], const String:description[], bool:enabled = true) {
	new Handle:hQuery = INVALID_HANDLE, iCategoryID;
	decl String:sQuery[1024];

	Format(sQuery, sizeof(sQuery),
			"SELECT categoryid FROM categories WHERE categoryname='%s'",
			category);
	hQuery = SQL_Query(g_hDatabase, sQuery);

	if (SQL_GetRowCount(hQuery) == 0) {
		Format(sQuery, sizeof(sQuery),
				"INSERT OR REPLACE INTO categories (categoryid, categoryname, categorydesc, enabled) VALUES (NULL, '%s', '%s', '%b')",
				category, description, enabled);
		SQL_FastQuery(g_hDatabase, sQuery);
	}
	CloseHandle(hQuery);

	Format(sQuery, sizeof(sQuery),
			"SELECT categoryid FROM categories WHERE categoryname='%s'",
			category);
	hQuery = SQL_Query(g_hDatabase, sQuery);

	SQL_FetchRow(hQuery);
	iCategoryID = SQL_FetchInt(hQuery, 0);

	CloseHandle(hQuery);

	DownloadCategoryAdded(iCategoryID);

	return iCategoryID;
}

public Native_RegClientDownloadCategory(Handle:hPlugin, nParams) {
	decl String:category[512], String:description[512];

	GetNativeString(1, category, sizeof(category));
	GetNativeString(2, description, sizeof(description));
	new bool:bDefault = GetNativeCell(3);

	return RegClientDownloadCategory(category, description, bDefault);
}

/**
 * Registers a file to a category.
 */
RegClientDownloadFile(categoryid, const String:filepath[]) {
	decl String:sQuery[1024];
	Format(sQuery, sizeof(sQuery),
			"INSERT OR REPLACE INTO files (categoryid, filepath) VALUES (%d, '%s')",
			categoryid, filepath);
	SQL_FastQuery(g_hDatabase, sQuery);
}

public Native_RegClientDownloadFile(Handle:hPlugin, nParams) {
	new categoryid = GetNativeCell(1);

	decl String:filepath[512];
	GetNativeString(2, filepath, sizeof(filepath));

	RegClientDownloadFile(categoryid, filepath);
}

/**
 * Stores the client's download preference.
 */
SetClientDownloadPreference(client, categoryid, bool:enabled) {
	decl String:sQuery[1024];

	new sid3 = GetSteamAccountID(client);

	if (ClientHasDownloadPreference(client, categoryid)) {
		// Update existing entry.
		Format(sQuery, sizeof(sQuery),
				"UPDATE downloadprefs SET enabled = %d WHERE sid3 = %d AND categoryid = %d",
				_:enabled, sid3, categoryid);
	} else {
		// Create new entry.
		Format(sQuery, sizeof(sQuery),
				"INSERT INTO downloadprefs (sid3, categoryid, enabled) VALUES (%d, %d, %d)",
				sid3, categoryid, _:enabled);
	}
	SQL_FastQuery(g_hDatabase, sQuery);
}

public Native_SetClientDownloadPreference(Handle:hPlugin, nParams) {
	new client = GetNativeCell(1),
		categoryid = GetNativeCell(2),
		bool:enabled = GetNativeCell(3);

	SetClientDownloadPreference(client, categoryid, enabled);
}

/**
 * Retrieves a client's download preference.  If non-existent, will return the default setting.
 * A client will keep their existing download preference until a map change or reconnect.
 */
bool:GetClientDownloadPreference(client, categoryid) {
	decl String:sQuery[1024];
	new bool:bEnabled;

	if (!ClientHasDownloadPreference(client, categoryid, bEnabled)) {
		Format(sQuery, sizeof(sQuery),
				"SELECT enabled FROM categories WHERE categoryid=%d",
				categoryid);
		bEnabled = bool:SQL_QuerySingleRowInt(g_hDatabase, sQuery);
	}

	return bEnabled;
}

public Native_GetClientDownloadPreference(Handle:hPlugin, nParams) {
	new client = GetNativeCell(1),
		categoryid = GetNativeCell(2);

	return GetClientDownloadPreference(client, categoryid);
}

/**
 * Checks whether or not the client has their own download preference set.
 */
bool:ClientHasDownloadPreference(client, categoryid, &any:result = 0) {
	new sid3 = GetSteamAccountID(client);
	decl String:sQuery[1024];
	new Handle:hQuery = INVALID_HANDLE;
	new bool:bHasRows;

	Format(sQuery, sizeof(sQuery),
			"SELECT enabled FROM downloadprefs WHERE sid3=%d AND categoryid=%d",
			sid3, categoryid);
	hQuery = SQL_Query(g_hDatabase, sQuery);

	bHasRows = (SQL_GetRowCount(hQuery) > 0);

	if (bHasRows) {
		SQL_FetchRow(hQuery);
		result = bool:SQL_FetchInt(hQuery, 0);
	}

	CloseHandle(hQuery);

	return bHasRows;
}

public Native_ClientHasDownloadPreference(Handle:hPlugin, nParams) {
	new client = GetNativeCell(1),
		categoryid = GetNativeCell(2),
		result = GetNativeCellRef(3);

	return ClientHasDownloadPreference(client, categoryid, result);
}

/**
 * Gets the description of the download category.
 */
bool:GetDownloadCategoryInfo(categoryid, String:title[], maxTitleLength, String:description[], maxDescLength) {
	decl String:sQuery[1024];
	new Handle:hQuery = INVALID_HANDLE;

	Format(sQuery, sizeof(sQuery),
			"SELECT categoryname, categorydesc FROM categories WHERE categoryid=%d",
			categoryid);
	hQuery = SQL_Query(g_hDatabase, sQuery);

	new bool:bHasRows = (SQL_GetRowCount(hQuery) > 0);

	if (bHasRows) {
		SQL_FetchRow(hQuery);
		SQL_FetchString(hQuery, 0, title, maxTitleLength);
		SQL_FetchString(hQuery, 1, description, maxDescLength);
	}

	CloseHandle(hQuery);
	return bHasRows;
}

public Native_GetDownloadCategoryInfo(Handle:hPlugin, nParams) {
	new categoryid = GetNativeCell(1),
		maxTitleLength = GetNativeCell(3),
		maxDescLength = GetNativeCell(5);

	new String:title[maxTitleLength],
		String:description[maxDescLength];

	new bool:bResult = GetDownloadCategoryInfo(categoryid, title, maxTitleLength, description, maxDescLength);

	SetNativeString(2, title, maxTitleLength);
	SetNativeString(3, description, maxDescLength);

	return bResult;
}

DownloadCategoryAdded(categoryid) {
	new bool:bFound = false;

	for (new i = 0; i < g_nDownloadPrefs; i++) {
		bFound |= (categoryid == g_rgiDownloadPrefs[i]);
	}

	if (!bFound && g_nDownloadPrefs < MAX_DOWNLOAD_PREFERENCES) {
		g_rgiDownloadPrefs[g_nDownloadPrefs++] = categoryid;
	}
}

public Native_GetLoadedDownloadCategories(Handle:hPlugin, nParams) {
	new size = GetNativeCell(2),
		start = GetNativeCell(3),
		nCategories;

	new categoryids[size];

	for (new i = start; i < g_nDownloadPrefs; i++) {
		if (g_rgiDownloadPrefs[i] != -1) {
			categoryids[nCategories++] = g_rgiDownloadPrefs[i];
		}
	}

	SetNativeArray(1, categoryids, size);

	return nCategories;
}

/**
 * Runs a query, returning the selected integer from the first row.
 */
_:SQL_QuerySingleRowInt(Handle:database, const String:query[]) {
	new result;
	new Handle:hQuery = SQL_Query(database, query);

	result = SQL_FetchInt(hQuery, 0);
	CloseHandle(hQuery);

	return result;
}

/**
 * Reads the database configuration.
 */
Handle:GetDatabase() {
	new Handle:hDatabase = INVALID_HANDLE;

	if (SQL_CheckConfig(DATABASE_NAME)) {
		decl String:sErrorBuffer[256];
		if ( (hDatabase = SQL_Connect(DATABASE_NAME, true, sErrorBuffer, sizeof(sErrorBuffer))) == INVALID_HANDLE ) {
			SetFailState("[downloadprefs] Could not connect to database: %s", sErrorBuffer);
		} else {
			return hDatabase;
		}
	} else {
		SetFailState("[downloadprefs] Could not find configuration %s to load database.", DATABASE_NAME);
	}
	return INVALID_HANDLE;
}

/**
 * Flow to check if a download is allowed.
 * When a client is authorized: -> Send custom virtual fastdl directory data
 * When a client downloads a file: ->
 *	  - Get SteamID3 from query string
 *	  - Get 'categoryid' by 'filepath' in table files (filepath obtained in query string),
 *	  - Get 'enabled' by 'sid3' and 'category' in downloadprefs,
 *	  - (If not found, get 'enabled' by 'categoryid' in table categories),
 *	  - If enabled, allow the download, otherwise redirect / 404.
 */


Add handle to update_map_play_time route

/**
 * vim: set ts=4 :
 * =============================================================================
 * map_votes
 * Interact with the Map Votes web api - https://github.com/CrimsonTautology/map_votes
 *
 * Copyright 2013 The Crimson Tautology
 * =============================================================================
 *
 */

#pragma semicolon 1

#include <sourcemod>
#include <steamtools>
#include <base64>
#include <smjansson>

#define PLUGIN_VERSION "0.1"

public Plugin:myinfo = {
    name = "MapVotes",
    author = "CrimsonTautology",
    description = "Interact with the Map Votes web api",
    version = PLUGIN_VERSION,
    url = "https://github.com/CrimsonTautology/sm_map_votes"
};

#define CAST_VOTE_ROUTE "/v1/api/cast_vote"
#define WRITE_MESSAGE_ROUTE "/v1/api/write_message"
#define FAVORITE_ROUTE "/v1/api/favorite"
#define UNFAVORITE_ROUTE "/v1/api/unfavorite"
#define GET_FAVORITES_ROUTE "/v1/api/get_favorites"
#define HAVE_NOT_VOTED_ROUTE "/v1/api/have_not_voted"
#define SERVER_QUERY_ROUTE "/v1/api/server_query"
#define UPDATE_MAP_PLAY_TIME_ROUTE "/v1/api/update_map_play_time"
#define MAPS_ROUTE "/maps"

#define MAX_STEAMID_LENGTH 21
#define MAX_COMMUNITYID_LENGTH 18

new Handle:g_Cvar_MapVotesUrl = INVALID_HANDLE;
new Handle:g_Cvar_MapVotesApiKey = INVALID_HANDLE;
new Handle:g_Cvar_MapVotesVotingEnabled = INVALID_HANDLE;
new Handle:g_Cvar_MapVotesCommentingEnabled = INVALID_HANDLE;
new Handle:g_Cvar_MapVotesNominationsName = INVALID_HANDLE;
new Handle:g_Cvar_MapVotesRequestCooldownTime = INVALID_HANDLE;

new bool:g_IsInCooldown[MAXPLAYERS+1];

new g_MapStartTimestamp = 0;

new g_MapFileSerial = -1;
new Handle:g_MapList = INVALID_HANDLE;
new Handle:g_MapTrie = INVALID_HANDLE;

new Handle:g_nominations = INVALID_HANDLE;
new Function:g_Handler_MapSelectMenu = INVALID_FUNCTION;

public OnPluginStart()
{

    g_Cvar_MapVotesUrl = CreateConVar("sm_map_votes_url", "", "URL to your Map Votes web page.  Remeber the \"http://\" part and to quote the entire url!");
    g_Cvar_MapVotesApiKey = CreateConVar("sm_map_votes_api_key", "", "The API key you generated to interact with the Map Votes web page");
    g_Cvar_MapVotesVotingEnabled = CreateConVar("sm_map_votes_voting_enabled", "1", "Whether players are allowed to vote on the current map");
    g_Cvar_MapVotesCommentingEnabled = CreateConVar("sm_map_votes_commenting_enabled", "1", "Whether players are allowed to comment on the current map");
    g_Cvar_MapVotesNominationsName = CreateConVar("sm_map_votes_nominations_plugin", "nominations.smx", "The nominations plugin used by the server");
    g_Cvar_MapVotesRequestCooldownTime = CreateConVar("sm_map_votes_request_cooldown_time", "2.0", "How long in seconds before a client can send another http request");

    RegConsoleCmd("sm_votemenu", Command_VoteMenu, "Bring up a menu to vote on the current map");
    RegConsoleCmd("sm_voteup", Command_VoteUp, "Vote that you like the current map");
    RegConsoleCmd("sm_like", Command_VoteUp, "Vote that you like the current map");
    RegConsoleCmd("sm_votedown", Command_VoteDown, "Vote that you hate the current map");
    RegConsoleCmd("sm_hate", Command_VoteDown, "Vote that you hate the current map");

    RegConsoleCmd("sm_fav", Command_Favorite, "Add this map to your favorites");
    RegConsoleCmd("sm_unfav", Command_Unfavorite, "Remove this map to your favorites");
    RegConsoleCmd("sm_nomfav", Command_GetFavorites, "Nominate from a list of your favorites");

    RegConsoleCmd("sm_mapcomment", Command_MapComment, "Comment on the current map");
    RegConsoleCmd("sm_mc", Command_MapComment, "Comment on the current map");

    RegConsoleCmd("sm_viewmap", Command_ViewMap, "View the Map Votes web page for this map");

    RegAdminCmd("sm_have_not_voted", Command_HaveNotVoted, ADMFLAG_VOTE, "Popup a vote panel to every player on the server that has not yet voted on this map");

    new array_size = ByteCountToCells(PLATFORM_MAX_PATH);
    g_MapList = CreateArray(array_size);
    g_MapTrie = CreateTrie();
    BuildMapListAndTrie();
}

public OnMapStart()
{
    g_MapStartTimestamp = GetTime();
}

public OnMapEnd()
{
    decl String:map[PLATFORM_MAX_PATH];
    GetCurrentMap(map, sizeof(map));
    UpdateMapPlayTime(g_MapStartTimestamp);
}

public OnConfigsExecuted()
{
    BuildMapListAndTrie();
}

public OnAllPluginsLoaded() {
    new String:noms[PLATFORM_MAX_PATH];
    GetConVarString(g_Cvar_MapVotesNominationsName, noms, sizeof(noms));
    g_nominations = FindPluginByFile(noms);

    //Check if nominations.smx is both available and currently running
    if(g_nominations == INVALID_HANDLE || GetPluginStatus(g_nominations) != Plugin_Running){
        SetFailState("[MapVotes] Error, nominations is currently not running");
    }
    else{
        //We should be clear to link the MapSelectMenu function
        g_Handler_MapSelectMenu = GetFunctionByName(g_nominations, "Handler_MapSelectMenu");
    }
}

public Action:Command_VoteMenu(client, args)
{
    if(IsClientInCooldown(client))
    {
        ReplyToCommand(client, "[MapVotes] User in cooldown");
        return Plugin_Handled;
    }

    if(!GetConVarBool(g_Cvar_MapVotesVotingEnabled))
    {
        ReplyToCommand(client, "[MapVotes] Voting not enabled");
        return Plugin_Handled;
    }

    if(client && IsClientAuthorized(client)){
        CallVoteOnClient(client);
    }

    return Plugin_Handled;
}

public Action:Command_VoteUp(client, args)
{
    if(IsClientInCooldown(client))
    {
        ReplyToCommand(client, "[MapVotes] User in cooldown");
        return Plugin_Handled;
    }

    if(!GetConVarBool(g_Cvar_MapVotesVotingEnabled))
    {
        ReplyToCommand(client, "[MapVotes] Voting not enabled");
        return Plugin_Handled;
    }

    if(client && IsClientAuthorized(client)){
        CastVote(client, 1);
    }

    return Plugin_Handled;
}

public Action:Command_VoteDown(client, args)
{
    if(IsClientInCooldown(client))
    {
        ReplyToCommand(client, "[MapVotes] User in cooldown");
        return Plugin_Handled;
    }

    if(!GetConVarBool(g_Cvar_MapVotesVotingEnabled))
    {
        ReplyToCommand(client, "[MapVotes] Voting not enabled");
        return Plugin_Handled;
    }

    if(client && IsClientAuthorized(client)){
        CastVote(client, -1);
    }

    return Plugin_Handled;
}

public Action:Command_Favorite(client, args)
{
    if(IsClientInCooldown(client))
    {
        ReplyToCommand(client, "[MapVotes] User in cooldown");
        return Plugin_Handled;
    }

    if(client && IsClientAuthorized(client)){
        new String:map[PLATFORM_MAX_PATH];
        if (args <= 0)
        {
            GetCurrentMap(map, sizeof(map));
            Favorite(map, client, true);
        }else{
            GetCmdArg(1, map, sizeof(map));
            MapSearch(client, map, g_MapList, FavoriteSearchHandler);
        }
    }

    return Plugin_Handled;
}
public Action:Command_Unfavorite(client, args)
{
    if(IsClientInCooldown(client))
    {
        ReplyToCommand(client, "[MapVotes] User in cooldown");
        return Plugin_Handled;
    }

    if(client && IsClientAuthorized(client)){
        new String:map[PLATFORM_MAX_PATH];
        if (args <= 0)
        {
            GetCurrentMap(map, sizeof(map));
            Favorite(map, client, false);
        }else{
            GetCmdArg(1, map, sizeof(map));
            MapSearch(client, map, g_MapList, UnfavoriteSearchHandler);
        }

    }

    return Plugin_Handled;
}
public Action:Command_GetFavorites(client, args)
{
    if(IsClientInCooldown(client))
    {
        ReplyToCommand(client, "[MapVotes] User in cooldown");
        return Plugin_Handled;
    }

    if(client && IsClientAuthorized(client)){
        GetFavorites(client);
    }

    return Plugin_Handled;
}

public Action:Command_ViewMap(client, args)
{
    if(client && IsClientAuthorized(client)){
        ViewMap(client);
    }

    return Plugin_Handled;
}

public Action:Command_MapComment(client, args)
{
    if(IsClientInCooldown(client))
    {
        ReplyToCommand(client, "[MapVotes] User in cooldown");
        return Plugin_Handled;
    }

    if (!GetConVarBool(g_Cvar_MapVotesCommentingEnabled))
    {
        ReplyToCommand(client, "[MapVotes] Commenting not enabled");
        return Plugin_Handled;
    }

    if (args < 1)
    {
        ReplyToCommand(client, "[MapVotes] Usage: !map_comment <comment>");
        return Plugin_Handled;
    }

    if(client && IsClientAuthorized(client)){
        decl String:comment[256];
        GetCmdArgString(comment, sizeof(comment));
        WriteMessage(client, comment);
    }

    return Plugin_Handled;
}

public Action:Command_HaveNotVoted(client, args)
{
    if(IsClientInCooldown(client))
    {
        ReplyToCommand(client, "[MapVotes] User in cooldown");
        return Plugin_Handled;
    }

    if(!GetConVarBool(g_Cvar_MapVotesVotingEnabled))
    {
        ReplyToCommand(client, "[MapVotes] Voting not enabled");
        return Plugin_Handled;
    }

    HaveNotVoted(client);

    return Plugin_Handled;
}

BuildMapListAndTrie()
{
    //Build the map list
    if (ReadMapList(g_MapList,
                g_MapFileSerial,
                "nominations",
                MAPLIST_FLAG_CLEARARRAY|MAPLIST_FLAG_MAPSFOLDER)
            == INVALID_HANDLE)
    {
        if (g_MapFileSerial == -1)
        {
            SetFailState("Unable to create a valid map list.");
        }else{
            //Build the map trie; note we don't care about the value, just if the map exists in the trie
            ClearTrie(g_MapTrie);
            new String:map[PLATFORM_MAX_PATH];

            for (new i = 0; i < GetArraySize(g_MapList); i++)
            {
                GetArrayString(g_MapList, i, map, sizeof(map));
                SetTrieValue(g_MapTrie, map, 1);
            }
        }
    }
}

public SetAccessCode(&HTTPRequestHandle:request)
{
    decl String:api_key[128];
    GetConVarString(g_Cvar_MapVotesApiKey, api_key, sizeof(api_key));
    Steam_SetHTTPRequestGetOrPostParameter(request, "access_token", api_key);
}

public HTTPRequestHandle:CreateMapVotesRequest(const String:route[])
{
    decl String:base_url[256], String:url[512];
    GetConVarString(g_Cvar_MapVotesUrl, base_url, sizeof(base_url));
    TrimString(base_url);
    new trim_length = strlen(base_url) - 1;

    if(trim_length < 0)
    {
        //MapVotes Url not set
        return INVALID_HTTP_HANDLE;
    }

    //check for forward slash after base_url;
    if(base_url[trim_length] == '/')
    {
        strcopy(base_url, trim_length + 1, base_url);
    }

    Format(url, sizeof(url),
            "%s%s", base_url, route);

    new HTTPRequestHandle:request = Steam_CreateHTTPRequest(HTTPMethod_POST, url);
    SetAccessCode(request);

    return request;
}

public Steam_SetHTTPRequestGetOrPostParameterInt(&HTTPRequestHandle:request, const String:param[], value)
{
    new String:tmp[64];
    IntToString(value, tmp, sizeof(tmp));
    Steam_SetHTTPRequestGetOrPostParameter(request, param, tmp);
}

public StartCooldown(client)
{
    //Ignore the server console
    if (client == 0)
        return;

    g_IsInCooldown[client] = true;
    CreateTimer(GetConVarFloat(g_Cvar_MapVotesRequestCooldownTime), RemoveCooldown, client);
}

public bool:IsClientInCooldown(client)
{
    if(client == 0)
        return false;
    else
        return g_IsInCooldown[client];
}

public Action:RemoveCooldown(Handle:timer, any:client)
{
    g_IsInCooldown[client] = false;
}

public WriteMessage(client, String:message[256])
{
    //Encode the message to be url safe
    decl String:base64[256], String:base64_url[256];
    EncodeBase64(base64, sizeof(base64), message);
    Base64MimeToUrl(base64_url, sizeof(base64_url), base64);

    decl String:uid[MAX_COMMUNITYID_LENGTH];
    Steam_GetCSteamIDForClient(client, uid, sizeof(uid));

    decl String:map[PLATFORM_MAX_PATH];
    GetCurrentMap(map, sizeof(map));

    new HTTPRequestHandle:request = CreateMapVotesRequest(WRITE_MESSAGE_ROUTE);

    if(request == INVALID_HTTP_HANDLE)
    {
        ReplyToCommand(client, "[MapVotes] sm_map_votes_url invalid; cannot create HTTP request");
        return;
    }

    Steam_SetHTTPRequestGetOrPostParameter(request, "map", map);
    Steam_SetHTTPRequestGetOrPostParameter(request, "uid", uid);
    Steam_SetHTTPRequestGetOrPostParameter(request, "comment", base64_url);
    Steam_SetHTTPRequestGetOrPostParameter(request, "base64", "1");
    Steam_SendHTTPRequest(request, ReceiveWriteMessage, GetClientUserId(client));
    StartCooldown(client);
}

public ReceiveWriteMessage(HTTPRequestHandle:request, bool:successful, HTTPStatusCode:code, any:userid)
{
    new client = GetClientOfUserId(userid);

    if(client && successful)
    {
        PrintToChat(client, "[MapVotes] Comment Added");
    }

    Steam_ReleaseHTTPRequest(request);
}

public CastVote(client, value)
{

    if(!(value == -1 || value == 0 || value == 1)){
        LogError("[MapVotes] invalid vote value %d (client: %d)", value, client);
    }else{
        decl String:map[PLATFORM_MAX_PATH];
        decl String:uid[MAX_COMMUNITYID_LENGTH];
        Steam_GetCSteamIDForClient(client, uid, sizeof(uid));

        GetCurrentMap(map, sizeof(map));

        new HTTPRequestHandle:request = CreateMapVotesRequest(CAST_VOTE_ROUTE);

        if(request == INVALID_HTTP_HANDLE)
        {
            ReplyToCommand(client, "[MapVotes] sm_map_votes_url invalid; cannot create HTTP request");
            return;
        }

        Steam_SetHTTPRequestGetOrPostParameter(request, "map", map);
        Steam_SetHTTPRequestGetOrPostParameter(request, "uid", uid);
        Steam_SetHTTPRequestGetOrPostParameterInt(request, "value", value);
        Steam_SendHTTPRequest(request, ReceiveCastVote, GetClientUserId(client));
        StartCooldown(client);
    }

}

public ReceiveCastVote(HTTPRequestHandle:request, bool:successful, HTTPStatusCode:code, any:userid)
{
    new client = GetClientOfUserId(userid);

    if(client && successful)
    {
        PrintToChat(client, "[MapVotes] Vote Cast");
    }

    Steam_ReleaseHTTPRequest(request);
}

public Favorite(String:map[PLATFORM_MAX_PATH], client, bool:favorite)
{
    decl String:uid[MAX_COMMUNITYID_LENGTH];
    Steam_GetCSteamIDForClient(client, uid, sizeof(uid));

    new HTTPRequestHandle:request;
    if(favorite)
    {
        request = CreateMapVotesRequest(FAVORITE_ROUTE);
    }else{
        request = CreateMapVotesRequest(UNFAVORITE_ROUTE);
    }

    if(request == INVALID_HTTP_HANDLE)
    {
        ReplyToCommand(client, "[MapVotes] sm_map_votes_url invalid; cannot create HTTP request");
        return;
    }

    Steam_SetHTTPRequestGetOrPostParameter(request, "map", map);
    Steam_SetHTTPRequestGetOrPostParameter(request, "uid", uid);
    Steam_SendHTTPRequest(request, ReceiveFavorite, GetClientUserId(client));
    StartCooldown(client);
}

public ReceiveFavorite(HTTPRequestHandle:request, bool:successful, HTTPStatusCode:code, any:userid)
{
    new client = GetClientOfUserId(userid);

    if(client && successful)
    {
        PrintToChat(client, "[MapVotes] Updated Favorites");
    }

    Steam_ReleaseHTTPRequest(request);
}

public MapSearch(client, String:search_key[PLATFORM_MAX_PATH], Handle:map_list, MenuHandler:handler)
{
    new String:map[PLATFORM_MAX_PATH];
    new Handle:menu = CreateMenu(handler, MENU_ACTIONS_DEFAULT|MenuAction_DrawItem|MenuAction_DisplayItem);
    new bool:found = false;

    for(new i=0; i<GetArraySize(map_list); i++)
    {
        GetArrayString(map_list, i, map, sizeof(map));

        //If this map matches the search key, add it to the menu
        if(StrContains(map, search_key, false) >= 0){
            AddMenuItem(menu, map, map);
            found = true;
        }
    }

    if(found)
    {
        SetMenuTitle(menu, "Found Maps");
        DisplayMenu(menu, client, MENU_TIME_FOREVER);
    }

}

public FavoriteSearchHandler(Handle:menu, MenuAction:action, param1, param2)
{
    if (action == MenuAction_End)
    {
        CloseHandle(menu);
    } else if (action == MenuAction_VoteCancel)
    {
    } else if (action == MenuAction_Select)
    {
        new client=param1;
        new String:map[PLATFORM_MAX_PATH];
        GetMenuItem(menu, param2, map, sizeof(map));
        Favorite(map, client, true);
    }
}

public UnfavoriteSearchHandler(Handle:menu, MenuAction:action, param1, param2)
{
    if (action == MenuAction_End)
    {
        CloseHandle(menu);
    } else if (action == MenuAction_VoteCancel)
    {
    } else if (action == MenuAction_Select)
    {
        new client=param1;
        new String:map[PLATFORM_MAX_PATH];
        GetMenuItem(menu, param2, map, sizeof(map));
        Favorite(map, client, false);
    }
}

public GetFavorites(client)
{
    decl String:uid[MAX_COMMUNITYID_LENGTH];
    Steam_GetCSteamIDForClient(client, uid, sizeof(uid));

    new HTTPRequestHandle:request = CreateMapVotesRequest(GET_FAVORITES_ROUTE);

    if(request == INVALID_HTTP_HANDLE)
    {
        ReplyToCommand(client, "[MapVotes] sm_map_votes_url invalid; cannot create HTTP request");
        return;
    }

    Steam_SetHTTPRequestGetOrPostParameterInt(request, "player", GetClientUserId(client));
    Steam_SetHTTPRequestGetOrPostParameter(request, "uid", uid);
    Steam_SendHTTPRequest(request, ReceiveGetFavorites, GetClientUserId(client));
    StartCooldown(client);
}

public ReceiveGetFavorites(HTTPRequestHandle:request, bool:successful, HTTPStatusCode:code, any:userid)
{
    new client = GetClientOfUserId(userid);
    if(!client)
    {
        //User logged off
        Steam_ReleaseHTTPRequest(request);
        return;
    }
    if(!successful || code != HTTPStatusCode_OK)
    {
        LogError("[MapVotes] Error at RecivedGetFavorites (HTTP Code %d; success %d)", code, successful);
        Steam_ReleaseHTTPRequest(request);
        return;
    }

    decl String:data[4096];
    Steam_GetHTTPResponseBodyData(request, data, sizeof(data));
    Steam_ReleaseHTTPRequest(request);

    new Handle:json = json_load(data);
    new Handle:maps = json_object_get(json, "maps");
    new String:map_buffer[PLATFORM_MAX_PATH];
    new junk;
    new bool:found;

    new Handle:menu = CreateMenu(NominateMapHandler, MENU_ACTIONS_DEFAULT|MenuAction_DrawItem|MenuAction_DisplayItem);

    for(new i = 0; i < json_array_size(maps); i++)
    {
        json_array_get_string(maps, i, map_buffer, sizeof(map_buffer));
        TrimString(map_buffer);
        found = GetTrieValue(g_MapTrie, map_buffer, junk);
        if(found)
        {
            AddMenuItem(menu, map_buffer, map_buffer);
        }
    }

    CloseHandle(json);
    CloseHandle(maps);

    //If no maps were found don't even bother displaying a menu
    if(GetMenuItemCount(menu) > 0){
        SetMenuTitle(menu, "Favorited Maps");
        DisplayMenu(menu, client, MENU_TIME_FOREVER);
    }else{
        ReplyToCommand(client, "[MapVotes] You have no favorited maps that are on this server.");
    }
}

public NominateMapHandler(Handle:menu, MenuAction:action, param1, param2)
{

    decl result;

    // Start function call
    Call_StartFunction(g_nominations, g_Handler_MapSelectMenu);

    // Push parameters one at a time
    Call_PushCell(menu);
    Call_PushCell(action);
    Call_PushCell(param1);
    Call_PushCell(param2);

    // Finish the call, get the result
    Call_Finish(result);

    return result;
}

public CallVoteOnClient(client)
{
    new Handle:menu = CreateMenu(VoteMenuHandler);
    SetMenuTitle(menu, "Do you like this map?");
    AddMenuItem(menu, "1","Like it.");
    AddMenuItem(menu, "-1","Hate it.");
    AddMenuItem(menu, "0","I have no strong feelings one way or the other.");
    DisplayMenu(menu, client, 20);
}

public VoteMenuHandler(Handle:menu, MenuAction:action, param1, param2)
{
    if (action == MenuAction_End)
    {
        CloseHandle(menu);
    } else if (action == MenuAction_VoteCancel)
    {
    } else if (action == MenuAction_Select)
    {
        new String:info[32];
        GetMenuItem(menu, param2, info, sizeof(info));
        new value = StringToInt(info);

        CastVote(param1, value);
    }
}

public HaveNotVoted(caller)
{
    decl String:uid[MAX_COMMUNITYID_LENGTH];
    decl String:map[PLATFORM_MAX_PATH];
    new player;
    new HTTPRequestHandle:request = CreateMapVotesRequest(HAVE_NOT_VOTED_ROUTE);

    if(request == INVALID_HTTP_HANDLE)
    {
        ReplyToCommand(caller, "[MapVotes] sm_map_votes_url invalid; cannot create HTTP request");
        return;
    }

    GetCurrentMap(map, sizeof(map));
    Steam_SetHTTPRequestGetOrPostParameter(request, "map", map);

    for (new client=1; client <= MaxClients; client++)
    {
        if (!IsClientInGame(client) || IsFakeClient(client))
        {
            continue;
        }
        Steam_GetCSteamIDForClient(client, uid, sizeof(uid));
        player = GetClientUserId(client);

        Steam_SetHTTPRequestGetOrPostParameter(request, "uids", uid);
        Steam_SetHTTPRequestGetOrPostParameterInt(request, "players", player);
    }

    Steam_SendHTTPRequest(request, ReceiveHaveNotVoted, caller);
}

public ReceiveHaveNotVoted(HTTPRequestHandle:request, bool:successful, HTTPStatusCode:code, any:userid)
{
    if(!successful || code != HTTPStatusCode_OK)
    {
        LogError("[MapVotes] Error at RecivedHaveNotVoted (HTTP Code %d)", code);
        Steam_ReleaseHTTPRequest(request);
        return;
    }

    decl String:data[4096];
    Steam_GetHTTPResponseBodyData(request, data, sizeof(data));
    Steam_ReleaseHTTPRequest(request);

    new Handle:json = json_load(data);
    new Handle:players = json_object_get(json, "players");
    decl p;

    for(new i = 0; i < json_array_size(players); i++)
    {
        p = json_array_get_int(players, i);
        CallVoteOnClient(GetClientOfUserId(p));
    }
    CloseHandle(json);
    CloseHandle(players);
}

public UpdateMapPlayTime(start_time)
{
    new time_played = GetTime() - start_time;

    //Reject way off values
    if(time_played < 0 || time_played > 86400000)
    {
        LogError("[MapVotes] cannot update map time played; time_played=%d", time_played);
        return;
    }

    decl String:map[PLATFORM_MAX_PATH];
    GetCurrentMap(map, sizeof(map));

    new HTTPRequestHandle:request = CreateMapVotesRequest(UPDATE_MAP_PLAY_TIME_ROUTE);

    if(request == INVALID_HTTP_HANDLE)
    {
        LogError("[MapVotes] sm_map_votes_url invalid; cannot create HTTP request");
        return;
    }

    Steam_SetHTTPRequestGetOrPostParameter(request, "map", map);
    Steam_SetHTTPRequestGetOrPostParameterInt(request, "time_played", time_played);
    Steam_SendHTTPRequest(request, ReceiveUpdateMapPlayTime);
}

public ReceiveUpdateMapPlayTime(HTTPRequestHandle:request, bool:successful, HTTPStatusCode:code, any:userid)
{
    if(!successful || code != HTTPStatusCode_OK)
    {
        LogError("[MapVotes] Error at ReceiveUpdateMapPlayTime (HTTP Code %d; success %d)", code, successful);
        Steam_ReleaseHTTPRequest(request);
        return;
    }

    Steam_ReleaseHTTPRequest(request);
}

public ViewMap(client)
{
    decl String:map[PLATFORM_MAX_PATH], String:url[256], String:base_url[128];
    GetCurrentMap(map, sizeof(map));
    GetConVarString(g_Cvar_MapVotesUrl, base_url, sizeof(base_url));

    TrimString(base_url);
    new trim_length = strlen(base_url) - 1;

    if(base_url[trim_length] == '/')
    {
        strcopy(base_url, trim_length + 1, base_url);
    }

    Format(url, sizeof(url),
            "%s%s/%s", base_url, MAPS_ROUTE, map);

    ShowMOTDPanel(client, "Map Viewer", url, MOTDPANEL_TYPE_URL);

}

/**
 * vim: set ts=4 :
 * =============================================================================
 * map_votes
 * Interact with the Map Votes web api - https://github.com/CrimsonTautology/map_votes
 *
 * Copyright 2013 The Crimson Tautology
 * =============================================================================
 *
 */

#pragma semicolon 1

#include <sourcemod>
#include <steamtools>
#include <base64>
#include <smjansson>

#define PLUGIN_VERSION "0.1"

public Plugin:myinfo = {
    name = "MapVotes",
    author = "CrimsonTautology",
    description = "Interact with the Map Votes web api",
    version = PLUGIN_VERSION,
    url = "https://github.com/CrimsonTautology/sm_map_votes"
};

#define CAST_VOTE_ROUTE "/v1/api/cast_vote"
#define WRITE_MESSAGE_ROUTE "/v1/api/write_message"
#define FAVORITE_ROUTE "/v1/api/favorite"
#define UNFAVORITE_ROUTE "/v1/api/unfavorite"
#define GET_FAVORITES_ROUTE "/v1/api/get_favorites"
#define HAVE_NOT_VOTED_ROUTE "/v1/api/have_not_voted"
#define SERVER_QUERY_ROUTE "/v1/api/server_query"
#define MAPS_ROUTE "/maps"

#define MAX_STEAMID_LENGTH 21
#define MAX_COMMUNITYID_LENGTH 18

new Handle:g_Cvar_MapVotesUrl = INVALID_HANDLE;
new Handle:g_Cvar_MapVotesApiKey = INVALID_HANDLE;
new Handle:g_Cvar_MapVotesVotingEnabled = INVALID_HANDLE;
new Handle:g_Cvar_MapVotesCommentingEnabled = INVALID_HANDLE;
new Handle:g_Cvar_MapVotesNominationsName = INVALID_HANDLE;
new Handle:g_Cvar_MapVotesRequestCooldownTime = INVALID_HANDLE;

new bool:g_IsInCooldown[MAXPLAYERS+1];

new g_MapFileSerial = -1;
new Handle:g_MapList = INVALID_HANDLE;
new Handle:g_MapTrie = INVALID_HANDLE;

new Handle:g_nominations = INVALID_HANDLE;
new Function:g_Handler_MapSelectMenu = INVALID_FUNCTION;

public OnPluginStart()
{

    g_Cvar_MapVotesUrl = CreateConVar("sm_map_votes_url", "", "URL to your Map Votes web page.  Remeber the \"http://\" part and to quote the entire url!");
    g_Cvar_MapVotesApiKey = CreateConVar("sm_map_votes_api_key", "", "The API key you generated to interact with the Map Votes web page");
    g_Cvar_MapVotesVotingEnabled = CreateConVar("sm_map_votes_voting_enabled", "1", "Whether players are allowed to vote on the current map");
    g_Cvar_MapVotesCommentingEnabled = CreateConVar("sm_map_votes_commenting_enabled", "1", "Whether players are allowed to comment on the current map");
    g_Cvar_MapVotesNominationsName = CreateConVar("sm_map_votes_nominations_plugin", "nominations.smx", "The nominations plugin used by the server");
    g_Cvar_MapVotesRequestCooldownTime = CreateConVar("sm_map_votes_request_cooldown_time", "2.0", "How long in seconds before a client can send another http request");

    RegConsoleCmd("sm_votemenu", Command_VoteMenu, "Bring up a menu to vote on the current map");
    RegConsoleCmd("sm_voteup", Command_VoteUp, "Vote that you like the current map");
    RegConsoleCmd("sm_like", Command_VoteUp, "Vote that you like the current map");
    RegConsoleCmd("sm_votedown", Command_VoteDown, "Vote that you hate the current map");
    RegConsoleCmd("sm_hate", Command_VoteDown, "Vote that you hate the current map");

    RegConsoleCmd("sm_fav", Command_Favorite, "Add this map to your favorites");
    RegConsoleCmd("sm_unfav", Command_Unfavorite, "Remove this map to your favorites");
    RegConsoleCmd("sm_nomfav", Command_GetFavorites, "Nominate from a list of your favorites");

    RegConsoleCmd("sm_mapcomment", Command_MapComment, "Comment on the current map");
    RegConsoleCmd("sm_mc", Command_MapComment, "Comment on the current map");

    RegConsoleCmd("sm_viewmap", Command_ViewMap, "View the Map Votes web page for this map");

    RegAdminCmd("sm_have_not_voted", Command_HaveNotVoted, ADMFLAG_VOTE, "Popup a vote panel to every player on the server that has not yet voted on this map");

    new array_size = ByteCountToCells(PLATFORM_MAX_PATH);
    g_MapList = CreateArray(array_size);
    g_MapTrie = CreateTrie();
    BuildMapListAndTrie();
}

public OnConfigsExecuted()
{
    BuildMapListAndTrie();
}

public OnAllPluginsLoaded() {
    new String:noms[PLATFORM_MAX_PATH];
    GetConVarString(g_Cvar_MapVotesNominationsName, noms, sizeof(noms));
    g_nominations = FindPluginByFile(noms);

    //Check if nominations.smx is both available and currently running
    if(g_nominations == INVALID_HANDLE || GetPluginStatus(g_nominations) != Plugin_Running){
        SetFailState("[MapVotes] Error, nominations is currently not running");
    }
    else{
        //We should be clear to link the MapSelectMenu function
        g_Handler_MapSelectMenu = GetFunctionByName(g_nominations, "Handler_MapSelectMenu");
    }
}

public Action:Command_VoteMenu(client, args)
{
    if(IsClientInCooldown(client))
    {
        ReplyToCommand(client, "[MapVotes] User in cooldown");
        return Plugin_Handled;
    }

    if(!GetConVarBool(g_Cvar_MapVotesVotingEnabled))
    {
        ReplyToCommand(client, "[MapVotes] Voting not enabled");
        return Plugin_Handled;
    }

    if(client && IsClientAuthorized(client)){
        CallVoteOnClient(client);
    }

    return Plugin_Handled;
}

public Action:Command_VoteUp(client, args)
{
    if(IsClientInCooldown(client))
    {
        ReplyToCommand(client, "[MapVotes] User in cooldown");
        return Plugin_Handled;
    }

    if(!GetConVarBool(g_Cvar_MapVotesVotingEnabled))
    {
        ReplyToCommand(client, "[MapVotes] Voting not enabled");
        return Plugin_Handled;
    }

    if(client && IsClientAuthorized(client)){
        CastVote(client, 1);
    }

    return Plugin_Handled;
}

public Action:Command_VoteDown(client, args)
{
    if(IsClientInCooldown(client))
    {
        ReplyToCommand(client, "[MapVotes] User in cooldown");
        return Plugin_Handled;
    }

    if(!GetConVarBool(g_Cvar_MapVotesVotingEnabled))
    {
        ReplyToCommand(client, "[MapVotes] Voting not enabled");
        return Plugin_Handled;
    }

    if(client && IsClientAuthorized(client)){
        CastVote(client, -1);
    }

    return Plugin_Handled;
}

public Action:Command_Favorite(client, args)
{
    if(IsClientInCooldown(client))
    {
        ReplyToCommand(client, "[MapVotes] User in cooldown");
        return Plugin_Handled;
    }

    if(client && IsClientAuthorized(client)){
        new String:map[PLATFORM_MAX_PATH];
        if (args <= 0)
        {
            GetCurrentMap(map, sizeof(map));
            Favorite(map, client, true);
        }else{
            GetCmdArg(1, map, sizeof(map));
            MapSearch(client, map, g_MapList, FavoriteSearchHandler);
        }
    }

    return Plugin_Handled;
}
public Action:Command_Unfavorite(client, args)
{
    if(IsClientInCooldown(client))
    {
        ReplyToCommand(client, "[MapVotes] User in cooldown");
        return Plugin_Handled;
    }

    if(client && IsClientAuthorized(client)){
        new String:map[PLATFORM_MAX_PATH];
        if (args <= 0)
        {
            GetCurrentMap(map, sizeof(map));
            Favorite(map, client, false);
        }else{
            GetCmdArg(1, map, sizeof(map));
            MapSearch(client, map, g_MapList, UnfavoriteSearchHandler);
        }

    }

    return Plugin_Handled;
}
public Action:Command_GetFavorites(client, args)
{
    if(IsClientInCooldown(client))
    {
        ReplyToCommand(client, "[MapVotes] User in cooldown");
        return Plugin_Handled;
    }

    if(client && IsClientAuthorized(client)){
        GetFavorites(client);
    }

    return Plugin_Handled;
}

public Action:Command_ViewMap(client, args)
{
    if(client && IsClientAuthorized(client)){
        ViewMap(client);
    }

    return Plugin_Handled;
}

public Action:Command_MapComment(client, args)
{
    if(IsClientInCooldown(client))
    {
        ReplyToCommand(client, "[MapVotes] User in cooldown");
        return Plugin_Handled;
    }

    if (!GetConVarBool(g_Cvar_MapVotesCommentingEnabled))
    {
        ReplyToCommand(client, "[MapVotes] Commenting not enabled");
        return Plugin_Handled;
    }

    if (args < 1)
    {
        ReplyToCommand(client, "[MapVotes] Usage: !map_comment <comment>");
        return Plugin_Handled;
    }

    if(client && IsClientAuthorized(client)){
        decl String:comment[256];
        GetCmdArgString(comment, sizeof(comment));
        WriteMessage(client, comment);
    }

    return Plugin_Handled;
}

public Action:Command_HaveNotVoted(client, args)
{
    if(IsClientInCooldown(client))
    {
        ReplyToCommand(client, "[MapVotes] User in cooldown");
        return Plugin_Handled;
    }

    if(!GetConVarBool(g_Cvar_MapVotesVotingEnabled))
    {
        ReplyToCommand(client, "[MapVotes] Voting not enabled");
        return Plugin_Handled;
    }

    HaveNotVoted(client);

    return Plugin_Handled;
}

BuildMapListAndTrie()
{
    //Build the map list
    if (ReadMapList(g_MapList,
                g_MapFileSerial,
                "nominations",
                MAPLIST_FLAG_CLEARARRAY|MAPLIST_FLAG_MAPSFOLDER)
            == INVALID_HANDLE)
    {
        if (g_MapFileSerial == -1)
        {
            SetFailState("Unable to create a valid map list.");
        }else{
            //Build the map trie; note we don't care about the value, just if the map exists in the trie
            ClearTrie(g_MapTrie);
            new String:map[PLATFORM_MAX_PATH];

            for (new i = 0; i < GetArraySize(g_MapList); i++)
            {
                GetArrayString(g_MapList, i, map, sizeof(map));
                SetTrieValue(g_MapTrie, map, 1);
            }
        }
    }
}

public SetAccessCode(&HTTPRequestHandle:request)
{
    decl String:api_key[128];
    GetConVarString(g_Cvar_MapVotesApiKey, api_key, sizeof(api_key));
    Steam_SetHTTPRequestGetOrPostParameter(request, "access_token", api_key);
}

public HTTPRequestHandle:CreateMapVotesRequest(const String:route[])
{
    decl String:base_url[256], String:url[512];
    GetConVarString(g_Cvar_MapVotesUrl, base_url, sizeof(base_url));
    TrimString(base_url);
    new trim_length = strlen(base_url) - 1;

    if(trim_length < 0)
    {
        //MapVotes Url not set
        return INVALID_HTTP_HANDLE;
    }

    //check for forward slash after base_url;
    if(base_url[trim_length] == '/')
    {
        strcopy(base_url, trim_length + 1, base_url);
    }

    Format(url, sizeof(url),
            "%s%s", base_url, route);

    new HTTPRequestHandle:request = Steam_CreateHTTPRequest(HTTPMethod_POST, url);
    SetAccessCode(request);

    return request;
}

public Steam_SetHTTPRequestGetOrPostParameterInt(&HTTPRequestHandle:request, const String:param[], value)
{
    new String:tmp[64];
    IntToString(value, tmp, sizeof(tmp));
    Steam_SetHTTPRequestGetOrPostParameter(request, param, tmp);
}

public StartCooldown(client)
{
    //Ignore the server console
    if (client == 0)
        return;

    g_IsInCooldown[client] = true;
    CreateTimer(GetConVarFloat(g_Cvar_MapVotesRequestCooldownTime), RemoveCooldown, client);
}

public bool:IsClientInCooldown(client)
{
    if(client == 0)
        return false;
    else
        return g_IsInCooldown[client];
}

public Action:RemoveCooldown(Handle:timer, any:client)
{
    g_IsInCooldown[client] = false;
}

public WriteMessage(client, String:message[256])
{
    //Encode the message to be url safe
    decl String:base64[256], String:base64_url[256];
    EncodeBase64(base64, sizeof(base64), message);
    Base64MimeToUrl(base64_url, sizeof(base64_url), base64);

    decl String:uid[MAX_COMMUNITYID_LENGTH];
    Steam_GetCSteamIDForClient(client, uid, sizeof(uid));

    decl String:map[PLATFORM_MAX_PATH];
    GetCurrentMap(map, sizeof(map));

    new HTTPRequestHandle:request = CreateMapVotesRequest(WRITE_MESSAGE_ROUTE);

    if(request == INVALID_HTTP_HANDLE)
    {
        ReplyToCommand(client, "[MapVotes] sm_map_votes_url invalid; cannot create HTTP request");
        return;
    }

    Steam_SetHTTPRequestGetOrPostParameter(request, "map", map);
    Steam_SetHTTPRequestGetOrPostParameter(request, "uid", uid);
    Steam_SetHTTPRequestGetOrPostParameter(request, "comment", base64_url);
    Steam_SetHTTPRequestGetOrPostParameter(request, "base64", "1");
    Steam_SendHTTPRequest(request, ReceiveWriteMessage, GetClientUserId(client));
    StartCooldown(client);
}

public ReceiveWriteMessage(HTTPRequestHandle:request, bool:successful, HTTPStatusCode:code, any:userid)
{
    new client = GetClientOfUserId(userid);

    if(client && successful)
    {
        PrintToChat(client, "[MapVotes] Comment Added");
    }

    Steam_ReleaseHTTPRequest(request);
}

public CastVote(client, value)
{

    if(!(value == -1 || value == 0 || value == 1)){
        LogError("[MapVotes] invalid vote value %d (client: %d)", value, client);
    }else{
        decl String:map[PLATFORM_MAX_PATH];
        decl String:uid[MAX_COMMUNITYID_LENGTH];
        Steam_GetCSteamIDForClient(client, uid, sizeof(uid));

        GetCurrentMap(map, sizeof(map));

        new HTTPRequestHandle:request = CreateMapVotesRequest(CAST_VOTE_ROUTE);

        if(request == INVALID_HTTP_HANDLE)
        {
            ReplyToCommand(client, "[MapVotes] sm_map_votes_url invalid; cannot create HTTP request");
            return;
        }

        Steam_SetHTTPRequestGetOrPostParameter(request, "map", map);
        Steam_SetHTTPRequestGetOrPostParameter(request, "uid", uid);
        Steam_SetHTTPRequestGetOrPostParameterInt(request, "value", value);
        Steam_SendHTTPRequest(request, ReceiveCastVote, GetClientUserId(client));
        StartCooldown(client);
    }

}

public ReceiveCastVote(HTTPRequestHandle:request, bool:successful, HTTPStatusCode:code, any:userid)
{
    new client = GetClientOfUserId(userid);

    if(client && successful)
    {
        PrintToChat(client, "[MapVotes] Vote Cast");
    }

    Steam_ReleaseHTTPRequest(request);
}

public Favorite(String:map[PLATFORM_MAX_PATH], client, bool:favorite)
{
    decl String:uid[MAX_COMMUNITYID_LENGTH];
    Steam_GetCSteamIDForClient(client, uid, sizeof(uid));

    new HTTPRequestHandle:request;
    if(favorite)
    {
        request = CreateMapVotesRequest(FAVORITE_ROUTE);
    }else{
        request = CreateMapVotesRequest(UNFAVORITE_ROUTE);
    }

    if(request == INVALID_HTTP_HANDLE)
    {
        ReplyToCommand(client, "[MapVotes] sm_map_votes_url invalid; cannot create HTTP request");
        return;
    }

    Steam_SetHTTPRequestGetOrPostParameter(request, "map", map);
    Steam_SetHTTPRequestGetOrPostParameter(request, "uid", uid);
    Steam_SendHTTPRequest(request, ReceiveFavorite, GetClientUserId(client));
    StartCooldown(client);
}

public ReceiveFavorite(HTTPRequestHandle:request, bool:successful, HTTPStatusCode:code, any:userid)
{
    new client = GetClientOfUserId(userid);

    if(client && successful)
    {
        PrintToChat(client, "[MapVotes] Updated Favorites");
    }

    Steam_ReleaseHTTPRequest(request);
}

public MapSearch(client, String:search_key[PLATFORM_MAX_PATH], Handle:map_list, MenuHandler:handler)
{
    new String:map[PLATFORM_MAX_PATH];
    new Handle:menu = CreateMenu(handler, MENU_ACTIONS_DEFAULT|MenuAction_DrawItem|MenuAction_DisplayItem);
    new bool:found = false;

    for(new i=0; i<GetArraySize(map_list); i++)
    {
        GetArrayString(map_list, i, map, sizeof(map));

        //If this map matches the search key, add it to the menu
        if(StrContains(map, search_key, false) >= 0){
            AddMenuItem(menu, map, map);
            found = true;
        }
    }

    if(found)
    {
        SetMenuTitle(menu, "Found Maps");
        DisplayMenu(menu, client, MENU_TIME_FOREVER);
    }

}

public FavoriteSearchHandler(Handle:menu, MenuAction:action, param1, param2)
{
    if (action == MenuAction_End)
    {
        CloseHandle(menu);
    } else if (action == MenuAction_VoteCancel)
    {
    } else if (action == MenuAction_Select)
    {
        new client=param1;
        new String:map[PLATFORM_MAX_PATH];
        GetMenuItem(menu, param2, map, sizeof(map));
        Favorite(map, client, true);
    }
}

public UnfavoriteSearchHandler(Handle:menu, MenuAction:action, param1, param2)
{
    if (action == MenuAction_End)
    {
        CloseHandle(menu);
    } else if (action == MenuAction_VoteCancel)
    {
    } else if (action == MenuAction_Select)
    {
        new client=param1;
        new String:map[PLATFORM_MAX_PATH];
        GetMenuItem(menu, param2, map, sizeof(map));
        Favorite(map, client, false);
    }
}

public GetFavorites(client)
{
    decl String:uid[MAX_COMMUNITYID_LENGTH];
    Steam_GetCSteamIDForClient(client, uid, sizeof(uid));

    new HTTPRequestHandle:request = CreateMapVotesRequest(GET_FAVORITES_ROUTE);

    if(request == INVALID_HTTP_HANDLE)
    {
        ReplyToCommand(client, "[MapVotes] sm_map_votes_url invalid; cannot create HTTP request");
        return;
    }

    Steam_SetHTTPRequestGetOrPostParameterInt(request, "player", GetClientUserId(client));
    Steam_SetHTTPRequestGetOrPostParameter(request, "uid", uid);
    Steam_SendHTTPRequest(request, ReceiveGetFavorites, GetClientUserId(client));
    StartCooldown(client);
}

public ReceiveGetFavorites(HTTPRequestHandle:request, bool:successful, HTTPStatusCode:code, any:userid)
{
    new client = GetClientOfUserId(userid);
    if(!client)
    {
        //User logged off
        Steam_ReleaseHTTPRequest(request);
        return;
    }
    if(!successful || code != HTTPStatusCode_OK)
    {
        LogError("[MapVotes] Error at RecivedGetFavorites (HTTP Code %d; success %d)", code, successful);
        Steam_ReleaseHTTPRequest(request);
        return;
    }

    decl String:data[4096];
    Steam_GetHTTPResponseBodyData(request, data, sizeof(data));
    Steam_ReleaseHTTPRequest(request);

    new Handle:json = json_load(data);
    new Handle:maps = json_object_get(json, "maps");
    new String:map_buffer[PLATFORM_MAX_PATH];
    new junk;
    new bool:found;

    new Handle:menu = CreateMenu(NominateMapHandler, MENU_ACTIONS_DEFAULT|MenuAction_DrawItem|MenuAction_DisplayItem);

    for(new i = 0; i < json_array_size(maps); i++)
    {
        json_array_get_string(maps, i, map_buffer, sizeof(map_buffer));
        TrimString(map_buffer);
        found = GetTrieValue(g_MapTrie, map_buffer, junk);
        if(found)
        {
            AddMenuItem(menu, map_buffer, map_buffer);
        }
    }

    CloseHandle(json);
    CloseHandle(maps);

    //If no maps were found don't even bother displaying a menu
    if(GetMenuItemCount(menu) > 0){
        SetMenuTitle(menu, "Favorited Maps");
        DisplayMenu(menu, client, MENU_TIME_FOREVER);
    }else{
        ReplyToCommand(client, "[MapVotes] You have no favorited maps that are on this server.");
    }
}

public NominateMapHandler(Handle:menu, MenuAction:action, param1, param2)
{

    decl result;

    // Start function call
    Call_StartFunction(g_nominations, g_Handler_MapSelectMenu);

    // Push parameters one at a time
    Call_PushCell(menu);
    Call_PushCell(action);
    Call_PushCell(param1);
    Call_PushCell(param2);

    // Finish the call, get the result
    Call_Finish(result);

    return result;
}

public CallVoteOnClient(client)
{
    new Handle:menu = CreateMenu(VoteMenuHandler);
    SetMenuTitle(menu, "Do you like this map?");
    AddMenuItem(menu, "1","Like it.");
    AddMenuItem(menu, "-1","Hate it.");
    AddMenuItem(menu, "0","I have no strong feelings one way or the other.");
    DisplayMenu(menu, client, 20);
}

public VoteMenuHandler(Handle:menu, MenuAction:action, param1, param2)
{
    if (action == MenuAction_End)
    {
        CloseHandle(menu);
    } else if (action == MenuAction_VoteCancel)
    {
    } else if (action == MenuAction_Select)
    {
        new String:info[32];
        GetMenuItem(menu, param2, info, sizeof(info));
        new value = StringToInt(info);

        CastVote(param1, value);
    }
}

public HaveNotVoted(caller)
{
    decl String:uid[MAX_COMMUNITYID_LENGTH];
    decl String:map[PLATFORM_MAX_PATH];
    new player;
    new HTTPRequestHandle:request = CreateMapVotesRequest(HAVE_NOT_VOTED_ROUTE);

    if(request == INVALID_HTTP_HANDLE)
    {
        ReplyToCommand(caller, "[MapVotes] sm_map_votes_url invalid; cannot create HTTP request");
        return;
    }

    GetCurrentMap(map, sizeof(map));
    Steam_SetHTTPRequestGetOrPostParameter(request, "map", map);

    for (new client=1; client <= MaxClients; client++)
    {
        if (!IsClientInGame(client) || IsFakeClient(client))
        {
            continue;
        }
        Steam_GetCSteamIDForClient(client, uid, sizeof(uid));
        player = GetClientUserId(client);

        Steam_SetHTTPRequestGetOrPostParameter(request, "uids", uid);
        Steam_SetHTTPRequestGetOrPostParameterInt(request, "players", player);
    }

    Steam_SendHTTPRequest(request, ReceiveHaveNotVoted, caller);
}

public ReceiveHaveNotVoted(HTTPRequestHandle:request, bool:successful, HTTPStatusCode:code, any:userid)
{
    if(!successful || code != HTTPStatusCode_OK)
    {
        LogError("[MapVotes] Error at RecivedHaveNotVoted (HTTP Code %d)", code);
        Steam_ReleaseHTTPRequest(request);
        return;
    }

    decl String:data[4096];
    Steam_GetHTTPResponseBodyData(request, data, sizeof(data));
    Steam_ReleaseHTTPRequest(request);

    new Handle:json = json_load(data);
    new Handle:players = json_object_get(json, "players");
    decl p;

    for(new i = 0; i < json_array_size(players); i++)
    {
        p = json_array_get_int(players, i);
        CallVoteOnClient(GetClientOfUserId(p));
    }
    CloseHandle(json);
    CloseHandle(players);
}

public ViewMap(client)
{
    decl String:map[PLATFORM_MAX_PATH], String:url[256], String:base_url[128];
    GetCurrentMap(map, sizeof(map));
    GetConVarString(g_Cvar_MapVotesUrl, base_url, sizeof(base_url));

    TrimString(base_url);
    new trim_length = strlen(base_url) - 1;

    if(base_url[trim_length] == '/')
    {
        strcopy(base_url, trim_length + 1, base_url);
    }

    Format(url, sizeof(url),
            "%s%s/%s", base_url, MAPS_ROUTE, map);

    ShowMOTDPanel(client, "Map Viewer", url, MOTDPANEL_TYPE_URL);

}