Merge branch 'main' into #17-automate-onboarding-mk-ii

# Conflicts:
#	Cargo.lock
#	src/commands/link_email.rs
#	src/lib.rs
This commit is contained in:
silver 2025-02-17 17:30:25 +00:00
commit c79113921d
Signed by untrusted user: silver
GPG key ID: 36F93D61BAD3FD7D
15 changed files with 1013 additions and 141 deletions

View file

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

37
.gitattributes vendored Normal file
View file

@ -0,0 +1,37 @@
# 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

94
Cargo.lock generated
View file

@ -107,9 +107,9 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.89" version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775"
[[package]] [[package]]
name = "async-channel" name = "async-channel"
@ -143,7 +143,7 @@ dependencies = [
"async-task", "async-task",
"concurrent-queue", "concurrent-queue",
"fastrand 2.1.1", "fastrand 2.1.1",
"futures-lite 2.3.0", "futures-lite 2.5.0",
"slab", "slab",
] ]
@ -158,21 +158,21 @@ dependencies = [
"async-io", "async-io",
"async-lock", "async-lock",
"blocking", "blocking",
"futures-lite 2.3.0", "futures-lite 2.5.0",
"once_cell", "once_cell",
] ]
[[package]] [[package]]
name = "async-io" name = "async-io"
version = "2.3.4" version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8" checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059"
dependencies = [ dependencies = [
"async-lock", "async-lock",
"cfg-if", "cfg-if",
"concurrent-queue", "concurrent-queue",
"futures-io", "futures-io",
"futures-lite 2.3.0", "futures-lite 2.5.0",
"parking", "parking",
"polling", "polling",
"rustix", "rustix",
@ -206,7 +206,7 @@ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",
"futures-io", "futures-io",
"futures-lite 2.3.0", "futures-lite 2.5.0",
"gloo-timers", "gloo-timers",
"kv-log-macro", "kv-log-macro",
"log", "log",
@ -381,7 +381,7 @@ dependencies = [
"async-channel 2.3.1", "async-channel 2.3.1",
"async-task", "async-task",
"futures-io", "futures-io",
"futures-lite 2.3.0", "futures-lite 2.5.0",
"piper", "piper",
] ]
@ -586,9 +586,9 @@ dependencies = [
[[package]] [[package]]
name = "curl" name = "curl"
version = "0.4.46" version = "0.4.47"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e2161dd6eba090ff1594084e95fd67aeccf04382ffea77999ea94ed42ec67b6" checksum = "d9fb4d13a1be2b58f14d60adba57c9834b78c62fd86c3e76a148f732686e9265"
dependencies = [ dependencies = [
"curl-sys", "curl-sys",
"libc", "libc",
@ -601,9 +601,9 @@ dependencies = [
[[package]] [[package]]
name = "curl-sys" name = "curl-sys"
version = "0.4.75+curl-8.10.0" version = "0.4.78+curl-8.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a4fd752d337342e4314717c0d9b6586b059a120c80029ebe4d49b11fec7875e" checksum = "8eec768341c5c7789611ae51cf6c459099f22e64a5d5d0ce4892434e33821eaf"
dependencies = [ dependencies = [
"cc", "cc",
"libc", "libc",
@ -923,9 +923,9 @@ dependencies = [
[[package]] [[package]]
name = "futures-lite" name = "futures-lite"
version = "2.3.0" version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1"
dependencies = [ dependencies = [
"fastrand 2.1.1", "fastrand 2.1.1",
"futures-core", "futures-core",
@ -1056,9 +1056,9 @@ dependencies = [
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.4.6" version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e"
dependencies = [ dependencies = [
"atomic-waker", "atomic-waker",
"bytes 1.7.1", "bytes 1.7.1",
@ -1307,14 +1307,14 @@ dependencies = [
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "1.5.0" version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" checksum = "97818827ef4f364230e16705d4706e2897df2bb60617d6ca15d598025a3c481f"
dependencies = [ dependencies = [
"bytes 1.7.1", "bytes 1.7.1",
"futures-channel", "futures-channel",
"futures-util", "futures-util",
"h2 0.4.6", "h2 0.4.7",
"http 1.1.0", "http 1.1.0",
"http-body 1.0.1", "http-body 1.0.1",
"httparse", "httparse",
@ -1347,9 +1347,9 @@ checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333"
dependencies = [ dependencies = [
"futures-util", "futures-util",
"http 1.1.0", "http 1.1.0",
"hyper 1.5.0", "hyper 1.5.1",
"hyper-util", "hyper-util",
"rustls 0.23.16", "rustls 0.23.18",
"rustls-pki-types", "rustls-pki-types",
"tokio", "tokio",
"tokio-rustls 0.26.0", "tokio-rustls 0.26.0",
@ -1364,7 +1364,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [ dependencies = [
"bytes 1.7.1", "bytes 1.7.1",
"http-body-util", "http-body-util",
"hyper 1.5.0", "hyper 1.5.1",
"hyper-util", "hyper-util",
"native-tls", "native-tls",
"tokio", "tokio",
@ -1383,7 +1383,7 @@ dependencies = [
"futures-util", "futures-util",
"http 1.1.0", "http 1.1.0",
"http-body 1.0.1", "http-body 1.0.1",
"hyper 1.5.0", "hyper 1.5.1",
"pin-project-lite", "pin-project-lite",
"socket2 0.5.7", "socket2 0.5.7",
"tokio", "tokio",
@ -1907,18 +1907,18 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]] [[package]]
name = "pin-project" name = "pin-project"
version = "1.1.5" version = "1.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95"
dependencies = [ dependencies = [
"pin-project-internal", "pin-project-internal",
] ]
[[package]] [[package]]
name = "pin-project-internal" name = "pin-project-internal"
version = "1.1.5" version = "1.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1977,9 +1977,9 @@ checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
[[package]] [[package]]
name = "polling" name = "polling"
version = "3.7.3" version = "3.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"concurrent-queue", "concurrent-queue",
@ -2205,11 +2205,11 @@ dependencies = [
"encoding_rs", "encoding_rs",
"futures-core", "futures-core",
"futures-util", "futures-util",
"h2 0.4.6", "h2 0.4.7",
"http 1.1.0", "http 1.1.0",
"http-body 1.0.1", "http-body 1.0.1",
"http-body-util", "http-body-util",
"hyper 1.5.0", "hyper 1.5.1",
"hyper-rustls 0.27.3", "hyper-rustls 0.27.3",
"hyper-tls", "hyper-tls",
"hyper-util", "hyper-util",
@ -2225,7 +2225,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
"sync_wrapper 1.0.1", "sync_wrapper 1.0.2",
"system-configuration 0.6.1", "system-configuration 0.6.1",
"tokio", "tokio",
"tokio-native-tls", "tokio-native-tls",
@ -2341,9 +2341,9 @@ dependencies = [
[[package]] [[package]]
name = "rustls" name = "rustls"
version = "0.23.16" version = "0.23.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e" checksum = "9c9cc1d47e243d655ace55ed38201c19ae02c148ae56412ab8750e8f0166ab7f"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"rustls-pki-types", "rustls-pki-types",
@ -2628,15 +2628,6 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "signature" name = "signature"
version = "2.2.0" version = "2.2.0"
@ -2657,7 +2648,6 @@ dependencies = [
"maud", "maud",
"rand 0.8.5", "rand 0.8.5",
"serde", "serde",
"serde_json",
"serenity", "serenity",
"sqlx", "sqlx",
"surf", "surf",
@ -3076,9 +3066,9 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
[[package]] [[package]]
name = "sync_wrapper" name = "sync_wrapper"
version = "1.0.1" version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
dependencies = [ dependencies = [
"futures-core", "futures-core",
] ]
@ -3252,9 +3242,7 @@ dependencies = [
"bytes 1.7.1", "bytes 1.7.1",
"libc", "libc",
"mio", "mio",
"parking_lot",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry",
"socket2 0.5.7", "socket2 0.5.7",
"tokio-macros", "tokio-macros",
"windows-sys 0.52.0", "windows-sys 0.52.0",
@ -3308,7 +3296,7 @@ version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
dependencies = [ dependencies = [
"rustls 0.23.16", "rustls 0.23.18",
"rustls-pki-types", "rustls-pki-types",
"tokio", "tokio",
] ]
@ -3533,9 +3521,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]] [[package]]
name = "value-bag" name = "value-bag"
version = "1.9.0" version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101" checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2"
[[package]] [[package]]
name = "vcpkg" name = "vcpkg"
@ -3929,7 +3917,7 @@ dependencies = [
[[package]] [[package]]
name = "wolves_oxidised" name = "wolves_oxidised"
version = "0.1.0" version = "0.1.0"
source = "git+https://forgejo.skynet.ie/Skynet/wolves-oxidised.git#eee6a76c695a36f1fe220fdeafd5a43757e50527" source = "git+https://forgejo.skynet.ie/Skynet/wolves-oxidised.git#867778128c1ef580ebfded7808bbd4e86f22903b"
dependencies = [ dependencies = [
"reqwest 0.12.9", "reqwest 0.12.9",
"serde", "serde",

View file

@ -23,6 +23,9 @@ tokio = { version = "1.0", features = ["macros", "rt-multi-thread", "full"] }
# TODO: move off of unstable # TODO: move off of unstable
wolves_oxidised = { git = "https://forgejo.skynet.ie/Skynet/wolves-oxidised.git", features = ["unstable"]} wolves_oxidised = { git = "https://forgejo.skynet.ie/Skynet/wolves-oxidised.git", features = ["unstable"]}
# wolves api
wolves_oxidised = { git = "https://forgejo.skynet.ie/Skynet/wolves-oxidised.git" }
# to make the http requests # to make the http requests
surf = "2.3.2" surf = "2.3.2"
@ -42,4 +45,4 @@ chrono = "0.4.26"
lettre = "0.10.4" lettre = "0.10.4"
maud = "0.25.0" maud = "0.25.0"
serde = "1.0.188" serde = "1.0"

View file

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

View file

@ -0,0 +1,3 @@
-- 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;

75
doc/Committee.md Normal file
View file

@ -0,0 +1,75 @@
# 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
6. ``server_name`` For example ``UL Computer Society``
* Will be removed in the future
7. ``wolves_link`` for example <https://ulwolves.ie/society/computer>
* Will be removed in the future
At this point the bot is set up and no further action is required.
### Minecraft
The bot is able to manage the whitelist of a Minecraft server managed by the Computer Society.
Talk to us to get a server.
#### 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``

26
doc/User.md Normal file
View file

@ -0,0 +1,26 @@
# 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

@ -119,7 +119,7 @@
# modify these # modify these
scripts = { scripts = {
# every 20 min # every 20 min
"update_data" = "*:0,20,40"; "update_data" = "*:0,10,20,30,40,50";
# groups are updated every hour, offset from teh ldap # groups are updated every hour, offset from teh ldap
"update_users" = "*:05:00"; "update_users" = "*:05:00";
# minecraft stuff is updated at 5am # minecraft stuff is updated at 5am

BIN
media/setup_user_01.png (Stored with Git LFS) Normal file

Binary file not shown.

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

Binary file not shown.

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

Binary file not shown.

View file

@ -163,7 +163,7 @@ pub mod link {
"h2, h4 { font-family: Arial, Helvetica, sans-serif; }" "h2, h4 { font-family: Arial, Helvetica, sans-serif; }"
} }
} }
div style="display: flex; flex-direction: column; align-items: center;" { div {
h2 { "Hello from Skynet!" } h2 { "Hello from Skynet!" }
// Substitute in the name of our recipient. // Substitute in the name of our recipient.
p { "Hi " (user) "," } p { "Hi " (user) "," }
@ -263,6 +263,7 @@ pub mod link {
.await .await
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)] #[serde(untagged)]
pub enum WolvesResultUserResult { pub enum WolvesResultUserResult {

View file

@ -15,6 +15,7 @@ pub(crate) mod user {
pub(crate) mod add { pub(crate) mod add {
use super::*; use super::*;
use crate::commands::link_email::link::get_server_member_discord; use crate::commands::link_email::link::get_server_member_discord;
use serde::{Deserialize, Serialize};
use serenity::model::id::UserId; use serenity::model::id::UserId;
use skynet_discord_bot::common::database::Wolves; use skynet_discord_bot::common::database::Wolves;
use skynet_discord_bot::common::minecraft::{whitelist_update, Minecraft}; use skynet_discord_bot::common::minecraft::{whitelist_update, Minecraft};
@ -22,13 +23,23 @@ pub(crate) mod user {
use sqlx::Error; use sqlx::Error;
pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand { pub fn register(command: &mut CreateApplicationCommand) -> &mut CreateApplicationCommand {
command.name("link_minecraft").description("Link your minecraft account").create_option(|option| { command
.name("link_minecraft")
.description("Link your minecraft account")
.create_option(|option| {
option option
.name("minecraft-username") .name("minecraft_username")
.description("Your Minecraft username") .description("Your Minecraft username")
.kind(CommandOptionType::String) .kind(CommandOptionType::String)
.required(true) .required(true)
}) })
.create_option(|option| {
option
.name("bedrock_account")
.description("Is this a Bedrock account?")
.kind(CommandOptionType::Boolean)
.required(false)
})
} }
pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String { pub async fn run(command: &ApplicationCommandInteraction, ctx: &Context) -> String {
@ -63,6 +74,16 @@ pub(crate) mod user {
return "Please provide a valid username".to_string(); return "Please provide a valid username".to_string();
}; };
// this is always true unless they state its not
let mut java = true;
if let Some(x) = command.data.options.get(1) {
if let Some(CommandDataOptionValue::Boolean(z)) = x.to_owned().resolved {
java = !z;
}
}
let username_mc;
if java {
// insert the username into the database // insert the username into the database
match add_minecraft(&db, &command.user.id, username).await { match add_minecraft(&db, &command.user.id, username).await {
Ok(_) => {} Ok(_) => {}
@ -71,11 +92,30 @@ pub(crate) mod user {
return format!("Failure to minecraft username {:?}", username); 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 // get a list of servers that the user is a member of
if let Ok(servers) = get_servers(&db, &command.user.id).await { if let Ok(servers) = get_servers(&db, &command.user.id).await {
for server in servers { for server in servers {
whitelist_update(&vec![username.to_string()], &server.minecraft, &config.discord_token_minecraft).await; whitelist_update(&vec![(username_mc.to_owned(), java)], &server.minecraft, &config.discord_token_minecraft).await;
} }
} }
@ -96,6 +136,51 @@ pub(crate) mod user {
.await .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.as_u64() as i64)
.bind(minecraft)
.fetch_optional(db)
.await
}
async fn get_servers(db: &Pool<Sqlite>, discord: &UserId) -> Result<Vec<Minecraft>, Error> { async fn get_servers(db: &Pool<Sqlite>, discord: &UserId) -> Result<Vec<Minecraft>, Error> {
sqlx::query_as::<_, Minecraft>( sqlx::query_as::<_, Minecraft>(
" "
@ -249,7 +334,7 @@ pub(crate) mod server {
ID: {id} ID: {id}
Online: {online} Online: {online}
Info: {description} Info: {description}
Link: <http://panel.games.skynet.ie/server/{id}> Link: <https://panel.games.skynet.ie/server/{id}>
"#, "#,
name = &x.attributes.name, name = &x.attributes.name,
online = !x.attributes.is_suspended, online = !x.attributes.is_suspended,

View file

@ -9,6 +9,8 @@ use serenity::model::prelude::application_command::ApplicationCommandInteraction
use serenity::prelude::TypeMapKey; use serenity::prelude::TypeMapKey;
use std::{env, sync::Arc}; use std::{env, sync::Arc};
use tokio::sync::RwLock; use tokio::sync::RwLock;
#[derive(Debug)]
pub struct Config { pub struct Config {
// manages where teh database is stored // manages where teh database is stored
pub home: String, pub home: String,
@ -17,6 +19,7 @@ pub struct Config {
// tokens for discord and other API's // tokens for discord and other API's
pub discord_token: String, pub discord_token: String,
pub discord_token_minecraft: String, pub discord_token_minecraft: String,
pub minecraft_mcprofile: String,
// email settings // email settings
pub mail_smtp: String, pub mail_smtp: String,
@ -44,6 +47,7 @@ pub fn get_config() -> Config {
let mut config = Config { let mut config = Config {
discord_token: "".to_string(), discord_token: "".to_string(),
discord_token_minecraft: "".to_string(), discord_token_minecraft: "".to_string(),
minecraft_mcprofile: "".to_string(),
home: ".".to_string(), home: ".".to_string(),
database: "database.db".to_string(), database: "database.db".to_string(),
@ -52,7 +56,6 @@ pub fn get_config() -> Config {
mail_user: "".to_string(), mail_user: "".to_string(),
mail_pass: "".to_string(), mail_pass: "".to_string(),
wolves_url: "".to_string(), wolves_url: "".to_string(),
wolves_api: "".to_string(),
committee_server: GuildId(0), committee_server: GuildId(0),
committee_role: RoleId(0), committee_role: RoleId(0),
committee_category: ChannelId(0), committee_category: ChannelId(0),
@ -71,6 +74,9 @@ pub fn get_config() -> Config {
if let Ok(x) = env::var("DISCORD_TOKEN_MINECRAFT") { if let Ok(x) = env::var("DISCORD_TOKEN_MINECRAFT") {
config.discord_token_minecraft = x.trim().to_string(); 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") { if let Ok(x) = env::var("EMAIL_SMTP") {
config.mail_smtp = x.trim().to_string(); config.mail_smtp = x.trim().to_string();
@ -85,7 +91,6 @@ pub fn get_config() -> Config {
if let Ok(x) = env::var("WOLVES_URL_BASE") { if let Ok(x) = env::var("WOLVES_URL_BASE") {
config.wolves_url = x.trim().to_string(); config.wolves_url = x.trim().to_string();
} }
if let Ok(x) = env::var("WOLVES_API") { if let Ok(x) = env::var("WOLVES_API") {
config.wolves_api = x.trim().to_string(); config.wolves_api = x.trim().to_string();
} }
@ -109,6 +114,295 @@ pub fn get_config() -> Config {
config config
} }
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ServerMembers {
pub server: GuildId,
pub id_wolves: i64,
pub expiry: String,
}
impl<'r> FromRow<'r, SqliteRow> for ServerMembers {
fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
let server_tmp: i64 = row.try_get("server")?;
let server = GuildId::from(server_tmp as u64);
Ok(Self {
server,
id_wolves: row.try_get("id_wolves")?,
expiry: row.try_get("expiry")?,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ServerMembersWolves {
pub server: GuildId,
pub id_wolves: i64,
pub expiry: String,
pub email: String,
pub discord: Option<UserId>,
pub minecraft: Option<String>,
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);
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")?,
minecraft_uid: row.try_get("minecraft_uid")?,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Wolves {
pub id_wolves: i64,
pub email: String,
pub discord: Option<UserId>,
pub minecraft: Option<String>,
}
impl<'r> FromRow<'r, SqliteRow> for Wolves {
fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
let discord = match row.try_get("discord") {
Ok(x) => {
let tmp: i64 = x;
if tmp == 0 {
None
} else {
Some(UserId::from(tmp as u64))
}
}
_ => None,
};
Ok(Self {
id_wolves: row.try_get("id_wolves")?,
email: row.try_get("email")?,
discord,
minecraft: row.try_get("minecraft")?,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct WolvesVerify {
pub email: String,
pub discord: UserId,
pub auth_code: String,
pub date_expiry: String,
}
impl<'r> FromRow<'r, SqliteRow> for WolvesVerify {
fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
let user_tmp: i64 = row.try_get("discord")?;
let discord = UserId::from(user_tmp as u64);
Ok(Self {
email: row.try_get("email")?,
discord,
auth_code: row.try_get("auth_code")?,
date_expiry: row.try_get("date_expiry")?,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Committee {
pub email: String,
pub discord: UserId,
pub auth_code: String,
pub committee: i64,
}
impl<'r> FromRow<'r, SqliteRow> for Committee {
fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
let user_tmp: i64 = row.try_get("discord")?;
let discord = UserId::from(user_tmp as u64);
Ok(Self {
email: row.try_get("email")?,
discord,
auth_code: row.try_get("auth_code")?,
committee: row.try_get("committee")?,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Servers {
pub server: GuildId,
pub wolves_api: String,
pub role_past: Option<RoleId>,
pub role_current: RoleId,
pub member_past: i64,
pub member_current: i64,
pub bot_channel_id: ChannelId,
// these can be removed in teh future with an API update
pub server_name: String,
pub wolves_link: String,
}
impl<'r> FromRow<'r, SqliteRow> for Servers {
fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
let server_tmp: i64 = row.try_get("server")?;
let server = GuildId::from(server_tmp as u64);
let role_past = match row.try_get("role_past") {
Ok(x) => {
let tmp: i64 = x;
if tmp == 0 {
None
} else {
Some(RoleId::from(tmp as u64))
}
}
_ => None,
};
let role_current = match row.try_get("role_current") {
Ok(x) => {
let tmp: i64 = x;
RoleId::from(tmp as u64)
}
_ => RoleId::from(0u64),
};
let bot_channel_tmp: i64 = row.try_get("bot_channel_id")?;
let bot_channel_id = ChannelId::from(bot_channel_tmp as u64);
Ok(Self {
server,
wolves_api: row.try_get("wolves_api")?,
role_past,
role_current,
member_past: row.try_get("member_past")?,
member_current: row.try_get("member_current")?,
bot_channel_id,
server_name: row.try_get("server_name")?,
wolves_link: row.try_get("wolves_link")?,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Minecraft {
pub discord: GuildId,
pub minecraft: String,
}
impl<'r> FromRow<'r, SqliteRow> for Minecraft {
fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
let server_tmp: i64 = row.try_get("server_discord")?;
let discord = GuildId::from(server_tmp as u64);
Ok(Self {
discord,
minecraft: row.try_get("server_minecraft")?,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RoleAdder {
pub server: GuildId,
pub role_a: RoleId,
pub role_b: RoleId,
pub role_c: RoleId,
}
impl<'r> FromRow<'r, SqliteRow> for RoleAdder {
fn from_row(row: &'r SqliteRow) -> Result<Self, Error> {
let server_tmp: i64 = row.try_get("server")?;
let server = GuildId::from(server_tmp as u64);
Ok(Self {
server,
role_a: get_role_from_row(row, "role_a"),
role_b: get_role_from_row(row, "role_b"),
role_c: get_role_from_row(row, "role_c"),
})
}
}
fn get_role_from_row(row: &SqliteRow, col: &str) -> RoleId {
match row.try_get(col) {
Ok(x) => {
let tmp: i64 = x;
RoleId(tmp as u64)
}
_ => RoleId::from(0u64),
}
}
pub async fn db_init(config: &Config) -> Result<Pool<Sqlite>, Error> {
let database = format!("{}/{}", &config.home, &config.database);
let pool = SqlitePoolOptions::new()
.max_connections(5)
.connect_with(
SqliteConnectOptions::from_str(&format!("sqlite://{}", database))?
.foreign_keys(true)
.create_if_missing(true),
)
.await?;
// migrations are amazing!
sqlx::migrate!("./db/migrations").run(&pool).await?;
Ok(pool)
}
pub async fn get_server_config(db: &Pool<Sqlite>, server: &GuildId) -> Option<Servers> {
sqlx::query_as::<_, Servers>(
r#"
SELECT *
FROM servers
WHERE server = ?
"#,
)
.bind(*server.as_u64() as i64)
.fetch_one(db)
.await
.ok()
}
pub async fn get_server_member(db: &Pool<Sqlite>, server: &GuildId, member: &guild::Member) -> Result<ServerMembersWolves, Error> {
sqlx::query_as::<_, ServerMembersWolves>(
r#"
SELECT *
FROM server_members
JOIN wolves USING (id_wolves)
WHERE server = ? AND discord = ?
"#,
)
.bind(*server.as_u64() as i64)
.bind(*member.user.id.as_u64() as i64)
.fetch_one(db)
.await
}
pub async fn get_server_config_bulk(db: &Pool<Sqlite>) -> Vec<Servers> {
sqlx::query_as::<_, Servers>(
r#"
SELECT *
FROM servers
"#,
)
.fetch_all(db)
.await
.unwrap_or_default()
}
pub fn get_now_iso(short: bool) -> String { pub fn get_now_iso(short: bool) -> String {
let now = Utc::now(); let now = Utc::now();
if short { if short {
@ -122,6 +416,255 @@ pub fn random_string(len: usize) -> String {
thread_rng().sample_iter(&Alphanumeric).take(len).map(char::from).collect() thread_rng().sample_iter(&Alphanumeric).take(len).map(char::from).collect()
} }
pub mod set_roles {
use super::*;
pub async fn update_server(ctx: &Context, server: &Servers, remove_roles: &[Option<RoleId>], members_changed: &[UserId]) {
let db_lock = {
let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Database in TypeMap.").clone()
};
let db = db_lock.read().await;
let Servers {
server,
role_past,
role_current,
..
} = server;
let mut roles_set = [0, 0, 0];
let mut members = vec![];
for member in get_server_member_bulk(&db, server).await {
if let Some(x) = member.discord {
members.push(x);
}
}
let mut members_all = members.len();
if let Ok(x) = server.members(ctx, None, None).await {
for mut member in x {
// members_changed acts as an override to only deal with teh users in it
if !members_changed.is_empty() && !members_changed.contains(&member.user.id) {
continue;
}
if members.contains(&member.user.id) {
let mut roles = vec![];
if let Some(role) = &role_past {
if !member.roles.contains(role) {
roles_set[0] += 1;
roles.push(role.to_owned());
}
}
if !member.roles.contains(role_current) {
roles_set[1] += 1;
roles.push(role_current.to_owned());
}
if let Err(e) = member.add_roles(ctx, &roles).await {
println!("{:?}", e);
}
} else {
// old and never
if let Some(role) = &role_past {
if member.roles.contains(role) {
members_all += 1;
}
}
if member.roles.contains(role_current) {
roles_set[2] += 1;
// if theya re not a current member and have the role then remove it
if let Err(e) = member.remove_role(ctx, role_current).await {
println!("{:?}", e);
}
}
}
for role in remove_roles.iter().flatten() {
if let Err(e) = member.remove_role(ctx, role).await {
println!("{:?}", e);
}
}
}
}
set_server_numbers(&db, server, members_all as i64, members.len() as i64).await;
// small bit of logging to note changes over time
println!("{:?} Changes: New: +{}, Current: +{}/-{}", server.as_u64(), roles_set[0], roles_set[1], roles_set[2]);
}
pub async fn get_server_member_bulk(db: &Pool<Sqlite>, server: &GuildId) -> Vec<ServerMembersWolves> {
sqlx::query_as::<_, ServerMembersWolves>(
r#"
SELECT *
FROM server_members
JOIN wolves USING (id_wolves)
WHERE (
server = ?
AND discord IS NOT NULL
AND expiry > ?
)
"#,
)
.bind(*server.as_u64() as i64)
.bind(get_now_iso(true))
.fetch_all(db)
.await
.unwrap_or_default()
}
async fn set_server_numbers(db: &Pool<Sqlite>, server: &GuildId, past: i64, current: i64) {
match sqlx::query_as::<_, Wolves>(
"
UPDATE servers
SET member_past = ?, member_current = ?
WHERE server = ?
",
)
.bind(past)
.bind(current)
.bind(*server.as_u64() as i64)
.fetch_optional(db)
.await
{
Ok(_) => {}
Err(e) => {
println!("Failure to insert into {}", server.as_u64());
println!("{:?}", e);
}
}
}
}
pub mod get_data {
use super::*;
use crate::set_roles::update_server;
use std::collections::BTreeMap;
use wolves_oxidised::WolvesUser;
pub async fn get_wolves(ctx: &Context) {
let db_lock = {
let data_read = ctx.data.read().await;
data_read.get::<DataBase>().expect("Expected Database in TypeMap.").clone()
};
let db = db_lock.read().await;
let config_lock = {
let data_read = ctx.data.read().await;
data_read.get::<Config>().expect("Expected Config in TypeMap.").clone()
};
let config = config_lock.read().await;
// set up teh client
let wolves = wolves_oxidised::Client::new(&config.wolves_url, Some(&config.wolves_api));
for server_config in get_server_config_bulk(&db).await {
let Servers {
server,
wolves_api,
..
} = &server_config;
let existing_tmp = get_server_member(&db, server).await;
let existing = existing_tmp.iter().map(|data| (data.id_wolves, data)).collect::<BTreeMap<_, _>>();
// list of users that need to be updated for this server
let mut user_to_update = vec![];
for user in wolves.get_members(wolves_api).await {
let id = user.member_id.parse::<u64>().unwrap_or_default();
match existing.get(&(id as i64)) {
None => {
// user does not exist already, add everything
add_users_wolves(&db, &user).await;
add_users_server_members(&db, server, &user).await;
}
Some(old) => {
// always update wolves table, in case data has changed
add_users_wolves(&db, &user).await;
if old.expiry != user.expiry {
add_users_server_members(&db, server, &user).await;
if let Some(discord_id) = old.discord {
user_to_update.push(discord_id);
}
}
}
}
}
if !user_to_update.is_empty() {
update_server(ctx, &server_config, &[], &user_to_update).await;
}
}
}
pub async fn get_server_member(db: &Pool<Sqlite>, server: &GuildId) -> Vec<ServerMembersWolves> {
sqlx::query_as::<_, ServerMembersWolves>(
r#"
SELECT *
FROM server_members
JOIN wolves USING (id_wolves)
WHERE (
server = ?
AND discord IS NOT NULL
)
"#,
)
.bind(*server.as_u64() as i64)
.fetch_all(db)
.await
.unwrap_or_default()
}
async fn add_users_wolves(db: &Pool<Sqlite>, user: &WolvesUser) {
// expiry
match sqlx::query_as::<_, Wolves>(
"
INSERT INTO wolves (id_wolves, email)
VALUES ($1, $2)
ON CONFLICT(id_wolves) DO UPDATE SET email = $2
",
)
.bind(&user.member_id)
.bind(&user.contact_email)
.fetch_optional(db)
.await
{
Ok(_) => {}
Err(e) => {
println!("Failure to insert into Wolves {:?}", user);
println!("{:?}", e);
}
}
}
async fn add_users_server_members(db: &Pool<Sqlite>, server: &GuildId, user: &WolvesUser) {
match sqlx::query_as::<_, ServerMembers>(
"
INSERT OR REPLACE INTO server_members (server, id_wolves, expiry)
VALUES (?1, ?2, ?3)
",
)
.bind(*server.as_u64() as i64)
.bind(&user.member_id)
.bind(&user.expiry)
.fetch_optional(db)
.await
{
Ok(_) => {}
Err(e) => {
println!("Failure to insert into ServerMembers {} {:?}", server.as_u64(), user);
println!("{:?}", e);
}
}
}
}
/** /**
For any time ye need to check if a user who calls a command has admin privlages For any time ye need to check if a user who calls a command has admin privlages
*/ */
@ -159,3 +702,153 @@ pub async fn is_admin(command: &ApplicationCommandInteraction, ctx: &Context) ->
None None
} }
} }
/**
loop through all members of server
get a list of folks with mc accounts that are members
and a list that arent members
*/
pub async fn update_server(server_id: &str, db: &Pool<Sqlite>, g_id: &GuildId, config: &Config) {
let mut usernames = vec![];
for member in get_server_member_bulk(db, g_id).await {
if let Some(x) = member.minecraft {
usernames.push((x, 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;
}
}
async fn post<T: Serialize>(url: &str, bearer: &str, data: &T) {
match surf::post(url)
.header("Authorization", bearer)
.header("Content-Type", "application/json")
.header("Accept", "Application/vnd.pterodactyl.v1+json")
.body_json(&data)
{
Ok(req) => {
req.await.ok();
}
Err(e) => {
dbg!(e);
}
}
}
#[derive(Deserialize, Serialize, Debug)]
pub struct ServerDetailsResSub {
pub identifier: String,
pub name: String,
pub description: String,
pub is_suspended: bool,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct ServerDetailsRes {
pub attributes: ServerDetailsResSub,
}
async fn get<T: Serialize + DeserializeOwned>(url: &str, bearer: &str) -> Option<T> {
match surf::get(url)
.header("Authorization", bearer)
.header("Content-Type", "application/json")
.header("Accept", "Application/vnd.pterodactyl.v1+json")
.recv_json()
.await
{
Ok(res) => Some(res),
Err(e) => {
dbg!(e);
None
}
}
}
#[derive(Deserialize, Serialize, Debug)]
struct BodyCommand {
command: String,
}
#[derive(Deserialize, Serialize, Debug)]
struct BodyDelete {
root: String,
files: Vec<String>,
}
pub async fn whitelist_update(add: &Vec<(String, 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;
}
}
pub async fn whitelist_wipe(server: &str, token: &str) {
println!("Wiping whitelist for {}", server);
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> {
println!("Get server information for {}", server);
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.as_u64() as i64)
.fetch_all(db)
.await
.unwrap_or_default()
}