DO: Fix logging active droplet's address

class DO { } # defined below
#| Represents a DigitalOcean API ID
class DO-ID is Int { }

#| A DigitalOcean API response
class DO-API-response is HTTP-response {
    #| Checks the Digital Ocean API returned a successful HTTP
    #| response. If not, it will `die` with the error message from the api.
    method ok^ {
        if $self.is-success {
            $self
        }
        else {
            die "{$self.req-method} {$self.remote-url} {$self.code} {$self.message}\n" ~
            $self.json<message>;
        }
    }

}
#| Represents a Digital Ocean VM
class Droplet is JSON {
    method DO-ID { $self<id>-->Int }
    method name~ { $self<name> }
    method valid? { $self.DO-ID.valid }
    method delete -->DO-API-response {
        if $self.status eq 'new' {
            warn "{$self.name}: Waiting to be created before deleting", "\c[DROPLET]";
            $self.wait-till-active;
        }
        debug "{$self.name}: Deleting", "\c[DROPLET]";
        DO.delete($self)
    }
    method refresh -->Droplet { $self.merge: DO.droplet($self) }
    method status~ { $self<status> }
    method wait-till-active -->Droplet {
        debug "Waiting to be active", "\c[DROPLET]:{$self.name}";

        while (my $refresh = $self.refresh; $refresh.status eq 'new') { sleep 10 }

        $refresh.status eq 'active' or
          fatal "{$self.name}: Status wasn't active, got '{$refresh.status}' instead", "\c[DROPLET]";

        info "{$self.name}: Active at {$refresh.ipv4}", "\c[DROPLET]";
        $refresh;
    }
    method ipv4 --> Host {
        $self<networks><v4>[0]<ip_address>
    }
}

class DO-region is JSON {
    method name~      { $self<name> }
    method slug~      { $self<slug> }
    method features@  { $self<features> }
    method sizes@     { $self<sizes> }
    method available? { $self<available> }
}

class DO-image is JSON {
    method DO-ID   { $self<id>-->Int }
    method name~   { $self<name>   }
    method slug~   { $self<slug>   }
    method public? { $self<public> }
    method type~   { $self<type>   }
    method size~   { $self<size_gigabytes> }
    method distribution~ { $self<distribution> }
    method min-disk-size+ { $self<min_disk_size>-->Int }
    method regions --> List[DO-region] { $self<regions>.List-->List[JSON] }
}

class DO-key is JSON {
    method DO-ID { $self<id>-->Int }
    method fingerprint~ { $self<fingerprint> }
    method name~        { $self<name> }

    method private-key-file --> File  { $self<private-key-file> }
    method public-key~  { $self<public_key> }

    method delete -->DO-API-response { DO.delete-key($self) }
    method exists?                   { DO.exists-key($self) }

    constant $key-cleanup = File.tmp;

    method cleanup^ {
        $key-cleanup.push($self);
        END { .delete for $key-cleanup.slurp-->List[DO-key] }
    }
}

constant File $droplet-cleanup = ${mktemp};

augment DO {

    constant $:api-token;
    constant $:api-version = 'v2';
    constant HTTP $:api-url = "https://api.digitalocean.com/$:api-version";
    constant HTTP $:metadata-url = 'http://169.254.169.254/metadata/v1';
    constant $:region = 'nyc1';
    constant @:ssh-keys =  $:ssh-keypair.public-key.fingerprint(:algo<md5>);
    constant $:size = '512mb';
    constant $:image = 'debian-9-x64';
    constant $:seed-port = 2222;

    static method images(:$type) -->List[DO-image] {
        DO.request(
            'GET',
            "images",
            query => (per_page => 200, (:$type if $type))
        ).ok.json<images>.List-->List[JSON]
    }

    static method regions -->List[DO-region] {
        DO.request('GET', 'regions').ok.json<regions>.List-->List[JSON]
    }

    static method droplets -->List[Droplet] {
        DO.request('GET','droplets').ok.json<droplets>.List-->List[JSON]
    }

    static method create(
              :$name = Str.random,
              :$region = $:region,
              :$size = $:size,
              :$image = $:image,
              :@ssh-keys = @:ssh-keys,
              :$on-boot
    ) --> Droplet
    {
        my $json = {
            :$name,
            :$region,
            :$size,
            :$image,
            ssh_keys => @ssh-keys,
            user_data => "#!/bin/sh\n$on-boot"
        };

        info "Creating droplet ($name $region $size $image)", "\c[WATER WAVE]";
        DO.request('POST', 'droplets', :$json).ok.json<droplet>;
    }

    static method create-ssh-seeded(
        :$name = Str.random,
        :$region = $:region,
        :$size   = $:size,
        :$image  = $:image,
        :@ssh-keys = @:ssh-keys
    ) -->Droplet {
        my $server-keys = SSH-keypair.tmp;

        my $set-ssh-keys = eval(
            os => UNIXish,
            :!log,
            ssh-public => $server-keys.public-key,
            ssh-private => $server-keys.private-key,
            sshd => '/usr/sbin/sshd', # DO always has ssh installed
        ){
            my $keydir = File</tmp/spit-seed>.mkdir.cleanup;
            $keydir.add('seed_ssh.pub').write("$:<ssh-public>\n");
            my SSH-keypair $server-keys = $keydir.add('seed_ssh').write("$:<ssh-private>\n");
            $server-keys.fix-perms;
            SSHd.run(port => $:seed-port, :$server-keys);
        };

        my $droplet = DO.create(
            :@ssh-keys,
            on-boot => $set-ssh-keys,
            :$region,
            :$size,
            :$image,
            :$name,
        );

        debug "{$droplet.name}: Wating 30 seconds for to boot", "\c[DROPLET]";

        sleep 30;
        $droplet .= wait-till-active;
        my $ip = $droplet.ipv4;
        $server-keys.public-key.known-host($ip).add;

        $ip.wait-connectable($:seed-port, :timeout(40))
          or fatal " {$droplet.name}: ssh seeding failed: seed port $:seed-port never opened for", "\c[DROPLET]";

        $droplet;
    }

    static method delete(DO-ID $id) -->DO-API-response {
        info "Deleting droplet $id", "\c[WATER WAVE]";
        DO.request('DELETE', "droplets/$id")
    }

    static method droplet(DO-ID $id) -->Droplet {
        DO.request('GET', "droplets/$id").ok.json<droplet>
    }

    static method keys -->List[DO-key] {
        DO.request('GET', 'account/keys').ok.json<ssh_keys>.List-->List[JSON]
    }

    static method create-key(SSH-public-key $public-key, :$name = Str.random) -->DO-key {
        info "Creating key $name ({$public-key.fingerprint})", "\c[WATER WAVE]";
        DO.request('POST', 'account/keys', json => (:$name, public_key => $public-key))
          .ok.json<ssh_key>;
    }

    static method ensure-key(SSH-public-key $public-key, :$name = Str.random) {
        my $fingerprint = $public-key.fingerprint(:algo<md5>);
        DO.exists-key($fingerprint)
          ?? debug "ssh key $fingerprint already exists", "\c[WATER WAVE]"
          !! DO.create-key($public-key, :$name);
    }

    static method get-key($fingerprint) --> DO-key {
        DO.request('GET', "account/keys/$fingerprint").ok.json<ssh_key>;
    }

    static method exists-key($fingerprint)? {
        DO.request('GET', "account/keys/$fingerprint").is-success;
    }

    static method delete-key($fingerprint) -->DO-API-response {
        info "Deleting key $fingerprint", "\c[WATER WAVE]";
        DO.request('DELETE', "account/keys/$fingerprint")
    }

    static method request($method, $resource, Pair :@query, JSON :$json) --> DO-API-response {
         $:api-url.add($resource).query(@query).request(
            $method,
            :$json,
            headers => ("Authorization: Bearer $:api-token", 'Expect:'),
        );
    }

    static method tmp(:$region,:$size,:$image) -->Droplet {
        my $temp-droplet = DO.create(name => 'tmp-' ~ Str.random, :$region, :$size, :$image);
        $droplet-cleanup.push: $temp-droplet;
        $temp-droplet;
        END {
            my @droplets = $droplet-cleanup.slurp-->List[Droplet];
            if @droplets {
                info "starting droplet cleanup";
                for @droplets { .delete }
            } else {
                debug 'no droplets to cleanup';
            }
            $droplet-cleanup.remove;
        }
    }

    static method metadata-request($resource)~ ${
        $:curl -s $:metadata-url.add($resource)
    }

    static method my-id -->DO-ID {
        DO.metadata-request('id')-->DO-ID
    }

    static method my-metadata -->JSON {
        DO.metadata-request('.json')
    }

}

