Compare commits

..

30 commits

Author SHA1 Message Date
ca55a78447
feat: switched over to using a library to interact with teh wolves API 2024-11-23 21:53:30 +00:00
77a7b7b81d
fix: slight change in env var used to get teh base URL 2024-11-09 16:53:26 +00:00
2f75dc41c8
feat: will properly re-order the channels created
Also focuses on anything in teh right category
2024-11-09 16:47:48 +00:00
c98baa9d72
feat: will now create a channel for any new club/soc 2024-11-09 16:23:03 +00:00
e4a8cce725
feat: new env var for teh specific channel that the general chat stuff will be under 2024-11-09 16:17:43 +00:00
5b22f699d6
fix: getting teh server config needs to happen after checking for committee 2024-11-09 14:59:05 +00:00
6739c7e068
feat: now use env vars to get teh server and roles for committee 2024-11-09 14:55:26 +00:00
d673dce6fa
fix: handle the just in case the user alrady exists as a different person 2024-11-09 12:53:53 +00:00
015f23b922
feat: no longer using teh "hardcoded" api key 2024-11-09 12:51:41 +00:00
7a6421469c
feat: now able to get the memebr_id from just email 2024-11-09 02:23:46 +00:00
733827c3e6
feat: added support for teh new api key 2024-11-09 01:17:43 +00:00
344d6d3585
fmt: formatting and clippy 2024-10-28 21:53:04 +00:00
b7161e2614
todo: added note 2024-10-28 21:51:14 +00:00
32249364ff
feat: new committee member joins the committee server they automagically get roles 2024-10-28 21:40:48 +00:00
f1138a3c81
fix: moved the methods that changes role into their own module folder 2024-10-28 21:34:21 +00:00
61e76db8dd
feat: this should be able to update teh roles of commettee members on teh discord server 2024-10-28 21:17:44 +00:00
3e6dc9d560
feat: actually get the data for teh committees and pop it in the database 2024-10-28 14:20:36 +00:00
aff6299ac7
feat: added the committee request from wolves 2024-10-28 12:49:35 +00:00
bd80bda22f
feat: added rough code to get an individuals member_id 2024-10-28 02:06:24 +00:00
fe5aa5b252
prep: rough format for requesting data for an indivual and committee 2024-10-28 01:34:23 +00:00
273c58d035
fmt: re-organise the regular data request 2024-10-28 01:31:36 +00:00
3927734083
feat: split out minecraft 2024-10-28 01:06:21 +00:00
41407ecefb
feat: split out all the databse interactions into their own file 2024-10-28 00:59:04 +00:00
79f880daea
feat: splitting up lib.rs into subfiles, starting with anythign taht interacts with teh api 2024-10-28 00:51:39 +00:00
ceade9b972
sql: slight reordering of the migrations 2024-10-28 00:03:52 +00:00
b2d8238c17
Merge branch 'main' into #17-automate-onboarding-mk-ii
# Conflicts:
#	.gitignore
#	src/commands/committee.rs
2024-10-27 23:56:48 +00:00
a7e8f5698e
git: expand out the .gitignore 2024-10-27 23:21:30 +00:00
0e1a7d56b6
feat: remove teh temp setup 2024-09-17 11:02:26 +01:00
fd32adb138
db: delete teh old table and recreate a new one with teh right fields 2024-09-17 11:02:26 +01:00
04a487cd8f
fix: rename `get_wolves` to be just for clubs/socs 2024-09-17 11:02:26 +01:00
25 changed files with 1200 additions and 1181 deletions

View file

