Compare commits

..

1 commit

22 changed files with 1028 additions and 2149 deletions

View file

@ -1,55 +0,0 @@
name: On_Push
on:
push:
branches:
- 'main'
paths:
- flake.*
- src/**/*
- Cargo.*
- .forgejo/**/*
- rust-toolchain.toml
jobs:
# rust code must be formatted for standardisation
lint_fmt:
# build it using teh base nixos system, helps with caching
runs-on: nix
steps:
# get the repo first
- uses: https://code.forgejo.org/actions/checkout@v4
- run: nix build .#fmt --verbose
# clippy is incredibly useful for making yer code better
lint_clippy:
# build it using teh base nixos system, helps with caching
runs-on: nix
permissions:
checks: write
steps:
# get the repo first
- uses: https://code.forgejo.org/actions/checkout@v4
- run: nix build .#clippy --verbose
build:
# build it using teh base nixos system, helps with caching
runs-on: nix
needs: [ lint_fmt, lint_clippy ]
steps:
# get the repo first
- uses: https://code.forgejo.org/actions/checkout@v4
- name: "Build it locally"
run: nix build --verbose
# deploy it upstream
deploy:
# runs on teh default docker container
runs-on: docker
needs: [ build ]
steps:
- name: "Deploy to Skynet"
uses: https://forgejo.skynet.ie/Skynet/actions-deploy-to-skynet@v2
with:
input: 'skynet_discord_bot'
token: ${{ secrets.API_TOKEN_FORGEJO }}

86
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,86 @@
# copied a good chunk from my bfom config
image: rust:latest
stages:
- lint
- build
- deploy
cache:
key: "$CI_JOB_NAME"
paths:
- target/
# Set any required environment variables here
variables:
RUST_BACKTRACE: FULL
# clippy and fmt are magic
# runs on all commits/branches
lint-clippy:
stage: lint
script:
- rustup component add clippy
- rustc --version
- cargo version
- cargo clippy
rules:
- if: $CI_COMMIT_TAG
when: never
- changes:
- src/**/*
- cargo.*
when: always
lint-fmt:
stage: lint
script:
- rustup component add rustfmt
- rustc --version
- cargo version
- cargo fmt -- --check
rules:
- if: $CI_COMMIT_TAG
when: never
- changes:
- src/**/*
- cargo.*
when: always
# has to actually compile
build:
stage: build
script:
- rustc --version
- cargo version
- cargo build --verbose
- RUST_BACKTRACE=1 cargo test --verbose
rules:
- if: $CI_COMMIT_TAG
when: never
- changes:
- src/**/*
- cargo.*
when: on_success
# from https://docs.gitlab.com/ee/ci/pipelines/multi_project_pipelines.html
# so simple to deploy now
nixos:
stage: deploy
variables:
PACKAGE_NAME: "skynet_discord_bot"
UPDATE_FLAKE: "yes"
trigger: compsoc1/skynet/nixos
rules:
- if: $CI_COMMIT_TAG
when: never
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: on_success

1416
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -11,11 +11,7 @@ name = "update_data"
[[bin]]
name = "update_users"
[[bin]]
name = "update_minecraft"
[dependencies]
# discord library
serenity = { version = "0.11.6", default-features = false, features = ["client", "gateway", "rustls_backend", "model", "cache"] }
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
@ -25,7 +21,7 @@ surf = "2.3.2"
dotenvy = "0.15.7"
# For sqlite
sqlx = { version = "0.7.1", features = [ "runtime-tokio", "sqlite", "migrate" ] }
sqlx = { version = "0.7.1", features = [ "runtime-tokio", "sqlite" ] }
# create random strings
rand = "0.8.5"

View file

@ -1,70 +0,0 @@
# Skynet Discord Bot
This bots core purpose is to give members roles based on their status on <ulwolves.ie>.
It uses an api key provided by wolves to get member lists.
Users are able to link their wolves account to the bot and that works across discord servers.
For example is a user links on the CompSoc Discord then they will also get their roles (automagically) on Games Dev if they are a member there.
## Commands - Admin
You need admin access to run any of the commands in this section.
Either the server owner or a suer with the ``Administrator`` permission
### Getting the Skynet Discord bot
1. Email ``keith@assurememberships.com`` from committee email and say ye want an api key for ``193.1.99.74``
2. Create a role for current members (maybe call it ``current-member`` ?)
3. (Optional) create a role for all past and current members (ye can use the existing ``member`` role for this, )
4. Invite the bot https://discord.com/api/oauth2/authorize?client_id=1145761669256069270&permissions=139855185984&scope=bot
5. Make sure the bot role ``@skynet`` is above these two roles (so it can manage them)
6. Make sure that you have a role that gives ye administrator powers
7. Use the command ``/add`` and insert the api key, role current and role all (desktop recommended)
The reason for both roles is ye have one for active members while the second is for all current and past members.
### Minecraft
The bot is able to manage the whitelist of a Minecraft server managed by the Computer Society.
Talk to us to get a server.
#### Add
This links a minecraft server with your club/society.
``/minecraft_add SERVER_ID``
#### List
List the servers linked to your club/society.
It is possible to have more than one minecraft server
``/minecraft_list``
#### Delete
This unlinks a minecraft server from your club/society.
``/minecraft_delete SERVER_ID``
## Commands - User
### Setup
* Start the process using ``/link_wolves WOLVES_EMAIL``
* The email that is in the Contact Email here: <https://ulwolves.ie/memberships/profile>
* An email will be sent to them that they need to verify using ``/verify CODE``
* This will only have to be done once.
* If the user is an active member on wolves
* If they are in any servers with teh Skynet Bot
* They will get relevant roles.
* If they Join a server with teh bot enabled.
* They will be granted the roles automatically
* If the user is **not** an active member on wolves.
* If they have no Roles
* No change
* If they have Past Member Role
* No change
* If they have both Roles
* The current-member role will be removed from them
* Past Member role will remain unchanged
### Minecraft
Users can link their Minecraft username to grant them access to any servers where teh whitelist is managed by teh bot.
``/link_minecraft MINECRAFT_USERNAME``

View file

@ -1,38 +0,0 @@
-- setup initial tables
-- this handles the users "floating" account
CREATE TABLE IF NOT EXISTS wolves (
id_wolves integer PRIMARY KEY,
email text not null,
discord integer,
minecraft text
);
CREATE INDEX IF NOT EXISTS index_discord ON wolves (discord);
-- used to verify the users email address
CREATE TABLE IF NOT EXISTS wolves_verify (
discord integer PRIMARY KEY,
email text not null,
auth_code text not null,
date_expiry text not null
);
CREATE INDEX IF NOT EXISTS index_date_expiry ON wolves_verify (date_expiry);
-- information on teh server the bot is registered on
CREATE TABLE IF NOT EXISTS servers (
server integer PRIMARY KEY,
wolves_api text not null,
role_past integer,
role_current integer,
member_past integer DEFAULT 0,
member_current integer DEFAULT 0
);
-- keep track of the members on the server
CREATE TABLE IF NOT EXISTS server_members (
server integer not null,
id_wolves integer not null,
expiry text not null,
PRIMARY KEY(server,id_wolves),
FOREIGN KEY (id_wolves) REFERENCES wolves (id_wolves)
);

