0.3.9.7.a1
Added random presence rotator, because... fun!
This commit is contained in:
parent
eb1e1da82f
commit
8fb7a9dab5
2
bot.py
2
bot.py
@ -9,7 +9,7 @@ from modules.common.boot_notice import post_boot_notice
|
||||
|
||||
# Version consists of:
|
||||
# Major.Enhancement.Minor.Patch.Test (Test is alphanumeric; doesn’t trigger auto update)
|
||||
VERSION = "0.3.9.6.a5"
|
||||
VERSION = "0.3.9.7.a1"
|
||||
|
||||
# ---------- Env loading ----------
|
||||
|
||||
|
0
modules/status/__init__.py
Normal file
0
modules/status/__init__.py
Normal file
318
modules/status/status_rotator.py
Normal file
318
modules/status/status_rotator.py
Normal 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.",
|
||||
"‘Muad’Dib’ 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 sandworm’s 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 one’s 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 body’s 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).")
|
Loading…
Reference in New Issue
Block a user