commit 980c9ffb954ebddd60f503eb1ca76d8a74b22450 Author: niansa Date: Sun Jan 10 17:29:59 2021 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..613beba --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +build +cmake-build-debug +.idea +CMakeLists.txt.user diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..3b1061e --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,68 @@ +cmake_minimum_required(VERSION 3.5) +project(discordlistforbots CXX) + +include(CheckIncludeFileCXX) + +check_include_file_cxx(any HAS_ANY) +check_include_file_cxx(string_view HAS_STRING_VIEW) +if(HAS_ANY AND HAS_STRING_VIEW) + set(CMAKE_CXX_STANDARD 17) +else() + set(CMAKE_CXX_STANDARD 14) +endif() + +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +add_executable(${PROJECT_NAME} main.cc) + +# ############################################################################## +# If you include the drogon source code locally in your project, use this method +# to add drogon +# add_subdirectory(drogon) +# target_link_libraries(${PROJECT_NAME} PRIVATE drogon) +# ############################################################################## + +find_package(Drogon CONFIG REQUIRED) +target_link_libraries(${PROJECT_NAME} PRIVATE Drogon::Drogon) + +if(CMAKE_CXX_STANDARD LESS 17) + # With C++14, use boost to support any and string_view + message(STATUS "use c++14") + find_package(Boost 1.61.0 REQUIRED) + target_include_directories(${PROJECT_NAME} PRIVATE ${Boost_INCLUDE_DIRS}) +else() + message(STATUS "use c++17") +endif() + +aux_source_directory(controllers CTL_SRC) +aux_source_directory(filters FILTER_SRC) +aux_source_directory(plugins PLUGIN_SRC) +aux_source_directory(models MODEL_SRC) + +drogon_create_views(${PROJECT_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/views + ${CMAKE_CURRENT_BINARY_DIR}) +# use the following line to create views with namespaces. +# drogon_create_views(${PROJECT_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/views +# ${CMAKE_CURRENT_BINARY_DIR} TRUE) + +target_include_directories(${PROJECT_NAME} + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/models) +target_sources(${PROJECT_NAME} + PRIVATE + ${SRC_DIR} + ${CTL_SRC} + ${FILTER_SRC} + ${PLUGIN_SRC} + ${MODEL_SRC}) +# ############################################################################## +# uncomment the following line for dynamically loading views +# set_property(TARGET ${PROJECT_NAME} PROPERTY ENABLE_EXPORTS ON) + +file(GLOB files "static/*") +foreach(file ${files}) + get_filename_component(filename "${file}" NAME) + message("${file}: ${filename}") + configure_file("${file}" "./${filename}" COPYONLY) +endforeach() diff --git a/bots.sql b/bots.sql new file mode 100644 index 0000000..cb13120 --- /dev/null +++ b/bots.sql @@ -0,0 +1,57 @@ +-- +-- PostgreSQL database dump +-- + +-- Dumped from database version 13.1 (Ubuntu 13.1-1.pgdg20.04+1) +-- Dumped by pg_dump version 13.1 (Ubuntu 13.1-1.pgdg20.04+1) + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: bots; Type: TABLE; Schema: public; Owner: nils +-- + +CREATE TABLE public.bots ( + name text, + short_description text, + long_description text, + avatar_url text, + owner text, + support_server text, + prefix text, + owner_id text, + app_id text, + votes integer, + approved boolean +); + + +ALTER TABLE public.bots OWNER TO nils; + +-- +-- Data for Name: bots; Type: TABLE DATA; Schema: public; Owner: nils +-- + +COPY public.bots (name, short_description, long_description, avatar_url, owner, support_server, prefix, owner_id, app_id, votes, approved) FROM stdin; +DFB This Bot is for the Server DFB This bot was created for our Discordlist for Bots Server https://cdn.discordapp.com/avatars/795612465130897420/c3bd0733f876a664b4b79ec03866f131.png Julius#1755 42vDtZxZSt dfb? 703944517048598568 795612465130897420 3744 t +Tuxiflux A fun but simple bot with globalchat Tuxiflux is a funny, useful and intuitive bot for server moderation and play with in-bot money. https://cdn.discordapp.com/embed/avatars/2.png Tuxifan#4660 6smrmKkjP7 t# 609486822715818000 788310535799308288 7 t +\. + + +-- +-- PostgreSQL database dump complete +-- + diff --git a/config.h b/config.h new file mode 100644 index 0000000..935bb82 --- /dev/null +++ b/config.h @@ -0,0 +1,14 @@ +#define LISTEN_ADDR "0.0.0.0" +#define LISTEN_PORT 8082 + +#define DB_TYPE "postgresql" +#define DB_HOST "localhost" +#define DB_PORT 5432 +#define DB_NAME "dfb" +#define DB_USER "nils" +#define DB_PASSWORD "1234" + +#define OAUTH_URL "https://discord.com/api/oauth2/authorize?client_id=797565592835457024&redirect_uri=http%3A%2F%2Flocalhost%3A8082%2Fdiscordauth&response_type=code&scope=identify" +#define CLIENT_ID "797565592835457024" +#define CLIENT_SECRET "8ZbKPseob8n1UmLLunPb06MNUKfPGRi1" +#define REDIRECT_URI "http://localhost:8082/discordauth" diff --git a/controllers/views.cc b/controllers/views.cc new file mode 100644 index 0000000..01b824f --- /dev/null +++ b/controllers/views.cc @@ -0,0 +1,202 @@ +#include +#include +#include +#include +#include "../config.h" +#include "views.h" + +static unordered_map last_votes; +#define isAuthed() getOptional("discord_authed").has_value() +#define authenticate(cb) cb(HttpResponse::newRedirectionResponse(OAUTH_URL)); return +#define toStart(cb) cb(HttpResponse::newRedirectionResponse("/")); return +#define cantVote(uid) auto _cantvoteres = last_votes.find(uid); auto now = trantor::Date::date(); if (_cantvoteres != last_votes.end() and now < _cantvoteres->second.after(43200)) + + + +views::views() { + db = drogon::app().getDbClient(); +} + +void views::start( + const HttpRequestPtr&, std::function &&callback + ) +{ + callback(HttpResponse::newRedirectionResponse("/bots/@all", HttpStatusCode::k301MovedPermanently)); +} + +Bot deserializeBot(orm::Row row) { + Bot thisbot; +# define sf(f) thisbot.f = row[#f] + sf(name).as(); + sf(short_description).as(); + sf(long_description).as(); + sf(avatar_url).as(); + sf(owner).as(); + sf(support_server).as(); + sf(prefix).as(); + sf(votes).as(); + sf(approved).as(); + thisbot.owner_id = std::stoul(row["owner_id"].as()); + thisbot.app_id = std::stoul(row["app_id"].as()); + return thisbot; +} + +auto dbErr = [](const orm::DrogonDbException &e) { + std::cerr << "Database error:" << e.base().what() << std::endl; +}; + +void views::botlist( + const HttpRequestPtr& req, std::function &&callback + ) +{ + auto session = req->session(); + auto authed = session->isAuthed(); + auto justMine = req->getPath()=="/bots/@me"; + auto modView = req->getPath()=="/bots/@unapproved"; + if (justMine and not authed) { + authenticate(callback); + } + + std::string q = "SELECT * FROM bots WHERE "; + if (justMine) { + q.append("owner_id = '"+std::to_string(session->get("discord_user_id"))+"'"); + } else { + q.append("approved = "+std::string(modView?"false":"true")); + } + q.append(" ORDER BY votes"); + + db->execSqlAsync(q, + [justMine, authed, callback] (const orm::Result &rows) { + std::map bot_list; + for (const auto& r : rows) { + Bot bot = deserializeBot(r); + bot_list[bot.app_id] = bot; + } + + HttpViewData data; + data.insert("modView", false); + data.insert("justMine", justMine); + data.insert("authed", authed); + data.insert("bots", bot_list); + + callback(HttpResponse::newHttpViewResponse("botlist.csp", data)); + }, dbErr); +} + +void views::botdetail( + const HttpRequestPtr& req, std::function &&callback, + uint64_t bot_id) +{ + db->execSqlAsync("SELECT * FROM bots WHERE app_id = '"+std::to_string(bot_id)+"'", + [req, callback] (const orm::Result &rows) { + if (rows.empty()) { + // Bot not found + callback(HttpResponse::newNotFoundResponse()); + } else { + // Bot found + auto bot = deserializeBot(rows[0]); + HttpViewData data; + data.insert("bot_id", bot.app_id); + data.insert("bot", bot); + {cantVote(req->session()->get("discord_user_id")) { + data.insert("canVote", false); + } else { + data.insert("canVote", true); + }} + + callback(HttpResponse::newHttpViewResponse("botdetail.csp", data)); + } + }, dbErr); +} + +void views::botvote(const HttpRequestPtr& req, std::function &&callback, + uint64_t bot_id) { + auto session = req->session(); + // Check if user is authenticated + if (not session->isAuthed()) { + authenticate(callback); + } + // Check if user is able to vote again + auto user_id = session->get("discord_user_id"); + {cantVote(user_id) { + callback(HttpResponse::newRedirectionResponse("detail")); + return; + }} + // Register vote + db->execSqlAsync("UPDATE bots SET votes = votes + 1 WHERE app_id = '"+std::to_string(bot_id)+"'", + [user_id, callback] (const orm::Result &rows) { + if (rows.affectedRows() == 0) { + // Bot not found + callback(HttpResponse::newNotFoundResponse()); + } else { + last_votes[user_id] = trantor::Date::date(); + // Redirect back + callback(HttpResponse::newRedirectionResponse("detail")); + } + }, dbErr); +} + +void views::discorddeauth( + const HttpRequestPtr& req, std::function &&callback + ) { + req->session()->clear(); + toStart(callback); +} + +void views::discordauth( + const HttpRequestPtr& client_req, std::function &&callback, + const std::string& code) +{ + if (code.empty()) { + authenticate(callback); + } else { + // Get token from API + auto discordapi = HttpClient::newHttpClient("https://discord.com"); + auto req = HttpRequest::newHttpRequest(); + req->setPath("/api/v8/oauth2/token"); + req->setContentTypeCode(ContentType::CT_APPLICATION_X_FORM); + req->setMethod(HttpMethod::Post); + { + req->setParameter("client_id", CLIENT_ID); + req->setParameter("client_secret", CLIENT_SECRET); + req->setParameter("redirect_uri", REDIRECT_URI); + req->setParameter("grant_type", "authorization_code"); + req->setParameter("scope", "identify"); + req->setParameter("code", code); + } + discordapi->sendRequest(req, [discordapi, client_req, callback] (ReqResult, const HttpResponsePtr &response) { + // Check for success + if (response->getStatusCode() == HttpStatusCode::k200OK) { + // Auth success + auto &data = *response->getJsonObject().get(); + auto session = client_req->session(); + auto access_token = data["access_token"].asString(); + session->clear(); + session->insert("discord_access_token", access_token); + // Get user data + auto req = HttpRequest::newHttpRequest(); + req->setPath("/api/v8/users/@me"); + req->setMethod(HttpMethod::Get); + req->addHeader("Authorization", "Bearer "+access_token); + discordapi->sendRequest(req, [client_req, callback, session] (ReqResult, const HttpResponsePtr &response) { + if (response->getStatusCode() == HttpStatusCode::k200OK) { + // Getting user data success + auto &userdata = *response->getJsonObject().get(); + auto fullname = userdata["username"].asString()+'#'+userdata["discriminator"].asString(); + session->insert("discord_authed", true); + session->insert("discord_user_id", std::stoul(userdata["id"].asString())); + session->insert("discord_user_fullname", fullname); + // Show success page + HttpViewData data; + data.insert("fullname", fullname); + callback(HttpResponse::newHttpViewResponse("authsuccess.csp", data)); + } else { + toStart(callback); + } + }); + } else { + toStart(callback); + } + }); + } +} diff --git a/controllers/views.h b/controllers/views.h new file mode 100644 index 0000000..62406ad --- /dev/null +++ b/controllers/views.h @@ -0,0 +1,34 @@ +#pragma once +#include +using namespace std; + + +struct Bot { + string name, short_description, long_description, avatar_url, owner, support_server, prefix; + uint64_t owner_id, app_id; + uint32_t votes = 0; + bool approved = false; +}; + +using namespace drogon; +class views: public drogon::HttpController { + orm::DbClientPtr db; +public: + views(); + void start(const HttpRequestPtr&, std::function &&); + void botlist(const HttpRequestPtr&, std::function &&); + void botdetail(const HttpRequestPtr&, std::function &&, uint64_t); + void botvote(const HttpRequestPtr&, std::function &&, uint64_t); + void discordauth(const HttpRequestPtr&, std::function &&, const std::string&); + void discorddeauth(const HttpRequestPtr&, std::function &&); + + METHOD_LIST_BEGIN + ADD_METHOD_TO(views::start, "/", Get); + ADD_METHOD_TO(views::botlist, "/bots/@all", Get); + ADD_METHOD_TO(views::botlist, "/bots/@me", Get); + ADD_METHOD_TO(views::botdetail, "/bots/{1}/detail", Get); + ADD_METHOD_TO(views::botvote, "/bots/{1}/vote", Get); + ADD_METHOD_TO(views::discordauth, "/discordauth?code={1}", Get); + ADD_METHOD_TO(views::discorddeauth, "/discorddeauth", Get); + METHOD_LIST_END +}; diff --git a/main.cc b/main.cc new file mode 100644 index 0000000..737642c --- /dev/null +++ b/main.cc @@ -0,0 +1,10 @@ +#include +#include "config.h" + +int main() { + //Set HTTP listener address and port + drogon::app().addListener(LISTEN_ADDR, LISTEN_PORT).enableSession(std::chrono::minutes(1200)).createDbClient(DB_TYPE, DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD); + //Run HTTP framework, the method will block in the internal event loop + drogon::app().run(); + return 0; +} diff --git a/models/model.json b/models/model.json new file mode 100644 index 0000000..cdc1d14 --- /dev/null +++ b/models/model.json @@ -0,0 +1,86 @@ +{ + //rdbms: server type, postgresql,mysql or sqlite3 + "rdbms": "postgresql", + //filename: sqlite3 db file name + //"filename":"", + //host: server address,localhost by default; + "host": "127.0.0.1", + //port: server port, 5432 by default; + "port": 5432, + //dbname: Database name; + "dbname": "", + //schema: valid for postgreSQL, "public" by default; + "schema": "public", + //user: User name + "user": "", + //password or passwd: Password + "password": "", + //client_encoding: The character set used by drogon_ctl. it is empty string by default which + //means use the default character set. + //"client_encoding": "", + //table: An array of tables to be modelized. if the array is empty, all revealed tables are modelized. + "tables": [], + "relationships": { + "enabled": false, + "items": [{ + "type": "has one", + "original_table_name": "products", + "original_table_alias": "product", + "original_key": "id", + "target_table_name": "skus", + "target_table_alias": "SKU", + "target_key": "product_id", + "enable_reverse": true + }, + { + "type": "has many", + "original_table_name": "products", + "original_table_alias": "product", + "original_key": "id", + "target_table_name": "reviews", + "target_table_alias": "", + "target_key": "product_id", + "enable_reverse": true + }, + { + "type": "many to many", + "original_table_name": "products", + "original_table_alias": "", + "original_key": "id", + "pivot_table": { + "table_name": "carts_products", + "original_key": "product_id", + "target_key": "cart_id" + }, + "target_table_name": "carts", + "target_table_alias": "", + "target_key": "id", + "enable_reverse": true + } + ] + }, + "restful_api_controllers": { + "enabled": false, + // resource_uri: The URI to access the resource, the default value + // is '/*' in which the asterisk represents the table name. + // If this option is set to a empty string, the URI is composed of the namespaces and the class name. + "resource_uri": "/*", + // class_name: "Restful*Ctrl" by default, the asterisk represents the table name. + // This option can contain namespaces. + "class_name": "Restful*Ctrl", + // filters: an array of filter names. + "filters": [], + // db_client: the database client used by the controller. this option must be consistent with + // the configuration of the application. + "db_client": { + //name: Name of the client,'default' by default + "name": "default", + //is_fast: + "is_fast": false + }, + // directory: The directory where the controller source files are stored. + "directory": "controllers", + // generate_base_only: false by default. Set to true to avoid overwriting custom subclasses. + "generate_base_only": false + } +} diff --git a/static/botdetail.css b/static/botdetail.css new file mode 100644 index 0000000..4240c0d --- /dev/null +++ b/static/botdetail.css @@ -0,0 +1,66 @@ +.bot-flex { + display: flex; + flex-direction: row; + align-items: center; + padding-bottom: 16px; +} + +.bot-flex * { + flex: 1; +} + +.bot-image { + max-height: 175px !important; + max-width: 175px !important; +} + +.bot-text { + padding-left: 20px; +} + +.long-description { + padding-top: 16px; + padding-bottom: 32px; +} + +.overview { + padding-top: 16px; + padding-bottom: 32px; +} + +.overview-key { + font-weight: bold; +} + +.actions { + margin: 10px; + display: flex; + flex-direction: column; + text-align: right; +} + +.actionsWrapper { + max-width: 150px; +} + +@media only screen and (max-width: 900px) { + .bot-flex { + flex-direction: column; + padding-bottom: 0px; + } + + .bot-text { + padding-left: 0px; + padding-top: 16px; + flex-grow: 1; + } + .container { + margin: 5%; + } + .actions { + flex-direction: row; + } + .actionsWrapper { + max-width: unset; +} +} diff --git a/static/favicon.ico b/static/favicon.ico new file mode 100644 index 0000000..7482460 Binary files /dev/null and b/static/favicon.ico differ diff --git a/static/global.css b/static/global.css new file mode 100644 index 0000000..66268a7 --- /dev/null +++ b/static/global.css @@ -0,0 +1,45 @@ +body { + background-color:#323232; + color:#FFFFFF; + font-family: sans-serif; +} + +.container { + margin: 20%; + margin-top: 0px; + margin-bottom: 0px; +} + +.linkButton { + background-color:transparent; + border-radius:4px; + border:2px solid #ffffff; + display:inline-block; + cursor:pointer; + color:#ffffff; + font-family:Arial; + font-size:15px; + text-align:center; + padding:9px 23px; + text-decoration:none; + text-shadow:0px 0px 11px #263666; + margin:10px; +} +.linkButton:hover { + background-color: #fff; + color: #263666; + text-shadow: 0px; + transform: scale(1.03); +} +.linkButton:active { + position:relative; + top:1px; +} + +.title { + font-size: 30px +} + +.text-center { + text-align: center; +} diff --git a/static/logo.png b/static/logo.png new file mode 100644 index 0000000..18fb0df Binary files /dev/null and b/static/logo.png differ diff --git a/views/authsuccess.csp b/views/authsuccess.csp new file mode 100644 index 0000000..612925b --- /dev/null +++ b/views/authsuccess.csp @@ -0,0 +1,21 @@ +<%inc#include "controllers/views.h" %> +<%c++ auto fullname = @@.get("fullname");%> + + + + + + + + + + Authentication success - DFB + + + +
+