View file

@ -1,3 +0,0 @@
-- add teh option to associate each discord server with a minecraft one managed by skynet
ALTER TABLE servers
ADD server_minecraft text;

View file

@ -1,18 +0,0 @@
-- Create the new table
CREATE TABLE IF NOT EXISTS minecraft
(
server_discord integer not null,
server_minecraft text not null,
PRIMARY KEY (server_discord, server_minecraft),
FOREIGN KEY (server_discord) REFERENCES servers (server)
);
-- Import in the data
INSERT INTO minecraft (server_discord, server_minecraft)
SELECT server, server_minecraft
FROM servers
where server_minecraft is not null;
-- delete the col in teh server table
ALTER TABLE servers
DROP COLUMN server_minecraft;

View file

@ -1,7 +0,0 @@
-- temp table to allow folks to verify by committee email.
CREATE TABLE IF NOT EXISTS committee (
discord integer PRIMARY KEY,
email text not null,
auth_code text not null,
committee integer DEFAULT 0
);

View file

@ -1,8 +0,0 @@
HOME="."
DATABASE="database.db"
DISCORD_TOKEN=""
DISCORD_TOKEN_MINECRAFT=""
EMAIL_SMTP=""
EMAIL_USER=""
EMAIL_PASS=""
WOLVES_URL=""

View file