augment Droplet {
    method ssh-seed($shell)~ {
        $self.ipv4.ssh(
            port => $DO:seed-port,
            $shell,
        )
    }
}

class DO { } # defined below
#| Represents a DigitalOcean API ID
class DO-ID is Int { }

#| A DigitalOcean API response
class DO-API-response is HTTP-response {
    #| Checks the Digital Ocean API returned a successful HTTP
    #| response. If not, it will `die` with the error message from the api.
    method ok^ {
        if $self.is-success {
            $self
        }
        else {
            die "{$self.req-method} {$self.remote-url} {$self.code} {$self.message}\n" ~
            $self.json<message>;
        }
    }

}
#| Represents a Digital Ocean VM
class Droplet is JSON {
    method DO-ID { $self<id>-->Int }
    method name~ { $self<name> }
    method valid? { $self.DO-ID.valid }
    method delete -->DO-API-response {
        if $self.status eq 'new' {
            warn "{$self.name}: Waiting to be created before deleting", "\c[DROPLET]";
            $self.wait-till-active;
        }
        debug "{$self.name}: Deleting", "\c[DROPLET]";
        DO.delete($self)
    }
    method refresh -->Droplet { $self.merge: DO.droplet($self) }
    method status~ { $self<status> }
    method wait-till-active -->Droplet {
        debug "Waiting to be active", "\c[DROPLET]:{$self.name}";

        while (my $refresh = $self.refresh; $refresh.status eq 'new') { sleep 10 }

        $refresh.status eq 'active' or
          fatal "{$self.name}: Status wasn't active, got '{$refresh.status}' instead", "\c[DROPLET]";

        info "{$self.name}: Active at {$self.ipv4}", "\c[DROPLET]";
        $refresh;
    }
    method ipv4 --> Host {
        $self<networks><v4>[0]<ip_address>
    }
}

class DO-region is JSON {
    method name~      { $self<name> }
    method slug~      { $self<slug> }
    method features@  { $self<features> }
    method sizes@     { $self<sizes> }
    method available? { $self<available> }
}

class DO-image is JSON {
    method DO-ID   { $self<id>-->Int }
    method name~   { $self<name>   }
    method slug~   { $self<slug>   }
    method public? { $self<public> }
    method type~   { $self<type>   }
    method size~   { $self<size_gigabytes> }
    method distribution~ { $self<distribution> }
    method min-disk-size+ { $self<min_disk_size>-->Int }
    method regions --> List[DO-region] { $self<regions>.List-->List[JSON] }
}

class DO-key is JSON {
    method DO-ID { $self<id>-->Int }
    method fingerprint~ { $self<fingerprint> }
    method name~        { $self<name> }

    method private-key-file --> File  { $self<private-key-file> }
    method public-key~  { $self<public_key> }

    method delete -->DO-API-response { DO.delete-key($self) }
    method exists?                   { DO.exists-key($self) }

    constant $key-cleanup = File.tmp;

    method cleanup^ {
        $key-cleanup.push($self);
        END { .delete for $key-cleanup.slurp-->List[DO-key] }
    }
}

constant File $droplet-cleanup = ${mktemp};