Authenticated as: {%fullname%}

+
+ + + diff --git a/views/botdetail.csp b/views/botdetail.csp new file mode 100644 index 0000000..b8dcb2b --- /dev/null +++ b/views/botdetail.csp @@ -0,0 +1,62 @@ +<%inc#include "controllers/views.h" %> +<%c++ auto bot_id = @@.get("bot_id");%> +<%c++ auto bot = @@.get("bot");%> +<%c++ auto canVote = @@.get("canVote");%> + + + + + + + + + + {%bot.name%} - DFB + + + +
+

+
+ + +
+ {%bot.name%} +
+
+
+ Invite + Vote +
+
+
+
+
+
+ {%bot.long_description%} +
+ +
+
+

Overview

+ + + + + + + + + + + + + +
Prefix{%bot.prefix%}
Owner{%bot.owner%}
Votes{%bot.votes%}
+
+ Support Server +
+
+ + + diff --git a/views/botlist.csp b/views/botlist.csp new file mode 100644 index 0000000..0f3205a --- /dev/null +++ b/views/botlist.csp @@ -0,0 +1,47 @@ +<%inc#include "controllers/views.h" %> +<%c++ auto viewHidden = @@.get("viewHidden");%> +<%c++ auto justMine = @@.get("justMine");%> + + + + + + + DFB + + +
+ +

Discordlist for Bots

+

Find a lot of bots that will be useful to your server

+ <%c++ if (@@.get("authed")) {%> + <%c++ if (justMine) {%> + All bots + <%c++ } else {%> + My bots + <%c++ }%> + Logout + <%c++ } else {%> + Login + <%c++ }%> +
+
+ <%c++ for (const auto& [bot_id, bot] : @@.get>("bots")) {%> + <%c++ if (bot.approved == viewHidden) continue;%> + + + + + + +
+ + +

{%bot.name%}

+

{%bot.short_description%}

+
+
+ <%c++ }%> +
+ +