diff --git a/commands/utility_commands.py b/commands/utility_commands.py index 735d657..f6f2c1a 100644 --- a/commands/utility_commands.py +++ b/commands/utility_commands.py @@ -11,6 +11,7 @@ import scratchattach as scratch +import config from config import scratch_orange @@ -316,6 +317,27 @@ async def scratchblocks( filename = await render_blocks_image(code=code, style=style) await ctx.send(file=interactions.File(file=filename)) + @interactions.slash_command( + name="community", + description="Check what the Scratch community is up to.", + ) + async def community(self, ctx: interactions.SlashContext): + stats = config.activity_stats + await ctx.send( + embed=interactions.Embed( + title=" What the community is up to:", + description=( + f"<:scratch:1330550333751169104> **{stats['scratch3']}** people are using Scratch 3\n" + f"- **{stats['streaming_scratch3']}** people are streaming it\n" + f"<:Turbowarp:1330552274774396979> **{stats['turbowarp']}** people are using TurboWarp Desktop\n" + f"- **{stats['streaming_turbowarp']}** people are streaming it\n" + f"**{stats['scratch2']}** people are using Scratch 2\n" + f"**{stats['scratch1']}** people are using Scratch 1.4\n" + ), + color=scratch_orange, + ) + ) + def setup(bot: interactions.Client): UtilityCommands(bot) diff --git a/config.py b/config.py index 13b3fdc..2c11542 100644 --- a/config.py +++ b/config.py @@ -48,4 +48,14 @@ pending_verifiers: dict = {} # Variable to indicate if the bot is ready -bot_ready = False \ No newline at end of file +bot_ready = False + +# Activity statistics +activity_stats = { + "turbowarp": 0, + "streaming_turbowarp": 0, + "scratch3": 0, + "streaming_scratch3": 0, + "scratch2": 0, + "scratch1": 0, +} \ No newline at end of file diff --git a/events/bot_events.py b/events/bot_events.py index 5be658b..eb80871 100644 --- a/events/bot_events.py +++ b/events/bot_events.py @@ -2,13 +2,58 @@ Discord bot events. """ +from re import match import interactions import logging from interactions.api.events import CommandError +import config from config import bot, bot_statuses, button_states, bot_ready from database import add_server +# ------------------------------------------------------------------ # +# Helper functions # +# ------------------------------------------------------------------ # +def classify(activity) -> str | None: + """Map an activity to a stats key, or None if irrelevant.""" + T = interactions.ActivityType + name = activity.name or "" + playing = activity.type == T.PLAYING + streaming = activity.type == T.STREAMING + + if name == "TurboWarp Desktop": + return "streaming_turbowarp" if streaming else "turbowarp" if playing else None + if match(r"Scratch 3\.\d+\.\d+", name): + return "streaming_scratch3" if streaming else "scratch3" if playing else None + if playing and name == "Scratch 2 Offline Editor": + return "scratch2" + if playing and match(r"Scratch 1\.4 of \d{2}-[A-Za-z]{3}-\d{2}", name): + return "scratch1" + return None + +_presence: dict[int, set[str]] = {} # people currently using Scratch or TurboWarp +_streaming: set[int] = set() # people currently streaming Scratch or TurboWarp +_applied: dict[int, set[str]] = {} # keys currently counted in stats + +def _effective(uid: int) -> set[str]: + keys = set(_presence.get(uid, set())) + if uid in _streaming: + for base in ("turbowarp", "scratch3"): + if base in keys: + keys.add(f"streaming_{base}") + return keys + +def _refresh(uid: int): + stats = config.activity_stats + new, old = _effective(uid), _applied.get(uid, set()) + for k in old - new: + stats[k] -= 1 + for k in new - old: + stats[k] += 1 + _applied[uid] = new + +_user_states: dict[int, set[str]] = {} + class BotEvents(interactions.Extension): """Extension for core bot lifecycle events.""" @@ -45,6 +90,22 @@ async def on_guild_join(self, event: interactions.events.GuildJoin): if bot_ready: add_server(event.guild.id) + @interactions.listen(interactions.events.PresenceUpdate) + async def on_presence_update(self, event: interactions.events.PresenceUpdate): + uid = int(event.user.id) + _presence[uid] = {k for a in event.activities if (k := classify(a))} + _refresh(uid) + + @interactions.listen(interactions.events.VoiceStateUpdate) + async def on_voice_state_update(self, event: interactions.events.VoiceStateUpdate): + state = event.after or event.before + uid = int(state.member.id) + if event.after and event.after.self_stream: + _streaming.add(uid) + else: + _streaming.discard(uid) # stopped streaming or left voice + _refresh(uid) + # ------------------------------------------------------------------ # # Component interaction handler (settings buttons) # # ------------------------------------------------------------------ # @@ -109,7 +170,7 @@ async def on_component(self, event: interactions.events.Component): # ------------------------------------------------------------------ # @interactions.listen() - async def on_command_error(event: CommandError): + async def on_command_error(self, event: CommandError): logging.exception(f"Error in command", exc_info=event.error) try: