Have karaoke call it's own http callback

This helps sync the song playback to the lyrics

/**
 * vim: set ts=4 :
 * =============================================================================
 * in_game_audio_karaoke
 * Karaoke player for IGA
 *
 * Copyright 2013 CrimsonTautology
 * =============================================================================
 *
 */

#pragma semicolon 1

#include <sourcemod>
#include <sdktools>
#include <steamworks>
#include <smjansson>
#include <in_game_audio>
#include <morecolors>

#define PLUGIN_VERSION "1.8.1"
#define PLUGIN_NAME "In Game Audio Karaoke"

#define MAX_KARAOKE_SONGS    64
#define MAX_LRC_LINE_LENGTH  256
#define MAX_KARAOKE_LYRICS   1024

#define SOUND_ATTENTION "vo/announcer_attention.mp3"

public Plugin:myinfo =
{
    name = PLUGIN_NAME,
    author = "CrimsonTautology",
    description = "Karaoke player for IGA",
    version = PLUGIN_VERSION,
    url = "https://github.com/CrimsonTautology/sm_in_game_audio"
};

//Parallel arrays to store karaoke songs
new String:g_KaraokeName[MAX_KARAOKE_SONGS][PLATFORM_MAX_PATH];
new String:g_KaraokeLRCPath[MAX_KARAOKE_SONGS][PLATFORM_MAX_PATH];
new g_KaraokeSongId[MAX_KARAOKE_SONGS];
new g_KaraokeSongCount = 0;

new String:g_KaraokeLyrics[MAX_KARAOKE_LYRICS][MAX_LRC_LINE_LENGTH];
new Float:g_KaraokeTimestamps[MAX_KARAOKE_LYRICS];

public OnPluginStart()
{
    LoadTranslations("in_game_audio.phrases");
    RegAdminCmd("sm_karaoke", Command_Karaoke, ADMFLAG_SLAY, "[ADMIN] Start a karaoke.");

    CreateConVar("sm_iga_karaoke_version", PLUGIN_VERSION, PLUGIN_NAME, FCVAR_PLUGIN | FCVAR_SPONLY | FCVAR_REPLICATED | FCVAR_NOTIFY | FCVAR_DONTRECORD);
}

public OnMapStart()
{
    ReadKaraokeSongs();

    PrecacheSound(SOUND_ATTENTION);
    PrecacheSound("vo/announcer_begins_1sec.mp3");
    PrecacheSound("vo/announcer_begins_2sec.mp3");
    PrecacheSound("vo/announcer_begins_3sec.mp3");
    PrecacheSound("vo/announcer_begins_4sec.mp3");
    PrecacheSound("vo/announcer_begins_5sec.mp3");
}

public Action:Command_Karaoke(client, args)
{
    if(!IsIGAEnabled())
    {
        CReplyToCommand(client, "%t", "not_enabled");
        return Plugin_Handled;
    }

    if(IsInPall())
    {
        CReplyToCommand(client, "%t", "pall_currently_in_use");
        return Plugin_Handled;
    }

    KaraokeMenu(client);

    return Plugin_Handled;
}

public Action:Timer_DisplayLyric(Handle:timer, any:index)
{
    PrintCenterTextAll("%s", g_KaraokeLyrics[index]);
}

public Action:Timer_StartKaraokeQuery(Handle:timer, any:selected)
{
    QueryKaraoke(selected, g_KaraokeSongId[selected]);
}

public Action:Timer_CountDown(Handle:timer, any:second)
{
    decl String:sound[PLATFORM_MAX_PATH];
    FormatEx(sound, sizeof(sound), "vo/announcer_begins_%dsec.mp3", second);
    EmitSoundToAll(sound);
}

SteamWorks_SetHTTPRequestGetOrPostParameterInt(&Handle:request, const String:param[], value)
{
    new String:tmp[64];
    IntToString(value, tmp, sizeof(tmp));
    SteamWorks_SetHTTPRequestGetOrPostParameter(request, param, tmp);
}

ReadKaraokeSongs()
{
    g_KaraokeSongCount = 0;

    new Handle:kv = CreateKeyValues("Karaoke");

    decl String:path[PLATFORM_MAX_PATH], String:tmp[PLATFORM_MAX_PATH], String:game_folder[PLATFORM_MAX_PATH];
    BuildPath(Path_SM, path, sizeof(path), "configs/iga.karaoke.cfg");//TODO make this path a cvar

    if(FileExists(path))
    {
        FileToKeyValues(kv, path);
        KvGotoFirstSubKey(kv);

        do
        {
            KvGetSectionName(kv, tmp, sizeof(tmp));
            if(StrEqual(tmp, "Song") )
            {
                KvGetKaraokeSong(kv, g_KaraokeSongCount);
            }

        } while(KvGotoNextKey(kv) && g_KaraokeSongCount < MAX_KARAOKE_SONGS);

    } else {
        LogError("File Not Found: %s", path);
    }

    CloseHandle(kv);
}

KvGetKaraokeSong(Handle:kv, &index)
{
    KvGetString(kv, "name", g_KaraokeName[index], PLATFORM_MAX_PATH);
    KvGetString(kv, "lrc_file", g_KaraokeLRCPath[index], PLATFORM_MAX_PATH);
    g_KaraokeSongId[index] = KvGetNum(kv, "song_id", 0);

    PrintToConsole(0, "---%s %s %d", g_KaraokeName[index], g_KaraokeLRCPath[index], g_KaraokeSongId[index]);//TODO

    index++;
}

ParseLRCFile(const String:file_name[], String:lyrics[][], Float:timestamps[])
{
    decl String:path[PLATFORM_MAX_PATH];
    new total_lyrics = 0;
    BuildPath(Path_SM, path, sizeof(path), "data/karaoke/%s", file_name);

    if(FileExists(path))
    {
        new Handle:file = OpenFile(path, "r");

        //Line by line reading
        decl String:line[MAX_LRC_LINE_LENGTH], index, split, Float:timestamp;
        while (total_lyrics < MAX_KARAOKE_LYRICS && !IsEndOfFile(file) && ReadFileLine(file, line, sizeof(line)))
        {
            //Skip non timestamp lines
            if(SimpleRegexMatch(line, "\\[[0-9]{2}\\:[0-6][0-9](\\.[0-6][0-9])?\\]([\\w:\\s]+)") <= 0) continue;

            //Strip linefeeds and returns
            ReplaceString(line, sizeof(line), "\r", "");
            ReplaceString(line, sizeof(line), "\n", "");
            ReplaceString(line, sizeof(line), "\xEF\xBB\xBF", ""); // UTF-8 "BOM"

            //Parse out timestamp and lyric
            index = FindCharInString(line, ']');
            if(index <= 0) continue; //Skip invalid line

            line[index] = '\0';
            split = FindCharInString(line, ':');

            g_KaraokeTimestamps[total_lyrics] = StringToInt(line[1]) * 60 + StringToFloat(line[split + 1]);
            strcopy(g_KaraokeLyrics[total_lyrics], MAX_LRC_LINE_LENGTH, line[index + 1]);

            total_lyrics++;
        }

    } else {
        LogError("Karaoke Lyrics File Not Found: %s", path);
    }

    return total_lyrics;
}

StartKaraokeCountDown(selected, Float:delay)
{
    EmitSoundToAll(SOUND_ATTENTION);
    PrintCenterTextAll("%s\nKaraoke Will Begin in 15 Seconds\nAdjust your volume now if needed!", g_KaraokeName[selected]);

    CreateTimer(delay - 5.0, Timer_CountDown, 5);
    CreateTimer(delay - 4.0, Timer_CountDown, 4);
    CreateTimer(delay - 3.0, Timer_CountDown, 3);
    CreateTimer(delay - 2.0, Timer_CountDown, 2);
    CreateTimer(delay - 1.0, Timer_CountDown, 1);

    CreateTimer(delay, Timer_StartKaraokeQuery, selected);
}

StartKaraokeLyricsDisplay(selected)
{
    new total, i;
    total = ParseLRCFile(g_KaraokeLRCPath[selected], g_KaraokeLyrics, g_KaraokeTimestamps);//TODO

    //Build timers to display lyrics for each parsed lyric
    for(i=0; i < total; i++)
    {
        CreateTimer(g_KaraokeTimestamps[i], Timer_DisplayLyric, i, TIMER_FLAG_NO_MAPCHANGE);
    }

}

