1
0
Fork 0
mirror of https://gitlab.com/niansa/discordlistforbots.git synced 2025-03-06 20:49:22 +01:00

Initial commit

This commit is contained in:
niansa 2021-01-10 17:29:59 +01:00
commit 980c9ffb95
15 changed files with 749 additions and 0 deletions

37
.gitignore vendored Normal file
View file

@ -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

68
CMakeLists.txt Normal file
View file

@ -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()

57
bots.sql Normal file
View file

@ -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
--

14
config.h Normal file
View file

@ -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"

202
controllers/views.cc Normal file
View file

@ -0,0 +1,202 @@
#include <string>
#include <unordered_map>
#include <fstream>
#include <drogon/drogon.h>
#include "../config.h"
#include "views.h"
static unordered_map <uint64_t, trantor::Date> last_votes;
#define isAuthed() getOptional<bool>("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<void (const HttpResponsePtr &)> &&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<std::string>();
sf(short_description).as<std::string>();
sf(long_description).as<std::string>();
sf(avatar_url).as<std::string>();
sf(owner).as<std::string>();
sf(support_server).as<std::string>();
sf(prefix).as<std::string>();
sf(votes).as<uint32_t>();
sf(approved).as<bool>();
thisbot.owner_id = std::stoul(row["owner_id"].as<std::string>());
thisbot.app_id = std::stoul(row["app_id"].as<std::string>());
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<void (const HttpResponsePtr &)> &&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<uint64_t>("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<uint64_t, Bot> 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<void (const HttpResponsePtr &)> &&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<uint64_t>("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<void (const HttpResponsePtr &)> &&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<uint64_t>("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<void (const HttpResponsePtr &)> &&callback
) {
req->session()->clear();
toStart(callback);
}
void views::discordauth(
const HttpRequestPtr& client_req, std::function<void (const HttpResponsePtr &)> &&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);
}
});
}
}

34
controllers/views.h Normal file
View file

@ -0,0 +1,34 @@
#pragma once
#include <drogon/HttpController.h>
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<views> {
orm::DbClientPtr db;
public:
views();
void start(const HttpRequestPtr&, std::function<void (const HttpResponsePtr &)> &&);
void botlist(const HttpRequestPtr&, std::function<void (const HttpResponsePtr &)> &&);
void botdetail(const HttpRequestPtr&, std::function<void (const HttpResponsePtr &)> &&, uint64_t);
void botvote(const HttpRequestPtr&, std::function<void (const HttpResponsePtr &)> &&, uint64_t);
void discordauth(const HttpRequestPtr&, std::function<void (const HttpResponsePtr &)> &&, const std::string&);
void discorddeauth(const HttpRequestPtr&, std::function<void (const HttpResponsePtr &)> &&);
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
};

10
main.cc Normal file
View file

@ -0,0 +1,10 @@
#include <drogon/drogon.h>
#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;
}

86
models/model.json Normal file
View file

@ -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
}
}

66
static/botdetail.css Normal file
View file

@ -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;
}
}

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

45
static/global.css Normal file
View file

@ -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;
}

BIN
static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

21
views/authsuccess.csp Normal file
View file

@ -0,0 +1,21 @@
<%inc#include "controllers/views.h" %>
<%c++ auto fullname = @@.get<std::string>("fullname");%>
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/global.css">
<meta http-equiv = "refresh" content = "2; url = /" />
<title>Authentication success - DFB</title>
</head>
<body>
<div class="container">
<p class="title text-center">Authenticated as: {%fullname%}</p>
</div>
</body>
</html>

62
views/botdetail.csp Normal file
View file

@ -0,0 +1,62 @@
<%inc#include "controllers/views.h" %>
<%c++ auto bot_id = @@.get<uint64_t>("bot_id");%>
<%c++ auto bot = @@.get<Bot>("bot");%>
<%c++ auto canVote = @@.get<bool>("canVote");%>
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/global.css">
<link rel="stylesheet" href="/botdetail.css">
<title>{%bot.name%} - DFB</title>
</head>
<body>
<div class="container">
<h1 class="text text-center"></h1>
<div class="bot-flex">
<img class="bot-image" src="{%bot.avatar_url%}"></img>
<div class="bot-text text text-center title">
{%bot.name%}
</div>
<div class="actionsWrapper">
<div class="actions">
<a class="linkButton" href="https://discord.com/oauth2/authorize?client_id={%bot_id%}&permissions=8&scope=applications.commands%20bot">Invite</a>
<a class="linkButton" {%(canVote?"href='vote'":"href='#' style='color:grey;'")%}>Vote</a>
</div>
</div>
</div>
<hr>
<br>
<div class="text long-description">
{%bot.long_description%}
</div>
<hr>
<div class="text overview">
<h2>Overview</h2>
<table>
<tr>
<td class="overview-key">Prefix</td>
<td class="overview-value">{%bot.prefix%}</td>
</tr>
<tr>
<td class="overview-key">Owner</td>
<td class="overview-value">{%bot.owner%}</td>
</tr>
<tr>
<td class="overview-key">Votes</td>
<td class="overview-value">{%bot.votes%}</td>
</tr>
</table>
<br>
<a class="linkButton" style="margin:0px;" href="https://discord.gg/{%bot.support_server%}">Support Server</a>
</div>
</div>
</body>
</html>

47
views/botlist.csp Normal file
View file

@ -0,0 +1,47 @@
<%inc#include "controllers/views.h" %>
<%c++ auto viewHidden = @@.get<bool>("viewHidden");%>
<%c++ auto justMine = @@.get<bool>("justMine");%>
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/global.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DFB</title>
</head>
<body>
<div class="container text-center">
<img style="width:15%;height:15%;" src="/logo.png">
<p class="title">Discordlist for Bots</p>
<p>Find a lot of bots that will be useful to your server</p>
<%c++ if (@@.get<bool>("authed")) {%>
<%c++ if (justMine) {%>
<a class="linkButton" href="@all">All bots</a>
<%c++ } else {%>
<a class="linkButton" href="@me">My bots</a>
<%c++ }%>
<a class="linkButton" href="/discorddeauth">Logout</a>
<%c++ } else {%>
<a class="linkButton" href="/discordauth">Login</a>
<%c++ }%>
<hr>
<br>
<%c++ for (const auto& [bot_id, bot] : @@.get<std::map<uint64_t, Bot>>("bots")) {%>
<%c++ if (bot.approved == viewHidden) continue;%>
<a class="linkButton" href="{%bot_id%}/detail">
<table>
<tr>
<td>
<img src="{%bot.avatar_url%}" style="width:128px;height:128px;">
</td>
<td style="padding:10px;">
<h1 style="line-height:0px;">{%bot.name%}</h1>
<p>{%bot.short_description%}</p>
</td>
</tr>
</table>
</a>
<%c++ }%>
</div>
</body>
</html>