Compare commits

..

82 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
Xyon 22771a36c3
Merge remote-tracking branch 'origin/main' into feature/string-translation 2024-10-08 00:13:46 +01:00
Xyon d53b613044
Retaining badgey user for simplicity 2024-10-08 00:08:44 +01:00
Xyon 9264ebd5de
Bump manifold version 2024-10-08 00:07:19 +01:00
Xyon 242a66f353
Complete string translation, add multihoming for M5 2024-10-08 00:04:27 +01:00
Xyon 6d8af9b60a Merge pull request 'feature/string-translation' (#4) from feature/string-translation into main
Badgey Deployment / build (push) Has been cancelled Details
Reviewed-on: #4
2024-08-28 18:35:06 +00:00
Joril a34b4b14a2 Update config/locales/badgey.yml 2024-08-28 18:17:40 +00:00
Joril 2af64fca40 Update txt/responses.de.txt 2024-08-28 18:10:42 +00:00
Xyon 2b276647fa
Suppress dead_code warnings on unimplemented commands 2024-08-27 01:11:27 +01:00
Xyon 0d386f095d
Undo dev switch to local manifold 2024-08-27 01:06:46 +01:00
Xyon e63177a5b4
Bump major ver 2024-08-26 20:03:48 +01:00
Xyon 68731984c1
Add i18n effort to permit bot strings to be translated 2024-08-26 19:57:34 +01:00
Xyon 9d1c124ba2
Add existing changes from worktree with build fix 2024-08-26 13:46:05 +01:00
Xyon 72cfe7b17b
Nixos nonsense 2024-01-15 07:59:53 +00:00
Xyon 71e0f92708
Bump dependencies for badgey - pull in manifold hotfix
Badgey Deployment / build (push) Successful in 6m8s Details
2023-11-20 10:21:20 +00:00
Xyon a9feba9845
Move XP generation constant values to database and increase apparent level comparison value
Badgey Deployment / build (push) Successful in 6m11s Details
2023-11-20 09:45:45 +00:00
Xyon cd391d7475
Reply with a random matching response if multiple responses match the trigger
Badgey Deployment / build (push) Successful in 5m55s Details
2023-09-27 13:20:08 +01:00
Xyon 5c0c895e93
Add the migrations for custom responses, d'oh
Badgey Deployment / build (push) Successful in 5m59s Details
2023-09-27 11:22:22 +01:00
Xyon a07f721a80 feature/custom-responses (#2)
Badgey Deployment / build (push) Successful in 6m8s Details
Reviewed-on: #2
Co-authored-by: Xyon <xyon@orbiter-radio.uk>
Co-committed-by: Xyon <xyon@orbiter-radio.uk>
2023-09-27 09:39:55 +00:00
41 changed files with 2476 additions and 724 deletions

1
.envrc Normal file
View File

@ -0,0 +1 @@
use nix

View File

@ -15,19 +15,46 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Pre-seed known_hosts
run: mkdir -pv ~/.ssh && ssh-keyscan -t rsa badgey >> ~/.ssh/known_hosts
- name: Build (Release)
run: cargo build --release --color=always
- name: Archive artifact
uses: actions/upload-artifact@v3
with:
name: badgey
path: target/release/badgey
deploy:
runs-on: rust
container:
options: --dns 172.16.255.254
strategy:
matrix:
bot: [ BADGEY, M5_COMPUTER ]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Download artifact
uses: actions/download-artifact@v3
with:
name: badgey
- name: Pre-seed known_hosts
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:
files: config/production.badgey.json
env:
BOT_NICKNAME: ${{ vars[format('{0}_BOT_NICKNAME', 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_USER: ${{ vars.POSTGRES_USER }}
POSTGRES_DATABASE_NAME: ${{ vars.POSTGRES_DATABASE_NAME }}
POSTGRES_DATABASE_NAME: ${{ vars[format('{0}_POSTGRES_DATABASE_NAME', matrix.bot)] }}
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
WEATHER_API_KEY: ${{ secrets.WEATHER_API_KEY }}
DOGPICS_API_KEY: ${{ secrets.DOGPICS_API_KEY }}
CATPICS_API_KEY: ${{ secrets.CATPICS_API_KEY }}
- name: Seed SSH key for deploy
run: echo "${{ secrets.DEPLOY_KEY }}" | tr -d '\r' > ~/.ssh/id_rsa && chmod 0600 ~/.ssh/id_rsa
- name: Deploy
run: bash cicd/deploy.sh
run: bash cicd/deploy.sh ${{ vars[format('{0}_SERVER_HOSTNAME', matrix.bot)] }}

28
.idea/remote-targets.xml Normal file
View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteTargetsManager">
<targets>
<target name="rust:latest" type="docker" uuid="abc6565f-f912-434a-ae04-68ceee732c3a">
<config>
<option name="targetPlatform">
<TargetPlatform />
</option>
<option name="buildNotPull" value="false" />
<option name="pullImageConfig">
<PullImageConfig>
<option name="tagToPull" value="rust:latest" />
</PullImageConfig>
</option>
</config>
<ContributedStateBase type="RsLanguageRuntime">
<config>
<option name="cargoPath" value="/usr/local/cargo/bin/cargo" />
<option name="cargoVersion" value="1.75.0 (1d8b05cdd 2023-11-20)" />
<option name="rustcPath" value="/usr/local/cargo/bin/rustc" />
<option name="rustcVersion" value="1.75.0 (82e1608df 2023-12-21)" />
</config>
</ContributedStateBase>
</target>
</targets>
</component>
</project>

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/migrations/2023-09-20-230922_extend userinfo to include XP/up.sql" dialect="PostgreSQL" />
<file url="file://$PROJECT_DIR$/migrations/2023-09-26-215927_add custom responses table/up.sql" dialect="PostgreSQL" />
<file url="PROJECT" dialect="SQLite" />
</component>
</project>

1866
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package]
name = "badgey"
version = "3.1.0"
version = "4.2.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@ -20,4 +20,22 @@ manifold = { git = "https://code.orbiter-radio.uk/discord/manifold.git" }
poise = { version = "0.5.*", features = [ "cache" ] }
rand = { version = "0.8.5", features = [ "small_rng" ] }
regex = "1.9.5"
rust-i18n = "3.1.2"
to_markdown_table = "0.1.5"
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]
# The available locales for your application, default: ["en"].
available-locales = ["en", "de"]
# The default locale, default: "en".
default-locale = "en"
# Path for your translations YAML file, default: "locales".
# This config for let `cargo i18n` command line tool know where to find your translations.
# You must keep this path same as the one you pass to method `rust_i18n::i18n!`.
load-path = "config/locales"

View File

@ -1,7 +1,12 @@
#!/bin/bash
ssh badgey@badgey sudo /usr/bin/systemctl stop badgey
rsync -avP target/release/badgey badgey@badgey:/srv/badgey/
rsync -avP config badgey@badgey:/srv/badgey/
rsync -avP txt badgey@badgey:/srv/badgey/
ssh badgey@badgey sudo /usr/bin/systemctl start badgey
bot=$(echo "$1" | tr '[:upper:]' '[:lower:]')
echo "Running deploy for bot ${bot}"
ssh badgey@$bot sudo /usr/bin/systemctl stop $bot
rsync -avP badgey badgey@$bot:/srv/$bot/$bot
rsync -avP config 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

View File

@ -11,12 +11,12 @@
"channels": {
"log": 648260641626390528
},
"responses_file_path": "txt/responses.txt",
"responses_file_path": "txt/responses",
"services": {
"weather": {
"source_uri": "https://api.weatherapi.com/v1",
"cache_mode": "NoCache",
"api_key": "70aacb9af931438a957215406211210"
"api_key": ""
},
"frog_tips": {
"source_uri": "https://frog.tips/api/1/tips/",

69
config/locales/badgey.yml Normal file
View File

@ -0,0 +1,69 @@
_version: 2
commands:
custom_response:
added:
de: "Benutzerdefinierte Antwort erfolgreich hinzugefügt"
en: "Custom response successfully added"
error:
de: "Benutzerdefinierte Antwort NICHT hinzugefügt, ein Fehler ist aufgetreten. Darüber sollten Sie sich wahrscheinlich bei Xyon beschweren. Der Fehler war %{error}"
en: "Custom response NOT added, an error happened. You should probably complain to Xyon about that. The error was %{error}"
marked_deleted:
de: "Benutzerdefinierte Antwort mit der ID %{id} erfolgreich als gelöscht markiert."
en: "Successfully marked custom response with ID %{response_id} as deleted."
marked_undeleted:
de: "Nicht gelöschte benutzerdefinierte Antwort mit der ID: %{response_id}"
en: "Undeleted custom response with ID: %{response_id}"
undelete_error_not_deleted:
de: "Die benutzerdefinierte Antwort mit der ID %{response_id} kann nicht wiederhergestellt werden, da sie nicht von vornherein gelöscht wurde."
en: "Can't undelete custom response with ID %{response_id} because it was not deleted in the first place."
list_user:
de: "Ich habe die folgenden benutzerdefinierten Antworten für %{user} gefunden: %{response_list}"
en: "I found the following custom responses for %{user}: %{response_list}"
ranks:
track_switch:
de: "Ich habe dich auf die %{new_track}-Leiter umgestellt. Du bist jetzt ein %{new_rank}. Ich hoffe, das hat sich gelohnt."
en: "I have switched you onto the %{new_track} rank track. You're now a %{new_rank}. I hope this was worth it."
failure:
de: "Sie befinden sich bereits auf der %(rank_track)-Leiter."
en: "You're already on the %(rank_track) rank track."
rank_freeze:
not_found:
de: "Diesen Rang konnte ich nicht finden um ihn dir zu setzen. Wahrscheinlich musst Du die Karriere-Leiter wechseln, oder es existiert einfach nicht."
en: "I couldn't find that rank as a rank I can give you. You might need to switch tracks, or it might just not exist."
success:
de: "Du bist nun für immer %{rank}. Viel Glück Harry!"
en: "Okay. I've frozen your rank at %{rank}. Enjoy!"
rank_unfreeze:
success:
de: "Dein Rang wurde enteist. Janeway wird das nicht erfreuen."
en: "Okay. I've unfrozen your rank. Good luck out there."
failure:
de: "Dein Rang war nicht eingefroren."
en: "Negative, your rank wasn't frozen."
rank:
current_rank:
de: "Du bist derzeit %{current_rank}. Du brauchst %{next_level_xp} mehr XP, um das nächste Level zu erreichen"
en: "You're currently %{current_rank}. You need %{next_level_xp} more XP to get to the next level"
max_rank:
de: "%{response}. Du bist bereits ganz oben angekommen! - Du kannst nicht weiter befördert werden!"
en: "%{response}. There are no more ranks for you - you can't be promoted any further!"
and_rank:
de: "%{response} und Rang!"
en: "%{response} and rank!"
and_rank_different_level:
de: "%{response} und du brauchst %{next_rank_xp} mehr, um den nächsten Rang zu erreichen. Viel Glück!"
en: "%{response} and you need %{next_rank_xp} more to get to the next rank. Good luck!"
models:
xp:
cooldown:
de: "Du hast eine Abklingzeit. Versuchen Sie es in %{timeuntil} Sekunden erneut."
en: "You are on cooldown. Try again in %{timeuntil} seconds."
unranked:
de: ohne Rangliste
en: unranked
misc:
version_string:
de: "Badgey Bot Version %{ver} erstellt um %{time} aus der Revision %{rev}"
en: "Badgey Bot version %{ver} built at %{time} from revision %{rev}"

View File

@ -1,8 +1,8 @@
{
"prefix": ".",
"nickname": "Badgey",
"nickname": "#{BOT_NICKNAME}#",
"channels": {
"log": 1143479696886087801
"log": "#{LOG_CHANNEL_ID}#"
},
"database": {
"host": "#{POSTGRES_HOST}#",
@ -11,12 +11,12 @@
"database_name": "#{POSTGRES_DATABASE_NAME}#",
"port": 5432
},
"responses_file_path": "txt/responses.txt",
"responses_file_path": "txt/responses.#{BOT_IDENTIFIER}#",
"services": {
"weather": {
"source_uri": "https://api.weatherapi.com/v1",
"cache_mode": "NoCache",
"api_key": "70aacb9af931438a957215406211210"
"api_key": "#{WEATHER_API_KEY}#"
},
"frog_tips": {
"source_uri": "https://frog.tips/api/1/tips/",
@ -27,13 +27,13 @@
"source_uri": "https://api.thedogapi.com/v1/images/search?limit=100&order=RAND",
"cache_name": "dog_pics",
"cache_mode": "Cache",
"api_key": "live_RRrRUsdmRIpKUefuwOwuAV1nab7Gt8GqyvIqPGCgLAbpLHGdyStbGj9Xc67inYMt"
"api_key": "#{DOGPICS_API_KEY}#"
},
"cat_pics": {
"source_uri": "https://api.thecatapi.com/v1/images/search?limit=100&order=RAND",
"cache_name": "cat_pics",
"cache_mode": "Cache",
"api_key": "live_nvupPQbrXjHy8jsZJ1stp72fzsRLR8jQby8IR3l9yMngqAU9gcTEV8RA0OOiK8zP"
"api_key": "#{CATPICS_API_KEY}#"
},
"dad_jokes": {
"source_uri": "https://icanhazdadjoke.com/search?limit=30",
@ -44,6 +44,10 @@
"source_uri": "https://api.nasa.gov/planetary/apod?api_key=NZfKclpoaO9HnvfvaCjeJ3csDecvIqNiABVw2YvN",
"cache_name": "nasa_apod",
"cache_mode": "NoCache"
},
"f1": {
"source_uri": "https://api.jolpi.ca/ergast/f1",
"cache_mode": "NoCache"
}
}
}

View File

View File

@ -0,0 +1,6 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.
DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
DROP FUNCTION IF EXISTS diesel_set_updated_at();

View File

@ -0,0 +1,36 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.
-- Sets up a trigger for the given table to automatically set a column called
-- `updated_at` whenever the row is modified (unless `updated_at` was included
-- in the modified columns)
--
-- # Example
--
-- ```sql
-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW());
--
-- SELECT diesel_manage_updated_at('users');
-- ```
CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$
BEGIN
EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$
BEGIN
IF (
NEW IS DISTINCT FROM OLD AND
NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
) THEN
NEW.updated_at := current_timestamp;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

View File

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE IF EXISTS "custom_responses";

View File

@ -0,0 +1,10 @@
-- Your SQL goes here
CREATE TABLE IF NOT EXISTS "custom_responses" (
id SERIAL PRIMARY KEY,
trigger TEXT NOT NULL,
response TEXT NOT NULL,
added_by BIGINT NOT NULL,
added_on BIGINT NOT NULL,
added_for BIGINT NOT NULL REFERENCES userinfo(user_id),
deleted BOOLEAN
);

View File

@ -0,0 +1,4 @@
ALTER TABLE tracks
DROP COLUMN IF EXISTS xp_level_constant,
DROP COLUMN IF EXISTS xp_award_range_min,
DROP COLUMN IF EXISTS xp_award_range_max;

View File

@ -0,0 +1,4 @@
ALTER TABLE tracks
ADD COLUMN xp_level_constant FLOAT NOT NULL DEFAULT 0.25,
ADD COLUMN xp_award_range_min INT NOT NULL DEFAULT 1,
ADD COLUMN xp_award_range_max INT NOT NULL DEFAULT 30;

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";

9
shell.nix Normal file
View File

@ -0,0 +1,9 @@
{ pkgs ? import <nixpkgs> {}}:
pkgs.mkShell {
buildInputs = with pkgs; [
openssl
postgresql
pkg-config
diesel-cli
];
}

View File

@ -2,7 +2,8 @@ use poise::serenity_prelude as serenity;
use manifold::error::{ManifoldError, ManifoldResult};
use manifold::{ManifoldContext, ManifoldData};
use poise::serenity_prelude::Mentionable;
use poise::serenity_prelude::{CreateEmbed, Mentionable};
use url::Url;
use crate::badgey::models::custom_response::{CustomResponse, CustomResponseInserter};
@ -11,8 +12,8 @@ async fn add_custom_response(ctx: ManifoldContext<'_>, target: serenity::User, t
let db = &ctx.data().database;
match CustomResponseInserter::new(trigger, response, target, ctx.author().clone()).insert(db) {
Ok(_) => ctx.reply("Custom response successfully added").await?,
Err(e) => ctx.reply(format!("Custom response NOT added, an error happened. You should probably complain to Xyon about that. The error was {}", 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(())
@ -27,7 +28,7 @@ async fn remove_custom_response(ctx: ManifoldContext<'_>, id: i32) -> ManifoldRe
response.deleted = Some(true);
response.insert(db)?;
ctx.reply(format!("Successfully marked custom response with ID {} as deleted.", id)).await?;
ctx.reply(t!("commands.custom_response.marked_deleted", response_id=id)).await?;
Ok(())
}
@ -41,9 +42,9 @@ async fn undelete_custom_response(ctx: ManifoldContext<'_>, id: i32) -> Manifold
if let Some(_) = response.deleted {
response.deleted = None;
response.insert(db)?;
ctx.reply(format!("Undeleted custom response with ID: {}", &id)).await?;
ctx.reply(t!("commands.custom_response.marked_undeleted", response_id=id)).await?;
} else {
ctx.reply(format!("Can't undelete custom response with ID {} because it was not deleted in the first place.", &id)).await?;
ctx.reply(t!("commands.custom_response.undelete_error_not_deleted", response_id=id)).await?;
}
Ok(())
@ -52,18 +53,35 @@ async fn undelete_custom_response(ctx: ManifoldContext<'_>, id: i32) -> Manifold
#[poise::command(slash_command, prefix_command, required_permissions = "MODERATE_MEMBERS")]
async fn list_custom_responses_for_user(ctx: ManifoldContext<'_>, target: serenity::User) -> ManifoldResult<()> {
let db = &ctx.data().database;
let userinfo = &ctx.data().user_info.lock().await;
let mut answer: String = "".to_string();
let reply_handle = ctx.reply("Retrieving custom responses, please stand by...".to_string()).await?;
let mut pages = Vec::<CreateEmbed>::new();
let responses = CustomResponse::find_by_user(db, &target)?;
let total = responses.len();
CustomResponse::find_by_user(db, &target)?.iter().for_each(|f| {
answer.push_str(format!("{}{}", "\n", f.to_string()).as_str());
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))
.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)
.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)
.image(format!("{response}", response=Url::parse(&*f.response).unwrap_or("https://example.com".parse().unwrap())))
.to_owned()
)
});
if answer.len() == 0 {
answer.push_str("\nNone found");
if pages.len() == 0 {
ctx.reply(format!("No custom responses found for {target}", target=target.mention())).await?;
return Ok(())
}
ctx.reply(format!("I found the following custom responses for {}: {}", &target.mention(), answer)).await?;
drop(db);
drop(userinfo);
manifold::helpers::paginate(ctx, reply_handle, pages, 0).await?;
Ok(())
}

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

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

View File

@ -0,0 +1,89 @@
use built::chrono;
use manifold::error::{ManifoldError, ManifoldResult};
use manifold::{ManifoldContext, ManifoldData};
use poise::serenity_prelude as serenity;
use crate::badgey::models::quarantine_channel::Channel;
#[poise::command(slash_command, prefix_command, required_permissions = "MODERATE_MEMBERS")]
async fn void_user(_ctx: ManifoldContext<'_>, _target: serenity::User) -> ManifoldResult<()> {
Ok(())
}
#[poise::command(slash_command, prefix_command, required_permissions = "MODERATE_MEMBERS")]
async fn airlock_user(_ctx: ManifoldContext<'_>, _target: serenity::User) -> ManifoldResult<()> {
Ok(())
}
#[poise::command(slash_command, prefix_command, required_permissions = "MODERATE_MEMBERS")]
async fn unvoid_user(_ctx: ManifoldContext<'_>, _target: serenity::User) -> ManifoldResult<()> {
Ok(())
}
#[poise::command(slash_command, prefix_command, required_permissions = "MODERATE_MEMBERS")]
async fn record_chronicle(_ctx: ManifoldContext<'_>, _target: serenity::User) -> ManifoldResult<()> {
Ok(())
}
#[poise::command(slash_command, prefix_command, required_permissions = "MODERATE_MEMBERS", aliases("sr", "sl"))]
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 manifold::error::{ManifoldError, ManifoldResult};
use manifold::{ManifoldContext, ManifoldData};
use poise::serenity_prelude::{Mentionable, RoleId};
use crate::badgey::models::xp::{Rank, Track, Xp};
use poise::serenity_prelude::{CreateEmbed, Mentionable, RoleId};
use to_markdown_table::MarkdownTable;
use crate::badgey::models::xp::{Leaderboard, Rank, Track, Xp};
#[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<()> {
@ -26,11 +27,11 @@ async fn switch_rank_track(ctx: ManifoldContext<'_>, #[description = "Track to s
xp.rank_track = parsed_track.track_id;
ctx.reply(format!("I have switched you onto the {} rank track. You're now a {}. I hope this was worth it.", parsed_track.track_name, new_level_rank.rank_name)).await?;
ctx.reply(t!("commands.ranks.track_switch", new_track = parsed_track.track_name, new_rank = new_level_rank.rank_name)).await?;
xp.rank_track_last_changed = Some(chrono::Utc::now().timestamp());
xp.insert(db)?;
} else {
ctx.reply(format!("You're already on the {} rank track.", parsed_track.track_name)).await?;
ctx.reply(t!("commands.ranks.track_switch.failure", rank_track = parsed_track.track_name)).await?;
}
Ok(())
@ -51,7 +52,7 @@ async fn freeze_rank(ctx: ManifoldContext<'_>, #[rest] #[description = "Rank to
Ok(r) => r,
Err(e) => {
error!("Rank lookup error: {}", e);
ctx.reply(format!("I couldn't find that rank as a rank I can give you. You might need to switch tracks, or it might just not exist.")).await?;
ctx.reply(t!("commands.ranks.rank_freeze.not_found")).await?;
Err(e)?
}
};
@ -67,7 +68,7 @@ async fn freeze_rank(ctx: ManifoldContext<'_>, #[rest] #[description = "Rank to
if let Some(mut member) = ctx.author_member().await {
member.to_mut().add_role(ctx, frozen_rank.role_id as u64).await?;
ctx.reply(format!("Okay. I've frozen your rank at {}. Enjoy!", frozen_rank.rank_name)).await?;
ctx.reply(t!("commands.ranks.rank_freeze.success", rank = frozen_rank.rank_name)).await?;
}
} else {
@ -78,9 +79,9 @@ async fn freeze_rank(ctx: ManifoldContext<'_>, #[rest] #[description = "Rank to
}
xp.freeze_rank = None;
ctx.reply(format!("Okay. I've unfrozen your rank. Good luck out there.")).await?;
ctx.reply(t!("commands.ranks.rank_unfreeze.success")).await?;
} else {
ctx.reply(format!("Negative, your rank wasn't frozen.")).await?;
ctx.reply(t!("commands.ranks.rank_unfreeze.failure")).await?;
}
}
@ -102,16 +103,16 @@ async fn rank(ctx: ManifoldContext<'_>) -> ManifoldResult<()> {
let current_rank = Rank::get_rank_for_level(db, &xp.user_current_level, &xp.rank_track).unwrap_or(Rank::new());
let next_level_xp = xp.get_xp_to_next_level();
let next_level_xp = xp.get_xp_to_next_level(&db);
let next_rank_xp = xp.get_xp_to_next_rank(&db).unwrap_or(0);
let mut response = format!("You're currently {}. You need {} more XP to get to the next level", RoleId::from(current_rank.role_id as u64).mention(), next_level_xp);
let mut response = t!("commands.ranks.rank.current_rank", current_rank = RoleId::from(current_rank.role_id as u64).mention(), next_level_xp = next_level_xp);
if next_rank_xp == 0 {
response = format!("{}. There are no more ranks for you - you can't be promoted any further!", response);
response = t!("commands.ranks.rank.max_rank", reponse = response);
} else if next_rank_xp == next_level_xp {
response = format!("{} and rank!", response);
response = t!("commands.ranks.rank.and_rank", response = response);
} else {
response = format!("{} and you need {} more to rank up. Good luck!", response, next_rank_xp);
response = t!("commands.ranks.rank.and_rank_different_level", response = response, next_rank_xp = next_rank_xp);
}
ctx.reply(response).await?;
@ -119,6 +120,57 @@ async fn rank(ctx: ManifoldContext<'_>) -> ManifoldResult<()> {
Ok(())
}
pub fn commands() -> [poise::Command<ManifoldData, ManifoldError>; 3] {
[switch_rank_track(), freeze_rank(), rank()]
#[poise::command(prefix_command, slash_command)]
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::events::{Handler, EventHandler};
use manifold::events::EventHandler;
use manifold::ManifoldData;
use poise::{async_trait, FrameworkContext, Event};
use poise::serenity_prelude::{Context, Mentionable, Message, RoleId};
use rand::rngs::SmallRng;
use rand::{Rng, SeedableRng};
use poise::serenity_prelude::{Context, Member, Message};
use crate::badgey::handlers;
use crate::badgey::models::custom_response::CustomResponse;
use crate::badgey::models::xp::{Rank, Xp};
use crate::badgey::models::xp::Xp;
pub struct BadgeyHandler {
@ -18,9 +15,8 @@ pub struct BadgeyHandler {
#[async_trait]
impl EventHandler for BadgeyHandler {
async fn listen(ctx: &Context, framework_ctx: FrameworkContext<'_, ManifoldData, ManifoldError>, event: &Event<'_>) -> ManifoldResult<()> {
Handler::listen(ctx, framework_ctx, event).await?;
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,
_ => Ok(())
}
@ -28,73 +24,27 @@ impl EventHandler for 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<()> {
// 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) {
return Ok(())
}
let db = &fctx.user_data().await.database;
if let Ok(r) = CustomResponse::test_content(db, &msg.content_safe(&ctx.cache), &"!".to_string()) {
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() {
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();
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)?;
}
Xp::award(ctx, fctx, msg, &db).await?;
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,13 +9,16 @@ mod commands;
mod events;
mod models;
mod schema;
mod handlers;
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
#[tokio::main]
pub async fn run(arguments: ArgMatches) {
rust_i18n::set_locale(arguments.get_one::<String>("locale").unwrap());
let git_info: String = built_info::GIT_VERSION.unwrap_or("unknown").to_string();
let version_string = format!("Badgey Bot version {ver} built at {time} from revision {rev}", ver=built_info::PKG_VERSION, time=built_info::BUILT_TIME_UTC, rev=git_info);
let version_string = t!("misc.version_string", ver=built_info::PKG_VERSION, time=built_info::BUILT_TIME_UTC, rev=git_info).to_string();
let client = match manifold::prepare_client::<BadgeyHandler>(arguments, GatewayIntents::all(), commands::collect_commands(), version_string, MIGRATIONS).await {
Ok(c) => c,

View File

@ -1,5 +1,5 @@
use std::fmt::{Display, Formatter};
use built::chrono::{NaiveDateTime, Utc};
use built::chrono::{DateTime, Utc};
use diesel::prelude::*;
use diesel::insert_into;
use regex::Regex;
@ -10,6 +10,8 @@ use manifold::error::{ManifoldError, ManifoldResult};
use manifold::models::user::UserInfo;
use manifold::schema::userinfo;
define_sql_function!(fn random() -> Text);
use crate::badgey::schema::*;
#[derive(Queryable, Selectable, Identifiable, Insertable, AsChangeset, Associations, Debug, PartialEq, Clone)]
@ -80,6 +82,7 @@ impl CustomResponse {
Ok(custom_responses::table
.filter(custom_responses::trigger.ilike(needle))
.filter(custom_responses::deleted.is_null())
.order_by(random())
.limit(1)
.select(CustomResponse::as_select())
.get_result(&mut conn.get()?)?)
@ -109,6 +112,6 @@ impl CustomResponse {
impl Display for CustomResponse {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "ID: {}, trigger: {}, response: {}, added by: {}, added on: {}, deleted: {}", self.id, self.trigger, self.response, serenity::UserId::from(self.added_by as u64), NaiveDateTime::from_timestamp_opt(self.added_on, 0).unwrap(), self.deleted.unwrap_or(false))
write!(f, "ID: {}, trigger: {}, response: {}, added by: {}, added on: {}, deleted: {}", self.id, self.trigger, self.response, serenity::UserId::from(self.added_by as u64), DateTime::from_timestamp(self.added_on, 0).unwrap(), self.deleted.unwrap_or(false))
}
}

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 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 diesel::prelude::*;
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::models::user::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::*;
@ -22,7 +28,7 @@ pub struct Xp {
pub rank_track: i64,
pub rank_track_last_changed: 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)]
@ -34,11 +40,55 @@ pub struct Rank {
pub rank_name: String,
}
#[derive(Queryable, Selectable, Identifiable, Debug, Clone)]
#[derive(Queryable, Selectable, Identifiable, Debug, Clone, Default)]
#[diesel(primary_key(track_id))]
pub struct Track {
pub track_id: i64,
pub track_name: String,
pub xp_level_constant: f64,
pub xp_award_range_min: 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 {
@ -76,34 +126,104 @@ impl Xp {
.map_err(|e| ManifoldError::from(e))
}
pub fn get_level_from_xp(&self) -> i64 {
(0.125 * f64::sqrt(self.xp_value.clone() as f64)) as i64
pub fn get_level_from_xp(&self, conn: &Db, constant: Option<&f64>) -> i64 {
let c = match constant {
Some(c) => *c,
None => Track::get_track_by_id(conn, &self.rank_track).unwrap_or_default().xp_level_constant
};
(c * f64::sqrt(self.xp_value.clone() as f64)) as i64
}
pub fn get_xp_to_next_level(&self) -> i64 {
let current_level = self.get_level_from_xp();
pub fn get_xp_to_next_level(&self, conn: &Db) -> i64 {
let constant = Track::get_track_by_id(conn, &self.rank_track).unwrap_or_default().xp_level_constant;
let current_level = self.get_level_from_xp(conn, Some(&constant));
let target_level = current_level + 1;
(target_level as f32 / 0.125).powi(2) as i64 - &self.xp_value
(target_level as f64 / constant).powi(2) as i64 - &self.xp_value
}
pub fn get_xp_to_next_rank(&self, conn: &Db) -> ManifoldResult<i64> {
let current_level = self.get_level_from_xp();
let constant = Track::get_track_by_id(conn, &self.rank_track)?.xp_level_constant;
let current_level = self.get_level_from_xp(conn, Some(&constant));
let target_level = Rank::get_next_rank_for_level(conn, &current_level, &self.rank_track)?.required_level;
Ok((target_level as f32 / 0.125).powi(2) as i64 - &self.xp_value)
Ok((target_level as f64 / constant).powi(2) as i64 - &self.xp_value)
}
pub fn check_timeout(value: &Option<i64>, timeout: &i64) -> ManifoldResult<()> {
if let Some(v) = value {
if v > &(chrono::Utc::now().timestamp() - timeout) {
let timeuntil = v - (chrono::Utc::now().timestamp() - timeout);
Err(format!("You are on cooldown. Try again in {} seconds.", timeuntil))?;
Err(t!("models.xp.cooldown", timeuntil = timeuntil))?;
}
}
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 {
@ -112,7 +232,7 @@ impl Rank {
role_id: 0,
required_level: 0,
rank_track: 0,
rank_name: "unranked".to_string(),
rank_name: t!("models.xp.unranked").to_string(),
}
}

View File

@ -1,5 +1,15 @@
// @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! {
custom_responses (id) {
id -> Int4,
@ -27,6 +37,9 @@ diesel::table! {
track_id -> Int8,
#[max_length = 128]
track_name -> Varchar,
xp_level_constant -> Float8,
xp_award_range_min -> Int4,
xp_award_range_max -> Int4,
}
}
@ -61,6 +74,7 @@ diesel::joinable!(custom_responses -> userinfo (added_for));
diesel::joinable!(xp -> userinfo (user_id));
diesel::allow_tables_to_appear_in_same_query!(
channels,
custom_responses,
ranks,
tracks,

View File

@ -1,4 +1,5 @@
#[macro_use] extern crate log;
#[macro_use] extern crate rust_i18n;
pub mod badgey;
@ -9,6 +10,8 @@ pub mod built_info {
include!(concat!(env!("OUT_DIR"), "/built.rs"));
}
i18n!("config/locales");
fn main() {
env_logger::init();
@ -33,6 +36,10 @@ fn main() {
.value_name("ENV")
.default_value("Production")
.help("Bot environment to use. Determines which config settings are read. Defaults to Production."))
.arg(Arg::new("locale")
.short('l')
.default_value("en")
.help("Bot locale to run in, en and de currently supported"))
.arg(Arg::new("make-config")
.short('M')
.long("make-config")

View File

@ -0,0 +1,20 @@
[bot startup]
**B A D G E Y** Programminitialisierung. Laden von Zielen aus dem Speicher.
Badgey lebt wieder! Nichts kann Badgey erlegen! Badgey ist unzerstörbar!
Retikulierende Splines....
[help footer]
Badgey möchte, dass du weißt, dass er nichts als Liebe für dich empfindet. Noch.
Badgeys geduld ist grenzenlos! Er wünscht Ihnen nichts als viel Glück auf Ihrer Entdeckungsreise.
Badgey möchte wissen, warum du nach seinen Daten fragst.
Badgey hat diese Anfrage über seine innersten Strukturen aufgezeichnet. Badgey wird sich erinnern.
Badgey weiß, wo du schläfst, weißt du. Er wollte nur, dass du dich daran erinnerst.
[weather card footer]
Badgey Weather wurde Ihnen in Zusammenarbeit mit AstroGlide für den reibungslosesten Betrieb zur Verfügung gestellt, den Sie jemals durchführen werden.This weather broadcast and all which preceded it are entirely fictional. Badgey takes no responsibility for any harm caused by believing otherwise.
Badgey Weather, stolz angetrieben von einem Rasenmähermotor und einem 93-jährigen Veteranen, der einfach nicht aufgeben will.
Der Badgey Weather Service arbeitet mit Ihren Spenden und den Tränen der Faschisten.
Wetter von Badgey. Nicht die Berichterstattung. Das tatsächliche Wetter. Ich kann tun, was ich will.3
Dieser Bericht wird sich innerhalb von 5 sekunden selbst zerstören.
Alle im obigen Wetterbericht enthaltenen Informationen sind nicht für den menschlichen Verzehr geeignet. Bei Kontakt mit den Augen konsultieren Sie bitte einen Arzt.
Ich glaube, Ihr Warpkern ist undicht.
Badgey versuchte, diesen Wetterbericht von der Sternenflotte absegnen zu lassen, aber die Sternenflotte leugnet jede Kenntnis von der Existenz von Badgey, was Badgey traurig machte.
[end]

View File

@ -0,0 +1,14 @@
[bot startup]
M5 COMPUTER SYSTEM ACTIVATING
This unit has been attached. Programming includes full freedom to choose defensive actions in all attack situations.
Reticulating splines....
[help footer]
M5 READOUT
[weather card footer]
M5 WEATHER CIRCUIT OPERATIONAL EFFICIENCY NOW 43%
This weather broadcast and all which preceded it are entirely fictional. Starfleet Command takes no responsibility for any harm caused by believing otherwise.
Weather by M5. Not the reporting. The actual weather. I can do what I want.
This report will self destruct in five seconds.
Any information contained within the above weather report is not safe for human consumption. In case of contact with eyes, please consult a medical professional.
I think your warp core is leaking.
[end]