QueryKaraoke(selected, song_id)
{
    if(IsInPall()) return;

    new Handle:request = CreateIGARequest(QUERY_SONG_ROUTE);

    SteamWorks_SetHTTPRequestGetOrPostParameter(request, "path", "");
    SteamWorks_SetHTTPRequestGetOrPostParameterInt(request, "pall", 1);
    SteamWorks_SetHTTPRequestGetOrPostParameterInt(request, "force", 1);

    SteamWorks_SetHTTPRequestGetOrPostParameterInt(request, "song_id", song_id);
    SteamWorks_SetHTTPRequestGetOrPostParameter(request, "uid", "76561197960804942");

    SteamWorks_SetHTTPCallbacks(request, ReceiveQueryKaraoke);
    SteamWorks_SetHTTPRequestContextValue(request, selected);
    SteamWorks_SendHTTPRequest(request);
}

public ReceiveQueryKaraoke(Handle:request, bool:failure, bool:successful, EHTTPStatusCode:code, any:selected)
{
    if(!successful || code != k_EHTTPStatusCode200OK)
    {
        LogError("[IGA] Error at RecivedQueryKaraoke (HTTP Code %d; success %d)", code, successful);
        CloseHandle(request);
        return;
    }

    new size = 0;
    SteamWorks_GetHTTPResponseBodySize(request, size);
    new String:data[size];
    SteamWorks_GetHTTPResponseBodyData(request, data, size);
    CloseHandle(request);

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

    if(found)
    {
        //Found a matching song
        new duration = json_object_get_int(json, "duration");
        new bool:pall = json_object_get_bool(json, "pall");
        new bool:force = json_object_get_bool(json, "force");
        new String:song_id[64], String:full_path[64], String:description[64], String:duration_formated[64], String:access_token[128];
        json_object_get_string(json, "song_id", song_id, sizeof(song_id));
        json_object_get_string(json, "full_path", full_path, sizeof(full_path));
        json_object_get_string(json, "description", description, sizeof(description));
        json_object_get_string(json, "duration_formated", duration_formated, sizeof(duration_formated));
        json_object_get_string(json, "access_token", access_token, sizeof(access_token));

        CPrintToChatAll("%t", "started_playing_to_all", description);
        CPrintToChatAll("%t", "duration", duration_formated);

        RegisterPall(duration, full_path, description);

        StartKaraokeLyricsDisplay(selected);
        PlaySongAll(song_id, access_token, force);
    }else{
        CPrintToChatAll("%t", "not_found");
    }

    CloseHandle(json);
}

KaraokeMenu(client)
{
    new Handle:menu = CreateMenu(KaraokeMenuHandler);

    SetMenuTitle(menu, "Choose Karaoke Song");

    decl String:buf[16];
    for(new i=0; i < g_KaraokeSongCount && i < MAX_KARAOKE_SONGS; i++)
    {
        IntToString(i, buf, sizeof(buf));
        AddMenuItem(menu,
                buf,
                g_KaraokeName[i],
                ITEMDRAW_DEFAULT);
    }

    DisplayMenu(menu, client, 20);
}

public KaraokeMenuHandler(Handle:menu, MenuAction:action, param1, param2)
{
    switch (action)
    {
        case MenuAction_Select:
            {
                new client = param1;
                new String:info[32];
                GetMenuItem(menu, param2, info, sizeof(info));
                new selected = StringToInt(info);

                StartKaraokeCountDown(selected, 15.0);
            }
        case MenuAction_End: CloseHandle(menu);
    }
}

/**
 * vim: set ts=4 :
 * =============================================================================
 * in_game_audio_karaoke
 * Karaoke player for IGA
 *
 * Copyright 2013 CrimsonTautology
 * =============================================================================
 *
 */

#pragma semicolon 1

#include <sourcemod>
#include <sdktools>
#include <in_game_audio>
#include <morecolors>

#define PLUGIN_VERSION "1.8.1"
#define PLUGIN_NAME "In Game Audio Karaoke"

#define MAX_KARAOKE_SONGS    64
#define MAX_LRC_LINE_LENGTH  256
#define MAX_KARAOKE_LYRICS   1024

#define SOUND_ATTENTION "vo/announcer_attention.mp3"

public Plugin:myinfo =
{
    name = PLUGIN_NAME,
    author = "CrimsonTautology",
    description = "Karaoke player for IGA",
    version = PLUGIN_VERSION,
    url = "https://github.com/CrimsonTautology/sm_in_game_audio"
};

//Parallel arrays to store karaoke songs
new String:g_KaraokeName[MAX_KARAOKE_SONGS][PLATFORM_MAX_PATH];
new String:g_KaraokeLRCPath[MAX_KARAOKE_SONGS][PLATFORM_MAX_PATH];
new g_KaraokeSongId[MAX_KARAOKE_SONGS];
new g_KaraokeSongCount = 0;

new String:g_KaraokeLyrics[MAX_KARAOKE_LYRICS][MAX_LRC_LINE_LENGTH];
new Float:g_KaraokeTimestamps[MAX_KARAOKE_LYRICS];

public OnPluginStart()
{
    LoadTranslations("in_game_audio.phrases");
    RegAdminCmd("sm_karaoke", Command_Karaoke, ADMFLAG_SLAY, "[ADMIN] Start a karaoke.");

    CreateConVar("sm_iga_karaoke_version", PLUGIN_VERSION, PLUGIN_NAME, FCVAR_PLUGIN | FCVAR_SPONLY | FCVAR_REPLICATED | FCVAR_NOTIFY | FCVAR_DONTRECORD);
}

public OnMapStart()
{
    ReadKaraokeSongs();

    PrecacheSound(SOUND_ATTENTION);
    PrecacheSound("vo/announcer_begins_1sec.mp3");
    PrecacheSound("vo/announcer_begins_2sec.mp3");
    PrecacheSound("vo/announcer_begins_3sec.mp3");
    PrecacheSound("vo/announcer_begins_4sec.mp3");
    PrecacheSound("vo/announcer_begins_5sec.mp3");
}

public Action:Command_Karaoke(client, args)
{
    if(!IsIGAEnabled())
    {
        CReplyToCommand(client, "%t", "not_enabled");
        return Plugin_Handled;
    }

    if(IsInPall())
    {
        CReplyToCommand(client, "%t", "pall_currently_in_use");
        return Plugin_Handled;
    }

    KaraokeMenu(client);

    return Plugin_Handled;
}

public Action:Timer_DisplayLyric(Handle:timer, any:index)
{
    PrintCenterTextAll("%s", g_KaraokeLyrics[index]);
}

public Action:Timer_StartSong(Handle:timer, any:song_id)
{
    QuerySong(0, "", true, true, song_id);
}

public Action:Timer_CountDown(Handle:timer, any:second)
{
    decl String:sound[PLATFORM_MAX_PATH];
    FormatEx(sound, sizeof(sound), "vo/announcer_begins_%dsec.mp3", second);
    EmitSoundToAll(sound);
}

ReadKaraokeSongs()
{
    g_KaraokeSongCount = 0;

    new Handle:kv = CreateKeyValues("Karaoke");

    decl String:path[PLATFORM_MAX_PATH], String:tmp[PLATFORM_MAX_PATH], String:game_folder[PLATFORM_MAX_PATH];
    BuildPath(Path_SM, path, sizeof(path), "configs/iga.karaoke.cfg");//TODO make this path a cvar

    if(FileExists(path))
    {
        FileToKeyValues(kv, path);
        KvGotoFirstSubKey(kv);

        do
        {
            KvGetSectionName(kv, tmp, sizeof(tmp));
            if(StrEqual(tmp, "Song") )
            {
                KvGetKaraokeSong(kv, g_KaraokeSongCount);
            }

        } while(KvGotoNextKey(kv) && g_KaraokeSongCount < MAX_KARAOKE_SONGS);

    } else {
        LogError("File Not Found: %s", path);
    }

    CloseHandle(kv);
}

KvGetKaraokeSong(Handle:kv, &index)
{
    KvGetString(kv, "name", g_KaraokeName[index], PLATFORM_MAX_PATH);
    KvGetString(kv, "lrc_file", g_KaraokeLRCPath[index], PLATFORM_MAX_PATH);
    g_KaraokeSongId[index] = KvGetNum(kv, "song_id", 0);

    PrintToConsole(0, "---%s %s %d", g_KaraokeName[index], g_KaraokeLRCPath[index], g_KaraokeSongId[index]);//TODO

    index++;
}

