Compare commits

..

1 commit

41 changed files with 1891 additions and 4427 deletions

View file

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

2
.gitignore vendored
View file

@ -2,13 +2,11 @@
/.idea
.env
*.env
result
/result
*.db
*.db.*
tmp/
tmp.*

86
.gitlab-ci.yml Normal file
View file

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

2487
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -11,38 +11,26 @@ name = "update_data"
[[bin]]
name = "update_users"
[[bin]]
name = "update_committee"
[[bin]]
name = "update_minecraft"
[dependencies]
# discord library
serenity = { version = "0.12", default-features = false, features = ["client", "gateway", "rustls_backend", "model", "cache"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "full"] }
# wolves api
wolves_oxidised = { git = "https://forgejo.skynet.ie/Skynet/wolves-oxidised.git", features = ["unstable"] }
# wolves_oxidised = { path = "../wolves-oxidised", features = ["unstable"] }
serenity = { version = "0.11.6", default-features = false, features = ["client", "gateway", "rustls_backend", "model", "cache"] }
tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] }
# to make the http requests
surf = "2.3"
surf = "2.3.2"
dotenvy = "0.15"
dotenvy = "0.15.7"
# For sqlite
sqlx = { version = "0.8", features = [ "runtime-tokio", "sqlite", "migrate" ] }
serde_json = { version = "1.0", features = ["raw_value"] }
sqlx = { version = "0.7.1", features = [ "runtime-tokio", "sqlite" ] }
# create random strings
rand = "0.9"
rand = "0.8.5"
# fancy time stuff
chrono = "0.4"
chrono = "0.4.26"
# for email
lettre = "0.11"
maud = "0.27"
lettre = "0.10.4"
maud = "0.25.0"
serde = "1.0"
serde = "1.0.188"

View file

@ -1,9 +0,0 @@
MIT License
Copyright (c) 2024 Skynet
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -1,10 +0,0 @@
# Skynet Discord Bot
The Skynet bot is designed to manage users on Discord.
It allows users to link their UL Wolves account with Wolves in a GDPR compliant manner.
Skynet (bot) is hosted is hosted by the Computer Society on Skynet (computer cluster).
## Documentation
We have split up the documentation into different segments depending on who the user is.
* [Committees](./doc/Committee.md)
* [Member](./doc/User.md)

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +0,0 @@
-- temp table to allow folks to verify by committee email.
-- delete the col in teh server table
ALTER TABLE servers ADD COLUMN bot_channel_id integer DEFAULT 0;
ALTER TABLE servers ADD COLUMN server_name text NOT NULL DEFAULT "";
ALTER TABLE servers ADD COLUMN wolves_link text NOT NULL DEFAULT "";

View file

@ -1,11 +0,0 @@
CREATE TABLE IF NOT EXISTS roles_adder (
server integer not null,
role_a integer not null,
role_b integer not null,
role_c integer not null,
PRIMARY KEY(server,role_a,role_b,role_c)
);
CREATE INDEX IF NOT EXISTS index_roles_adder_server ON roles_adder (server);
CREATE INDEX IF NOT EXISTS index_roles_adder_from ON roles_adder (role_a,role_b);
CREATE INDEX IF NOT EXISTS index_roles_adder_to ON roles_adder (role_c);
CREATE INDEX IF NOT EXISTS index_roles_adder_search ON roles_adder (server,role_a,role_b);

View file

@ -1,3 +0,0 @@
-- Using this col we will be able to see if someone has a bedrock account (as well as a java one)
ALTER TABLE wolves ADD COLUMN minecraft_uid TEXT;

View file

@ -1,14 +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_profile text not null,
name_plain text not null,
name_full text not null,
link text not null,
committee text not null
);
ALTER TABLE servers DROP COLUMN wolves_link;

View file

@ -1,71 +0,0 @@
# Skynet Discord Bot
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.
## Setup - Committee
You need admin access to run any of the commands in this section.
Either the server owner or a user with the ``Administrator`` permission.
### 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
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.
#### Add
This links a minecraft server with your club/society.
``/minecraft_add SERVER_ID``
#### List
List the servers linked to your club/society.
It is possible to have more than one minecraft server
``/minecraft_list``
#### Delete
This unlinks a minecraft server from your club/society.
``/minecraft_delete SERVER_ID``

View file

@ -1,26 +0,0 @@
# Skynet Discord Bot
The Skynet bot is designed to make it easy to verify that you are a member of a Club/Society.
The bot will be able to give you member roles for any partnered servers.
It also provides secondary manifests such as granting access to minecraft servers managed by teh Computer Society.
## Setup
This is to link your Discord account with your UL Wolves account.
**You will only need to do this once**.
### Setup
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.
You will get member roles on any Discord that is using the bot that you are a member of.
### Minecraft
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,8 +0,0 @@
HOME="."
DATABASE="database.db"
DISCORD_TOKEN=""
DISCORD_TOKEN_MINECRAFT=""
EMAIL_SMTP=""
EMAIL_USER=""
EMAIL_PASS=""
WOLVES_URL=""

26
flake.lock generated
View file

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

378
flake.nix
View file

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

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

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

View file

@ -1,54 +0,0 @@
use serenity::{
async_trait,
client::{Context, EventHandler},
model::gateway::{GatewayIntents, Ready},
Client,
};
use skynet_discord_bot::common::database::{db_init, DataBase};
use skynet_discord_bot::common::set_roles::committee;
use skynet_discord_bot::{get_config, Config};
use std::{process, sync::Arc};
use tokio::sync::RwLock;
#[tokio::main]
async fn main() {
let config = get_config();
let db = match db_init(&config).await {
Ok(x) => x,
Err(_) => return,
};
// Intents are a bitflag, bitwise operations can be used to dictate which intents to use
let intents = GatewayIntents::GUILDS | GatewayIntents::GUILD_MESSAGES | GatewayIntents::MESSAGE_CONTENT | GatewayIntents::GUILD_MEMBERS;
// Build our client.
let mut client = Client::builder(&config.discord_token, intents)
.event_handler(Handler {})
.await
.expect("Error creating client");
{
let mut data = client.data.write().await;
data.insert::<Config>(Arc::new(RwLock::new(config)));
data.insert::<DataBase>(Arc::new(RwLock::new(db)));
}
if let Err(why) = client.start().await {
println!("Client error: {:?}", why);
}
}
struct Handler;
#[async_trait]
impl EventHandler for Handler {
async fn ready(&self, ctx: Context, ready: Ready) {
let ctx = Arc::new(ctx);
println!("{} is connected!", ready.user.name);
// u[date committee server
committee::check_committee(Arc::clone(&ctx)).await;
// finish up
process::exit(0);
}
}

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;
@ -16,10 +13,7 @@ async fn main() {
let config = get_config();
let db = match db_init(&config).await {
Ok(x) => x,
Err(e) => {
dbg!(e);
return;
}
Err(_) => return,
};
// Intents are a bitflag, bitwise operations can be used to dictate which intents to use
@ -49,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,27 +0,0 @@
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;
#[tokio::main]
async fn main() {
let config = get_config();
let db = match db_init(&config).await {
Ok(x) => x,
Err(_) => return,
};
let servers = get_minecraft_config(&db).await;
let mut wiped = HashSet::new();
for server in &servers {
// wipe whitelist first
if !wiped.contains(&server.minecraft) {
whitelist_wipe(&server.minecraft, &config.discord_token_minecraft).await;
// add it to teh done list so its not done again
wiped.insert(&server.minecraft);
}
update_server(&server.minecraft, &db, &server.discord, &config).await;
}
}

View file

@ -4,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::normal;
use skynet_discord_bot::{get_config, Config};
use skynet_discord_bot::{db_init, get_config, get_server_config_bulk, set_roles::update_server, Config, DataBase};
use std::{process, sync::Arc};
use tokio::sync::RwLock;
@ -45,15 +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;
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()
@ -62,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;
update_server(&ctx, &server_config, &[], &vec![]).await;
}
}