augment DO {

    constant $:api-token;
    constant $:api-version = 'v2';
    constant HTTP $:api-url = "https://api.digitalocean.com/$:api-version";
    constant HTTP $:metadata-url = 'http://169.254.169.254/metadata/v1';
    constant $:region = 'nyc1';
    constant @:ssh-keys =  $:ssh-keypair.public-key.fingerprint(:algo<md5>);
    constant $:size = '512mb';
    constant $:image = 'debian-9-x64';
    constant $:seed-port = 2222;

    static method images(:$type) -->List[DO-image] {
        DO.request(
            'GET',
            "images",
            query => (per_page => 200, (:$type if $type))
        ).ok.json<images>.List-->List[JSON]
    }

    static method regions -->List[DO-region] {
        DO.request('GET', 'regions').ok.json<regions>.List-->List[JSON]
    }

    static method droplets -->List[Droplet] {
        DO.request('GET','droplets').ok.json<droplets>.List-->List[JSON]
    }

    static method create(
              :$name = Str.random,
              :$region = $:region,
              :$size = $:size,
              :$image = $:image,
              :@ssh-keys = @:ssh-keys,
              :$on-boot
    ) --> Droplet
    {
        my $json = {
            :$name,
            :$region,
            :$size,
            :$image,
            ssh_keys => @ssh-keys,
            user_data => "#!/bin/sh\n$on-boot"
        };

        info "Creating droplet ($name $region $size $image)", "\c[WATER WAVE]";
        DO.request('POST', 'droplets', :$json).ok.json<droplet>;
    }

    static method create-ssh-seeded(
        :$name = Str.random,
        :$region = $:region,
        :$size   = $:size,
        :$image  = $:image,
        :@ssh-keys = @:ssh-keys
    ) -->Droplet {
        my $server-keys = SSH-keypair.tmp;

        my $set-ssh-keys = eval(
            os => UNIXish,
            :!log,
            ssh-public => $server-keys.public-key,
            ssh-private => $server-keys.private-key,
            sshd => '/usr/sbin/sshd', # DO always has ssh installed
        ){
            my $keydir = File</tmp/spit-seed>.mkdir.cleanup;
            $keydir.add('seed_ssh.pub').write("$:<ssh-public>\n");
            my SSH-keypair $server-keys = $keydir.add('seed_ssh').write("$:<ssh-private>\n");
            $server-keys.fix-perms;
            SSHd.run(port => $:seed-port, :$server-keys);
        };

        my $droplet = DO.create(
            :@ssh-keys,
            on-boot => $set-ssh-keys,
            :$region,
            :$size,
            :$image,
            :$name,
        );

        debug "{$droplet.name}: Wating 30 seconds for to boot", "\c[DROPLET]";

        sleep 30;
        $droplet .= wait-till-active;
        my $ip = $droplet.ipv4;
        $server-keys.public-key.known-host($ip).add;

        $ip.wait-connectable($:seed-port, :timeout(40))
          or fatal " {$droplet.name}: ssh seeding failed: seed port $:seed-port never opened for", "\c[DROPLET]";

        $droplet;
    }

    static method delete(DO-ID $id) -->DO-API-response {
        info "Deleting droplet $id", "\c[WATER WAVE]";
        DO.request('DELETE', "droplets/$id")
    }

    static method droplet(DO-ID $id) -->Droplet {
        DO.request('GET', "droplets/$id").ok.json<droplet>
    }

    static method keys -->List[DO-key] {
        DO.request('GET', 'account/keys').ok.json<ssh_keys>.List-->List[JSON]
    }

    static method create-key(SSH-public-key $public-key, :$name = Str.random) -->DO-key {
        info "Creating key $name ({$public-key.fingerprint})", "\c[WATER WAVE]";
        DO.request('POST', 'account/keys', json => (:$name, public_key => $public-key))
          .ok.json<ssh_key>;
    }

    static method ensure-key(SSH-public-key $public-key, :$name = Str.random) {
        my $fingerprint = $public-key.fingerprint(:algo<md5>);
        DO.exists-key($fingerprint)
          ?? debug "ssh key $fingerprint already exists", "\c[WATER WAVE]"
          !! DO.create-key($public-key, :$name);
    }

    static method get-key($fingerprint) --> DO-key {
        DO.request('GET', "account/keys/$fingerprint").ok.json<ssh_key>;
    }

    static method exists-key($fingerprint)? {
        DO.request('GET', "account/keys/$fingerprint").is-success;
    }

    static method delete-key($fingerprint) -->DO-API-response {
        info "Deleting key $fingerprint", "\c[WATER WAVE]";
        DO.request('DELETE', "account/keys/$fingerprint")
    }

    static method request($method, $resource, Pair :@query, JSON :$json) --> DO-API-response {
         $:api-url.add($resource).query(@query).request(
            $method,
            :$json,
            headers => ("Authorization: Bearer $:api-token", 'Expect:'),
        );
    }

    static method tmp(:$region,:$size,:$image) -->Droplet {
        my $temp-droplet = DO.create(name => 'tmp-' ~ Str.random, :$region, :$size, :$image);
        $droplet-cleanup.push: $temp-droplet;
        $temp-droplet;
        END {
            my @droplets = $droplet-cleanup.slurp-->List[Droplet];
            if @droplets {
                info "starting droplet cleanup";
                for @droplets { .delete }
            } else {
                debug 'no droplets to cleanup';
            }
            $droplet-cleanup.remove;
        }
    }

    static method metadata-request($resource)~ ${
        $:curl -s $:metadata-url.add($resource)
    }

    static method my-id -->DO-ID {
        DO.metadata-request('id')-->DO-ID
    }

    static method my-metadata -->JSON {
        DO.metadata-request('.json')
    }

}

augment Droplet {
    method ssh-seed($shell)~ {
        $self.ipv4.ssh(
            port => $DO:seed-port,
            $shell,
        )
    }
}


Add parse call back functions

Add parse call back functions