ParseLRCFile(const String:file_name[], String:lyrics[][], Float:timestamps[])
{
    decl String:path[PLATFORM_MAX_PATH];
    new total_lyrics = 0;
    BuildPath(Path_SM, path, sizeof(path), "data/karaoke/%s", file_name);

    if(FileExists(path))
    {
        new Handle:file = OpenFile(path, "r");

        //Line by line reading
        decl String:line[MAX_LRC_LINE_LENGTH], index, split, Float:timestamp;
        while (total_lyrics < MAX_KARAOKE_LYRICS && !IsEndOfFile(file) && ReadFileLine(file, line, sizeof(line)))
        {
            //Skip non timestamp lines
            if(SimpleRegexMatch(line, "\\[[0-9]{2}\\:[0-6][0-9](\\.[0-6][0-9])?\\]([\\w:\\s]+)") <= 0) continue;

            //Strip linefeeds and returns
            ReplaceString(line, sizeof(line), "\r", "");
            ReplaceString(line, sizeof(line), "\n", "");
            ReplaceString(line, sizeof(line), "\xEF\xBB\xBF", ""); // UTF-8 "BOM"

            //Parse out timestamp and lyric
            index = FindCharInString(line, ']');
            if(index <= 0) continue; //Skip invalid line

            line[index] = '\0';
            split = FindCharInString(line, ':');

            g_KaraokeTimestamps[total_lyrics] = StringToInt(line[1]) * 60 + StringToFloat(line[split + 1]);
            strcopy(g_KaraokeLyrics[total_lyrics], MAX_LRC_LINE_LENGTH, line[index + 1]);

            total_lyrics++;
        }

    } else {
        LogError("Karaoke Lyrics File Not Found: %s", path);
    }

    return total_lyrics;
}

StartKaraoke(selected, Float:delay)
{
    if(IsInPall()) return;

    new total, i;
    total = ParseLRCFile(g_KaraokeLRCPath[selected], g_KaraokeLyrics, g_KaraokeTimestamps);//TODO

    //Build timers to display lyrics for each parsed lyric
    for(i=0; i < total; i++)
    {
        CreateTimer(g_KaraokeTimestamps[i] + delay, Timer_DisplayLyric, i, TIMER_FLAG_NO_MAPCHANGE);
    }

    EmitSoundToAll(SOUND_ATTENTION);
    PrintCenterTextAll("%s\nKaraoke Will Begin in 15 Seconds\nAdjust your volume now if needed!", g_KaraokeName[selected]);

    CreateTimer(delay - 5.0, Timer_CountDown, 5);
    CreateTimer(delay - 4.0, Timer_CountDown, 4);
    CreateTimer(delay - 3.0, Timer_CountDown, 3);
    CreateTimer(delay - 2.0, Timer_CountDown, 2);
    CreateTimer(delay - 1.0, Timer_CountDown, 1);

    PrintToConsole(0, "%s %s %d", g_KaraokeName[selected], g_KaraokeLRCPath[selected], g_KaraokeSongId[selected]);//TODO

    CreateTimer(delay, Timer_StartSong, g_KaraokeSongId[selected]);
}

KaraokeMenu(client)
{
    new Handle:menu = CreateMenu(KaraokeMenuHandler);

    SetMenuTitle(menu, "Choose Karaoke Song");

    decl String:buf[16];
    for(new i=0; i < g_KaraokeSongCount && i < MAX_KARAOKE_SONGS; i++)
    {
        IntToString(i, buf, sizeof(buf));
        AddMenuItem(menu,
                buf,
                g_KaraokeName[i],
                ITEMDRAW_DEFAULT);
    }

    DisplayMenu(menu, client, 20);
}

public KaraokeMenuHandler(Handle:menu, MenuAction:action, param1, param2)
{
    switch (action)
    {
        case MenuAction_Select:
            {
                new client = param1;
                new String:info[32];
                GetMenuItem(menu, param2, info, sizeof(info));
                new selected = StringToInt(info);

                StartKaraoke(selected, 15.0);
            }
        case MenuAction_End: CloseHandle(menu);
    }
}


Begin fixes to true democracy

Begin fixes to true democracy

Remake how votes are saved, adding more checks

#pragma semicolon 1
#pragma newdecls required

#include <sourcemod>
#include <tf2_stocks>
#include <sdkhooks>
#include <colorvariables>

#define PLUGIN_VERSION "1.0.0"

// ConVar BleedChance;
Menu voteMenu[MAXPLAYERS + 1];

bool voteEnabled;

int results[MAXPLAYERS + 1];
int resultcount[5] = {0, 0, 0, 0, 0};

int numOptions = 0;

char question[1024], options[5][2][48];
//new Handle:BleedTime;

public Plugin myinfo = {
	name             = "[NGS] Randomized Votes - True Democracy",
	author         = "TheXeon",
	description     = "Chance for bottles to break.",
	version         = PLUGIN_VERSION,
	url             = "https://www.neogenesisnetwork.net/"
};

public void OnPluginStart( )
{
	CreateConVar("sm_truedemocracy_version", PLUGIN_VERSION, "True democracy randomized vote version");

	RegAdminCmd("sm_rvote", CommandRandomVote, ADMFLAG_VOTE, "Creates a randomized vote.");
	RegAdminCmd("sm_rvoteresults", CommandRandomVoteResults, ADMFLAG_VOTE, "Prints vote results.");
	RegAdminCmd("sm_rvoteend", CommandRandomVoteEnd, ADMFLAG_VOTE, "Ends a vote.");
	RegAdminCmd("sm_rvoteclear", CommandRandomVoteClear, ADMFLAG_VOTE, "Clears results of last vote.");

	LoadTranslations("common.phrases");
}

public Action CommandRandomVote(int client, int args)
{
	if (args < 3)
	{
		CReplyToCommand(client, "{GREEN}[SM]{DEFAULT} Please provide a question and two options!");
		return Plugin_Handled;
	}
	voteEnabled = true;
	numOptions = args - 1;
	char voteTitle[64], optionNum[18];

	ClearVoteResults();

	GetCmdArg(1, question, sizeof(question));
	for (int i = 2; i <= numOptions + 1; i++)
	{
//		if (args < i) break;
		int place = i - 2;
		GetCmdArg(i, options[place][0], 48);
		Format(options[place][1], sizeof(options[place][1]), "option%d", place + 1);
	}

	Format(voteTitle, sizeof(voteTitle), "%s (random options)", question);
	for (int i = 1; i <= MaxClients; i++)
	{
		if (!IsValidClient(i)) continue;
		voteMenu[i] = new Menu(RandomizedVoteMenuHandler);
		voteMenu[i].SetTitle(voteTitle);
		RandomizeOptions(numOptions);
		for (int k = 0; k < numOptions; k++)
		{
			voteMenu[i].AddItem(options[k][1], options[k][0]);
		}
		voteMenu[i].Display(i, 30);
	}
	CreateTimer(30.0, OnVoteTimerEnd);
	return Plugin_Handled;
}

public void RandomizeOptions(int numOptions)
{
	for (int j = 0; j < numOptions; j++)
	{
		int randPos = GetRandomInt(0, numOptions - 1);
		char tmpQuestion[48], tmpOption[48];
		strcopy(tmpQuestion, 48, options[j][0]);
		strcopy(tmpOption, 48, options[j][1]);

		// TODO: Convert the rest down below!
		int pos = optionplaces[j];
		strcopy(options[j], 48, options[randPos]);
		optionplaces[j] = optionplaces[randPos];
		strcopy(options[randPos], 48, tmp);
		optionplaces[randPos] = pos;
	}
}

public Action OnVoteTimerEnd(Handle timer, any data)
{
	ServerCommand("sm_rvoteend");
	ServerCommand("sm_rvoteresults");
}

public Action CommandRandomVoteResults(int client, int args)
{
	CPrintToChatAll("{GREEN}[SM]{DEFAULT} === Vote results ===");
	for (int i = 0; i < numOptions; i++)
	{
		CPrintToChatAll("{CRIMSON}%d.{DEFAULT} {YELLOW}%s{DEFAULT}: %d.", i + 1, baseoptions[i], results[i]);
	}
	return Plugin_Handled;
}

public Action CommandRandomVoteEnd(int client, int args)
{
	voteEnabled = false;
	CPrintToChatAll("{GREEN}[SM]{DEFAULT} Vote has ended.");
	return Plugin_Handled;
}

public Action CommandRandomVoteClear(int client, int args)
{
	ClearVoteResults();
	CReplyToCommand(client, "{GREEN}[SM]{DEFAULT} Vote results cleared.");
	return Plugin_Handled;
}

public int RandomizedVoteMenuHandler(Menu menu, MenuAction action, int param1, int param2)
{
	switch(action)
	{
		case MenuAction_Select:
		{
			if (!voteEnabled) return 0;
			char info[32];
			menu.GetItem(param2, info, sizeof(info));
			int place = info[6] - 49;
//			CPrintToChatAdmins(ADMFLAG_ROOT, "info is %s", info);
//			CPrintToChatAdmins(ADMFLAG_ROOT, "info[6] - 49 is %d", place);
			results[place]++;
		}

		case MenuAction_Cancel:
		{
			PrintToServer("Client %d's menu was cancelled for reason %d", param1, param2);
		}

		case MenuAction_End:
		{
			delete menu;
		}
	}

	return 0;
}

