Skip to content

Commit

Permalink
Merge pull request #8 from perpetualcacophony/main
Browse files Browse the repository at this point in the history
merge 1.3 to prod
  • Loading branch information
perpetualcacophony committed Jan 19, 2024
2 parents 7d69a0b + 23d06b0 commit a5beded
Show file tree
Hide file tree
Showing 17 changed files with 973 additions and 111 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/docker-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ on:
push:
branches: [ "prod" ]
# Publish semver tags as releases.
tags: [ 'v*.*.*' ]
tags: [ 'v*.*' ]

env:
# Use docker.io for Docker Hub if empty
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
[package]
name = "slimebot"
version = "1.2.0"
version = "1.3.0"
edition = "2021"
readme = "README.md"
license = "AGPL-3.0"

[profile.release]
strip = true
Expand Down
661 changes: 661 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# 🌸 slimebot 🌸
slimebot is a small self-hosted discord bot made for a private server with friends.

# 🐞 coming from the server?
hey, thanks for checking out the code!! if you have a feature to request or a bug to report, you can always dm it to me directly, but i would really really appreciate if you put it in the [issues](https://github.com/perpetualcacophony/slimebot/issues) page.

## want to contribute?
developing this bot *is* fun, but does take a good amount of time and effort, so contributing would be super helpful!! the bot itself is entirely written in rust, which i can absolutely help you learn if you're interested, but in the future there might be additional features that involve web development.

## what's with slimebot-dev?
slimebot-dev also runs on this codebase! slimebot proper runs on an actual webserver that lets it stay up all the time, while slimebot-dev just runs off my computer. additionally, slimebot proper runs on the stable code in the [`prod`](https://github.com/perpetualcacophony/slimebot/tree/prod) branch, while slimebot-dev runs on whatever unstable branch i'm currently writing and testing. slimebot-dev exists so i can develop the bot while keeping your experience using slimebot relatively seamless!

# 🐞 coming from somewhere else?
hi!! this bot (and the server it's built for) is riddled with in-jokes and dumb features. it *is* a bot that *does* work—the [`prod`](https://github.com/perpetualcacophony/slimebot/tree/prod) branch is, at least—and you could probably deploy it to your own hardware, but you're probably just better off taking what you like from the codebase. unless you *want* the bot to post [this image of joe biden](https://files.catbox.moe/v7itt0.webp) every time someone says "L", i guess?

## can i use this bot in my own server?
yes and no. you're completely free to compile and run the code yourself, or use the docker image at [`ghcr.io/perpetualcacophony/slimebot:prod`](https://ghcr.io/perpetualcacophony/slimebot) (check out the example [`compose.yaml`](example-compose.yaml)!) however, you'll need to use your own bot user—the bot application i operate is private server-only, which is why you won't find any invite link for slimebot.
21 changes: 21 additions & 0 deletions example-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# example slimebot compose.toml
# IMPORTANT: docker compose will accept `compose.yaml`, NOT `example-compose.yaml`

services:
db:
image: mongo
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: #mongodb root username
MONGO_INITDB_ROOT_PASSWORD: #mongodb root password
ports:
- 27017:27017 #mongodb port

bot:
build: .
restart: always
volumes:
- ./.env:/.env #mount .env to /.env in the container
- ./slimebot.toml:/slimebot.toml #mount slimebot.toml to /slimebot.toml in the container
ports:
- 443:443
4 changes: 4 additions & 0 deletions example-dotenv.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# example slimebot .env
# IMPORTANT: slimebot will accept `.env`, NOT `example-dotenv.env`

RUST_LOG = "slimebot,tracing_unwrap" # crates allowed to emit logs - should not need editing
20 changes: 20 additions & 0 deletions example-slimebot.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# example slimebot.toml
# IMPORTANT: slimebot will accept `slimebot.toml`, NOT `example-slimebot.toml`

[bot]
token = "your token here"
id = 1111111111111111111 #bot id
activity = "playing on YOUR SERVER" #activity options: 'playing', 'listening to', 'watching', 'competing'
prefix = ".."

[db]
url = "localhost:27017" #mongodb host (without `mongodb://`)
username = "mongodb username"
password = "mongodb password"

[watchers]
allow_by_default = true #enables watchers for all channels not specifically disallowed

[[watchers.channels]]
id = 1111111111111111111 #channel id
allow = false #disables watchers for this channel
78 changes: 58 additions & 20 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use poise::serenity_prelude::{ChannelId, GuildId, UserId};
use poise::serenity_prelude::{Activity, ChannelId, GuildId, UserId};
use serde::Deserialize;
use tracing::{info, warn};
use tracing::{debug, error, info, warn};
use tracing_unwrap::OptionExt;

use crate::DiscordToken;
Expand All @@ -18,7 +18,8 @@ pub struct BotConfig {
token: Option<DiscordToken>,
id: Option<UserId>,
pub testing_server: Option<GuildId>,
pub status: Option<String>,
activity: Option<String>,
prefix: String,
}

impl BotConfig {
Expand All @@ -32,6 +33,44 @@ impl BotConfig {
self.id
.expect_or_log("no user id in config or environment!")
}

pub fn activity(&self) -> Option<Activity> {
if let Some(activity) = &self.activity {
if activity.is_empty() {
warn!("bot.activity provided in config as empty string, defaulting to none");
return None;
}

let parsed_activity = if activity.starts_with("playing ") {
Activity::playing(activity.strip_prefix("playing ").unwrap())
} else if activity.starts_with("listening to ") {
Activity::playing(activity.strip_prefix("listening to ").unwrap())
} else if activity.starts_with("watching ") {
Activity::playing(activity.strip_prefix("watching ").unwrap())
} else if activity.starts_with("competing in ") {
Activity::playing(activity.strip_prefix("competing in ").unwrap())
} else {
error!("bot.activity in config could not be parsed - must start with `playing`, `listening to`, `watching` or `competing in`");
warn!("disabling bot activity");
return None;
};

debug!(
"bot.activity parsed as {:?}: {}",
parsed_activity.kind, parsed_activity.name
);
info!("successfully parsed bot activity from config");

Some(parsed_activity)
} else {
warn!("no bot.activity provided in config, defaulting to none");
None
}
}

pub fn prefix(&self) -> &str {
&self.prefix
}
}

#[derive(Deserialize, Debug, Clone)]
Expand All @@ -57,12 +96,11 @@ impl DiscordConfig {

pub fn channel(&self) -> Option<ChannelId> {
if self.enabled {
match self.channel {
Some(_) => Some(self.channel.unwrap()),
None => {
warn!("no channel configured for discord logger");
None
}
if let Some(channel) = self.channel {
Some(channel)
} else {
warn!("no channel configured for discord logger");
None
}
} else {
None
Expand Down Expand Up @@ -98,24 +136,24 @@ pub struct WatchersConfig {
}

impl WatchersConfig {
pub fn allow_by_default(&self) -> bool {
pub const fn allow_by_default(&self) -> bool {
self.allow_by_default
}

pub fn channels(&self) -> Option<&Vec<WatchersChannelConfig>> {
pub const fn channels(&self) -> Option<&Vec<WatchersChannelConfig>> {
self.channels.as_ref()
}

pub fn channel_allowed(&self, id: ChannelId) -> bool {
if let Some(channels) = self.channels() {
if let Some(channel) = channels.iter().find(|c| c.id == id) {
channel.allow
} else {
self.allow_by_default()
}
} else {
self.allow_by_default()
}
self.channels().map_or_else(
|| self.allow_by_default(),
|channels| {
channels
.iter()
.find(|c| c.id == id)
.map_or_else(|| self.allow_by_default(), |channel| channel.allow)
},
)
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use mongodb::{

use crate::config::DbConfig;

pub async fn connect(config: &DbConfig) -> Database {
pub fn database(config: &DbConfig) -> Database {
let credential = Credential::builder()
.username(config.username().to_string())
.password(config.password().to_string())
Expand Down
10 changes: 5 additions & 5 deletions src/discord/commands/ban.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ pub async fn joke_ban(
moderator_id: u64,
reason: impl Into<Option<String>>,
) -> Result<(), Error> {
let reason = reason.into().unwrap_or("No reason".to_string());
let reason = reason.into().unwrap_or_else(|| "No reason".to_string());

let embed = ban_embed(&reason, &moderator_id, &user.name);
let embed = ban_embed(&reason, moderator_id, &user.name);
let webhook = wick_webhook(ctx).await;
webhook
.execute(ctx.http(), false, |w| w.embeds(vec![embed]))
Expand Down Expand Up @@ -60,7 +60,7 @@ async fn wick_webhook(ctx: Context<'_>) -> Webhook {
if &hook.name.clone().unwrap() != wick.display_name().as_ref() {
hook.edit_name(ctx.http(), wick.display_name().as_ref())
.await
.unwrap_or_log()
.unwrap_or_log();
}

if hook.avatar.clone().is_none() || hook.avatar.clone().unwrap() != wick.face() {
Expand All @@ -69,13 +69,13 @@ async fn wick_webhook(ctx: Context<'_>) -> Webhook {
AttachmentType::Image(Url::parse(&wick.face()).unwrap()),
)
.await
.unwrap_or_log()
.unwrap_or_log();
}

hook
}

fn ban_embed(reason: &str, moderator_id: &u64, user: &str) -> serde_json::value::Value {
fn ban_embed(reason: &str, moderator_id: u64, user: &str) -> serde_json::value::Value {
Embed::fake(|e| {
e
.title("Ban result:")
Expand Down
83 changes: 70 additions & 13 deletions src/discord/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,60 @@ mod ban;
mod watch_fic;

use poise::serenity_prelude::{Channel, Member, User};
use tokio::join;
use tracing::{error, info, instrument};

type Error = Box<dyn std::error::Error + Send + Sync>;
type Context<'a> = poise::Context<'a, crate::Data, Error>;

pub use watch_fic::watch_fic;

use crate::FormatDuration;

/// Responds on successful execution.

#[instrument(skip_all)]
#[poise::command(slash_command, prefix_command)]
pub async fn ping(ctx: Context<'_>) -> Result<(), Error> {
let channel = ctx
.channel_id()
.name(ctx.cache())
.await
.map_or("dms".to_string(), |c| format!("#{c}"));
let (channel, ping) = join!(ctx.channel_id().name(ctx.cache()), ctx.ping(),);

info!(
"@{} ({}): {}",
ctx.author().name,
channel,
channel.map_or("dms".to_string(), |c| format!("#{c}")),
ctx.invocation_string()
);

let ping = ping.as_millis();
if ping == 0 {
ctx.say("pong! (please try again later to display latency)")
.await?;
} else {
ctx.say(format!("pong! ({}ms)", ping)).await?;
}

Ok(())
}

#[instrument(skip_all)]
#[poise::command(slash_command, prefix_command)]
pub async fn pong(ctx: Context<'_>) -> Result<(), Error> {
let (channel, ping) = join!(ctx.channel_id().name(ctx.cache()), ctx.ping(),);

info!(
"@{} ({}): {}",
ctx.author().name,
channel.map_or("dms".to_string(), |c| format!("#{c}")),
ctx.invocation_string()
);

ctx.say("pong!").await?;
let ping = ping.as_millis();
if ping == 0 {
ctx.say("ping! (please try again later to display latency)")
.await?;
} else {
ctx.say(format!("ping! ({}ms)", ping)).await?;
}

Ok(())
}
Expand All @@ -53,7 +82,7 @@ pub async fn pfp(
// debug!("{:?}", ctx.guild_id());

if ctx.defer().await.is_err() {
error!("failed to defer - lag will cause errors!")
error!("failed to defer - lag will cause errors!");
}

let user = match user {
Expand All @@ -74,18 +103,19 @@ pub async fn pfp(
// required args are ugly
let global = global.map_or(false, |b| b);

let (pfp, pfp_type) = match global {
true => (
let (pfp, pfp_type) = if global {
(
user.user.face(),
user.avatar_url()
.map_or(PfpType::Unset, |_| PfpType::Global),
),
false => (
)
} else {
(
user.face(),
user.user
.avatar_url()
.map_or(PfpType::Unset, |_| PfpType::Guild),
),
)
};

let flavor_text = match pfp_type {
Expand Down Expand Up @@ -190,3 +220,30 @@ pub async fn banban(ctx: Context<'_>) -> Result<(), Error> {

Ok(())
}

#[instrument(skip(ctx))]
#[poise::command(prefix_command)]
pub async fn uptime(ctx: Context<'_>) -> Result<(), Error> {
let channel = ctx
.channel_id()
.name(ctx.cache())
.await
.map_or("dms".to_string(), |c| format!("#{c}"));
info!(
"@{} ({}): {}",
ctx.author().name,
channel,
ctx.invocation_string()
);

let started = ctx.data().started;
let uptime = chrono::Utc::now() - started;

ctx.say(format!(
"uptime: {} (since {})",
uptime.format_full(),
started.format("%Y-%m-%d %H:%M UTC")
)).await.unwrap();

Ok(())
}
Loading

0 comments on commit a5beded

Please sign in to comment.