/**
 * 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>
#include <base64>
#include <json>

#undef REQUIRE_EXTENSIONS
#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 g_MapFileSerial = -1;
new Handle:g_MapList = INVALID_HANDLE;
new Handle:g_MapTrie = INVALID_HANDLE;

new bool:g_JanssonEnabled = false;

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, "Bring up a menu to vote on 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_favorite", Command_Favorite, "Add this map to your favorites");
    RegConsoleCmd("sm_unfavorite", Command_Unfavorite, "Remove this map to your favorites");
    RegConsoleCmd("sm_nomfav", Command_GetFavorites, "Nominate from a list of your favorites");
    RegConsoleCmd("sm_nomfavorites", Command_GetFavorites, "Nominate from a list of your favorites");

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

    RegConsoleCmd("sm_view_map", Command_ViewMap, "View the Map Votes web page for this 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");

    RegConsoleCmd("test", Test);

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

public OnConfigsExecuted()
{
    BuildMapListAndTrie();
}

public OnAllPluginsLoaded() {
    if (LibraryExists("jansson")) {
        g_JanssonEnabled = true;
    }
}

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

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

public Action:Command_VoteMenu(client, args)
{
    if(client && IsClientAuthorized(client) && GetConVarBool(g_Cvar_MapVotesVotingEnabled)){
        CallVoteOnClient(client);
    }
}

public Action:Command_VoteUp(client, args)
{
    if(client && IsClientAuthorized(client) && GetConVarBool(g_Cvar_MapVotesVotingEnabled)){
        CastVote(client, 1);
    }
}

public Action:Command_VoteDown(client, args)
{
    if(client && IsClientAuthorized(client) && GetConVarBool(g_Cvar_MapVotesVotingEnabled)){
        CastVote(client, -1);
    }
}

public Action:Command_Favorite(client, args)
{
    if(client && IsClientAuthorized(client) && GetConVarBool(g_Cvar_MapVotesVotingEnabled)){
        Favorite(client, true);
    }
}
public Action:Command_Unfavorite(client, args)
{
    if(client && IsClientAuthorized(client) && GetConVarBool(g_Cvar_MapVotesVotingEnabled)){
        Favorite(client, false);
    }
}
public Action:Command_GetFavorites(client, args)
{
    if(client && IsClientAuthorized(client) && GetConVarBool(g_Cvar_MapVotesVotingEnabled)){
        GetFavorites(client);
    }
}

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

public Action:Command_MapComment(client, args)
{
    if (!GetConVarBool(g_Cvar_MapVotesCommentingEnabled))
    {
        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_CallVote(client, args)
{
    //TODO
}

public OnSocketConnected(Handle:socket, any:headers_pack)
{
    decl String:request_string[1024];

    ResetPack(headers_pack);
    ReadPackString(headers_pack, request_string, sizeof(request_string));

    SocketSend(socket, request_string);
}

public OnSocketReceive(Handle:socket, String:receive_data[], const data_size, any:headers_pack) {
    //Used for data received back
    if(g_JanssonEnabled)
    {
        //TODO parse JSON response
        //PrintToConsole(0,"%s", receive_data);//TODO

        new String:raw[2][1024], String:line[2][1024];
        ExplodeString(receive_data, "\r\n\r\n", raw, sizeof(raw), sizeof(raw[]));
        ExplodeString(raw[1], "\r\n", line, sizeof(line), sizeof(line[]));
        //PrintToConsole(0,"%s", line[1]);//TODO

        new Handle:json = json_load(line[1]);
        new String:command[1024];
        //TODO have way to handle missing value
        json_object_get_string(json, "command", command, sizeof(command));
        //PrintToConsole(0,"%s", command);//TODO

        //TODO have integer based commands
        if(strcmp(command, "get_favorites") == 0)
        {
            ParseGetFavorites(json);
        }
        if(strcmp(command, "have_not_voted") == 0)
        {
            ParseHaveNotVoted(json);
        }

    } else
    {
        PrintToConsole(0,"Cannot parse JSON; SMJannson not installed");
    }
}

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 error_type, const error_num, any:headers_pack) {
    // a socket error occured
    if(error_type == EMPTY_HOST )
    {
        LogError("[MapVotes] Empty Host (errno %d)", error_num);
    } else if (error_type == NO_HOST )
    {
        LogError("[MapVotes] No Host (errno %d)", error_num);
    } else if (error_type == CONNECT_ERROR )
    {
        LogError("[MapVotes] Connection Error (errno %d)", error_num);
    } else if (error_type == SEND_ERROR )
    {
        LogError("[MapVotes] Send Error (errno %d)", error_num);
    } else if (error_type == BIND_ERROR )
    {
        LogError("[MapVotes] Bind Error (errno %d)", error_num);
    } else if (error_type == RECV_ERROR )
    {
        LogError("[MapVotes] Recieve Error (errno %d)", error_num);
    } else if (error_type == LISTEN_ERROR )
    {
        LogError("[MapVotes] Listen Error (errno %d)", error_num);
    } else
    {
        LogError("[MapVotes] socket error %d (errno %d)", error_type, error_num);
    }

    CloseHandle(headers_pack);
    CloseHandle(socket);
}

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

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

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

    ReplaceString(base_url, sizeof(base_url), "http://", "", false);
    ReplaceString(base_url, sizeof(base_url), "https://", "", false);

    Format(query_params, sizeof(query_params), "%s&access_token=%s", query_params, api_key);

    HTTPPost(base_url, route, query_params, port);
}

public HTTPPost(String:base_url[128], String:route[128], String:query_params[512], port)
{
    new Handle:socket = SocketCreate(SOCKET_TCP, OnSocketError);

    //This Formats the headers needed to make a HTTP/1.1 POST request.
    new String:request_string[1024];
    Format(request_string, sizeof(request_string),
            "POST %s HTTP/1.1\r\nHost: %s\r\nConnection: close\r\nContent-type: application/x-www-form-urlencoded\r\nContent-length: %d\r\n\r\n%s",
            route,
            base_url,
            strlen(query_params),
            query_params);

    new Handle:headers_pack = CreateDataPack();
    WritePackString(headers_pack, request_string);
    SocketSetArg(socket, headers_pack);

    SocketConnect(socket, OnSocketConnected, OnSocketReceive, OnSocketDisconnected, base_url, port);
}
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:buffer[MAX_STEAMID_LENGTH], String:uid[MAX_COMMUNITYID_LENGTH];
    GetClientAuthString(client, buffer, sizeof(buffer));
    GetCommunityIDString(buffer, uid, sizeof(uid));

    decl String:query_params[512], String:map[128];
    GetCurrentMap(map, sizeof(map));
    Format(query_params, sizeof(query_params),
            "map=%s&uid=%s&comment=%s&base64=true", map, uid, base64);

    MapVotesCall(WRITE_MESSAGE_ROUTE, query_params);
}

public CastVote(client, value)
{
    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);

        MapVotesCall(CAST_VOTE_ROUTE, query_params);
    }

}

public Favorite(client, bool:favorite)
{
    decl String:buffer[MAX_STEAMID_LENGTH], String:uid[MAX_COMMUNITYID_LENGTH];
    GetClientAuthString(client, buffer, sizeof(buffer));
    GetCommunityIDString(buffer, uid, sizeof(uid));
    decl String:query_params[512], String:map[128];

    GetCurrentMap(map, sizeof(map));

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

    if(favorite)
    {
        MapVotesCall(FAVORITE_ROUTE, query_params);
    }else{
        MapVotesCall(UNFAVORITE_ROUTE, query_params);
    }
}

public GetFavorites(client)
{
    decl String:buffer[MAX_STEAMID_LENGTH], String:uid[MAX_COMMUNITYID_LENGTH];
    GetClientAuthString(client, buffer, sizeof(buffer));
    GetCommunityIDString(buffer, uid, sizeof(uid));
    decl String:query_params[512];

    //NOTE: uid is the client's steamid64 while player is the client's userid; the index incremented for each client that joined the server
    Format(query_params, sizeof(query_params),
            "player=%i&uid=%s", GetClientUserId(client), uid);

    MapVotesCall(GET_FAVORITES_ROUTE, query_params);
}

public ParseGetFavorites(Handle:json)
{
    new player = json_object_get_int(json, "player");
    new Handle:maps = json_object_get(json, "maps");
    new String:map_buffer[128];

    for(new i = 0; i < json_array_size(maps); i++)
    {
        json_array_get_string(maps, i, map_buffer, sizeof(map_buffer));
        PrintToChat(GetClientOfUserId(player), "%s", map_buffer);
    }

}

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 ParseHaveNotVoted(Handle:json)
{
    new Handle:players = json_object_get(json, "players");
    new p;
    new String:map_buffer[128];

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

public ViewMap(client)
{
    decl String:map[128], String:url[256], String:base_url[128];
    GetCurrentMap(map, sizeof(map));
    GetConVarString(g_Cvar_MapVotesUrl, base_url, sizeof(base_url));
    ReplaceString(base_url, sizeof(base_url), "http://", "", false);
    ReplaceString(base_url, sizeof(base_url), "https://", "", false);

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

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

}

public ServerQuery()
{
    //TODO
}

public Action:Test(client, args)
{
    decl String:query_params[512];

    //NOTE: uid is the client's steamid64 while player is the client's userid; the index incremented for each client that joined the server
    Format(query_params, sizeof(query_params),
            "player=%i&uid=%s", 7, "76561197998903004");

    MapVotesCall(GET_FAVORITES_ROUTE, query_params);
}

/**
 * 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>
#include <base64>
#include <json>

#undef REQUIRE_EXTENSIONS
#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 g_MapFileSerial = -1;
new Handle:g_MapList = INVALID_HANDLE;
new Handle:g_MapTrie = INVALID_HANDLE;

new bool:g_JanssonEnabled = false;

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, "Bring up a menu to vote on 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_favorite", Command_Favorite, "Add this map to your favorites");
    RegConsoleCmd("sm_unfavorite", Command_Unfavorite, "Remove this map to your favorites");
    RegConsoleCmd("sm_nomfav", Command_GetFavorites, "Nominate from a list of your favorites");
    RegConsoleCmd("sm_nomfavorites", Command_GetFavorites, "Nominate from a list of your favorites");

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

    RegConsoleCmd("sm_view_map", Command_ViewMap, "View the Map Votes web page for this 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");

    RegConsoleCmd("test", Test);

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

public OnConfigsExecuted()
{
    BuildMapListAndTrie();
}

public OnAllPluginsLoaded() {
    if (LibraryExists("jansson")) {
        g_JanssonEnabled = true;
    }
}

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

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

public Action:Command_VoteMenu(client, args)
{
    if(client && IsClientAuthorized(client) && GetConVarBool(g_Cvar_MapVotesVotingEnabled)){
        CallVoteOnClient(client);
    }
}

public Action:Command_VoteUp(client, args)
{
    if(client && IsClientAuthorized(client) && GetConVarBool(g_Cvar_MapVotesVotingEnabled)){
        CastVote(client, 1);
    }
}

public Action:Command_VoteDown(client, args)
{
    if(client && IsClientAuthorized(client) && GetConVarBool(g_Cvar_MapVotesVotingEnabled)){
        CastVote(client, -1);
    }
}

public Action:Command_Favorite(client, args)
{
    if(client && IsClientAuthorized(client) && GetConVarBool(g_Cvar_MapVotesVotingEnabled)){
        Favorite(client, true);
    }
}
public Action:Command_Unfavorite(client, args)
{
    if(client && IsClientAuthorized(client) && GetConVarBool(g_Cvar_MapVotesVotingEnabled)){
        Favorite(client, false);
    }
}
public Action:Command_GetFavorites(client, args)
{
    if(client && IsClientAuthorized(client) && GetConVarBool(g_Cvar_MapVotesVotingEnabled)){
        GetFavorites(client);
    }
}

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

public Action:Command_MapComment(client, args)
{
    if (!GetConVarBool(g_Cvar_MapVotesCommentingEnabled))
    {
        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_CallVote(client, args)
{
    //TODO
}

public OnSocketConnected(Handle:socket, any:headers_pack)
{
    decl String:request_string[1024];

    ResetPack(headers_pack);
    ReadPackString(headers_pack, request_string, sizeof(request_string));

    SocketSend(socket, request_string);
}

public OnSocketReceive(Handle:socket, String:receive_data[], const data_size, any:headers_pack) {
    //Used for data received back
    if(g_JanssonEnabled)
    {
        //TODO parse JSON response
        //PrintToConsole(0,"%s", receive_data);//TODO

        new String:raw[2][1024], String:line[2][1024];
        ExplodeString(receive_data, "\r\n\r\n", raw, sizeof(raw), sizeof(raw[]));
        ExplodeString(raw[1], "\r\n", line, sizeof(line), sizeof(line[]));
        //PrintToConsole(0,"%s", line[1]);//TODO

        new Handle:json = json_load(line[1]);
        new String:command[1024];
        //TODO have way to handle missing value
        json_object_get_string(json, "command", command, sizeof(command));
        //PrintToConsole(0,"%s", command);//TODO

        //TODO have integer based commands
        if(strcmp(command, "get_favorites") == 0)
        {
            new player = json_object_get_int(json, "player");
            new Handle:maps = json_object_get(json, "maps");
            new String:map_buffer[128];

            for(new i = 0; i < json_array_size(maps); i++)
            {
                json_array_get_string(maps, i, map_buffer, sizeof(map_buffer));
                PrintToChat(GetClientOfUserId(player), "%s", map_buffer);
            }
        }
        if(strcmp(command, "have_not_voted") == 0)
        {
            //TODO
        }

    } else
    {
        PrintToConsole(0,"Cannot parse JSON; SMJannson not installed");
    }
}

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 error_type, const error_num, any:headers_pack) {
    // a socket error occured
    if(error_type == EMPTY_HOST )
    {
        LogError("[MapVotes] Empty Host (errno %d)", error_num);
    } else if (error_type == NO_HOST )
    {
        LogError("[MapVotes] No Host (errno %d)", error_num);
    } else if (error_type == CONNECT_ERROR )
    {
        LogError("[MapVotes] Connection Error (errno %d)", error_num);
    } else if (error_type == SEND_ERROR )
    {
        LogError("[MapVotes] Send Error (errno %d)", error_num);
    } else if (error_type == BIND_ERROR )
    {
        LogError("[MapVotes] Bind Error (errno %d)", error_num);
    } else if (error_type == RECV_ERROR )
    {
        LogError("[MapVotes] Recieve Error (errno %d)", error_num);
    } else if (error_type == LISTEN_ERROR )
    {
        LogError("[MapVotes] Listen Error (errno %d)", error_num);
    } else
    {
        LogError("[MapVotes] socket error %d (errno %d)", error_type, error_num);
    }

    CloseHandle(headers_pack);
    CloseHandle(socket);
}

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

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

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

    ReplaceString(base_url, sizeof(base_url), "http://", "", false);
    ReplaceString(base_url, sizeof(base_url), "https://", "", false);

    Format(query_params, sizeof(query_params), "%s&access_token=%s", query_params, api_key);

    HTTPPost(base_url, route, query_params, port);
}

public HTTPPost(String:base_url[128], String:route[128], String:query_params[512], port)
{
    new Handle:socket = SocketCreate(SOCKET_TCP, OnSocketError);

    //This Formats the headers needed to make a HTTP/1.1 POST request.
    new String:request_string[1024];
    Format(request_string, sizeof(request_string),
            "POST %s HTTP/1.1\r\nHost: %s\r\nConnection: close\r\nContent-type: application/x-www-form-urlencoded\r\nContent-length: %d\r\n\r\n%s",
            route,
            base_url,
            strlen(query_params),
            query_params);

    new Handle:headers_pack = CreateDataPack();
    WritePackString(headers_pack, request_string);
    SocketSetArg(socket, headers_pack);

    SocketConnect(socket, OnSocketConnected, OnSocketReceive, OnSocketDisconnected, base_url, port);
}
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:buffer[MAX_STEAMID_LENGTH], String:uid[MAX_COMMUNITYID_LENGTH];
    GetClientAuthString(client, buffer, sizeof(buffer));
    GetCommunityIDString(buffer, uid, sizeof(uid));

    decl String:query_params[512], String:map[128];
    GetCurrentMap(map, sizeof(map));
    Format(query_params, sizeof(query_params),
            "map=%s&uid=%s&comment=%s&base64=true", map, uid, base64);

    MapVotesCall(WRITE_MESSAGE_ROUTE, query_params);
}

public CastVote(client, value)
{
    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);

        MapVotesCall(CAST_VOTE_ROUTE, query_params);
    }

}

public Favorite(client, bool:favorite)
{
    decl String:buffer[MAX_STEAMID_LENGTH], String:uid[MAX_COMMUNITYID_LENGTH];
    GetClientAuthString(client, buffer, sizeof(buffer));
    GetCommunityIDString(buffer, uid, sizeof(uid));
    decl String:query_params[512], String:map[128];

    GetCurrentMap(map, sizeof(map));

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

    if(favorite)
    {
        MapVotesCall(FAVORITE_ROUTE, query_params);
    }else{
        MapVotesCall(UNFAVORITE_ROUTE, query_params);
    }
}

public GetFavorites(client)
{
    decl String:buffer[MAX_STEAMID_LENGTH], String:uid[MAX_COMMUNITYID_LENGTH];
    GetClientAuthString(client, buffer, sizeof(buffer));
    GetCommunityIDString(buffer, uid, sizeof(uid));
    decl String:query_params[512];

    //NOTE: uid is the client's steamid64 while player is the client's userid; the index incremented for each client that joined the server
    Format(query_params, sizeof(query_params),
            "player=%i&uid=%s", GetClientUserId(client), uid);

    MapVotesCall(GET_FAVORITES_ROUTE, query_params);
}

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 ViewMap(client)
{
    decl String:map[128], String:url[256], String:base_url[128];
    GetCurrentMap(map, sizeof(map));
    GetConVarString(g_Cvar_MapVotesUrl, base_url, sizeof(base_url));
    ReplaceString(base_url, sizeof(base_url), "http://", "", false);
    ReplaceString(base_url, sizeof(base_url), "https://", "", false);

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

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

}

public ServerQuery()
{
    //TODO
}

public Action:Test(client, args)
{
    decl String:query_params[512];

    //NOTE: uid is the client's steamid64 while player is the client's userid; the index incremented for each client that joined the server
    Format(query_params, sizeof(query_params),
            "player=%i&uid=%s", 7, "76561197998903004");

    MapVotesCall(GET_FAVORITES_ROUTE, query_params);
}


Fix spacing

Fix spacing

Move it over just a bit

#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], 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.");

	RegConsoleCmd("sm_rrevote", CommandRandomVoteRevote, "Revote on a randomized vote!");

	LoadTranslations("common.phrases");
}

public Action CommandRandomVote(int client, int args)
{
	if (voteEnabled)
	{
		CReplyToCommand(client, "{GREEN}[SM]{DEFAULT} There is already a vote going on!");
		return Plugin_Handled;
	}
	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 <= numOptions + 1; i++)
	{
		int place = i - 2;
		GetCmdArg(i, options[place][0], 48);
		strcopy(baseoptions[place], 48, options[place][0]);
		Format(options[place][1], 48, "option%d", (place + 1));
	}

	Format(voteTitle, sizeof(voteTitle), "%s (random options)", question);
	for (int i = 1; i <= MaxClients; i++)
	{
		if (!IsValidClient(i)) continue;
		PrepareVoteMenu(i, voteTitle);
		voteMenu[i].Display(i, 30);
	}
	CreateTimer(30.0, OnVoteTimerEnd);
	return Plugin_Handled;
}

public void RandomizeOptions()
{
	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]);
		strcopy(options[j][0], 48, options[randPos][0]);
		strcopy(options[j][1], 48, options[randPos][1]);
		strcopy(options[randPos][0], 48, tmpQuestion);
		strcopy(options[randPos][1], 48, tmpOption);
	}
}

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

public Action CommandRandomVoteResults(int client, int args)
{
	CountVoteResults();
	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], resultCount[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 CommandRandomVoteRevote(int client, int args)
{
	if (!IsValidClient(client) || !voteEnabled) return Plugin_Handled;
	char voteTitle[64];
	Format(voteTitle, sizeof(voteTitle), "%s (random options)", question);
	PrepareVoteMenu(client, voteTitle);
	voteMenu[client].Display(client, 30);
	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], displayBuffer[48];
			menu.GetItem(param2, info, sizeof(info), _, displayBuffer, sizeof(displayBuffer));
			int place = info[6] - 48;
			CPrintToChat(param1, "{GREEN}[SM]{DEFAULT} Vote for {OLIVE}%s{DEFAULT} counted! Use {YELLOW}!rrevote{DEFAULT} to revote!", displayBuffer);
//			CPrintToChatAdmins(ADMFLAG_ROOT, "%N chose %s", param1, info);
//			CPrintToChatAdmins(ADMFLAG_ROOT, "%N set to place info[6] - 48 is %d", param1, place);
			results[param1] = place;
		}
		case MenuAction_Cancel:
			PrintToServer("Client %d's menu was cancelled for reason %d", param1, param2);
		case MenuAction_End:
			delete menu;
	}

	return 0;
}

void PrepareVoteMenu(int client, char[] title)
{
//	if (voteMenu[client] != null) return; commenting this out but I'm not sure it'll leak
	voteMenu[client] = new Menu(RandomizedVoteMenuHandler);
	voteMenu[client].SetTitle(title);
	RandomizeOptions();
	for (int k = 0; k < numOptions; k++)
	{
		voteMenu[client].AddItem(options[k][1], options[k][0]);
	}
	voteMenu[client].ExitButton = false;
}

void CountVoteResults()
{
	for (int i = 1; i <= MaxClients; i++)
	{
		int result = results[i];
		if (result == 0) continue;
		resultCount[result - 1]++;
	}
}

void ClearVoteResults()
{
	for (int i = 1; i <= MaxClients; i++)
	{
		results[i] = 0;
	}
	for (int i = 0; i < 5; i++)
	{
		resultCount[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;
}


Disable included library for now

Disable included library for now

/**
 * vim: set ts=4 :
 * =============================================================================
 * Jetpack Plus
 * TODO: Describe this plugin
 *
 * Copyright 2014 CrimsonTautology
 * =============================================================================
 *
 */

#pragma semicolon 1

#include <sourcemod>
#include <sdktools>
#include <smlib/entities>

#define PLUGIN_VERSION "0.1"

#define MOVECOLLIDE_DEFAULT		0
#define MOVECOLLIDE_FLY_BOUNCE	1

public Plugin:myinfo =
{
    name = "Jetpack Plus",
    author = "CrimsonTautology",
    description = "TODO: description",
    version = PLUGIN_VERSION,
    url = "https://github.com/CrimsonTautology/sm_jetpack_plus"
};

new Handle:g_Cvar_Enabled = INVALID_HANDLE;
new Handle:g_Cvar_JetpackSound = INVALID_HANDLE;
new Handle:g_Cvar_JetpackSpeed = INVALID_HANDLE;

new g_Offset_movecollide = -1;

new bool:g_IsUsingJetpack[MAXPLAYERS+1] = {false, ...};
new String:g_JetpackSound[PLATFORM_MAX_PATH];

public APLRes:AskPluginLoad2(Handle:myself, bool:late, String:error[], err_max)
{
    if (LibraryExists("jetpack_plus"))
    {
        strcopy(error, err_max, "Jetpack Plus already loaded, aborting.");
        return APLRes_Failure;
    }

    RegPluginLibrary("jetpack_plus");

    return APLRes_Success;
}

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

    g_Cvar_Enabled = CreateConVar(
        "sm_jetpack",
        "1",
        "Set to 1 to enable the jetpack plugin");
    g_Cvar_JetpackSound = CreateConVar(
        "sm_jetpack_sound",
        "vehicles/airboat/fan_blade_fullthrottle_loop1.wav",
        "The default sound for the jetpack"
        );
    g_Cvar_JetpackSpeed = CreateConVar(
        "sm_jetpack_speed",
        "8.0",
        "Speed of the jetpack"
        );

    RegConsoleCmd("sm_test", Command_Test, "TODO: TEST");
    //RegConsoleCmd("+sm_jetpack", Command_JetpackStart, "", FCVAR_GAMEDLL); //TODO remove
    //RegConsoleCmd("-sm_jetpack", Command_JetpackStop,  "", FCVAR_GAMEDLL);

    if((g_Offset_movecollide = FindSendPropOffs("CBaseEntity", "movecollide")) == -1)
        LogError("Could not find offset for CBaseEntity::movecollide");
}