@ -19,10 +19,6 @@ jobs:
steps: steps:
# get the repo first # get the repo first
- uses: https://code.forgejo.org/actions/checkout@v4 - uses: https://code.forgejo.org/actions/checkout@v4
- uses: https://forgejo.skynet.ie/Skynet/actions/get_lfs@v3
with:
repository: ${{ gitea.repository }}
ref_name: ${{ gitea.ref_name }}
- run: nix build .#fmt --verbose - run: nix build .#fmt --verbose
# clippy is incredibly useful for making yer code better # clippy is incredibly useful for making yer code better
@ -34,10 +30,6 @@ jobs:
steps: steps:
# get the repo first # get the repo first
- uses: https://code.forgejo.org/actions/checkout@v4 - uses: https://code.forgejo.org/actions/checkout@v4
- uses: https://forgejo.skynet.ie/Skynet/actions/get_lfs@v3
with:
repository: ${{ gitea.repository }}
ref_name: ${{ gitea.ref_name }}
- run: nix build .#clippy --verbose - run: nix build .#clippy --verbose
build: build:
@ -47,10 +39,6 @@ jobs:
steps: steps:
# get the repo first # get the repo first
- uses: https://code.forgejo.org/actions/checkout@v4 - uses: https://code.forgejo.org/actions/checkout@v4
- uses: https://forgejo.skynet.ie/Skynet/actions/get_lfs@v3
with:
repository: ${{ gitea.repository }}
ref_name: ${{ gitea.ref_name }}
- name: "Build it locally" - name: "Build it locally"
run: nix build --verbose run: nix build --verbose
@ -61,7 +49,7 @@ jobs:
needs: [ build ] needs: [ build ]
steps: steps:
- name: "Deploy to Skynet" - name: "Deploy to Skynet"
uses: https://forgejo.skynet.ie/Skynet/actions/deploy@v3 uses: https://forgejo.skynet.ie/Skynet/actions-deploy-to-skynet@v2
with: with:
input: 'skynet_discord_bot' input: 'skynet_discord_bot'
token: ${{ secrets.API_TOKEN_FORGEJO }} token: ${{ secrets.API_TOKEN_FORGEJO }}

37
.gitattributes vendored
View file

@ -1,37 +0,0 @@
# Git config here
* text eol=lf
#############################################
# Git lfs stuff
# Documents
*.pdf filter=lfs diff=lfs merge=lfs -text
*.doc filter=lfs diff=lfs merge=lfs -text
*.docx filter=lfs diff=lfs merge=lfs -text
# Excel
*.xls filter=lfs diff=lfs merge=lfs -text
*.xlsx filter=lfs diff=lfs merge=lfs -text
*.xlsm filter=lfs diff=lfs merge=lfs -text
# Powerpoints
*.ppt filter=lfs diff=lfs merge=lfs -text
*.pptx filter=lfs diff=lfs merge=lfs -text
*.ppsx filter=lfs diff=lfs merge=lfs -text
# Images
*.png filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text
# Video
*.mkv filter=lfs diff=lfs merge=lfs -text
*.mp4 filter=lfs diff=lfs merge=lfs -text
*.wmv filter=lfs diff=lfs merge=lfs -text
# Misc
*.zip filter=lfs diff=lfs merge=lfs -text
# ET4011
*.cbe filter=lfs diff=lfs merge=lfs -text
*.pbs filter=lfs diff=lfs merge=lfs -text
# Open/Libre office
# from https://www.libreoffice.org/discover/what-is-opendocument/
*.odt filter=lfs diff=lfs merge=lfs -text
*.ods filter=lfs diff=lfs merge=lfs -text
*.odp filter=lfs diff=lfs merge=lfs -text
*.odg filter=lfs diff=lfs merge=lfs -text
# QT
*.ui filter=lfs diff=lfs merge=lfs -text

50
Cargo.lock generated
View file