@ -5,11 +5,11 @@
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1721727458,
"narHash": "sha256-r/xppY958gmZ4oTfLiHN0ZGuQ+RSTijDblVgVLFi1mw=",
"lastModified": 1692351612,
"narHash": "sha256-KTGonidcdaLadRnv9KFgwSMh1ZbXoR/OBmPjeNMhFwU=",
"owner": "nix-community",
"repo": "naersk",
"rev": "3fb418eaf352498f6b6c30592e3beb63df42ef11",
"rev": "78789c30d64dea2396c9da516bbcc8db3a475207",
"type": "github"
},
"original": {
@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1723151389,
"narHash": "sha256-9AVY0ReCmSGXHrlx78+1RrqcDgVSRhHUKDVV1LLBy28=",
"lastModified": 1693060755,
"narHash": "sha256-KNsbfqewEziFJEpPR0qvVz4rx0x6QXxw1CcunRhlFdk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "13fe00cb6c75461901f072ae62b5805baef9f8b2",
"rev": "c66ccfa00c643751da2fd9290e096ceaa30493fc",
"type": "github"
},
"original": {
@ -34,16 +34,16 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1722995383,
"narHash": "sha256-UzuXo7ZM8ZK0SkWFhHocKkLSGQPHS4JxaE1jvVR4fUo=",
"lastModified": 1693087214,
"narHash": "sha256-Kn1SSqRfPpqcI1MDy82JXrPT1WI8c03TA2F0xu6kS+4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "957d95fc8b9bf1eb60d43f8d2eba352b71bbf2be",
"rev": "f155f0cf4ea43c4e3c8918d2d327d44777b6cad4",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-unstable",
"ref": "nixos-23.05",
"type": "indirect"
}
},
@ -74,11 +74,11 @@
"systems": "systems"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"lastModified": 1692799911,
"narHash": "sha256-3eihraek4qL744EvQXsK1Ha6C3CR7nnT8X2qWap4RNk=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"rev": "f9e7cf818399d17d347f847525c5a5a8032e4e44",
"type": "github"
},
"original": {

376
flake.nix
View file

@ -2,213 +2,203 @@
description = "Skynet Discord Bot";
inputs = {
nixpkgs.url = "nixpkgs/nixos-unstable";
naersk.url = "github:nix-community/naersk";
utils.url = "github:numtide/flake-utils";
nixpkgs.url = "nixpkgs/nixos-23.05";
naersk.url = "github:nix-community/naersk";
utils.url = "github:numtide/flake-utils";
};
nixConfig = {
extra-substituters = "https://nix-cache.skynet.ie/skynet-cache";
extra-trusted-public-keys = "skynet-cache:zMFLzcRZPhUpjXUy8SF8Cf7KGAZwo98SKrzeXvdWABo=";
};
outputs = {
self,
nixpkgs,
utils,
naersk,
}:
utils.lib.eachDefaultSystem (
system: let
pkgs = (import nixpkgs) {inherit system;};
naersk' = pkgs.callPackage naersk {};
package_name = "skynet_discord_bot";
desc = "Skynet Discord Bot";
buildInputs = with pkgs; [
openssl
pkg-config
rustfmt
outputs = { self, nixpkgs, utils, naersk }: utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages."${system}";
naersk-lib = naersk.lib."${system}";
package_name = "skynet_discord_bot";
desc = "Skynet Discord Bot";
in rec {
# `nix build`
packages."${package_name}" = naersk-lib.buildPackage {
pname = "${package_name}";
root = ./.;
buildInputs = [
pkgs.openssl
pkgs.pkg-config
];
in rec {
packages = {
# For `nix build` & `nix run`:
default = naersk'.buildPackage {
pname = "${package_name}";
src = ./.;
buildInputs = buildInputs;
};
defaultPackage = packages."${package_name}";
# `nix run`
apps."${package_name}" = utils.lib.mkApp {
drv = packages."${package_name}";
};
defaultApp = apps."${package_name}";
# `nix develop`
devShell = pkgs.mkShell {
nativeBuildInputs = with pkgs; [ rustc cargo pkg-config openssl];
};
nixosModule = { lib, pkgs, config, ... }:
with lib;
let
cfg = config.services."${package_name}";
# secret options are in the env file(s) loaded separately
environment_config = {
LDAP_API = cfg.ldap;
SKYNET_SERVER = cfg.discord.server;
HOME = cfg.home;
DATABASE = "database.db";
};
# Run `nix build .#fmt` to run tests
fmt = naersk'.buildPackage {
src = ./.;
mode = "fmt";
buildInputs = buildInputs;
};
# Run `nix build .#clippy` to lint code
clippy = naersk'.buildPackage {
src = ./.;
mode = "clippy";
buildInputs = buildInputs;
};
};
service_name = script: lib.strings.sanitizeDerivationName("${cfg.user}@${script}");
defaultPackage = packages.default;
# `nix run`
apps."${package_name}" = utils.lib.mkApp {
drv = packages."${package_name}";
};
defaultApp = apps."${package_name}";
# `nix develop`
devShell = pkgs.mkShell {
nativeBuildInputs = with pkgs; [rustc cargo pkg-config openssl rustfmt];
};
nixosModule = {
lib,
pkgs,
config,
...
}:
with lib; let
cfg = config.services."${package_name}";
# secret options are in the env file(s) loaded separately
environment_config = {
HOME = cfg.home;
DATABASE = "database.db";
# oneshot scripts to run
serviceGenerator = mapAttrs' (script: time: nameValuePair (service_name script) {
description = "Service for ${desc} ${script}";
wantedBy = [ ];
after = [ "network-online.target" ];
environment = environment_config;
serviceConfig = {
Type = "oneshot";
User = "${cfg.user}";
Group = "${cfg.user}";
ExecStart = "${self.defaultPackage."${system}"}/bin/${script}";
EnvironmentFile = [
"${cfg.env.ldap}"
"${cfg.env.discord}"
"${cfg.env.mail}"
"${cfg.env.wolves}"
];
};
service_name = script: lib.strings.sanitizeDerivationName "${cfg.user}@${script}";
# oneshot scripts to run
serviceGenerator = mapAttrs' (script: time:
nameValuePair (service_name script) {
description = "Service for ${desc} ${script}";
wantedBy = [];
after = ["network-online.target"];
});
# each timer will run the above service
timerGenerator = mapAttrs' (script: time: nameValuePair (service_name script) {
description = "Timer for ${desc} ${script}";
wantedBy = [ "timers.target" ];
partOf = [ "${service_name script}.service" ];
timerConfig = {
OnCalendar = time;
Unit = "${service_name script}.service";
Persistent = true;
};
});
# modify these
scripts = {
# every 20 min
"update_data" = "*:0,20,40";
# groups are updated every hour, offset from teh ldap
"update_users" = "*:05:00";
};
in {
options.services."${package_name}" = {
enable = mkEnableOption "enable ${package_name}";
env = {
ldap = mkOption rec {
type = types.str;
description = "ENV file with LDAP_DISCORD_AUTH";
};
discord = mkOption rec {
type = types.str;
description = "ENV file with DISCORD_TOKEN";
};
mail = mkOption rec {
type = types.str;
description = "ENV file with EMAIL_SMTP, EMAIL_USER, EMAIL_PASS";
};
wolves = mkOption rec {
type = types.str;
description = "Mail details, has WOLVES_URL, WOLVES_KEY";
};
};
discord = {
server = mkOption rec {
type = types.str;
description = "ID of the server the bot runs on";
};
};
ldap = mkOption rec {
type = types.str;
default = "https://api.account.skynet.ie";
description = "Location of the ldap api";
};
user = mkOption rec {
type = types.str;
default = "${package_name}";
description = "The user to run the service";
};
home = mkOption rec {
type = types.str;
default = "/etc/${cfg.prefix}${package_name}";
description = "The home for the user";
};
prefix = mkOption rec {
type = types.str;
default = "skynet_";
example = default;
description = "The prefix used to name service/folders";
};
};
config = mkIf cfg.enable {
users.groups."${cfg.user}" = { };
users.users."${cfg.user}" = {
createHome = true;
isSystemUser = true;
home = "${cfg.home}";
group = "${cfg.user}";
};
systemd.services = {
# main service
"${package_name}" = {
description = desc;
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" ];
wants = [ ];
environment = environment_config;
serviceConfig = {
Type = "oneshot";
User = "${cfg.user}";
Group = "${cfg.user}";
ExecStart = "${self.defaultPackage."${system}"}/bin/${script}";
User = "${cfg.user}";
Group = "${cfg.user}";
Restart = "always";
ExecStart = "${self.defaultPackage."${system}"}/bin/${package_name}";
# can have multiple env files
EnvironmentFile = [
"${cfg.env.ldap}"
"${cfg.env.discord}"
"${cfg.env.mail}"
"${cfg.env.wolves}"
];
};
});
# each timer will run the above service
timerGenerator = mapAttrs' (script: time:
nameValuePair (service_name script) {
description = "Timer for ${desc} ${script}";
wantedBy = ["timers.target"];
partOf = ["${service_name script}.service"];
timerConfig = {
OnCalendar = time;
Unit = "${service_name script}.service";
Persistent = true;
};
});
# modify these
scripts = {
# every 20 min
"update_data" = "*:0,20,40";
# groups are updated every hour, offset from teh ldap
"update_users" = "*:05:00";
# minecraft stuff is updated at 5am
"update_minecraft" = "5:10:00";
};
in {
options.services."${package_name}" = {
enable = mkEnableOption "enable ${package_name}";
env = {
discord = mkOption rec {
type = types.str;
description = "ENV file with DISCORD_TOKEN, DISCORD_TOKEN_MINECRAFT";
};
mail = mkOption rec {
type = types.str;
description = "ENV file with EMAIL_SMTP, EMAIL_USER, EMAIL_PASS";
};
wolves = mkOption rec {
type = types.str;
description = "Mail details, has WOLVES_URL";
};
restartTriggers = [
"${cfg.env.ldap}"
"${cfg.env.discord}"
"${cfg.env.mail}"
"${cfg.env.wolves}"
];
};
user = mkOption rec {
type = types.str;
default = "${package_name}";
description = "The user to run the service";
};
home = mkOption rec {
type = types.str;
default = "/etc/${cfg.prefix}${package_name}";
description = "The home for the user";
};
prefix = mkOption rec {
type = types.str;
default = "skynet_";
example = default;
description = "The prefix used to name service/folders";
};
};
config = mkIf cfg.enable {
users.groups."${cfg.user}" = {};
users.users."${cfg.user}" = {
createHome = true;
isSystemUser = true;
home = "${cfg.home}";
group = "${cfg.user}";
};
systemd.services =
{
# main service
"${package_name}" = {
description = desc;
wantedBy = ["multi-user.target"];
after = ["network-online.target"];
wants = [];
environment = environment_config;
serviceConfig = {
User = "${cfg.user}";
Group = "${cfg.user}";
Restart = "always";
ExecStart = "${self.defaultPackage."${system}"}/bin/${package_name}";
# can have multiple env files
EnvironmentFile = [
"${cfg.env.discord}"
"${cfg.env.mail}"
"${cfg.env.wolves}"
];
};
restartTriggers = [
"${cfg.env.discord}"
"${cfg.env.mail}"
"${cfg.env.wolves}"
];
};
}
// serviceGenerator scripts;
# timers to run the above services
systemd.timers = timerGenerator scripts;
};
} // serviceGenerator scripts;
# timers to run the above services
systemd.timers = timerGenerator scripts;
};
}
);
};
}
);
}

View file

@ -1,2 +0,0 @@
[toolchain]
channel = "1.80"

View file

@ -1,25 +0,0 @@
use skynet_discord_bot::{db_init, get_config, get_minecraft_config, update_server, whitelist_wipe};
use std::collections::HashSet;
#[tokio::main]
async fn main() {
let config = get_config();
let db = match db_init(&config).await {
Ok(x) => x,
Err(_) => return,
};
let servers = get_minecraft_config(&db).await;
let mut wiped = HashSet::new();
for server in &servers {
// wipe whitelist first
if !wiped.contains(&server.minecraft) {
whitelist_wipe(&server.minecraft, &config.discord_token_minecraft).await;
// add it to teh done list so its not done again
wiped.insert(&server.minecraft);
}
update_server(&server.minecraft, &db, &server.discord, &config).await;
}
}

View file

