Compare commits

..

6 commits

Author SHA1 Message Date
01d883d367
doc: feedback 2024-11-18 16:38:33 +00:00
d8d4bbd706
doc: resized images using html tags 2024-11-18 16:22:40 +00:00
436295bd61
doc: added images to suer signup
All checks were successful
On_Push / lint_fmt (push) Successful in 1m11s
On_Push / lint_clippy (push) Successful in 3m23s
On_Push / build (push) Successful in 7m31s
On_Push / deploy (push) Successful in 12s
2024-11-18 16:14:16 +00:00
94292fa388
fix: style was causing issues 2024-11-18 16:09:43 +00:00
2daa010d25
doc: updated committee instructions 2024-11-04 12:43:09 +00:00
da4d006bc0
doc: update user docs 2024-10-29 14:00:43 +00:00
23 changed files with 1173 additions and 1758 deletions

341
Cargo.lock generated
View file

@ -1024,26 +1024,7 @@ dependencies = [
"futures-core",
"futures-sink",
"futures-util",
"http 0.2.12",
"indexmap",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "h2"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205"
dependencies = [
"atomic-waker",
"bytes 1.7.1",
"fnv",
"futures-core",
"futures-sink",
"http 1.1.0",
"http",
"indexmap",
"slab",
"tokio",
@ -1166,17 +1147,6 @@ dependencies = [
"itoa",
]
[[package]]
name = "http"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258"
dependencies = [
"bytes 1.7.1",
"fnv",
"itoa",
]
[[package]]
name = "http-body"
version = "0.4.6"
@ -1184,30 +1154,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
dependencies = [
"bytes 1.7.1",
"http 0.2.12",
"pin-project-lite",
]
[[package]]
name = "http-body"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
dependencies = [
"bytes 1.7.1",
"http 1.1.0",
]
[[package]]
name = "http-body-util"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
dependencies = [
"bytes 1.7.1",
"futures-util",
"http 1.1.0",
"http-body 1.0.1",
"http",
"pin-project-lite",
]
@ -1269,9 +1216,9 @@ dependencies = [
"futures-channel",
"futures-core",
"futures-util",
"h2 0.3.26",
"http 0.2.12",
"http-body 0.4.6",
"h2",
"http",
"http-body",
"httparse",
"httpdate",
"itoa",
@ -1283,26 +1230,6 @@ dependencies = [
"want",
]
[[package]]
name = "hyper"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a"
dependencies = [
"bytes 1.7.1",
"futures-channel",
"futures-util",
"h2 0.4.6",
"http 1.1.0",
"http-body 1.0.1",
"httparse",
"itoa",
"pin-project-lite",
"smallvec",
"tokio",
"want",
]
[[package]]
name = "hyper-rustls"
version = "0.24.2"
@ -1310,65 +1237,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
dependencies = [
"futures-util",
"http 0.2.12",
"hyper 0.14.30",
"http",
"hyper",
"rustls 0.21.12",
"tokio",
"tokio-rustls 0.24.1",
]
[[package]]
name = "hyper-rustls"
version = "0.27.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333"
dependencies = [
"futures-util",
"http 1.1.0",
"hyper 1.5.0",
"hyper-util",
"rustls 0.23.16",
"rustls-pki-types",
"tokio",
"tokio-rustls 0.26.0",
"tower-service",
]
[[package]]
name = "hyper-tls"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [
"bytes 1.7.1",
"http-body-util",
"hyper 1.5.0",
"hyper-util",
"native-tls",
"tokio",
"tokio-native-tls",
"tower-service",
]
[[package]]
name = "hyper-util"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
dependencies = [
"bytes 1.7.1",
"futures-channel",
"futures-util",
"http 1.1.0",
"http-body 1.0.1",
"hyper 1.5.0",
"pin-project-lite",
"socket2 0.5.7",
"tokio",
"tower-service",
"tracing",
]
[[package]]
name = "iana-time-zone"
version = "0.1.61"
@ -1455,7 +1330,7 @@ dependencies = [
"curl-sys",
"flume 0.9.2",
"futures-lite 1.13.0",
"http 0.2.12",
"http",
"log",
"once_cell",
"slab",
@ -2139,11 +2014,11 @@ dependencies = [
"encoding_rs",
"futures-core",
"futures-util",
"h2 0.3.26",
"http 0.2.12",
"http-body 0.4.6",
"hyper 0.14.30",
"hyper-rustls 0.24.2",
"h2",
"http",
"http-body",
"hyper",
"hyper-rustls",
"ipnet",
"js-sys",
"log",
@ -2153,12 +2028,12 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"rustls 0.21.12",
"rustls-pemfile 1.0.4",
"rustls-pemfile",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper 0.1.2",
"system-configuration 0.5.1",
"sync_wrapper",
"system-configuration",
"tokio",
"tokio-rustls 0.24.1",
"tokio-util",
@ -2172,49 +2047,6 @@ dependencies = [
"winreg",
]
[[package]]
name = "reqwest"
version = "0.12.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a77c62af46e79de0a562e1a9849205ffcb7fc1238876e9bd743357570e04046f"
dependencies = [
"base64 0.22.1",
"bytes 1.7.1",
"encoding_rs",
"futures-core",
"futures-util",
"h2 0.4.6",
"http 1.1.0",
"http-body 1.0.1",
"http-body-util",
"hyper 1.5.0",
"hyper-rustls 0.27.3",
"hyper-tls",
"hyper-util",
"ipnet",
"js-sys",
"log",
"mime",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
"rustls-pemfile 2.2.0",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper 1.0.1",
"system-configuration 0.6.1",
"tokio",
"tokio-native-tls",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows-registry",
]
[[package]]
name = "ring"
version = "0.16.20"
@ -2313,23 +2145,10 @@ checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
dependencies = [
"log",
"ring 0.17.8",
"rustls-webpki 0.101.7",
"rustls-webpki",
"sct",
]
[[package]]
name = "rustls"
version = "0.23.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e"
dependencies = [
"once_cell",
"rustls-pki-types",
"rustls-webpki 0.102.8",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-pemfile"
version = "1.0.4"
@ -2339,21 +2158,6 @@ dependencies = [
"base64 0.21.7",
]
[[package]]
name = "rustls-pemfile"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "rustls-pki-types"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b"
[[package]]
name = "rustls-webpki"
version = "0.101.7"
@ -2364,17 +2168,6 @@ dependencies = [
"untrusted 0.9.0",
]
[[package]]
name = "rustls-webpki"
version = "0.102.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
dependencies = [
"ring 0.17.8",
"rustls-pki-types",
"untrusted 0.9.0",
]
[[package]]
name = "ryu"
version = "1.0.18"
@ -2528,7 +2321,7 @@ dependencies = [
"mime_guess",
"parking_lot",
"percent-encoding",
"reqwest 0.11.27",
"reqwest",
"serde",
"serde-value",
"serde_json",
@ -2606,15 +2399,6 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
dependencies = [
"libc",
]
[[package]]
name = "signature"
version = "2.2.0"
@ -2634,9 +2418,7 @@ dependencies = [
"lettre",
"maud",
"rand 0.8.5",
"reqwest 0.12.9",
"serde",
"serde_json",
"serenity",
"sqlx",
"surf",
@ -3052,15 +2834,6 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
[[package]]
name = "sync_wrapper"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
dependencies = [
"futures-core",
]
[[package]]
name = "system-configuration"
version = "0.5.1"
@ -3069,18 +2842,7 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"system-configuration-sys 0.5.0",
]
[[package]]
name = "system-configuration"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
dependencies = [
"bitflags 2.6.0",
"core-foundation",
"system-configuration-sys 0.6.0",
"system-configuration-sys",
]
[[package]]
@ -3093,16 +2855,6 @@ dependencies = [
"libc",
]
[[package]]
name = "system-configuration-sys"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "tempfile"
version = "3.12.0"
@ -3230,9 +2982,7 @@ dependencies = [
"bytes 1.7.1",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2 0.5.7",
"tokio-macros",
"windows-sys 0.52.0",
@ -3249,16 +2999,6 @@ dependencies = [
"syn 2.0.77",
]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
dependencies = [
"native-tls",
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.23.4"
@ -3280,17 +3020,6 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-rustls"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
dependencies = [
"rustls 0.23.16",
"rustls-pki-types",
"tokio",
]
[[package]]
name = "tokio-stream"
version = "0.1.16"
@ -3378,7 +3107,7 @@ dependencies = [
"base64 0.13.1",
"byteorder",
"bytes 1.7.1",
"http 0.2.12",
"http",
"httparse",
"log",
"rand 0.8.5",
@ -3703,36 +3432,6 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-registry"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
dependencies = [
"windows-result",
"windows-strings",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-result"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-strings"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
dependencies = [
"windows-result",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.48.0"

View file

@ -17,8 +17,7 @@ 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", "full"] }
reqwest = { version = "0.12", features = ["json"] }
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
# to make the http requests
surf = "2.3.2"
@ -27,7 +26,6 @@ dotenvy = "0.15.7"
# For sqlite
sqlx = { version = "0.7.1", features = [ "runtime-tokio", "sqlite", "migrate" ] }
serde_json = { version = "1.0", features = ["raw_value"] }
# create random strings
rand = "0.8.5"

View file

@ -1,26 +1,58 @@
# 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.
This bots core purpose is to give members roles based on their status on <https://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
## Setup - Committee
You need admin access to run any of the commands in this section.
Either the server owner or a suer with the ``Administrator`` permission
Either the server owner or a user 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)
### Get the API Key
The ``api_key`` is used by the Bot in order to request information, it will be used later in the process.
1. Email ``keith@assurememberships.com`` from committee email and say you want an ``api_key`` for ``193.1.99.74``
* The committee email is the one here: <https://cp.ulwolves.ie/mailbox/>
* This may take up to a week to get the key.
### Setup Server
The Bot reason for existing is being able to give members Roles.
So we have to create those.
1. Create a role for Current Members.
* You can call it whatever you want.
* ``member-current`` is a good choice.
* This should be a new role
2. **Optional**: you can create a role that is given to folks who were but no longer a member.
* ``member`` would be a good choice for this
* If you have an existing member role this is also a good fit.
The reason for both roles is ye have one for active members while the second is for all current and past members.
### Invite Bot
1. Invite the bot https://discord.com/api/oauth2/authorize?client_id=1145761669256069270&permissions=139855185984&scope=bot
2. Make sure the bot role ``@skynet`` is above these two roles created in the previous step
* This is so it can manage the roles (give and remove them from users)
### Setup Bot
This is where the bot is configured.
You will need the ``api_key`` from the start of the process.
You (personally) will need a role with ``Administrator`` permission to be able to do this.
1. Use the command ``/add`` and a list of options will pop up.
2. ``api_key`` is the key you got from Keith earlier.
3. ``role_current`` is the ``member-current`` that you created earlier.
4. ``role_past`` (optional) is the role for all current and past members.
5. ``bot_channel`` is a channel that folks are recommended to use the bot.
* You can have it so folks cannot see message history
6. ``server_name`` For example ``UL Computer Society``
* Will be removed in the future
7. ``wolves_link`` for example <https://ulwolves.ie/society/computer>
* Will be removed in the future
At this point the bot is set up and no further action is required.
### 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.
@ -42,29 +74,24 @@ This unlinks a minecraft server from your club/society.
``/minecraft_delete SERVER_ID``
## Commands - User
## Setup - Users
This is to link your Discord account with your UL Wolves account.
**You will only need to do this once**.
### 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.
1. In a Discord server with the Skynet Bot enter ``/link_wolves YOUR_WOLVES_CONTACT_EMAIL``
<img src="media/setup_user_01.png" alt="link process start" width="50%" height="50%">
* Your ``YOUR_WOLVES_CONTACT_EMAIL`` is the email in the Contact Email here: <https://ulwolves.ie/memberships/profile>
* This is most likely your student mail
2. An email will be sent to you with a verification code.
<img src="media/setup_user_02.png" alt="signup email" width="50%" height="50%">
3. Verify the code using ``/verify CODE_FROM_EMAIL`` in Discord.
<img src="media/setup_user_03.png" alt="verify in discord" width="50%" height="50%">
4. Once complete your Wolves and Discord accounts will be linked.
* 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
You will get member roles on any Discord that is using the bot that you are a member of.
### Minecraft
Users can link their Minecraft username to grant them access to any servers where teh whitelist is managed by teh bot.
You can link your Minecraft username to grant you access to any Minecraft server run by UL Computer Society.
``/link_minecraft MINECRAFT_USERNAME``

View file

@ -1,10 +0,0 @@
-- No longer using the previous committee table
DROP TABLE committee;
-- new table pulling from teh api
CREATE TABLE IF NOT EXISTS committees (
id integer PRIMARY KEY,
name text not null,
link text not null,
committee text not null
);

BIN
media/setup_user_01.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

BIN
media/setup_user_02.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

BIN
media/setup_user_03.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View file

@ -4,10 +4,7 @@ use serenity::{
model::gateway::{GatewayIntents, Ready},
Client,
};
use skynet_discord_bot::common::database::{db_init, DataBase};
use skynet_discord_bot::common::wolves::cns::get_wolves;
use skynet_discord_bot::common::wolves::committees::get_cns;
use skynet_discord_bot::{get_config, Config};
use skynet_discord_bot::{db_init, get_config, get_data::get_wolves, Config, DataBase};
use std::{process, sync::Arc};
use tokio::sync::RwLock;
@ -46,12 +43,8 @@ impl EventHandler for Handler {
let ctx = Arc::new(ctx);
println!("{} is connected!", ready.user.name);
// get the data for each individual club/soc
get_wolves(&ctx).await;
// get teh data for the clubs/socs committees
get_cns(&ctx).await;
// finish up
process::exit(0);
}

View file

@ -1,6 +1,4 @@
use skynet_discord_bot::common::database::db_init;
use skynet_discord_bot::common::minecraft::{get_minecraft_config, update_server, whitelist_wipe};
use skynet_discord_bot::get_config;
use skynet_discord_bot::{db_init, get_config, get_minecraft_config, update_server, whitelist_wipe};
use std::collections::HashSet;
#[tokio::main]

View file

@ -4,9 +4,7 @@ use serenity::{
model::gateway::{GatewayIntents, Ready},
Client,
};
use skynet_discord_bot::common::database::{db_init, get_server_config_bulk, DataBase};
use skynet_discord_bot::common::set_roles::{committee, normal};
use skynet_discord_bot::{get_config, Config};
use skynet_discord_bot::{db_init, get_config, get_server_config_bulk, set_roles, Config, DataBase};
use std::{process, sync::Arc};
use tokio::sync::RwLock;
@ -45,18 +43,14 @@ impl EventHandler for Handler {
let ctx = Arc::new(ctx);
println!("{} is connected!", ready.user.name);
// this goes into each server and sets roles for each wolves member
check_bulk(Arc::clone(&ctx)).await;
// u[date committee server
committee::check_committee(Arc::clone(&ctx)).await;
bulk_check(Arc::clone(&ctx)).await;
// finish up
process::exit(0);
}
}
async fn check_bulk(ctx: Arc<Context>) {
async fn bulk_check(ctx: Arc<Context>) {
let db_lock = {
let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Config in TypeMap.").clone()
@ -65,6 +59,6 @@ async fn check_bulk(ctx: Arc<Context>) {
let db = db_lock.read().await;
for server_config in get_server_config_bulk(&db).await {
normal::update_server(&ctx, &server_config, &[], &[]).await;
set_roles::update_server(&ctx, &server_config, &[], &[]).await;
}
}

View file

@ -6,10 +6,8 @@ use serenity::{
prelude::{command::CommandOptionType, interaction::application_command::CommandDataOptionValue},
},
};
use skynet_discord_bot::common::database::{get_server_config, DataBase, Servers};
use skynet_discord_bot::common::set_roles::normal::update_server;
use skynet_discord_bot::common::wolves::cns::get_wolves;
use skynet_discord_bot::is_admin;
use skynet_discord_bot::get_data::get_wolves;
use skynet_discord_bot::{get_server_config, is_admin, set_roles::update_server, DataBase, Servers};
use sqlx::{Error, Pool, Sqlite};
pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String {

311
src/commands/committee.rs Normal file
View file

@ -0,0 +1,311 @@
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)?.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

@ -13,13 +13,10 @@ use serenity::{
prelude::{command::CommandOptionType, interaction::application_command::CommandDataOptionValue},
},
};
use skynet_discord_bot::common::database::{DataBase, Wolves, WolvesVerify};
use skynet_discord_bot::{get_now_iso, random_string, Config};
use skynet_discord_bot::{get_now_iso, random_string, Config, DataBase, Wolves, WolvesVerify};
use sqlx::{Pool, Sqlite};
pub mod link {
use super::*;
use serde::{Deserialize, Serialize};
pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String {
let db_lock = {
@ -62,32 +59,7 @@ pub mod link {
// check if email exists
let details = match get_server_member_email(&db, email).await {
None => {
let invalid_user = "Please check it matches (including case) your preferred contact on https://ulwolves.ie/memberships/profile and that you are fully paid up.".to_string();
// see if the user actually exists
let id = match get_user(&config, email).await {
None => {
return invalid_user;
}
Some(x) => x,
};
// save teh user id and email to teh db
match save_to_db_user(&db, id, email).await {
Ok(x) => x,
Err(x) => {
dbg!(x);
return "Error: unable to save user to teh database, contact Computer Society".to_string();
}
};
// pull it back out (technically could do it in previous step but more explicit)
match get_server_member_email(&db, email).await {
None => {
return "Error: failed to read user from database.".to_string();
}
Some(x) => x,
}
return "Please check it matches (including case) your preferred contact on https://ulwolves.ie/memberships/profile and that you are fully paid up.".to_string()
}
Some(x) => x,
};
@ -161,7 +133,7 @@ pub mod link {
"h2, h4 { font-family: Arial, Helvetica, sans-serif; }"
}
}
div style="display: flex; flex-direction: column; align-items: center;" {
div {
h2 { "Hello from Skynet!" }
// Substitute in the name of our recipient.
p { "Hi " (user) "," }
@ -260,68 +232,13 @@ pub mod link {
.fetch_optional(db)
.await
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)]
pub enum WolvesResultUserResult {
B(bool),
S(String),
}
#[derive(Deserialize, Serialize, Debug)]
struct WolvesResultUser {
success: i64,
result: WolvesResultUserResult,
}
async fn get_user(config: &Config, email: &str) -> Option<i64> {
let url = format!("{}/get_id_from_email", &config.wolves_url);
match reqwest::Client::new()
.post(&url)
.form(&[("email", email)])
.header("X-AM-Identity", &config.wolves_api)
.send()
.await
{
Ok(x) => {
if let Ok(y) = x.json::<WolvesResultUser>().await {
// this is the only time we will get a positive response, the None at the end catches everything else
if let WolvesResultUserResult::S(z) = y.result {
if let Ok(id) = z.parse::<i64>() {
return Some(id);
}
}
}
}
Err(e) => {
dbg!(e);
}
}
None
}
async fn save_to_db_user(db: &Pool<Sqlite>, id_wolves: i64, email: &str) -> Result<Option<Wolves>, sqlx::Error> {
sqlx::query_as::<_, Wolves>(
"
INSERT INTO wolves (id_wolves, email)
VALUES ($1, $2)
ON CONFLICT(id_wolves) DO UPDATE SET email = $2
",
)
.bind(id_wolves)
.bind(email)
.fetch_optional(db)
.await
}
}
pub mod verify {
use super::*;
use crate::commands::link_email::link::{db_pending_clear_expired, get_verify_from_db};
use serenity::model::user::User;
use skynet_discord_bot::common::database::get_server_config;
use skynet_discord_bot::common::database::{ServerMembersWolves, Servers};
use skynet_discord_bot::{get_server_config, ServerMembersWolves, Servers};
use sqlx::Error;
pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String {

View file

@ -7,7 +7,7 @@ use serenity::{
},
};
use skynet_discord_bot::common::database::DataBase;
use skynet_discord_bot::DataBase;
use sqlx::{Pool, Sqlite};
pub(crate) mod user {
@ -16,9 +16,7 @@ pub(crate) mod user {
use super::*;
use crate::commands::link_email::link::get_server_member_discord;
use serenity::model::id::UserId;
use skynet_discord_bot::common::database::Wolves;
use skynet_discord_bot::common::minecraft::{whitelist_update, Minecraft};
use skynet_discord_bot::Config;
use skynet_discord_bot::{whitelist_update, Config, Minecraft, Wolves};
use sqlx::Error;
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
@ -124,9 +122,7 @@ pub(crate) mod server {
use sqlx::Error;
// this is to managfe the server side of commands related to minecraft
use super::*;
use skynet_discord_bot::common::minecraft::update_server;
use skynet_discord_bot::common::minecraft::Minecraft;
use skynet_discord_bot::{is_admin, Config};
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| {
@ -205,9 +201,7 @@ pub(crate) mod server {
use serenity::builder::CreateApplicationCommand;
use serenity::client::Context;
use serenity::model::prelude::application_command::ApplicationCommandInteraction;
use skynet_discord_bot::common::database::DataBase;
use skynet_discord_bot::common::minecraft::{get_minecraft_config_server, server_information};
use skynet_discord_bot::{is_admin, Config};
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")
@ -268,9 +262,7 @@ pub(crate) mod server {
use serenity::model::application::command::CommandOptionType;
use serenity::model::id::GuildId;
use serenity::model::prelude::application_command::{ApplicationCommandInteraction, CommandDataOptionValue};
use skynet_discord_bot::common::database::DataBase;
use skynet_discord_bot::common::minecraft::Minecraft;
use skynet_discord_bot::is_admin;
use skynet_discord_bot::{is_admin, DataBase, Minecraft};
use sqlx::{Error, Pool, Sqlite};
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {

View file

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

View file

@ -7,8 +7,7 @@ use serenity::{
},
};
use skynet_discord_bot::common::database::{DataBase, RoleAdder};
use skynet_discord_bot::is_admin;
use skynet_discord_bot::{is_admin, DataBase, RoleAdder};
use sqlx::{Error, Pool, Sqlite};
pub mod edit {
@ -189,7 +188,7 @@ pub mod list {}
pub mod tools {
use serenity::client::Context;
use serenity::model::guild::Member;
use skynet_discord_bot::common::database::RoleAdder;
use skynet_discord_bot::RoleAdder;
use sqlx::{Pool, Sqlite};
pub async fn on_role_change(db: &Pool<Sqlite>, ctx: &Context, mut new_data: Member) {

View file

@ -1,270 +0,0 @@
use crate::Config;
use serde::{Deserialize, Serialize};
use serenity::model::guild;
use serenity::model::id::{ChannelId, GuildId, RoleId, UserId};
use serenity::prelude::TypeMapKey;
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions, SqliteRow};
use sqlx::{Error, FromRow, Pool, Row, Sqlite};
use std::str::FromStr;
use std::sync::Arc;
use tokio::sync::RwLock;
pub struct DataBase;
impl TypeMapKey for DataBase {
type Value = Arc<RwLock<Pool<Sqlite>>>;
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ServerMembers {
pub server: GuildId,
pub id_wolves: i64,
pub expiry: String,
}
impl<'r> FromRow<'r, SqliteRow> for ServerMembers {
fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
let server_tmp: i64 = row.try_get("server")?;
let server = GuildId::from(server_tmp as u64);
Ok(Self {
server,
id_wolves: row.try_get("id_wolves")?,
expiry: row.try_get("expiry")?,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ServerMembersWolves {
pub server: GuildId,
pub id_wolves: i64,
pub expiry: String,
pub email: String,
pub discord: Option<UserId>,
pub minecraft: Option<String>,
}
impl<'r> FromRow<'r, SqliteRow> for ServerMembersWolves {
fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
let server_tmp: i64 = row.try_get("server")?;
let server = GuildId::from(server_tmp as u64);
let discord = match row.try_get("discord") {
Ok(x) => {
let tmp: i64 = x;
if tmp == 0 {
None
} else {
Some(UserId::from(tmp as u64))
}
}
_ => None,
};
Ok(Self {
server,
id_wolves: row.try_get("id_wolves")?,
expiry: row.try_get("expiry")?,
email: row.try_get("email")?,
discord,
minecraft: row.try_get("minecraft")?,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Wolves {
pub id_wolves: i64,
pub email: String,
pub discord: Option<UserId>,
pub minecraft: Option<String>,
}
impl<'r> FromRow<'r, SqliteRow> for Wolves {
fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
let discord = match row.try_get("discord") {
Ok(x) => {
let tmp: i64 = x;
if tmp == 0 {
None
} else {
Some(UserId::from(tmp as u64))
}
}
_ => None,
};
Ok(Self {
id_wolves: row.try_get("id_wolves")?,
email: row.try_get("email")?,
discord,
minecraft: row.try_get("minecraft")?,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct WolvesVerify {
pub email: String,
pub discord: UserId,
pub auth_code: String,
pub date_expiry: String,
}
impl<'r> FromRow<'r, SqliteRow> for WolvesVerify {
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")?,
date_expiry: row.try_get("date_expiry")?,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Servers {
pub server: GuildId,
pub wolves_api: String,
pub role_past: Option<RoleId>,
pub role_current: RoleId,
pub member_past: i64,
pub member_current: i64,
pub bot_channel_id: ChannelId,
// TODO: these can be removed in teh future with an API update
pub server_name: String,
pub wolves_link: String,
}
impl<'r> FromRow<'r, SqliteRow> for Servers {
fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
let server_tmp: i64 = row.try_get("server")?;
let server = GuildId::from(server_tmp as u64);
let role_past = match row.try_get("role_past") {
Ok(x) => {
let tmp: i64 = x;
if tmp == 0 {
None
} else {
Some(RoleId::from(tmp as u64))
}
}
_ => None,
};
let role_current = match row.try_get("role_current") {
Ok(x) => {
let tmp: i64 = x;
RoleId::from(tmp as u64)
}
_ => RoleId::from(0u64),
};
let bot_channel_tmp: i64 = row.try_get("bot_channel_id")?;
let bot_channel_id = ChannelId::from(bot_channel_tmp as u64);
Ok(Self {
server,
wolves_api: row.try_get("wolves_api")?,
role_past,
role_current,
member_past: row.try_get("member_past")?,
member_current: row.try_get("member_current")?,
bot_channel_id,
server_name: row.try_get("server_name")?,
wolves_link: row.try_get("wolves_link")?,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RoleAdder {
pub server: GuildId,
pub role_a: RoleId,
pub role_b: RoleId,
pub role_c: RoleId,
}
impl<'r> FromRow<'r, SqliteRow> for RoleAdder {
fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
let server_tmp: i64 = row.try_get("server")?;
let server = GuildId::from(server_tmp as u64);
Ok(Self {
server,
role_a: get_role_from_row(row, "role_a"),
role_b: get_role_from_row(row, "role_b"),
role_c: get_role_from_row(row, "role_c"),
})
}
}
fn get_role_from_row(row: &SqliteRow, col: &str) -> RoleId {
match row.try_get(col) {
Ok(x) => {
let tmp: i64 = x;
RoleId(tmp as u64)
}
_ => RoleId::from(0u64),
}
}
pub async fn db_init(config: &Config) -> Result<Pool<Sqlite>, Error> {
let database = format!("{}/{}", &config.home, &config.database);
let pool = SqlitePoolOptions::new()
.max_connections(5)
.connect_with(
SqliteConnectOptions::from_str(&format!("sqlite://{}", database))?
.foreign_keys(true)
.create_if_missing(true),
)
.await?;
// migrations are amazing!
sqlx::migrate!("./db/migrations").run(&pool).await?;
Ok(pool)
}
pub async fn get_server_config(db: &Pool<Sqlite>, server: &GuildId) -> Option<Servers> {
sqlx::query_as::<_, Servers>(
r#"
SELECT *
FROM servers
WHERE server = ?
"#,
)
.bind(*server.as_u64() as i64)
.fetch_one(db)
.await
.ok()
}
pub async fn get_server_member(db: &Pool<Sqlite>, server: &GuildId, member: &guild::Member) -> Result<ServerMembersWolves, Error> {
sqlx::query_as::<_, ServerMembersWolves>(
r#"
SELECT *
FROM server_members
JOIN wolves USING (id_wolves)
WHERE server = ? AND discord = ?
"#,
)
.bind(*server.as_u64() as i64)
.bind(*member.user.id.as_u64() as i64)
.fetch_one(db)
.await
}
pub async fn get_server_config_bulk(db: &Pool<Sqlite>) -> Vec<Servers> {
sqlx::query_as::<_, Servers>(
r#"
SELECT *
FROM servers
"#,
)
.fetch_all(db)
.await
.unwrap_or_default()
}

View file

@ -1,164 +0,0 @@
use crate::common::set_roles::normal::get_server_member_bulk;
use crate::Config;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serenity::model::id::GuildId;
use sqlx::sqlite::SqliteRow;
use sqlx::{Error, FromRow, Pool, Row, Sqlite};
#[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")?,
})
}
}
/**
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 +0,0 @@
pub mod database;
pub mod minecraft;
pub mod set_roles;
pub mod wolves;

View file

@ -1,330 +0,0 @@
pub mod normal {
use crate::common::database::{DataBase, ServerMembersWolves, Servers, Wolves};
use crate::get_now_iso;
use serenity::client::Context;
use serenity::model::id::{GuildId, RoleId, UserId};
use sqlx::{Pool, Sqlite};
pub async fn update_server(ctx: &Context, server: &Servers, remove_roles: &[Option<RoleId>], members_changed: &[UserId]) {
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;
let Servers {
server,
role_past,
role_current,
..
} = server;
let mut roles_set = [0, 0, 0];
let mut members = vec![];
for member in get_server_member_bulk(&db, server).await {
if let Some(x) = member.discord {
members.push(x);
}
}
let mut members_all = members.len();
if let Ok(x) = server.members(ctx, None, None).await {
for mut member in x {
// members_changed acts as an override to only deal with teh users in it
if !members_changed.is_empty() && !members_changed.contains(&member.user.id) {
continue;
}
if members.contains(&member.user.id) {
let mut roles = vec![];
if let Some(role) = &role_past {
if !member.roles.contains(role) {
roles_set[0] += 1;
roles.push(role.to_owned());
}
}
if !member.roles.contains(role_current) {
roles_set[1] += 1;
roles.push(role_current.to_owned());
}
if let Err(e) = member.add_roles(ctx, &roles).await {
println!("{:?}", e);
}
} else {
// old and never
if let Some(role) = &role_past {
if member.roles.contains(role) {
members_all += 1;
}
}
if member.roles.contains(role_current) {
roles_set[2] += 1;
// if theya re not a current member and have the role then remove it
if let Err(e) = member.remove_role(ctx, role_current).await {
println!("{:?}", e);
}
}
}
for role in remove_roles.iter().flatten() {
if let Err(e) = member.remove_role(ctx, role).await {
println!("{:?}", e);
}
}
}
}
set_server_numbers(&db, server, members_all as i64, members.len() as i64).await;
// small bit of logging to note changes over time
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> {
sqlx::query_as::<_, ServerMembersWolves>(
r#"
SELECT *
FROM server_members
JOIN wolves USING (id_wolves)
WHERE (
server = ?
AND discord IS NOT NULL
AND expiry > ?
)
"#,
)
.bind(*server.as_u64() as i64)
.bind(get_now_iso(true))
.fetch_all(db)
.await
.unwrap_or_default()
}
async fn set_server_numbers(db: &Pool<Sqlite>, server: &GuildId, past: i64, current: i64) {
match sqlx::query_as::<_, Wolves>(
"
UPDATE servers
SET member_past = ?, member_current = ?
WHERE server = ?
",
)
.bind(past)
.bind(current)
.bind(*server.as_u64() as i64)
.fetch_optional(db)
.await
{
Ok(_) => {}
Err(e) => {
println!("Failure to insert into {}", server.as_u64());
println!("{:?}", e);
}
}
}
}
// for updating committee members
pub mod committee {
use crate::common::database::{DataBase, Wolves};
use crate::common::wolves::committees::Committees;
use crate::Config;
use serenity::client::Context;
use serenity::model::channel::ChannelType;
use serenity::model::guild::Member;
use sqlx::{Pool, Sqlite};
use std::collections::HashMap;
use std::sync::Arc;
pub async fn check_committee(ctx: Arc<Context>) {
let db_lock = {
let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Config 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_global = config_lock.read().await;
let server = config_global.committee_server;
// because to use it to update a single user we need to pre-get the members of teh server
let mut members = server.members(&ctx, None, None).await.unwrap_or_default();
update_committees(&db, &ctx, &config_global, &mut members).await;
}
/**
This function can take a vec of members (or just one) and gives tehm the appropiate roles on teh committee server
*/
pub async fn update_committees(db: &Pool<Sqlite>, ctx: &Context, config: &Config, members: &mut Vec<Member>) {
let server = config.committee_server;
let committee_member = config.committee_role;
let committees = get_committees(db).await;
// information about the server
let roles = server.roles(&ctx).await.unwrap_or_default();
let channels = server.channels(&ctx).await.unwrap_or_default();
// make a hashmap of the nameof roles to quickly get them out again
let mut roles_name = HashMap::new();
for role in roles.values() {
roles_name.insert(role.name.to_owned(), role.to_owned());
}
let mut channels_name = HashMap::new();
for channel in channels.values() {
// we only care about teh channels in teh category
if let Some(x) = channel.parent_id {
if x.eq(&config.committee_category) {
channels_name.insert(channel.name.to_owned(), channel.to_owned());
}
}
}
// a map of users and the roles they are goign to be getting
let mut users_roles = HashMap::new();
// a list of all the roles that can be removed from folks who should have them
let mut committee_roles = vec![committee_member];
for committee in &committees {
// get the role for this committee/club/soc
let role = match roles_name.get(&committee.name) {
Some(x) => Some(x.to_owned()),
None => {
// create teh role if it does not exist
match server.create_role(&ctx, |r| r.hoist(false).mentionable(true).name(&committee.name)).await {
Ok(x) => Some(x),
Err(_) => None,
}
}
};
// create teh channel if it does nto exist
if !channels_name.contains_key(&committee.name) {
match server
.create_channel(&ctx, |c| c.name(&committee.name).kind(ChannelType::Text).category(config.committee_category))
.await
{
Ok(x) => {
// update teh channels name list
channels_name.insert(x.name.to_owned(), x.to_owned());
println!("Created channel: {}", &committee.name);
}
Err(x) => {
dbg!("Unable to create channel: ", x);
}
}
};
// so if the role exists
if let Some(r) = role {
committee_roles.push(r.id);
for id_wolves in &committee.committee {
// ID in this is the wolves ID, so we need to get a matching discord ID (if one exists)
if let Some(x) = get_server_member_discord(db, id_wolves).await {
if let Some(member_tmp) = x.discord {
let values = users_roles.entry(member_tmp).or_insert(vec![]);
values.push(r.id);
}
}
}
}
}
// now we have a map of all users that should get roles time to go through all the folks on teh server
for member in members {
let roles_required = match users_roles.get(&member.user.id) {
None => {
vec![]
}
Some(x) => {
let mut combined = x.to_owned();
// this is the main role, since it provides access to everything.
combined.push(committee_member);
combined
}
};
// get a list of all the roles to remove from someone
let mut roles_rem = vec![];
for role in &committee_roles {
if !roles_required.contains(role) {
roles_rem.push(role.to_owned());
}
}
if !roles_rem.is_empty() {
member.remove_roles(&ctx, &roles_rem).await.unwrap_or_default();
}
if !roles_required.is_empty() {
// these roles are flavor roles, only there to make folks mentionable
member.add_roles(&ctx, &roles_required).await.unwrap_or_default();
}
}
// finally re-order teh channels to make them visually apealing
let mut channel_names = channels_name.clone().into_keys().collect::<Vec<String>>();
channel_names.sort();
// get a list of all teh new positions
let mut new_positions = vec![];
for (i, name) in channel_names.iter().enumerate() {
if let Some(channel) = channels_name.get_mut(name) {
let position_new = i as u64;
if position_new != channel.position as u64 {
new_positions.push((channel.id.to_owned(), position_new));
}
}
}
if !new_positions.is_empty() {
match server.reorder_channels(&ctx, new_positions).await {
Ok(_) => {
println!("Successfully re-orderd the committee category");
}
Err(e) => {
dbg!("Failed to re-order ", e);
}
}
}
}
async fn get_committees(db: &Pool<Sqlite>) -> Vec<Committees> {
sqlx::query_as::<_, Committees>(
r#"
SELECT *
FROM committees
"#,
)
.fetch_all(db)
.await
.unwrap_or_default()
}
async fn get_server_member_discord(db: &Pool<Sqlite>, user: &i64) -> Option<Wolves> {
sqlx::query_as::<_, Wolves>(
r#"
SELECT *
FROM wolves
WHERE id_wolves = ?
"#,
)
.bind(user)
.fetch_one(db)
.await
.ok()
}
}

View file

@ -1,433 +0,0 @@
use crate::common::database::Wolves;
use serde::{Deserialize, Serialize};
use sqlx::{Pool, Sqlite};
/**
This file relates to anything that directly interacts with teh wolves API
*/
#[derive(Deserialize, Serialize, Debug)]
struct WolvesResultUserMin {
// committee: String,
member_id: String,
// first_name: String,
// last_name: String,
contact_email: String,
// opt_in_email: String,
// student_id: Option<String>,
// note: Option<String>,
// expiry: String,
// requested: String,
// approved: String,
// sitename: String,
// domain: String,
}
async fn add_users_wolves(db: &Pool<Sqlite>, user: &WolvesResultUserMin) {
// expiry
match sqlx::query_as::<_, Wolves>(
"
INSERT INTO wolves (id_wolves, email)
VALUES ($1, $2)
ON CONFLICT(id_wolves) DO UPDATE SET email = $2
",
)
.bind(&user.member_id)
.bind(&user.contact_email)
.fetch_optional(db)
.await
{
Ok(_) => {}
Err(e) => {
println!("Failure to insert into Wolves {:?}", user);
println!("{:?}", e);
}
}
}
/**
This is getting data for Clubs and Socs
*/
pub mod cns {
use crate::common::database::{get_server_config_bulk, DataBase, ServerMembers, ServerMembersWolves, Servers};
use crate::common::set_roles::normal::update_server;
use crate::common::wolves::{add_users_wolves, WolvesResultUserMin};
use crate::Config;
use serde::{Deserialize, Serialize};
use serenity::client::Context;
use serenity::model::id::GuildId;
use sqlx::{Pool, Sqlite};
use std::collections::BTreeMap;
#[derive(Deserialize, Serialize, Debug)]
struct WolvesResultUser {
// TODO: Might be worth trying to get this replaced with the club/soc ID?
committee: String,
member_id: String,
first_name: String,
last_name: String,
contact_email: String,
opt_in_email: String,
student_id: Option<String>,
note: Option<String>,
expiry: String,
requested: String,
approved: String,
sitename: String,
domain: String,
}
impl From<&WolvesResultUser> for WolvesResultUserMin {
fn from(value: &WolvesResultUser) -> Self {
Self {
member_id: value.member_id.to_owned(),
contact_email: value.contact_email.to_owned(),
}
}
}
#[derive(Deserialize, Serialize, Debug)]
struct WolvesResult {
success: i8,
result: Vec<WolvesResultUser>,
}
#[derive(Deserialize, Serialize, Debug)]
struct WolvesResultLocal {
pub id_wolves: String,
pub email: String,
pub expiry: String,
}
pub async fn get_wolves(ctx: &Context) {
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;
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;
for server_config in get_server_config_bulk(&db).await {
let Servers {
server,
wolves_api,
..
} = &server_config;
let existing_tmp = get_server_member(&db, server).await;
let existing = existing_tmp.iter().map(|data| (data.id_wolves, data)).collect::<BTreeMap<_, _>>();
// 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();
match existing.get(&(id as i64)) {
None => {
// user does not exist already, add everything
add_users_wolves(&db, &WolvesResultUserMin::from(&user)).await;
add_users_server_members(&db, server, &user).await;
}
Some(old) => {
// always update wolves table, in case data has changed
add_users_wolves(&db, &WolvesResultUserMin::from(&user)).await;
if old.expiry != user.expiry {
add_users_server_members(&db, server, &user).await;
if let Some(discord_id) = old.discord {
user_to_update.push(discord_id);
}
}
}
}
}
if !user_to_update.is_empty() {
update_server(ctx, &server_config, &[], &user_to_update).await;
}
}
}
async fn get_server_member(db: &Pool<Sqlite>, server: &GuildId) -> Vec<ServerMembersWolves> {
sqlx::query_as::<_, ServerMembersWolves>(
r#"
SELECT *
FROM server_members
JOIN wolves USING (id_wolves)
WHERE (
server = ?
AND discord IS NOT NULL
)
"#,
)
.bind(*server.as_u64() as i64)
.fetch_all(db)
.await
.unwrap_or_default()
}
async fn get_wolves_sub(config: &Config, wolves_api: &str) -> Vec<WolvesResultUser> {
if config.wolves_url.is_empty() {
return vec![];
}
let url = format!("{}/get_members", &config.wolves_url);
// get wolves data
if let Ok(mut res) = surf::post(&url).header("X-AM-Identity", wolves_api).await {
if let Ok(WolvesResult {
success,
result,
}) = res.body_json().await
{
if success != 1 {
return vec![];
}
return result;
}
}
vec![]
}
async fn add_users_server_members(db: &Pool<Sqlite>, server: &GuildId, user: &WolvesResultUser) {
match sqlx::query_as::<_, ServerMembers>(
"
INSERT OR REPLACE INTO server_members (server, id_wolves, expiry)
VALUES (?1, ?2, ?3)
",
)
.bind(*server.as_u64() as i64)
.bind(&user.member_id)
.bind(&user.expiry)
.fetch_optional(db)
.await
{
Ok(_) => {}
Err(e) => {
println!("Failure to insert into ServerMembers {} {:?}", server.as_u64(), user);
println!("{:?}", e);
}
}
}
}
/**
Get and store the data on C&S committees
*/
pub mod committees {
use crate::common::database::DataBase;
use crate::Config;
use serde::{Deserialize, Serialize};
use serenity::client::Context;
use sqlx::{Pool, Sqlite};
// This is what Wolves returns to us
#[derive(Deserialize, Serialize, Debug)]
struct WolvesResult {
success: i8,
result: Vec<WolvesResultCNS>,
}
// this is teh actual data we care about
#[derive(Deserialize, Serialize, Debug)]
struct WolvesResultCNS {
id: String,
name: String,
// Link to their page such as https://ulwolves.ie/society/computer
link: String,
// array of Committee members member_id's
committee: Vec<String>,
}
// Database entry for it
#[derive(Debug, Clone, sqlx::FromRow)]
pub struct Committees {
pub id: i64,
pub name: String,
pub link: String,
#[sqlx(json)]
pub committee: Vec<i64>,
}
impl From<WolvesResultCNS> for Committees {
fn from(value: WolvesResultCNS) -> Self {
Self {
id: value.id.parse().unwrap_or(0),
name: value.name,
link: value.link,
committee: value.committee.iter().map(|x| x.parse::<i64>().unwrap_or(0)).collect(),
}
}
}
pub async fn get_cns(ctx: &Context) {
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;
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;
// request data from wolves
for committee_wolves in get_committees(&config).await {
let committee = Committees::from(committee_wolves);
add_committee(&db, &committee).await;
}
}
async fn get_committees(config: &Config) -> Vec<WolvesResultCNS> {
if config.wolves_url.is_empty() {
return vec![];
}
// TODO: Change teh stored env value to teh base domain
// TODO: this address may change
let url = format!("{}/get_cns", &config.wolves_url);
// get wolves data
if let Ok(mut res) = surf::post(&url).header("X-AM-Identity", &config.wolves_api).await {
if let Ok(WolvesResult {
success,
result,
}) = res.body_json().await
{
if success != 1 {
return vec![];
}
return result;
}
}
vec![]
}
async fn add_committee(db: &Pool<Sqlite>, committee: &Committees) {
match sqlx::query_as::<_, Committees>(
"
INSERT INTO committees (id, name, link, committee)
VALUES ($1, $2, $3, $4)
ON CONFLICT(id) DO UPDATE SET committee = $4
",
)
.bind(committee.id)
.bind(&committee.name)
.bind(&committee.link)
.bind(serde_json::to_string(&committee.committee).unwrap_or_default())
.fetch_optional(db)
.await
{
Ok(_) => {}
Err(e) => {
println!("Failure to insert into Committees {:?}", committee);
println!("{:?}", e);
}
}
}
}
/**
get the data for an individual user
*/
pub mod individual {
use crate::common::database::DataBase;
use crate::common::wolves::{add_users_wolves, WolvesResultUserMin};
use crate::Config;
use serde::{Deserialize, Serialize};
use serenity::client::Context;
#[derive(Deserialize, Serialize, Debug)]
struct WolvesResultUser {
// committee: String,
member_id: String,
// first_name: String,
// last_name: String,
contact_email: String,
// opt_in_email: String,
// student_id: Option<String>,
// note: Option<String>,
// expiry: String,
// requested: String,
// approved: String,
// sitename: String,
// domain: String,
}
impl From<&WolvesResultUser> for WolvesResultUserMin {
fn from(value: &WolvesResultUser) -> Self {
Self {
member_id: value.member_id.to_owned(),
contact_email: value.contact_email.to_owned(),
}
}
}
#[derive(Deserialize, Serialize, Debug)]
struct WolvesResult {
success: i8,
result: WolvesResultUser,
}
pub async fn get_user(ctx: &Context, email: &str) -> bool {
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;
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;
// TODO: proper api key management
let api_key = "";
// request data from wolves
match get_user_sub(&config, api_key, email).await {
None => false,
// if exists save it and return true
Some(user) => {
// add to db
add_users_wolves(&db, &WolvesResultUserMin::from(&user)).await;
true
}
}
}
async fn get_user_sub(config: &Config, wolves_api: &str, _email: &str) -> Option<WolvesResultUser> {
if config.wolves_url.is_empty() {
return None;
}
// TODO: Change teh stored env value to teh base domain
let url = format!("{}/get_member", &config.wolves_url);
// get wolves data
if let Ok(mut res) = surf::post(&url).header("X-AM-Identity", wolves_api).await {
if let Ok(WolvesResult {
success,
result,
}) = res.body_json().await
{
if success != 1 {
return None;
}
return Some(result);
}
}
None
}
}

View file

@ -1,14 +1,27 @@
pub mod common;
use chrono::{Datelike, SecondsFormat, Utc};
use dotenvy::dotenv;
use serde::{Deserialize, Serialize};
use serenity::{
model::{
guild,
id::{ChannelId, GuildId, RoleId},
},
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::{ChannelId, GuildId, RoleId};
use serenity::model::id::UserId;
use serenity::model::prelude::application_command::ApplicationCommandInteraction;
use serenity::prelude::TypeMapKey;
use std::{env, sync::Arc};
use sqlx::{
sqlite::{SqliteConnectOptions, SqlitePoolOptions, SqliteRow},
Error, FromRow, Pool, Row, Sqlite,
};
use std::{env, str::FromStr, sync::Arc};
use tokio::sync::RwLock;
pub struct Config {
// manages where teh database is stored
pub home: String,
@ -25,18 +38,16 @@ pub struct Config {
// wolves API base for clubs/socs
pub wolves_url: String,
// API key for accessing more general resources
pub wolves_api: String,
// discord server for committee
pub committee_server: GuildId,
pub committee_role: RoleId,
pub committee_category: ChannelId,
}
impl TypeMapKey for Config {
type Value = Arc<RwLock<Config>>;
}
pub struct DataBase;
impl TypeMapKey for DataBase {
type Value = Arc<RwLock<Pool<Sqlite>>>;
}
pub fn get_config() -> Config {
dotenv().ok();
@ -52,10 +63,6 @@ pub fn get_config() -> Config {
mail_user: "".to_string(),
mail_pass: "".to_string(),
wolves_url: "".to_string(),
wolves_api: "".to_string(),
committee_server: GuildId(0),
committee_role: RoleId(0),
committee_category: ChannelId(0),
};
if let Ok(x) = env::var("DATABASE_HOME") {
@ -82,33 +89,300 @@ pub fn get_config() -> Config {
config.mail_pass = x.trim().to_string();
}
if let Ok(x) = env::var("WOLVES_URL_BASE") {
if let Ok(x) = env::var("WOLVES_URL") {
config.wolves_url = x.trim().to_string();
}
if let Ok(x) = env::var("WOLVES_API") {
config.wolves_api = x.trim().to_string();
}
if let Ok(x) = env::var("COMMITTEE_DISCORD") {
if let Ok(x) = x.trim().parse::<u64>() {
config.committee_server = GuildId(x);
}
}
if let Ok(x) = env::var("COMMITTEE_DISCORD") {
if let Ok(x) = x.trim().parse::<u64>() {
config.committee_role = RoleId(x);
}
}
if let Ok(x) = env::var("COMMITTEE_CATEGORY") {
if let Ok(x) = x.trim().parse::<u64>() {
config.committee_category = ChannelId(x);
}
}
config
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ServerMembers {
pub server: GuildId,
pub id_wolves: i64,
pub expiry: String,
}
impl<'r> FromRow<'r, SqliteRow> for ServerMembers {
fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
let server_tmp: i64 = row.try_get("server")?;
let server = GuildId::from(server_tmp as u64);
Ok(Self {
server,
id_wolves: row.try_get("id_wolves")?,
expiry: row.try_get("expiry")?,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ServerMembersWolves {
pub server: GuildId,
pub id_wolves: i64,
pub expiry: String,
pub email: String,
pub discord: Option<UserId>,
pub minecraft: Option<String>,
}
impl<'r> FromRow<'r, SqliteRow> for ServerMembersWolves {
fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
let server_tmp: i64 = row.try_get("server")?;
let server = GuildId::from(server_tmp as u64);
let discord = match row.try_get("discord") {
Ok(x) => {
let tmp: i64 = x;
if tmp == 0 {
None
} else {
Some(UserId::from(tmp as u64))
}
}
_ => None,
};
Ok(Self {
server,
id_wolves: row.try_get("id_wolves")?,
expiry: row.try_get("expiry")?,
email: row.try_get("email")?,
discord,
minecraft: row.try_get("minecraft")?,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Wolves {
pub id_wolves: i64,
pub email: String,
pub discord: Option<UserId>,
pub minecraft: Option<String>,
}
impl<'r> FromRow<'r, SqliteRow> for Wolves {
fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
let discord = match row.try_get("discord") {
Ok(x) => {
let tmp: i64 = x;
if tmp == 0 {
None
} else {
Some(UserId::from(tmp as u64))
}
}
_ => None,
};
Ok(Self {
id_wolves: row.try_get("id_wolves")?,
email: row.try_get("email")?,
discord,
minecraft: row.try_get("minecraft")?,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct WolvesVerify {
pub email: String,
pub discord: UserId,
pub auth_code: String,
pub date_expiry: String,
}
impl<'r> FromRow<'r, SqliteRow> for WolvesVerify {
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")?,
date_expiry: row.try_get("date_expiry")?,
})
}
}
#[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,
pub wolves_api: String,
pub role_past: Option<RoleId>,
pub role_current: RoleId,
pub member_past: i64,
pub member_current: i64,
pub bot_channel_id: ChannelId,
// these can be removed in teh future with an API update
pub server_name: String,
pub wolves_link: String,
}
impl<'r> FromRow<'r, SqliteRow> for Servers {
fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
let server_tmp: i64 = row.try_get("server")?;
let server = GuildId::from(server_tmp as u64);
let role_past = match row.try_get("role_past") {
Ok(x) => {
let tmp: i64 = x;
if tmp == 0 {
None
} else {
Some(RoleId::from(tmp as u64))
}
}
_ => None,
};
let role_current = match row.try_get("role_current") {
Ok(x) => {
let tmp: i64 = x;
RoleId::from(tmp as u64)
}
_ => RoleId::from(0u64),
};
let bot_channel_tmp: i64 = row.try_get("bot_channel_id")?;
let bot_channel_id = ChannelId::from(bot_channel_tmp as u64);
Ok(Self {
server,
wolves_api: row.try_get("wolves_api")?,
role_past,
role_current,
member_past: row.try_get("member_past")?,
member_current: row.try_get("member_current")?,
bot_channel_id,
server_name: row.try_get("server_name")?,
wolves_link: row.try_get("wolves_link")?,
})
}
}
#[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")?,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RoleAdder {
pub server: GuildId,
pub role_a: RoleId,
pub role_b: RoleId,
pub role_c: RoleId,
}
impl<'r> FromRow<'r, SqliteRow> for RoleAdder {
fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
let server_tmp: i64 = row.try_get("server")?;
let server = GuildId::from(server_tmp as u64);
Ok(Self {
server,
role_a: get_role_from_row(row, "role_a"),
role_b: get_role_from_row(row, "role_b"),
role_c: get_role_from_row(row, "role_c"),
})
}
}
fn get_role_from_row(row: &SqliteRow, col: &str) -> RoleId {
match row.try_get(col) {
Ok(x) => {
let tmp: i64 = x;
RoleId(tmp as u64)
}
_ => RoleId::from(0u64),
}
}
pub async fn db_init(config: &Config) -> Result<Pool<Sqlite>, Error> {
let database = format!("{}/{}", &config.home, &config.database);
let pool = SqlitePoolOptions::new()
.max_connections(5)
.connect_with(
SqliteConnectOptions::from_str(&format!("sqlite://{}", database))?
.foreign_keys(true)
.create_if_missing(true),
)
.await?;
// migrations are amazing!
sqlx::migrate!("./db/migrations").run(&pool).await?;
Ok(pool)
}
pub async fn get_server_config(db: &Pool<Sqlite>, server: &GuildId) -> Option<Servers> {
sqlx::query_as::<_, Servers>(
r#"
SELECT *
FROM servers
WHERE server = ?
"#,
)
.bind(*server.as_u64() as i64)
.fetch_one(db)
.await
.ok()
}
pub async fn get_server_member(db: &Pool<Sqlite>, server: &GuildId, member: &guild::Member) -> Result<ServerMembersWolves, Error> {
sqlx::query_as::<_, ServerMembersWolves>(
r#"
SELECT *
FROM server_members
JOIN wolves USING (id_wolves)
WHERE server = ? AND discord = ?
"#,
)
.bind(*server.as_u64() as i64)
.bind(*member.user.id.as_u64() as i64)
.fetch_one(db)
.await
}
pub async fn get_server_config_bulk(db: &Pool<Sqlite>) -> Vec<Servers> {
sqlx::query_as::<_, Servers>(
r#"
SELECT *
FROM servers
"#,
)
.fetch_all(db)
.await
.unwrap_or_default()
}
pub fn get_now_iso(short: bool) -> String {
let now = Utc::now();
if short {
@ -122,6 +396,303 @@ pub fn random_string(len: usize) -> String {
thread_rng().sample_iter(&Alphanumeric).take(len).map(char::from).collect()
}
pub mod set_roles {
use super::*;
pub async fn update_server(ctx: &Context, server: &Servers, remove_roles: &[Option<RoleId>], members_changed: &[UserId]) {
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;
let Servers {
server,
role_past,
role_current,
..
} = server;
let mut roles_set = [0, 0, 0];
let mut members = vec![];
for member in get_server_member_bulk(&db, server).await {
if let Some(x) = member.discord {
members.push(x);
}
}
let mut members_all = members.len();
if let Ok(x) = server.members(ctx, None, None).await {
for mut member in x {
// members_changed acts as an override to only deal with teh users in it
if !members_changed.is_empty() && !members_changed.contains(&member.user.id) {
continue;
}
if members.contains(&member.user.id) {
let mut roles = vec![];
if let Some(role) = &role_past {
if !member.roles.contains(role) {
roles_set[0] += 1;
roles.push(role.to_owned());
}
}
if !member.roles.contains(role_current) {
roles_set[1] += 1;
roles.push(role_current.to_owned());
}
if let Err(e) = member.add_roles(ctx, &roles).await {
println!("{:?}", e);
}
} else {
// old and never
if let Some(role) = &role_past {
if member.roles.contains(role) {
members_all += 1;
}
}
if member.roles.contains(role_current) {
roles_set[2] += 1;
// if theya re not a current member and have the role then remove it
if let Err(e) = member.remove_role(ctx, role_current).await {
println!("{:?}", e);
}
}
}
for role in remove_roles.iter().flatten() {
if let Err(e) = member.remove_role(ctx, role).await {
println!("{:?}", e);
}
}
}
}
set_server_numbers(&db, server, members_all as i64, members.len() as i64).await;
// small bit of logging to note changes over time
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> {
sqlx::query_as::<_, ServerMembersWolves>(
r#"
SELECT *
FROM server_members
JOIN wolves USING (id_wolves)
WHERE (
server = ?
AND discord IS NOT NULL
AND expiry > ?
)
"#,
)
.bind(*server.as_u64() as i64)
.bind(get_now_iso(true))
.fetch_all(db)
.await
.unwrap_or_default()
}
async fn set_server_numbers(db: &Pool<Sqlite>, server: &GuildId, past: i64, current: i64) {
match sqlx::query_as::<_, Wolves>(
"
UPDATE servers
SET member_past = ?, member_current = ?
WHERE server = ?
",
)
.bind(past)
.bind(current)
.bind(*server.as_u64() as i64)
.fetch_optional(db)
.await
{
Ok(_) => {}
Err(e) => {
println!("Failure to insert into {}", server.as_u64());
println!("{:?}", e);
}
}
}
}
pub mod get_data {
use super::*;
use crate::set_roles::update_server;
use std::collections::BTreeMap;
#[derive(Deserialize, Serialize, Debug)]
struct WolvesResultUser {
committee: String,
member_id: String,
first_name: String,
last_name: String,
contact_email: String,
opt_in_email: String,
student_id: Option<String>,
note: Option<String>,
expiry: String,
requested: String,
approved: String,
sitename: String,
domain: String,
}
#[derive(Deserialize, Serialize, Debug)]
struct WolvesResult {
success: i8,
result: Vec<WolvesResultUser>,
}
#[derive(Deserialize, Serialize, Debug)]
struct WolvesResultLocal {
pub id_wolves: String,
pub email: String,
pub expiry: String,
}
pub async fn get_wolves(ctx: &Context) {
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;
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;
for server_config in get_server_config_bulk(&db).await {
let Servers {
server,
wolves_api,
..
} = &server_config;
let existing_tmp = get_server_member(&db, server).await;
let existing = existing_tmp.iter().map(|data| (data.id_wolves, data)).collect::<BTreeMap<_, _>>();
// 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();
match existing.get(&(id as i64)) {
None => {
// user does not exist already, add everything
add_users_wolves(&db, &user).await;
add_users_server_members(&db, server, &user).await;
}
Some(old) => {
// always update wolves table, in case data has changed
add_users_wolves(&db, &user).await;
if old.expiry != user.expiry {
add_users_server_members(&db, server, &user).await;
if let Some(discord_id) = old.discord {
user_to_update.push(discord_id);
}
}
}
}
}
if !user_to_update.is_empty() {
update_server(ctx, &server_config, &[], &user_to_update).await;
}
}
}
pub async fn get_server_member(db: &Pool<Sqlite>, server: &GuildId) -> Vec<ServerMembersWolves> {
sqlx::query_as::<_, ServerMembersWolves>(
r#"
SELECT *
FROM server_members
JOIN wolves USING (id_wolves)
WHERE (
server = ?
AND discord IS NOT NULL
)
"#,
)
.bind(*server.as_u64() as i64)
.fetch_all(db)
.await
.unwrap_or_default()
}
async fn get_wolves_sub(config: &Config, wolves_api: &str) -> Vec<WolvesResultUser> {
if config.wolves_url.is_empty() {
return vec![];
}
// get wolves data
if let Ok(mut res) = surf::post(&config.wolves_url).header("X-AM-Identity", wolves_api).await {
if let Ok(WolvesResult {
success,
result,
}) = res.body_json().await
{
if success != 1 {
return vec![];
}
return result;
}
}
vec![]
}
async fn add_users_wolves(db: &Pool<Sqlite>, user: &WolvesResultUser) {
// expiry
match sqlx::query_as::<_, Wolves>(
"
INSERT INTO wolves (id_wolves, email)
VALUES ($1, $2)
ON CONFLICT(id_wolves) DO UPDATE SET email = $2
",
)
.bind(&user.member_id)
.bind(&user.contact_email)
.fetch_optional(db)
.await
{
Ok(_) => {}
Err(e) => {
println!("Failure to insert into Wolves {:?}", user);
println!("{:?}", e);
}
}
}
async fn add_users_server_members(db: &Pool<Sqlite>, server: &GuildId, user: &WolvesResultUser) {
match sqlx::query_as::<_, ServerMembers>(
"
INSERT OR REPLACE INTO server_members (server, id_wolves, expiry)
VALUES (?1, ?2, ?3)
",
)
.bind(*server.as_u64() as i64)
.bind(&user.member_id)
.bind(&user.expiry)
.fetch_optional(db)
.await
{
Ok(_) => {}
Err(e) => {
println!("Failure to insert into ServerMembers {} {:?}", server.as_u64(), user);
println!("{:?}", e);
}
}
}
}
/**
For any time ye need to check if a user who calls a command has admin privlages
*/
@ -159,3 +730,141 @@ pub async fn is_admin(command: &ApplicationCommandInteraction, ctx: &Context) ->
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

@ -13,9 +13,7 @@ use serenity::{
},
Client,
};
use skynet_discord_bot::common::database::{db_init, get_server_config, get_server_member, DataBase};
use skynet_discord_bot::common::set_roles::committee::update_committees;
use skynet_discord_bot::{get_config, Config};
use skynet_discord_bot::{db_init, get_config, get_server_config, get_server_member, Config, DataBase};
use std::sync::Arc;
use tokio::sync::RwLock;
@ -31,21 +29,7 @@ impl EventHandler for Handler {
};
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_global = config_lock.read().await;
// committee server takes priority
if new_member.guild_id.eq(&config_global.committee_server) {
let mut member = vec![new_member.clone()];
update_committees(&db, &ctx, &config_global, &mut member).await;
return;
}
let config_server = match get_server_config(&db, &new_member.guild_id).await {
let config = match get_server_config(&db, &new_member.guild_id).await {
None => return,
Some(x) => x,
};
@ -53,14 +37,14 @@ impl EventHandler for Handler {
if get_server_member(&db, &new_member.guild_id, &new_member).await.is_ok() {
let mut roles = vec![];
if let Some(role) = &config_server.role_past {
if let Some(role) = &config.role_past {
if !new_member.roles.contains(role) {
roles.push(role.to_owned());
}
}
if !new_member.roles.contains(&config_server.role_current) {
roles.push(config_server.role_current.to_owned());
if !new_member.roles.contains(&config.role_current) {
roles.push(config.role_current.to_owned());
}
if let Err(e) = new_member.add_roles(&ctx, &roles).await {
@ -73,10 +57,10 @@ Welcome {} to the {} server!
Sign up on [UL Wolves]({}) and go to https://discord.com/channels/{}/{} and use ``/link_wolves`` to get full access.
"#,
new_member.display_name(),
&config_server.server_name,
&config_server.wolves_link,
&config_server.server,
&config_server.bot_channel_id
&config.server_name,
&config.wolves_link,
&config.server,
&config.bot_channel_id
);
if let Err(err) = new_member.user.direct_message(&ctx, |m| m.content(&msg)).await {
@ -99,6 +83,9 @@ Sign up on [UL Wolves]({}) and go to https://discord.com/channels/{}/{} and use
.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
{
@ -125,6 +112,9 @@ Sign up on [UL Wolves]({}) and go to https://discord.com/channels/{}/{} and use
"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,
_ => "not implemented :(".to_string(),
};