diff --git a/.gitignore b/.gitignore index 89403e6..8e994da 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ logs/ scores.json scores.json +*.json +/__pycache__ diff --git a/README.md b/README.md index e8de686..967f864 100644 --- a/README.md +++ b/README.md @@ -4,3 +4,40 @@ ive been spawncamped! # i made a personal bot made it for my friend group's discord server its fun to play with. + +# Make it your own +# First time setup +Clone the repo via whichever way you want. I recomment using git for this. + +Creating a virtual requirement is recommended, especially for Raspberry PIs. +If you're planning to host this on another linux machine, you may skip this part. + +Then you should +``` +cd spawncamped (if not in the directory already) +python3 -m venv botenv +source botenv/bin/activate +``` +Whenever you want to update the bot, remember to activate the bot's virtual enviroment by typing `source botenv/bin/activate` + +Then go ahead and install all dependencies using `pip install -r requirements.txt` + +Now you should create these 4 things: (if not already existing.) +- scores.json +- logs/discord.log +- logs/webpanel.log +- .env + +Skipping these will make the bot **not** work properly. + +In .env, add `DISCORD_TOKEN=""` and insert your bot's token there. Also make sure you add an `owner=""` variable and your discord user id as well. + +That's it! You may run the bot via `update.sh`. Make sure to give the script appropiate permissions via chmod first. +Web panel is active at port `8000`. You can change the port at the bottom of `webpanel.py`. + +## Updating bot + +Always use `update.sh` to update. Make sure git is installed first! + +## Important +Some code in this bot is specific to my (closed) discord server and will not work unless you do some editing. \ No newline at end of file diff --git a/logs/discord.log b/logs/discord.log new file mode 100644 index 0000000..e69de29 diff --git a/logs/webpanel.log b/logs/webpanel.log new file mode 100644 index 0000000..e69de29 diff --git a/main.py b/main.py index bd39f1b..eabaf8f 100644 --- a/main.py +++ b/main.py @@ -3,7 +3,6 @@ import os import logging import time -from rich import print as say from dotenv import load_dotenv from discord.ext import commands import json @@ -11,6 +10,8 @@ load_dotenv() token = os.getenv('DISCORD_TOKEN') +guildId = int(os.getenv('guildId')) +owner = int(os.getenv('owner')) # Bot Owner. Change it in the .env file. reconnect = 0 handler = logging.FileHandler(filename='logs/discord.log', encoding='utf-8', mode='w') @@ -22,10 +23,23 @@ activity = discord.Activity(type=discord.ActivityType.listening, name="i was spawncamped!", details="do !help for help!") bot = commands.Bot(command_prefix='!', intents=intents, activity=activity, status=discord.Status.idle, help_command=None) -MY_GUILD = discord.Object(id=1433854304678318183) -scotty = 429526435732914188 -bbq = 550259849896656907 +MY_GUILD = discord.Object(id=guildId) SCORES_FILE = 'scores.json' +STATUS_FILE = 'status.json' # This is such a finniky workaround + +def say(message): + from rich import print + print(f"[blue]BOT:[/blue] {message}") + +# external helper functions +async def shutdownBot(): + # await bot.close() + return True + +async def isBotOnline(): + with open(STATUS_FILE, 'r') as f: + status = ["online"] + json.dump() # functions for score management def load_scores(): # fetches scores from scores.json @@ -53,6 +67,19 @@ def can_claim_daily(user_id): # function to check if user can claim daily points return last_claimed != today +def scoreCheck(user_id): # checks if user exists in scores.json + scores = load_scores() + user_id_str = str(user_id) + + try: + if user_id_str not in scores: + scores[user_id_str] = {'total_score': 0, 'daily_debt': 0, 'daily_score': 0, 'bonus_multiplier': 1} + return + except KeyError: + logging.error(f"KeyError: User ID {user_id_str} not found in scores.") + return False + + def add_score(user_id, points): # modify a user's score scores = load_scores() # gets current scores user_id_str = str(user_id) @@ -106,6 +133,22 @@ def check_daily_loan(user_id): else: return None +def check_hasDailyYet(user_id): + scores = load_scores() + user_id_str = str(user_id) + + if user_id_str in scores: + last_claimed = scores[user_id_str].get('last_daily_claimed') + today = datetime.now().strftime('%Y-%m-%d') + if last_claimed == today: + return False # has already claimed today + else: # has not claimed today + embed = discord.Embed(title="Daily Reminder", description="You have not claimed your daily points yet! Use `!daily` to claim them.", color=0xffff00) + return embed + else: + return Exception("User not found in `scores.json`") + + def check_debt(user_id): scores = load_scores() user_id_str = str(user_id) @@ -146,11 +189,22 @@ async def on_ready(): say(" ") say("[green][bold]----------------------------") say(f'[green]logged in as {bot.user}') - say(f"platform: {os.name}, python version: {os.sys.version}, discord.py version: {discord.__version__}") - say(f"system: {os.uname() if hasattr(os, 'uname') else 'N/A'}") say("[green][bold]----------------------------") - +@bot.event +async def on_command_error(ctx, error): + if isinstance(error, commands.CommandOnCooldown): # Command cooldown (ratelimits) + embed = discord.Embed( + title="Command on Cooldown", + description=f"This command is on cooldown. Try again in {error.retry_after:.1f} seconds.", + color=0xff0000 + ) + else: # Other errors, bugs, etc. + embed = discord.Embed(title="Error", description="An error occurred while processing the command.", color=0xff0000) + embed.add_field(name="Details", value=str(error), inline=False) + embed.set_footer(text="ping scotty for this") + say(f"[red]Error: {error}") + logging.error(f"Error processing command from {ctx.author}: {error}") @bot.command(name="help", description="shows this message") async def help(ctx, type: str = None): @@ -223,7 +277,7 @@ async def about(ctx): @bot.command(name="source", description="shows the bot source code link") async def source(ctx): say(f"Source command called by [blue]{ctx.author}") - await ctx.send("You can find my source code [here](https://github.com/ScottN13/spawncamped)") + await ctx.reply("You can find my source code [here](https://github.com/ScottN13/spawncamped)") logging.info(f"Provided source code link to {ctx.author}") @bot.command(name="createrules", description="creates the server rules embed") @@ -235,10 +289,10 @@ async def createrules(ctx, title, *, description): try: # await discord.TextChannel.send(id=rules, embed=embed) # Rules channel ID # this doesnt send it to the rules channel for some reason await rules.send(embed=embed) - await ctx.send("Done, i created the rules embed.") + await ctx.reply("Done, i created the rules embed.") logging.info(f"Created rules embed for {ctx.author} with contents: {title} - {description}") except Exception as e: - await ctx.send(f"i uhm: {e}") + await ctx.reply(f"i uhm: {e}") say(f"[red]Error: {e}") logging.error(f"Error creating rules embed for {ctx.author}: {e}") @@ -247,17 +301,17 @@ async def sync(ctx): bot.tree.copy_global_to(guild=discord.Object(id=1433854304678318183)) synced = await bot.tree.sync(guild=discord.Object(id=1433854304678318183)) - await ctx.send(f"{len(synced)} Slash commands synced.") await bot.add_cog(Social(bot)) await bot.add_cog(leaderboard(bot)) await bot.add_cog(Gambling(bot)) + await ctx.reply(f"{len(synced)} Slash commands synced. Enabled cogs.") say(f"[green]{len(synced)} slash commands synced by {ctx.author}") logging.info(f"{len(synced)} slash commands synced by {ctx.author}") @bot.command(name="ping", description="ping") async def ping(ctx): say(f"Ping command called by [blue]{ctx.author}") - await ctx.send(f"`Pong! Latency is {bot.latency} ms`") + await ctx.reply(f"`Pong! Latency is {bot.latency} ms`") logging.info(f"Ping command used by {ctx.author} with latency {bot.latency} ms") @@ -270,28 +324,16 @@ async def add(ctx, left: int, right: int): logging.info(f"Add command used by {ctx.author} with arguments: {left}, {right}") """ -@bot.command(name="!stopgamble") -async def stopgamble(ctx): - if ctx.author.id == scotty or bbq: - say(f"StopGamble command called by [blue]{ctx.author}") - bot.remove_cog("Gambling") - await ctx.send("no more gambling") - logging.info(f"Gambling disabled by {ctx.author} for maintenance") - @bot.command(name="stop", description="Stops the bot (owner only)") async def stop(ctx): - if ctx.author.id == scotty or bbq: + if ctx.author.id == owner: say(f"Shutdown command issued by {ctx.author}") - await ctx.send("FUCK ALL OF YOU") - time.sleep(1) - await ctx.send("DONT KILL ME PLEASE!") - time.sleep(1) - await ctx.send("*AA-*") + await ctx.reply("*ok*") logging.info(f"stopped by {ctx.author}") await bot.close() else: - await ctx.send("Foolish mortal, you do not have permission to do that.") + await ctx.reply("no.") logging.info(f"{ctx.author} tried to stop bot") say(f"{ctx.author} tried to stop bot") @@ -303,27 +345,27 @@ async def enlist(ctx, receiever: discord.Member, role_type: str): trusted = discord.utils.get(ctx.guild.roles, id=1433854562875215972) role_type = role_type.lower() - if ctx.author.id == scotty or ctx.author.id == bbq: # checks if its me or BBQ + if ctx.author.id == owner: # checks if its bot owner if role_type in ["friends", "friend"]: try: await receiever.add_roles(member) await receiever.add_roles(friends) - await ctx.send(f"Done, verified {receiever} to the server (friend privileges).") + await ctx.reply(f"Done, verified {receiever} to the server (friend privileges).") logging.info(f"{ctx.author} granted {role_type} role to {receiever}") say(f"[green]{ctx.author} granted {role_type} role to {receiever}") except Exception as e: - await ctx.send(f"An error occurred: {e}") + await ctx.reply(f"An error occurred: {e}") say(f"[red]Error: {e}") logging.error(f"Error: {e}") elif role_type in ["member", "members"]: try: await receiever.add_roles(member) - await ctx.send(f"Done, verified {receiever} to the server.") + await ctx.reply(f"Done, verified {receiever} to the server.") logging.info(f"{ctx.author} granted {role_type} role to {receiever}") say(f"[green]{ctx.author} granted {role_type} role to {receiever}") except Exception as e: - await ctx.send(f"An error occurred: {e}") + await ctx.reply(f"An error occurred: {e}") say(f"[red]Error: {e}") logging.error(f"Error: {e}") @@ -332,20 +374,20 @@ async def enlist(ctx, receiever: discord.Member, role_type: str): await receiever.add_roles(member) await receiever.add_roles(friends) await receiever.add_roles(trusted) - await ctx.send(f"Done, entrusted {receiever}.") + await ctx.reply(f"Done, entrusted {receiever}.") logging.info(f"{ctx.author} granted {role_type} role to {receiever}") say(f"[green]{ctx.author} granted {role_type} role to {receiever}") except Exception as e: - await ctx.send(f"An error occurred: {e}") + await ctx.reply(f"An error occurred: {e}") say(f"[red]Error: {e}") logging.error(f"Error: {e}") elif role_type not in ["friends", "friend", "member", "members", "trusted"]: - await ctx.send(f"I don't know what `{role_type}` means. Maybe you made a typo?") + await ctx.reply(f"I don't know what `{role_type}` means. Maybe you made a typo?") logging.warning(f"{ctx.author} tried verifying {receiever} with provided invalid role type: {role_type}") else: - await ctx.send("You have no permission to do that!") + await ctx.reply("You have no permission to do that!") say(f"[red]{ctx.author} just tried to auto verify someone!") logging.warning(f"{ctx.author} tried to enlist {receiever} with role type: {role_type} without permission") @@ -359,7 +401,7 @@ async def pin(ctx, message_id: int): await discord.Message.forward(message, destination=discord.utils.get(ctx.guild.channels, id=channel.id)) logging.info(f"Pinned message for {ctx.author} with id {message_id}") except Exception as e: - await ctx.send(f"An error occurred: {e}") + await ctx.reply(f"An error occurred: {e}") say(f"[red]Error: {e}") logging.error(f"Error pinning message for {ctx.author} with id {message_id}: {e}") @@ -428,7 +470,7 @@ async def mc(self, ctx): embed.add_field(name="Server IP", value="none yet", inline=False) embed.add_field(name="Version", value="Java 1.20.1 - Forge 47.4.9", inline=False) embed.add_field(name="Modpack link:", value="[click here](https://drive.google.com/drive/folders/1QC7TeQf4ISNDqhdWa06QLQbj7MVGeqCE)", inline=False) - await ctx.send(embed=embed) + await ctx.reply(embed=embed) class leaderboard(commands.Cog): # i seperated these for organization def __init__(self, bot): @@ -438,7 +480,7 @@ def __init__(self, bot): @commands.command(name="daily", description="gives daily points") async def daily(self, ctx): if not can_claim_daily(ctx.author.id): - await ctx.send(f"You already claimed your daily points.") + await ctx.reply(f"You already claimed your daily points.") logging.info(f"{ctx.author} tried to claim daily reward twice in one day") return @@ -456,7 +498,7 @@ async def daily(self, ctx): embed.add_field(name="Total Score", value=total, inline=True) embed.set_footer(text="come back tomorrow for more!") - await ctx.send(embed=embed) + await ctx.reply(embed=embed) logging.info(f"{ctx.author} claimed daily reward: {points} points") say(f"[green]{ctx.author} claimed daily reward: +{points} points") @@ -523,12 +565,12 @@ async def paydebt(self, ctx, amount: int): amount = debt if debt is None or debt <= 0: - await ctx.send("You have no debt to pay off. :)") + await ctx.reply("You have no debt to pay off. :)") logging.info(f"{ctx.author} tried to pay debt but has none") return if amount <= 0: - await ctx.send("tf you want me to do with 0 money") + await ctx.reply("tf you want me to do with 0 money") logging.warning(f"{ctx.author} tried to pay debt with invalid amount: {amount}") return @@ -537,7 +579,7 @@ async def paydebt(self, ctx, amount: int): add_score(ctx.author.id, -debt) add_debt(ctx.author.id, -debt) embed = discord.Embed(title="Banker - Debt Payment", description=f"You have paid off all of your debt ({debt} points).", color=0x00ff00) - await ctx.send(embed=embed) + await ctx.reply(embed=embed) logging.info(f"{ctx.author} paid off all of their debt: {debt} points") return @@ -545,7 +587,7 @@ async def paydebt(self, ctx, amount: int): add_score(ctx.author.id, -amount) add_debt(ctx.author.id, -amount) embed = discord.Embed(title="Banker - Debt Payment", description=f"You have paid off {amount} points of your debt.", color=0x00ff00) - await ctx.send(embed=embed) + await ctx.reply(embed=embed) logging.info(f"{ctx.author} paid off {amount} points of their debt") return @@ -618,12 +660,14 @@ async def shop(self, ctx): embed.add_field(name="1.5x Multiplier", value="Cost: 500 points\nIncreases all earnings by 50%.", inline=False) view = ShopView() - await ctx.send(embed=embed, view=view) + await ctx.send(embed=embed, view=view, ephemeral=True) logging.info(f"{ctx.author} viewed the multiplier shop") class Gambling(commands.Cog): # gambling commands def __init__(self, bot): self.bot = bot + for command in self.get_commands(): + command.add_check(commands.cooldown(1, 5, commands.BucketType.user)) # i don't think this works. @commands.command(name="roll", description="rolls a dice") async def roll(self, ctx, sides: int = 6): # default to 6 sided dice @@ -750,7 +794,7 @@ async def spin(self, ctx, wager: int = 0, color: str = "red"): # Black: 2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36 # 0: House wins red_numbers = [1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35] - black_numbers = [2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36] + black_numbers = [2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36] # I understand this isn't used but I'll continue to improve upon all gambling games. result = random.randint(0, 36) @@ -784,4 +828,23 @@ async def spin(self, ctx, wager: int = 0, color: str = "red"): logging.info(f"{ctx.author} spun roulette and landed on {result}") say(f"[green]{ctx.author} spun roulette and landed on {result}") -bot.run(token, log_handler=handler, log_level=logging.INFO, root_logger=True) \ No newline at end of file +""" +class Jobs(commands.Cog): # more ways of earning points + def __init__(self, bot): + self.bot = bot + + @commands.Command(name="fish", description="Go fishing.") + async def fish(self, ctx): # Soon. + + # the more weight, the better? + # add other types of baits to shop? + + return +""" + +def startBot(): + if __name__ == "__main__": # effectively the same thing + bot.run(token, log_handler=handler, log_level=logging.INFO, root_logger=True) + +if __name__ == "__main__": + bot.run(token, log_handler=handler, log_level=logging.INFO, root_logger=True) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e51cb22..0c47d7f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,8 @@ rich discord.py>=2.0.0 python-dotenv logging -discord \ No newline at end of file +discord +fastapi>=0.110.0 +uvicorn[standard]>=0.27.0 +jinja2>=3.1.0 +python-multipart>=0.0.9 \ No newline at end of file diff --git a/scores.json b/scores.json index 9e26dfe..bcd532c 100644 --- a/scores.json +++ b/scores.json @@ -1 +1,30 @@ -{} \ No newline at end of file +{ + "429526435732914188": { + "total_score": 48137, + "daily_debt": 4, + "daily_score": 0, + "last_daily_claimed": "2026-01-02", + "bonus_multiplier": 1.5 + }, + "670703737265848399": { + "total_score": 1480, + "daily_debt": 0, + "daily_score": 0, + "last_daily_claimed": "2025-12-30", + "bonus_multiplier": 1 + }, + "963491431253676172": { + "total_score": 81, + "daily_debt": 0, + "daily_score": 0, + "last_daily_claimed": "2025-12-30", + "bonus_multiplier": 1 + }, + "550259849896656907": { + "total_score": 9141, + "daily_debt": 0, + "daily_score": 0, + "last_daily_claimed": "2025-12-31", + "bonus_multiplier": 1.5 + } +} \ No newline at end of file diff --git a/starter.py b/starter.py index bb04b9e..69f5835 100644 --- a/starter.py +++ b/starter.py @@ -1,14 +1,17 @@ -import uvicorn -import os -import logging -from datetime import datetime -import json -from rich import print as say -from fastapi import FastAPI -from fastapi import APIRouter -from fastapi import Request -from fastapi.responses import HTMLResponse -from fastapi.staticfiles import StaticFiles - -app = FastAPI() -app.mount("/static", StaticFiles(directory="static"), name="static") \ No newline at end of file +import threading +import time +import subprocess + +async def stopBot(): + bot_thread + +if __name__ == "__main__": + # webpanel_process = subprocess.Popen(["uvicorn", "webpanel:app", "--host", "0.0.0.0", "--port", "8000"]) + + bot_thread = threading.Thread(target=subprocess.run, args=(["python", "main.py"],)) + webpanel_thread = threading.Thread(target=subprocess.run, args=(["python", "webpanel.py"],)) + + bot_thread.start() + webpanel_thread.start() + +# This script starts both the bot and the web panel in separate threads. \ No newline at end of file diff --git a/startup.sh b/startup.sh deleted file mode 100644 index 3177ea8..0000000 --- a/startup.sh +++ /dev/null @@ -1,3 +0,0 @@ -sleep 5 -source /home/david/spawncamped/bot-env/bin/activate -nohup python main.py \ No newline at end of file diff --git a/static/index.js b/static/index.js new file mode 100644 index 0000000..c17c31b --- /dev/null +++ b/static/index.js @@ -0,0 +1,25 @@ +async function refreshStatus() { + try { + const response = await fetch("/api/status"); + const data = await response.json(); + + const statusEl = document.getElementById("bot-status"); + + if (data.online) { + statusEl.textContent = "ONLINE"; + statusEl.className = "status-online"; + } else { + statusEl.textContent = "OFFLINE"; + statusEl.className = "status-offline"; + } + } catch (err) { + console.error("Failed to fetch bot status", err); + } +} + + +// Refresh every 5 seconds +// setInterval(refreshStatus, 5000); + +// Run once immediately on page load +//refreshStatus(); \ No newline at end of file diff --git a/static/style.css b/static/style.css index 7c9cd8c..931e987 100644 --- a/static/style.css +++ b/static/style.css @@ -2,9 +2,88 @@ body{ font-family: Arial, sans-serif; margin: 0; padding: 0; + background-color: #000000; } -.online-badge { +p{ + color: #ccc; +} + +h1{ + color:#ccc; +} + +h2{ + color: #ccc; +} + +button { + margin: 10px; + padding: 14px 28px; + font-size: 18px; + border-radius: 10px; + border: none; + cursor: pointer; + transition: transform .2s; +} + +button:hover { + transform: scale(1.05); +} + +table { + margin: auto; + border-collapse: collapse; +} + +th, td { + padding: 8px 12px; + border: 1px solid #444; + color:#ccc; +} + +input { + width: 100px; + padding: 4px; + text-align: center; + color: #ccc; +} + +.scores-form{ + padding-top: 5px; +} + +.submit-div{ + margin: 20px auto; + padding-left: 50%; + +} + +.heading{ + border: 2px solid #ffffff; + border-bottom: #ffffff; + border-style: none none solid none; + border-radius: 15px; + border-width: 5%; + text-align: center; + padding-top: 15px; + padding-bottom: 5px; + margin-left: 5%; + margin-right: 5%; +} + +.panel{ + color: #ccc; + width: 80%; + margin: 20px auto; + padding: 20px; + border: 1px solid #ccc; + border-radius: 8px; + background-color: #252323; + box-shadow: 4px 4px 8px 8px rgba(53, 53, 53, 0.2), 2px 6px 20px 2px rgba(78, 73, 73, 0.19); +} + +.status-online { display: inline-block; background-color: #4CAF50; /* Green color */ color: white; @@ -13,7 +92,7 @@ body{ font-size: 14px; font-weight: bold; } -.offline-badge { +.status-offline { display: inline-block; background-color: #f44336; /* Red color */ color: white; @@ -21,4 +100,20 @@ body{ border-radius: 12px; font-size: 14px; font-weight: bold; -} \ No newline at end of file +} + +.msg{ + color:#ccc; +} + +.displayDiv{ + overflow-wrap: break-word; + text-wrap: initial; + background-color: #252323; + border: #ccc solid 2px; + box-shadow: 4px 4px 8px 8px rgba(53, 53, 53, 0.2), 2px 6px 20px 2px rgba(78, 73, 73, 0.19); + width: 80%; + border-radius: 8px; + font: Consolas, 'Courier New', monospace; +} + diff --git a/templates/index.html b/templates/index.html index 9ebc686..b270546 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,11 +1,29 @@ spawncamped control panel - + + -

Control Panel

-