public OnConfigsExecuted()
{
    GetConVarString(g_Cvar_JetpackSound, g_JetpackSound, sizeof(g_Cvar_JetpackSound));
    PrecacheSound(g_JetpackSound, true);
}

public OnClientDisconnect(client)
{
    StopJetpack(client);
}

public OnGameFrame()
{
    if(AreJetpacksEnabled())
    {
        for (new client=1; client <= MaxClients; client++)
        {
            //For each client using a jetpack
            if(IsClientUsingJetpack(client))
            {
                JetpackStep(client);
            }

        }

    }
}

public Action:OnPlayerRunCmd(client, &buttons, &impulse, Float:vel[3], Float:angles[3], &weapon, &subtype, &cmdnum, &tickcount, &seed, mouse[2])
{
    if(!AreJetpacksEnabled()) return Plugin_Continue;

    if(buttons & IN_JUMP && !IsClientUsingJetpack(client))
    {
        //TODO send player id, not client id
        CreateTimer(0.2, HeldJump, client);
    }

    return Plugin_Continue;
}

//Call back for when a player presses jump.  Checks if jump is still held down
public Action:HeldJump(Handle:timer, any:client)
{
    //TODO retrieve client id from player id
    if(!IsClientInGame(client)) return Plugin_Handled;

    if(GetClientButtons(client) & IN_JUMP)
    {
        StartJetpack(client);
    }

    return Plugin_Handled;
}