@ -4,7 +4,7 @@ use serenity::{
model::gateway::{GatewayIntents, Ready},
Client,
};
use skynet_discord_bot::{db_init, get_config, get_server_config_bulk, set_roles, Config, DataBase};
use skynet_discord_bot::{db_init, get_config, get_server_config_bulk, set_roles::update_server, Config, DataBase};
use std::{process, sync::Arc};
use tokio::sync::RwLock;
@ -59,6 +59,6 @@ async fn bulk_check(ctx: Arc<Context>) {
let db = db_lock.read().await;
for server_config in get_server_config_bulk(&db).await {
set_roles::update_server(&ctx, &server_config, &[], &[]).await;
update_server(&ctx, &server_config, &[], &vec![]).await;
}
}

View file

@ -7,19 +7,46 @@ use serenity::{
},
};
use skynet_discord_bot::get_data::get_wolves;
use skynet_discord_bot::{get_server_config, is_admin, set_roles::update_server, DataBase, Servers};
use skynet_discord_bot::{get_server_config, set_roles::update_server, DataBase, Servers};
use sqlx::{Error, Pool, Sqlite};
pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String {
// check if user has high enough permisssions
if let Some(msg) = is_admin(command, ctx).await {
return msg;
let mut admin = false;
let g_id = match command.guild_id {
None => return "Not in a server".to_string(),
Some(x) => x,
};
let roles_server = g_id.roles(&ctx.http).await.unwrap_or_default();
if let Ok(member) = g_id.member(&ctx.http, command.user.id).await {
if let Some(permissions) = member.permissions {
if permissions.administrator() {
admin = true;
}
}
for role_id in member.roles {
if admin {
break;
}
if let Some(role) = roles_server.get(&role_id) {
if role.permissions.administrator() {
admin = true;
}
}
}
}
if !admin {
return "Administrator permission required".to_string();
}
let api_key = if let CommandDataOptionValue::String(key) = command
.data
.options
.first()
.get(0)
.expect("Expected user option")
.resolved
.as_ref()
@ -156,7 +183,7 @@ async fn add_server(db: &Pool<Sqlite>, ctx: &Context, server: &Servers) -> Resul
if past_remove {
roles_remove.push(past_role)
}
update_server(ctx, server, &roles_remove, &[]).await;
update_server(ctx, server, &roles_remove, &vec![]).await;
}
insert

View file

@ -1,311 +0,0 @@
use lettre::{
message::{header, MultiPart, SinglePart},
transport::smtp::{self, authentication::Credentials},
Message, SmtpTransport, Transport,
};
use maud::html;
use serenity::{
builder::CreateApplicationCommand,
client::Context,
model::{
application::interaction::application_command::ApplicationCommandInteraction,
id::UserId,
prelude::{command::CommandOptionType, interaction::application_command::CommandDataOptionValue},
},
};
use skynet_discord_bot::{random_string, Config, DataBase};
use sqlx::{Pool, Sqlite};
pub mod link {
use super::*;
use serenity::model::id::GuildId;
use skynet_discord_bot::Committee;
pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String {
let committee_server = GuildId(1220150752656363520);
match command.guild_id {
None => {
return "Not in correct discord server.".to_string();
}
Some(x) => {
if x != committee_server {
return "Not in correct discord server.".to_string();
}
}
}
let option = command
.data
.options
.first()
.expect("Expected email option")
.resolved
.as_ref()
.expect("Expected email object");
let email = if let CommandDataOptionValue::String(email) = option {
email.trim()
} else {
return "Please provide a valid committee email.".to_string();
};
// fail early
if !email.ends_with("@ulwolves.ie") {
return "Please use a @ulwolves.ie address you have access to.".to_string();
}
let db_lock = {
let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Databse in TypeMap.").clone()
};
let db = db_lock.read().await;
let config_lock = {
let data_read = ctx.data.read().await;
data_read.get::<Config>().expect("Expected Config in TypeMap.").clone()
};
let config = config_lock.read().await;
if get_server_member_discord(&db, &command.user.id).await.is_some() {
return "Already linked".to_string();
}
if get_verify_from_db(&db, &command.user.id).await.is_some() {
return "Linking already in process, please check email.".to_string();
}
// generate a auth key
let auth = random_string(20);
match send_mail(&config, email, &auth, &command.user.name) {
Ok(_) => match save_to_db(&db, email, &auth, &command.user.id).await {
Ok(_) => {}
Err(e) => {
return format!("Unable to save to db {} {e:?}", email);
}
},
Err(e) => {
return format!("Unable to send mail to {} {e:?}", email);
}
}
format!("Verification email sent to {}, it may take up to 15 min for it to arrive. If it takes longer check the Junk folder.", email)
}
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
command
.name("link_committee")
.description("Verify you are a committee member")
.create_option(|option| {
option
.name("email")
.description("UL Wolves Committee Email")
.kind(CommandOptionType::String)
.required(true)
})
}
pub async fn get_server_member_discord(db: &Pool<Sqlite>, user: &UserId) -> Option<Committee> {
sqlx::query_as::<_, Committee>(
r#"
SELECT *
FROM committee
WHERE discord = ?
"#,
)
.bind(*user.as_u64() as i64)
.fetch_one(db)
.await
.ok()
}
fn send_mail(config: &Config, mail: &str, auth: &str, user: &str) -> Result<smtp::response::Response, smtp::Error> {
let sender = format!("UL Computer Society <{}>", &config.mail_user);
// Create the html we want to send.
let html = html! {
head {
title { "Hello from Skynet!" }
style type="text/css" {
"h2, h4 { font-family: Arial, Helvetica, sans-serif; }"
}
}
div style="display: flex; flex-direction: column; align-items: center;" {
h2 { "Hello from Skynet!" }
// Substitute in the name of our recipient.
p { "Hi " (user) "," }
p {
"Please use " pre { "/verify_committee code: " (auth)} " to verify your discord account."
}
p {
"Skynet Team"
br;
"UL Computer Society"
}
}
};
let body_text = format!(
r#"
Hi {user}
Please use "/verify_committee code: {auth}" to verify your discord account.
Skynet Team
UL Computer Society
"#
);
// Build the message.
let email = Message::builder()
.from(sender.parse().unwrap())
.to(mail.parse().unwrap())
.subject("Skynet-Discord: Link Committee.")
.multipart(
// This is composed of two parts.
// also helps not trip spam settings (uneven number of url's
MultiPart::alternative()
.singlepart(SinglePart::builder().header(header::ContentType::TEXT_PLAIN).body(body_text))
.singlepart(SinglePart::builder().header(header::ContentType::TEXT_HTML).body(html.into_string())),
)
.expect("failed to build email");
let creds = Credentials::new(config.mail_user.clone(), config.mail_pass.clone());
// Open a remote connection to gmail using STARTTLS
let mailer = SmtpTransport::starttls_relay(&config.mail_smtp).unwrap().credentials(creds).build();
// Send the email
mailer.send(&email)
}
pub async fn get_verify_from_db(db: &Pool<Sqlite>, user: &UserId) -> Option<Committee> {
sqlx::query_as::<_, Committee>(
r#"
SELECT *
FROM committee
WHERE discord = ?
"#,
)
.bind(*user.as_u64() as i64)
.fetch_one(db)
.await
.ok()
}
async fn save_to_db(db: &Pool<Sqlite>, email: &str, auth: &str, user: &UserId) -> Result<Option<Committee>, sqlx::Error> {
sqlx::query_as::<_, Committee>(
"
INSERT INTO committee (email, discord, auth_code)
VALUES (?1, ?2, ?3)
",
)
.bind(email.to_owned())
.bind(*user.as_u64() as i64)
.bind(auth.to_owned())
.fetch_optional(db)
.await
}
}
pub mod verify {
use super::*;
use crate::commands::committee::link::get_verify_from_db;
use serenity::model::id::{GuildId, RoleId};
use serenity::model::user::User;
use skynet_discord_bot::Committee;
use sqlx::Error;
pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String {
let committee_server = GuildId(1220150752656363520);
match command.guild_id {
None => {
return "Not in correct discord server.".to_string();
}
Some(x) => {
if x != committee_server {
return "Not in correct discord server.".to_string();
}
}
}
let db_lock = {
let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Database in TypeMap.").clone()
};
let db = db_lock.read().await;
// check if user has used /link_committee
let details = if let Some(x) = get_verify_from_db(&db, &command.user.id).await {
x
} else {
return "Please use /link_committee first".to_string();
};
let option = command
.data
.options
.first()
.expect("Expected code option")
.resolved
.as_ref()
.expect("Expected code object");
let code = if let CommandDataOptionValue::String(code) = option {
code
} else {
return "Please provide a verification code".to_string();
};
if &details.auth_code != code {
return "Invalid verification code".to_string();
}
match set_discord(&db, &command.user.id).await {
Ok(_) => {
// get teh right roles for the user
set_server_roles(&command.user, ctx).await;
"Discord username linked to Wolves for committee".to_string()
}
Err(e) => {
println!("{:?}", e);
"Failed to save, please try /link_committee again".to_string()
}
}
}
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
command
.name("verify_committee")
.description("Verify Wolves Committee Email")
.create_option(|option| {
option
.name("code")
.description("Code from verification email")
.kind(CommandOptionType::String)
.required(true)
})
}
async fn set_discord(db: &Pool<Sqlite>, discord: &UserId) -> Result<Option<Committee>, Error> {
sqlx::query_as::<_, Committee>(
"
UPDATE committee
SET committee = 1
WHERE discord = ?
",
)
.bind(*discord.as_u64() as i64)
.fetch_optional(db)
.await
}
async fn set_server_roles(discord: &User, ctx: &Context) {
let committee_server = GuildId(1220150752656363520);
if let Ok(mut member) = committee_server.member(&ctx.http, &discord.id).await {
let committee_member = RoleId(1226602779968274573);
if let Err(e) = member.add_role(&ctx, committee_member).await {
println!("{:?}", e);
}
}
}
}

