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:
|
# Version consists of:
|
||||||
# Major.Enhancement.Minor.Patch.Test (Test is alphanumeric; doesn’t trigger auto update)
|
# 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 ----------
|
# ---------- 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