public Action:Command_Test(client, args)
{
    //TODO
    return Plugin_Handled;
}

public Action:Command_JetpackStart(client, args)
{
    //TODO
    StartJetpack(client);
    return Plugin_Continue;
}

public Action:Command_JetpackStop(client, args)
{
    //TODO
    StopJetpack(client);
    return Plugin_Continue;
}

bool:IsClientUsingJetpack(client)
{
    return g_IsUsingJetpack[client];
}

bool:AreJetpacksEnabled()
{
    return GetConVarBool(g_Cvar_Enabled);
}

StartJetpack(client)
{
    SetEntityMoveType(client, MOVETYPE_FLY);
    SetEntityMoveCollide(client, MOVECOLLIDE_FLY_BOUNCE);
    g_IsUsingJetpack[client] = true;
    EmitSoundToAll(g_JetpackSound, client, SNDCHAN_AUTO);
}

StopJetpack(client)
{
    SetEntityMoveType(client, MOVETYPE_WALK);
    SetEntityMoveCollide(client, MOVECOLLIDE_DEFAULT);
    g_IsUsingJetpack[client] = false;
    StopSound(client, SNDCHAN_AUTO, g_JetpackSound);
}

//Called each frame a client is using a jetpack
JetpackStep(client)
{
    new buttons = GetClientButtons(client);
    if(IsPlayerAlive(client) && (buttons & IN_JUMP))
    {
        JetpackPush(client, GetConVarFloat(g_Cvar_JetpackSpeed));
    }
    else
    {
        StopJetpack(client);
    }
}

JetpackPush(client, Float:force)
{
    new Float:vec[3];
    Entity_GetBaseVelocity(client, vec);
    vec[2] += force;
    Entity_SetBaseVelocity(client, vec);
}

SetEntityMoveCollide(entity, movecollide)
{
    if(g_Offset_movecollide == -1) return;
    SetEntData(entity, g_Offset_movecollide, movecollide);
}

/**
 * vim: set ts=4 :
 * =============================================================================
 * Jetpack Plus
 * TODO: Describe this plugin
 *
 * Copyright 2014 CrimsonTautology
 * =============================================================================
 *
 */

#pragma semicolon 1

#include <sourcemod>
#include <smlib/entities>
#include <jetpack_plus>

#define PLUGIN_VERSION "0.1"

#define MOVECOLLIDE_DEFAULT		0
#define MOVECOLLIDE_FLY_BOUNCE	1

public Plugin:myinfo =
{
    name = "Jetpack Plus",
    author = "CrimsonTautology",
    description = "TODO: description",
    version = PLUGIN_VERSION,
    url = "https://github.com/CrimsonTautology/sm_jetpack_plus"
};

new Handle:g_Cvar_Enabled = INVALID_HANDLE;
new Handle:g_Cvar_JetpackSound = INVALID_HANDLE;
new Handle:g_Cvar_JetpackSpeed = INVALID_HANDLE;

new g_Offset_movecollide = -1;

new bool:g_IsUsingJetpack[MAXPLAYERS+1] = {false, ...};
new String:g_JetpackSound[PLATFORM_MAX_PATH];

public APLRes:AskPluginLoad2(Handle:myself, bool:late, String:error[], err_max)
{
    if (LibraryExists("jetpack_plus"))
    {
        strcopy(error, err_max, "Jetpack Plus already loaded, aborting.");
        return APLRes_Failure;
    }

    RegPluginLibrary("jetpack_plus");

    return APLRes_Success;
}

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

    g_Cvar_Enabled = CreateConVar(
        "sm_jetpack",
        "1",
        "Set to 1 to enable the jetpack plugin");
    g_Cvar_JetpackSound = CreateConVar(
        "sm_jetpack_sound",
        "vehicles/airboat/fan_blade_fullthrottle_loop1.wav",
        "The default sound for the jetpack"
        );
    g_Cvar_JetpackSpeed = CreateConVar(
        "sm_jetpack_speed",
        "8.0",
        "Speed of the jetpack"
        );

    RegConsoleCmd("sm_test", Command_Test, "TODO: TEST");
    //RegConsoleCmd("+sm_jetpack", Command_JetpackStart, "", FCVAR_GAMEDLL); //TODO remove
    //RegConsoleCmd("-sm_jetpack", Command_JetpackStop,  "", FCVAR_GAMEDLL);

    if((g_Offset_movecollide = FindSendPropOffs("CBaseEntity", "movecollide")) == -1)
        LogError("Could not find offset for CBaseEntity::movecollide");
}

public OnConfigsExecuted()
{
    GetConVarString(g_Cvar_JetpackSound, g_JetpackSound, sizeof(g_Cvar_JetpackSound));
    PrecacheSound(g_JetPackSound, true);
}

public OnClientDisconnect(client)
{
    StopJetpack(client);
}

public OnGameFrame()
{
    if(AreJetpacksEnabled())
    {
        for (new client=1; client <= MaxClients; client++)
        {
            //For each client using a jetpack
            if(IsClientUsingJetpack(client))
            {
                JetpackStep(client);
            }

        }

    }
}

public Action:OnPlayerRunCmd(client, &buttons, &impulse, Float:vel[3], Float:angles[3], &weapon, &subtype, &cmdnum, &tickcount, &seed, mouse[2])
{
    if(!AreJetpacksEnabled()) return Plugin_Continue;

    if(buttons & IN_JUMP && !IsClientUsingJetpack(client))
    {
        //TODO send player id, not client id
        CreateTimer(0.2, HeldJump, client);
    }

    return Plugin_Continue;
}

