0.3.9.7.a1

Added random presence rotator, because... fun!
This commit is contained in:
Franz Rolfsvaag 2025-08-11 03:24:46 +02:00
parent eb1e1da82f
commit 8fb7a9dab5
3 changed files with 319 additions and 1 deletions

2
bot.py
View File

@ -9,7 +9,7 @@ from modules.common.boot_notice import post_boot_notice
# Version consists of: # Version consists of:
# Major.Enhancement.Minor.Patch.Test (Test is alphanumeric; doesnt trigger auto update) # Major.Enhancement.Minor.Patch.Test (Test is alphanumeric; doesnt trigger auto update)
VERSION = "0.3.9.6.a5" VERSION = "0.3.9.7.a1"
# ---------- Env loading ---------- # ---------- Env loading ----------

View File

View File

@ -0,0 +1,318 @@
# modules/status/status_rotator.py
import random
import asyncio
import time
from datetime import datetime, timezone, timedelta
import discord
from discord.ext import commands, tasks
from modules.common.settings import cfg
# ============== Tunables / lists you can expand freely ==============
# Long list of expressive unicode emojis (safe for all clients)
EMOTES = [
"❤️","🧡","💛","💚","💙","💜","🤎","🖤","🤍","","🌟","","🎉","🎊","🔥","💫","","🌈",
"😄","😁","😆","😊","🙂","😉","😎","🤩","🥳","🤗","🙌","👏","👍","🤝","🫶","🙏","🫡","🤘","💪",
"👀","🤔","🧐","😼","😹","😏","😌","😇","😴","🤖","👾","🧠","🫠",
"🏜️","🌵","🐪","🐛","🪱","🧂","🧪","🗡️","⚔️","🛡️","🚁","🛩️","🚀","🧭","🌪️"
]
# Short Dune phrases / lore snippets (kept short for presence)
DUNE_PHRASES = [
"Arrakis. Dune. Desert Planet.",
"Shai-Hulud stirs beneath the sands.",
"The spice must flow.",
"Bene Gesserit whispers in the dark.",
"Kwisatz Haderach foretold.",
"House Atreides rises.",
"House Harkonnen plots.",
"Fremen walk without rhythm.",
"Crysknife unsheathed.",
"Sietch Tabr stands strong.",
"CHOAM counts its profits.",
"The Spacing Guild navigates the void.",
"Water is life.",
"Fear is the mind-killer.",
"Gom Jabbar at the throat.",
"Stillsuits conserve every drop.",
"Ornithopters in the storm.",
"Sardaukar march.",
"Prescience veils the future.",
"Fedāykin watchful in the dunes."
]
# Concise fun facts (will be numbered). Keep them reasonably short.
FUN_FACTS = [
"Frank Herbert conceived Dune after reporting on sand dune stabilization in the Oregon coast.",
"MuadDib is a small desert mouse whose footprints taught Paul the sandwalk.",
"Melange extends life, heightens awareness, and enables prescient navigation.",
"Fremen walking without rhythm avoids attracting sandworms.",
"The crysknife is made from a sandworms tooth and must never be sheathed unblooded.",
"Spacing Guild Navigators require spice to safely fold space.",
"The Litany Against Fear is recited to focus and master ones emotions.",
"Bene Gesserit use prana-bindu training to control every muscle fiber.",
"Sietch means a Fremen community cave complex.",
"Stillsuits can reclaim over 90% of the bodys moisture.",
"The Imperial throne balances the Landsraad, CHOAM, and the Guild.",
"Kanly refers to a formal vendetta between noble houses.",
"Arrakis once had open water—long before the events of Dune.",
"Shai-Hulud is the Fremen name for the grand sandworm.",
"A lasgun-shield interaction can cause a subatomic chain reaction.",
"Mentats are human computers trained to replace forbidden thinking machines.",
"Duncan Idaho appears across eras via ghola rebirths.",
"The Water of Life is a deadly spice exhalation—surviving it transforms the Reverend Mother.",
"Fedaykin are the Fremen elite commandos sworn to their leader.",
"Sandtrout are juvenile forms of the great sandworms.",
"The Butlerian Jihad outlawed thinking machines, reshaping human society.",
"Caladan is the Atreides ocean world before their move to Arrakis.",
"The Harkonnen homeworld is Giedi Prime, an industrialized, harsh planet.",
"He who controls the spice controls the universe.",
"The Weirding Way is a Bene Gesserit martial art emphasizing speed and economy."
]
# ============== Cog implementation ==============
class StatusRotatorCog(commands.Cog):
"""
Cycles bot presence every N minutes with randomized status entries:
- Member count (excluding bots)
- Pirates count
- Encounters count
- Random shout-out: {emoji} {display_name} (members with SHAI_FULL_ACCESS_ROLE_ID)
- Count of fully initiated members (role members)
- Random Dune phrase
- Random Dune fun fact (#n -> fact)
- Top threat pirate
- Latest report (pending/most recent)
- Encounters in last 24h
- Uptime
- Current running version
"""
def __init__(self, bot: commands.Bot):
self.bot = bot
r = cfg(bot)
self.home_gid = r.int('home_guild_id', 0)
self.full_access_role_id = r.int('full_access_role_id', 0) # SHAI_FULL_ACCESS_ROLE_ID
self.interval_min = r.int('status_interval_min', 5) # SHAI_STATUS_INTERVAL_MIN (optional)
self.max_len = 120 # clip safety for PC clients (keeps presence tidy)
# Build generator list (enable/disable here if needed)
self._generators = [
self._gen_members_count,
self._gen_pirates_count,
self._gen_encounters_count,
self._gen_random_shoutout,
self._gen_initiated_count,
self._gen_random_phrase,
self._gen_random_fun_fact,
self._gen_top_threat,
self._gen_latest_report,
self._gen_encounters_last_24h,
self._gen_uptime,
self._gen_running_version,
]
self._queue = []
# ---- lifecycle ----
@commands.Cog.listener()
async def on_ready(self):
# ensure loop interval reflects config
try:
if self.rotate_status.is_running():
self.rotate_status.change_interval(minutes=max(1, self.interval_min))
else:
self.rotate_status.change_interval(minutes=max(1, self.interval_min))
self.rotate_status.start()
except Exception as e:
print("[status] failed to start/change loop:", repr(e))
def cog_unload(self):
try:
self.rotate_status.cancel()
except Exception:
pass
# ---- the loop ----
@tasks.loop(minutes=5)
async def rotate_status(self):
await self.bot.wait_until_ready()
guild = self._resolve_guild()
if not guild:
return
text = await self._next_status_text(guild)
if not text:
return
atype = random.choice([
discord.ActivityType.watching,
discord.ActivityType.playing,
discord.ActivityType.listening
])
try:
await self.bot.change_presence(activity=discord.Activity(type=atype, name=text))
except Exception as e:
print("[status] change_presence failed:", repr(e))
# ---- helpers ----
def _resolve_guild(self) -> discord.Guild | None:
if self.home_gid:
g = self.bot.get_guild(self.home_gid)
if g:
return g
return self.bot.guilds[0] if self.bot.guilds else None
async def _next_status_text(self, guild: discord.Guild) -> str | None:
# Try up to len(generators) times to get a non-empty status
for _ in range(len(self._generators)):
if not self._queue:
self._queue = random.sample(self._generators, k=len(self._generators))
gen = self._queue.pop(0)
try:
s = await gen(guild)
s = self._clip(s)
if s:
return s
except Exception as e:
print(f"[status] generator {gen.__name__} failed:", repr(e))
continue
return None
def _clip(self, s: str | None) -> str | None:
if not s:
return None
s = s.strip()
return (s[: self.max_len - 1] + "") if len(s) > self.max_len else s
@staticmethod
def _fmt_duration(seconds: float) -> str:
seconds = int(max(0, seconds))
d, rem = divmod(seconds, 86400)
h, rem = divmod(rem, 3600)
m, _ = divmod(rem, 60)
if d > 0:
return f"{d}d {h}h {m}m"
if h > 0:
return f"{h}h {m}m"
return f"{m}m"
# ---- individual generators (originals) ----
async def _gen_members_count(self, guild: discord.Guild) -> str:
count = sum(1 for m in guild.members if not m.bot)
return f"{count} server members"
async def _gen_pirates_count(self, guild: discord.Guild) -> str:
dm = getattr(self.bot, "data_manager", None)
n = len(dm.get('pirates')) if dm else 0
return f"{n} pirates reported"
async def _gen_encounters_count(self, guild: discord.Guild) -> str:
dm = getattr(self.bot, "data_manager", None)
n = len(dm.get('encounters')) if dm else 0
return f"{n} reported encounters"
async def _gen_random_shoutout(self, guild: discord.Guild) -> str | None:
if not self.full_access_role_id:
return None
role = guild.get_role(self.full_access_role_id)
if not role:
return None
candidates = [m for m in role.members if not m.bot]
if not candidates:
return None
member = random.choice(candidates)
em = random.choice(EMOTES)
name = member.display_name
return f"{em} {name}"
async def _gen_initiated_count(self, guild: discord.Guild) -> str | None:
if not self.full_access_role_id:
return None
role = guild.get_role(self.full_access_role_id)
if not role:
return None
n = sum(1 for m in role.members if not m.bot)
return f"{n} initiated members"
async def _gen_random_phrase(self, guild: discord.Guild) -> str:
return random.choice(DUNE_PHRASES)
async def _gen_random_fun_fact(self, guild: discord.Guild) -> str:
idx = random.randrange(len(FUN_FACTS)) # 0-based
num = idx + 1
fact = FUN_FACTS[idx]
return f"Fun fact #{num} ->\n{fact}"
async def _gen_top_threat(self, guild: discord.Guild) -> str | None:
dm = getattr(self.bot, "data_manager", None)
pirates = dm.get('pirates') if dm else []
if not pirates:
return None
# sort by threat_level desc, then encounter_count desc, then newest added
pirates_sorted = sorted(
pirates,
key=lambda p: (
int(p.get('threat_level', 0)),
int(p.get('encounter_count', 0)),
float(p.get('added_ts', 0.0))
),
reverse=True
)
top = pirates_sorted[0]
tl = int(top.get('threat_level', 0))
name = top.get('character_name') or top.get('account_name') or "Unknown"
return f"Top threat: {name} ({tl}%)"
async def _gen_latest_report(self, guild: discord.Guild) -> str | None:
dm = getattr(self.bot, "data_manager", None)
reports = dm.get('reports') if dm else []
if not reports:
return None
latest = max(reports, key=lambda r: float(r.get('ts', 0.0)))
char = latest.get('character_name') or "Unknown"
acct = latest.get('account_name') or ""
# keep brief
return f"Latest report: {char}" + (f" ({acct})" if acct else "")
async def _gen_encounters_last_24h(self, guild: discord.Guild) -> str | None:
dm = getattr(self.bot, "data_manager", None)
encs = dm.get('encounters') if dm else []
if not encs:
return "0 encounters last 24h"
since = time.time() - 86400
n = sum(1 for e in encs if float(e.get('timestamp', 0.0)) >= since)
return f"{n} encounters last 24h"
async def _gen_uptime(self, guild: discord.Guild) -> str | None:
dm = getattr(self.bot, "data_manager", None)
st = (dm.get('boot_state') or [{}])[-1] if (dm and dm.get('boot_state')) else {}
boot_ts = float(st.get('last_boot_ts', 0.0))
if boot_ts <= 0:
return None
dur = self._fmt_duration(time.time() - boot_ts)
return f"Uptime {dur}"
async def _gen_running_version(self, guild: discord.Guild) -> str | None:
dm = getattr(self.bot, "data_manager", None)
st = (dm.get('boot_state') or [{}])[-1] if (dm and dm.get('boot_state')) else {}
ver = st.get('last_version')
if not ver:
return None
# Occasionally include version (kept as its own generator for randomness)
return f"Running {ver}"
# ============== setup() ==============
async def setup(bot: commands.Bot):
cog = StatusRotatorCog(bot)
await bot.add_cog(cog)
print("[status] StatusRotatorCog loaded; rotating presence every",
max(1, cfg(bot).int('status_interval_min', 5)), "minute(s).")