Compare commits

...

26 Commits
1 ... release

Author SHA1 Message Date
e5a9b6557d Add TrueName attribute to users, ignores nickname 2016-11-22 23:27:37 +00:00
09830a6736 Remove unused travis.yml 2016-11-14 00:45:16 +00:00
bc63dabb81 Missed one rename 2016-11-14 00:44:57 +00:00
a802e6ae38 Rename (trivia-bot -> Toast) 2016-11-14 00:40:02 +00:00
301d99ca2f Fix bad role name checking code 2016-08-19 17:47:07 +01:00
b68ac1d3b2 Merge pull request #8 from jackb-p/develop
Merge Develop branch
2016-08-19 17:41:54 +01:00
a365456dd8 Update README.md 2016-08-19 17:41:33 +01:00
0498b22c8c Apply same permissions from ~createjs to ~js 2016-08-19 17:30:00 +01:00
c566e7f04f Update build status image 2016-08-16 01:58:24 +01:00
04565187a2 Update README.md 2016-08-16 01:34:17 +01:00
c9af4566c6 Merge pull request #7 from jackb-p/v8-integration
V8 Integration
2016-08-16 01:01:52 +01:00
9c7e4f5a6d Squash bugs and warnings, optimise CMake builds. 2016-08-15 21:12:47 +02:00
4b2d0ee50a Add config, neaten up some things 2016-08-15 17:47:34 +01:00
da93eda0be Fix shutting down, other minor cleanups 2016-08-14 17:46:29 +01:00
8ebe6c2c4e Squash some bugs and fix some warnings. Update CMakeLists 2016-08-12 15:42:03 +01:00
7739404646 Compatability for g++ 4.9 2016-08-12 15:30:47 +01:00
ec19b784b3 Fix some warnings 2016-08-11 20:21:46 +01:00
e4cc023055 Overhaul a lot, fix the JS implementation 2016-08-10 17:07:41 +01:00
f47d8adc36 Most of Boobot's JS object implementation 2016-08-05 22:32:47 +01:00
47de861f45 Now tracks and caches all (I think) required data 2016-08-05 02:27:46 +01:00
1b2ea3c6bf Add runtime error checking for custom JS 2016-08-04 18:16:09 +01:00
a1c50b253d Added logger, resolves #4 2016-08-04 17:39:52 +01:00
893a6cbbe1 Update README.md 2016-08-04 02:53:54 +01:00
3656080a43 Basic command creation/overwriting system 2016-08-04 02:41:02 +01:00
005dad899b Basic JS implementation 2016-08-03 22:04:05 +01:00
d49c502869 Update gitignore 2016-08-03 21:52:14 +01:00
42 changed files with 2634 additions and 730 deletions

24
.gitignore vendored
View File

@ -1,11 +1,23 @@
# VS files
/.vs/
/TriviaBot.sln
/TriviaBot.VC*
/TriviaBot/TriviaBot.vcxproj*
/TriviaBot/x64/
/Toast.sln
/Toast.VC*
/Toast/Toast.vcxproj*
/Toast/x64/
/x64/
# Data files
/TriviaBot/data_management/questions
/TriviaBot/bot/db/trivia.db
/Toast/data_management/questions
/Toast/bot/db/trivia.db
# Config file
config.json
# Compiled sqlite file
sqlite3.obj
# V8 lib
lib/v8
# Built files
build/

View File

@ -1,28 +0,0 @@
sudo: required
dist: trusty
language: cpp
compiler: gcc
addons:
apt:
sources:
- ubuntu-toolchain-r-test
- boost-latest
packages:
- g++-5
- gcc-5
- libboost1.55-all-dev
- libcurl4-openssl-dev
- cmake
install: export CXX="g++-5" CC="gcc-5";
script:
- cd TriviaBot
- cmake .
- make
deploy:
provider: releases
api_key:
secure: sbZntceUqjyFZ13TlwuU0Jdtfma/jXHprv4z+pYrmuO3/YarKMtwlvM6UniagF2wVcHTFtiBmlbxfSyRfpy+P6XGkpvIJtJFrsMaSAoZpLI6mbSPsPxUCvc1VW3EESGIlWTJ97TZmU3opLjLSQIgYK1NB1+1KtLedVnQy6KCtTFthjupNwDIeSIFyPR31BgL0yQ7owPYZ63koS4U32ABPFxTYUPDbkI+Xq02nrzbn3OGKQY1cXmLJtvaTi+QQARfCFGYNTB4Ngt04LtpzBP5eeyj2P3YslZj9Xyr9PgqDt0uS2I5m/hLWsSLf/ssJ06EIBt2mYIORVV/XcgBw13VUBHawbd6hLfxSmSwhYpTewcjVbU1gE09kYmyVJ+KKKcbJjhWknIRd6z9+rLGwGb+zlgFnR9KFa4VqBSxGzfujwtBJVntaz0QVWb8vNL5U1xz1FyOBUT+6jWbr0L9d8QyK0ivPUbiYXwMQAoAEjjq7VbG985eyrTjxB6oxiWg9W8RveS1Mbb++/NWRSvsIfA1oEPrOgJONiOUP1+1XQ0rFLWpVHw5n4yuWFo6XpbfauCujIA4bNPGPqR0cfHj8QbUK9JDoA1SiOWsburQqUNqo0WHTUvrgut3OaP8jysDKsmoh8/tWeKlc6l5IqKQ4qu28sOQLd8RHcKOToKSfIOwRdA=
file: TriviaBot
skip_cleanup: true
on:
tags: true

View File