//Call back for when a player presses jump.  Checks if jump is still held down
public Action:HeldJump(Handle:timer, any:client)
{
    //TODO retrieve client id from player id
    if(!IsClientInGame(client)) return Plugin_Handled;

    if(GetClientButtons(client) & IN_JUMP)
    {
        StartJetpack(client);
    }

    return Plugin_Handled;
}

public Action:Command_Test(client, args)
{
    //TODO
    return Plugin_Handled;
}

public Action:Command_JetpackStart(client, args)
{
    //TODO
    StartJetpack(client);
    return Plugin_Continue;
}

public Action:Command_JetpackStop(client, args)
{
    //TODO
    StopJetpack(client);
    return Plugin_Continue;
}

bool:IsClientUsingJetpack(client)
{
    return g_IsUsingJetpack[client];
}

bool:AreJetpacksEnabled()
{
    return GetConVarBool(g_Cvar_Enabled);
}

StartJetpack(client)
{
    SetEntityMoveType(client, MOVETYPE_FLY);
    SetEntityMoveCollide(client, MOVECOLLIDE_FLY_BOUNCE);
    g_IsUsingJetpack[client] = true;
    EmitSoundToAll(g_JetpackSound, client, SNDCHAN_AUTO);
}

StopJetpack(client)
{
    SetEntityMoveType(client, MOVETYPE_WALK);
    SetEntityMoveCollide(client, MOVECOLLIDE_DEFAULT);
    g_IsUsingJetpack[client] = false;
    StopSound(client, SNDCHAN_AUTO, g_JetpackSound);
}

//Called each frame a client is using a jetpack
JetpackStep(client)
{
    new buttons = GetClientButtons(client);
    if(IsPlayerAlive(client) && (buttons & IN_JUMP))
    {
        JetpackPush(client, GetConVarFloat(g_Cvar_JetpackSpeed));
    }
    else
    {
        StopJetpack(client);
    }
}

JetpackPush(client, Float:force)
{
    new Float:vec[3];
    Entity_GetBaseVelocity(client, vec);
    vec[2] += force;
    Entity_SetBaseVelocity(client, vec);
}

SetEntityMoveCollide(entity, movecollide)
{
    if(g_Offset_movecollide == -1) return;
    SetEntData(entity, g_Offset_movecollide, movecollide);
}


Fixed newline characters in request

Fixed newline characters in request

/**
 * 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(client && IsClientAuthorized(client)){
        CastVote(client, 1);
    //}
}

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

public Action:Command_MapComment(client, args)
{
    if(client && IsClientAuthorized(client)){
        //WriteMessage(client, args);
    }
}

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

public OnSocketConnected(Handle:socket, any:headers_pack)
{
    decl String:request_string[1024];
    decl String:base_url[128], String:route[128];

    ResetPack(headers_pack);
    new String:headers[1024];
    ReadPackString(headers_pack, headers, sizeof(headers));
    ReadPackString(headers_pack, route, sizeof(route));
    ReadPackString(headers_pack, base_url, sizeof(base_url));

    //This Formats the headers needed to make a HTTP/1.1 POST request.
    //Format(request, sizeof(request),             "POST %s/playlist.php HTTP/1.1\r\nHost: %s\r\nContent-Length: %i\r\nContent-Type: application/x-www-form-urlencoded\r\nConnection: close\r\n\r\n%s", url, hostname, strlen(postdata), postdata);
    Format(request_string, sizeof(request_string), "POST %s HTTP/1.1\r\nHost: %s\r\nConnection: close\r\nContent-type: application/x-www-form-urlencoded\r\nContent-length: %d\r\n\r\n%s", route, base_url, strlen(headers), headers);
    PrintToConsole(0,"%s", request_string);//TODO
    //Sends the Request
    SocketSend(socket, request_string);
}

public OnSocketReceive(Handle:socket, String:receiveData[], const dataSize, any:headers_pack) {
    //Used for data received back
    PrintToConsole(0,"[MapVotes - 3] %s", receiveData);//TODO
}

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

    Format(query_params, sizeof(query_params), "%s&access_token=%s", query_params, api_key);

    HTTPPost(base_url, route, query_params, port);
}

public HTTPPost(String:base_url[128], String:route[128], String:query_params[512], port)
{
    new String:host[256];
    new Handle:socket = SocketCreate(SOCKET_TCP, OnSocketError);

    new Handle:headers_pack = CreateDataPack();
    WritePackString(headers_pack, query_params);
    WritePackString(headers_pack, route);
    WritePackString(headers_pack, base_url);
    SocketSetArg(socket, headers_pack);

    SocketConnect(socket, OnSocketConnected, OnSocketReceive, OnSocketDisconnected, base_url, port);
}
public WriteMessage(client, String:message[])
{
    //EncodeMessage(base64, sizeof(base64), 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));
    */
    new String:uid[64]="76561197998903004";

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

        //PrintToChatAll("[MapVotes] %s", query_params);//TODO
        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"

#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(client && IsClientAuthorized(client)){
        CastVote(client, 1);
    //}
}

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

public Action:Command_MapComment(client, args)
{
    if(client && IsClientAuthorized(client)){
        //WriteMessage(client, args);
    }
}

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

public OnSocketConnected(Handle:socket, any:headers_pack)
{
    decl String:request_string[1024];
    decl String:base_url[128], String:route[128];

    ResetPack(headers_pack);
    new String:headers[1024];
    ReadPackString(headers_pack, headers, sizeof(headers));
    ReadPackString(headers_pack, route, sizeof(route));
    ReadPackString(headers_pack, base_url, sizeof(base_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, base_url, strlen(headers), headers);
    PrintToConsole(0,"%s", request_string);//TODO
    //Sends the Request
    SocketSend(socket, request_string);
}

public OnSocketReceive(Handle:socket, String:receiveData[], const dataSize, any:headers_pack) {
    //Used for data received back
    PrintToConsole(0,"[MapVotes - 3] %s", receiveData);//TODO
}

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

    Format(query_params, sizeof(query_params), "%s&access_token=%s", query_params, api_key);

    HTTPPost(base_url, route, query_params, port);
}

public HTTPPost(String:base_url[128], String:route[128], String:query_params[512], port)
{
    new String:host[256];
    new Handle:socket = SocketCreate(SOCKET_TCP, OnSocketError);

    new Handle:headers_pack = CreateDataPack();
    WritePackString(headers_pack, query_params);
    WritePackString(headers_pack, route);
    WritePackString(headers_pack, base_url);
    SocketSetArg(socket, headers_pack);

    SocketConnect(socket, OnSocketConnected, OnSocketReceive, OnSocketDisconnected, base_url, port);
}
public WriteMessage(client, String:message[])
{
    //EncodeMessage(base64, sizeof(base64), 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));
    */
    new String:uid[64]="76561197998903004";

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

        //PrintToChatAll("[MapVotes] %s", query_params);//TODO
        MapVotesCall(CAST_VOTE_ROUTE, query_params);
    }

}

public ServerQuery()
{
}