@ -1056,9 +1056,9 @@ dependencies = [
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.4.7" version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205"
dependencies = [ dependencies = [
"atomic-waker", "atomic-waker",
"bytes 1.7.1", "bytes 1.7.1",
@ -1298,7 +1298,7 @@ dependencies = [
"httpdate", "httpdate",
"itoa", "itoa",
"pin-project-lite", "pin-project-lite",
"socket2 0.4.10", "socket2 0.5.7",
"tokio", "tokio",
"tower-service", "tower-service",
"tracing", "tracing",
@ -1307,14 +1307,14 @@ dependencies = [
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "1.5.1" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f" checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a"
dependencies = [ dependencies = [
"bytes 1.7.1", "bytes 1.7.1",
"futures-channel", "futures-channel",
"futures-util", "futures-util",
"h2 0.4.7", "h2 0.4.6",
"http 1.1.0", "http 1.1.0",
"http-body 1.0.1", "http-body 1.0.1",
"httparse", "httparse",
@ -1347,9 +1347,9 @@ checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333"
dependencies = [ dependencies = [
"futures-util", "futures-util",
"http 1.1.0", "http 1.1.0",
"hyper 1.5.1", "hyper 1.5.0",
"hyper-util", "hyper-util",
"rustls 0.23.18", "rustls 0.23.16",
"rustls-pki-types", "rustls-pki-types",
"tokio", "tokio",
"tokio-rustls 0.26.0", "tokio-rustls 0.26.0",
@ -1364,7 +1364,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [ dependencies = [
"bytes 1.7.1", "bytes 1.7.1",
"http-body-util", "http-body-util",
"hyper 1.5.1", "hyper 1.5.0",
"hyper-util", "hyper-util",
"native-tls", "native-tls",
"tokio", "tokio",
@ -1383,7 +1383,7 @@ dependencies = [
"futures-util", "futures-util",
"http 1.1.0", "http 1.1.0",
"http-body 1.0.1", "http-body 1.0.1",
"hyper 1.5.1", "hyper 1.5.0",
"pin-project-lite", "pin-project-lite",
"socket2 0.5.7", "socket2 0.5.7",
"tokio", "tokio",
@ -2205,11 +2205,11 @@ dependencies = [
"encoding_rs", "encoding_rs",
"futures-core", "futures-core",
"futures-util", "futures-util",
"h2 0.4.7", "h2 0.4.6",
"http 1.1.0", "http 1.1.0",
"http-body 1.0.1", "http-body 1.0.1",
"http-body-util", "http-body-util",
"hyper 1.5.1", "hyper 1.5.0",
"hyper-rustls 0.27.3", "hyper-rustls 0.27.3",
"hyper-tls", "hyper-tls",
"hyper-util", "hyper-util",
@ -2225,7 +2225,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
"sync_wrapper 1.0.2", "sync_wrapper 1.0.1",
"system-configuration 0.6.1", "system-configuration 0.6.1",
"tokio", "tokio",
"tokio-native-tls", "tokio-native-tls",
@ -2341,9 +2341,9 @@ dependencies = [
[[package]] [[package]]
name = "rustls" name = "rustls"
version = "0.23.18" version = "0.23.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c9cc1d47e243d655ace55ed38201c19ae02c148ae56412ab8750e8f0166ab7f" checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"rustls-pki-types", "rustls-pki-types",
@ -2628,6 +2628,15 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 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]] [[package]]
name = "signature" name = "signature"
version = "2.2.0" version = "2.2.0"
@ -2648,6 +2657,7 @@ dependencies = [
"maud", "maud",
"rand 0.8.5", "rand 0.8.5",
"serde", "serde",
"serde_json",
"serenity", "serenity",
"sqlx", "sqlx",
"surf", "surf",
@ -3066,9 +3076,9 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
[[package]] [[package]]
name = "sync_wrapper" name = "sync_wrapper"
version = "1.0.2" version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
dependencies = [ dependencies = [
"futures-core", "futures-core",
] ]
@ -3242,7 +3252,9 @@ dependencies = [
"bytes 1.7.1", "bytes 1.7.1",
"libc", "libc",
"mio", "mio",
"parking_lot",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry",
"socket2 0.5.7", "socket2 0.5.7",
"tokio-macros", "tokio-macros",
"windows-sys 0.52.0", "windows-sys 0.52.0",
@ -3296,7 +3308,7 @@ version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
dependencies = [ dependencies = [
"rustls 0.23.18", "rustls 0.23.16",
"rustls-pki-types", "rustls-pki-types",
"tokio", "tokio",
] ]
@ -3917,7 +3929,7 @@ dependencies = [
[[package]] [[package]]
name = "wolves_oxidised" name = "wolves_oxidised"
version = "0.1.0" version = "0.1.0"
source = "git+https://forgejo.skynet.ie/Skynet/wolves-oxidised.git#867778128c1ef580ebfded7808bbd4e86f22903b" source = "git+https://forgejo.skynet.ie/Skynet/wolves-oxidised.git#eee6a76c695a36f1fe220fdeafd5a43757e50527"
dependencies = [ dependencies = [
"reqwest 0.12.9", "reqwest 0.12.9",
"serde", "serde",

View file

@ -17,10 +17,11 @@ name = "update_minecraft"
[dependencies] [dependencies]
# discord library # discord library
serenity = { version = "0.11.6", default-features = false, features = ["client", "gateway", "rustls_backend", "model", "cache"] } serenity = { version = "0.11.6", default-features = false, features = ["client", "gateway", "rustls_backend", "model", "cache"] }
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } tokio = { version = "1.0", features = ["macros", "rt-multi-thread", "full"] }
# wolves api # wolves api
wolves_oxidised = { git = "https://forgejo.skynet.ie/Skynet/wolves-oxidised.git" } # TODO: move off of unstable
wolves_oxidised = { git = "https://forgejo.skynet.ie/Skynet/wolves-oxidised.git", features = ["unstable"]}
# to make the http requests # to make the http requests
surf = "2.3.2" surf = "2.3.2"
@ -29,6 +30,7 @@ dotenvy = "0.15.7"
# For sqlite # For sqlite
sqlx = { version = "0.7.1", features = [ "runtime-tokio", "sqlite", "migrate" ] } sqlx = { version = "0.7.1", features = [ "runtime-tokio", "sqlite", "migrate" ] }
serde_json = { version = "1.0", features = ["raw_value"] }
# create random strings # create random strings
rand = "0.8.5" rand = "0.8.5"
@ -40,4 +42,4 @@ chrono = "0.4.26"
lettre = "0.10.4" lettre = "0.10.4"
maud = "0.25.0" maud = "0.25.0"
serde = "1.0" serde = "1.0.188"

View file

@ -1,58 +1,26 @@
# Skynet Discord Bot # Skynet Discord Bot
This bots core purpose is to give members roles based on their status on <https://ulwolves.ie>. 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. 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. 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. 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.
## Setup - Committee ## Commands - Admin
You need admin access to run any of the commands in this section. You need admin access to run any of the commands in this section.
Either the server owner or a user with the ``Administrator`` permission. Either the server owner or a suer with the ``Administrator`` permission
### Get the API Key ### Getting the Skynet Discord bot
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 ye want an api key for ``193.1.99.74``
2. Create a role for current members (maybe call it ``current-member`` ?)
1. Email ``keith@assurememberships.com`` from committee email and say you want an ``api_key`` for ``193.1.99.74`` 3. (Optional) create a role for all past and current members (ye can use the existing ``member`` role for this, )
* The committee email is the one here: <https://cp.ulwolves.ie/mailbox/> 4. Invite the bot https://discord.com/api/oauth2/authorize?client_id=1145761669256069270&permissions=139855185984&scope=bot
* This may take up to a week to get the key. 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
### Setup Server 7. Use the command ``/add`` and insert the api key, role current and role all (desktop recommended)
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. 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 ### Minecraft
The bot is able to manage the whitelist of a Minecraft server managed by the Computer Society. The bot is able to manage the whitelist of a Minecraft server managed by the Computer Society.
Talk to us to get a server. Talk to us to get a server.
@ -74,24 +42,29 @@ This unlinks a minecraft server from your club/society.
``/minecraft_delete SERVER_ID`` ``/minecraft_delete SERVER_ID``
## Setup - Users ## Commands - User
This is to link your Discord account with your UL Wolves account.
**You will only need to do this once**.
### Setup ### Setup
1. In a Discord server with the Skynet Bot enter ``/link_wolves YOUR_WOLVES_CONTACT_EMAIL`` * Start the process using ``/link_wolves WOLVES_EMAIL``
<img src="media/setup_user_01.png" alt="link process start" width="50%" height="50%"> * The email that is in the Contact Email here: <https://ulwolves.ie/memberships/profile>
* Your ``YOUR_WOLVES_CONTACT_EMAIL`` is the email 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 is most likely your student mail * This will only have to be done once.
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.
You will get member roles on any Discord that is using the bot that you are a member of. * 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 ### Minecraft
You can link your Minecraft username to grant you access to any Minecraft server run by UL Computer Society. 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`` ``/link_minecraft MINECRAFT_USERNAME``

View file

@ -0,0 +1,10 @@
-- 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 (Stored with Git LFS)

Binary file not shown.

BIN
media/setup_user_02.png (Stored with Git LFS)

Binary file not shown.

BIN
media/setup_user_03.png (Stored with Git LFS)

Binary file not shown.

View file

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

View file

@ -1,4 +1,6 @@
use skynet_discord_bot::{db_init, get_config, get_minecraft_config, update_server, whitelist_wipe}; 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 std::collections::HashSet; use std::collections::HashSet;
#[tokio::main] #[tokio::main]

View file

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

View file

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

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)?.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,10 +13,13 @@ use serenity::{
prelude::{command::CommandOptionType, interaction::application_command::CommandDataOptionValue}, prelude::{command::CommandOptionType, interaction::application_command::CommandDataOptionValue},
}, },
}; };
use skynet_discord_bot::{get_now_iso, random_string, Config, DataBase, Wolves, WolvesVerify}; use skynet_discord_bot::common::database::{DataBase, Wolves, WolvesVerify};
use skynet_discord_bot::{get_now_iso, random_string, Config};
use sqlx::{Pool, Sqlite}; use sqlx::{Pool, Sqlite};
pub mod link { pub mod link {
use super::*; use super::*;
use serde::{Deserialize, Serialize};
pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String { pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String {
let db_lock = { let db_lock = {
@ -160,7 +163,7 @@ pub mod link {
"h2, h4 { font-family: Arial, Helvetica, sans-serif; }" "h2, h4 { font-family: Arial, Helvetica, sans-serif; }"
} }
} }
div { div style="display: flex; flex-direction: column; align-items: center;" {
h2 { "Hello from Skynet!" } h2 { "Hello from Skynet!" }
// Substitute in the name of our recipient. // Substitute in the name of our recipient.
p { "Hi " (user) "," } p { "Hi " (user) "," }
@ -260,6 +263,19 @@ pub mod link {
.await .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 save_to_db_user(db: &Pool<Sqlite>, id_wolves: i64, email: &str) -> Result<Option<Wolves>, sqlx::Error> { async fn save_to_db_user(db: &Pool<Sqlite>, id_wolves: i64, email: &str) -> Result<Option<Wolves>, sqlx::Error> {
sqlx::query_as::<_, Wolves>( sqlx::query_as::<_, Wolves>(
" "
@ -279,7 +295,8 @@ pub mod verify {
use super::*; use super::*;
use crate::commands::link_email::link::{db_pending_clear_expired, get_verify_from_db}; use crate::commands::link_email::link::{db_pending_clear_expired, get_verify_from_db};
use serenity::model::user::User; use serenity::model::user::User;
use skynet_discord_bot::{get_server_config, ServerMembersWolves, Servers}; use skynet_discord_bot::common::database::get_server_config;
use skynet_discord_bot::common::database::{ServerMembersWolves, Servers};
use sqlx::Error; use sqlx::Error;
pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String { pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String {

View file

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

View file

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

View file

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

270
src/common/database.rs Normal file
View file

@ -0,0 +1,270 @@
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()
}

164
src/common/minecraft.rs Normal file
View file

@ -0,0 +1,164 @@
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()
}

4
src/common/mod.rs Normal file
View file

@ -0,0 +1,4 @@
pub mod database;
pub mod minecraft;
pub mod set_roles;
pub mod wolves;

330
src/common/set_roles.rs Normal file
View file

@ -0,0 +1,330 @@
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()
}
}

239
src/common/wolves.rs Normal file
View file

@ -0,0 +1,239 @@
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 serenity::client::Context;
use serenity::model::id::GuildId;
use sqlx::{Pool, Sqlite};
use std::collections::BTreeMap;
impl From<&wolves_oxidised::WolvesUser> for WolvesResultUserMin {
fn from(value: &wolves_oxidised::WolvesUser) -> Self {
Self {
member_id: value.member_id.to_owned(),
contact_email: value.contact_email.to_owned(),
}
}
}
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;
// set up teh client
let wolves = wolves_oxidised::Client::new(&config.wolves_url, Some(&config.wolves_api));
for server_config in get_server_config_bulk(&db).await {
let Servers {
server,
// this is the unique api key for each club/soc
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 wolves.get_members(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 add_users_server_members(db: &Pool<Sqlite>, server: &GuildId, user: &wolves_oxidised::WolvesUser) {
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 serenity::client::Context;
use sqlx::{Pool, Sqlite};
// 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<wolves_oxidised::WolvesCNS> for Committees {
fn from(value: wolves_oxidised::WolvesCNS) -> 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;
let wolves = wolves_oxidised::Client::new(&config.wolves_url, Some(&config.wolves_api));
// request data from wolves
for committee_wolves in wolves.get_committees().await {
let committee = Committees::from(committee_wolves);
add_committee(&db, &committee).await;
}
}
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);
}
}
}
}

View file

@ -1,28 +1,14 @@
use dotenvy::dotenv; pub mod common;
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 chrono::{Datelike, SecondsFormat, Utc};
use dotenvy::dotenv;
use rand::{distributions::Alphanumeric, thread_rng, Rng}; use rand::{distributions::Alphanumeric, thread_rng, Rng};
use serde::de::DeserializeOwned;
use serenity::client::Context; use serenity::client::Context;
use serenity::model::id::UserId; use serenity::model::id::{ChannelId, GuildId, RoleId};
use serenity::model::prelude::application_command::ApplicationCommandInteraction; use serenity::model::prelude::application_command::ApplicationCommandInteraction;
use sqlx::{ use serenity::prelude::TypeMapKey;
sqlite::{SqliteConnectOptions, SqlitePoolOptions, SqliteRow}, use std::{env, sync::Arc};
Error, FromRow, Pool, Row, Sqlite,
};
use std::{env, str::FromStr, sync::Arc};
use tokio::sync::RwLock; use tokio::sync::RwLock;
#[derive(Debug)]
pub struct Config { pub struct Config {
// manages where teh database is stored // manages where teh database is stored
pub home: String, pub home: String,
@ -41,16 +27,16 @@ pub struct Config {
pub wolves_url: String, pub wolves_url: String,
// API key for accessing more general resources // API key for accessing more general resources
pub wolves_api: String, pub wolves_api: String,
// discord server for committee
pub committee_server: GuildId,
pub committee_role: RoleId,
pub committee_category: ChannelId,
} }
impl TypeMapKey for Config { impl TypeMapKey for Config {
type Value = Arc<RwLock<Config>>; type Value = Arc<RwLock<Config>>;
} }
pub struct DataBase;
impl TypeMapKey for DataBase {
type Value = Arc<RwLock<Pool<Sqlite>>>;
}
pub fn get_config() -> Config { pub fn get_config() -> Config {
dotenv().ok(); dotenv().ok();
@ -67,6 +53,9 @@ pub fn get_config() -> Config {
mail_pass: "".to_string(), mail_pass: "".to_string(),
wolves_url: "".to_string(), wolves_url: "".to_string(),
wolves_api: "".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") { if let Ok(x) = env::var("DATABASE_HOME") {
@ -96,298 +85,28 @@ pub fn get_config() -> Config {
if let Ok(x) = env::var("WOLVES_URL_BASE") { if let Ok(x) = env::var("WOLVES_URL_BASE") {
config.wolves_url = x.trim().to_string(); config.wolves_url = x.trim().to_string();
} }
if let Ok(x) = env::var("WOLVES_API") { if let Ok(x) = env::var("WOLVES_API") {
config.wolves_api = x.trim().to_string(); config.wolves_api = x.trim().to_string();
} }
config if let Ok(x) = env::var("COMMITTEE_DISCORD") {
} if let Ok(x) = x.trim().parse::<u64>() {
config.committee_server = GuildId(x);
#[derive(Debug, Clone, Deserialize, Serialize)] }
pub struct ServerMembers { }
pub server: GuildId, if let Ok(x) = env::var("COMMITTEE_DISCORD") {
pub id_wolves: i64, if let Ok(x) = x.trim().parse::<u64>() {
pub expiry: String, config.committee_role = RoleId(x);
} }
impl<'r> FromRow<'r, SqliteRow> for ServerMembers { }
fn from_row(row: &'r SqliteRow) -> Result<Self, Error> { if let Ok(x) = env::var("COMMITTEE_CATEGORY") {
let server_tmp: i64 = row.try_get("server")?; if let Ok(x) = x.trim().parse::<u64>() {
let server = GuildId::from(server_tmp as u64); config.committee_category = ChannelId(x);
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> { config
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 { pub fn get_now_iso(short: bool) -> String {
@ -403,255 +122,6 @@ pub fn random_string(len: usize) -> String {
thread_rng().sample_iter(&Alphanumeric).take(len).map(char::from).collect() 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;
use wolves_oxidised::WolvesUser;
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;
// set up teh client
let wolves = wolves_oxidised::Client::new(&config.wolves_url, Some(&config.wolves_api));
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 wolves.get_members(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 add_users_wolves(db: &Pool<Sqlite>, user: &WolvesUser) {
// 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: &WolvesUser) {
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 For any time ye need to check if a user who calls a command has admin privlages
*/ */
@ -689,141 +159,3 @@ pub async fn is_admin(command: &ApplicationCommandInteraction, ctx: &Context) ->
None 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,7 +13,9 @@ use serenity::{
}, },
Client, Client,
}; };
use skynet_discord_bot::{db_init, get_config, get_server_config, get_server_member, Config, DataBase}; 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 std::sync::Arc; use std::sync::Arc;
use tokio::sync::RwLock; use tokio::sync::RwLock;
@ -29,7 +31,21 @@ impl EventHandler for Handler {
}; };
let db = db_lock.read().await; let db = db_lock.read().await;
let config = match get_server_config(&db, &new_member.guild_id).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 {
None => return, None => return,
Some(x) => x, Some(x) => x,
}; };
@ -37,14 +53,14 @@ impl EventHandler for Handler {
if get_server_member(&db, &new_member.guild_id, &new_member).await.is_ok() { if get_server_member(&db, &new_member.guild_id, &new_member).await.is_ok() {
let mut roles = vec![]; let mut roles = vec![];
if let Some(role) = &config.role_past { if let Some(role) = &config_server.role_past {
if !new_member.roles.contains(role) { if !new_member.roles.contains(role) {
roles.push(role.to_owned()); roles.push(role.to_owned());
} }
} }
if !new_member.roles.contains(&config.role_current) { if !new_member.roles.contains(&config_server.role_current) {
roles.push(config.role_current.to_owned()); roles.push(config_server.role_current.to_owned());
} }
if let Err(e) = new_member.add_roles(&ctx, &roles).await { if let Err(e) = new_member.add_roles(&ctx, &roles).await {
@ -57,10 +73,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. Sign up on [UL Wolves]({}) and go to https://discord.com/channels/{}/{} and use ``/link_wolves`` to get full access.
"#, "#,
new_member.display_name(), new_member.display_name(),
&config.server_name, &config_server.server_name,
&config.wolves_link, &config_server.wolves_link,
&config.server, &config_server.server,
&config.bot_channel_id &config_server.bot_channel_id
); );
if let Err(err) = new_member.user.direct_message(&ctx, |m| m.content(&msg)).await { if let Err(err) = new_member.user.direct_message(&ctx, |m| m.content(&msg)).await {
@ -83,9 +99,6 @@ 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::list::register(command))
.create_application_command(|command| commands::minecraft::server::delete::register(command)) .create_application_command(|command| commands::minecraft::server::delete::register(command))
.create_application_command(|command| commands::minecraft::user::add::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 .await
{ {
@ -112,9 +125,6 @@ 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_add" => commands::minecraft::server::add::run(&command, &ctx).await,
"minecraft_list" => commands::minecraft::server::list::run(&command, &ctx).await, "minecraft_list" => commands::minecraft::server::list::run(&command, &ctx).await,
"minecraft_delete" => commands::minecraft::server::delete::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(), _ => "not implemented :(".to_string(),
}; };