Compare commits

...

11 Commits

Author SHA1 Message Date
f5996467d9 remove workflow 2025-06-27 14:07:49 -04:00
a1cd0e6a25 use sqlx's query macro in some places
Some checks failed
Docker Image CI / build_amd64 (push) Has been cancelled
Docker Image CI / build_arm64 (push) Has been cancelled
Docker Image CI / create_manifest (push) Has been cancelled
2025-06-27 13:30:38 -04:00
c054ef1c1c use sqlx migrations 2025-06-26 12:58:44 -04:00
4509359c0e fix timeout cheating 2025-05-27 11:44:44 -04:00
3b741835bd fix formatting and remove instances of printing IDs 2025-05-18 16:24:15 -04:00
d35426ec07 add forget admin command 2025-05-18 16:18:01 -04:00
e798e82395 improved admin controls over self roles 2025-05-18 16:10:07 -04:00
e5ca4473bc remove unecessary use 2025-05-17 23:35:02 -04:00
1874058c45 more concise guild getting 2025-05-17 23:31:48 -04:00
6f1adcee5e use consistent guild getter syntax 2025-05-17 23:16:59 -04:00
f75c0aa057 update dependencies 2025-05-17 15:04:48 -04:00
29 changed files with 607 additions and 608 deletions

3
.envrc Normal file
View File

@@ -0,0 +1,3 @@
set -a
source .env
set +a

View File

@@ -1,64 +0,0 @@
name: Docker Image CI
on:
push:
branches: [ "main" ]
repository_dispatch:
types: [ lamm-updated ]
jobs:
build_amd64:
runs-on: ubuntu-latest
steps:
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- uses: actions/checkout@v4
- name: Build and push the Docker image
run: docker build . --file Dockerfile --tag ${{ secrets.DOCKERHUB_USERNAME }}/discord-bot:amd64
- uses: actions/checkout@v4
- name: Push the Docker image
run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/discord-bot:amd64
build_arm64:
runs-on: self-hosted
steps:
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- uses: actions/checkout@v4
- name: Build the Docker image
run: docker build . --file Dockerfile --tag ${{ secrets.DOCKERHUB_USERNAME }}/discord-bot:arm64
- uses: actions/checkout@v4
- name: Push the Docker image
run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/discord-bot:arm64
create_manifest:
needs: [build_amd64, build_arm64]
runs-on: ubuntu-latest
steps:
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Create and push multi-arch manifest
run: |
docker pull ${{ secrets.DOCKERHUB_USERNAME }}/discord-bot:amd64
docker pull ${{ secrets.DOCKERHUB_USERNAME }}/discord-bot:arm64
docker manifest create ${{ secrets.DOCKERHUB_USERNAME }}/discord-bot:latest \
--amend ${{ secrets.DOCKERHUB_USERNAME }}/discord-bot:amd64 \
--amend ${{ secrets.DOCKERHUB_USERNAME }}/discord-bot:arm64
docker manifest push ${{ secrets.DOCKERHUB_USERNAME }}/discord-bot:latest

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
/target /target
.env .env
.vscode

428
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,4 @@
CREATE TABLE IF NOT EXISTS bank (
id BIGINT PRIMARY KEY,
balance INT
)

View File

@@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS selfroles (
userid BIGINT NOT NULL,
guildid BIGINT NOT NULL,
roleid BIGINT,
UNIQUE (userid, guildid)
)

View File

@@ -0,0 +1,6 @@
CREATE TABLE IF NOT EXISTS games (
id BIGSERIAL PRIMARY KEY,
name CHAR[255]
)

View File

@@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS items (
id BIGSERIAL PRIMARY KEY,
owner BIGINT NOT NULL,
game BIGINT NOT NULL,
item BIGINT NOT NULL,
data JSON NOT NULL,
name TEXT
)

View File

@@ -0,0 +1,5 @@
CREATE TABLE IF NOT EXISTS dailies (
userid BIGINT NOT NULL PRIMARY KEY,
last TIMESTAMPTZ,
streak INT
)

View File

@@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS settings (
guildid BIGINT NOT NULL PRIMARY KEY,
positional_role BIGINT,
banrole BIGINT,
hoist_selfroles BOOLEAN,
prefix TEXT
)

View File