void ClearVoteResults()
{
	for (int i = 0; i < 5; i++)
	{
		results[i] = 0;
	}
}

stock bool IsValidClient(int client, bool aliveTest=false, bool botTest=true, bool rangeTest=true,
	bool ingameTest=true)
{
	if (client > 4096) client = EntRefToEntIndex(client);
	if (rangeTest && (client < 1 || client > MaxClients)) return false;
	if (ingameTest && !IsClientInGame(client)) return false;
	if (botTest && IsFakeClient(client)) return false;
	if (GetEntProp(client, Prop_Send, "m_bIsCoaching")) return false;
	if (aliveTest && !IsPlayerAlive(client)) return false;
	return true;
}

#pragma semicolon 1
#pragma newdecls required

#include <sourcemod>
#include <tf2_stocks>
#include <sdkhooks>
#include <colorvariables>

#define PLUGIN_VERSION "1.0.0"

// ConVar BleedChance;
Menu voteMenu[MAXPLAYERS + 1];

bool voteEnabled;

int results[5] = {0, 0, 0, 0, 0};
int optionplaces[5] = {1, 2, 3, 4, 5};

int numOptions = 0;

char question[1024], options[5][48], baseoptions[5][48];
//new Handle:BleedTime;

public Plugin myinfo = {
	name             = "[NGS] Randomized Votes - True Democracy",
	author         = "TheXeon",
	description     = "Chance for bottles to break.",
	version         = PLUGIN_VERSION,
	url             = "https://www.neogenesisnetwork.net/"
};

public void OnPluginStart( )
{
	CreateConVar("sm_truedemocracy_version", PLUGIN_VERSION, "True democracy randomized vote version");

	RegAdminCmd("sm_rvote", CommandRandomVote, ADMFLAG_VOTE, "Creates a randomized vote.");
	RegAdminCmd("sm_rvoteresults", CommandRandomVoteResults, ADMFLAG_VOTE, "Prints vote results.");
	RegAdminCmd("sm_rvoteend", CommandRandomVoteEnd, ADMFLAG_VOTE, "Ends a vote.");
	RegAdminCmd("sm_rvoteclear", CommandRandomVoteClear, ADMFLAG_VOTE, "Clears results of last vote.");

	LoadTranslations("common.phrases");
}

public Action CommandRandomVote(int client, int args)
{
	if (args < 3)
	{
		CReplyToCommand(client, "{GREEN}[SM]{DEFAULT} Please provide a question and two options!");
		return Plugin_Handled;
	}
	voteEnabled = true;
	numOptions = args - 1;
	char voteTitle[64];

	clearVoteResults();

	GetCmdArg(1, question, sizeof(question));
	for (int i = 2; i <= 6; i++)
	{
		if (args < i) break;
		int place = i - 2;
		GetCmdArg(i, options[place], 48);
		strcopy(baseoptions[place], 48, options[place]);
	}

	Format(voteTitle, sizeof(voteTitle), "%s (random options)", question);

	for (int i = 1; i <= MaxClients; i++)
	{
		if (!IsValidClient(i)) continue;
		voteMenu[i] = new Menu(RandomizedVoteMenuHandler);
		voteMenu[i].SetTitle(voteTitle);
		for (int j = 0; j < numOptions; j++)
		{
			int randPos = GetRandomInt(0, numOptions - 1);
			char tmp[48];
			strcopy(tmp, 48, options[j]);
			int pos = optionplaces[j];
			strcopy(options[j], 48, options[randPos]);
			optionplaces[j] = optionplaces[randPos];
			strcopy(options[randPos], 48, tmp);
			optionplaces[randPos] = pos;
		}
		for (int k = 0; k < numOptions; k++)
		{
			char info[32];
			Format(info, sizeof(info), "option%d", optionplaces[k]);
			voteMenu[i].AddItem(info, options[k]);
		}
		voteMenu[i].Display(i, 20);
	}
	CreateTimer(25.0, OnVoteTimerEnd);
	return Plugin_Handled;
}

public Action OnVoteTimerEnd(Handle timer, any data)
{
	ServerCommand("sm_rvoteend");
	ServerCommand("sm_rvoteresults");
}

public Action CommandRandomVoteResults(int client, int args)
{
	CPrintToChatAll("{GREEN}[SM]{DEFAULT} === Vote results ===");
	for (int i = 0; i < numOptions; i++)
	{
		CPrintToChatAll("{CRIMSON}%d.{DEFAULT} {YELLOW}%s{DEFAULT}: %d.", i + 1, baseoptions[i], results[i]);
	}
	return Plugin_Handled;
}

public Action CommandRandomVoteEnd(int client, int args)
{
	voteEnabled = false;
	CPrintToChatAll("{GREEN}[SM]{DEFAULT} Vote has ended.");
	return Plugin_Handled;
}

public Action CommandRandomVoteClear(int client, int args)
{
	clearVoteResults();
	CReplyToCommand(client, "{GREEN}[SM]{DEFAULT} Vote results cleared.");
	return Plugin_Handled;
}

public int RandomizedVoteMenuHandler(Menu menu, MenuAction action, int param1, int param2)
{
	switch(action)
	{
		case MenuAction_Select:
		{
			if (!voteEnabled) return 0;
			char info[32];
			menu.GetItem(param2, info, sizeof(info));
			int place = info[6] - 49;
//			CPrintToChatAdmins(ADMFLAG_ROOT, "info is %s", info);
//			CPrintToChatAdmins(ADMFLAG_ROOT, "info[6] - 49 is %d", place);
			results[place]++;
		}

		case MenuAction_Cancel:
		{
			PrintToServer("Client %d's menu was cancelled for reason %d", param1, param2);
		}

		case MenuAction_End:
		{
			delete menu;
		}
	}

	return 0;
}

void clearVoteResults()
{
	for (int i = 0; i < 5; i++)
	{
		results[i] = 0;
	}
}

stock bool IsValidClient(int client, bool aliveTest=false, bool botTest=true, bool rangeTest=true,
	bool ingameTest=true)
{
	if (client > 4096) client = EntRefToEntIndex(client);
	if (rangeTest && (client < 1 || client > MaxClients)) return false;
	if (ingameTest && !IsClientInGame(client)) return false;
	if (botTest && IsFakeClient(client)) return false;
	if (GetEntProp(client, Prop_Send, "m_bIsCoaching")) return false;
	if (aliveTest && !IsPlayerAlive(client)) return false;
	return true;
}


Fix compiler errors

Fix compiler errors