View file

@ -1,55 +1,81 @@
use serenity::all::{CommandDataOption, CommandDataOptionValue, CommandInteraction, CommandOptionType, CreateCommandOption};
use serenity::{builder::CreateCommand, client::Context};
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 serenity::{
builder::CreateApplicationCommand,
client::Context,
model::{
application::interaction::application_command::ApplicationCommandInteraction,
prelude::{command::CommandOptionType, interaction::application_command::CommandDataOptionValue},
},
};
use skynet_discord_bot::get_data::get_wolves;
use skynet_discord_bot::{get_server_config, set_roles::update_server, DataBase, Servers};
use sqlx::{Error, Pool, Sqlite};
pub async fn run(command: &CommandInteraction, ctx: &Context) -> String {
pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String {
// check if user has high enough permisssions
if let Some(msg) = is_admin(command, ctx).await {
return msg;
let mut admin = false;
let g_id = match command.guild_id {
None => return "Not in a server".to_string(),
Some(x) => x,
};
let roles_server = g_id.roles(&ctx.http).await.unwrap_or_default();
if let Ok(member) = g_id.member(&ctx.http, command.user.id).await {
if let Some(permissions) = member.permissions {
if permissions.administrator() {
admin = true;
}
}
for role_id in member.roles {
if admin {
break;
}
if let Some(role) = roles_server.get(&role_id) {
if role.permissions.administrator() {
admin = true;
}
}
}
}
if !admin {
return "Administrator permission required".to_string();
}
let wolves_api = if let Some(CommandDataOption {
value: CommandDataOptionValue::String(key),
..
}) = command.data.options.first()
let api_key = if let CommandDataOptionValue::String(key) = command
.data
.options
.get(0)
.expect("Expected user option")
.resolved
.as_ref()
.expect("Expected user object")
{
key.to_string()
key
} else {
return "Please provide a wolves API key".to_string();
};
let role_current = if let Some(CommandDataOption {
value: CommandDataOptionValue::Role(role),
..
}) = command.data.options.get(1)
let role_current = if let CommandDataOptionValue::Role(role) = command
.data
.options
.get(1)
.expect("Expected role option")
.resolved
.as_ref()
.expect("Expected role object")
{
role.to_owned()
Some(role.id.to_owned())
} else {
return "Please provide a valid role for ``Role Current``".to_string();
};
let role_past = if let Some(CommandDataOption {
value: CommandDataOptionValue::Role(role),
..
}) = command.data.options.get(5)
{
Some(role.to_owned())
} else {
None
};
let bot_channel_id = if let Some(CommandDataOption {
value: CommandDataOptionValue::Channel(channel),
..
}) = command.data.options.get(2)
{
channel.to_owned()
} else {
return "Please provide a valid channel for ``Bot Channel``".to_string();
let mut role_past = None;
if let Some(x) = command.data.options.get(2) {
if let Some(CommandDataOptionValue::Role(role)) = &x.resolved {
role_past = Some(role.id.to_owned());
}
};
let db_lock = {
@ -60,13 +86,11 @@ pub async fn run(command: &CommandInteraction, ctx: &Context) -> String {
let server_data = Servers {
server: command.guild_id.unwrap_or_default(),
wolves_api,
wolves_api: api_key.to_owned(),
role_past,
role_current,
member_past: 0,
member_current: 0,
bot_channel_id,
server_name: "".to_string(),
};
match add_server(&db, ctx, &server_data).await {
@ -80,30 +104,48 @@ pub async fn run(command: &CommandInteraction, ctx: &Context) -> String {
"Added/Updated server info".to_string()
}
pub fn register() -> CreateCommand {
CreateCommand::new("add")
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
command
.name("add")
.description("Enable the bot for this discord")
.add_option(CreateCommandOption::new(CommandOptionType::String, "api_key", "UL Wolves API Key").required(true))
.add_option(CreateCommandOption::new(CommandOptionType::Role, "role_current", "Role for Current members").required(true))
.add_option(CreateCommandOption::new(CommandOptionType::Channel, "bot_channel", "Safe space for folks to use the bot commands.").required(true))
.add_option(CreateCommandOption::new(CommandOptionType::Role, "role_past", "Role for Past members").required(false))
.create_option(|option| {
option
.name("api_key")
.description("UL Wolves API Key")
.kind(CommandOptionType::String)
.required(true)
})
.create_option(|option| {
option
.name("role_current")
.description("Role for Current members")
.kind(CommandOptionType::Role)
.required(true)
})
.create_option(|option| {
option
.name("role_past")
.description("Role for Past members")
.kind(CommandOptionType::Role)
.required(false)
})
}
async fn add_server(db: &Pool<Sqlite>, ctx: &Context, server: &Servers) -> Result<Option<Servers>, Error> {
let existing = get_server_config(db, &server.server).await;
let role_past = server.role_past.map(|x| x.get() as i64);
let role_past = server.role_past.map(|x| *x.as_u64() as i64);
let role_current = server.role_current.map(|x| *x.as_u64() as i64);
let insert = sqlx::query_as::<_, Servers>(
"
INSERT OR REPLACE INTO servers (server, wolves_api, role_past, role_current, bot_channel_id)
VALUES (?1, ?2, ?3, ?4, ?5)
INSERT OR REPLACE INTO servers (server, wolves_api, role_past, role_current)
VALUES (?1, ?2, ?3, ?4)
",
)
.bind(server.server.get() as i64)
.bind(*server.server.as_u64() as i64)
.bind(&server.wolves_api)
.bind(role_past)
.bind(server.role_current.get() as i64)
.bind(server.bot_channel_id.get() as i64)
.bind(role_current)
.fetch_optional(db)
.await;
@ -118,7 +160,7 @@ async fn add_server(db: &Pool<Sqlite>, ctx: &Context, server: &Servers) -> Resul
if x.role_current != server.role_current {
result.0 = true;
result.1 = true;
result.2 = Some(x.role_current);
result.2 = x.role_current;
}
if x.role_past != server.role_past {
result.0 = true;
@ -141,7 +183,7 @@ async fn add_server(db: &Pool<Sqlite>, ctx: &Context, server: &Servers) -> Resul
if past_remove {
roles_remove.push(past_role)
}
update_server(ctx, server, &roles_remove, &[]).await;
update_server(ctx, server, &roles_remove, &vec![]).await;
}
insert

View file

@ -4,17 +4,21 @@ use lettre::{
Message, SmtpTransport, Transport,
};
use maud::html;
use serenity::{builder::CreateCommand, client::Context, model::id::UserId};
use skynet_discord_bot::common::database::{DataBase, Wolves, WolvesVerify};
use skynet_discord_bot::{get_now_iso, random_string, Config};
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::{get_now_iso, random_string, Config, DataBase, Wolves, WolvesVerify};
use sqlx::{Pool, Sqlite};
pub mod link {
pub(crate) mod link {
use super::*;
use serde::{Deserialize, Serialize};
use serenity::all::{CommandDataOption, CommandDataOptionValue, CommandInteraction, CommandOptionType, CreateCommand, CreateCommandOption};
pub async fn run(command: &CommandInteraction, ctx: &Context) -> String {
pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String {
let db_lock = {
let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Databse in TypeMap.").clone()
@ -37,11 +41,16 @@ pub mod link {
return "Linking already in process, please check email.".to_string();
}
let email = if let Some(CommandDataOption {
value: CommandDataOptionValue::String(email),
..
}) = command.data.options.first()
{
let option = command
.data
.options
.get(0)
.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 user".to_string();
@ -50,34 +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();
let wolves = wolves_oxidised::Client::new(&config.wolves_url, Some(&config.wolves_api));
// see if the user actually exists
let id = match wolves.get_member(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 is your preferred contact on https://ulwolves.ie/memberships/profile and that you are fully paid up.".to_string()
}
Some(x) => x,
};
@ -103,13 +85,14 @@ pub mod link {
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() -> CreateCommand {
CreateCommand::new("link_wolves")
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
command
.name("link")
.description("Set Wolves Email")
.add_option(CreateCommandOption::new(CommandOptionType::String, "email", "UL Wolves Email").required(true))
.create_option(|option| option.name("email").description("UL Wolves Email").kind(CommandOptionType::String).required(true))
}
pub async fn get_server_member_discord(db: &Pool<Sqlite>, user: &UserId) -> Option<Wolves> {
async fn get_server_member_discord(db: &Pool<Sqlite>, user: &UserId) -> Option<Wolves> {
sqlx::query_as::<_, Wolves>(
r#"
SELECT *
@ -117,7 +100,7 @@ pub mod link {
WHERE discord = ?
"#,
)
.bind(user.get() as i64)
.bind(*user.as_u64() as i64)
.fetch_one(db)
.await
.ok()
@ -150,7 +133,7 @@ pub mod link {
"h2, h4 { font-family: Arial, Helvetica, sans-serif; }"
}
}
div {
div style="display: flex; flex-direction: column; align-items: center;" {
h2 { "Hello from Skynet!" }
// Substitute in the name of our recipient.
p { "Hi " (user) "," }
@ -201,7 +184,7 @@ pub mod link {
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();
let mailer = SmtpTransport::starttls_relay(&config.mail_smtp).unwrap().credentials(creds).build();
// Send the email
mailer.send(&email)
@ -229,7 +212,7 @@ pub mod link {
WHERE discord = ?
"#,
)
.bind(user.get() as i64)
.bind(*user.as_u64() as i64)
.fetch_one(db)
.await
.ok()
@ -243,70 +226,45 @@ pub mod link {
",
)
.bind(record.email.to_owned())
.bind(user.get() as i64)
.bind(*user.as_u64() as i64)
.bind(auth.to_owned())
.bind(get_now_iso(false))
.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 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 {
pub(crate) mod verify {
use super::*;
use crate::commands::link_email::link::{db_pending_clear_expired, get_server_member_discord, get_verify_from_db};
use serenity::all::{CommandDataOption, CommandDataOptionValue, CommandInteraction, CommandOptionType, CreateCommandOption, GuildId, RoleId};
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::common::wolves::committees::Committees;
use skynet_discord_bot::{get_server_config, ServerMembersWolves, Servers};
use sqlx::Error;
pub async fn run(command: &CommandInteraction, ctx: &Context) -> String {
pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String {
let db_lock = {
let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Databse in TypeMap.").clone()
};
let db = db_lock.read().await;
// check if user has used /link_wolves
// check if user has used /link
let details = if let Some(x) = get_verify_from_db(&db, &command.user.id).await {
x
} else {
return "Please use /link_wolves first".to_string();
return "Please use /link first".to_string();
};
let code = if let Some(CommandDataOption {
value: CommandDataOptionValue::String(code),
..
}) = command.data.options.first()
{
let option = command
.data
.options
.get(0)
.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();
@ -324,15 +282,11 @@ pub mod verify {
Ok(_) => {
// get teh right roles for the user
set_server_roles(&db, &command.user, ctx).await;
// check if they are a committee member, and on that server
set_server_roles_committee(&db, &command.user, ctx).await;
"Discord username linked to Wolves".to_string()
}
Err(e) => {
println!("{:?}", e);
"Failed to save, please try /link_wolves again".to_string()
"Failed to save, please try /link again".to_string()
}
};
}
@ -342,10 +296,14 @@ pub mod verify {
"Failed to verify".to_string()
}
pub fn register() -> CreateCommand {
CreateCommand::new("verify")
.description("Verify Wolves Email")
.add_option(CreateCommandOption::new(CommandOptionType::String, "code", "Code from verification email").required(true))
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
command.name("verify").description("Verify Wolves Email").create_option(|option| {
option
.name("code")
.description("Code from verification email")
.kind(CommandOptionType::String)
.required(true)
})
}
async fn db_pending_clear_successful(pool: &Pool<Sqlite>, user: &UserId) -> Result<Option<WolvesVerify>, Error> {
@ -356,7 +314,7 @@ pub mod verify {
WHERE discord = ?
"#,
)
.bind(user.get() as i64)
.bind(*user.as_u64() as i64)
.fetch_optional(pool)
.await
}
@ -369,7 +327,7 @@ pub mod verify {
WHERE email = ?
",
)
.bind(discord.get() as i64)
.bind(*discord.as_u64() as i64)
.bind(email)
.fetch_optional(db)
.await
@ -378,7 +336,7 @@ pub mod verify {
async fn set_server_roles(db: &Pool<Sqlite>, discord: &User, ctx: &Context) {
if let Ok(servers) = get_servers(db, &discord.id).await {
for server in servers {
if let Ok(member) = server.server.member(&ctx.http, &discord.id).await {
if let Ok(mut member) = server.server.member(&ctx.http, &discord.id).await {
if let Some(config) = get_server_config(db, &server.server).await {
let Servers {
role_past,
@ -394,8 +352,10 @@ pub mod verify {
}
}
if !member.roles.contains(&role_current) {
roles.push(role_current.to_owned());
if let Some(role) = &role_current {
if !member.roles.contains(role) {
roles.push(role.to_owned());
}
}
if let Err(e) = member.add_roles(&ctx, &roles).await {
@ -407,37 +367,6 @@ pub mod verify {
}
}
async fn get_committees_id(db: &Pool<Sqlite>, wolves_id: i64) -> Vec<Committees> {
sqlx::query_as::<_, Committees>(
r#"
SELECT *
committee like '%?1%'
"#,
)
.bind(wolves_id)
.fetch_all(db)
.await
.unwrap_or_else(|e| {
dbg!(e);
vec![]
})
}
async fn set_server_roles_committee(db: &Pool<Sqlite>, discord: &User, ctx: &Context) {
if let Some(x) = get_server_member_discord(db, &discord.id).await {
// if they are a member of one or more committees, and in teh committee server then give the teh general committee role
// they will get teh more specific vanity role later
if !get_committees_id(db, x.id_wolves).await.is_empty() {
let server = GuildId::new(1220150752656363520);
let committee_member = RoleId::new(1226602779968274573);
if let Ok(member) = server.member(ctx, &discord.id).await {
member.add_roles(&ctx, &[committee_member]).await.unwrap_or_default();
}
}
}
}
async fn get_servers(db: &Pool<Sqlite>, discord: &UserId) -> Result<Vec<ServerMembersWolves>, Error> {
sqlx::query_as::<_, ServerMembersWolves>(
"
@ -447,7 +376,7 @@ pub mod verify {
WHERE discord = ?
",
)
.bind(discord.get() as i64)
.bind(*discord.as_u64() as i64)
.fetch_all(db)
.await
}

View file

@ -1,391 +0,0 @@
use serenity::{builder::CreateCommand, client::Context};
use skynet_discord_bot::common::database::DataBase;
use sqlx::{Pool, Sqlite};
pub(crate) mod user {
use super::*;
pub(crate) mod add {
use super::*;
use crate::commands::link_email::link::get_server_member_discord;
use serde::{Deserialize, Serialize};
use serenity::all::{CommandDataOption, CommandDataOptionValue, CommandInteraction, CommandOptionType, CreateCommandOption};
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 sqlx::Error;
pub fn register() -> CreateCommand {
CreateCommand::new("link_minecraft")
.description("Link your minecraft account")
.add_option(CreateCommandOption::new(CommandOptionType::String, "minecraft_username", "Your Minecraft username").required(true))
.add_option(CreateCommandOption::new(CommandOptionType::Boolean, "bedrock_account", "Is this a Bedrock account?").required(false))
}
pub async fn run(command: &CommandInteraction, ctx: &Context) -> String {
let db_lock = {
let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Databse in TypeMap.").clone()
};
let db = db_lock.read().await;
let config_lock = {
let data_read = ctx.data.read().await;
data_read.get::<Config>().expect("Expected Config in TypeMap.").clone()
};
let config = config_lock.read().await;
// user has to have previously linked with wolves
if get_server_member_discord(&db, &command.user.id).await.is_none() {
return "Not linked with wolves, please use ``/link_wolves`` with your wolves email.".to_string();
}
let username = if let Some(CommandDataOption {
value: CommandDataOptionValue::String(username),
..
}) = command.data.options.first()
{
username.trim()
} else {
return "Please provide a valid username".to_string();
};
// this is always true unless they state its not
let java = if let Some(CommandDataOption {
value: CommandDataOptionValue::Boolean(z),
..
}) = command.data.options.get(1)
{
!z
} else {
true
};
let username_mc;
if java {
// insert the username into the database
match add_minecraft(&db, &command.user.id, username).await {
Ok(_) => {}
Err(e) => {
dbg!("{:?}", e);
return format!("Failure to minecraft username {:?}", username);
}
}
username_mc = username.to_string();
} else {
match get_minecraft_bedrock(username, &config.minecraft_mcprofile).await {
None => {
return format!("No UID found for {:?}", username);
}
Some(x) => {
match add_minecraft_bedrock(&db, &command.user.id, &x.floodgateuid).await {
Ok(_) => {}
Err(e) => {
dbg!("{:?}", e);
return format!("Failure to minecraft UID {:?}", &x.floodgateuid);
}
}
username_mc = x.floodgateuid;
}
}
}
// get a list of servers that the user is a member of
if let Ok(servers) = get_servers(&db, &command.user.id).await {
for server in servers {
whitelist_update(&vec![(username_mc.to_owned(), java)], &server.minecraft, &config.discord_token_minecraft).await;
}
}
"Added/Updated minecraft_user info".to_string()
}
async fn add_minecraft(db: &Pool<Sqlite>, user: &UserId, minecraft: &str) -> Result<Option<Wolves>, Error> {
sqlx::query_as::<_, Wolves>(
"
UPDATE wolves
SET minecraft = ?2
WHERE discord = ?1;
",
)
.bind(user.get() as i64)
.bind(minecraft)
.fetch_optional(db)
.await
}
#[derive(Serialize, Deserialize, Debug)]
struct BedrockDetails {
pub gamertag: String,
pub xuid: String,
pub floodgateuid: String,
pub icon: String,
pub gamescore: String,
pub accounttier: String,
pub textureid: String,
pub skin: String,
pub linked: bool,
pub java_uuid: String,
pub java_name: String,
}
async fn get_minecraft_bedrock(username: &str, api_key: &str) -> Option<BedrockDetails> {
let url = format!("https://mcprofile.io/api/v1/bedrock/gamertag/{username}/");
match surf::get(url)
.header("x-api-key", api_key)
.header("User-Agent", "UL Computer Society")
.recv_json()
.await
{
Ok(res) => Some(res),
Err(e) => {
dbg!(e);
None
}
}
}
async fn add_minecraft_bedrock(db: &Pool<Sqlite>, user: &UserId, minecraft: &str) -> Result<Option<Wolves>, Error> {
sqlx::query_as::<_, Wolves>(
"
UPDATE wolves
SET minecraft_uid = ?2
WHERE discord = ?1;
",
)
.bind(user.get() as i64)
.bind(minecraft)
.fetch_optional(db)
.await
}
async fn get_servers(db: &Pool<Sqlite>, discord: &UserId) -> Result<Vec<Minecraft>, Error> {
sqlx::query_as::<_, Minecraft>(
"
SELECT minecraft.*
FROM minecraft
JOIN (
SELECT server
FROM server_members
JOIN wolves USING (id_wolves)
WHERE discord = ?1
) sub on minecraft.server_discord = sub.server
",
)
.bind(discord.get() as i64)
.fetch_all(db)
.await
}
}
}
pub(crate) mod server {
use super::*;
pub(crate) mod add {
use serenity::all::{CommandDataOption, CommandDataOptionValue, CommandInteraction, CommandOptionType, CreateCommand, CreateCommandOption};
use serenity::model::id::GuildId;
use sqlx::Error;
// this is to managfe the server side of commands related to minecraft
use super::*;
use skynet_discord_bot::common::minecraft::update_server;
use skynet_discord_bot::common::minecraft::Minecraft;
use skynet_discord_bot::{is_admin, Config};
pub fn register() -> CreateCommand {
CreateCommand::new("minecraft_add").description("Add a minecraft server").add_option(
CreateCommandOption::new(CommandOptionType::String, "server_id", "ID of the Minecraft server hosted by the Computer Society").required(true),
)
}
pub async fn run(command: &CommandInteraction, ctx: &Context) -> String {
// check if user has high enough permisssions
if let Some(msg) = is_admin(command, ctx).await {
return msg;
}
let g_id = match command.guild_id {
None => return "Not in a server".to_string(),
Some(x) => x,
};
let server_minecraft = if let Some(CommandDataOption {
value: CommandDataOptionValue::String(id),
..
}) = command.data.options.first()
{
id.to_string()
} else {
return String::from("Expected Server ID");
};
let db_lock = {
let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Databse in TypeMap.").clone()
};
let db = db_lock.read().await;
match add_server(&db, &g_id, &server_minecraft).await {
Ok(_) => {}
Err(e) => {
println!("{:?}", e);
return format!("Failure to insert into Minecraft {} {}", &g_id, &server_minecraft);
}
}
let config_lock = {
let data_read = ctx.data.read().await;
data_read.get::<Config>().expect("Expected Config in TypeMap.").clone()
};
let config = config_lock.read().await;
update_server(&server_minecraft, &db, &g_id, &config).await;
"Added/Updated minecraft_server info".to_string()
}
async fn add_server(db: &Pool<Sqlite>, discord: &GuildId, minecraft: &str) -> Result<Option<Minecraft>, Error> {
sqlx::query_as::<_, Minecraft>(
"
INSERT OR REPLACE INTO minecraft (server_discord, server_minecraft)
VALUES (?1, ?2)
",
)
.bind(discord.get() as i64)
.bind(minecraft)
.fetch_optional(db)
.await
}
}
pub(crate) mod list {
use serenity::all::CommandInteraction;
use serenity::builder::CreateCommand;
use serenity::client::Context;
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() -> CreateCommand {
CreateCommand::new("minecraft_list").description("List your minecraft servers")
}
pub async fn run(command: &CommandInteraction, ctx: &Context) -> String {
if let Some(msg) = is_admin(command, ctx).await {
return msg;
}
let g_id = match command.guild_id {
None => return "Not in a server".to_string(),
Some(x) => x,
};
let db_lock = {
let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Databse in TypeMap.").clone()
};
let db = db_lock.read().await;
let servers = get_minecraft_config_server(&db, g_id).await;
if servers.is_empty() {
return "No minecraft servers, use /minecraft_add to add one".to_string();
}
let config_lock = {
let data_read = ctx.data.read().await;
data_read.get::<Config>().expect("Expected Config in TypeMap.").clone()
};
let config = config_lock.read().await;
let mut result = "Server Information:\n".to_string();
for server in get_minecraft_config_server(&db, g_id).await {
if let Some(x) = server_information(&server.minecraft, &config.discord_token_minecraft).await {
result.push_str(&format!(
r#"
Name: {name}
ID: {id}
Online: {online}
Info: {description}
Link: <https://panel.games.skynet.ie/server/{id}>
"#,
name = &x.attributes.name,
online = !x.attributes.is_suspended,
description = &x.attributes.description,
id = &x.attributes.identifier
));
}
}
result.to_string()
}
}
pub(crate) mod delete {
use serenity::all::{CommandDataOption, CommandDataOptionValue, CommandInteraction, CommandOptionType, CreateCommandOption};
use serenity::builder::CreateCommand;
use serenity::client::Context;
use serenity::model::id::GuildId;
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};
pub fn register() -> CreateCommand {
CreateCommand::new("minecraft_delete").description("Delete a minecraft server").add_option(
CreateCommandOption::new(CommandOptionType::String, "server_id", "ID of the Minecraft server hosted by the Computer Society").required(true),
)
}
pub async fn run(command: &CommandInteraction, ctx: &Context) -> String {
// check if user has high enough permisssions
if let Some(msg) = is_admin(command, ctx).await {
return msg;
}
let g_id = match command.guild_id {
None => return "Not in a server".to_string(),
Some(x) => x,
};
let server_minecraft = if let Some(CommandDataOption {
value: CommandDataOptionValue::String(id),
..
}) = command.data.options.first()
{
id.to_string()
} else {
return String::from("Expected Server ID");
};
let db_lock = {
let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Databse in TypeMap.").clone()
};
let db = db_lock.read().await;
match server_remove(&db, &g_id, &server_minecraft).await {
Ok(_) => {}
Err(e) => {
println!("{:?}", e);
return format!("Failure to insert into Minecraft {} {}", &g_id, &server_minecraft);
}
}
// no need to clear teh whitelist as it will be reset within 24hr anyways
"Removed minecraft_server info".to_string()
}
async fn server_remove(db: &Pool<Sqlite>, discord: &GuildId, minecraft: &str) -> Result<Option<Minecraft>, Error> {
sqlx::query_as::<_, Minecraft>(
"
DELETE FROM minecraft
WHERE server_discord = ?1 AND server_minecraft = ?2
",
)
.bind(discord.get() as i64)
.bind(minecraft)
.fetch_optional(db)
.await
}
}
}

View file

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

View file

@ -1,203 +0,0 @@
use serenity::client::Context;
use skynet_discord_bot::common::database::{DataBase, RoleAdder};
use skynet_discord_bot::is_admin;
use sqlx::{Error, Pool, Sqlite};
pub mod edit {
use super::*;
use serenity::all::{CommandDataOption, CommandDataOptionValue, CommandInteraction, CommandOptionType, CreateCommand, CreateCommandOption};
pub async fn run(command: &CommandInteraction, ctx: &Context) -> String {
// check if user has high enough permisssions
if let Some(msg) = is_admin(command, ctx).await {
return msg;
}
let role_a = if let Some(CommandDataOption {
value: CommandDataOptionValue::Role(role),
..
}) = command.data.options.first()
{
role.to_owned()
} else {
return "Please provide a valid role for ``Role Current``".to_string();
};
let role_b = if let Some(CommandDataOption {
value: CommandDataOptionValue::Role(role),
..
}) = command.data.options.get(1)
{
role.to_owned()
} else {
return "Please provide a valid role for ``Role Current``".to_string();
};
let role_c = if let Some(CommandDataOption {
value: CommandDataOptionValue::Role(role),
..
}) = command.data.options.get(2)
{
role.to_owned()
} else {
return "Please provide a valid role for ``Role Current``".to_string();
};
if role_a == role_b {
return "Roles A and B must be different".to_string();
}
if (role_c == role_a) || (role_c == role_b) {
return "Role C cannot be same as A or B".to_string();
}
let delete = if let Some(CommandDataOption {
value: CommandDataOptionValue::Boolean(z),
..
}) = command.data.options.get(3)
{
*z
} else {
false
};
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 server = command.guild_id.unwrap_or_default();
let server_data = RoleAdder {
server,
role_a,
role_b,
role_c,
};
match add_server(&db, &server_data, delete).await {
Ok(_) => {}
Err(e) => {
println!("{:?}", e);
return format!("Failure to insert into Servers {:?}", server_data);
}
}
let mut role_a_name = String::new();
let mut role_b_name = String::new();
let mut role_c_name = String::new();
if let Ok(x) = server.roles(&ctx).await {
if let Some(y) = x.get(&role_a) {
role_a_name = y.to_owned().name;
}
if let Some(y) = x.get(&role_b) {
role_b_name = y.to_owned().name;
}
if let Some(y) = x.get(&role_b) {
role_c_name = y.to_owned().name;
}
}
if delete {
format!("Removed {} + {} = {}", role_a_name, role_b_name, role_c_name)
} else {
format!("Added {} + {} = {}", role_a_name, role_b_name, role_c_name)
}
}
pub fn register() -> CreateCommand {
CreateCommand::new("roles_adder")
.description("Combine roles together to an new one")
.add_option(CreateCommandOption::new(CommandOptionType::Role, "role_a", "A role you want to add to Role B").required(true))
.add_option(CreateCommandOption::new(CommandOptionType::Role, "role_b", "A role you want to add to Role A").required(true))
.add_option(CreateCommandOption::new(CommandOptionType::Role, "role_c", "Sum of A and B").required(true))
.add_option(CreateCommandOption::new(CommandOptionType::Boolean, "delete", "Delete this entry.").required(false))
}
async fn add_server(db: &Pool<Sqlite>, server: &RoleAdder, delete: bool) -> Result<Option<RoleAdder>, Error> {
if delete {
sqlx::query_as::<_, RoleAdder>(
"
DELETE FROM roles_adder
WHERE server = ?1 AND role_a = ?2 AND role_b = ?3 AND role_c = ?4
",
)
.bind(server.server.get() as i64)
.bind(server.role_a.get() as i64)
.bind(server.role_b.get() as i64)
.bind(server.role_c.get() as i64)
.fetch_optional(db)
.await
} else {
sqlx::query_as::<_, RoleAdder>(
"
INSERT OR REPLACE INTO roles_adder (server, role_a, role_b, role_c)
VALUES (?1, ?2, ?3, ?4)
",
)
.bind(server.server.get() as i64)
.bind(server.role_a.get() as i64)
.bind(server.role_b.get() as i64)
.bind(server.role_c.get() as i64)
.fetch_optional(db)
.await
}
}
}
// TODO
pub mod list {}
pub mod tools {
use serenity::client::Context;
use serenity::model::guild::Member;
use skynet_discord_bot::common::database::RoleAdder;
use sqlx::{Pool, Sqlite};
pub async fn on_role_change(db: &Pool<Sqlite>, ctx: &Context, new_data: Member) {
// check if the role changed is part of the oens for this server
if let Ok(role_adders) = sqlx::query_as::<_, RoleAdder>(
r#"
SELECT *
FROM roles_adder
WHERE server = ?
"#,
)
.bind(new_data.guild_id.get() as i64)
.fetch_all(db)
.await
{
let mut roles_add = vec![];
let mut roles_remove = vec![];
for role_adder in role_adders {
// if the user has both A dnd B give them C
if new_data.roles.contains(&role_adder.role_a) && new_data.roles.contains(&role_adder.role_b) && !new_data.roles.contains(&role_adder.role_c)
{
roles_add.push(role_adder.role_c);
}
// If the suer has C but not A or B remove C
if new_data.roles.contains(&role_adder.role_c)
&& (!new_data.roles.contains(&role_adder.role_a) || !new_data.roles.contains(&role_adder.role_b))
{
roles_remove.push(role_adder.role_c);
}
}
if !roles_add.is_empty() {
if let Err(e) = new_data.add_roles(&ctx, &roles_add).await {
println!("{:?}", e);
}
}
if !roles_remove.is_empty() {
if let Err(e) = new_data.remove_roles(&ctx, &roles_remove).await {
println!("{:?}", e);
}
}
}
}
}

View file

@ -1,260 +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>,
pub minecraft_uid: 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);
Ok(Self {
server,
id_wolves: row.try_get("id_wolves")?,
expiry: row.try_get("expiry")?,
email: row.try_get("email")?,
discord: get_discord_from_row(row),
minecraft: row.try_get("minecraft")?,
minecraft_uid: row.try_get("minecraft_uid")?,
})
}
}
fn get_discord_from_row(row: &SqliteRow) -> Option<UserId> {
match row.try_get("discord") {
Ok(x) => {
let tmp: i64 = x;
if tmp == 0 {
None
} else {
Some(UserId::from(tmp as u64))
}
}
_ => None,
}
}
#[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> {
Ok(Self {
id_wolves: row.try_get("id_wolves")?,
email: row.try_get("email")?,
discord: get_discord_from_row(row),
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,
pub server_name: 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")?,
})
}
}
#[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::new(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.get() 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.get() as i64)
.bind(member.user.id.get() 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,174 +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, true));
}
if let Some(x) = member.minecraft_uid {
usernames.push((x, false));
}
}
if !usernames.is_empty() {
whitelist_update(&usernames, server_id, &config.discord_token_minecraft).await;
}
}
pub 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_wipe(server: &str, token: &str) {
let url_base = format!("https://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!("https://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.get() as i64)
.fetch_all(db)
.await
.unwrap_or_default()
}
pub async fn whitelist_update(add: &Vec<(String, bool)>, server: &str, token: &str) {
println!("Update whitelist for {}", server);
let url_base = format!("https://panel.games.skynet.ie/api/client/servers/{server}");
let bearer = format!("Bearer {token}");
for (name, java) in add {
let data = if *java {
BodyCommand {
command: format!("whitelist add {name}"),
}
} else {
BodyCommand {
command: format!("fwhitelist add {name}"),
}
};
post(&format!("{url_base}/command"), &bearer, &data).await;
}
}

View file

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

View file

@ -1,393 +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 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.get(), 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.get() 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.get() as i64)
.fetch_optional(db)
.await
{
Ok(_) => {}
Err(e) => {
println!("Failure to insert into {}", server.get());
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::all::EditRole;
use serenity::builder::CreateChannel;
use serenity::client::Context;
use serenity::model::channel::ChannelType;
use serenity::model::guild::Member;
use serenity::model::id::ChannelId;
use serenity::model::prelude::RoleId;
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 = RoleId::new(1226602779968274573);
let committees = get_committees(db).await;
let categories = vec![
// C&S Chats 1
ChannelId::new(1226606560973815839),
// C&S Chats 2
ChannelId::new(1341457244973305927),
// C&S Chats 3
ChannelId::new(1341457509717639279),
];
// 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 {
for category in &categories {
if x.eq(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];
let mut category_index = 0;
let mut i = 0;
loop {
if i >= committees.len() {
break;
}
let committee = &committees[i];
// get the role for this committee/club/soc
let role = match roles_name.get(&committee.name_full) {
Some(x) => Some(x.to_owned()),
None => {
// create teh role if it does not exist
match server
.create_role(&ctx, EditRole::new().name(&committee.name_full).hoist(false).mentionable(true))
.await
{
Ok(x) => Some(x),
Err(_) => None,
}
}
};
// create teh channel if it does nto exist
if !channels_name.contains_key(&committee.name_profile) {
match server
.create_channel(
&ctx,
CreateChannel::new(&committee.name_profile)
.kind(ChannelType::Text)
.category(categories[category_index]),
)
.await
{
Ok(x) => {
// update teh channels name list
channels_name.insert(x.name.to_owned(), x.to_owned());
println!("Created channel: {}", &committee.name_profile);
}
Err(x) => {
let tmp = x.to_string();
if x.to_string().contains("Maximum number of channels in category reached (50)") {
category_index += 1;
continue;
}
dbg!("Unable to create channel: ", &tmp, &tmp.contains("Maximum number of channels in category reached (50)"));
}
}
};
// 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);
}
}
}
}
i += 1;
}
// 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_current = member.roles(ctx).unwrap_or_default();
let roles_required = match users_roles.get(&member.user.id) {
None => {
vec![]
}
Some(x) => {
let mut tmp = x.to_owned();
if !tmp.is_empty() {
tmp.push(committee_member);
}
tmp
}
};
let mut roles_rem = vec![];
let mut roles_add = vec![];
// get a list of all the roles to remove from someone
let mut roles_current_id = vec![];
for role in &roles_current {
roles_current_id.push(role.id.to_owned());
if !roles_required.contains(&role.id) {
roles_rem.push(role.id.to_owned());
}
}
if !roles_required.is_empty() {
// if there are committee roles then give the general purporse role
roles_add.push(committee_member);
}
for role in &roles_required {
if !roles_current_id.contains(role) {
roles_add.push(role.to_owned());
}
}
if !roles_rem.is_empty() {
member.remove_roles(&ctx, &roles_rem).await.unwrap_or_default();
}
if !roles_add.is_empty() {
// these roles are flavor roles, only there to make folks mentionable
member.add_roles(&ctx, &roles_add).await.unwrap_or_default();
} else {
member.remove_roles(&ctx, &[committee_member]).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_else(|e| {
dbg!(e);
vec![]
})
}
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,278 +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 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_name,
..
} = &server_config;
// dbg!(&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![];
let mut server_name_tmp = None;
for user in wolves.get_members(wolves_api).await {
// dbg!(&user.committee);
if server_name_tmp.is_none() {
server_name_tmp = Some(user.committee.to_owned());
}
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 let Some(name) = server_name_tmp {
if &name != server_name {
set_server_member(&db, server, &name).await;
}
}
if !user_to_update.is_empty() {
update_server(ctx, &server_config, &[], &user_to_update).await;
}
}
}
async fn set_server_member(db: &Pool<Sqlite>, server: &GuildId, name: &str) {
match sqlx::query_as::<_, Servers>(
"
UPDATE servers
SET server_name = ?
WHERE server = ?
",
)
.bind(name)
.bind(server.get() as i64)
.fetch_optional(db)
.await
{
Ok(_) => {}
Err(e) => {
println!("Failure to set server name {}", server.get());
println!("{:?}", e);
}
}
}
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.get() 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.get() as i64)
.bind(&user.member_id)
.bind(&user.expiry)
.fetch_optional(db)
.await
{
Ok(_) => {}
Err(e) => {
println!("Failure to insert into ServerMembers {} {:?}", server.get(), 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_full: String,
pub name_profile: String,
pub name_plain: 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,
name_full: value.name_full,
name_profile: value.name_profile,
name_plain: value.name_plain,
link: value.link,
committee: value.committee,
}
}
}
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_profile, name_full, name_plain, link, committee)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT(id) DO UPDATE SET committee = $6
",
)
.bind(committee.id)
.bind(&committee.name_profile)
.bind(&committee.name_full)
.bind(&committee.name_plain)
.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,53 +1,57 @@
pub mod common;
use dotenvy::dotenv;
use serde::{Deserialize, Serialize};
use serenity::{
model::{
guild,
id::{GuildId, RoleId},
},
prelude::TypeMapKey,
};
use chrono::{Datelike, SecondsFormat, Utc};
use dotenvy::dotenv;
use rand::{distr::Alphanumeric, rng, Rng};
use serenity::all::CommandInteraction;
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use serenity::client::Context;
use serenity::model::id::{ChannelId, GuildId, RoleId};
use serenity::prelude::TypeMapKey;
use std::{env, sync::Arc};
use serenity::model::id::UserId;
use sqlx::{
sqlite::{SqliteConnectOptions, SqlitePoolOptions, SqliteRow},
Error, FromRow, Pool, Row, Sqlite,
};
use std::{env, str::FromStr, sync::Arc};
use tokio::sync::RwLock;
#[derive(Debug)]
pub struct Config {
// manages where teh database is stored
pub skynet_server: GuildId,
pub ldap_api: String,
pub home: String,
pub database: String,
// tokens for discord and other API's
pub auth: String,
pub discord_token: String,
pub discord_token_minecraft: String,
pub minecraft_mcprofile: String,
// email settings
pub mail_smtp: String,
pub mail_user: String,
pub mail_pass: String,
// wolves API base for clubs/socs
pub wolves_url: String,
// 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();
// reasonable defaults
let mut config = Config {
skynet_server: Default::default(),
ldap_api: "https://api.account.skynet.ie".to_string(),
auth: "".to_string(),
discord_token: "".to_string(),
discord_token_minecraft: "".to_string(),
minecraft_mcprofile: "".to_string(),
home: ".".to_string(),
database: "database.db".to_string(),
@ -56,28 +60,28 @@ pub fn get_config() -> Config {
mail_user: "".to_string(),
mail_pass: "".to_string(),
wolves_url: "".to_string(),
wolves_api: "".to_string(),
committee_server: GuildId::new(1),
committee_role: RoleId::new(1),
committee_category: ChannelId::new(1),
};
if let Ok(x) = env::var("DATABASE_HOME") {
if let Ok(x) = env::var("LDAP_API") {
config.ldap_api = x.trim().to_string();
}
if let Ok(x) = env::var("SKYNET_SERVER") {
config.skynet_server = GuildId::from(str_to_num::<u64>(&x));
}
if let Ok(x) = env::var("HOME") {
config.home = x.trim().to_string();
}
if let Ok(x) = env::var("DATABASE") {
config.database = x.trim().to_string();
}
if let Ok(x) = env::var("LDAP_DISCORD_AUTH") {
config.auth = x.trim().to_string();
}
if let Ok(x) = env::var("DISCORD_TOKEN") {
config.discord_token = x.trim().to_string();
}
if let Ok(x) = env::var("DISCORD_TOKEN_MINECRAFT") {
config.discord_token_minecraft = x.trim().to_string();
}
if let Ok(x) = env::var("MINECRAFT_MCPROFILE_KEY") {
config.minecraft_mcprofile = x.trim().to_string();
}
if let Ok(x) = env::var("EMAIL_SMTP") {
config.mail_smtp = x.trim().to_string();
@ -89,32 +93,279 @@ 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::new(x);
}
}
if let Ok(x) = env::var("COMMITTEE_DISCORD") {
if let Ok(x) = x.trim().parse::<u64>() {
config.committee_role = RoleId::new(x);
}
}
if let Ok(x) = env::var("COMMITTEE_CATEGORY") {
if let Ok(x) = x.trim().parse::<u64>() {
config.committee_category = ChannelId::new(x);
}
}
config
}
fn str_to_num<T: FromStr + Default>(x: &str) -> T {
x.trim().parse::<T>().unwrap_or_default()
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ServerMembers {
pub server: GuildId,
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: Option<RoleId>,
pub member_past: i64,
pub member_current: i64,
}
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;
if tmp == 0 {
None
} else {
Some(RoleId::from(tmp as u64))
}
}
_ => None,
};
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")?,
})
}
}
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?;
sqlx::query(
"CREATE TABLE IF NOT EXISTS wolves (
id_wolves integer PRIMARY KEY,
email text not null,
discord integer,
minecraft text
)",
)
.execute(&pool)
.await?;
sqlx::query("CREATE INDEX IF NOT EXISTS index_discord ON wolves (discord)").execute(&pool).await?;
sqlx::query(
"CREATE TABLE IF NOT EXISTS wolves_verify (
discord integer PRIMARY KEY,
email text not null,
auth_code text not null,
date_expiry text not null
)",
)
.execute(&pool)
.await?;
sqlx::query("CREATE INDEX IF NOT EXISTS index_date_expiry ON wolves_verify (date_expiry)")
.execute(&pool)
.await?;
sqlx::query(
"CREATE TABLE IF NOT EXISTS server_members (
server integer not null,
id_wolves integer not null,
expiry text not null,
PRIMARY KEY(server,id_wolves),
FOREIGN KEY (id_wolves) REFERENCES wolves (id_wolves)
)",
)
.execute(&pool)
.await?;
sqlx::query(
"CREATE TABLE IF NOT EXISTS servers (
server integer PRIMARY KEY,
wolves_api text not null,
role_past integer,
role_current integer,
member_past integer DEFAULT 0,
member_current integer DEFAULT 0
)",
)
.execute(&pool)
.await?;
Ok(pool)
}
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 {
@ -125,43 +376,305 @@ pub fn get_now_iso(short: bool) -> String {
}
pub fn random_string(len: usize) -> String {
rng().sample_iter(&Alphanumeric).take(len).map(char::from).collect()
thread_rng().sample_iter(&Alphanumeric).take(len).map(char::from).collect()
}
/**
For any time ye need to check if a user who calls a command has admin privlages
*/
pub async fn is_admin(command: &CommandInteraction, ctx: &Context) -> Option<String> {
let mut admin = false;
pub mod set_roles {
use super::*;
pub async fn update_server(ctx: &Context, server: &Servers, remove_roles: &[Option<RoleId>], members_changed: &Vec<UserId>) {
let db_lock = {
let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Database in TypeMap.").clone()
};
let g_id = match command.guild_id {
None => return Some("Not in a server".to_string()),
Some(x) => x,
};
let db = db_lock.read().await;
let roles_server = g_id.roles(&ctx.http).await.unwrap_or_default();
let Servers {
server,
role_past,
role_current,
..
} = server;
if let Ok(member) = g_id.member(&ctx.http, command.user.id).await {
if let Some(permissions) = member.permissions {
if permissions.administrator() {
admin = true;
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();
for role_id in member.roles {
if admin {
break;
}
if let Some(role) = roles_server.get(&role_id) {
if role.permissions.administrator() {
admin = true;
if 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 let Some(role) = &role_current {
if !member.roles.contains(role) {
roles_set[1] += 1;
roles.push(role.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 let Some(role) = &role_current {
if member.roles.contains(role) {
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).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]);
}
if !admin {
Some("Administrator permission required".to_string())
} else {
None
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,
wolves_id: String,
first_name: String,
last_name: String,
contact_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.wolves_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.wolves_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.wolves_id)
.bind(&user.expiry)
.fetch_optional(db)
.await
{
Ok(_) => {}
Err(e) => {
println!("Failure to insert into ServerMembers {} {:?}", server.as_u64(), user);
println!("{:?}", e);
}
}
}
}

View file

@ -1,52 +1,32 @@
pub mod commands;
mod commands;
use crate::commands::role_adder::tools::on_role_change;
use serenity::all::{ActivityData, Command, CreateMessage, EditInteractionResponse, GuildMemberUpdateEvent, Interaction};
use serenity::model::guild::Member;
use serenity::{
async_trait,
client::{Context, EventHandler},
model::{
application::{command::Command, interaction::Interaction},
gateway::{GatewayIntents, Ready},
user::OnlineStatus,
guild,
},
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::common::wolves::committees::Committees;
use skynet_discord_bot::{get_config, Config};
use sqlx::{Pool, Sqlite};
use std::sync::Arc;
use skynet_discord_bot::{db_init, get_config, get_server_config, get_server_member, Config, DataBase};
use tokio::sync::RwLock;
struct Handler;
#[async_trait]
impl EventHandler for Handler {
// handles previously linked accounts joining the server
async fn guild_member_addition(&self, ctx: Context, new_member: Member) {
async fn guild_member_addition(&self, ctx: Context, mut new_member: guild::Member) {
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;
// 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,
};
@ -54,75 +34,33 @@ 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 let Some(role) = &config.role_current {
if !new_member.roles.contains(role) {
roles.push(role.to_owned());
}
}
if let Err(e) = new_member.add_roles(&ctx, &roles).await {
println!("{:?}", e);
}
} else {
let tmp = get_committee(&db, &config_server.server_name).await;
if !tmp.is_empty() {
let committee = &tmp[0];
let msg = format!(
r#"
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(),
committee.name_full,
committee.link,
&config_server.server,
&config_server.bot_channel_id
);
if let Err(err) = new_member.user.direct_message(&ctx, CreateMessage::new().content(&msg)).await {
dbg!(err);
}
}
}
}
// handles role updates
async fn guild_member_update(&self, ctx: Context, _old_data: Option<Member>, new_data: Option<Member>, _: GuildMemberUpdateEvent) {
// get config/db
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;
// check if the role changed is part of the oens for this server
if let Some(x) = new_data {
on_role_change(&db, &ctx, x).await;
}
}
async fn ready(&self, ctx: Context, ready: Ready) {
println!("[Main] {} is connected!", ready.user.name);
ctx.set_presence(Some(ActivityData::playing("with humanity's fate")), OnlineStatus::Online);
match Command::set_global_commands(
&ctx.http,
vec![
commands::add_server::register(),
commands::role_adder::edit::register(),
commands::link_email::link::register(),
commands::link_email::verify::register(),
commands::minecraft::server::add::register(),
commands::minecraft::server::list::register(),
commands::minecraft::server::delete::register(),
commands::minecraft::user::add::register(),
],
)
match Command::set_global_application_commands(&ctx.http, |commands| {
commands
.create_application_command(|command| commands::add_server::register(command))
.create_application_command(|command| commands::link_email::link::register(command))
.create_application_command(|command| commands::link_email::verify::register(command))
})
.await
{
Ok(_) => {}
@ -133,54 +71,30 @@ Sign up on [UL Wolves]({}) and go to https://discord.com/channels/{}/{} and use
}
async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
if let Interaction::Command(command) = interaction {
if let Interaction::ApplicationCommand(command) = interaction {
let _ = command.defer_ephemeral(&ctx.http).await;
//println!("Received command interaction: {:#?}", command);
let content = match command.data.name.as_str() {
// user commands
"link_wolves" => commands::link_email::link::run(&command, &ctx).await,
"verify" => commands::link_email::verify::run(&command, &ctx).await,
"link_minecraft" => commands::minecraft::user::add::run(&command, &ctx).await,
// admin commands
"add" => commands::add_server::run(&command, &ctx).await,
"roles_adder" => commands::role_adder::edit::run(&command, &ctx).await,
"minecraft_add" => commands::minecraft::server::add::run(&command, &ctx).await,
"minecraft_list" => commands::minecraft::server::list::run(&command, &ctx).await,
"minecraft_delete" => commands::minecraft::server::delete::run(&command, &ctx).await,
"link" => commands::link_email::link::run(&command, &ctx).await,
"verify" => commands::link_email::verify::run(&command, &ctx).await,
_ => "not implemented :(".to_string(),
};
if let Err(why) = command.edit_response(&ctx.http, EditInteractionResponse::new().content(content)).await {
if let Err(why) = command.edit_original_interaction_response(&ctx.http, |response| response.content(content)).await {
println!("Cannot respond to slash command: {}", why);
}
}
}
}
async fn get_committee(db: &Pool<Sqlite>, committee: &str) -> Vec<Committees> {
sqlx::query_as::<_, Committees>(
r#"
SELECT *
FROM committees
WHERE name_plain = ?
"#,
)
.bind(committee)
.fetch_all(db)
.await
.unwrap_or_default()
}
#[tokio::main]
async fn main() {
let config = get_config();
let db = match db_init(&config).await {
Ok(x) => x,
Err(err) => {
dbg!(err);
return;
}
Err(_) => return,
};
// Intents are a bitflag, bitwise operations can be used to dictate which intents to use