Compare commits
3 Commits
main
...
feature/cu
| Author | SHA1 | Date |
|---|---|---|
|
|
09ff09d48f | |
|
|
6458598026 | |
|
|
a8120c9466 |
|
|
@ -15,46 +15,19 @@ 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[format('{0}_POSTGRES_DATABASE_NAME', matrix.bot)] }}
|
||||
POSTGRES_DATABASE_NAME: ${{ vars.POSTGRES_DATABASE_NAME }}
|
||||
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 ${{ vars[format('{0}_SERVER_HOSTNAME', matrix.bot)] }}
|
||||
run: bash cicd/deploy.sh
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
<?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>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<?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>
|
||||
File diff suppressed because it is too large
Load Diff
20
Cargo.toml
20
Cargo.toml
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "badgey"
|
||||
version = "4.2.0"
|
||||
version = "3.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
|
@ -20,22 +20,4 @@ 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"
|
||||
|
|
|
|||
|
|
@ -1,12 +1,7 @@
|
|||
#!/bin/bash
|
||||
|
||||
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
|
||||
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
|
||||
|
|
@ -11,12 +11,12 @@
|
|||
"channels": {
|
||||
"log": 648260641626390528
|
||||
},
|
||||
"responses_file_path": "txt/responses",
|
||||
"responses_file_path": "txt/responses.txt",
|
||||
"services": {
|
||||
"weather": {
|
||||
"source_uri": "https://api.weatherapi.com/v1",
|
||||
"cache_mode": "NoCache",
|
||||
"api_key": ""
|
||||
"api_key": "70aacb9af931438a957215406211210"
|
||||
},
|
||||
"frog_tips": {
|
||||
"source_uri": "https://frog.tips/api/1/tips/",
|
||||
|
|
|
|||
|
|
@ -1,69 +0,0 @@
|
|||
_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}"
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"prefix": ".",
|
||||
"nickname": "#{BOT_NICKNAME}#",
|
||||
"nickname": "Badgey",
|
||||
"channels": {
|
||||
"log": "#{LOG_CHANNEL_ID}#"
|
||||
"log": 1143479696886087801
|
||||
},
|
||||
"database": {
|
||||
"host": "#{POSTGRES_HOST}#",
|
||||
|
|
@ -11,12 +11,12 @@
|
|||
"database_name": "#{POSTGRES_DATABASE_NAME}#",
|
||||
"port": 5432
|
||||
},
|
||||
"responses_file_path": "txt/responses.#{BOT_IDENTIFIER}#",
|
||||
"responses_file_path": "txt/responses.txt",
|
||||
"services": {
|
||||
"weather": {
|
||||
"source_uri": "https://api.weatherapi.com/v1",
|
||||
"cache_mode": "NoCache",
|
||||
"api_key": "#{WEATHER_API_KEY}#"
|
||||
"api_key": "70aacb9af931438a957215406211210"
|
||||
},
|
||||
"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": "#{DOGPICS_API_KEY}#"
|
||||
"api_key": "live_RRrRUsdmRIpKUefuwOwuAV1nab7Gt8GqyvIqPGCgLAbpLHGdyStbGj9Xc67inYMt"
|
||||
},
|
||||
"cat_pics": {
|
||||
"source_uri": "https://api.thecatapi.com/v1/images/search?limit=100&order=RAND",
|
||||
"cache_name": "cat_pics",
|
||||
"cache_mode": "Cache",
|
||||
"api_key": "#{CATPICS_API_KEY}#"
|
||||
"api_key": "live_nvupPQbrXjHy8jsZJ1stp72fzsRLR8jQby8IR3l9yMngqAU9gcTEV8RA0OOiK8zP"
|
||||
},
|
||||
"dad_jokes": {
|
||||
"source_uri": "https://icanhazdadjoke.com/search?limit=30",
|
||||
|
|
@ -44,10 +44,6 @@
|
|||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
-- 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();
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
-- 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;
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
-- This file should undo anything in `up.sql`
|
||||
DROP TABLE IF EXISTS "custom_responses";
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
-- 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
|
||||
);
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
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;
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
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;
|
||||
|
|
@ -1 +0,0 @@
|
|||
DROP TABLE IF EXISTS "quarantine_channels";
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
CREATE TABLE IF NOT EXISTS "quarantine_channels" (
|
||||
id SERIAL PRIMARY KEY,
|
||||
qc_role_id BIGINT NOT NULL,
|
||||
qc_channel_id BIGINT NOT NULL
|
||||
);
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
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;
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
-- 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";
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
{ pkgs ? import <nixpkgs> {}}:
|
||||
pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
openssl
|
||||
postgresql
|
||||
pkg-config
|
||||
diesel-cli
|
||||
];
|
||||
}
|
||||
|
|
@ -2,8 +2,7 @@ use poise::serenity_prelude as serenity;
|
|||
|
||||
use manifold::error::{ManifoldError, ManifoldResult};
|
||||
use manifold::{ManifoldContext, ManifoldData};
|
||||
use poise::serenity_prelude::{CreateEmbed, Mentionable};
|
||||
use url::Url;
|
||||
use poise::serenity_prelude::Mentionable;
|
||||
use crate::badgey::models::custom_response::{CustomResponse, CustomResponseInserter};
|
||||
|
||||
|
||||
|
|
@ -12,8 +11,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(t!("commands.custom_response.added")).await?,
|
||||
Err(e) => ctx.reply(t!("commands.custom_response.added.error", error = e)).await?,
|
||||
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(())
|
||||
|
|
@ -28,7 +27,7 @@ async fn remove_custom_response(ctx: ManifoldContext<'_>, id: i32) -> ManifoldRe
|
|||
response.deleted = Some(true);
|
||||
response.insert(db)?;
|
||||
|
||||
ctx.reply(t!("commands.custom_response.marked_deleted", response_id=id)).await?;
|
||||
ctx.reply(format!("Successfully marked custom response with ID {} as deleted.", id)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -42,9 +41,9 @@ async fn undelete_custom_response(ctx: ManifoldContext<'_>, id: i32) -> Manifold
|
|||
if let Some(_) = response.deleted {
|
||||
response.deleted = None;
|
||||
response.insert(db)?;
|
||||
ctx.reply(t!("commands.custom_response.marked_undeleted", response_id=id)).await?;
|
||||
ctx.reply(format!("Undeleted custom response with ID: {}", &id)).await?;
|
||||
} else {
|
||||
ctx.reply(t!("commands.custom_response.undelete_error_not_deleted", response_id=id)).await?;
|
||||
ctx.reply(format!("Can't undelete custom response with ID {} because it was not deleted in the first place.", &id)).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
@ -53,35 +52,18 @@ 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 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();
|
||||
let mut answer: String = "".to_string();
|
||||
|
||||
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()
|
||||
)
|
||||
CustomResponse::find_by_user(db, &target)?.iter().for_each(|f| {
|
||||
answer.push_str(format!("{}{}", "\n", f.to_string()).as_str());
|
||||
});
|
||||
|
||||
if pages.len() == 0 {
|
||||
ctx.reply(format!("No custom responses found for {target}", target=target.mention())).await?;
|
||||
return Ok(())
|
||||
if answer.len() == 0 {
|
||||
answer.push_str("\nNone found");
|
||||
}
|
||||
|
||||
drop(db);
|
||||
drop(userinfo);
|
||||
|
||||
manifold::helpers::paginate(ctx, reply_handle, pages, 0).await?;
|
||||
ctx.reply(format!("I found the following custom responses for {}: {}", &target.mention(), answer)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,62 +0,0 @@
|
|||
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()]
|
||||
}
|
||||
|
|
@ -4,15 +4,11 @@ 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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,89 +0,0 @@
|
|||
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()]
|
||||
}
|
||||
|
|
@ -1,9 +1,8 @@
|
|||
use built::chrono;
|
||||
use manifold::error::{ManifoldError, ManifoldResult};
|
||||
use manifold::{ManifoldContext, ManifoldData};
|
||||
use poise::serenity_prelude::{CreateEmbed, Mentionable, RoleId};
|
||||
use to_markdown_table::MarkdownTable;
|
||||
use crate::badgey::models::xp::{Leaderboard, Rank, Track, Xp};
|
||||
use poise::serenity_prelude::{Mentionable, RoleId};
|
||||
use crate::badgey::models::xp::{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<()> {
|
||||
|
|
@ -27,11 +26,11 @@ async fn switch_rank_track(ctx: ManifoldContext<'_>, #[description = "Track to s
|
|||
|
||||
xp.rank_track = parsed_track.track_id;
|
||||
|
||||
ctx.reply(t!("commands.ranks.track_switch", new_track = parsed_track.track_name, new_rank = new_level_rank.rank_name)).await?;
|
||||
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?;
|
||||
xp.rank_track_last_changed = Some(chrono::Utc::now().timestamp());
|
||||
xp.insert(db)?;
|
||||
} else {
|
||||
ctx.reply(t!("commands.ranks.track_switch.failure", rank_track = parsed_track.track_name)).await?;
|
||||
ctx.reply(format!("You're already on the {} rank track.", parsed_track.track_name)).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
@ -52,7 +51,7 @@ async fn freeze_rank(ctx: ManifoldContext<'_>, #[rest] #[description = "Rank to
|
|||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
error!("Rank lookup error: {}", e);
|
||||
ctx.reply(t!("commands.ranks.rank_freeze.not_found")).await?;
|
||||
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?;
|
||||
Err(e)?
|
||||
}
|
||||
};
|
||||
|
|
@ -68,7 +67,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(t!("commands.ranks.rank_freeze.success", rank = frozen_rank.rank_name)).await?;
|
||||
ctx.reply(format!("Okay. I've frozen your rank at {}. Enjoy!", frozen_rank.rank_name)).await?;
|
||||
}
|
||||
|
||||
} else {
|
||||
|
|
@ -79,9 +78,9 @@ async fn freeze_rank(ctx: ManifoldContext<'_>, #[rest] #[description = "Rank to
|
|||
}
|
||||
|
||||
xp.freeze_rank = None;
|
||||
ctx.reply(t!("commands.ranks.rank_unfreeze.success")).await?;
|
||||
ctx.reply(format!("Okay. I've unfrozen your rank. Good luck out there.")).await?;
|
||||
} else {
|
||||
ctx.reply(t!("commands.ranks.rank_unfreeze.failure")).await?;
|
||||
ctx.reply(format!("Negative, your rank wasn't frozen.")).await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -103,16 +102,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(&db);
|
||||
let next_level_xp = xp.get_xp_to_next_level();
|
||||
let next_rank_xp = xp.get_xp_to_next_rank(&db).unwrap_or(0);
|
||||
|
||||
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);
|
||||
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);
|
||||
if next_rank_xp == 0 {
|
||||
response = t!("commands.ranks.rank.max_rank", reponse = response);
|
||||
response = format!("{}. There are no more ranks for you - you can't be promoted any further!", response);
|
||||
} else if next_rank_xp == next_level_xp {
|
||||
response = t!("commands.ranks.rank.and_rank", response = response);
|
||||
response = format!("{} and rank!", response);
|
||||
} else {
|
||||
response = t!("commands.ranks.rank.and_rank_different_level", response = response, next_rank_xp = next_rank_xp);
|
||||
response = format!("{} and you need {} more to rank up. Good luck!", response, next_rank_xp);
|
||||
}
|
||||
|
||||
ctx.reply(response).await?;
|
||||
|
|
@ -120,57 +119,6 @@ async fn rank(ctx: ManifoldContext<'_>) -> ManifoldResult<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[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()]
|
||||
pub fn commands() -> [poise::Command<ManifoldData, ManifoldError>; 3] {
|
||||
[switch_rank_track(), freeze_rank(), rank()]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
use built::chrono;
|
||||
use manifold::error::{ManifoldError, ManifoldResult};
|
||||
use manifold::events::EventHandler;
|
||||
use manifold::events::{Handler, EventHandler};
|
||||
use manifold::ManifoldData;
|
||||
use poise::{async_trait, FrameworkContext, Event};
|
||||
use poise::serenity_prelude::{Context, Member, Message};
|
||||
use crate::badgey::handlers;
|
||||
use poise::serenity_prelude::{Context, Mentionable, Message, RoleId};
|
||||
use rand::rngs::SmallRng;
|
||||
use rand::{Rng, SeedableRng};
|
||||
use crate::badgey::models::custom_response::CustomResponse;
|
||||
|
||||
use crate::badgey::models::xp::Xp;
|
||||
use crate::badgey::models::xp::{Rank, Xp};
|
||||
|
||||
|
||||
pub struct BadgeyHandler {
|
||||
|
||||
|
|
@ -15,8 +18,9 @@ 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(())
|
||||
}
|
||||
|
|
@ -24,27 +28,73 @@ 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?;
|
||||
}
|
||||
|
||||
Xp::award(ctx, fctx, msg, &db).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)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
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(())
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
pub mod airlock;
|
||||
|
|
@ -9,16 +9,13 @@ 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 = t!("misc.version_string", ver=built_info::PKG_VERSION, time=built_info::BUILT_TIME_UTC, rev=git_info).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 client = match manifold::prepare_client::<BadgeyHandler>(arguments, GatewayIntents::all(), commands::collect_commands(), version_string, MIGRATIONS).await {
|
||||
Ok(c) => c,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use std::fmt::{Display, Formatter};
|
||||
use built::chrono::{DateTime, Utc};
|
||||
use built::chrono::{NaiveDateTime, Utc};
|
||||
use diesel::prelude::*;
|
||||
use diesel::insert_into;
|
||||
use regex::Regex;
|
||||
|
|
@ -10,8 +10,6 @@ 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)]
|
||||
|
|
@ -82,7 +80,6 @@ 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()?)?)
|
||||
|
|
@ -112,6 +109,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), DateTime::from_timestamp(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), NaiveDateTime::from_timestamp_opt(self.added_on, 0).unwrap(), self.deleted.unwrap_or(false))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,230 +0,0 @@
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,2 @@
|
|||
pub mod custom_response;
|
||||
pub mod xp;
|
||||
pub mod quarantine_channel;
|
||||
pub mod f1;
|
||||
|
|
|
|||
|
|
@ -1,59 +0,0 @@
|
|||
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()?)?)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +1,11 @@
|
|||
use built::chrono;
|
||||
use diesel::prelude::*;
|
||||
use diesel::insert_into;
|
||||
use rand::rngs::SmallRng;
|
||||
use rand::{Rng, SeedableRng};
|
||||
use to_markdown_table::{TableRow};
|
||||
|
||||
use manifold::{Db, ManifoldData};
|
||||
use manifold::Db;
|
||||
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::*;
|
||||
|
||||
|
|
@ -28,7 +22,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)]
|
||||
|
|
@ -40,55 +34,11 @@ pub struct Rank {
|
|||
pub rank_name: String,
|
||||
}
|
||||
|
||||
#[derive(Queryable, Selectable, Identifiable, Debug, Clone, Default)]
|
||||
#[derive(Queryable, Selectable, Identifiable, Debug, Clone)]
|
||||
#[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 {
|
||||
|
|
@ -126,104 +76,34 @@ impl Xp {
|
|||
.map_err(|e| ManifoldError::from(e))
|
||||
}
|
||||
|
||||
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_level_from_xp(&self) -> i64 {
|
||||
(0.125 * f64::sqrt(self.xp_value.clone() as f64)) as i64
|
||||
}
|
||||
|
||||
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));
|
||||
pub fn get_xp_to_next_level(&self) -> i64 {
|
||||
let current_level = self.get_level_from_xp();
|
||||
let target_level = current_level + 1;
|
||||
|
||||
(target_level as f64 / constant).powi(2) as i64 - &self.xp_value
|
||||
(target_level as f32 / 0.125).powi(2) as i64 - &self.xp_value
|
||||
}
|
||||
|
||||
pub fn get_xp_to_next_rank(&self, conn: &Db) -> ManifoldResult<i64> {
|
||||
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 current_level = self.get_level_from_xp();
|
||||
let target_level = Rank::get_next_rank_for_level(conn, ¤t_level, &self.rank_track)?.required_level;
|
||||
|
||||
Ok((target_level as f64 / constant).powi(2) as i64 - &self.xp_value)
|
||||
Ok((target_level as f32 / 0.125).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(t!("models.xp.cooldown", timeuntil = timeuntil))?;
|
||||
Err(format!("You are on cooldown. Try again in {} seconds.", 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 {
|
||||
|
|
@ -232,7 +112,7 @@ impl Rank {
|
|||
role_id: 0,
|
||||
required_level: 0,
|
||||
rank_track: 0,
|
||||
rank_name: t!("models.xp.unranked").to_string(),
|
||||
rank_name: "unranked".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +1,5 @@
|
|||
// @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,
|
||||
|
|
@ -37,9 +27,6 @@ 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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -74,7 +61,6 @@ 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,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
#[macro_use] extern crate log;
|
||||
#[macro_use] extern crate rust_i18n;
|
||||
|
||||
pub mod badgey;
|
||||
|
||||
|
|
@ -10,8 +9,6 @@ pub mod built_info {
|
|||
include!(concat!(env!("OUT_DIR"), "/built.rs"));
|
||||
}
|
||||
|
||||
i18n!("config/locales");
|
||||
|
||||
fn main() {
|
||||
|
||||
env_logger::init();
|
||||
|
|
@ -36,10 +33,6 @@ 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")
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
[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]
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
[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]
|
||||
Loading…
Reference in New Issue