@@ -1,4 +1,4 @@
use crate::common::{Context, Error}; use crate::common::{Context, Error, BigBirbError};
use crate::commands::settings; use crate::commands::settings;
use poise::serenity_prelude as serenity; use poise::serenity_prelude as serenity;
@@ -10,13 +10,7 @@ pub async fn ban(ctx: Context<'_>,
#[rest] #[rest]
reason: Option<String>) -> Result<(), Error> reason: Option<String>) -> Result<(), Error>
{ {
let guild = match ctx.guild_id() { let guild = ctx.guild_id().ok_or(BigBirbError::GuildOnly)?;
Some(g) => g,
None => {
ctx.reply("This command must be ran within a guild.").await?;
return Ok(());
}
};
if let Some(role) = settings::get_banrole(ctx, guild).await? { if let Some(role) = settings::get_banrole(ctx, guild).await? {
let member = guild.member(&ctx, user.id).await?; let member = guild.member(&ctx, user.id).await?;
@@ -35,13 +29,7 @@ pub async fn ban(ctx: Context<'_>,
#[poise::command(slash_command, prefix_command)] #[poise::command(slash_command, prefix_command)]
pub async fn unban(ctx: Context<'_>, user: serenity::User) -> Result<(), Error> pub async fn unban(ctx: Context<'_>, user: serenity::User) -> Result<(), Error>
{ {
let guild = match ctx.guild_id() { let guild = ctx.guild_id().ok_or(BigBirbError::GuildOnly)?;
Some(g) => g,
None => {
ctx.reply("This command must be ran within a guild.").await?;
return Ok(());
}
};
if let Some(role) = settings::get_banrole(ctx, guild).await? { if let Some(role) = settings::get_banrole(ctx, guild).await? {
let member = guild.member(&ctx, user.id).await?; let member = guild.member(&ctx, user.id).await?;

View File

@@ -6,9 +6,9 @@ use poise::serenity_prelude as serenity;
#[poise::command(slash_command, prefix_command, aliases("bal", "b"))] #[poise::command(slash_command, prefix_command, aliases("bal", "b"))]
pub async fn balance(ctx: Context<'_>, user: Option<serenity::User>) -> Result<(), Error> { pub async fn balance(ctx: Context<'_>, user: Option<serenity::User>) -> Result<(), Error> {
let user = user.as_ref().unwrap_or(ctx.author()); let user = user.as_ref().unwrap_or(ctx.author());
let db = &ctx.data().database; let mut tx = ctx.data().database.begin().await?;
let wealth = super::get_balance(user.id, db).await?; let wealth = super::get_balance(user.id, &mut tx).await?;
common::no_ping_reply(&ctx, format!("{} **{}** token(s).", common::no_ping_reply(&ctx, format!("{} **{}** token(s).",
if user.id == ctx.author().id { if user.id == ctx.author().id {

View File

@@ -106,6 +106,9 @@ pub async fn blackjack(ctx: Context<'_>, amount: String) -> Result<(), Error>
let mut tx = ctx.data().database.begin().await?; let mut tx = ctx.data().database.begin().await?;
let mut balance = super::get_balance(ctx.author().id, &mut *tx).await?; let mut balance = super::get_balance(ctx.author().id, &mut *tx).await?;
// whether the player is going to time out in the next 60 seconds
let mut timeout = false;
let amount = match amount.to_lowercase().as_str() { let amount = match amount.to_lowercase().as_str() {
"all" => balance, "all" => balance,
"half" => balance / 2, "half" => balance / 2,
@@ -165,7 +168,7 @@ pub async fn blackjack(ctx: Context<'_>, amount: String) -> Result<(), Error>
"**Dealer's hand**: {} ({})\n", "**Dealer's hand**: {} ({})\n",
"**Your hand**: {} ({})\n\n", "**Your hand**: {} ({})\n\n",
"**Bet**: {}\n", "**Bet**: {}\n",
"Bust! You've lost **{}** tokens." "Bust! You've lost **{}** token(s)."
), ),
dealers_hand.iter().map(|card| format!("`{card}`")).collect::<Vec<String>>().join(", "), dealers_hand.iter().map(|card| format!("`{card}`")).collect::<Vec<String>>().join(", "),
dealers_count, dealers_count,
@@ -194,13 +197,15 @@ pub async fn blackjack(ctx: Context<'_>, amount: String) -> Result<(), Error>
concat!( concat!(
"**Dealer's hand**: {} ({})\n", "**Dealer's hand**: {} ({})\n",
"**Your hand**: {} ({})\n\n", "**Your hand**: {} ({})\n\n",
"**Bet**: {}" "**Bet**: {}",
"{}"
), ),
format!("`{}`, `XX`", dealers_hand[0]), format!("`{}`, `XX`", dealers_hand[0]),
dealers_hand[0].value(matches!(dealers_hand[0], Card { rank: Rank::Ace, .. })), dealers_hand[0].value(matches!(dealers_hand[0], Card { rank: Rank::Ace, .. })),
players_hand.iter().map(|card| format!("`{card}`")).collect::<Vec<String>>().join(", "), players_hand.iter().map(|card| format!("`{card}`")).collect::<Vec<String>>().join(", "),
players_count, players_count,
amount, amount,
if timeout { "\n*You have 60 seconds to make a decision.*" } else { "" }
) )
) )
.components(components) .components(components)
@@ -209,10 +214,36 @@ pub async fn blackjack(ctx: Context<'_>, amount: String) -> Result<(), Error>
msg.edit(ctx, reply).await?; msg.edit(ctx, reply).await?;
let Some(mci) = serenity::ComponentInteractionCollector::new(ctx.serenity_context()) let Some(mci) = serenity::ComponentInteractionCollector::new(ctx.serenity_context())
.timeout(Duration::from_secs(120)) .timeout(Duration::from_secs(60))
.filter(move |mci| mci.data.custom_id.starts_with("blackjack")).await else { .filter(move |mci| mci.data.custom_id.starts_with("blackjack")).await else {
ctx.reply("failed interaction!").await?; if timeout {
msg.edit(ctx, poise::CreateReply::default()
.components(vec![])
.content(
format!(
concat!(
"**Dealer's hand**: {} ({})\n",
"**Your hand**: {} ({})\n\n",
"**Bet**: {}\n",
"{}"
),
dealers_hand.iter().map(|card| format!("`{card}`")).collect::<Vec<String>>().join(", "),
dealers_count,
players_hand.iter().map(|card| format!("`{card}`")).collect::<Vec<String>>().join(", "),
players_count,
amount, format!("No bets go without a game! You've lost **{amount}** token(s).")
)
)).await?;
balance -= amount;
super::change_balance(ctx.author().id, balance, &mut *tx).await?;
tx.commit().await?;
return Ok(()); return Ok(());
} else {
timeout = true;
continue;
}
}; };
if mci.member.clone().unwrap().user.id != ctx.author().id { if mci.member.clone().unwrap().user.id != ctx.author().id {
@@ -225,6 +256,9 @@ pub async fn blackjack(ctx: Context<'_>, amount: String) -> Result<(), Error>
continue; continue;
} }
// Reset timeout after player interacts
timeout = false;
mci.create_response(ctx, serenity::CreateInteractionResponse::Acknowledge).await?; mci.create_response(ctx, serenity::CreateInteractionResponse::Acknowledge).await?;
match &mci.data.custom_id[..] { match &mci.data.custom_id[..] {
@@ -251,20 +285,20 @@ pub async fn blackjack(ctx: Context<'_>, amount: String) -> Result<(), Error>
if players_count == 21 && players_hand.len() == 2 { if players_count == 21 && players_hand.len() == 2 {
let amount = amount * 3 / 2; let amount = amount * 3 / 2;
balance += amount; balance += amount;
format!("You've won with a Blackjack! You've gained **{amount}** tokens.") format!("You've won with a Blackjack! You've gained **{amount}** token(s).")
} else { } else {
balance += amount; balance += amount;
format!("You've won! **{amount}** tokens have been added to your account.") format!("You've won! **{amount}** token(s) have been added to your account.")
} }
} }
Ordering::Greater if dealers_count > 21 => { Ordering::Greater if dealers_count > 21 => {
if players_count == 21 && players_hand.len() == 2 { if players_count == 21 && players_hand.len() == 2 {
let amount = amount * 3 / 2; let amount = amount * 3 / 2;
balance += amount; balance += amount;
format!("You've won with a Blackjack! You've gained **{amount}** tokens.") format!("You've won with a Blackjack! You've gained **{amount}** token(s).")
} else { } else {
balance += amount; balance += amount;
format!("You've won! **{amount}** tokens have been added to your account.") format!("You've won! **{amount}** token(s) have been added to your account.")
} }
} }
Ordering::Equal => { Ordering::Equal => {
@@ -272,7 +306,7 @@ pub async fn blackjack(ctx: Context<'_>, amount: String) -> Result<(), Error>
} }
Ordering::Greater => { Ordering::Greater => {
balance -= amount; balance -= amount;
format!("You've lost. **{amount}** tokens to the dealer.") format!("You've lost. **{amount}** token(s) to the dealer.")
} }
}; };

View File

@@ -1,58 +1,54 @@
use crate::{Context, Error}; use crate::{Context, Error};
use poise::serenity_prelude::{UserId, User}; use poise::serenity_prelude::{UserId, User};
use sqlx::{types::chrono::{DateTime, Utc, TimeZone}, PgExecutor, Row}; use sqlx::{types::chrono::{DateTime, TimeZone, Utc}, PgConnection};
use std::time::Duration; use std::time::Duration;
async fn get_streak<'a, E>(db: E, user: UserId) -> Result<Option<i32>, Error> pub async fn get_streak(conn: &mut PgConnection, user: UserId) -> Result<Option<i32>, Error> {
where let result = sqlx::query!(
E: PgExecutor<'a>, "SELECT streak FROM dailies WHERE userid = $1",
{ user.get() as i64
match sqlx::query( )
"SELECT streak FROM dailies WHERE userid = $1" .fetch_optional(conn)
).bind(user.get() as i64).fetch_one(db).await .await?;
{
Ok(row) => Ok(Some(row.get(0))), Ok(result.map(|r| r.streak).unwrap_or(None))
Err(sqlx::Error::RowNotFound) => Ok(None),
Err(e) => Err(Box::new(e)),
}
} }
async fn set_streak<'a, E>(db: E, user: UserId, streak: i32) -> Result<(), Error> pub async fn set_streak(conn: &mut PgConnection, user: UserId, streak: i32) -> Result<(), Error> {
where sqlx::query!(
E: PgExecutor<'a>, "INSERT INTO dailies (userid, streak) VALUES ($1, $2)
{ ON CONFLICT (userid) DO UPDATE SET streak = EXCLUDED.streak",
sqlx::query("INSERT INTO dailies (userid, streak) VALUES ($1, $2) ON CONFLICT (userid) DO UPDATE SET streak = EXCLUDED.streak") user.get() as i64,
.bind(user.get() as i64) streak
.bind(streak) )
.execute(db).await?; .execute(conn)
.await?;
Ok(()) Ok(())
} }
async fn get_last<'a, E>(db: E, user: UserId) -> Result<Option<DateTime<Utc>>, Error> pub async fn get_last(conn: &mut PgConnection, user: UserId) -> Result<Option<DateTime<Utc>>, Error> {
where let result = sqlx::query!(
E: PgExecutor<'a>, "SELECT last FROM dailies WHERE userid = $1",
{ user.get() as i64
match sqlx::query( )
"SELECT last FROM dailies WHERE userid = $1" .fetch_optional(conn)
).bind(user.get() as i64).fetch_one(db).await .await?;
{
Ok(row) => Ok(Some(row.get(0))), Ok(result.map(|r| r.last).unwrap_or(None))
Err(sqlx::Error::RowNotFound) => Ok(None),
Err(e) => Err(Box::new(e)),
}
} }
async fn set_last<'a, E>(db: E, user: UserId, last: DateTime<Utc>) -> Result<(), Error> pub async fn set_last(conn: &mut PgConnection, user: UserId, last: DateTime<Utc>) -> Result<(), Error> {
where sqlx::query!(
E: PgExecutor<'a>, "INSERT INTO dailies (userid, last) VALUES ($1, $2)
{ ON CONFLICT (userid) DO UPDATE SET last = EXCLUDED.last",
sqlx::query("INSERT INTO dailies (userid, last) VALUES ($1, $2) ON CONFLICT (userid) DO UPDATE SET last = EXCLUDED.last") user.get() as i64,
.bind(user.get() as i64) last
.bind(last) )
.execute(db).await?; .execute(conn)
.await?;
Ok(()) Ok(())
} }
@@ -60,13 +56,14 @@ where
/// Tells you what your current daily streak is /// Tells you what your current daily streak is
#[poise::command(slash_command, prefix_command)] #[poise::command(slash_command, prefix_command)]
pub async fn streak(ctx: Context<'_>, user: Option<User>) -> Result<(), Error> { pub async fn streak(ctx: Context<'_>, user: Option<User>) -> Result<(), Error> {
let db = &ctx.data().database; let mut tx = ctx.data().database.begin().await?;
let (user, who) = match user { let (user, who) = match user {
Some(user) => (user.id, format!("{} has", user.display_name())), Some(user) => (user.id, format!("{} has", user.display_name())),
None => (ctx.author().id, "You have".to_string()), None => (ctx.author().id, "You have".to_string()),
}; };
ctx.reply(format!("{who} a daily streak of **{}**", get_streak(db, user).await?.unwrap_or(0))).await?; ctx.reply(format!("{who} a daily streak of **{}**", get_streak(&mut tx, user).await?.unwrap_or(0))).await?;
Ok(()) Ok(())
} }

View File

@@ -1,7 +1,6 @@
use crate::common::{Context, Error}; use crate::common::{Context, Error};
use poise::serenity_prelude::UserId; use poise::serenity_prelude::UserId;
use sqlx::Row;
enum LeaderboardType { enum LeaderboardType {
Tokens(usize), Tokens(usize),
@@ -13,15 +12,16 @@ async fn display_leaderboard(ctx: Context<'_>, t: LeaderboardType) -> Result<(),
match t { match t {
LeaderboardType::Tokens(count) => { LeaderboardType::Tokens(count) => {
let rows = sqlx::query( let rows = sqlx::query!(
r#" r#"
SELECT id, balance FROM bank SELECT id, balance FROM bank
ORDER BY balance DESC ORDER BY balance DESC
LIMIT $1 LIMIT $1
"# "#,
).bind(count as i32).fetch_all(db).await?; count as i32
).fetch_all(db).await?;
let users: Vec<(_, i32)> = rows.iter().map(|row| (UserId::new(row.get::<i64, _>(0) as u64), row.get(1))).collect(); let users: Vec<(_, i32)> = rows.iter().map(|row| (UserId::new(row.id as u64), row.balance.unwrap_or(100))).collect();
let mut output = String::new(); let mut output = String::new();
for (id, balance) in users { for (id, balance) in users {
@@ -32,15 +32,16 @@ async fn display_leaderboard(ctx: Context<'_>, t: LeaderboardType) -> Result<(),
ctx.reply(format!("```\n{output}```")).await?; ctx.reply(format!("```\n{output}```")).await?;
} }
LeaderboardType::Dailies(count) => { LeaderboardType::Dailies(count) => {
let rows = sqlx::query( let rows = sqlx::query!(
r#" r#"
SELECT userid, streak FROM dailies SELECT userid, streak FROM dailies
ORDER BY streak DESC ORDER BY streak DESC
LIMIT $1 LIMIT $1
"# "#,
).bind(count as i32).fetch_all(db).await?; count as i32
).fetch_all(db).await?;
let users: Vec<(_, i32)> = rows.iter().map(|row| (UserId::new(row.get::<i64, _>(0) as u64), row.get(1))).collect(); let users: Vec<(_, i32)> = rows.iter().map(|row| (UserId::new(row.userid as u64), row.streak.unwrap_or(0))).collect();
let mut output = String::new(); let mut output = String::new();
for (id, streak) in users { for (id, streak) in users {

View File

@@ -9,7 +9,7 @@ pub mod blackjack;
use crate::{inventory::{self, Inventory}, common::{Context, Error}}; use crate::{inventory::{self, Inventory}, common::{Context, Error}};
use poise::serenity_prelude::{self as serenity, futures::StreamExt, UserId}; use poise::serenity_prelude::{self as serenity, futures::StreamExt, UserId};
use sqlx::{Row, PgExecutor}; use sqlx::PgConnection;
use std::collections::HashMap; use std::collections::HashMap;
#[derive(Clone)] #[derive(Clone)]
@@ -79,16 +79,15 @@ mod items {
} }
} }
pub async fn get_balance<'a, E>(id: UserId, db: E) -> Result<i32, Error> pub async fn get_balance(id: UserId, db: &mut PgConnection) -> Result<i32, Error>
where
E: PgExecutor<'a>,
{ {
let row = sqlx::query("SELECT balance FROM bank WHERE id = $1") let row = sqlx::query!(
.bind(id.get() as i64) "SELECT balance FROM bank WHERE id = $1",
.fetch_one(db).await.ok(); id.get() as i64
).fetch_one(db).await.ok();
let balance = if let Some(row) = row { let balance = if let Some(row) = row {
row.try_get("balance")? row.balance.unwrap_or(100)
} else { } else {
100 100
}; };
@@ -96,14 +95,13 @@ where
Ok(balance) Ok(balance)
} }
pub async fn change_balance<'a, E>(id: UserId, balance: i32, db: E) -> Result<(), Error> pub async fn change_balance(id: UserId, balance: i32, db: &mut PgConnection) -> Result<(), Error>
where
E: PgExecutor<'a>,
{ {
sqlx::query("INSERT INTO bank (id, balance) VALUES ($1, $2) ON CONFLICT (id) DO UPDATE SET balance = EXCLUDED.balance") sqlx::query!(
.bind(id.get() as i64) r#"INSERT INTO bank (id, balance) VALUES ($1, $2)
.bind(balance) ON CONFLICT (id) DO UPDATE SET balance = EXCLUDED.balance"#,
.execute(db).await?; id.get() as i64, balance
).execute(db).await?;
Ok(()) Ok(())
} }

View File

@@ -16,8 +16,8 @@ async fn autocomplete_shop<'a>(
ctx: Context<'_>, ctx: Context<'_>,
partial: &'a str, partial: &'a str,
) -> impl Iterator<Item = serenity::AutocompleteChoice> + use<'a> { ) -> impl Iterator<Item = serenity::AutocompleteChoice> + use<'a> {
let db = &ctx.data().database; let mut tx = ctx.data().database.begin().await.unwrap();
let balance = super::get_balance(ctx.author().id, db).await; let balance = super::get_balance(ctx.author().id, &mut *tx).await;
ITEMS.values() ITEMS.values()
.filter(move |(_, item)| item.name.contains(partial)) .filter(move |(_, item)| item.name.contains(partial))

View File

@@ -38,6 +38,7 @@ pub fn commands() -> Vec<Command<Data, Error>> {
gambling::blackjack::blackjack(), gambling::blackjack::blackjack(),
eval::eval(), eval::eval(),
self_roles::role(), self_roles::role(),
self_roles::editrole(),
settings::setting(), settings::setting(),
administration::ban::ban(), administration::ban::ban(),
administration::ban::unban(), administration::ban::unban(),

View File

@@ -0,0 +1,125 @@
use crate::common::{self, Context, Error, BigBirbError};
use poise::serenity_prelude::{User, Role};
/// Change the name of a user's personal role
#[poise::command(slash_command, prefix_command, required_permissions = "MANAGE_ROLES")]
pub async fn name(ctx: Context<'_>, user: User, #[rest] name: String) -> Result<(), Error> {
let guild = ctx.guild_id().ok_or(BigBirbError::GuildOnly)?;
let role = guild.role(ctx, super::name::change_user_role_name(ctx, &user, guild, name).await?).await?;
common::no_ping_reply(&ctx, format!("{role} has been updated.")).await?;
Ok(())
}
/// Change the name of a user's personal role
#[poise::command(slash_command, prefix_command, required_permissions = "MANAGE_ROLES")]
pub async fn color(ctx: Context<'_>, user: User, color: String) -> Result<(), Error> {
let guild = ctx.guild_id().ok_or(BigBirbError::GuildOnly)?;
let color = super::color::parse_color(&color)?;
let role = guild.role(ctx, super::color::change_user_role_color(ctx, &user, guild, color).await?).await?;
common::no_ping_reply(&ctx, format!("{role}'s color has been updated.")).await?;
Ok(())
}
/// Change a user's role name and color at once
#[poise::command(slash_command, prefix_command, required_permissions = "MANAGE_ROLES")]
pub async fn set(ctx: Context<'_>, user: User, color: String, #[rest] name: String) -> Result<(), Error> {
let guild = ctx.guild_id().ok_or(BigBirbError::GuildOnly)?;
let color = super::color::parse_color(&color)?;
super::color::change_user_role_color(ctx, &user, guild, color).await?;
let role = guild.role(ctx, super::name::change_user_role_name(ctx, &user, guild, name).await?).await?;
common::no_ping_reply(&ctx, format!("{role} has been updated.")).await?;
Ok(())
}
/// Remove and delete user's self role
#[poise::command(slash_command, prefix_command, aliases("disown"), required_permissions = "MANAGE_ROLES")]
pub async fn remove(ctx: Context<'_>, user: User) -> Result<(), Error> {
let guild = ctx.guild_id().ok_or(BigBirbError::GuildOnly)?;
let mut tx = ctx.data().database.begin().await?;
if let Some(role) = super::get_user_role(user.id, guild, &mut *tx).await? {
guild.delete_role(ctx, role).await?;
super::remove_role(role, guild, &mut *tx).await?;
tx.commit().await?;
common::no_ping_reply(&ctx, format!("{user}'s self role has been deleted.")).await?;
} else {
common::no_ping_reply(&ctx, format!("{user} has no self role.")).await?;
}
Ok(())
}
/// Remove a selfrole from the database without deleting it or attempting to take it from the user
#[poise::command(slash_command, prefix_command, required_permissions = "MANAGE_ROLES")]
pub async fn forget(ctx: Context<'_>, user: User) -> Result<(), Error> {
let guild = ctx.guild_id().ok_or(BigBirbError::GuildOnly)?;
let mut tx = ctx.data().database.begin().await?;
if let Some(role) = super::get_user_role(user.id, guild, &mut *tx).await? {
super::remove_role(role, guild, &mut *tx).await?;
common::no_ping_reply(&ctx, format!("{user}'s selfrole has been forgotten.")).await?;
} else {
common::no_ping_reply(&ctx, format!("{user} has no selfrole set.")).await?;
}
tx.commit().await?;
Ok(())
}
/// Give a user an existing role as their self role
#[poise::command(slash_command, prefix_command, required_permissions = "MANAGE_ROLES")]
pub async fn give(ctx: Context<'_>, user: User, role: Role, force: Option<bool>) -> Result<(), Error> {
let guild = ctx.guild_id().ok_or(BigBirbError::GuildOnly)?;
let force = force.unwrap_or(false);
let member = guild.member(ctx, user).await?;
let mut tx = ctx.data().database.begin().await?;
if force {
// delete existing self role for user
if let Some(original) = super::get_user_role(member.user.id, guild, &mut *tx).await? {
guild.delete_role(ctx, original).await?;
super::remove_role(role.id, guild, &mut *tx).await?;
}
// remove role from another user if it is already registered as their self role
if let Some(user) = super::get_user_by_role(role.id, guild, &mut *tx).await? {
let m = guild.member(ctx, user).await?;
m.remove_role(ctx, role.id).await?;
super::remove_role(role.id, guild, &mut *tx).await?;
}
super::update_user_role(member.user.id, guild, role.id, &mut *tx).await?;
member.add_role(ctx, role.id).await?;
} else {
if let Some(original) = super::get_user_role(member.user.id, guild, &mut *tx).await? {
let original = guild.role(ctx, original).await?;
common::no_ping_reply(&ctx, format!("{original} is already set as this user's self role, enable force to overwrite.")).await?;
return Ok(());
}
if let Some(owner) = super::get_user_by_role(role.id, guild, &mut *tx).await? {
let owner = owner.to_user(ctx).await?;
common::no_ping_reply(&ctx, format!("{role} is already owned by {owner}, enable force to overwrite.")).await?;
return Ok(());
}
super::update_user_role(member.user.id, guild, role.id, &mut *tx).await?;
}
tx.commit().await?;
common::no_ping_reply(&ctx, format!("{member} has been given the self role {role}.")).await?;
Ok(())
}

View File

@@ -1,11 +1,11 @@
use crate::common::{self, Context, Error}; use crate::common::{self, Context, Error, BigBirbError};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::collections::HashMap; use std::collections::HashMap;
use hex_color::HexColor; use hex_color::HexColor;
use poise::serenity_prelude::{colours, Color, EditRole}; use poise::serenity_prelude::{colours, Color, User, GuildId, RoleId, EditRole};
static COLORS: Lazy<HashMap<&'static str, Color>> = Lazy::new(|| { static COLORS: Lazy<HashMap<&'static str, Color>> = Lazy::new(|| {
HashMap::from([ HashMap::from([
@@ -59,6 +59,25 @@ async fn autocomplete_colors<'a>(
COLORS.clone().into_keys().filter(move |x| x.split_whitespace().any(|x| x.starts_with(partial))) COLORS.clone().into_keys().filter(move |x| x.split_whitespace().any(|x| x.starts_with(partial)))
} }
pub fn parse_color(s: &str) -> Result<Color, Error> {
let color = if let Some(named) = COLORS.get(s) {
named.clone()
} else {
let rgb = HexColor::parse_rgb(&s)?;
Color::from_rgb(rgb.r, rgb.g, rgb.b)
};
Ok(color)
}
pub async fn change_user_role_color(ctx: Context<'_>, user: &User, guild: GuildId, color: Color) -> Result<RoleId, Error> {
let mut tx = ctx.data().database.begin().await?;
let role = super::edit_role(ctx, user.id, guild, EditRole::new().colour(color), &mut *tx).await?;
tx.commit().await?;
Ok(role)
}
/// Change the color of your personal role /// Change the color of your personal role
#[poise::command(slash_command, prefix_command)] #[poise::command(slash_command, prefix_command)]
pub async fn color(ctx: Context<'_>, pub async fn color(ctx: Context<'_>,
@@ -66,27 +85,12 @@ pub async fn color(ctx: Context<'_>,
#[rest] #[rest]
color: String) -> Result<(), Error> color: String) -> Result<(), Error>
{ {
let color = if let Some(named) = COLORS.get(color.as_str()) { let guild = ctx.guild_id().ok_or(BigBirbError::GuildOnly)?;
named.clone()
} else {
let rgb = HexColor::parse_rgb(&color)?;
Color::from_rgb(rgb.r, rgb.g, rgb.b)
};
let guild = if let Some(guild) = ctx.guild_id() {
guild
} else {
ctx.reply("This command can only be run inside of a guild.").await?;
return Ok(());
};
let user = ctx.author(); let user = ctx.author();
let color = parse_color(&color)?;
let mut tx = ctx.data().database.begin().await?; let role = guild.role(ctx, change_user_role_color(ctx, &user, guild, color).await?).await?;
let role = super::edit_role(ctx, user.id, guild, EditRole::new().colour(color), &mut *tx).await?; common::no_ping_reply(&ctx, format!("{role}'s color has been updated.")).await?;
tx.commit().await?;
common::no_ping_reply(&ctx, format!("{}'s color has been updated.", guild.role(ctx, role).await?)).await?;
Ok(()) Ok(())
} }

View File

@@ -1,16 +1,10 @@
use crate::common::{Context, Error}; use crate::common::{Context, Error, BigBirbError};
/// Remove and delete your personal role /// Remove and delete your personal role
#[poise::command(slash_command, prefix_command)] #[poise::command(slash_command, prefix_command)]
pub async fn disown(ctx: Context<'_>) -> Result<(), Error> { pub async fn disown(ctx: Context<'_>) -> Result<(), Error> {
let guild = if let Some(guild) = ctx.guild_id() { let guild = ctx.guild_id().ok_or(BigBirbError::GuildOnly)?;
guild
} else {
ctx.reply("This command can only be run inside of a guild.").await?;
return Ok(());
};
let user = ctx.author(); let user = ctx.author();
let mut tx = ctx.data().database.begin().await?; let mut tx = ctx.data().database.begin().await?;

View File

@@ -1,31 +1,47 @@
use crate::common::{Context, Error}; use crate::common::{Context, Error};
use sqlx::{PgConnection, Row}; use sqlx::PgConnection;
use poise::serenity_prelude::{EditRole, GuildId, Permissions, RoleId, UserId}; use poise::serenity_prelude::{EditRole, GuildId, Permissions, RoleId, UserId};
mod register;
mod whois; mod whois;
mod color; pub mod color;
mod name; pub mod name;
mod disown; mod disown;
mod remove;
mod admin;
#[poise::command( #[poise::command(
prefix_command, prefix_command,
slash_command, slash_command,
subcommands( subcommands(
"register::register",
"whois::whois",
"color::color",
"name::name", "name::name",
"color::color",
"disown::disown", "disown::disown",
"remove::remove", "whois::whois",
) )
)] )]
pub async fn role(_ctx: Context<'_>) -> Result<(), Error> { pub async fn role(_ctx: Context<'_>) -> Result<(), Error> {
Ok(()) Ok(())
} }
#[poise::command(
prefix_command,
slash_command,
subcommands(
"admin::name",
"admin::color",
"admin::remove",
"admin::forget",
"admin::set",
"admin::give",
"whois::whois",
),
required_permissions = "MANAGE_ROLES",
)]
pub async fn editrole(_ctx: Context<'_>) -> Result<(), Error> {
Ok(())
}
/// Edit a user's personal role, creates it with some default values if it doesn't exist. /// Edit a user's personal role, creates it with some default values if it doesn't exist.
pub async fn edit_role(ctx: Context<'_>, user: UserId, guild: GuildId, edit: EditRole<'_>, db: &mut PgConnection) -> Result<RoleId, Error> pub async fn edit_role(ctx: Context<'_>, user: UserId, guild: GuildId, edit: EditRole<'_>, db: &mut PgConnection) -> Result<RoleId, Error>
{ {
@@ -67,18 +83,14 @@ async fn create_role(
/// Remove a row concerning a user's self role from the database /// Remove a row concerning a user's self role from the database
pub async fn remove_user_role(user: UserId, guild: GuildId, db: &mut PgConnection) -> Result<(), Error> { pub async fn remove_user_role(user: UserId, guild: GuildId, db: &mut PgConnection) -> Result<(), Error> {
sqlx::query("DELETE FROM selfroles WHERE userid = $1 AND guildid = $2") sqlx::query!("DELETE FROM selfroles WHERE userid = $1 AND guildid = $2", user.get() as i64, guild.get() as i64)
.bind(user.get() as i64)
.bind(guild.get() as i64)
.execute(db).await?; .execute(db).await?;
Ok(()) Ok(())
} }
pub async fn remove_role(role: RoleId, guild: GuildId, db: &mut PgConnection) -> Result<(), Error> { pub async fn remove_role(role: RoleId, guild: GuildId, db: &mut PgConnection) -> Result<(), Error> {
sqlx::query("DELETE FROM selfroles WHERE roleid = $1 AND guildid = $2") sqlx::query!("DELETE FROM selfroles WHERE roleid = $1 AND guildid = $2", role.get() as i64, guild.get() as i64)
.bind(role.get() as i64)
.bind(guild.get() as i64)
.execute(db).await?; .execute(db).await?;
Ok(()) Ok(())
@@ -86,10 +98,10 @@ pub async fn remove_role(role: RoleId, guild: GuildId, db: &mut PgConnection) ->
/// Replace a user's custom role with a new one /// Replace a user's custom role with a new one
pub async fn update_user_role(user: UserId, guild: GuildId, role: RoleId, db: &mut PgConnection) -> Result<(), Error> { pub async fn update_user_role(user: UserId, guild: GuildId, role: RoleId, db: &mut PgConnection) -> Result<(), Error> {
sqlx::query("INSERT INTO selfroles (userid, guildid, roleid) VALUES($1, $2, $3) ON CONFLICT (userid, guildid) DO UPDATE SET roleid = EXCLUDED.roleid") sqlx::query!(
.bind(user.get() as i64) r#"INSERT INTO selfroles (userid, guildid, roleid) VALUES($1, $2, $3)
.bind(guild.get() as i64) ON CONFLICT (userid, guildid) DO UPDATE SET roleid = EXCLUDED.roleid"#,
.bind(role.get() as i64) user.get() as i64, guild.get() as i64, role.get() as i64)
.execute(db).await?; .execute(db).await?;
Ok(()) Ok(())
@@ -97,12 +109,21 @@ pub async fn update_user_role(user: UserId, guild: GuildId, role: RoleId, db: &m
/// Get a user's personal role id from the database /// Get a user's personal role id from the database
pub async fn get_user_role(user: UserId, guild: GuildId, db: &mut PgConnection) -> Result<Option<RoleId>, Error> { pub async fn get_user_role(user: UserId, guild: GuildId, db: &mut PgConnection) -> Result<Option<RoleId>, Error> {
match sqlx::query("SELECT roleid FROM selfroles WHERE userid = $1 AND guildid = $2") match sqlx::query!("SELECT roleid FROM selfroles WHERE userid = $1 AND guildid = $2", user.get() as i64, guild.get() as i64)
.bind(user.get() as i64)
.bind(guild.get() as i64)
.fetch_one(db).await .fetch_one(db).await
{ {
Ok(row) => Ok(Some(RoleId::new(row.try_get::<i64, usize>(0)? as u64))), Ok(row) => Ok(Some(RoleId::new(row.roleid.unwrap() as u64))),
Err(sqlx::Error::RowNotFound) => Ok(None),
Err(e) => return Err(Box::new(e)),
}
}
/// Get a user from the role id
pub async fn get_user_by_role(role: RoleId, guild: GuildId, db: &mut PgConnection) -> Result<Option<UserId>, Error> {
match sqlx::query!("SELECT userid FROM selfroles WHERE roleid = $1 AND guildid = $2", role.get() as i64, guild.get() as i64)
.fetch_one(db).await
{
Ok(row) => Ok(Some(UserId::new(row.userid as u64))),
Err(sqlx::Error::RowNotFound) => Ok(None), Err(sqlx::Error::RowNotFound) => Ok(None),
Err(e) => return Err(Box::new(e)), Err(e) => return Err(Box::new(e)),
} }

View File

@@ -1,26 +1,23 @@
use crate::common::{self, Context, Error}; use crate::common::{self, Context, Error, BigBirbError};
use poise::serenity_prelude::{EditRole, User, GuildId, RoleId};
use poise::serenity_prelude::EditRole;
/// Change the name of your personal role
#[poise::command(slash_command, prefix_command)]
pub async fn name(ctx: Context<'_>, #[rest] name: String) -> Result<(), Error> {
let guild = if let Some(guild) = ctx.guild_id() {
guild
} else {
ctx.reply("This command can only be run inside of a guild.").await?;
return Ok(());
};
let user = ctx.author();
pub async fn change_user_role_name(ctx: Context<'_>, user: &User, guild: GuildId, name: String) -> Result<RoleId, Error> {
let mut tx = ctx.data().database.begin().await?; let mut tx = ctx.data().database.begin().await?;
let role = super::edit_role(ctx, user.id, guild, EditRole::new().name(name), &mut *tx).await?; let role = super::edit_role(ctx, user.id, guild, EditRole::new().name(name), &mut *tx).await?;
tx.commit().await?; tx.commit().await?;
common::no_ping_reply(&ctx, format!("{} has been updated.", guild.role(ctx, role).await?)).await?; Ok(role)
}
/// Change the name of your personal role
#[poise::command(slash_command, prefix_command, )]
pub async fn name(ctx: Context<'_>, #[rest] name: String) -> Result<(), Error> {
let guild = ctx.guild_id().ok_or(BigBirbError::GuildOnly)?;
let user = ctx.author();
let role = guild.role(ctx, change_user_role_name(ctx, user, guild, name).await?).await?;
common::no_ping_reply(&ctx, format!("{role} has been updated.")).await?;
Ok(()) Ok(())
} }

View File

@@ -1,31 +0,0 @@
use crate::common::{self, Context, Error};
use poise::serenity_prelude as serenity;
/// Register an existing role as a user's custom role. This deletes their current self role.
#[poise::command(slash_command, prefix_command, required_permissions = "MANAGE_ROLES")]
pub async fn register(ctx: Context<'_>, user: serenity::User, role: serenity::Role) -> Result<(), Error> {
let guild = if let Some(guild) = ctx.guild_id() {
guild
} else {
ctx.reply("This command can only be run inside of a guild.").await?;
return Ok(());
};
let mut tx = ctx.data().database.begin().await?;
if let Some(role) = super::get_user_role(user.id, guild, &mut *tx).await? {
guild.delete_role(ctx, role).await?;
}
let member = guild.member(ctx, user).await?;
member.add_role(ctx, role.id).await?;
super::update_user_role(member.user.id, guild, role.id, &mut *tx).await?;
tx.commit().await?;
common::no_ping_reply(&ctx, format!("{} has been set as {}'s self role.", role, member.user)).await?;
Ok(())
}

View File

@@ -1,27 +0,0 @@
use crate::common::{self, Context, Error};
use poise::serenity_prelude as serenity;
#[poise::command(slash_command, prefix_command, required_permissions = "MANAGE_ROLES")]
pub async fn remove(ctx: Context<'_>, user: serenity::User) -> Result<(), Error> {
let guild = if let Some(guild) = ctx.guild_id() {
guild
} else {
ctx.reply("This command can only be run inside of a guild.").await?;
return Ok(());
};
let mut tx = ctx.data().database.begin().await?;
if let Some(role) = super::get_user_role(user.id, guild, &mut *tx).await? {
guild.delete_role(ctx, role).await?;
super::remove_role(role, guild, &mut *tx).await?;
tx.commit().await?;
common::no_ping_reply(&ctx, format!("{}'s self role has been deleted.", user)).await?;
} else {
common::no_ping_reply(&ctx, format!("{} has no self role.", user)).await?;
}
Ok(())
}

View File

@@ -3,7 +3,6 @@ use crate::common::{self, Context, Error};
use poise::serenity_prelude as serenity; use poise::serenity_prelude as serenity;
use serenity::UserId; use serenity::UserId;
use sqlx::Row;
/// Let you know who is the owner of a role. /// Let you know who is the owner of a role.
#[poise::command(slash_command, prefix_command)] #[poise::command(slash_command, prefix_command)]
@@ -11,11 +10,10 @@ pub async fn whois(ctx: Context<'_>, role: serenity::Role) -> Result<(), Error>
let db = &ctx.data().database; let db = &ctx.data().database;
if let Some(guild) = ctx.guild_id() { if let Some(guild) = ctx.guild_id() {
let user = match sqlx::query("SELECT userid FROM selfroles WHERE roleid = $1") let user = match sqlx::query!("SELECT userid FROM selfroles WHERE roleid = $1", role.id.get() as i64)
.bind(role.id.get() as i64)
.fetch_one(db).await .fetch_one(db).await
{ {
Ok(row) => UserId::new(row.try_get::<i64, usize>(0)? as u64), Ok(row) => UserId::new(row.userid as u64),
Err(sqlx::Error::RowNotFound) => { Err(sqlx::Error::RowNotFound) => {
ctx.reply("This role is not owned by anyone.").await?; ctx.reply("This role is not owned by anyone.").await?;
return Ok(()); return Ok(());
@@ -25,7 +23,7 @@ pub async fn whois(ctx: Context<'_>, role: serenity::Role) -> Result<(), Error>
let member = guild.member(ctx, user).await?; let member = guild.member(ctx, user).await?;
common::no_ping_reply(&ctx, format!("{} owns this role.", member)).await?; common::no_ping_reply(&ctx, format!("{member} owns this role.")).await?;
} else { } else {
ctx.reply("This command must be used within a server!").await?; ctx.reply("This command must be used within a server!").await?;
} }

View File

@@ -1,4 +1,4 @@
use crate::common::{self, Context, Error}; use crate::common::{self, Context, Error, BigBirbError};
use poise::serenity_prelude::{Role, RoleId, GuildId}; use poise::serenity_prelude::{Role, RoleId, GuildId};
use sqlx::Row; use sqlx::Row;
@@ -20,13 +20,7 @@ async fn get_prefix(ctx: Context<'_>, guild: GuildId) -> Result<Option<String>,
#[poise::command(prefix_command, slash_command)] #[poise::command(prefix_command, slash_command)]
async fn prefix(ctx: Context<'_>, prefix: Option<String>) -> Result<(), Error> { async fn prefix(ctx: Context<'_>, prefix: Option<String>) -> Result<(), Error> {
let guild = match ctx.guild_id() { let guild = ctx.guild_id().ok_or(BigBirbError::GuildOnly)?;
Some(g) => g,
None => {
ctx.reply("This command must be ran within a guild.").await?;
return Ok(());
}
};
match prefix { match prefix {
Some(prefix) => { Some(prefix) => {
@@ -74,14 +68,7 @@ pub async fn get_positional_role(ctx: Context<'_>, guild: GuildId) -> Result<Opt
#[poise::command(prefix_command, slash_command)] #[poise::command(prefix_command, slash_command)]
pub async fn position(ctx: Context<'_>, role: Option<Role>) -> Result<(), Error> { pub async fn position(ctx: Context<'_>, role: Option<Role>) -> Result<(), Error> {
let guild = match ctx.guild_id() { let guild = ctx.guild_id().ok_or(BigBirbError::GuildOnly)?;
Some(g) => g,
None => {
ctx.reply("This command must be ran within a guild.").await?;
return Ok(());
}
};
let member = ctx.author_member().await.unwrap(); let member = ctx.author_member().await.unwrap();
if !member.permissions(ctx).iter().any(|p| p.manage_guild()) { if !member.permissions(ctx).iter().any(|p| p.manage_guild()) {
@@ -132,13 +119,7 @@ pub async fn get_hoist_selfroles(ctx: Context<'_>, guild: GuildId) -> Result<boo
#[poise::command(prefix_command, slash_command)] #[poise::command(prefix_command, slash_command)]
pub async fn hoist(ctx: Context<'_>, hoist: Option<bool>) -> Result<(), Error> { pub async fn hoist(ctx: Context<'_>, hoist: Option<bool>) -> Result<(), Error> {
let guild = match ctx.guild_id() { let guild = ctx.guild_id().ok_or(BigBirbError::GuildOnly)?;
Some(g) => g,
None => {
ctx.reply("This command must be ran within a guild.").await?;
return Ok(());
}
};
match hoist { match hoist {
Some(hoist) => { Some(hoist) => {
@@ -195,14 +176,7 @@ pub async fn get_banrole(ctx: Context<'_>, guild: GuildId) -> Result<Option<Role
#[poise::command(prefix_command, slash_command)] #[poise::command(prefix_command, slash_command)]
pub async fn banrole(ctx: Context<'_>, role: Option<Role>) -> Result<(), Error> { pub async fn banrole(ctx: Context<'_>, role: Option<Role>) -> Result<(), Error> {
let guild = match ctx.guild_id() { let guild = ctx.guild_id().ok_or(BigBirbError::GuildOnly)?;
Some(g) => g,
None => {
ctx.reply("This command must be ran within a guild.").await?;
return Ok(());
}
};
let member = ctx.author_member().await.unwrap(); let member = ctx.author_member().await.unwrap();
if !member.permissions(ctx).iter().any(|p| p.manage_guild()) { if !member.permissions(ctx).iter().any(|p| p.manage_guild()) {

View File

@@ -1,3 +1,4 @@
use std::{error, fmt};
use poise::ReplyHandle; use poise::ReplyHandle;
use sqlx::{Pool, Postgres}; use sqlx::{Pool, Postgres};
@@ -20,3 +21,20 @@ pub async fn no_ping_reply<'a>(ctx: &'a Context<'_>, text: impl Into<String>) ->
.allowed_mentions(CreateAllowedMentions::new()) .allowed_mentions(CreateAllowedMentions::new())
).await?) ).await?)
} }
#[derive(Debug, Clone, Copy)]
pub enum BigBirbError {
GuildOnly,
}
impl fmt::Display for BigBirbError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = match self {
Self::GuildOnly => "This command must be run inside of a guild.",
};
write!(f, "{s}")
}
}
impl error::Error for BigBirbError {}

View File

@@ -55,9 +55,8 @@ async fn event_handler(
} }
async fn get_prefix(ctx: PartialContext<'_, Data, Error>) -> Result<Option<String>, Error> { async fn get_prefix(ctx: PartialContext<'_, Data, Error>) -> Result<Option<String>, Error> {
let guild = match ctx.guild_id { let Some(guild) = ctx.guild_id else {
Some(guild) => guild, return Ok(ctx.data.prefix.clone());
None => return Ok(ctx.data.prefix.clone()),
}; };
let db = &ctx.data.database; let db = &ctx.data.database;
@@ -112,69 +111,9 @@ async fn main() -> Result<(), Error> {
.max_connections(5) .max_connections(5)
.connect(&database_url).await?; .connect(&database_url).await?;
sqlx::query( sqlx::migrate!()
r#" .run(&database)
CREATE TABLE IF NOT EXISTS bank ( .await?;
id BIGINT PRIMARY KEY,
balance INT
)
"#,
).execute(&database).await?;
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS selfroles (
userid BIGINT NOT NULL,
guildid BIGINT NOT NULL,
roleid BIGINT,
UNIQUE (userid, guildid)
)
"#,
).execute(&database).await?;
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS games (
id BIGSERIAL PRIMARY KEY,
name CHAR[255]
)
"#
).execute(&database).await?;
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS items (
id BIGSERIAL PRIMARY KEY,
owner BIGINT NOT NULL,
game BIGINT NOT NULL,
item BIGINT NOT NULL,
data JSON NOT NULL,
name TEXT
)
"#
).execute(&database).await?;
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS dailies (
userid BIGINT NOT NULL PRIMARY KEY,
last TIMESTAMPTZ,
streak INT
)
"#
).execute(&database).await?;
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS settings (
guildid BIGINT NOT NULL PRIMARY KEY,
positional_role BIGINT,
banrole BIGINT,
hoist_selfroles BOOLEAN,
prefix TEXT
)
"#
).execute(&database).await?;
println!("Bot is ready!"); println!("Bot is ready!");