View file

@ -15,7 +15,7 @@ use serenity::{
};
use skynet_discord_bot::{get_now_iso, random_string, Config, DataBase, Wolves, WolvesVerify};
use sqlx::{Pool, Sqlite};
pub mod link {
pub(crate) mod link {
use super::*;
pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String {
@ -44,7 +44,7 @@ pub mod link {
let option = command
.data
.options
.first()
.get(0)
.expect("Expected email option")
.resolved
.as_ref()
@ -87,12 +87,12 @@ pub mod link {
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
command
.name("link_wolves")
.name("link")
.description("Set Wolves Email")
.create_option(|option| option.name("email").description("UL Wolves Email").kind(CommandOptionType::String).required(true))
}
pub async fn get_server_member_discord(db: &Pool<Sqlite>, user: &UserId) -> Option<Wolves> {
async fn get_server_member_discord(db: &Pool<Sqlite>, user: &UserId) -> Option<Wolves> {
sqlx::query_as::<_, Wolves>(
r#"
SELECT *
@ -234,7 +234,7 @@ pub mod link {
}
}
pub mod verify {
pub(crate) mod verify {
use super::*;
use crate::commands::link_email::link::{db_pending_clear_expired, get_verify_from_db};
use serenity::model::user::User;
@ -248,17 +248,17 @@ pub mod verify {
};
let db = db_lock.read().await;
// check if user has used /link_wolves
// check if user has used /link
let details = if let Some(x) = get_verify_from_db(&db, &command.user.id).await {
x
} else {
return "Please use /link_wolves first".to_string();
return "Please use /link first".to_string();
};
let option = command
.data
.options
.first()
.get(0)
.expect("Expected code option")
.resolved
.as_ref()
@ -286,7 +286,7 @@ pub mod verify {
}
Err(e) => {
println!("{:?}", e);
"Failed to save, please try /link_wolves again".to_string()
"Failed to save, please try /link again".to_string()
}
};
}

View file

@ -1,334 +0,0 @@
use serenity::{
builder::CreateApplicationCommand,
client::Context,
model::{
application::interaction::application_command::ApplicationCommandInteraction,
prelude::{command::CommandOptionType, interaction::application_command::CommandDataOptionValue},
},
};
use skynet_discord_bot::DataBase;
use sqlx::{Pool, Sqlite};
pub(crate) mod user {
use super::*;
pub(crate) mod add {
use super::*;
use crate::commands::link_email::link::get_server_member_discord;
use serenity::model::id::UserId;
use skynet_discord_bot::{whitelist_update, Config, Minecraft, Wolves};
use sqlx::Error;
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
command.name("link_minecraft").description("Link your minecraft account").create_option(|option| {
option
.name("minecraft-username")
.description("Your Minecraft username")
.kind(CommandOptionType::String)
.required(true)
})
}
pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String {
let db_lock = {
let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Databse in TypeMap.").clone()
};
let db = db_lock.read().await;
let config_lock = {
let data_read = ctx.data.read().await;
data_read.get::<Config>().expect("Expected Config in TypeMap.").clone()
};
let config = config_lock.read().await;
// user has to have previously linked with wolves
if get_server_member_discord(&db, &command.user.id).await.is_none() {
return "Not linked with wolves, please use ``/link_wolves`` with your wolves email.".to_string();
}
let username = if let CommandDataOptionValue::String(username) = command
.data
.options
.first()
.expect("Expected username option")
.resolved
.as_ref()
.expect("Expected username object")
{
username.trim()
} else {
return "Please provide a valid username".to_string();
};
// insert the username into the database
match add_minecraft(&db, &command.user.id, username).await {
Ok(_) => {}
Err(e) => {
dbg!("{:?}", e);
return format!("Failure to minecraft username {:?}", username);
}
}
// get a list of servers that the user is a member of
if let Ok(servers) = get_servers(&db, &command.user.id).await {
for server in servers {
whitelist_update(&vec![username.to_string()], &server.minecraft, &config.discord_token_minecraft).await;
}
}
"Added/Updated minecraft_user info".to_string()
}
async fn add_minecraft(db: &Pool<Sqlite>, user: &UserId, minecraft: &str) -> Result<Option<Wolves>, Error> {
sqlx::query_as::<_, Wolves>(
"
UPDATE wolves
SET minecraft = ?2
WHERE discord = ?1;
",
)
.bind(*user.as_u64() as i64)
.bind(minecraft)
.fetch_optional(db)
.await
}
async fn get_servers(db: &Pool<Sqlite>, discord: &UserId) -> Result<Vec<Minecraft>, Error> {
sqlx::query_as::<_, Minecraft>(
"
SELECT minecraft.*
FROM minecraft
JOIN (
SELECT server
FROM server_members
JOIN wolves USING (id_wolves)
WHERE discord = ?1
) sub on minecraft.server_discord = sub.server
",
)
.bind(*discord.as_u64() as i64)
.fetch_all(db)
.await
}
}
}
pub(crate) mod server {
use super::*;
pub(crate) mod add {
use serenity::model::id::GuildId;
use sqlx::Error;
// this is to managfe the server side of commands related to minecraft
use super::*;
use skynet_discord_bot::{is_admin, update_server, Config, Minecraft};
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
command.name("minecraft_add").description("Add a minecraft server").create_option(|option| {
option
.name("server_id")
.description("ID of the Minecraft server hosted by the Computer Society")
.kind(CommandOptionType::String)
.required(true)
})
}
pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String {
// check if user has high enough permisssions
if let Some(msg) = is_admin(command, ctx).await {
return msg;
}
let g_id = match command.guild_id {
None => return "Not in a server".to_string(),
Some(x) => x,
};
let server_minecraft = if let CommandDataOptionValue::String(id) = command
.data
.options
.first()
.expect("Expected server_id option")
.resolved
.as_ref()
.expect("Expected server_id object")
{
id.to_owned()
} else {
return String::from("Expected Server ID");
};
let db_lock = {
let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Databse in TypeMap.").clone()
};
let db = db_lock.read().await;
match add_server(&db, &g_id, &server_minecraft).await {
Ok(_) => {}
Err(e) => {
println!("{:?}", e);
return format!("Failure to insert into Minecraft {} {}", &g_id, &server_minecraft);
}
}
let config_lock = {
let data_read = ctx.data.read().await;
data_read.get::<Config>().expect("Expected Config in TypeMap.").clone()
};
let config = config_lock.read().await;
update_server(&server_minecraft, &db, &g_id, &config).await;
"Added/Updated minecraft_server info".to_string()
}
async fn add_server(db: &Pool<Sqlite>, discord: &GuildId, minecraft: &str) -> Result<Option<Minecraft>, Error> {
sqlx::query_as::<_, Minecraft>(
"
INSERT OR REPLACE INTO minecraft (server_discord, server_minecraft)
VALUES (?1, ?2)
",
)
.bind(*discord.as_u64() as i64)
.bind(minecraft)
.fetch_optional(db)
.await
}
}
pub(crate) mod list {
use serenity::builder::CreateApplicationCommand;
use serenity::client::Context;
use serenity::model::prelude::application_command::ApplicationCommandInteraction;
use skynet_discord_bot::{get_minecraft_config_server, is_admin, server_information, Config, DataBase};
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
command.name("minecraft_list").description("List your minecraft servers")
}
pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String {
if let Some(msg) = is_admin(command, ctx).await {
return msg;
}
let g_id = match command.guild_id {
None => return "Not in a server".to_string(),
Some(x) => x,
};
let db_lock = {
let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Databse in TypeMap.").clone()
};
let db = db_lock.read().await;
let servers = get_minecraft_config_server(&db, g_id).await;
if servers.is_empty() {
return "No minecraft servers, use /minecraft_add to add one".to_string();
}
let config_lock = {
let data_read = ctx.data.read().await;
data_read.get::<Config>().expect("Expected Config in TypeMap.").clone()
};
let config = config_lock.read().await;
let mut result = "Server Information:\n".to_string();
for server in get_minecraft_config_server(&db, g_id).await {
if let Some(x) = server_information(&server.minecraft, &config.discord_token_minecraft).await {
result.push_str(&format!(
r#"
Name: {name}
ID: {id}
Online: {online}
Info: {description}
Link: <http://panel.games.skynet.ie/server/{id}>
"#,
name = &x.attributes.name,
online = !x.attributes.is_suspended,
description = &x.attributes.description,
id = &x.attributes.identifier
));
}
}
result.to_string()
}
}
pub(crate) mod delete {
use serenity::builder::CreateApplicationCommand;
use serenity::client::Context;
use serenity::model::application::command::CommandOptionType;
use serenity::model::id::GuildId;
use serenity::model::prelude::application_command::{ApplicationCommandInteraction, CommandDataOptionValue};
use skynet_discord_bot::{is_admin, DataBase, Minecraft};
use sqlx::{Error, Pool, Sqlite};
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
command.name("minecraft_delete").description("Delete a minecraft server").create_option(|option| {
option
.name("server_id")
.description("ID of the Minecraft server hosted by the Computer Society")
.kind(CommandOptionType::String)
.required(true)
})
}
pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String {
// check if user has high enough permisssions
if let Some(msg) = is_admin(command, ctx).await {
return msg;
}
let g_id = match command.guild_id {
None => return "Not in a server".to_string(),
Some(x) => x,
};
let server_minecraft = if let CommandDataOptionValue::String(id) = command
.data
.options
.first()
.expect("Expected server_id option")
.resolved
.as_ref()
.expect("Expected server_id object")
{
id.to_owned()
} else {
return String::from("Expected Server ID");
};
let db_lock = {
let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Databse in TypeMap.").clone()
};
let db = db_lock.read().await;
match server_remove(&db, &g_id, &server_minecraft).await {
Ok(_) => {}
Err(e) => {
println!("{:?}", e);
return format!("Failure to insert into Minecraft {} {}", &g_id, &server_minecraft);
}
}
// no need to clear teh whitelist as it will be reset within 24hr anyways
"Removed minecraft_server info".to_string()
}
async fn server_remove(db: &Pool<Sqlite>, discord: &GuildId, minecraft: &str) -> Result<Option<Minecraft>, Error> {
sqlx::query_as::<_, Minecraft>(
"
DELETE FROM minecraft
WHERE server_discord = ?1 AND server_minecraft = ?2
",
)
.bind(*discord.as_u64() as i64)
.bind(minecraft)
.fetch_optional(db)
.await
}
}
}

View file

@ -1,4 +1,2 @@
pub mod add_server;
pub mod committee;
pub mod link_email;
pub mod minecraft;

View file

@ -8,13 +8,10 @@ use serenity::{
prelude::TypeMapKey,
};
use crate::set_roles::get_server_member_bulk;
use chrono::{Datelike, SecondsFormat, Utc};
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use serde::de::DeserializeOwned;
use serenity::client::Context;
use serenity::model::id::UserId;
use serenity::model::prelude::application_command::ApplicationCommandInteraction;
use sqlx::{
sqlite::{SqliteConnectOptions, SqlitePoolOptions, SqliteRow},
Error, FromRow, Pool, Row, Sqlite,
@ -23,20 +20,18 @@ use std::{env, str::FromStr, sync::Arc};
use tokio::sync::RwLock;
pub struct Config {
// manages where teh database is stored
pub skynet_server: GuildId,
pub ldap_api: String,
pub home: String,
pub database: String,
// tokens for discord and other API's
pub auth: String,
pub discord_token: String,
pub discord_token_minecraft: String,
// email settings
pub mail_smtp: String,
pub mail_user: String,
pub mail_pass: String,
// wolves API base for clubs/socs
pub wolves_url: String,
}
impl TypeMapKey for Config {
@ -53,8 +48,10 @@ pub fn get_config() -> Config {
// reasonable defaults
let mut config = Config {
skynet_server: Default::default(),
ldap_api: "https://api.account.skynet.ie".to_string(),
auth: "".to_string(),
discord_token: "".to_string(),
discord_token_minecraft: "".to_string(),
home: ".".to_string(),
database: "database.db".to_string(),
@ -65,6 +62,12 @@ pub fn get_config() -> Config {
wolves_url: "".to_string(),
};
if let Ok(x) = env::var("LDAP_API") {
config.ldap_api = x.trim().to_string();
}
if let Ok(x) = env::var("SKYNET_SERVER") {
config.skynet_server = GuildId::from(str_to_num::<u64>(&x));
}
if let Ok(x) = env::var("HOME") {
config.home = x.trim().to_string();
}
@ -72,12 +75,13 @@ pub fn get_config() -> Config {
config.database = x.trim().to_string();
}
if let Ok(x) = env::var("LDAP_DISCORD_AUTH") {
config.auth = x.trim().to_string();
}
if let Ok(x) = env::var("DISCORD_TOKEN") {
config.discord_token = x.trim().to_string();
}
if let Ok(x) = env::var("DISCORD_TOKEN_MINECRAFT") {
config.discord_token_minecraft = x.trim().to_string();
}
if let Ok(x) = env::var("EMAIL_SMTP") {
config.mail_smtp = x.trim().to_string();
@ -96,6 +100,10 @@ pub fn get_config() -> Config {
config
}
fn str_to_num<T: FromStr + Default>(x: &str) -> T {
x.trim().parse::<T>().unwrap_or_default()
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ServerMembers {
pub server: GuildId,
@ -202,27 +210,6 @@ impl<'r> FromRow<'r, SqliteRow> for WolvesVerify {
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Committee {
pub email: String,
pub discord: UserId,
pub auth_code: String,
pub committee: i64,
}
impl<'r> FromRow<'r, SqliteRow> for Committee {
fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
let user_tmp: i64 = row.try_get("discord")?;
let discord = UserId::from(user_tmp as u64);
Ok(Self {
email: row.try_get("email")?,
discord,
auth_code: row.try_get("auth_code")?,
committee: row.try_get("committee")?,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Servers {
pub server: GuildId,
@ -270,23 +257,6 @@ impl<'r> FromRow<'r, SqliteRow> for Servers {
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Minecraft {
pub discord: GuildId,
pub minecraft: String,
}
impl<'r> FromRow<'r, SqliteRow> for Minecraft {
fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
let server_tmp: i64 = row.try_get("server_discord")?;
let discord = GuildId::from(server_tmp as u64);
Ok(Self {
discord,
minecraft: row.try_get("server_minecraft")?,
})
}
}
pub async fn db_init(config: &Config) -> Result<Pool<Sqlite>, Error> {
let database = format!("{}/{}", &config.home, &config.database);
@ -299,8 +269,58 @@ pub async fn db_init(config: &Config) -> Result<Pool<Sqlite>, Error> {
)
.await?;
// migrations are amazing!
sqlx::migrate!("./db/migrations").run(&pool).await.unwrap();
sqlx::query(
"CREATE TABLE IF NOT EXISTS wolves (
id_wolves integer PRIMARY KEY,
email text not null,
discord integer,
minecraft text
)",
)
.execute(&pool)
.await?;
sqlx::query("CREATE INDEX IF NOT EXISTS index_discord ON wolves (discord)").execute(&pool).await?;
sqlx::query(
"CREATE TABLE IF NOT EXISTS wolves_verify (
discord integer PRIMARY KEY,
email text not null,
auth_code text not null,
date_expiry text not null
)",
)
.execute(&pool)
.await?;
sqlx::query("CREATE INDEX IF NOT EXISTS index_date_expiry ON wolves_verify (date_expiry)")
.execute(&pool)
.await?;
sqlx::query(
"CREATE TABLE IF NOT EXISTS server_members (
server integer not null,
id_wolves integer not null,
expiry text not null,
PRIMARY KEY(server,id_wolves),
FOREIGN KEY (id_wolves) REFERENCES wolves (id_wolves)
)",
)
.execute(&pool)
.await?;
sqlx::query(
"CREATE TABLE IF NOT EXISTS servers (
server integer PRIMARY KEY,
wolves_api text not null,
role_past integer,
role_current integer,
member_past integer DEFAULT 0,
member_current integer DEFAULT 0
)",
)
.execute(&pool)
.await?;
Ok(pool)
}
@ -361,7 +381,7 @@ pub fn random_string(len: usize) -> String {
pub mod set_roles {
use super::*;
pub async fn update_server(ctx: &Context, server: &Servers, remove_roles: &[Option<RoleId>], members_changed: &[UserId]) {
pub async fn update_server(ctx: &Context, server: &Servers, remove_roles: &[Option<RoleId>], members_changed: &Vec<UserId>) {
let db_lock = {
let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Database in TypeMap.").clone()
@ -446,7 +466,7 @@ pub mod set_roles {
println!("{:?} Changes: New: +{}, Current: +{}/-{}", server.as_u64(), roles_set[0], roles_set[1], roles_set[2]);
}
pub async fn get_server_member_bulk(db: &Pool<Sqlite>, server: &GuildId) -> Vec<ServerMembersWolves> {
async fn get_server_member_bulk(db: &Pool<Sqlite>, server: &GuildId) -> Vec<ServerMembersWolves> {
sqlx::query_as::<_, ServerMembersWolves>(
r#"
SELECT *
@ -497,11 +517,10 @@ pub mod get_data {
#[derive(Deserialize, Serialize, Debug)]
struct WolvesResultUser {
committee: String,
member_id: String,
wolves_id: String,
first_name: String,
last_name: String,
contact_email: String,
opt_in_email: String,
student_id: Option<String>,
note: Option<String>,
expiry: String,
@ -549,7 +568,7 @@ pub mod get_data {
// list of users that need to be updated for this server
let mut user_to_update = vec![];
for user in get_wolves_sub(&config, wolves_api).await {
let id = user.member_id.parse::<u64>().unwrap_or_default();
let id = user.wolves_id.parse::<u64>().unwrap_or_default();
match existing.get(&(id as i64)) {
None => {
// user does not exist already, add everything
@ -626,7 +645,7 @@ pub mod get_data {
ON CONFLICT(id_wolves) DO UPDATE SET email = $2
",
)
.bind(&user.member_id)
.bind(&user.wolves_id)
.bind(&user.contact_email)
.fetch_optional(db)
.await
@ -646,7 +665,7 @@ pub mod get_data {
",
)
.bind(*server.as_u64() as i64)
.bind(&user.member_id)
.bind(&user.wolves_id)
.bind(&user.expiry)
.fetch_optional(db)
.await
@ -659,179 +678,3 @@ pub mod get_data {
}
}
}
/**
For any time ye need to check if a user who calls a command has admin privlages
*/
pub async fn is_admin(command: &ApplicationCommandInteraction, ctx: &Context) -> Option<String> {
let mut admin = false;
let g_id = match command.guild_id {
None => return Some("Not in a server".to_string()),
Some(x) => x,
};
let roles_server = g_id.roles(&ctx.http).await.unwrap_or_default();
if let Ok(member) = g_id.member(&ctx.http, command.user.id).await {
if let Some(permissions) = member.permissions {
if permissions.administrator() {
admin = true;
}
}
for role_id in member.roles {
if admin {
break;
}
if let Some(role) = roles_server.get(&role_id) {
if role.permissions.administrator() {
admin = true;
}
}
}
}
if !admin {
Some("Administrator permission required".to_string())
} else {
None
}
}
/**
loop through all members of server
get a list of folks with mc accounts that are members
and a list that arent members
*/
pub async fn update_server(server_id: &str, db: &Pool<Sqlite>, g_id: &GuildId, config: &Config) {
let mut usernames = vec![];
for member in get_server_member_bulk(db, g_id).await {
if let Some(x) = member.minecraft {
usernames.push(x);
}
}
if !usernames.is_empty() {
whitelist_update(&usernames, server_id, &config.discord_token_minecraft).await;
}
}
async fn post<T: Serialize>(url: &str, bearer: &str, data: &T) {
match surf::post(url)
.header("Authorization", bearer)
.header("Content-Type", "application/json")
.header("Accept", "Application/vnd.pterodactyl.v1+json")
.body_json(&data)
{
Ok(req) => {
req.await.ok();
}
Err(e) => {
dbg!(e);
}
}
}
#[derive(Deserialize, Serialize, Debug)]
pub struct ServerDetailsResSub {
pub identifier: String,
pub name: String,
pub description: String,
pub is_suspended: bool,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct ServerDetailsRes {
pub attributes: ServerDetailsResSub,
}
async fn get<T: Serialize + DeserializeOwned>(url: &str, bearer: &str) -> Option<T> {
match surf::get(url)
.header("Authorization", bearer)
.header("Content-Type", "application/json")
.header("Accept", "Application/vnd.pterodactyl.v1+json")
.recv_json()
.await
{
Ok(res) => Some(res),
Err(e) => {
dbg!(e);
None
}
}
}
#[derive(Deserialize, Serialize, Debug)]
struct BodyCommand {
command: String,
}
#[derive(Deserialize, Serialize, Debug)]
struct BodyDelete {
root: String,
files: Vec<String>,
}
pub async fn whitelist_update(add: &Vec<String>, server: &str, token: &str) {
let url_base = format!("http://panel.games.skynet.ie/api/client/servers/{server}");
let bearer = format!("Bearer {token}");
for name in add {
let data = BodyCommand {
command: format!("whitelist add {name}"),
};
post(&format!("{url_base}/command"), &bearer, &data).await;
}
}
pub async fn whitelist_wipe(server: &str, token: &str) {
let url_base = format!("http://panel.games.skynet.ie/api/client/servers/{server}");
let bearer = format!("Bearer {token}");
// delete whitelist
let deletion = BodyDelete {
root: "/".to_string(),
files: vec!["whitelist.json".to_string()],
};
post(&format!("{url_base}/files/delete"), &bearer, &deletion).await;
// recreate teh file, passing in the type here so the compiler knows what type of vec it is
post::<Vec<&str>>(&format!("{url_base}/files/write?file=%2Fwhitelist.json"), &bearer, &vec![]).await;
// reload the whitelist
let data = BodyCommand {
command: "whitelist reload".to_string(),
};
post(&format!("{url_base}/command"), &bearer, &data).await;
}
pub async fn server_information(server: &str, token: &str) -> Option<ServerDetailsRes> {
let url_base = format!("http://panel.games.skynet.ie/api/client/servers/{server}");
let bearer = format!("Bearer {token}");
get::<ServerDetailsRes>(&format!("{url_base}/"), &bearer).await
}
pub async fn get_minecraft_config(db: &Pool<Sqlite>) -> Vec<Minecraft> {
sqlx::query_as::<_, Minecraft>(
r#"
SELECT *
FROM minecraft
"#,
)
.fetch_all(db)
.await
.unwrap_or_default()
}
pub async fn get_minecraft_config_server(db: &Pool<Sqlite>, g_id: GuildId) -> Vec<Minecraft> {
sqlx::query_as::<_, Minecraft>(
r#"
SELECT *
FROM minecraft
WHERE server_discord = ?1
"#,
)
.bind(*g_id.as_u64() as i64)
.fetch_all(db)
.await
.unwrap_or_default()
}

View file

@ -1,4 +1,4 @@
pub mod commands;
mod commands;
use serenity::{
async_trait,
@ -60,13 +60,6 @@ impl EventHandler for Handler {
.create_application_command(|command| commands::add_server::register(command))
.create_application_command(|command| commands::link_email::link::register(command))
.create_application_command(|command| commands::link_email::verify::register(command))
.create_application_command(|command| commands::minecraft::server::add::register(command))
.create_application_command(|command| commands::minecraft::server::list::register(command))
.create_application_command(|command| commands::minecraft::server::delete::register(command))
.create_application_command(|command| commands::minecraft::user::add::register(command))
// for committee server, temp
.create_application_command(|command| commands::committee::link::register(command))
.create_application_command(|command| commands::committee::verify::register(command))
})
.await
{
@ -83,18 +76,9 @@ impl EventHandler for Handler {
//println!("Received command interaction: {:#?}", command);
let content = match command.data.name.as_str() {
// user commands
"link_wolves" => commands::link_email::link::run(&command, &ctx).await,
"verify" => commands::link_email::verify::run(&command, &ctx).await,
"link_minecraft" => commands::minecraft::user::add::run(&command, &ctx).await,
// admin commands
"add" => commands::add_server::run(&command, &ctx).await,
"minecraft_add" => commands::minecraft::server::add::run(&command, &ctx).await,
"minecraft_list" => commands::minecraft::server::list::run(&command, &ctx).await,
"minecraft_delete" => commands::minecraft::server::delete::run(&command, &ctx).await,
// for teh committee server, temporary
"link_committee" => commands::committee::link::run(&command, &ctx).await,
"verify_committee" => commands::committee::verify::run(&command, &ctx).await,
"link" => commands::link_email::link::run(&command, &ctx).await,
"verify" => commands::link_email::verify::run(&command, &ctx).await,
_ => "not implemented :(".to_string(),
};