/**
 * 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_MapVotesPort = 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");
    g_Cvar_MapVotesPort = CreateConVar("sm_map_votes_port", "80", "HTTP Port used");
    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");

    //RegConsoleCmd("test", Test, "Test function");

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

public OnConfigsExecuted()
{
    BuildMapListAndTrie();
}

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

    //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, "User in cooldown");
        return Plugin_Handled;
    }

    if(GetConVarBool(g_Cvar_MapVotesVotingEnabled))
    {
        ReplyToCommand(client, "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, "User in cooldown");
        return Plugin_Handled;
    }

    if(GetConVarBool(g_Cvar_MapVotesVotingEnabled))
    {
        ReplyToCommand(client, "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, "User in cooldown");
        return Plugin_Handled;
    }

    if(GetConVarBool(g_Cvar_MapVotesVotingEnabled))
    {
        ReplyToCommand(client, "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, "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, "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, "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, "User in cooldown");
        return Plugin_Handled;
    }

    if (!GetConVarBool(g_Cvar_MapVotesCommentingEnabled))
    {
        ReplyToCommand(client, "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, "User in cooldown");
        return Plugin_Handled;
    }

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

    HaveNotVoted();

    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));

    //TODO - check for forward slash after base_url;
    new bool:check =  (FindCharInString(base_url, '/', true) == sizeof(base_url) - 1);

    Format(url, sizeof(url),
            "%s%s%s", base_url, (!check ? "/" : ""), 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);
}

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);
    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)
    {
        ReplyToCommand(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);
        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)
    {
        ReplyToCommand(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);
    }

    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)
    {
        ReplyToCommand(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], String:info[16];
    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);
    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)", 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:maps = json_object_get(json, "maps");
    new String:map_buffer[PLATFORM_MAX_PATH];
    new junk;

    new Handle:menu = CreateMenu(NominateMapHandler);

    for(new i = 0; i < json_array_size(maps); i++)
    {
        json_array_get_string(maps, i, map_buffer, sizeof(map_buffer));
        if(GetTrieValue(g_MapTrie, map_buffer, junk))
        {
            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()
{
    decl String:uid[MAX_COMMUNITYID_LENGTH];
    decl String:map[PLATFORM_MAX_PATH];
    new player;
    new HTTPRequestHandle:request = CreateMapVotesRequest(HAVE_NOT_VOTED_ROUTE);

    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, 0);
}

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;
    new String:map_buffer[PLATFORM_MAX_PATH];

    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));
    //TODO
    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, MAPS_ROUTE, map);

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

}

public Action:Test(client, args)
{
    //MapVotesCall(GET_FAVORITES_ROUTE, query_params, 0, ReceiveGetFavorites);
    new HTTPRequestHandle:request = CreateMapVotesRequest(GET_FAVORITES_ROUTE);
    Steam_SetHTTPRequestGetOrPostParameterInt(request, "player", 7);
    Steam_SetHTTPRequestGetOrPostParameter(request, "uid",  "76561197998903004");
    Steam_SendHTTPRequest(request, ReceiveGetFavorites, 0);
}

/**
 * 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_MapVotesPort = 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[MaxClients+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");
    g_Cvar_MapVotesPort = CreateConVar("sm_map_votes_port", "80", "HTTP Port used");
    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");

    RegConsoleCmd("test", Test);

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

public OnConfigsExecuted()
{
    BuildMapListAndTrie();
}

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

    //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, "User in cooldown");
        return Plugin_Handled;
    }

    if(GetConVarBool(g_Cvar_MapVotesVotingEnabled))
    {
        ReplyToCommand(client, "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, "User in cooldown");
        return Plugin_Handled;
    }

    if(GetConVarBool(g_Cvar_MapVotesVotingEnabled))
    {
        ReplyToCommand(client, "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, "User in cooldown");
        return Plugin_Handled;
    }

    if(GetConVarBool(g_Cvar_MapVotesVotingEnabled))
    {
        ReplyToCommand(client, "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, "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, "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, "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, "User in cooldown");
        return Plugin_Handled;
    }

    if (!GetConVarBool(g_Cvar_MapVotesCommentingEnabled))
    {
        ReplyToCommand(client, "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, "User in cooldown");
        return Plugin_Handled;
    }

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

    HaveNotVoted();

    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));

    //TODO - check for forward slash after base_url;
    bool:check =  FindCharInString(base_url, '/', true) == sizeof(base_url) - 1;

    Format(url, sizeof(url),
            "%s%s%s", base_url, (!check ? "/" : ""), route);

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

    return request;
}

public Steam_SetHTTPRequestGetOrPostParameterInt(&HTTPRequestHandle:request, const String:param[], value)
{
    String[64] tmp;
    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_MapVotesRequestCooldown), RemoveCooldown);
}

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);
	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)
    {
        ReplyToCommand(client, "[MapVotes] Comment Added");
    }

	Steam_ReleaseHTTPRequest(request);
}

public CastVote(client, value)
{

    if(!(value == -1 || value == 0 || value == 1)){
        LogError("[MapVotes] invalid vote value %d (steam_user: %s)", value, uid);
    }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);
        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)
    {
        ReplyToCommand(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);
    }

    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)
    {
        ReplyToCommand(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], String:info[16];
    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);
    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)", 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:maps = json_object_get(json, "maps");
    new String:map_buffer[PLATFORM_MAX_PATH];
    new junk;

    new Handle:menu = CreateMenu(NominateMapHandler);

    for(new i = 0; i < json_array_size(maps); i++)
    {
        json_array_get_string(maps, i, map_buffer, sizeof(map_buffer));
        if(GetTrieValue(g_MapTrie, map_buffer, junk))
        {
            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()
{
    decl String:uid[MAX_COMMUNITYID_LENGTH];
    decl String:map[PLATFORM_MAX_PATH];
    new player;
    new HTTPRequestHandle:request = CreateMapVotesRequest(HAVE_NOT_VOTED_ROUTE);

    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, 0);
}

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);

    decl Handle:json = json_load(data);
    decl Handle:players = json_object_get(json, "players");
    decl p;
    new String:map_buffer[PLATFORM_MAX_PATH];

    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));
    //TODO
    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, MAPS_ROUTE, map);

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

}

public Action:Test(client, args)
{
    //MapVotesCall(GET_FAVORITES_ROUTE, query_params, 0, ReceiveGetFavorites);
    new HTTPRequestHandle:request = CreateMapVotesRequest(GET_FAVORITES_ROUTE);
    Steam_SetHTTPRequestGetOrPostParameterInt(request, "player", 7);
    Steam_SetHTTPRequestGetOrPostParameter(request, "uid",  "76561197998903004");
    Steam_SendHTTPRequest(request, ReceiveGetFavorites, 0);
}


Remove FormatEx Repetition

Remove FormatEx Repetition

Improve FormatEx by moving if statement direct to argument.

// *************************************************************************
//  This file is part of SourceBans-Fork.
//
//  Copyright (C) 2014-2015 Sarabveer Singh <sarabveer@sarabveer.me>
//
//  SourceBans-Fork is free software: you can redistribute it and/or modify
//  it under the terms of the GNU Affero General Public License as published by
//  the Free Software Foundation, per version 3 of the License.
//
//  SourceBans-Fork is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
//
//  You should have received a copy of the GNU Affero General Public License
//  along with SourceBans-Fork. If not, see <http://www.gnu.org/licenses/>.
//
//  This file incorporates work covered by the following copyright(s):
//
//   SourceSleuth 1.3 fix
//   Copyright (C) 2013-2015 ecca
//   Licensed under GNU GPL version 3, or later.
//   Page: <https://forums.alliedmods.net/showthread.php?p=1818793> - <https://github.com/ecca/SourceMod-Plugins>
//
// *************************************************************************

#pragma semicolon 1
#include <sourcemod>
#undef REQUIRE_PLUGIN
#include <sourcebans>

#define PLUGIN_VERSION "SBF-1.5.2F-R2-dev"

#define LENGTH_ORIGINAL 1
#define LENGTH_CUSTOM 2
#define LENGTH_DOUBLE 3
#define LENGTH_NOTIFY 4

//- Handles -//
new Handle:hDatabase = INVALID_HANDLE;
new Handle:g_hAllowedArray = INVALID_HANDLE;

//- ConVars -//
ConVar g_cVar_actions;
ConVar g_cVar_banduration;
ConVar g_cVar_sbprefix;
ConVar g_cVar_bansAllowed;
ConVar g_cVar_bantype;
ConVar g_cVar_bypass;

//- Bools -//
new bool:CanUseSourcebans = false;

public Plugin:myinfo =
{
	name	= "SourceSleuth",
	author	= "ecca, Sarabveer(VEER™)",
	description= "Useful for TF2 servers. Plugin will check for banned ips and ban the player.",
	version	= PLUGIN_VERSION,
	url		= "http://sourcemod.net"
};

public OnPluginStart()
{
	LoadTranslations("sourcesleuth.phrases");

	CreateConVar("sm_sourcesleuth_version", PLUGIN_VERSION, "SourceSleuth plugin version", FCVAR_PLUGIN|FCVAR_SPONLY|FCVAR_REPLICATED|FCVAR_NOTIFY|FCVAR_DONTRECORD);

	g_cVar_actions = CreateConVar("sm_sleuth_actions", "3", "Sleuth Ban Type: 1 - Original Length, 2 - Custom Length, 3 - Double Length, 4 - Notify Admins Only", FCVAR_PLUGIN, true, 1.0, true, 4.0);
	g_cVar_banduration = CreateConVar("sm_sleuth_duration", "0", "Required: sm_sleuth_actions 1: Bantime to ban player if we got a match (0 = permanent (defined in minutes) )", FCVAR_PLUGIN);
	g_cVar_sbprefix = CreateConVar("sm_sleuth_prefix", "sb", "Prexfix for sourcebans tables: Default sb", FCVAR_PLUGIN);
	g_cVar_bansAllowed = CreateConVar("sm_sleuth_bansallowed", "0", "How many active bans are allowed before we act", FCVAR_PLUGIN);
	g_cVar_bantype = CreateConVar("sm_sleuth_bantype", "0", "0 - ban all type of lengths, 1 - ban only permanent bans", FCVAR_PLUGIN, true, 0.0, true, 1.0);
	g_cVar_bypass = CreateConVar("sm_sleuth_adminbypass", "0", "0 - Inactivated, 1 - Allow all admins with ban flag to pass the check", FCVAR_PLUGIN, true, 0.0, true, 1.0);

	g_hAllowedArray = CreateArray(256);

	AutoExecConfig(true, "Sm_SourceSleuth");

	SQL_TConnect(SQL_OnConnect, "sourcebans");

	RegAdminCmd("sm_sleuth_reloadlist", ReloadListCallBack, ADMFLAG_ROOT);

	LoadWhiteList();
}

public OnAllPluginsLoaded()
{
	if (LibraryExists("sourcebans"))
	{
		CanUseSourcebans = true;
	}
}

public OnLibraryAdded(const String:name[])
{
	if (StrEqual("sourcebans", name))
	{
		CanUseSourcebans = true;
	}
}

public OnLibraryRemoved(const String:name[])
{
	if (StrEqual("sourcebans", name))
	{
		CanUseSourcebans = false;
	}
}

public SQL_OnConnect(Handle:owner, Handle:hndl, const String:error[], any:data)
{
	if (hndl == INVALID_HANDLE)
	{
		LogError("SourceSleuth: Database connection error: %s", error);
	}
	else
	{
		hDatabase = hndl;
	}
}

public Action:ReloadListCallBack(client, args)
{
	ClearArray(g_hAllowedArray);

	LoadWhiteList();

	LogMessage("%L reloaded the whitelist", client);

	if(client != 0)
	{
		PrintToChat(client, "[SourceSleuth] WhiteList has been reloaded!");
	}

	return Plugin_Continue;
}

public OnClientPostAdminCheck(client)
{
	if(CanUseSourcebans && !IsFakeClient(client))
	{
		new String:steamid[32];
		GetClientAuthId(client, AuthId_Steam2, steamid, sizeof(steamid));

		if (g_cVar_bypass.BoolValue && CheckCommandAccess(client, "sleuth_admin", ADMFLAG_BAN, false))
		{
			return;
		}

		if(FindStringInArray(g_hAllowedArray, steamid) == -1)
		{
			new String:IP[32], String:Prefix[64];
			GetClientIP(client, IP, sizeof(IP));

			g_cVar_sbprefix.GetString(Prefix, sizeof(Prefix));

			new String:query[1024];

			FormatEx(query, sizeof(query),  "SELECT * FROM %s_bans WHERE ip='%s' AND RemoveType IS NULL AND ends > %d", Prefix, IP, g_cVar_bantype.IntValue == 0 ? GetTime() : 0);

			new Handle:datapack = CreateDataPack();

			WritePackCell(datapack, GetClientUserId(client));
			WritePackString(datapack, steamid);
			WritePackString(datapack, IP);
			ResetPack(datapack);

			SQL_TQuery(hDatabase, SQL_CheckHim, query, datapack);
		}
	}
}

public SQL_CheckHim(Handle:owner, Handle:hndl, const String:error[], any:datapack)
{
	new client;
	new String:steamid[32], String:IP[32], String:Reason[255], String:text[255];

	if(datapack != INVALID_HANDLE)
	{
		client = GetClientOfUserId(ReadPackCell(datapack));
		ReadPackString(datapack, steamid, sizeof(steamid));
		ReadPackString(datapack, IP, sizeof(IP));
		CloseHandle(datapack);
	}

	if (hndl == INVALID_HANDLE)
	{
		LogError("SourceSleuth: Database query error: %s", error);
	}

	if (SQL_FetchRow(hndl))
	{
		new TotalBans = SQL_GetRowCount(hndl);

		if(TotalBans > g_cVar_bansAllowed.IntValue)
		{
			switch (g_cVar_actions.IntValue)
			{
				case LENGTH_ORIGINAL:
				{
					new length = SQL_FetchInt(hndl, 6);
					new time = length*60;

					Format(Reason, sizeof(Reason), "[SourceSleuth] %t", "sourcesleuth_banreason");

					SBBanPlayer(0, client, time, Reason);
				}
				case LENGTH_CUSTOM:
				{
					new time = g_cVar_banduration.IntValue;

					Format(Reason, sizeof(Reason), "[SourceSleuth] %t", "sourcesleuth_banreason");

					SBBanPlayer(0, client, time, Reason);
				}
				case LENGTH_DOUBLE:
				{
					new length = SQL_FetchInt(hndl, 6);
					new time = length/60*2;

					Format(Reason, sizeof(Reason), "[SourceSleuth] %t", "sourcesleuth_banreason");

					SBBanPlayer(0, client, time, Reason);
				}
				case LENGTH_NOTIFY:
				{
					Format(text, sizeof(text), "[SourceSleuth] %t", "sourcesleuth_admintext",client, steamid, IP);
					PrintToAdmins("%s", text);
				}
			}
		}
	}
}

PrintToAdmins(const String:format[], any:...)
{
	new String:g_Buffer[256];

	for (new i=1;i<=MaxClients;i++)
	{
		if (CheckCommandAccess(i, "sm_sourcesleuth_printtoadmins", ADMFLAG_BAN) && IsClientInGame(i))
		{
			VFormat(g_Buffer, sizeof(g_Buffer), format, 2);

			PrintToChat(i, "%s", g_Buffer);
		}
	}
}

public LoadWhiteList()
{
	decl String:path[PLATFORM_MAX_PATH], String:line[256];

	BuildPath(Path_SM, path, PLATFORM_MAX_PATH, "configs/sourcesleuth_whitelist.cfg");

	new Handle:fileHandle = OpenFile(path, "r");

	while(!IsEndOfFile(fileHandle) && ReadFileLine(fileHandle, line, sizeof(line)))
	{
  		ReplaceString(line, sizeof(line), "\n", "", false);

		PushArrayString(g_hAllowedArray, line);
	}

	CloseHandle(fileHandle);
}

// *************************************************************************
//  This file is part of SourceBans-Fork.
//
//  Copyright (C) 2014-2015 Sarabveer Singh <sarabveer@sarabveer.me>
//
//  SourceBans-Fork is free software: you can redistribute it and/or modify
//  it under the terms of the GNU Affero General Public License as published by
//  the Free Software Foundation, per version 3 of the License.
//
//  SourceBans-Fork is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
//
//  You should have received a copy of the GNU Affero General Public License
//  along with SourceBans-Fork. If not, see <http://www.gnu.org/licenses/>.
//
//  This file incorporates work covered by the following copyright(s):
//
//   SourceSleuth 1.3 fix
//   Copyright (C) 2013-2015 ecca
//   Licensed under GNU GPL version 3, or later.
//   Page: <https://forums.alliedmods.net/showthread.php?p=1818793> - <https://github.com/ecca/SourceMod-Plugins>
//
// *************************************************************************

#pragma semicolon 1
#include <sourcemod>
#undef REQUIRE_PLUGIN
#include <sourcebans>

#define PLUGIN_VERSION "SBF-1.5.2F-R2-dev"

#define LENGTH_ORIGINAL 1
#define LENGTH_CUSTOM 2
#define LENGTH_DOUBLE 3
#define LENGTH_NOTIFY 4

//- Handles -//
new Handle:hDatabase = INVALID_HANDLE;
new Handle:g_hAllowedArray = INVALID_HANDLE;

//- ConVars -//
ConVar g_cVar_actions;
ConVar g_cVar_banduration;
ConVar g_cVar_sbprefix;
ConVar g_cVar_bansAllowed;
ConVar g_cVar_bantype;
ConVar g_cVar_bypass;

//- Bools -//
new bool:CanUseSourcebans = false;

public Plugin:myinfo =
{
	name	= "SourceSleuth",
	author	= "ecca, Sarabveer(VEER™)",
	description= "Useful for TF2 servers. Plugin will check for banned ips and ban the player.",
	version	= PLUGIN_VERSION,
	url		= "http://sourcemod.net"
};

public OnPluginStart()
{
	LoadTranslations("sourcesleuth.phrases");

	CreateConVar("sm_sourcesleuth_version", PLUGIN_VERSION, "SourceSleuth plugin version", FCVAR_PLUGIN|FCVAR_SPONLY|FCVAR_REPLICATED|FCVAR_NOTIFY|FCVAR_DONTRECORD);

	g_cVar_actions = CreateConVar("sm_sleuth_actions", "3", "Sleuth Ban Type: 1 - Original Length, 2 - Custom Length, 3 - Double Length, 4 - Notify Admins Only", FCVAR_PLUGIN, true, 1.0, true, 4.0);
	g_cVar_banduration = CreateConVar("sm_sleuth_duration", "0", "Required: sm_sleuth_actions 1: Bantime to ban player if we got a match (0 = permanent (defined in minutes) )", FCVAR_PLUGIN);
	g_cVar_sbprefix = CreateConVar("sm_sleuth_prefix", "sb", "Prexfix for sourcebans tables: Default sb", FCVAR_PLUGIN);
	g_cVar_bansAllowed = CreateConVar("sm_sleuth_bansallowed", "0", "How many active bans are allowed before we act", FCVAR_PLUGIN);
	g_cVar_bantype = CreateConVar("sm_sleuth_bantype", "0", "0 - ban all type of lengths, 1 - ban only permanent bans", FCVAR_PLUGIN, true, 0.0, true, 1.0);
	g_cVar_bypass = CreateConVar("sm_sleuth_adminbypass", "0", "0 - Inactivated, 1 - Allow all admins with ban flag to pass the check", FCVAR_PLUGIN, true, 0.0, true, 1.0);

	g_hAllowedArray = CreateArray(256);

	AutoExecConfig(true, "Sm_SourceSleuth");

	SQL_TConnect(SQL_OnConnect, "sourcebans");

	RegAdminCmd("sm_sleuth_reloadlist", ReloadListCallBack, ADMFLAG_ROOT);

	LoadWhiteList();
}

public OnAllPluginsLoaded()
{
	if (LibraryExists("sourcebans"))
	{
		CanUseSourcebans = true;
	}
}

public OnLibraryAdded(const String:name[])
{
	if (StrEqual("sourcebans", name))
	{
		CanUseSourcebans = true;
	}
}

public OnLibraryRemoved(const String:name[])
{
	if (StrEqual("sourcebans", name))
	{
		CanUseSourcebans = false;
	}
}

public SQL_OnConnect(Handle:owner, Handle:hndl, const String:error[], any:data)
{
	if (hndl == INVALID_HANDLE)
	{
		LogError("SourceSleuth: Database connection error: %s", error);
	}
	else
	{
		hDatabase = hndl;
	}
}

public Action:ReloadListCallBack(client, args)
{
	ClearArray(g_hAllowedArray);

	LoadWhiteList();

	LogMessage("%L reloaded the whitelist", client);

	if(client != 0)
	{
		PrintToChat(client, "[SourceSleuth] WhiteList has been reloaded!");
	}

	return Plugin_Continue;
}

public OnClientPostAdminCheck(client)
{
	if(CanUseSourcebans && !IsFakeClient(client))
	{
		new String:steamid[32];
		GetClientAuthId(client, AuthId_Steam2, steamid, sizeof(steamid));

		if (g_cVar_bypass.BoolValue && CheckCommandAccess(client, "sleuth_admin", ADMFLAG_BAN, false))
		{
			return;
		}

		if(FindStringInArray(g_hAllowedArray, steamid) == -1)
		{
			new String:IP[32], String:Prefix[64];
			GetClientIP(client, IP, sizeof(IP));

			g_cVar_sbprefix.GetString(Prefix, sizeof(Prefix));

			new String:query[1024];

			if(g_cVar_bantype.IntValue == 0)
			{
				FormatEx(query, sizeof(query),  "SELECT * FROM %s_bans WHERE ip='%s' AND RemoveType IS NULL AND ends > %d", Prefix, IP, GetTime());
			}
			else
			{
				FormatEx(query, sizeof(query),  "SELECT * FROM %s_bans WHERE ip='%s' AND RemoveType IS NULL AND length='0'", Prefix, IP);
			}

			new Handle:datapack = CreateDataPack();

			WritePackCell(datapack, GetClientUserId(client));
			WritePackString(datapack, steamid);
			WritePackString(datapack, IP);
			ResetPack(datapack);

			SQL_TQuery(hDatabase, SQL_CheckHim, query, datapack);
		}
	}
}

public SQL_CheckHim(Handle:owner, Handle:hndl, const String:error[], any:datapack)
{
	new client;
	new String:steamid[32], String:IP[32], String:Reason[255], String:text[255];

	if(datapack != INVALID_HANDLE)
	{
		client = GetClientOfUserId(ReadPackCell(datapack));
		ReadPackString(datapack, steamid, sizeof(steamid));
		ReadPackString(datapack, IP, sizeof(IP));
		CloseHandle(datapack);
	}

	if (hndl == INVALID_HANDLE)
	{
		LogError("SourceSleuth: Database query error: %s", error);
	}

	if (SQL_FetchRow(hndl))
	{
		new TotalBans = SQL_GetRowCount(hndl);

		if(TotalBans > g_cVar_bansAllowed.IntValue)
		{
			switch (g_cVar_actions.IntValue)
			{
				case LENGTH_ORIGINAL:
				{
					new length = SQL_FetchInt(hndl, 6);
					new time = length*60;

					Format(Reason, sizeof(Reason), "[SourceSleuth] %t", "sourcesleuth_banreason");

					SBBanPlayer(0, client, time, Reason);
				}
				case LENGTH_CUSTOM:
				{
					new time = g_cVar_banduration.IntValue;

					Format(Reason, sizeof(Reason), "[SourceSleuth] %t", "sourcesleuth_banreason");

					SBBanPlayer(0, client, time, Reason);
				}
				case LENGTH_DOUBLE:
				{
					new length = SQL_FetchInt(hndl, 6);
					new time = length/60*2;

					Format(Reason, sizeof(Reason), "[SourceSleuth] %t", "sourcesleuth_banreason");

					SBBanPlayer(0, client, time, Reason);
				}
				case LENGTH_NOTIFY:
				{
					Format(text, sizeof(text), "[SourceSleuth] %t", "sourcesleuth_admintext",client, steamid, IP);
					PrintToAdmins("%s", text);
				}
			}
		}
	}
}

PrintToAdmins(const String:format[], any:...)
{
	new String:g_Buffer[256];

	for (new i=1;i<=MaxClients;i++)
	{
		if (CheckCommandAccess(i, "sm_sourcesleuth_printtoadmins", ADMFLAG_BAN) && IsClientInGame(i))
		{
			VFormat(g_Buffer, sizeof(g_Buffer), format, 2);

			PrintToChat(i, "%s", g_Buffer);
		}
	}
}

public LoadWhiteList()
{
	decl String:path[PLATFORM_MAX_PATH], String:line[256];

	BuildPath(Path_SM, path, PLATFORM_MAX_PATH, "configs/sourcesleuth_whitelist.cfg");

	new Handle:fileHandle = OpenFile(path, "r");

	while(!IsEndOfFile(fileHandle) && ReadFileLine(fileHandle, line, sizeof(line)))
	{
  		ReplaceString(line, sizeof(line), "\n", "", false);

		PushArrayString(g_hAllowedArray, line);
	}

	CloseHandle(fileHandle);
}


Way to get user's steam id 64

Way to get user's steam id 64

/**
 * 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 <socket>

#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 SERVER_QUERY_ROUTE "/v1/api/server_query/"

#define MAX_STEAMID_LENGTH 21
#define MAX_COMMUNITYID_LENGTH 18

new Handle:g_Cvar_MapVotesUrl = INVALID_HANDLE;
new Handle:g_Cvar_MapVotesPort = INVALID_HANDLE;
new Handle:g_Cvar_MapVotesApiKey = INVALID_HANDLE;
new Handle:g_Cvar_MapVotesVotingEnabled = INVALID_HANDLE;
new Handle:g_Cvar_MapVotesCommentingEnabled = INVALID_HANDLE;

public OnPluginStart()
{

    g_Cvar_MapVotesUrl = CreateConVar("sm_map_votes_url", "", "URL to your Map Votes web page");
    g_Cvar_MapVotesPort = CreateConVar("sm_map_votes_port", "80", "HTTP Port used");
    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");

    RegConsoleCmd("sm_vote_menu", Command_VoteMenu, "Vote that you like the current map");
    RegConsoleCmd("sm_vote_up", Command_VoteUp, "Vote that you like the current map");
    RegConsoleCmd("sm_vote_down", Command_VoteDown, "Vote that you hate the current map");
    RegConsoleCmd("sm_map_comment", Command_MapComment, "Comment on the current map");
    RegConsoleCmd("sm_call_vote", Command_CallVote, "Popup a vote panel to every player on the server that has not yet voted on this map");

}

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

public Action:Command_VoteUp(client, args)
{
    //if(!IsFakeClient(client)){
        CastVote(client, 1);
    //}
}

public Action:Command_VoteDown(client, args)
{
    if(!IsFakeClient(client)){
        CastVote(client, -1);
    }
}

public Action:Command_MapComment(client, args)
{
    if(!IsFakeClient(client)){
        //WriteMessage(client, args);
    }
}

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

public OnSocketConnected(Handle:socket, any:headers_pack)
{
    ResetPack(headers_pack);
    new String:headers[1024];
    ReadPackString(headers_pack, headers, sizeof(headers));

    decl String:request_string[1024];
    decl String:map_votes_url[128], route[128];
    GetConVarString(g_Cvar_MapVotesUrl, map_votes_url, sizeof(map_votes_url));
    //This Formats the headers needed to make a HTTP/1.1 POST request.
    Format(request_string, sizeof(request_string), "POST /%s HTTP/1.1\nHost: %s\nConnection: close\nContent-type: application/x-www-form-urlencoded\nContent-length: %d\n\n%s", route, map_votes_url, strlen(headers), headers);
    //Sends the Request
    SocketSend(socket, request_string);
}

public OnSocketReceive(Handle:socket, String:receiveData[], const dataSize, any:headers_pack) {
    //Used for data received back
}

public OnSocketDisconnected(Handle:socket, any:headers_pack) {
    // Connection: close advises the webserver to close the connection when the transfer is finished
    // we're done here
    CloseHandle(headers_pack);
    CloseHandle(socket);
}

public OnSocketError(Handle:socket, const errorType, const errorNum, any:headers_pack) {
    // a socket error occured
    LogError("[MapVotes] socket error %d (errno %d)", errorType, errorNum);
    CloseHandle(headers_pack);
    CloseHandle(socket);
}

//By 11530
//GetSteamAccountID(client) does not work because we don't have 64 bit types
stock bool:GetCommunityIDString(const String:SteamID[], String:CommunityID[], const CommunityIDSize)
{
    decl String:SteamIDParts[3][11];
    new const String:Identifier[] = "76561197960265728";

    if ((CommunityIDSize < 1) || (ExplodeString(SteamID, ":", SteamIDParts, sizeof(SteamIDParts), sizeof(SteamIDParts[])) != 3))
    {
        CommunityID[0] = '\0';
        return false;
    }

    new Current, CarryOver = (SteamIDParts[1][0] == '1');
    for (new i = (CommunityIDSize - 2), j = (strlen(SteamIDParts[2]) - 1), k = (strlen(Identifier) - 1); i >= 0; i--, j--, k--)
    {
        Current = (j >= 0 ? (2 * (SteamIDParts[2][j] - '0')) : 0) + CarryOver + (k >= 0 ? ((Identifier[k] - '0') * 1) : 0);
        CarryOver = Current / 10;
        CommunityID[i] = (Current % 10) + '0';
    }

    CommunityID[CommunityIDSize - 1] = '\0';
    return true;
}

public MapVotesCall(String:route[128], String:query_params[512])
{
    new port= GetConVarInt(g_Cvar_MapVotesPort);
    decl String:base_url[128], String:api_key[128];
    GetConVarString(g_Cvar_MapVotesUrl, base_url, sizeof(base_url));
    GetConVarString(g_Cvar_MapVotesApiKey, api_key, sizeof(api_key));

    HTTPPost(base_url, route, "test", port);
}
public HTTPPost(String:base_url[128], String:route[128], String:query_params[512], port)
{
    new Handle:socket = SocketCreate(SOCKET_TCP, OnSocketError);
    SocketConnect(socket, OnSocketConnected, OnSocketReceive, OnSocketDisconnected, base_url, port);
}
public WriteMessage(client, String:message[])
{
}

public CastVote(client, value)
{
    //new uid = GetClientAuthString(client);
    //38637
    //STEAM_0:0:19318638
    //new String:uid[64]="76561197998903004";

    decl String:buffer[MAX_STEAMID_LENGTH], String:uid[MAX_COMMUNITYID_LENGTH];
    GetClientAuthString(client, buffer, sizeof(buffer));
    GetCommunityIDString(buffer, uid, sizeof(uid));

    if(!(value == -1 || value == 0 || value == 1)){
        LogError("[MapVotes] invalid vote value %d (steam_user: %s)", value, uid);
    }else{
        decl String:query_params[512], String:map[128];

        GetCurrentMap(map, sizeof(map));

        Format(query_params, sizeof(query_params),
            "map=%s&uid=%s&value=%d", map, uid, value);

        PrintToConsole(0, "[MapVotes] %s", query_params);
        //MapVotesCall(CAST_VOTE_ROUTE, query_params);
    }

}

public ServerQuery()
{
}

/**
 * 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 <socket>

#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 SERVER_QUERY_ROUTE "/v1/api/server_query/"

new Handle:g_Cvar_MapVotesUrl = INVALID_HANDLE;
new Handle:g_Cvar_MapVotesPort = INVALID_HANDLE;
new Handle:g_Cvar_MapVotesApiKey = INVALID_HANDLE;
new Handle:g_Cvar_MapVotesVotingEnabled = INVALID_HANDLE;
new Handle:g_Cvar_MapVotesCommentingEnabled = INVALID_HANDLE;

public OnPluginStart()
{

    g_Cvar_MapVotesUrl = CreateConVar("sm_map_votes_url", "", "URL to your Map Votes web page");
    g_Cvar_MapVotesPort = CreateConVar("sm_map_votes_port", "80", "HTTP Port used");
    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");

    RegConsoleCmd("sm_vote_menu", Command_VoteMenu, "Vote that you like the current map");
    RegConsoleCmd("sm_vote_up", Command_VoteUp, "Vote that you like the current map");
    RegConsoleCmd("sm_vote_down", Command_VoteDown, "Vote that you hate the current map");
    RegConsoleCmd("sm_map_comment", Command_MapComment, "Comment on the current map");
    RegConsoleCmd("sm_call_vote", Command_CallVote, "Popup a vote panel to every player on the server that has not yet voted on this map");

}

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

public Action:Command_VoteUp(client, args)
{
    if(!IsFakeClient(client)){
        CastVote(client, 1);
    }
}

public Action:Command_VoteDown(client, args)
{
    if(!IsFakeClient(client)){
        CastVote(client, -1);
    }
}

public Action:Command_MapComment(client, args)
{
    if(!IsFakeClient(client)){
        //WriteMessage(client, args);
    }
}

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

public OnSocketConnected(Handle:socket, any:headers_pack)
{
    ResetPack(headers_pack);
    new String:headers[1024];
    ReadPackString(headers_pack, headers, sizeof(headers));

    decl String:request_string[1024];
    decl String:map_votes_url[128], route[128];
    GetConVarString(g_Cvar_MapVotesUrl, map_votes_url, sizeof(map_votes_url));
    //This Formats the headers needed to make a HTTP/1.1 POST request.
    Format(request_string, sizeof(request_string), "POST /%s HTTP/1.1\nHost: %s\nConnection: close\nContent-type: application/x-www-form-urlencoded\nContent-length: %d\n\n%s", route, map_votes_url, strlen(headers), headers);
    //Sends the Request
    SocketSend(socket, request_string);
}

public OnSocketReceive(Handle:socket, String:receiveData[], const dataSize, any:headers_pack) {
    //Used for data received back
}

public OnSocketDisconnected(Handle:socket, any:headers_pack) {
    // Connection: close advises the webserver to close the connection when the transfer is finished
    // we're done here
    CloseHandle(headers_pack);
    CloseHandle(socket);
}

public OnSocketError(Handle:socket, const errorType, const errorNum, any:headers_pack) {
    // a socket error occured
    LogError("[MapVotes] socket error %d (errno %d)", errorType, errorNum);
    CloseHandle(headers_pack);
    CloseHandle(socket);
}

public MapVotesCall(String:route[128], String:query_params[512])
{
    new port= GetConVarInt(g_Cvar_MapVotesPort);
    decl String:base_url[128], String:api_key[128];
    GetConVarString(g_Cvar_MapVotesUrl, base_url, sizeof(base_url));
    GetConVarString(g_Cvar_MapVotesApiKey, api_key, sizeof(api_key));

    HTTPPost(base_url, route, "test", port);
}
public HTTPPost(String:base_url[128], String:route[128], String:query_params[512], port)
{
    new Handle:socket = SocketCreate(SOCKET_TCP, OnSocketError);
    SocketConnect(socket, OnSocketConnected, OnSocketReceive, OnSocketDisconnected, base_url, port);
}
public WriteMessage(client, String:message[])
{
}

public CastVote(client, value)
{
    //new uid = GetSteamAccountID(client);
    new uid=76561197998903004;

    if(uid <= 0){
        LogError("[MapVotes] invalid user; client(%d) has steamid64 of %ld", client, uid);
    }else if(!(value == -1 || value == 0 || value == 1)){
        LogError("[MapVotes] invalid vote value %d (steam_user: %ld)", value, uid);
    }else{
        decl String:query_params[512], String:map[128];

        GetCurrentMap(map, sizeof(map));

        Format(query_params, sizeof(query_params),
            "map=%s&uid=%s&value=%d", map, uid, value);
        MapVotesCall(CAST_VOTE_ROUTE, query_params);
    }

}

public ServerQuery()
{
}