@ -1,4 +1,4 @@
#trivia-bot <img src="https://cdn.discordapp.com/attachments/164732409919569920/205700949304541184/emoji.png" width="30" height="30" /> <img src="https://travis-ci.org/jackb-p/trivia-bot.svg?branch=release" />
# Toast <img src="https://www.ahealthiermichigan.org/wp-content/uploads/2014/09/Transform-toast-into-breakfast.jpg" width="30" height="30" />
A bot which provides a Trivia game for [Discord](https://discordapp.com/).
@ -10,15 +10,27 @@ It requires no special permissions at this time (only read/write to channels).
### Installation
[Releases](https://github.com/jackb-p/trivia-bot/releases) are available for tagged versions. These are compiled on Ubuntu by Travis CI. To run on other systems you will need to compile yourself - a CMake configuration is included. Windows releases will happen one day.
[Releases](https://github.com/jackb-p/trivia-bot/releases) are available for tagged versions. These are compiled on Debian Jessie by Jenkins. Note that you still require the dependency library files, so you will still have to build V8 and add it to your `LD_LIBRARY_PATH`. To run on other systems you will need to compile yourself - a CMake configuration is included. Windows releases will happen one day.
If you want to install a version for which a release does not exist, you will also have to compile manually. Compilation instructions are available for Linux below.
### Running
To run simply execute the program: `./TriviaBot`
To run simply execute the program: `./Toast`
If you do not want to be prompted for your token every launch, provide it as an argument: `./TriviaBot {TOKEN}`
#### Configuration
The config file is automatically generated if it is not present. The JSON format is used. You must edit the config file for the bot to work correctly, the bot token is required.
The current configuration options are as follows:
1. **General**
| Field | Description |
| --- | --- |
| `api_cert_file` | The path to the Discord API .crt file for HTTPS. |
| `bot_token` | Your Discord bot token. |
| `owner_id` | The user ID of the owner of the bot. This allows owner-only (maintenance) commands, such as `shutdown`. |
| `js_allowed_roles` | List of role names which are allowed to use the `createjs` ands `js` commands. |
### Trivia Questions
Questions are obtained from [trivia-db on Sourceforge](https://sourceforge.net/projects/triviadb/).
@ -30,32 +42,40 @@ LoadDB.cpp takes some time to execute.
### Commands
#### Trivia Game
`` `trivia`` is the base command.
| Argument | Description |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `questions` `interval` | Where `questions` and `interval` are integers. Makes the game last `questions` number of questions, optionally sets the time interval between hints to `interval` seconds. |
| stop | Stops the trivia game currently in the channel the message is sent from, if there is one. |
| help | Prints a help list, similar to this table. |
| Argument | Description |
| --- | --- |
| `questions` `interval` | Where `questions` and `interval` are integers. Makes the game last `questions` number of questions, optionally sets the time interval between hints to `interval` seconds. |
| stop | Stops the trivia game currently in the channel the message is sent from, if there is one. |
| help | Prints a help list, similar to this table. |
#### Javascript Commands
The Javascript system is designed to mirror the old [Boobot implementation](https://www.boobot.party/). For now there are some exceptions:
1. Message objects aren't implemented.
2. Properties *are* case sensitive. You must use `server.Name`, not `server.name`. This will not be changed.
### Compiling
#### Dependencies
| Name | Website | Notes |
| ------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| boost | [boost.org](http://www.boost.org/) | |
| websocketpp | [zaphoyd/websocketpp](https://github.com/zaphoyd/websocketpp) | Included as submodule. |
| cURL | [curl.haxx.se](https://curl.haxx.se/) | |
| sqlite3 | [sqlite.org](https://www.sqlite.org/) | Included as submodule. Uses a [different repo](https://github.com/azadkuh/sqlite-amalgamation/). |
| nlohmann/json | [nlohmann/json](https://github.com/nlohmann/json) | (Slightly modified) source file included in repo. |
| Name | Website | Notes |
| --- | --- | --- |
| boost | [boost.org](http://www.boost.org/) | |
| websocketpp | [zaphoyd/websocketpp](https://github.com/zaphoyd/websocketpp) | Included as submodule. |
| cURL | [curl.haxx.se](https://curl.haxx.se/) | |
| sqlite3 | [sqlite.org](https://www.sqlite.org/) | Included as submodule. Uses a [different repo](https://github.com/azadkuh/sqlite-amalgamation/). |
| nlohmann/json | [nlohmann/json](https://github.com/nlohmann/json) | (Slightly modified) source file included in repo. |
| V8 | [Google V8](https://developers.google.com/v8/) | Debian/Ubuntu `libv8` packages are too outdated. Must be built manually. |
#### Linux (debian)
c++14 support is required. gcc 5 and above recommended.
#### Linux (Debian)
c++14 support is required. gcc 5 and above recommended, however it compiles on 4.9.2 (and possibly some versions below.)
1. Clone the github repo: `git clone https://github.com/jackb-p/TriviaDiscord.git TriviaDiscord`
2. Navigate to repository directory: `cd TriviaDiscord`
1. Clone the github repo: `git clone https://github.com/jackb-p/Toast.git ToastBot`
2. Navigate to repository directory: `cd ToastBot`
3. Clone the submodules: `git submodule init` and `git submodule update`
4. Install other dependencies: `sudo apt-get install cmake libboost-all-dev libcurl-dev` (Package managers and names may vary, but all of these should be easy to find through a simple Google search.)
5. `cd TriviaBot`
6. `cmake .`
7. `make`
4. Install other dependencies: `sudo apt-get install build-essential cmake libboost-all-dev libcurl4-openssl-dev libssl-dev` (Package managers and names may vary, but all of these should be easy to find through a simple Google search.) V8 may require other dependencies.
5. Build V8. Put the library files into lib/v8/lib/ and the include files into lib/v8/include. More instructions will be added at some point for this step.
6. `cd Toast`
7. `cmake .`
8. `make`

View File

@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 2.8.7)
project(TriviaBot)
project(Toast)
###############################################################################
## get source ## ##############################################################
@ -7,12 +7,14 @@ project(TriviaBot)
file(GLOB_RECURSE sources bot/*.cpp bot/*.hpp ../lib/sqlite3/sqlite3.c)
link_directories(../lib/v8/lib)
###############################################################################
## target definitions #########################################################
###############################################################################
# add the data to the target, so it becomes visible in some IDE
add_executable(TriviaBot ${sources})
add_executable(Toast ${sources})
# add some compiler flags
set (CMAKE_CXX_FLAGS "-std=c++14 -Wall ${CMAKE_CXX_FLAGS}")
@ -25,12 +27,18 @@ find_package(Boost COMPONENTS system thread regex REQUIRED)
find_package(OpenSSL REQUIRED)
find_package(CURL REQUIRED)
target_link_libraries(TriviaBot PUBLIC
target_link_libraries(Toast PUBLIC
${Boost_LIBRARIES}
${OPENSSL_LIBRARIES}
${CURL_LIBRARIES}
pthread
v8
v8_libplatform
v8_libbase
icui18n
icuuc
rt
dl
pthread
)
include_directories(
@ -39,7 +47,10 @@ include_directories(
${CURL_INCLUDE_DIR}
../lib/websocketpp
../lib/sqlite3
../lib/v8
)
# don't know if necessary, too scared to remove
add_definitions(-D_WEBSOCKETPP_CPP11_STL_)
add_definitions(-D_WEBSOCKETPP_CPP11_STL_)
set(CMAKE_BUILD_TYPE Release)

60
Toast/bot/BotConfig.cpp Normal file
View File

@ -0,0 +1,60 @@
#include "BotConfig.hpp"
#include <sstream>
#include <fstream>
#include <ostream>
#include "json/json.hpp"
#include "Logger.hpp"
using json = nlohmann::json;
BotConfig::BotConfig() {
is_new_config = false;
std::stringstream ss;
std::ifstream config_file("config.json");
if(!config_file) {
config_file.close();
create_new_file();
return;
}
ss << config_file.rdbuf();
config_file.close();
std::string config = ss.str();
load_from_json(config);
}
void BotConfig::load_from_json(std::string data) {
json parsed = json::parse(data);
token = parsed.value("bot_token", "");
owner_id = parsed.value("owner_id", "");
cert_location = parsed.value("api_cert_file", "bot/http/DiscordCA.crt");
js_allowed_roles = parsed["v8"].value("js_allowed_roles", std::unordered_set<std::string> { "Admin", "Coder" });
Logger::write("config.json file loaded", Logger::LogLevel::Info);
}
void BotConfig::create_new_file() {
std::string config = json {
{ "bot_token", "" },
{ "owner_id", "" },
{ "api_cert_file", "bot/http/DiscordCA.crt" },
{ "v8", {
{ "js_allowed_roles", {
"Admin", "Coder", "Bot Commander"
} }
} }
}.dump(4);
std::ofstream config_file("config.json");
config_file << config;
config_file.close();
Logger::write("Created new config.json file", Logger::LogLevel::Info);
is_new_config = true;
}

23
Toast/bot/BotConfig.hpp Normal file
View File

@ -0,0 +1,23 @@
#ifndef BOT_BOTCONFIG
#define BOT_BOTCONFIG
#include <string>
#include <unordered_set>
class BotConfig {
public:
BotConfig();
bool is_new_config;
std::string token;
std::string owner_id;
std::string cert_location;
std::unordered_set<std::string> js_allowed_roles;
private:
void load_from_json(std::string data);
void create_new_file();
};
#endif

View File

@ -3,73 +3,74 @@
#include <cstdio>
#include <iostream>
#include "GatewayHandler.hpp"
#include "Logger.hpp"
#include "BotConfig.hpp"
ClientConnection::ClientConnection() {
ClientConnection::ClientConnection(BotConfig &c) : config(c), gh(config) {
// Reset the log channels
c.clear_access_channels(websocketpp::log::alevel::all);
cli.clear_access_channels(websocketpp::log::alevel::all);
// Only want application logging, logging from the initial connection stages or any error logging
c.set_access_channels(websocketpp::log::alevel::app | websocketpp::log::alevel::connect);
c.set_error_channels(websocketpp::log::elevel::all);
cli.set_access_channels(websocketpp::log::alevel::app | websocketpp::log::alevel::connect);
cli.set_error_channels(websocketpp::log::elevel::all);
// Initialize ASIO
c.init_asio();
cli.init_asio();
// Bind handlers
c.set_socket_init_handler(bind(
cli.set_socket_init_handler(bind(
&ClientConnection::on_socket_init,
this,
websocketpp::lib::placeholders::_1
));
c.set_tls_init_handler(bind<context_ptr>(
cli.set_tls_init_handler(bind<context_ptr>(
&ClientConnection::on_tls_init,
this,
websocketpp::lib::placeholders::_1
));
c.set_message_handler(bind(
cli.set_message_handler(bind(
&ClientConnection::on_message,
this,
websocketpp::lib::placeholders::_1,
websocketpp::lib::placeholders::_2
));
c.set_open_handler(bind(
cli.set_open_handler(bind(
&ClientConnection::on_open,
this,
websocketpp::lib::placeholders::_1
));
c.set_close_handler(bind(
cli.set_close_handler(bind(
&ClientConnection::on_close,
this,
websocketpp::lib::placeholders::_1
));
c.set_fail_handler(bind(
cli.set_fail_handler(bind(
&ClientConnection::on_fail,
this,
websocketpp::lib::placeholders::_1
));
gh = std::make_unique<GatewayHandler>();
}
// Open a connection to the URI provided
void ClientConnection::start(std::string uri) {
websocketpp::lib::error_code ec;
client::connection_ptr con = c.get_connection(uri, ec);
client::connection_ptr con = cli.get_connection(uri, ec);
if (ec) { // failed to create connection
c.get_alog().write(websocketpp::log::alevel::app, ec.message());
Logger::write("Failed to create connection: " + ec.message(), Logger::LogLevel::Severe);
return;
}
// Open the connection
c.connect(con);
c.run();
cli.connect(con);
cli.run();
Logger::write("Finished running", Logger::LogLevel::Debug);
}
// Event handlers
void ClientConnection::on_socket_init(websocketpp::connection_hdl) {
c.get_alog().write(websocketpp::log::alevel::app, "Socket intialised.");
Logger::write("Socket initialised", Logger::LogLevel::Debug);
}
context_ptr ClientConnection::on_tls_init(websocketpp::connection_hdl) {
@ -81,41 +82,43 @@ context_ptr ClientConnection::on_tls_init(websocketpp::connection_hdl) {
boost::asio::ssl::context::no_sslv3 |
boost::asio::ssl::context::single_dh_use);
}
catch (std::exception& e) {
std::cout << "Error in context pointer: " << e.what() << std::endl;
catch (std::exception &e) {
Logger::write("[tls] Error in context pointer: " + std::string(e.what()), Logger::LogLevel::Severe);
}
return ctx;
}
void ClientConnection::on_fail(websocketpp::connection_hdl hdl) {
client::connection_ptr con = c.get_con_from_hdl(hdl);
client::connection_ptr con = cli.get_con_from_hdl(hdl);
// Print as much information as possible
c.get_elog().write(websocketpp::log::elevel::warn,
"Fail handler: \n" +
Logger::write("Fail handler: \n" +
std::to_string(con->get_state()) + "\n" +
std::to_string(con->get_local_close_code()) + "\n" +
con->get_local_close_reason() + "\n" +
std::to_string(con->get_remote_close_code()) + "\n" +
con->get_remote_close_reason() + "\n" +
std::to_string(con->get_ec().value()) + " - " + con->get_ec().message() + "\n");
std::to_string(con->get_ec().value()) + " - " + con->get_ec().message() + "\n",
Logger::LogLevel::Severe);
}
void ClientConnection::on_open(websocketpp::connection_hdl hdl) {
Logger::write("Connection opened", Logger::LogLevel::Debug);
}
void ClientConnection::on_message(websocketpp::connection_hdl hdl, message_ptr message) {
if (message->get_opcode() != websocketpp::frame::opcode::text) {
// If the message is not text, just print as hex
std::cout << "<< " << websocketpp::utility::to_hex(message->get_payload()) << std::endl;
Logger::write("Non-text message received: " + websocketpp::utility::to_hex(message->get_payload()), Logger::LogLevel::Warning);
return;
}
// Pass the message to the gateway handler
gh->handle_data(message->get_payload(), c, hdl);
gh.handle_data(message->get_payload(), cli, hdl);
}
void ClientConnection::on_close(websocketpp::connection_hdl) {
std::cout << "Closed." << std::endl;
Logger::write("Connection closed", Logger::LogLevel::Info);
cli.stop();
}

View File

@ -5,6 +5,8 @@
#include <websocketpp/config/asio_client.hpp>
#include "json/json.hpp"
#include "GatewayHandler.hpp"
typedef websocketpp::client<websocketpp::config::asio_tls_client> client;
using websocketpp::lib::bind;
@ -14,15 +16,20 @@ typedef websocketpp::config::asio_tls_client::message_type::ptr message_ptr;
typedef websocketpp::lib::shared_ptr<boost::asio::ssl::context> context_ptr;
typedef client::connection_ptr connection_ptr;
#include "GatewayHandler.hpp"
class BotConfig;
class ClientConnection {
public:
ClientConnection();
ClientConnection(BotConfig &c);
// Open a connection to the URI provided
void start(std::string uri);
private:
client cli;
BotConfig &config;
GatewayHandler gh;
// Event handlers
void on_socket_init(websocketpp::connection_hdl);
context_ptr on_tls_init(websocketpp::connection_hdl);
@ -30,10 +37,6 @@ public:
void on_open(websocketpp::connection_hdl hdl);
void on_message(websocketpp::connection_hdl hdl, message_ptr message);
void on_close(websocketpp::connection_hdl);
private:
client c;
std::unique_ptr<GatewayHandler> gh;
};
#endif

84
Toast/bot/DiscordAPI.cpp Normal file
View File

@ -0,0 +1,84 @@
#include "DiscordAPI.hpp"
#include <cstdio>
#include <thread>
#include <chrono>
#include "http/HTTP.hpp"
#include "Logger.hpp"
using namespace std::chrono_literals;
namespace DiscordAPI {
const std::string base_url = "https://discordapp.com/api";
const std::string channels_url = base_url + "/channels";
const std::string gateway_url = base_url + "/gateway";
const std::string json_mime_type = "application/json";
void send_message(std::string channel_id, std::string message, std::string token, std::string ca_location) {
if (message == "") {
Logger::write("[API] [send_message] Tried to send empty message", Logger::LogLevel::Warning);
return;
}
if (message.length() > 4000) {
Logger::write("[API] [send_message] Tried to send a message over 4000 characters", Logger::LogLevel::Warning);
return;
}
else if (message.length() > 2000) {
std::cout << message.length() << std::endl;
std::string first = message.substr(0, 2000);
std::string second = message.substr(2000);
send_message(channel_id, first, token, ca_location);
std::this_thread::sleep_for(50ms);
send_message(channel_id, second, token, ca_location);
return;
}
const std::string url = channels_url + "/" + channel_id + "/messages";
json data = {
{ "content", message }
};
std::string response;
long response_code = 0;
response = HTTP::post_request(url, json_mime_type, data.dump(), &response_code, token, ca_location);
int retries = 0;
while (response_code != 200 && retries < 2) {
Logger::write("[API] [send_message] Got non-200 response code, retrying", Logger::LogLevel::Warning);
std::this_thread::sleep_for(100ms);
// try 3 times. usually enough to prevent 502 bad gateway issues
response = HTTP::post_request(url, json_mime_type, data.dump(), &response_code, token, ca_location);
retries++;
}
if (response_code != 200) {
Logger::write("[API] [send_message] Giving up on sending message", Logger::LogLevel::Warning);
}
}
json get_gateway(std::string ca_location) {
std::string response;
long response_code;
response = HTTP::get_request(gateway_url, &response_code, "", ca_location);
int retries = 0;
while (response_code != 200 && retries < 4) {
Logger::write("[API] [get_gateway] Got non-200 response code, retrying", Logger::LogLevel::Warning);
std::this_thread::sleep_for(100ms);
// try 3 times. usually enough to prevent 502 bad gateway issues
response = HTTP::get_request(gateway_url, &response_code, "", ca_location);
retries++;
}
if (response_code != 200) {
Logger::write("[API] [get_gateway] Giving up on getting gateway url", Logger::LogLevel::Warning);
return json {};
}
return json::parse(response);
}
}

17
Toast/bot/DiscordAPI.hpp Normal file
View File

@ -0,0 +1,17 @@
#ifndef BOT_APIHELPER
#define BOT_APIHELPER
#include <string>
#include "json/json.hpp"
using json = nlohmann::json;
class BotConfig;
namespace DiscordAPI {
json get_gateway(std::string ca_location);
void send_message(std::string channel_id, std::string message, std::string token, std::string ca_location);
}
#endif

View File

@ -0,0 +1,678 @@
#include "GatewayHandler.hpp"
#include <boost/algorithm/string.hpp>
#include "DiscordAPI.hpp"
#include "Logger.hpp"
#include "data_structures/GuildMember.hpp"
#include "BotConfig.hpp"
GatewayHandler::GatewayHandler(BotConfig &c) : config(c) {
last_seq = 0;
CommandHelper::init();
}
void GatewayHandler::handle_data(std::string data, client &c, websocketpp::connection_hdl &hdl) {
json decoded = json::parse(data);
int op = decoded["op"];
switch (op) {
case 0: // Event dispatch
on_dispatch(decoded, c, hdl);
break;
case 10: // Hello
on_hello(decoded, c, hdl);
break;
case 11:
Logger::write("Heartbeat acknowledged", Logger::LogLevel::Debug);
break;
}
}
void GatewayHandler::send_heartbeat(client *c, websocketpp::connection_hdl hdl, int interval) {
while (true) {
boost::this_thread::sleep(boost::posix_time::milliseconds(interval));
if (!c) {
Logger::write("[send_heartbeat] Client pointer is null", Logger::LogLevel::Severe);
break;
}
else if (c->stopped()) {
break;
}
json heartbeat = {
{ "op", 1 },
{ "d", last_seq }
};
c->send(hdl, heartbeat.dump(), websocketpp::frame::opcode::text);
Logger::write("Sent heartbeat (seq: " + std::to_string(last_seq) + ")", Logger::LogLevel::Debug);
}
}
void GatewayHandler::send_identify(client &c, websocketpp::connection_hdl &hdl) {
json identify = {
{ "op", 2 },
{ "d", {
{ "token", config.token },
{ "properties",{
{ "$browser", "Microsoft Windows 10" },
{ "$device", "TriviaBot-0.0" },
{ "$referrer", "" },
{ "$referring_domain", "" }
} },
{ "compress", false },
{ "large_threshold", 250 },
{ "shard",{ 0, 1 } }
} }
};
c.send(hdl, identify.dump(), websocketpp::frame::opcode::text);
Logger::write("Sent identify payload", Logger::LogLevel::Debug);
}
void GatewayHandler::send_request_guild_members(client &c, websocketpp::connection_hdl &hdl, std::string guild_id) {
json request_guild_members = {
{ "op", 8 },
{ "d", {
{ "guild_id", guild_id },
{ "query", "" },
{ "limit", 0 }
} }
};
c.send(hdl, request_guild_members.dump(), websocketpp::frame::opcode::text);
Logger::write("Requested guild members for " + guild_id, Logger::LogLevel::Debug);
}
void GatewayHandler::on_hello(json decoded, client &c, websocketpp::connection_hdl &hdl) {
heartbeat_interval = decoded["d"]["heartbeat_interval"];
Logger::write("Heartbeat interval: " + std::to_string(heartbeat_interval / 1000.0f) + " seconds", Logger::LogLevel::Debug);
heartbeat_thread = std::make_unique<boost::thread>(boost::bind(&GatewayHandler::send_heartbeat, this, &c, hdl, heartbeat_interval));
send_identify(c, hdl);
}
void GatewayHandler::on_dispatch(json decoded, client &c, websocketpp::connection_hdl &hdl) {
last_seq = decoded["s"];
std::string event_name = decoded["t"];
json data = decoded["d"];
if (event_name == "READY") {
on_event_ready(data);
}
else if (event_name == "GUILD_CREATE") {
on_event_guild_create(data);
}
else if (event_name == "GUILD_UPDATE") {
on_event_guild_update(data);
}
else if (event_name == "GUILD_DELETE") {
on_event_guild_delete(data);
}
else if (event_name == "GUILD_MEMBER_ADD") {
on_event_guild_member_add(data);
}
else if (event_name == "GUILD_MEMBER_UPDATE") {
on_event_guild_member_update(data);
}
else if (event_name == "GUILD_MEMBER_REMOVE") {
on_event_guild_member_remove(data);
}
else if (event_name == "GUILD_ROLE_CREATE") {
on_event_guild_role_create(data);
}
else if (event_name == "GUILD_ROLE_UPDATE") {
on_event_guild_role_update(data);
}
else if (event_name == "GUILD_ROLE_DELETE") {
on_event_guild_role_delete(data);
}
else if (event_name == "CHANNEL_CREATE") {
on_event_channel_create(data);
}
else if (event_name == "CHANNEL_UPDATE") {
on_event_channel_update(data);
}
else if (event_name == "CHANNEL_DELETE") {
on_event_channel_delete(data);
}
else if (event_name == "MESSAGE_CREATE") {
on_event_message_create(data, c, hdl);
}
else if (event_name == "PRESENCE_UPDATE") {
on_event_presence_update(data);
}
}
void GatewayHandler::on_event_ready(json data) {
user_object.load_from_json(data["user"]);
Logger::write("Sign-on confirmed. (@" + user_object.username + "#" + user_object.discriminator + ")", Logger::LogLevel::Info);
}
void GatewayHandler::on_event_presence_update(json data) {
std::string user_id = data["user"]["id"];
auto it = users.find(user_id);
if (it != users.end()) {
it->second.status = data.value("status", "offline");
if (data["game"] == nullptr) {
it->second.game = "null";
}
else {
it->second.game = data["game"].value("name", "null");
}
}
else {
Logger::write("Tried to add presence for user " + user_id + " who doesn't exist", Logger::LogLevel::Warning);
}
}
void GatewayHandler::on_event_guild_create(json data) {
guilds[data["id"]] = DiscordObjects::Guild(data);
DiscordObjects::Guild &guild = guilds[data["id"]];
Logger::write("Received info for guild " + guild.id + ", now in " + std::to_string(guilds.size()) + " guild(s)", Logger::LogLevel::Info);
int channels_added = 0, roles_added = 0, members_added = 0, presences_added = 0;
for (json channel : data["channels"]) {
std::string channel_id = channel["id"];
channel["guild_id"] = guild.id;
channels[channel_id] = DiscordObjects::Channel(channel);
guilds[guild.id].channels.push_back(&channels[channel_id]);
channels_added++;
}
for (json role : data["roles"]) {
std::string role_id = role["id"];
roles[role_id] = DiscordObjects::Role(role);
guilds[guild.id].roles.push_back(&roles[role_id]);
roles_added++;
}
for (json member : data["members"]) {
std::string user_id = member["user"]["id"];
auto it = users.find(user_id);
if (it == users.end()) { // new user
users[user_id] = DiscordObjects::User(member["user"]);
}
users[user_id].guilds.push_back(guild.id);
DiscordObjects::GuildMember *guild_member = new DiscordObjects::GuildMember(member, &users[user_id]);
for (std::string role_id : member["roles"]) {
guild_member->roles.push_back(&roles[role_id]);
}
guilds[guild.id].members.push_back(guild_member);
members_added++;
}
for (json presence : data["presences"]) {
std::string user_id = presence["user"]["id"];
auto it = users.find(user_id);
if (it != users.end()) {
it->second.status = presence.value("status", "offline");
if (presence["game"] == nullptr) {
it->second.game = "null";
} else {
it->second.game = presence["game"].value("name", "null");
}
presences_added++;
}
else {
Logger::write("Tried to add presence for user " + user_id + " who doesn't exist", Logger::LogLevel::Warning);
}
}
if (v8_instances.count(guild.id) == 0) {
v8_instances[guild.id] = std::make_unique<V8Instance>(config, guild.id, &guilds, &channels, &users, &roles);
Logger::write("Created v8 instance for guild " + guild.id, Logger::LogLevel::Debug);
}
Logger::write("Loaded " + std::to_string(channels_added) + " channels, " + std::to_string(roles_added) + " roles and "
+ std::to_string(members_added) + " members (with " + std::to_string(presences_added) + " presences) to guild " + guild.id, Logger::LogLevel::Debug);
}
void GatewayHandler::on_event_guild_update(json data) {
std::string guild_id = data["id"];
guilds[guild_id].load_from_json(data);
Logger::write("Updated guild " + guild_id, Logger::LogLevel::Debug);
}
void GatewayHandler::on_event_guild_delete(json data) {
std::string guild_id = data["id"];
bool unavailable = data.value("unavailable", false);
if (unavailable) {
Logger::write("Guild " + guild_id + " has become unavailable", Logger::LogLevel::Info);
guilds[guild_id].unavailable = true;
} else {
int channels_removed = 0;
for (auto it = channels.cbegin(); it != channels.cend();) {
if (it->second.guild_id == guild_id) {
channels.erase(it++);
channels_removed++;
} else {
++it;
}
}
guilds.erase(guilds.find(guild_id));
Logger::write("Guild " + guild_id + " and " + std::to_string(channels_removed) + " channels removed", Logger::LogLevel::Info);
}
}
void GatewayHandler::on_event_guild_member_add(json data) {
std::string guild_id = data["guild_id"];
std::string user_id = data["user"]["id"];
auto it = users.find(user_id);
if (it == users.end()) { // new user
users[user_id] = DiscordObjects::User(data["user"]);
}
users[user_id].guilds.push_back(guild_id);
DiscordObjects::GuildMember *guild_member = new DiscordObjects::GuildMember(data, &users[user_id]);
for (std::string role_id : data["roles"]) {
guild_member->roles.push_back(&roles[role_id]);
}
guilds[guild_id].members.push_back(guild_member);
Logger::write("Added new member " + guild_member->user->id + " to guild " + guild_id, Logger::LogLevel::Debug);
}
void GatewayHandler::on_event_guild_member_update(json data) {
std::string user_id = data["user"]["id"];
DiscordObjects::Guild &guild = guilds[data["guild_id"]];
auto it = std::find_if(guild.members.begin(), guild.members.end(), [user_id](DiscordObjects::GuildMember *member) {
return user_id == member->user->id;
});
if (it != guild.members.end()) {
bool nick_changed = false;
int roles_change = 0;
DiscordObjects::GuildMember *member = (*it);
std::string nick = data.value("nick", "null");
if (member->nick != nick) {
member->nick = nick;
nick_changed = true;
}
roles_change = member->roles.size();
member->roles.clear(); // reset and re-fill, changing the differences is probably more expensive anyway.
for (std::string role_id : data["roles"]) {
member->roles.push_back(&roles[role_id]);
}
roles_change = member->roles.size() - roles_change;
std::string debug_string = "Updated member " + user_id + " of guild " + guild.id;
if (nick_changed) debug_string += ". Nick changed to " + nick;
if (roles_change != 0) debug_string += ". No. of roles changed by " + std::to_string(roles_change);
debug_string += ".";
Logger::write(debug_string, Logger::LogLevel::Debug);
}
else {
Logger::write("Tried to update member " + user_id + " (of guild " + guild.id + ") who does not exist.", Logger::LogLevel::Warning);
}
}
void GatewayHandler::on_event_guild_member_remove(json data) {
DiscordObjects::Guild &guild = guilds[data["guild_id"]];
std::string user_id = data["user"]["id"];
auto it = std::find_if(guild.members.begin(), guild.members.end(), [user_id](DiscordObjects::GuildMember *member) {
return user_id == member->user->id;
});
if (it != guild.members.end()) {
delete (*it);
guild.members.erase(it);
users[user_id].guilds.erase(std::remove(users[user_id].guilds.begin(), users[user_id].guilds.end(), guild.id));
if (users[user_id].guilds.size() == 0) {
users.erase(users.find(user_id));
Logger::write("User " + user_id + " removed from guild " + guild.id + " and no longer visible, deleted.", Logger::LogLevel::Debug);
}
else {
Logger::write("User " + user_id + " removed from guild " + guild.id, Logger::LogLevel::Debug);
}
}
else {
Logger::write("Tried to remove guild member " + user_id + " who doesn't exist", Logger::LogLevel::Warning);
}
}
void GatewayHandler::on_event_guild_role_create(json data) {
std::string role_id = data["role"]["id"];
std::string guild_id = data["guild_id"];
roles[role_id] = DiscordObjects::Role(data["role"]);
guilds[guild_id].roles.push_back(&roles[role_id]);
Logger::write("Created role " + role_id + " on guild " + guild_id, Logger::LogLevel::Debug);
}
void GatewayHandler::on_event_guild_role_update(json data) {
std::string role_id = data["role"]["id"];
roles[role_id].load_from_json(data["role"]);
}
void GatewayHandler::on_event_guild_role_delete(json data) {
std::string role_id = data["role_id"];
auto it = roles.find(role_id);
if (it != roles.end()) {
DiscordObjects::Guild &guild = guilds[data["guild_id"]];
auto check_lambda = [role_id](const DiscordObjects::Role *r) {
return r->id == role_id;
};
auto it2 = std::find_if(guild.roles.begin(), guild.roles.end(), check_lambda);
if (it2 != guild.roles.end()) {
guild.roles.erase(it2);
}
else {
Logger::write("Tried to delete role " + role_id + " from guild " + guild.id + " but it doesn't exist there", Logger::LogLevel::Warning);
}
roles.erase(it);
Logger::write("Deleted role " + role_id + " (guild " + guild.id + ").", Logger::LogLevel::Debug);
}
else {
Logger::write("Tried to delete role " + role_id + " but it doesn't exist.", Logger::LogLevel::Warning);
}
}
void GatewayHandler::on_event_channel_create(json data) {
std::string channel_id = data["id"];
std::string guild_id = data["guild_id"];
channels[channel_id] = DiscordObjects::Channel(data);
Logger::write("Added channel " + channel_id + " to channel list. Now " + std::to_string(channels.size()) + " channels stored", Logger::LogLevel::Debug);
guilds[guild_id].channels.push_back(&channels[channel_id]);
Logger::write("Added channel " + channel_id + " to guild " + guild_id + "'s list. Now " + std::to_string(guilds[guild_id].channels.size()) + " channels stored", Logger::LogLevel::Debug);
}
void GatewayHandler::on_event_channel_update(json data) {
std::string channel_id = data["id"];
auto it = channels.find(channel_id);
if (it == channels.end()) {
Logger::write("Got channel update for channel " + channel_id + " that doesn't exist. Creating channel instead.", Logger::LogLevel::Warning);
on_event_channel_create(data);
} else {
channels[channel_id].load_from_json(data);
Logger::write("Updated channel " + channel_id, Logger::LogLevel::Debug);
}
}
void GatewayHandler::on_event_channel_delete(json data) {
std::string channel_id = data["id"];
std::string guild_id = data["guild_id"];
auto it = channels.find(channel_id);
if (it == channels.end()) {
Logger::write("Tried to delete channel " + channel_id + " which doesn't exist", Logger::LogLevel::Warning);
}
else {
auto it2 = std::find_if(guilds[guild_id].channels.begin(), guilds[guild_id].channels.begin(), [channel_id](const DiscordObjects::Channel *c) {
return c->id == channel_id;
});
guilds[guild_id].channels.erase(it2);
Logger::write("Removed channel " + channel_id + " from guild " + guild_id + "'s list. Now "
+ std::to_string(guilds[guild_id].channels.size()) + " channels stored", Logger::LogLevel::Debug);
channels.erase(it);
Logger::write("Removed channel " + channel_id + " from channel list. Now " + std::to_string(channels.size()) + " channels stored.", Logger::LogLevel::Debug);
}
}
void GatewayHandler::on_event_message_create(json data, client &c, websocketpp::connection_hdl &hdl) {
std::string message = data["content"];
DiscordObjects::Channel &channel = channels[data["channel_id"]];
DiscordObjects::Guild &guild = guilds[channel.guild_id];
DiscordObjects::User &sender = users[data["author"]["id"]];
if (sender.bot) return;
std::vector<std::string> words;
boost::split(words, message, boost::is_any_of(" "));
CommandHelper::Command custom_command;
if (words[0] == "`trivia" || words[0] == "`t") {
int questions = 10;
int delay = 8;
if (words.size() > 3) {
DiscordAPI::send_message(channel.id, ":exclamation: Invalid arguments!", config.token, config.cert_location);
return;
}
else if (words.size() > 1) {
if (words[1] == "help" || words[1] == "h") {
std::string help = "**Base command \\`t[rivia]**. Arguments:\n";
help += "\\`trivia **{x}** **{y}**: Makes the game last **x** number of questions, optionally sets the time interval between hints to **y** seconds\n";
help += "\\`trivia **stop**: stops the ongoing game.\n";
help += "\\`trivia **help**: prints this message\n";
DiscordAPI::send_message(channel.id, help, config.token, config.cert_location);
return;
}
else if (words[1] == "stop" || words[1] == "s") {
if (games.find(channel.id) != games.end()) {
delete_game(channel.id);
}
else {
DiscordAPI::send_message(channel.id, ":warning: Couldn't find an ongoing trivia game for this channel.", config.token, config.cert_location);
}
return;
}
else {
try {
questions = std::stoi(words[1]);
if (words.size() == 3) {
delay = std::stoi(words[2]);
}
}
catch (std::invalid_argument e) {
DiscordAPI::send_message(channel.id, ":exclamation: Invalid arguments!", config.token, config.cert_location);
return;
}
}
}
games[channel.id] = std::make_unique<TriviaGame>(config, this, channel.id, questions, delay);
games[channel.id]->start();
}
else if (words[0] == "`guilds") {
std::string m = "**Guild List:**\n";
for (auto &gu : guilds) {
m += ":small_orange_diamond: " + gu.second.name + " (" + gu.second.id + ") Channels: " + std::to_string(gu.second.channels.size()) + "\n";
}
DiscordAPI::send_message(channel.id, m, config.token, config.cert_location);
}
else if (words[0] == "`info") {
DiscordAPI::send_message(channel.id, ":information_source: **toast** by Jack. <http://github.com/jackb-p/Toast>", config.token, config.cert_location);
}
else if (words[0] == "~js" && words.size() > 1) {
DiscordObjects::GuildMember *member = *std::find_if(guild.members.begin(), guild.members.end(), [sender](DiscordObjects::GuildMember *m) {
return sender.id == m->user->id;
});
BotConfig &conf = config;
bool disallowed = std::find_if(member->roles.begin(), member->roles.end(), [conf](DiscordObjects::Role *r) -> bool {
return conf.js_allowed_roles.count(r->name);
}) == member->roles.end(); // checks if the user has the required roles
if (disallowed) {
DiscordAPI::send_message(channel.id, ":warning: You do not have permission to use this command.", config.token, config.cert_location);
return;
}
std::string js = message.substr(4);
auto it = v8_instances.find(channel.guild_id);
if (it != v8_instances.end() && js.length() > 0) {
it->second->exec_js(js, &channel, member);
}
}
else if (words[0] == "~createjs" && words.size() > 1) {
auto &member = *std::find_if(guild.members.begin(), guild.members.end(), [sender](DiscordObjects::GuildMember *m) { return sender.id == m->user->id; });
BotConfig &conf = config;
bool disallowed = std::find_if(member->roles.begin(), member->roles.end(), [conf](DiscordObjects::Role *r) -> bool {
return conf.js_allowed_roles.count(r->name);
}) == member->roles.end(); // checks if the user has the required roles
if (disallowed) {
DiscordAPI::send_message(channel.id, ":warning: You do not have permission to use this command.", config.token, config.cert_location);
return;
}
std::string args = message.substr(10);
size_t seperator_loc = args.find("|");
if (seperator_loc != std::string::npos) {
std::string command_name = args.substr(0, seperator_loc);
std::string script = args.substr(seperator_loc + 1);
int result = CommandHelper::insert_command(channel.guild_id, command_name, script);
switch (result) {
case 0:
DiscordAPI::send_message(channel.id, ":warning: Error!", config.token, config.cert_location); break;
case 1:
DiscordAPI::send_message(channel.id, ":new: Command `" + command_name + "` successfully created.", config.token, config.cert_location); break;
case 2:
DiscordAPI::send_message(channel.id, ":arrow_heading_up: Command `" + command_name + "` successfully updated.", config.token, config.cert_location); break;
}
}
}
else if (words[0] == "`shutdown" && sender.id == "82232146579689472") { // it me
DiscordAPI::send_message(channel.id, ":zzz: Goodbye!", config.token, config.cert_location);
for (auto &game : games) {
delete_game(game.first);
}
v8_instances.clear();
c.close(hdl, websocketpp::close::status::going_away, "");
}
else if (words[0] == "`debug") {
if (words[1] == "channel" && words.size() == 3) {
auto it = channels.find(words[2]);
if (it == channels.end()) {
DiscordAPI::send_message(channel.id, ":question: Unrecognised channel.", config.token, config.cert_location);
return;
}
DiscordAPI::send_message(channel.id, it->second.to_debug_string(), config.token, config.cert_location);
}
else if (words[1] == "guild" && words.size() == 3) {
auto it = guilds.find(words[2]);
if (it == guilds.end()) {
DiscordAPI::send_message(channel.id, ":question: Unrecognised guild.", config.token, config.cert_location);
return;
}
DiscordAPI::send_message(channel.id, it->second.to_debug_string(), config.token, config.cert_location);
}
else if (words[1] == "member" && words.size() == 4) {
auto it = guilds.find(words[2]);
if (it == guilds.end()) {
DiscordAPI::send_message(channel.id, ":question: Unrecognised guild.", config.token, config.cert_location);
return;
}
std::string user_id = words[3];
auto it2 = std::find_if(it->second.members.begin(), it->second.members.end(), [user_id](DiscordObjects::GuildMember *member) {
return user_id == member->user->id;
});
if (it2 == it->second.members.end()) {
DiscordAPI::send_message(channel.id, ":question: Unrecognised user.", config.token, config.cert_location);
return;
}
DiscordAPI::send_message(channel.id, (*it2)->to_debug_string(), config.token, config.cert_location);
}
else if (words[1] == "role" && words.size() == 3) {
auto it = roles.find(words[2]);
if (it == roles.end()) {
DiscordAPI::send_message(channel.id, ":question: Unrecognised role.", config.token, config.cert_location);
return;
}
DiscordAPI::send_message(channel.id, it->second.to_debug_string(), config.token, config.cert_location);
}
else if (words[1] == "role" && words.size() == 4) {
std::string role_name = words[3];
auto it = guilds.find(words[2]);
if (it == guilds.end()) {
DiscordAPI::send_message(channel.id, ":question: Unrecognised guild.", config.token, config.cert_location);
return;
}
auto it2 = std::find_if(it->second.roles.begin(), it->second.roles.end(), [role_name](DiscordObjects::Role *r) {
return role_name == r->name;
});
if (it2 == it->second.roles.end()) {
DiscordAPI::send_message(channel.id, ":question: Unrecognised role.", config.token, config.cert_location);
return;
}
DiscordAPI::send_message(channel.id, (*it2)->to_debug_string(), config.token, config.cert_location);
}
else {
DiscordAPI::send_message(channel.id, ":question: Unknown parameters.", config.token, config.cert_location);
}
}
else if (CommandHelper::get_command(channel.guild_id, words[0], custom_command)) {
std::string args = "";
if (message.length() > (words[0].length() + 1)) {
args = message.substr(words[0].length() + 1);
}
if (custom_command.script.length() == 0) {
DiscordAPI::send_message(channel.id, ":warning: Script has 0 length.", config.token, config.cert_location);
return;
}
auto it = v8_instances.find(channel.guild_id);
if (it == v8_instances.end()) {
DiscordAPI::send_message(channel.id, ":warning: No V8 instance exists for this server - it's our fault not yours!", config.token, config.cert_location);
return;
}
DiscordObjects::GuildMember *member = *std::find_if(guild.members.begin(), guild.members.end(), [sender](DiscordObjects::GuildMember *m) {
return sender.id == m->user->id;
});
it->second->exec_js(custom_command.script, &channel, member, args);
}
else if (games.find(channel.id) != games.end()) { // message received in channel with ongoing trivia game
games[channel.id]->handle_answer(message, sender);
}
}
void GatewayHandler::delete_game(std::string channel_id) {
auto it = games.find(channel_id);
if (it != games.end()) {
it->second->interrupt();
// remove from map
games.erase(it);
} else {
Logger::write("Tried to delete a game that didn't exist (channel_id: " + channel_id + ")", Logger::LogLevel::Warning);
}
}

View File

@ -0,0 +1,107 @@
#ifndef BOT_GATEWAYHANDLER
#define BOT_GATEWAYHANDLER
#include <map>
#include <string>
#include <websocketpp/client.hpp>
#include <websocketpp/config/asio_client.hpp>
#include "json/json.hpp"
#include "TriviaGame.hpp"
#include "js/CommandHelper.hpp"
#include "js/V8Instance.hpp"
#include "data_structures/User.hpp"
#include "data_structures/Guild.hpp"
#include "data_structures/Channel.hpp"
#include "data_structures/Role.hpp"
typedef websocketpp::client<websocketpp::config::asio_tls_client> client;
using json = nlohmann::json;
/************ Opcodes **************************************************************************************************
* Code | Name | Description *
* --------------------------------------------------------------------------------------------------------------------------*
* 0 | Dispatch | dispatches an event *
* 1 | Heartbeat | used for ping checking *
* 2 | Identify | used for client handshake *
* 3 | Status Update | used to update the client status *
* 4 | Voice State Update | used to join/move/leave voice channels *
* 5 | Voice Server Ping | used for voice ping checking *
* 6 | Resume | used to resume a closed connection *
* 7 | Reconnect | used to tell clients to reconnect to the gateway *
* 8 | Request Guild Members | used to request guild members *
* 9 | Invalid Session | used to notify client they have an invalid session id *
* 10 | Hello | sent immediately after connecting, contains heartbeat and server debug information *
* 11 | Heartback ACK | sent immediately following a client heartbeat that was received *
*****************************************************************************************************************************/
class TriviaGame;
class BotConfig;
class GatewayHandler {
public:
GatewayHandler(BotConfig &c);
void handle_data(std::string data, client &c, websocketpp::connection_hdl &hdl);
void delete_game(std::string channel_id);
private:
BotConfig &config;
int last_seq;
int heartbeat_interval;
/* payload dispatchers */
void send_heartbeat(client *c, websocketpp::connection_hdl hdl, int interval);
void send_identify(client &c, websocketpp::connection_hdl &hdl);
void send_request_guild_members(client &c, websocketpp::connection_hdl &hdl, std::string guild_id); // not sure if required atm
/* payload handlers */
void on_hello(json decoded, client &c, websocketpp::connection_hdl &hdl);
void on_dispatch(json decoded, client &c, websocketpp::connection_hdl &hdl);
/* misc events */
void on_event_ready(json data); // https://discordapp.com/developers/docs/topics/gateway#ready
void on_event_presence_update(json data); // https://discordapp.com/developers/docs/topics/gateway#presence-update
/* guild events */
void on_event_guild_create(json data); // https://discordapp.com/developers/docs/topics/gateway#guild-create
void on_event_guild_update(json data); // https://discordapp.com/developers/docs/topics/gateway#guild-update
void on_event_guild_delete(json data); // https://discordapp.com/developers/docs/topics/gateway#guild-delete
void on_event_guild_member_add(json data); // https://discordapp.com/developers/docs/topics/gateway#guild-member-add
void on_event_guild_member_update(json data); // https://discordapp.com/developers/docs/topics/gateway#guild-member-update
void on_event_guild_member_remove(json data); // https://discordapp.com/developers/docs/topics/gateway#guild-member-remove
void on_event_guild_role_create(json data); // https://discordapp.com/developers/docs/topics/gateway#guild-role-create
void on_event_guild_role_update(json data); // https://discordapp.com/developers/docs/topics/gateway#guild-role-update
void on_event_guild_role_delete(json data); // https://discordapp.com/developers/docs/topics/gateway#guild-role-delete
/* channel events */
void on_event_channel_create(json data); // https://discordapp.com/developers/docs/topics/gateway#channel-create
void on_event_channel_update(json data); // https://discordapp.com/developers/docs/topics/gateway#channel-update
void on_event_channel_delete(json data); // https://discordapp.com/developers/docs/topics/gateway#channel-delete
/* message events */
void on_event_message_create(json data, client &c, websocketpp::connection_hdl &hdl); // https://discordapp.com/developers/docs/topics/gateway#message-create
const int protocol_version = 5;
// bot's user obj
DiscordObjects::User user_object;
/* <id, obj> */
std::map<std::string, DiscordObjects::Guild> guilds;
std::map<std::string, DiscordObjects::Channel> channels;
std::map<std::string, DiscordObjects::User> users;
std::map<std::string, DiscordObjects::Role> roles;
// <channel_id, game obj>
std::map<std::string, std::unique_ptr<TriviaGame>> games;
// <guild_id, v8 instance>
std::map<std::string, std::unique_ptr<V8Instance>> v8_instances;
std::unique_ptr<boost::thread> heartbeat_thread;
};
#endif

47
Toast/bot/Logger.cpp Normal file
View File

@ -0,0 +1,47 @@
#include "Logger.hpp"
#include <iostream>
#include <ctime>
namespace Logger {
std::ostream &operator<<(std::ostream &out, const LogLevel log_level) {
switch (log_level) {
case LogLevel::Debug:
return out << "debug";
case LogLevel::Info:
return out << "info";
case LogLevel::Warning:
return out << "warning";
case LogLevel::Severe:
return out << "severe";
}
return out << "";
}
std::ostream &get_ostream(LogLevel log_level) {
switch (log_level) {
case LogLevel::Debug:
case LogLevel::Info:
return std::clog;
case LogLevel::Severe:
case LogLevel::Warning:
return std::cerr;
}
return std::cerr;
}
void write(std::string text, LogLevel log_level) {
time_t rawtime;
struct tm *timeinfo;
char buffer[80];
time(&rawtime);
timeinfo = localtime(&rawtime);
strftime(buffer, 80, "%Y-%m-%d %H:%M:%S", timeinfo);
std::string time_str(buffer);
get_ostream(log_level) << "[" << time_str << "] [" << log_level << "] " << text << std::endl;
}
}

14
Toast/bot/Logger.hpp Normal file
View File

@ -0,0 +1,14 @@
#ifndef BOT_LOGGER
#define BOT_LOGGER
#include <string>
namespace Logger {
enum class LogLevel {
Debug, Info, Warning, Severe
};
void write(std::string text, LogLevel log_level);
}
#endif

66
Toast/bot/Toast.cpp Normal file
View File

@ -0,0 +1,66 @@
#include <thread>
#include <chrono>
#include <curl/curl.h>
#include <include/libplatform/libplatform.h>
#include <include/v8.h>
#include "ClientConnection.hpp"
#include "Logger.hpp"
#include "DiscordAPI.hpp"
#include "BotConfig.hpp"
int main(int argc, char *argv[]) {
BotConfig config;
if (config.is_new_config) {
Logger::write("Since the config.json file is newly generated, the program will exit now to allow you to edit it.", Logger::LogLevel::Info);
return 0;
}
curl_global_init(CURL_GLOBAL_DEFAULT);
v8::V8::InitializeICUDefaultLocation(argv[0]);
v8::V8::InitializeExternalStartupData(argv[0]);
v8::Platform* platform = v8::platform::CreateDefaultPlatform();
v8::V8::InitializePlatform(platform);
v8::V8::Initialize();
Logger::write("Initialised V8 and curl", Logger::LogLevel::Debug);
std::string args = "/?v=5&encoding=json";
std::string url = DiscordAPI::get_gateway(config.cert_location).value("url", "wss://gateway.discord.gg");
bool retry = true;
int exit_code = 0;
while (retry) {
retry = false;
try {
ClientConnection conn(config);
conn.start(url + args);
}
catch (const std::exception &e) {
Logger::write("std exception: " + std::string(e.what()), Logger::LogLevel::Severe);
exit_code = 1;
}
catch (websocketpp::lib::error_code e) {
Logger::write("websocketpp exception: " + e.message(), Logger::LogLevel::Severe);
std::this_thread::sleep_for(std::chrono::seconds(10));
retry = true; // should just be an occasional connection issue
}
catch (...) {
Logger::write("other exception.", Logger::LogLevel::Severe);
exit_code = 2;
}
}
v8::V8::Dispose();
v8::V8::ShutdownPlatform();
delete platform;
curl_global_cleanup();
Logger::write("Cleaned up", Logger::LogLevel::Info);
return exit_code;
}

View File

@ -10,12 +10,13 @@
#include <boost/regex.hpp>
#include "GatewayHandler.hpp"
#include "APIHelper.hpp"
#include "DiscordAPI.hpp"
#include "data_structures/User.hpp"
#include "Logger.hpp"
#include "BotConfig.hpp"
TriviaGame::TriviaGame(GatewayHandler *gh, APIHelper *ah, std::string channel_id, int total_questions, int delay) : interval(delay) {
TriviaGame::TriviaGame(BotConfig &c, GatewayHandler *gh, std::string channel_id, int total_questions, int delay) : config(c), interval(delay) {
this->gh = gh;
this->ah = ah;
this->channel_id = channel_id;
this->total_questions = total_questions;
@ -26,7 +27,7 @@ TriviaGame::~TriviaGame() {
current_thread.reset();
if (scores.size() == 0) {
ah->send_message(channel_id, ":red_circle: Game cancelled!");
DiscordAPI::send_message(channel_id, ":red_circle: Game cancelled!", config.token, config.cert_location);
return;
}
@ -50,13 +51,13 @@ TriviaGame::~TriviaGame() {
average_time.pop_back(); average_time.pop_back(); average_time.pop_back();
message += ":small_blue_diamond: <@!" + p.first + ">: " + std::to_string(p.second) + " (Avg: " + average_time + " seconds)\n";
}
ah->send_message(channel_id, message);
DiscordAPI::send_message(channel_id, message, config.token, config.cert_location);
sqlite3 *db; int rc; std::string sql;
rc = sqlite3_open("bot/db/trivia.db", &db);
if (rc) {
std::cerr << "Cant't open database: " << sqlite3_errmsg(db) << std::endl;
Logger::write("Can't open database: " + *sqlite3_errmsg(db), Logger::LogLevel::Severe);
}
std::string sql_in_list;
@ -71,7 +72,7 @@ TriviaGame::~TriviaGame() {
rc = sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, 0);
if (rc != SQLITE_OK) {
std::cerr << "SQL error." << std::endl;
Logger::write("Error creating prepared statement: " + *sqlite3_errmsg(db), Logger::LogLevel::Severe);
}
// insert arguments
@ -79,7 +80,7 @@ TriviaGame::~TriviaGame() {
rc = sqlite3_bind_text(stmt, i + 1, pairs[i].first.c_str(), -1, (sqlite3_destructor_type) -1);
if (rc != SQLITE_OK) {
std::cerr << "SQL error." << std::endl;
Logger::write("Error binding prepared statement argument: " + *sqlite3_errmsg(db), Logger::LogLevel::Severe);
break;
}
}
@ -97,7 +98,7 @@ TriviaGame::~TriviaGame() {
data[id] = std::pair<int, int>(total_score, average_time);
} else if (rc != SQLITE_DONE) {
sqlite3_finalize(stmt);
std::cerr << "SQLite error." << std::endl;
Logger::write("Error fetching results: " + *sqlite3_errmsg(db), Logger::LogLevel::Severe);
break;
}
}
@ -105,7 +106,7 @@ TriviaGame::~TriviaGame() {
std::string update_sql;
if (data.size() < scores.size()) { // some users dont have entries yet
std::string sql = "INSERT INTO TotalScores (User, TotalScore, AverageTime) VALUES ";
sql = "INSERT INTO TotalScores (User, TotalScore, AverageTime) VALUES ";
for (auto &i : scores) {
if (data.find(i.first) == data.end()) {
sql += "(?, ?, ?),";
@ -116,7 +117,7 @@ TriviaGame::~TriviaGame() {
rc = sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, 0);
if (rc != SQLITE_OK) {
std::cerr << "SQL error." << std::endl;
Logger::write("Error creating prepared statement: " + *sqlite3_errmsg(db), Logger::LogLevel::Severe);
}
int count = 1;
@ -140,7 +141,7 @@ TriviaGame::~TriviaGame() {
if (update_sql != "") {
rc = sqlite3_prepare_v2(db, update_sql.c_str(), -1, &stmt, 0);
if (rc != SQLITE_OK) {
std::cerr << "SQL error." << std::endl;
Logger::write("Error creating prepared statement: " + *sqlite3_errmsg(db), Logger::LogLevel::Severe);
}
int index = 1;
@ -180,16 +181,15 @@ void TriviaGame::question() {
/// open db
rc = sqlite3_open("bot/db/trivia.db", &db);
if (rc) {
std::cerr << "Cant't open database: " << sqlite3_errmsg(db) << std::endl;
Logger::write("Error opening database: " + *sqlite3_errmsg(db), Logger::LogLevel::Severe);
}
// prepare statement
sqlite3_stmt *stmt;
sql = "SELECT * FROM Questions ORDER BY RANDOM() LIMIT 1;";
rc = sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, 0);
if (rc != SQLITE_OK) {
std::cerr << "SQL error." << std::endl;
Logger::write("Error creating prepared statement: " + *sqlite3_errmsg(db), Logger::LogLevel::Severe);
}
rc = sqlite3_step(stmt);
@ -205,16 +205,17 @@ void TriviaGame::question() {
boost::split(current_answers, answer, boost::is_any_of("*"));
}
else if (rc != SQLITE_DONE) {
else {
sqlite3_finalize(stmt);
std::cerr << "SQLite error." << std::endl;
Logger::write("Error fetching question: " + *sqlite3_errmsg(db), Logger::LogLevel::Severe);
}
sqlite3_finalize(stmt);
sqlite3_close(db);
questions_asked++;
ah->send_message(channel_id, ":question: **(" + std::to_string(questions_asked) + "/" + std::to_string(total_questions) + ")** " + current_question);
DiscordAPI::send_message(channel_id, ":question: **(" + std::to_string(questions_asked) + "/" + std::to_string(total_questions) + ")** " + current_question,
config.token, config.cert_location);
question_start = boost::posix_time::microsec_clock::universal_time();
give_hint(0, "");
@ -253,8 +254,8 @@ void TriviaGame::give_hint(int hints_given, std::string hint) {
// count number of *s
int length = 0;
for (unsigned int i = 0; i < word.length(); i++) {
if (word[i] == hide_char) {
for (unsigned int j = 0; j < word.length(); j++) {
if (word[j] == hide_char) {
length++;
}
}
@ -281,11 +282,11 @@ void TriviaGame::give_hint(int hints_given, std::string hint) {
hints_given++; // now equal to the amount of [hide_char]s that need to be present in each word
if (print) {
ah->send_message(channel_id, ":small_orange_diamond: Hint: **`" + hint + "`**");
DiscordAPI::send_message(channel_id, ":small_orange_diamond: Hint: **`" + hint + "`**", config.token, config.cert_location);
}
}
boost::this_thread::sleep(interval);
ah->send_message(channel_id, ":exclamation: Question failed. Answer: ** `" + *current_answers.begin() + "` **");
DiscordAPI::send_message(channel_id, ":exclamation: Question failed. Answer: ** `" + *current_answers.begin() + "` **", config.token, config.cert_location);
}
void TriviaGame::handle_answer(std::string answer, DiscordObjects::User sender) {
@ -300,7 +301,7 @@ void TriviaGame::handle_answer(std::string answer, DiscordObjects::User sender)
// remove the last three 0s
time_taken.pop_back(); time_taken.pop_back(); time_taken.pop_back();
ah->send_message(channel_id, ":heavy_check_mark: <@!" + sender.id + "> You got it! (" + time_taken + " seconds)");
DiscordAPI::send_message(channel_id, ":heavy_check_mark: <@!" + sender.id + "> You got it! (" + time_taken + " seconds)", config.token, config.cert_location);
increase_score(sender.id);
update_average_time(sender.id, diff.total_milliseconds());
@ -338,4 +339,4 @@ void TriviaGame::update_average_time(std::string user_id, int time) {
// yeah it probably loses accuracy here, doesn't really matter
average_times[user_id] = (int) (total / questions_answered);
}
}
}

View File

@ -11,15 +11,14 @@
#include <boost/date_time/posix_time/posix_time.hpp>
class GatewayHandler;
class APIHelper;
class BotConfig;
namespace DiscordObjects {
class User;
}
class TriviaGame {
public:
TriviaGame(GatewayHandler *gh, APIHelper *ah, std::string channel_id, int total_questions, int delay);
TriviaGame(BotConfig &c, GatewayHandler *gh, std::string channel_id, int total_questions, int delay);
~TriviaGame();
void start();
@ -27,6 +26,8 @@ public:
void handle_answer(std::string answer, DiscordObjects::User sender);
private:
BotConfig &config;
int questions_asked;
int total_questions;
boost::posix_time::seconds interval;
@ -38,7 +39,6 @@ private:
std::string channel_id;
GatewayHandler *gh;
APIHelper *ah;
const char hide_char = '#';

View File

@ -32,6 +32,7 @@ namespace DiscordObjects {
Channel(json data);
void load_from_json(json data);
std::string to_debug_string();
bool operator==(Channel rhs);
@ -56,7 +57,7 @@ namespace DiscordObjects {
type = "text";
}
inline Channel::Channel(json data) {
inline Channel::Channel(json data) : Channel() {
load_from_json(data);
}
@ -73,6 +74,19 @@ namespace DiscordObjects {
user_limit = data.value("user_limit", -1);
}
inline std::string Channel::to_debug_string() {
return "**__Channel " + id + "__**"
+ "\n**guild_id:** " + guild_id
+ "\n**name:** " + name
+ "\n**type:** " + type
+ "\n**position:** " + std::to_string(position)
+ "\n**is_private:** " + std::to_string(is_private)
+ "\n**topic:** " + (topic == "" ? "[empty]" : topic)
+ "\n**last_message_id:** " + last_message_id
+ "\n**bitrate:** " + std::to_string(bitrate)
+ "\n**user_limit:** " + std::to_string(user_limit);
}
inline bool Channel::operator==(Channel rhs) {
return id == rhs.id && id != "null";
}

View File

@ -1,13 +1,16 @@
#ifndef BOT_DATA__STRUCTURES_Guild
#define BOT_DATA__STRUCTURES_Guild
#ifndef BOT_DATA__STRUCTURES_GUILD
#define BOT_DATA__STRUCTURES_GUILD
#include <string>
#include <memory>
#include <vector>
#include <map>
#include "../json/json.hpp"
#include "Channel.hpp"
#include "User.hpp"
#include "Role.hpp"
#include "GuildMember.hpp"
using json = nlohmann::json;
@ -48,6 +51,7 @@ namespace DiscordObjects {
Guild(json data);
void load_from_json(json data);
std::string to_debug_string();
bool operator==(Guild rhs);
@ -62,13 +66,15 @@ namespace DiscordObjects {
// bool embed_enabled;
// std::string embed_channel_id;
int verification_level;
// TODO: Implement all guil fields
// TODO: Implement all guild fields
// std::vector<?> voice_states
// std::vector<?> roles
// std::vector<?> emojis
// std::vector<?> features
bool unavailable;
std::vector<std::shared_ptr<Channel>> channels;
std::vector<Channel *> channels;
std::vector<GuildMember *> members;
std::vector<Role *> roles;
//std::vector<std::unique_ptr<DiscordObjects::User>> users;
};
@ -77,13 +83,11 @@ namespace DiscordObjects {
afk_timeout = verification_level = -1;
}
inline Guild::Guild(json data) {
inline Guild::Guild(json data) : Guild() {
load_from_json(data);
}
inline void Guild::load_from_json(json data) {
Guild();
id = data.value("id", "null");
name = data.value("name", "null");
icon = data.value("icon", "null");
@ -93,6 +97,23 @@ namespace DiscordObjects {
afk_channel_id = data.value("afk_channel_id", "null");
afk_timeout = data.value("afk_timeout", -1);
verification_level = data.value("verification_level", -1);
unavailable = data.value("unavailable", false);
}
inline std::string Guild::to_debug_string() {
return "**__Guild " + id + "__**"
+ "\n**name:** " + name
+ "\n**icon:** " + icon
+ "\n**splash:** " + splash
+ "\n**owner_id:** " + owner_id
+ "\n**region:** " + region
+ "\n**afk_channel_id:** " + afk_channel_id
+ "\n**afk_timeout:** " + std::to_string(afk_timeout)
+ "\n**verification_level:** " + std::to_string(verification_level)
+ "\n**unavailable:** " + std::to_string(unavailable)
+ "\n**channels:** " + std::to_string(channels.size())
+ "\n**roles:** " + std::to_string(roles.size())
+ "\n**members:** " + std::to_string(members.size());
}
inline bool Guild::operator==(Guild rhs) {

View File

@ -0,0 +1,69 @@
#ifndef BOT_DATA__STRUCTURES_GUILDMEMBER
#define BOT_DATA__STRUCTURES_GUILDMEMBER
#include <string>
#include <vector>
#include "../json/json.hpp"
#include "User.hpp"
#include "Role.hpp"
namespace DiscordObjects {
class GuildMember {
public:
GuildMember();
GuildMember(json data, User *user);
void load_from_json(json data);
std::string to_debug_string();
bool operator==(GuildMember rhs);
User *user;
std::string nick;
std::vector<Role *> roles;
std::string joined_at; // TODO: better type
bool deaf;
bool mute;
};
inline GuildMember::GuildMember() {
user = nullptr;
nick = joined_at = "null";
deaf = false;
mute = false;
}
inline GuildMember::GuildMember(json data, User *user) : GuildMember() {
this->user = user;
load_from_json(data);
}
inline void GuildMember::load_from_json(json data) {
nick = data.value("nick", "null");
joined_at = data.value("joined_at", "null");
deaf = data.value("deaf", false);
mute = data.value("mute", false);
}
inline std::string GuildMember::to_debug_string() {
return "**__GuildMember " + user->id + "__**"
+ "\n**mention:** <@" + user->id + "> / " + user->username + "#" + user->discriminator
+ "\n**bot:** " + std::to_string(user->bot)
+ "\n**mfa_enabled:** " + std::to_string(user->mfa_enabled)
+ "\n**avatar:** " + user->avatar
+ "\n**status:** " + user->status
+ "\n**game name:** " + user->game
+ "\n**nick:** " + nick
+ "\n**joined_at:** " + joined_at
+ "\n**deaf:** " + std::to_string(deaf)
+ "\n**mute:** " + std::to_string(mute);
}
inline bool GuildMember::operator==(GuildMember rhs) {
return user->id == rhs.user->id;
}
}
#endif

View File

@ -0,0 +1,114 @@
#ifndef BOT_DATA__STRUCTURES_ROLE
#define BOT_DATA__STRUCTURES_ROLE
#include <string>
#include <sstream>
#include <iomanip>
#include "../json/json.hpp"
using json = nlohmann::json;
namespace DiscordObjects {
class Role {
public:
Role();
Role(json data);
void load_from_json(json data);
std::string to_debug_string();
bool operator==(Role rhs);
std::string id;
std::string name;
int colour;
bool hoist;
int position;
int permissions;
bool managed;
bool mentionable;
};
inline Role::Role() {
id = "null";
name = "null";
colour = -1;
hoist = false;
position = -1;
permissions = 0;
managed = false;
mentionable = false;
}
inline Role::Role(json data) : Role() {
load_from_json(data);
}
inline void Role::load_from_json(json data) {
id = data.value("id", "null");
name = data.value("name", "null");
colour = data.value("color", -1);
hoist = data.value("hoist", false);
position = data.value("position", -1);
permissions = data.value("permissions", 0);
managed = data.value("managed", false);
mentionable = data.value("mentionable", false);
}
inline std::string Role::to_debug_string() {
return "**__Role " + id + "__**"
+ "\n**name:** " + name
+ "\n**colour:** " + std::to_string(colour)
+ "\n**hoist:** " + std::to_string(hoist)
+ "\n**position:** " + std::to_string(position)
+ "\n**permissions:** " + std::to_string(permissions)
+ "\n**managed:** " + std::to_string(managed)
+ "\n**mentionable:** " + std::to_string(mentionable);
}
inline bool Role::operator==(Role rhs) {
return id == rhs.id;
}
/* permission values */
enum class Permission {
CreateInstantInvite = 0x00000001, // Allows creation of instant invites
KickMembers = 0x00000002, // Allows kicking members
BanMembers = 0x00000004, // Allows banning members
Administrator = 0x00000008, // Allows all permissions and bypasses channel permission overwrites
ManageChannels = 0x00000010, // Allows management and editing of channels
ManageGuild = 0x00000020, // Allows management and editing of the guild
ReadMessages = 0x00000400, // Allows reading messages in a channel.The channel will not appear for users without this permission
SendMessages = 0x00000800, // Allows for sending messages in a channel.
SendTTSMessages = 0x00001000, // Allows for sending of / tts messages
ManageMessages = 0x00002000, // Allows for deletion of other users messages
EmbedLinks = 0x00004000, // Links sent by this user will be auto - embedded
AttachFiles = 0x00008000, // Allows for uploading images and files
ReadMessageHistory = 0x00010000, // Allows for reading of message history
MentionEveryone = 0x00020000, // Allows for using the @everyone tag to notify all users in a channel, and the @here tag to notify all online users in a channel
Connect = 0x00100000, // Allows for joining of a voice channel
Speak = 0x00200000, // Allows for speaking in a voice channel
MuteMembers = 0x00400000, // Allows for muting members in a voice channel
DeafenMembers = 0x00800000, // Allows for deafening of members in a voice channel
MoveMembers = 0x01000000, // Allows for moving of members between voice channels
UseVAD = 0x02000000, // Allows for using voice - activity - detection in a voice channel
ChangeNickname = 0x04000000, // Allows for modification of own nickname
ManageNicknames = 0x08000000, // Allows for modification of other users nicknames
ManageRoles = 0x10000000 // Allows management and editing of roles
};
/* implement bitwise operators */
inline Permission operator|(Permission lhs, Permission rhs) {
return static_cast<Permission>(static_cast<int>(lhs) | static_cast<int>(rhs));
}
inline Permission operator|=(Permission &lhs, Permission rhs) {
lhs = static_cast<Permission>(static_cast<int>(lhs) | static_cast<int>(rhs));
return lhs;
}
}
#endif

View File

@ -2,6 +2,7 @@
#define BOT_DATA__STRUCTURES_USER
#include <string>
#include <vector>
#include "../json/json.hpp"
@ -37,14 +38,21 @@ namespace DiscordObjects {
std::string avatar;
bool bot;
bool mfa_enabled;
// presence
std::string game;
std::string status;
std::vector<std::string> guilds;
};
inline User::User() {
id = username = discriminator = avatar = "null";
id = username = discriminator = avatar = game = "null";
status = "offline";
bot = mfa_enabled = false;
}
inline User::User(json data) {
inline User::User(json data) : User() {
load_from_json(data);
}

View File

@ -0,0 +1,20 @@
BEGIN TRANSACTION;
CREATE TABLE "TotalScores" (
`User` TEXT NOT NULL,
`TotalScore` INTEGER NOT NULL,
`AverageTime` INTEGER NOT NULL,
PRIMARY KEY(User)
);
CREATE TABLE "Questions" (
`ID` INTEGER PRIMARY KEY AUTOINCREMENT,
`Category` TEXT NOT NULL,
`Question` TEXT NOT NULL,
`Answer` TEXT NOT NULL
);
CREATE TABLE `CustomJS` (
`ID` INTEGER PRIMARY KEY AUTOINCREMENT,
`GuildID` TEXT NOT NULL,
`CommandName` TEXT NOT NULL,
`Script` TEXT NOT NULL
);
COMMIT;

102
Toast/bot/http/HTTP.cpp Normal file
View File

@ -0,0 +1,102 @@
#include "HTTP.hpp"
#include "../Logger.hpp"
#include "../BotConfig.hpp"
/*
* Warning: (Awful) C Code
*/
namespace HTTP {
size_t write_callback(void *contents, size_t size, size_t nmemb, void *read_buffer) {
static_cast<std::string *>(read_buffer)->append(static_cast<char *>(contents), size * nmemb);
return size * nmemb;
}
std::string post_request(std::string url, std::string content_type, std::string data, long *response_code, std::string token, std::string ca_location) {
CURL *curl;
CURLcode res;
std::string read_buffer;
struct curl_slist *headers = nullptr;
curl = curl_easy_init();
if (curl) {
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
// Now with real HTTPS!
curl_easy_setopt(curl, CURLOPT_CAINFO, ca_location.c_str());
std::string header_arr[3];
header_arr[0] = "Content-Type: " + content_type;
header_arr[1] = "Authorization: Bot " + token;
header_arr[2] = "User-Agent: DiscordBot(http://github.com/jackb-p/triviadiscord, 1.0)";
for (std::string h : header_arr) {
headers = curl_slist_append(headers, h.c_str());
}
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &read_buffer);
res = curl_easy_perform(curl);
if (res == CURLE_OK) {
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, response_code);
}
else {
return "";
}
curl_easy_cleanup(curl);
curl_slist_free_all(headers);
}
return read_buffer;
}
std::string get_request(std::string url, long *response_code, std::string token, std::string ca_location) {
CURL *curl;
CURLcode res;
std::string read_buffer;
struct curl_slist *headers = nullptr;
curl = curl_easy_init();
if (curl) {
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
// Now with real HTTPS!
curl_easy_setopt(curl, CURLOPT_CAINFO, ca_location.c_str());
std::string header_arr[2];
header_arr[0] = "Authorization: Bot " + token;
header_arr[1] = "User-Agent: DiscordBot (http://github.com/jackb-p/triviadiscord, 1.0)";
for (std::string h : header_arr) {
headers = curl_slist_append(headers, h.c_str());
}
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &HTTP::write_callback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &read_buffer);
res = curl_easy_perform(curl);
if (res == CURLE_OK) {
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, response_code);
}
else {
Logger::write("curl error: " + std::string(curl_easy_strerror(res)), Logger::LogLevel::Warning);
return read_buffer;
}
curl_easy_cleanup(curl);
curl_slist_free_all(headers);
}
return read_buffer;
}
}

15
Toast/bot/http/HTTP.hpp Normal file
View File

@ -0,0 +1,15 @@
#ifndef BOT_HTTP_HTTP
#define BOT_HTTP_HTTP
#include <iostream>
#include <curl/curl.h>
class BotConfig;
namespace HTTP {
std::string post_request(std::string url, std::string content_type, std::string data, long *response_code, std::string token, std::string ca_location);
std::string get_request(std::string url, long *response_code, std::string token, std::string ca_location);
}
#endif

View File

@ -0,0 +1,164 @@
#include "CommandHelper.hpp"
#include <iostream>
#include <algorithm>
#include <sqlite3.h>
#include "../Logger.hpp"
namespace CommandHelper {
std::vector<Command> commands;
void init() {
sqlite3 *db; int return_code;
return_code = sqlite3_open("bot/db/trivia.db", &db);
std::string sql = "SELECT GuildID, CommandName, Script FROM CustomJS";
sqlite3_stmt *stmt;
return_code = sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, 0);
while (return_code != SQLITE_DONE) {
return_code = sqlite3_step(stmt);
if (return_code == SQLITE_ROW) {
std::string guild_id = reinterpret_cast<const char *>(sqlite3_column_text(stmt, 0));
std::string command_name = reinterpret_cast<const char *>(sqlite3_column_text(stmt, 1));
std::string script = reinterpret_cast<const char *>(sqlite3_column_text(stmt, 2));
commands.push_back({ guild_id, command_name, script });
}
else if (return_code != SQLITE_DONE) {
sqlite3_finalize(stmt);
std::cerr << "SQLite error." << std::endl;
return;
}
}
Logger::write(std::to_string(commands.size()) + " custom command(s) loaded", Logger::LogLevel::Info);
sqlite3_finalize(stmt);
sqlite3_close(db);
}
bool return_code_ok(int return_code) {
// TODO: NotLikeThis
if (return_code != SQLITE_OK) {
Logger::write("SQLite error", Logger::LogLevel::Severe);
return false;
}
return true;
}
bool get_command(std::string guild_id, std::string command_name, Command &command) {
auto check_lambda = [guild_id, command_name](const Command &c) {
return guild_id == c.guild_id && command_name == c.command_name;
};
auto it = std::find_if(commands.begin(), commands.end(), check_lambda);
if (it == commands.end()) {
command = {};
return false;
}
else {
command = { it->guild_id, it->command_name, it->script };
return true;
}
}
// returns: 0 error, 1 inserted, 2 updated
int insert_command(std::string guild_id, std::string command_name, std::string script) {
// TODO: if script empty, delete command
Command command{ guild_id, command_name, script };
int ret_value;
std::string sql;
if (command_in_db(guild_id, command_name)) {
sql = "UPDATE CustomJS SET Script=?1 WHERE GuildID=?2 AND CommandName=?3;";
Logger::write("Command already exists, updating.", Logger::LogLevel::Debug);
ret_value = 2;
}
else {
sql = "INSERT INTO CustomJS(Script, GuildID, CommandName) VALUES (?1, ?2, ?3);";
Logger::write("Inserting new command.", Logger::LogLevel::Debug);
ret_value = 1;
}
sqlite3 *db; int return_code;
return_code = sqlite3_open("bot/db/trivia.db", &db);
if (!return_code_ok(return_code)) return 0;
sqlite3_stmt *stmt;
return_code = sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, 0);
if (!return_code_ok(return_code)) return 0;
return_code = sqlite3_bind_text(stmt, 1, script.c_str(), -1, (sqlite3_destructor_type)-1);
if (!return_code_ok(return_code)) return 0;
return_code = sqlite3_bind_text(stmt, 2, guild_id.c_str(), -1, (sqlite3_destructor_type)-1);
if (!return_code_ok(return_code)) return 0;
return_code = sqlite3_bind_text(stmt, 3, command_name.c_str(), -1, (sqlite3_destructor_type)-1);
if (!return_code_ok(return_code)) return 0;
return_code = sqlite3_step(stmt);
bool success = return_code == SQLITE_DONE;
sqlite3_finalize(stmt);
sqlite3_close(db);
if (success) {
if (ret_value == 1) {
commands.push_back({ guild_id, command_name, script });
}
if (ret_value == 2) {
// update command, don't add
auto check_lambda = [guild_id, command_name](const Command &c) {
return guild_id == c.guild_id && command_name == c.command_name;
};
auto it = std::find_if(commands.begin(), commands.end(), check_lambda);
if (it == commands.end()) {
return 0;
}
else {
it->script = script;
}
}
return ret_value;
}
return 0;
}
bool command_in_db(std::string guild_id, std::string command_name) {
sqlite3 *db; int return_code;
return_code = sqlite3_open("bot/db/trivia.db", &db);
if (!return_code_ok(return_code)) return false;
std::string sql = "SELECT EXISTS(SELECT 1 FROM CustomJS WHERE GuildID=?1 AND CommandName=?2);";
sqlite3_stmt *stmt;
return_code = sqlite3_prepare_v2(db, sql.c_str(), -1, &stmt, 0);
if (!return_code_ok(return_code)) return false;
return_code = sqlite3_bind_text(stmt, 1, guild_id.c_str(), -1, (sqlite3_destructor_type)-1);
if (!return_code_ok(return_code)) return false;
return_code = sqlite3_bind_text(stmt, 2, command_name.c_str(), -1, (sqlite3_destructor_type)-1);
if (!return_code_ok(return_code)) return false;
sqlite3_step(stmt);
bool exists = sqlite3_column_int(stmt, 0) == 1; // returns 1 (true) if exists
sqlite3_finalize(stmt);
sqlite3_close(db);
return exists;
}
}

View File

@ -0,0 +1,20 @@
#ifndef BOT_JS_COMMANDHELPER
#define BOT_JS_COMMANDHELPER
#include <string>
#include <vector>
namespace CommandHelper {
struct Command {
std::string guild_id;
std::string command_name;
std::string script;
};
void init();
int insert_command(std::string guild_id, std::string command_name, std::string script);
bool get_command(std::string guild_id, std::string name, Command &command);
bool command_in_db(std::string guild_id, std::string command_name);
}
#endif

733
Toast/bot/js/V8Instance.cpp Normal file
View File

@ -0,0 +1,733 @@
#include <iostream>
#include <string>
#include <chrono>
#include <algorithm>
#include "V8Instance.hpp"
#include "../DiscordAPI.hpp"
#include "../Logger.hpp"
#include "../BotConfig.hpp"
using namespace v8;
V8Instance::V8Instance(BotConfig &c, std::string guild_id, std::map<std::string, DiscordObjects::Guild> *guilds, std::map<std::string, DiscordObjects::Channel> *channels,
std::map<std::string, DiscordObjects::User> *users, std::map<std::string, DiscordObjects::Role> *roles) : config(c) {
rng = std::mt19937(std::random_device()());
this->guild_id = guild_id;
this->guilds = guilds;
this->channels = channels;
this->users = users;
this->roles = roles;
create();
}
void V8Instance::create() {
Isolate::CreateParams create_params;
create_params.array_buffer_allocator = ArrayBuffer::Allocator::NewDefaultAllocator();
isolate = Isolate::New(create_params);
isolate->Enter();
Logger::write("[v8] Created isolate", Logger::LogLevel::Debug);
Isolate::Scope isolate_scope(isolate);
HandleScope handle_scope(isolate);
// set global context
Local<Context> context = create_context();
context_.Reset(isolate, context);
Context::Scope context_scope(context);
initialise(context);
Logger::write("[v8] Created context and context scope", Logger::LogLevel::Debug);
}
void V8Instance::initialise(Local<Context> context) {
HandleScope handle_scope(isolate);
Local<Object> server_obj = wrap_server(&(*guilds)[guild_id]);
context->Global()->Set(
context,
String::NewFromUtf8(isolate, "server", NewStringType::kNormal).ToLocalChecked(),
server_obj
).FromJust();
Logger::write("[v8] Bound server template", Logger::LogLevel::Debug);
}
v8::Local<v8::Context> V8Instance::create_context() {
Local<ObjectTemplate> global = ObjectTemplate::New(isolate);
Local<External> self = External::New(isolate, (void *) this);
global->Set(
String::NewFromUtf8(isolate, "print", NewStringType::kNormal).ToLocalChecked(),
FunctionTemplate::New(isolate, V8Instance::js_print, self)
);
global->Set(
String::NewFromUtf8(isolate, "random", NewStringType::kNormal).ToLocalChecked(),
FunctionTemplate::New(isolate, V8Instance::js_random, self)
);
global->Set(
String::NewFromUtf8(isolate, "shuffle", NewStringType::kNormal).ToLocalChecked(),
FunctionTemplate::New(isolate, V8Instance::js_shuffle, self)
);
Logger::write("[v8] Created global context, added print function", Logger::LogLevel::Debug);
return Context::New(isolate, NULL, global);
}
/* server */
Local<ObjectTemplate> V8Instance::make_server_template() {
EscapableHandleScope handle_scope(isolate);
Local<ObjectTemplate> templ = ObjectTemplate::New(isolate);
templ->SetInternalFieldCount(1);
templ->SetHandler(
NamedPropertyHandlerConfiguration(
V8Instance::js_get_server,
(GenericNamedPropertySetterCallback) 0,
(GenericNamedPropertyQueryCallback) 0,
(GenericNamedPropertyDeleterCallback) 0,
(GenericNamedPropertyEnumeratorCallback) 0,
External::New(isolate, (void *) this)
)
);
return handle_scope.Escape(templ);
}
Local<Object> V8Instance::wrap_server(DiscordObjects::Guild *guild) {
EscapableHandleScope handle_scope(isolate);
if (server_template.IsEmpty()) {
Local<ObjectTemplate> raw_template = make_server_template();
server_template.Reset(isolate, raw_template);
}
Local<ObjectTemplate> templ = Local<ObjectTemplate>::New(isolate, server_template);
Local<Object> result = templ->NewInstance(isolate->GetCurrentContext()).ToLocalChecked();
Local<External> guild_ptr = External::New(isolate, guild);
result->SetInternalField(0, guild_ptr);
return handle_scope.Escape(result);
}
void V8Instance::js_get_server(Local<Name> property, const PropertyCallbackInfo<Value> &info) {
void *self_v = info.Data().As<External>()->Value();
if (!self_v) {
Logger::write("[v8] [js_get_server] Class pointer empty", Logger::LogLevel::Warning);
return;
}
V8Instance *self = static_cast<V8Instance *>(self_v);
void *guild_v = info.Holder()->GetInternalField(0).As<External>()->Value();
if (!guild_v) {
Logger::write("[v8] [js_get_server] Guild pointer empty", Logger::LogLevel::Warning);
return;
}
DiscordObjects::Guild *guild = static_cast<DiscordObjects::Guild *>(guild_v);
if (!property->IsString()) {
return;
}
std::string property_s = *String::Utf8Value(property);
if (property_s == "Id") {
info.GetReturnValue().Set(String::NewFromUtf8(info.GetIsolate(), guild->id.c_str(), NewStringType::kNormal).ToLocalChecked());
}
else if (property_s == "Name") {
info.GetReturnValue().Set(String::NewFromUtf8(info.GetIsolate(), guild->name.c_str(), NewStringType::kNormal).ToLocalChecked());
}
else if (property_s == "IconUrl") {
std::string icon_url = "https://discordapp.com/api/guilds/" + guild->id + "/icons/" + guild->icon + ".jpg";
info.GetReturnValue().Set(String::NewFromUtf8(info.GetIsolate(), icon_url.c_str(), NewStringType::kNormal).ToLocalChecked());
}
else if (property_s == "Owner") {
std::string owner_id = guild->owner_id;
DiscordObjects::GuildMember *owner = *std::find_if(guild->members.begin(), guild->members.end(), [owner_id](DiscordObjects::GuildMember *m) {
return owner_id == m->user->id;
});
Local<Object> owner_obj = self->wrap_user(owner);
info.GetReturnValue().Set(owner_obj);
}
else if (property_s == "Roles") {
Local<Object> roles_obj = self->wrap_role_list(&guild->roles);
info.GetReturnValue().Set(roles_obj);
}
else if (property_s == "Channels") {
Local<Object> channels_obj = self->wrap_channel_list(&guild->channels);
info.GetReturnValue().Set(channels_obj);
}
else if (property_s == "Users") {
Local<Object> users_obj = self->wrap_user_list(&guild->members);
info.GetReturnValue().Set(users_obj);
}
}
/* channel */
Local<ObjectTemplate> V8Instance::make_channel_template() {
EscapableHandleScope handle_scope(isolate);
Local<ObjectTemplate> templ = ObjectTemplate::New(isolate);
templ->SetInternalFieldCount(1);
templ->SetHandler(
NamedPropertyHandlerConfiguration(
V8Instance::js_get_channel,
(GenericNamedPropertySetterCallback) 0,
(GenericNamedPropertyQueryCallback) 0,
(GenericNamedPropertyDeleterCallback) 0,
(GenericNamedPropertyEnumeratorCallback) 0,
External::New(isolate, (void *) this)
)
);
return handle_scope.Escape(templ);
}
Local<Object> V8Instance::wrap_channel(DiscordObjects::Channel *channel) {
EscapableHandleScope handle_scope(isolate);
if (role_template.IsEmpty()) {
Local<ObjectTemplate> raw_template = make_channel_template();
channel_template.Reset(isolate, raw_template);
}
Local<ObjectTemplate> templ = Local<ObjectTemplate>::New(isolate, channel_template);
Local<Object> result = templ->NewInstance(isolate->GetCurrentContext()).ToLocalChecked();
Local<External> channel_ptr = External::New(isolate, channel);
result->SetInternalField(0, channel_ptr);
return handle_scope.Escape(result);
}
void V8Instance::js_get_channel(Local<Name> property, const PropertyCallbackInfo<Value> &info) {
void *channel_v = info.Holder()->GetInternalField(0).As<External>()->Value();
if (!channel_v) {
Logger::write("[v8] [js_get_channel] Channel pointer empty", Logger::LogLevel::Warning);
return;
}
DiscordObjects::Channel *channel = static_cast<DiscordObjects::Channel *>(channel_v);
if (!property->IsString()) {
return;
}
std::string property_s = *String::Utf8Value(property);
if (property_s == "Id") {
info.GetReturnValue().Set(String::NewFromUtf8(info.GetIsolate(), channel->id.c_str(), NewStringType::kNormal).ToLocalChecked());
}
else if (property_s == "Name") {
info.GetReturnValue().Set(String::NewFromUtf8(info.GetIsolate(), channel->name.c_str(), NewStringType::kNormal).ToLocalChecked());
}
else if (property_s == "Topic") {
info.GetReturnValue().Set(String::NewFromUtf8(info.GetIsolate(), channel->topic.c_str(), NewStringType::kNormal).ToLocalChecked());
}
else if (property_s == "IsVoice") {
info.GetReturnValue().Set(Boolean::New(info.GetIsolate(), channel->type == "voice"));
}
else if (property_s == "Users") {
info.GetIsolate()->ThrowException(String::NewFromUtf8(info.GetIsolate(), "Channel.Users not implemented.", NewStringType::kNormal).ToLocalChecked());
}
}
/* channel list */
Local<ObjectTemplate> V8Instance::make_channel_list_template() {
EscapableHandleScope handle_scope(isolate);
Local<ObjectTemplate> templ = ObjectTemplate::New(isolate);
templ->SetInternalFieldCount(1);
templ->SetHandler(
IndexedPropertyHandlerConfiguration(
V8Instance::js_get_channel_list,
(IndexedPropertySetterCallback) 0,
(IndexedPropertyQueryCallback) 0,
(IndexedPropertyDeleterCallback) 0,
(IndexedPropertyEnumeratorCallback) 0,
External::New(isolate, (void *) this)
)
);
return handle_scope.Escape(templ);
}
Local<Object> V8Instance::wrap_channel_list(std::vector<DiscordObjects::Channel *> *channel_list) {
EscapableHandleScope handle_scope(isolate);
if (channel_list_template.IsEmpty()) {
Local<ObjectTemplate> raw_template = make_channel_list_template();
channel_list_template.Reset(isolate, raw_template);
}
Local<ObjectTemplate> templ = Local<ObjectTemplate>::New(isolate, channel_list_template);
Local<Object> result = templ->NewInstance(isolate->GetCurrentContext()).ToLocalChecked();
// imitate an array
result->Set(String::NewFromUtf8(isolate, "length", NewStringType::kNormal).ToLocalChecked(), Integer::New(isolate, (*channel_list).size()));
result->SetPrototype(Array::New(isolate)->GetPrototype());
Local<External> channel_list_ptr = External::New(isolate, channel_list);
result->SetInternalField(0, channel_list_ptr);
return handle_scope.Escape(result);
}
void V8Instance::js_get_channel_list(uint32_t index, const PropertyCallbackInfo<Value> &info) {
void *self_v = info.Data().As<External>()->Value();
if (!self_v) {
Logger::write("[v8] [js_get_channel_list] Class pointer empty", Logger::LogLevel::Warning);
return;
}
V8Instance *self = static_cast<V8Instance *>(self_v);
void *channel_list_v = info.Holder()->GetInternalField(0).As<External>()->Value();
if (!channel_list_v) {
Logger::write("[v8] [js_get_channel_list] Channel List pointer empty", Logger::LogLevel::Warning);
return;
}
std::vector<DiscordObjects::Channel *> *channel_list = static_cast<std::vector<DiscordObjects::Channel *> *>(channel_list_v);
if (index < (*channel_list).size()) {
Local<Object> channel_obj = self->wrap_channel((*channel_list)[index]);
info.GetReturnValue().Set(channel_obj);
}
else {
info.GetReturnValue().SetUndefined();
}
}
/* user */
Local<ObjectTemplate> V8Instance::make_user_template() {
EscapableHandleScope handle_scope(isolate);
Local<ObjectTemplate> templ = ObjectTemplate::New(isolate);
templ->SetInternalFieldCount(1);
templ->SetHandler(
NamedPropertyHandlerConfiguration(
V8Instance::js_get_user,
(GenericNamedPropertySetterCallback)0,
(GenericNamedPropertyQueryCallback)0,
(GenericNamedPropertyDeleterCallback)0,
(GenericNamedPropertyEnumeratorCallback)0,
External::New(isolate, (void *) this)
)
);
return handle_scope.Escape(templ);
}
Local<Object> V8Instance::wrap_user(DiscordObjects::GuildMember *member) {
EscapableHandleScope handle_scope(isolate);
if (user_template.IsEmpty()) {
Local<ObjectTemplate> raw_template = make_user_template();
user_template.Reset(isolate, raw_template);
}
Local<ObjectTemplate> templ = Local<ObjectTemplate>::New(isolate, user_template);
Local<Object> result = templ->NewInstance(isolate->GetCurrentContext()).ToLocalChecked();
Local<External> member_ptr = External::New(isolate, member);
result->SetInternalField(0, member_ptr);
return handle_scope.Escape(result);
}
void V8Instance::js_get_user(Local<Name> property, const PropertyCallbackInfo<Value> &info) {
void *self_v = info.Data().As<External>()->Value();
if (!self_v) {
Logger::write("[v8] [js_get_user] Class pointer empty", Logger::LogLevel::Warning);
return;
}
V8Instance *self = static_cast<V8Instance *>(self_v);
void *member_v = info.Holder()->GetInternalField(0).As<External>()->Value();
if (!member_v) {
Logger::write("[v8] [js_get_user] GuildMember pointer empty", Logger::LogLevel::Warning);
return;
}
DiscordObjects::GuildMember *member = static_cast<DiscordObjects::GuildMember *>(member_v);
if (!property->IsString()) {
return;
}
std::string property_s = *String::Utf8Value(property);
if (property_s == "Id") {
info.GetReturnValue().Set(String::NewFromUtf8(info.GetIsolate(), member->user->id.c_str(), NewStringType::kNormal).ToLocalChecked());
}
else if (property_s == "Name") {
std::string name = member->nick == "null" ? member->user->username : member->nick;
info.GetReturnValue().Set(String::NewFromUtf8(info.GetIsolate(), name.c_str(), NewStringType::kNormal).ToLocalChecked());
}
else if (property_s == "TrueName") { // ignores nick
std::string name = member->user->username;
info.GetReturnValue().Set(String::NewFromUtf8(info.GetIsolate(), name.c_str(), NewStringType::kNormal).ToLocalChecked());
}
else if (property_s == "Mention") {
std::string mention = "<@" + member->user->id + ">";
info.GetReturnValue().Set(String::NewFromUtf8(info.GetIsolate(), mention.c_str(), NewStringType::kNormal).ToLocalChecked());
}
else if (property_s == "AvatarUrl") {
std::string avatar_url = "https://discordapp.com/api/users/" + member->user->id + "/avatars/" + member->user->avatar + ".jpg";
info.GetReturnValue().Set(String::NewFromUtf8(info.GetIsolate(), avatar_url.c_str(), NewStringType::kNormal).ToLocalChecked());
}
else if (property_s == "Roles") {
Local<Object> roles_obj = self->wrap_role_list(&member->roles);
info.GetReturnValue().Set(roles_obj);
}
else if (property_s == "State") {
info.GetReturnValue().Set(String::NewFromUtf8(info.GetIsolate(), member->user->status.c_str(), NewStringType::kNormal).ToLocalChecked());
}
else if (property_s == "CurrentGame") {
if (member->user->game == "null") {
info.GetReturnValue().SetNull();
} else {
info.GetReturnValue().Set(String::NewFromUtf8(info.GetIsolate(), member->user->game.c_str(), NewStringType::kNormal).ToLocalChecked());
}
}
}
/* user list */
Local<ObjectTemplate> V8Instance::make_user_list_template() {
EscapableHandleScope handle_scope(isolate);
Local<ObjectTemplate> templ = ObjectTemplate::New(isolate);
templ->SetInternalFieldCount(1);
templ->SetHandler(
IndexedPropertyHandlerConfiguration(
V8Instance::js_get_user_list,
(IndexedPropertySetterCallback) 0,
(IndexedPropertyQueryCallback) 0,
(IndexedPropertyDeleterCallback) 0,
(IndexedPropertyEnumeratorCallback) 0,
External::New(isolate, (void *) this)
)
);
return handle_scope.Escape(templ);
}
Local<Object> V8Instance::wrap_user_list(std::vector<DiscordObjects::GuildMember *> *user_list) {
EscapableHandleScope handle_scope(isolate);
if (user_list_template.IsEmpty()) {
Local<ObjectTemplate> raw_template = make_user_list_template();
user_list_template.Reset(isolate, raw_template);
}
Local<ObjectTemplate> templ = Local<ObjectTemplate>::New(isolate, user_list_template);
Local<Object> result = templ->NewInstance(isolate->GetCurrentContext()).ToLocalChecked();
// imitate an array
result->Set(String::NewFromUtf8(isolate, "length", NewStringType::kNormal).ToLocalChecked(), Integer::New(isolate, (*user_list).size()));
result->SetPrototype(Array::New(isolate)->GetPrototype());
Local<External> user_list_ptr = External::New(isolate, user_list);
result->SetInternalField(0, user_list_ptr);
return handle_scope.Escape(result);
}
void V8Instance::js_get_user_list(uint32_t index, const PropertyCallbackInfo<Value> &info) {
void *self_v = info.Data().As<External>()->Value();
if (!self_v) {
Logger::write("[v8] [js_get_user_list] Class pointer empty", Logger::LogLevel::Warning);
return;
}
V8Instance *self = static_cast<V8Instance *>(self_v);
void *user_list_v = info.Holder()->GetInternalField(0).As<External>()->Value();
if (!user_list_v) {
Logger::write("[v8] [js_get_user_list] GuildMember List pointer empty", Logger::LogLevel::Warning);
return;
}
std::vector<DiscordObjects::GuildMember *> *user_list = static_cast<std::vector<DiscordObjects::GuildMember *> *>(user_list_v);
if (index < (*user_list).size()) {
Local<Object> role_obj = self->wrap_user((*user_list)[index]);
info.GetReturnValue().Set(role_obj);
}
else {
info.GetReturnValue().SetUndefined();
}
}
/* role */
Local<ObjectTemplate> V8Instance::make_role_template() {
EscapableHandleScope handle_scope(isolate);
Local<ObjectTemplate> templ = ObjectTemplate::New(isolate);
templ->SetInternalFieldCount(1);
templ->SetHandler(
NamedPropertyHandlerConfiguration(
V8Instance::js_get_role,
(GenericNamedPropertySetterCallback)0,
(GenericNamedPropertyQueryCallback)0,
(GenericNamedPropertyDeleterCallback)0,
(GenericNamedPropertyEnumeratorCallback)0,
External::New(isolate, (void *) this)
)
);
return handle_scope.Escape(templ);
}
Local<Object> V8Instance::wrap_role(DiscordObjects::Role *role) {
EscapableHandleScope handle_scope(isolate);
if (role_template.IsEmpty()) {
Local<ObjectTemplate> raw_template = make_role_template();
role_template.Reset(isolate, raw_template);
}
Local<ObjectTemplate> templ = Local<ObjectTemplate>::New(isolate, role_template);
Local<Object> result = templ->NewInstance(isolate->GetCurrentContext()).ToLocalChecked();
Local<External> role_ptr = External::New(isolate, role);
result->SetInternalField(0, role_ptr);
return handle_scope.Escape(result);
}
void V8Instance::js_get_role(Local<Name> property, const PropertyCallbackInfo<Value> &info) {
void *role_v = info.Holder()->GetInternalField(0).As<External>()->Value();
if (!role_v) {
Logger::write("[v8] [js_get_role] Role pointer empty", Logger::LogLevel::Warning);
return;
}
DiscordObjects::Role *role = static_cast<DiscordObjects::Role *>(role_v);
if (!property->IsString()) {
return;
}
std::string property_s = *String::Utf8Value(property);
if (property_s == "Id") {
info.GetReturnValue().Set(String::NewFromUtf8(info.GetIsolate(), role->id.c_str(), NewStringType::kNormal).ToLocalChecked());
}
else if (property_s == "Name") {
info.GetReturnValue().Set(String::NewFromUtf8(info.GetIsolate(), role->name.c_str(), NewStringType::kNormal).ToLocalChecked());
}
else if (property_s == "Position") {
info.GetReturnValue().Set(Integer::New(info.GetIsolate(), role->position));
}
else if (property_s == "Red" || property_s == "Green" || property_s == "Blue") {
info.GetIsolate()->ThrowException(String::NewFromUtf8(info.GetIsolate(), "Role.[Colour] not implemented.", NewStringType::kNormal).ToLocalChecked());
}
}
/* role list */
Local<ObjectTemplate> V8Instance::make_role_list_template() {
EscapableHandleScope handle_scope(isolate);
Local<ObjectTemplate> templ = ObjectTemplate::New(isolate);
templ->SetInternalFieldCount(1);
templ->SetHandler(
IndexedPropertyHandlerConfiguration(
V8Instance::js_get_role_list,
(IndexedPropertySetterCallback) 0,
(IndexedPropertyQueryCallback) 0,
(IndexedPropertyDeleterCallback) 0,
(IndexedPropertyEnumeratorCallback) 0,
External::New(isolate, (void *) this)
)
);
return handle_scope.Escape(templ);
}
Local<Object> V8Instance::wrap_role_list(std::vector<DiscordObjects::Role *> *role_list) {
EscapableHandleScope handle_scope(isolate);
if (role_list_template.IsEmpty()) {
Local<ObjectTemplate> raw_template = make_role_list_template();
role_list_template.Reset(isolate, raw_template);
}
Local<ObjectTemplate> templ = Local<ObjectTemplate>::New(isolate, role_list_template);
Local<Object> result = templ->NewInstance(isolate->GetCurrentContext()).ToLocalChecked();
// imitate an array
result->Set(String::NewFromUtf8(isolate, "length", NewStringType::kNormal).ToLocalChecked(), Integer::New(isolate, (*role_list).size()));
result->SetPrototype(Array::New(isolate)->GetPrototype());
Local<External> role_list_ptr = External::New(isolate, role_list);
result->SetInternalField(0, role_list_ptr);
return handle_scope.Escape(result);
}
void V8Instance::js_get_role_list(uint32_t index, const PropertyCallbackInfo<Value> &info) {
void *self_v = info.Data().As<External>()->Value();
if (!self_v) {
Logger::write("[v8] [js_get_role_list] Class pointer empty", Logger::LogLevel::Warning);
return;
}
V8Instance *self = static_cast<V8Instance *>(self_v);
void *role_list_v = info.Holder()->GetInternalField(0).As<External>()->Value();
if (!role_list_v) {
Logger::write("[v8] [js_get_role_list] Role List pointer empty", Logger::LogLevel::Warning);
return;
}
std::vector<DiscordObjects::Role *> *role_list = static_cast<std::vector<DiscordObjects::Role *> *>(role_list_v);
if (index < (*role_list).size()) {
Local<Object> role_obj = self->wrap_role((*role_list)[index]);
info.GetReturnValue().Set(role_obj);
}
else {
info.GetReturnValue().SetUndefined();
}
}
/* global functions */
void V8Instance::js_print(const v8::FunctionCallbackInfo<v8::Value> &args) {
auto data = args.Data().As<External>();
V8Instance *self = static_cast<V8Instance *>(data->Value());
std::string output = "";
for (int i = 0; i < args.Length(); i++) {
v8::String::Utf8Value str(args[i]);
self->print_text += *str;
}
self->print_text += "\n";
}
void V8Instance::js_random(const v8::FunctionCallbackInfo<v8::Value> &args) {
auto data = args.Data().As<External>();
V8Instance *self = static_cast<V8Instance *>(data->Value());
int number_args = args.Length();
if (number_args == 0) {
std::uniform_real_distribution<double> dist(0, 1);
double random_val = dist(self->rng);
args.GetReturnValue().Set(Number::New(args.GetIsolate(), random_val));
}
else if (number_args == 1) {
int64_t max = args[0]->IntegerValue();
std::uniform_int_distribution<int> dist(0, max);
int random_val = dist(self->rng);
args.GetReturnValue().Set(Integer::New(args.GetIsolate(), random_val));
}
else if (number_args == 2) {
int64_t min = args[0]->IntegerValue();
int64_t max = args[1]->IntegerValue();
std::uniform_int_distribution<int> dist(min, max);
int random_val = dist(self->rng);
args.GetReturnValue().Set(Integer::New(args.GetIsolate(), random_val));
}
else {
std::string err_msg = "random() requires 0-2 arguments. You gave: " + std::to_string(number_args);
args.GetIsolate()->ThrowException(String::NewFromUtf8(args.GetIsolate(), err_msg.c_str(), NewStringType::kNormal).ToLocalChecked());
}
}
void V8Instance::js_shuffle(const v8::FunctionCallbackInfo<v8::Value> &args) {
auto data = args.Data().As<External>();
V8Instance *self = static_cast<V8Instance *>(data->Value());
if (!args[0]->IsArray()) {
std::string err_msg = "shuffle() requires an array as it's argument. You gave: " + std::string(*String::Utf8Value(args[0]->TypeOf(args.GetIsolate())));
args.GetIsolate()->ThrowException(String::NewFromUtf8(args.GetIsolate(), err_msg.c_str(), NewStringType::kNormal).ToLocalChecked());
}
else {
Local<Array> given_arr = Local<Array>::Cast(args[0]);
const int length = given_arr->Length();
Local<Array> return_arr = Array::New(args.GetIsolate(), length);
std::vector<Local<Value>> cpp_arr;
for (uint32_t i = 0; i < given_arr->Length(); i++) {
cpp_arr.push_back(given_arr->Get(i));
}
std::shuffle(cpp_arr.begin(), cpp_arr.end(), self->rng);
for (uint32_t i = 0; i < given_arr->Length(); i++) {
return_arr->Set(i, cpp_arr[i]);
}
args.GetReturnValue().Set(return_arr);
}
}
void V8Instance::exec_js(std::string js, DiscordObjects::Channel *channel, DiscordObjects::GuildMember *sender, std::string args) {
HandleScope handle_scope(isolate);
Local<Context> context = Local<Context>::New(isolate, context_);
Context::Scope context_scope(context);
context->Global()->Set(
String::NewFromUtf8(isolate, "input", NewStringType::kNormal).ToLocalChecked(),
String::NewFromUtf8(isolate, args.c_str(), NewStringType::kNormal).ToLocalChecked()
);
Local<Object> user_obj = wrap_user(sender);
context->Global()->Set(
String::NewFromUtf8(isolate, "user", NewStringType::kNormal).ToLocalChecked(),
user_obj
);
Local<Object> channel_obj = wrap_channel(channel);
context->Global()->Set(
String::NewFromUtf8(isolate, "channel", NewStringType::kNormal).ToLocalChecked(),
channel_obj
);
// TODO: 'message' object here too, although it's fairly pointless
current_sender = sender;
current_channel = channel;
Logger::write("[v8] Preparing JS (guild " + (*guilds)[guild_id].id + ", channel " + channel->id + ")", Logger::LogLevel::Debug);
Local<String> source = String::NewFromUtf8(isolate, js.c_str(), NewStringType::kNormal).ToLocalChecked();
// compile
Logger::write("[v8] Isolate nullptr? " + std::to_string(isolate == nullptr) + " Context empty? " + std::to_string(context.IsEmpty()), Logger::LogLevel::Debug);
TryCatch compile_try_catch(isolate);
Local<Script> script;
auto begin = std::chrono::steady_clock::now();
if (!Script::Compile(context, source).ToLocal(&script)) {
String::Utf8Value error(compile_try_catch.Exception());
std::string err_msg = *error;
Logger::write("[v8] Compilation error: " + err_msg, Logger::LogLevel::Debug);
DiscordAPI::send_message(channel->id, ":warning: **Compilation error:** `" + err_msg + "`", config.token, config.cert_location);
return;
}
TryCatch run_try_catch(isolate);
MaybeLocal<Value> v = script->Run(context);
if (v.IsEmpty()) {
String::Utf8Value error(run_try_catch.Exception());
std::string err_msg = *error;
Logger::write("[v8] Runtime error: " + err_msg, Logger::LogLevel::Debug);
DiscordAPI::send_message(channel->id, ":warning: **Runtime error:** `" + err_msg + "`", config.token, config.cert_location);
}
auto end = std::chrono::steady_clock::now();
long long time_taken = std::chrono::duration_cast<std::chrono::milliseconds>(end - begin).count();
Logger::write("[v8] Script compiled and run in " + std::to_string(time_taken) + "ms", Logger::LogLevel::Debug);
current_sender = nullptr;
current_channel = nullptr;
if (print_text != "") {
DiscordAPI::send_message(channel->id, print_text, config.token, config.cert_location);
print_text = "";
}
}

View File

@ -0,0 +1,99 @@
#ifndef BOT_JS_V8INSTANCE
#define BOT_JS_V8INSTANCE
#include <memory>
#include <map>
#include <random>
#include <include/v8.h>
#include <include/libplatform/libplatform.h>
#include "../data_structures/Guild.hpp"
#include "../data_structures/Channel.hpp"
#include "../data_structures/Role.hpp"
#include "../data_structures/GuildMember.hpp"
#include "../data_structures/User.hpp"
class BotConfig;
class V8Instance {
public:
V8Instance(BotConfig &c, std::string guild_id, std::map<std::string, DiscordObjects::Guild> *guilds,
std::map<std::string, DiscordObjects::Channel> *channels, std::map<std::string, DiscordObjects::User> *users, std::map<std::string, DiscordObjects::Role> *roles);
void exec_js(std::string js, DiscordObjects::Channel *channel, DiscordObjects::GuildMember *sender, std::string args = "");
private:
BotConfig &config;
void create();
v8::Local<v8::Context> create_context();
void initialise(v8::Local<v8::Context> context);
/* server */
v8::Global<v8::ObjectTemplate> server_template;
v8::Local<v8::ObjectTemplate> make_server_template();
v8::Local<v8::Object> wrap_server(DiscordObjects::Guild *guild);
static void js_get_server(v8::Local<v8::Name> property, const v8::PropertyCallbackInfo<v8::Value> &info);
/* user */
v8::Global<v8::ObjectTemplate> user_template;
v8::Local<v8::ObjectTemplate> make_user_template();
v8::Local<v8::Object> wrap_user(DiscordObjects::GuildMember *member);
static void js_get_user(v8::Local<v8::Name> property, const v8::PropertyCallbackInfo<v8::Value> &info);
v8::Global<v8::ObjectTemplate> user_list_template;
v8::Local<v8::ObjectTemplate> make_user_list_template();
v8::Local<v8::Object> wrap_user_list(std::vector<DiscordObjects::GuildMember *> *user_list);
static void js_get_user_list(uint32_t index, const v8::PropertyCallbackInfo<v8::Value> &info);
/* channel */
v8::Global<v8::ObjectTemplate> channel_template;
v8::Local<v8::ObjectTemplate> make_channel_template();
v8::Local<v8::Object> wrap_channel(DiscordObjects::Channel *channel);
static void js_get_channel(v8::Local<v8::Name> property, const v8::PropertyCallbackInfo<v8::Value> &info);
v8::Global<v8::ObjectTemplate> channel_list_template;
v8::Local<v8::ObjectTemplate> make_channel_list_template();
v8::Local<v8::Object> wrap_channel_list(std::vector<DiscordObjects::Channel *> *channel_list);
static void js_get_channel_list(uint32_t index, const v8::PropertyCallbackInfo<v8::Value> &info);
/* role */
v8::Global<v8::ObjectTemplate> role_template;
v8::Local<v8::ObjectTemplate> make_role_template();
v8::Local<v8::Object> wrap_role(DiscordObjects::Role *role);
static void js_get_role(v8::Local<v8::Name> property, const v8::PropertyCallbackInfo<v8::Value> &info);
v8::Global<v8::ObjectTemplate> role_list_template;
v8::Local<v8::ObjectTemplate> make_role_list_template();
v8::Local<v8::Object> wrap_role_list(std::vector<DiscordObjects::Role *> *role_list);
static void js_get_role_list(uint32_t index, const v8::PropertyCallbackInfo<v8::Value> &info);
/* print function */
static void js_print(const v8::FunctionCallbackInfo<v8::Value> &args);
/* randomness functions */
static void js_random(const v8::FunctionCallbackInfo<v8::Value> &args);
static void js_shuffle(const v8::FunctionCallbackInfo<v8::Value> &args);
std::map<std::string, DiscordObjects::Guild> *guilds;
std::map<std::string, DiscordObjects::Channel> *channels;
std::map<std::string, DiscordObjects::User> *users;
std::map<std::string, DiscordObjects::Role> *roles;
std::string guild_id;
v8::Isolate *isolate;
v8::Global<v8::Context> context_;
/* random generating variables */
std::mt19937 rng;
/* variables which change when a new command is executed */
std::string print_text;
DiscordObjects::Channel *current_channel;
DiscordObjects::GuildMember *current_sender;
};
#endif

View File

@ -16,17 +16,8 @@
/ Hideous code, but only needs to be run one time.
**/
static int callback(void *x, int argc, char **argv, char **azColName) {
int i;
for (i = 0; i<argc; i++) {
std::cout << azColName[i] << " = " << (argv[i] ? argv[i] : "NULL") << std::endl;
}
return 0;
}
int load_questions() {
sqlite3 *db;
char *zErrMsg = 0;
int rc;
rc = sqlite3_open("bot/db/trivia.db", &db);

View File

@ -1,33 +0,0 @@
#include "http/HTTPHelper.hpp"
#include <cstdio>
#include <thread>
#include <chrono>
#include "APIHelper.hpp"
using namespace std::chrono_literals;
APIHelper::APIHelper() : BASE_URL("https://discordapp.com/api"), CHANNELS_URL(BASE_URL + "/channels"),
JSON_CTYPE("application/json") {
http = new HTTPHelper();
}
void APIHelper::send_message(std::string channel_id, std::string message) {
const std::string url = CHANNELS_URL + "/" + channel_id + "/messages";
json data = {
{ "content", message }
};
std::string response;
long response_code = -1;
response = http->post_request(url, JSON_CTYPE, data.dump(), &response_code);
int retries = 0;
while (response_code != 200 && retries < 2) {
std::this_thread::sleep_for(100ms);
// try 3 times. usually enough to prevent 502 bad gateway issues
response = http->post_request(url, JSON_CTYPE, data.dump(), &response_code);
retries++;
}
}

View File

@ -1,26 +0,0 @@
#ifndef BOT_APIHELPER
#define BOT_APIHELPER
#include <string>
#include "json/json.hpp"
using json = nlohmann::json;
class HTTPHelper;
class APIHelper {
public:
APIHelper();
void send_message(std::string channel_id, std::string message);
private:
const std::string BASE_URL;
const std::string CHANNELS_URL;
const std::string JSON_CTYPE;
HTTPHelper *http;
};
#endif

View File

@ -1,196 +0,0 @@
#include "GatewayHandler.hpp"
#include <boost/algorithm/string.hpp>
#include "APIHelper.hpp"
#include "data_structures/User.hpp"
extern std::string bot_token;
GatewayHandler::GatewayHandler() {
last_seq = 0;
ah = new APIHelper();
}
void GatewayHandler::handle_data(std::string data, client &c, websocketpp::connection_hdl &hdl) {
json decoded = json::parse(data);
int op = decoded["op"];
switch (op) {
case 0: // Event dispatch
on_dispatch(decoded, c, hdl);
break;
case 10: // Hello
on_hello(decoded, c, hdl);
break;
case 11:
c.get_alog().write(websocketpp::log::alevel::app, "Heartbeat acknowledged.");
break;
}
}
void GatewayHandler::heartbeat(client *c, websocketpp::connection_hdl hdl, int interval) {
while (true) {
boost::this_thread::sleep(boost::posix_time::milliseconds(interval));
json heartbeat = {
{ "op", 1 },
{ "d", last_seq }
};
c->send(hdl, heartbeat.dump(), websocketpp::frame::opcode::text);
c->get_alog().write(websocketpp::log::alevel::app, "Sent heartbeat. (seq: " + std::to_string(last_seq) + ")");
}
}
void GatewayHandler::on_hello(json decoded, client &c, websocketpp::connection_hdl &hdl) {
heartbeat_interval = decoded["d"]["heartbeat_interval"];
c.get_alog().write(websocketpp::log::alevel::app, "Heartbeat interval: " + std::to_string(heartbeat_interval / 1000.0f) + " seconds");
heartbeat_thread = std::make_unique<boost::thread>(boost::bind(&GatewayHandler::heartbeat, this, &c, hdl, heartbeat_interval));
identify(c, hdl);
}
void GatewayHandler::on_dispatch(json decoded, client &c, websocketpp::connection_hdl &hdl) {
last_seq = decoded["s"];
std::string event_name = decoded["t"];
json data = decoded["d"];
if (event_name == "READY") {
user_object.load_from_json(data["user"]);
c.get_alog().write(websocketpp::log::alevel::app, "Sign-on confirmed. (@" + user_object.username + "#" + user_object.discriminator + ")");
}
else if (event_name == "GUILD_CREATE") {
std::string guild_id = data["id"];
try {
guilds[guild_id] = std::make_unique<DiscordObjects::Guild>(data);
}
catch (std::domain_error err) {
// this doesn't even work
c.get_alog().write(websocketpp::log::elevel::rerror, "Domain error");
}
c.get_alog().write(websocketpp::log::alevel::app, "Loaded guild: " + guilds[guild_id]->name);
for (json channel : data["channels"]) {
std::string channel_id = channel["id"];
channel["guild_id"] = guild_id;
// create channel obj, add to overall channel list
channels[channel_id] = std::make_shared<DiscordObjects::Channel>(channel);
// add ptr to said channel list to guild's channel list
guilds[guild_id]->channels.push_back(std::shared_ptr<DiscordObjects::Channel>(channels[channel_id]));
}
}
else if (event_name == "TYPING_START") {}
else if (event_name == "MESSAGE_CREATE") {
std::string message = data["content"];
auto channel = channels[data["channel_id"]];
DiscordObjects::User sender(data["author"]);
std::vector<std::string> words;
boost::split(words, message, boost::is_any_of(" "));
if (words[0] == "`trivia" || words[0] == "`t") {
int questions = 10;
int delay = 8;
if (words.size() > 3) {
ah->send_message(channel->id, ":exclamation: Invalid arguments!");
return;
}
else if(words.size() > 1) {
if (words[1] == "help" || words[1] == "h") {
std::string help = "**Base command \\`t[rivia]**. Arguments:\n";
help += "\\`trivia **{x}** **{y}**: Makes the game last **x** number of questions, optionally sets the time interval between hints to **y** seconds\n";
help += "\\`trivia **stop**: stops the ongoing game.\n";
help += "\\`trivia **help**: prints this message\n";
ah->send_message(channel->id, help);
return;
}
else if (words[1] == "stop" || words[1] == "s") {
if (games.find(channel->id) != games.end()) {
delete_game(channel->id);
return;
}
}
else {
try {
questions = std::stoi(words[1]);
if (words.size() == 3) {
delay = std::stoi(words[2]);
}
}
catch (std::invalid_argument e) {
ah->send_message(channel->id, ":exclamation: Invalid arguments!");
return;
}
}
}
games[channel->id] = std::make_unique<TriviaGame>(this, ah, channel->id, questions, delay);
games[channel->id]->start();
}
else if (words[0] == "`channels") {
std::string m = "Channel List:\n";
for (auto ch : channels) {
m += "> " + ch.second->name + " (" + ch.second->id + ") [" + ch.second->type + "] Guild: "
+ guilds[ch.second->guild_id]->name + " (" + ch.second->guild_id + ")\n";
}
ah->send_message(channel->id, m);
}
else if (words[0] == "`guilds") {
std::string m = "Guild List:\n";
for (auto &gu : guilds) {
m += "> " + gu.second->name + " (" + gu.second->id + ") Channels: " + std::to_string(gu.second->channels.size()) + "\n";
}
ah->send_message(channel->id, m);
}
else if (words[0] == "`info") {
ah->send_message(channel->id, ":information_source: trivia-bot by Jack. <http://github.com/jackb-p/TriviaDiscord>");
}
else if (games.find(channel->id) != games.end()) { // message received in channel with ongoing game
games[channel->id]->handle_answer(message, sender);
}
}
}
void GatewayHandler::identify(client &c, websocketpp::connection_hdl &hdl) {
json identify = {
{ "op", 2 },
{ "d", {
{ "token", bot_token },
{ "properties", {
{ "$browser", "Microsoft Windows 10" },
{ "$device", "TriviaBot-0.0" },
{ "$referrer", "" },
{ "$referring_domain", "" }
} },
{ "compress", false },
{ "large_threshold", 250 },
{ "shard", { 0, 1 } }
} }
};
c.send(hdl, identify.dump(), websocketpp::frame::opcode::text);
c.get_alog().write(websocketpp::log::alevel::app, "Sent identify payload.");
}
void GatewayHandler::delete_game(std::string channel_id) {
auto it = games.find(channel_id);
if (it != games.end()) {
it->second->interrupt();
// remove from map
games.erase(it);
} else {
std::cerr << "Tried to delete a game that didn't exist.";
}
}

View File

@ -1,77 +0,0 @@
#ifndef BOT_GATEWAYHANDLER
#define BOT_GATEWAYHANDLER
#include <map>
#include <string>
#include <websocketpp/client.hpp>
#include <websocketpp/config/asio_client.hpp>
#include "json/json.hpp"
#include "TriviaGame.hpp"
#include "data_structures/User.hpp"
#include "data_structures/Guild.hpp"
#include "data_structures/Channel.hpp"
typedef websocketpp::client<websocketpp::config::asio_tls_client> client;
using json = nlohmann::json;
/************ Opcodes **************************************************************************************************
* Code | Name | Description *
* --------------------------------------------------------------------------------------------------------------------------*
* 0 | Dispatch | dispatches an event *
* 1 | Heartbeat | used for ping checking *
* 2 | Identify | used for client handshake *
* 3 | Status Update | used to update the client status *
* 4 | Voice State Update | used to join/move/leave voice channels *
* 5 | Voice Server Ping | used for voice ping checking *
* 6 | Resume | used to resume a closed connection *
* 7 | Reconnect | used to tell clients to reconnect to the gateway *
* 8 | Request Guild Members | used to request guild members *
* 9 | Invalid Session | used to notify client they have an invalid session id *
* 10 | Hello | sent immediately after connecting, contains heartbeat and server debug information *
* 11 | Heartback ACK | sent immediately following a client heartbeat that was received *
*****************************************************************************************************************************/
class TriviaGame;
class APIHelper;
class GatewayHandler {
public:
GatewayHandler();
void handle_data(std::string data, client &c, websocketpp::connection_hdl &hdl);
void heartbeat(client *c, websocketpp::connection_hdl hdl, int interval);
void on_hello(json decoded, client &c, websocketpp::connection_hdl &hdl);
void on_dispatch(json decoded, client &c, websocketpp::connection_hdl &hdl);
void identify(client &c, websocketpp::connection_hdl &hdl);
void delete_game(std::string channel_id);
private:
int last_seq;
int heartbeat_interval;
const int protocol_version = 5;
// bot's user obj
DiscordObjects::User user_object;
// <id, ptr to data>
std::map<std::string, std::unique_ptr<DiscordObjects::Guild>> guilds;
// channels pointers are shared pointers, held here but also in guild objects
std::map<std::string, std::shared_ptr<DiscordObjects::Channel>> channels;
// <channel_id, game obj>
std::map<std::string, std::unique_ptr<TriviaGame>> games;
std::unique_ptr<boost::thread> heartbeat_thread;
APIHelper *ah;
};
#endif

View File

@ -1,40 +0,0 @@
#include <curl/curl.h>
#include "ClientConnection.hpp"
std::string bot_token;
int main(int argc, char *argv[]) {
curl_global_init(CURL_GLOBAL_DEFAULT);
if (argc == 2) {
bot_token = argv[1];
}
else {
std::cout << "Please enter your bot token: " << std::endl;
std::cin >> bot_token;
}
// todo: get this using API
std::string uri = "wss://gateway.discord.gg/?v=5&encoding=json";
try {
ClientConnection conn;
conn.start(uri);
}
catch (const std::exception & e) {
std::cerr << e.what() << std::endl;
}
catch (websocketpp::lib::error_code e) {
std::cerr << e.message() << std::endl;
}
catch (...) {
std::cerr << "other exception" << std::endl;
}
std::getchar();
curl_global_cleanup();
return 0;
}

View File

@ -1,125 +0,0 @@
Example data:
!!!!!!!!!!!!!!!!! GUILD_CREATE Event
{
"afk_channel_id": null,
"afk_timeout": 300,
"channels": [
{
"id": "200398901767962624",
"is_private": false,
"last_message_id": "201355522635595776",
"name": "general",
"permission_overwrites": [],
"position": 0,
"topic": "",
"type": "text"
},
{
"bitrate": 64000,
"id": "200398901767962625",
"is_private": false,
"name": "General",
"permission_overwrites": [],
"position": 0,
"type": "voice",
"user_limit": 0
}
],
"default_message_notifications": 0,
"emojis": [],
"features": [],
"icon": null,
"id": "200398901767962624",
"joined_at": "2016-07-06T23:54:20.824000+00:00",
"large": false,
"member_count": 2,
"members": [
{
"deaf": false,
"joined_at": "2016-07-06T23:53:41.425000+00:00",
"mute": false,
"roles": [
"200399346498273280"
],
"user": {
"avatar": "1dc076d2d273615dd23546c86dbdfd9c",
"discriminator": "8212",
"id": "82232146579689472",
"username": "Jack"
}
},
{
"deaf": false,
"joined_at": "2016-07-06T23:54:20.824000+00:00",
"mute": false,
"roles": [
"200399601507893248"
],
"user": {
"avatar": "e871ceecaa362718af6d3174bc941977",
"bot": true,
"discriminator": "8194",
"id": "199657095258177539",
"username": "trivia-bot"
}
}
],
"mfa_level": 0,
"name": "EleGiggle",
"owner_id": "82232146579689472",
"presences": [
{
"game": null,
"status": "online",
"user": {
"id": "82232146579689472"
}
},
{
"game": null,
"status": "online",
"user": {
"id": "199657095258177539"
}
}
],
"region": "london",
"roles": [
{
"color": 0,
"hoist": false,
"id": "200398901767962624",
"managed": false,
"mentionable": false,
"name": "@everyone",
"permissions": 36953089,
"position": 0
},
{
"color": 3066993,
"hoist": true,
"id": "200399346498273280",
"managed": false,
"mentionable": false,
"name": "All Perms",
"permissions": 506715199,
"position": 1
},
{
"color": 15844367,
"hoist": true,
"id": "200399601507893248",
"managed": false,
"mentionable": false,
"name": "Robot",
"permissions": 536083519,
"position": 1
}
],
"splash": null,
"unavailable": false,
"verification_level": 0,
"voice_states": []
}

View File

@ -1,14 +0,0 @@
BEGIN TRANSACTION;
CREATE TABLE "TotalScores" (
`User` TEXT UNIQUE,
`TotalScore` INTEGER,
`AverageTime` INTEGER,
PRIMARY KEY(User)
);
CREATE TABLE "Questions" (
`ID` INTEGER PRIMARY KEY AUTOINCREMENT,
`Category` TEXT,
`Question` TEXT,
`Answer` TEXT
);
COMMIT;

View File

@ -1,57 +0,0 @@
#include "HTTPHelper.hpp"
extern std::string bot_token;
/*
* Warning: (Awful) C Code
*/
std::string HTTPHelper::post_request(std::string url, std::string content_type, std::string data, long *response_code) {
CURL *curl;
CURLcode res;
std::string read_buffer;
struct curl_slist *headers = nullptr;
curl = curl_easy_init();
if (curl) {
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
// Now with real HTTPS!
curl_easy_setopt(curl, CURLOPT_CAINFO, "bot/http/DiscordCA.crt");
std::string header_arr[3];
header_arr[0] = "Content-Type: " + content_type;
header_arr[1] = "Authorization: Bot " + bot_token;
header_arr[2] = "User-Agent: DiscordBot(http://github.com/jackb-p/triviadiscord, 1.0)";
for (std::string h : header_arr) {
headers = curl_slist_append(headers, h.c_str());
}
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &read_buffer);
res = curl_easy_perform(curl);
if (res == CURLE_OK) {
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, response_code);
} else {
return "";
}
/* always cleanup */
curl_easy_cleanup(curl);
curl_slist_free_all(headers);
}
return read_buffer;
}
size_t HTTPHelper::write_callback(void *contents, size_t size, size_t nmemb, void *userp) {
((std::string *) userp)->append((char *) contents, size * nmemb);
return size * nmemb;
}

View File

@ -1,16 +0,0 @@
#ifndef BOT_HTTP_HTTPHELPER
#define BOT_HTTP_HTTPHELPER
#include <iostream>
#include <curl/curl.h>
class HTTPHelper {
public:
std::string post_request(std::string url, std::string content_type, std::string data, long *response_code);
private:
static size_t write_callback(void *contents, size_t size, size_t nmemb, void *userp);
};
#endif