Compare commits

...

64 Commits

Author SHA1 Message Date
Xyon db37ee7516 Merge pull request 'Add quick and dirty command to allow admins to make the bot say stuff' (#22) from feature/arbitrary-messaging into main
Badgey Deployment / build (push) Successful in 6m30s Details
Badgey Deployment / deploy (BADGEY) (push) Successful in 10s Details
Badgey Deployment / deploy (M5_COMPUTER) (push) Successful in 8s Details
Reviewed-on: #22
2025-10-05 11:23:42 +00:00
Xyon 71340f9db1 Add quick and dirty command to allow admins to make the bot say stuff 2025-10-05 12:22:04 +01:00
Xyon 8ab3d67542 Merge pull request 'drop locks before holding pagination timeouts' (#21) from xyon-patch-1 into main
Badgey Deployment / build (push) Successful in 6m36s Details
Badgey Deployment / deploy (BADGEY) (push) Successful in 11s Details
Badgey Deployment / deploy (M5_COMPUTER) (push) Successful in 10s Details
Reviewed-on: #21
2025-06-13 02:26:20 +00:00
Xyon 1b31ca39b4 drop locks before holding pagination timeouts 2025-06-13 02:26:01 +00:00
Xyon fcca9b4ea7 Merge pull request 'Allow channels to be ignored for XP gain' (#20) from feature/ignore-for-xp into main
Badgey Deployment / build (push) Successful in 6m20s Details
Badgey Deployment / deploy (BADGEY) (push) Successful in 8s Details
Badgey Deployment / deploy (M5_COMPUTER) (push) Successful in 9s Details
Reviewed-on: #20
2025-02-17 21:55:09 +00:00
Xyon 8f8fe1df0b Merge branch 'main' into feature/ignore-for-xp 2025-02-17 21:54:54 +00:00
Xyon bb17e1c471
Allow channels to be ignored for XP gain 2025-02-17 21:50:00 +00:00
Xyon d90eeef19d Merge pull request 'Add initial support for pulling F1 championship standings' (#19) from feature/f1-standings into main
Badgey Deployment / build (push) Successful in 7m19s Details
Badgey Deployment / deploy (BADGEY) (push) Successful in 10s Details
Badgey Deployment / deploy (M5_COMPUTER) (push) Successful in 9s Details
Reviewed-on: #19
2024-11-03 22:24:34 +00:00
Xyon c20c1165aa
Add initial support for pulling F1 championship standings 2024-11-03 22:24:22 +00:00
Xyon cd50d26f30 Merge pull request 'drop the fucking mutex lock on the fucking userobject info BEFORE sitting and doing fucking nothing for 300 fucking seconds like a fucking idiot' (#18) from hotfix/fucking-field-limits-ffs into main
Badgey Deployment / build (push) Successful in 6m19s Details
Badgey Deployment / deploy (BADGEY) (push) Successful in 6s Details
Badgey Deployment / deploy (M5_COMPUTER) (push) Successful in 6s Details
Reviewed-on: #18
2024-10-20 15:51:22 +00:00
Xyon fee32c8e51 Merge branch 'main' into hotfix/fucking-field-limits-ffs 2024-10-20 15:51:16 +00:00
Xyon e6efaf3a06
drop the fucking mutex lock on the fucking userobject info BEFORE sitting and doing fucking nothing for 300 fucking seconds like a fucking idiot 2024-10-20 16:51:06 +01:00
Xyon fa68df0a94 Merge pull request 'Fix formatting on mobile by refactoring the whole approach' (#17) from hotfix/fucking-field-limits-ffs into main
Badgey Deployment / build (push) Successful in 6m10s Details
Badgey Deployment / deploy (BADGEY) (push) Successful in 6s Details
Badgey Deployment / deploy (M5_COMPUTER) (push) Successful in 7s Details
Reviewed-on: #17
2024-10-20 15:17:39 +00:00
Xyon 7ee321f691 Merge branch 'main' into hotfix/fucking-field-limits-ffs 2024-10-20 15:17:35 +00:00
Xyon 6300b45368
Fix formatting on mobile by refactoring the whole approach 2024-10-20 16:17:17 +01:00
Xyon 474c7d6b7c Merge pull request 'Fix indexes, pin to 20 per page' (#16) from hotfix/fucking-field-limits-ffs into main
Badgey Deployment / build (push) Successful in 6m7s Details
Badgey Deployment / deploy (BADGEY) (push) Successful in 6s Details
Badgey Deployment / deploy (M5_COMPUTER) (push) Successful in 6s Details
Reviewed-on: #16
2024-10-20 12:50:05 +00:00
Xyon 3a99a72880 Merge branch 'main' into hotfix/fucking-field-limits-ffs 2024-10-20 12:50:00 +00:00
Xyon 2c5e184a01
Fix indexes, pin to 20 per page 2024-10-20 13:48:58 +01:00
Xyon bc1890b59b Merge pull request 'Fields are capped at 1024, dumdum' (#15) from hotfix/fucking-field-limits-ffs into main
Badgey Deployment / build (push) Successful in 6m10s Details
Badgey Deployment / deploy (BADGEY) (push) Successful in 6s Details
Badgey Deployment / deploy (M5_COMPUTER) (push) Successful in 6s Details
Reviewed-on: #15
2024-10-20 12:13:32 +00:00
Xyon 51fa9dcd05 Merge branch 'main' into hotfix/fucking-field-limits-ffs 2024-10-20 12:13:26 +00:00
Xyon e7c3654408
Fields are capped at 1024, dumdum 2024-10-20 13:12:50 +01:00
Xyon 54d4764458 Merge pull request 'Pivot to a different approach that only uses three fields like i should've done in the first place' (#14) from hotfix/fucking-field-limits-ffs into main
Badgey Deployment / build (push) Successful in 6m27s Details
Badgey Deployment / deploy (BADGEY) (push) Successful in 9s Details
Badgey Deployment / deploy (M5_COMPUTER) (push) Successful in 7s Details
Reviewed-on: #14
2024-10-20 11:42:38 +00:00
Xyon b2e18f23f6 Merge branch 'main' into hotfix/fucking-field-limits-ffs 2024-10-20 11:42:32 +00:00
Xyon 9c65c14bf8
Pivot to a different approach that only uses three fields like i should've done in the first place 2024-10-20 12:42:16 +01:00
Xyon 5a2f32733c Merge pull request 'kldalkjdhsdadsdad' (#13) from hotfix/fucking-field-limits-ffs into main
Badgey Deployment / build (push) Successful in 6m22s Details
Badgey Deployment / deploy (BADGEY) (push) Successful in 7s Details
Badgey Deployment / deploy (M5_COMPUTER) (push) Successful in 7s Details
Reviewed-on: #13
2024-10-19 16:07:02 +00:00
Xyon 999cef6dcd
fucking field limits 2024-10-19 17:06:52 +01:00
Xyon bb13e8fab7 Merge pull request 'quick fix - cap leaderboard to 7 per page due to field constraints' (#12) from feature/xp-leaderboard into main
Badgey Deployment / build (push) Successful in 6m17s Details
Badgey Deployment / deploy (BADGEY) (push) Successful in 7s Details
Badgey Deployment / deploy (M5_COMPUTER) (push) Successful in 6s Details
Reviewed-on: #12
2024-10-19 15:54:26 +00:00
Xyon 571eed6bf5 Merge branch 'main' into feature/xp-leaderboard 2024-10-19 15:54:21 +00:00
Xyon 60a0cfc812
quick fix - cap leaderboard to 7 per page due to field constraints 2024-10-19 16:53:25 +01:00
Xyon 0551c8aaa2 Merge pull request 'feature/xp-leaderboard' (#11) from feature/xp-leaderboard into main
Badgey Deployment / build (push) Successful in 6m22s Details
Badgey Deployment / deploy (BADGEY) (push) Successful in 8s Details
Badgey Deployment / deploy (M5_COMPUTER) (push) Successful in 7s Details
Reviewed-on: #11
2024-10-19 15:23:56 +00:00
Xyon 6c3b4c6ad7
Don't use development copy of manifold in prod bot 2024-10-19 16:23:30 +01:00
Xyon 074454e555
Add leaderboard, paginate custom responses output 2024-10-19 16:22:11 +01:00
Xyon 67c17aced6 Merge pull request 'Don't award XP in quarantine channels' (#10) from feature/no-xp-in-quarantine-channels into main
Badgey Deployment / build (push) Successful in 6m25s Details
Badgey Deployment / deploy (BADGEY) (push) Successful in 7s Details
Badgey Deployment / deploy (M5_COMPUTER) (push) Successful in 7s Details
Reviewed-on: #10
2024-10-13 14:44:11 +00:00
Xyon ed93d60a69
Don't award XP in quarantine channels 2024-10-13 15:43:51 +01:00
Xyon 090223388d Merge pull request 'Clean up merge, finish delete, clean up warnings' (#9) from hotfix/build-error into main
Badgey Deployment / build (push) Successful in 6m12s Details
Badgey Deployment / deploy (BADGEY) (push) Successful in 7s Details
Badgey Deployment / deploy (M5_COMPUTER) (push) Successful in 7s Details
Reviewed-on: #9
2024-10-13 13:59:16 +00:00
Xyon 2bcf7c8d42
Clean up merge, finish delete, clean up warnings 2024-10-13 14:58:53 +01:00
Xyon 751bc8aca5 Merge pull request 'Implement user greetings when added to defined quarantine channels via role' (#8) from feature/airlock-greetings into main
Badgey Deployment / build (push) Failing after 5m41s Details
Badgey Deployment / deploy (BADGEY) (push) Failing after 6s Details
Badgey Deployment / deploy (M5_COMPUTER) (push) Failing after 4s Details
Reviewed-on: #8
2024-10-13 13:45:52 +00:00
Xyon 8a52ab5783
Merge remote-tracking branch 'origin/main' into feature/airlock-greetings 2024-10-13 14:45:36 +01:00
Xyon e61f8aec21
Implement user greetings when added to defined quarantine channels via role 2024-10-13 14:43:47 +01:00
Xyon ea81778099 Merge pull request 'Add missing closing square bracket' (#7) from hotfix/deploy-error into main
Badgey Deployment / build (push) Successful in 6m14s Details
Badgey Deployment / deploy (BADGEY) (push) Successful in 6s Details
Badgey Deployment / deploy (M5_COMPUTER) (push) Successful in 6s Details
Reviewed-on: #7
2024-10-12 17:38:47 +00:00
Xyon 4cb966c038
Add missing closing square bracket 2024-10-12 18:38:36 +01:00
Xyon a97460d739 Merge pull request 'feature/mod-convenience' (#6) from feature/mod-convenience into main
Badgey Deployment / build (push) Successful in 6m13s Details
Badgey Deployment / deploy (BADGEY) (push) Successful in 6s Details
Badgey Deployment / deploy (M5_COMPUTER) (push) Successful in 7s Details
Reviewed-on: #6
2024-10-12 16:25:33 +00:00
Xyon a64f66cc7a
Remove unused include 2024-10-12 17:25:09 +01:00
Xyon 54bd863934
Reimplement 'sr' helper command 2024-10-12 17:24:11 +01:00
Xyon 04552bb029
Don't need sudo for this deploy action
Badgey Deployment / build (push) Successful in 6m10s Details
Badgey Deployment / deploy (BADGEY) (push) Successful in 6s Details
Badgey Deployment / deploy (M5_COMPUTER) (push) Successful in 7s Details
2024-10-12 16:42:36 +01:00
Xyon 2001c61b17
retain badgey user, set exec perms on delivered binary
Badgey Deployment / build (push) Successful in 6m6s Details
Badgey Deployment / deploy (BADGEY) (push) Successful in 6s Details
Badgey Deployment / deploy (M5_COMPUTER) (push) Failing after 7s Details
2024-10-12 16:30:18 +01:00
Xyon 9f363180bf
switch back to matrix deploy to deploy both bots
Badgey Deployment / build (push) Successful in 6m8s Details
Badgey Deployment / deploy (BADGEY) (push) Successful in 6s Details
Badgey Deployment / deploy (M5_COMPUTER) (push) Failing after 5s Details
2024-10-12 16:20:31 +01:00
Xyon e2a6c39f15
Add missing closing bracket
Badgey Deployment / build (push) Successful in 6m6s Details
Badgey Deployment / deploy (push) Successful in 5s Details
2024-10-12 15:58:22 +01:00
Xyon 0b0958be39
Adjust encapsulation on ssh pre-seed step
Badgey Deployment / build (push) Successful in 6m6s Details
Badgey Deployment / deploy (push) Failing after 4s Details
2024-10-12 15:38:16 +01:00
Xyon a03c950eb3
Adjust encapsulation on ssh pre-seed step
Badgey Deployment / build (push) Successful in 6m13s Details
Badgey Deployment / deploy (push) Failing after 5s Details
2024-10-12 15:30:23 +01:00
Xyon 17c016590d
Complete set of token replacements for badgey
Badgey Deployment / build (push) Successful in 6m14s Details
Badgey Deployment / deploy (push) Failing after 5s Details
2024-10-12 15:09:02 +01:00
Xyon 326e5a55bb
Iterate further; dynamic variable name injection into config
Badgey Deployment / build (push) Successful in 6m11s Details
Badgey Deployment / deploy (push) Successful in 6s Details
2024-10-12 14:59:43 +01:00
Xyon fe87fce2a0
Iterate another step; inject badgey details in via variables
Badgey Deployment / build (push) Successful in 6m12s Details
Badgey Deployment / deploy (push) Successful in 6s Details
2024-10-12 14:33:06 +01:00
Xyon 796966cfdd
Accomodate file no longer in build output dir
Badgey Deployment / build (push) Successful in 6m13s Details
Badgey Deployment / deploy (push) Successful in 6s Details
2024-10-12 14:22:00 +01:00
Xyon b48de73829
Split build from deploy job, remain single-homed for now
Badgey Deployment / build (push) Successful in 6m9s Details
Badgey Deployment / deploy (push) Successful in 6s Details
2024-10-12 14:09:09 +01:00
Xyon 6f5af282da
Revert deploy changes to previous base and rethink life choices
Badgey Deployment / build (push) Successful in 6m4s Details
2024-10-12 13:53:18 +01:00
Xyon e50f95c6f6
Revert "Retaining badgey user for simplicity"
This reverts commit d53b613044.
2024-10-12 13:50:15 +01:00
Xyon 5ba7f03188
Revert "More work on multi-deployment"
This reverts commit 7883307136.
2024-10-12 13:49:42 +01:00
Xyon 7883307136
More work on multi-deployment
Badgey Deployment / deploy (BADGEY) (push) Waiting to run Details
Badgey Deployment / deploy (M5_COMPUTER) (push) Waiting to run Details
Badgey Deployment / build (push) Has been cancelled Details
2024-10-08 18:36:05 +01:00
Xyon 880a339e9a
Downgrade action to supported ver
Badgey Deployment / build (push) Successful in 6m19s Details
Badgey Deployment / deploy (BADGEY) (push) Failing after 5s Details
Badgey Deployment / deploy (M5_COMPUTER) (push) Failing after 5s Details
2024-10-08 01:43:51 +01:00
Xyon bb35f1a10e
manifold container is updated, let's try plan N
Badgey Deployment / build (push) Failing after 6m10s Details
Badgey Deployment / deploy (BADGEY) (push) Failing after 5s Details
Badgey Deployment / deploy (M5_COMPUTER) (push) Failing after 4s Details
2024-10-08 01:24:31 +01:00
Xyon f239338989
manifold container is updated, let's try plan M
Badgey Deployment / build (push) Failing after 12s Details
Badgey Deployment / deploy (BADGEY) (push) Failing after 4s Details
Badgey Deployment / deploy (M5_COMPUTER) (push) Failing after 4s Details
2024-10-08 01:21:35 +01:00
Xyon a5696dd764
Flip container image to docker repo one, manifold one appears to be FUBAR
Badgey Deployment / build (push) Failing after 3s Details
Badgey Deployment / deploy (BADGEY) (push) Failing after 3s Details
Badgey Deployment / deploy (M5_COMPUTER) (push) Failing after 3s Details
2024-10-08 00:47:33 +01:00
Xyon cac7d8f0e2 Merge pull request 'feature/string-translation' (#5) from feature/string-translation into main
Badgey Deployment / build (push) Failing after 29s Details
Badgey Deployment / deploy (BADGEY) (push) Failing after 9s Details
Badgey Deployment / deploy (M5_COMPUTER) (push) Failing after 5s Details
Reviewed-on: #5
2024-10-07 23:14:06 +00:00
25 changed files with 1343 additions and 378 deletions

View File

@ -8,19 +8,17 @@ on:
jobs: jobs:
build: build:
runs-on: rust runs-on: rust
container:
options: --dns 172.16.255.254
env: env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Pre-seed known_hosts (Badgey)
run: mkdir -pv ~/.ssh && ssh-keyscan -t rsa badgey >> ~/.ssh/known_hosts
- name: Pre-seed known_hosts (M5)
run: mkdir -pv ~/.ssh && ssh-keyscan -t rsa m5-computer >> ~/.ssh/known_hosts
- name: Build (Release) - name: Build (Release)
run: cargo build --release --color=always run: cargo build --release --color=always
- name: Archive artifact - name: Archive artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: badgey name: badgey
path: target/release/badgey path: target/release/badgey
@ -35,19 +33,23 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Download artifact - name: Download artifact
uses: actions/download-artifact@v4 uses: actions/download-artifact@v3
with: with:
name: badgey name: badgey
- name: Seed config file - name: Pre-seed known_hosts
uses: cschleiden/replace-tokens@v1.2 run: mkdir -pv ~/.ssh && ssh-keyscan -t rsa $BOT_SERVER_HOSTNAME >> ~/.ssh/known_hosts
env:
BOT_SERVER_HOSTNAME: ${{ vars[format('{0}_SERVER_HOSTNAME', matrix.bot)] }}
- uses: cschleiden/replace-tokens@v1.2
with: with:
files: config/production.badgey.json files: config/production.badgey.json
env: env:
BOT_NICKNAME: ${{ vars[format('{0}_BOT_NICKNAME', matrix.bot) }} BOT_NICKNAME: ${{ vars[format('{0}_BOT_NICKNAME', matrix.bot)] }}
LOG_CHANNEL_ID: ${{ vars[format('{0}_LOG_CHANNEL_ID', matrix.bot) }} BOT_IDENTIFIER: ${{ vars[format('{0}_SERVER_HOSTNAME', matrix.bot)] }}
LOG_CHANNEL_ID: ${{ vars[format('{0}_LOG_CHANNEL_ID', matrix.bot)] }}
POSTGRES_HOST: ${{ vars.POSTGRES_HOST }} POSTGRES_HOST: ${{ vars.POSTGRES_HOST }}
POSTGRES_USER: ${{ vars.POSTGRES_USER }} POSTGRES_USER: ${{ vars.POSTGRES_USER }}
POSTGRES_DATABASE_NAME: ${{ vars[format('{0}_POSTGRES_DATABASE_NAME', matrix.bot) }} POSTGRES_DATABASE_NAME: ${{ vars[format('{0}_POSTGRES_DATABASE_NAME', matrix.bot)] }}
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }} POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
WEATHER_API_KEY: ${{ secrets.WEATHER_API_KEY }} WEATHER_API_KEY: ${{ secrets.WEATHER_API_KEY }}
DOGPICS_API_KEY: ${{ secrets.DOGPICS_API_KEY }} DOGPICS_API_KEY: ${{ secrets.DOGPICS_API_KEY }}
@ -55,4 +57,4 @@ jobs:
- name: Seed SSH key for deploy - name: Seed SSH key for deploy
run: echo "${{ secrets.DEPLOY_KEY }}" | tr -d '\r' > ~/.ssh/id_rsa && chmod 0600 ~/.ssh/id_rsa run: echo "${{ secrets.DEPLOY_KEY }}" | tr -d '\r' > ~/.ssh/id_rsa && chmod 0600 ~/.ssh/id_rsa
- name: Deploy - name: Deploy
run: bash cicd/deploy.sh ${{ matrix.bot }} run: bash cicd/deploy.sh ${{ vars[format('{0}_SERVER_HOSTNAME', matrix.bot)] }}

744
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package] [package]
name = "badgey" name = "badgey"
version = "4.1.0" version = "4.2.0"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -21,7 +21,12 @@ poise = { version = "0.5.*", features = [ "cache" ] }
rand = { version = "0.8.5", features = [ "small_rng" ] } rand = { version = "0.8.5", features = [ "small_rng" ] }
regex = "1.9.5" regex = "1.9.5"
rust-i18n = "3.1.2" rust-i18n = "3.1.2"
to_markdown_table = "0.1.5"
tokio = { version = "1.16.1", features = ["sync", "macros", "rt-multi-thread"] } tokio = { version = "1.16.1", features = ["sync", "macros", "rt-multi-thread"] }
url = "2.5.2"
serde = { version = "1.0.210", features = ["derive"] }
reqwest = "0.12.9"
serde_json = "1.0.132"
[package.metadata.i18n] [package.metadata.i18n]
# The available locales for your application, default: ["en"]. # The available locales for your application, default: ["en"].

View File

@ -2,8 +2,11 @@
bot=$(echo "$1" | tr '[:upper:]' '[:lower:]') bot=$(echo "$1" | tr '[:upper:]' '[:lower:]')
echo "Running deploy for bot ${bot}"
ssh badgey@$bot sudo /usr/bin/systemctl stop $bot ssh badgey@$bot sudo /usr/bin/systemctl stop $bot
rsync -avP badgey badgey@$bot:/srv/$bot/ rsync -avP badgey badgey@$bot:/srv/$bot/$bot
rsync -avP config badgey@$bot:/srv/$bot/ rsync -avP config badgey@$bot:/srv/$bot/
rsync -avP txt badgey@$bot:/srv/$bot/ rsync -avP txt badgey@$bot:/srv/$bot/
ssh badgey@$bot chmod a+x /srv/$bot/$bot
ssh badgey@$bot sudo /usr/bin/systemctl start $bot ssh badgey@$bot sudo /usr/bin/systemctl start $bot

View File

@ -44,6 +44,10 @@
"source_uri": "https://api.nasa.gov/planetary/apod?api_key=NZfKclpoaO9HnvfvaCjeJ3csDecvIqNiABVw2YvN", "source_uri": "https://api.nasa.gov/planetary/apod?api_key=NZfKclpoaO9HnvfvaCjeJ3csDecvIqNiABVw2YvN",
"cache_name": "nasa_apod", "cache_name": "nasa_apod",
"cache_mode": "NoCache" "cache_mode": "NoCache"
},
"f1": {
"source_uri": "https://api.jolpi.ca/ergast/f1",
"cache_mode": "NoCache"
} }
} }
} }

View File

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS "quarantine_channels";

View File

@ -0,0 +1,5 @@
CREATE TABLE IF NOT EXISTS "quarantine_channels" (
id SERIAL PRIMARY KEY,
qc_role_id BIGINT NOT NULL,
qc_channel_id BIGINT NOT NULL
);

View File

@ -0,0 +1,18 @@
ALTER TABLE IF EXISTS "channels"
RENAME TO "quarantine_channels";
-- Make the quarantine channels table hold more generic channel info
ALTER TABLE IF EXISTS "quarantine_channels"
RENAME COLUMN "channel_id" TO "qc_channel_id";
-- Channels with this flag should retain old qc behaviour
ALTER TABLE IF EXISTS "quarantine_channels"
DROP COLUMN "is_quarantine_channel";
-- Setting flag to false allows channels to be excluded from XP even if not QC (though QC channels via the above flag imply this flag too)
ALTER TABLE IF EXISTS "quarantine_channels"
DROP COLUMN "is_valid_for_xp";
-- Ensure that the role ID can be null for non-qc channels
ALTER TABLE IF EXISTS "quarantine_channels"
ALTER COLUMN "qc_role_id" SET NOT NULL;

View File

@ -0,0 +1,19 @@
-- Make the quarantine channels table hold more generic channel info
ALTER TABLE IF EXISTS "quarantine_channels"
RENAME COLUMN "qc_channel_id" TO "channel_id";
-- Channels with this flag should retain old qc behaviour
ALTER TABLE IF EXISTS "quarantine_channels"
ADD COLUMN "is_quarantine_channel" BOOL NOT NULL DEFAULT FALSE;
-- Setting flag to false allows channels to be excluded from XP even if not QC (though QC channels via the above flag imply this flag too)
ALTER TABLE IF EXISTS "quarantine_channels"
ADD COLUMN "is_valid_for_xp" BOOL NOT NULL DEFAULT TRUE;
-- Ensure that the role ID can be null for non-qc channels
ALTER TABLE IF EXISTS "quarantine_channels"
ALTER COLUMN "qc_role_id" DROP NOT NULL;
-- Rename table to something more suitable
ALTER TABLE IF EXISTS "quarantine_channels"
RENAME TO "channels";

View File

@ -4,5 +4,6 @@ pkgs.mkShell {
openssl openssl
postgresql postgresql
pkg-config pkg-config
diesel-cli
]; ];
} }

View File

@ -1,73 +1,91 @@
use poise::serenity_prelude as serenity; use poise::serenity_prelude as serenity;
use manifold::error::{ManifoldError, ManifoldResult}; use manifold::error::{ManifoldError, ManifoldResult};
use manifold::{ManifoldContext, ManifoldData}; use manifold::{ManifoldContext, ManifoldData};
use poise::serenity_prelude::Mentionable; use poise::serenity_prelude::{CreateEmbed, Mentionable};
use crate::badgey::models::custom_response::{CustomResponse, CustomResponseInserter}; use url::Url;
use crate::badgey::models::custom_response::{CustomResponse, CustomResponseInserter};
#[poise::command(slash_command, prefix_command, required_permissions = "MODERATE_MEMBERS" )]
async fn add_custom_response(ctx: ManifoldContext<'_>, target: serenity::User, trigger: String, response: String) -> ManifoldResult<()> { #[poise::command(slash_command, prefix_command, required_permissions = "MODERATE_MEMBERS" )]
let db = &ctx.data().database; async fn add_custom_response(ctx: ManifoldContext<'_>, target: serenity::User, trigger: String, response: String) -> ManifoldResult<()> {
let db = &ctx.data().database;
match CustomResponseInserter::new(trigger, response, target, ctx.author().clone()).insert(db) {
Ok(_) => ctx.reply(t!("commands.custom_response.added")).await?, match CustomResponseInserter::new(trigger, response, target, ctx.author().clone()).insert(db) {
Err(e) => ctx.reply(t!("commands.custom_response.added.error", error = e)).await?, Ok(_) => ctx.reply(t!("commands.custom_response.added")).await?,
}; Err(e) => ctx.reply(t!("commands.custom_response.added.error", error = e)).await?,
};
Ok(())
} Ok(())
}
#[poise::command(slash_command, prefix_command, required_permissions = "MODERATE_MEMBERS")]
async fn remove_custom_response(ctx: ManifoldContext<'_>, id: i32) -> ManifoldResult<()> { #[poise::command(slash_command, prefix_command, required_permissions = "MODERATE_MEMBERS")]
let db = &ctx.data().database; async fn remove_custom_response(ctx: ManifoldContext<'_>, id: i32) -> ManifoldResult<()> {
let db = &ctx.data().database;
let mut response = CustomResponse::find_by_id(db, &id)?;
let mut response = CustomResponse::find_by_id(db, &id)?;
response.deleted = Some(true);
response.insert(db)?; response.deleted = Some(true);
response.insert(db)?;
ctx.reply(t!("commands.custom_response.marked_deleted", response_id=id)).await?;
ctx.reply(t!("commands.custom_response.marked_deleted", response_id=id)).await?;
Ok(())
} Ok(())
}
#[poise::command(slash_command, prefix_command, required_permissions = "MODERATE_MEMBERS")]
async fn undelete_custom_response(ctx: ManifoldContext<'_>, id: i32) -> ManifoldResult<()> { #[poise::command(slash_command, prefix_command, required_permissions = "MODERATE_MEMBERS")]
let db = &ctx.data().database; async fn undelete_custom_response(ctx: ManifoldContext<'_>, id: i32) -> ManifoldResult<()> {
let db = &ctx.data().database;
let mut response = CustomResponse::find_by_id(db, &id)?;
let mut response = CustomResponse::find_by_id(db, &id)?;
if let Some(_) = response.deleted {
response.deleted = None; if let Some(_) = response.deleted {
response.insert(db)?; response.deleted = None;
ctx.reply(t!("commands.custom_response.marked_undeleted", response_id=id)).await?; response.insert(db)?;
} else { ctx.reply(t!("commands.custom_response.marked_undeleted", response_id=id)).await?;
ctx.reply(t!("commands.custom_response.undelete_error_not_deleted", response_id=id)).await?; } else {
} ctx.reply(t!("commands.custom_response.undelete_error_not_deleted", response_id=id)).await?;
}
Ok(())
} Ok(())
}
#[poise::command(slash_command, prefix_command, required_permissions = "MODERATE_MEMBERS")]
async fn list_custom_responses_for_user(ctx: ManifoldContext<'_>, target: serenity::User) -> ManifoldResult<()> { #[poise::command(slash_command, prefix_command, required_permissions = "MODERATE_MEMBERS")]
let db = &ctx.data().database; async fn list_custom_responses_for_user(ctx: ManifoldContext<'_>, target: serenity::User) -> ManifoldResult<()> {
let db = &ctx.data().database;
let mut answer: String = "".to_string(); let userinfo = &ctx.data().user_info.lock().await;
CustomResponse::find_by_user(db, &target)?.iter().for_each(|f| { let reply_handle = ctx.reply("Retrieving custom responses, please stand by...".to_string()).await?;
answer.push_str(format!("{}{}", "\n", f.to_string()).as_str()); let mut pages = Vec::<CreateEmbed>::new();
}); let responses = CustomResponse::find_by_user(db, &target)?;
let total = responses.len();
if answer.len() == 0 {
answer.push_str("\nNone found"); responses.iter().enumerate().for_each(|(i, f)| {
} pages.push(CreateEmbed::default()
.title(format!("Custom Response {item} of {total} for user {target}", item=(i + 1), total=&total, target=target.name))
ctx.reply(t!("commands.custom_response.list_user", user = &target.mention(), response_list = answer)).await?; .description(format!("Added by {added_by} on <t:{added_on}:F> (<t:{added_on}:R>)", added_by=userinfo.get(&(f.added_by as u64)).unwrap().username, added_on=f.added_on))
.field("ID", format!("{id}", id=f.id), true)
Ok(()) .field("Deleted", format!("{deleted}", deleted=(if f.deleted.unwrap_or(false) {"yes"} else {"no"})), true)
} .field("Trigger", format!("{trigger}", trigger=f.trigger), true)
.field("Response", format!("{response}", response=f.response), false)
pub fn commands() -> [poise::Command<ManifoldData, ManifoldError>; 4] { .image(format!("{response}", response=Url::parse(&*f.response).unwrap_or("https://example.com".parse().unwrap())))
[add_custom_response(), remove_custom_response(), undelete_custom_response(), list_custom_responses_for_user()] .to_owned()
} )
});
if pages.len() == 0 {
ctx.reply(format!("No custom responses found for {target}", target=target.mention())).await?;
return Ok(())
}
drop(db);
drop(userinfo);
manifold::helpers::paginate(ctx, reply_handle, pages, 0).await?;
Ok(())
}
pub fn commands() -> [poise::Command<ManifoldData, ManifoldError>; 4] {
[add_custom_response(), remove_custom_response(), undelete_custom_response(), list_custom_responses_for_user()]
}

62
src/badgey/commands/f1.rs Normal file
View File

@ -0,0 +1,62 @@
use manifold::error::{ManifoldError, ManifoldResult};
use manifold::{ManifoldContext, ManifoldData};
use poise::serenity_prelude::CreateEmbed;
use to_markdown_table::MarkdownTable;
use crate::badgey::models::f1::{F1Client, StandingsListType};
#[poise::command(slash_command, prefix_command, subcommands("standings"))]
async fn f1(_ctx: ManifoldContext<'_>) -> ManifoldResult<()> {
Ok(())
}
#[derive(poise::ChoiceParameter)]
pub enum Championships {
Drivers,
Constructors,
}
#[poise::command(slash_command, prefix_command)]
async fn standings(ctx: ManifoldContext<'_>, championship: Championships) -> ManifoldResult<()> {
let config = &ctx.data().bot_config;
let f1_api_client = config.services.get("f1").ok_or_else(|| ManifoldError::from("No F1 API client available"))?;
let mut embed = CreateEmbed::default();
if let Some(standings) = f1_api_client.get_standings("current".to_string(), championship.to_string()).await? {
let _ = embed.title(format!("F1 {season} {championship} Championship Standings after {round} rounds:", season = standings.season, round = standings.round)).to_owned();
let mut leaderboard_headings = Vec::new();
let mut leaderboard_rows = Vec::new();
standings.standings_lists.iter().for_each(|f| {
match f {
StandingsListType::ConstructorStandings(c) => {
leaderboard_headings = vec!["Pos".to_string(), "Team".to_string(), "Pts".to_string()];
c.constructor_standings.iter().for_each(|r| {
leaderboard_rows.push(vec![r.position_text.clone(), r.constructor.name.clone(), r.points.clone()])
});
}
StandingsListType::DriversStandings(d) => {
leaderboard_headings = vec!["Pos".to_string(), "Driver".to_string(), "Team".to_string(), "Pts".to_string()];
d.driver_standings.iter().for_each(|r| {
leaderboard_rows.push(vec![r.position_text.clone(), format!("{first} {last}", first = r.driver.given_name.clone(), last = r.driver.family_name.clone()), r.constructors.first().unwrap().name.clone(), r.points.clone()])
});
}
}
let leaderboard_table = MarkdownTable::new(Some(leaderboard_headings.clone()), leaderboard_rows.clone()).unwrap();
embed.description(format!("```{table}```", table=leaderboard_table));
});
}
ctx.send(|m| {
m.embeds = vec![embed];
m
}).await?;
Ok(())
}
pub fn commands() -> [poise::Command<ManifoldData, ManifoldError>; 1] {
[f1()]
}

View File

@ -5,12 +5,14 @@ use poise::Command;
pub mod custom_responses; pub mod custom_responses;
pub mod ranks; pub mod ranks;
pub mod moderation; pub mod moderation;
pub mod f1;
pub fn collect_commands() -> Vec<Command<ManifoldData, ManifoldError>> { pub fn collect_commands() -> Vec<Command<ManifoldData, ManifoldError>> {
commands().into_iter() commands().into_iter()
.chain(custom_responses::commands()) .chain(custom_responses::commands())
.chain(ranks::commands()) .chain(ranks::commands())
.chain(moderation::commands()) .chain(moderation::commands())
.chain(f1::commands())
.collect() .collect()
} }

View File

@ -1,6 +1,8 @@
use built::chrono;
use manifold::error::{ManifoldError, ManifoldResult}; use manifold::error::{ManifoldError, ManifoldResult};
use manifold::{ManifoldContext, ManifoldData}; use manifold::{ManifoldContext, ManifoldData};
use poise::serenity_prelude as serenity; use poise::serenity_prelude as serenity;
use crate::badgey::models::quarantine_channel::Channel;
#[poise::command(slash_command, prefix_command, required_permissions = "MODERATE_MEMBERS")] #[poise::command(slash_command, prefix_command, required_permissions = "MODERATE_MEMBERS")]
async fn void_user(_ctx: ManifoldContext<'_>, _target: serenity::User) -> ManifoldResult<()> { async fn void_user(_ctx: ManifoldContext<'_>, _target: serenity::User) -> ManifoldResult<()> {
@ -22,6 +24,66 @@ async fn record_chronicle(_ctx: ManifoldContext<'_>, _target: serenity::User) ->
Ok(()) Ok(())
} }
pub fn commands() -> [poise::Command<ManifoldData, ManifoldError>; 4] { #[poise::command(slash_command, prefix_command, required_permissions = "MODERATE_MEMBERS", aliases("sr", "sl"))]
[void_user(), unvoid_user(), airlock_user(), record_chronicle()] async fn security_record(ctx: ManifoldContext<'_>) -> ManifoldResult<()> {
let author = ctx.author().id;
let timestamp = chrono::Utc::now().timestamp();
let response_body = format!("```**__Server Security Report__**\n\n**Date:** <t:{timestamp}:f>\n**Moderator(s):** <@{author}>\n**Offending User(s):**\n\n**Description:**\n\n**Actions Taken:**```", timestamp = timestamp, author = author);
ctx.reply(response_body).await?;
Ok(())
}
#[poise::command(slash_command, prefix_command, required_permissions = "MANAGE_GUILD")]
async fn add_quarantine_channel(ctx: ManifoldContext<'_>, channel: serenity::ChannelId, role: serenity::RoleId) -> ManifoldResult<()> {
let qc = Channel::new(Some(role.as_u64().clone() as i64), channel.as_u64().clone() as i64, true, false);
qc.insert(&ctx.data().database)?;
ctx.reply(format!("OK: Quarantine channel <#{channel}> defined with quarantine role <@{role}>", channel = channel, role = role)).await?;
Ok(())
}
#[poise::command(slash_command, prefix_command, required_permissions = "MANAGE_GUILD")]
async fn remove_quarantine_channel(ctx: ManifoldContext<'_>, channel: serenity::ChannelId) -> ManifoldResult<()> {
let db = &ctx.data().database;
Channel::get_by_channel_id(db, channel.as_u64().clone() as i64)?.remove(db)?;
ctx.reply(format!("OK: Quarantine channel <#{channel}> is no longer defined.", channel = channel)).await?;
Ok(())
}
#[poise::command(slash_command, prefix_command, required_permissions = "MANAGE_GUILD")]
async fn ignore_channel_for_xp(ctx: ManifoldContext<'_>, channel: serenity::ChannelId) -> ManifoldResult<()> {
let qc = Channel::new(None, channel.as_u64().clone() as i64, false, false);
qc.insert(&ctx.data().database)?;
ctx.reply(format!("OK: Channel <#{channel}> will no longer allow users to accrue XP.", channel = channel)).await?;
Ok(())
}
#[poise::command(slash_command, prefix_command, required_permissions = "MANAGE_GUILD")]
async fn unignore_channel_for_xp(ctx: ManifoldContext<'_>, channel: serenity::ChannelId) -> ManifoldResult<()> {
let db = &ctx.data().database;
Channel::get_by_channel_id(db, channel.as_u64().clone() as i64)?.remove(db)?;
ctx.reply(format!("OK: Channel <#{channel}> will now permit users to accrue XP.", channel = channel)).await?;
Ok(())
}
#[poise::command(slash_command, prefix_command, required_permissions = "ADMINISTRATOR")]
async fn send_message(ctx: ManifoldContext<'_>, channel: serenity::ChannelId, message: String) -> ManifoldResult<()> {
channel.say(&ctx, message).await?;
ctx.reply(format!("Sent message to channel <#{channel}>", channel=channel)).await?;
Ok(())
}
pub fn commands() -> [poise::Command<ManifoldData, ManifoldError>; 10] {
[void_user(), unvoid_user(), airlock_user(), record_chronicle(), security_record(), add_quarantine_channel(), remove_quarantine_channel(), ignore_channel_for_xp(), unignore_channel_for_xp(), send_message()]
} }

View File

@ -1,8 +1,9 @@
use built::chrono; use built::chrono;
use manifold::error::{ManifoldError, ManifoldResult}; use manifold::error::{ManifoldError, ManifoldResult};
use manifold::{ManifoldContext, ManifoldData}; use manifold::{ManifoldContext, ManifoldData};
use poise::serenity_prelude::{Mentionable, RoleId}; use poise::serenity_prelude::{CreateEmbed, Mentionable, RoleId};
use crate::badgey::models::xp::{Rank, Track, Xp}; use to_markdown_table::MarkdownTable;
use crate::badgey::models::xp::{Leaderboard, Rank, Track, Xp};
#[poise::command(prefix_command, slash_command, user_cooldown = 86400)] #[poise::command(prefix_command, slash_command, user_cooldown = 86400)]
async fn switch_rank_track(ctx: ManifoldContext<'_>, #[description = "Track to switch to: officer or enlisted"] track: String) -> ManifoldResult<()> { async fn switch_rank_track(ctx: ManifoldContext<'_>, #[description = "Track to switch to: officer or enlisted"] track: String) -> ManifoldResult<()> {
@ -119,6 +120,57 @@ async fn rank(ctx: ManifoldContext<'_>) -> ManifoldResult<()> {
Ok(()) Ok(())
} }
pub fn commands() -> [poise::Command<ManifoldData, ManifoldError>; 3] { #[poise::command(prefix_command, slash_command)]
[switch_rank_track(), freeze_rank(), rank()] async fn leaderboard(ctx: ManifoldContext<'_>) -> ManifoldResult<()> {
let reply_handle = ctx.reply("Retrieving leaderboard, please stand by...".to_string()).await?;
let entries_per_page = 20;
let mut pages = Vec::<CreateEmbed>::new();
let leaderboard = Leaderboard::get_leaderboard(&ctx.data().database)?;
let userinfo = ctx.data().user_info.lock().await;
let total = leaderboard.rows.len();
let pages_needed = (total / entries_per_page) + 1;
for i in 0..pages_needed {
let mut page = CreateEmbed::default()
.title(format!("XP Leaderboard Page {page} of {total_pages}", page=(i + 1), total_pages=&pages_needed)).to_owned();
let offset = i*entries_per_page;
let mut leaderboard_rows = Vec::new();
leaderboard.rows.iter().skip(offset).enumerate().for_each(|(j, f)| {
// cap at per-page limit
if j >= entries_per_page {
return;
}
let mut row = f.to_owned();
row.rank = (j + 1 + offset) as i32;
row.user_name = userinfo.get(&(row.uid as u64)).unwrap().username.clone();
leaderboard_rows.push(row);
});
let leaderboard_table = MarkdownTable::new(Some(vec!["Rank".to_string(), "User".to_string(), "XP".to_string()]), leaderboard_rows)?;
page.description(format!("```{table}```", table=leaderboard_table));
pages.push(page);
}
if pages.len() == 0 {
ctx.reply("No leaderboard entries yet!".to_string()).await?;
return Ok(())
}
// Release our mutex lock on this object before the paginate call to avoid holding a lock on it for the full paginate interaction listener timeout duration
drop(userinfo);
manifold::helpers::paginate(ctx, reply_handle, pages, 0).await?;
Ok(())
}
pub fn commands() -> [poise::Command<ManifoldData, ManifoldError>; 4] {
[switch_rank_track(), freeze_rank(), rank(), leaderboard()]
} }

View File

@ -1,15 +1,12 @@
use built::chrono;
use manifold::error::{ManifoldError, ManifoldResult}; use manifold::error::{ManifoldError, ManifoldResult};
use manifold::events::EventHandler; use manifold::events::EventHandler;
use manifold::ManifoldData; use manifold::ManifoldData;
use poise::{async_trait, FrameworkContext, Event}; use poise::{async_trait, FrameworkContext, Event};
use poise::serenity_prelude::{Context, Mentionable, Message, RoleId}; use poise::serenity_prelude::{Context, Member, Message};
use rand::rngs::SmallRng; use crate::badgey::handlers;
use rand::{Rng, SeedableRng};
use crate::badgey::models::custom_response::CustomResponse; use crate::badgey::models::custom_response::CustomResponse;
use crate::badgey::models::xp::{Rank, Xp}; use crate::badgey::models::xp::Xp;
pub struct BadgeyHandler { pub struct BadgeyHandler {
@ -19,6 +16,7 @@ pub struct BadgeyHandler {
impl EventHandler for BadgeyHandler { impl EventHandler for BadgeyHandler {
async fn listen(ctx: &Context, framework_ctx: FrameworkContext<'_, ManifoldData, ManifoldError>, event: &Event<'_>) -> ManifoldResult<()> { async fn listen(ctx: &Context, framework_ctx: FrameworkContext<'_, ManifoldData, ManifoldError>, event: &Event<'_>) -> ManifoldResult<()> {
match event { match event {
Event::GuildMemberUpdate { old_if_available, new } => BadgeyHandler::guild_member_update(&ctx, &framework_ctx, old_if_available, new).await,
Event::Message { new_message } => BadgeyHandler::message(&ctx, &framework_ctx, new_message).await, Event::Message { new_message } => BadgeyHandler::message(&ctx, &framework_ctx, new_message).await,
_ => Ok(()) _ => Ok(())
} }
@ -26,73 +24,27 @@ impl EventHandler for BadgeyHandler {
} }
impl BadgeyHandler { impl BadgeyHandler {
pub async fn guild_member_update (ctx: &Context, fctx: &FrameworkContext<'_, ManifoldData, ManifoldError>, old_if_available: &Option<Member>, new: &Member) -> ManifoldResult<()> {
handlers::airlock::airlock_handler(ctx, fctx, old_if_available, new).await?;
Ok(())
}
pub async fn message(ctx: &Context, fctx: &FrameworkContext<'_, ManifoldData, ManifoldError>, msg: &Message) -> ManifoldResult<()> { pub async fn message(ctx: &Context, fctx: &FrameworkContext<'_, ManifoldData, ManifoldError>, msg: &Message) -> ManifoldResult<()> {
// Ignore our own messages
if msg.author.id == ctx.http.get_current_user().await?.id || msg.content.starts_with(&fctx.user_data().await.bot_config.prefix) { if msg.author.id == ctx.http.get_current_user().await?.id || msg.content.starts_with(&fctx.user_data().await.bot_config.prefix) {
return Ok(()) return Ok(())
} }
let db = &fctx.user_data().await.database; let db = &fctx.user_data().await.database;
if let Ok(r) = CustomResponse::test_content(db, &msg.content_safe(&ctx.cache), &"!".to_string()) { if let Ok(r) = CustomResponse::test_content(db, &msg.content_safe(&ctx.cache), &"!".to_string()) {
msg.channel_id.say(ctx, r.response).await?; msg.channel_id.say(ctx, r.response).await?;
} }
if fctx.user_data().await.user_info.lock().await.get_mut(&msg.author.id.as_u64()).is_none() { Xp::award(ctx, fctx, msg, &db).await?;
debug!("Tried to add XP to a user we don't know about, aborting.");
return Ok(())
}
let mut rng = SmallRng::from_entropy();
let xp_reward = rng.gen_range(1..30).clone();
drop(rng);
let user_id_i64 = msg.author.id.as_u64().clone() as i64;
let mut xp = match Xp::get(db, &user_id_i64) {
Ok(x) => x,
Err(_) => Xp::new(&user_id_i64)
};
let valid = match xp.last_given_xp {
Some(t) => {
if (chrono::Utc::now().timestamp() - 60) > t {
true
} else {
false
}
},
None => true
};
if valid {
xp.last_given_xp = Some(chrono::Utc::now().timestamp());
xp.xp_value += &xp_reward;
let calculated_level = xp.get_level_from_xp(&db, None);
if xp.freeze_rank.is_some() {
xp.user_current_level = calculated_level.clone();
xp.insert(db)?;
return Ok(())
}
if (xp.user_current_level != calculated_level) || xp.user_current_level == 1 {
if let Some(guild) = msg.guild(&ctx.cache) {
let mut member = guild.member(&ctx, msg.author.id).await?;
let old_role_id = RoleId::from(Rank::get_rank_for_level(db, &xp.user_current_level, &xp.rank_track).unwrap_or(Rank::new()).role_id as u64);
xp.user_current_level = calculated_level;
let new_role = RoleId::from(Rank::get_rank_for_level(db, &calculated_level, &xp.rank_track)?.role_id as u64);
if (new_role != old_role_id || xp.user_current_level == 1) && msg.author.has_role(ctx, guild.id, new_role).await? == false {
member.remove_role(ctx, old_role_id).await?;
member.add_role(ctx, new_role).await?;
msg.channel_id.say(ctx, format!("{} is now a {}", msg.author.mention(), new_role.mention())).await?;
}
} else {
error!("Needed to add level to user, but couldn't get the member from the guild!");
}
}
xp.insert(db)?;
}
Ok(()) Ok(())
} }

View File

@ -0,0 +1,36 @@
use manifold::error::{ManifoldError, ManifoldResult};
use manifold::ManifoldData;
use poise::FrameworkContext;
use poise::serenity_prelude::{ChannelId, Context, Member, Mentionable};
use crate::badgey::models::quarantine_channel::Channel;
pub async fn airlock_handler(ctx: &Context, fctx: &FrameworkContext<'_, ManifoldData, ManifoldError>, old: &Option<Member>, new: &Member) -> ManifoldResult<()> {
let db = &fctx.user_data.database;
// Is this user quarantined?
for role in new.roles.iter() {
if let Ok(channel) = Channel::get_by_qc_role_id(db, role.as_u64().clone() as i64) {
// Was this user JUST added to a quarantine channel?
if let Some(m) = old {
for role in m.roles.iter() {
if let Ok(_) = Channel::get_by_qc_role_id(db, role.as_u64().clone() as i64) {
// User already had quarantine role, return early
return Ok(())
}
}
let target_channel: ChannelId = ChannelId::from(channel.channel_id as u64);
// User was just added to a quarantine channel. Post the initial ping message.
target_channel.say(ctx, format!("{mention}, please respond to this message", mention = new.mention())).await?;
target_channel.await_reply(ctx).author_id(new.user.id).await;
target_channel.say(ctx, format!("{mention}, you have been brought to a private area by a server staff member. This does not necessarily mean you have broken the server rules; just that a staff member wishes to discuss something with you in a private and recorded environemnt.\n\nPlease note that the message history of this channel is not visible to you, and if your discord client loses focus on this channel it may then appear empty to you when you return. All message sent will remain visible to staff.\n\nPlease wait - a staff member should arrive shortly to explain why you are here.", mention = new.mention())).await?;
return Ok(())
}
}
}
Ok(())
}

View File

@ -0,0 +1 @@
pub mod airlock;

View File

@ -9,6 +9,7 @@ mod commands;
mod events; mod events;
mod models; mod models;
mod schema; mod schema;
mod handlers;
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations"); pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");

230
src/badgey/models/f1.rs Normal file
View File

@ -0,0 +1,230 @@
use manifold::error::ManifoldResult;
use manifold::models::fueltank::FuelTank;
use reqwest::header::CONTENT_TYPE;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct CircuitLocation {
pub lat: String,
pub long: String,
pub locality: String,
pub country: String,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Circuit {
pub circuit_id: String,
pub url: String,
pub circuit_name: String,
#[serde(alias = "Location")]
pub location: CircuitLocation,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct RaceSession {
pub date: Option<String>,
pub time: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Race {
pub season: String,
pub round: String,
pub url: String,
pub race_name: String,
#[serde(alias = "Circuit")]
pub circuit: Circuit,
pub date: String,
pub time: Option<String>,
pub first_practice: Option<RaceSession>,
pub second_practice: Option<RaceSession>,
pub third_practice: Option<RaceSession>,
pub qualifying: Option<RaceSession>,
pub sprint: Option<RaceSession>,
pub sprint_qualifying: Option<RaceSession>,
pub sprint_shootout: Option<RaceSession>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct PitStop {
pub driver_id: String,
pub lap: Option<String>,
pub stop: Option<String>,
pub time: Option<String>,
pub duration: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Qualifying {
pub number: String,
pub position: Option<String>,
pub driver: Driver,
pub constructor: Constructor,
pub q1: Option<String>,
pub q2: Option<String>,
pub q3: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Timing {
pub driver_id: String,
pub position: String,
pub time: String,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Lap {
pub number: String,
pub timings: Vec<Timing>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct RaceResult {
pub number: String,
pub position: String,
pub position_text: String,
pub points: String,
pub driver: Driver,
pub constructor: Option<Constructor>,
pub grid: Option<String>,
pub laps: Option<String>,
pub status: Option<String>,
pub fastest_lap: Option<Lap>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Constructor {
pub constructor_id: Option<String>,
pub url: Option<String>,
pub name: String,
pub nationality: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ConstructorStandings {
pub position: Option<String>,
pub position_text: String,
pub points: String,
pub wins: String,
#[serde(alias = "Constructor")]
pub constructor: Constructor,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Driver {
pub driver_id: String,
pub permanent_number: Option<String>,
pub code: Option<String>,
pub url: Option<String>,
pub given_name: String,
pub family_name: String,
pub date_of_birth: Option<String>,
pub nationality: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct DriverStandings {
pub position: Option<String>,
pub position_text: String,
pub points: String,
pub wins: String,
#[serde(alias = "Driver")]
pub driver: Driver,
#[serde(alias = "Constructors")]
pub constructors: Vec<Constructor>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct DriverStandingsList {
pub season: String,
pub round: String,
#[serde(alias = "DriverStandings")]
pub driver_standings: Vec<DriverStandings>
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ConstructorStandingsList {
pub season: String,
pub round: String,
#[serde(alias = "ConstructorStandings")]
pub constructor_standings: Vec<ConstructorStandings>
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(untagged, rename_all_fields = "camelCase")]
pub enum StandingsListType {
ConstructorStandings(ConstructorStandingsList),
DriversStandings(DriverStandingsList)
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct StandingsTable {
pub season: String,
pub round: String,
#[serde(alias = "StandingsLists")]
pub standings_lists: Vec<StandingsListType>
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Season {
pub season: String,
pub url: String,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct F1APIResponse {
#[serde(alias = "MRData")]
mrdata: MRData,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct MRData {
pub series: String,
pub xmlns: String,
pub url: String,
pub limit: String,
pub offset: String,
pub total: String,
#[serde(alias = "StandingsTable")]
pub standings_table: Option<StandingsTable>
}
pub type F1 = FuelTank;
pub trait F1Client {
async fn get_standings(&self, season: String, championship: String) -> ManifoldResult<Option<StandingsTable>>;
}
impl F1Client for F1 {
async fn get_standings(&self, season: String, championship: String) -> ManifoldResult<Option<StandingsTable>> {
let base_url = format!("{base}/{season}/{championship}tandings/", base = self.source_uri, season = season, championship = championship.to_lowercase());
let client = reqwest::Client::new();
let response = client.get(base_url)
.header(CONTENT_TYPE, "application/json")
.send()
.await?
.text()
.await?;
let decoded_result: F1APIResponse = serde_json::from_str(&*response)?;
Ok(decoded_result.mrdata.standings_table)
}
}

View File

@ -1,2 +1,4 @@
pub mod custom_response; pub mod custom_response;
pub mod xp; pub mod xp;
pub mod quarantine_channel;
pub mod f1;

View File

@ -0,0 +1,59 @@
use diesel::{insert_into, Identifiable, Insertable, QueryDsl, Queryable, RunQueryDsl, Selectable};
use diesel::dsl::delete;
use diesel::prelude::*;
use manifold::Db;
use manifold::error::{ManifoldError, ManifoldResult};
use crate::badgey::schema::channels as channel_table;
use crate::badgey::schema::*;
#[derive(Queryable, Selectable, Identifiable, Insertable, Debug, Clone)]
#[diesel(table_name = channels)]
pub struct Channel {
#[diesel(deserialize_as = i32)]
pub id: Option<i32>,
pub qc_role_id: Option<i64>,
pub channel_id: i64,
pub is_quarantine_channel: bool,
pub is_valid_for_xp: bool,
}
impl Channel {
pub fn new(qc_role_id: Option<i64>, channel_id: i64, is_quarantine_channel: bool, is_valid_for_xp: bool) -> Self {
Channel {
id: None,
qc_role_id,
channel_id,
is_quarantine_channel,
is_valid_for_xp,
}
}
pub fn insert(&self, conn: &Db) -> ManifoldResult<usize> {
insert_into(channel_table::dsl::channels)
.values(self)
.execute(&mut conn.get()?)
.map_err(|e| ManifoldError::from(e))
}
pub fn remove(&self, conn: &Db) -> ManifoldResult<usize> {
Ok(delete(channel_table::dsl::channels)
.filter(channel_table::qc_role_id.eq(self.qc_role_id))
.execute(&mut conn.get()?)?)
}
pub fn get_by_channel_id(conn: &Db, needle: i64) -> ManifoldResult<Self> {
Ok(channel_table::dsl::channels
.filter(channel_table::channel_id.eq(needle))
.limit(1)
.select(Channel::as_select())
.get_result(&mut conn.get()?)?)
}
pub fn get_by_qc_role_id(conn: &Db, needle: i64) -> ManifoldResult<Self> {
Ok(channel_table::dsl::channels
.filter(channel_table::qc_role_id.eq(needle))
.limit(1)
.select(Channel::as_select())
.get_result(&mut conn.get()?)?)
}
}

View File

@ -1,11 +1,17 @@
use built::chrono; use built::chrono;
use diesel::prelude::*; use diesel::prelude::*;
use diesel::insert_into; use diesel::insert_into;
use manifold::Db; use rand::rngs::SmallRng;
use rand::{Rng, SeedableRng};
use to_markdown_table::{TableRow};
use manifold::{Db, ManifoldData};
use manifold::error::{ManifoldError, ManifoldResult}; use manifold::error::{ManifoldError, ManifoldResult};
use manifold::models::user::UserInfo; use manifold::models::user::UserInfo;
use manifold::schema::userinfo; use manifold::schema::userinfo;
use poise::FrameworkContext;
use poise::serenity_prelude::{Context, Mentionable, Message, RoleId};
use crate::badgey::models::quarantine_channel::Channel;
use crate::badgey::schema::xp as xp_table; use crate::badgey::schema::xp as xp_table;
use crate::badgey::schema::*; use crate::badgey::schema::*;
@ -22,7 +28,7 @@ pub struct Xp {
pub rank_track: i64, pub rank_track: i64,
pub rank_track_last_changed: Option<i64>, pub rank_track_last_changed: Option<i64>,
pub freeze_rank: Option<i64>, pub freeze_rank: Option<i64>,
pub freeze_rank_last_changed: Option<i64> pub freeze_rank_last_changed: Option<i64>,
} }
#[derive(Queryable, Selectable, Identifiable, Debug, Clone)] #[derive(Queryable, Selectable, Identifiable, Debug, Clone)]
@ -44,6 +50,47 @@ pub struct Track {
pub xp_award_range_max: i32, pub xp_award_range_max: i32,
} }
pub struct Leaderboard {
pub rows: Vec<LeaderboardRow>
}
#[derive(Clone)]
pub struct LeaderboardRow {
pub rank: i32,
pub uid: i64,
pub user_name: String,
pub value: i64,
}
impl Into<TableRow> for LeaderboardRow {
fn into(self) -> TableRow {
TableRow::new(vec![self.rank.to_string(), self.user_name, self.value.to_string()])
}
}
impl From<&Xp> for LeaderboardRow {
fn from(value: &Xp) -> Self {
LeaderboardRow {
rank: 0,
uid: value.user_id,
user_name: "".to_string(),
value: value.xp_value,
}
}
}
impl Leaderboard {
pub fn get_leaderboard(conn: &Db) -> ManifoldResult<Self> {
let xp_rows: Vec<LeaderboardRow> = xp_table::dsl::xp
.order_by(xp::xp_value.desc())
.load::<Xp>(&mut conn.get()?)?.iter().map(|x| {LeaderboardRow::from(x)}).collect();
Ok(Leaderboard {
rows: xp_rows
})
}
}
impl Xp { impl Xp {
pub fn new(user_id: &i64) -> Self { pub fn new(user_id: &i64) -> Self {
Self { Self {
@ -113,6 +160,70 @@ impl Xp {
Ok(()) Ok(())
} }
pub async fn award(ctx: &Context, fctx: &FrameworkContext<'_, ManifoldData, ManifoldError>, msg: &Message, db: &Db) -> ManifoldResult<()>{
if fctx.user_data().await.user_info.lock().await.get_mut(&msg.author.id.as_u64()).is_none() {
debug!("Tried to add XP to a user we don't know about, aborting.");
return Ok(())
}
let mut rng = SmallRng::from_entropy();
let xp_reward = rng.gen_range(1..30).clone();
drop(rng);
let user_id_i64 = msg.author.id.as_u64().clone() as i64;
let channel_id_i64 = msg.channel_id.as_u64().clone() as i64;
let mut valid_channel = true;
if let Ok(c) = Channel::get_by_channel_id(&fctx.user_data.database, channel_id_i64) {
valid_channel = c.is_valid_for_xp && !c.is_quarantine_channel;
}
let mut xp = match Xp::get(db, &user_id_i64) {
Ok(x) => x,
Err(_) => Xp::new(&user_id_i64)
};
let valid_user = match xp.last_given_xp {
Some(t) => (chrono::Utc::now().timestamp() - 60) > t,
None => true
};
if valid_user && valid_channel {
xp.last_given_xp = Some(chrono::Utc::now().timestamp());
xp.xp_value += &xp_reward;
let calculated_level = xp.get_level_from_xp(&db, None);
if xp.freeze_rank.is_some() {
xp.user_current_level = calculated_level.clone();
xp.insert(db)?;
return Ok(())
}
if (xp.user_current_level != calculated_level) || xp.user_current_level == 1 {
if let Some(guild) = msg.guild(&ctx.cache) {
let mut member = guild.member(&ctx, msg.author.id).await?;
let old_role_id = RoleId::from(Rank::get_rank_for_level(db, &xp.user_current_level, &xp.rank_track).unwrap_or(Rank::new()).role_id as u64);
xp.user_current_level = calculated_level;
let new_role = RoleId::from(Rank::get_rank_for_level(db, &calculated_level, &xp.rank_track)?.role_id as u64);
if (new_role != old_role_id || xp.user_current_level == 1) && msg.author.has_role(ctx, guild.id, new_role).await? == false {
member.remove_role(ctx, old_role_id).await?;
member.add_role(ctx, new_role).await?;
msg.channel_id.say(ctx, format!("{} is now a {}", msg.author.mention(), new_role.mention())).await?;
}
} else {
error!("Needed to add level to user, but couldn't get the member from the guild!");
}
}
xp.insert(db)?;
}
Ok(())
}
} }
impl Rank { impl Rank {

View File

@ -1,5 +1,15 @@
// @generated automatically by Diesel CLI. // @generated automatically by Diesel CLI.
diesel::table! {
channels (id) {
id -> Int4,
qc_role_id -> Nullable<Int8>,
channel_id -> Int8,
is_quarantine_channel -> Bool,
is_valid_for_xp -> Bool,
}
}
diesel::table! { diesel::table! {
custom_responses (id) { custom_responses (id) {
id -> Int4, id -> Int4,
@ -64,6 +74,7 @@ diesel::joinable!(custom_responses -> userinfo (added_for));
diesel::joinable!(xp -> userinfo (user_id)); diesel::joinable!(xp -> userinfo (user_id));
diesel::allow_tables_to_appear_in_same_query!( diesel::allow_tables_to_appear_in_same_query!(
channels,
custom_responses, custom_responses,
ranks, ranks,
tracks, tracks,